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.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 ->

View File

@ -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
)
}
}

View File

@ -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"
}
}

View File

@ -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)
}
}

View File

@ -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);

View File

@ -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;
}

View File

@ -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)
*/

View File

@ -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"

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="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>