mirror of
https://github.com/element-hq/element-android
synced 2024-11-28 13:38:49 +03:00
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:
parent
e895dbd923
commit
9ae03b76cd
6 changed files with 69 additions and 67 deletions
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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 -> {
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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"
|
||||
|
|
Loading…
Reference in a new issue