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.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 ->
|
||||||
|
|
|
@ -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
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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"
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -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
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -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);
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
@ -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>
|
||||||
|
|
Loading…
Reference in New Issue