Android Compose筆記

界面架构

生命周期

可组合项的生命周期通过以下事件定义:

  • 进入组合,执行 0 次或多次重组,然后退出组合。

key 可组合项:

  • 通过调用带有一个或多个传入值的键可组合项来封装代码块,这些值将被组合以用于在组合中标识该实例,重复使用

稳定类型:

  • 对于相同的两个实例,其 equals 的结果将始终相同。
  • 如果类型的某个公共属性发生变化,组合将收到通知。
  • 所有公共属性类型也都是稳定。

稳定不可变类型:

  • 所有基元值类型:Boolean、Int、Long、Float、Char 等。
  • 字符串
  • 所有函数类型 (lambda)

稳定但可变的类型:

  • MutableState

强制 Compose 将其视为稳定类型:

  • 使用 @Stable 注解对其进行标记

在重组期间,如果某些符合条件的可组合函数的输入与上一个组合相比未发生变化,则可以完全跳过其执行。

可组合函数符合跳过条件,除非:

  • 函数的返回值类型不是 Unit
  • 函数带有 @NonRestartableComposable 或 @NonSkippableComposable 注解
  • 必需参数属于非稳定类型

附带效应(副作用)

指发生在可组合函数作用域之外的应用状态的变化。

  • LaunchedEffect:在某个可组合项的作用域内运行挂起函数
  • rememberCoroutineScope:获取组合感知作用域,以便在可组合项外启动协程
  • rememberUpdatedState:在效应中引用某个值,该效应在值改变时不应重启
  • DisposableEffect:需要清理的效应。需要在键发生变化或可组合项退出组合后进行清理的附带效应
  • SideEffect:将 Compose 状态发布为非 Compose 代码
  • produceState:将非 Compose 状态转换为 Compose 状态
  • derivedStateOf:将一个或多个状态对象转换为其他状态
  • snapshotFlow:将 Compose 的 State 转换为 Flow

阶段

3 个主要阶段:组合、布局(包含两个步骤:测量和放置)和绘制

一般原则是,对于应该以彼此相对的方式进行测量和放置的多个界面元素,我们要提供单一的可信来源

状态管理

rememberSaveable 会自动保存可保存在 Bundle 中的任何值。对于其他值,您可以将其传入自定义 Saver 对象。

一些可以根据 Android 应用中使用的常见可观察类型创建 State 的函数:

  • Flow:collectAsStateWithLifecycle() 仅适用于 Android
  • Flow:collectAsState() 平台通用
  • LiveData: observeAsState()
  • RxJava2: subscribeAsState()
  • RxJava3: subscribeAsState()
  • 自定义可观察类,请使用 produceState API 对其进行转换,以生成 State

无状态可组合项是指不保持任何状态的可组合项。
Compose 中的状态提升,是一种将状态移至可组合项的调用方,使可组合项变成无状态的模式。
常规状态提升模式是将状态变量替换为两个参数:

  • value: T:要显示的当前值
  • onValueChange: (T) -> Unit:请求更改值的事件,其中 T 是建议的新值
    具有一些重要的属性:单一可信来源、封装、可共享、可拦截、解耦
    状态下降、事件上升的这种模式称为“单向数据流”。

rememberSaveable可确保在重组后保留状态,以及在重新创建 activity 或进程后保留状态。

  • 添加到 Bundle 的所有数据类型都会自动保存。
  • 添加 @Parcelize 注解的数据类型
  • 使用 mapSaver 定义自己的规则
  • ListSaver 将其索引用作键,可避免为映射定义键

(重看)Compose 中的状态容器

Compose 会使用该类的 equals 实现来确定key是否已发生变化,并使存储的值无效。

提升状态的场景

界面状态是描述界面的属性。界面状态有两种类型:

  • 屏幕界面状态是需要在屏幕上显示的内容。
  • 界面元素状态是指界面元素的固有属性,这些属性会影响界面元素的呈现方式。

应用中的逻辑可以是业务逻辑或界面逻辑:

界面逻辑

  • 以可组合项作为状态所有者
  • 不需要状态提升
  • 在可组合项中提升
  • 以普通状态容器类作为状态所有

业务逻辑

  • ViewModel 作为状态所有者
  • 屏幕界面状态
  • 属性深入分析
  • 界面元素状态

保存界面状态

界面逻辑

  • 使用 rememberSaveable保留状态,通过保存的实例状态机制将界面元素状态存储在 Bundle 中,不应在 Bundle 中存储大型复杂对象或对象列表
    业务逻辑
  • SavedStateHandle(最佳实践),是一个键值对映射,用于通过 set() 和 get() 方法向已保存的状态写入数据以及从中检索数据。

架构

单向数据流模式UDF

在该模式下状态向下流动,事件向上流动。
为了促进分离和重复使用,每个可组合项都应包含尽可能少的信息。

Compose 中的事件

架构分层

