From 5ae201b81b6ab0e7c553a0cce1b52f08f49538dd Mon Sep 17 00:00:00 2001 From: Niels Andriesse Date: Wed, 16 Jun 2021 14:50:41 +1000 Subject: [PATCH] Add basic voice message recording UI --- .../conversation/v2/ConversationActivityV2.kt | 5 + .../conversation/v2/input_bar/InputBar.kt | 34 +++--- .../v2/input_bar/InputBarButton.kt | 51 +++++++-- .../v2/input_bar/InputBarRecordingView.kt | 18 ++++ .../v2/messages/VisibleMessageView.kt | 23 ++-- .../res/layout/activity_conversation_v2.xml | 8 ++ .../res/layout/view_input_bar_recording.xml | 100 ++++++++++++++++++ 7 files changed, 198 insertions(+), 41 deletions(-) create mode 100644 app/src/main/java/org/thoughtcrime/securesms/conversation/v2/input_bar/InputBarRecordingView.kt create mode 100644 app/src/main/res/layout/view_input_bar_recording.xml diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationActivityV2.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationActivityV2.kt index 76b513509a..13cad79ae6 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationActivityV2.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationActivityV2.kt @@ -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 diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/input_bar/InputBar.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/input_bar/InputBar.kt index 9593bd888b..00af1e6198 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/input_bar/InputBar.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/input_bar/InputBar.kt @@ -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() } \ No newline at end of file diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/input_bar/InputBarButton.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/input_bar/InputBarButton.kt index a9f4732543..f42273b482 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/input_bar/InputBarButton.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/input_bar/InputBarButton.kt @@ -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() + } + } } \ No newline at end of file diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/input_bar/InputBarRecordingView.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/input_bar/InputBarRecordingView.kt new file mode 100644 index 0000000000..ffcb403ab7 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/input_bar/InputBarRecordingView.kt @@ -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) + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/VisibleMessageView.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/VisibleMessageView.kt index 5d8ef91856..1b26830350 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/VisibleMessageView.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/VisibleMessageView.kt @@ -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() { diff --git a/app/src/main/res/layout/activity_conversation_v2.xml b/app/src/main/res/layout/activity_conversation_v2.xml index bc483643b8..d50a07e101 100644 --- a/app/src/main/res/layout/activity_conversation_v2.xml +++ b/app/src/main/res/layout/activity_conversation_v2.xml @@ -18,4 +18,12 @@ android:layout_height="wrap_content" android:layout_alignParentBottom="true" /> + + \ No newline at end of file diff --git a/app/src/main/res/layout/view_input_bar_recording.xml b/app/src/main/res/layout/view_input_bar_recording.xml new file mode 100644 index 0000000000..f0e10cf7c2 --- /dev/null +++ b/app/src/main/res/layout/view_input_bar_recording.xml @@ -0,0 +1,100 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file