长按回收器查看项目动画

Recycler view item animation on long press

我需要长按recyclerview中的圆角矩形裁剪所有项目,并在每个项目视图的两个极端显示两个视图到select并重新排列项目。如何做到这一点? 我正在使用 Paging 3 库和由 roomdb 支持的 RemoteMediator 来显示项目。

长按:

  1. 动画将所有项目翻译到左边 ->
  2. 动画形状蒙版(不缩放项目但应用圆角矩形蒙版(或剪辑)以显示更少)并应用渐变叠加以显示编辑模式
  3. 每个项目视图两侧的两个视图的动画外观(显示)

最后,我创建了独立的 Animator 对象并使用 AnimatorSet 一起播放它们。

RecyclerViewItemAnimatorSet class 获取所有视图持有者并应用在其构造函数中收到的动画

import android.animation.Animator
import android.animation.AnimatorSet
import androidx.core.animation.doOnEnd
import androidx.recyclerview.widget.RecyclerView
import timber.log.Timber
import java.util.concurrent.atomic.AtomicBoolean

/**
 * @author sundara.santhanam
 * @since 09-09-2021
 */
class RecyclerViewItemAnimatorSet<in VH : RecyclerView.ViewHolder>(
    private val recyclerView: RecyclerView,
    private val animatorProviders: List<AnimatorProvider<VH>>,
    private val duration: Long,
    private val afterAnim: (SelectionModeAnimationState) -> Unit
) {
    private val forwardPlayInProgress = AtomicBoolean(false)
    private val reversePlayInProgress = AtomicBoolean(false)


    fun playTogether() {
        if (reversePlayInProgress.get().not()) {
            forwardPlayInProgress.set(true)
            playAnimation({ viewHolder ->
                animatorProviders.map { it.getForwardAnimator(viewHolder as VH) }
            }) {
                forwardPlayInProgress.set(false)
                theEnd(true)
            }
        }
    }

    private fun theEnd(selectionMode: Boolean) {
        var selectionModeAnimationState = SelectionModeAnimationState(selectionMode = selectionMode)
        animatorProviders.forEach {
            selectionModeAnimationState = it.mutateWithFinalValue(selectionModeAnimationState)
        }
        Timber.d("SelectionModeAnimationState: %s", selectionModeAnimationState)
        afterAnim(selectionModeAnimationState)
    }

    fun reversePlayTogether() {
        if (forwardPlayInProgress.get().not()) {
            reversePlayInProgress.set(true)
            playAnimation({ viewHolder -> animatorProviders.map { it.getReverseAnimator(viewHolder as VH) } }) {
                reversePlayInProgress.set(false)
                theEnd(false)
            }
        }
    }

    private fun playAnimation(
        animatorFetcher: (RecyclerView.ViewHolder) -> List<Animator>,
        performAtEnd: () -> Unit
    ) {
        for (index in recyclerView.visibleRange()) {
            recyclerView.findViewHolderForAdapterPosition(index)?.let { viewHolder ->
                val animatorSet = AnimatorSet()
                animatorSet.playTogether(animatorFetcher(viewHolder))
                animatorSet.duration = duration
                animatorSet.start()
                animatorSet.doOnEnd {
                    performAtEnd.invoke()
                }
            }
        }
    }
}

AnimatorProvider 合约:


/**
 * @author sundara.santhanam
 * @since 09-09-2021
 */
abstract class AnimatorProvider<in VH : RecyclerView.ViewHolder> {
    private var prevMode = 0
    fun getForwardAnimator(viewHolder: VH): Animator {
        val mode = 1
        if (prevMode != mode) {
            prevMode = mode
            onModeChange(mode, viewHolder)
        }
        return getAnimator(viewHolder, mode)
    }

    fun getReverseAnimator(viewHolder: VH): Animator {
        val mode = -1
        if (prevMode != mode) {
            prevMode = mode
            onModeChange(mode, viewHolder)
        }
        return getAnimator(viewHolder, mode)
    }

    abstract fun mutateWithFinalValue(selectionModeAnimationState: SelectionModeAnimationState): SelectionModeAnimationState
    val defaultValueAnimator: ValueAnimator = ValueAnimator.ofFloat(0f, 1f)

    // @param mode =1 is forward animation mode and -1 is reverse animation mode
    abstract fun onModeChange(
        mode: Int,
        viewHolder: VH
    )

    abstract fun getAnimator(viewHolder: VH, mode: Int): Animator
}

动画师提供者示例:OutlineProvider 动画师

/**
 * @author sundara.santhanam
 * @since 09-09-2021
 */
class OutLineAnimatorProvider : AnimatorProvider<FavoritesViewDelegate.FavViewHolder>() {
    private var finalRight: Int = -1
    private var initRight: Int = -1
    private var initHeight: Int = -1
    private var diff = 0f

