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