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