mirror of
https://github.com/oxen-io/session-android.git
synced 2024-12-25 01:07:47 +00:00
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:
parent
21835800ff
commit
ce098fe918
@ -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 ->
|
||||||
|
child.getHitRect(testRect)
|
||||||
|
if (Rect.intersects(hitRect, testRect)) {
|
||||||
|
// hit intersects with this particular child
|
||||||
|
slides.getOrNull(index)?.let { slide ->
|
||||||
|
return Hit.SlideHit(slide)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onClick(v: View?, slide: Slide?) {
|
|
||||||
// slide thumbnail clicked
|
|
||||||
if (slide==null) return
|
|
||||||
slideClickListener?.invoke(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
|
||||||
}
|
}
|
||||||
|
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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) }
|
||||||
}
|
}
|
||||||
|
@ -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))
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
|
@ -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)
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user