Compose焦点控制与应用

34 阅读2分钟

写在前面

很早之前,开始了解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 支持设置 leftrightupdownstartend,甚至可以通过 enterexit 来控制焦点进入/离开容器节点时的跳转逻辑。

典型场景如下:在 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() 之前/上方,才能正确获取焦点状态。

总结

占个坑位,后续详细讲解