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

View File

@ -3,7 +3,6 @@ package org.thoughtcrime.securesms.conversation.v2.input_bar
import android.content.Context
import android.util.AttributeSet
import android.view.LayoutInflater
import android.widget.LinearLayout
import android.widget.RelativeLayout
import androidx.core.view.isVisible
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 kotlin.math.max
class InputBar : LinearLayout, InputBarEditTextDelegate {
class InputBar : RelativeLayout, InputBarEditTextDelegate {
var delegate: InputBarDelegate? = null
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) }
// region Lifecycle
constructor(context: Context) : super(context) {
setUpViewHierarchy()
}
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() }
constructor(context: Context, attrs: AttributeSet) : super(context, attrs) {
setUpViewHierarchy()
}
constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(context, attrs, defStyleAttr) {
setUpViewHierarchy()
}
private fun setUpViewHierarchy() {
private fun initialize() {
LayoutInflater.from(context).inflate(R.layout.view_input_bar, this)
// Attachments button
attachmentsButtonContainer.addView(attachmentsButton)
attachmentsButton.layoutParams = RelativeLayout.LayoutParams(RelativeLayout.LayoutParams.MATCH_PARENT, RelativeLayout.LayoutParams.MATCH_PARENT)
attachmentsButton.setOnClickListener { }
// Microphone button
microphoneOrSendButtonContainer.addView(microphoneButton)
microphoneButton.layoutParams = RelativeLayout.LayoutParams(RelativeLayout.LayoutParams.MATCH_PARENT, RelativeLayout.LayoutParams.MATCH_PARENT)
microphoneButton.setOnClickListener { }
microphoneButton.onLongPress = {
showVoiceMessageUI()
}
// Send button
microphoneOrSendButtonContainer.addView(sendButton)
sendButton.layoutParams = RelativeLayout.LayoutParams(RelativeLayout.LayoutParams.MATCH_PARENT, RelativeLayout.LayoutParams.MATCH_PARENT)
sendButton.setOnClickListener { }
sendButton.isVisible = false
// Edit text
inputBarEditText.imeOptions = inputBarEditText.imeOptions or 16777216 // Always use incognito keyboard
inputBarEditText.delegate = this
}
@ -63,10 +58,15 @@ class InputBar : LinearLayout, InputBarEditTextDelegate {
inputBarLinearLayout.layoutParams = layoutParams
delegate?.inputBarHeightChanged(newHeight)
}
private fun showVoiceMessageUI() {
delegate?.showVoiceMessageUI()
}
// endregion
}
interface InputBarDelegate {
fun inputBarHeightChanged(newValue: Int)
fun showVoiceMessageUI()
}

View File

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

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) {
longPressCallback?.let { gestureHandler.removeCallbacks(it) }
animate()
.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()
resetPosition()
}
private fun onUp(event: MotionEvent) {
@ -251,6 +240,10 @@ class VisibleMessageView : LinearLayout {
longPressCallback?.let { gestureHandler.removeCallbacks(it) }
onPress?.invoke()
}
resetPosition()
}
private fun resetPosition() {
animate()
.translationX(0.0f)
.setDuration(150)
@ -260,9 +253,9 @@ class VisibleMessageView : LinearLayout {
.start()
// Bit of a hack to keep the date break text view from moving
dateBreakTextView.animate()
.translationX(0.0f)
.setDuration(150)
.start()
.translationX(0.0f)
.setDuration(150)
.start()
}
private fun onLongPress() {

View File

@ -18,4 +18,12 @@
android:layout_height="wrap_content"
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>

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>