mirror of
https://github.com/signalapp/Signal-Android.git
synced 2025-12-24 04:58:45 +00:00
Hoist video editor state out of VideoEditorFragment.
This commit is contained in:
committed by
Alex Hart
parent
619038f27d
commit
ccc9752485
@@ -23,24 +23,19 @@ import org.thoughtcrime.securesms.util.Throttler
|
||||
import org.thoughtcrime.securesms.video.VideoPlayer
|
||||
import org.thoughtcrime.securesms.video.VideoPlayer.PlayerCallback
|
||||
import org.thoughtcrime.securesms.video.videoconverter.VideoThumbnailsRangeSelectorView
|
||||
import org.thoughtcrime.securesms.video.videoconverter.VideoThumbnailsRangeSelectorView.OnRangeChangeListener
|
||||
import org.thoughtcrime.securesms.video.videoconverter.VideoThumbnailsRangeSelectorView.Thumb
|
||||
import org.thoughtcrime.securesms.video.videoconverter.VideoThumbnailsRangeSelectorView.PositionDragListener
|
||||
import java.io.IOException
|
||||
import java.util.concurrent.TimeUnit
|
||||
import kotlin.math.max
|
||||
|
||||
class VideoEditorFragment : Fragment(), OnRangeChangeListener, MediaSendPageFragment {
|
||||
class VideoEditorFragment : Fragment(), PositionDragListener, MediaSendPageFragment {
|
||||
private val sharedViewModel: MediaSelectionViewModel by viewModels(ownerProducer = { requireActivity() })
|
||||
private val videoScanThrottle = Throttler(150)
|
||||
private val handler = Handler(Looper.getMainLooper())
|
||||
|
||||
private var data = VideoTrimData()
|
||||
private var canEdit = false
|
||||
private var isVideoGif = false
|
||||
private var isInEdit = false
|
||||
private var isFocused = false
|
||||
private var wasPlayingBeforeEdit = false
|
||||
private var maxVideoDurationUs: Long = 0
|
||||
private var maxSend: Long = 0
|
||||
private lateinit var uri: Uri
|
||||
private lateinit var controller: Controller
|
||||
@@ -86,7 +81,6 @@ class VideoEditorFragment : Fragment(), OnRangeChangeListener, MediaSendPageFrag
|
||||
uri = requireArguments().getParcelable(KEY_URI)!!
|
||||
isVideoGif = requireArguments().getBoolean(KEY_IS_VIDEO_GIF)
|
||||
maxSend = requireArguments().getLong(KEY_MAX_SEND)
|
||||
maxVideoDurationUs = TimeUnit.MILLISECONDS.toMicros(requireArguments().getLong(KEY_MAX_DURATION))
|
||||
|
||||
val state = sharedViewModel.state.value!!
|
||||
val slide = VideoSlide(requireContext(), uri, 0, isVideoGif)
|
||||
@@ -136,15 +130,24 @@ class VideoEditorFragment : Fragment(), OnRangeChangeListener, MediaSendPageFrag
|
||||
})
|
||||
}
|
||||
|
||||
sharedViewModel.state.observe(viewLifecycleOwner) { state ->
|
||||
val focusedMedia = state.focusedMedia
|
||||
val currentlyFocused = focusedMedia?.uri != null && focusedMedia.uri == uri
|
||||
if (MediaConstraints.isVideoTranscodeAvailable() && canEdit && !isFocused && currentlyFocused) {
|
||||
bindVideoTimeline(state)
|
||||
}
|
||||
|
||||
if (!currentlyFocused) {
|
||||
stopPositionUpdates()
|
||||
sharedViewModel.state.observe(viewLifecycleOwner) { incomingState ->
|
||||
val focusedUri = incomingState.focusedMedia?.uri
|
||||
val currentlyFocused = focusedUri != null && focusedUri == uri
|
||||
if (MediaConstraints.isVideoTranscodeAvailable() && canEdit) {
|
||||
if (currentlyFocused) {
|
||||
if (!isFocused) {
|
||||
bindVideoTimeline(incomingState)
|
||||
} else {
|
||||
val videoTrimData = if (focusedUri != null) {
|
||||
incomingState.getOrCreateVideoTrimData(focusedUri)
|
||||
} else {
|
||||
VideoTrimData()
|
||||
}
|
||||
onEditVideoDuration(videoTrimData, incomingState.isTouchEnabled)
|
||||
}
|
||||
} else {
|
||||
stopPositionUpdates()
|
||||
}
|
||||
}
|
||||
isFocused = currentlyFocused
|
||||
}
|
||||
@@ -160,13 +163,15 @@ class VideoEditorFragment : Fragment(), OnRangeChangeListener, MediaSendPageFrag
|
||||
val autoplay = isVideoGif
|
||||
val slide = VideoSlide(requireContext(), uri, 0, autoplay)
|
||||
|
||||
val data = state.getOrCreateVideoTrimData(uri)
|
||||
if (data.isDurationEdited) {
|
||||
player.clip(data.startTimeUs, data.endTimeUs, autoplay)
|
||||
}
|
||||
|
||||
if (slide.hasVideo()) {
|
||||
canEdit = true
|
||||
try {
|
||||
videoTimeLine.setOnRangeChangeListener(this)
|
||||
videoTimeLine.registerPlayerOnRangeChangeListener(this)
|
||||
|
||||
hud.visibility = View.VISIBLE
|
||||
startPositionUpdates()
|
||||
@@ -184,16 +189,6 @@ class VideoEditorFragment : Fragment(), OnRangeChangeListener, MediaSendPageFrag
|
||||
onSeek(position, true)
|
||||
}
|
||||
|
||||
@RequiresApi(23)
|
||||
override fun onRangeDrag(minValueUs: Long, maxValueUs: Long, durationUs: Long, thumb: Thumb) {
|
||||
onEditVideoDuration(durationUs, minValueUs, maxValueUs, thumb == Thumb.MIN, false)
|
||||
}
|
||||
|
||||
@RequiresApi(23)
|
||||
override fun onRangeDragEnd(minValueUs: Long, maxValueUs: Long, durationUs: Long, thumb: Thumb) {
|
||||
onEditVideoDuration(durationUs, minValueUs, maxValueUs, thumb == Thumb.MIN, true)
|
||||
}
|
||||
|
||||
override fun onDestroyView() {
|
||||
super.onDestroyView()
|
||||
|
||||
@@ -241,17 +236,9 @@ class VideoEditorFragment : Fragment(), OnRangeChangeListener, MediaSendPageFrag
|
||||
return uri
|
||||
}
|
||||
|
||||
override fun saveState(): Any {
|
||||
return data
|
||||
}
|
||||
override fun saveState(): Any = Unit
|
||||
|
||||
override fun restoreState(state: Any) {
|
||||
if (state is VideoTrimData) {
|
||||
data = state
|
||||
} else {
|
||||
Log.w(TAG, "Received a bad saved state. Received class: " + state.javaClass.name)
|
||||
}
|
||||
}
|
||||
override fun restoreState(state: Any) = Unit
|
||||
|
||||
override fun notifyHidden() {
|
||||
pausePlayback()
|
||||
@@ -263,19 +250,9 @@ class VideoEditorFragment : Fragment(), OnRangeChangeListener, MediaSendPageFrag
|
||||
}
|
||||
|
||||
@RequiresApi(23)
|
||||
private fun onEditVideoDuration(totalDurationUs: Long, startTimeUs: Long, endTimeUs: Long, fromEdited: Boolean, editingComplete: Boolean) {
|
||||
controller.onTouchEventsNeeded(!editingComplete)
|
||||
|
||||
private fun onEditVideoDuration(data: VideoTrimData, editingComplete: Boolean) {
|
||||
hud.hidePlayButton()
|
||||
|
||||
val clampedStartTime = max(startTimeUs.toDouble(), 0.0).toLong()
|
||||
|
||||
val wasEdited = data.isDurationEdited
|
||||
val durationEdited = clampedStartTime > 0 || endTimeUs < totalDurationUs
|
||||
val endMoved = data.endTimeUs != endTimeUs
|
||||
|
||||
val updatedData = MediaSelectionViewModel.clampToMaxClipDuration(VideoTrimData(durationEdited, totalDurationUs, clampedStartTime, endTimeUs), maxVideoDurationUs, !endMoved)
|
||||
|
||||
if (editingComplete) {
|
||||
isInEdit = false
|
||||
videoScanThrottle.clear()
|
||||
@@ -289,10 +266,10 @@ class VideoEditorFragment : Fragment(), OnRangeChangeListener, MediaSendPageFrag
|
||||
if (!editingComplete) {
|
||||
player.removeClip(false)
|
||||
}
|
||||
player.playbackPosition = if (fromEdited || editingComplete) clampedStartTime / 1000 else endTimeUs / 1000
|
||||
player.playbackPosition = if (editingComplete) data.startTimeUs / 1000 else data.endTimeUs / 1000
|
||||
if (editingComplete) {
|
||||
if (durationEdited) {
|
||||
player.clip(clampedStartTime, endTimeUs, wasPlayingBeforeEdit)
|
||||
if (data.isDurationEdited) {
|
||||
player.clip(data.startTimeUs, data.endTimeUs, wasPlayingBeforeEdit)
|
||||
} else {
|
||||
player.removeClip(wasPlayingBeforeEdit)
|
||||
}
|
||||
@@ -302,18 +279,6 @@ class VideoEditorFragment : Fragment(), OnRangeChangeListener, MediaSendPageFrag
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!wasEdited && durationEdited) {
|
||||
controller.onVideoBeginEdit(uri)
|
||||
}
|
||||
|
||||
if (editingComplete) {
|
||||
controller.onVideoEndEdit(uri)
|
||||
}
|
||||
|
||||
uri.let {
|
||||
sharedViewModel.setEditorState(it, updatedData)
|
||||
}
|
||||
}
|
||||
|
||||
private fun onSeek(position: Long, dragComplete: Boolean) {
|
||||
@@ -333,10 +298,6 @@ class VideoEditorFragment : Fragment(), OnRangeChangeListener, MediaSendPageFrag
|
||||
fun onPlayerError()
|
||||
|
||||
fun onTouchEventsNeeded(needed: Boolean)
|
||||
|
||||
fun onVideoBeginEdit(uri: Uri)
|
||||
|
||||
fun onVideoEndEdit(uri: Uri)
|
||||
}
|
||||
|
||||
companion object {
|
||||
@@ -345,14 +306,12 @@ class VideoEditorFragment : Fragment(), OnRangeChangeListener, MediaSendPageFrag
|
||||
private const val KEY_URI = "uri"
|
||||
private const val KEY_MAX_SEND = "max_send_size"
|
||||
private const val KEY_IS_VIDEO_GIF = "is_video_gif"
|
||||
private const val KEY_MAX_DURATION = "max_duration"
|
||||
|
||||
fun newInstance(uri: Uri, maxAttachmentSize: Long, isVideoGif: Boolean, maxVideoDuration: Long): VideoEditorFragment {
|
||||
fun newInstance(uri: Uri, maxAttachmentSize: Long, isVideoGif: Boolean): VideoEditorFragment {
|
||||
val args = Bundle()
|
||||
args.putParcelable(KEY_URI, uri)
|
||||
args.putLong(KEY_MAX_SEND, maxAttachmentSize)
|
||||
args.putBoolean(KEY_IS_VIDEO_GIF, isVideoGif)
|
||||
args.putLong(KEY_MAX_DURATION, maxVideoDuration)
|
||||
|
||||
val fragment = VideoEditorFragment()
|
||||
fragment.arguments = args
|
||||
|
||||
@@ -45,8 +45,8 @@ data class MediaSelectionState(
|
||||
|
||||
val canSend = !isSent && selectedMedia.isNotEmpty()
|
||||
|
||||
fun getVideoTrimData(uri: Uri): VideoTrimData? {
|
||||
return editorStateMap[uri] as? VideoTrimData
|
||||
fun getOrCreateVideoTrimData(uri: Uri): VideoTrimData {
|
||||
return editorStateMap[uri] as? VideoTrimData ?: VideoTrimData()
|
||||
}
|
||||
|
||||
enum class ViewOnceToggleState(val code: Int) {
|
||||
|
||||
@@ -44,7 +44,9 @@ import org.thoughtcrime.securesms.util.MediaUtil
|
||||
import org.thoughtcrime.securesms.util.Util
|
||||
import org.thoughtcrime.securesms.util.livedata.Store
|
||||
import java.util.Collections
|
||||
import kotlin.math.max
|
||||
import kotlin.time.Duration.Companion.milliseconds
|
||||
import kotlin.time.Duration.Companion.seconds
|
||||
|
||||
/**
|
||||
* ViewModel which maintains the list of selected media and other shared values.
|
||||
@@ -342,6 +344,31 @@ class MediaSelectionViewModel(
|
||||
store.update { it.copy(viewOnceToggleState = it.viewOnceToggleState.next()) }
|
||||
}
|
||||
|
||||
fun onEditVideoDuration(context: Context, totalDurationUs: Long, startTimeUs: Long, endTimeUs: Long, touchEnabled: Boolean) {
|
||||
store.update {
|
||||
val uri = it.focusedMedia?.uri ?: return@update it
|
||||
val data = it.getOrCreateVideoTrimData(uri)
|
||||
val clampedStartTime = max(startTimeUs.toDouble(), 0.0).toLong()
|
||||
|
||||
val alreadyEdited = data.isDurationEdited
|
||||
val durationEdited = clampedStartTime > 0 || endTimeUs < totalDurationUs
|
||||
val endMoved = data.endTimeUs != endTimeUs
|
||||
val maxVideoDurationUs: Long = if (it.isStory && !MediaConstraints.isVideoTranscodeAvailable()) {
|
||||
Stories.MAX_VIDEO_DURATION_MILLIS
|
||||
} else {
|
||||
it.transcodingPreset.calculateMaxVideoUploadDurationInSeconds(getMediaConstraints().getVideoMaxSize(context)).seconds.inWholeMicroseconds
|
||||
}
|
||||
val updatedData = clampToMaxClipDuration(VideoTrimData(durationEdited, totalDurationUs, clampedStartTime, endTimeUs), maxVideoDurationUs, !alreadyEdited || !endMoved)
|
||||
if (!alreadyEdited && durationEdited) {
|
||||
cancelUpload(MediaBuilder.buildMedia(uri))
|
||||
}
|
||||
it.copy(
|
||||
isTouchEnabled = touchEnabled,
|
||||
editorStateMap = it.editorStateMap + (uri to updatedData)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun getEditorState(uri: Uri): Any? {
|
||||
return store.state.editorStateMap[uri]
|
||||
}
|
||||
@@ -352,10 +379,6 @@ class MediaSelectionViewModel(
|
||||
}
|
||||
}
|
||||
|
||||
fun onVideoBeginEdit(uri: Uri) {
|
||||
cancelUpload(MediaBuilder.buildMedia(uri))
|
||||
}
|
||||
|
||||
fun send(
|
||||
selectedContacts: List<ContactSearchKey.RecipientSearchKey> = emptyList(),
|
||||
scheduledDate: Long? = null
|
||||
|
||||
@@ -43,7 +43,6 @@ import org.thoughtcrime.securesms.conversation.ScheduleMessageTimePickerBottomSh
|
||||
import org.thoughtcrime.securesms.conversation.mutiselect.forward.MultiselectForwardActivity
|
||||
import org.thoughtcrime.securesms.conversation.mutiselect.forward.MultiselectForwardFragmentArgs
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||
import org.thoughtcrime.securesms.media.DecryptableUriMediaInput
|
||||
import org.thoughtcrime.securesms.mediasend.Media
|
||||
import org.thoughtcrime.securesms.mediasend.MediaSendActivityResult
|
||||
import org.thoughtcrime.securesms.mediasend.v2.HudCommand
|
||||
@@ -77,7 +76,7 @@ import kotlin.math.roundToInt
|
||||
/**
|
||||
* Allows the user to view and edit selected media.
|
||||
*/
|
||||
class MediaReviewFragment : Fragment(R.layout.v2_media_review_fragment), ScheduleMessageTimePickerBottomSheet.ScheduleCallback {
|
||||
class MediaReviewFragment : Fragment(R.layout.v2_media_review_fragment), ScheduleMessageTimePickerBottomSheet.ScheduleCallback, VideoThumbnailsRangeSelectorView.RangeDragListener {
|
||||
|
||||
private val sharedViewModel: MediaSelectionViewModel by viewModels(
|
||||
ownerProducer = { requireActivity() }
|
||||
@@ -297,6 +296,10 @@ class MediaReviewFragment : Fragment(R.layout.v2_media_review_fragment), Schedul
|
||||
}
|
||||
})
|
||||
|
||||
if (MediaConstraints.isVideoTranscodeAvailable()) {
|
||||
videoTimeLine.registerEditorOnRangeChangeListener(this)
|
||||
}
|
||||
|
||||
val selectionAdapter = MappingAdapter(false)
|
||||
MediaReviewAddItem.register(selectionAdapter) {
|
||||
launchGallery()
|
||||
@@ -527,7 +530,8 @@ class MediaReviewFragment : Fragment(R.layout.v2_media_review_fragment), Schedul
|
||||
return
|
||||
}
|
||||
val uri = mediaItem.uri
|
||||
videoTimeLine.setInput(DecryptableUriMediaInput.createForUri(requireContext(), uri))
|
||||
videoTimeLine.unregisterPlayerOnRangeChangeListener()
|
||||
videoTimeLine.setInput(uri)
|
||||
val size: Long = tryGetUriSize(requireContext(), uri, Long.MAX_VALUE)
|
||||
val maxSend = sharedViewModel.getMediaConstraints().getVideoMaxSize(requireContext())
|
||||
if (size > maxSend) {
|
||||
@@ -535,7 +539,7 @@ class MediaReviewFragment : Fragment(R.layout.v2_media_review_fragment), Schedul
|
||||
}
|
||||
|
||||
if (state.isTouchEnabled) {
|
||||
val data = state.getVideoTrimData(uri) ?: return
|
||||
val data = state.getOrCreateVideoTrimData(uri)
|
||||
|
||||
if (data.totalInputDurationUs > 0) {
|
||||
videoTimeLine.setRange(data.startTimeUs, data.endTimeUs)
|
||||
@@ -545,9 +549,9 @@ class MediaReviewFragment : Fragment(R.layout.v2_media_review_fragment), Schedul
|
||||
|
||||
private fun presentVideoSizeHint(state: MediaSelectionState) {
|
||||
val focusedMedia = state.focusedMedia ?: return
|
||||
val trimData = state.getVideoTrimData(focusedMedia.uri)
|
||||
val trimData = state.getOrCreateVideoTrimData(focusedMedia.uri)
|
||||
|
||||
videoSizeHint.text = if (state.isVideoTrimmingVisible && trimData != null) {
|
||||
videoSizeHint.text = if (state.isVideoTrimmingVisible) {
|
||||
val seconds = trimData.getDuration().inWholeSeconds
|
||||
val bytes = TranscodingQuality.createFromPreset(state.transcodingPreset, trimData.getDuration().inWholeMilliseconds).byteCountEstimate
|
||||
String.format(Locale.getDefault(), "%d:%02d • %s", seconds / 60, seconds % 60, MemoryUnitFormat.formatBytes(bytes, MemoryUnitFormat.MEGA_BYTES, true))
|
||||
@@ -769,4 +773,8 @@ class MediaReviewFragment : Fragment(R.layout.v2_media_review_fragment), Schedul
|
||||
scheduledSendTime = scheduledTime
|
||||
sendButton.performClick()
|
||||
}
|
||||
|
||||
override fun onRangeDrag(minValue: Long, maxValue: Long, duration: Long, end: Boolean) {
|
||||
sharedViewModel.onEditVideoDuration(context = requireContext(), totalDurationUs = duration, startTimeUs = minValue, endTimeUs = maxValue, touchEnabled = end)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,8 +10,6 @@ import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.mediasend.VideoEditorFragment
|
||||
import org.thoughtcrime.securesms.mediasend.v2.HudCommand
|
||||
import org.thoughtcrime.securesms.mediasend.v2.MediaSelectionViewModel
|
||||
import org.thoughtcrime.securesms.mms.MediaConstraints
|
||||
import org.thoughtcrime.securesms.stories.Stories
|
||||
|
||||
private const val VIDEO_EDITOR_TAG = "video.editor.fragment"
|
||||
|
||||
@@ -34,16 +32,6 @@ class MediaReviewVideoPageFragment : Fragment(R.layout.fragment_container), Vide
|
||||
restoreVideoEditorState()
|
||||
}
|
||||
|
||||
override fun onSaveInstanceState(outState: Bundle) {
|
||||
super.onSaveInstanceState(outState)
|
||||
saveEditorState()
|
||||
}
|
||||
|
||||
private fun saveEditorState() {
|
||||
val saveState = videoEditorFragment.saveState()
|
||||
sharedViewModel.setEditorState(requireUri(), saveState)
|
||||
}
|
||||
|
||||
override fun onPlayerReady() {
|
||||
sharedViewModel.sendCommand(HudCommand.ResumeEntryTransition)
|
||||
}
|
||||
@@ -55,15 +43,6 @@ class MediaReviewVideoPageFragment : Fragment(R.layout.fragment_container), Vide
|
||||
override fun onTouchEventsNeeded(needed: Boolean) {
|
||||
sharedViewModel.setTouchEnabled(!needed)
|
||||
}
|
||||
|
||||
override fun onVideoBeginEdit(uri: Uri) {
|
||||
sharedViewModel.onVideoBeginEdit(uri)
|
||||
}
|
||||
|
||||
override fun onVideoEndEdit(uri: Uri) {
|
||||
saveEditorState()
|
||||
}
|
||||
|
||||
private fun restoreVideoEditorState() {
|
||||
val data = sharedViewModel.getEditorState(requireUri()) as? VideoTrimData
|
||||
|
||||
@@ -81,8 +60,7 @@ class MediaReviewVideoPageFragment : Fragment(R.layout.fragment_container), Vide
|
||||
val videoEditorFragment = VideoEditorFragment.newInstance(
|
||||
requireUri(),
|
||||
requireMaxAttachmentSize(),
|
||||
requireIsVideoGif(),
|
||||
requireMaxVideoDuration()
|
||||
requireIsVideoGif()
|
||||
)
|
||||
|
||||
childFragmentManager.beginTransaction()
|
||||
@@ -100,7 +78,6 @@ class MediaReviewVideoPageFragment : Fragment(R.layout.fragment_container), Vide
|
||||
private fun requireUri(): Uri = requireNotNull(requireArguments().getParcelableCompat(ARG_URI, Uri::class.java))
|
||||
private fun requireMaxAttachmentSize(): Long = sharedViewModel.getMediaConstraints().getVideoMaxSize(requireContext())
|
||||
private fun requireIsVideoGif(): Boolean = requireNotNull(requireArguments().getBoolean(ARG_IS_VIDEO_GIF))
|
||||
private fun requireMaxVideoDuration(): Long = if (sharedViewModel.isStory() && !MediaConstraints.isVideoTranscodeAvailable()) Stories.MAX_VIDEO_DURATION_MILLIS else Long.MAX_VALUE
|
||||
|
||||
companion object {
|
||||
private const val ARG_URI = "arg.uri"
|
||||
|
||||
@@ -51,7 +51,8 @@ public final class VideoThumbnailsRangeSelectorView extends VideoThumbnailsView
|
||||
private long downMax;
|
||||
private Thumb dragThumb;
|
||||
private Thumb lastDragThumb;
|
||||
private OnRangeChangeListener onRangeChangeListener;
|
||||
private PositionDragListener playerOnRangeChangeListener;
|
||||
private RangeDragListener editorOnRangeChangeListener;
|
||||
@Px private int thumbSizePixels;
|
||||
@Px private int thumbTouchRadius;
|
||||
@ColorInt private int thumbColor;
|
||||
@@ -109,15 +110,7 @@ public final class VideoThumbnailsRangeSelectorView extends VideoThumbnailsView
|
||||
|
||||
@Override
|
||||
protected void afterDurationChange(long duration) {
|
||||
super.afterDurationChange(duration);
|
||||
|
||||
if (maxValue != null && duration < maxValue) {
|
||||
maxValue = duration;
|
||||
}
|
||||
|
||||
if (minValue != null && duration < minValue) {
|
||||
minValue = duration;
|
||||
}
|
||||
maxValue = duration;
|
||||
|
||||
if (duration > 0) {
|
||||
if (externalMaxValue != null) {
|
||||
@@ -131,15 +124,21 @@ public final class VideoThumbnailsRangeSelectorView extends VideoThumbnailsView
|
||||
}
|
||||
}
|
||||
|
||||
if (onRangeChangeListener != null) {
|
||||
onRangeChangeListener.onRangeDragEnd(getMinValue(), getMaxValue(), getDuration(), Thumb.MIN);
|
||||
}
|
||||
onRangeDrag(getMinValue(), getMaxValue(), duration, true);
|
||||
|
||||
invalidate();
|
||||
}
|
||||
|
||||
public void setOnRangeChangeListener(OnRangeChangeListener onRangeChangeListener) {
|
||||
this.onRangeChangeListener = onRangeChangeListener;
|
||||
public void registerPlayerOnRangeChangeListener(PositionDragListener playerOnRangeChangeListener) {
|
||||
this.playerOnRangeChangeListener = playerOnRangeChangeListener;
|
||||
}
|
||||
|
||||
public void registerEditorOnRangeChangeListener(RangeDragListener editorOnRangeChangeListener) {
|
||||
this.editorOnRangeChangeListener = editorOnRangeChangeListener;
|
||||
}
|
||||
|
||||
public void unregisterPlayerOnRangeChangeListener() {
|
||||
this.playerOnRangeChangeListener = null;
|
||||
}
|
||||
|
||||
public void setActualPosition(long position) {
|
||||
@@ -349,22 +348,22 @@ public final class VideoThumbnailsRangeSelectorView extends VideoThumbnailsView
|
||||
changed = setMaxValue(downMax + delta);
|
||||
break;
|
||||
}
|
||||
if (changed && onRangeChangeListener != null) {
|
||||
if (changed) {
|
||||
if (dragThumb == Thumb.POSITION) {
|
||||
onRangeChangeListener.onPositionDrag(dragPosition);
|
||||
onPositionDrag(dragPosition);
|
||||
} else {
|
||||
onRangeChangeListener.onRangeDrag(getMinValue(), getMaxValue(), getDuration(), dragThumb);
|
||||
onRangeDrag(getMinValue(), getMaxValue(), getDuration(), false);
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
if (actionMasked == MotionEvent.ACTION_UP) {
|
||||
if (onRangeChangeListener != null) {
|
||||
if (editorOnRangeChangeListener != null) {
|
||||
if (dragThumb == Thumb.POSITION) {
|
||||
onRangeChangeListener.onEndPositionDrag(dragPosition);
|
||||
onEndPositionDrag(dragPosition);
|
||||
} else {
|
||||
onRangeChangeListener.onRangeDragEnd(getMinValue(), getMaxValue(), getDuration(), dragThumb);
|
||||
onRangeDrag(getMinValue(), getMaxValue(), getDuration(), true);
|
||||
}
|
||||
lastDragThumb = dragThumb;
|
||||
dragEndTimeMs = System.currentTimeMillis();
|
||||
@@ -418,20 +417,36 @@ public final class VideoThumbnailsRangeSelectorView extends VideoThumbnailsView
|
||||
maximumSelectableRangeMicros = timeUnit.toMicros(t);
|
||||
}
|
||||
|
||||
private void onPositionDrag(long position) {
|
||||
if (playerOnRangeChangeListener != null) {
|
||||
playerOnRangeChangeListener.onPositionDrag(position);
|
||||
}
|
||||
}
|
||||
|
||||
private void onEndPositionDrag(long position) {
|
||||
if (playerOnRangeChangeListener != null) {
|
||||
playerOnRangeChangeListener.onEndPositionDrag(position);
|
||||
}
|
||||
}
|
||||
|
||||
private void onRangeDrag(long minValue, long maxValue, long duration, boolean end) {
|
||||
if (editorOnRangeChangeListener != null) {
|
||||
editorOnRangeChangeListener.onRangeDrag(minValue, maxValue, duration, end);
|
||||
}
|
||||
}
|
||||
|
||||
public enum Thumb {
|
||||
MIN,
|
||||
MAX,
|
||||
POSITION
|
||||
}
|
||||
|
||||
public interface OnRangeChangeListener {
|
||||
|
||||
public interface PositionDragListener {
|
||||
void onPositionDrag(long position);
|
||||
|
||||
void onEndPositionDrag(long position);
|
||||
}
|
||||
|
||||
void onRangeDrag(long minValue, long maxValue, long duration, Thumb thumb);
|
||||
|
||||
void onRangeDragEnd(long minValue, long maxValue, long duration, Thumb thumb);
|
||||
public interface RangeDragListener {
|
||||
void onRangeDrag(long minValue, long maxValue, long duration, boolean start);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ import android.graphics.Paint;
|
||||
import android.graphics.Path;
|
||||
import android.graphics.Rect;
|
||||
import android.graphics.RectF;
|
||||
import android.net.Uri;
|
||||
import android.os.AsyncTask;
|
||||
import android.util.AttributeSet;
|
||||
import android.view.View;
|
||||
@@ -16,6 +17,7 @@ import androidx.annotation.Nullable;
|
||||
import androidx.annotation.RequiresApi;
|
||||
|
||||
import org.signal.core.util.logging.Log;
|
||||
import org.thoughtcrime.securesms.media.DecryptableUriMediaInput;
|
||||
import org.thoughtcrime.securesms.util.ViewUtil;
|
||||
import org.thoughtcrime.securesms.video.interfaces.MediaInput;
|
||||
|
||||
@@ -26,11 +28,13 @@ import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
@RequiresApi(api = 23)
|
||||
public class VideoThumbnailsView extends View {
|
||||
abstract public class VideoThumbnailsView extends View {
|
||||
|
||||
private static final String TAG = Log.tag(VideoThumbnailsView.class);
|
||||
private static final int CORNER_RADIUS = ViewUtil.dpToPx(8);
|
||||
|
||||
protected Uri currentUri;
|
||||
|
||||
private MediaInput input;
|
||||
private volatile ArrayList<Bitmap> thumbnails;
|
||||
private AsyncTask<Void, Bitmap, Void> thumbnailsTask;
|
||||
@@ -55,12 +59,13 @@ public class VideoThumbnailsView extends View {
|
||||
super(context, attrs, defStyleAttr);
|
||||
}
|
||||
|
||||
public void setInput(@NonNull MediaInput input) {
|
||||
if (this.input != null && input.hasSameInput(this.input)) {
|
||||
public void setInput(@NonNull Uri uri) throws IOException {
|
||||
if (uri.equals(this.currentUri)) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.input = input;
|
||||
this.currentUri = uri;
|
||||
this.input = DecryptableUriMediaInput.createForUri(getContext(), uri);
|
||||
this.thumbnails = null;
|
||||
if (thumbnailsTask != null) {
|
||||
thumbnailsTask.cancel(true);
|
||||
@@ -163,15 +168,14 @@ public class VideoThumbnailsView extends View {
|
||||
}
|
||||
}
|
||||
|
||||
public void setDuration(long duration) {
|
||||
private void setDuration(long duration) {
|
||||
if (this.duration != duration) {
|
||||
this.duration = duration;
|
||||
afterDurationChange(duration);
|
||||
}
|
||||
}
|
||||
|
||||
protected void afterDurationChange(long duration) {
|
||||
}
|
||||
abstract void afterDurationChange(long duration);
|
||||
|
||||
public long getDuration() {
|
||||
return duration;
|
||||
@@ -241,14 +245,10 @@ public class VideoThumbnailsView extends View {
|
||||
VideoThumbnailsView view = viewReference.get();
|
||||
List<Bitmap> thumbnails = view != null ? view.thumbnails : null;
|
||||
if (view != null) {
|
||||
view.setDuration(duration);
|
||||
view.setDuration(ThumbnailsTask.this.duration);
|
||||
view.invalidate();
|
||||
Log.i(TAG, "onPostExecute, we have " + (thumbnails != null ? thumbnails.size() : "null") + " thumbs");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public interface OnDurationListener {
|
||||
void onDurationKnown(long duration);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user