Jetpack Compose 的主要层包括:Runtime>UI>Foundation>Material
设计原则:提供可以组合在一起的重点突出的小块功能片段,而不是几个单体式组件。
优点:控制,自定义,

CompositionLocal

是通过组合隐式向下传递数据的工具
可让您创建以树为作用域的具名对象,这可以用作让数据流经界面树的一种隐式方式。
MaterialTheme 对象提供了三个 CompositionLocal 实例:colorScheme、typography 和 shapes
MaterialTheme.colorScheme.primary

创建自己的CompositionLocal
创建:internal val LocalElevations = staticCompositionLocalOf { Elevations() }
提供值:CompositionLocalProvider(LocalElevations provides elevation) {}
使用:LocalElevations.current

替代方式:传递显式参数,控制反转

导航

您创建的每个 NavHost 各自都有对应的 NavController。NavController 提供对 NavHost 的图表的访问权限。

DeepLink深链接是一个能够直接到达app中特定目的地的链接,可以导航到任意图表的任意 URI,甚至可以跨模块导航

与底部导航栏集成

应用布局

布局基础知识

  • 父节点会在其子节点之前进行测量,但会在其子节点的尺寸和放置位置确定之后再对自身进行调整。
  • 由于测量和放置是布局传递的不同子阶段,因此任何仅影响项的放置而不影响测量的更改都可以单独执行。
  • BoxWithConstraints可獲取父项的约束条件并相应地设计布局
  • 基于槽位的布局: TopAppBar、BottomAppBar、FloatingActionButton 和 Drawer、Scaffold

修饰符

  • 修饰符函数的顺序非常重要
  • requiredSize:如果您希望可组合项的尺寸固定不变,而不考虑传入的约束条件。布局系统会假定子项遵守这些约束条件,将子项居中放置在父项分配的空间中。开发者可以通过对子项应用 wrapContentSize 修饰符来替换此居中行为。

Compose 中的作用域安全

matchParentSize与fillMaxSize的差别

提取和重复使用修饰符

  • 在观察频繁变化的状态时提取和重复使用修饰符:频繁变化的状态(如动画状态或 scrollState)
  • 提取和重复使用未限定作用域的修饰符:建议对所有潜在的重要项目使用完全相同的修饰符
  • 提取和重复使用限定作用域的修饰符:将其提取到尽可能高的级别,并在适当的情况下重复使用

进一步链接提取的修饰符

  • .then()

约束条件和修饰符顺序

界面树中的修饰符

修饰符是一层一层包裹,最上面依次包裹下面,最后包裹布局节点

布局阶段中的约束条件

三步算法来查找每种布局 节点的宽度、高度以及 x、y 坐标:

1. 测量子节点:节点会测量其子节点(如果有)。
2. 确定自己的尺寸:节点根据这些测量结果确定自己的尺寸。
3. 放置子节点:每个子节点都是相对于节点自身的节点放置的 排名。

限制条件的类型: 受限,无界限,完全匹配,组合
如何将约束条件从父项传递给子项

布影响限制条件的修饰符

  • 串联多个 size 修饰符
  • requiredSize 修饰符:将替换 传入约束条件,并将您指定的尺寸作为确切边界进行传递
  • width 和 height 修饰符
  • sizeIn 修饰符
  • fillMaxSize 修饰符会更改约束条件
  • wrapContentSize 修饰符会重置最小约束条件,具有特殊的属性。它会获取其子项,并将其放置在传递给它的可用最小边界的中心。因此,它向父级传递的大小等于最小边界。
  • padding 修饰符会降低最大约束条件

**會更改約束條件的:fillMaxSize,wrapContentSize,requiredSize

创建自定义修饰符

将现有修饰符链接在一起

使用可组合项修饰符工厂,创建自定义修饰符。请勿中断修饰符链。您必须始终引用 this,否则之前添加的任何修饰符都会被舍弃。

- 系统绝不会跳过可组合函数修饰符
-  必须在可组合函数内调用可组合函数修饰符

使用 Modifier.Node 实现自定义修饰符行为

分为三个部分:

  1. 用于存储修饰符的逻辑和状态的 Modifier.Node 实现。
  2. 用于创建和更新修饰符节点实例的 ModifierNodeElement。
  3. 如上所述的可选修饰符工厂。

尋呼機Pager

  • HorizontalPager
  • VerticalPager
  • 延迟创建

流式布局

FlowRow 和 FlowColumn
延迟加载流程项ContextualFlowRow 和 ContextualFlowColumn
權重
fillMaxWidth:占整个容器宽度比例,和Row中區別(占据剩余 Row 宽度的比例)
fillMaxColumnWidth() 和 fillMaxRowHeight()

自定义版式

每个元素在其父元素中都有一个位置,指定为 (x, y) 位置;也都有一个尺寸,指定为 width 和 height。
父元素定义其子元素的约束条件。元素需要在这些约束条件内定义尺寸。
只能在测量和布局传递期间测量布局,并且只能在布局传递期间(且仅在已进行测量之后)才能放置子项。
由于 Compose 作用域(如 MeasureScope 和 PlacementScope),此操作在编译时强制执行。

