From 9ae03b76cdba2c90bcd2131caf67c73d8e28acab Mon Sep 17 00:00:00 2001 From: Adam Brown Date: Wed, 17 Nov 2021 15:31:41 +0000 Subject: [PATCH] allows locking and cancelling to occur after choosing either option - fixes other quirks caused by porting to the inverted display logic --- .../home/room/detail/RoomDetailFragment.kt | 15 +++--- .../detail/composer/TextComposerViewState.kt | 6 +++ .../composer/voice/DraggableStateProcessor.kt | 47 ++++++++----------- .../voice/VoiceMessageRecorderView.kt | 44 ++++++++++------- .../composer/voice/VoiceMessageViews.kt | 23 ++++----- .../layout/view_voice_message_recorder.xml | 1 + 6 files changed, 69 insertions(+), 67 deletions(-) diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailFragment.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailFragment.kt index 844de4c980..271bb9b775 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailFragment.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailFragment.kt @@ -712,9 +712,10 @@ class RoomDetailFragment @Inject constructor( roomDetailViewModel.handle(RoomDetailAction.PlayOrPauseRecordingPlayback) } - override fun onRecordingStopped() { - roomDetailViewModel.handle(RoomDetailAction.EndRecordingVoiceMessage(isCancelled = true)) - if (currentState() != RecordingUiState.Locked) { + override fun onVoiceRecordingEnded(lastKnownState: RecordingUiState?) { + if (lastKnownState != RecordingUiState.Locked) { + val isCancelled = lastKnownState == RecordingUiState.Cancelled + roomDetailViewModel.handle(RoomDetailAction.EndRecordingVoiceMessage(isCancelled = isCancelled)) display(RecordingUiState.None) } } @@ -729,7 +730,7 @@ class RoomDetailFragment @Inject constructor( override fun deleteVoiceMessage() { roomDetailViewModel.handle(RoomDetailAction.EndRecordingVoiceMessage(isCancelled = true)) - display(RecordingUiState.Cancelled) + display(RecordingUiState.None) } override fun onRecordingLimitReached() { @@ -743,8 +744,6 @@ class RoomDetailFragment @Inject constructor( private fun display(state: RecordingUiState) { textComposerViewModel.handle(TextComposerAction.OnVoiceRecordingUiStateChanged(state)) } - - override fun currentState() = withState(textComposerViewModel) { it.voiceRecordingUiState } } } @@ -1986,7 +1985,7 @@ class RoomDetailFragment @Inject constructor( roomDetailViewModel.handle(RoomDetailAction.UpdateQuickReactAction(action.eventId, action.clickedOn, action.add)) } is EventSharedAction.Edit -> { - if (!views.voiceMessageRecorderView.isActive()) { + if (withState(textComposerViewModel) { it.isVoiceMessageIdle }) { textComposerViewModel.handle(TextComposerAction.EnterEditMode(action.eventId, views.composerLayout.text.toString())) } else { requireActivity().toast(R.string.error_voice_message_cannot_reply_or_edit) @@ -1996,7 +1995,7 @@ class RoomDetailFragment @Inject constructor( textComposerViewModel.handle(TextComposerAction.EnterQuoteMode(action.eventId, views.composerLayout.text.toString())) } is EventSharedAction.Reply -> { - if (!views.voiceMessageRecorderView.isActive()) { + if (withState(textComposerViewModel) { it.isVoiceMessageIdle }) { textComposerViewModel.handle(TextComposerAction.EnterReplyMode(action.eventId, views.composerLayout.text.toString())) } else { requireActivity().toast(R.string.error_voice_message_cannot_reply_or_edit) diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/TextComposerViewState.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/TextComposerViewState.kt index 99cd4b0e30..4eb70138bb 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/TextComposerViewState.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/TextComposerViewState.kt @@ -60,8 +60,14 @@ data class TextComposerViewState( VoiceMessageRecorderView.RecordingUiState.Started -> true } + val isVoiceMessageIdle = when (voiceRecordingUiState) { + VoiceMessageRecorderView.RecordingUiState.None, VoiceMessageRecorderView.RecordingUiState.Cancelled -> false + else -> true + } + val isComposerVisible = canSendMessage && !isVoiceRecording val isVoiceMessageRecorderVisible = canSendMessage && !isSendButtonVisible + @Suppress("UNUSED") // needed by mavericks constructor(args: RoomDetailArgs) : this(roomId = args.roomId) } diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/voice/DraggableStateProcessor.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/voice/DraggableStateProcessor.kt index 23e973afda..a8b19d6f6a 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/voice/DraggableStateProcessor.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/voice/DraggableStateProcessor.kt @@ -28,23 +28,18 @@ class DraggableStateProcessor( dimensionConverter: DimensionConverter, ) { - private val minimumMove = dimensionConverter.dpToPx(16) private val distanceToLock = dimensionConverter.dpToPx(48).toFloat() private val distanceToCancel = dimensionConverter.dpToPx(120).toFloat() private val rtlXMultiplier = resources.getInteger(R.integer.rtl_x_multiplier) private var firstX: Float = 0f private var firstY: Float = 0f - private var lastX: Float = 0f - private var lastY: Float = 0f private var lastDistanceX: Float = 0f private var lastDistanceY: Float = 0f - fun reset(event: MotionEvent) { + fun initialize(event: MotionEvent) { firstX = event.rawX firstY = event.rawY - lastX = firstX - lastY = firstY lastDistanceX = 0F lastDistanceY = 0F } @@ -54,49 +49,48 @@ class DraggableStateProcessor( val currentY = event.rawY val distanceX = firstX - currentX val distanceY = firstY - currentY - return nextRecordingState(recordingState, currentX, currentY, distanceX, distanceY).also { - lastX = currentX - lastY = currentY + return recordingState.nextRecordingState(currentX, currentY, distanceX, distanceY).also { lastDistanceX = distanceX lastDistanceY = distanceY } } - private fun nextRecordingState(recordingState: RecordingUiState, currentX: Float, currentY: Float, distanceX: Float, distanceY: Float): RecordingUiState { - return when (recordingState) { + private fun RecordingUiState.nextRecordingState(currentX: Float, currentY: Float, distanceX: Float, distanceY: Float): RecordingUiState { + return when (this) { RecordingUiState.Started -> { - // Determine if cancelling or locking for the first move action. when { - (isSlidingToCancel(currentX)) && distanceX > distanceY && distanceX > lastDistanceX -> DraggingState.Cancelling(distanceX) - isSlidingToLock(currentY) && distanceY > distanceX && distanceY > lastDistanceY -> DraggingState.Locking(distanceY) - else -> recordingState + isDraggingToCancel(currentX, distanceX, distanceY) -> DraggingState.Cancelling(distanceX) + isDraggingToLock(currentY, distanceX, distanceY) -> DraggingState.Locking(distanceY) + else -> this } } is DraggingState.Cancelling -> { - // Check if cancelling conditions met, also check if it should be initial state when { - distanceX < minimumMove && distanceX < lastDistanceX -> RecordingUiState.Started - shouldCancelRecording(distanceX) -> RecordingUiState.Cancelled - else -> DraggingState.Cancelling(distanceX) + isDraggingToLock(currentY, distanceX, distanceY) -> DraggingState.Locking(distanceY) + shouldCancelRecording(distanceX) -> RecordingUiState.Cancelled + else -> DraggingState.Cancelling(distanceX) } } is DraggingState.Locking -> { - // Check if locking conditions met, also check if it should be initial state when { - distanceY < minimumMove && distanceY < lastDistanceY -> RecordingUiState.Started - shouldLockRecording(distanceY) -> RecordingUiState.Locked - else -> DraggingState.Locking(distanceY) + isDraggingToCancel(currentX, distanceX, distanceY) -> DraggingState.Cancelling(distanceX) + shouldLockRecording(distanceY) -> RecordingUiState.Locked + else -> DraggingState.Locking(distanceY) } } else -> { - recordingState + this } } } - private fun isSlidingToLock(currentY: Float) = currentY < firstY + private fun isDraggingToLock(currentY: Float, distanceX: Float, distanceY: Float) = (currentY < firstY) && + distanceY > distanceX && distanceY > lastDistanceY - private fun isSlidingToCancel(currentX: Float) = (currentX < firstX && rtlXMultiplier == 1) || (currentX > firstX && rtlXMultiplier == -1) + private fun isDraggingToCancel(currentX: Float, distanceX: Float, distanceY: Float) = isDraggingHorizontal(currentX) && + distanceX > distanceY && distanceX > lastDistanceX + + private fun isDraggingHorizontal(currentX: Float) = (currentX < firstX && rtlXMultiplier == 1) || (currentX > firstX && rtlXMultiplier == -1) private fun shouldCancelRecording(distanceX: Float): Boolean { return distanceX >= distanceToCancel @@ -106,4 +100,3 @@ class DraggableStateProcessor( return distanceY >= distanceToLock } } - diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/voice/VoiceMessageRecorderView.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/voice/VoiceMessageRecorderView.kt index 8c3eadca1c..cd784dd9b6 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/voice/VoiceMessageRecorderView.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/voice/VoiceMessageRecorderView.kt @@ -43,13 +43,12 @@ class VoiceMessageRecorderView @JvmOverloads constructor( fun onVoiceRecordingStarted() fun onVoiceRecordingPlaybackModeOn() fun onVoicePlaybackButtonClicked() - fun onRecordingStopped() fun onUiStateChanged(state: RecordingUiState) fun sendVoiceMessage() fun deleteVoiceMessage() fun onRecordingLimitReached() fun recordingWaveformClicked() - fun currentState(): RecordingUiState + fun onVoiceRecordingEnded(lastKnownState: RecordingUiState?) } // We need to define views as lateinit var to be able to check if initialized for the bug fix on api 21 and 22. @@ -85,17 +84,23 @@ class VoiceMessageRecorderView @JvmOverloads constructor( callback.onVoiceRecordingStarted() } - override fun onRecordingStopped() { - callback.onRecordingStopped() + override fun onMicButtonReleased() { + callback.onVoiceRecordingEnded(lastKnownState) } - override fun isActive() = callback.currentState() != RecordingUiState.Cancelled - override fun updateState(updater: (RecordingUiState) -> RecordingUiState) { - updater(lastKnownState ?: RecordingUiState.None).also { newState -> - when (newState) { - is DraggingState -> display(newState) - else -> callback.onUiStateChanged(newState) + when (val currentState = lastKnownState) { + null, RecordingUiState.None -> { + // ignore drag events when the view is idle + } + else -> { + updater(currentState).also { newState -> + when (newState) { + // display drag events directly without leaving the view for faster UI feedback + is DraggingState -> display(newState) + else -> callback.onUiStateChanged(newState) + } + } } } } @@ -120,6 +125,7 @@ class VoiceMessageRecorderView @JvmOverloads constructor( fun display(recordingState: RecordingUiState) { if (lastKnownState == recordingState) return + val previousState = lastKnownState lastKnownState = recordingState when (recordingState) { RecordingUiState.None -> { @@ -151,7 +157,12 @@ class VoiceMessageRecorderView @JvmOverloads constructor( } is DraggingState -> when (recordingState) { is DraggingState.Cancelling -> voiceMessageViews.renderCancelling(recordingState.distanceX) - is DraggingState.Locking -> voiceMessageViews.renderLocking(recordingState.distanceY) + is DraggingState.Locking -> { + if (previousState is DraggingState.Cancelling) { + voiceMessageViews.showRecordingViews() + } + voiceMessageViews.renderLocking(recordingState.distanceY) + } }.exhaustive } } @@ -170,7 +181,8 @@ class VoiceMessageRecorderView @JvmOverloads constructor( } private fun onRecordingTick(milliseconds: Long) { - voiceMessageViews.renderRecordingTimer(callback.currentState(), milliseconds / 1_000) + val currentState = lastKnownState ?: return + voiceMessageViews.renderRecordingTimer(currentState, milliseconds / 1_000) val timeDiffToRecordingLimit = BuildConfig.VOICE_MESSAGE_DURATION_LIMIT_MS - milliseconds if (timeDiffToRecordingLimit <= 0) { post { @@ -178,7 +190,8 @@ class VoiceMessageRecorderView @JvmOverloads constructor( } } else if (timeDiffToRecordingLimit in 10_000..10_999) { post { - voiceMessageViews.renderToast(context.getString(R.string.voice_message_n_seconds_warning_toast, floor(timeDiffToRecordingLimit / 1000f).toInt())) + val secondsRemaining = floor(timeDiffToRecordingLimit / 1000f).toInt() + voiceMessageViews.renderToast(context.getString(R.string.voice_message_n_seconds_warning_toast, secondsRemaining)) vibrate(context) } } @@ -189,11 +202,6 @@ class VoiceMessageRecorderView @JvmOverloads constructor( recordingTicker = null } - /** - * Returns true if the voice message is recording or is in playback mode - */ - fun isActive() = callback.currentState() !in listOf(RecordingUiState.None, RecordingUiState.Cancelled) - override fun onUpdate(state: VoiceMessagePlaybackTracker.Listener.State) { when (state) { is VoiceMessagePlaybackTracker.Listener.State.Recording -> { diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/voice/VoiceMessageViews.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/voice/VoiceMessageViews.kt index 938ae74983..0b2696931c 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/voice/VoiceMessageViews.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/voice/VoiceMessageViews.kt @@ -19,7 +19,6 @@ package im.vector.app.features.home.room.detail.composer.voice import android.annotation.SuppressLint import android.content.res.Resources import android.text.format.DateUtils -import android.util.Log import android.view.MotionEvent import android.view.View import android.view.ViewGroup @@ -74,22 +73,17 @@ class VoiceMessageViews( views.voiceMessageMicButton.setOnTouchListener { _, event -> when (event.action) { MotionEvent.ACTION_DOWN -> { - Log.e("!!!", "event down: $event") - positions.reset(event) + positions.initialize(event) actions.onRequestRecording() true } MotionEvent.ACTION_UP -> { - actions.onRecordingStopped() + actions.onMicButtonReleased() true } MotionEvent.ACTION_MOVE -> { - if (actions.isActive()) { - actions.updateState { currentState -> positions.process(event, currentState) } - true - } else { - false - } + actions.updateState { currentState -> positions.process(event, currentState) } + true } else -> false } @@ -128,6 +122,7 @@ class VoiceMessageViews( views.voiceMessageLockBackground.isVisible = false views.voiceMessageLockImage.isVisible = false views.voiceMessageLockArrow.isVisible = false + views.voiceMessageSlideToCancelDivider.isVisible = true // Reset Y translations views.voiceMessageMicButton.translationY = 0F views.voiceMessageLockArrow.translationY = 0F @@ -167,11 +162,14 @@ class VoiceMessageViews( } else { animateLockImageWithBackground() } + views.voiceMessageSlideToCancelDivider.isVisible = false views.voiceMessageLockArrow.isVisible = false views.voiceMessageLockArrow.animate().translationY(0f).start() views.voiceMessageSlideToCancel.isVisible = false views.voiceMessageSlideToCancel.animate().translationX(0f).translationY(0f).start() views.voiceMessagePlaybackLayout.isVisible = false + views.voiceMessageTimerIndicator.isVisible = false + views.voiceMessageTimer.isVisible = false if (recordingState != RecordingUiState.Locked) { views.voiceMessageMicButton @@ -182,8 +180,6 @@ class VoiceMessageViews( .translationY(0f) .setDuration(150) .withEndAction { - views.voiceMessageTimerIndicator.isVisible = false - views.voiceMessageTimer.isVisible = false resetMicButtonUi() isCancelled?.let { onVoiceRecordingEnded(it) @@ -349,8 +345,7 @@ class VoiceMessageViews( interface Actions { fun onRequestRecording() - fun onRecordingStopped() - fun isActive(): Boolean + fun onMicButtonReleased() fun updateState(updater: (RecordingUiState) -> RecordingUiState) fun sendMessage() fun delete() diff --git a/vector/src/main/res/layout/view_voice_message_recorder.xml b/vector/src/main/res/layout/view_voice_message_recorder.xml index 051928b73d..81d9c64e33 100644 --- a/vector/src/main/res/layout/view_voice_message_recorder.xml +++ b/vector/src/main/res/layout/view_voice_message_recorder.xml @@ -95,6 +95,7 @@