blob: 5143e429768e11b06f88e09a1add0a7c76db2e59 [file] [log] [blame]
Selim Cinek2de5ebb2020-05-20 15:39:03 -07001/*
2 * Copyright (C) 2020 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License
15 */
16
17package com.android.systemui.util.animation
18
19import android.animation.ValueAnimator
Lucas Dupin84f5a0e2020-06-08 19:55:33 -070020import android.graphics.PointF
Selim Cinek2de5ebb2020-05-20 15:39:03 -070021import android.util.MathUtils
22import com.android.systemui.Interpolators
23
24/**
25 * The fraction after which we start fading in when going from a gone widget to a visible one
26 */
27private const val GONE_FADE_FRACTION = 0.8f
28
29/**
30 * The amont we're scaling appearing views
31 */
32private const val GONE_SCALE_AMOUNT = 0.8f
33
34/**
35 * A controller for a [TransitionLayout] which handles state transitions and keeps the transition
36 * layout up to date with the desired state.
37 */
38open class TransitionLayoutController {
39
40 /**
41 * The layout that this controller controls
42 */
43 private var transitionLayout: TransitionLayout? = null
44 private var currentState = TransitionViewState()
45 private var animationStartState: TransitionViewState? = null
46 private var state = TransitionViewState()
Lucas Dupin84f5a0e2020-06-08 19:55:33 -070047 private var pivot = PointF()
Selim Cinek2de5ebb2020-05-20 15:39:03 -070048 private var animator: ValueAnimator = ValueAnimator.ofFloat(0.0f, 1.0f)
Selim Cinekafae4e72020-06-16 18:21:41 -070049 private var currentHeight: Int = 0
50 private var currentWidth: Int = 0
51 var sizeChangedListener: ((Int, Int) -> Unit)? = null
Selim Cinek2de5ebb2020-05-20 15:39:03 -070052
53 init {
54 animator.apply {
55 addUpdateListener {
56 updateStateFromAnimation()
57 }
58 interpolator = Interpolators.FAST_OUT_SLOW_IN
59 }
60 }
61
62 private fun updateStateFromAnimation() {
63 if (animationStartState == null || !animator.isRunning) {
64 return
65 }
66 val view = transitionLayout ?: return
67 getInterpolatedState(
68 startState = animationStartState!!,
69 endState = state,
70 progress = animator.animatedFraction,
Lucas Dupin84f5a0e2020-06-08 19:55:33 -070071 pivot = pivot,
Selim Cinek2de5ebb2020-05-20 15:39:03 -070072 resultState = currentState)
Selim Cinekafae4e72020-06-16 18:21:41 -070073 applyStateToLayout(currentState)
74 }
75
76 private fun applyStateToLayout(state: TransitionViewState) {
77 transitionLayout?.setState(state)
78 if (currentHeight != state.height || currentWidth != state.width) {
79 currentHeight = state.height
80 currentWidth = state.width
81 sizeChangedListener?.invoke(currentWidth, currentHeight)
82 }
Selim Cinek2de5ebb2020-05-20 15:39:03 -070083 }
84
85 /**
86 * Get an interpolated state between two viewstates. This interpolates all positions for all
87 * widgets as well as it's bounds based on the given input.
88 */
89 fun getInterpolatedState(
90 startState: TransitionViewState,
91 endState: TransitionViewState,
92 progress: Float,
Lucas Dupin84f5a0e2020-06-08 19:55:33 -070093 pivot: PointF,
Selim Cinek2de5ebb2020-05-20 15:39:03 -070094 resultState: TransitionViewState
95 ) {
Lucas Dupin84f5a0e2020-06-08 19:55:33 -070096 this.pivot.set(pivot)
Selim Cinek2de5ebb2020-05-20 15:39:03 -070097 val view = transitionLayout ?: return
98 val childCount = view.childCount
99 for (i in 0 until childCount) {
100 val id = view.getChildAt(i).id
101 val resultWidgetState = resultState.widgetStates[id] ?: WidgetState()
102 val widgetStart = startState.widgetStates[id] ?: continue
103 val widgetEnd = endState.widgetStates[id] ?: continue
104 var alphaProgress = progress
105 var widthProgress = progress
106 val resultMeasureWidth: Int
107 val resultMeasureHeight: Int
108 val newScale: Float
109 val resultX: Float
110 val resultY: Float
111 if (widgetStart.gone != widgetEnd.gone) {
112 // A view is appearing or disappearing. Let's not just interpolate between them as
113 // this looks quite ugly
114 val nowGone: Boolean
115 if (widgetStart.gone) {
116
117 // Only fade it in at the very end
118 alphaProgress = MathUtils.map(GONE_FADE_FRACTION, 1.0f, 0.0f, 1.0f, progress)
119 nowGone = progress < GONE_FADE_FRACTION
120
121 // Scale it just a little, not all the way
122 val endScale = widgetEnd.scale
123 newScale = MathUtils.lerp(GONE_SCALE_AMOUNT * endScale, endScale, progress)
124
125 // don't clip
126 widthProgress = 1.0f
127
128 // Let's directly measure it with the end state
129 resultMeasureWidth = widgetEnd.measureWidth
130 resultMeasureHeight = widgetEnd.measureHeight
131
132 // Let's make sure we're centering the view in the gone view instead of having
133 // the left at 0
134 resultX = MathUtils.lerp(widgetStart.x - resultMeasureWidth / 2.0f,
135 widgetEnd.x,
136 progress)
137 resultY = MathUtils.lerp(widgetStart.y - resultMeasureHeight / 2.0f,
138 widgetEnd.y,
139 progress)
140 } else {
141
142 // Fadeout in the very beginning
143 alphaProgress = MathUtils.map(0.0f, 1.0f - GONE_FADE_FRACTION, 0.0f, 1.0f,
144 progress)
145 nowGone = progress > 1.0f - GONE_FADE_FRACTION
146
147 // Scale it just a little, not all the way
148 val startScale = widgetStart.scale
149 newScale = MathUtils.lerp(startScale, startScale * GONE_SCALE_AMOUNT, progress)
150
151 // Don't clip
152 widthProgress = 0.0f
153
154 // Let's directly measure it with the start state
155 resultMeasureWidth = widgetStart.measureWidth
156 resultMeasureHeight = widgetStart.measureHeight
157
158 // Let's make sure we're centering the view in the gone view instead of having
159 // the left at 0
160 resultX = MathUtils.lerp(widgetStart.x,
161 widgetEnd.x - resultMeasureWidth / 2.0f,
162 progress)
163 resultY = MathUtils.lerp(widgetStart.y,
164 widgetEnd.y - resultMeasureHeight / 2.0f,
165 progress)
166 }
167 resultWidgetState.gone = nowGone
168 } else {
169 resultWidgetState.gone = widgetStart.gone
170 // Let's directly measure it with the end state
171 resultMeasureWidth = widgetEnd.measureWidth
172 resultMeasureHeight = widgetEnd.measureHeight
173 newScale = MathUtils.lerp(widgetStart.scale, widgetEnd.scale, progress)
174 resultX = MathUtils.lerp(widgetStart.x, widgetEnd.x, progress)
175 resultY = MathUtils.lerp(widgetStart.y, widgetEnd.y, progress)
176 }
177 resultWidgetState.apply {
178 x = resultX
179 y = resultY
180 alpha = MathUtils.lerp(widgetStart.alpha, widgetEnd.alpha, alphaProgress)
181 width = MathUtils.lerp(widgetStart.width.toFloat(), widgetEnd.width.toFloat(),
182 widthProgress).toInt()
183 height = MathUtils.lerp(widgetStart.height.toFloat(), widgetEnd.height.toFloat(),
184 widthProgress).toInt()
185 scale = newScale
186
187 // Let's directly measure it with the end state
188 measureWidth = resultMeasureWidth
189 measureHeight = resultMeasureHeight
190 }
191 resultState.widgetStates[id] = resultWidgetState
192 }
193 resultState.apply {
194 width = MathUtils.lerp(startState.width.toFloat(), endState.width.toFloat(),
195 progress).toInt()
196 height = MathUtils.lerp(startState.height.toFloat(), endState.height.toFloat(),
197 progress).toInt()
Lucas Dupin84f5a0e2020-06-08 19:55:33 -0700198 translation.x = (endState.width - width) * pivot.x
199 translation.y = (endState.height - height) * pivot.y
Selim Cinek2de5ebb2020-05-20 15:39:03 -0700200 }
201 }
202
203 fun attach(transitionLayout: TransitionLayout) {
204 this.transitionLayout = transitionLayout
205 }
206
207 /**
208 * Set a new state to be applied to the dynamic view.
209 *
210 * @param state the state to be applied
211 * @param animate should this change be animated. If [false] the we will either apply the
212 * state immediately if no animation is running, and if one is running, we will update the end
213 * value to match the new state.
214 * @param applyImmediately should this change be applied immediately, canceling all running
215 * animations
216 */
217 fun setState(
218 state: TransitionViewState,
219 applyImmediately: Boolean,
220 animate: Boolean,
221 duration: Long = 0,
222 delay: Long = 0
223 ) {
224 val animated = animate && currentState.width != 0
225 this.state = state.copy()
226 if (applyImmediately || transitionLayout == null) {
227 animator.cancel()
Selim Cinekafae4e72020-06-16 18:21:41 -0700228 applyStateToLayout(this.state)
Selim Cinek2de5ebb2020-05-20 15:39:03 -0700229 currentState = state.copy(reusedState = currentState)
230 } else if (animated) {
231 animationStartState = currentState.copy()
232 animator.duration = duration
233 animator.startDelay = delay
234 animator.start()
235 } else if (!animator.isRunning) {
Selim Cinekafae4e72020-06-16 18:21:41 -0700236 applyStateToLayout(this.state)
Selim Cinek2de5ebb2020-05-20 15:39:03 -0700237 currentState = state.copy(reusedState = currentState)
238 }
239 // otherwise the desired state was updated and the animation will go to the new target
240 }
241
242 /**
243 * Set a new state that will be used to measure the view itself and is useful during
244 * transitions, where the state set via [setState] may differ from how the view
245 * should be measured.
246 */
247 fun setMeasureState(
248 state: TransitionViewState
249 ) {
250 transitionLayout?.measureState = state
251 }
252}