概要
在Android13的Launcher3中,集成了最近任务功能,其中我们在滑动的时候就可以丝滑的切换每一个任务卡片。但是当我们使用遥控模式的时候,发现最近任务中的任务卡片切换是比较突兀的,没有任务过度动画。下面是两张对比图,一张是原始的无动画切换的,一张是有动画的,大家可以直观的感受下。
无动画切换:
有动画的切换:
一、遥控模式下最近任务卡片切换分析
提示:本文使用的是Android13的版本,其他版本应该大同小异
相信看到这篇文章的伙伴都是从事Android Launcher开发的,对原生的Launcher3项目比较熟悉。本文中涉及到最近任务功能,核心就是关注两个view:PagedView、RecentsView。
PagedView是Launcher3项目中核心的View之一。其内部有一个OverScroller用于控制子View的滚动动画。它有两个非常重要的子类:Workspace
、RecentsView
。
Workspace
:代表着桌面,内部包含多个screen(CellLayout)。手指在桌面左右滑动,screen就跟着一起滑动,从而达到切换每一页屏幕的效果。说白了就是OverScroller的作用,实时计算每个子View的位置,并执行实时滚动的动画。
同理RecentsView也一样,毕竟都是继承的PagedView,每个任务卡片滚动切换的动画都是由OverScroller来控制的。当然这个是触摸模式下的。那遥控模式下的呢?
遥控遥控,体现在代码中,无非就是View的dispatchKeyEvents()或者onKeyDown()事件回调嘛。那就看看RecentsView中有没有重写这些方法咯。一看果然有:
@Override
public boolean dispatchKeyEvent(KeyEvent event) {
if (event.getAction() == KeyEvent.ACTION_DOWN) {
switch (event.getKeyCode()) {
case KeyEvent.KEYCODE_TAB:
return snapToPageRelative(getTaskViewCount(), event.isShiftPressed() ? -1 : 1,
event.isAltPressed() /* cycle */);
case KeyEvent.KEYCODE_DPAD_RIGHT: // 核心: 遥控器”右“按键
return snapToPageRelative(getPageCount(), mIsRtl ? -1 : 1, false /* cycle */);
case KeyEvent.KEYCODE_DPAD_LEFT: // 核心: 遥控器”左“按键
return snapToPageRelative(getPageCount(), mIsRtl ? 1 : -1, false /* cycle */);
case KeyEvent.KEYCODE_DEL:
case KeyEvent.KEYCODE_FORWARD_DEL:
dismissCurrentTask();
return true;
case KeyEvent.KEYCODE_NUMPAD_DOT:
if (event.isAltPressed()) {
// Numpad DEL pressed while holding Alt.
dismissCurrentTask();
return true;
}
}
}
return super.dispatchKeyEvent(event);
}
找到左右按键所触发的方法了snapToPageRelative()
接着就看看这个方法干了啥:
private boolean snapToPageRelative(int pageCount, int delta, boolean cycle) {
if (pageCount == 0) {
return false;
}
final int newPageUnbound = getNextPage() + delta;
if (!cycle && (newPageUnbound < 0 || newPageUnbound >= pageCount)) {
return false;
}
// 核心:滚动到对应的页数上去
snapToPage((newPageUnbound + pageCount) % pageCount);
getChildAt(getNextPage()).requestFocus();
return true;
}
snapToPage方法是PageView中公共的方法,该方法最终调用的mScroller.startScroll()。这不就是OverScroller中滚动的方法,理论上这个方法会触发滚动动画,所以遥控器左右按键天然的就有滚动效果
。为什么实际效果上看着没有滚动动画呢?
OverScroller要停止动画,除了动画本身执行完了,还有两个方法会暂停动画:
1、abortAnimation()
2、forceFinished()
所以接着看RecentsView或者PagedView中有没有这两个方法,一看果然有
PageView#
private void abortScrollerAnimation(boolean resetNextPage) {
mScroller.abortAnimation(); // 立马停止动画,并跳转到滚动条动画的末尾
onScrollerAnimationAborted();
// We need to clean up the next page here to avoid computeScrollHelper from
// updating current page on the pass.
if (resetNextPage) {
mNextPage = INVALID_PAGE;
pageEndTransition();
}
}
public void forceFinishScroller() {
mScroller.forceFinished(true); // 强制结束
// We need to clean up the next page here to avoid computeScrollHelper from
// updating current page on the pass.
mNextPage = INVALID_PAGE;
pageEndTransition();
}
既然动画强制停止了,并且瞬间滚动到了应该去的位置。肯定调用了这两个方法中的一个或者全部。既然如此,那就在这两个方法内部添加异常,并打印下堆栈信息,看看哪个鬼地方调用导致的:
PagedView#
private void abortScrollerAnimation(boolean resetNextPage) {
Log.d(TAG, "abortScrollerAnimation: here");
mScroller.abortAnimation();
onScrollerAnimationAborted();
// We need to clean up the next page here to avoid computeScrollHelper from
// updating current page on the pass.
if (resetNextPage) {
mNextPage = INVALID_PAGE;
pageEndTransition();
}
// 这里添加异常,打印异常堆栈,反向查找该方法的调用过程
Exception e = new Exception();
Log.e(TAG, "abortScrollerAnimation: ", e);
}
得到的log信息如下:
java.lang.Exception
at com.android.launcher3.PagedView.abortScrollerAnimation(PagedView.java:273)
at com.android.launcher3.PagedView.setCurrentPage(PagedView.java:433)
at com.android.launcher3.PagedView.setCurrentPage(PagedView.java:425)
at com.android.launcher3.PagedView.requestChildFocus(PagedView.java:1586)
at android.view.View.handleFocusGainInternal(View.java:7872)
at android.view.ViewGroup.handleFocusGainInternal(ViewGroup.java:846)
at android.view.View.requestFocusNoSearch(View.java:14001)
at android.view.View.requestFocus(View.java:13975)
at android.view.ViewGroup.requestFocus(ViewGroup.java:3323)
at android.view.View.requestFocus(View.java:13942)
at android.view.View.requestFocus(View.java:13884)
at com.android.quickstep.views.RecentsView.snapToPageRelative(RecentsView.java:3947)
折腾了一圈,发现是页面请求焦点造成了。回到snapToPageRelative方法,果然发现了请求焦点:
其在调用了snapToPage后,紧接着调用了
getChildAt(getNextPage()).requestFocus();
去请求焦点。从而造成了页面滚动动画消失。
二、修改的代码
经过上面的排查过程,只要去掉请求焦点的代码语句即可。修改后的代码如下:
RecentsView#
private boolean snapToPageRelative(int pageCount, int delta, boolean cycle) {
if (pageCount == 0) {
return false;
}
final int newPageUnbound = getNextPage() + delta;
if (!cycle && (newPageUnbound < 0 || newPageUnbound >= pageCount)) {
return false;
}
// 核心:滚动到对应的页数上去
snapToPage((newPageUnbound + pageCount) % pageCount);
// getChildAt(getNextPage()).requestFocus(); // 注释掉这句话,否则会立刻停止任务卡片的滚动动画
return true;
}
总结
本文主要介绍如何解决遥控模式下,最近任务卡片切换时的动画缺失的问题。虽然最终改动特别的小,仅仅是一句请求焦点的代码造成的。但重点关注过程,学习排查问题的思路吧。
此外,getChildAt(getNextPage()).requestFocus();
这句话理论上是有用的,如果我们对任务卡片的focus状态有相关的UI变化的话,如果注释掉这句话,就造成了任务卡片虽然切换了,但是其没有聚焦,从而其Focus时的UI无法显示。
解决思路,在滚动动画结束后,在把这句话加上。
什么时候执行完呢?
有多种方案吧,大家可以看下OverScroller的资料。PagedView中有 computeScroll()方法咯,该方法在滚动时会实时回调,在该方法中就有判断动画是否执行完,在执行完毕后的分支中,添加一个getChildAt(getNextPage()).requestFocus();
方法即可。