    override fun getAnimator(
        viewHolder: FavoritesViewDelegate.FavViewHolder,
        mode: Int
    ): Animator {
        Timber.d(
            "%s initRight:%d finalRight:%s", (if (mode == 1) {
                "reverse"
            } else {
                "forward"
            }), initRight, finalRight
        )
        defaultValueAnimator.addUpdateListener {
            viewHolder.binding.itemContainer.updateRoundedCornersOutlineProvider(
                (initRight - mode * diff * (it.animatedValue as Float)).toInt(), initHeight
            )
            viewHolder.binding.itemContainer.requestLayout()
        }
        return defaultValueAnimator
    }

    override fun onModeChange(
        mode: Int,
        viewHolder: FavoritesViewDelegate.FavViewHolder
    ) {
        val prevInit = initRight
        initRight = if (finalRight == -1) {
            val bounds = Rect()
            viewHolder.binding.itemContainer.getDrawingRect(bounds)
            initHeight = bounds.height()
            diff = bounds.width() * FINAL_RIGHT_FACTOR
            Timber.d("initRight=%d width=%d diff=%f", bounds.right, bounds.width(), diff)
            bounds.right
        } else {
            finalRight
        }
        finalRight =
            if (prevInit != -1) {
                prevInit
            } else {
                (initRight - mode * diff).toInt()
            }
    }

    override fun mutateWithFinalValue(selectionModeAnimationState: SelectionModeAnimationState) =
        selectionModeAnimationState.copy(
            finalWidth = finalRight,
            initHeight = initHeight
        )

    companion object {
        const val FINAL_RIGHT_FACTOR = 0.2f
    }
}

圆角轮廓提供者:


/**
 * @author sundara.santhanam
 * @since 09-09-2021
 */
data class RoundedCornersOutlineProvider(
    val radius: Float? = null,
    val width: Int? = null,
    val height: Int? = null
) : ViewOutlineProvider() {

    override fun getOutline(view: View, outline: Outline) {
        val left = 0
        val top = 0
        val right = width ?: view.width
        val bottom = height ?: view.height

        if (radius != null) {
            val cornerRadius = radius
            outline.setRoundRect(left, top, right, bottom, cornerRadius)
        }
    }
}

fun View.updateRoundedCornersOutlineProvider(width: Int, height: Int) {

    outlineProvider = try {
        (outlineProvider as RoundedCornersOutlineProvider).copy(
            width = width,
            height = height
        )
    } catch (e: Exception) {
        RoundedCornersOutlineProvider(
            width = width,
            height = height
        )
    }
}

fun View.setRoundedCornerOutlineProvider(radiusDp: Float) {
    Utils.init(context)
    val radius = convertDpToPixel(radiusDp)
    val bounds = Rect()
    getDrawingRect(bounds)
    outlineProvider =
        RoundedCornersOutlineProvider(radius)
    clipToOutline = true
}

fun View.getOutlineRight(): Int {
    val bounds = Rect()
    getDrawingRect(bounds)
    return bounds.right
}

recyclerview 中的用法:


    private fun setupRecyclerView() {
        favoriteViewAnimatorSet = RecyclerViewItemAnimatorSet(
            binding.rvWishList, listOf(
                OutLineAnimatorProvider(),
                TranslationAnimatorProvider(),
                AlphaAnimatorProvider()
            ), 500L
        ) { selectionMode ->
            favoritesViewDelegate.selectionModeAnimationState.set(selectionMode)
            binding.rvWishList.adapter?.run {
                repeat(itemCount) { index ->
                    if (index !in binding.rvWishList.visibleRange()) {
                        notifyItemChanged(index)
                                            }
                }
            }
            binding.rvWishList.enableScroll(recyclerTouchDisabler, recyclerTouchListener)
        }
        binding.rvWishList.adapter =
            FavoritesAdapter(getListOfDelegates())
        recyclerTouchListener = RecyclerTouchListener(
            context, binding.rvWishList,
            clickListener = object : ClickListener {
                override fun onClick(view: View?, position: Int) {
                }

                override fun onLongClick(view: View?, position: Int) {
                    favoritesViewDelegate.selectionModeAnimationState.set(
                        selectionMode().copy(
                            selectionMode = !selectionMode().selectionMode
                        )
                    )
                    if (selectionMode().selectionMode) {
                        recyclerTouchDisabler =
                            binding.rvWishList.disableScrollAndGetDisabler(recyclerTouchListener)
                        favoriteViewAnimatorSet.playTogether()
                    } else {
                        recyclerTouchDisabler =
                            binding.rvWishList.disableScrollAndGetDisabler(recyclerTouchListener)
                        favoriteViewAnimatorSet.reversePlayTogether()
                    }
                }
            })

        binding.rvWishList.addOnItemTouchListener(recyclerTouchListener)
    }