开发TV遇到的焦点总结

TV开发,个人理解,一共三个核心,焦点的走向处理,按键的监听处理,焦点记忆

1. 焦点属性介绍以及焦点监听和获取

    /**
     * 控制视图是否可以获取焦点。默认情况下,这是“自动”,它让框架确定用户是否可以将焦点移动到视图。
     * 通过将此属性设置为 true,视图可以获取焦点。通过将其设置为“false”,视图将不会获得焦点。
     * 此值不会影响直接调用 {@link android.view.ViewrequestFocus} 的行为,无论此视图如何,它都会始终请求焦点。
     * 它只影响焦点导航尝试移动焦点的位置。(也就是说只有在操作时发挥作用)
     */
    <attr name="focusable" format="boolean|enum">

   /**
     * 控制视图是否可以在触摸模式下获得焦点的布尔值。
     * 如果对于一个视图是这样,则该视图可以在单击时获得焦点,并且如果单击另一个未将此属性设置为 true 的视图,则可以保持焦点。
     * 这个开启后是有一个副作用的,点击一个View需要点击二次,第一次点击变成了获取焦点,第二次点击才会执行onClickListener
     */
    <attr name="focusableInTouchMode" format="boolean" />

    /**
     * 此视图是否为默认焦点视图。可以将此属性设置为 true。
     * 也就是说页面可见时,设置该属性的View会默认获焦
     * 请参阅 {@link android.view.ViewsetFocusedByDefault(boolean)}。
     */
    <attr name="focusedByDefault" format="boolean" />

    /**
     * 此视图在获得焦点但未在其背景中定义 {@link android.R.attrstate_focused} 时是否应使用默认焦点突出显示。
     * 也就是说如果View的该属性设置成true后,焦点选中后会存在一个跟正常无焦点显示不一样的样式
     */
    <attr name="defaultFocusHighlightEnabled" format="boolean" />

   /**
     * 屏幕阅读器辅助工具是否应将此视图视为可聚焦单元。
     * 请参阅 {@link android.view.ViewsetScreenReaderFocusable(boolean)}。
     * 默认值 {@code false} 让屏幕阅读器考虑其他信号,例如可聚焦性或文本的存在,以决定它关注的内容
     * 也就是说如果在布局文件跟布局设置该属性为true,其他子View设置成false,系统会顺序阅读内容
     * 一般不常用
     */
    <attr name="screenReaderFocusable" format="boolean" />

    /**
     * 如果子View设置了该属性,那么当前View的焦点状态时跟随父布局的
     */
    android:duplicateParentState="true"

还有一个,这个在TV上平时用的比较多

    android:descendantFocusability是View的一个属性。可以理解是viewGroup和其子控件焦点相关的属性。

    beforeDescendants :viewGroup会优先其子类控件而获取到焦点

    afterDescendants :viewGroup只有当其子类控件不需要获取焦点时才获取焦点

    blocksDescendants :viewGroup会覆盖子类控件而直接获得焦点

如果想要一个控件可以获取到焦点,往往用如下代码

    /**
     * focusable代表的是能不能获取到焦点,
     * focusableInTouchMode代表的是TouchMode下能否获取到焦点。
     */
    android:focusableInTouchMode="true"
    android:focusable="true"

如果进入某个页面需要固定静态View获取焦点请尽量使用

defaultView.isFocusedByDefault = true

主动获取焦点使用,主动获取焦点使用方法 requestFocus(),但是可能会失败,所以需要注意等待UI刷新完后在调用

//通常使用
needFocusView. requestFocus()

//如果获取失败的,可能是UI未绘制完成,没办法控制时机的,可以这样
needFocusView.post {
    needFocusView.requestFocus()
}

