mirror of
https://github.com/element-hq/element-android
synced 2024-11-25 02:45:37 +03:00
lifting voice display logic out of the view and to the layer above
This commit is contained in:
parent
f0ef9e9706
commit
f2690552a2
4 changed files with 114 additions and 88 deletions
|
@ -139,6 +139,7 @@ import im.vector.app.features.home.room.detail.composer.TextComposerViewEvents
|
|||
import im.vector.app.features.home.room.detail.composer.TextComposerViewModel
|
||||
import im.vector.app.features.home.room.detail.composer.TextComposerViewState
|
||||
import im.vector.app.features.home.room.detail.composer.voice.VoiceMessageRecorderView
|
||||
import im.vector.app.features.home.room.detail.composer.voice.VoiceMessageRecorderView.RecordingUiState
|
||||
import im.vector.app.features.home.room.detail.readreceipts.DisplayReadReceiptsBottomSheet
|
||||
import im.vector.app.features.home.room.detail.timeline.TimelineEventController
|
||||
import im.vector.app.features.home.room.detail.timeline.action.EventSharedAction
|
||||
|
@ -694,15 +695,15 @@ class RoomDetailFragment @Inject constructor(
|
|||
private fun setupVoiceMessageView() {
|
||||
voiceMessagePlaybackTracker.track(VoiceMessagePlaybackTracker.RECORDING_ID, views.voiceMessageRecorderView)
|
||||
views.voiceMessageRecorderView.callback = object : VoiceMessageRecorderView.Callback {
|
||||
override fun onVoiceRecordingStarted(): Boolean {
|
||||
return if (checkPermissions(PERMISSIONS_FOR_VOICE_MESSAGE, requireActivity(), permissionVoiceMessageLauncher)) {
|
||||
|
||||
private var currentUiState: RecordingUiState = RecordingUiState.None
|
||||
|
||||
override fun onVoiceRecordingStarted() {
|
||||
if (checkPermissions(PERMISSIONS_FOR_VOICE_MESSAGE, requireActivity(), permissionVoiceMessageLauncher)) {
|
||||
roomDetailViewModel.handle(RoomDetailAction.StartRecordingVoiceMessage)
|
||||
textComposerViewModel.handle(TextComposerAction.OnVoiceRecordingStateChanged(true))
|
||||
vibrate(requireContext())
|
||||
true
|
||||
} else {
|
||||
// Permission dialog is displayed
|
||||
false
|
||||
views.voiceMessageRecorderView.display(RecordingUiState.Started)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -718,6 +719,29 @@ class RoomDetailFragment @Inject constructor(
|
|||
override fun onVoicePlaybackButtonClicked() {
|
||||
roomDetailViewModel.handle(RoomDetailAction.PlayOrPauseRecordingPlayback)
|
||||
}
|
||||
|
||||
override fun onRecordingStopped() {
|
||||
if (currentUiState != RecordingUiState.Locked && currentUiState != RecordingUiState.None) {
|
||||
views.voiceMessageRecorderView.display(RecordingUiState.None)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onUiStateChanged(state: RecordingUiState) {
|
||||
currentUiState = state
|
||||
views.voiceMessageRecorderView.display(state)
|
||||
}
|
||||
|
||||
override fun sendVoiceMessage() {
|
||||
views.voiceMessageRecorderView.display(RecordingUiState.None)
|
||||
}
|
||||
|
||||
override fun deleteVoiceMessage() {
|
||||
views.voiceMessageRecorderView.display(RecordingUiState.None)
|
||||
}
|
||||
|
||||
override fun onRecordingLimitReached() {
|
||||
views.voiceMessageRecorderView.display(RecordingUiState.Playback)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -21,7 +21,7 @@ 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.RecordingState
|
||||
import im.vector.app.features.home.room.detail.composer.voice.VoiceMessageRecorderView.RecordingUiState
|
||||
import kotlin.math.abs
|
||||
|
||||
class DraggableStateProcessor(
|
||||
|
@ -50,7 +50,7 @@ class DraggableStateProcessor(
|
|||
lastDistanceY = 0F
|
||||
}
|
||||
|
||||
fun process(event: MotionEvent, recordingState: RecordingState): RecordingState {
|
||||
fun process(event: MotionEvent, recordingState: RecordingUiState): RecordingUiState {
|
||||
val currentX = event.rawX
|
||||
val currentY = event.rawY
|
||||
val distanceX = abs(firstX - currentX)
|
||||
|
@ -63,9 +63,9 @@ class DraggableStateProcessor(
|
|||
}
|
||||
}
|
||||
|
||||
private fun nextRecordingState(recordingState: RecordingState, currentX: Float, currentY: Float, distanceX: Float, distanceY: Float): RecordingState {
|
||||
private fun nextRecordingState(recordingState: RecordingUiState, currentX: Float, currentY: Float, distanceX: Float, distanceY: Float): RecordingUiState {
|
||||
return when (recordingState) {
|
||||
RecordingState.Started -> {
|
||||
RecordingUiState.Started -> {
|
||||
// Determine if cancelling or locking for the first move action.
|
||||
if (((currentX < firstX && rtlXMultiplier == 1) || (currentX > firstX && rtlXMultiplier == -1)) && distanceX > distanceY && distanceX > lastDistanceX) {
|
||||
DraggingState.Cancelling(distanceX)
|
||||
|
@ -78,9 +78,9 @@ class DraggableStateProcessor(
|
|||
is DraggingState.Cancelling -> {
|
||||
// Check if cancelling conditions met, also check if it should be initial state
|
||||
if (distanceX < minimumMove && distanceX < lastDistanceX) {
|
||||
RecordingState.Started
|
||||
RecordingUiState.Started
|
||||
} else if (shouldCancelRecording(distanceX)) {
|
||||
RecordingState.Cancelled
|
||||
RecordingUiState.Cancelled
|
||||
} else {
|
||||
DraggingState.Cancelling(distanceX)
|
||||
}
|
||||
|
@ -88,9 +88,9 @@ class DraggableStateProcessor(
|
|||
is DraggingState.Locking -> {
|
||||
// Check if locking conditions met, also check if it should be initial state
|
||||
if (distanceY < minimumMove && distanceY < lastDistanceY) {
|
||||
RecordingState.Started
|
||||
RecordingUiState.Started
|
||||
} else if (shouldLockRecording(distanceY)) {
|
||||
RecordingState.Locked
|
||||
RecordingUiState.Locked
|
||||
} else {
|
||||
DraggingState.Locking(distanceY)
|
||||
}
|
||||
|
|
|
@ -28,7 +28,6 @@ import im.vector.app.core.utils.CountUpTimer
|
|||
import im.vector.app.core.utils.DimensionConverter
|
||||
import im.vector.app.databinding.ViewVoiceMessageRecorderBinding
|
||||
import im.vector.app.features.home.room.detail.timeline.helper.VoiceMessagePlaybackTracker
|
||||
import org.matrix.android.sdk.api.extensions.orFalse
|
||||
import kotlin.math.floor
|
||||
|
||||
/**
|
||||
|
@ -41,11 +40,15 @@ class VoiceMessageRecorderView @JvmOverloads constructor(
|
|||
) : ConstraintLayout(context, attrs, defStyleAttr), VoiceMessagePlaybackTracker.Listener {
|
||||
|
||||
interface Callback {
|
||||
// Return true if the recording is started
|
||||
fun onVoiceRecordingStarted(): Boolean
|
||||
fun onVoiceRecordingStarted()
|
||||
fun onVoiceRecordingEnded(isCancelled: Boolean)
|
||||
fun onVoiceRecordingPlaybackModeOn()
|
||||
fun onVoicePlaybackButtonClicked()
|
||||
fun onRecordingStopped()
|
||||
fun onUiStateChanged(state: RecordingUiState)
|
||||
fun sendVoiceMessage()
|
||||
fun deleteVoiceMessage()
|
||||
fun onRecordingLimitReached()
|
||||
}
|
||||
|
||||
// We need to define views as lateinit var to be able to check if initialized for the bug fix on api 21 and 22.
|
||||
|
@ -54,7 +57,7 @@ class VoiceMessageRecorderView @JvmOverloads constructor(
|
|||
|
||||
var callback: Callback? = null
|
||||
|
||||
private var recordingState: RecordingState = RecordingState.None
|
||||
private var currentUiState: RecordingUiState = RecordingUiState.None
|
||||
private var recordingTicker: CountUpTimer? = null
|
||||
|
||||
init {
|
||||
|
@ -78,7 +81,6 @@ class VoiceMessageRecorderView @JvmOverloads constructor(
|
|||
}
|
||||
|
||||
fun initVoiceRecordingViews() {
|
||||
recordingState = RecordingState.None
|
||||
stopRecordingTicker()
|
||||
voiceMessageViews.initViews(onVoiceRecordingEnded = {})
|
||||
}
|
||||
|
@ -86,36 +88,39 @@ class VoiceMessageRecorderView @JvmOverloads constructor(
|
|||
private fun initListeners() {
|
||||
voiceMessageViews.start(object : VoiceMessageViews.Actions {
|
||||
override fun onRequestRecording() {
|
||||
if (callback?.onVoiceRecordingStarted().orFalse()) {
|
||||
display(RecordingState.Started)
|
||||
}
|
||||
callback?.onVoiceRecordingStarted()
|
||||
}
|
||||
|
||||
override fun onRecordingStopped() {
|
||||
if (recordingState != RecordingState.Locked && recordingState != RecordingState.None) {
|
||||
display(RecordingState.None)
|
||||
}
|
||||
callback?.onRecordingStopped()
|
||||
}
|
||||
|
||||
override fun isActive() = recordingState != RecordingState.Cancelled
|
||||
override fun isActive() = currentUiState != RecordingUiState.Cancelled
|
||||
|
||||
override fun updateState(updater: (RecordingState) -> RecordingState) {
|
||||
updater(recordingState).also {
|
||||
display(it)
|
||||
override fun updateState(updater: (RecordingUiState) -> RecordingUiState) {
|
||||
updater(currentUiState).also { newState ->
|
||||
when (newState) {
|
||||
is DraggingState -> display(newState)
|
||||
else -> {
|
||||
if (newState != currentUiState) {
|
||||
callback?.onUiStateChanged(newState)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun sendMessage() {
|
||||
display(RecordingState.None)
|
||||
callback?.sendVoiceMessage()
|
||||
}
|
||||
|
||||
override fun delete() {
|
||||
// this was previously marked as cancelled true
|
||||
display(RecordingState.None)
|
||||
callback?.deleteVoiceMessage()
|
||||
}
|
||||
|
||||
override fun waveformClicked() {
|
||||
display(RecordingState.Playback)
|
||||
display(RecordingUiState.Playback)
|
||||
}
|
||||
|
||||
override fun onVoicePlaybackButtonClicked() {
|
||||
|
@ -124,43 +129,41 @@ class VoiceMessageRecorderView @JvmOverloads constructor(
|
|||
})
|
||||
}
|
||||
|
||||
fun display(recordingState: RecordingState) {
|
||||
val previousState = this.recordingState
|
||||
val stateHasChanged = recordingState != this.recordingState
|
||||
this.recordingState = recordingState
|
||||
fun display(recordingState: RecordingUiState) {
|
||||
if (recordingState == this.currentUiState) return
|
||||
|
||||
if (stateHasChanged) {
|
||||
when (recordingState) {
|
||||
RecordingState.None -> {
|
||||
val isCancelled = previousState == RecordingState.Cancelled
|
||||
voiceMessageViews.hideRecordingViews(recordingState, isCancelled = isCancelled) { callback?.onVoiceRecordingEnded(it) }
|
||||
stopRecordingTicker()
|
||||
}
|
||||
RecordingState.Started -> {
|
||||
startRecordingTicker()
|
||||
voiceMessageViews.renderToast(context.getString(R.string.voice_message_release_to_send_toast))
|
||||
voiceMessageViews.showRecordingViews()
|
||||
}
|
||||
RecordingState.Cancelled -> {
|
||||
voiceMessageViews.hideRecordingViews(recordingState, isCancelled = true) { callback?.onVoiceRecordingEnded(it) }
|
||||
vibrate(context)
|
||||
}
|
||||
RecordingState.Locked -> {
|
||||
voiceMessageViews.renderLocked()
|
||||
postDelayed({
|
||||
voiceMessageViews.showRecordingLockedViews(recordingState) { callback?.onVoiceRecordingEnded(it) }
|
||||
}, 500)
|
||||
}
|
||||
RecordingState.Playback -> {
|
||||
stopRecordingTicker()
|
||||
voiceMessageViews.showPlaybackViews()
|
||||
callback?.onVoiceRecordingPlaybackModeOn()
|
||||
}
|
||||
is DraggingState -> when (recordingState) {
|
||||
is DraggingState.Cancelling -> voiceMessageViews.renderCancelling(recordingState.distanceX)
|
||||
is DraggingState.Locking -> voiceMessageViews.renderLocking(recordingState.distanceY)
|
||||
}.exhaustive
|
||||
val previousState = this.currentUiState
|
||||
this.currentUiState = recordingState
|
||||
when (recordingState) {
|
||||
RecordingUiState.None -> {
|
||||
val isCancelled = previousState == RecordingUiState.Cancelled
|
||||
voiceMessageViews.hideRecordingViews(recordingState, isCancelled = isCancelled) { callback?.onVoiceRecordingEnded(it) }
|
||||
stopRecordingTicker()
|
||||
}
|
||||
RecordingUiState.Started -> {
|
||||
startRecordingTicker()
|
||||
voiceMessageViews.renderToast(context.getString(R.string.voice_message_release_to_send_toast))
|
||||
voiceMessageViews.showRecordingViews()
|
||||
}
|
||||
RecordingUiState.Cancelled -> {
|
||||
voiceMessageViews.hideRecordingViews(recordingState, isCancelled = true) { callback?.onVoiceRecordingEnded(it) }
|
||||
vibrate(context)
|
||||
}
|
||||
RecordingUiState.Locked -> {
|
||||
voiceMessageViews.renderLocked()
|
||||
postDelayed({
|
||||
voiceMessageViews.showRecordingLockedViews(recordingState) { callback?.onVoiceRecordingEnded(it) }
|
||||
}, 500)
|
||||
}
|
||||
RecordingUiState.Playback -> {
|
||||
stopRecordingTicker()
|
||||
voiceMessageViews.showPlaybackViews()
|
||||
callback?.onVoiceRecordingPlaybackModeOn()
|
||||
}
|
||||
is DraggingState -> when (recordingState) {
|
||||
is DraggingState.Cancelling -> voiceMessageViews.renderCancelling(recordingState.distanceX)
|
||||
is DraggingState.Locking -> voiceMessageViews.renderLocking(recordingState.distanceY)
|
||||
}.exhaustive
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -178,11 +181,11 @@ class VoiceMessageRecorderView @JvmOverloads constructor(
|
|||
}
|
||||
|
||||
private fun onRecordingTick(milliseconds: Long) {
|
||||
voiceMessageViews.renderRecordingTimer(recordingState, milliseconds / 1_000)
|
||||
voiceMessageViews.renderRecordingTimer(currentUiState, milliseconds / 1_000)
|
||||
val timeDiffToRecordingLimit = BuildConfig.VOICE_MESSAGE_DURATION_LIMIT_MS - milliseconds
|
||||
if (timeDiffToRecordingLimit <= 0) {
|
||||
post {
|
||||
display(RecordingState.Playback)
|
||||
callback?.onRecordingLimitReached()
|
||||
}
|
||||
} else if (timeDiffToRecordingLimit in 10_000..10_999) {
|
||||
post {
|
||||
|
@ -200,7 +203,7 @@ class VoiceMessageRecorderView @JvmOverloads constructor(
|
|||
/**
|
||||
* Returns true if the voice message is recording or is in playback mode
|
||||
*/
|
||||
fun isActive() = recordingState !in listOf(RecordingState.None, RecordingState.Cancelled)
|
||||
fun isActive() = currentUiState !in listOf(RecordingUiState.None, RecordingUiState.Cancelled)
|
||||
|
||||
override fun onUpdate(state: VoiceMessagePlaybackTracker.Listener.State) {
|
||||
when (state) {
|
||||
|
@ -217,17 +220,16 @@ class VoiceMessageRecorderView @JvmOverloads constructor(
|
|||
}
|
||||
}
|
||||
|
||||
sealed interface RecordingState {
|
||||
object None : RecordingState
|
||||
object Started : RecordingState
|
||||
object Cancelled : RecordingState
|
||||
object Locked : RecordingState
|
||||
object Playback : RecordingState
|
||||
sealed interface RecordingUiState {
|
||||
object None : RecordingUiState
|
||||
object Started : RecordingUiState
|
||||
object Cancelled : RecordingUiState
|
||||
object Locked : RecordingUiState
|
||||
object Playback : RecordingUiState
|
||||
}
|
||||
|
||||
sealed interface DraggingState : RecordingState {
|
||||
sealed interface DraggingState : RecordingUiState {
|
||||
data class Cancelling(val distanceX: Float) : DraggingState
|
||||
data class Locking(val distanceY: Float) : DraggingState
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -32,7 +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.RecordingState
|
||||
import im.vector.app.features.home.room.detail.composer.voice.VoiceMessageRecorderView.RecordingUiState
|
||||
import im.vector.app.features.home.room.detail.timeline.helper.VoiceMessagePlaybackTracker
|
||||
import org.matrix.android.sdk.api.extensions.orFalse
|
||||
|
||||
|
@ -155,9 +155,9 @@ class VoiceMessageViews(
|
|||
views.voiceMessageSendButton.isVisible = false
|
||||
}
|
||||
|
||||
fun hideRecordingViews(recordingState: RecordingState, isCancelled: Boolean?, onVoiceRecordingEnded: (Boolean) -> Unit) {
|
||||
fun hideRecordingViews(recordingState: RecordingUiState, isCancelled: Boolean?, onVoiceRecordingEnded: (Boolean) -> Unit) {
|
||||
// We need to animate the lock image first
|
||||
if (recordingState != RecordingState.Locked || isCancelled.orFalse()) {
|
||||
if (recordingState != RecordingUiState.Locked || isCancelled.orFalse()) {
|
||||
views.voiceMessageLockImage.isVisible = false
|
||||
views.voiceMessageLockImage.animate().translationY(0f).start()
|
||||
views.voiceMessageLockBackground.isVisible = false
|
||||
|
@ -171,7 +171,7 @@ class VoiceMessageViews(
|
|||
views.voiceMessageSlideToCancel.animate().translationX(0f).translationY(0f).start()
|
||||
views.voiceMessagePlaybackLayout.isVisible = false
|
||||
|
||||
if (recordingState != RecordingState.Locked) {
|
||||
if (recordingState != RecordingUiState.Locked) {
|
||||
views.voiceMessageMicButton
|
||||
.animate()
|
||||
.scaleX(1f)
|
||||
|
@ -203,7 +203,7 @@ class VoiceMessageViews(
|
|||
}
|
||||
|
||||
// Hide toasts if user cancelled recording before the timeout of the toast.
|
||||
if (recordingState == RecordingState.Cancelled || recordingState == RecordingState.None) {
|
||||
if (recordingState == RecordingUiState.Cancelled || recordingState == RecordingUiState.None) {
|
||||
hideToast()
|
||||
}
|
||||
}
|
||||
|
@ -266,7 +266,7 @@ class VoiceMessageViews(
|
|||
views.voiceMessageToast.isVisible = false
|
||||
}
|
||||
|
||||
fun showRecordingLockedViews(recordingState: RecordingState, onVoiceRecordingEnded: (Boolean) -> Unit) {
|
||||
fun showRecordingLockedViews(recordingState: RecordingUiState, onVoiceRecordingEnded: (Boolean) -> Unit) {
|
||||
hideRecordingViews(recordingState, null, onVoiceRecordingEnded)
|
||||
views.voiceMessagePlaybackLayout.isVisible = true
|
||||
views.voiceMessagePlaybackTimerIndicator.isVisible = true
|
||||
|
@ -283,7 +283,7 @@ class VoiceMessageViews(
|
|||
}
|
||||
|
||||
fun initViews(onVoiceRecordingEnded: (Boolean) -> Unit) {
|
||||
hideRecordingViews(RecordingState.None, null, onVoiceRecordingEnded)
|
||||
hideRecordingViews(RecordingUiState.None, null, onVoiceRecordingEnded)
|
||||
views.voiceMessageMicButton.isVisible = true
|
||||
views.voiceMessageSendButton.isVisible = false
|
||||
views.voicePlaybackWaveform.post { views.voicePlaybackWaveform.recreate() }
|
||||
|
@ -312,9 +312,9 @@ class VoiceMessageViews(
|
|||
views.voiceMessageToast.isVisible = false
|
||||
}
|
||||
|
||||
fun renderRecordingTimer(recordingState: RecordingState, recordingTimeMillis: Long) {
|
||||
fun renderRecordingTimer(recordingState: RecordingUiState, recordingTimeMillis: Long) {
|
||||
val formattedTimerText = DateUtils.formatElapsedTime(recordingTimeMillis)
|
||||
if (recordingState == RecordingState.Locked) {
|
||||
if (recordingState == RecordingUiState.Locked) {
|
||||
views.voicePlaybackTime.apply {
|
||||
post {
|
||||
text = formattedTimerText
|
||||
|
@ -349,7 +349,7 @@ class VoiceMessageViews(
|
|||
fun onRequestRecording()
|
||||
fun onRecordingStopped()
|
||||
fun isActive(): Boolean
|
||||
fun updateState(updater: (RecordingState) -> RecordingState)
|
||||
fun updateState(updater: (RecordingUiState) -> RecordingUiState)
|
||||
fun sendMessage()
|
||||
fun delete()
|
||||
fun waveformClicked()
|
||||
|
|
Loading…
Reference in a new issue