Android Jetpack结构运用Compose实现微博长按点赞彩虹效果

目录
  • 原版
  • 1. Compose 动画 API 概览
  • 2. 长按点赞动画分解
  • 3. 彩虹动画
    • 3.1 状态管理
      • AnimatedRainbow
      • animatedRainbows 列表
    • 3.2 内容绘制
  • 4. 表情动画
    • 4.1 状态管理
      • AnimatedEmoji
      • infiniteRepeatable
      • CubicBezierEasing
      • 抛物线动画
      • animatedEmojis 列表
    • 4.2 内容绘制
  • 5. 烟花动画
    • 5.1 状态管理
      • AnimatedFlower
      • keyframes
      • animatedFlower 列表
    • 5.2 内容绘制
  • 6. 合体效果

原版

效果高仿效果

1. Compose 动画 API 概览

Compose 动画 API 在使用场景的维度上大体分为两类:高级别 API 和低级别 API。就像编程语言分为高级语言和低级语言一样,这列高级低级指 API 的易用性:

高级别 API 主打开箱即用,适用于一些 UI 元素的展现/退出/切换等常见场景,例如常见的 AnimatedVisibility 以及 AnimatedContent 等,它们被设计成 Composable 组件,可以在声明式布局中与其他组件融为一体。

//Text通过动画淡入
var editable by remember { mutableStateOf(true) }
AnimatedVisibility(visible = editable) {
    Text(text = "Edit")
}

低级别 API 使用成本更高但是更加灵活,可以更精准地实现 UI 元素个别属性的动画,多个低级别动画还可以组合实现更复杂的动画效果。最常见的低级别 animateFloatAsState 系列了,它们也是 Composable 函数,可以参与 Composition 的组合过程。

//动画改变 Box 透明度
val alpha: Float by animateFloatAsState(if (enabled) 1f else 0.5f)
Box(
    Modifier.fillMaxSize()
        .graphicsLayer(alpha = alpha)
        .background(Color.Red)
)

处于上层的 API 由底层 API 支撑实现,TargetBasedAnimation 是开发者可直接使用的最低级 API。Animatable 也是一个相对低级的 API,它是一个动画值的包装器,在协程中完成状态值的变化,向上提供对 animate*AsState 的支撑。它与其他 API 不同,是一个普通类而非一个 Composable 函数,所以可以在 Composable 之外使用,因此更具灵活性。本例子的动画主要也是依靠它完成的。

// Animtable 包装了一个颜色状态值
val color = remember { Animatable(Color.Gray) }
LaunchedEffect(ok) {
    // animateTo 是个挂起函数,驱动状态之变化
    color.animateTo(if (ok) Color.Green else Color.Gray)
}
Box(Modifier.fillMaxSize().background(color.value))

无论高级别 API 还是低级别 API ,它们都遵循状态驱动的动画方式,即目标对象通过观察状态变化实现自身的动画。

2. 长按点赞动画分解

长按点赞的动画乍看之下非常复杂,但是稍加分解后,不难发现它也是由一些常见的动画形式组合而成,因此我们可以对其拆解后逐个实现:

  • 彩虹动画:全屏范围内不断扩散的彩虹效果。可以通过半径不断扩大的圆形图案并依次叠加来实现
  • 表情动画:从按压位置不断抛出的表情。可以进一步拆解为三个动画:透明度动画,旋转动画以及抛物线轨迹动画。
  • 烟花动画:抛出的表情在消失时会有一个烟花炸裂的效果。其实就是围绕中心的八个圆点逐渐消失的过程,圆点的颜色提取自表情本身。

传统视图动画可以作用在 View 上,通过动画改变其属性;也可以在 onDraw 中通过不断重绘实现逐帧的动画效果。 Compose 也同样,我们可以在 Composable 中观察动画状态,通过重组实现动画效果(本质是改变 UI 组件的布局属性),也可以在 Canvas 中观察动画状态,只在重绘中实现动画(跳过组合)。这个例子的动画效果也需要通过 Canvas 的不断重绘来实现。

Compose 的 Canvas 也可以像 Composable 一样声明式的调用,基本写法如下:

Canvas {
    ...
    drawRainbow(rainbowState) //绘制彩虹
    ...
    drawEmoji(emojiState) //绘制表情
    ...
    drawFlow(flowState) //绘制烟花
    ...
}

