写在前面
很早之前,开始了解Compose,写了篇记录文章 Jetpack Compose的了解。
和过往告别,写了篇 Android TV开发-按键焦点了解与应用。
为了更好学习Compose,自己尝试写Compose-TV-Launcher,如何用Compose TV写电视Launcher。
自从进入Compose-first,时代不一样了,需要真正进入Compose环抱了。
最近又在写复古游戏模拟器(D-pad方向键,A,B,X,Y等操作),随手在这里进行一个小总结记录吧。
关键技术
Compose 维护了一棵 Focus Tree(焦点树),系统焦点发生变化时,通过 FocusRequester、focusProperties、focusGroup 等机制决定“下一个焦点是谁”。
Compose并不像以前View的焦点搜索了,它会结合 方向,几何位置,次轴距离,Focus Group,Focus Properties,当前焦点层级等。
它和传统 View 的 focusSearch() 思路不太一样。
Window
│
▼
Compose FocusOwner
│
▼
Focus Tree
│
├── LazyColumn_FocusNode(高分必玩榜/个性推荐/最近大家都在玩...)
│ ├── Card(拳皇98)
│ ├── Card(合金弹头)
│ └── Card...
│
├── GridLayout_FocusNode(一拖多布局)
│ ├── Card(1x1)
│ └── Card(2x1)
│ └── Card(2x2)
│ └── Card...
│
└── FocusNode
├── Menu(CPS1)
└── Menu(CPS3)
└── Menu...
// 每一个可以获取焦点的 Composable,最终都会对应到 Compose 内部的 FocusNode。
有点类似 原生view的setFocusable(true)
Box(modifier = Modifier.focusable())
// clickable() 也可以获取焦点
focusGroup 告诉焦点系统:这些元素属于一个焦点区域,优先在这个区域内部寻找焦点。
LazyColumn(
modifier = Modifier.focusGroup()
.onFocusChanged {
state -> onRailFocusChanged(state.hasFocus)
}
) { itemsIndexed(consoles) { index, console -> ConsoleRailItem( ) } }
focusProperties:例如实现循环滚动、边界阻尼、跨区域跳转
FocusProperties 支持设置 left、right、up、down、start、end,甚至可以通过 enter 和 exit 来控制焦点进入/离开容器节点时的跳转逻辑。
典型场景如下:在 Launcher 首页,顶部是 Header/Tab 栏(如:精选、电影、电视剧),下方是一个大型的 Banner 轮播图。
// DAPD_LEFT时,焦点跑到了菜单上
GameList -> Modifier.focusProperties { left = menuTargetRequester }
主机Menu -> Modifier.focusRequester(menuTargetRequester)
// 禁止继续向下寻找焦点
Modifier.focusProperties { down = FocusRequester.Cancel }
// 表示恢复默认的焦点搜索行为。
FocusRequester.Default
// 向系统发送焦点请求或清除焦点的控制器。
FocusRequester
val focusRequester = remember { FocusRequester() }
modifier = Modifier.focusRequester(focusRequester)
// 某个地方调用,就会让上面的代码主动上焦
focusRequester.requestFocus()
Modifier.focusProperties {
right = rightRequester
}
onFocusChanged:监听焦点变化
Modifier.onFocusChanged { state ->
focused = it.isFocused
if (state.isFocused) {
println("获得焦点")
} else {
println("失去焦点")
}
}
var focused by remember { mutableStateOf(false) }
// 焦点放大 或 上焦后有光影移动等等或动画
scale(if (focused) 1.1f else 1f)
事件处理焦点
onPreviewKeyEvent:自顶向下(Tunneling)传递。在子节点响应之前优先拦截按键。onKeyEvent:自底向上(Bubbling)传递。子节点未处理时由父节点响应。
Box(modifier = Modifier .onKeyEvent { keyEvent ->
if (keyEvent.type == KeyEventType.KeyDown) {
when (keyEvent.key) {
...
focusTarget / focusable 的关系:focusable() -> FocusTarget -> FocusNode -> Focus Tree
内部源码还有这些
FocusOwner
FocusManager
FocusNode
FocusTargetNode
FocusEvent
FocusRequester
focusRestorer// 焦点记忆,自动恢复上一次获焦的子卡片
FocusProperties
注意
focusRequester() 与 onFocusChanged() 必须放置在 focusable() 或 clickable() 之前/上方,才能正确获取焦点状态。
总结
占个坑位,后续详细讲解