前言

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

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

裁剪原理

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

但是可见一半该怎么做?

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

能否不自界说View,而是在外部经过接口裁剪它呢?能够的——

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

View 的 默许 OutlineProvider

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

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 ——

// 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 这个静态变量怎么界说的吧——

/**
 * 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 算了,留个优化空间在这。

private const val CommonDuration: Long = 350L
private val CommonInterpolator = DecelerateInterpolator()

View 状况康复与保存

根据上述内容,咱们需求在动画过程中临时更改一些view的特点,

并在动画结束后康复。

View 状况记录类:ViewState

  • 运用data class是因为它自动完成了equalstoStringcopy办法,咱们稍后会用到copy

所以有 ViewState 类界说如下(我直接界说成文件级私有类了)——

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,做多个时再去抽象处理,方案永远赶不上改动,程序永远是越简短越好维护,也越好扩大)

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 画了下流程——

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

值得留意的是:

  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 的函数列表。

/**
 * 用于多级菜单的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 内部匿名类的目标,它涉及详细的动画处理。

咱们能够先略过他们,优先完成全体逻辑。

/**
 * 用于多级菜单的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


/**
 * 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 和其他代码的交互还需求再看下面这段代码——

留意:请结合前面的【整理运转逻辑】一小节看——

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

如此,大功告成!