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.content.Context
20 import android.graphics.Canvas
21 import android.graphics.PointF
22 import android.graphics.Rect
23 import android.text.Layout
24 import android.util.AttributeSet
25 import android.view.View
26 import android.view.ViewTreeObserver
27 import android.widget.TextView
28 import androidx.constraintlayout.widget.ConstraintLayout
29 import androidx.constraintlayout.widget.ConstraintSet
30 import com.android.systemui.statusbar.CrossFadeHelper
31 
32 /**
33  * A view that handles displaying of children and transitions of them in an optimized way,
34  * minimizing the number of measure passes, while allowing for maximum flexibility
35  * and interruptibility.
36  */
37 class TransitionLayout @JvmOverloads constructor(
38     context: Context,
39     attrs: AttributeSet? = null,
40     defStyleAttr: Int = 0
41 ) : ConstraintLayout(context, attrs, defStyleAttr) {
42 
43     private val boundsRect = Rect()
44     private val originalGoneChildrenSet: MutableSet<Int> = mutableSetOf()
45     private val originalViewAlphas: MutableMap<Int, Float> = mutableMapOf()
46     private var measureAsConstraint: Boolean = false
47     private var currentState: TransitionViewState = TransitionViewState()
48     private var updateScheduled = false
49 
50     private var desiredMeasureWidth = 0
51     private var desiredMeasureHeight = 0
52     /**
53      * The measured state of this view which is the one we will lay ourselves out with. This
54      * may differ from the currentState if there is an external animation or transition running.
55      * This state will not be used to measure the widgets, where the current state is preferred.
56      */
57     var measureState: TransitionViewState = TransitionViewState()
58         set(value) {
59             val newWidth = value.width
60             val newHeight = value.height
61             if (newWidth != desiredMeasureWidth || newHeight != desiredMeasureHeight) {
62                 desiredMeasureWidth = newWidth
63                 desiredMeasureHeight = newHeight
64                 // We need to make sure next time we're measured that our onMeasure will be called.
65                 // Otherwise our parent thinks we still have the same height
66                 if (isInLayout()) {
67                     forceLayout()
68                 } else {
69                     requestLayout()
70                 }
71             }
72         }
73     private val preDrawApplicator = object : ViewTreeObserver.OnPreDrawListener {
onPreDrawnull74         override fun onPreDraw(): Boolean {
75             updateScheduled = false
76             viewTreeObserver.removeOnPreDrawListener(this)
77             applyCurrentState()
78             return true
79         }
80     }
81 
onFinishInflatenull82     override fun onFinishInflate() {
83         super.onFinishInflate()
84         val childCount = childCount
85         for (i in 0 until childCount) {
86             val child = getChildAt(i)
87             if (child.id == View.NO_ID) {
88                 child.id = i
89             }
90             if (child.visibility == GONE) {
91                 originalGoneChildrenSet.add(child.id)
92             }
93             originalViewAlphas[child.id] = child.alpha
94         }
95     }
96 
97     /**
98      * Apply the current state to the view and its widgets
99      */
applyCurrentStatenull100     private fun applyCurrentState() {
101         val childCount = childCount
102         val contentTranslationX = currentState.contentTranslation.x.toInt()
103         val contentTranslationY = currentState.contentTranslation.y.toInt()
104         for (i in 0 until childCount) {
105             val child = getChildAt(i)
106             val widgetState = currentState.widgetStates.get(child.id) ?: continue
107 
108             // TextViews which are measured and sized differently should be handled with a
109             // "clip mode", which means we clip explicitly rather than implicitly by passing
110             // different sizes to measure/layout than setLeftTopRightBottom.
111             // Then to accommodate RTL text, we need a "clip shift" which allows us to have the
112             // clipBounds be attached to the right side of the view instead of the left.
113             val clipModeShift =
114                     if (child is TextView && widgetState.width < widgetState.measureWidth) {
115                 if (child.layout.getParagraphDirection(0) == Layout.DIR_RIGHT_TO_LEFT) {
116                     widgetState.measureWidth - widgetState.width
117                 } else {
118                     0
119                 }
120             } else {
121                 null
122             }
123 
124             if (child.measuredWidth != widgetState.measureWidth ||
125                     child.measuredHeight != widgetState.measureHeight) {
126                 val measureWidthSpec = MeasureSpec.makeMeasureSpec(widgetState.measureWidth,
127                         MeasureSpec.EXACTLY)
128                 val measureHeightSpec = MeasureSpec.makeMeasureSpec(widgetState.measureHeight,
129                         MeasureSpec.EXACTLY)
130                 child.measure(measureWidthSpec, measureHeightSpec)
131                 child.layout(0, 0, child.measuredWidth, child.measuredHeight)
132             }
133             val clipShift = clipModeShift ?: 0
134             val left = widgetState.x.toInt() + contentTranslationX - clipShift
135             val top = widgetState.y.toInt() + contentTranslationY
136             val clipMode = clipModeShift != null
137             val boundsWidth = if (clipMode) widgetState.measureWidth else widgetState.width
138             val boundsHeight = if (clipMode) widgetState.measureHeight else widgetState.height
139             child.setLeftTopRightBottom(left, top, left + boundsWidth, top + boundsHeight)
140             child.scaleX = widgetState.scale
141             child.scaleY = widgetState.scale
142             val clipBounds = child.clipBounds ?: Rect()
143             clipBounds.set(clipShift, 0, widgetState.width + clipShift, widgetState.height)
144             child.clipBounds = clipBounds
145             CrossFadeHelper.fadeIn(child, widgetState.alpha)
146             child.visibility = if (widgetState.gone || widgetState.alpha == 0.0f) {
147                 View.INVISIBLE
148             } else {
149                 View.VISIBLE
150             }
151         }
152         updateBounds()
153         translationX = currentState.translation.x
154         translationY = currentState.translation.y
155         CrossFadeHelper.fadeIn(this, currentState.alpha)
156     }
157 
applyCurrentStateOnPredrawnull158     private fun applyCurrentStateOnPredraw() {
159         if (!updateScheduled) {
160             updateScheduled = true
161             viewTreeObserver.addOnPreDrawListener(preDrawApplicator)
162         }
163     }
164 
onMeasurenull165     override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
166         if (measureAsConstraint) {
167             super.onMeasure(widthMeasureSpec, heightMeasureSpec)
168         } else {
169             for (i in 0 until childCount) {
170                 val child = getChildAt(i)
171                 val widgetState = currentState.widgetStates.get(child.id) ?: continue
172                 val measureWidthSpec = MeasureSpec.makeMeasureSpec(widgetState.measureWidth,
173                         MeasureSpec.EXACTLY)
174                 val measureHeightSpec = MeasureSpec.makeMeasureSpec(widgetState.measureHeight,
175                         MeasureSpec.EXACTLY)
176                 child.measure(measureWidthSpec, measureHeightSpec)
177             }
178             setMeasuredDimension(desiredMeasureWidth, desiredMeasureHeight)
179         }
180     }
181 
onLayoutnull182     override fun onLayout(changed: Boolean, l: Int, t: Int, r: Int, b: Int) {
183         if (measureAsConstraint) {
184             super.onLayout(changed, left, top, right, bottom)
185         } else {
186             val childCount = childCount
187             for (i in 0 until childCount) {
188                 val child = getChildAt(i)
189                 child.layout(0, 0, child.measuredWidth, child.measuredHeight)
190             }
191             // Reapply the bounds to update the background
192             applyCurrentState()
193         }
194     }
195 
dispatchDrawnull196     override fun dispatchDraw(canvas: Canvas?) {
197         canvas?.save()
198         canvas?.clipRect(boundsRect)
199         super.dispatchDraw(canvas)
200         canvas?.restore()
201     }
202 
updateBoundsnull203     private fun updateBounds() {
204         val layoutLeft = left
205         val layoutTop = top
206         setLeftTopRightBottom(layoutLeft, layoutTop, layoutLeft + currentState.width,
207                 layoutTop + currentState.height)
208         boundsRect.set(0, 0, width.toInt(), height.toInt())
209     }
210 
211     /**
212      * Calculates a view state for a given ConstraintSet and measurement, saving all positions
213      * of all widgets.
214      *
215      * @param input the measurement input this should be done with
216      * @param constraintSet the constraint set to apply
217      * @param resusableState the result that we can reuse to minimize memory impact
218      */
calculateViewStatenull219     fun calculateViewState(
220         input: MeasurementInput,
221         constraintSet: ConstraintSet,
222         existing: TransitionViewState? = null
223     ): TransitionViewState {
224 
225         val result = existing ?: TransitionViewState()
226         // Reset gone children to the original state
227         applySetToFullLayout(constraintSet)
228         val previousHeight = measuredHeight
229         val previousWidth = measuredWidth
230 
231         // Let's measure outselves as a ConstraintLayout
232         measureAsConstraint = true
233         measure(input.widthMeasureSpec, input.heightMeasureSpec)
234         val layoutLeft = left
235         val layoutTop = top
236         layout(layoutLeft, layoutTop, layoutLeft + measuredWidth, layoutTop + measuredHeight)
237         measureAsConstraint = false
238         result.initFromLayout(this)
239         ensureViewsNotGone()
240 
241         // Let's reset our layout to have the right size again
242         setMeasuredDimension(previousWidth, previousHeight)
243         applyCurrentStateOnPredraw()
244         return result
245     }
246 
applySetToFullLayoutnull247     private fun applySetToFullLayout(constraintSet: ConstraintSet) {
248         // Let's reset our views to the initial gone state of the layout, since the constraintset
249         // might only be a subset of the views. Otherwise the gone state would be calculated
250         // wrongly later if we made this invisible in the layout (during apply we make sure they
251         // are invisible instead
252         val childCount = childCount
253         for (i in 0 until childCount) {
254             val child = getChildAt(i)
255             if (originalGoneChildrenSet.contains(child.id)) {
256                 child.visibility = View.GONE
257             }
258             // Reset the alphas, to only have the alphas present from the constraintset
259             child.alpha = originalViewAlphas[child.id] ?: 1.0f
260         }
261         // Let's now apply the constraintSet to get the full state
262         constraintSet.applyTo(this)
263     }
264 
265     /**
266      * Ensures that our views are never gone but invisible instead, this allows us to animate them
267      * without remeasuring.
268      */
ensureViewsNotGonenull269     private fun ensureViewsNotGone() {
270         val childCount = childCount
271         for (i in 0 until childCount) {
272             val child = getChildAt(i)
273             val widgetState = currentState.widgetStates.get(child.id)
274             child.visibility = if (widgetState?.gone != false) View.INVISIBLE else View.VISIBLE
275         }
276     }
277 
278     /**
279      * Set the state that should be applied to this View
280      *
281      */
setStatenull282     fun setState(state: TransitionViewState) {
283         currentState = state
284         applyCurrentState()
285     }
286 }
287 
288 class TransitionViewState {
289     var widgetStates: MutableMap<Int, WidgetState> = mutableMapOf()
290     var width: Int = 0
291     var height: Int = 0
292     var alpha: Float = 1.0f
293     val translation = PointF()
294     val contentTranslation = PointF()
copynull295     fun copy(reusedState: TransitionViewState? = null): TransitionViewState {
296         // we need a deep copy of this, so we can't use a data class
297         val copy = reusedState ?: TransitionViewState()
298         copy.width = width
299         copy.height = height
300         copy.alpha = alpha
301         copy.translation.set(translation.x, translation.y)
302         copy.contentTranslation.set(contentTranslation.x, contentTranslation.y)
303         for (entry in widgetStates) {
304             copy.widgetStates[entry.key] = entry.value.copy()
305         }
306         return copy
307     }
308 
initFromLayoutnull309     fun initFromLayout(transitionLayout: TransitionLayout) {
310         val childCount = transitionLayout.childCount
311         for (i in 0 until childCount) {
312             val child = transitionLayout.getChildAt(i)
313             val widgetState = widgetStates.getOrPut(child.id, {
314                 WidgetState(0.0f, 0.0f, 0, 0, 0, 0, 0.0f)
315             })
316             widgetState.initFromLayout(child)
317         }
318         width = transitionLayout.measuredWidth
319         height = transitionLayout.measuredHeight
320         translation.set(0.0f, 0.0f)
321         contentTranslation.set(0.0f, 0.0f)
322         alpha = 1.0f
323     }
324 }
325 
326 data class WidgetState(
327     var x: Float = 0.0f,
328     var y: Float = 0.0f,
329     var width: Int = 0,
330     var height: Int = 0,
331     var measureWidth: Int = 0,
332     var measureHeight: Int = 0,
333     var alpha: Float = 1.0f,
334     var scale: Float = 1.0f,
335     var gone: Boolean = false
336 ) {
initFromLayoutnull337     fun initFromLayout(view: View) {
338         gone = view.visibility == View.GONE
339         if (gone) {
340             val layoutParams = view.layoutParams as ConstraintLayout.LayoutParams
341             x = layoutParams.constraintWidget.left.toFloat()
342             y = layoutParams.constraintWidget.top.toFloat()
343             width = layoutParams.constraintWidget.width
344             height = layoutParams.constraintWidget.height
345             measureHeight = height
346             measureWidth = width
347             alpha = 0.0f
348             scale = 0.0f
349         } else {
350             x = view.left.toFloat()
351             y = view.top.toFloat()
352             width = view.width
353             height = view.height
354             measureWidth = width
355             measureHeight = height
356             gone = view.visibility == View.GONE
357             alpha = view.alpha
358             // No scale by default. Only during transitions!
359             scale = 1.0f
360         }
361     }
362 }
363