mirror of
https://github.com/nextcloud/talk-android
synced 2025-07-18 18:25:03 +01:00
Proper swiping magic
Signed-off-by: Mario Danic <mario@lovelyhq.com>
This commit is contained in:
parent
70d4f97320
commit
060c72b244
@ -305,7 +305,6 @@ dependencies {
|
||||
implementation 'com.github.mario:PopupBubble:a365177d96'
|
||||
implementation 'com.amulyakhare:com.amulyakhare.textdrawable:1.0.1'
|
||||
implementation 'eu.medsea.mimeutil:mime-util:2.1.3'
|
||||
implementation 'com.github.izjumovfs:SwipeToReply:1.0.1'
|
||||
|
||||
implementation 'com.afollestad.material-dialogs:core:3.1.0'
|
||||
implementation 'com.afollestad.material-dialogs:datetime:3.1.0'
|
||||
|
@ -47,6 +47,7 @@ import com.nextcloud.talk.R
|
||||
import com.nextcloud.talk.activities.MainActivity
|
||||
import com.nextcloud.talk.controllers.SwitchAccountController
|
||||
import com.nextcloud.talk.controllers.base.providers.ActionBarProvider
|
||||
import com.nextcloud.talk.newarch.utils.dp
|
||||
import com.nextcloud.talk.newarch.utils.px
|
||||
import com.nextcloud.talk.utils.preferences.AppPreferences
|
||||
import com.uber.autodispose.lifecycle.LifecycleScopeProvider
|
||||
@ -183,7 +184,7 @@ abstract class BaseController : ButterKnifeController(), ComponentCallbacks {
|
||||
|
||||
if (getAppBarLayoutType() != AppBarLayoutType.EMPTY) {
|
||||
it.toolbar.isVisible = !value
|
||||
appBarLayoutParams.height = 56.px
|
||||
appBarLayoutParams.height = 56.dp
|
||||
} else {
|
||||
appBarLayoutParams.height = 0
|
||||
}
|
||||
|
@ -54,7 +54,6 @@ import com.bluelinelabs.conductor.archlifecycle.ControllerLifecycleOwner
|
||||
import com.bluelinelabs.conductor.autodispose.ControllerScopeProvider
|
||||
import com.bluelinelabs.conductor.changehandler.HorizontalChangeHandler
|
||||
import com.bluelinelabs.conductor.changehandler.VerticalChangeHandler
|
||||
import com.capybaralabs.swipetoreply.ISwipeControllerActions
|
||||
import com.nextcloud.talk.R
|
||||
import com.nextcloud.talk.activities.MagicCallActivity
|
||||
import com.nextcloud.talk.callbacks.MentionAutocompleteCallback
|
||||
@ -69,9 +68,10 @@ import com.nextcloud.talk.newarch.local.models.getMaxMessageLength
|
||||
import com.nextcloud.talk.newarch.local.models.toUserEntity
|
||||
import com.nextcloud.talk.newarch.mvvm.BaseView
|
||||
import com.nextcloud.talk.newarch.mvvm.ext.initRecyclerView
|
||||
import com.nextcloud.talk.newarch.utils.ChatSwipeCallback
|
||||
import com.nextcloud.talk.newarch.utils.swipe.ChatMessageSwipeCallback
|
||||
import com.nextcloud.talk.newarch.utils.Images
|
||||
import com.nextcloud.talk.newarch.utils.NetworkComponents
|
||||
import com.nextcloud.talk.newarch.utils.swipe.ChatMessageSwipeInterface
|
||||
import com.nextcloud.talk.presenters.MentionAutocompletePresenter
|
||||
import com.nextcloud.talk.utils.*
|
||||
import com.nextcloud.talk.utils.AccountUtils.canWeOpenFilesApp
|
||||
@ -249,13 +249,16 @@ class ChatView(private val bundle: Bundle) : BaseView(), ImageLoaderInterface {
|
||||
|
||||
view.cancelReplyButton.setOnClickListener { hideReplyView() }
|
||||
|
||||
val controller = ChatSwipeCallback(messagesAdapter, context, ISwipeControllerActions {
|
||||
val element = messagesAdapter.elementAt(it)
|
||||
if (element != null) {
|
||||
val adapterChatElement = element.element as Element<ChatElement>
|
||||
if (adapterChatElement.data is ChatElement) {
|
||||
val chatElement = adapterChatElement.data as ChatElement
|
||||
view.messagesRecyclerView.postDelayed({ showReplyView(chatElement.data as ChatMessage)}, 125)
|
||||
val controller = ChatMessageSwipeCallback(context, messagesAdapter, object : ChatMessageSwipeInterface {
|
||||
override fun onSwipePerformed(position: Int) {
|
||||
val element = messagesAdapter.elementAt(position)
|
||||
if (element != null) {
|
||||
val adapterChatElement = element.element as Element<ChatElement>
|
||||
if (adapterChatElement.data is ChatElement) {
|
||||
val chatElement = adapterChatElement.data as ChatElement
|
||||
showReplyView(chatElement.data as ChatMessage)
|
||||
//view.messagesRecyclerView.postDelayed({ showReplyView(chatElement.data as ChatMessage)}, 125)
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
@ -49,6 +49,7 @@ import com.nextcloud.talk.newarch.local.models.toUser
|
||||
import com.nextcloud.talk.newarch.mvvm.BaseView
|
||||
import com.nextcloud.talk.newarch.mvvm.ext.initRecyclerView
|
||||
import com.nextcloud.talk.newarch.utils.ElementPayload
|
||||
import com.nextcloud.talk.newarch.utils.dp
|
||||
import com.nextcloud.talk.newarch.utils.px
|
||||
import com.nextcloud.talk.utils.bundle.BundleKeys
|
||||
import com.otaliastudios.elements.Adapter
|
||||
@ -98,8 +99,8 @@ class ContactsView(private val bundle: Bundle? = null) : BaseView() {
|
||||
.addPresenter(Presenter.forLoadingIndicator(activity as Context, R.layout.loading_state))
|
||||
.addPresenter(AdvancedEmptyPresenter(activity as Context, R.layout.message_state, null) { view ->
|
||||
val layoutParams = view.messageStateImageView.layoutParams as RelativeLayout.LayoutParams
|
||||
layoutParams.height = 128.px
|
||||
layoutParams.width = 128.px
|
||||
layoutParams.height = 128.dp
|
||||
layoutParams.width = 128.dp
|
||||
view.messageStateImageView.layoutParams = layoutParams
|
||||
view.messageStateTextView.setText(R.string.nc_search_empty_contacts)
|
||||
view.messageStateImageView.load(context.getDrawable(R.drawable.ic_undraw_not_found_60pq))
|
||||
@ -107,8 +108,8 @@ class ContactsView(private val bundle: Bundle? = null) : BaseView() {
|
||||
})
|
||||
.addPresenter(Presenter.forErrorIndicator(activity as Context, R.layout.message_state) { view, throwable ->
|
||||
val layoutParams = view.messageStateImageView.layoutParams as RelativeLayout.LayoutParams
|
||||
layoutParams.height = 128.px
|
||||
layoutParams.width = 128.px
|
||||
layoutParams.height = 128.dp
|
||||
layoutParams.width = 128.dp
|
||||
view.messageStateImageView.layoutParams = layoutParams
|
||||
view.messageStateTextView.setText(R.string.nc_oops)
|
||||
view.messageStateImageView.load((activity as Context).getDrawable(R.drawable.ic_undraw_server_down_s4lk))
|
||||
|
@ -1,27 +0,0 @@
|
||||
package com.nextcloud.talk.newarch.utils
|
||||
|
||||
import android.content.Context
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.capybaralabs.swipetoreply.ISwipeControllerActions
|
||||
import com.capybaralabs.swipetoreply.SwipeController
|
||||
import com.nextcloud.talk.newarch.features.chat.ChatElement
|
||||
import com.nextcloud.talk.newarch.features.chat.ChatElementTypes
|
||||
import com.otaliastudios.elements.Adapter
|
||||
import com.otaliastudios.elements.Element
|
||||
|
||||
class ChatSwipeCallback(private val adapter: Adapter, context: Context?, swipeControllerActions: ISwipeControllerActions?) : SwipeController(context, swipeControllerActions) {
|
||||
override fun getMovementFlags(recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder): Int {
|
||||
val position: Int = viewHolder.adapterPosition
|
||||
val element = adapter.elementAt(position)
|
||||
if (element != null) {
|
||||
val adapterChatElement = element.element as Element<ChatElement>
|
||||
if (adapterChatElement.data is ChatElement) {
|
||||
val chatElement = adapterChatElement.data as ChatElement
|
||||
if (chatElement.elementType == ChatElementTypes.CHAT_MESSAGE) {
|
||||
return super.getMovementFlags(recyclerView, viewHolder)
|
||||
}
|
||||
}
|
||||
}
|
||||
return 0
|
||||
}
|
||||
}
|
@ -34,8 +34,8 @@ fun String.hashWithAlgorithm(algorithm: String): String {
|
||||
return bytes.fold("", { str, it -> str + "%02x".format(it) })
|
||||
}
|
||||
|
||||
val Int.dp: Int
|
||||
val Int.px: Int
|
||||
get() = (this / Resources.getSystem().displayMetrics.density).toInt()
|
||||
|
||||
val Int.px: Int
|
||||
val Int.dp: Int
|
||||
get() = (this * Resources.getSystem().displayMetrics.density).toInt()
|
@ -0,0 +1,230 @@
|
||||
/*
|
||||
*
|
||||
* * Nextcloud Talk application
|
||||
* *
|
||||
* * @author Mario Danic
|
||||
* * Copyright (C) 2017-2020 Mario Danic <mario@lovelyhq.com>
|
||||
* *
|
||||
* * 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/>.
|
||||
*
|
||||
* Inspired by: https://github.com/izjumovfs/SwipeToReply
|
||||
*/
|
||||
package com.nextcloud.talk.newarch.utils.swipe
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Context
|
||||
import android.graphics.Canvas
|
||||
import android.graphics.PorterDuff
|
||||
import android.graphics.Rect
|
||||
import android.graphics.drawable.Drawable
|
||||
import android.view.HapticFeedbackConstants
|
||||
import android.view.MotionEvent
|
||||
import android.view.View
|
||||
import android.view.View.OnTouchListener
|
||||
import androidx.recyclerview.widget.ItemTouchHelper
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.nextcloud.talk.R
|
||||
import com.nextcloud.talk.newarch.features.chat.ChatElement
|
||||
import com.nextcloud.talk.newarch.features.chat.ChatElementTypes
|
||||
import com.nextcloud.talk.newarch.utils.dp
|
||||
import com.nextcloud.talk.newarch.utils.px
|
||||
import com.otaliastudios.elements.Adapter
|
||||
import com.otaliastudios.elements.Element
|
||||
import kotlin.math.abs
|
||||
import kotlin.math.min
|
||||
|
||||
open class ChatMessageSwipeCallback : ItemTouchHelper.Callback {
|
||||
private var context: Context
|
||||
private var adapter: Adapter
|
||||
private var swiped: Boolean = false
|
||||
private var chatMessageSwipeInterface: ChatMessageSwipeInterface
|
||||
private var replyIcon: Drawable
|
||||
private var replyIconBackground: Drawable
|
||||
private var currentViewHolder: RecyclerView.ViewHolder? = null
|
||||
private var view: View? = null
|
||||
private var dx = 0f
|
||||
private var replyButtonProgress = 0f
|
||||
private var lastReplyButtonAnimationTime: Long = 0
|
||||
private var swipeBack = false
|
||||
private var isVibrating = false
|
||||
private var startTracking = false
|
||||
private var mBackgroundColor = 0x20606060
|
||||
private val replyBackgroundOffset = 18
|
||||
private val replyIconXOffset = 12
|
||||
private val replyIconYOffset = 11
|
||||
|
||||
constructor(context: Context, adapter: Adapter, chatMessageSwipeInterface: ChatMessageSwipeInterface) {
|
||||
this.context = context
|
||||
this.adapter = adapter
|
||||
this.chatMessageSwipeInterface = chatMessageSwipeInterface
|
||||
replyIcon = context.resources.getDrawable(R.drawable.ic_reply_white_24dp)
|
||||
replyIconBackground = context.resources.getDrawable(R.drawable.ic_round_shape)
|
||||
}
|
||||
|
||||
constructor(context: Context, adapter: Adapter, chatMessageSwipeInterface: ChatMessageSwipeInterface, replyIcon: Int, replyIconBackground: Int, backgroundColor: Int) {
|
||||
this.context = context
|
||||
this.adapter = adapter
|
||||
this.chatMessageSwipeInterface = chatMessageSwipeInterface
|
||||
this.replyIcon = context.resources.getDrawable(replyIcon)
|
||||
this.replyIconBackground = context.resources.getDrawable(replyIconBackground)
|
||||
mBackgroundColor = backgroundColor
|
||||
}
|
||||
|
||||
|
||||
override fun getMovementFlags(recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder): Int {
|
||||
val position: Int = viewHolder.adapterPosition
|
||||
val element = adapter.elementAt(position)
|
||||
if (element != null) {
|
||||
val adapterChatElement = element.element as Element<ChatElement>
|
||||
if (adapterChatElement.data is ChatElement) {
|
||||
val chatElement = adapterChatElement.data as ChatElement
|
||||
if (chatElement.elementType == ChatElementTypes.CHAT_MESSAGE) {
|
||||
view = viewHolder.itemView
|
||||
return makeMovementFlags(ItemTouchHelper.ACTION_STATE_IDLE, ItemTouchHelper.RIGHT)
|
||||
}
|
||||
}
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
override fun onMove(recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder, target: RecyclerView.ViewHolder): Boolean {
|
||||
return false
|
||||
}
|
||||
|
||||
override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) {
|
||||
}
|
||||
|
||||
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 == ItemTouchHelper.ACTION_STATE_SWIPE) {
|
||||
setTouchListener(recyclerView, viewHolder)
|
||||
}
|
||||
if (view!!.translationX < convertToDp(130) || dX < dx) {
|
||||
super.onChildDraw(c, recyclerView, viewHolder, dX, dY, actionState, isCurrentlyActive)
|
||||
dx = dX
|
||||
startTracking = true
|
||||
}
|
||||
currentViewHolder = viewHolder
|
||||
drawReplyButton(c)
|
||||
}
|
||||
|
||||
@SuppressLint("ClickableViewAccessibility")
|
||||
private fun setTouchListener(recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder) {
|
||||
recyclerView.setOnTouchListener(OnTouchListener { v, event ->
|
||||
swipeBack = event.action == MotionEvent.ACTION_CANCEL || event.action == MotionEvent.ACTION_UP
|
||||
if (swipeBack) {
|
||||
if (abs(view!!.translationX) >= convertToDp(100)) {
|
||||
swiped = true
|
||||
}
|
||||
}
|
||||
false
|
||||
})
|
||||
}
|
||||
|
||||
private fun convertToDp(pixels: Int): Int {
|
||||
return pixels.dp
|
||||
}
|
||||
|
||||
private fun drawReplyButton(canvas: Canvas) {
|
||||
currentViewHolder?.let { currentViewHolder ->
|
||||
view?.let { view ->
|
||||
val translationX = view.translationX
|
||||
val newTime = System.currentTimeMillis()
|
||||
val dt = min(17, newTime - lastReplyButtonAnimationTime)
|
||||
lastReplyButtonAnimationTime = newTime
|
||||
var showing = false
|
||||
if (translationX >= convertToDp(30)) {
|
||||
showing = true
|
||||
}
|
||||
if (showing) {
|
||||
if (replyButtonProgress < 1.0f) {
|
||||
replyButtonProgress += dt / 180.0f
|
||||
if (replyButtonProgress > 1.0f) {
|
||||
replyButtonProgress = 1.0f
|
||||
} else {
|
||||
view.invalidate()
|
||||
}
|
||||
}
|
||||
} else if (translationX <= 0.0f) {
|
||||
replyButtonProgress = 0f
|
||||
startTracking = false
|
||||
isVibrating = false
|
||||
if (swiped) {
|
||||
chatMessageSwipeInterface.onSwipePerformed(currentViewHolder.adapterPosition)
|
||||
swiped = false
|
||||
}
|
||||
} else {
|
||||
if (replyButtonProgress > 0.0f) {
|
||||
replyButtonProgress -= dt / 180.0f
|
||||
if (replyButtonProgress < 0.1f) {
|
||||
replyButtonProgress = 0f
|
||||
}
|
||||
}
|
||||
view.invalidate()
|
||||
}
|
||||
val alpha: Int
|
||||
val scale: Float
|
||||
if (showing) {
|
||||
scale = if (replyButtonProgress <= 0.8f) {
|
||||
1.2f * (replyButtonProgress / 0.8f)
|
||||
} else {
|
||||
1.2f - 0.2f * ((replyButtonProgress - 0.8f) / 0.2f)
|
||||
}
|
||||
alpha = min(255, 255 * (replyButtonProgress / 0.8f).toInt())
|
||||
} else {
|
||||
scale = replyButtonProgress
|
||||
alpha = min(255, 255 * replyButtonProgress.toInt())
|
||||
}
|
||||
replyIconBackground.alpha = alpha
|
||||
replyIcon.alpha = alpha
|
||||
if (startTracking) {
|
||||
if (!isVibrating && view.translationX >= convertToDp(100)) {
|
||||
view.performHapticFeedback(HapticFeedbackConstants.KEYBOARD_TAP, HapticFeedbackConstants.FLAG_IGNORE_GLOBAL_SETTING)
|
||||
}
|
||||
isVibrating = true
|
||||
}
|
||||
val x: Int = if (view.translationX > convertToDp(130)) {
|
||||
convertToDp(130) / 2
|
||||
} else {
|
||||
view.translationX.toInt() / 2
|
||||
}
|
||||
val y: Float = view.top + view.measuredHeight.toFloat() / 2
|
||||
replyIconBackground.setColorFilter(mBackgroundColor, PorterDuff.Mode.MULTIPLY)
|
||||
replyIconBackground.bounds = Rect(
|
||||
(x - convertToDp(replyBackgroundOffset) * scale).toInt(),
|
||||
(y - convertToDp(replyBackgroundOffset) * scale).toInt(),
|
||||
(x + convertToDp(replyBackgroundOffset) * scale).toInt(),
|
||||
(y + convertToDp(replyBackgroundOffset) * scale).toInt()
|
||||
)
|
||||
replyIconBackground.draw(canvas)
|
||||
replyIcon.bounds = Rect(
|
||||
(x - convertToDp(replyIconXOffset) * scale).toInt(),
|
||||
(y - convertToDp(replyIconYOffset) * scale).toInt(),
|
||||
(x + convertToDp(replyIconXOffset) * scale).toInt(),
|
||||
(y + convertToDp(replyIconYOffset) * scale).toInt()
|
||||
)
|
||||
replyIcon.draw(canvas)
|
||||
replyIconBackground.alpha = 255
|
||||
replyIcon.alpha = 255
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,27 @@
|
||||
/*
|
||||
*
|
||||
* * Nextcloud Talk application
|
||||
* *
|
||||
* * @author Mario Danic
|
||||
* * Copyright (C) 2017-2020 Mario Danic <mario@lovelyhq.com>
|
||||
* *
|
||||
* * 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/>.
|
||||
*
|
||||
* Inspired by: https://github.com/izjumovfs/SwipeToReply
|
||||
*/
|
||||
package com.nextcloud.talk.newarch.utils.swipe
|
||||
|
||||
interface ChatMessageSwipeInterface {
|
||||
fun onSwipePerformed(position: Int)
|
||||
}
|
@ -22,7 +22,7 @@
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
<RelativeLayout android:layout_height="wrap_content" android:layout_width="match_parent">
|
||||
<RelativeLayout android:layout_height="wrap_content" android:layout_width="match_parent" android:animateLayoutChanges="true">
|
||||
<include layout="@layout/item_message_quote"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
|
Loading…
Reference in New Issue
Block a user