mirror of
https://github.com/nextcloud/talk-android
synced 2025-03-06 14:27:24 +00:00
Merge pull request #1313 from nextcloud/swipeToReply
Swipe-right to reply
This commit is contained in:
commit
3e17bf31ff
@ -58,6 +58,7 @@ import androidx.appcompat.view.ContextThemeWrapper
|
|||||||
import androidx.core.graphics.drawable.RoundedBitmapDrawableFactory
|
import androidx.core.graphics.drawable.RoundedBitmapDrawableFactory
|
||||||
import androidx.emoji.text.EmojiCompat
|
import androidx.emoji.text.EmojiCompat
|
||||||
import androidx.emoji.widget.EmojiTextView
|
import androidx.emoji.widget.EmojiTextView
|
||||||
|
import androidx.recyclerview.widget.ItemTouchHelper
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import androidx.work.Data
|
import androidx.work.Data
|
||||||
@ -107,6 +108,8 @@ import com.nextcloud.talk.models.json.generic.GenericOverall
|
|||||||
import com.nextcloud.talk.models.json.mention.Mention
|
import com.nextcloud.talk.models.json.mention.Mention
|
||||||
import com.nextcloud.talk.presenters.MentionAutocompletePresenter
|
import com.nextcloud.talk.presenters.MentionAutocompletePresenter
|
||||||
import com.nextcloud.talk.ui.dialog.AttachmentDialog
|
import com.nextcloud.talk.ui.dialog.AttachmentDialog
|
||||||
|
import com.nextcloud.talk.ui.recyclerview.MessageSwipeActions
|
||||||
|
import com.nextcloud.talk.ui.recyclerview.MessageSwipeCallback
|
||||||
import com.nextcloud.talk.utils.ApiUtils
|
import com.nextcloud.talk.utils.ApiUtils
|
||||||
import com.nextcloud.talk.utils.ConductorRemapping
|
import com.nextcloud.talk.utils.ConductorRemapping
|
||||||
import com.nextcloud.talk.utils.ConductorRemapping.remapChatController
|
import com.nextcloud.talk.utils.ConductorRemapping.remapChatController
|
||||||
@ -448,6 +451,21 @@ class ChatController(args: Bundle) :
|
|||||||
adapter?.setDateHeadersFormatter { format(it) }
|
adapter?.setDateHeadersFormatter { format(it) }
|
||||||
adapter?.setOnMessageViewLongClickListener { view, message -> onMessageViewLongClick(view, message) }
|
adapter?.setOnMessageViewLongClickListener { view, message -> onMessageViewLongClick(view, message) }
|
||||||
|
|
||||||
|
if (context != null) {
|
||||||
|
val messageSwipeController = MessageSwipeCallback(
|
||||||
|
activity!!,
|
||||||
|
object : MessageSwipeActions {
|
||||||
|
override fun showReplyUI(position: Int) {
|
||||||
|
val chatMessage = adapter?.items?.get(position)?.item as ChatMessage?
|
||||||
|
replyToMessage(chatMessage, chatMessage?.jsonMessageId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
val itemTouchHelper = ItemTouchHelper(messageSwipeController)
|
||||||
|
itemTouchHelper.attachToRecyclerView(binding.messagesListView)
|
||||||
|
}
|
||||||
|
|
||||||
layoutManager = binding.messagesListView.layoutManager as LinearLayoutManager?
|
layoutManager = binding.messagesListView.layoutManager as LinearLayoutManager?
|
||||||
|
|
||||||
binding.popupBubbleView.setRecyclerView(binding.messagesListView)
|
binding.popupBubbleView.setRecyclerView(binding.messagesListView)
|
||||||
@ -1357,10 +1375,8 @@ class ChatController(args: Bundle) :
|
|||||||
if (TextUtils.isEmpty(chatMessageList[i].systemMessage) &&
|
if (TextUtils.isEmpty(chatMessageList[i].systemMessage) &&
|
||||||
TextUtils.isEmpty(chatMessageList[i + 1].systemMessage) &&
|
TextUtils.isEmpty(chatMessageList[i + 1].systemMessage) &&
|
||||||
chatMessageList[i + 1].actorId == chatMessageList[i].actorId &&
|
chatMessageList[i + 1].actorId == chatMessageList[i].actorId &&
|
||||||
countGroupedMessages < 4 && DateFormatter.isSameDay(
|
countGroupedMessages < 4 &&
|
||||||
chatMessageList[i].createdAt,
|
DateFormatter.isSameDay(chatMessageList[i].createdAt, chatMessageList[i + 1].createdAt)
|
||||||
chatMessageList[i + 1].createdAt
|
|
||||||
)
|
|
||||||
) {
|
) {
|
||||||
chatMessageList[i].isGrouped = true
|
chatMessageList[i].isGrouped = true
|
||||||
countGroupedMessages++
|
countGroupedMessages++
|
||||||
@ -1624,58 +1640,7 @@ class ChatController(args: Bundle) :
|
|||||||
}
|
}
|
||||||
R.id.action_reply_to_message -> {
|
R.id.action_reply_to_message -> {
|
||||||
val chatMessage = message as ChatMessage?
|
val chatMessage = message as ChatMessage?
|
||||||
chatMessage?.let {
|
replyToMessage(chatMessage, message?.jsonMessageId)
|
||||||
binding.messageInputView.findViewById<ImageButton>(R.id.attachmentButton)?.visibility =
|
|
||||||
View.GONE
|
|
||||||
binding.messageInputView.findViewById<Space>(R.id.attachmentButtonSpace)?.visibility =
|
|
||||||
View.GONE
|
|
||||||
binding.messageInputView.findViewById<ImageButton>(R.id.cancelReplyButton)?.visibility =
|
|
||||||
View.VISIBLE
|
|
||||||
|
|
||||||
val quotedMessage = binding
|
|
||||||
.messageInputView
|
|
||||||
.findViewById<EmojiTextView>(R.id.quotedMessage)
|
|
||||||
|
|
||||||
quotedMessage?.maxLines = 2
|
|
||||||
quotedMessage?.ellipsize = TextUtils.TruncateAt.END
|
|
||||||
quotedMessage?.text = it.text
|
|
||||||
binding.messageInputView.findViewById<EmojiTextView>(R.id.quotedMessageAuthor)?.text =
|
|
||||||
it.actorDisplayName ?: context!!.getText(R.string.nc_nick_guest)
|
|
||||||
|
|
||||||
conversationUser?.let { currentUser ->
|
|
||||||
val quotedMessageImage = binding
|
|
||||||
.messageInputView
|
|
||||||
.findViewById<ImageView>(R.id.quotedMessageImage)
|
|
||||||
chatMessage.imageUrl?.let { previewImageUrl ->
|
|
||||||
quotedMessageImage?.visibility = View.VISIBLE
|
|
||||||
|
|
||||||
val px = TypedValue.applyDimension(
|
|
||||||
TypedValue.COMPLEX_UNIT_DIP,
|
|
||||||
96f,
|
|
||||||
resources?.displayMetrics
|
|
||||||
)
|
|
||||||
|
|
||||||
quotedMessageImage?.maxHeight = px.toInt()
|
|
||||||
val layoutParams = quotedMessageImage?.layoutParams as FlexboxLayout.LayoutParams
|
|
||||||
layoutParams.flexGrow = 0f
|
|
||||||
quotedMessageImage.layoutParams = layoutParams
|
|
||||||
quotedMessageImage.load(previewImageUrl) {
|
|
||||||
addHeader("Authorization", credentials!!)
|
|
||||||
}
|
|
||||||
} ?: run {
|
|
||||||
binding
|
|
||||||
.messageInputView
|
|
||||||
.findViewById<ImageView>(R.id.quotedMessageImage)
|
|
||||||
?.visibility = View.GONE
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val quotedChatMessageView = binding
|
|
||||||
.messageInputView
|
|
||||||
.findViewById<RelativeLayout>(R.id.quotedChatMessageView)
|
|
||||||
quotedChatMessageView?.tag = message?.jsonMessageId
|
|
||||||
quotedChatMessageView?.visibility = View.VISIBLE
|
|
||||||
}
|
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
R.id.action_reply_privately -> {
|
R.id.action_reply_privately -> {
|
||||||
@ -1820,6 +1785,61 @@ class ChatController(args: Bundle) :
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun replyToMessage(chatMessage: ChatMessage?, jsonMessageId: Int?) {
|
||||||
|
chatMessage?.let {
|
||||||
|
binding.messageInputView.findViewById<ImageButton>(R.id.attachmentButton)?.visibility =
|
||||||
|
View.GONE
|
||||||
|
binding.messageInputView.findViewById<Space>(R.id.attachmentButtonSpace)?.visibility =
|
||||||
|
View.GONE
|
||||||
|
binding.messageInputView.findViewById<ImageButton>(R.id.cancelReplyButton)?.visibility =
|
||||||
|
View.VISIBLE
|
||||||
|
|
||||||
|
val quotedMessage = binding
|
||||||
|
.messageInputView
|
||||||
|
.findViewById<EmojiTextView>(R.id.quotedMessage)
|
||||||
|
|
||||||
|
quotedMessage?.maxLines = 2
|
||||||
|
quotedMessage?.ellipsize = TextUtils.TruncateAt.END
|
||||||
|
quotedMessage?.text = it.text
|
||||||
|
binding.messageInputView.findViewById<EmojiTextView>(R.id.quotedMessageAuthor)?.text =
|
||||||
|
it.actorDisplayName ?: context!!.getText(R.string.nc_nick_guest)
|
||||||
|
|
||||||
|
conversationUser?.let { currentUser ->
|
||||||
|
val quotedMessageImage = binding
|
||||||
|
.messageInputView
|
||||||
|
.findViewById<ImageView>(R.id.quotedMessageImage)
|
||||||
|
chatMessage.imageUrl?.let { previewImageUrl ->
|
||||||
|
quotedMessageImage?.visibility = View.VISIBLE
|
||||||
|
|
||||||
|
val px = TypedValue.applyDimension(
|
||||||
|
TypedValue.COMPLEX_UNIT_DIP,
|
||||||
|
96f,
|
||||||
|
resources?.displayMetrics
|
||||||
|
)
|
||||||
|
|
||||||
|
quotedMessageImage?.maxHeight = px.toInt()
|
||||||
|
val layoutParams = quotedMessageImage?.layoutParams as FlexboxLayout.LayoutParams
|
||||||
|
layoutParams.flexGrow = 0f
|
||||||
|
quotedMessageImage.layoutParams = layoutParams
|
||||||
|
quotedMessageImage.load(previewImageUrl) {
|
||||||
|
addHeader("Authorization", credentials!!)
|
||||||
|
}
|
||||||
|
} ?: run {
|
||||||
|
binding
|
||||||
|
.messageInputView
|
||||||
|
.findViewById<ImageView>(R.id.quotedMessageImage)
|
||||||
|
?.visibility = View.GONE
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val quotedChatMessageView = binding
|
||||||
|
.messageInputView
|
||||||
|
.findViewById<RelativeLayout>(R.id.quotedChatMessageView)
|
||||||
|
quotedChatMessageView?.tag = jsonMessageId
|
||||||
|
quotedChatMessageView?.visibility = View.VISIBLE
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private fun setMessageAsDeleted(message: IMessage?) {
|
private fun setMessageAsDeleted(message: IMessage?) {
|
||||||
val messageTemp = message as ChatMessage
|
val messageTemp = message as ChatMessage
|
||||||
messageTemp.isDeleted = true
|
messageTemp.isDeleted = true
|
||||||
|
@ -0,0 +1,37 @@
|
|||||||
|
/*
|
||||||
|
* Nextcloud Talk application
|
||||||
|
*
|
||||||
|
* @author Shain Singh
|
||||||
|
* @author Andy Scherzinger
|
||||||
|
* Copyright (C) 2021 Shain Singh <shainsingh89@gmail.com>
|
||||||
|
* Copyright (C) 2021 Andy Scherzinger <info@andy-scherzinger.de>
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*
|
||||||
|
* Based on the MessageSwipeController by Shain Singh at:
|
||||||
|
* https://github.com/shainsingh89/SwipeToReply/blob/master/app/src/main/java/com/shain/messenger/SwipeControllerActions.kt
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.nextcloud.talk.ui.recyclerview
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Actions executed within a swipe gesture.
|
||||||
|
*/
|
||||||
|
interface MessageSwipeActions {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Display reply message including the original, quoted message of/at [position].
|
||||||
|
*/
|
||||||
|
fun showReplyUI(position: Int)
|
||||||
|
}
|
@ -0,0 +1,300 @@
|
|||||||
|
/*
|
||||||
|
* Nextcloud Talk application
|
||||||
|
*
|
||||||
|
* @author Shain Singh
|
||||||
|
* @author Andy Scherzinger
|
||||||
|
* Copyright (C) 2021 Shain Singh <shainsingh89@gmail.com>
|
||||||
|
* Copyright (C) 2021 Andy Scherzinger <info@andy-scherzinger.de>
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*
|
||||||
|
* Based on the MessageSwipeController by Shain Singh at:
|
||||||
|
* https://github.com/shainsingh89/SwipeToReply/blob/master/app/src/main/java/com/shain/messenger/MessageSwipeController.kt
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.nextcloud.talk.ui.recyclerview
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint
|
||||||
|
import android.content.Context
|
||||||
|
import android.graphics.Canvas
|
||||||
|
import android.graphics.PorterDuff
|
||||||
|
import android.graphics.PorterDuffColorFilter
|
||||||
|
import android.graphics.drawable.Drawable
|
||||||
|
import android.util.Log
|
||||||
|
import android.view.HapticFeedbackConstants
|
||||||
|
import android.view.MotionEvent
|
||||||
|
import android.view.View
|
||||||
|
import androidx.appcompat.content.res.AppCompatResources
|
||||||
|
import androidx.core.content.ContextCompat
|
||||||
|
import androidx.recyclerview.widget.ItemTouchHelper
|
||||||
|
import androidx.recyclerview.widget.ItemTouchHelper.ACTION_STATE_IDLE
|
||||||
|
import androidx.recyclerview.widget.ItemTouchHelper.ACTION_STATE_SWIPE
|
||||||
|
import androidx.recyclerview.widget.ItemTouchHelper.RIGHT
|
||||||
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
|
import com.nextcloud.talk.R
|
||||||
|
import com.nextcloud.talk.adapters.messages.MagicIncomingTextMessageViewHolder
|
||||||
|
import com.nextcloud.talk.adapters.messages.MagicOutcomingTextMessageViewHolder
|
||||||
|
import com.nextcloud.talk.adapters.messages.MagicPreviewMessageViewHolder
|
||||||
|
import kotlin.math.abs
|
||||||
|
import kotlin.math.ceil
|
||||||
|
import kotlin.math.min
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Callback implementation for swipe-right-gesture on messages.
|
||||||
|
*
|
||||||
|
* @property context activity's context to load resources like drawables.
|
||||||
|
* @property messageSwipeActions the actions to be executed upon swipe-right.
|
||||||
|
* @constructor Creates as swipe-right callback for messages
|
||||||
|
*/
|
||||||
|
class MessageSwipeCallback(private val context: Context, private val messageSwipeActions: MessageSwipeActions) :
|
||||||
|
ItemTouchHelper.Callback() {
|
||||||
|
|
||||||
|
private var density = DENSITY_DEFAULT
|
||||||
|
|
||||||
|
private lateinit var imageDrawable: Drawable
|
||||||
|
private lateinit var shareRound: Drawable
|
||||||
|
|
||||||
|
private var currentItemViewHolder: RecyclerView.ViewHolder? = null
|
||||||
|
private lateinit var view: View
|
||||||
|
private var dX = 0f
|
||||||
|
|
||||||
|
private var replyButtonProgress: Float = NO_PROGRESS
|
||||||
|
private var lastReplyButtonAnimationTime: Long = 0
|
||||||
|
private var swipeBack = false
|
||||||
|
private var isVibrate = false
|
||||||
|
private var startTracking = false
|
||||||
|
|
||||||
|
override fun getMovementFlags(recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder): Int {
|
||||||
|
if (viewHolder is MagicPreviewMessageViewHolder ||
|
||||||
|
viewHolder is MagicIncomingTextMessageViewHolder ||
|
||||||
|
viewHolder is MagicOutcomingTextMessageViewHolder
|
||||||
|
) {
|
||||||
|
view = viewHolder.itemView
|
||||||
|
imageDrawable = AppCompatResources.getDrawable(context, R.drawable.ic_reply)!!
|
||||||
|
shareRound = AppCompatResources.getDrawable(context, R.drawable.round_bgnd)!!
|
||||||
|
return makeMovementFlags(ACTION_STATE_IDLE, RIGHT)
|
||||||
|
}
|
||||||
|
|
||||||
|
// disable swiping any other message type
|
||||||
|
return NO_SWIPE_FLAG
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onMove(
|
||||||
|
recyclerView: RecyclerView,
|
||||||
|
viewHolder: RecyclerView.ViewHolder,
|
||||||
|
target: RecyclerView.ViewHolder
|
||||||
|
): Boolean {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) {
|
||||||
|
// unused atm
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun convertToAbsoluteDirection(flags: Int, layoutDirection: Int): Int {
|
||||||
|
if (swipeBack) {
|
||||||
|
swipeBack = false
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
return super.convertToAbsoluteDirection(flags, layoutDirection)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onChildDraw(
|
||||||
|
c: Canvas,
|
||||||
|
recyclerView: RecyclerView,
|
||||||
|
viewHolder: RecyclerView.ViewHolder,
|
||||||
|
dX: Float,
|
||||||
|
dY: Float,
|
||||||
|
actionState: Int,
|
||||||
|
isCurrentlyActive: Boolean
|
||||||
|
) {
|
||||||
|
|
||||||
|
if (actionState == ACTION_STATE_SWIPE) {
|
||||||
|
setTouchListener(recyclerView, viewHolder)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (view.translationX < convertToDp(SWIPE_LIMIT) || dX < this.dX) {
|
||||||
|
super.onChildDraw(c, recyclerView, viewHolder, dX, dY, actionState, isCurrentlyActive)
|
||||||
|
this.dX = dX
|
||||||
|
startTracking = true
|
||||||
|
}
|
||||||
|
currentItemViewHolder = viewHolder
|
||||||
|
drawReplyButton(c)
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressLint("ClickableViewAccessibility")
|
||||||
|
private fun setTouchListener(recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder) {
|
||||||
|
recyclerView.setOnTouchListener { _, event ->
|
||||||
|
swipeBack = event.action == MotionEvent.ACTION_CANCEL || event.action == MotionEvent.ACTION_UP
|
||||||
|
if (swipeBack) {
|
||||||
|
if (abs(view.translationX) >= this@MessageSwipeCallback.convertToDp(REPLY_POINT)) {
|
||||||
|
messageSwipeActions.showReplyUI(viewHolder.adapterPosition)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun drawReplyButton(canvas: Canvas) {
|
||||||
|
if (currentItemViewHolder == null) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
val translationX = view.translationX
|
||||||
|
val newTime = System.currentTimeMillis()
|
||||||
|
val dt = min(MIN_ANIMATION_TIME_IN_MILLIS, newTime - lastReplyButtonAnimationTime)
|
||||||
|
lastReplyButtonAnimationTime = newTime
|
||||||
|
val showing = translationX >= convertToDp(SHOW_REPLY_ICON_POINT)
|
||||||
|
if (showing) {
|
||||||
|
if (replyButtonProgress < FULL_PROGRESS) {
|
||||||
|
replyButtonProgress += dt / PROGRESS_CALCULATION_TIME_BASE
|
||||||
|
if (replyButtonProgress > FULL_PROGRESS) {
|
||||||
|
replyButtonProgress = FULL_PROGRESS
|
||||||
|
} else {
|
||||||
|
view.invalidate()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (translationX <= NO_PROGRESS) {
|
||||||
|
replyButtonProgress = NO_PROGRESS
|
||||||
|
startTracking = false
|
||||||
|
isVibrate = false
|
||||||
|
} else {
|
||||||
|
if (replyButtonProgress > NO_PROGRESS) {
|
||||||
|
replyButtonProgress -= dt / PROGRESS_CALCULATION_TIME_BASE
|
||||||
|
if (replyButtonProgress < PROGRESS_THRESHOLD) {
|
||||||
|
replyButtonProgress = NO_PROGRESS
|
||||||
|
} else {
|
||||||
|
view.invalidate()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val alpha: Int
|
||||||
|
val scale: Float
|
||||||
|
if (showing) {
|
||||||
|
scale = if (replyButtonProgress <= SCALE_PROGRESS_TOP_THRESHOLD) {
|
||||||
|
SCALE_PROGRESS_MULTIPLIER * (replyButtonProgress / SCALE_PROGRESS_TOP_THRESHOLD)
|
||||||
|
} else {
|
||||||
|
SCALE_PROGRESS_MULTIPLIER -
|
||||||
|
SCALE_PROGRESS_BOTTOM_THRESHOLD *
|
||||||
|
((replyButtonProgress - SCALE_PROGRESS_TOP_THRESHOLD) / SCALE_PROGRESS_BOTTOM_THRESHOLD)
|
||||||
|
}
|
||||||
|
alpha = min(FULLY_OPAQUE, FULLY_OPAQUE * (replyButtonProgress / SCALE_PROGRESS_TOP_THRESHOLD)).toInt()
|
||||||
|
} else {
|
||||||
|
scale = replyButtonProgress
|
||||||
|
alpha = min(FULLY_OPAQUE, FULLY_OPAQUE * replyButtonProgress).toInt()
|
||||||
|
}
|
||||||
|
|
||||||
|
if (startTracking && !isVibrate && view.translationX >= convertToDp(REPLY_POINT)) {
|
||||||
|
view.performHapticFeedback(
|
||||||
|
HapticFeedbackConstants.KEYBOARD_TAP,
|
||||||
|
HapticFeedbackConstants.FLAG_IGNORE_GLOBAL_SETTING
|
||||||
|
)
|
||||||
|
isVibrate = true
|
||||||
|
}
|
||||||
|
|
||||||
|
drawReplyIcon(alpha, scale, canvas)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun drawReplyIcon(alpha: Int, scale: Float, canvas: Canvas) {
|
||||||
|
val x: Int = if (view.translationX > convertToDp(SWIPE_LIMIT)) {
|
||||||
|
convertToDp(SWIPE_LIMIT) / AXIS_BASE
|
||||||
|
} else {
|
||||||
|
(view.translationX / AXIS_BASE).toInt()
|
||||||
|
}
|
||||||
|
|
||||||
|
val y = (view.top + view.measuredHeight / AXIS_BASE).toFloat()
|
||||||
|
|
||||||
|
shareRound.alpha = alpha
|
||||||
|
imageDrawable.alpha = alpha
|
||||||
|
|
||||||
|
shareRound.colorFilter = PorterDuffColorFilter(
|
||||||
|
ContextCompat.getColor(context, R.color.bg_message_list_incoming_bubble),
|
||||||
|
PorterDuff.Mode.SRC_IN
|
||||||
|
)
|
||||||
|
imageDrawable.colorFilter = PorterDuffColorFilter(
|
||||||
|
ContextCompat.getColor(context, R.color.high_emphasis_text),
|
||||||
|
PorterDuff.Mode.SRC_IN
|
||||||
|
)
|
||||||
|
|
||||||
|
shareRound.setBounds(
|
||||||
|
(x - convertToDp(BACKGROUND_BOUNDS_PIXEL) * scale).toInt(),
|
||||||
|
(y - convertToDp(BACKGROUND_BOUNDS_PIXEL) * scale).toInt(),
|
||||||
|
(x + convertToDp(BACKGROUND_BOUNDS_PIXEL) * scale).toInt(),
|
||||||
|
(y + convertToDp(BACKGROUND_BOUNDS_PIXEL) * scale).toInt()
|
||||||
|
)
|
||||||
|
shareRound.draw(canvas)
|
||||||
|
|
||||||
|
imageDrawable.setBounds(
|
||||||
|
(x - convertToDp(ICON_BOUNDS_PIXEL_LEFT) * scale).toInt(),
|
||||||
|
(y - convertToDp(ICON_BOUNDS_PIXEL_TOP) * scale).toInt(),
|
||||||
|
(x + convertToDp(ICON_BOUNDS_PIXEL_RIGHT) * scale).toInt(),
|
||||||
|
(y + convertToDp(ICON_BOUNDS_PIXEL_BOTTOM) * scale).toInt()
|
||||||
|
)
|
||||||
|
imageDrawable.draw(canvas)
|
||||||
|
|
||||||
|
shareRound.alpha = FULLY_OPAQUE_INT
|
||||||
|
imageDrawable.alpha = FULLY_OPAQUE_INT
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun convertToDp(pixel: Int): Int {
|
||||||
|
return dp(pixel.toFloat(), context)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun dp(value: Float, context: Context): Int {
|
||||||
|
if (density == DENSITY_DEFAULT) {
|
||||||
|
checkDisplaySize(context)
|
||||||
|
}
|
||||||
|
return if (value == DENSITY_ZERO) {
|
||||||
|
DENSITY_ZERO_INT
|
||||||
|
} else {
|
||||||
|
ceil((density * value).toDouble()).toInt()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Suppress("Detekt.TooGenericExceptionCaught")
|
||||||
|
private fun checkDisplaySize(context: Context) {
|
||||||
|
try {
|
||||||
|
density = context.resources.displayMetrics.density
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.w(TAG, "Error calculating density", e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
const val TAG = "MessageSwipeCallback"
|
||||||
|
const val NO_SWIPE_FLAG: Int = 0
|
||||||
|
const val FULLY_OPAQUE: Float = 255f
|
||||||
|
const val FULLY_OPAQUE_INT: Int = 255
|
||||||
|
const val DENSITY_DEFAULT: Float = 1f
|
||||||
|
const val DENSITY_ZERO: Float = 0f
|
||||||
|
const val DENSITY_ZERO_INT: Int = 0
|
||||||
|
const val REPLY_POINT: Int = 100
|
||||||
|
const val SWIPE_LIMIT: Int = 130
|
||||||
|
const val SHOW_REPLY_ICON_POINT: Int = 30
|
||||||
|
const val MIN_ANIMATION_TIME_IN_MILLIS: Long = 17
|
||||||
|
const val FULL_PROGRESS: Float = 1.0f
|
||||||
|
const val NO_PROGRESS: Float = 0.0f
|
||||||
|
const val PROGRESS_THRESHOLD: Float = 0.1f
|
||||||
|
const val PROGRESS_CALCULATION_TIME_BASE: Float = 180.0f
|
||||||
|
const val SCALE_PROGRESS_MULTIPLIER: Float = 1.2f
|
||||||
|
const val SCALE_PROGRESS_TOP_THRESHOLD: Float = 0.8f
|
||||||
|
const val SCALE_PROGRESS_BOTTOM_THRESHOLD: Float = 0.2f
|
||||||
|
const val AXIS_BASE: Int = 2
|
||||||
|
const val BACKGROUND_BOUNDS_PIXEL: Int = 18
|
||||||
|
const val ICON_BOUNDS_PIXEL_LEFT: Int = 12
|
||||||
|
const val ICON_BOUNDS_PIXEL_TOP: Int = 13
|
||||||
|
const val ICON_BOUNDS_PIXEL_RIGHT: Int = 12
|
||||||
|
const val ICON_BOUNDS_PIXEL_BOTTOM: Int = 11
|
||||||
|
}
|
||||||
|
}
|
@ -1,5 +1,5 @@
|
|||||||
build:
|
build:
|
||||||
maxIssues: 201
|
maxIssues: 202
|
||||||
weights:
|
weights:
|
||||||
# complexity: 2
|
# complexity: 2
|
||||||
# LongParameterList: 1
|
# LongParameterList: 1
|
||||||
|
Loading…
Reference in New Issue
Block a user