如果要动态改变某个View获焦,可以先设置默认获焦的View先设置focusable=false,然后再将需要获焦的View设置成requestFocues=true;如果还存在转场动画的情况,容易造成焦点跳动,这里可以通过延时来处理,可以规避页面VIew没有绘制出来的问题以及转场动画存在时间间隔导致获焦先执行找不到View的问题,代码可以如下

defaultView.isFocusable = false
needFocusView.postDelayed({
  isFocusable = true
}, delayMillis)

监听某个View焦点的监听,需要对单独的View做处理的如下

focusView.setOnFocusChangeListener { view, b ->
     Log.e("tag", "focus status $b ")
 }

如果需要做通用全局性的View焦点处理(如统一边框的),可以使用如下监听方式处理

    /**
     * 获取全局焦点监听
     */
    private val globalFocusListener: ViewTreeObserver.OnGlobalFocusChangeListener =
        ViewTreeObserver.OnGlobalFocusChangeListener { oldFocus, newFocus ->
            Log.e("tag","上一个获焦View:$oldFocus,当前获焦的View:$newFocus")
    }
      /**
     * 页面可见的时候注册监听
     */
    containerView.viewTreeObserver.addOnGlobalFocusChangeListener(globalFocusListener)
    /**
     * 页面不可见的时候移除监听
     */
    containerView.viewTreeObserver.removeOnGlobalFocusChangeListener(globalFocusListener)      

2. 焦点移动逻辑以及开发注意处理

首先按键事件分发方法顺序如下Activity->ViewGroup->View,事件处理则相反


 1. 先是activity的
    override fun onKeyDown(keyCode: Int, event: KeyEvent?): Boolean {
        return super.onKeyDown(keyCode, event)
   }
 2. 然后是View的dispatchKeyEvent,最后是子View的dispatchKeyEvent
    override fun dispatchKeyEvent(event: KeyEvent?): Boolean {
        return super.dispatchKeyEvent(event)
    }

a.如果是ViewGroup时,焦点走向处理

1.当我们明确知道,下一个获取焦点的控件时,可以直接调用下面的方法,系统查找下一个焦点的时候,就会返回我们设置id的那个控件。

   setNextFocusUpId()    //设置按上键获焦的View,通过View的id来设置
   setNextFocusDownId()   //设置按下键获焦的View,通过View的id来设置
   setNextFocusLeftId()      //设置按左键键获焦的View,通过View的id来设置
   setNextFocusRightId()     //设置按右键键获焦的View,通过View的id来设置

2.当我们不知道下一个控件的ID的时候,可以通过重写focusSearch方法来实现

    /**
     * 调用该方法时,会调用parant的focusSearch(@FocusRealDirection int direction),
     * 接着会执行parent(ViewGroup)的focusSearch(View focused,int ditection)的方法,
     * 然后会执行判断当前节点是不是根节点,如果不是就调用父节点的focusSearch继续查找,直到根节点为止。在根节点调用
     * findNextFocus(ViewGroup root, View focused, int direction)的方法,并把根节点赋值给它。
     */
   override fun focusSearch(focused: View?, direction: Int): View? {
        when (focused) {
            //处理自己的焦点走向逻辑,得到下一个获焦的View,并设置获焦
        }
        return super.focusSearch(focused, direction)
    }

当然这里需要注意 FocusFinder.getInstance().findNextFocus(ViewGroup root, View focused, int direction).它的第一个参数是ViewGruop,所以,如果你需要限定在某个ViewGruop中查找控件,就可以在focusSeach中调用这个方法。同时addFocusables()的方法也是在这个方法里面执行的, 这里贴下这个源码

  /**
     * Find the nearest view in the specified direction that wants to take
     * focus.
     *
     * @param focused The view that currently has focus
     * @param direction One of FOCUS_UP, FOCUS_DOWN, FOCUS_LEFT, and
     *        FOCUS_RIGHT, or 0 for not applicable.
     */
    @Override
    public View focusSearch(View focused, int direction) {
        if (isRootNamespace()) {
            // root namespace means we should consider ourselves the top of the
            // tree for focus searching; otherwise we could be focus searching
            // into other tabs.  see LocalActivityManager and TabHost for more info.
            return FocusFinder.getInstance().findNextFocus(this, focused, direction);
        } else if (mParent != null) {
            return mParent.focusSearch(focused, direction);
        }
        return null;
    }

