mirror of
https://github.com/oxen-io/session-android.git
synced 2025-02-20 09:28:26 +00:00
Implement better swipe to reply gesture
This commit is contained in:
parent
fed95ce784
commit
834ac1106b
@ -84,9 +84,6 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity() {
|
|||||||
adapter.changeCursor(null)
|
adapter.changeCursor(null)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
val touchHelperCallback = ConversationTouchHelperCallback(adapter, this) { reply(it) }
|
|
||||||
val touchHelper = ItemTouchHelper(touchHelperCallback)
|
|
||||||
touchHelper.attachToRecyclerView(conversationRecyclerView)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun setUpToolbar() {
|
private fun setUpToolbar() {
|
||||||
|
@ -73,11 +73,6 @@ class ConversationAdapter(context: Context, cursor: Cursor, private val onItemPr
|
|||||||
view.messageTimestampTextView.isVisible = isSelected
|
view.messageTimestampTextView.isVisible = isSelected
|
||||||
val position = viewHolder.adapterPosition
|
val position = viewHolder.adapterPosition
|
||||||
view.bind(message, getMessageBefore(position, cursor), getMessageAfter(position, cursor))
|
view.bind(message, getMessageBefore(position, cursor), getMessageAfter(position, cursor))
|
||||||
view.setOnClickListener { onItemPress(message, viewHolder.adapterPosition) }
|
|
||||||
view.setOnLongClickListener {
|
|
||||||
onItemLongPress(message, viewHolder.adapterPosition)
|
|
||||||
true
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
is ControlMessageViewHolder -> viewHolder.view.bind(message)
|
is ControlMessageViewHolder -> viewHolder.view.bind(message)
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,40 @@
|
|||||||
|
package org.thoughtcrime.securesms.conversation.v2
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.util.AttributeSet
|
||||||
|
import android.view.MotionEvent
|
||||||
|
import android.view.VelocityTracker
|
||||||
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
|
import kotlin.math.abs
|
||||||
|
|
||||||
|
class ConversationRecyclerView : RecyclerView {
|
||||||
|
private var velocityTracker: VelocityTracker? = null
|
||||||
|
|
||||||
|
constructor(context: Context) : super(context)
|
||||||
|
constructor(context: Context, attrs: AttributeSet) : super(context, attrs)
|
||||||
|
constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(context, attrs, defStyleAttr)
|
||||||
|
|
||||||
|
override fun onInterceptTouchEvent(e: MotionEvent): Boolean {
|
||||||
|
return false
|
||||||
|
/*
|
||||||
|
val velocityTracker = velocityTracker ?: return super.onInterceptTouchEvent(e)
|
||||||
|
velocityTracker.computeCurrentVelocity(1000) // Specifying 1000 gives pixels per second
|
||||||
|
val vx = velocityTracker.xVelocity
|
||||||
|
val vy = velocityTracker.yVelocity
|
||||||
|
// Only allow swipes to the left; allowing swipes to the right interferes with some back gestures
|
||||||
|
if (vx > 0) { return super.onInterceptTouchEvent(e) }
|
||||||
|
// Return false if abs(v.x) > abs(v.y) so that only swipes that are more horizontal than vertical
|
||||||
|
// get passed on to the message view
|
||||||
|
return abs(vx) < abs(vy)
|
||||||
|
*/
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onTouchEvent(e: MotionEvent): Boolean {
|
||||||
|
when (e.action) {
|
||||||
|
MotionEvent.ACTION_DOWN -> velocityTracker = VelocityTracker.obtain()
|
||||||
|
MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL -> velocityTracker = null
|
||||||
|
}
|
||||||
|
velocityTracker?.addMovement(e)
|
||||||
|
return super.onTouchEvent(e)
|
||||||
|
}
|
||||||
|
}
|
@ -1,72 +0,0 @@
|
|||||||
package org.thoughtcrime.securesms.conversation.v2
|
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import android.graphics.Canvas
|
|
||||||
import android.graphics.Rect
|
|
||||||
import android.view.HapticFeedbackConstants
|
|
||||||
import androidx.core.content.ContextCompat
|
|
||||||
import androidx.recyclerview.widget.ItemTouchHelper
|
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
|
||||||
import kotlinx.android.synthetic.main.view_visible_message.view.*
|
|
||||||
import network.loki.messenger.R
|
|
||||||
import org.thoughtcrime.securesms.conversation.v2.messages.VisibleMessageView
|
|
||||||
import org.thoughtcrime.securesms.loki.utilities.toDp
|
|
||||||
import org.thoughtcrime.securesms.loki.utilities.toPx
|
|
||||||
import kotlin.math.abs
|
|
||||||
import kotlin.math.min
|
|
||||||
import kotlin.math.roundToInt
|
|
||||||
|
|
||||||
class ConversationTouchHelperCallback(private val adapter: ConversationAdapter, private val context: Context,
|
|
||||||
private val onSwipe: (Int) -> Unit) : ItemTouchHelper.SimpleCallback(0, ItemTouchHelper.LEFT) {
|
|
||||||
private val background = ContextCompat.getDrawable(context, R.drawable.ic_baseline_reply_24)!!
|
|
||||||
private var previousX: Float = 0.0f
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
const val swipeToReplyThreshold = 200.0f // dp
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onMove(recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder, target: RecyclerView.ViewHolder): Boolean {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) {
|
|
||||||
background.alpha = 0
|
|
||||||
adapter.notifyItemChanged(viewHolder.adapterPosition)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onChildDraw(c: Canvas, recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder,
|
|
||||||
dX: Float, dY: Float, actionState: Int, isCurrentlyActive: Boolean) {
|
|
||||||
val adjustedDistanceInPx = dX / 4
|
|
||||||
super.onChildDraw(c, recyclerView, viewHolder, adjustedDistanceInPx, dY, actionState, isCurrentlyActive)
|
|
||||||
val absDistanceInDp = abs(toDp(dX, context.resources))
|
|
||||||
val threshold = ConversationTouchHelperCallback.swipeToReplyThreshold
|
|
||||||
val view = viewHolder.itemView
|
|
||||||
if (view !is VisibleMessageView) { return }
|
|
||||||
// Draw the background
|
|
||||||
val messageContentView = view.messageContentView
|
|
||||||
if (dX < 0) { // Swipe to the left
|
|
||||||
val alpha = min(absDistanceInDp, threshold) / threshold
|
|
||||||
background.alpha = (alpha * 255.0f).roundToInt()
|
|
||||||
val spacing = context.resources.getDimension(R.dimen.medium_spacing).toInt()
|
|
||||||
val itemViewTop = viewHolder.itemView.top
|
|
||||||
val itemViewBottom = viewHolder.itemView.bottom
|
|
||||||
val height = itemViewBottom - itemViewTop
|
|
||||||
val iconSize = toPx(24, context.resources)
|
|
||||||
val offset = (height - iconSize) / 2
|
|
||||||
background.bounds = Rect(
|
|
||||||
messageContentView.right + adjustedDistanceInPx.toInt() + spacing,
|
|
||||||
itemViewTop + offset,
|
|
||||||
messageContentView.right + adjustedDistanceInPx.toInt() + iconSize + spacing,
|
|
||||||
itemViewTop + offset + iconSize
|
|
||||||
)
|
|
||||||
}
|
|
||||||
background.draw(c)
|
|
||||||
// Perform haptic feedback and invoke onSwipe callback if threshold has been reached
|
|
||||||
if (absDistanceInDp > threshold && previousX < threshold) {
|
|
||||||
view.isHapticFeedbackEnabled = true
|
|
||||||
view.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS)
|
|
||||||
onSwipe(viewHolder.adapterPosition)
|
|
||||||
}
|
|
||||||
previousX = absDistanceInDp
|
|
||||||
}
|
|
||||||
}
|
|
@ -25,6 +25,8 @@ import java.lang.IllegalStateException
|
|||||||
|
|
||||||
class VisibleMessageContentView : LinearLayout {
|
class VisibleMessageContentView : LinearLayout {
|
||||||
|
|
||||||
|
// TODO: Large emojis
|
||||||
|
|
||||||
// region Lifecycle
|
// region Lifecycle
|
||||||
constructor(context: Context) : super(context) {
|
constructor(context: Context) : super(context) {
|
||||||
setUpViewHierarchy()
|
setUpViewHierarchy()
|
||||||
|
@ -1,11 +1,10 @@
|
|||||||
package org.thoughtcrime.securesms.conversation.v2.messages
|
package org.thoughtcrime.securesms.conversation.v2.messages
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
import android.os.Build
|
||||||
import android.util.AttributeSet
|
import android.util.AttributeSet
|
||||||
import android.view.Gravity
|
import android.util.Log
|
||||||
import android.view.LayoutInflater
|
import android.view.*
|
||||||
import android.view.View
|
|
||||||
import android.view.ViewGroup
|
|
||||||
import android.widget.LinearLayout
|
import android.widget.LinearLayout
|
||||||
import androidx.core.view.isVisible
|
import androidx.core.view.isVisible
|
||||||
import kotlinx.android.synthetic.main.view_visible_message.view.*
|
import kotlinx.android.synthetic.main.view_visible_message.view.*
|
||||||
@ -14,28 +13,38 @@ import org.session.libsession.messaging.contacts.Contact.ContactContext
|
|||||||
import org.session.libsession.utilities.ViewUtil
|
import org.session.libsession.utilities.ViewUtil
|
||||||
import org.thoughtcrime.securesms.database.DatabaseFactory
|
import org.thoughtcrime.securesms.database.DatabaseFactory
|
||||||
import org.thoughtcrime.securesms.database.model.MessageRecord
|
import org.thoughtcrime.securesms.database.model.MessageRecord
|
||||||
|
import org.thoughtcrime.securesms.loki.utilities.toDp
|
||||||
import org.thoughtcrime.securesms.util.DateUtils
|
import org.thoughtcrime.securesms.util.DateUtils
|
||||||
import java.util.*
|
import java.util.*
|
||||||
|
import kotlin.math.abs
|
||||||
import kotlin.math.roundToInt
|
import kotlin.math.roundToInt
|
||||||
|
import kotlin.math.sqrt
|
||||||
|
|
||||||
class VisibleMessageView : LinearLayout {
|
class VisibleMessageView : LinearLayout {
|
||||||
|
private var dx = 0.0f
|
||||||
|
private var previousTranslationX = 0.0f
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
const val swipeToReplyThreshold = 100.0f // dp
|
||||||
|
}
|
||||||
|
|
||||||
// region Lifecycle
|
// region Lifecycle
|
||||||
constructor(context: Context) : super(context) {
|
constructor(context: Context) : super(context) {
|
||||||
setUpViewHierarchy()
|
initialize()
|
||||||
}
|
}
|
||||||
|
|
||||||
constructor(context: Context, attrs: AttributeSet) : super(context, attrs) {
|
constructor(context: Context, attrs: AttributeSet) : super(context, attrs) {
|
||||||
setUpViewHierarchy()
|
initialize()
|
||||||
}
|
}
|
||||||
|
|
||||||
constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(context, attrs, defStyleAttr) {
|
constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(context, attrs, defStyleAttr) {
|
||||||
setUpViewHierarchy()
|
initialize()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun setUpViewHierarchy() {
|
private fun initialize() {
|
||||||
LayoutInflater.from(context).inflate(R.layout.view_visible_message, this)
|
LayoutInflater.from(context).inflate(R.layout.view_visible_message, this)
|
||||||
layoutParams = ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT)
|
layoutParams = ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT)
|
||||||
|
isHapticFeedbackEnabled = true
|
||||||
}
|
}
|
||||||
// endregion
|
// endregion
|
||||||
|
|
||||||
@ -122,4 +131,46 @@ class VisibleMessageView : LinearLayout {
|
|||||||
messageContentView.recycle()
|
messageContentView.recycle()
|
||||||
}
|
}
|
||||||
// endregion
|
// endregion
|
||||||
|
|
||||||
|
// region Interaction
|
||||||
|
override fun onTouchEvent(event: MotionEvent): Boolean {
|
||||||
|
when (event.action) {
|
||||||
|
MotionEvent.ACTION_DOWN -> onDown(event)
|
||||||
|
MotionEvent.ACTION_MOVE -> onMove(event)
|
||||||
|
MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL -> onFinish(event)
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun onDown(event: MotionEvent) {
|
||||||
|
dx = x - event.rawX
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun onMove(event: MotionEvent) {
|
||||||
|
val translationX = toDp(event.rawX + dx, context.resources)
|
||||||
|
// The idea here is to asymptotically approach a maximum drag distance
|
||||||
|
val damping = 50.0f
|
||||||
|
val sign = -1.0f
|
||||||
|
val x = (damping * (sqrt(abs(translationX)) / sqrt(damping))) * sign
|
||||||
|
this.translationX = x
|
||||||
|
if (abs(x) > VisibleMessageView.swipeToReplyThreshold && abs(previousTranslationX) < VisibleMessageView.swipeToReplyThreshold) {
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||||
|
performHapticFeedback(HapticFeedbackConstants.CONTEXT_CLICK)
|
||||||
|
} else {
|
||||||
|
performHapticFeedback(HapticFeedbackConstants.LONG_PRESS)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
previousTranslationX = x
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun onFinish(event: MotionEvent) {
|
||||||
|
if (abs(translationX) > VisibleMessageView.swipeToReplyThreshold) {
|
||||||
|
Log.d("Test", "Reply")
|
||||||
|
}
|
||||||
|
animate()
|
||||||
|
.translationX(0.0f)
|
||||||
|
.setDuration(150)
|
||||||
|
.start()
|
||||||
|
}
|
||||||
|
// endregion
|
||||||
}
|
}
|
@ -6,7 +6,7 @@
|
|||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
android:orientation="vertical">
|
android:orientation="vertical">
|
||||||
|
|
||||||
<androidx.recyclerview.widget.RecyclerView
|
<org.thoughtcrime.securesms.conversation.v2.ConversationRecyclerView
|
||||||
android:id="@+id/conversationRecyclerView"
|
android:id="@+id/conversationRecyclerView"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent" />
|
android:layout_height="match_parent" />
|
||||||
|
Loading…
x
Reference in New Issue
Block a user