Progress on video crop
This commit is contained in:
parent
76e2a43de4
commit
896c9634eb
@ -47,7 +47,6 @@ import java.io.OutputStream
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.*
|
||||
import kotlin.math.roundToInt
|
||||
import kotlin.collections.flatten
|
||||
|
||||
const val TAG = "Post Creation Activity"
|
||||
|
||||
@ -131,8 +130,13 @@ class PostCreationActivity : BaseThemedWithoutBarActivity() {
|
||||
uiState.newEncodingJobVideoStart?.let { videoStart ->
|
||||
uiState.newEncodingJobVideoEnd?.let { videoEnd ->
|
||||
uiState.newEncodingJobSpeedIndex?.let { speedIndex ->
|
||||
startEncoding(position, muted, videoStart, videoEnd, speedIndex)
|
||||
model.encodingStarted()
|
||||
uiState.newEncodingJobVideoCrop?.let { crop ->
|
||||
startEncoding(position, muted,
|
||||
videoStart, videoEnd,
|
||||
speedIndex, crop
|
||||
)
|
||||
model.encodingStarted()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -331,7 +335,8 @@ class PostCreationActivity : BaseThemedWithoutBarActivity() {
|
||||
muted: Boolean,
|
||||
videoStart: Float?,
|
||||
videoEnd: Float?,
|
||||
speedIndex: Int
|
||||
speedIndex: Int,
|
||||
crop: VideoEditActivity.RelativeCropPosition
|
||||
) {
|
||||
val originalUri = model.getPhotoData().value!![position].imageUri
|
||||
|
||||
@ -352,29 +357,33 @@ class PostCreationActivity : BaseThemedWithoutBarActivity() {
|
||||
|
||||
val speed = VideoEditActivity.speedChoices[speedIndex]
|
||||
|
||||
//TODO also have audio when speed is changed?
|
||||
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 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)
|
||||
listOf("-filter:v", "setpts=PTS/${speed}")
|
||||
// iw and ih are variables for the original width and height values, FFmpeg will know them
|
||||
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
|
||||
else listOf("-c", "copy")
|
||||
|
||||
// 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 =
|
||||
FFmpegKit.executeWithArgumentsAsync(listOfNotNull(
|
||||
startString[0], startString[1],
|
||||
"-i", ffmpegCompliantUri,
|
||||
speedString[0], speedString[1],
|
||||
speedAndCropString[0], speedAndCropString[1],
|
||||
endString[0], endString[1],
|
||||
mutedString, "-y",
|
||||
encodePreset[0], encodePreset[1], encodePreset[2], encodePreset[3],
|
||||
outputVideoPath
|
||||
outputVideoPath,
|
||||
).toTypedArray(),
|
||||
//val session: FFmpegSession = FFmpegKit.executeAsync("$startString -i $inputSafePath $endString -c:v libvpx-vp9 -c:a copy -an -y $outputVideoPath",
|
||||
{ session ->
|
||||
|
@ -24,6 +24,7 @@ import org.pixeldroid.app.MainActivity
|
||||
import org.pixeldroid.app.R
|
||||
import org.pixeldroid.app.postCreation.photoEdit.PhotoEditActivity
|
||||
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.api.objects.Attachment
|
||||
import org.pixeldroid.app.utils.db.entities.InstanceDatabaseEntity
|
||||
@ -61,6 +62,8 @@ data class PostCreationActivityUiState(
|
||||
val newEncodingJobSpeedIndex: Int? = null,
|
||||
val newEncodingJobVideoStart: Float? = null,
|
||||
val newEncodingJobVideoEnd: Float? = null,
|
||||
val newEncodingJobVideoCrop: RelativeCropPosition? = null,
|
||||
|
||||
)
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
val videoCrop: RelativeCropPosition = data.getSerializableExtra(VideoEditActivity.VIDEO_CROP) as RelativeCropPosition
|
||||
|
||||
videoEncodeProgress = 0
|
||||
sessionMap[position]?.let { FFmpegKit.cancel(it) }
|
||||
_uiState.update { currentUiState ->
|
||||
@ -380,7 +385,8 @@ class PostCreationViewModel(application: Application, clipdata: ClipData? = null
|
||||
newEncodingJobMuted = muted,
|
||||
newEncodingJobSpeedIndex = speedIndex,
|
||||
newEncodingJobVideoStart = videoStart,
|
||||
newEncodingJobVideoEnd = videoEnd
|
||||
newEncodingJobVideoEnd = videoEnd,
|
||||
newEncodingJobVideoCrop = videoCrop
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -2,15 +2,13 @@ package org.pixeldroid.app.postCreation.photoEdit
|
||||
|
||||
import android.app.Activity
|
||||
import android.app.AlertDialog
|
||||
import android.content.ContentUris
|
||||
import android.content.Intent
|
||||
import android.graphics.Rect
|
||||
import android.media.AudioManager
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import android.provider.MediaStore
|
||||
import android.text.format.DateUtils
|
||||
import android.util.Log
|
||||
import android.view.Menu
|
||||
@ -24,8 +22,11 @@ import androidx.media.AudioAttributesCompat
|
||||
import androidx.media2.common.MediaMetadata
|
||||
import androidx.media2.common.UriMediaItem
|
||||
import androidx.media2.player.MediaPlayer
|
||||
import androidx.media2.player.MediaPlayer.PlayerCallback
|
||||
import com.arthenica.ffmpegkit.*
|
||||
import com.arthenica.ffmpegkit.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.google.android.material.slider.RangeSlider
|
||||
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.ffmpegCompliantUri
|
||||
import java.io.File
|
||||
import java.io.Serializable
|
||||
import kotlin.math.absoluteValue
|
||||
|
||||
|
||||
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 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
|
||||
set(value) {
|
||||
field = value
|
||||
@ -75,17 +93,11 @@ class VideoEditActivity : BaseThemedWithBarActivity() {
|
||||
|
||||
val uri = intent.getParcelableExtra<Uri>(PhotoEditActivity.PICTURE_URI)!!
|
||||
|
||||
binding.cropImageView.setImageUriAsync(uri)
|
||||
|
||||
videoPosition = intent.getIntExtra(PhotoEditActivity.PICTURE_POSITION, -1)
|
||||
|
||||
val inputVideoPath = ffmpegCompliantUri(uri)
|
||||
val mediaInformation: MediaInformation? = FFprobeKit.getMediaInformation(inputVideoPath).mediaInformation
|
||||
|
||||
binding.muter.setOnClickListener {
|
||||
binding.muter.isSelected = !binding.muter.isSelected
|
||||
}
|
||||
|
||||
//Duration in seconds, or null
|
||||
val duration: Float? = mediaInformation?.duration?.toFloatOrNull()
|
||||
|
||||
@ -126,6 +138,38 @@ class VideoEditActivity : BaseThemedWithBarActivity() {
|
||||
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)
|
||||
|
||||
mediaPlayer.seekTo((binding.videoRangeSeekBar.values[1]*1000).toLong())
|
||||
@ -230,9 +274,33 @@ class VideoEditActivity : BaseThemedWithBarActivity() {
|
||||
val muted = binding.muter.isSelected
|
||||
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() {
|
||||
val intent = Intent(this, PostCreationActivity::class.java)
|
||||
@ -243,6 +311,7 @@ class VideoEditActivity : BaseThemedWithBarActivity() {
|
||||
putExtra(MODIFIED, !noEdits())
|
||||
putExtra(VIDEO_START, binding.videoRangeSeekBar.values.first())
|
||||
putExtra(VIDEO_END, binding.videoRangeSeekBar.values[2])
|
||||
putExtra(VIDEO_CROP, cropRelativeDimensions)
|
||||
addFlags(Intent.FLAG_ACTIVITY_REORDER_TO_FRONT)
|
||||
}
|
||||
|
||||
@ -253,6 +322,7 @@ class VideoEditActivity : BaseThemedWithBarActivity() {
|
||||
private fun resetControls() {
|
||||
binding.videoRangeSeekBar.values = listOf(0f, binding.videoRangeSeekBar.valueTo/2, binding.videoRangeSeekBar.valueTo)
|
||||
binding.muter.isSelected = false
|
||||
binding.cropImageView.resetCropRect()
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
@ -315,6 +385,7 @@ class VideoEditActivity : BaseThemedWithBarActivity() {
|
||||
val speedChoices: List<Number> = listOf(0.5, 1, 2, 4, 8)
|
||||
const val VIDEO_START = "VideoEditVideoStartTag"
|
||||
const val VIDEO_END = "VideoEditVideoEndTag"
|
||||
const val VIDEO_CROP = "VideoEditVideoCropTag"
|
||||
const val MODIFIED = "VideoEditModifiedTag"
|
||||
}
|
||||
}
|
@ -8,108 +8,94 @@ import android.net.Uri
|
||||
import android.util.AttributeSet
|
||||
import android.view.LayoutInflater
|
||||
import android.widget.FrameLayout
|
||||
import android.widget.ImageView
|
||||
import androidx.core.graphics.toRect
|
||||
import com.bumptech.glide.Glide
|
||||
import com.bumptech.glide.load.DataSource
|
||||
import com.bumptech.glide.load.engine.GlideException
|
||||
import com.bumptech.glide.request.RequestListener
|
||||
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. */
|
||||
class CropImageView @JvmOverloads constructor(context: Context?, attrs: AttributeSet? = null) :
|
||||
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 var mLoadedSampleSize = 1
|
||||
private val binding: CropImageViewBinding =
|
||||
CropImageViewBinding.inflate(LayoutInflater.from(context), this, true)
|
||||
|
||||
init {
|
||||
val inflater = LayoutInflater.from(context)
|
||||
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()
|
||||
binding.CropOverlayView.setInitialAttributeValues()
|
||||
}
|
||||
|
||||
/**
|
||||
* 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?
|
||||
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. */
|
||||
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>
|
||||
* Can be used with URI from gallery or camera source.<br></br>
|
||||
* Will rotate the image by exif data.<br></br>
|
||||
* Sets the image loaded from the given URI as the content of the CropImageView
|
||||
*
|
||||
* @param uri the URI to load the image from
|
||||
*/
|
||||
fun setImageUriAsync(uri: Uri) {
|
||||
// either no existing task is working or we canceled it, need to load new URI
|
||||
mCropOverlayView!!.initialCropWindowRect = null
|
||||
fun setImageUriAsync(uri: Uri, cropRelativeDimensions: VideoEditActivity.RelativeCropPosition) {
|
||||
// either no existing task is working or we canceled it, need to load new URI
|
||||
binding.CropOverlayView.initialCropWindowRect = null
|
||||
|
||||
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(
|
||||
resource: Drawable?,
|
||||
model: Any?,
|
||||
target: Target<Drawable>?,
|
||||
dataSource: DataSource?,
|
||||
isFirstResource: Boolean
|
||||
isFirstResource: Boolean,
|
||||
): Boolean {
|
||||
// Get width and height that the image will take on the screen
|
||||
val drawnWidth = resource?.intrinsicWidth ?: width
|
||||
val drawnHeight = resource?.intrinsicHeight ?: height
|
||||
|
||||
mCropOverlayView.cropWindowRect =
|
||||
RectF((width - drawnWidth)/2f, (height - drawnHeight)/2f, (width + drawnWidth)/2f, (height + drawnHeight)/2f)
|
||||
mCropOverlayView.initialCropWindowRect = mCropOverlayView.cropWindowRect.toRect()
|
||||
mCropOverlayView.setCropWindowLimits(drawnWidth.toFloat(), drawnHeight.toFloat(), 1f, 1f)
|
||||
setBitmap()
|
||||
binding.CropOverlayView.initialCropWindowRect = RectF(
|
||||
(width - drawnWidth) / 2f,
|
||||
(height - drawnHeight) / 2f,
|
||||
(width + drawnWidth) / 2f,
|
||||
(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
|
||||
return false
|
||||
}
|
||||
}).into(mImageView)
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
}
|
||||
}).into(binding.ImageViewImage)
|
||||
}
|
||||
}
|
@ -13,6 +13,10 @@ import android.util.TypedValue;
|
||||
import android.view.MotionEvent;
|
||||
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. */
|
||||
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. */
|
||||
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. */
|
||||
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. */
|
||||
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. */
|
||||
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 */
|
||||
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.
|
||||
*
|
||||
*/
|
||||
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
|
||||
* Set the max width/height and scale factor of the shown image to original image to scale the
|
||||
* limits appropriately.
|
||||
*/
|
||||
public void setCropWindowLimits(
|
||||
float maxWidth, float maxHeight, float scaleFactorWidth, float scaleFactorHeight) {
|
||||
mCropWindowHandler.setCropWindowLimits(
|
||||
maxWidth, maxHeight, scaleFactorWidth, scaleFactorHeight);
|
||||
public void setCropWindowLimits(float maxWidth, float maxHeight) {
|
||||
mCropWindowHandler.setCropWindowLimits(maxWidth, maxHeight);
|
||||
}
|
||||
|
||||
/** Get crop window initial rectangle. */
|
||||
@ -164,6 +118,19 @@ public class CropOverlayView extends View {
|
||||
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. */
|
||||
public void setInitialCropWindowRect(Rect rect) {
|
||||
mInitialCropWindowRect.set(rect != null ? rect : new Rect());
|
||||
@ -188,25 +155,21 @@ public class CropOverlayView extends View {
|
||||
public void setInitialAttributeValues() {
|
||||
DisplayMetrics dm = Resources.getSystem().getDisplayMetrics();
|
||||
|
||||
setAspectRatioX();
|
||||
|
||||
setAspectRatioY();
|
||||
|
||||
mInitialCropWindowPaddingRatio = 0.1f;
|
||||
|
||||
mBorderPaint = getNewPaintOrNull(TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 3, dm), Color.argb(170, 255, 255, 255));
|
||||
mBorderPaint =
|
||||
getNewPaintOfThickness(TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 3, dm), Color.argb(170, 255, 255, 255));
|
||||
|
||||
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
|
||||
|
||||
/**
|
||||
* 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() {
|
||||
|
||||
@ -281,7 +244,7 @@ public class CropOverlayView extends View {
|
||||
|
||||
super.onDraw(canvas);
|
||||
|
||||
// Draw translucent background for the cropped area.
|
||||
// Draw translucent background for the notCropped area.
|
||||
drawBackground(canvas);
|
||||
|
||||
if (mCropWindowHandler.showGuidelines()) {
|
||||
@ -302,7 +265,7 @@ public class CropOverlayView extends View {
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
/** Creates the Paint object for given thickness and color, if thickness < 0 return null. */
|
||||
private static Paint getNewPaintOrNull(float thickness, int color) {
|
||||
if (thickness > 0) {
|
||||
Paint borderPaint = new Paint();
|
||||
borderPaint.setColor(color);
|
||||
borderPaint.setStrokeWidth(thickness);
|
||||
borderPaint.setStyle(Paint.Style.STROKE);
|
||||
borderPaint.setAntiAlias(true);
|
||||
return borderPaint;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
/** Creates the Paint object for given thickness and color */
|
||||
private static Paint getNewPaintOfThickness(float thickness, int color) {
|
||||
Paint borderPaint = new Paint();
|
||||
borderPaint.setColor(color);
|
||||
borderPaint.setStrokeWidth(thickness);
|
||||
borderPaint.setStyle(Paint.Style.STROKE);
|
||||
borderPaint.setAntiAlias(true);
|
||||
return borderPaint;
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -523,8 +482,6 @@ public class CropOverlayView extends View {
|
||||
/**
|
||||
* Calculate the bounding rectangle for current crop window
|
||||
* The bounds rectangle is the bitmap rectangle
|
||||
*
|
||||
* @param rect the crop window rectangle to start finding bounded rectangle from
|
||||
*/
|
||||
private void calculateBounds(RectF rect) {
|
||||
mCalcBounds.set(mInitialCropWindowRect);
|
||||
|
@ -25,11 +25,6 @@ final class CropWindowHandler {
|
||||
/** Maximum height in pixels that the crop window can CURRENTLY get. */
|
||||
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
|
||||
|
||||
/** Get the left/top/right/bottom coordinates of the crop window. */
|
||||
@ -46,7 +41,7 @@ final class CropWindowHandler {
|
||||
*/
|
||||
DisplayMetrics dm = Resources.getSystem().getDisplayMetrics();
|
||||
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. */
|
||||
@ -57,49 +52,27 @@ final class CropWindowHandler {
|
||||
*/
|
||||
DisplayMetrics dm = Resources.getSystem().getDisplayMetrics();
|
||||
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. */
|
||||
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;
|
||||
return Math.min(mMaxCropWindowWidth, mMaxCropResultWidth / mScaleFactorWidth);
|
||||
return Math.min(mMaxCropWindowWidth, mMaxCropResultWidth);
|
||||
}
|
||||
|
||||
/** Maximum height in pixels that the crop window can get. */
|
||||
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;
|
||||
return Math.min(mMaxCropWindowHeight, mMaxCropResultHeight / mScaleFactorHeight);
|
||||
}
|
||||
|
||||
/** 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;
|
||||
return Math.min(mMaxCropWindowHeight, mMaxCropResultHeight);
|
||||
}
|
||||
|
||||
/**
|
||||
* set the max width/height and scale factor of the shown image to original image to scale the
|
||||
* limits appropriately.
|
||||
* Set the max width/height of the shown image to original image to scale the limits appropriately
|
||||
*/
|
||||
public void setCropWindowLimits(
|
||||
float maxWidth, float maxHeight, float scaleFactorWidth, float scaleFactorHeight) {
|
||||
public void setCropWindowLimits(float maxWidth, float maxHeight) {
|
||||
mMaxCropWindowWidth = maxWidth;
|
||||
mMaxCropWindowHeight = maxHeight;
|
||||
mScaleFactorWidth = scaleFactorWidth;
|
||||
mScaleFactorHeight = scaleFactorHeight;
|
||||
}
|
||||
|
||||
/** 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
|
||||
* @return the Handle that was pressed; null if no Handle was pressed
|
||||
*/
|
||||
public CropWindowMoveHandler getMoveHandler(
|
||||
float x, float y, float targetRadius) {
|
||||
public CropWindowMoveHandler getMoveHandler(float x, float y, float targetRadius) {
|
||||
CropWindowMoveHandler.Type type = getRectanglePressedMoveType(x, y, targetRadius);
|
||||
return type != null ? new CropWindowMoveHandler(type, this, x, y) : null;
|
||||
}
|
||||
|
@ -236,7 +236,7 @@ final class CropWindowMoveHandler {
|
||||
* and the image's bounding box and snap radius.
|
||||
*
|
||||
* @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)
|
||||
*/
|
||||
private void adjustLeft(
|
||||
@ -282,7 +282,7 @@ final class CropWindowMoveHandler {
|
||||
* and the image's bounding box and snap radius.
|
||||
*
|
||||
* @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 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.
|
||||
*
|
||||
* @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)
|
||||
*/
|
||||
private void adjustTop(
|
||||
@ -378,7 +378,7 @@ final class CropWindowMoveHandler {
|
||||
* and the image's bounding box and snap radius.
|
||||
*
|
||||
* @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 snapMargin the snap distance to the image edge (in pixels)
|
||||
*/
|
||||
|
@ -22,12 +22,26 @@
|
||||
<org.pixeldroid.app.postCreation.photoEdit.cropper.CropImageView
|
||||
android:id="@+id/cropImageView"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="0dp"
|
||||
app:layout_constraintBottom_toBottomOf="@+id/videoView"
|
||||
android:layout_height="match_parent"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="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
|
||||
android:id="@+id/muter"
|
||||
@ -42,7 +56,6 @@
|
||||
app:layout_constraintBottom_toTopOf="@+id/thumbnail1"
|
||||
app:layout_constraintStart_toStartOf="parent" />
|
||||
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/speeder"
|
||||
android:layout_width="60dp"
|
||||
@ -56,6 +69,20 @@
|
||||
app:layout_constraintBottom_toTopOf="@+id/thumbnail1"
|
||||
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
|
||||
android:id="@+id/videoRangeSeekBar"
|
||||
android:layout_width="match_parent"
|
||||
|
@ -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="mute_video">Mute video</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="new_post_shortcut_long">Create 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="notification_thumbnail">"Thumbnail of image in this notification's post"</string>
|
||||
<string name="post_preview">Preview of a post</string>
|
||||
<string name="save_crop">Save crop</string>
|
||||
<plurals name="replies_count">
|
||||
<item quantity="one">%d reply</item>
|
||||
<item quantity="other">%d replies</item>
|
||||
|
Loading…
x
Reference in New Issue
Block a user