Design review fixes.

This commit is contained in:
Onuray Sahin 2021-07-29 18:41:29 +03:00
parent 4c8a8d8cfb
commit cdd2fca258
5 changed files with 167 additions and 61 deletions

View File

@ -16,15 +16,14 @@
package im.vector.app.features.home.room.detail.composer
import android.animation.Animator
import android.content.Context
import android.text.format.DateUtils
import android.util.AttributeSet
import android.view.MotionEvent
import androidx.constraintlayout.widget.ConstraintLayout
import androidx.core.view.isInvisible
import androidx.core.view.isVisible
import androidx.core.view.updateLayoutParams
import com.visualizer.amplitude.AudioRecordView
import im.vector.app.BuildConfig
import im.vector.app.R
import im.vector.app.core.hardware.vibrate
@ -75,8 +74,8 @@ class VoiceMessageRecorderView @JvmOverloads constructor(
private var recordingTicker: CountUpTimer? = null
private val dimensionConverter = DimensionConverter(context.resources)
private val minimumMove = dimensionConverter.dpToPx(10)
private val distanceToLock = dimensionConverter.dpToPx(34).toFloat()
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)
@ -89,28 +88,28 @@ class VoiceMessageRecorderView @JvmOverloads constructor(
}
fun initVoiceRecordingViews() {
hideRecordingViews()
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()
hideRecordingViews(isCancelled = false)
views.voiceMessageSendButton.isVisible = false
recordingState = RecordingState.NONE
callback?.onVoiceRecordingEnded(isCancelled = false)
}
views.voiceMessageDeletePlayback.setOnClickListener {
stopRecordingTicker()
hideRecordingViews()
hideRecordingViews(isCancelled = true)
views.voiceMessageSendButton.isVisible = false
recordingState = RecordingState.NONE
callback?.onVoiceRecordingEnded(isCancelled = true)
}
views.voicePlaybackWaveform.setOnClickListener {
@ -135,6 +134,7 @@ class VoiceMessageRecorderView @JvmOverloads constructor(
true
}
MotionEvent.ACTION_MOVE -> {
if (recordingState == RecordingState.CANCELLED) return@setOnTouchListener false
handleMicActionMove(event)
true
}
@ -162,12 +162,11 @@ class VoiceMessageRecorderView @JvmOverloads constructor(
}
private fun handleMicActionUp() {
if (recordingState != RecordingState.LOCKED) {
if (recordingState != RecordingState.LOCKED && recordingState != RecordingState.NONE) {
stopRecordingTicker()
val isCancelled = recordingState == RecordingState.NONE || recordingState == RecordingState.CANCELLED
callback?.onVoiceRecordingEnded(isCancelled)
recordingState = RecordingState.NONE
hideRecordingViews()
hideRecordingViews(isCancelled = isCancelled)
}
}
@ -185,7 +184,10 @@ class VoiceMessageRecorderView @JvmOverloads constructor(
val translationAmount = distanceX.coerceAtMost(distanceToCancel)
views.voiceMessageMicButton.translationX = -translationAmount * rtlXMultiplier
views.voiceMessageSlideToCancel.translationX = -translationAmount / 2 * rtlXMultiplier
views.voiceMessageSlideToCancel.alpha = 1 - translationAmount / distanceToCancel / 3
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
@ -198,13 +200,13 @@ class VoiceMessageRecorderView @JvmOverloads constructor(
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 -> {
callback?.onVoiceRecordingEnded(true)
hideRecordingViews()
hideRecordingViews(isCancelled = true)
}
RecordingState.LOCKED -> {
if (isRecordingStateChanged) { // Do not update views if it was already in locked state.
@ -299,6 +301,10 @@ class VoiceMessageRecorderView @JvmOverloads constructor(
views.voiceMessageToast.postDelayed(hideToastRunnable, 2_000)
}
private fun hideToast() {
views.voiceMessageToast.isVisible = false
}
private val hideToastRunnable = Runnable {
views.voiceMessageToast.isVisible = false
}
@ -322,7 +328,8 @@ class VoiceMessageRecorderView @JvmOverloads constructor(
views.voicePlaybackWaveform.apply {
post {
// TODO We could avoid recreating the whole view here and just call update() with the new value(s).
//recreate()
// Currently it is broken if a configuration change occurs.
recreate()
amplitudeList.forEach { amplitude ->
update(amplitude)
}
@ -340,46 +347,132 @@ class VoiceMessageRecorderView @JvmOverloads constructor(
views.voiceMessageMicButton.updateLayoutParams<MarginLayoutParams> {
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(148).toFloat()).start()
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(148).toFloat()).start()
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
views.voiceMessageLockImage.setImageResource(R.drawable.ic_voice_message_unlocked)
}
private fun hideRecordingViews() {
views.voiceMessageMicButton.setImageResource(R.drawable.ic_voice_mic)
views.voiceMessageMicButton.animate().translationX(0f).translationY(0f).setDuration(0).start()
views.voiceMessageMicButton.updateLayoutParams<MarginLayoutParams> {
if (rtlXMultiplier == -1) {
// RTL
setMargins(dimensionConverter.dpToPx(10), 0, 0, dimensionConverter.dpToPx(12))
} else {
setMargins(0, 0, dimensionConverter.dpToPx(12), dimensionConverter.dpToPx(10))
}
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.voiceMessageLockBackground.isVisible = false
views.voiceMessageLockBackground.animate().translationY(0f).start()
views.voiceMessageLockImage.isVisible = false
views.voiceMessageLockImage.animate().translationY(0f).start()
views.voiceMessageLockArrow.isVisible = false
views.voiceMessageLockArrow.animate().translationY(0f).start()
views.voiceMessageSlideToCancel.isVisible = false
views.voiceMessageSlideToCancel.animate().translationX(0f).translationY(0f).start()
views.voiceMessageTimerIndicator.isVisible = false
views.voiceMessageTimer.isVisible = false
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.updateLayoutParams<MarginLayoutParams> {
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()
hideRecordingViews(null)
views.voiceMessagePlaybackLayout.isVisible = true
views.voiceMessagePlaybackTimerIndicator.isVisible = true
views.voicePlaybackControlButton.isVisible = false

View File

@ -1,14 +1,14 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
<size
android:width="56dp"
android:width="78dp"
android:height="160dp" />
<solid android:color="#F00" />
<corners
android:bottomLeftRadius="28dp"
android:bottomRightRadius="28dp"
android:topLeftRadius="28dp"
android:topRightRadius="28dp" />
android:bottomLeftRadius="39dp"
android:bottomRightRadius="39dp"
android:topLeftRadius="39dp"
android:topRightRadius="39dp" />
</shape>

View File

@ -3,13 +3,13 @@
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#F00"
android:fillType="evenOdd"
android:pathData="M12,22C17.5228,22 22,17.5228 22,12C22,6.4771 17.5228,2 12,2C6.4771,2 2,6.4771 2,12C2,17.5228 6.4771,22 12,22ZM15.389,13.7659C15.6794,13.3162 16.2793,13.187 16.729,13.4774C17.1788,13.7677 17.308,14.3676 17.0176,14.8174C15.9565,16.461 14.1059,17.5526 11.9996,17.5526C9.8934,17.5526 8.0428,16.461 6.9817,14.8174C6.6913,14.3677 6.8205,13.7677 7.2702,13.4774C7.72,13.187 8.3199,13.3162 8.6103,13.7659C9.3295,14.88 10.5791,15.6141 11.9996,15.6141C13.4202,15.6141 14.6698,14.88 15.389,13.7659ZM10,10C10,10.8284 9.4404,11.5 8.75,11.5C8.0596,11.5 7.5,10.8284 7.5,10C7.5,9.1716 8.0596,8.5 8.75,8.5C9.4404,8.5 10,9.1716 10,10ZM15.25,11.5C15.9404,11.5 16.5,10.8284 16.5,10C16.5,9.1716 15.9404,8.5 15.25,8.5C14.5596,8.5 14,9.1716 14,10C14,10.8284 14.5596,11.5 15.25,11.5Z" />
<group>
<clip-path
android:fillType="evenOdd"
android:pathData="M12,22C17.5228,22 22,17.5228 22,12C22,6.4771 17.5228,2 12,2C6.4771,2 2,6.4771 2,12C2,17.5228 6.4771,22 12,22ZM15.389,13.7659C15.6794,13.3162 16.2793,13.187 16.729,13.4774C17.1788,13.7677 17.308,14.3676 17.0176,14.8174C15.9565,16.461 14.1059,17.5526 11.9996,17.5526C9.8934,17.5526 8.0428,16.461 6.9817,14.8174C6.6913,14.3677 6.8205,13.7677 7.2702,13.4774C7.72,13.187 8.3199,13.3162 8.6103,13.7659C9.3295,14.88 10.5791,15.6141 11.9996,15.6141C13.4202,15.6141 14.6698,14.88 15.389,13.7659ZM10,10C10,10.8284 9.4404,11.5 8.75,11.5C8.0596,11.5 7.5,10.8284 7.5,10C7.5,9.1716 8.0596,8.5 8.75,8.5C9.4404,8.5 10,9.1716 10,10ZM15.25,11.5C15.9404,11.5 16.5,10.8284 16.5,10C16.5,9.1716 15.9404,8.5 15.25,8.5C14.5596,8.5 14,9.1716 14,10C14,10.8284 14.5596,11.5 15.25,11.5Z" />
</group>
<path
android:pathData="M12,22C17.5228,22 22,17.5228 22,12C22,6.4771 17.5228,2 12,2C6.4771,2 2,6.4771 2,12C2,17.5228 6.4771,22 12,22ZM15.389,13.7659C15.6794,13.3162 16.2793,13.187 16.729,13.4774C17.1788,13.7677 17.308,14.3676 17.0176,14.8174C15.9565,16.461 14.1059,17.5526 11.9996,17.5526C9.8934,17.5526 8.0428,16.461 6.9817,14.8174C6.6913,14.3677 6.8205,13.7677 7.2702,13.4774C7.72,13.187 8.3199,13.3162 8.6103,13.7659C9.3295,14.88 10.5791,15.6141 11.9996,15.6141C13.4202,15.6141 14.6698,14.88 15.389,13.7659ZM10,10C10,10.8284 9.4404,11.5 8.75,11.5C8.0596,11.5 7.5,10.8284 7.5,10C7.5,9.1716 8.0596,8.5 8.75,8.5C9.4404,8.5 10,9.1716 10,10ZM15.25,11.5C15.9404,11.5 16.5,10.8284 16.5,10C16.5,9.1716 15.9404,8.5 15.25,8.5C14.5596,8.5 14,9.1716 14,10C14,10.8284 14.5596,11.5 15.25,11.5Z"
android:fillColor="#F00"
android:fillType="evenOdd"/>
<group>
<clip-path
android:pathData="M12,22C17.5228,22 22,17.5228 22,12C22,6.4771 17.5228,2 12,2C6.4771,2 2,6.4771 2,12C2,17.5228 6.4771,22 12,22ZM15.389,13.7659C15.6794,13.3162 16.2793,13.187 16.729,13.4774C17.1788,13.7677 17.308,14.3676 17.0176,14.8174C15.9565,16.461 14.1059,17.5526 11.9996,17.5526C9.8934,17.5526 8.0428,16.461 6.9817,14.8174C6.6913,14.3677 6.8205,13.7677 7.2702,13.4774C7.72,13.187 8.3199,13.3162 8.6103,13.7659C9.3295,14.88 10.5791,15.6141 11.9996,15.6141C13.4202,15.6141 14.6698,14.88 15.389,13.7659ZM10,10C10,10.8284 9.4404,11.5 8.75,11.5C8.0596,11.5 7.5,10.8284 7.5,10C7.5,9.1716 8.0596,8.5 8.75,8.5C9.4404,8.5 10,9.1716 10,10ZM15.25,11.5C15.9404,11.5 16.5,10.8284 16.5,10C16.5,9.1716 15.9404,8.5 15.25,8.5C14.5596,8.5 14,9.1716 14,10C14,10.8284 14.5596,11.5 15.25,11.5Z"
android:fillType="evenOdd"/>
</group>
</vector>

View File

@ -9,21 +9,21 @@
<View
android:id="@+id/voiceMessageLockBackground"
android:layout_width="52dp"
android:layout_height="160dp"
android:layout_width="78dp"
android:layout_height="180dp"
android:background="@drawable/bg_voice_message_lock"
android:backgroundTint="?vctr_system"
android:visibility="gone"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@id/voiceMessageMicButton"
tools:translationY="-148dp"
tools:translationY="-180dp"
tools:visibility="visible" />
<ImageButton
android:id="@+id/voiceMessageMicButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="10dp"
android:layout_marginEnd="12dp"
android:layout_marginBottom="12dp"
android:background="?android:attr/selectableItemBackground"
android:contentDescription="@string/a11y_start_voice_message"
@ -64,8 +64,10 @@
android:id="@+id/voiceMessageTimer"
style="@style/Widget.Vector.TextView.Body.Medium"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_height="0dp"
android:layout_marginStart="4dp"
android:gravity="center"
android:includeFontPadding="false"
android:textColor="?vctr_content_secondary"
android:visibility="gone"
app:layout_constraintBottom_toBottomOf="@id/voiceMessageMicButton"
@ -91,23 +93,35 @@
app:layout_constraintTop_toTopOf="@id/voiceMessageMicButton"
tools:visibility="visible" />
<!-- Slide to cancel text should go under this view -->
<View
android:layout_width="48dp"
android:layout_height="0dp"
android:background="?android:colorBackground"
app:layout_constraintBottom_toBottomOf="@id/voiceMessageTimer"
app:layout_constraintStart_toEndOf="@id/voiceMessageTimer"
app:layout_constraintTop_toTopOf="@id/voiceMessageTimer" />
<ImageView
android:id="@+id/voiceMessageLockImage"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:layout_marginTop="28dp"
android:contentDescription="@string/a11y_lock_voice_message"
android:src="@drawable/ic_voice_message_unlocked"
android:visibility="gone"
app:layout_constraintEnd_toEndOf="@id/voiceMessageLockBackground"
app:layout_constraintStart_toStartOf="@id/voiceMessageLockBackground"
app:layout_constraintEnd_toEndOf="@id/voiceMessageMicButton"
app:layout_constraintStart_toStartOf="@id/voiceMessageMicButton"
app:layout_constraintTop_toTopOf="@id/voiceMessageLockBackground"
tools:translationY="-180dp"
tools:visibility="visible" />
<ImageView
android:id="@+id/voiceMessageLockArrow"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="4dp"
android:layout_marginBottom="14dp"
android:importantForAccessibility="no"
android:src="@drawable/ic_voice_lock_arrow"
android:visibility="gone"
@ -173,7 +187,7 @@
android:layout_height="32dp"
android:layout_marginStart="8dp"
android:background="@drawable/bg_voice_play_pause_button"
android:backgroundTint="?android:colorBackground"
android:backgroundTint="?vctr_system"
android:contentDescription="@string/a11y_play_voice_message"
android:src="@drawable/ic_play_pause_play"
app:layout_constraintBottom_toBottomOf="parent"

View File

@ -3449,9 +3449,8 @@
<string name="a11y_delete_recorded_voice_message">Delete recorded voice message</string>
<string name="voice_message_release_to_send_toast">Hold to record, release to send</string>
<string name="voice_message_n_seconds_warning_toast">%1$ds left</string>
<string name="voice_message_tap_on_waveform_to_stop_toast">Tap on your recording to stop or listen</string>
<string name="voice_message_tap_to_stop_toast">Tap on your recording to stop or listen</string>
<string name="labs_use_voice_message">Enable voice message</string>
<string name="voice_message_tap_to_stop_toast">Tap on the wavelength to stop and playback</string>
<string name="error_voice_message_unable_to_play">Cannot play this voice message</string>
<string name="error_voice_message_unable_to_record">Cannot record a voice message</string>
<string name="error_voice_message_cannot_reply_or_edit">Cannot reply or edit while voice message is active</string>