在界面树中布置每个节点的过程分为三个步骤

  1. 测量所有子项
  2. 确定自己的尺寸
  3. 放置其子项

使用布局修饰符

可以使用 layout 修饰符来修改元素的测量和布局方式,仅更改调用可组合项

创建自定义布局

如需测量和布置多个可组合项,请改用 Layout 可组合项

布局方向

LocalLayoutDirection,更改可组合项的布局方向

自适应布局

规范布局

  • 列表详情
  • 信息流
  • 辅助窗格

支持不同的显示屏尺寸

可组合项分类:

  1. 应用级可组合项
  2. 内容级可组合项
  3. 个别可组合项

灵活的嵌套可组合项可以重复使用

  • 如果您想更改内容的显示位置或方式,请使用一系列修饰符或自定义布局来构建自适应布局
  • 如果您想更改显示的内容,请使用 BoxWithConstraints 作为更强大的替代方案。

确保所有数据在不同屏幕尺寸下都可以呈现

始终传递足够的内容,可通过降低自适应布局的有状态程度来使其更简单,并可以避免在不同显示屏尺寸之间切换所带来的负面影响。
这一原则还可以在布局发生变化时保留状态。

使用窗口大小类别

Compact,Medium,Expanded
600dp ≤ width < 840dp
480dp ≤ height < 900dp

支持多窗口模式

  • 分屏模式
  • 启动 adjacent
  • 多窗口模式下的 activity 生命周期
  • 专属资源访问权限:onTopResumedActivityChanged,摄像头的处理等
  • 多屏幕
    • activity 上下文与应用上下文:您应始终使用 activity 上下文(或其他基于界面的上下文)来获取有关当前窗口或显示屏的信息
    • 刘海屏
    • 辅助显示屏
    • 多显示屏支持:软件键盘,壁纸,启动器
  • 多窗口模式配置

支持桌面窗口化

构建自适应导航

NavigationSuiteScaffold

构建列表详情布局

NavigableListDetailPaneScaffold

构建辅助窗格布局

NavigableSupportingPaneScaffold

自适应注意事项

可折叠设备

响应式/自适应设计

  • 响应式设计:BoxWithConstraints:让应用在各种尺寸的显示屏上呈现良好的视觉效果和运行效果
  • 自适应设计:支持可折叠设备的折叠和展开屏幕,布局需要进行自适应
  • 可折叠设备状态和折叠状态:完全展开,桌面折叠状态,图书折叠状态
  • 应用连续性:重构应用布局时恢复状态
  • 多任务处理:分屏模式,桌面窗口模式
  • 拖放:

让应用具备折叠感知能力

  • 窗口信息
  • 桌上模式,图书模式

支持可折叠显示屏模式

  • 双屏幕模式
  • 后置显示屏模式

对齐线

使用 AlignmentLine 创建自定义对齐线,供父布局用来对齐和定位其子项。

固有特性测量

Compose 有一项规则,即,子项只能测量一次,测量两次就会引发运行时异常。
借助固有特性,您可以先查询子项,然后再进行实际测量。

ConstraintLayout

  • 为了避免在屏幕上定位元素时嵌套多个 Column 和 Row,以便提高代码的可读性。
  • 相对于其它可组合项来定位可组合项,或根据引导线、屏障线或链来定位可组合项。
  • 使用ConstraintSet将约束条件与应用它们的布局分离开来

createRefs,constrainAs,linkTo,createRefFor,layoutId
createGuidelineFromStart,createTopBarrier

图像和图形

圖像

使用 painterResource 不会将图片缩放至屏幕上可见可组合项的大小。如果在较小的可组合项中包含较大的图片,请务必使用图片加载库,以便将图片缩小以适应边界。

优化图片的性能

  • 请仅加载所需大小的位图
  • 尽可能使用矢量而非位图
  • 针对不同屏幕尺寸提供备用资源
  • 使用 ImageBitmap 时,请先调用 prepareToDraw,然后再绘制
  • 好将 Int DrawableRes 或网址作为参数(而不是 Painter)传递给可组合项
  • 避免位图在内存中存储的时间超过所需要的时间
  • 请勿将大型图片与 AAB/APK 文件打包在一起

圖形

drawWithContent,
drawBehind:在可组合项后面绘制内容,
drawWithCache
Canvas 可组合项

基本转换

这些转换仅适用于可组合项生命周期的绘制阶段,更改尺寸或位置并不会更改布局的尺寸和位置。如果元素超出自己的布局尺寸和位置,可能会在其他元素上绘制。

  • 缩放:DrawScope.scale()
  • 平移:DrawScope.translate()
  • 旋转:DrawScope.rotate()
  • 边衬区:DrawScope.inset()
  • 多个转换:DrawScope.withTransform()

常用绘制操作