State 的变化会驱动 Canvas 会自动重绘,无需手动调用 invalidate 之类的方法。那么接下来针对彩虹、表情、烟花等各种动画的实现,我们的工作主要有两个:

  • 状态管理:定义相关 State,并在在动画中驱动其变化,如前所述这主要依靠 Animatable 实现。
  • 内容绘制:通过 Canvas API 基于当前状态绘制图案

3. 彩虹动画

3.1 状态管理

对于彩虹动画,唯一的动画状态就是圆的半径,其值从 0F 过渡到 screensize,圆形面积铺满至整个屏幕。我们使用 Animatable 包装这个状态值,调用 animateTo 方法可以驱动状态变化:

val raduis = Animatable(0f) //初始值 0f
radius.animateTo(
    targetValue = screenSize, //目标值
    animationSpec = tween(
        durationMillis = duration, //动画时长
        easing = FastOutSlowInEasing //动画衰减效果
    )
)

animationSpec 用来指定动画规格,不同的动画规格决定了了状态值变化的节奏。Compose 中常用的创建动画规格的方法有以下几种,它们创建不同类型的动画规格,但都是 AnimationSpec 的子类:

  • tween:创建补间动画规格,补间动画是一个固定时长动画,比如上面例子中这样设置时长 duration,此外,tween 还能通过 easiing 指定动画衰减效果,后文详细介绍。
  • spring: 弹跳动画:spring 可以创建基于物理特性的弹簧动画,它通过设置阻尼比实现符合物理规律的动画衰减,因此不需要也不能指定动画时长
  • Keyframes:创建关键帧动画规格,关键帧动画可以逐帧设置当前动画的轨迹,后文会详细介绍。

AnimatedRainbow

要实现上面这样多个彩虹叠加的效果,我们还需有多个 Animtable 同时运行,在 Canvas 中依次对它们进行绘制。绘制彩虹除了依靠 Animtable 的状态值,还有 Color 等其他信息,因此我们定义一个 AnimatedRainbow 类保存包括 Animtable 在内的绘制所需的的状态

class AnimatedRainbow(
    //屏幕尺寸(宽边长边大的一方)
    private val screenSize: Float,
    //RainbowColors是彩虹的候选颜色
    private val color: Brush = RainbowColors.random(),
    //动画时长
    private val duration: Int = 3000
) {
    private val radius = Animatable(0f)
    suspend fun startAnim() = radius.animateTo(
        targetValue = screenSize * 1.6f, // 关于 1.6f 后文说明
        animationSpec = tween(
            durationMillis = duration,
            easing = FastOutSlowInEasing
        )
    )
}

animatedRainbows 列表

我们还需要一个集合来管理运行中的 AnimatedRainbow。这里我们使用 Compose 的 MutableStateList 作为集合容器,MutableStateList 中的元素发生增减时,可以被观察到,而当我们观察到新的 AnimatedRainbow 被添加时,为它启动动画。关键代码如下:

//MutableStateList 保存 AnimatedRainbow
val animatedRainbows = mutableStateListOf<AnimatedRainbow>()
//长按屏幕时,向列表加入 AnimtaedRainbow, 意味着增加一个新的彩虹
animatedRainbows.add(
    AnimatedRainbow(
        screenHeightPx.coerceAtLeast(screenWidthPx),
        RainbowColors.random()
    )
)

我们使用 LaunchedEffect + snapshotFlow 观察 animatedRainbows 的变化,代码如下:

LaunchedEffect(Unit) {
    //监听到新添加的 AnimatedRainbow
    snapshotFlow { animatedRainbows.lastOrNull() }
        .filterNotNull()
        .collect {
            launch {
                //启动 AnimatedRainbow 动画
                val result = it.startAnim()
                //动画结束后,从列表移除,避免泄露
                if (result.endReason == AnimationEndReason.Finished) {
                    animatedRainbows.remove(it)
                }
            }
        }
}

