Android Compose实现一个文字跑马灯效果控件
前言
Android Compose实现一个文字跑马灯效果控件;文章最后贴出代码:
1、文字超出后自动滚动
2、使用自定义动画方便修改
3、代码量修改可以根据提供思路自己添加功能。
4、目前只支持横向滚动
关于Compose动画
相比传统布局而言,Compose在各个方面代码量减少,且清晰。
传统布局动画和 Jetpack Compose 动画对比
在 Android 开发中,传统的布局动画和 Jetpack Compose 动画各有优缺点。了解这些差异有助于你根据项目需求选择合适的动画方案。以下是两者的对比分析:
传统布局动画
优势
-
兼容性:
- 传统布局动画支持所有版本的 Android,包括较老的设备。
- 对于需要广泛兼容性的项目,传统动画是一个可靠的选择。
-
成熟稳定:
- 传统动画 API 已经存在多年,经过了大量项目的验证,稳定性较高。
- 社区中有大量的文档和示例可供参考。
-
灵活性:
- 传统动画提供了丰富的 API,可以实现复杂的动画效果,如路径动画、关键帧动画等。
- 可以通过 XML 文件定义动画,方便管理和复用。
劣势
-
代码复杂性:
- 传统动画通常涉及多个步骤,如创建
Animator
、设置属性、添加监听器等,代码较为冗长。 - 需要手动管理动画的状态和生命周期,容易出错。
- 传统动画通常涉及多个步骤,如创建
-
性能问题:
- 传统动画可能会导致过度绘制,尤其是在复杂的布局中。
- 动画过程中可能会影响 UI 的响应性,特别是在低端设备上。
-
维护成本:
- 由于代码复杂,维护和调试的成本较高。
- 修改动画效果时,需要修改多处代码,容易引入错误。
Jetpack Compose 动画
优势
-
声明式编程:
- Jetpack Compose 采用声明式编程模型,动画代码更加简洁、易读。
- 可以直接在 Composable 函数中定义动画,逻辑清晰。
-
性能优化:
- Compose 的动画系统经过优化,能够高效地处理动画效果,减少不必要的重绘。
- 动画过程中不会影响其他 UI 组件的性能。
-
集成性:
- Compose 动画与 Compose 的其他功能无缝集成,如状态管理、布局等。
- 可以轻松地组合多个动画效果,实现复杂的动画序列。
-
易用性:
- 提供了丰富的内置动画 API,如
animate*AsState
、Animatable
、Crossfade
等,使用简单。 - 动画状态和生命周期管理由框架自动处理,开发者只需关注动画效果本身。
- 提供了丰富的内置动画 API,如
-
可测试性:
- 由于 Compose 的声明式特性,动画更容易进行单元测试和集成测试。
- 可以使用 Compose 的测试工具来验证动画的正确性。
劣势
-
兼容性:
- Compose 目前仅支持 Android 5.0(API 级别 21)及以上版本。
- 对于需要支持更老设备的项目,可能需要考虑其他方案。
-
学习曲线:
- 对于习惯了传统布局的开发者,Compose 的学习曲线可能较陡峭。
- 需要时间来适应新的编程范式和概念。
-
社区支持:
- 相比传统布局,Compose 的社区支持相对较少,尤其是在解决特定问题时。
- 文档和示例仍在不断完善中。
总结
-
传统布局动画:
- 优势:兼容性好、成熟稳定、灵活性高。
- 劣势:代码复杂、性能问题、维护成本高。
-
Jetpack Compose 动画:
- 优势:声明式编程、性能优化、集成性好、易用性强、可测试性高。
- 劣势:兼容性限制、学习曲线陡峭、社区支持相对较少。
选择哪种动画方案取决于你的项目需求、目标设备和团队的技术栈。如果你的项目需要广泛的兼容性和复杂的动画效果,传统布局动画可能是更好的选择。如果你追求代码简洁、高性能和易用性,Jetpack Compose 动画则是更佳的选择。
跑马灯组件代码:
@Composable
fun MarqueeText(
text: String
) {
val density = LocalDensity.current
var innerBoxWidthDp by remember {
mutableIntStateOf(8000) // 再大点就是小说了没有必要跑马灯效果了
}
var outBoxWidthPx by remember {
mutableIntStateOf(0)
}
var fontWidthPx by remember {
mutableIntStateOf(0)
}
var offsetPx by remember {
mutableIntStateOf(0)
}
var itemOffsetDp by remember {
mutableIntStateOf(0)
}
var itemOffsetDp2 by remember {
mutableIntStateOf(0)
}
LaunchedEffect(key1 = offsetPx) {
if (offsetPx > 0 && fontWidthPx > 0) {
val itemWidthPx = fontWidthPx + 50
var index = 0
var index2 = 0
var start = offsetPx
var start2 = offsetPx + itemWidthPx
val end = -itemWidthPx + offsetPx
while (true) {
itemOffsetDp = with(density) {
((start - index).toFloat() / this.density).toInt()
}
itemOffsetDp2 = with(density) {
((start2 - index2).toFloat() / this.density).toInt()
}
index += 2
index2 += 2
if ((start - index) < end) {
index = 0
start = offsetPx + itemWidthPx
}
if ((start2 - index2) < end) {
index2 = 0
start2 = offsetPx + itemWidthPx
}
delay(10)
}
}
}
Box(
modifier = Modifier
.fillMaxWidth()
.onGloballyPositioned {
if (it.size.width != outBoxWidthPx) {
outBoxWidthPx = it.size.width
Log.i("TAG_jason", "MarqueeText: change out width")
}
}
.background(Color.LightGray)
) {
Box(
modifier = Modifier
.requiredWidth(innerBoxWidthDp.dp)
.offset(itemOffsetDp.dp)
) {
Text(
modifier = Modifier
.widthIn(80.dp)
.onGloballyPositioned {
if (it.size.width != fontWidthPx) {
fontWidthPx = it.size.width
innerBoxWidthDp = with(density) {
(fontWidthPx.toFloat() / this.density).toInt()
}
if (fontWidthPx > outBoxWidthPx) {
offsetPx = ((fontWidthPx - outBoxWidthPx).toFloat() / 2).toInt()
}
Log.i("TAG_jason", "MarqueeText: change font width")
}
}
.wrapContentWidth(),
text = text,
fontSize = 16.sp,
color = Color.Black
)
}
if (offsetPx > 0) {
Box(
modifier = Modifier
.requiredWidth(innerBoxWidthDp.dp)
.offset(itemOffsetDp2.dp)
) {
Text(
modifier = Modifier
.widthIn(80.dp)
.wrapContentWidth(),
text = text,
fontSize = 16.sp,
color = Color.Red
)
}
}
}
}
使用:
class TestVideoPlayer : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
Box(Modifier.fillMaxSize()) {
MarqueeText(text = "<------------>再大点就是小说了没有必要跑马灯效果了再大点就是小说了没有必要跑马灯效果了再大点就是小说了没有必要跑马灯效果了再大点就是小说了没有必要跑马灯效果了++++")
}
}
}
}
如果文章对你有用记得点赞啊 !!!