Add basic voice message recording UI

This commit is contained in:
Niels Andriesse 2021-06-16 14:50:41 +10:00
parent bf25a44f7b
commit 5ae201b81b
7 changed files with 198 additions and 41 deletions

View File

@ -6,6 +6,7 @@ import android.view.ActionMode
import android.view.Menu import android.view.Menu
import android.view.MenuItem import android.view.MenuItem
import android.widget.RelativeLayout import android.widget.RelativeLayout
import androidx.core.view.isVisible
import androidx.loader.app.LoaderManager import androidx.loader.app.LoaderManager
import androidx.loader.content.Loader import androidx.loader.content.Loader
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
@ -112,6 +113,10 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
recyclerViewLayoutParams.bottomMargin = newValue recyclerViewLayoutParams.bottomMargin = newValue
conversationRecyclerView.layoutParams = recyclerViewLayoutParams conversationRecyclerView.layoutParams = recyclerViewLayoutParams
} }
override fun showVoiceMessageUI() {
inputBarRecordingView.isVisible = true
}
// endregion // endregion
// region Interaction // region Interaction

View File

@ -3,7 +3,6 @@ package org.thoughtcrime.securesms.conversation.v2.input_bar
import android.content.Context import android.content.Context
import android.util.AttributeSet import android.util.AttributeSet
import android.view.LayoutInflater import android.view.LayoutInflater
import android.widget.LinearLayout
import android.widget.RelativeLayout import android.widget.RelativeLayout
import androidx.core.view.isVisible import androidx.core.view.isVisible
import kotlinx.android.synthetic.main.view_input_bar.view.* import kotlinx.android.synthetic.main.view_input_bar.view.*
@ -12,7 +11,7 @@ import org.thoughtcrime.securesms.loki.utilities.toDp
import org.thoughtcrime.securesms.loki.utilities.toPx import org.thoughtcrime.securesms.loki.utilities.toPx
import kotlin.math.max import kotlin.math.max
class InputBar : LinearLayout, InputBarEditTextDelegate { class InputBar : RelativeLayout, InputBarEditTextDelegate {
var delegate: InputBarDelegate? = null var delegate: InputBarDelegate? = null
private val attachmentsButton by lazy { InputBarButton(context, R.drawable.ic_plus_24) } private val attachmentsButton by lazy { InputBarButton(context, R.drawable.ic_plus_24) }
@ -20,30 +19,26 @@ class InputBar : LinearLayout, InputBarEditTextDelegate {
private val sendButton by lazy { InputBarButton(context, R.drawable.ic_arrow_up, true) } private val sendButton by lazy { InputBarButton(context, R.drawable.ic_arrow_up, true) }
// region Lifecycle // region Lifecycle
constructor(context: Context) : super(context) { constructor(context: Context) : super(context) { initialize() }
setUpViewHierarchy() constructor(context: Context, attrs: AttributeSet) : super(context, attrs) { initialize() }
} constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(context, attrs, defStyleAttr) { initialize() }
constructor(context: Context, attrs: AttributeSet) : super(context, attrs) { private fun initialize() {
setUpViewHierarchy()
}
constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(context, attrs, defStyleAttr) {
setUpViewHierarchy()
}
private fun setUpViewHierarchy() {
LayoutInflater.from(context).inflate(R.layout.view_input_bar, this) LayoutInflater.from(context).inflate(R.layout.view_input_bar, this)
// Attachments button
attachmentsButtonContainer.addView(attachmentsButton) attachmentsButtonContainer.addView(attachmentsButton)
attachmentsButton.layoutParams = RelativeLayout.LayoutParams(RelativeLayout.LayoutParams.MATCH_PARENT, RelativeLayout.LayoutParams.MATCH_PARENT) attachmentsButton.layoutParams = RelativeLayout.LayoutParams(RelativeLayout.LayoutParams.MATCH_PARENT, RelativeLayout.LayoutParams.MATCH_PARENT)
attachmentsButton.setOnClickListener { } // Microphone button
microphoneOrSendButtonContainer.addView(microphoneButton) microphoneOrSendButtonContainer.addView(microphoneButton)
microphoneButton.layoutParams = RelativeLayout.LayoutParams(RelativeLayout.LayoutParams.MATCH_PARENT, RelativeLayout.LayoutParams.MATCH_PARENT) microphoneButton.layoutParams = RelativeLayout.LayoutParams(RelativeLayout.LayoutParams.MATCH_PARENT, RelativeLayout.LayoutParams.MATCH_PARENT)
microphoneButton.setOnClickListener { } microphoneButton.onLongPress = {
showVoiceMessageUI()
}
// Send button
microphoneOrSendButtonContainer.addView(sendButton) microphoneOrSendButtonContainer.addView(sendButton)
sendButton.layoutParams = RelativeLayout.LayoutParams(RelativeLayout.LayoutParams.MATCH_PARENT, RelativeLayout.LayoutParams.MATCH_PARENT) sendButton.layoutParams = RelativeLayout.LayoutParams(RelativeLayout.LayoutParams.MATCH_PARENT, RelativeLayout.LayoutParams.MATCH_PARENT)
sendButton.setOnClickListener { }
sendButton.isVisible = false sendButton.isVisible = false
// Edit text
inputBarEditText.imeOptions = inputBarEditText.imeOptions or 16777216 // Always use incognito keyboard inputBarEditText.imeOptions = inputBarEditText.imeOptions or 16777216 // Always use incognito keyboard
inputBarEditText.delegate = this inputBarEditText.delegate = this
} }
@ -63,10 +58,15 @@ class InputBar : LinearLayout, InputBarEditTextDelegate {
inputBarLinearLayout.layoutParams = layoutParams inputBarLinearLayout.layoutParams = layoutParams
delegate?.inputBarHeightChanged(newHeight) delegate?.inputBarHeightChanged(newHeight)
} }
private fun showVoiceMessageUI() {
delegate?.showVoiceMessageUI()
}
// endregion // endregion
} }
interface InputBarDelegate { interface InputBarDelegate {
fun inputBarHeightChanged(newValue: Int) fun inputBarHeightChanged(newValue: Int)
fun showVoiceMessageUI()
} }