LaunchedEffectsnapshotFlow 都是 Compose 处理副作用的 API,由于不是本文重点就不做深入介绍了,这里只需要知道 LaunchedEffect 是一个提供了执行副作用的协程环境,而 snapshotFlow 可以将 animatedRainbows 中的变化转化为 Flow 发射给下游。当通过 Flow 收集到新加入的 AnimtaedRainbow 时,调用 startAnim 启动动画,这里充分发挥了挂起函数的优势,同步等待动画执行完毕,从 animatedRainbows 中移除 AnimtaedRainbow 即可。

值得一提的是,MutableStateList 的主要目的是在组合中观察列表的状态变化,本例子的动画不发生在组合中(只发生在重绘中),完全可以使用普通的集合类型替代,这里使用 MutableStateList 有两个好处:

  • 可以响应式地观察列表变化
  • 在 LaunchEffect 中响应变化并启动动画,协程可以随当前 Composable 的生命周期结束而终止,避免泄露。

3.2 内容绘制

我们在 Canvas 中遍历 animatedRainbows 所有的 AnimtaedRainbow 完成彩虹的绘制。彩虹的图形主要依靠 DrawScopedrawCircle 完成,比较简单。一点需要特别注意,彩虹动画结束时也要以一个圆形图案逐渐退出直至漏出底部内容,要实现这个效果,用到一个小技巧,我们的圆形绘制使用空心圆 (Stroke ) 而非 实心圆( Fill )

  • 出现彩虹:圆环逐渐铺满屏幕却不能漏出空心。这要求 StrokeWidth 宽度覆盖 ScreenSize,且始终保持 CircleRadius 的两倍
  • 结束彩虹:圆环空心部分逐渐覆盖屏幕。此时要求 CircleRadius 减去 StrokeWidth / 2 之后依然能覆盖 ScreenSize

基于以上原则,我们为 AnimatedRainbow 添加单个 AnnimatedRainbow 的绘制方法:

fun DrawScope.draw() {
    drawCircle(
        brush = color, //圆环颜色
        center = center, //圆心:点赞位置
        radius = radius.value,// Animtable 中变化的 radius 值,
        style = Stroke((radius.value * 2).coerceAtMost(_screenSize)),
    )
}

如上,StrokeWidth 覆盖 ScreenSize 之后无需继续增长,而 CircleRadius 的最终尺寸除去 ScreenSize 之外还要将 StrokeWidth 考虑进去,因此前面代码中将 Animtable 的 targetValue 设置为 ScreenSize 的 1.6 倍。

4. 表情动画

4.1 状态管理

表情动画又由三个子动画组成:旋转动画、透明度动画以及抛物线轨迹动画。像 AnimtaedRainbow 一样,我们定义 AnimatedEmoji 管理每个表情动画的状态,AnimatedEmoji 中通过多个 Animatable 分别管理前面提到的几个子动画

AnimatedEmoji

