Android HorizontalGridView焦点问题及探索

前言

Google针对Android TV开发,放出了 leanback support包,其中包含了Google认为的TV端APP页面推荐的呈现方式,以及针对这类交互封装好的页面控件。其中HorizontalGridView&VerticalGridView两个控件对于用遥控器/键盘操作焦点浏览多个视频ITEM这种电视端APP常见交互非常有帮助。

引用方法

compile 'com.android.support:recyclerview-v7:22.2.1'
compile 'com.android.support:leanback-v17:22.2.1'

简介

HorizontalGridView&VerticalGridView均继承自leanback中的BaseGridViewBaseGridView又继承自RecyclerViewRecyclerView作为Google建议的ListView的替代者,其使用方法也不用赘述了。
HorizontalGridView(以下简称HGV)&VerticalGridView(以下简称VGV)使用方法也基本一致,顾名思义,HGV是横向呈现,VGV是竖直呈现。
我总结这两个控件与RecyclerView最大的不同在于:
1、默认实现中,当前获取到Focus的Item最后绘制。
2、在>1屏的Item中移动选择时,Focus的Item保持在屏幕中间。
3、父类BaseGridView实现了自己的LayoutManager,即GridLayoutManager,与RecyclerView可选的GridLayoutManager有很大不同。

问题

预期效果:获取焦点的Item放大显示并且可以盖住左右有交集的Item。
这种基本效果也实现过很多次了:注册onFocusChangeListener,让获取焦点的Item做放大动画,最后为了盖住有交集的Item,再调用View.bringToFront方法将获取焦点的Item最后一个绘制。
在以前的各种LayoutListViewRecyclerView中用这样的方法都能得到预期的效果,但在HGV中使用这种方法,左右移动焦点,会发现焦点移动的Item会改变,举例来说:从1->2,然后从2->1,之后预期是1->2但实际变成了1->3。如此反复差距会越来越大。

解决方案

去掉View.bringToFront语句后正常。

WHY

为什么会有这样的区别?HGV作为RecyclerView的子类,究竟做了什么不同的事情?为什么必须去掉View.bringToFront

分析