View File

@ -6,7 +6,10 @@ import android.content.Context
import android.content.res.ColorStateList import android.content.res.ColorStateList
import android.graphics.PointF import android.graphics.PointF
import android.os.Build import android.os.Build
import android.os.Handler
import android.os.Looper
import android.util.AttributeSet import android.util.AttributeSet
import android.util.Log
import android.view.Gravity import android.view.Gravity
import android.view.HapticFeedbackConstants import android.view.HapticFeedbackConstants
import android.view.MotionEvent import android.view.MotionEvent
@ -14,13 +17,22 @@ import android.widget.ImageView
import android.widget.RelativeLayout import android.widget.RelativeLayout
import androidx.annotation.DrawableRes import androidx.annotation.DrawableRes
import network.loki.messenger.R import network.loki.messenger.R
import org.thoughtcrime.securesms.conversation.v2.messages.VisibleMessageView
import org.thoughtcrime.securesms.loki.utilities.* import org.thoughtcrime.securesms.loki.utilities.*
import org.thoughtcrime.securesms.loki.views.GlowViewUtilities import org.thoughtcrime.securesms.loki.views.GlowViewUtilities
import org.thoughtcrime.securesms.loki.views.InputBarButtonImageViewContainer import org.thoughtcrime.securesms.loki.views.InputBarButtonImageViewContainer
import java.util.*
import kotlin.math.abs
class InputBarButton : RelativeLayout { class InputBarButton : RelativeLayout {
private val gestureHandler = Handler(Looper.getMainLooper())
private var isSendButton = false private var isSendButton = false
@DrawableRes private var iconID = 0 @DrawableRes private var iconID = 0
private var longPressCallback: Runnable? = null
private var onDownTimestamp = 0L
var onPress: (() -> Unit)? = null
var onLongPress: (() -> Unit)? = null
companion object { companion object {
val animationDuration = 250.toLong() val animationDuration = 250.toLong()
@ -99,16 +111,37 @@ class InputBarButton : RelativeLayout {
override fun onTouchEvent(event: MotionEvent): Boolean { override fun onTouchEvent(event: MotionEvent): Boolean {
when (event.action) { when (event.action) {
MotionEvent.ACTION_DOWN -> { MotionEvent.ACTION_DOWN -> onDown(event)
MotionEvent.ACTION_UP -> onUp(event)
MotionEvent.ACTION_CANCEL -> onCancel(event)
}
return true
}
private fun onDown(event: MotionEvent) {
expand() expand()
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
performHapticFeedback(HapticFeedbackConstants.CONTEXT_CLICK) performHapticFeedback(HapticFeedbackConstants.CONTEXT_CLICK)
} else { } else {
performHapticFeedback(HapticFeedbackConstants.LONG_PRESS) performHapticFeedback(HapticFeedbackConstants.LONG_PRESS)
} }
longPressCallback?.let { gestureHandler.removeCallbacks(it) }
val newLongPressCallback = Runnable { onLongPress?.invoke() }
this.longPressCallback = newLongPressCallback
gestureHandler.postDelayed(newLongPressCallback, VisibleMessageView.longPressDurationThreshold)
onDownTimestamp = Date().time
} }
MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL -> { collapse() }
private fun onCancel(event: MotionEvent) {
collapse()
longPressCallback?.let { gestureHandler.removeCallbacks(it) }
}
private fun onUp(event: MotionEvent) {
collapse()
if ((Date().time - onDownTimestamp) < VisibleMessageView.longPressDurationThreshold) {
longPressCallback?.let { gestureHandler.removeCallbacks(it) }
onPress?.invoke()
} }
return true
} }
} }

View File

@ -0,0 +1,18 @@
package org.thoughtcrime.securesms.conversation.v2.input_bar
import android.content.Context
import android.util.AttributeSet
import android.view.LayoutInflater
import android.widget.RelativeLayout
import network.loki.messenger.R
class InputBarRecordingView : RelativeLayout {
constructor(context: Context) : super(context) { initialize() }
constructor(context: Context, attrs: AttributeSet) : super(context, attrs) { initialize() }
constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(context, attrs, defStyleAttr) { initialize() }
private fun initialize() {
LayoutInflater.from(context).inflate(R.layout.view_input_bar_recording, this)
}
}

View File

@ -230,18 +230,7 @@ class VisibleMessageView : LinearLayout {
private fun onCancel(event: MotionEvent) { private fun onCancel(event: MotionEvent) {
longPressCallback?.let { gestureHandler.removeCallbacks(it) } longPressCallback?.let { gestureHandler.removeCallbacks(it) }
animate() resetPosition()
.translationX(0.0f)
.setDuration(150)
.setUpdateListener {
postInvalidate() // Ensure onDraw(canvas:) is called
}
.start()
// Bit of a hack to keep the date break text view from moving
dateBreakTextView.animate()
.translationX(0.0f)
.setDuration(150)
.start()
} }
private fun onUp(event: MotionEvent) { private fun onUp(event: MotionEvent) {
@ -251,6 +240,10 @@ class VisibleMessageView : LinearLayout {
longPressCallback?.let { gestureHandler.removeCallbacks(it) } longPressCallback?.let { gestureHandler.removeCallbacks(it) }
onPress?.invoke() onPress?.invoke()
} }
resetPosition()
}
private fun resetPosition() {
animate() animate()
.translationX(0.0f) .translationX(0.0f)
.setDuration(150) .setDuration(150)

View File

@ -18,4 +18,12 @@
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_alignParentBottom="true" /> android:layout_alignParentBottom="true" />
<org.thoughtcrime.securesms.conversation.v2.input_bar.InputBarRecordingView
android:id="@+id/inputBarRecordingView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="-12dp"
android:visibility="gone"
android:layout_alignParentBottom="true" />
</RelativeLayout> </RelativeLayout>

View File

@ -0,0 +1,100 @@
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="80dp">
<!-- The height of the fake input bar below is 68 dp
because the input bar is 56 dp but we have to
account for the fact that this whole view has a
negative bottom margin of 12 dp -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="68dp"
android:layout_alignParentBottom="true"
android:orientation="vertical">
<View
android:layout_width="match_parent"
android:layout_height="1px"
android:background="@color/separator" />
<View
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/input_bar_background" />
</LinearLayout>
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="@dimen/medium_spacing"
android:layout_alignParentStart="true"
android:layout_centerVertical="true"
android:gravity="center_vertical"
android:orientation="horizontal">
<View
android:layout_width="16dp"
android:layout_height="16dp"
android:background="@drawable/circle_tintable"
android:backgroundTint="@color/destructive" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginLeft="@dimen/small_spacing"
android:text="00:00"
android:textSize="@dimen/small_font_size"
android:textColor="@color/text" />
</LinearLayout>
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerInParent="true"
android:orientation="horizontal"
android:gravity="center_vertical">
<ImageView
android:layout_width="16dp"
android:layout_height="16dp"
android:src="@drawable/ic_keyboard_arrow_left_grey600_24dp"
android:layout_marginTop="1dp"
app:tint="@color/text"
android:alpha="0.6" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginLeft="4dp"
android:text="@string/conversation_input_panel__slide_to_cancel"
android:textSize="@dimen/very_small_font_size"
android:textColor="@color/text"
android:alpha="0.6" />
</LinearLayout>
<RelativeLayout
android:layout_width="80dp"
android:layout_height="80dp"
android:layout_alignParentEnd="true"
android:layout_marginEnd="-8dp"
android:background="@drawable/circle_tintable"
android:backgroundTint="@color/destructive" >
<ImageView
android:layout_width="24dp"
android:layout_height="24dp"
app:tint="@color/white"
android:scaleType="centerInside"
android:layout_centerInParent="true"
android:src="@drawable/ic_microphone" />
</RelativeLayout>
</RelativeLayout>