class AnimatedEmoji(
    private val start: Offset, //表情抛点位置,即长按的屏幕位置
    private val screenWidth: Float, //屏幕宽度
    private val screenHeight: Float, //屏幕高度
    private val duration: Int = 1500 //动画时长
) {
    //抛出距离(x方向移动终点),在左右一个屏幕之间取随机数
    private val throwDistance by lazy {
        ((start.x - screenWidth).toInt()..(start.x + screenWidth).toInt()).random()
    }
    //抛出高度(y方向移动终点),在屏幕顶端到抛点之间取随机数
    private val throwHeight by lazy {
        (0..start.y.toInt()).random()
    }
    private val x = Animatable(start.x)//x方向移动动画值
    private val y = Animatable(start.y)//y方向移动动画值
    private val rotate = Animatable(0f)//旋转动画值
    private val alpha = Animatable(1f)//透明度动画值
    suspend fun CoroutineScope.startAnim() {
        async {
            //执行旋转动画
            rotate.animateTo(
                360f, infiniteRepeatable(
                    animation = tween(_duration / 2, easing = LinearEasing),
                    repeatMode = RepeatMode.Restart
                )
            )
        }
        awaitAll(
            async {
                //执行x方向移动动画
                x.animateTo(
                    throwDistance.toFloat(),
                    animationSpec = tween(durationMillis = duration, easing = LinearEasing)
                )
            },
            async {
                //执行y方向移动动画(上升)
                y.animateTo(
                    throwHeight.toFloat(),
                    animationSpec = tween(
                        duration / 2,
                        easing = LinearOutSlowInEasing
                    )
                )
                //执行y方向移动动画(下降)
                y.animateTo(
                    screenHeight,
                    animationSpec = tween(
                        duration / 2,
                        easing = FastOutLinearInEasing
                    )
                )
            },
            async {
                //执行透明度动画,最终状态是半透明
                alpha.animateTo(
                    0.5f,
                    tween(duration, easing = CubicBezierEasing(1f, 0f, 1f, 0.8f))
                )
            }
        )
    }

infiniteRepeatable

上面代码中,旋转动画的 AnimationSpec 使用 infiniteRepeatable 创建了一个无限循环的动画,RepeatMode.Restart 表示它的从 0F 过渡到 360F 之后,再次重复这个过程。

除了旋转动画之外,其他动画都会在 duration 之后结束,它们分别在 async 中启动并行执行,awaitAll 等待它们全部结束。而由于旋转动画不会结束,因此不能放到 awaitAll 中,否则 startAnim 的调用方将永远无法恢复执行。

CubicBezierEasing

透明度动画中的 easing 指定了一个 CubicBezierEasing。easing 是动画衰减效果,即动画状态以何种速率逼近目标值。Compose 提供了几个默认的 Easing 类型可供使用,分别是:

//默认的 Easing 类型,以加速度起步,减速度收尾
val FastOutSlowInEasing: Easing = CubicBezierEasing(0.4f, 0.0f, 0.2f, 1.0f)
//匀速起步,减速度收尾
val LinearOutSlowInEasing: Easing = CubicBezierEasing(0.0f, 0.0f, 0.2f, 1.0f)
//加速度起步,匀速收尾
val FastOutLinearInEasing: Easing = CubicBezierEasing(0.4f, 0.0f, 1.0f, 1.0f)
//匀速接近目标值
val LinearEasing: Easing = Easing { fraction -> fraction }

上图横轴是时间,纵轴是逼近目标值的进度,可以看到除了 LinearEasing 之外,其它的的曲线变化都满足 CubicBezierEasing 三阶贝塞尔曲线,如果默认 Easing 不符合你的使用要求,可以使用 CubicBezierEasing,通过参数,自定义合适的曲线效果。比如例子中曲线如下:

这个曲线前半程状态值进度非常缓慢,临近时间结束才快速逼近最终状态。因为我们希望表情动画全程清晰可见,透明度的衰减尽量后置,默认 easiing 无法提供这种效果,因此我们自定义 CubicBezierEasing

抛物线动画

再来看一下抛物线动画的实现。通常我们可以借助抛物线公式,基于一些动画状态变量计算抛物线坐标来实现动画,但这个例子中我们借助 Easing 更加巧妙的实现了抛物线动画。

我们将抛物线动画拆解为 x 轴和 y 轴两个方向两个并行执行的位移动画,x 轴位移通过 LinearEasing 匀速完成,y 轴又拆分成两个过程

上升到最高点,使用 LinearOutSlowInEasing 上升时速度加速衰减

下落到屏幕底端,使用 FastOutLinearInEasing 下落时速度加速增加

上升和下降的 Easing 曲线互相对称,符合抛物线规律

animatedEmojis 列表

像彩虹动画一样,我们同样使用一个 MutableStateList 集合管理 AnimatedEmoji 对象,并在 LaunchedEffect 中监听新元素的插入,并执行动画。只是表情动画每次会批量增加多个

//MutableStateList 保存 animatedEmojis
val animatedEmojis = mutableStateListOf<AnimatedEmoji>()
//一次增加 EmojiCnt 个表情
animatedEmojis.addAll(buildList {
    repeat(EmojiCnt) {
        add(AnimatedEmoji(offset, screenWidthPx, screenHeightPx, res))
    }
})
//监听 animatedEmojis 变化
LaunchedEffect(Unit) {
        //监听到新加入的 EmojiCnt 个表情
        snapshotFlow { animatedEmojis.takeLast(EmojiCnt) }
            .flatMapMerge { it.asFlow() }
            .collect {
                launch {
                    with(it) {
                        startAnim()//启动表情动画,等待除了旋转动画外的所有动画结束
                        animatedEmojis.remove(it) //从列表移除
                    }
                }
            }
    }

4.2 内容绘制

单个 AnimatedEmoji 绘制代码很简单,借助 DrawScopedrawImage 绘制表情素材即可

//当前 x,y 位移的位置
val offset get() = Offset(x.value, y.value)
//图片topLeft相对于offset的距离
val d by lazy { Offset(img.width / 2f, img.height / 2f) }
//绘制表情
fun DrawScope.draw() {
    rotate(rotate.value, pivot = offset) {
        drawImage(
            image = img, //表情素材
            topLeft = offset - dCenter,//当前位置
            alpha = alpha.value, //透明度
        )
    }
}

注意旋转动画实际上是借助 DrawScoperotate 方法实现的,在 block 内部调用 drawImage 指定当前的 alphatopLeft 即可。

5. 烟花动画

5.1 状态管理

烟花动画紧跟在表情动画结束时发生,动画不涉及位置变化,主要是几个花瓣不断缩小的过程。花瓣用圆形绘制,动画状态值就是圆形半径,使用 Animatable 包装。

AnimatedFlower

烟花的绘制还要用到颜色等信息,我们定义 AnimatedFlower 保存包括 Animtable 在内的相关状态。

class AnimatedFlower(
    private val intial: Float, //花瓣半径初始值,一般是表情的尺寸
    private val duration: Int = 2500
) {
    //花瓣半径
    private val radius = Animatable(intial)
    suspend fun startAnim() {
        radius.animateTo(0f, keyframes {
            durationMillis = duration
            intial / 3 at 0 with FastOutLinearInEasing
            intial / 5 at (duration * 0.95f).toInt()
        })
    }

keyframes

这里又出现了一种 AnimationSpec,即帧动画 keyframes,相对于 tween ,keyframes 可以更精确指定时间区间内的动画进度。比如代码中 radius / 3 at 0 表示 0 秒时状态值达到 intial / 3 ,相当于以初始值的 1/3 尺寸出现,这是一般的 tween 难以实现的。另外我们希望花瓣可以持久可见,所以使用 keyframe 确保时间进行到 95% 时,radius 的尺寸仍然清晰可见。

animatedFlower 列表

由于烟花动画设计是表情动画的延续,所以它紧跟表情动画执行,共享 CoroutienScope ,不需要借助 LaunchedEffect ,所以使用普通列表定义 animatedFlower 即可:

//animatedFlowers 使用普通列表创建
val animatedFlowers = mutableListOf<AnimatedFlower>()
launch {
    with(it) {//表情动画执行
        startAnim()
        animatedEmojis.remove(it)
    }
    //创建 AnimatedFlower 动画
    val anim = AnimatedFlower(
        center = it.offset,
        //使用 Palette 从表情图片提取烟花颜色
        color = Palette.from(it.img.asAndroidBitmap()).generate().let {
            arrayOf(
                Color(it.getDominantColor(Color.Transparent.toArgb())),
                Color(it.getVibrantColor(Color.Transparent.toArgb()))
            )
        },
        initial = it.img.run { width.coerceAtLeast(height) / 2 }.toFloat()
    )
    animatedFlowers.add(anim) //添加进列表
    anim.startAnim() //执行烟花动画
    animatedFlowers.remove(anim) //移除动画
}

5.2 内容绘制

烟花的内容绘制,需要计算每个花瓣的位置,一共8个花瓣,各自位置计算如下:

//计算 sin45 的值
val sin by lazy { sin(Math.PI / 4).toFloat() }
val points
    get() = run {
        val d1 = initial - radius.value
        val d2 = (initial - radius.value) * sin
        arrayOf(
            center.copy(y = center.y - d1), //0点方向
            center.copy(center.x + d2, center.y - d2),
            center.copy(x = center.x + d1),//3点方向
            center.copy(center.x + d2, center.y + d2),
            center.copy(y = center.y + d1),//6点方向
            center.copy(center.x - d2, center.y + d2),
            center.copy(x = center.x - d1),//9点方向
            center.copy(center.x - d2, center.y - d2),
        )
    }

center 是烟花的中心位置,随着花瓣的变小,同时越来越远离中心位置,因此 d1d2 就是偏离 center 的距离,与 radius 大小成反比。

最后在 Canvas 中绘制这些 points 即可:

fun DrawScope.draw() {
    points.forEachIndexed { index, point ->
        drawCircle(color = color[index % 2], center = point, radius = radius.value)
    }
}

6. 合体效果

最后我们定义一个 AnimatedLike 的 Composable ,整合上面代码

@Composable
fun AnimatedLike(modifier: Modifier = Modifier, state: LikeAnimState = rememberLikeAnimState()) {
    LaunchedEffect(Unit) {
        //监听新增表情
        snapshotFlow { state.animatedEmojis.takeLast(EmojiCnt) }
            .flatMapMerge { it.asFlow() }
            .collect {
                launch {
                    with(it) {
                        startAnim()
                        state.animatedEmojis.remove(it)
                    }
                    //添加烟花动画
                    val anim = AnimatedFlower(
                        center = it.offset,
                        color = Palette.from(it.img.asAndroidBitmap()).generate().let {
                            arrayOf(
                                Color(it.getDominantColor(Color.Transparent.toArgb())),
                                Color(it.getVibrantColor(Color.Transparent.toArgb()))
                            )
                        },
                        initial = it.img.run { width.coerceAtLeast(height) / 2 }.toFloat()
                    )
                    state.animatedFlowers.add(anim)
                    anim.startAnim()
                    state.animatedFlowers.remove(anim)
                }
            }
    }
    LaunchedEffect(Unit) {
        //监听新增彩虹
        snapshotFlow { state.animatedRainbows.lastOrNull() }
            .filterNotNull()
            .collect {
                launch {
                    val result = it.startAnim()
                    if (result.endReason == AnimationEndReason.Finished) {
                        state.animatedRainbows.remove(it)
                    }
                }
            }
    }
    //绘制动画
    Canvas(modifier.fillMaxSize()) {
        //绘制彩虹
        state.animatedRainbows.forEach { animatable ->
            with(animatable) { draw() }
        }
        //绘制表情
        state.animatedEmojis.forEach { animatable ->
            with(animatable) { draw() }
        }
        //绘制烟花
        state.animatedFlowers.forEach { animatable ->
            with(animatable) { draw() }
        }
    }
}

我们使用 AnimatedLike 布局就可以为页面添加动画效果了,由于 Canvas 本身是基于 modifier.drawBehind 实现的,我们也可以将 AnimatedLike 改为 Modifier 修饰符使用,这里就不赘述了。

最后,复习一下本文例子中的内容:

  • Animatable :包装动画状态值,并且在协程中执行动画,同步返回动画结果
  • AnimationSpec:动画规格,可以配置动画时长、Easing 等,例子中用到了 tween,keyframes,infiniteRepeatable 等多个动画规格
  • Easing:动画状态值随时间变化的趋势,通常使用默认类型即可, 也可以基于 CubicBezierEasing 定制。

一个例子不可能覆盖到 Compose 所有的动画 API ,但是借由这个例子我们可以掌握一些基础 API 的使用,了解 Compose 动画开发的基本思想,这之后再学习其他 API 就是水到渠成的事情了。

到此这篇关于Android Jetpack结构运用Compose实现微博长按点赞彩虹效果的文章就介绍到这了,更多相关Android Jetpack Compose内容请搜索我们以前的文章或继续浏览下面的相关文章希望大家以后多多支持我们!

(0)

相关推荐

  • Android Jetpack库剖析之LiveData组件篇

    目录 LiveData简介 LiveData用法 数据订阅过程 PostValue过程 SetValue过程 生命周期变化 LiveData简介 在日常安卓开发中,一些耗时的操比如列网络请求,数据库读写都不能在主线程执行,必须开一条子线程去执行这些耗时操作,但我们往往需要在这些耗时操作执行完毕后更新UI,但安卓不能在子线程进行UI的更新,这时我们只能通过创建一个Handler来切回到主线程进行UI的更新,直到LiveData出现,LiveData是一个可被观察的数据容器,它将数据包装起来,使数据

  • Android Mms之:深入理解Compose

    Mms中的ComposeMessageActivity(以下简称Composer)是整个Mms中最重要的一个组件,它负责编辑信息,发送信息,管理信息,接收信息,与外部应用接口.在Mms内部与Composer关联的类和组件特别多,几乎所有的类和组件都与Composer有关联,关于信息的所有操作流程都起始于Composer:在外部Composer也是公开的接口,能够处理Intent.ACTION_SEND和Intent.ACTION_SENDTO以及文件类型为audio/*,image/*,vide

  • Android Compose自定义TextField实现自定义的输入框

    目录 概览 简单自定义BasicTextField示例 实现自定义样式的BasicTextField 使用BasicTextField自定义百度输入框 概览 众所周知Compose中默认的TextField和OutlineTextField样式并不能满足所有的使用场景,所以自定义TextField就成了必备技能,本文就揭露一下自定义TextField的实现.自定义TextField主要使用BasicTextField实现. 简单自定义BasicTextField示例 1.代码 var text

  • Android之Compose页面切换动画介绍

    目录 前因后果 开始尝试 开始撸码 仓促的结尾 前因后果 Compose 正式版已经发布了一个多月了,从 Compose beta 版本发布之后各大网站中热度就一直不减,官方也一直在为开发者们推出学习 Compose 的文章,更加说明了 Android 开发的未来趋势. 在之前我写了 Compose 版本的玩安卓,当然也有 MVVM 版本的,只是不同分支而已,这是 Github地址:https://github.com/zhujiang521/PlayAndroid 但之前一直存在着一个问题,就

  • Android Jetpack Compose无限加载列表

    目录 前言 方法一: paging-compose 方法二:自定义实现 添加 LoadingIndicator 总结 前言 Android 中使用 ListView 或者 RecycleView 经常有滚动到底部自动 LoadMore 的需求,那么在 Compose 中该如何实现呢? 两种方法可供选择: 基于 paging-compose 自定义实现 方法一: paging-compose Jetpack 的 Paging 组件提供了对 Compose 的支持 dependencies { ..

  • Android Compose实现底部按钮以及首页内容详细过程第1/2页

    目录 前言 Column.Row.ConstraintLayout布局先知 Column纵向排列布局 Row横向排列布局 ConstraintLayout 约束布局 Modifier的简单使用 底部导航栏的实现 首页内容的实现 Banner的实现 首页ViewModel 前言 compose作为Android现在主推的UI框架,各种文章铺天盖地的席卷而来,作为一名Android开发人员也是很有必要的学习一下了,这里就使用wanandroid的开放api来编写一个compose版本的玩安卓客户端,

  • Android Jetpack Compose实现列表吸顶效果

    目录 stickyHeader 实体类 加载假数据 吸顶标题 二级条目 完整代码 效果图 安卓传统的 Recyclerview 打造悬浮头部StickyHeader的吸顶效果,十分麻烦,而在Compose中就简单多了 stickyHeader Compose设计的时候考虑得很周到,他们提供了stickyHeader 作用就是添加一个粘性标题项,即使在它后面滚动时也会保持固定.标头将保持固定,直到下一个标头取而代之. 参数key - 表示唯一的密钥键. 它不允许对列表出现使用相同的键.密钥的类型应

  • Android Compose实现伸缩ToolBar的思路详解

    目录 ScrollableAppBar 效果图 主要思路 布局预览 实现过程 ScrollableAppBar 效果图 当列表向上移动时,会先带动ToolBar向上位移,等ToolBar向上移动到最大位移量时列表向上滑动 当列表向下移动时,会先带动ToolBar向下位移,等ToolBar向下移动到最大位移量时列表向下滑动 主要思路 布局预览 伸缩前布局: 伸缩后布局: 实现过程 布局实现 首先我们要定义两个尺寸变量 // 应用栏高度 private val toolBarHeight = 56.

  • Android Jetpack库剖析之ViewModel组件篇

    前言 今天让我们一起去探究一下ViewModel的实现原理,描述的不对或不足还请海涵,仅作为参考 ViewModel简介 ViewModel是一个可感知Activity或Fragment生命周期的一个架构组件,当视图销毁,数据也会被清除,所以它的本质就是用来存储与视图相关的数据,让视图显示控制与数据分离,即使界面配置发生改变数据也不会被销毁,通常配合LiveData使用 ViewModel用法 class MainActivity : AppCompatActivity() { override

  • Android Jetpack结构运用Compose实现微博长按点赞彩虹效果

    目录 原版 1. Compose 动画 API 概览 2. 长按点赞动画分解 3. 彩虹动画 3.1 状态管理 AnimatedRainbow animatedRainbows 列表 3.2 内容绘制 4. 表情动画 4.1 状态管理 AnimatedEmoji infiniteRepeatable CubicBezierEasing 抛物线动画 animatedEmojis 列表 4.2 内容绘制 5. 烟花动画 5.1 状态管理 AnimatedFlower keyframes animat

  • Android仿QQ、微信聊天界面长按提示框效果

    先来看看效果图 如何使用 示例代码 PromptViewHelper pvHelper = new PromptViewHelper(mActivity); pvHelper.setPromptViewManager(new ChatPromptViewManager(mActivity)); pvHelper.addPrompt(holder.itemView.findViewById(R.id.textview_content)); 使用起来还是很简单的 首先new一个PromptViewH

  • Android Jetpack Compose开发实用小技巧

    目录 前言 实用小技巧 如何移除View点击阴影 Text文本如何垂直居中 如何移除Button的点击阴影 Dialog宽度如何全屏 如何提升编码效率 前言 在Compose开发的过程中,我们会经常遇到一些看起来很简单却不知道如何处理的小问题,比如去除点击阴影.Dialog全屏等问题,本文记录了这些常见小问题的处理方式.如有更好方案欢迎大佬们交流探讨- 实用小技巧 如何移除View点击阴影 这里的View指的是除了Button系列的之外,如Button.TextButton等,也就是自身没有on

  • Android Jetpack架构组件 ViewModel详解

    前言 前面两篇文章我们已经学习了Lifecycle和DataBind,本篇文章我们来学习Jetpack系列中比较重要的ViewModel,Jetpack的很多很多组件都是搭配使用的,所以单独的知识点可能会有些"无意义"但却是我们项目实战的基础! ViewModel的使用 ViewModel类旨在以注重生命周期的方式存储和管理界面相关的数据.ViewModel类让数据可在发生屏幕旋转等配置更改后继续存在.这句话很好理解,还记得我们在讲解Lifecycle的时候 举的例子吗,我们还是使用那

  • 通过Jetpack Compose实现双击点赞动画效果

    目录 实现步骤 先红色画个爱心 点击事件加动画 完整代码 效果图 实现步骤 先红色画个爱心 Icon( Icons.Filled.Favorite, "爱心", Modifier .align(Alignment.Center) tint = Color.Red ) 点击事件加动画 双击监听 .pointerInput(Unit) { detectTapGestures( onDoubleTap = { ... } ) } #### **API 介绍** | API名称 | 作用 |

  • Android Jetpack组件Navigation导航组件的基本使用

    目录 1.Navigation 基本概念 2.Navigation 使用入门 2.1 添加Navigation依赖 2.2 创建导航图 2.3 导航图中添加目的地Fragment 2.4 Activity添加 NavHost 2.5 LoginFragment 代码编写 2.6 welcomeFragment 代码编写 总结 本篇主要介绍一下 Android Jetpack 组件 Navigation 导航组件的 基本使用 当看到 Navigation单词的时候 应该就大概知道 这是一个关于导航

  • Android Jetpack架构中ViewModel接口暴露的不合理探究

    目录 暴露 Mutable 状态 暴露 Suspend 方法 在 Jetpack 架构规范中, ViewModel 与 View 之间应该遵循单向数据流的通信方式,Events 永远从 View 流向 VM ,而 State 从 VM 流向 View. 如果 ViewModel 对 View 暴露了不适当的接口类型,则会破坏单向数据流的形成.不适当的接口类型常见于以下两点: 暴露 Mutable 状态 暴露 Suspend 方法 暴露 Mutable 状态 ViewModel 对外暴露的数据状态

  • Android Jetpack库重要组件WorkManager的使用

    目录 前言 后台处理指南 后台处理面临的挑战 如何选择合适的后台解决方案 WorkManager概述 WorkManager使用 1 声明依赖项 2 自定义一个继承自Worker的类 3 选择worker执行的条件 4 下面贴出自定义worker类的全部源码 5 执行任务的方式 6 取消任务的执行 前言 WorkManager是Jetpack很重要的一个组件: 本篇我们就先来讲讲它是如何使用的,在讲解之前我们先了解关于后台处理的一些痛点 后台处理指南 我们知道每个 Android 应用都有一个主

随机推荐