fix: change the content click to be hit-rect based to determine child object intersection for views with multiple content objects

This commit is contained in:
jubb 2021-06-25 14:43:22 +10:00
parent 21835800ff
commit ce098fe918
6 changed files with 66 additions and 68 deletions

View File

@ -2,11 +2,12 @@ package org.thoughtcrime.securesms.conversation.v2
import android.content.Context import android.content.Context
import android.graphics.Canvas import android.graphics.Canvas
import android.graphics.Rect
import android.util.AttributeSet import android.util.AttributeSet
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.widget.FrameLayout import android.widget.FrameLayout
import androidx.core.view.children
import androidx.core.view.isVisible import androidx.core.view.isVisible
import kotlinx.android.synthetic.main.album_thumbnail_view.view.* import kotlinx.android.synthetic.main.album_thumbnail_view.view.*
import network.loki.messenger.R import network.loki.messenger.R
@ -15,24 +16,33 @@ import org.thoughtcrime.securesms.conversation.v2.utilities.KThumbnailView
import org.thoughtcrime.securesms.database.model.MmsMessageRecord import org.thoughtcrime.securesms.database.model.MmsMessageRecord
import org.thoughtcrime.securesms.mms.GlideRequests import org.thoughtcrime.securesms.mms.GlideRequests
import org.thoughtcrime.securesms.mms.Slide import org.thoughtcrime.securesms.mms.Slide
import org.thoughtcrime.securesms.mms.SlideClickListener
import org.thoughtcrime.securesms.mms.SlidesClickedListener
class AlbumThumbnailView: FrameLayout, SlideClickListener, SlidesClickedListener, View.OnClickListener { class AlbumThumbnailView : FrameLayout {
// region Lifecycle // region Lifecycle
constructor(context: Context) : super(context) { initialize() } constructor(context: Context) : super(context) {
constructor(context: Context, attrs: AttributeSet) : super(context, attrs) { initialize() } initialize()
constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(context, attrs, defStyleAttr) { initialize() } }
constructor(context: Context, attrs: AttributeSet) : super(context, attrs) {
initialize()
}
constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(context, attrs, defStyleAttr) {
initialize()
}
private var slideClickListener: ((Slide) -> Unit)? = null
private var downloadClickListener: ((Slide) -> Unit)? = null
private var readMoreListener: (() -> Unit)? = null
private val cornerMask by lazy { CornerMask(this) } private val cornerMask by lazy { CornerMask(this) }
private var slides: List<Slide> = listOf()
sealed class Hit {
object ReadMoreHit : Hit()
data class SlideHit(val slide: Slide) : Hit()
data class DownloadHit(val slide: Slide) : Hit()
}
private fun initialize() { private fun initialize() {
LayoutInflater.from(context).inflate(R.layout.album_thumbnail_view, this) LayoutInflater.from(context).inflate(R.layout.album_thumbnail_view, this)
albumCellBodyTextReadMore.setOnClickListener(this)
} }
override fun dispatchDraw(canvas: Canvas?) { override fun dispatchDraw(canvas: Canvas?) {
@ -44,36 +54,31 @@ class AlbumThumbnailView: FrameLayout, SlideClickListener, SlidesClickedListener
// region Interaction // region Interaction
override fun onClick(v: View?) { fun calculateHitObject(hitRect: Rect): Hit? {
// clicked the view or one of its children // Z-check in specific order
if (v === albumCellBodyTextReadMore) { val testRect = Rect()
readMoreListener?.invoke() // test "Read More"
albumCellBodyTextReadMore.getHitRect(testRect)
if (Rect.intersects(hitRect, testRect)) {
return Hit.ReadMoreHit
} }
} // test each album child
albumCellContainer.findViewById<ViewGroup>(R.id.album_thumbnail_root)?.children?.forEachIndexed { index, child ->
override fun onClick(v: View?, slide: Slide?) { child.getHitRect(testRect)
// slide thumbnail clicked if (Rect.intersects(hitRect, testRect)) {
if (slide==null) return // hit intersects with this particular child
slideClickListener?.invoke(slide) slides.getOrNull(index)?.let { slide ->
} return Hit.SlideHit(slide)
}
override fun onClick(v: View?, slides: MutableList<Slide>?) { }
// slide download clicked
if (slides.isNullOrEmpty()) return
slides.firstOrNull().let { slide ->
if (slide == null) return@let
downloadClickListener?.invoke(slide)
} }
return null
} }
fun bind(glideRequests: GlideRequests, message: MmsMessageRecord, fun bind(glideRequests: GlideRequests, message: MmsMessageRecord,
clickListener: (Slide)->Unit, downloadClickListener: (Slide)->Unit, readMoreListener: ()->Unit,
isStart: Boolean, isEnd: Boolean) { isStart: Boolean, isEnd: Boolean) {
this.slideClickListener = clickListener
this.downloadClickListener = downloadClickListener
this.readMoreListener = readMoreListener
// TODO: optimize for same size // TODO: optimize for same size
val slides = message.slideDeck.thumbnailSlides slides = message.slideDeck.thumbnailSlides
if (slides.isEmpty()) { if (slides.isEmpty()) {
// this should never be encountered because it's checked by parent // this should never be encountered because it's checked by parent
return return
@ -84,16 +89,15 @@ class AlbumThumbnailView: FrameLayout, SlideClickListener, SlidesClickedListener
// iterate // iterate
slides.take(5).forEachIndexed { position, slide -> slides.take(5).forEachIndexed { position, slide ->
val thumbnailView = getThumbnailView(position) val thumbnailView = getThumbnailView(position)
thumbnailView.thumbnailClickListener = this
thumbnailView.setImageResource(glideRequests, slide, showControls = false, isPreview = false) thumbnailView.setImageResource(glideRequests, slide, showControls = false, isPreview = false)
thumbnailView.setDownloadClickListener(this)
} }
albumCellBodyParent.isVisible = message.body.isNotEmpty() albumCellBodyParent.isVisible = message.body.isNotEmpty()
albumCellBodyText.text = message.body albumCellBodyText.text = message.body
post { post {
// post to await layout of text // post to await layout of text
albumCellBodyText.layout?.let { layout -> albumCellBodyText.layout?.let { layout ->
val maxEllipsis = (0 until layout.lineCount).maxByOrNull { lineNum -> layout.getEllipsisCount(lineNum) } ?: 0 val maxEllipsis = (0 until layout.lineCount).maxByOrNull { lineNum -> layout.getEllipsisCount(lineNum) }
?: 0
// show read more text if at least one line is ellipsized // show read more text if at least one line is ellipsized
albumCellBodyTextReadMore.isVisible = maxEllipsis > 0 albumCellBodyTextReadMore.isVisible = maxEllipsis > 0
} }

View File

@ -76,8 +76,8 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
val adapter = ConversationAdapter( val adapter = ConversationAdapter(
this, this,
cursor, cursor,
onItemPress = { message, position, view -> onItemPress = { message, position, view, rect ->
handlePress(message, position, view) handlePress(message, position, view, rect)
}, },
onItemSwipeToReply = { message, position -> onItemSwipeToReply = { message, position ->
handleSwipeToReply(message, position) handleSwipeToReply(message, position)
@ -452,7 +452,7 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
} }
// `position` is the adapter position; not the visual position // `position` is the adapter position; not the visual position
private fun handlePress(message: MessageRecord, position: Int, view: VisibleMessageView) { private fun handlePress(message: MessageRecord, position: Int, view: VisibleMessageView, hitRect: Rect) {
val actionMode = this.actionMode val actionMode = this.actionMode
if (actionMode != null) { if (actionMode != null) {
adapter.toggleSelection(message, position) adapter.toggleSelection(message, position)
@ -467,7 +467,7 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
// We have to use onContentClick (rather than a click listener directly on // We have to use onContentClick (rather than a click listener directly on
// the view) so as to not interfere with all the other gestures. Do not add // the view) so as to not interfere with all the other gestures. Do not add
// onClickListeners directly to message content views. // onClickListeners directly to message content views.
view.onContentClick() view.onContentClick(hitRect)
} }
} }

View File

@ -2,6 +2,7 @@ package org.thoughtcrime.securesms.conversation.v2
import android.content.Context import android.content.Context
import android.database.Cursor import android.database.Cursor
import android.graphics.Rect
import android.view.ViewGroup import android.view.ViewGroup
import androidx.core.view.isVisible import androidx.core.view.isVisible
import androidx.recyclerview.widget.RecyclerView.ViewHolder import androidx.recyclerview.widget.RecyclerView.ViewHolder
@ -13,7 +14,7 @@ import org.thoughtcrime.securesms.database.DatabaseFactory
import org.thoughtcrime.securesms.database.model.MessageRecord import org.thoughtcrime.securesms.database.model.MessageRecord
import org.thoughtcrime.securesms.mms.GlideRequests import org.thoughtcrime.securesms.mms.GlideRequests
class ConversationAdapter(context: Context, cursor: Cursor, private val onItemPress: (MessageRecord, Int, VisibleMessageView) -> Unit, class ConversationAdapter(context: Context, cursor: Cursor, private val onItemPress: (MessageRecord, Int, VisibleMessageView, Rect) -> Unit,
private val onItemSwipeToReply: (MessageRecord, Int) -> Unit, private val onItemLongPress: (MessageRecord, Int) -> Unit, private val onItemSwipeToReply: (MessageRecord, Int) -> Unit, private val onItemLongPress: (MessageRecord, Int) -> Unit,
private val glide: GlideRequests) private val glide: GlideRequests)
: CursorRecyclerViewAdapter<ViewHolder>(context, cursor) { : CursorRecyclerViewAdapter<ViewHolder>(context, cursor) {
@ -68,7 +69,7 @@ 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), glide) view.bind(message, getMessageBefore(position, cursor), getMessageAfter(position, cursor), glide)
view.onPress = { onItemPress(message, viewHolder.adapterPosition, view) } view.onPress = { x, y -> onItemPress(message, viewHolder.adapterPosition, view, Rect(x,y,x,y)) }
view.onSwipeToReply = { onItemSwipeToReply(message, viewHolder.adapterPosition) } view.onSwipeToReply = { onItemSwipeToReply(message, viewHolder.adapterPosition) }
view.onLongPress = { onItemLongPress(message, viewHolder.adapterPosition) } view.onLongPress = { onItemLongPress(message, viewHolder.adapterPosition) }
} }

View File

@ -1,6 +1,7 @@
package org.thoughtcrime.securesms.conversation.v2.messages package org.thoughtcrime.securesms.conversation.v2.messages
import android.content.Context import android.content.Context
import android.graphics.Rect
import android.graphics.drawable.Drawable import android.graphics.drawable.Drawable
import android.text.util.Linkify import android.text.util.Linkify
import android.util.AttributeSet import android.util.AttributeSet
@ -30,7 +31,7 @@ import org.thoughtcrime.securesms.mms.GlideRequests
import kotlin.math.roundToInt import kotlin.math.roundToInt
class VisibleMessageContentView : LinearLayout { class VisibleMessageContentView : LinearLayout {
var onContentClick: (() -> Unit)? = null var onContentClick: ((Rect) -> Unit)? = null
// region Lifecycle // region Lifecycle
constructor(context: Context) : super(context) { initialize() } constructor(context: Context) : super(context) { initialize() }
@ -93,18 +94,18 @@ class VisibleMessageContentView : LinearLayout {
glideRequests = glide, glideRequests = glide,
message = message, message = message,
isStart = isStartOfMessageCluster, isStart = isStartOfMessageCluster,
isEnd = isEndOfMessageCluster, isEnd = isEndOfMessageCluster
clickListener = { slide ->
Log.d("Loki-UI","clicked to display the slide $slide")
},
downloadClickListener = { slide ->
// trigger download of content?
Log.d("Loki-UI","clicked to download the slide $slide")
},
readMoreListener = {
Log.d("Loki-UI", "clicked to read more the message $message")
}
) )
onContentClick = {
when (val hitObject = albumThumbnailView.calculateHitObject(it)) {
is AlbumThumbnailView.Hit.SlideHit -> Log.d("Loki-UI", "clicked display slide ${hitObject.slide}")// open the slide preview
is AlbumThumbnailView.Hit.DownloadHit -> Log.d("Loki-UI", "clicked display download")
AlbumThumbnailView.Hit.ReadMoreHit -> Log.d("Loki-UI", "clicked the read more display")
else -> {
Log.d("Loki-UI", "DIDN'T click anything important")
}
}
}
} else if (message.isOpenGroupInvitation) { } else if (message.isOpenGroupInvitation) {
val openGroupInvitationView = OpenGroupInvitationView(context) val openGroupInvitationView = OpenGroupInvitationView(context)
openGroupInvitationView.bind(message, VisibleMessageContentView.getTextColor(context, message)) openGroupInvitationView.bind(message, VisibleMessageContentView.getTextColor(context, message))

View File

@ -50,7 +50,7 @@ class VisibleMessageView : LinearLayout {
private var onDownTimestamp = 0L private var onDownTimestamp = 0L
var snIsSelected = false var snIsSelected = false
set(value) { field = value; handleIsSelectedChanged()} set(value) { field = value; handleIsSelectedChanged()}
var onPress: (() -> Unit)? = null var onPress: ((x: Int, y: Int) -> Unit)? = null
var onSwipeToReply: (() -> Unit)? = null var onSwipeToReply: (() -> Unit)? = null
var onLongPress: (() -> Unit)? = null var onLongPress: (() -> Unit)? = null
@ -272,7 +272,7 @@ class VisibleMessageView : LinearLayout {
onSwipeToReply?.invoke() onSwipeToReply?.invoke()
} else if ((Date().time - onDownTimestamp) < VisibleMessageView.longPressDurationThreshold) { } else if ((Date().time - onDownTimestamp) < VisibleMessageView.longPressDurationThreshold) {
longPressCallback?.let { gestureHandler.removeCallbacks(it) } longPressCallback?.let { gestureHandler.removeCallbacks(it) }
onPress?.invoke() onPress?.invoke(event.x.toInt(), event.y.toInt())
} }
resetPosition() resetPosition()
} }
@ -297,8 +297,8 @@ class VisibleMessageView : LinearLayout {
onLongPress?.invoke() onLongPress?.invoke()
} }
fun onContentClick() { fun onContentClick(hitRect: Rect) {
messageContentView.onContentClick?.invoke() messageContentView.onContentClick?.invoke(hitRect)
} }
// endregion // endregion
} }

View File

@ -27,7 +27,7 @@ import org.thoughtcrime.securesms.components.TransferControlView
import org.thoughtcrime.securesms.mms.* import org.thoughtcrime.securesms.mms.*
import org.thoughtcrime.securesms.mms.DecryptableStreamUriLoader.DecryptableUri import org.thoughtcrime.securesms.mms.DecryptableStreamUriLoader.DecryptableUri
open class KThumbnailView: FrameLayout, View.OnClickListener { open class KThumbnailView: FrameLayout {
companion object { companion object {
private const val WIDTH = 0 private const val WIDTH = 0
@ -66,7 +66,6 @@ open class KThumbnailView: FrameLayout, View.OnClickListener {
typedArray.recycle() typedArray.recycle()
} }
setOnClickListener(this)
} }
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) { override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
@ -89,13 +88,6 @@ open class KThumbnailView: FrameLayout, View.OnClickListener {
// endregion // endregion
// region Interaction // region Interaction
override fun onClick(v: View?) {
if (v === this) {
thumbnailClickListener?.onClick(v, slide)
}
}
fun setImageResource(glide: GlideRequests, slide: Slide, showControls: Boolean, isPreview: Boolean): ListenableFuture<Boolean> { fun setImageResource(glide: GlideRequests, slide: Slide, showControls: Boolean, isPreview: Boolean): ListenableFuture<Boolean> {
return setImageResource(glide, slide, showControls, isPreview, 0, 0) return setImageResource(glide, slide, showControls, isPreview, 0, 0)
} }