mirror of
https://github.com/oxen-io/session-android.git
synced 2024-12-25 09:17:44 +00:00
Add basic voice message recording UI
This commit is contained in:
parent
bf25a44f7b
commit
5ae201b81b
@ -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
|
||||
|
@ -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()
|
||||
}
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
@ -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)
|
||||
}
|
||||
}
|
@ -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() {
|
||||
|
@ -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>
|
100
app/src/main/res/layout/view_input_bar_recording.xml
Normal file
100
app/src/main/res/layout/view_input_bar_recording.xml
Normal 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>
|
Loading…
x
Reference in New Issue
Block a user