小白也能懂的RecyclerView界面动效定制,让ExpandableListView彻底退出舞台!(下)

前言

上篇指路: 小白也能懂的RecyclerView界面动效定制,让ExpandableListView彻底退出舞台!(上)

裁剪原理

ItemAnimator 的本质是对 ViewHolderitemView 做动画,动画过程中使View可见和消失很简单。

但是可见一半该怎么做?

再定制一个ViewHolder?让itemView内部裁切支持此功能?

能否不自定义View,而是在外部通过接口裁剪它呢?可以的------

通过 View.setClipToOutline(true) ,再提供一个 ViewOutlineProvider 即可做到这一点。

View 的 默认 OutlineProvider

可以在view的构造函数中看到,它初始化时,即从资源中读取了自己 outlinProvider 的类型

java 复制代码
public View(Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) {
    this(context);
        // ...
    final int N = a.getIndexCount();
    for (int i = 0; i < N; i++) {
        int attr = a.getIndex(i);
        switch (attr) {
        // ...
         case R.styleable.View_outlineProvider:
            setOutlineProviderFromAttribute(a.getInt(R.styleable.View_outlineProvider,
                    PROVIDER_BACKGROUND));
            break;
        // ...

关键即在这个 setOutlineProviderFromAttribute() 方法了。

PROVIDER_BACKGROUND

可以看到它读取了xml中指定的值,并默认是 PROVIDER_BACKGROUND ------

java 复制代码
// correspond to the enum values of View_outlineProvider
private static final int PROVIDER_BACKGROUND = 0; // 这里
private static final int PROVIDER_NONE = 1;
private static final int PROVIDER_BOUNDS = 2;
private static final int PROVIDER_PADDED_BOUNDS = 3;
private void setOutlineProviderFromAttribute(int providerInt) {
    switch (providerInt) {
        case PROVIDER_BACKGROUND:
            // 看这里
            setOutlineProvider(ViewOutlineProvider.BACKGROUND);
            break;
        case PROVIDER_NONE:
            setOutlineProvider(null);
            break;
        case PROVIDER_BOUNDS:
            setOutlineProvider(ViewOutlineProvider.BOUNDS);
            break;
        case PROVIDER_PADDED_BOUNDS:
            setOutlineProvider(ViewOutlineProvider.PADDED_BOUNDS);
            break;
    }
}

所以默认是从 Background 中生成该 view 的 outlineoutline 生成后,配合 View.setClipToOutline(true) 即可裁剪 view 到对应的范围中。


瞅瞅 ViewOutlineProvider.BACKGROUND 这个静态变量怎么定义的吧------

java 复制代码
/**
 * view 的默认 outlineProvider。
 * 它从 view 的 background 查询 outline。
 * 或者如果 background 不存在,则生成一个 0 alpha 的矩形 outline,
 * 其大小为 view 的大小
**/
public static final ViewOutlineProvider BACKGROUND = new ViewOutlineProvider() {
    @Override
    public void getOutline(View view, Outline outline) {
        Drawable background = view.getBackground();
        if (background != null) {
            background.getOutline(outline);
        } else {
            outline.setRect(0, 0, view.getWidth(), view.getHeight());
            outline.setAlpha(0.0f);
        }
    }
};

很好理解,我们的 itemView 做动画过程中肯定是不能使用这个 outlineProvider 了,但我们通过这里知道了 outlinProvider 是怎么工作的。

之后我们照猫画虎 ,提供一个根据动画进度改变outline边界outlineProvider 即可。


而 View 的位置移动 ,使用 translation 系列 API 即可。

动画基本参数

应该用弹簧去实现的,当前实现直接使用 view 自带的 ValueAnimator 算了,留个优化空间在这。

kotlin 复制代码
private const val CommonDuration: Long = 350L  
private val CommonInterpolator = DecelerateInterpolator()

View 状态恢复与保存

根据上述内容,我们需要在动画过程中临时更改一些view的属性,

并在动画完毕后恢复。

View 状态记录类:ViewState

  • 使用data class是因为它自动实现了equalstoStringcopy 方法,我们稍后会用到copy

所以有 ViewState 类定义如下(我直接定义成文件级私有类了)------

kotlin 复制代码
private data class ViewState(
    var clipToOutline: Boolean,
    var outlineProvider: ViewOutlineProvider?,
    var translationX: Float,
    var translationY: Float,
) {
    fun applyTo(view: View) {
        view.clipToOutline = clipToOutline
        view.outlineProvider = outlineProvider
        view.translationX = translationX
        view.translationY = translationY
    }

    companion object {
        /**
         * 帮助直接从ViewHolder生成
        **/
        fun ViewHolder.genState(
            clipToOutline: Boolean = itemView.clipToOutline,
            outlineProvider: ViewOutlineProvider = itemView.outlineProvider,
            translationX: Float = itemView.translationX,
            translationY: Float = itemView.translationY,
        ): ViewState = ViewState(clipToOutline, outlineProvider, translationX, translationY)
    }
}

View 状态操作类:Op

同样是文件级私有类 ,都放在 ItemAnimator 所在的文件中。

(毕竟逻辑紧密相连且我们只需要做一个ItemAnimator,做多个时再去抽象处理,计划永远赶不上变化,程序永远是越简短越好维护,也越好扩充)

kotlin 复制代码
private data class Op(
    val holder: ViewHolder,
    val oriState: ViewState, // view 的原始状态
    val animStartState: ViewState = oriState.copy(clipToOutline = true),
    val animEndState: ViewState = animStartState.copy(),
) {
    var duration: Long = CommonDuration
    var interpolator: TimeInterpolator = CommonInterpolator

    /**
     * 动画完毕的监听器
     */
    var notifyAnimFinish: () -> Unit = {}

    /**
     * view 进入动画的第0帧状态
     */
    fun prepareView() {
        animStartState.applyTo(holder.itemView)
        // 使得view第0帧的outline能够因此触发一次计算
        holder.itemView.invalidateOutline()
    }
    
    /**
     * view 动画完毕后恢复回原始状态
     */
    fun recoverView() {
        // 取消正在进行的其他动画
        holder.itemView.animate().cancel()
        // 应用 oriState
        oriState.applyTo(holder.itemView)
        notifyAnimFinish()
    }
}

整体流程

1. 梳理运行逻辑

用 xMind 画了下流程------

值得注意的是:

  1. 上述 animateXxx() 方法需要返回 true ,才会引起后面安排执行 runPendingAnimations
  2. SimpleItemAnimatordispatchAnimationFinished(ViewHolder) 方法封装成了对应4种情况的 dispatchXxxStartdispatchXxxFinished 方法,我们不考虑给子类重写的需要,最简化处理就好。
  3. (重要) 在 animateXxx() 方法传入 viewHolder 时,其 itemView 已经准备好了,假设没有动画的话,下一帧整个rv就将显示这个最终状态了。这意味着对于我们想要的动画而言------
    • 对于 animateAdd ,view已经被添加到正常的位置。
      • 我们把 view 立即 设置不可见、且在最终目标上方的位置。
    • 对于 animateRemove ,因为最终状态是移除vie,所以view属性不会发生额外变化。
      • 当前状态无需改变
    • 对于 animateMove ,我们将从"当前位置"移动到目标位置,但"当前位置"已经不存在了,传入的view已经是目标位置。
      • 所以我们需要根据传参得到delta ,把 itemView "重置回上一刻的位置"。

2. 确定 override 范围

根据上述运行逻辑可以得到这么一个类和需要 override 的函数列表。

kotlin 复制代码
/**
 * 用于多级菜单的ItemAnimator
 */
class RecyclerViewMultilevelMenuItemAnimator : SimpleItemAnimator() {

    override fun getRemoveDuration(): Long = CommonDuration
    override fun getAddDuration(): Long = CommonDuration
    override fun getMoveDuration(): Long = CommonDuration
    
    override fun animateAdd(holder: ViewHolder): Boolean
    
    override fun animateRemove(holder: ViewHolder): Boolean

    override fun animateMove(holder: ViewHolder, fromX: Int, fromY: Int, toX: Int, toY: Int): Boolean

    override fun animateChange(oldHolder: ViewHolder, newHolder: ViewHolder, fromLeft: Int, fromTop: Int, toLeft: Int, toTop: Int): Boolean

    override fun runPendingAnimations()

    // 帮助rv可以强制停止我们的动画,需要我们做好处理
    override fun endAnimation(item: ViewHolder)

    // 帮助rv可以强制停止我们的动画,需要我们做好处理
    override fun endAnimations()
    
    // 帮助判断还有没有动画在运行,rv的isAnimating()方法直通这里
    override fun isRunning(): Boolean
}

局势是不是一下子就明朗且简单起来了?!

3. 实现 ItemAnimator

这里有定义一个关键的 AnimSet 及其子类; 以及 animatingOps 内部匿名类的对象,它涉及具体的动画处理。

我们可以先略过他们,优先实现整体逻辑。

kotlin 复制代码
/**
 * 用于多级菜单的ItemAnimator
 */
class RecyclerViewMultilevelMenuItemAnimator : SimpleItemAnimator() {

    override fun getRemoveDuration(): Long = CommonDuration
    override fun getAddDuration(): Long = CommonDuration
    override fun getMoveDuration(): Long = CommonDuration

    private val removeSet = object : AnimSet() 
    private val addSet = object : AnimSet() 
    private val moveSet = object : AnimSet() 
    
    private val animatingOps = object : ArrayList<Op>()

    /**
     * 通知动画结束,是一个扩展属性,会在调用时再生成对应的操作对象
     */
    private val ViewHolder.notifyAnimFinished: () -> Unit
        get() = {
            dispatchAnimationFinished(this)
            // 见其函数注释
            tryDispatchAnimationsDone()
        }

    override fun animateAdd(holder: ViewHolder): Boolean {
        holder.itemView.animate().cancel()
        Op(holder, holder.genState()).apply {
            duration = addDuration
            notifyAnimFinish = holder.notifyAnimFinished
            addSet.add(this)
        }
        return true
    }

    override fun animateRemove(holder: ViewHolder): Boolean {
        holder.itemView.animate().cancel()
        Op(holder, holder.genState()).apply {
            duration = removeDuration
            notifyAnimFinish = holder.notifyAnimFinished
            removeSet.add(this)
        }
        return true
    }

    override fun animateMove(holder: ViewHolder, fromX: Int, fromY: Int, toX: Int, toY: Int): Boolean {
        holder.itemView.animate().cancel()
        val curState = holder.genState()
        // 根据to和from算出delta
        val deltaX = toX - fromX
        val deltaY = toY - fromY

        // 将delta应用到初始状态上
        Op(
            holder,
            oriState = curState,
            animStartState = curState.copy(translationX = curState.translationX - deltaX, translationY = curState.translationY - deltaY),
            animEndState = curState.copy(translationX = curState.translationX, translationY = curState.translationY)
        ).apply {
            duration = moveDuration
            notifyAnimFinish = holder.notifyAnimFinished
            moveSet.add(this)
        }

        return true
    }

    override fun animateChange(oldHolder: ViewHolder, newHolder: ViewHolder, fromLeft: Int, fromTop: Int, toLeft: Int, toTop: Int): Boolean {
        if (oldHolder === newHolder) {
            return animateMove(oldHolder, fromLeft, fromTop, toLeft, toTop)
        }
        // change 在设计中无动画
        oldHolder.itemView.animate().cancel()
        newHolder.itemView.animate().cancel()
        oldHolder.notifyAnimFinished()
        newHolder.notifyAnimFinished()
        return false
    }

    override fun runPendingAnimations() {
        if (!isRunning) {
            return
        }
        removeSet.forEach { op ->
            startAnimationImpl(op)
        }
        removeSet.clear()
        moveSet.forEach { op ->
            startAnimationImpl(op)
        }
        moveSet.clear()
        addSet.forEach { op ->
            startAnimationImpl(op)
        }
        addSet.clear()
    }

    private fun startAnimationImpl(op: Op) {
        animatingOps.add(op)
        op.holder.itemView.animate()
            .setDuration(op.duration)
            .setInterpolator(op.interpolator)
            .translationX(op.animEndState.translationX)
            .translationY(op.animEndState.translationY)
            .setUpdateListener {
                // 动画每一帧都存在可能的outline更新
                op.holder.itemView.invalidateOutline()
            }
            .setListener(object : AnimatorListenerAdapter() {
                /**
                 * 内部function,cancel和end时需要做的操作
                 */
                fun onFinish() {
                    // 通过set方式删除listener
                    op.holder.itemView.animate().apply {
                        setListener(null)
                        setUpdateListener(null)
                    }
                    animatingOps.remove(op)
                }

                override fun onAnimationCancel(animation: Animator) {
                    onFinish()
                }

                override fun onAnimationEnd(animation: Animator) {
                    onFinish()
                }
            })
            .start()
    }

    /**
     * 抄的 defaultItemAnimator 的实现,用来尝试通知动画已经结束
     */
    private fun tryDispatchAnimationsDone() {
        if (!isRunning) {
            dispatchAnimationsFinished()
        }
    }

    override fun endAnimation(item: ViewHolder) {
        animatingOps.remove(item)

        addSet.removeViewHolder(item)
        moveSet.removeViewHolder(item)
        removeSet.removeViewHolder(item)
    }

    override fun endAnimations() {
        animatingOps.clear()

        addSet.clearAndRecover()
        moveSet.clearAndRecover()
        removeSet.clearAndRecover()
    }

    /**
     * 只要有已经加入待动画队列的、或者动画队列中还有内容,就认为动画还在运行
     */
    override fun isRunning(): Boolean {
        return addSet.isNotEmpty() 
        || removeSet.isNotEmpty() 
        || moveSet.isNotEmpty()
        || animatingOps.isNotEmpty()
    }
}

可以看到,这个 animator 的核心计算之外的逻辑就这么写完了!

加上注释都不到170行!

* 4. 实现 AnimSet 和 animatingOps

kotlin 复制代码
/**
 * set中的view按照界面中的上到下排序
 */
private open class AnimSet : TreeSet<Op>(
    Comparator<Op> { o1, o2 ->
        // 按[ViewHolder.itemView]的[View.getTop]从小到大排序
        // 就能使得这些view按照界面中的上到下排序
        o1.holder.itemView.top - o2.holder.itemView.top
    }
) {

    /**
     * 两op对应holder的itemView紧密相接
     */
    protected fun Op.isCloseNeighborOf(op: Op) =
        this.holder.itemView.top == op.holder.itemView.bottom
                || op.holder.itemView.top == this.holder.itemView.bottom

    /**
     * 所有紧密相接的view都应该一同进行位移计算,
     * 所以通过此函数找到和它相距最远且能接起来的view
     * @param toLower 寻找的方向,两个方向都需要找
     */
    protected fun Op.getConnectedButFarthestItem(toLower: Boolean): Op {
        var curOp = this // 有可能找不到,保底是自己
        var tmpOp: Op?
        while (true) {
            // TreeSet的lower(e)和higher(e)会通过比较器寻找符合要求的内容
            // 找不到会返回null
            tmpOp = if (toLower) lower(curOp) else higher(curOp)
            if (tmpOp?.isCloseNeighborOf(curOp) == true) {
                curOp = tmpOp
            } else {
                break
            }
        }
        return curOp
    }

    /**
     * 调整相邻元素
     * @param action 需要进行的调整操作
     */
    protected fun Op.judgeNeighborElements(action: (op: Op, delta: Number, top: Int) -> Unit) {
        // 找到相邻的第一个和最后一个
        val first = getConnectedButFarthestItem(true)
        val last = getConnectedButFarthestItem(false)
        // 确认需要位移的总距离
        val delta = first.holder.itemView.top - last.holder.itemView.bottom
        // 从第一个开始
        var curOp: Op? = first
        var tmpOp: Op?
        // 一直处理到相邻元素的最后一个
        while (curOp != null) {
            // 执行调整操作
            action(curOp, delta, first.holder.itemView.top)
            // 调整完毕后,进入动画第0帧
            curOp.prepareView()
            // 找到相邻的下一个
            tmpOp = higher(curOp)
            if (tmpOp == null || !curOp.isCloseNeighborOf(tmpOp)) {
                break
            }
            curOp = tmpOp
        }
    }

    /**
     * 移除此set中对应viewHolder的操作,并恢复itemView状态
     */
    fun removeViewHolder(viewHolder: ViewHolder) {
        removeIf {
            if (it.holder == viewHolder) {
                it.recoverView()
                return@removeIf true
            }
            return@removeIf false
        }
    }

    /**
     * 清空set并恢复itemView状态
     */
    fun clearAndRecover() {
        forEach {
            it.recoverView()
        }
        clear()
    }

    /**
     * 帮助生成一个[ViewOutlineProvider]
     */
    protected fun getClipOutlineProvider(outlineProvider: (view: View, outline: Outline) -> Unit): ViewOutlineProvider = object : ViewOutlineProvider() {
        override fun getOutline(view: View, outline: Outline) {
            outlineProvider(view, outline)
        }
    }

    /**
     * 每次Outline失效都将调用的一个方法,add和remove共享逻辑
     * @param bottom 和view相邻的、最底下的那个view的bottom值
     * @param deltaToExpandState 当前translation过渡到完全展开状态下的translation需要多少数值,正数
     */
    protected fun View.updateOutline(outline: Outline, bottom: Int, deltaToExpandState: Float) {
        // 最底下view的底部、和当前view的顶部的距离,减去一个delta
        val visibleHeight = (bottom - this.top - deltaToExpandState).roundToInt()
        when {
            // 比当前view高度要高,意味着当前view还不可见
            visibleHeight > height     -> outline.setRect(0, 0, 0, 0)
            // 介于0和当前view高度之间,意味着当前view掉出来一点点可见区域了
            visibleHeight in 0..height -> outline.setRect(0, visibleHeight, width, height)
            // 小于0,当前view已被完全展示
            else                       -> outline.setRect(0, 0, width, height)
        }
    }
}

最关键的是这段代码最后的一个 updateOutline 方法。它可能需要你仔细在脑海里琢磨琢磨了,简单的数学问题。

(可能确实对高中生刚刚好,对程序员复杂了点)

updateOutline 和其他代码的交互还需要再看下面这段代码------

注意:请结合前面的【梳理运行逻辑】一小节看------

kotlin 复制代码
private val addSet = object : AnimSet() {
    override fun add(element: Op): Boolean = super.add(element).also {
        element.judgeNeighborElements { op, delta, top ->
            delta as Int
            // 动画初始状态是基于当前状态的delta
            op.animStartState.translationY = op.oriState.translationY + delta
            op.animEndState.translationY = op.oriState.translationY
            op.animStartState.outlineProvider = getClipOutlineProvider { view, outline ->
                val animProcessed = abs(view.translationY - op.animStartState.translationY) 
                view.updateOutline(outline, top + abs(delta), animProcessed)
            }
        }
    }
}
private val removeSet = object : AnimSet() {
    override fun add(element: Op): Boolean = super.add(element).also {
        element.judgeNeighborElements { op, delta, top ->
            delta as Int
            // 动画初始状态就是当前状态
            op.animStartState.translationY = op.oriState.translationY
            // 动画最终状态是基于当前状态的delta
            op.animEndState.translationY = delta + op.oriState.translationY
            op.animStartState.outlineProvider = getClipOutlineProvider { view, outline ->
                val animProcessed = abs(view.translationY - op.animEndState.translationY)
                view.updateOutline(outline, top + abs(delta), animProcessed)
            }
        }
    }
}
private val moveSet = object : AnimSet() {
    override fun add(element: Op): Boolean = super.add(element).also {
        element.prepareView()
    }
}

private val animatingOps = object : ArrayList<Op>() {
    fun remove(viewHolder: ViewHolder) = removeAll {
        it.holder === viewHolder
    }

    override fun remove(element: Op): Boolean {
        return super.remove(element).apply {
            element.recoverView()
        }
    }

    override fun clear() {
        forEach {
            it.recoverView()
        }
        super.clear()
    }
}

如此,大功告成!

相关推荐
88号技师2 小时前
2024年12月一区SCI-加权平均优化算法Weighted average algorithm-附Matlab免费代码
人工智能·算法·matlab·优化算法
IT猿手2 小时前
多目标应用(一):多目标麋鹿优化算法(MOEHO)求解10个工程应用,提供完整MATLAB代码
开发语言·人工智能·算法·机器学习·matlab
88号技师2 小时前
几款性能优秀的差分进化算法DE(SaDE、JADE,SHADE,LSHADE、LSHADE_SPACMA、LSHADE_EpSin)-附Matlab免费代码
开发语言·人工智能·算法·matlab·优化算法
我要学编程(ಥ_ಥ)3 小时前
一文详解“二叉树中的深搜“在算法中的应用
java·数据结构·算法·leetcode·深度优先
埃菲尔铁塔_CV算法3 小时前
FTT变换Matlab代码解释及应用场景
算法
许野平4 小时前
Rust: enum 和 i32 的区别和互换
python·算法·rust·enum·i32
chenziang14 小时前
leetcode hot100 合并区间
算法
chenziang14 小时前
leetcode hot100 对称二叉树
算法·leetcode·职场和发展
zhangphil4 小时前
Android绘图Path基于LinearGradient线性动画渐变,Kotlin(2)
android·kotlin
szuzhan.gy5 小时前
DS查找—二叉树平衡因子
数据结构·c++·算法