前言
上篇指路: 小白也能懂的RecyclerView界面动效定制,让ExpandableListView彻底退出舞台!(上)
裁剪原理
ItemAnimator
的本质是对 ViewHolder
的 itemView
做动画,动画过程中使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 的 outline
, outline
生成后,配合 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是因为它自动实现了
equals
、toString
和copy
方法,我们稍后会用到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 画了下流程------
值得注意的是:
- 上述
animateXxx()
方法需要返回 true ,才会引起后面安排执行runPendingAnimations
。 - SimpleItemAnimator 将
dispatchAnimationFinished(ViewHolder)
方法封装成了对应4种情况的 dispatchXxxStart 和 dispatchXxxFinished 方法,我们不考虑给子类重写的需要,最简化处理就好。 - (重要) 在 animateXxx() 方法传入 viewHolder 时,其 itemView 已经准备好了,假设没有动画的话,下一帧整个rv就将显示这个最终状态了。这意味着对于我们想要的动画而言------
- 对于 animateAdd ,view已经被添加到正常的位置。
- 我们把 view 立即 设置不可见、且在最终目标上方的位置。
- 对于 animateRemove ,因为最终状态是移除vie,所以view属性不会发生额外变化。
- 当前状态无需改变。
- 对于 animateMove ,我们将从"当前位置"移动到目标位置,但"当前位置"已经不存在了,传入的view已经是目标位置。
- 所以我们需要根据传参得到delta ,把 itemView "重置回上一刻的位置"。
- 对于 animateAdd ,view已经被添加到正常的位置。
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()
}
}
如此,大功告成!