Progress on video crop

This commit is contained in:
Matthieu 2022-10-19 00:19:42 +02:00
parent 76e2a43de4
commit 896c9634eb
9 changed files with 230 additions and 200 deletions

View File

@ -47,7 +47,6 @@ import java.io.OutputStream
import java.text.SimpleDateFormat import java.text.SimpleDateFormat
import java.util.* import java.util.*
import kotlin.math.roundToInt import kotlin.math.roundToInt
import kotlin.collections.flatten
const val TAG = "Post Creation Activity" const val TAG = "Post Creation Activity"
@ -131,8 +130,13 @@ class PostCreationActivity : BaseThemedWithoutBarActivity() {
uiState.newEncodingJobVideoStart?.let { videoStart -> uiState.newEncodingJobVideoStart?.let { videoStart ->
uiState.newEncodingJobVideoEnd?.let { videoEnd -> uiState.newEncodingJobVideoEnd?.let { videoEnd ->
uiState.newEncodingJobSpeedIndex?.let { speedIndex -> uiState.newEncodingJobSpeedIndex?.let { speedIndex ->
startEncoding(position, muted, videoStart, videoEnd, speedIndex) uiState.newEncodingJobVideoCrop?.let { crop ->
model.encodingStarted() startEncoding(position, muted,
videoStart, videoEnd,
speedIndex, crop
)
model.encodingStarted()
}
} }
} }
} }
@ -331,7 +335,8 @@ class PostCreationActivity : BaseThemedWithoutBarActivity() {
muted: Boolean, muted: Boolean,
videoStart: Float?, videoStart: Float?,
videoEnd: Float?, videoEnd: Float?,
speedIndex: Int speedIndex: Int,
crop: VideoEditActivity.RelativeCropPosition
) { ) {
val originalUri = model.getPhotoData().value!![position].imageUri val originalUri = model.getPhotoData().value!![position].imageUri
@ -352,29 +357,33 @@ class PostCreationActivity : BaseThemedWithoutBarActivity() {
val speed = VideoEditActivity.speedChoices[speedIndex] val speed = VideoEditActivity.speedChoices[speedIndex]
//TODO also have audio when speed is changed?
val mutedString = if(muted || speedIndex != 1) "-an" else null val mutedString = if(muted || speedIndex != 1) "-an" else null
val startString: List<String?> = if(videoStart != null) listOf("-ss", "${videoStart/speed.toFloat()}") else listOf(null, null) val startString: List<String?> = if(videoStart != null) listOf("-ss", "${videoStart/speed.toFloat()}") else listOf(null, null)
val endString: List<String?> = if(videoEnd != null) listOf("-to", "${videoEnd/speed.toFloat() - (videoStart ?: 0f)/speed.toFloat()}") else listOf(null, null) val endString: List<String?> = if(videoEnd != null) listOf("-to", "${videoEnd/speed.toFloat() - (videoStart ?: 0f)/speed.toFloat()}") else listOf(null, null)
val speedString: List<String?> = if(speedIndex!= 1) // iw and ih are variables for the original width and height values, FFmpeg will know them
listOf("-filter:v", "setpts=PTS/${speed}") val cropString = if(crop.notCropped()) "" else "crop=${crop.relativeWidth}*iw:${crop.relativeHeight}*ih:${crop.relativeX}*iw:${crop.relativeY}*ih"
val separator = if(speedIndex != 1 && !crop.notCropped()) "," else ""
val speedString = if(speedIndex != 1) "setpts=PTS/${speed}" else ""
val speedAndCropString: List<String?> = if(speedIndex!= 1 || !crop.notCropped())
listOf("-filter:v", speedString + separator + cropString)
// Stream copy is not compatible with filter, but when not filtering we can copy the stream without re-encoding // Stream copy is not compatible with filter, but when not filtering we can copy the stream without re-encoding
else listOf("-c", "copy") else listOf("-c", "copy")
// This should be set when re-encoding is required (otherwise it defaults to mpeg which then doesn't play) // This should be set when re-encoding is required (otherwise it defaults to mpeg which then doesn't play)
val encodePreset: List<String?> = if(speedIndex != 1) listOf("-c:v", "libx264", "-preset", "ultrafast") else listOf(null, null, null, null) val encodePreset: List<String?> = if(speedIndex != 1 && !crop.notCropped()) listOf("-c:v", "libx264", "-preset", "ultrafast") else listOf(null, null, null, null)
val session: FFmpegSession = val session: FFmpegSession =
FFmpegKit.executeWithArgumentsAsync(listOfNotNull( FFmpegKit.executeWithArgumentsAsync(listOfNotNull(
startString[0], startString[1], startString[0], startString[1],
"-i", ffmpegCompliantUri, "-i", ffmpegCompliantUri,
speedString[0], speedString[1], speedAndCropString[0], speedAndCropString[1],
endString[0], endString[1], endString[0], endString[1],
mutedString, "-y", mutedString, "-y",
encodePreset[0], encodePreset[1], encodePreset[2], encodePreset[3], encodePreset[0], encodePreset[1], encodePreset[2], encodePreset[3],
outputVideoPath outputVideoPath,
).toTypedArray(), ).toTypedArray(),
//val session: FFmpegSession = FFmpegKit.executeAsync("$startString -i $inputSafePath $endString -c:v libvpx-vp9 -c:a copy -an -y $outputVideoPath", //val session: FFmpegSession = FFmpegKit.executeAsync("$startString -i $inputSafePath $endString -c:v libvpx-vp9 -c:a copy -an -y $outputVideoPath",
{ session -> { session ->

View File

@ -24,6 +24,7 @@ import org.pixeldroid.app.MainActivity
import org.pixeldroid.app.R import org.pixeldroid.app.R
import org.pixeldroid.app.postCreation.photoEdit.PhotoEditActivity import org.pixeldroid.app.postCreation.photoEdit.PhotoEditActivity
import org.pixeldroid.app.postCreation.photoEdit.VideoEditActivity import org.pixeldroid.app.postCreation.photoEdit.VideoEditActivity
import org.pixeldroid.app.postCreation.photoEdit.VideoEditActivity.RelativeCropPosition
import org.pixeldroid.app.utils.PixelDroidApplication import org.pixeldroid.app.utils.PixelDroidApplication
import org.pixeldroid.app.utils.api.objects.Attachment import org.pixeldroid.app.utils.api.objects.Attachment
import org.pixeldroid.app.utils.db.entities.InstanceDatabaseEntity import org.pixeldroid.app.utils.db.entities.InstanceDatabaseEntity
@ -61,6 +62,8 @@ data class PostCreationActivityUiState(
val newEncodingJobSpeedIndex: Int? = null, val newEncodingJobSpeedIndex: Int? = null,
val newEncodingJobVideoStart: Float? = null, val newEncodingJobVideoStart: Float? = null,
val newEncodingJobVideoEnd: Float? = null, val newEncodingJobVideoEnd: Float? = null,
val newEncodingJobVideoCrop: RelativeCropPosition? = null,
) )
class PostCreationViewModel(application: Application, clipdata: ClipData? = null, val instance: InstanceDatabaseEntity? = null) : AndroidViewModel(application) { class PostCreationViewModel(application: Application, clipdata: ClipData? = null, val instance: InstanceDatabaseEntity? = null) : AndroidViewModel(application) {
@ -372,6 +375,8 @@ class PostCreationViewModel(application: Application, clipdata: ClipData? = null
if(it == -1f) null else it if(it == -1f) null else it
} }
val videoCrop: RelativeCropPosition = data.getSerializableExtra(VideoEditActivity.VIDEO_CROP) as RelativeCropPosition
videoEncodeProgress = 0 videoEncodeProgress = 0
sessionMap[position]?.let { FFmpegKit.cancel(it) } sessionMap[position]?.let { FFmpegKit.cancel(it) }
_uiState.update { currentUiState -> _uiState.update { currentUiState ->
@ -380,7 +385,8 @@ class PostCreationViewModel(application: Application, clipdata: ClipData? = null
newEncodingJobMuted = muted, newEncodingJobMuted = muted,
newEncodingJobSpeedIndex = speedIndex, newEncodingJobSpeedIndex = speedIndex,
newEncodingJobVideoStart = videoStart, newEncodingJobVideoStart = videoStart,
newEncodingJobVideoEnd = videoEnd newEncodingJobVideoEnd = videoEnd,
newEncodingJobVideoCrop = videoCrop
) )
} }
} }

View File

@ -2,15 +2,13 @@ package org.pixeldroid.app.postCreation.photoEdit
import android.app.Activity import android.app.Activity
import android.app.AlertDialog import android.app.AlertDialog
import android.content.ContentUris
import android.content.Intent import android.content.Intent
import android.graphics.Rect
import android.media.AudioManager import android.media.AudioManager
import android.net.Uri import android.net.Uri
import android.os.Build
import android.os.Bundle import android.os.Bundle
import android.os.Handler import android.os.Handler
import android.os.Looper import android.os.Looper
import android.provider.MediaStore
import android.text.format.DateUtils import android.text.format.DateUtils
import android.util.Log import android.util.Log
import android.view.Menu import android.view.Menu
@ -24,8 +22,11 @@ import androidx.media.AudioAttributesCompat
import androidx.media2.common.MediaMetadata import androidx.media2.common.MediaMetadata
import androidx.media2.common.UriMediaItem import androidx.media2.common.UriMediaItem
import androidx.media2.player.MediaPlayer import androidx.media2.player.MediaPlayer
import androidx.media2.player.MediaPlayer.PlayerCallback import com.arthenica.ffmpegkit.FFmpegKit
import com.arthenica.ffmpegkit.* import com.arthenica.ffmpegkit.FFmpegKitConfig
import com.arthenica.ffmpegkit.FFprobeKit
import com.arthenica.ffmpegkit.MediaInformation
import com.arthenica.ffmpegkit.ReturnCode
import com.bumptech.glide.Glide import com.bumptech.glide.Glide
import com.google.android.material.slider.RangeSlider import com.google.android.material.slider.RangeSlider
import org.pixeldroid.app.R import org.pixeldroid.app.R
@ -35,14 +36,31 @@ import org.pixeldroid.app.postCreation.carousel.dpToPx
import org.pixeldroid.app.utils.BaseThemedWithBarActivity import org.pixeldroid.app.utils.BaseThemedWithBarActivity
import org.pixeldroid.app.utils.ffmpegCompliantUri import org.pixeldroid.app.utils.ffmpegCompliantUri
import java.io.File import java.io.File
import java.io.Serializable
import kotlin.math.absoluteValue
class VideoEditActivity : BaseThemedWithBarActivity() { class VideoEditActivity : BaseThemedWithBarActivity() {
data class RelativeCropPosition(
val relativeWidth: Float,
val relativeHeight: Float,
val relativeX: Float,
val relativeY: Float,
): Serializable {
fun notCropped(): Boolean =
(relativeX - 1f).absoluteValue < 0.001f
&& (relativeY - 1f).absoluteValue < 0.001f
&& relativeX.absoluteValue < 0.001f
&& relativeWidth.absoluteValue < 0.001f
}
private lateinit var mediaPlayer: MediaPlayer private lateinit var mediaPlayer: MediaPlayer
private var videoPosition: Int = -1 private var videoPosition: Int = -1
//TODO react to change of playbackSpeed (when changed in the player itself) private var cropRelativeDimensions: RelativeCropPosition = RelativeCropPosition(1f,1f,0f,0f)
private var speed: Int = 1 private var speed: Int = 1
set(value) { set(value) {
field = value field = value
@ -75,17 +93,11 @@ class VideoEditActivity : BaseThemedWithBarActivity() {
val uri = intent.getParcelableExtra<Uri>(PhotoEditActivity.PICTURE_URI)!! val uri = intent.getParcelableExtra<Uri>(PhotoEditActivity.PICTURE_URI)!!
binding.cropImageView.setImageUriAsync(uri)
videoPosition = intent.getIntExtra(PhotoEditActivity.PICTURE_POSITION, -1) videoPosition = intent.getIntExtra(PhotoEditActivity.PICTURE_POSITION, -1)
val inputVideoPath = ffmpegCompliantUri(uri) val inputVideoPath = ffmpegCompliantUri(uri)
val mediaInformation: MediaInformation? = FFprobeKit.getMediaInformation(inputVideoPath).mediaInformation val mediaInformation: MediaInformation? = FFprobeKit.getMediaInformation(inputVideoPath).mediaInformation
binding.muter.setOnClickListener {
binding.muter.isSelected = !binding.muter.isSelected
}
//Duration in seconds, or null //Duration in seconds, or null
val duration: Float? = mediaInformation?.duration?.toFloatOrNull() val duration: Float? = mediaInformation?.duration?.toFloatOrNull()
@ -126,6 +138,38 @@ class VideoEditActivity : BaseThemedWithBarActivity() {
binding.muter.isSelected = !binding.muter.isSelected binding.muter.isSelected = !binding.muter.isSelected
} }
binding.cropper.setOnClickListener {
//TODO set crop from saved value
showCropInterface(show = true, uri = uri)
}
binding.saveCropButton.setOnClickListener {
// This is the rectangle selected by the crop
val cropRect = binding.cropImageView.cropWindowRect ?: return@setOnClickListener
// This is the rectangle of the whole image
val fullImageRect: Rect = binding.cropImageView.getInitialCropWindowRect()
// x, y are coordinates of top left, in the ImageView
val x = cropRect.left - fullImageRect.left
val y = cropRect.top - fullImageRect.top
// width and height selected by the crop
val width = cropRect.width()
val height = cropRect.height()
// To avoid having to calculate the dimensions of the video here, we pass
// relative width, height and x, y back to be treated in FFmpeg
cropRelativeDimensions = RelativeCropPosition(
relativeWidth = width/fullImageRect.width(),
relativeHeight = height/fullImageRect.height(),
relativeX = x/fullImageRect.width(),
relativeY = y/fullImageRect.height()
)
showCropInterface(show = false)
}
binding.videoView.setPlayer(mediaPlayer) binding.videoView.setPlayer(mediaPlayer)
mediaPlayer.seekTo((binding.videoRangeSeekBar.values[1]*1000).toLong()) mediaPlayer.seekTo((binding.videoRangeSeekBar.values[1]*1000).toLong())
@ -230,9 +274,33 @@ class VideoEditActivity : BaseThemedWithBarActivity() {
val muted = binding.muter.isSelected val muted = binding.muter.isSelected
val speedUnchanged = speed == 1 val speedUnchanged = speed == 1
return !muted && videoPositions && speedUnchanged return !muted && videoPositions && speedUnchanged && cropRelativeDimensions.notCropped()
} }
private fun showCropInterface(show: Boolean, uri: Uri? = null){
val visibilityOfOthers = if(show) View.GONE else View.VISIBLE
val visibilityOfCrop = if(show) View.VISIBLE else View.GONE
if(show) mediaPlayer.pause()
binding.muter.visibility = visibilityOfOthers
binding.speeder.visibility = visibilityOfOthers
binding.cropper.visibility = visibilityOfOthers
binding.videoRangeSeekBar.visibility = visibilityOfOthers
binding.videoView.visibility = visibilityOfOthers
binding.thumbnail1.visibility = visibilityOfOthers
binding.thumbnail2.visibility = visibilityOfOthers
binding.thumbnail3.visibility = visibilityOfOthers
binding.thumbnail4.visibility = visibilityOfOthers
binding.thumbnail5.visibility = visibilityOfOthers
binding.thumbnail6.visibility = visibilityOfOthers
binding.thumbnail7.visibility = visibilityOfOthers
binding.cropImageView.visibility = visibilityOfCrop
binding.saveCropButton.visibility = visibilityOfCrop
if(show && uri != null) binding.cropImageView.setImageUriAsync(uri, cropRelativeDimensions)
}
private fun returnWithValues() { private fun returnWithValues() {
val intent = Intent(this, PostCreationActivity::class.java) val intent = Intent(this, PostCreationActivity::class.java)
@ -243,6 +311,7 @@ class VideoEditActivity : BaseThemedWithBarActivity() {
putExtra(MODIFIED, !noEdits()) putExtra(MODIFIED, !noEdits())
putExtra(VIDEO_START, binding.videoRangeSeekBar.values.first()) putExtra(VIDEO_START, binding.videoRangeSeekBar.values.first())
putExtra(VIDEO_END, binding.videoRangeSeekBar.values[2]) putExtra(VIDEO_END, binding.videoRangeSeekBar.values[2])
putExtra(VIDEO_CROP, cropRelativeDimensions)
addFlags(Intent.FLAG_ACTIVITY_REORDER_TO_FRONT) addFlags(Intent.FLAG_ACTIVITY_REORDER_TO_FRONT)
} }
@ -253,6 +322,7 @@ class VideoEditActivity : BaseThemedWithBarActivity() {
private fun resetControls() { private fun resetControls() {
binding.videoRangeSeekBar.values = listOf(0f, binding.videoRangeSeekBar.valueTo/2, binding.videoRangeSeekBar.valueTo) binding.videoRangeSeekBar.values = listOf(0f, binding.videoRangeSeekBar.valueTo/2, binding.videoRangeSeekBar.valueTo)
binding.muter.isSelected = false binding.muter.isSelected = false
binding.cropImageView.resetCropRect()
} }
override fun onDestroy() { override fun onDestroy() {
@ -315,6 +385,7 @@ class VideoEditActivity : BaseThemedWithBarActivity() {
val speedChoices: List<Number> = listOf(0.5, 1, 2, 4, 8) val speedChoices: List<Number> = listOf(0.5, 1, 2, 4, 8)
const val VIDEO_START = "VideoEditVideoStartTag" const val VIDEO_START = "VideoEditVideoStartTag"
const val VIDEO_END = "VideoEditVideoEndTag" const val VIDEO_END = "VideoEditVideoEndTag"
const val VIDEO_CROP = "VideoEditVideoCropTag"
const val MODIFIED = "VideoEditModifiedTag" const val MODIFIED = "VideoEditModifiedTag"
} }
} }

View File

@ -8,108 +8,94 @@ import android.net.Uri
import android.util.AttributeSet import android.util.AttributeSet
import android.view.LayoutInflater import android.view.LayoutInflater
import android.widget.FrameLayout import android.widget.FrameLayout
import android.widget.ImageView
import androidx.core.graphics.toRect import androidx.core.graphics.toRect
import com.bumptech.glide.Glide import com.bumptech.glide.Glide
import com.bumptech.glide.load.DataSource import com.bumptech.glide.load.DataSource
import com.bumptech.glide.load.engine.GlideException import com.bumptech.glide.load.engine.GlideException
import com.bumptech.glide.request.RequestListener import com.bumptech.glide.request.RequestListener
import com.bumptech.glide.request.target.Target import com.bumptech.glide.request.target.Target
import org.pixeldroid.app.R import org.pixeldroid.app.databinding.CropImageViewBinding
import org.pixeldroid.app.postCreation.photoEdit.VideoEditActivity
/** Custom view that provides cropping capabilities to an image. */ /** Custom view that provides cropping capabilities to an image. */
class CropImageView @JvmOverloads constructor(context: Context?, attrs: AttributeSet? = null) : class CropImageView @JvmOverloads constructor(context: Context?, attrs: AttributeSet? = null) :
FrameLayout(context!!, attrs) { FrameLayout(context!!, attrs) {
/** Image view widget used to show the image for cropping. */
private val mImageView: ImageView
/** Overlay over the image view to show cropping UI. */
private val mCropOverlayView: CropOverlayView?
/** The sample size the image was loaded by if was loaded by URI */ private val binding: CropImageViewBinding =
private var mLoadedSampleSize = 1 CropImageViewBinding.inflate(LayoutInflater.from(context), this, true)
init { init {
val inflater = LayoutInflater.from(context) binding.CropOverlayView.setInitialAttributeValues()
val v = inflater.inflate(R.layout.crop_image_view, this, true)
mImageView = v.findViewById(R.id.ImageView_image)
mCropOverlayView = v.findViewById(R.id.CropOverlayView)
mCropOverlayView.setInitialAttributeValues()
} }
/** /**
* Gets the crop window's position relative to the parent's view at screen. * Gets the crop window's position relative to the parent's view at screen.
* *
* @return a Rect instance containing cropped area boundaries of the source Bitmap * @return a Rect instance containing notCropped area boundaries of the source Bitmap
*/ */
val cropWindowRect: RectF? val cropWindowRect: RectF?
get() = mCropOverlayView?.cropWindowRect// Get crop window position relative to the displayed image. get() = binding.CropOverlayView.cropWindowRect
/**
* Set the crop window position and size to the given rectangle.
* Image to crop must be first set before invoking this, for async - after complete callback.
*
* @param rect window rectangle (position and size) relative to source bitmap
*/
fun setCropRect(rect: Rect?) {
mCropOverlayView!!.initialCropWindowRect = rect
}
/** Reset crop window to initial rectangle. */ /** Reset crop window to initial rectangle. */
fun resetCropRect() { fun resetCropRect() {
mCropOverlayView!!.resetCropWindowRect() binding.CropOverlayView.resetCropWindowRect()
} }
fun getInitialCropWindowRect(): Rect = binding.CropOverlayView.initialCropWindowRect
/** /**
* Sets a bitmap loaded from the given Android URI as the content of the CropImageView.<br></br> * Sets the image loaded from the given URI as the content of the CropImageView
* Can be used with URI from gallery or camera source.<br></br>
* Will rotate the image by exif data.<br></br>
* *
* @param uri the URI to load the image from * @param uri the URI to load the image from
*/ */
fun setImageUriAsync(uri: Uri) { fun setImageUriAsync(uri: Uri, cropRelativeDimensions: VideoEditActivity.RelativeCropPosition) {
// either no existing task is working or we canceled it, need to load new URI // either no existing task is working or we canceled it, need to load new URI
mCropOverlayView!!.initialCropWindowRect = null binding.CropOverlayView.initialCropWindowRect = null
Glide.with(this).load(uri).fitCenter().listener(object : RequestListener<Drawable> { Glide.with(this).load(uri).fitCenter().listener(object : RequestListener<Drawable> {
override fun onLoadFailed(e: GlideException?, model: Any?, target: Target<Drawable>?, isFirstResource: Boolean): Boolean {return false } override fun onLoadFailed(
e: GlideException?,
m: Any?,
t: Target<Drawable>?,
i: Boolean,
): Boolean {
return false
}
override fun onResourceReady( override fun onResourceReady(
resource: Drawable?, resource: Drawable?,
model: Any?, model: Any?,
target: Target<Drawable>?, target: Target<Drawable>?,
dataSource: DataSource?, dataSource: DataSource?,
isFirstResource: Boolean isFirstResource: Boolean,
): Boolean { ): Boolean {
// Get width and height that the image will take on the screen // Get width and height that the image will take on the screen
val drawnWidth = resource?.intrinsicWidth ?: width val drawnWidth = resource?.intrinsicWidth ?: width
val drawnHeight = resource?.intrinsicHeight ?: height val drawnHeight = resource?.intrinsicHeight ?: height
mCropOverlayView.cropWindowRect = binding.CropOverlayView.initialCropWindowRect = RectF(
RectF((width - drawnWidth)/2f, (height - drawnHeight)/2f, (width + drawnWidth)/2f, (height + drawnHeight)/2f) (width - drawnWidth) / 2f,
mCropOverlayView.initialCropWindowRect = mCropOverlayView.cropWindowRect.toRect() (height - drawnHeight) / 2f,
mCropOverlayView.setCropWindowLimits(drawnWidth.toFloat(), drawnHeight.toFloat(), 1f, 1f) (width + drawnWidth) / 2f,
setBitmap() (height + drawnHeight) / 2f
).toRect()
binding.CropOverlayView.setCropWindowLimits(
drawnWidth.toFloat(),
drawnHeight.toFloat()
)
binding.CropOverlayView.invalidate()
binding.CropOverlayView.setBounds(width, height)
binding.CropOverlayView.resetCropOverlayView()
if (!cropRelativeDimensions.notCropped()) binding.CropOverlayView.setRecordedCropWindowRect(cropRelativeDimensions)
binding.CropOverlayView.visibility = VISIBLE
// Indicate to Glide that the image hasn't been set yet // Indicate to Glide that the image hasn't been set yet
return false return false
} }
}).into(mImageView) }).into(binding.ImageViewImage)
}
/**
* Set the given bitmap to be used in for cropping<br></br>
* Optionally clear full if the bitmap is new, or partial clear if the bitmap has been
* manipulated.
*/
private fun setBitmap() {
mLoadedSampleSize = 1
if (mCropOverlayView != null) {
mCropOverlayView.invalidate()
mCropOverlayView.setBounds(width, height)
mCropOverlayView.resetCropOverlayView()
mCropOverlayView.visibility = VISIBLE
}
} }
} }

View File

@ -13,6 +13,10 @@ import android.util.TypedValue;
import android.view.MotionEvent; import android.view.MotionEvent;
import android.view.View; import android.view.View;
import androidx.annotation.NonNull;
import org.pixeldroid.app.postCreation.photoEdit.VideoEditActivity;
/** A custom View representing the crop window and the shaded background outside the crop window. */ /** A custom View representing the crop window and the shaded background outside the crop window. */
public class CropOverlayView extends View { public class CropOverlayView extends View {
@ -30,9 +34,6 @@ public class CropOverlayView extends View {
/** The Paint used to draw the guidelines within the crop area when pressed. */ /** The Paint used to draw the guidelines within the crop area when pressed. */
private Paint mGuidelinePaint; private Paint mGuidelinePaint;
/** The Paint used to darken the surrounding areas outside the crop area. */
private final Paint mBackgroundPaint = getNewPaint(Color.argb(119, 0, 0, 0));
/** The bounding box around the Bitmap that we are cropping. */ /** The bounding box around the Bitmap that we are cropping. */
private final RectF mCalcBounds = new RectF(); private final RectF mCalcBounds = new RectF();
@ -42,18 +43,9 @@ public class CropOverlayView extends View {
/** The bounding image view height used to know the crop overlay is at view edges. */ /** The bounding image view height used to know the crop overlay is at view edges. */
private int mViewHeight; private int mViewHeight;
/** The initial crop window padding from image borders */
private float mInitialCropWindowPaddingRatio;
/** The Handle that is currently pressed; null if no Handle is pressed. */ /** The Handle that is currently pressed; null if no Handle is pressed. */
private CropWindowMoveHandler mMoveHandler; private CropWindowMoveHandler mMoveHandler;
/** save the current aspect ratio of the image */
private int mAspectRatioX;
/** save the current aspect ratio of the image */
private int mAspectRatioY;
/** the initial crop window rectangle to set */ /** the initial crop window rectangle to set */
private final Rect mInitialCropWindowRect = new Rect(); private final Rect mInitialCropWindowRect = new Rect();
@ -113,50 +105,12 @@ public class CropOverlayView extends View {
} }
} }
/** the X value of the aspect ratio; */
public int getAspectRatioX() {
return mAspectRatioX;
}
/** Sets the X value of the aspect ratio to 1. */
public void setAspectRatioX() {
if (mAspectRatioX != 1) {
mAspectRatioX = 1;
if (initializedCropWindow) {
initCropWindow();
invalidate();
}
}
}
/** the Y value of the aspect ratio; */
public int getAspectRatioY() {
return mAspectRatioY;
}
/** /**
* Sets the Y value of the aspect ratio to 1. * Set the max width/height and scale factor of the shown image to original image to scale the
*
*/
public void setAspectRatioY() {
if (mAspectRatioY != 1) {
mAspectRatioY = 1;
if (initializedCropWindow) {
initCropWindow();
invalidate();
}
}
}
/**
* set the max width/height and scale factor of the shown image to original image to scale the
* limits appropriately. * limits appropriately.
*/ */
public void setCropWindowLimits( public void setCropWindowLimits(float maxWidth, float maxHeight) {
float maxWidth, float maxHeight, float scaleFactorWidth, float scaleFactorHeight) { mCropWindowHandler.setCropWindowLimits(maxWidth, maxHeight);
mCropWindowHandler.setCropWindowLimits(
maxWidth, maxHeight, scaleFactorWidth, scaleFactorHeight);
} }
/** Get crop window initial rectangle. */ /** Get crop window initial rectangle. */
@ -164,6 +118,19 @@ public class CropOverlayView extends View {
return mInitialCropWindowRect; return mInitialCropWindowRect;
} }
public void setRecordedCropWindowRect(@NonNull VideoEditActivity.RelativeCropPosition relativeCropPosition) {
Rect rect = new Rect(
(int) (mInitialCropWindowRect.left + relativeCropPosition.getRelativeX() * mInitialCropWindowRect.width()),
(int) (mInitialCropWindowRect.top + relativeCropPosition.getRelativeY() * mInitialCropWindowRect.height()),
(int) (relativeCropPosition.getRelativeWidth() * mInitialCropWindowRect.width()
+ mInitialCropWindowRect.left + relativeCropPosition.getRelativeX() * mInitialCropWindowRect.width()),
(int) (relativeCropPosition.getRelativeHeight() * mInitialCropWindowRect.height()
+ mInitialCropWindowRect.top + relativeCropPosition.getRelativeY() * mInitialCropWindowRect.width())
);
//TODO call correct thing instead of initial (which sets the limits...)
setInitialCropWindowRect(rect);
}
/** Set crop window initial rectangle to be used instead of default. */ /** Set crop window initial rectangle to be used instead of default. */
public void setInitialCropWindowRect(Rect rect) { public void setInitialCropWindowRect(Rect rect) {
mInitialCropWindowRect.set(rect != null ? rect : new Rect()); mInitialCropWindowRect.set(rect != null ? rect : new Rect());
@ -188,25 +155,21 @@ public class CropOverlayView extends View {
public void setInitialAttributeValues() { public void setInitialAttributeValues() {
DisplayMetrics dm = Resources.getSystem().getDisplayMetrics(); DisplayMetrics dm = Resources.getSystem().getDisplayMetrics();
setAspectRatioX(); mBorderPaint =
getNewPaintOfThickness(TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 3, dm), Color.argb(170, 255, 255, 255));
setAspectRatioY();
mInitialCropWindowPaddingRatio = 0.1f;
mBorderPaint = getNewPaintOrNull(TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 3, dm), Color.argb(170, 255, 255, 255));
mBorderCornerPaint = mBorderCornerPaint =
getNewPaintOrNull(TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 2, dm), Color.WHITE); getNewPaintOfThickness(TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 2, dm), Color.WHITE);
mGuidelinePaint = getNewPaintOrNull(TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 1, dm), Color.argb(170, 255, 255, 255)); mGuidelinePaint =
getNewPaintOfThickness(TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 1, dm), Color.argb(170, 255, 255, 255));
} }
// region: Private methods // region: Private methods
/** /**
* Set the initial crop window size and position. This is dependent on the size and position of * Set the initial crop window size and position. This is dependent on the size and position of
* the image being cropped. * the image being notCropped.
*/ */
private void initCropWindow() { private void initCropWindow() {
@ -281,7 +244,7 @@ public class CropOverlayView extends View {
super.onDraw(canvas); super.onDraw(canvas);
// Draw translucent background for the cropped area. // Draw translucent background for the notCropped area.
drawBackground(canvas); drawBackground(canvas);
if (mCropWindowHandler.showGuidelines()) { if (mCropWindowHandler.showGuidelines()) {
@ -302,7 +265,7 @@ public class CropOverlayView extends View {
RectF rect = mCropWindowHandler.getRect(); RectF rect = mCropWindowHandler.getRect();
canvas.drawRect(rect.left, rect.top, rect.right, rect.bottom, mBackgroundPaint); canvas.drawRect(rect.left, rect.top, rect.right, rect.bottom, getNewPaint(Color.argb(119, 0, 0, 0)));
} }
/** /**
@ -433,18 +396,14 @@ public class CropOverlayView extends View {
return paint; return paint;
} }
/** Creates the Paint object for given thickness and color, if thickness < 0 return null. */ /** Creates the Paint object for given thickness and color */
private static Paint getNewPaintOrNull(float thickness, int color) { private static Paint getNewPaintOfThickness(float thickness, int color) {
if (thickness > 0) { Paint borderPaint = new Paint();
Paint borderPaint = new Paint(); borderPaint.setColor(color);
borderPaint.setColor(color); borderPaint.setStrokeWidth(thickness);
borderPaint.setStrokeWidth(thickness); borderPaint.setStyle(Paint.Style.STROKE);
borderPaint.setStyle(Paint.Style.STROKE); borderPaint.setAntiAlias(true);
borderPaint.setAntiAlias(true); return borderPaint;
return borderPaint;
} else {
return null;
}
} }
@Override @Override
@ -523,8 +482,6 @@ public class CropOverlayView extends View {
/** /**
* Calculate the bounding rectangle for current crop window * Calculate the bounding rectangle for current crop window
* The bounds rectangle is the bitmap rectangle * The bounds rectangle is the bitmap rectangle
*
* @param rect the crop window rectangle to start finding bounded rectangle from
*/ */
private void calculateBounds(RectF rect) { private void calculateBounds(RectF rect) {
mCalcBounds.set(mInitialCropWindowRect); mCalcBounds.set(mInitialCropWindowRect);

View File

@ -25,11 +25,6 @@ final class CropWindowHandler {
/** Maximum height in pixels that the crop window can CURRENTLY get. */ /** Maximum height in pixels that the crop window can CURRENTLY get. */
private float mMaxCropWindowHeight; private float mMaxCropWindowHeight;
/** The width scale factor of shown image and actual image */
private float mScaleFactorWidth = 1;
/** The height scale factor of shown image and actual image */
private float mScaleFactorHeight = 1;
// endregion // endregion
/** Get the left/top/right/bottom coordinates of the crop window. */ /** Get the left/top/right/bottom coordinates of the crop window. */
@ -46,7 +41,7 @@ final class CropWindowHandler {
*/ */
DisplayMetrics dm = Resources.getSystem().getDisplayMetrics(); DisplayMetrics dm = Resources.getSystem().getDisplayMetrics();
float mMinCropResultWidth = 40; float mMinCropResultWidth = 40;
return Math.max((int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 42, dm), mMinCropResultWidth / mScaleFactorWidth); return Math.max((int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 42, dm), mMinCropResultWidth);
} }
/** Minimum height in pixels that the crop window can get. */ /** Minimum height in pixels that the crop window can get. */
@ -57,49 +52,27 @@ final class CropWindowHandler {
*/ */
DisplayMetrics dm = Resources.getSystem().getDisplayMetrics(); DisplayMetrics dm = Resources.getSystem().getDisplayMetrics();
float mMinCropResultHeight = 40; float mMinCropResultHeight = 40;
return Math.max((int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 42, dm), mMinCropResultHeight / mScaleFactorHeight); return Math.max((int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 42, dm), mMinCropResultHeight);
} }
/** Maximum width in pixels that the crop window can get. */ /** Maximum width in pixels that the crop window can get. */
public float getMaxCropWidth() { public float getMaxCropWidth() {
/*
* Maximum width in pixels that the result of cropping an image can get, affects crop window width
* adjusted by width scale factor.
*/
float mMaxCropResultWidth = 99999; float mMaxCropResultWidth = 99999;
return Math.min(mMaxCropWindowWidth, mMaxCropResultWidth / mScaleFactorWidth); return Math.min(mMaxCropWindowWidth, mMaxCropResultWidth);
} }
/** Maximum height in pixels that the crop window can get. */ /** Maximum height in pixels that the crop window can get. */
public float getMaxCropHeight() { public float getMaxCropHeight() {
/*
* Maximum height in pixels that the result of cropping an image can get, affects crop window
* height adjusted by height scale factor.
*/
float mMaxCropResultHeight = 99999; float mMaxCropResultHeight = 99999;
return Math.min(mMaxCropWindowHeight, mMaxCropResultHeight / mScaleFactorHeight); return Math.min(mMaxCropWindowHeight, mMaxCropResultHeight);
}
/** get the scale factor (on width) of the shown image to original image. */
public float getScaleFactorWidth() {
return mScaleFactorWidth;
}
/** get the scale factor (on height) of the shown image to original image. */
public float getScaleFactorHeight() {
return mScaleFactorHeight;
} }
/** /**
* set the max width/height and scale factor of the shown image to original image to scale the * Set the max width/height of the shown image to original image to scale the limits appropriately
* limits appropriately.
*/ */
public void setCropWindowLimits( public void setCropWindowLimits(float maxWidth, float maxHeight) {
float maxWidth, float maxHeight, float scaleFactorWidth, float scaleFactorHeight) {
mMaxCropWindowWidth = maxWidth; mMaxCropWindowWidth = maxWidth;
mMaxCropWindowHeight = maxHeight; mMaxCropWindowHeight = maxHeight;
mScaleFactorWidth = scaleFactorWidth;
mScaleFactorHeight = scaleFactorHeight;
} }
/** Set the left/top/right/bottom coordinates of the crop window. */ /** Set the left/top/right/bottom coordinates of the crop window. */
@ -126,8 +99,7 @@ final class CropWindowHandler {
* @param targetRadius the target radius in pixels * @param targetRadius the target radius in pixels
* @return the Handle that was pressed; null if no Handle was pressed * @return the Handle that was pressed; null if no Handle was pressed
*/ */
public CropWindowMoveHandler getMoveHandler( public CropWindowMoveHandler getMoveHandler(float x, float y, float targetRadius) {
float x, float y, float targetRadius) {
CropWindowMoveHandler.Type type = getRectanglePressedMoveType(x, y, targetRadius); CropWindowMoveHandler.Type type = getRectanglePressedMoveType(x, y, targetRadius);
return type != null ? new CropWindowMoveHandler(type, this, x, y) : null; return type != null ? new CropWindowMoveHandler(type, this, x, y) : null;
} }

View File

@ -236,7 +236,7 @@ final class CropWindowMoveHandler {
* and the image's bounding box and snap radius. * and the image's bounding box and snap radius.
* *
* @param left the position that the left edge is dragged to * @param left the position that the left edge is dragged to
* @param bounds the bounding box of the image that is being cropped * @param bounds the bounding box of the image that is being notCropped
* @param snapMargin the snap distance to the image edge (in pixels) * @param snapMargin the snap distance to the image edge (in pixels)
*/ */
private void adjustLeft( private void adjustLeft(
@ -282,7 +282,7 @@ final class CropWindowMoveHandler {
* and the image's bounding box and snap radius. * and the image's bounding box and snap radius.
* *
* @param right the position that the right edge is dragged to * @param right the position that the right edge is dragged to
* @param bounds the bounding box of the image that is being cropped * @param bounds the bounding box of the image that is being notCropped
* @param viewWidth * @param viewWidth
* @param snapMargin the snap distance to the image edge (in pixels) * @param snapMargin the snap distance to the image edge (in pixels)
*/ */
@ -332,7 +332,7 @@ final class CropWindowMoveHandler {
* the image's bounding box and snap radius. * the image's bounding box and snap radius.
* *
* @param top the x-position that the top edge is dragged to * @param top the x-position that the top edge is dragged to
* @param bounds the bounding box of the image that is being cropped * @param bounds the bounding box of the image that is being notCropped
* @param snapMargin the snap distance to the image edge (in pixels) * @param snapMargin the snap distance to the image edge (in pixels)
*/ */
private void adjustTop( private void adjustTop(
@ -378,7 +378,7 @@ final class CropWindowMoveHandler {
* and the image's bounding box and snap radius. * and the image's bounding box and snap radius.
* *
* @param bottom the position that the bottom edge is dragged to * @param bottom the position that the bottom edge is dragged to
* @param bounds the bounding box of the image that is being cropped * @param bounds the bounding box of the image that is being notCropped
* @param viewHeight * @param viewHeight
* @param snapMargin the snap distance to the image edge (in pixels) * @param snapMargin the snap distance to the image edge (in pixels)
*/ */

View File

@ -22,12 +22,26 @@
<org.pixeldroid.app.postCreation.photoEdit.cropper.CropImageView <org.pixeldroid.app.postCreation.photoEdit.cropper.CropImageView
android:id="@+id/cropImageView" android:id="@+id/cropImageView"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="0dp" android:layout_height="match_parent"
app:layout_constraintBottom_toBottomOf="@+id/videoView" app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintTop_toTopOf="parent" app:layout_constraintTop_toTopOf="parent"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
android:visibility="visible"/> android:visibility="gone"/>
<com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton
android:id="@+id/save_crop_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="bottom|end"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
android:layout_margin="16dp"
android:visibility="gone"
android:text="@string/save_crop"
android:contentDescription="@string/save_crop"
app:icon="@drawable/ic_crop_black_24dp"/>
<ImageView <ImageView
android:id="@+id/muter" android:id="@+id/muter"
@ -42,7 +56,6 @@
app:layout_constraintBottom_toTopOf="@+id/thumbnail1" app:layout_constraintBottom_toTopOf="@+id/thumbnail1"
app:layout_constraintStart_toStartOf="parent" /> app:layout_constraintStart_toStartOf="parent" />
<ImageView <ImageView
android:id="@+id/speeder" android:id="@+id/speeder"
android:layout_width="60dp" android:layout_width="60dp"
@ -56,6 +69,20 @@
app:layout_constraintBottom_toTopOf="@+id/thumbnail1" app:layout_constraintBottom_toTopOf="@+id/thumbnail1"
app:layout_constraintStart_toEndOf="@+id/muter" /> app:layout_constraintStart_toEndOf="@+id/muter" />
<ImageView
android:id="@+id/cropper"
android:layout_width="60dp"
android:layout_height="40dp"
android:layout_marginStart="16dp"
android:layout_marginBottom="48dp"
android:contentDescription="@string/video_crop"
android:background="?attr/selectableItemBackgroundBorderless"
android:padding="4dp"
app:tint="@android:color/white"
android:src="@drawable/ic_crop_black_24dp"
app:layout_constraintBottom_toTopOf="@+id/thumbnail1"
app:layout_constraintStart_toEndOf="@+id/speeder" />
<com.google.android.material.slider.RangeSlider <com.google.android.material.slider.RangeSlider
android:id="@+id/videoRangeSeekBar" android:id="@+id/videoRangeSeekBar"
android:layout_width="match_parent" android:layout_width="match_parent"

View File

@ -271,6 +271,7 @@ For more info about Pixelfed, you can check here: https://pixelfed.org"</string>
<string name="select_video_range">Select what to keep of the video</string> <string name="select_video_range">Select what to keep of the video</string>
<string name="mute_video">Mute video</string> <string name="mute_video">Mute video</string>
<string name="video_speed">Change video speed</string> <string name="video_speed">Change video speed</string>
<string name="video_crop">Crop video</string>
<string name="still_encoding">One or more videos are still encoding. Wait for them to finish before uploading</string> <string name="still_encoding">One or more videos are still encoding. Wait for them to finish before uploading</string>
<string name="new_post_shortcut_long">Create new post</string> <string name="new_post_shortcut_long">Create new post</string>
<string name="new_post_shortcut_short">New post</string> <string name="new_post_shortcut_short">New post</string>
@ -293,6 +294,7 @@ For more info about Pixelfed, you can check here: https://pixelfed.org"</string>
<string name="add_images_error">Error while adding images</string> <string name="add_images_error">Error while adding images</string>
<string name="notification_thumbnail">"Thumbnail of image in this notification's post"</string> <string name="notification_thumbnail">"Thumbnail of image in this notification's post"</string>
<string name="post_preview">Preview of a post</string> <string name="post_preview">Preview of a post</string>
<string name="save_crop">Save crop</string>
<plurals name="replies_count"> <plurals name="replies_count">
<item quantity="one">%d reply</item> <item quantity="one">%d reply</item>
<item quantity="other">%d replies</item> <item quantity="other">%d replies</item>