Launcher3最近任务遥控切换动画

概要

在Android13的Launcher3中,集成了最近任务功能,其中我们在滑动的时候就可以丝滑的切换每一个任务卡片。但是当我们使用遥控模式的时候,发现最近任务中的任务卡片切换是比较突兀的,没有任务过度动画。下面是两张对比图,一张是原始的无动画切换的,一张是有动画的,大家可以直观的感受下。
无动画切换:
在这里插入图片描述
有动画的切换:
在这里插入图片描述

一、遥控模式下最近任务卡片切换分析

提示:本文使用的是Android13的版本,其他版本应该大同小异

相信看到这篇文章的伙伴都是从事Android Launcher开发的,对原生的Launcher3项目比较熟悉。本文中涉及到最近任务功能,核心就是关注两个view:PagedView、RecentsView。

PagedView是Launcher3项目中核心的View之一。其内部有一个OverScroller用于控制子View的滚动动画。它有两个非常重要的子类:WorkspaceRecentsView

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();方法即可。

  • 20
    点赞
  • 14
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值