我将从用户按下左/右键后开始分析。

  1. 用户按下右键,系统开始分发KeyEvent.KeyCode_DPAD_RIGHT按键。经过了底层的工作和拦截,最终交给ViewRootImpl.java向APP进行按键分发。由preIme->postIme,均为内部InputStage。在postIme的onProcess中,如果是按键,会调用processKeyEvent往下分发。
    Android HorizontalGridView焦点问题及探索_第1张图片

  2. mViewPhoneWindow.java中的内部类DecorView对象实体,由DecorView->Activity->ViewGroup->View一层一层分发处理,如果在每一层中用户都没有消费此事件(return true代表已消费),那么就会回到上图继续向下走。在本文中,HGV和RecyclerView,如果用户没有自己注册按键监听,那么默认都是没有消费按键的。因此回到上图继续向下,函数将判断按键方向,之后开始根据按键方向查找下一个获取焦点的View。
    Android HorizontalGridView焦点问题及探索_第2张图片

  3. 可以看到,此处查找下一个要获取焦点的view使用的方法是focusSearch,其中会调用mParent.focusSearch。对于本文中的Item来说,其mParent显然是HGV,最后发现HGV、BGV都没有实现,查看RecyclerView,发现其中覆写了此方法。
    Android HorizontalGridView焦点问题及探索_第3张图片

  4. result就是我们要找出来的View。那么继续跟进到FocusFinder.java中。
    Android HorizontalGridView焦点问题及探索_第4张图片

  5. 发现FocusFinder通过root.addFocusables方法,填充了一个List。实际上,focusables中就是root.addFocusables函数认为可能获得下一个焦点的所有View。对于本文的Item,root显然要从HGV找起,最终在RecyclerView中发现了覆写。
    附代码

  6. 看来挺简单,先调用LayoutManager中的onAddFocusables方法,如果LayoutManager没有自己的实现,就返回调用父类的addFocusables,即ViewGroup中的方法(RecycleView父类是ViewGroup)。
    此处是HGV和RecyclerView的第一个不同之处

    • RecyclerView的默认LayoutManager实现中(包括GridLayoutManager、LinearLayoutManager等)均未实现onAddFocusables。而HGV、VGV实现的GridLayouyManager中实现了此方法。
    • 因此这个函数最终走向了两条道路:
    • RecyclerView->ViewGroup->addFocusables
      Android HorizontalGridView焦点问题及探索_第5张图片

    • HGV->RecyclerView->GridLayoutManager->onAddFocusables
      Android HorizontalGridView焦点问题及探索_第6张图片

  7. 下面还有一些分支,先不考虑了。可以看到两者的算法是截然不同的:
    ViewGroup中,只是单纯的将mChildren数组(该数组包含ViewGroup中所有View)中的View一个一个加入focusables
    GridLayoutManager中,首先获取到父类ViewGroupmChildren数组,然后根据移动的方向,进行不同方向的循环,最终加入focusables中的View仅仅只有一个。

  8. 回到FocusFinder中,填充完毕focusables后,FocusFinder根据移动方向,依次判断了当前View和可获取焦点View之间距离,找出了最合适的一个View,然后一路返回至ViewRootImpl中,通过view.requestFocus让其获取到焦点。调用view.requestFocus,会走到mParent.requestChildFocus
    Android HorizontalGridView焦点问题及探索_第7张图片

  9. 里面的mParent.requestChildFocus也就是RecyclerView中覆写的requestChildFocus
    Android HorizontalGridView焦点问题及探索_第8张图片

  10. 在其中,和addFocusables处理类似,先调用LayoutManager中的方法,如果没有再走自己的实现。此处是HGV和RecyclerView的第二个不同之处
    HGV的GridLayoutManager中同样实现了onRequestChildFocus
    Android HorizontalGridView焦点问题及探索_第9张图片

  11. scrollToView方法,就是根据焦点的移动来让HGV进行滚动,这是为了实现HGV获取焦点的Item一直处于中间位置这一特性。该方法中,有这样一个判断。
    附代码

  12. isChildrenDrawingOrderEnabledInternal调用父类的isChildrenOrderEnabled方法,RecyclerView中没有覆写,我们可以在ViewGroup中找到,并且在HGV的直接父类BaseGridView初始化时看到其设置了此项为true
    也即是说,在此处,HGV会执行一次重新绘制。

  13. 回到文章开头,在RecyclerView中,我们为了让放大的Item盖住两边的Item,会调用View.bringToFront函数。并且在Android 4.4以前,我们还要再调用invalidate函数(Android 4.4之后View.bringToFront内部就调用了invalidate)。
    这两者的作用就是,将指定的view放到ViewGroupmChildren数组的最后一位,然后重新绘制,最后绘制的就是获取到Focus的View,如此一来自然在最上层了。
    那么HGV中这个判断,就是不用调用View.bringToFront也能让Focus的Item在最上层的关键之一。

  14. 另一处关键,我们要继续跟进invalidateinvalidate会调用到dispatchDraw来进行一次视图的绘制,在ViewGroup中的dispatchDraw中看到如下判断。
    Android HorizontalGridView焦点问题及探索_第10张图片

  15. 也就是说,如果设置过childrenDrawingOrdertrue,就要通过getChildDrawingOrder方法来确定绘制view的顺序。此处是HGV和RecyclerView的第三个不同之处

    • RecyclerView的实现
      Android HorizontalGridView焦点问题及探索_第11张图片

    • HGV(BaseGridView)的实现
      BaseGridView的实现
      Android HorizontalGridView焦点问题及探索_第12张图片

  16. 如此看来,HGV在焦点变更后,默认调用一次重新绘制,绘制中根据GridLayoutManagergetChildDrawingOrder方法,将当前Focus的Item移动到最后,由ViewGroup重新绘制。
    其实原理和bringToFront&invalidate是一样的,但一个不会改变ViewGroup中的mChildren数组,一个会改变。

追问1

那么,RecyclerView能否也用这种方法?
当然可以。
首先,调用RecyclerViewsetChildDrawingOrderCallback方法,填充获取顺序的逻辑实现
之后,在onFocusChangeLisntener中,获取到焦点做完动画后调用一次invalidate方法。

追问2

HorizontalGridView能否支持View.bringToFront
当然可以。
只要把源码中onAddFocusables,找到view后break的逻辑去掉。这样就让focusables列表包含了所有可获取焦点的view。再执行FocusFinder下面的函数,自然会又比较一遍算出正确的最近一个焦点。
但这样改就是把Google的优化措施改没了。

回顾

回到我们一开始的问题,为什么HGV调用了bringToFront会出现开头的问题呢?关键就在GridLayoutManager中的onAddFocusables,其中的算法首先是获取mChildren数组。在获取完焦点的流程后用户调用了bringToFront函数,mChildren数组就变为焦点view在最后,此时在用onAddFocusables中原来的算法,向后取一次再向前取一次自然就会跳过一个Item,如此反复会越差越多,有兴趣可以自己算一下。

总结

HorizontalGridViewRecyclerView中的不同点:
- findFocus后,可获取焦点的列表中,HGV仅有一项,RecyclerView包含多项。
- requestFocus后,HGV中进行了独有的滚动操作并刷新视图,RecyclerView中采用默认实现。
- 绘制视图时,HGV使用了自定义的绘制顺序,RecyclerView使用ViewGroup默认的顺序。

你可能感兴趣的:(Android,App)