b.如果是RecyclerView时,焦点走向处理

调用FocusSearch()方法,留了一个onInterceptFocusSearch回调,将拦截事件转发给LayoutManager来实现特定的拦截焦点逻辑,比如常用的列表边界拦截。

开发注意

  1. 弹框无法获取焦点怎么处理?
    一般的dialog只要设置focus 属性,然后在初始化调用 requestFocus即可
    但是针对PopupWindow,建议直接使用setOnKeyListener来做监听控制焦点走向,不然有可能会出现焦点不好处理的问题;当然也可以自定义弹窗再xml里面实现焦点
  2. 焦点点击出现相应多次?
    可以通过监听的 KeyEvent 参数中 repeatCount 判断,这里最好等于某个数字时触发,防止多次重复触发
  3. 前排后排隔离
    如果在xml使用可以通过include实现,创建两个布局文件,如果是代码通过判断,用扩展类来实现TV相关的单独逻辑
  4. Viewpage2嵌套fragment
    需要将Viewpage2里面自己的recyclerView禁止获取焦点
     //目前想到的是通过反射处理的
        try {
            val field = viewPager::class.java.getDeclaredField("mRecyclerView")
            field.isAccessible = true
            val rv = field.get(viewPager) as RecyclerView
            rv.descendantFocusability = ViewGroup.FOCUS_AFTER_DESCENDANTS
            rv.overScrollMode = View.OVER_SCROLL_NEVER
        } catch (exception: Exception) {
            exception.printStackTrace()
        }
  1. 左右结构的页面处理
    方案1:不论是在左侧页面还是右侧页面,都可以查找顶层的页面根节点,然后下发到各个页面,然后做焦点处理
    方案2:如果实在太复杂,可以通过flow通知处理吧,但是最好还是按方案1处理

3. 焦点记忆

实现焦点记忆需要实现如下方法

    /**
     * 将有焦点的View添加到焦点列表里
     */
    override fun addFocusables(views: ArrayList<View>?, direction: Int, focusableMode: Int) {
        if (hasFocus()) {
            super.addFocusables(views, direction, focusableMode);
        } else {
            views?.add(this);
        }
    }
   
    /**
     * 直接取获焦的View
     */
    override fun requestChildFocus(child: View?, focused: View?) {
        super.requestChildFocus(child, focused)
        child?.let { lastFocusedView = child }
    }
    
     /**
     * 是否当前的ViewGroup整体参与焦点处理,如果加上这个方法就是需要,
     * 如果不加入就是粗暴的直接是上一个获焦的View处理焦点
     */
    override fun onRequestFocusInDescendants(
        direction: Int,
        previouslyFocusedRect: Rect?
    ): Boolean {
        // 返回true表示已处理焦点请求,返回false则交给父视图处理
        if (lastFocusedView != null && lastFocusedView.requestFocus()) {
            return true
        }
        return super.onRequestFocusInDescendants(direction, previouslyFocusedRect)
    }


4. 焦点测试

正常来说直接使用遥控器操作即可,如果没有遥控器可以通过adb命令来执行,下面是几种常见的按键处理

上:adb shell input keyevent KEYCODE_DPAD_UP
下:adb shell input keyevent KEYCODE_DPAD_DOWN
左:adb shell input keyevent KEYCODE_DPAD_LEFT
右:adb shell input keyevent KEYCODE_DPAD_RIGHT
确认:adb shell input keyevent KEYCODE_ENTER
返回:adb shell input keyevent KEYCODE_BACK

如果需要其他按键处理可以去查看,类名如下截图
在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值