/* * Copyright 2019 New Vector Ltd * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package im.vector.riotx.features.home.room.detail import android.annotation.SuppressLint import android.content.Context import android.graphics.Canvas import android.graphics.drawable.Drawable import android.util.TypedValue import android.view.HapticFeedbackConstants 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.RecyclerView import com.airbnb.epoxy.EpoxyModel import com.airbnb.epoxy.EpoxyTouchHelperCallback import com.airbnb.epoxy.EpoxyViewHolder import timber.log.Timber class RoomMessageTouchHelperCallback(private val context: Context, @DrawableRes actionIcon: Int, private val handler: QuickReplayHandler) : EpoxyTouchHelperCallback() { interface QuickReplayHandler { fun performQuickReplyOnHolder(model: EpoxyModel<*>) fun canSwipeModel(model: EpoxyModel<*>): Boolean } private var swipeBack: Boolean = false private var dX = 0f private var startTracking = false private var isVibrate = false private var replyButtonProgress: Float = 0F private var lastReplyButtonAnimationTime: Long = 0 private var imageDrawable: Drawable = ContextCompat.getDrawable(context, actionIcon)!! private val triggerDistance = convertToPx(100) private val minShowDistance = convertToPx(20) private val triggerDelta = convertToPx(20) override fun onSwiped(viewHolder: EpoxyViewHolder?, direction: Int) { } override fun onMove(recyclerView: RecyclerView?, viewHolder: EpoxyViewHolder?, target: EpoxyViewHolder?): Boolean { return false } override fun getMovementFlags(recyclerView: RecyclerView, viewHolder: EpoxyViewHolder): Int { if (handler.canSwipeModel(viewHolder.model)) { return ItemTouchHelper.Callback.makeMovementFlags(0, ItemTouchHelper.START) //Should we use Left? } else { return 0 } } //We never let items completely go out 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: EpoxyViewHolder, dX: Float, dY: Float, actionState: Int, isCurrentlyActive: Boolean) { if (actionState == ACTION_STATE_SWIPE) { setTouchListener(c, recyclerView, viewHolder, dX, dY, actionState, isCurrentlyActive) } val size = triggerDistance if (Math.abs(viewHolder.itemView.translationX) < size || dX > this.dX /*going back*/) { super.onChildDraw(c, recyclerView, viewHolder, dX, dY, actionState, isCurrentlyActive) this.dX = dX startTracking = true } drawReplyButton(c, viewHolder.itemView) } @SuppressLint("ClickableViewAccessibility") private fun setTouchListener(c: Canvas, recyclerView: RecyclerView, viewHolder: EpoxyViewHolder, dX: Float, dY: Float, actionState: Int, isCurrentlyActive: Boolean) { //TODO can this interfer with other interactions? should i remove it recyclerView.setOnTouchListener { v, event -> swipeBack = event.action == MotionEvent.ACTION_CANCEL || event.action == MotionEvent.ACTION_UP if (swipeBack) { if (Math.abs(dX) >= triggerDistance) { try { viewHolder.model?.let { handler.performQuickReplyOnHolder(it) } } catch (e: IllegalStateException) { Timber.e(e) } } } false } } private fun drawReplyButton(canvas: Canvas, itemView: View) { //Timber.v("drawReplyButton") val translationX = Math.abs(itemView.translationX) val newTime = System.currentTimeMillis() val dt = Math.min(17, newTime - lastReplyButtonAnimationTime) lastReplyButtonAnimationTime = newTime val showing = translationX >= minShowDistance if (showing) { if (replyButtonProgress < 1.0f) { replyButtonProgress += dt / 180.0f if (replyButtonProgress > 1.0f) { replyButtonProgress = 1.0f } else { itemView.invalidate() } } } else if (translationX <= 0.0f) { replyButtonProgress = 0f startTracking = false isVibrate = false } else { if (replyButtonProgress > 0.0f) { replyButtonProgress -= dt / 180.0f if (replyButtonProgress < 0.1f) { replyButtonProgress = 0f } else { itemView.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 = Math.min(255f, 255 * (replyButtonProgress / 0.8f)).toInt() } else { scale = replyButtonProgress alpha = Math.min(255f, 255 * replyButtonProgress).toInt() } imageDrawable.alpha = alpha if (startTracking) { if (!isVibrate && translationX >= triggerDistance) { itemView.performHapticFeedback( HapticFeedbackConstants.LONG_PRESS // , HapticFeedbackConstants.FLAG_IGNORE_GLOBAL_SETTING ) isVibrate = true } } val x: Int = itemView.width - if (translationX > triggerDistance + triggerDelta) { (convertToPx(130) / 2).toInt() } else { (translationX / 2).toInt() } val y = (itemView.top + itemView.measuredHeight / 2).toFloat() val hw = imageDrawable.intrinsicWidth / 2f val hh = imageDrawable.intrinsicHeight / 2f imageDrawable.setBounds( (x - hw * scale).toInt(), (y - hh * scale).toInt(), (x + hw * scale).toInt(), (y + hh * scale).toInt() ) imageDrawable.draw(canvas) imageDrawable.alpha = 255 } private fun convertToPx(dp: Int): Float { return TypedValue.applyDimension( TypedValue.COMPLEX_UNIT_DIP, dp.toFloat(), context.resources.displayMetrics ) } }