惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

阮一峰的网络日志
阮一峰的网络日志
Apple Machine Learning Research
Apple Machine Learning Research
量子位
D
DataBreaches.Net
云风的 BLOG
云风的 BLOG
博客园 - 聂微东
博客园_首页
D
Docker
博客园 - 叶小钗
S
SegmentFault 最新的问题
大猫的无限游戏
大猫的无限游戏
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
J
Java Code Geeks
H
Hackread – Cybersecurity News, Data Breaches, AI and More
A
About on SuperTechFans
博客园 - 三生石上(FineUI控件)
F
Fortinet All Blogs
小众软件
小众软件
aimingoo的专栏
aimingoo的专栏
爱范儿
爱范儿
腾讯CDC
罗磊的独立博客
雷峰网
雷峰网
博客园 - Franky

博客园 - 翻滚的咸鱼

Ops权限行为记录源码解析 TaskView方案框架原理 视频解码器问题 常用命令 扫光动效 引导记录 性能优化 View共享动效 常用脚本文件 Launcher 卡片框架多模块集成 Launcher 桌面源码笔记二 Launcher 桌面源码笔记一 氛围灯动态屏保取色方案二 氛围灯动态屏保取色方案一 自定义最近任务管理器 anr问题 多类型适配器 多屏下字体自动取色 阻尼拉伸方案 图片区域点击处理 RecyclerView设置默认焦点跟多页面焦点抢占 TV RecyclerView 滑动后操作保持落焦在左侧第一个View Fragment翻盖动效 当个View下,使用Drawable入场退场动画
TV RecyclerView 焦点处理笔记
翻滚的咸鱼 · 2025-07-14 · via 博客园 - 翻滚的咸鱼

面对RecyclerView焦点,特别是复杂视图,多类型情况下,需求有时候不按系统定义的走,比如要求首次落焦在第二个,或者焦点移动到边界就不能移动

如果不遵循焦点流程直接粗暴处理,会导致系统分发事件出异常,焦点乱飞

默认焦点使用 addOnChildAttachStateChangeListener 监听

recyclerView.addOnChildAttachStateChangeListener(object :
                RecyclerView.OnChildAttachStateChangeListener {
                override fun onChildViewAttachedToWindow(view: View) {
                    val position = recyclerView.getChildAdapterPosition(view)
                    log("position $position view $view")
                    if (position == 3) view.requestFocus()
                }

                override fun onChildViewDetachedFromWindow(view: View) {

                }
            })

View Code

对于超出边界时,系统会触发onFocusSearchFailed

    override fun onFocusSearchFailed(
        focused: View,
        focusDirection: Int,
        recycler: RecyclerView.Recycler,
        state: RecyclerView.State
    ): View? {
        return when (focusDirection) {
            View.FOCUS_UP -> {
                if (focused.parent.parent is RecyclerView) {
                    val position = (focused.parent as? View)?.let { getPosition(it) } ?: -1
                    //...
                } else {
                    focused.rootView.findViewById<View>(R.id.btn_myself)
                }
            }

            else -> super.onFocusSearchFailed(focused, focusDirection, recycler, state)
        }
    }

View Code

针对焦点移动时自动滚动列表到可见位置,可以使用 onRequestChildFocus

    override fun onRequestChildFocus(
        parent: RecyclerView,
        state: RecyclerView.State,
        child: View,
        focused: View?
    ): Boolean {
        val position = parent.getChildAdapterPosition(child)
        scrollToPosition(position)
        return super.onRequestChildFocus(parent, state, child, focused)
    }

View Code

如果针对焦点到边界位置后不能移动,或者边界触底动效,可以使用焦点拦截 onInterceptFocusSearch

完整代码

class ScreenMainGridManager(private val mAdapter: BaseAdapter, context: Context) :
    GridLayoutManager(context, 4) {

    private val marginStart = context.resources.getDimension(R.dimen.common_dp_54)

    init {
        spanSizeLookup = object : SpanSizeLookup() {
            override fun getSpanSize(position: Int): Int {
                val viewType = mAdapter.getItemViewType(position)
                return when (viewType) {
                    ViewType.TITLE.value, ViewType.BANNER.value -> 4
                    else -> 1
                }
            }
        }
    }

    override fun onInterceptFocusSearch(focused: View, direction: Int): View? {
        return when (direction) {
            View.FOCUS_LEFT -> {
                findLeftFocusView(focused) ?: super.onInterceptFocusSearch(focused, direction)
            }

            View.FOCUS_RIGHT -> {
                var isRightFocus = false
                findRecyclerView(focused)?.let {
                    findRootView(focused)?.let { root ->
                        val position = it.getChildAdapterPosition(root)
                        if (position > 0 && position + 1 < itemCount) {
                            val viewType = mAdapter.getItemViewType(position + 1)
                            if (viewType == ViewType.TITLE.value) {
                                isRightFocus = true
                            }
                        } else if (position + 1 == itemCount) {
                            isRightFocus = true
                        }
                    }
                }
                if (isRightFocus) focused else super.onInterceptFocusSearch(focused, direction)
            }

            else -> super.onInterceptFocusSearch(focused, direction)
        }
    }

    private fun findLeftFocusView(focused: View): View? {
        findRecyclerView(focused)?.let {
            val location = IntArray(2)
            focused.getLocationOnScreen(location)
            if (location[0] < marginStart) {
                return focused
            }
        }
        return null
    }

    private fun findRecyclerView(focused: View): RecyclerView? {
        var parent: ViewParent? = focused.parent
        for (i in 0 until 2) {
            if (parent is RecyclerView) break
            parent = parent?.parent
        }
        return parent as? RecyclerView
    }

    /** item root view */
    private fun findRootView(focused: View): View? {
        var result: View? = focused
        var parent = focused.parent
        for (i in 0 until 2) {
            if (parent is RecyclerView) {
                return result
            }
            result = parent as? View
            parent = parent?.parent
        }
        return result
    }

    override fun onRequestChildFocus(
        parent: RecyclerView,
        state: RecyclerView.State,
        child: View,
        focused: View?
    ): Boolean {
        val position = parent.getChildAdapterPosition(child)
        scrollToPosition(position)
        return super.onRequestChildFocus(parent, state, child, focused)
    }

    override fun onFocusSearchFailed(
        focused: View,
        focusDirection: Int,
        recycler: RecyclerView.Recycler,
        state: RecyclerView.State
    ): View? {
        return when (focusDirection) {
            View.FOCUS_UP -> {
                if (focused.parent.parent is RecyclerView) {
                    val position = (focused.parent as? View)?.let { getPosition(it) } ?: -1
                    if (position < 0) {
                        super.onFocusSearchFailed(focused, focusDirection, recycler, state)
                    } else {
                        val item = mAdapter.getItem(position)
                        if (item is MainBannerItem || item is MeAddItem) {
                            focused.rootView.findViewById<View>(R.id.btn_myself)
                                ?: focused.rootView.findViewById<View>(R.id.btn_back)
                        } else {
                            scrollToPosition(0)
                            super.onFocusSearchFailed(focused, focusDirection, recycler, state)
                        }
                    }
                } else {
                    focused.rootView.findViewById<View>(R.id.btn_myself)
                }
            }

            else -> super.onFocusSearchFailed(focused, focusDirection, recycler, state)
        }
    }

}

View Code

避免直接request或者clear焦点

posted on 2025-07-14 15:21  翻滚的咸鱼  阅读(159)  评论()    收藏  举报