allows locking and cancelling to occur after choosing either option

- fixes other quirks caused by porting to the inverted display logic
This commit is contained in:
Adam Brown 2021-11-17 15:31:41 +00:00
parent e895dbd923
commit 9ae03b76cd
6 changed files with 69 additions and 67 deletions

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -95,6 +95,7 @@
<!-- Slide to cancel text should go under this view -->
<View
android:id="@+id/voiceMessageSlideToCancelDivider"
android:layout_width="48dp"
android:layout_height="0dp"
android:background="?android:colorBackground"