绘制文本:DrawScope.drawText()
textMeasurer.measure:调整约束条件、字体大小或任何会影响测量尺寸的属性,系统就会报告新的尺寸
绘制图片:drawImage()
绘制基本形状:drawCircle(),drawRect(),drawRoundedRect(),drawLine(),drawOval(),drawArc(),drawPoints()
绘制路径:drawPath()
drawIntoCanvas():获取 DrawScope的Canvas 对象的访问权限

绘制修饰符

drawWithContent:在其中确定可组合项的绘制顺序以及从修饰符内发出的绘制命令,请务必调用 drawContent 来渲染可组合项的实际内容
drawBehind: 绘制顺序设为可组合项内容的后方
drawWithCache:会在其内部调用 onDrawBehind 或 onDrawWithContent,并提供一种机制来缓存在其中创建的对象。

图形修饰符

Modifier.graphicsLayer:不会更改可组合项的测量尺寸或位置,因为它只会影响绘制阶段

  • 缩放 :scaleX 和 scaleY
  • 平移:translationX 和 translationY
  • 旋转:rotationX 设为水平旋转,将 rotationY 设为垂直旋转,将 rotationZ 设为绕 Z 轴旋转
  • 原点:transformOrigin,然后将其用作转换的发生点
  • 裁剪和形状:graphicsLayer
  • alpha 值:当 alpha 设为小于 1.0f 时,图层的整个内容都会绘制到屏幕外缓冲区
  • CompositingStrategy:BlendMode.Clear,CompositingStrategy.Offscreen 会创建一个屏幕外纹理来执行命令(仅将 BlendMode 应用于此可组合项的内容)。然后,会在屏幕上已渲染的内容之上进行渲染,而不会影响已绘制的内容
  • ModulateAlpha: CompositingStrategy.ModulateAlpha此合成策略会调整 graphicsLayer 中记录的每条绘制指令的 alpha 值

将可组合项的内容写入位图

rememberGraphicsLayer(),graphicsLayer.record(),graphicsLayer.toImageBitmap()

自定义绘制修饰符

实现 DrawModifier 接口

画笔

Brush:渐变和着色器

Brush.horizontalGradient(colorList)
Brush.linearGradient(colorList)
Brush.verticalGradient(colorList)
Brush.sweepGradient(colorList)
Brush.radialGradient(colorList)

TileMode
TileMode.Repeated:边缘从上一种颜色到第一种颜色重复。
TileMode.Mirror:边缘按从最后一种颜色到第一种颜色的顺序镜像。
TileMode.Clamp:边缘固定为最终的颜色。然后,它会将区域的剩余空间绘制为距离最近的颜色。
TileMode.Decal:仅在边界大小的范围内渲染。

更改笔刷大小

扩展 Shader 并在 createShader 函数中利用绘制区域大小

使用图片作为画笔

