Skip to content

Commit 82b6147

Browse files
committed
perf: 优化动画的实现
1 parent c6c60bb commit 82b6147

File tree

9 files changed

+202
-115
lines changed

9 files changed

+202
-115
lines changed

app/src/main/kotlin/com/xiaocydx/inputview/sample/ImeAnimatorActivity.kt

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -31,14 +31,8 @@ class ImeAnimatorActivity : AppCompatActivity() {
3131
val animator = InputView.animator(editText)
3232
// 1. 点击imageView,隐藏IME
3333
imageView.onClick(animator::hideIme)
34-
3534
// 2. 当支持手势导航栏EdgeToEdge时,设置etContainer.paddingBottom
36-
etContainer.doOnApplyWindowInsets { view, insets, _ ->
37-
val supportGestureNavBarEdgeToEdge = insets.supportGestureNavBarEdgeToEdge(view)
38-
val bottom = if (supportGestureNavBarEdgeToEdge) insets.navigationBarHeight else 0
39-
etContainer.updatePadding(bottom = bottom)
40-
}
41-
35+
etContainer.handleGestureNavBarEdgeToEdgeOnApply()
4236
// 3. 显示和隐藏IME,运行动画设置root.paddingBottom
4337
animator.addAnimationCallback(onUpdate = { state ->
4438
val bottom = state.currentOffset - state.navBarOffset
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
@file:JvmName("TransitionInternalKt")
2+
@file:Suppress("PackageDirectoryMismatch")
3+
4+
package androidx.transition
5+
6+
import android.graphics.Rect
7+
import android.view.View
8+
9+
inline fun View.getBounds(bounds: Rect, change: Rect.() -> Unit) {
10+
bounds.set(left, top, right, bottom)
11+
bounds.change()
12+
}
13+
14+
fun View.setLeftTopRightBottomCompat(rect: Rect) {
15+
setLeftTopRightBottomCompat(rect.left, rect.top, rect.right, rect.bottom)
16+
}
17+
18+
fun View.setLeftTopRightBottomCompat(left: Int, top: Int, right: Int, bottom: Int) {
19+
ViewUtils.setLeftTopRightBottom(this, left, top, right, bottom)
20+
}

app/src/main/kotlin/com/xiaocydx/inputview/sample/edit/CommonFragment.kt

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@ import android.view.View
1010
import android.view.ViewGroup
1111
import androidx.appcompat.widget.AppCompatTextView
1212
import androidx.core.os.bundleOf
13-
import androidx.core.view.updatePadding
1413
import androidx.fragment.app.Fragment
1514
import androidx.lifecycle.Lifecycle
1615
import androidx.lifecycle.Lifecycle.State.RESUMED
@@ -44,12 +43,8 @@ class CommonFragment : Fragment() {
4443
}
4544

4645
override fun onViewCreated(view: View, savedInstanceState: Bundle?) = EdgeToEdgeHelper {
47-
view.doOnApplyWindowInsets { view, insets, initialState ->
48-
val supportGestureNavBarEdgeToEdge = insets.supportGestureNavBarEdgeToEdge(view)
49-
val bottom = if (supportGestureNavBarEdgeToEdge) insets.navigationBarHeight else 0
50-
view.updatePadding(bottom = initialState.paddings.bottom + bottom)
51-
}
52-
46+
// 设置通用的手势导航栏EdgeToEdge处理逻辑
47+
view.handleGestureNavBarEdgeToEdgeOnApply()
5348
viewLifecycleOwner.lifecycle.addObserver(object : LifecycleEventObserver {
5449
val title = arguments?.getString(KEY_TITLE) ?: ""
5550
override fun onStateChanged(source: LifecycleOwner, event: Lifecycle.Event) {
Lines changed: 71 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,18 @@
1+
@file:Suppress("OPT_IN_USAGE")
2+
13
package com.xiaocydx.inputview.sample.edit
24

5+
import android.graphics.Rect
36
import android.os.Bundle
7+
import android.view.View
48
import androidx.activity.viewModels
59
import androidx.appcompat.app.AppCompatActivity
10+
import androidx.core.view.children
611
import androidx.core.view.isVisible
712
import androidx.lifecycle.flowWithLifecycle
813
import androidx.lifecycle.lifecycleScope
14+
import androidx.transition.getBounds
15+
import androidx.transition.setLeftTopRightBottomCompat
916
import com.xiaocydx.inputview.AnimationState
1017
import com.xiaocydx.inputview.EdgeToEdgeHelper
1118
import com.xiaocydx.inputview.EditorMode
@@ -21,8 +28,9 @@ import com.xiaocydx.inputview.sample.edit.VideoEditor.Text
2128
import com.xiaocydx.inputview.sample.edit.VideoEditor.Video
2229
import com.xiaocydx.inputview.sample.isDispatchTouchEventEnabled
2330
import com.xiaocydx.inputview.sample.onClick
31+
import kotlinx.coroutines.flow.distinctUntilChanged
2432
import kotlinx.coroutines.flow.launchIn
25-
import kotlinx.coroutines.flow.onEach
33+
import kotlinx.coroutines.flow.mapLatest
2634

2735
/**
2836
* 通过视频编辑这类复杂的切换场景,演示[InputView]的使用
@@ -42,15 +50,15 @@ class VideoEditActivity : AppCompatActivity() {
4250

4351
private fun ActivityVideoEditBinding.initShowOrHide() = apply {
4452
val adapter = VideoEditorAdapter(this@VideoEditActivity)
45-
inputView.editorAdapter = adapter
46-
inputView.editorMode = EditorMode.ADJUST_PAN
47-
inputView.setEditBackgroundColor(0xFF1D1D1D.toInt())
48-
53+
inputView.apply {
54+
editorAdapter = adapter
55+
editorMode = EditorMode.ADJUST_PAN
56+
setEditBackgroundColor(0xFF1D1D1D.toInt())
57+
}
4958
arrayOf(
5059
tvInput to Text.Input, btnText to Text.Emoji,
5160
btnVideo to Video, btnAudio to Audio, btnImage to Image
5261
).forEach { (view, editor) -> view.onClick { viewModel.show(editor) } }
53-
5462
// 不排除有其它代码显示IME的可能性,通过EditorChangedListener完成状态同步,
5563
// 双向同步的过程,EditorAdapter和StateFlow会做差异对比,不会形成循环同步。
5664
adapter.addEditorChangedListener { _, current -> viewModel.show(current) }
@@ -59,29 +67,26 @@ class VideoEditActivity : AppCompatActivity() {
5967
val action = commonAction + textAction
6068
viewModel.state
6169
.flowWithLifecycle(lifecycle)
62-
.onEach {
63-
action.update(inputView, container, it)
64-
adapter.notifyShowOrHide(it)
65-
}
70+
.distinctUntilChanged()
71+
.mapLatest { action.toggle(inputView, container, it) }
6672
.launchIn(lifecycleScope)
6773
}
6874

6975
private fun ActivityVideoEditBinding.initAnimation() = apply {
70-
inputView.editorAnimator = FadeEditorAnimator(durationMillis = 300)
76+
val animator = FadeEditorAnimator(durationMillis = 300)
77+
inputView.editorAnimator = animator
7178

72-
// 在动画运行时拦截触摸事件
73-
inputView.editorAnimator.addAnimationCallback(
79+
// 1. 在动画运行时拦截触摸事件
80+
animator.addAnimationCallback(
7481
onStart = { window.isDispatchTouchEventEnabled = false },
7582
onEnd = { window.isDispatchTouchEventEnabled = true },
7683
)
7784

78-
// 处理titleBar的偏移
85+
// 2. 处理titleBar的显示偏移,让titleBar能在root下面
7986
var titleBarHeight = 0f
8087
fun AnimationState.canTranslation() = startOffset == 0 || endOffset == 0
81-
inputView.editorAnimator.addAnimationCallback(
88+
animator.addAnimationCallback(
8289
onStart = start@{ state ->
83-
// 确定endOffset了,才显示titleBar
84-
container.isVisible = true
8590
if (!state.canTranslation()) return@start
8691
titleBarHeight = container.height.toFloat()
8792
inputView.translationY = titleBarHeight
@@ -91,37 +96,76 @@ class VideoEditActivity : AppCompatActivity() {
9196
var fraction = state.animatedFraction
9297
if (state.startOffset == 0) fraction = 1 - fraction
9398
inputView.translationY = titleBarHeight * fraction
99+
}
100+
)
101+
102+
// 3. 处理titleBar的切换变换,跟EditorAnimator的动画状态保持同步
103+
var canTransform = false
104+
var previousBar: View? = null
105+
var currentBar: View? = null
106+
val changeBounds = Rect()
107+
animator.addAnimationCallback(
108+
onStart = { state ->
109+
previousBar = currentBar
110+
currentBar = container.children.firstOrNull { it.isVisible }
111+
canTransform = state.previous != null && state.current != null
112+
&& previousBar != null && currentBar != null
113+
&& previousBar !== currentBar
114+
if (canTransform) previousBar?.isVisible = true
115+
if (canTransform && previousBar?.height != currentBar?.height) {
116+
// 当动画开始时,需要将边界修正回previous的值,避免产生一帧的抖动
117+
container.getBounds(changeBounds) { top = bottom - (previousBar?.height ?: 0) }
118+
}
119+
},
120+
onUpdate = { state ->
121+
val fraction = state.interpolatedFraction
122+
if (canTransform) {
123+
previousBar?.alpha = animator.calculateAlpha(state, start = true)
124+
currentBar?.alpha = animator.calculateAlpha(state, start = false)
125+
}
126+
if (!changeBounds.isEmpty) {
127+
val start = previousBar?.height ?: 0
128+
val end = currentBar?.height ?: 0
129+
val dy = start + (end - start) * fraction
130+
container.getBounds(changeBounds) { top = bottom - dy.toInt() }
131+
}
94132
},
95-
onEnd = { container.isVisible = it.endOffset != 0 }
133+
onEnd = {
134+
previousBar?.alpha = 1f
135+
currentBar?.alpha = 1f
136+
if (canTransform) previousBar?.isVisible = false
137+
canTransform = false
138+
changeBounds.setEmpty()
139+
}
96140
)
97141

98-
// 处理preview的缩放,通过PreDraw兼容手势导航栏高度变更
99-
root.viewTreeObserver.addOnPreDrawListener {
142+
// 4. 处理preview的缩放,通过OnDrawListener兼容手势导航栏高度变更,
143+
// 显示EditText的光标时,OnDrawListener会一直调用,这不会产生影响。
144+
root.viewTreeObserver.addOnDrawListener {
145+
// 此时才应用changeBounds,是为了修正边界,避免动画被layout阶段影响,
146+
// 执行时序是onStart() -> DrawListener,onUpdate() -> DrawListener。
147+
changeBounds.takeIf { !it.isEmpty }?.let(container::setLeftTopRightBottomCompat)
100148
val titleBarTop = container.top + inputView.translationY
101149
val dy = (preview.bottom - titleBarTop).coerceAtLeast(0f)
102150
val scale = 1f - dy / preview.height
103151
preview.apply {
104-
// 变换属性自带差异对比,不会形成循环PreDraw
152+
// 变换属性自带差异对比,不会形成循环OnDrawListener
105153
scaleX = scale
106154
scaleY = scale
107155
pivotX = preview.width.toFloat() / 2
108156
pivotY = 0f
109157
}
110-
true
111158
}
112159
}
113160

114161
private fun ActivityVideoEditBinding.initEdgeToEdge() = EdgeToEdgeHelper {
115162
// 禁用手势导航栏偏移,自行处理手势导航栏
116163
inputView.disableGestureNavBarOffset()
164+
// 设置通用的手势导航栏EdgeToEdge处理逻辑
165+
space.handleGestureNavBarEdgeToEdgeOnApply()
117166
preview.doOnApplyWindowInsets { view, insets, initialState ->
118167
view.updateMargins(top = initialState.params.marginTop + insets.statusBarHeight)
119168
}
120-
space.doOnApplyWindowInsets { view, insets, initialState ->
121-
val supportGestureNavBarEdgeToEdge = insets.supportGestureNavBarEdgeToEdge(view)
122-
val bottom = if (supportGestureNavBarEdgeToEdge) insets.navigationBarHeight else 0
123-
view.updateMargins(bottom = initialState.params.marginBottom + bottom)
124-
}
125169
return@EdgeToEdgeHelper this@initEdgeToEdge
126170
}
127171
}

0 commit comments

Comments
 (0)