声明式 UI:不手动更新 UI
独立于平台(独立于最新版Android),Android Studio 预览功能
Compose 会跨到 iOS 吗?短期内希望渺茫。可支持桌面版、Web 版。多平台(multi-platform)
Coil
FrameLayout -> Box()
ScrollView -> Modifier.verticalScroll()
RelativeLayout -> Box()
ConstraintLayout -> 依然可以用
RecyclerView -> LazyColumn()
ScrollView -> Modifier.verticalScroll()
ViewPager -> Pager()
Modifier.clip(CircleShape).size(128.dp)
Modifier.background(Color.Green).padding(8.dp).background(Color.Red).padding(8.dp)
match_parent = Modifier.fillMaxWidth / Height / Size()
通用的设置用Modifier,专项的设置用函数参数。
点击监听器用Modifier
compile -> runtime -> ui -> animation -> foundation -> material
包依赖三条原则
- 依赖 material(3) 就够了;可能跳过 material 依赖 foundation
- 如果你需要 ui-tooling,需要单独依赖:提供预览功能
- 如果你需要 material-icons-extended,需要单独依赖
什么是 Composable,带 @Composable 注解的函数
自定义 Composable 的首字母大写
自定义 Composable 内部只能调用一个别的 Composable
自定义 Composable = 自定义View + XML布局
val name = mutableStateOf("Hello World")
setContext {
Text(name.value)
}
lifecycleScope.launch {
delay(3000)
name.value = "I do need a car."
}
// by 关键字:左边的对象由右边代理;自动订阅
// import getValue & setValue 扩展函数
var name by mutableStateOf("Hello World")
setContext {
Text(name)
}Recompose Scope
// remember:缓存作用,防止 Recompose 时重复初始化,建议全部包上;变量不在 Compose 函数内不需要 remember
// 用在可能进入 Recompose 的对象上,无法判断
val name by remember { mutableStateOf("Hello World") }
// 带参数版本的 remember
// remember(key...),key 一样用缓存Stateless
状态:控件的属性,比如 TextView 的 text
State Hoisting:状态提升,状态放入函数参数;尽量不往上提
Single Source of Truth
val name by remember { mutableStateOf("Hello World") }
TextField(name, { newValue ->
name = newValue
})Unidirectional Data Flow - 单向数据流
mutableStateOf()
mutableStateListOf()
mutableStateMapOf()
Recompose 的执行过程
Recompose Scope 重组范围
Compose 自动更新 -> 更新范围过大,超过需求 -> 智能优化跳过没必要的更新
可靠的类:使用 Structual Equality 结构性相等; Kotlin ==; Java equals
不可靠的类直接进入 Recompose
给不可靠的类加 @Stable 注解,告诉编译器插件此类型人工保证可靠
@Stable 注解的类不使用 data class,使用原始的 equals 方法保证可靠
@Stable 的稳定:
- 现在相等就永远相等:不要重写 equals 函数;不能使用 data class
- 当 var 修饰的公开属性改变的时候,通知到用这个属性的 Composition:var 使用 by mutableStateOf 代理
- 公开属性也必须是可靠类型或者稳定类型
// 实战中这样写
class User(name: String){
var name by mutableStateOf(name)
}derivedStateOf:convert one or multiple state objects into another state
https://developer.android.com/jetpack/compose/side-effects?hl=zh-cn
- 监听状态变化从而自动刷新有两种写法
- 带参数的 remember()
- 不带参数的 remember() + derivedStateOf()
- 对于状态对象来说,比如 mutableStateListOf() 带参数的 remember() 不可使用,无法监听内部状态的改变,只能使用 derivedStateOf()
- 对于函数参数里的字符串,由于监听链条会被掐断,不能用 derivedStateOf(),而只能用带参数的 remember()
带参数的 remember() 作用:Recompose 的时候判断参数是否同一个对象
derivedStateOf() 作用:适用于监听状态对象
var name by remember { mutableStateOf("AAA") }
val processedName by remember { derivedStateOf { name.uppercase() } }
val processedName = remember(name) { name.uppercase() }
Text(processedName)
var names by remember { mutableStateListOf("AAA", "BBB") }
// 放入自定义 Composable 内需要传入 State 状态对象而不能是 String,不实用
val processedNames by remember { derivedStateOf { names.map { it.uppercase() } } }
// 无效,remember 记录的 names 对象不变,自己跟自己比较
// 原因:String 的改变只有一种方式,重新赋值;List 的改变除了赋值,主要是内部元素的改变
val processedNames = remember(names) { names.map { it.uppercase() } }
Column{
for(processedName in processedNames){
Text(processedName)
}
}CompositionLocal:具有穿透函数功能的局部变量,不需要显示传递的函数参数;无形传参
CompositionLocalProvider(LocalName provides "seckill") {
User()
}
@Composable
fun User() {
Text(LocalName.current)
}
// 缩小刷新范围,经常改变时使用
val LocalName = compositionLocalOf { error("no default value.") }
// 消除记录,但是能全量刷新,计算量大,不经常改变时使用
val LocalName = staticCompositionLocalOf { error("no default value.") }
CompositionLocalProvider(LocalBackground provides Color.Yellow) {
TextWithBackground()
}
@Composable
fun TextWithBackground(){
Surface(color = LocalBackground.current){
Text("有背景的文字")
}
}
LocalBackground.current
LocalContext.current// var size by remember { mutableStateOf(48.dp) }
var big by mutableStateOf(false)
setContent{
var size by animateDpAsState(if(big) 96.dp else 48.dp)
Box(
Modifier.size(size)
.background(Color.Green)
.clickable{
big = !big
}
)
}var big by mutableStateOf(false)
setContent{
val size = remember(big) { if(big) 96.dp else 48.dp }
val anim = remember { Animatable(size, Dp.VectorConverter) }
LaunchedEffect(big){
anim.snapTo(if(big) 192.dp else 0.dp)
anim.animateTo(size)
}
Box(
Modifier.size(anim.value)
.background(Color.Green)
.clickable{
big = !big
}
)
}补间动画
Easing:缓动
FastOutSlowInEasing 快出慢进,先加速再减速;从A状态变成B状态
LinearOutSlowInEasing 适合元素入场动画;全程减速;
FastOutLinearInEasing 适合元素出场动画;全程加速;
LinearEasing 全程匀速运动
var big by mutableStateOf(false)
setContent{
val size = remember(big) { if(big) 96.dp else 48.dp }
val offset = remember(big) { if(big) (-48).dp else 148.dp }
val anim = remember { Animatable(size, Dp.VectorConverter) }
val offsetAnim = remember { Animatable(offset, Dp.VectorConverter) }
LaunchedEffect(big){
anim.snapTo(if(big) 192.dp else 0.dp)
anim.animateTo(size, spring(Spring.DampingRatioMediumBouncy))
anim.animateTo(size, TweenSpec(easing = LinearEasing))
offsetAnim.animateTo(offset, TweenSpec(easing = FastOutLinearInEasing))
offsetAnim.animateTo(offset, tween())
}
Box(
Modifier.size(anim.value)
.offset(offsetAnim.value, offsetAnim.value)
.background(Color.Green)
.clickable{
big = !big
}
)
}// 和 anim.snapTo 作用一样,唯一区别可以延迟
offsetAnim.animateTo(offset, SnapSpec(3000))
offsetAnim.animateTo(offset, snap(3000))分段式 TweenSpec
anim.animateTo(size, KeyframesSpec(KeyframesSpec.KeyframesSpecConfig<Dp>().apply{
}))
// =
anim.animateTo(size, keyframes {
durationMillis = 450
delayMillis = 500
// 150ms的时候需要运行到144dp的位置
144dp at 150 with FastOutLinearInEasing
200dp at 300 with FastOutSlowInEasing
})无法设置时长
// dampingRatio 阻尼比
// stiffness 刚度
// visiblityThreshold 阈值
anim.animateTo(size, spring())
anim.animateTo(size, spring(Spring.DampingRatioMediumBouncy))
anim.animateTo(size, spring(0.1f, Spring.StiffnessHigh), 200.dp)// iterations
// animation
// repeatMode
// initialStartOffset 时间偏移,延迟或快进
anim.animateTo(size, repeatable(3, tween(), RepeatMode.Reverse, StartOffset(500, StartOffsetType.Delay)))// 无限循环,和 RepeatableSpec 本质上没有区别
anim.animateTo(size, infiniteRepeatable(tween(), RepeatMode.Reverse, StartOffset(500, StartOffsetType.Delay)))FloatTweenSpec 针对 Float 类型
FloatSpringSpec 针对 Float 类型
var big by mutableStateOf(false)
setContent{
val float = remember(big) { if(big) (-48).dp else 148.dp }
val floatAnim = remember { Animatable(float) }
LaunchedEffect(big){
floatAnim.animateTo(200f, tween())
floatAnim.animateTo(200f, spring())
}
}VectorizedAnimationSpec 系列是之前的 AnimationSpec 底层使用
DecayAnimationSpec
惯性衰减,初始速度需要精确指定
// initialVelocity 初始速度,像素
// animationSpec
// block
val decay = remember { ntialDecay<Dp>() }
anim.animateDecay(100.dp, decay)
// frictionMultiplier 摩擦力系数
// absVelocityThreshold 速度阈值绝对值,速度绝对值小于阈值动画直接结束
// 指数型衰减,不会针对像素密度修正,面向Dp;使用范围广
// 由于没有用到 density(用户可以在系统调节) 所以没有带remember版本;但还是需要自己包一层remember避免对象重复创建
exponentialDecay<Dp>()
// 用来实现惯性滑动
splineBasedDecay<Int>(LocalDensity.current)
// 建议直接用带 remember 版本,不同屏幕像素手机滑动距离不一样;根据像素密度修正,Dp依然修正;所以只能使用像素不能使用Dp,面向像素;
rememberSplineBasedDecay()animateTo 和 animateDecay 都有 block 参数
val decay = remember { ntialDecay<Dp>() }
anim.animateDecay(100.dp, decay) {
// 每一帧刷新都会执行,相当于对动画的监听
// 可以让 A 动画更新实时值传递给 B 动画
println("hhh")
}Animatable
// animateDecay animateTo snapTo 可以相互打断;新动画打断旧动画
// anim.stop() 主动打断,不能和要被掐断的动画同一个协程
// anim.updateBounds() 设置边界;动画边界触达;正常结束不会抛异常
var big by mutableStateOf(false)
setContent{
BoxWithConstraints{
LaunchedEffect(Unit){
delay(1000)
try{
val animResult = anim.animateDecay(2000.dp, decay)
if(animResult.endReason == AnimationEndReason.BoundReached){
// 反弹
animResult.animateDecay(-animResult.endState.velocity, decay)
}
} catch (e: CancellationException){
println("被打断了")
}
}
LaunchedEffect(Unit){
delay(1500)
anim.animateDecay(-1000.dp, decay)
}
anim.updateBounds(0.dp, upperBound = maxWidth - 100.dp)
// 可以分来设置边届,动画分开停止
animY.updateBounds(upperBound = maxHeight - 100.dp)
// 手动计算回弹值交给 padding
val paddingX = remember(anim.value){
val usedValue = anim.value
while (usedValue >= (maxWidth -100.dp) * 2) {
usedValue -= (maxWidth -100.dp) * 2
}
if(usedValue < maxWidth - 100.dp){
usedValue
} else {
(maxWidth -100.dp) * 2 - usedValue
}
}
}
}转场动画,状态切换的动画
val big by remember { mutableStateOf(false) }
// val bigTransition = updateTransition(big, "big")
val bigTransition = updateTransition(
MutableTransitionState(big).apply{ targetState = true }, "big")
// Transition 会统一管理,label在预览页面可以看
val size by bigTransitiom.animateDp({
when {
false isTransitioningTo true -> spring()
else tween()
}
}, label = "size"){ if(big) 96.dp else 48.dp }
val corner by bigTransitiom.animateDp(label = "corner"){ if(big) 0.dp else 18.dp }
Box(
Modifier
.size(size)
.clip(RoundedCornerShape(corner))
.background(Color.Green)
.clickable{
big = !big
}
)扩展 Transition
var shown by remember { mutableStateOf(true) }
// AnimatedVisibility(shown, enter = fadeIn(initialAlpha = 0.5f)) {
// TransitionSquare()
// }
AnimatedVisibility(shown, enter = slideIn{ IntOffset(-it.width, -it.height) }) {
TransitionSquare()
}
AnimatedVisibility(shown, enter = expandIn(
tween(5000),
expandFrom = Alignment.TopStart,
initialSize = { IntSize(it.width / 2, it.height / 2) }
clip = false
)) {
TransitionSquare()
}
AnimatedVisibility(shown, enter = scaleIn(transformOrigin = TransformOrigin())) {
TransitionSquare()
}
// 0.5f 废弃
AnimatedVisibility(shown, enter = fadeIn(initialAlpha = 0.3f) + fadeIn(initialAlpha = 0.5f)) {
TransitionSquare()
}
// + 把两个动画进行合并
AnimatedVisibility(shown, enter = scaleIn() + expandIn()]) {
TransitionSquare()
}
Button(onClick = { shown = !shown }){
Text("切换")
}
val big by remember { mutableStateOf(false) }
val bigTransition = updateTransition(big, "big")
bigTransition.AnimatedVisibility({ it }){
}让两个交替出现的组件渐变
var shown by remember { mutableStateOf(true) }
Crossfade(shown, animationSpec = tween()){
if (it) {
A()
} else{
Box(Modifier.size(24.dp).background(Color.Red))
}
}面向多个组件出场和入场定制
var shown by remember { mutableStateOf(true) }
AnimatedContent(shown, transitionSpec = {
// ContentTransform(fadeIn(), fadeOut())
// fadeIn() with fadeOut()
fadeIn(tween(3000)) with fadeOut(tween(3000, 3000))
}) {
when (it) {
1 -> Box()
2 -> Box()
3 -> Box()
else -> Box()
}
}Modifier.Companion 伴生对象
modifier:Modifier = Modifier 默认值,开发者可以选填
Modifier 建议作为第一个有默认值的参数
then Modifier 融合
CombinedModifier 融合器,链式装载多个 Modifier
使用 Modifier.composed 函数来使用 ComposedModifier
// 好处,用在 Modifier 有状态的场景
// 把同一个 Modifier 作用在多处
// 作用:创建带状态的 Modifier,用在自定义 Modifier
fun Modifier.paddingJumpModifier() = composed{
val padding by remember { mutableStateOf(8.dp) }
Modifier.padding(padding)
.clickable{ padding = 0.dp }
}
使用 Modifier.layout 函数来使用 LayoutModifier,修改组件的属性和位置;测量和布局
Text("Hello", Modifier.layout{ measurable, constraints ->
val paddingPx = 10.dp.roundToPx()
val placeable = measurable.measure(constraints.copy(
maxWidth = constraints.maxWidth - paddingPx * 2
maxHeight = constraints.maxHeight - paddingPx * 2
))
layout(placeable.width + paddingPx * 2, placeable.height + paddingPx * 2){
placeable.placeRelative(paddingPx, paddingPx)
}
})
Modifier 对顺序敏感
Modifier.size(40.dp).size(80.dp).background(Color.Blue) 40dp
requiredSize 不听左边的话
Modifier.size(40.dp).requiredSize(80.dp).background(Color.Blue) 80dp
管理绘制
Box(Modifier.size(40.dp).drawWithContent{
drawContent()
})用于定制触摸算法
Modifier.combindClickable{
}
// 触摸反馈,触摸范围右边最近的Modifier,左边覆盖右边
Modifier.pointInput(Unit){
detectTapGestures(onTap = {}, onDoubleTap = {}, onLongPress = {}, onPress = {})
forEachGesture{
awaitPointEventScope {
val down = awaitFirstDown()
}
}
}给子组件设置属性,用于父组件辅助测量
Modifier.layoutId("big")
@Composable
fun CustomLayout(modifier: Modifier = Modifier, content: @Composable () -> Unit){
Layout(content, modifier) { measurable, constraints ->
measurable.forEach {
when (it.layoutId) {
"big" -> it.measure(constraints.xxx)
"small" -> it.measure(constraints.xxx)
else -> it.measure(constraints)
}
}
layout(100, 100){
...
}
}
}自定义 Composable 用到子组件特殊信息才需要用到 ParentDataModifier
@Composable
fun CustomLayout(modifier: Modifier = Modifier, content: @Composable CustomLayout2Scope.() -> Unit){
Layout(content, modifier) { measurable, constraints ->
measurable.forEach {
val data = it.parentData as? Layout2Data
val big = data?.big
val weight = data?.weight
}
layout(100, 100){
...
}
}
}
class Layout2Data(var weight: Float = 0f, var big: Boolean = false)
fun Modifier.weightData(weight: Float) = then(object : ParentDataModifier {
// parentData 参数为了适配多条数据
override fun Density.modifyParentData(parentData: Any?): Any? {
return if (parentData == null) Layout2Data(weight)
else (parentData as Layout2Data).apply { this.weight = weight }
}
})
fun Modifier.bigData(big: Boolean) = then(object : ParentDataModifier {
// parentData 参数为了适配多条数据
override fun Density.modifyParentData(parentData: Any?): Any? {
return (parentData as Layout2Data) ?: Layout2Data().also { it.big = big }
}
})
@LayoutScopeMarker // 限制不在内部无法写
@Immutable // 减少不必要的重组
interface CustomLayout2Scope {
}
CusteomLayout{
// 忽略了2f
Text("1", Modifier.weightData(1f).bigData(true))
Text("2")
}提供 semantics tree 语义树;无障碍功能
Box(Modifier.width(40.dp).height(40.dp).semantics{
contentDescription = "XXX"
})
OnRemeasuredModifier 相当于 onMeasure(),重新测量会触发
Text("Hello", Modifier.onSizeChanged{ }类似 OnRemeasuredModifier,每次摆放的时候回调;类似 onMeasure 和 onLayout
Modifier.onPlaced{ layoutCoordinates ->
val pos = layoutCoordinates.positionInParent()
}.layout { measurable, constraints ->
val placeable = measurable.measure()
layout()
}
LookaheadLayout(content = , measurePilicy = )
全局定位;OnGloballyPositionedModifier 管的是左边
Modifier.onGloballyPositioned{ }.size(10.dp).onGloballyPositioned{ }
Modifier.modifierLocalProvider {
modifierLocalOf { "空" }
}{
"Hello"
}.modifierLocalConsumer {
sharedKey.current
}可以丢弃的 SideEffect
// 参数改变之后才会重新执行
DisposableEffect(Unit){
println("进去界面啦。。。")
onDispose{
println("离开界面啦。。。")
}
}特殊的 DisposableEffect,和 DisposableEffect 底层实现一致
LaunchedEffect(Unit){
delay(3000)
...
}var rememberedWelcome by remember { mutableStateOf(welcome) }
rememberedWelcome = welcome
// 替代上面代码
val rememberedWelcome by rememberUpdatedState(welcome)
// 解决参数方法传递失效问题// 在需要 Composable 组件外部调用的时候派上用场
val scope = rememberCoroutineScope()
val coroutine = remember { scope.launch{} }val geoManager: GeoManager = TODO()
val positionData: LiveData<Point> = TODO()
var position by remember { mutableStateOf(Point(0, 0)) }
val state = produceState(initialValue = ) {
val callback = object: ??? { newPosition ->
position = newPosition
}
geoManager.register(callback)
onDispose{
geoManager.unregister(callback)
}
}
Text(state)val geoManager: GeoManager = TODO()
val positionData: LiveData<Point> = TODO()
val positionState: StateFlow<Point> = TODO()
setContent{
CustomTheme{
var position by remember { mutableStateOf(Point(0, 0)) }
DisposableEffect(Unit) {
val callback = object: ??? { newPosition ->
position = newPosition
}
geoManager.register(callback)
onDispose{
geoManager.unregister(callback)
}
}
DisposableEffect(Unit) {
val observer = Observer<Point> { newPosition ->
position = newPosition
}
positionData.observe(this@Mainactivity, observer)
onDispose{
geoManager.unregister(callback)
}
}
// 需要添加 androidx.compose.runtime:runtime-livedata:xxx 依赖
// 订阅回调,帮你取消订阅
positionData.observeAsState()
LaunchedEffect(Unit){
positionState.collect { newPosition ->
position = newPosition
}
}
}
}var position by remember { mutableStateOf(Point(0, 0)) }
LaunchedEffect(Unit){
positionState.collect { newPosition ->
position = newPosition
}
}
// 等价于上面,在协程里更新 State 的值
val produceState = produceState(Point(0, 0)) {
positionState.collect { newPosition ->
position = newPosition
}
}var name by remember { mutableStateOf("Hello") }
val flow = snapshotFlow{ name }
LaunchedEffect(Unit){
flow.collect { name ->
println(name)
}
}Text("Hello", Modifier.drawBehind{
drawRect(Color.Yellow)
})
Box(Modifier.size(80.dp).drawBehind{
})
// 自由决定绘制顺序
Text("Hello", Modifier.drawWithContent{
drawRect(Color.Yellow)
// drawContent 上面xxx 被覆盖
drawContent()
// drawContent 下面xxx 盖住原先内容
drawLine(Color.Red, Offset(0f, size.height/2),
Offset(size.width, size.height/2), 2.dp.toPx())
})
val image = ImageBitmap.imageResource(R.drawable.avatar)
val paint by remember { mutableStateOf(Paint()) }
// 两种旋转 1. graphicsLayer 使用 RenderNode 旋转坐标系
Canvas(Modifier.size(80.dp).graphicsLayer{
rotationX = 45f
rotationY = 45f
}){
rotate(30f){
drawImage(image, dstSize = IntSize(size.width.roundToInt(), size.height.roundToInt()))
}
// rotateRad() 弧度
drawInCanvas{
it.drawImageRect(image, dstSize = IntSize(size.width.roundToInt(), size.height.roundToInt()), paint = paint)
}
}
// 旋转动画
val rotationAnimatable = remember{
Animatable(0f)
}
LaunchedEffect(Unit){
rotationAnimatable.animateTo(360f, infiniteRepeatable(tween(2000)))
}
// 多维旋转使用 Camera
val camera by remember { mutableStateOf(Camera()) }// .apply{
// value.rotateX(45f)
// value.rotateY(45f)
// }
val paint by remember { mutableStateOf(Paint()) }
Canvas(Modifier.size(80.dp)){
// 下沉到Compose的Canvas
drawInCanvas{
// 使用更下层的接口
it.translate(size.width/2, size.height/2)
it.rotate(-45f)
camera.save()
camera.ratateX(rotationAnimatable.value)
// 下沉到原生Canvas
camera.applyToCanvas(it.nativeCanvas)
camera.restore()
it.rotate(45f)
it.translate(-size.width/2, -size.height/2)
it.drawImage(image, dstSize = IntSize(size.width.roundToInt(), size.height.roundToInt()), paint = paint)
}
}// 对自己进行测量
Modifier.layout{ measurable, constraints ->
}
@Preview
@Composable
fun CustomLayoutPreview{
CustomLayout{
Box(Modifier.size(80.dp)).background(Color.Red)
Box(Modifier.size(80.dp)).background(Color.Yellow)
Box(Modifier.size(80.dp)).background(Color.Blue)
}
}
// 自定义布局,简单版自定义Column
@Composable
fun CustomLayout(modifier: Modifier = Modifier, content: @Composable () -> Unit) {
// Layout 等价于 ViewGroup
Layout(content) {measurables, constraints ->
var width = 0
var height = 0
val placeables = measurables.map{ measurable ->
measurable.measure(constraints).also{ placeable ->
width = max(width, placeable.width)
height += max(height, placeable.height)
}
}
layout(width, height){
var totalHeight = 0
placeables.forEach {
it.placeRelative(0, totalHeight)
totalHeight += it.height
}
}
}
}LazyColumn:动态控制显示数量和细节等,避免性能消耗。Scaffold;
缺点:对界面性能有负面影响,能不加入Subcompose就不用。也不用因噎废食
BoxWithConstraints() {
// 可以拿到测量过程父组件对子组件的尺寸限制再决定如何组合;Box拿不到
constraints
if(minWidth >= 360.dp){
Text("Hello", Modifier.align(Alignment.Center))
} else {
Text("World", Modifier.align(Alignment.Center))
}
}
// Subway,sub次一级
SubcomposeLayout{ constraints ->
// 组合
val measureable = subcompose(1){
Text("Hello")
}[0]
// 测量
val placeable = measureable.measure(constraints)
// 布局
layout(placeable.width, placeable.height){
placeable.placeRelative(0, 0)
}
}设计用来做过度动画
// 前瞻布局过程
LookaheadLayout({
Column{
Text("Hello")
Text("World")
}
}){ measurables, constraints ->
// 不能多次测量
val placeables = measurables.map { it.measure(constraints) }
val width = placeables.maxOf { it.width }
val height = placeables.maxOf { it.height }
layout(width, height){
placeables.forEach { it.placeRelative(0, 0) }
}
}
Layout({
Text("Hello", Modifier.layout{ measurable, constraints ->
val placeables = measurables.map { it.measure(constraints) }
layout(placeables.width, placeables.height){
placeables.placeRelative(0, 0)
}
})
}){ measurables, constraints ->
// 不能多次测量
val placeables = measurables.map { it.measure(constraints) }
val width = placeables.maxOf { it.width }
val height = placeables.maxOf { it.height }
layout(width, height){
placeables.forEach { it.placeRelative(0, 0) }
}
}
@Composable
private fun CustomLookaheadLayout(){
var textHeight by remember { mutableStateOf(100.dp) }
var textHeightAnim by animateDpAsState(textHeight)
Column{
LookaheadLayout({
Text("Hello", Modifier.height(textHeightAnim).clickable{
textHeight = if (textHeight == 100.dp) 200.dp else 100.dp
})
}){ measurables, constraints ->
val placeables = measurables.map { it.measure(constraints) }
val width = placeables.maxOf { it.width }
val height = placeables.maxOf { it.height }
layout(width, height){
placeables.forEach { it.placeRelative(0, 0) }
}
}
Text("World")
}
}@Composable
private fun CustomLookaheadLayout(){
var isTextHeight200dp by remember { mutableStateOf(false) }
var textHeightPx by remember { mutableStateOf(0) }
var textHeightAnim by animateDpAsState(textHeightPx)
Column{
SimpleLookaheadLayout{
Text("Hello",
Modifier
.intermediateLayout{ measurable, constraints, lookaheadSize ->
textHeightPx = lookaheadSize.height
val placeable = measurable.measure(
Constraints.fixed(lookaheadSize.width, textHeightAnim)
)
layout(width, height){
placeables.forEach { it.placeRelative(0, 0) }
}
}
.then(if (isTextHeight200dp) Modifier.height(200dp) else Modifier)
.clickable{
isTextHeight200dp = !isTextHeight200dp
})
}
Text("World")
}
}
@Composable
private fun SimpleLookaheadLayout(content: @Composable LookaheadLayoutScope.() -> Unit){
LookaheadLayout(content){ measurables, constraints ->
val placeables = measurables.map { it.measure(constraints) }
val width = placeables.maxOf { it.width }
val height = placeables.maxOf { it.height }
layout(width, height){
placeables.forEach { it.placeRelative(0, 0) }
}
}
}Modifier.clickable{}
Modifier.combinedClickable{}
Modifier.pointerInput(Unit){
// 太底层,用得少
awaitEachGesture{
val event = awaitPointerEvent()
}
detectTapGestures {
}
}
val interactionSource = remember { MutableInteractionSource() }
val offsetX by remember { mutableStateOf(0f) }
Text("Hello world.", Modifier
.offset{ IntOffset(offsetX.roundToInt, 0) }
// 监测一维滑动
.draggable(rememberDraggableState{ delta ->
println("$delta px.")
offsetX += it
}, Orientation.Horizontal))
val isDragged = interactionSource.collectIsDraggedAsState()
Text(if (isDragged) "拖动中" else "静止")
// 增加滑动功能
Modifier.verticalScroll()
Modifier.HorizontalScrollable()
// 滑动监测
Modifier.scrollable(rememberScrollableState{
println("scroll $it")
}, Orientation.Horizontal, overscrollEffect = ScrollableDefaults.overscrollEffect())
SwipToDismiss()Modifier.nestedScroll()
Scaffold{
LargeTopAppBar(title="", scrollBehavior = TopAppBarDefaults.exitUntilCollapsedScrollBehavior())
}
var offsetY by remember{ mutableStateOf(0f) }
val dispatcher = remember{ NestedScrollDispatcher() }
Column(
Modifier.offset{ IntOffset(0, offsetY.roundToInt()) }
.draggable(rememberDraggableState{
dispatcher.dispatchPreScroll(Offset(0f, it), NestedScrollSource.Drag)
offsetY += it
dispatcher.dispatchPostScroll()
}, Orientation.Vertical)
.nestedScroll()
)Modifier.pointInput(Unit){
detectDragGestures{ change, dragAmout ->
}
}
val draggableState = rememberDraggableState{}
Modifier.draggable(draggableState, Orientation.Horizontal)
LaunchedEffect(Unit){
draggableState.drag{
dragBy(100f)
}
}Modifier.pointInput(Unit){
detectTransformGestures{ centroid, pan, zoom, rotation ->
}
}Text("", Modifier.pointerInput(Unit){
awaitPointerEventScope{
var event = awaitPointEvent()
print("${event.type}")
if(event.type == PointerEventType.Release){
}
}
})SurfaceView、TextureView 在 Compose中没有对等实现
// View 里面使用 Compose
val linearLayout = LinearLayout(this)
val composeView = ComposeView(this).apply{
setContent{ CustomText() }
}
linearLayout.addView(composeView, ViewGroup.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.WARP_CONTENT,
))
// xml 添加 androidx.compose.ui.platform.ComposeView
// <androidx.compose.ui.platform.ComposeView
// android:layout_width="match_parent"
// android:layout_height="wrap_content"/>
findViewById<ComposeView>(R.id.composeView).setContent{ CustomText() }
// Compose 使用 View
setContent{
val context = LocalContext.current
val name by remember{ mutableStateOf("hello world") }
Theme{
AndroidView(factory = {
TextView(context).apply{
text = "hello world"
}
}){
// 重组过程中会 update
it.text = name
}
}
}