Add proper click handling support to ConversationItem V2.
This commit is contained in:
parent
21c70039f4
commit
3738997832
11 changed files with 201 additions and 13 deletions
|
@ -2308,6 +2308,10 @@ public final class ConversationItem extends RelativeLayout implements BindableCo
|
|||
// Intentionally left blank.
|
||||
}
|
||||
|
||||
@Override public @Nullable SnapshotStrategy getSnapshotStrategy() {
|
||||
return null;
|
||||
}
|
||||
|
||||
private class SharedContactEventListener implements SharedContactView.EventListener {
|
||||
@Override
|
||||
public void onAddToContactsClicked(@NonNull Contact contact) {
|
||||
|
|
|
@ -43,6 +43,13 @@ object ConversationItemSelection {
|
|||
drawConversationItem: Boolean,
|
||||
hasReaction: Boolean
|
||||
): Bitmap {
|
||||
val snapshotStrategy = target.getSnapshotStrategy()
|
||||
if (snapshotStrategy != null) {
|
||||
return createBitmap(target.root.width, target.root.height).applyCanvas {
|
||||
snapshotStrategy.snapshot(this)
|
||||
}
|
||||
}
|
||||
|
||||
val bodyBubble = target.bubbleView
|
||||
val reactionsView = target.reactionsView
|
||||
|
||||
|
|
|
@ -2871,7 +2871,7 @@ class ConversationFragment :
|
|||
snapshot,
|
||||
itemView.x,
|
||||
itemView.y + binding.conversationItemRecycler.translationY,
|
||||
bodyBubble.x,
|
||||
if (target.getSnapshotStrategy() != null) itemView.x else bodyBubble.x,
|
||||
bodyBubble.y,
|
||||
bodyBubble.width,
|
||||
audioUri,
|
||||
|
|
|
@ -5,6 +5,7 @@
|
|||
|
||||
package org.thoughtcrime.securesms.conversation.v2.items
|
||||
|
||||
import android.graphics.Canvas
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
|
@ -44,4 +45,10 @@ interface InteractiveConversationElement : ChatColorsDrawable.ChatColorsDrawable
|
|||
fun getSnapshotProjections(coordinateRoot: ViewGroup, clipOutMedia: Boolean): ProjectionList
|
||||
|
||||
fun getSnapshotProjections(coordinateRoot: ViewGroup, clipOutMedia: Boolean, outgoingOnly: Boolean): ProjectionList
|
||||
|
||||
fun getSnapshotStrategy(): SnapshotStrategy?
|
||||
|
||||
interface SnapshotStrategy {
|
||||
fun snapshot(canvas: Canvas)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -7,6 +7,8 @@ package org.thoughtcrime.securesms.conversation.v2.items
|
|||
|
||||
import android.content.Context
|
||||
import android.util.AttributeSet
|
||||
import android.view.MotionEvent
|
||||
import android.view.View
|
||||
import androidx.constraintlayout.widget.ConstraintLayout
|
||||
|
||||
/**
|
||||
|
@ -19,6 +21,25 @@ class V2ConversationItemLayout @JvmOverloads constructor(
|
|||
) : ConstraintLayout(context, attrs) {
|
||||
|
||||
private var onMeasureListeners: Set<OnMeasureListener> = emptySet()
|
||||
var onDispatchTouchEventListener: OnDispatchTouchEventListener? = null
|
||||
|
||||
override fun dispatchTouchEvent(ev: MotionEvent?): Boolean {
|
||||
if (ev != null) {
|
||||
onDispatchTouchEventListener?.onDispatchTouchEvent(this, ev)
|
||||
}
|
||||
|
||||
return super.dispatchTouchEvent(ev)
|
||||
}
|
||||
|
||||
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
|
||||
onMeasureListeners.forEach { it.onPreMeasure() }
|
||||
super.onMeasure(widthMeasureSpec, heightMeasureSpec)
|
||||
|
||||
val remeasure = onMeasureListeners.map { it.onPostMeasure() }.any { it }
|
||||
if (remeasure) {
|
||||
super.onMeasure(widthMeasureSpec, heightMeasureSpec)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the onMeasureListener to be invoked by this view whenever onMeasure is called.
|
||||
|
@ -31,14 +52,8 @@ class V2ConversationItemLayout @JvmOverloads constructor(
|
|||
this.onMeasureListeners -= onMeasureListener
|
||||
}
|
||||
|
||||
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
|
||||
onMeasureListeners.forEach { it.onPreMeasure() }
|
||||
super.onMeasure(widthMeasureSpec, heightMeasureSpec)
|
||||
|
||||
val remeasure = onMeasureListeners.map { it.onPostMeasure() }.any { it }
|
||||
if (remeasure) {
|
||||
super.onMeasure(widthMeasureSpec, heightMeasureSpec)
|
||||
}
|
||||
interface OnDispatchTouchEventListener {
|
||||
fun onDispatchTouchEvent(view: View, motionEvent: MotionEvent)
|
||||
}
|
||||
|
||||
interface OnMeasureListener {
|
||||
|
|
|
@ -82,9 +82,9 @@ class V2ConversationItemTheme(
|
|||
Color.TRANSPARENT
|
||||
} else {
|
||||
if (conversationContext.hasWallpaper()) {
|
||||
ContextCompat.getColor(context, R.color.signal_colorSurface)
|
||||
ContextCompat.getColor(context, R.color.conversation_item_recv_bubble_color_wallpaper)
|
||||
} else {
|
||||
ContextCompat.getColor(context, R.color.signal_colorSurface2)
|
||||
ContextCompat.getColor(context, R.color.conversation_item_recv_bubble_color_normal)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -88,6 +88,7 @@ class V2TextOnlyViewHolder<Model : MappingModel<Model>>(
|
|||
|
||||
private val projections = ProjectionList()
|
||||
private val footerDelegate = V2FooterPositionDelegate(binding)
|
||||
private val dispatchTouchEventListener = V2OnDispatchTouchEventListener(conversationContext, binding)
|
||||
|
||||
override lateinit var conversationMessage: ConversationMessage
|
||||
|
||||
|
@ -115,6 +116,7 @@ class V2TextOnlyViewHolder<Model : MappingModel<Model>>(
|
|||
|
||||
init {
|
||||
binding.root.addOnMeasureListener(footerDelegate)
|
||||
binding.root.onDispatchTouchEventListener = dispatchTouchEventListener
|
||||
|
||||
binding.conversationItemReactions.setOnClickListener {
|
||||
conversationContext.clickListener
|
||||
|
@ -135,7 +137,10 @@ class V2TextOnlyViewHolder<Model : MappingModel<Model>>(
|
|||
true
|
||||
}
|
||||
|
||||
binding.conversationItemBody.isClickable = false
|
||||
val passthroughClickListener = PassthroughClickListener()
|
||||
binding.conversationItemBody.setOnClickListener(passthroughClickListener)
|
||||
binding.conversationItemBody.setOnLongClickListener(passthroughClickListener)
|
||||
|
||||
binding.conversationItemBody.isFocusable = false
|
||||
binding.conversationItemBody.setTextSize(TypedValue.COMPLEX_UNIT_SP, SignalStore.settings().messageFontSize.toFloat())
|
||||
binding.conversationItemBody.movementMethod = LongClickMovementMethod.getInstance(context)
|
||||
|
@ -220,6 +225,10 @@ class V2TextOnlyViewHolder<Model : MappingModel<Model>>(
|
|||
return projections
|
||||
}
|
||||
|
||||
override fun getSnapshotStrategy(): InteractiveConversationElement.SnapshotStrategy {
|
||||
return V2TextOnlySnapshotStrategy(binding)
|
||||
}
|
||||
|
||||
/**
|
||||
* Note: This is not necessary for CFV2 Text-Only items because the background is rendered by
|
||||
* [ChatColorsDrawable]
|
||||
|
@ -603,4 +612,19 @@ class V2TextOnlyViewHolder<Model : MappingModel<Model>>(
|
|||
return binding.conversationItemReactions.setReactions(conversationMessage.messageRecord.reactions, binding.conversationItemBodyWrapper.width)
|
||||
}
|
||||
}
|
||||
|
||||
private inner class PassthroughClickListener : View.OnClickListener, View.OnLongClickListener {
|
||||
override fun onClick(v: View?) {
|
||||
binding.root.performClick()
|
||||
}
|
||||
|
||||
override fun onLongClick(v: View?): Boolean {
|
||||
if (binding.conversationItemBody.hasSelection()) {
|
||||
return false
|
||||
}
|
||||
|
||||
binding.root.performLongClick()
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,75 @@
|
|||
/*
|
||||
* Copyright 2023 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.thoughtcrime.securesms.conversation.v2.items
|
||||
|
||||
import android.view.MotionEvent
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import org.thoughtcrime.securesms.conversation.ConversationItemDisplayMode
|
||||
|
||||
/**
|
||||
* Responsible for the shrink-and-return feel of conversation bubbles when the user
|
||||
* touches them.
|
||||
*/
|
||||
class V2OnDispatchTouchEventListener(
|
||||
private val conversationContext: V2ConversationContext,
|
||||
private val binding: V2ConversationItemTextOnlyBindingBridge
|
||||
) : V2ConversationItemLayout.OnDispatchTouchEventListener {
|
||||
|
||||
companion object {
|
||||
private const val LONG_PRESS_SCALE_FACTOR = 0.95f
|
||||
private const val SHRINK_BUBBLE_DELAY_MILLIS = 100L
|
||||
}
|
||||
|
||||
private val viewsToPivot = listOfNotNull(
|
||||
binding.conversationItemFooterBackground,
|
||||
binding.conversationItemFooterDate,
|
||||
binding.conversationItemFooterExpiry,
|
||||
binding.conversationItemDeliveryStatus,
|
||||
binding.conversationItemReactions
|
||||
)
|
||||
|
||||
private val shrinkBubble = Runnable {
|
||||
binding.conversationItemBodyWrapper.animate()
|
||||
.scaleX(LONG_PRESS_SCALE_FACTOR)
|
||||
.scaleY(LONG_PRESS_SCALE_FACTOR)
|
||||
.setUpdateListener {
|
||||
(binding.root.parent as? ViewGroup)?.invalidate()
|
||||
}
|
||||
|
||||
viewsToPivot.forEach {
|
||||
it.animate()
|
||||
.scaleX(LONG_PRESS_SCALE_FACTOR)
|
||||
.scaleY(LONG_PRESS_SCALE_FACTOR)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDispatchTouchEvent(view: View, motionEvent: MotionEvent) {
|
||||
if (conversationContext.displayMode == ConversationItemDisplayMode.CONDENSED) {
|
||||
return
|
||||
}
|
||||
|
||||
viewsToPivot.forEach {
|
||||
val deltaX = it.x - binding.conversationItemBodyWrapper.x
|
||||
val deltaY = it.y - binding.conversationItemBodyWrapper.y
|
||||
|
||||
it.pivotX = -(deltaX / 2f)
|
||||
it.pivotY = -(deltaY / 2f)
|
||||
}
|
||||
|
||||
when (motionEvent.action) {
|
||||
MotionEvent.ACTION_DOWN -> view.handler.postDelayed(shrinkBubble, SHRINK_BUBBLE_DELAY_MILLIS)
|
||||
MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL -> {
|
||||
view.handler.removeCallbacks(shrinkBubble)
|
||||
(viewsToPivot + binding.conversationItemBodyWrapper).forEach {
|
||||
it.animate()
|
||||
.scaleX(1f)
|
||||
.scaleY(1f)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,52 @@
|
|||
/*
|
||||
* Copyright 2023 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.thoughtcrime.securesms.conversation.v2.items
|
||||
|
||||
import android.graphics.Canvas
|
||||
import androidx.core.view.isVisible
|
||||
import org.thoughtcrime.securesms.util.visible
|
||||
|
||||
/**
|
||||
* Responsible for drawing the conversation bubble when a user long-presses it and the reaction
|
||||
* overlay appears.
|
||||
*/
|
||||
class V2TextOnlySnapshotStrategy(
|
||||
private val binding: V2ConversationItemTextOnlyBindingBridge
|
||||
) : InteractiveConversationElement.SnapshotStrategy {
|
||||
|
||||
private val viewsToRestoreScale = listOfNotNull(
|
||||
binding.conversationItemBodyWrapper,
|
||||
binding.conversationItemFooterBackground,
|
||||
binding.conversationItemFooterDate,
|
||||
binding.conversationItemFooterExpiry,
|
||||
binding.conversationItemDeliveryStatus,
|
||||
binding.conversationItemReactions
|
||||
)
|
||||
|
||||
private val viewsToHide = listOfNotNull(
|
||||
binding.senderPhoto,
|
||||
binding.senderBadge
|
||||
)
|
||||
|
||||
override fun snapshot(canvas: Canvas) {
|
||||
val originalScales = viewsToRestoreScale.associateWith { Pair(it.scaleX, it.scaleY) }
|
||||
viewsToRestoreScale.forEach {
|
||||
it.scaleX = 1f
|
||||
it.scaleY = 1f
|
||||
}
|
||||
|
||||
val originalIsVisible = viewsToHide.associateWith { it.isVisible }
|
||||
viewsToHide.forEach { it.visible = false }
|
||||
|
||||
binding.root.draw(canvas)
|
||||
|
||||
originalIsVisible.forEach { (view, isVisible) -> view.isVisible = isVisible }
|
||||
originalScales.forEach { view, (scaleX, scaleY) ->
|
||||
view.scaleX = scaleX
|
||||
view.scaleY = scaleY
|
||||
}
|
||||
}
|
||||
}
|
|
@ -99,6 +99,8 @@
|
|||
android:textColorLink="@color/conversation_item_sent_text_primary_color"
|
||||
android:textSize="16sp"
|
||||
app:emoji_maxLength="1000"
|
||||
app:emoji_renderMentions="true"
|
||||
app:emoji_renderSpoilers="true"
|
||||
app:measureLastLine="true"
|
||||
app:scaleEmojis="true"
|
||||
tools:text="Testy test test test" />
|
||||
|
|
|
@ -55,6 +55,8 @@
|
|||
android:textColorLink="@color/conversation_item_sent_text_primary_color"
|
||||
android:textSize="16sp"
|
||||
app:emoji_maxLength="1000"
|
||||
app:emoji_renderMentions="true"
|
||||
app:emoji_renderSpoilers="true"
|
||||
app:measureLastLine="true"
|
||||
app:scaleEmojis="true"
|
||||
tools:text="Mango pickle lorem ipsum" />
|
||||
|
@ -149,8 +151,8 @@
|
|||
android:id="@+id/conversation_item_reactions"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="-4dp"
|
||||
android:layout_marginStart="5dp"
|
||||
android:layout_marginTop="-4dp"
|
||||
android:orientation="horizontal"
|
||||
app:layout_constraintStart_toStartOf="@id/conversation_item_body_wrapper"
|
||||
app:layout_constraintTop_toBottomOf="@id/conversation_item_body_wrapper"
|
||||
|
|
Loading…
Add table
Reference in a new issue