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() {
onDeleteVoiceMessage()
roomDetailViewModel.handle(RoomDetailAction.EndRecordingVoiceMessage(isCancelled = true))
updateRecordingUiState(RecordingUiState.Cancelled)
}
override fun onVoiceRecordingLocked() {
updateRecordingUiState(RecordingUiState.Locked)
}
override fun onVoiceRecordingEnded() {
onSendVoiceMessage()
}
override fun onUiStateChanged(state: RecordingUiState) {
updateRecordingUiState(state)
}
override fun onSendVoiceMessage() {
roomDetailViewModel.handle(RoomDetailAction.EndRecordingVoiceMessage(isCancelled = false))
updateRecordingUiState(RecordingUiState.None)

View File

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

View File

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

View File

@ -44,7 +44,7 @@ class VoiceMessageRecorderView @JvmOverloads constructor(
fun onVoiceRecordingEnded()
fun onVoicePlaybackButtonClicked()
fun onVoiceRecordingCancelled()
fun onUiStateChanged(state: RecordingUiState)
fun onVoiceRecordingLocked()
fun onSendVoiceMessage()
fun onDeleteVoiceMessage()
fun onRecordingLimitReached()
@ -58,6 +58,7 @@ class VoiceMessageRecorderView @JvmOverloads constructor(
private var recordingTicker: CountUpTimer? = null
private var lastKnownState: RecordingUiState? = null
private var dragState: DraggingState = DraggingState.Ignored
init {
inflate(this.context, R.layout.view_voice_message_recorder, this)
@ -74,13 +75,13 @@ class VoiceMessageRecorderView @JvmOverloads constructor(
voiceMessageViews.start(object : VoiceMessageViews.Actions {
override fun onRequestRecording() = callback.onVoiceRecordingStarted()
override fun onMicButtonReleased() {
when (lastKnownState) {
RecordingUiState.Locked -> {
when (dragState) {
DraggingState.Lock -> {
// do nothing,
// onSendVoiceMessage, onDeleteVoiceMessage or onRecordingLimitReached will be triggered instead
}
RecordingUiState.Cancelled -> callback.onVoiceRecordingCancelled()
else -> callback.onVoiceRecordingEnded()
DraggingState.Cancel -> callback.onVoiceRecordingCancelled()
else -> callback.onVoiceRecordingEnded()
}
}
@ -88,21 +89,8 @@ class VoiceMessageRecorderView @JvmOverloads constructor(
override fun onDeleteVoiceMessage() = callback.onDeleteVoiceMessage()
override fun onWaveformClicked() = callback.onRecordingWaveformClicked()
override fun onVoicePlaybackButtonClicked() = callback.onVoicePlaybackButtonClicked()
override fun onMicButtonDrag(updater: (RecordingUiState) -> RecordingUiState) {
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)
}
}
}
}
override fun onMicButtonDrag(nextDragStateCreator: (DraggingState) -> DraggingState) {
onDrag(dragState, newDragState = nextDragStateCreator(dragState))
}
})
}
@ -117,21 +105,19 @@ class VoiceMessageRecorderView @JvmOverloads constructor(
fun display(recordingState: RecordingUiState) {
if (lastKnownState == recordingState) return
val previousState = lastKnownState
lastKnownState = recordingState
when (recordingState) {
RecordingUiState.None -> {
stopRecordingTicker()
voiceMessageViews.initViews()
reset()
}
RecordingUiState.Started -> {
startRecordingTicker()
voiceMessageViews.renderToast(context.getString(R.string.voice_message_release_to_send_toast))
voiceMessageViews.showRecordingViews()
dragState = DraggingState.Ready
}
RecordingUiState.Cancelled -> {
stopRecordingTicker()
voiceMessageViews.hideRecordingViews(recordingState)
reset()
vibrate(context)
}
RecordingUiState.Locked -> {
@ -144,18 +130,34 @@ class VoiceMessageRecorderView @JvmOverloads constructor(
stopRecordingTicker()
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() {
recordingTicker?.stop()
recordingTicker = CountUpTimer().apply {
@ -214,8 +216,12 @@ class VoiceMessageRecorderView @JvmOverloads constructor(
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 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.utils.DimensionConverter
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.timeline.helper.VoiceMessagePlaybackTracker
@ -68,11 +69,11 @@ class VoiceMessageViews(
@SuppressLint("ClickableViewAccessibility")
private fun observeMicButton(actions: Actions) {
val positions = DraggableStateProcessor(resources, dimensionConverter)
val draggableStateProcessor = DraggableStateProcessor(resources, dimensionConverter)
views.voiceMessageMicButton.setOnTouchListener { _, event ->
when (event.action) {
MotionEvent.ACTION_DOWN -> {
positions.initialize(event)
draggableStateProcessor.initialize(event)
actions.onRequestRecording()
true
}
@ -81,7 +82,7 @@ class VoiceMessageViews(
true
}
MotionEvent.ACTION_MOVE -> {
actions.onMicButtonDrag { currentState -> positions.process(event, currentState) }
actions.onMicButtonDrag { currentState -> draggableStateProcessor.process(event, currentState) }
true
}
else -> false
@ -339,7 +340,7 @@ class VoiceMessageViews(
interface Actions {
fun onRequestRecording()
fun onMicButtonReleased()
fun onMicButtonDrag(updater: (RecordingUiState) -> RecordingUiState)
fun onMicButtonDrag(nextDragStateCreator: (DraggingState) -> DraggingState)
fun onSendVoiceMessage()
fun onDeleteVoiceMessage()
fun onWaveformClicked()