From 40d762c37d384f670f80be4e26bc4b4bbfcc40c4 Mon Sep 17 00:00:00 2001 From: Adam Brown Date: Thu, 11 Nov 2021 15:50:00 +0000 Subject: [PATCH] lifting current recording state out of the view --- .../home/room/detail/RoomDetailFragment.kt | 36 +- .../composer/VoiceMessageRecorderView.kt | 551 ------------------ .../voice/VoiceMessageRecorderView.kt | 58 +- .../composer/voice/VoiceMessageViews.kt | 4 +- 4 files changed, 49 insertions(+), 600 deletions(-) delete mode 100644 vector/src/main/java/im/vector/app/features/home/room/detail/composer/VoiceMessageRecorderView.kt 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 e1dab55979..ba45348cab 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 @@ -506,7 +506,7 @@ class RoomDetailFragment @Inject constructor( private fun onCannotRecord() { // Update the UI, cancel the animation - views.voiceMessageRecorderView.initVoiceRecordingViews() + views.voiceMessageRecorderView.display(RecordingUiState.None) } private fun acceptIncomingCall(event: RoomDetailViewEvents.DisplayAndAcceptCall) { @@ -698,12 +698,16 @@ class RoomDetailFragment @Inject constructor( private var currentUiState: RecordingUiState = RecordingUiState.None + init { + display(currentUiState) + } + override fun onVoiceRecordingStarted() { if (checkPermissions(PERMISSIONS_FOR_VOICE_MESSAGE, requireActivity(), permissionVoiceMessageLauncher)) { roomDetailViewModel.handle(RoomDetailAction.StartRecordingVoiceMessage) textComposerViewModel.handle(TextComposerAction.OnVoiceRecordingStateChanged(true)) vibrate(requireContext()) - views.voiceMessageRecorderView.display(RecordingUiState.Started) + display(RecordingUiState.Started) } } @@ -721,27 +725,39 @@ class RoomDetailFragment @Inject constructor( } override fun onRecordingStopped() { - if (currentUiState != RecordingUiState.Locked && currentUiState != RecordingUiState.None) { - views.voiceMessageRecorderView.display(RecordingUiState.None) + if (currentUiState != RecordingUiState.Locked) { + display(RecordingUiState.None) } } override fun onUiStateChanged(state: RecordingUiState) { - currentUiState = state - views.voiceMessageRecorderView.display(state) + display(state) } override fun sendVoiceMessage() { - views.voiceMessageRecorderView.display(RecordingUiState.None) + display(RecordingUiState.None) } override fun deleteVoiceMessage() { - views.voiceMessageRecorderView.display(RecordingUiState.None) + display(RecordingUiState.Cancelled) } override fun onRecordingLimitReached() { - views.voiceMessageRecorderView.display(RecordingUiState.Playback) + display(RecordingUiState.Playback) } + + override fun recordingWaveformClicked() { + display(RecordingUiState.Playback) + } + + private fun display(state: RecordingUiState) { + if (currentUiState != state) { + views.voiceMessageRecorderView.display(state) + } + currentUiState = state + } + + override fun currentState() = currentUiState } } @@ -1132,7 +1148,7 @@ class RoomDetailFragment @Inject constructor( // We should improve the UX to support going into playback mode when paused and delete the media when the view is destroyed. roomDetailViewModel.handle(RoomDetailAction.EndAllVoiceActions(deleteRecord = false)) - views.voiceMessageRecorderView.initVoiceRecordingViews() + views.voiceMessageRecorderView.display(RecordingUiState.None) } private val attachmentFileActivityResultLauncher = registerStartForActivityResult { diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/VoiceMessageRecorderView.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/VoiceMessageRecorderView.kt deleted file mode 100644 index f7b8cead37..0000000000 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/VoiceMessageRecorderView.kt +++ /dev/null @@ -1,551 +0,0 @@ -/* - * Copyright (c) 2021 New Vector Ltd - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package im.vector.app.features.home.room.detail.composer - -import android.content.Context -import android.text.format.DateUtils -import android.util.AttributeSet -import android.view.MotionEvent -import android.view.View -import androidx.constraintlayout.widget.ConstraintLayout -import androidx.core.view.isInvisible -import androidx.core.view.isVisible -import androidx.core.view.updateLayoutParams -import im.vector.app.BuildConfig -import im.vector.app.R -import im.vector.app.core.extensions.setAttributeBackground -import im.vector.app.core.extensions.setAttributeTintedBackground -import im.vector.app.core.extensions.setAttributeTintedImageResource -import im.vector.app.core.hardware.vibrate -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 timber.log.Timber -import kotlin.math.abs -import kotlin.math.floor - -/** - * Encapsulates the voice message recording view and animations. - */ -class VoiceMessageRecorderView : ConstraintLayout, VoiceMessagePlaybackTracker.Listener { - - interface Callback { - // Return true if the recording is started - fun onVoiceRecordingStarted(): Boolean - fun onVoiceRecordingEnded(isCancelled: Boolean) - fun onVoiceRecordingPlaybackModeOn() - fun onVoicePlaybackButtonClicked() - } - - private lateinit var views: ViewVoiceMessageRecorderBinding - - var callback: Callback? = null - var voiceMessagePlaybackTracker: VoiceMessagePlaybackTracker? = null - set(value) { - field = value - value?.track(VoiceMessagePlaybackTracker.RECORDING_ID, this) - } - - private var recordingState: RecordingState = RecordingState.NONE - - 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 - - private var recordingTicker: CountUpTimer? = null - - private val dimensionConverter = DimensionConverter(context.resources) - private val minimumMove = dimensionConverter.dpToPx(16) - private val distanceToLock = dimensionConverter.dpToPx(48).toFloat() - private val distanceToCancel = dimensionConverter.dpToPx(120).toFloat() - private val rtlXMultiplier = context.resources.getInteger(R.integer.rtl_x_multiplier) - - // Don't convert to primary constructor. - // We need to define views as lateinit var to be able to check if initialized for the bug fix on api 21 and 22. - @JvmOverloads constructor( - context: Context, - attrs: AttributeSet? = null, - defStyleAttr: Int = 0 - ) : super(context, attrs, defStyleAttr) { - initialize() - } - - fun initialize() { - inflate(context, R.layout.view_voice_message_recorder, this) - views = ViewVoiceMessageRecorderBinding.bind(this) - - initVoiceRecordingViews() - initListeners() - } - - override fun onVisibilityChanged(changedView: View, visibility: Int) { - super.onVisibilityChanged(changedView, visibility) - // onVisibilityChanged is called by constructor on api 21 and 22. - if (!this::views.isInitialized) return - - if (changedView == this && visibility == VISIBLE) { - views.voiceMessageMicButton.contentDescription = context.getString(R.string.a11y_start_voice_message) - } else { - views.voiceMessageMicButton.contentDescription = "" - } - } - - fun initVoiceRecordingViews() { - recordingState = RecordingState.NONE - - hideRecordingViews(null) - stopRecordingTicker() - - views.voiceMessageMicButton.isVisible = true - views.voiceMessageSendButton.isVisible = false - - views.voicePlaybackWaveform.post { views.voicePlaybackWaveform.recreate() } - } - - private fun initListeners() { - views.voiceMessageSendButton.setOnClickListener { - stopRecordingTicker() - hideRecordingViews(isCancelled = false) - views.voiceMessageSendButton.isVisible = false - recordingState = RecordingState.NONE - } - - views.voiceMessageDeletePlayback.setOnClickListener { - stopRecordingTicker() - hideRecordingViews(isCancelled = true) - views.voiceMessageSendButton.isVisible = false - recordingState = RecordingState.NONE - } - - views.voicePlaybackWaveform.setOnClickListener { - if (recordingState != RecordingState.PLAYBACK) { - recordingState = RecordingState.PLAYBACK - showPlaybackViews() - } - } - - views.voicePlaybackControlButton.setOnClickListener { - callback?.onVoicePlaybackButtonClicked() - } - - views.voiceMessageMicButton.setOnTouchListener { _, event -> - when (event.action) { - MotionEvent.ACTION_DOWN -> { - handleMicActionDown(event) - true - } - MotionEvent.ACTION_UP -> { - handleMicActionUp() - true - } - MotionEvent.ACTION_MOVE -> { - if (recordingState == RecordingState.CANCELLED) return@setOnTouchListener false - handleMicActionMove(event) - true - } - else -> - false - } - } - } - - private fun handleMicActionDown(event: MotionEvent) { - val recordingStarted = callback?.onVoiceRecordingStarted().orFalse() - if (recordingStarted) { - startRecordingTicker() - renderToast(context.getString(R.string.voice_message_release_to_send_toast)) - recordingState = RecordingState.STARTED - showRecordingViews() - - firstX = event.rawX - firstY = event.rawY - lastX = firstX - lastY = firstY - lastDistanceX = 0F - lastDistanceY = 0F - } - } - - private fun handleMicActionUp() { - if (recordingState != RecordingState.LOCKED && recordingState != RecordingState.NONE) { - stopRecordingTicker() - val isCancelled = recordingState == RecordingState.NONE || recordingState == RecordingState.CANCELLED - recordingState = RecordingState.NONE - hideRecordingViews(isCancelled = isCancelled) - } - } - - private fun handleMicActionMove(event: MotionEvent) { - val currentX = event.rawX - val currentY = event.rawY - - val distanceX = abs(firstX - currentX) - val distanceY = abs(firstY - currentY) - - val isRecordingStateChanged = updateRecordingState(currentX, currentY, distanceX, distanceY) - - when (recordingState) { - RecordingState.CANCELLING -> { - val translationAmount = distanceX.coerceAtMost(distanceToCancel) - views.voiceMessageMicButton.translationX = -translationAmount * rtlXMultiplier - views.voiceMessageSlideToCancel.translationX = -translationAmount / 2 * rtlXMultiplier - val reducedAlpha = (1 - translationAmount / distanceToCancel / 1.5).toFloat() - views.voiceMessageSlideToCancel.alpha = reducedAlpha - views.voiceMessageTimerIndicator.alpha = reducedAlpha - views.voiceMessageTimer.alpha = reducedAlpha - views.voiceMessageLockBackground.isVisible = false - views.voiceMessageLockImage.isVisible = false - views.voiceMessageLockArrow.isVisible = false - // Reset Y translations - views.voiceMessageMicButton.translationY = 0F - views.voiceMessageLockArrow.translationY = 0F - } - RecordingState.LOCKING -> { - views.voiceMessageLockImage.setAttributeTintedImageResource(R.drawable.ic_voice_message_locked, R.attr.colorPrimary) - val translationAmount = -distanceY.coerceIn(0F, distanceToLock) - views.voiceMessageMicButton.translationY = translationAmount - views.voiceMessageLockArrow.translationY = translationAmount - views.voiceMessageLockArrow.alpha = 1 - (-translationAmount / distanceToLock) - // Reset X translations - views.voiceMessageMicButton.translationX = 0F - views.voiceMessageSlideToCancel.translationX = 0F - } - RecordingState.CANCELLED -> { - hideRecordingViews(isCancelled = true) - vibrate(context) - } - RecordingState.LOCKED -> { - if (isRecordingStateChanged) { // Do not update views if it was already in locked state. - views.voiceMessageLockImage.setImageResource(R.drawable.ic_voice_message_locked) - views.voiceMessageLockImage.postDelayed({ - showRecordingLockedViews() - }, 500) - } - } - RecordingState.STARTED -> { - showRecordingViews() - val translationAmount = distanceX.coerceAtMost(distanceToCancel) - views.voiceMessageMicButton.translationX = -translationAmount * rtlXMultiplier - views.voiceMessageSlideToCancel.translationX = -translationAmount / 2 * rtlXMultiplier - } - RecordingState.NONE -> Timber.d("VoiceMessageRecorderView shouldn't be in NONE state while moving.") - RecordingState.PLAYBACK -> Timber.d("VoiceMessageRecorderView shouldn't be in PLAYBACK state while moving.") - } - lastX = currentX - lastY = currentY - lastDistanceX = distanceX - lastDistanceY = distanceY - } - - private fun updateRecordingState(currentX: Float, currentY: Float, distanceX: Float, distanceY: Float): Boolean { - val previousRecordingState = recordingState - if (recordingState == RecordingState.STARTED) { - // Determine if cancelling or locking for the first move action. - if (((currentX < firstX && rtlXMultiplier == 1) || (currentX > firstX && rtlXMultiplier == -1)) && - distanceX > distanceY && distanceX > lastDistanceX) { - recordingState = RecordingState.CANCELLING - } else if (currentY < firstY && distanceY > distanceX && distanceY > lastDistanceY) { - recordingState = RecordingState.LOCKING - } - } else if (recordingState == RecordingState.CANCELLING) { - // Check if cancelling conditions met, also check if it should be initial state - if (distanceX < minimumMove && distanceX < lastDistanceX) { - recordingState = RecordingState.STARTED - } else if (shouldCancelRecording(distanceX)) { - recordingState = RecordingState.CANCELLED - } - } else if (recordingState == RecordingState.LOCKING) { - // Check if locking conditions met, also check if it should be initial state - if (distanceY < minimumMove && distanceY < lastDistanceY) { - recordingState = RecordingState.STARTED - } else if (shouldLockRecording(distanceY)) { - recordingState = RecordingState.LOCKED - } - } - return previousRecordingState != recordingState - } - - private fun shouldCancelRecording(distanceX: Float): Boolean { - return distanceX >= distanceToCancel - } - - private fun shouldLockRecording(distanceY: Float): Boolean { - return distanceY >= distanceToLock - } - - private fun startRecordingTicker() { - recordingTicker?.stop() - recordingTicker = CountUpTimer().apply { - tickListener = object : CountUpTimer.TickListener { - override fun onTick(milliseconds: Long) { - onRecordingTick(milliseconds) - } - } - resume() - } - onRecordingTick(0L) - } - - private fun onRecordingTick(milliseconds: Long) { - renderRecordingTimer(milliseconds / 1_000) - val timeDiffToRecordingLimit = BuildConfig.VOICE_MESSAGE_DURATION_LIMIT_MS - milliseconds - if (timeDiffToRecordingLimit <= 0) { - views.voiceMessageRecordingLayout.post { - recordingState = RecordingState.PLAYBACK - showPlaybackViews() - stopRecordingTicker() - } - } else if (timeDiffToRecordingLimit in 10_000..10_999) { - views.voiceMessageRecordingLayout.post { - renderToast(context.getString(R.string.voice_message_n_seconds_warning_toast, floor(timeDiffToRecordingLimit / 1000f).toInt())) - vibrate(context) - } - } - } - - private fun renderToast(message: String) { - views.voiceMessageToast.removeCallbacks(hideToastRunnable) - views.voiceMessageToast.text = message - views.voiceMessageToast.isVisible = true - views.voiceMessageToast.postDelayed(hideToastRunnable, 2_000) - } - - private fun hideToast() { - views.voiceMessageToast.isVisible = false - } - - private val hideToastRunnable = Runnable { - views.voiceMessageToast.isVisible = false - } - - private fun renderRecordingTimer(recordingTimeMillis: Long) { - val formattedTimerText = DateUtils.formatElapsedTime(recordingTimeMillis) - if (recordingState == RecordingState.LOCKED) { - views.voicePlaybackTime.apply { - post { - text = formattedTimerText - } - } - } else { - views.voiceMessageTimer.post { - views.voiceMessageTimer.text = formattedTimerText - } - } - } - - private fun renderRecordingWaveform(amplitudeList: Array) { - post { - views.voicePlaybackWaveform.apply { - amplitudeList.iterator().forEach { - update(it) - } - } - } - } - - private fun stopRecordingTicker() { - recordingTicker?.stop() - recordingTicker = null - } - - private fun showRecordingViews() { - views.voiceMessageMicButton.setImageResource(R.drawable.ic_voice_mic_recording) - views.voiceMessageMicButton.setAttributeTintedBackground(R.drawable.circle_with_halo, R.attr.colorPrimary) - views.voiceMessageMicButton.updateLayoutParams { - setMargins(0, 0, 0, 0) - } - views.voiceMessageMicButton.animate().scaleX(1.5f).scaleY(1.5f).setDuration(300).start() - - views.voiceMessageLockBackground.isVisible = true - views.voiceMessageLockBackground.animate().setDuration(300).translationY(-dimensionConverter.dpToPx(180).toFloat()).start() - views.voiceMessageLockImage.isVisible = true - views.voiceMessageLockImage.setImageResource(R.drawable.ic_voice_message_unlocked) - views.voiceMessageLockImage.animate().setDuration(500).translationY(-dimensionConverter.dpToPx(180).toFloat()).start() - views.voiceMessageLockArrow.isVisible = true - views.voiceMessageLockArrow.alpha = 1f - views.voiceMessageSlideToCancel.isVisible = true - views.voiceMessageTimerIndicator.isVisible = true - views.voiceMessageTimer.isVisible = true - views.voiceMessageSlideToCancel.alpha = 1f - views.voiceMessageTimerIndicator.alpha = 1f - views.voiceMessageTimer.alpha = 1f - views.voiceMessageSendButton.isVisible = false - } - - private fun hideRecordingViews(isCancelled: Boolean?) { - // We need to animate the lock image first - if (recordingState != RecordingState.LOCKED || isCancelled.orFalse()) { - views.voiceMessageLockImage.isVisible = false - views.voiceMessageLockImage.animate().translationY(0f).start() - views.voiceMessageLockBackground.isVisible = false - views.voiceMessageLockBackground.animate().translationY(0f).start() - } else { - animateLockImageWithBackground() - } - 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 - - if (recordingState != RecordingState.LOCKED) { - views.voiceMessageMicButton - .animate() - .scaleX(1f) - .scaleY(1f) - .translationX(0f) - .translationY(0f) - .setDuration(150) - .withEndAction { - views.voiceMessageTimerIndicator.isVisible = false - views.voiceMessageTimer.isVisible = false - resetMicButtonUi() - isCancelled?.let { - callback?.onVoiceRecordingEnded(it) - } - } - .start() - } else { - views.voiceMessageTimerIndicator.isVisible = false - views.voiceMessageTimer.isVisible = false - views.voiceMessageMicButton.apply { - scaleX = 1f - scaleY = 1f - translationX = 0f - translationY = 0f - } - isCancelled?.let { - callback?.onVoiceRecordingEnded(it) - } - } - - // Hide toasts if user cancelled recording before the timeout of the toast. - if (recordingState == RecordingState.CANCELLED || recordingState == RecordingState.NONE) { - hideToast() - } - } - - private fun resetMicButtonUi() { - views.voiceMessageMicButton.isVisible = true - views.voiceMessageMicButton.setImageResource(R.drawable.ic_voice_mic) - views.voiceMessageMicButton.setAttributeBackground(android.R.attr.selectableItemBackgroundBorderless) - views.voiceMessageMicButton.updateLayoutParams { - if (rtlXMultiplier == -1) { - // RTL - setMargins(dimensionConverter.dpToPx(12), 0, 0, dimensionConverter.dpToPx(12)) - } else { - setMargins(0, 0, dimensionConverter.dpToPx(12), dimensionConverter.dpToPx(12)) - } - } - } - - private fun animateLockImageWithBackground() { - views.voiceMessageLockBackground.updateLayoutParams { - height = dimensionConverter.dpToPx(78) - } - views.voiceMessageLockBackground.apply { - animate() - .scaleX(0f) - .scaleY(0f) - .setDuration(400L) - .withEndAction { - updateLayoutParams { - height = dimensionConverter.dpToPx(180) - } - isVisible = false - scaleX = 1f - scaleY = 1f - animate().translationY(0f).start() - } - .start() - } - - // Lock image animation - views.voiceMessageMicButton.isInvisible = true - views.voiceMessageLockImage.apply { - isVisible = true - animate() - .scaleX(0f) - .scaleY(0f) - .setDuration(400L) - .withEndAction { - isVisible = false - scaleX = 1f - scaleY = 1f - translationY = 0f - resetMicButtonUi() - } - .start() - } - } - - private fun showRecordingLockedViews() { - hideRecordingViews(null) - views.voiceMessagePlaybackLayout.isVisible = true - views.voiceMessagePlaybackTimerIndicator.isVisible = true - views.voicePlaybackControlButton.isVisible = false - views.voiceMessageSendButton.isVisible = true - views.voicePlaybackWaveform.importantForAccessibility = View.IMPORTANT_FOR_ACCESSIBILITY_YES - renderToast(context.getString(R.string.voice_message_tap_to_stop_toast)) - } - - private fun showPlaybackViews() { - views.voiceMessagePlaybackTimerIndicator.isVisible = false - views.voicePlaybackControlButton.isVisible = true - views.voicePlaybackWaveform.importantForAccessibility = View.IMPORTANT_FOR_ACCESSIBILITY_NO - callback?.onVoiceRecordingPlaybackModeOn() - } - - private enum class RecordingState { - NONE, - STARTED, - CANCELLING, - CANCELLED, - LOCKING, - LOCKED, - PLAYBACK - } - - /** - * Returns true if the voice message is recording or is in playback mode - */ - fun isActive() = recordingState !in listOf(RecordingState.NONE, RecordingState.CANCELLED) - - override fun onUpdate(state: VoiceMessagePlaybackTracker.Listener.State) { - when (state) { - is VoiceMessagePlaybackTracker.Listener.State.Recording -> { - renderRecordingWaveform(state.amplitudeList.toTypedArray()) - } - is VoiceMessagePlaybackTracker.Listener.State.Playing -> { - views.voicePlaybackControlButton.setImageResource(R.drawable.ic_play_pause_pause) - views.voicePlaybackControlButton.contentDescription = context.getString(R.string.a11y_pause_voice_message) - val formattedTimerText = DateUtils.formatElapsedTime((state.playbackTime / 1000).toLong()) - views.voicePlaybackTime.text = formattedTimerText - } - is VoiceMessagePlaybackTracker.Listener.State.Paused, - is VoiceMessagePlaybackTracker.Listener.State.Idle -> { - views.voicePlaybackControlButton.setImageResource(R.drawable.ic_play_pause_play) - views.voicePlaybackControlButton.contentDescription = context.getString(R.string.a11y_play_voice_message) - } - } - } -} 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 79898dad32..2227405507 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 @@ -49,15 +49,15 @@ class VoiceMessageRecorderView @JvmOverloads constructor( fun sendVoiceMessage() fun deleteVoiceMessage() fun onRecordingLimitReached() + fun recordingWaveformClicked() + fun currentState(): 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. @Suppress("UNNECESSARY_LATEINIT") private lateinit var voiceMessageViews: VoiceMessageViews + lateinit var callback: Callback - var callback: Callback? = null - - private var currentUiState: RecordingUiState = RecordingUiState.None private var recordingTicker: CountUpTimer? = null init { @@ -68,7 +68,6 @@ class VoiceMessageRecorderView @JvmOverloads constructor( ViewVoiceMessageRecorderBinding.bind(this), dimensionConverter ) - initVoiceRecordingViews() initListeners() } @@ -80,65 +79,49 @@ class VoiceMessageRecorderView @JvmOverloads constructor( voiceMessageViews.renderVisibilityChanged(parentChanged, visibility) } - fun initVoiceRecordingViews() { - stopRecordingTicker() - voiceMessageViews.initViews(onVoiceRecordingEnded = {}) - } - private fun initListeners() { voiceMessageViews.start(object : VoiceMessageViews.Actions { override fun onRequestRecording() { - callback?.onVoiceRecordingStarted() + callback.onVoiceRecordingStarted() } override fun onRecordingStopped() { - callback?.onRecordingStopped() + callback.onRecordingStopped() } - override fun isActive() = currentUiState != RecordingUiState.Cancelled + override fun isActive() = callback.currentState() != RecordingUiState.Cancelled override fun updateState(updater: (RecordingUiState) -> RecordingUiState) { - updater(currentUiState).also { newState -> - when (newState) { - is DraggingState -> display(newState) - else -> { - if (newState != currentUiState) { - callback?.onUiStateChanged(newState) - } - } - } + updater(callback.currentState()).also { newState -> + callback.onUiStateChanged(newState) } } override fun sendMessage() { - callback?.sendVoiceMessage() + callback.sendVoiceMessage() } override fun delete() { // this was previously marked as cancelled true - callback?.deleteVoiceMessage() + callback.deleteVoiceMessage() } override fun waveformClicked() { - display(RecordingUiState.Playback) + callback.recordingWaveformClicked() } override fun onVoicePlaybackButtonClicked() { - callback?.onVoicePlaybackButtonClicked() + callback.onVoicePlaybackButtonClicked() } }) } fun display(recordingState: RecordingUiState) { - if (recordingState == this.currentUiState) return - - 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() + voiceMessageViews.initViews() + callback.onVoiceRecordingEnded(false) } RecordingUiState.Started -> { startRecordingTicker() @@ -146,19 +129,20 @@ class VoiceMessageRecorderView @JvmOverloads constructor( voiceMessageViews.showRecordingViews() } RecordingUiState.Cancelled -> { - voiceMessageViews.hideRecordingViews(recordingState, isCancelled = true) { callback?.onVoiceRecordingEnded(it) } + stopRecordingTicker() + voiceMessageViews.hideRecordingViews(recordingState, isCancelled = true) { callback.onVoiceRecordingEnded(it) } vibrate(context) } RecordingUiState.Locked -> { voiceMessageViews.renderLocked() postDelayed({ - voiceMessageViews.showRecordingLockedViews(recordingState) { callback?.onVoiceRecordingEnded(it) } + voiceMessageViews.showRecordingLockedViews(recordingState) { callback.onVoiceRecordingEnded(it) } }, 500) } RecordingUiState.Playback -> { stopRecordingTicker() voiceMessageViews.showPlaybackViews() - callback?.onVoiceRecordingPlaybackModeOn() + callback.onVoiceRecordingPlaybackModeOn() } is DraggingState -> when (recordingState) { is DraggingState.Cancelling -> voiceMessageViews.renderCancelling(recordingState.distanceX) @@ -181,11 +165,11 @@ class VoiceMessageRecorderView @JvmOverloads constructor( } private fun onRecordingTick(milliseconds: Long) { - voiceMessageViews.renderRecordingTimer(currentUiState, milliseconds / 1_000) + voiceMessageViews.renderRecordingTimer(callback.currentState(), milliseconds / 1_000) val timeDiffToRecordingLimit = BuildConfig.VOICE_MESSAGE_DURATION_LIMIT_MS - milliseconds if (timeDiffToRecordingLimit <= 0) { post { - callback?.onRecordingLimitReached() + callback.onRecordingLimitReached() } } else if (timeDiffToRecordingLimit in 10_000..10_999) { post { @@ -203,7 +187,7 @@ class VoiceMessageRecorderView @JvmOverloads constructor( /** * Returns true if the voice message is recording or is in playback mode */ - fun isActive() = currentUiState !in listOf(RecordingUiState.None, RecordingUiState.Cancelled) + fun isActive() = callback.currentState() !in listOf(RecordingUiState.None, RecordingUiState.Cancelled) override fun onUpdate(state: VoiceMessagePlaybackTracker.Listener.State) { when (state) { 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 ce4ec4b519..63b5dc17ee 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 @@ -282,8 +282,8 @@ class VoiceMessageViews( views.voicePlaybackWaveform.importantForAccessibility = View.IMPORTANT_FOR_ACCESSIBILITY_NO } - fun initViews(onVoiceRecordingEnded: (Boolean) -> Unit) { - hideRecordingViews(RecordingUiState.None, null, onVoiceRecordingEnded) + fun initViews() { + hideRecordingViews(RecordingUiState.None, null, onVoiceRecordingEnded = {}) views.voiceMessageMicButton.isVisible = true views.voiceMessageSendButton.isVisible = false views.voicePlaybackWaveform.post { views.voicePlaybackWaveform.recreate() }