1+ @file:Suppress(" OPT_IN_USAGE" )
2+
13package com.xiaocydx.inputview.sample.edit
24
5+ import android.graphics.Rect
36import android.os.Bundle
7+ import android.view.View
48import androidx.activity.viewModels
59import androidx.appcompat.app.AppCompatActivity
10+ import androidx.core.view.children
611import androidx.core.view.isVisible
712import androidx.lifecycle.flowWithLifecycle
813import androidx.lifecycle.lifecycleScope
14+ import androidx.transition.getBounds
15+ import androidx.transition.setLeftTopRightBottomCompat
916import com.xiaocydx.inputview.AnimationState
1017import com.xiaocydx.inputview.EdgeToEdgeHelper
1118import com.xiaocydx.inputview.EditorMode
@@ -21,8 +28,9 @@ import com.xiaocydx.inputview.sample.edit.VideoEditor.Text
2128import com.xiaocydx.inputview.sample.edit.VideoEditor.Video
2229import com.xiaocydx.inputview.sample.isDispatchTouchEventEnabled
2330import com.xiaocydx.inputview.sample.onClick
31+ import kotlinx.coroutines.flow.distinctUntilChanged
2432import 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