separating the drag state from the main UI state in order to clarify which actions should be handled in each layer

This commit is contained in:
Adam Brown 2021-11-19 11:31:10 +00:00
parent 7d262ebc32
commit 331bcbfc8a
5 changed files with 59 additions and 54 deletions

View File

@ -709,17 +709,18 @@ class RoomDetailFragment @Inject constructor(
} }
override fun onVoiceRecordingCancelled() { override fun onVoiceRecordingCancelled() {
onDeleteVoiceMessage() roomDetailViewModel.handle(RoomDetailAction.EndRecordingVoiceMessage(isCancelled = true))
updateRecordingUiState(RecordingUiState.Cancelled)
}
override fun onVoiceRecordingLocked() {
updateRecordingUiState(RecordingUiState.Locked)
} }
override fun onVoiceRecordingEnded() { override fun onVoiceRecordingEnded() {
onSendVoiceMessage() onSendVoiceMessage()
} }
override fun onUiStateChanged(state: RecordingUiState) {
updateRecordingUiState(state)
}
override fun onSendVoiceMessage() { override fun onSendVoiceMessage() {
roomDetailViewModel.handle(RoomDetailAction.EndRecordingVoiceMessage(isCancelled = false)) roomDetailViewModel.handle(RoomDetailAction.EndRecordingVoiceMessage(isCancelled = false))
updateRecordingUiState(RecordingUiState.None) updateRecordingUiState(RecordingUiState.None)

View File

@ -54,8 +54,6 @@ data class TextComposerViewState(
VoiceMessageRecorderView.RecordingUiState.None, VoiceMessageRecorderView.RecordingUiState.None,
VoiceMessageRecorderView.RecordingUiState.Cancelled, VoiceMessageRecorderView.RecordingUiState.Cancelled,
VoiceMessageRecorderView.RecordingUiState.Playback -> false VoiceMessageRecorderView.RecordingUiState.Playback -> false
is VoiceMessageRecorderView.DraggingState.Cancelling,
is VoiceMessageRecorderView.DraggingState.Locking,
VoiceMessageRecorderView.RecordingUiState.Locked, VoiceMessageRecorderView.RecordingUiState.Locked,
VoiceMessageRecorderView.RecordingUiState.Started -> true VoiceMessageRecorderView.RecordingUiState.Started -> true
} }

View File

@ -21,7 +21,6 @@ import android.view.MotionEvent
import im.vector.app.R import im.vector.app.R
import im.vector.app.core.utils.DimensionConverter import im.vector.app.core.utils.DimensionConverter
import im.vector.app.features.home.room.detail.composer.voice.VoiceMessageRecorderView.DraggingState import im.vector.app.features.home.room.detail.composer.voice.VoiceMessageRecorderView.DraggingState
import im.vector.app.features.home.room.detail.composer.voice.VoiceMessageRecorderView.RecordingUiState
class DraggableStateProcessor( class DraggableStateProcessor(
resources: Resources, resources: Resources,
@ -44,37 +43,37 @@ class DraggableStateProcessor(
lastDistanceY = 0F lastDistanceY = 0F
} }
fun process(event: MotionEvent, recordingState: RecordingUiState): RecordingUiState { fun process(event: MotionEvent, draggingState: DraggingState): DraggingState {
val currentX = event.rawX val currentX = event.rawX
val currentY = event.rawY val currentY = event.rawY
val distanceX = firstX - currentX val distanceX = firstX - currentX
val distanceY = firstY - currentY val distanceY = firstY - currentY
return recordingState.nextRecordingState(currentX, currentY, distanceX, distanceY).also { return draggingState.nextDragState(currentX, currentY, distanceX, distanceY).also {
lastDistanceX = distanceX lastDistanceX = distanceX
lastDistanceY = distanceY lastDistanceY = distanceY
} }
} }
private fun RecordingUiState.nextRecordingState(currentX: Float, currentY: Float, distanceX: Float, distanceY: Float): RecordingUiState { private fun DraggingState.nextDragState(currentX: Float, currentY: Float, distanceX: Float, distanceY: Float): DraggingState {
return when (this) { return when (this) {
RecordingUiState.Started -> { DraggingState.Ready -> {
when { when {
isDraggingToCancel(currentX, distanceX, distanceY) -> DraggingState.Cancelling(distanceX) isDraggingToCancel(currentX, distanceX, distanceY) -> DraggingState.Cancelling(distanceX)
isDraggingToLock(currentY, distanceX, distanceY) -> DraggingState.Locking(distanceY) isDraggingToLock(currentY, distanceX, distanceY) -> DraggingState.Locking(distanceY)
else -> this else -> DraggingState.Ready
} }
} }
is DraggingState.Cancelling -> { is DraggingState.Cancelling -> {
when { when {
isDraggingToLock(currentY, distanceX, distanceY) -> DraggingState.Locking(distanceY) isDraggingToLock(currentY, distanceX, distanceY) -> DraggingState.Locking(distanceY)
shouldCancelRecording(distanceX) -> RecordingUiState.Cancelled shouldCancelRecording(distanceX) -> DraggingState.Cancel
else -> DraggingState.Cancelling(distanceX) else -> DraggingState.Cancelling(distanceX)
} }
} }
is DraggingState.Locking -> { is DraggingState.Locking -> {
when { when {
isDraggingToCancel(currentX, distanceX, distanceY) -> DraggingState.Cancelling(distanceX) isDraggingToCancel(currentX, distanceX, distanceY) -> DraggingState.Cancelling(distanceX)
shouldLockRecording(distanceY) -> RecordingUiState.Locked shouldLockRecording(distanceY) -> DraggingState.Lock
else -> DraggingState.Locking(distanceY) else -> DraggingState.Locking(distanceY)
} }
} }

View File

@ -44,7 +44,7 @@ class VoiceMessageRecorderView @JvmOverloads constructor(
fun onVoiceRecordingEnded() fun onVoiceRecordingEnded()
fun onVoicePlaybackButtonClicked() fun onVoicePlaybackButtonClicked()
fun onVoiceRecordingCancelled() fun onVoiceRecordingCancelled()
fun onUiStateChanged(state: RecordingUiState) fun onVoiceRecordingLocked()
fun onSendVoiceMessage() fun onSendVoiceMessage()
fun onDeleteVoiceMessage() fun onDeleteVoiceMessage()
fun onRecordingLimitReached() fun onRecordingLimitReached()
@ -58,6 +58,7 @@ class VoiceMessageRecorderView @JvmOverloads constructor(
private var recordingTicker: CountUpTimer? = null private var recordingTicker: CountUpTimer? = null
private var lastKnownState: RecordingUiState? = null private var lastKnownState: RecordingUiState? = null
private var dragState: DraggingState = DraggingState.Ignored
init { init {
inflate(this.context, R.layout.view_voice_message_recorder, this) inflate(this.context, R.layout.view_voice_message_recorder, this)
@ -74,13 +75,13 @@ class VoiceMessageRecorderView @JvmOverloads constructor(
voiceMessageViews.start(object : VoiceMessageViews.Actions { voiceMessageViews.start(object : VoiceMessageViews.Actions {
override fun onRequestRecording() = callback.onVoiceRecordingStarted() override fun onRequestRecording() = callback.onVoiceRecordingStarted()
override fun onMicButtonReleased() { override fun onMicButtonReleased() {
when (lastKnownState) { when (dragState) {
RecordingUiState.Locked -> { DraggingState.Lock -> {
// do nothing, // do nothing,
// onSendVoiceMessage, onDeleteVoiceMessage or onRecordingLimitReached will be triggered instead // onSendVoiceMessage, onDeleteVoiceMessage or onRecordingLimitReached will be triggered instead
} }
RecordingUiState.Cancelled -> callback.onVoiceRecordingCancelled() DraggingState.Cancel -> callback.onVoiceRecordingCancelled()
else -> callback.onVoiceRecordingEnded() else -> callback.onVoiceRecordingEnded()
} }
} }
@ -88,21 +89,8 @@ class VoiceMessageRecorderView @JvmOverloads constructor(
override fun onDeleteVoiceMessage() = callback.onDeleteVoiceMessage() override fun onDeleteVoiceMessage() = callback.onDeleteVoiceMessage()
override fun onWaveformClicked() = callback.onRecordingWaveformClicked() override fun onWaveformClicked() = callback.onRecordingWaveformClicked()
override fun onVoicePlaybackButtonClicked() = callback.onVoicePlaybackButtonClicked() override fun onVoicePlaybackButtonClicked() = callback.onVoicePlaybackButtonClicked()
override fun onMicButtonDrag(updater: (RecordingUiState) -> RecordingUiState) { override fun onMicButtonDrag(nextDragStateCreator: (DraggingState) -> DraggingState) {
when (val currentState = lastKnownState) { onDrag(dragState, newDragState = nextDragStateCreator(dragState))
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)
}
}
}
}
} }
}) })
} }
@ -117,21 +105,19 @@ class VoiceMessageRecorderView @JvmOverloads constructor(
fun display(recordingState: RecordingUiState) { fun display(recordingState: RecordingUiState) {
if (lastKnownState == recordingState) return if (lastKnownState == recordingState) return
val previousState = lastKnownState
lastKnownState = recordingState lastKnownState = recordingState
when (recordingState) { when (recordingState) {
RecordingUiState.None -> { RecordingUiState.None -> {
stopRecordingTicker() reset()
voiceMessageViews.initViews()
} }
RecordingUiState.Started -> { RecordingUiState.Started -> {
startRecordingTicker() startRecordingTicker()
voiceMessageViews.renderToast(context.getString(R.string.voice_message_release_to_send_toast)) voiceMessageViews.renderToast(context.getString(R.string.voice_message_release_to_send_toast))
voiceMessageViews.showRecordingViews() voiceMessageViews.showRecordingViews()
dragState = DraggingState.Ready
} }
RecordingUiState.Cancelled -> { RecordingUiState.Cancelled -> {
stopRecordingTicker() reset()
voiceMessageViews.hideRecordingViews(recordingState)
vibrate(context) vibrate(context)
} }
RecordingUiState.Locked -> { RecordingUiState.Locked -> {
@ -144,18 +130,34 @@ class VoiceMessageRecorderView @JvmOverloads constructor(
stopRecordingTicker() stopRecordingTicker()
voiceMessageViews.showPlaybackViews() voiceMessageViews.showPlaybackViews()
} }
is DraggingState -> when (recordingState) {
is DraggingState.Cancelling -> voiceMessageViews.renderCancelling(recordingState.distanceX)
is DraggingState.Locking -> {
if (previousState is DraggingState.Cancelling) {
voiceMessageViews.showRecordingViews()
}
voiceMessageViews.renderLocking(recordingState.distanceY)
}
}.exhaustive
} }
} }
private fun reset() {
stopRecordingTicker()
voiceMessageViews.initViews()
dragState = DraggingState.Ignored
}
private fun onDrag(currentDragState: DraggingState, newDragState: DraggingState) {
when (newDragState) {
is DraggingState.Cancelling -> voiceMessageViews.renderCancelling(newDragState.distanceX)
is DraggingState.Locking -> {
if (currentDragState is DraggingState.Cancelling) {
voiceMessageViews.showRecordingViews()
}
voiceMessageViews.renderLocking(newDragState.distanceY)
}
DraggingState.Cancel -> callback.onVoiceRecordingCancelled()
DraggingState.Lock -> callback.onVoiceRecordingLocked()
DraggingState.Ignored,
DraggingState.Ready -> {
// do nothing
}
}.exhaustive
dragState = newDragState
}
private fun startRecordingTicker() { private fun startRecordingTicker() {
recordingTicker?.stop() recordingTicker?.stop()
recordingTicker = CountUpTimer().apply { recordingTicker = CountUpTimer().apply {
@ -214,8 +216,12 @@ class VoiceMessageRecorderView @JvmOverloads constructor(
object Playback : RecordingUiState object Playback : RecordingUiState
} }
sealed interface DraggingState : RecordingUiState { sealed interface DraggingState {
object Ready : DraggingState
object Ignored : DraggingState
data class Cancelling(val distanceX: Float) : DraggingState data class Cancelling(val distanceX: Float) : DraggingState
data class Locking(val distanceY: Float) : DraggingState data class Locking(val distanceY: Float) : DraggingState
object Cancel : DraggingState
object Lock : DraggingState
} }
} }

View File

@ -32,6 +32,7 @@ import im.vector.app.core.extensions.setAttributeTintedBackground
import im.vector.app.core.extensions.setAttributeTintedImageResource import im.vector.app.core.extensions.setAttributeTintedImageResource
import im.vector.app.core.utils.DimensionConverter import im.vector.app.core.utils.DimensionConverter
import im.vector.app.databinding.ViewVoiceMessageRecorderBinding import im.vector.app.databinding.ViewVoiceMessageRecorderBinding
import im.vector.app.features.home.room.detail.composer.voice.VoiceMessageRecorderView.DraggingState
import im.vector.app.features.home.room.detail.composer.voice.VoiceMessageRecorderView.RecordingUiState import im.vector.app.features.home.room.detail.composer.voice.VoiceMessageRecorderView.RecordingUiState
import im.vector.app.features.home.room.detail.timeline.helper.VoiceMessagePlaybackTracker import im.vector.app.features.home.room.detail.timeline.helper.VoiceMessagePlaybackTracker
@ -68,11 +69,11 @@ class VoiceMessageViews(
@SuppressLint("ClickableViewAccessibility") @SuppressLint("ClickableViewAccessibility")
private fun observeMicButton(actions: Actions) { private fun observeMicButton(actions: Actions) {
val positions = DraggableStateProcessor(resources, dimensionConverter) val draggableStateProcessor = DraggableStateProcessor(resources, dimensionConverter)
views.voiceMessageMicButton.setOnTouchListener { _, event -> views.voiceMessageMicButton.setOnTouchListener { _, event ->
when (event.action) { when (event.action) {
MotionEvent.ACTION_DOWN -> { MotionEvent.ACTION_DOWN -> {
positions.initialize(event) draggableStateProcessor.initialize(event)
actions.onRequestRecording() actions.onRequestRecording()
true true
} }
@ -81,7 +82,7 @@ class VoiceMessageViews(
true true
} }
MotionEvent.ACTION_MOVE -> { MotionEvent.ACTION_MOVE -> {
actions.onMicButtonDrag { currentState -> positions.process(event, currentState) } actions.onMicButtonDrag { currentState -> draggableStateProcessor.process(event, currentState) }
true true
} }
else -> false else -> false
@ -339,7 +340,7 @@ class VoiceMessageViews(
interface Actions { interface Actions {
fun onRequestRecording() fun onRequestRecording()
fun onMicButtonReleased() fun onMicButtonReleased()
fun onMicButtonDrag(updater: (RecordingUiState) -> RecordingUiState) fun onMicButtonDrag(nextDragStateCreator: (DraggingState) -> DraggingState)
fun onSendVoiceMessage() fun onSendVoiceMessage()
fun onDeleteVoiceMessage() fun onDeleteVoiceMessage()
fun onWaveformClicked() fun onWaveformClicked()