21、DataBinding与动画:属性动画绑定、Transition与DataBinding、布局动画、动画状态管理
动画这东西,说白了就是给UI注入灵魂。没有动画的App,就像一杯白开水——解渴,但没滋味。DataBinding和动画结合,能让你的代码量砍掉一半,而且逻辑更清晰。我早期做动画时,动不动就写几十行findViewById和ObjectAnimator,后来用了DataBinding绑定动画,嗯,真香。
21.1 属性动画绑定:把动画写进布局里
传统做法是:在Activity里找到View,然后创建Animator,再start。DataBinding的做法是:直接在XML里声明动画参数,通过BindingAdapter触发。我个人习惯把动画相关的逻辑封装在ViewModel里,布局只负责绑定。
先看一个最简单的例子——点击按钮让图片旋转:
// ViewModel
class AnimationViewModel : ViewModel() {
val rotation = ObservableFloat(0f)
fun rotate() {
rotation.set(rotation.get() + 360f)
}
}
// 布局文件 activity_animation.xml
<layout>
<data>
<variable name="vm" type="com.example.AnimationViewModel"/>
</data>
<ImageView
android:layout_width="100dp"
android:layout_height="100dp"
android:src="@drawable/ic_launcher"
android:onClick="@{() -> vm.rotate()}"
android:rotation="@{vm.rotation}"/>
</layout>
等等,这样直接设置rotation是没有动画效果的,它是瞬间跳转。我们需要一个BindingAdapter来添加动画过渡:
@BindingAdapter("android:rotation")
fun setRotationWithAnimation(view: View, rotation: Float) {
view.animate()
.rotation(rotation)
.setDuration(300)
.setInterpolator(AccelerateDecelerateInterpolator())
.start()
}
这样每次rotation值变化时,都会触发一个300ms的旋转动画。我在项目中遇到过一个问题:如果快速点击多次,动画会互相覆盖,导致旋转角度错乱。解决办法是取消之前的动画:
@BindingAdapter("android:rotation")
fun setRotationWithAnimation(view: View, rotation: Float) {
view.animate().cancel() // 取消当前动画
view.animate()
.rotation(rotation)
.setDuration(300)
.start()
}
21.2 Transition与DataBinding:场景切换的优雅方案
Transition框架是Android提供的场景动画工具。配合DataBinding,你可以实现「数据变化→自动触发场景切换」的效果。说白了,就是数据变了,界面自动用动画过渡到新状态。
举个例子:一个列表项展开和收起。传统做法要手动记录展开状态、调用beginDelayedTransition、更新布局。用DataBinding可以这样:
// ViewModel
class ExpandViewModel : ViewModel() {
val isExpanded = ObservableBoolean(false)
fun toggle() {
isExpanded.set(!isExpanded.get())
}
}
// 布局
<layout>
<data>
<variable name="vm" type="com.example.ExpandViewModel"/>
</data>
<LinearLayout
android:onClick="@{() -> vm.toggle()}"
android:visibility="@{vm.isExpanded ? View.VISIBLE : View.GONE}">
<!-- 展开的内容 -->
</LinearLayout>
</layout>
然后在Activity或Fragment中监听数据变化,触发Transition:
vm.isExpanded.addOnPropertyChangedCallback(object : Observable.OnPropertyChangedCallback() {
override fun onPropertyChanged(sender: Observable?, propertyId: Int) {
TransitionManager.beginDelayedTransition(rootView)
}
})
这样每次isExpanded变化时,TransitionManager会自动捕捉布局变化并播放动画。我曾经踩过一个坑:如果布局变化太快(比如连续点击),TransitionManager会丢失中间状态。解决方案是加一个防抖,或者用AutoTransition代替默认Transition。
21.3 布局动画:让列表和容器动起来
布局动画指的是容器内子View的入场、出场、移动动画。比如RecyclerView的item动画,或者LinearLayout添加子View时的动画。DataBinding可以配合LayoutAnimationController使用。
我个人习惯在XML中直接声明布局动画:
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:animateLayoutChanges="true">
<!-- 子View -->
</LinearLayout>
加上android:animateLayoutChanges="true"后,系统会自动为子View的添加、移除、可见性变化添加默认动画。但默认动画比较简陋,你可以自定义:
// 在代码中设置自定义LayoutTransition
val transition = LayoutTransition()
transition.setAnimator(LayoutTransition.APPEARING, ObjectAnimator.ofFloat(null, "alpha", 0f, 1f).setDuration(500))
transition.setAnimator(LayoutTransition.DISAPPEARING, ObjectAnimator.ofFloat(null, "alpha", 1f, 0f).setDuration(300))
container.layoutTransition = transition
如果配合DataBinding,你可以在ViewModel中控制子View的添加和移除,布局动画会自动触发。比如:
// ViewModel
class ListViewModel : ViewModel() {
val items = ObservableArrayList<String>()
fun addItem() {
items.add("Item ${items.size + 1}")
}
fun removeItem(index: Int) {
items.removeAt(index)
}
}
// 布局
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:animateLayoutChanges="true">
<include layout="@layout/item_view"
android:visibility="@{items.size() > 0 ? View.VISIBLE : View.GONE}"/>
</LinearLayout>
这里要注意:animateLayoutChanges只对直接子View的添加/移除有效。如果你用include标签,需要确保include的根布局是直接子View。
21.4 动画状态管理:别让动画失控
动画状态管理是很多开发者容易忽略的点。你想想看,如果用户快速点击按钮,动画还没播完又触发新的动画,界面会变得一团糟。我早期做过一个点赞动画,用户疯狂点击时,心形图标乱飞,最后不得不加一个状态锁。
DataBinding配合ViewModel可以很好地管理动画状态。我的做法是在ViewModel中维护一个动画状态枚举:
enum class AnimationState {
IDLE, // 空闲
PLAYING, // 播放中
FINISHED // 已完成
}
class AnimationStateViewModel : ViewModel() {
private val _state = MutableLiveData(AnimationState.IDLE)
val state: LiveData<AnimationState> = _state
fun startAnimation() {
if (_state.value != AnimationState.IDLE) return // 防止重复触发
_state.value = AnimationState.PLAYING
// 执行动画...
// 动画结束后
_state.value = AnimationState.FINISHED
}
fun reset() {
_state.value = AnimationState.IDLE
}
}
然后在布局中根据状态控制View的可见性或启用状态:
<Button
android:enabled="@{vm.state == AnimationState.IDLE}"
android:onClick="@{() -> vm.startAnimation()}"/>
这样用户在动画播放期间无法再次点击按钮,从根本上避免了状态冲突。
21.5 实战:一个带动画的点赞按钮
把上面的知识点串起来,做一个完整的点赞按钮。功能:点击后心形图标放大并变色,同时数字+1,动画期间按钮不可点击。
// ViewModel
class LikeViewModel : ViewModel() {
val likeCount = ObservableInt(0)
val isLiked = ObservableBoolean(false)
val scale = ObservableFloat(1f)
fun like() {
if (isLiked.get()) return
isLiked.set(true)
scale.set(1.5f) // 触发放大动画
likeCount.set(likeCount.get() + 1)
// 动画结束后恢复
Handler(Looper.getMainLooper()).postDelayed({
scale.set(1f)
isLiked.set(false)
}, 300)
}
}
// BindingAdapter
@BindingAdapter("app:scaleAnimation")
fun setScaleAnimation(view: View, scale: Float) {
view.animate().cancel()
view.animate()
.scaleX(scale)
.scaleY(scale)
.setDuration(200)
.start()
}
// 布局
<ImageButton
android:src="@{vm.isLiked ? @drawable/ic_liked : @drawable/ic_unliked}"
app:scaleAnimation="@{vm.scale}"
android:enabled="@{!vm.isLiked}"
android:onClick="@{() -> vm.like()}"/>
这个例子虽然简单,但涵盖了属性动画绑定、状态管理、防重复触发等核心知识点。我在实际项目中用类似方案做过直播间的礼物动画,效果很稳定。
21.6 知识体系图
下面这张图总结了DataBinding与动画的核心关系:
这张图展示了DataBinding与动画的四个核心方向。属性动画绑定负责单个View的属性变化;Transition负责场景级别的布局切换;布局动画处理容器内子View的增删;动画状态管理则确保整个过程有序进行。四者结合,基本能覆盖日常开发中90%的动画需求。
最后说一句:动画不是越多越好。我见过有些App每个按钮都带弹跳动画,看着眼花。动画的目的是引导用户注意力、反馈操作结果,而不是炫技。用DataBinding做动画,能让你把精力放在「什么时候触发动画」上,而不是「怎么实现动画」上——这才是DataBinding的真正价值。