Proper swiping magic

Signed-off-by: Mario Danic <mario@lovelyhq.com>
This commit is contained in:
Mario Danic 2020-04-09 10:14:27 +02:00
parent 70d4f97320
commit 060c72b244
No known key found for this signature in database
GPG Key ID: CDE0BBD2738C4CC0
9 changed files with 279 additions and 45 deletions

View File

@ -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'

View File

@ -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
}

View File

@ -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)
}
}
}
})

View File

@ -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))

View File

@ -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
}
}

View File

@ -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()

View File

@ -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
}
}
}
}

View File

@ -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)
}

View File

@ -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"