• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
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 
17 package com.android.systemui.util.animation
18 
19 import android.animation.ValueAnimator
20 import android.graphics.PointF
21 import android.util.MathUtils
22 import com.android.systemui.animation.Interpolators
23 
24 /**
25  * The fraction after which we start fading in when going from a gone widget to a visible one
26  */
27 private const val GONE_FADE_FRACTION = 0.8f
28 
29 /**
30  * The amont we're scaling appearing views
31  */
32 private 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  */
38 open 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()
47     private var animator: ValueAnimator = ValueAnimator.ofFloat(0.0f, 1.0f)
48     private var currentHeight: Int = 0
49     private var currentWidth: Int = 0
50     var sizeChangedListener: ((Int, Int) -> Unit)? = null
51 
52     init {
<lambda>null53         animator.apply {
54             addUpdateListener {
55                 updateStateFromAnimation()
56             }
57             interpolator = Interpolators.FAST_OUT_SLOW_IN
58         }
59     }
60 
updateStateFromAnimationnull61     private fun updateStateFromAnimation() {
62         if (animationStartState == null || !animator.isRunning) {
63             return
64         }
65         currentState = getInterpolatedState(
66                 startState = animationStartState!!,
67                 endState = state,
68                 progress = animator.animatedFraction,
69                 reusedState = currentState)
70         applyStateToLayout(currentState)
71     }
72 
applyStateToLayoutnull73     private fun applyStateToLayout(state: TransitionViewState) {
74         transitionLayout?.setState(state)
75         if (currentHeight != state.height || currentWidth != state.width) {
76             currentHeight = state.height
77             currentWidth = state.width
78             sizeChangedListener?.invoke(currentWidth, currentHeight)
79         }
80     }
81 
82     /**
83      * Obtain a state that is gone, based on parameters given.
84      *
85      * @param viewState the viewState to make gone
86      * @param disappearParameters parameters that determine how the view should disappear
87      * @param goneProgress how much is the view gone? 0 for not gone at all and 1 for fully
88      *                     disappeared
89      * @param reusedState optional parameter for state to be reused to avoid allocations
90      */
getGoneStatenull91     fun getGoneState(
92         viewState: TransitionViewState,
93         disappearParameters: DisappearParameters,
94         goneProgress: Float,
95         reusedState: TransitionViewState? = null
96     ): TransitionViewState {
97         var remappedProgress = MathUtils.map(
98                 disappearParameters.disappearStart,
99                 disappearParameters.disappearEnd,
100                 0.0f, 1.0f,
101                 goneProgress)
102         remappedProgress = MathUtils.constrain(remappedProgress, 0.0f, 1.0f)
103         val result = viewState.copy(reusedState).apply {
104             width = MathUtils.lerp(
105                     viewState.width.toFloat(),
106                     viewState.width * disappearParameters.disappearSize.x,
107                     remappedProgress).toInt()
108             height = MathUtils.lerp(
109                     viewState.height.toFloat(),
110                     viewState.height * disappearParameters.disappearSize.y,
111                     remappedProgress).toInt()
112             translation.x = (viewState.width - width) * disappearParameters.gonePivot.x
113             translation.y = (viewState.height - height) * disappearParameters.gonePivot.y
114             contentTranslation.x = (disappearParameters.contentTranslationFraction.x - 1.0f) *
115                     translation.x
116             contentTranslation.y = (disappearParameters.contentTranslationFraction.y - 1.0f) *
117                     translation.y
118             val alphaProgress = MathUtils.map(
119                     disappearParameters.fadeStartPosition, 1.0f, 1.0f, 0.0f, remappedProgress)
120             alpha = MathUtils.constrain(alphaProgress, 0.0f, 1.0f)
121         }
122         return result
123     }
124 
125     /**
126      * Get an interpolated state between two viewstates. This interpolates all positions for all
127      * widgets as well as it's bounds based on the given input.
128      */
getInterpolatedStatenull129     fun getInterpolatedState(
130         startState: TransitionViewState,
131         endState: TransitionViewState,
132         progress: Float,
133         reusedState: TransitionViewState? = null
134     ): TransitionViewState {
135         val resultState = reusedState ?: TransitionViewState()
136         val view = transitionLayout ?: return resultState
137         val childCount = view.childCount
138         for (i in 0 until childCount) {
139             val id = view.getChildAt(i).id
140             val resultWidgetState = resultState.widgetStates[id] ?: WidgetState()
141             val widgetStart = startState.widgetStates[id] ?: continue
142             val widgetEnd = endState.widgetStates[id] ?: continue
143             var alphaProgress = progress
144             var widthProgress = progress
145             val resultMeasureWidth: Int
146             val resultMeasureHeight: Int
147             val newScale: Float
148             val resultX: Float
149             val resultY: Float
150             if (widgetStart.gone != widgetEnd.gone) {
151                 // A view is appearing or disappearing. Let's not just interpolate between them as
152                 // this looks quite ugly
153                 val nowGone: Boolean
154                 if (widgetStart.gone) {
155 
156                     // Only fade it in at the very end
157                     alphaProgress = MathUtils.map(GONE_FADE_FRACTION, 1.0f, 0.0f, 1.0f, progress)
158                     nowGone = progress < GONE_FADE_FRACTION
159 
160                     // Scale it just a little, not all the way
161                     val endScale = widgetEnd.scale
162                     newScale = MathUtils.lerp(GONE_SCALE_AMOUNT * endScale, endScale, progress)
163 
164                     // don't clip
165                     widthProgress = 1.0f
166 
167                     // Let's directly measure it with the end state
168                     resultMeasureWidth = widgetEnd.measureWidth
169                     resultMeasureHeight = widgetEnd.measureHeight
170 
171                     // Let's make sure we're centering the view in the gone view instead of having
172                     // the left at 0
173                     resultX = MathUtils.lerp(widgetStart.x - resultMeasureWidth / 2.0f,
174                             widgetEnd.x,
175                             progress)
176                     resultY = MathUtils.lerp(widgetStart.y - resultMeasureHeight / 2.0f,
177                             widgetEnd.y,
178                             progress)
179                 } else {
180 
181                     // Fadeout in the very beginning
182                     alphaProgress = MathUtils.map(0.0f, 1.0f - GONE_FADE_FRACTION, 0.0f, 1.0f,
183                             progress)
184                     nowGone = progress > 1.0f - GONE_FADE_FRACTION
185 
186                     // Scale it just a little, not all the way
187                     val startScale = widgetStart.scale
188                     newScale = MathUtils.lerp(startScale, startScale * GONE_SCALE_AMOUNT, progress)
189 
190                     // Don't clip
191                     widthProgress = 0.0f
192 
193                     // Let's directly measure it with the start state
194                     resultMeasureWidth = widgetStart.measureWidth
195                     resultMeasureHeight = widgetStart.measureHeight
196 
197                     // Let's make sure we're centering the view in the gone view instead of having
198                     // the left at 0
199                     resultX = MathUtils.lerp(widgetStart.x,
200                             widgetEnd.x - resultMeasureWidth / 2.0f,
201                             progress)
202                     resultY = MathUtils.lerp(widgetStart.y,
203                             widgetEnd.y - resultMeasureHeight / 2.0f,
204                             progress)
205                 }
206                 resultWidgetState.gone = nowGone
207             } else {
208                 resultWidgetState.gone = widgetStart.gone
209                 // Let's directly measure it with the end state
210                 resultMeasureWidth = widgetEnd.measureWidth
211                 resultMeasureHeight = widgetEnd.measureHeight
212                 newScale = MathUtils.lerp(widgetStart.scale, widgetEnd.scale, progress)
213                 resultX = MathUtils.lerp(widgetStart.x, widgetEnd.x, progress)
214                 resultY = MathUtils.lerp(widgetStart.y, widgetEnd.y, progress)
215             }
216             resultWidgetState.apply {
217                 x = resultX
218                 y = resultY
219                 alpha = MathUtils.lerp(widgetStart.alpha, widgetEnd.alpha, alphaProgress)
220                 width = MathUtils.lerp(widgetStart.width.toFloat(), widgetEnd.width.toFloat(),
221                         widthProgress).toInt()
222                 height = MathUtils.lerp(widgetStart.height.toFloat(), widgetEnd.height.toFloat(),
223                         widthProgress).toInt()
224                 scale = newScale
225 
226                 // Let's directly measure it with the end state
227                 measureWidth = resultMeasureWidth
228                 measureHeight = resultMeasureHeight
229             }
230             resultState.widgetStates[id] = resultWidgetState
231         }
232         resultState.apply {
233             width = MathUtils.lerp(startState.width.toFloat(), endState.width.toFloat(),
234                     progress).toInt()
235             height = MathUtils.lerp(startState.height.toFloat(), endState.height.toFloat(),
236                     progress).toInt()
237             translation.x = MathUtils.lerp(startState.translation.x, endState.translation.x,
238                     progress)
239             translation.y = MathUtils.lerp(startState.translation.y, endState.translation.y,
240                     progress)
241             alpha = MathUtils.lerp(startState.alpha, endState.alpha, progress)
242             contentTranslation.x = MathUtils.lerp(
243                     startState.contentTranslation.x,
244                     endState.contentTranslation.x,
245                     progress)
246             contentTranslation.y = MathUtils.lerp(
247                     startState.contentTranslation.y,
248                     endState.contentTranslation.y,
249                     progress)
250         }
251         return resultState
252     }
253 
attachnull254     fun attach(transitionLayout: TransitionLayout) {
255         this.transitionLayout = transitionLayout
256     }
257 
258     /**
259      * Set a new state to be applied to the dynamic view.
260      *
261      * @param state the state to be applied
262      * @param animate should this change be animated. If [false] the we will either apply the
263      * state immediately if no animation is running, and if one is running, we will update the end
264      * value to match the new state.
265      * @param applyImmediately should this change be applied immediately, canceling all running
266      * animations
267      */
setStatenull268     fun setState(
269         state: TransitionViewState,
270         applyImmediately: Boolean,
271         animate: Boolean,
272         duration: Long = 0,
273         delay: Long = 0
274     ) {
275         val animated = animate && currentState.width != 0
276         this.state = state.copy()
277         if (applyImmediately || transitionLayout == null) {
278             animator.cancel()
279             applyStateToLayout(this.state)
280             currentState = state.copy(reusedState = currentState)
281         } else if (animated) {
282             animationStartState = currentState.copy()
283             animator.duration = duration
284             animator.startDelay = delay
285             animator.start()
286         } else if (!animator.isRunning) {
287             applyStateToLayout(this.state)
288             currentState = state.copy(reusedState = currentState)
289         }
290         // otherwise the desired state was updated and the animation will go to the new target
291     }
292 
293     /**
294      * Set a new state that will be used to measure the view itself and is useful during
295      * transitions, where the state set via [setState] may differ from how the view
296      * should be measured.
297      */
setMeasureStatenull298     fun setMeasureState(
299         state: TransitionViewState
300     ) {
301         transitionLayout?.measureState = state
302     }
303 }
304 
305 class DisappearParameters() {
306 
307     /**
308      * The pivot point when clipping view when disappearing, which describes how the content will
309      * be translated.
310      * The default value of (0.0f, 1.0f) means that the view will not be translated in horizontally
311      * and the vertical disappearing will be aligned on the bottom of the view,
312      */
313     var gonePivot = PointF(0.0f, 1.0f)
314 
315     /**
316      * The fraction of the width and height that will remain when disappearing. The default of
317      * (1.0f, 0.0f) means that 100% of the width, but 0% of the height will remain at the end of
318      * the transition.
319      */
320     var disappearSize = PointF(1.0f, 0.0f)
321 
322     /**
323      * The fraction of the normal translation, by which the content will be moved during the
324      * disappearing. The values here can be both negative as well as positive. The default value
325      * of (0.0f, 0.2f) means that the content doesn't move horizontally but moves 20% of the
326      * translation imposed by the pivot downwards. 1.0f means that the content will be translated
327      * in sync with the translation of the bounds
328      */
329     var contentTranslationFraction = PointF(0.0f, 0.8f)
330 
331     /**
332      * The point during the progress from [0.0, 1.0f] where the view is fully appeared. 0.0f
333      * means that the content will start disappearing immediately, while 0.5f means that it
334      * starts disappearing half way through the progress.
335      */
336     var disappearStart = 0.0f
337 
338     /**
339      * The point during the progress from [0.0, 1.0f] where the view has fully disappeared. 1.0f
340      * means that the view will disappear in sync with the progress, while 0.5f means that it
341      * is fully gone half way through the progress.
342      */
343     var disappearEnd = 1.0f
344 
345     /**
346      * The point during the mapped progress from [0.0, 1.0f] where the view starts fading out. 1.0f
347      * means that the view doesn't fade at all, while 0.5 means that the content fades starts
348      * fading at the midpoint between [disappearStart] and [disappearEnd]
349      */
350     var fadeStartPosition = 0.9f
351 
equalsnull352     override fun equals(other: Any?): Boolean {
353         if (!(other is DisappearParameters)) {
354             return false
355         }
356         if (!disappearSize.equals(other.disappearSize)) {
357             return false
358         }
359         if (!gonePivot.equals(other.gonePivot)) {
360             return false
361         }
362         if (!contentTranslationFraction.equals(other.contentTranslationFraction)) {
363             return false
364         }
365         if (disappearStart != other.disappearStart) {
366             return false
367         }
368         if (disappearEnd != other.disappearEnd) {
369             return false
370         }
371         if (fadeStartPosition != other.fadeStartPosition) {
372             return false
373         }
374         return true
375     }
376 
hashCodenull377     override fun hashCode(): Int {
378         var result = disappearSize.hashCode()
379         result = 31 * result + gonePivot.hashCode()
380         result = 31 * result + contentTranslationFraction.hashCode()
381         result = 31 * result + disappearStart.hashCode()
382         result = 31 * result + disappearEnd.hashCode()
383         result = 31 * result + fadeStartPosition.hashCode()
384         return result
385     }
386 
deepCopynull387     fun deepCopy(): DisappearParameters {
388         val result = DisappearParameters()
389         result.disappearSize.set(disappearSize)
390         result.gonePivot.set(gonePivot)
391         result.contentTranslationFraction.set(contentTranslationFraction)
392         result.disappearStart = disappearStart
393         result.disappearEnd = disappearEnd
394         result.fadeStartPosition = fadeStartPosition
395         return result
396     }
397 }
398