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 0c3d9ddee..db271d4d8 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 @@ -84,9 +84,6 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity() { adapter.changeCursor(null) } }) - val touchHelperCallback = ConversationTouchHelperCallback(adapter, this) { reply(it) } - val touchHelper = ItemTouchHelper(touchHelperCallback) - touchHelper.attachToRecyclerView(conversationRecyclerView) } private fun setUpToolbar() { diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationAdapter.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationAdapter.kt index 6c0664cf4..7ddf025c0 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationAdapter.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationAdapter.kt @@ -73,11 +73,6 @@ class ConversationAdapter(context: Context, cursor: Cursor, private val onItemPr view.messageTimestampTextView.isVisible = isSelected val position = viewHolder.adapterPosition 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) } diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationRecyclerView.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationRecyclerView.kt new file mode 100644 index 000000000..4cdf88396 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationRecyclerView.kt @@ -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) + } +} \ No newline at end of file diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationTouchHelperCallback.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationTouchHelperCallback.kt deleted file mode 100644 index be1753603..000000000 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationTouchHelperCallback.kt +++ /dev/null @@ -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 - } -} \ No newline at end of file diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/VisibleMessageContentView.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/VisibleMessageContentView.kt index 1c3f6f17a..ac0fd77b2 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/VisibleMessageContentView.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/VisibleMessageContentView.kt @@ -25,6 +25,8 @@ import java.lang.IllegalStateException class VisibleMessageContentView : LinearLayout { + // TODO: Large emojis + // region Lifecycle constructor(context: Context) : super(context) { setUpViewHierarchy() 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 c56f9a08b..e0582c6dd 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 @@ -1,11 +1,10 @@ package org.thoughtcrime.securesms.conversation.v2.messages import android.content.Context +import android.os.Build import android.util.AttributeSet -import android.view.Gravity -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup +import android.util.Log +import android.view.* import android.widget.LinearLayout import androidx.core.view.isVisible 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.thoughtcrime.securesms.database.DatabaseFactory import org.thoughtcrime.securesms.database.model.MessageRecord +import org.thoughtcrime.securesms.loki.utilities.toDp import org.thoughtcrime.securesms.util.DateUtils import java.util.* +import kotlin.math.abs import kotlin.math.roundToInt +import kotlin.math.sqrt class VisibleMessageView : LinearLayout { + private var dx = 0.0f + private var previousTranslationX = 0.0f + + companion object { + const val swipeToReplyThreshold = 100.0f // dp + } // region Lifecycle constructor(context: Context) : super(context) { - setUpViewHierarchy() + initialize() } constructor(context: Context, attrs: AttributeSet) : super(context, attrs) { - setUpViewHierarchy() + initialize() } 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) layoutParams = ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT) + isHapticFeedbackEnabled = true } // endregion @@ -122,4 +131,46 @@ class VisibleMessageView : LinearLayout { messageContentView.recycle() } // 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 } \ No newline at end of file diff --git a/app/src/main/res/layout/activity_conversation_v2.xml b/app/src/main/res/layout/activity_conversation_v2.xml index 13dc9d7e4..e54dcf482 100644 --- a/app/src/main/res/layout/activity_conversation_v2.xml +++ b/app/src/main/res/layout/activity_conversation_v2.xml @@ -6,7 +6,7 @@ android:layout_height="match_parent" android:orientation="vertical"> -