From 5f9edb4fccf9b64f7b7cbf341e6f5c751c3d792f Mon Sep 17 00:00:00 2001 From: Sylvain Berfini Date: Wed, 22 Nov 2023 11:34:09 +0100 Subject: [PATCH] Added swipe left to right on a chat bubble to reply --- .../chat/fragment/ConversationFragment.kt | 25 ++++ .../linphone/utils/RecyclerViewSwipeUtils.kt | 107 ++++++++---------- 2 files changed, 72 insertions(+), 60 deletions(-) diff --git a/app/src/main/java/org/linphone/ui/main/chat/fragment/ConversationFragment.kt b/app/src/main/java/org/linphone/ui/main/chat/fragment/ConversationFragment.kt index 33320f2b0..0d646a613 100644 --- a/app/src/main/java/org/linphone/ui/main/chat/fragment/ConversationFragment.kt +++ b/app/src/main/java/org/linphone/ui/main/chat/fragment/ConversationFragment.kt @@ -76,6 +76,8 @@ import org.linphone.utils.AppUtils import org.linphone.utils.Event import org.linphone.utils.FileUtils import org.linphone.utils.LinphoneUtils +import org.linphone.utils.RecyclerViewSwipeUtils +import org.linphone.utils.RecyclerViewSwipeUtilsCallback import org.linphone.utils.addCharacterAtPosition import org.linphone.utils.hideKeyboard import org.linphone.utils.setKeyboardInsetListener @@ -218,6 +220,29 @@ class ConversationFragment : GenericFragment() { binding.eventsList.setHasFixedSize(true) binding.eventsList.layoutManager = LinearLayoutManager(requireContext()) + val callbacks = RecyclerViewSwipeUtilsCallback( + R.drawable.reply, + ConversationEventAdapter.EventViewHolder::class.java + ) { viewHolder -> + val index = viewHolder.bindingAdapterPosition + if (index < 0 || index >= adapter.currentList.size) { + Log.e("$TAG Swipe viewHolder index [$index] is out of bounds!") + } else { + adapter.notifyItemChanged(index) + + val chatMessageEventLog = adapter.currentList[index] + val chatMessageModel = (chatMessageEventLog.model as? ChatMessageModel) + if (chatMessageModel != null) { + sendMessageViewModel.replyToMessage(chatMessageModel) + } else { + Log.e( + "$TAG Can't reply, failed to get a ChatMessageModel from adapter item #[$index]" + ) + } + } + } + RecyclerViewSwipeUtils(callbacks).attachToRecyclerView(binding.eventsList) + val localSipUri = args.localSipUri val remoteSipUri = args.remoteSipUri Log.i( diff --git a/app/src/main/java/org/linphone/utils/RecyclerViewSwipeUtils.kt b/app/src/main/java/org/linphone/utils/RecyclerViewSwipeUtils.kt index e83205053..d4bfd2104 100644 --- a/app/src/main/java/org/linphone/utils/RecyclerViewSwipeUtils.kt +++ b/app/src/main/java/org/linphone/utils/RecyclerViewSwipeUtils.kt @@ -21,41 +21,30 @@ package org.linphone.utils import android.annotation.SuppressLint import android.graphics.Canvas -import android.view.MotionEvent -import android.view.View +import androidx.annotation.DrawableRes +import androidx.core.content.ContextCompat import androidx.recyclerview.widget.ItemTouchHelper import androidx.recyclerview.widget.ItemTouchHelper.ACTION_STATE_SWIPE -import androidx.recyclerview.widget.ItemTouchHelper.LEFT import androidx.recyclerview.widget.ItemTouchHelper.RIGHT import androidx.recyclerview.widget.RecyclerView -class RecyclerViewSwipeUtils(callbacks: RecyclerViewSwipeUtilsCallback) : ItemTouchHelper(callbacks) - -class RecyclerViewSwipeUtilsCallback(val rightButton: View? = null) : ItemTouchHelper.Callback() { - companion object { - private const val TAG = "[RecyclerViewSwipeUtilsCallback]" - } - - private var swipeBack: Boolean = false - private var rightButtonWidth: Int = 0 - - init { - if (rightButton != null) { - val widthSpec = View.MeasureSpec.makeMeasureSpec( - 0, - View.MeasureSpec.UNSPECIFIED - ) - val heightSpec = View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED) - rightButton.measure(widthSpec, heightSpec) - rightButtonWidth = rightButton.measuredWidth - } - } +class RecyclerViewSwipeUtils( + callbacks: RecyclerViewSwipeUtilsCallback +) : ItemTouchHelper(callbacks) +class RecyclerViewSwipeUtilsCallback( + @DrawableRes private val icon: Int, + private val disableActionForViewHolderClass: Class<*>? = null, + private val onSwiped: ((viewHolder: RecyclerView.ViewHolder) -> Unit)? = null +) : ItemTouchHelper.Callback() { override fun getMovementFlags( recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder ): Int { - return makeMovementFlags(0, LEFT or RIGHT) + if (disableActionForViewHolderClass?.isInstance(viewHolder) == true) { + return makeMovementFlags(0, 0) + } + return makeMovementFlags(0, RIGHT) } override fun onMove( @@ -66,7 +55,17 @@ class RecyclerViewSwipeUtilsCallback(val rightButton: View? = null) : ItemTouchH return false } - override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) { + override fun onSwiped( + viewHolder: RecyclerView.ViewHolder, + direction: Int + ) { + if (direction == RIGHT) { + onSwiped?.invoke(viewHolder) + } + } + + override fun getSwipeThreshold(viewHolder: RecyclerView.ViewHolder): Float { + return .2f // Percentage of the screen width the swipe action has to reach to validate swipe move (default is .5f) } @SuppressLint("ClickableViewAccessibility") @@ -80,45 +79,33 @@ class RecyclerViewSwipeUtilsCallback(val rightButton: View? = null) : ItemTouchH isCurrentlyActive: Boolean ) { if (actionState == ACTION_STATE_SWIPE) { - recyclerView.setOnTouchListener { _, event -> - swipeBack = - event.action == MotionEvent.ACTION_CANCEL || event.action == MotionEvent.ACTION_UP + val iconDrawable = ContextCompat.getDrawable(recyclerView.context, icon) + val iconWidth = iconDrawable?.intrinsicWidth ?: 0 + val margin = 20 + if (iconDrawable != null && dX > iconWidth + margin) { + val halfIcon = iconDrawable.intrinsicHeight / 2 + val top = + viewHolder.itemView.top + ((viewHolder.itemView.bottom - viewHolder.itemView.top) / 2 - halfIcon) - val showRightButton = (rightButtonWidth != 0 && dX < -rightButtonWidth) - val position = viewHolder.bindingAdapterPosition - val clickable = !showRightButton || swipeBack - try { - recyclerView.getChildAt(position).isClickable = clickable - } catch (e: IndexOutOfBoundsException) { - } - if (rightButton != null && showRightButton) { - val itemView = viewHolder.itemView - val left = (itemView.right - rightButton.measuredWidth) - val top = itemView.top - - canvas.save() - canvas.translate(left.toFloat(), top.toFloat()) - rightButton.layout( - 0, - 0, - rightButton.width, - rightButton.height - ) - rightButton.draw(canvas) - canvas.restore() + // Icon won't move past the swipe threshold, thus indicating to the user + // it has reached the required distance for swipe action to be done + val threshold = getSwipeThreshold(viewHolder) * viewHolder.itemView.right + val left = if (dX < threshold) { + viewHolder.itemView.left + dX.toInt() - iconWidth - margin + } else { + viewHolder.itemView.left + threshold.toInt() - iconWidth - margin } - false + iconDrawable.setBounds( + left, + top, + left + iconWidth, + top + iconDrawable.intrinsicHeight + ) + iconDrawable.draw(canvas) } } + super.onChildDraw(canvas, recyclerView, viewHolder, dX, dY, actionState, isCurrentlyActive) } - - override fun convertToAbsoluteDirection(flags: Int, layoutDirection: Int): Int { - if (swipeBack) { - swipeBack = false - return 0 - } - return super.convertToAbsoluteDirection(flags, layoutDirection) - } }