val imageBrush = ShaderBrush(ImageShader(ImageBitmap.imageResource(id = R.drawable.dog)))

  • background(imageBrush)
  • TextStyle(brush = imageBrush,
  • Canvas(onDraw = {drawCircle(imageBrush)

高级示例:自定义画笔

形状

RoundedPolygon:对多边形的角进行圆化处理
Morph: 变形形状
使用多边形作为裁剪
点击时变形按钮
无限循环形状变形动画
自定义多边形

動畫

选择动画 API表格

动画快速指南

以动画形式出现 / 消失

  1. AnimatedVisibility :会从组合中移除该项
  2. animateFloatAsState 在时间轴上为 Alpha 添加动画效果:会保留在组合中,并继续占用其布局的空间

为可组合项的大小添加动画效果

  • animateContentSize()
  • 还可以使用 AnimatedContent 和 SizeTransform 来描述大小变化的发生方式

为可组合项的位置添加动画效果

  • Modifier.offset{ } 組合 animateIntOffsetAsState():这不会更改父级感知的可组合项的放置位置
  • Modifier.layout{ }:此修饰符会将尺寸和位置更改传播到父项,然后父项会影响其他子项

为可组合项的内边距添加动画

  • animateDpAsState 与 Modifier.padding() 结合

为可组合项添加高度动画

  • 结合使用 animateDpAsState 与 Modifier.graphicsLayer
  • 对于一次性的高度更改,请使用 Modifier.shadow()
  • 使用 Card 可组合项,并将 elevation 属性设置为每种状态的不同值

为文本缩放、平移或旋转添加动画效果

  • 使用 Modifier.graphicsLayer{ },textMotion 参数设置为 TextMotion.Animated,

为文本添加动画效果

  • 对 BasicText 可组合项使用 color lambda

在不同类型的内容之间切换

  • 使用 AnimatedContent 在不同的可组合项之间添加动画效果
  • 如果您只希望在可组合项之间呈现标准淡出效果,请使用 Crossfade

在导航到不同目的地时添加动画

  • 为可组合项指定 enterTransition 和 exitTransition

重复动画

  • 将 rememberInfiniteTransition 与 infiniteRepeatable animationSpec 搭配使用,可让动画不断重复播放。更改 RepeatModes 以指定其应该如何来回移动

在启动可组合项时启动动画

  • 用LaunchedEffect 驱动动画状态更改。将 Animatable 与 animateTo 方法结合使用

创建顺序动画

  • LaunchedEffect中依次对 Animatable 调用 animateTo

创建并发动画

  • LaunchedEffect中launch多個調用動畫函數
  • 可以使用 updateTransition API 使用同一状态,同时驱动多个不同的属性动画。

优化动画性能

  • 请尽可能选择 Modifier 的 lambda 版本,这会跳过重组并在组合阶段之外执行动画
  • 否则请使用 Modifier.graphicsLayer{ },因为此修饰符始终在绘制阶段运行

更改动画时间设置

  • 设置animationSpec 来自定义动画的运行方式

动画修饰符和可组合项

AnimatedVisibility

  • 兩鍾構造方法,參數visible和參數visibleState
  • 通过指定 EnterTransition 和 ExitTransition 来自定义这种过渡效果
  • animateEnterExit 修饰符为每个子项指定不同的动画行为
  • 添加自定义动画:通过 AnimatedVisibility 的内容 lambda 内的 transition 属性访问底层 Transition 实例,将与 AnimatedVisibility 的进入和退出动画同时运行。AnimatedVisibility 会等到 Transition 中的所有动画都完成后再移除其内容

AnimatedContent

会在内容根据目标状态发生变化时,为内容添加动画效果

  • transitionSpec 参数指定 ContentTransform 对象,以自定义此动画行为
  • +,togetherWith,using,SizeTransform
  • SizeTransform 定义了大小应如何在初始内容与目标内容之间添加动画效果,还可控制在动画播放期间是否应将内容裁剪为组件大小
  • animateEnterExit 修饰符可将 EnterAnimation 和 ExitAnimation 分别应用于每个直接或间接子项
  • 添加自定义动画:同AnimatedVisibility一样
  • Crossfade 可使用淡入淡出动画在两个布局之间添加动画效果

animateContentSize

  • animateContentSize 在修饰符链中的位置顺序很重要。为了确保流畅的动画,请务必将其放置在任何大小修饰符(如 size 或 defaultMinSize)前面

延迟布局项动画

  • animateItem

animate*AsState

为单个值添加动画效果
Compose 为 Float、Color、Dp、Size、Offset、Rect、Int、IntOffset 和 IntSize 提供开箱即用的 animate*AsState 函数

Transition

  • 可管理一个或多个动画作为其子项,并在多个状态之间同时运行这些动画
  • animate* 扩展函数来定义此过渡效果中的子动画
  • transitionSpec 参数,为过渡状态变化的每个组合指定不同的 AnimationSpec
  • 初始状态与第一个目标状态不同:结合使用 updateTransition 和 MutableTransitionState 来实现
  • 对于涉及多个可组合函数的更复杂的过渡,可使用 createChildTransition 来创建子过渡

transition 与 AnimatedVisibility 和 AnimatedContent 搭配使用

  • transition.AnimatedVisibility
  • transition.AnimatedContent

rememberInfiniteTransition

创建无限重复的动画

低级别动画 API

Animatable:基于协程的单值动画

与 animate*AsState 相比,使用 Animatable 可以直接对以下几个方面进行更精细的控制:

  1. Animatable 的初始值可以与第一个目标值不同
  2. Animatable 对内容值提供更多操作(即 snapTo 和 animateDecay)
  • splineBasedDecay 一般只能用于 像素的变化,因为这个东西可以针对不同像素密度的设备而变化
  • exponentialDecay 这个就是典型的不会根据像素密度变化而变化,比如颜色,角度之类的

Animation:手动控制的动画

Animation 只能用于手动控制动画的时间。Animation 是无状态的,它没有任何生命周期概念。它充当更高级别 API 使用的动画计算引擎。

  • TargetBasedAnimation:可以直接让您自己控制动画的播放时间
  • DecayAnimation:不需要提供 targetValue,而是根据起始条件(由 initialVelocity 和 initialValue 设置)以及所提供的 DecayAnimationSpec 计算其 targetValue

自定义动画

使用 AnimationSpec 参数自定义动画

  • spring:创建基于物理特性的动画。dampingRatio 定义弹簧的弹性;stiffness: 定义弹簧应向结束值移动的速度
  • tween:在起始值和结束值之间添加动画效果,并使用缓和曲线。
  • keyframes:在特定时间点以动画方式呈现特定值
  • keyframesWithSplines:在关键帧之间流畅地添加动画
  • repeatable 重复动画:repeatMode(RepeatMode.Restart、RepeatMode.Reverse)
  • infiniteRepeatable无限重复动画:
  • snap 立即跳转到结束值:
  • Easing自定义缓动函数:使用 Easing 来调整动画的小数值。这样可让动画值加速和减速,而不是以恒定的速率移动
  • 一些内置的Easing:FastOutSlowInEasing、LinearOutSlowInEasing、FastOutLinearEasing、LinearEasing、CubicBezierEasing

转换为 AnimationVector,来为自定义数据类型添加动画效果

在动画播放期间,任何动画值都表示为 AnimationVector

  • 使用相应的 TwoWayConverter 即可将值转换为 AnimationVector:AnimationVector1D,AnimationVector2D,AnimationVector3D,AnimationVector4D
  • 一些内置 VectorConverter:Color.VectorConverter,Dp.VectorConverter,Offset.VectorConverter,Int.VectorConverter,Float.VectorConverter,IntSize.VectorConverter

共享元素转换

创建共享元素:SharedTransitionLayout,Modifier.sharedElement(),Modifier.sharedBounds()
在 Compose 中创建共享元素时,一个重要的概念是共享元素如何与叠加层和剪裁功能搭配使用
请务必将SharedTransitionLayout放置在界面层次结构中包含要共享的元素的同一顶级位置。

sharedElement共享元素

  • 在修饰符链中的 sharedElement() 之前放置您不希望分享的任何内容。
  • sharedElement在修饰符链中的位置

sharedBounds共享边界

  • sharedBounds() 适用于视觉上不同的内容,但在不同状态之间应共享相同的区域,而 sharedElement() 则要求内容保持一致
  • 使用 Text 可组合项时,最好使用 sharedBounds() 来支持字体更改

AnimatedContent与sharedElement、sharedBounds组合使用

AnimatedVisibility与sharedElement、sharedBounds组合使用

了解镜重

  • 如果您有多个要跟踪的镜重或深层嵌套的层次结构,请使用 CompositionLocals

修饰符排序

请与匹配项的修饰符顺序保持一致。将大小修饰符放在共享元素修饰符后面,但使用 requiredSize() 时除外
如果 requiredSize() 位于共享元素修饰符之前,则 requiredSize() 的父元素永远无法观察共享元素 animatedSize。

唯一键

  • 在处理复杂的共享元素时,最好创建非字符串的键,因为字符串在匹配时容易出错
  • 每个键都必须是唯一的,这样才能进行匹配
  • 建议为键使用数据类,因为它们会实现 hashCode() 和 isEquals()。

手动管理共享元素的可见性

  • 如果您可能不使用 AnimatedVisibility 或 AnimatedContent,则可以自行管理共享元素的公开范围
  • 使用 Modifier.sharedElementWithCallerManagedVisibility() 并提供您自己的条件,以确定何时应显示或不显示项
  • 即使 visible == false,共享元素仍会保留在界面树中,建议您在转换完成后,通过观察 SharedTransitionScope.isTransitionActive 从树中移除不显示 的共享元素。

自定义共享元素过渡

动画规范

如需更改用于大小和位置移动的动画规范,您可以在 Modifier.sharedElement() 上指定不同的 boundsTransform 参数
指定sharedBounds的 boundsTransform 参数

调整大小模式

ScaleToBounds:对于 Text 可组合项,建议使用 ScaleToBounds,因为它可以避免将文本重新布局并重新流动到不同的行。
RemeasureToBounds:如果边界具有不同的宽高比,并且您希望两个共享元素之间保持流畅的连续性,建议使用 RemeasureToBounds。

跳至最终布局

添加 Modifier.skipToLookaheadSize() 可防止随着内容增加而发生重新流布局(随着容器大小的增大,文本会在进入时重新流动)。

剪辑和叠加层

  • 如需将共享元素剪裁为形状,请使用标准的 Modifier.clip() 函数。将其放在 sharedElement() 后面
  • 如果您需要确保共享元素绝不会在父容器之外呈现,可以对 sharedElement() 设置 clipInOverlayDuringTransition
  • 如需支持在共享元素过渡期间始终将特定界面元素(例如底部栏或悬浮操作按钮)保持在顶部,请使用 Modifier.renderInSharedTransitionScopeOverlay()
  • 希望非共享可组合项在转换前以动画效果消失,同时保持在其他可组合项之上,请在共享元素转换运行时使用 renderInSharedTransitionScopeOverlay().animateEnterExit() 为可组合项添加动画效果
  • 希望共享元素不渲染在叠加层中,可以将 sharedElement() 上的 renderInOverlayDuringTransition 设置为 false

通知同级布局共享元素大小的更改

默认情况下,sharedBounds() 和 sharedElement() 不会在布局转换时通知父级容器任何大小更改。
为了在父级容器转换时将大小更改传播到父级容器,请将 placeHolderSize 参数更改为 PlaceHolderSize.animatedSize。这样做会导致项放大或缩小。布局中的所有其他项都会响应此更改

常见共享元素用例

图片AsyncImage

  • 建议将 placeholderMemoryCacheKey() 和 memoryCacheKey() 设置为与从共享元素键派生的字符串相同的键,以便匹配的共享元素具有相同的缓存键

文本Text

  • 请使用 Modifier.sharedBounds()、resizeMode = ScaleToBounds()
  • 默认情况下,TextAlign 更改不会呈现动画效果。请改为使用 Modifier.wrapContentSize() 或 Modifier.wrapContentWidth() 来处理共享转场效果,而不是使用不同的 TextAlign

使用 Navigation Compose 构建共享元素

  • SharedTransitionLayout 包住NavHost,子組合佈局中用with(sharedTransitionScope) , sharedElement
  • 使用共享元素的预测性返回

其他示例

SharedElementWithCallerManagedVisibility:用戶操作,隱藏顯示切換動畫

动画矢量图像

  • AnimatedVectorDrawable 资源:AnimatedImageVector.animatedVectorResource()
  • 将 ImageVector 与 Compose 动画 API 搭配使用
  • Lottie 等第三方解决方案

触控和输入操作

指针输入

  • 一些级别较高,旨在覆盖最常用的手势。例如,clickable ,提供无障碍功能,并在点按时显示视觉指示。
  • 一些在较低级别提供更大的灵活性,例如 PointerInputScope.detectTapGestures 或 PointerInputScope.detectDragGestures,但不提供额外功能。

了解手势

  • 指针:可用于与应用交互的实体对象。
  • 指针事件:描述一个或多个指针的低级交互与应用共享的时间。
  • 手势:可解读为单个事件的一系列指针事件操作。

组件支持

  • Compose 中的许多开箱即用组件都包含某种内部手势 处理。此类手势处理会自动运行。
  • 如果适合您的应用场景,则应首选组件中包含的手势, 包含对焦点和无障碍功能的开箱即用型支持,并且 经过充分测试。
    自定义手势,可以使用 pointerInput 修饰符

使用修饰符 向任意可组合项添加特定手势

  • 可以将手势修饰符应用于任意可组合项, 可组合监听手势:clickable
  • 使用 clickable 处理点按和按下操作, combinedClickable、selectable、toggleable 和 triStateToggleable 修饰符。
  • 使用 horizontalScroll 处理滚动, verticalScroll 以及更通用的 scrollable 修饰符。
  • 使用 draggable 和 swipeable 处理拖动操作修饰符。
  • 处理多点触控手势,例如平移、旋转和缩放 transformable 修饰符。

自定义手势 使用 pointerInput 修饰符向任意可组合项添加

  • pointerInput当 其中一个键的值发生变化,那么辅助键内容 lambda 就是 已重新执行
  • awaitPointerEventScope 会创建一个协程作用域,可用于 等待指针事件
  • awaitPointerEvent 会挂起协程,直到发生下一个指针事件 。

监听特定手势

  • 按住、点按、点按两次和长按:detectTapGestures
  • 拖动:detectHorizontalDragGestures, detectVerticalDragGestures、detectDragGestures和 detectDragGesturesAfterLongPress
  • 转换:detectTransformGestures
    添加多个手势监听器可组合项,请改用单独的 pointerInput 修饰符实例

等待特定事件或子手势

  • 挂起,直到通过 awaitFirstDown 按下指针,或者等待所有 点击 waitForUpOrCancellation 即可向上移动。
  • 创建低级拖动监听器 ,awaitTouchSlopOrCancellation,awaitDragOrCancellation,awaitHorizontalTouchSlopOrCancellation,awaitHorizontalDragOrCancellation,awaitVerticalTouchSlopOrCancellation, awaitVerticalDragOrCancellation
  • 暂停,直到长按 awaitLongPressOrCancellation 为止。
    -使用 drag 方法持续监听拖动事件,或 horizontalDrag 或 verticalDrag,用于在一个设备上监听拖动事件 轴。

计算多接触点事件
如果使用 transformable 修饰符或 detectTransformGestures 方法没有针对用例提供足够精细的控制,您可以 监听原始事件并对其应用计算。
这些辅助方法 是calculateCentroid、calculateCentroidSize、 calculatePan、calculateRotation 和 calculateZoom。

事件调度和点击测试

并非所有指针事件都会发送到每个 pointerInput 修饰符

  • 指针事件会分派给一个可组合项层次结构。
  • 默认情况下,如果多个符合条件的可组合项位于同一级别上, 则只有 Z-index 最高的可组合项才是“hit”(可以通过创建自己的 PointerInputModifierNode 来替换此行为。 并将 sharePointerInputWithSiblings 设置为 true)
  • 同一指针的后续事件也会分派到 可组合项和数据流(根据事件传播逻辑)

事件消耗

  • 编写自己的自定义手势,则必须手动处理事件。您需要执行此操作 使用 PointerInputChange.consume 方法
  • 使用事件不会停止事件传播到其他可组合项,可组合项需要明确忽略已使用的事件

事件传播

指针事件会流经每个可组合项三次 "通过"

  • 在 initial 初始传递 中,事件从界面树顶部流向 底部。此流程允许父级在子级之前拦截事件 消耗掉。
  • 在 main 主通道 中,事件从界面树的叶节点流向界面树的根目录这个阶段是你通常使用手势的阶段, 在监听事件时默认传递。
  • 在 Final Pass 中,事件从界面顶部再次流动 传递给叶节点。该流程允许堆栈中较高级别的元素 响应其父级使用的事件。

点按并按下

  • 点按并按下:onClick lambda
  • 响应点按或点击操作:clickable 是一个常用的修饰符,可让可组合项对 轻按或点击
  • 长按可显示上下文菜单:combinedClickable 可让您在以下元素中添加点按两次或长按行为: 正常点击行为以外的功能
  • 点按纱罩以关闭可组合项:直接使用 pointerInput 修饰符 与 detectTapGestures 方法结合使用,沒有onclick的特效

点按两次即可缩放

有时,clickable 和 combinedClickable 提供的信息不足 以正确的方式响应互动
可以结合使用 pointerInput 修饰符 并使用 detectTapGestures 實現點按處理

滚动

滚动修饰符

  • verticalScroll 和 horizontalScroll

可滚动的修饰符

  • scrollable 修饰符与滚动修饰符不同,区别在于 scrollable 可检测滚动手势并捕获增量,但不会自动偏移其内容。
  • rememberScrollableState

嵌套滚动

自动嵌套滚动

  • 启动滚动操作的手势会自动从子级传播到父级,这样一来,当子级无法进一步滚动时,手势就会由其父元素处理。
  • 原生支持自动嵌套滚动,包括:verticalScroll、horizontalScroll、scrollable、Lazy API 和 TextField

使用 nestedScroll 修饰符

  • 对于不可自动滚动的可组合项(例如 Box 或 Column),此类组件上的滚动增量不会在嵌套滚动系统中传播,并且增量不会到达 NestedScrollConnection 或父组件。 您可以使用 nestedScroll 向其他组件(包括自定义组件)提供此类支持。
  • 嵌套滚动周期的各个阶段:滚动前、节点消耗和滚动后。
  • nestedScroll 修饰符是一种拦截并插入这些更改的方法,并影响在链中传播的数据(滚动增量),此修饰符可放置在层次结构中的任意位置
  • 此修饰符的构建块是 NestedScrollConnection 和 NestedScrollDispatcher
  • NestedScrollDispatcher 会初始化嵌套滚动周期:nestedScrollDispatcher.dispatchPreScroll(),nestedScrollDispatcher.dispatchPostScroll()
  • NestedScrollConnection 提供了一种响应嵌套滚动周期各个阶段并影响嵌套滚动系统的方法:NestedScrollConnection-onPreScroll(),NestedScrollConnection-onPostScroll(),

嵌套滚动互操作性

任何可滚动容器都必须通过 NestedScrollConnection 作为父项参与嵌套滚动链,并通过 NestedScrollDispatcher 作为子项参与嵌套滚动链

包含子级 ComposeView 的协作式父级

包含子级 AndroidView 的父级可组合项

包含子级 ComposeView 的非协作式父级 View

拖动

  • 单一方向拖动手势:draggable 修饰符
  • 多个方向拖动手势,通过 pointerInput 修饰符使用detectDragGestures检测器

滑动和快速滑动

  • anchoredDraggable:通常朝一个方向定义的两个或多个锚点呈现动画效果

多点触控:平移、缩放、旋转

鍵盤輸入

突出焦點

用戶互動

  • 当用户与界面组件互动时,系统会生成多个 Interaction 事件来表示其行为。
  • 这些互动通常成对出现,包含起始互动和结尾互动,。第二次互动包含对第一次互动的引用。
  • InteractionSource 提供互动的只读流,而 MutableInteractionSource 则允许您在数据流中添加新互动。
  • collectIsPressedAsState()、collectIsFocusedAsState()、collectIsDraggedAsState() 和 collectIsHoveredAsState()

使用 Indication 创建和应用可重复使用的自定义效果

  • Indication 表示可重复使用的视觉效果,Indication一分为二 部分:
  1. IndicationNodeFactory:用于创建 Modifier.Node 实例的工厂, 为组件渲染视觉效果对于 它可以是单一实例(对象),并且可以在整个 整个应用
  2. Modifier.indication: 一个修饰符,用于绘制Indication 组件。Modifier.clickable 和其他高级互动修饰符 直接接受指示参数,这样它们不仅能发出 Interaction,但也可以为其 Interaction 绘制视觉效果 emit。

迁移到 Indication API 和 Ripple API

  • ripple 设置,自定义

拖放

dragAndDropSource:将可组合项指定为拖动手势的起点
dragAndDropTarget:指定接受用户放下数据的可组合项

觸控筆輸入

複製粘貼

  • SelectionContainer
  • ClipboardManager:setText(),getText(),setClip()
  • ClipData,ClipEntry:剪贴板只能有一个 ClipEntry
posted @ 2025-04-01 15:02  jokerpoker  阅读(109)  评论(0)    收藏  举报