• 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.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     private var isPreDrawApplicatorRegistered = false
50 
51     private var desiredMeasureWidth = 0
52     private var desiredMeasureHeight = 0
53     private var transitionVisibility = View.VISIBLE
54 
55     /**
56      * The measured state of this view which is the one we will lay ourselves out with. This
57      * may differ from the currentState if there is an external animation or transition running.
58      * This state will not be used to measure the widgets, where the current state is preferred.
59      */
60     var measureState: TransitionViewState = TransitionViewState()
61         set(value) {
62             val newWidth = value.width
63             val newHeight = value.height
64             if (newWidth != desiredMeasureWidth || newHeight != desiredMeasureHeight) {
65                 desiredMeasureWidth = newWidth
66                 desiredMeasureHeight = newHeight
67                 // We need to make sure next time we're measured that our onMeasure will be called.
68                 // Otherwise our parent thinks we still have the same height
69                 if (isInLayout()) {
70                     forceLayout()
71                 } else {
72                     requestLayout()
73                 }
74             }
75         }
76     private val preDrawApplicator = object : ViewTreeObserver.OnPreDrawListener {
onPreDrawnull77         override fun onPreDraw(): Boolean {
78             updateScheduled = false
79             viewTreeObserver.removeOnPreDrawListener(this)
80             isPreDrawApplicatorRegistered = false
81             applyCurrentState()
82             return true
83         }
84     }
85 
setTransitionVisibilitynull86     override fun setTransitionVisibility(visibility: Int) {
87         // We store the last transition visibility assigned to this view to restore it later if
88         // necessary.
89         super.setTransitionVisibility(visibility)
90         transitionVisibility = visibility
91     }
92 
onFinishInflatenull93     override fun onFinishInflate() {
94         super.onFinishInflate()
95         val childCount = childCount
96         for (i in 0 until childCount) {
97             val child = getChildAt(i)
98             if (child.id == View.NO_ID) {
99                 child.id = i
100             }
101             if (child.visibility == GONE) {
102                 originalGoneChildrenSet.add(child.id)
103             }
104             originalViewAlphas[child.id] = child.alpha
105         }
106     }
107 
onDetachedFromWindownull108     override fun onDetachedFromWindow() {
109         super.onDetachedFromWindow()
110         if (isPreDrawApplicatorRegistered) {
111             viewTreeObserver.removeOnPreDrawListener(preDrawApplicator)
112             isPreDrawApplicatorRegistered = false
113         }
114     }
115 
116     /**
117      * Apply the current state to the view and its widgets
118      */
applyCurrentStatenull119     private fun applyCurrentState() {
120         val childCount = childCount
121         val contentTranslationX = currentState.contentTranslation.x.toInt()
122         val contentTranslationY = currentState.contentTranslation.y.toInt()
123         for (i in 0 until childCount) {
124             val child = getChildAt(i)
125             val widgetState = currentState.widgetStates.get(child.id) ?: continue
126 
127             // TextViews which are measured and sized differently should be handled with a
128             // "clip mode", which means we clip explicitly rather than implicitly by passing
129             // different sizes to measure/layout than setLeftTopRightBottom.
130             // Then to accommodate RTL text, we need a "clip shift" which allows us to have the
131             // clipBounds be attached to the right side of the view instead of the left.
132             val clipModeShift =
133                     if (child is TextView && widgetState.width < widgetState.measureWidth) {
134                 if (child.layout.getParagraphDirection(0) == Layout.DIR_RIGHT_TO_LEFT) {
135                     widgetState.measureWidth - widgetState.width
136                 } else {
137                     0
138                 }
139             } else {
140                 null
141             }
142 
143             if (child.measuredWidth != widgetState.measureWidth ||
144                     child.measuredHeight != widgetState.measureHeight) {
145                 val measureWidthSpec = MeasureSpec.makeMeasureSpec(widgetState.measureWidth,
146                         MeasureSpec.EXACTLY)
147                 val measureHeightSpec = MeasureSpec.makeMeasureSpec(widgetState.measureHeight,
148                         MeasureSpec.EXACTLY)
149                 child.measure(measureWidthSpec, measureHeightSpec)
150                 child.layout(0, 0, child.measuredWidth, child.measuredHeight)
151             }
152             val clipShift = clipModeShift ?: 0
153             val left = widgetState.x.toInt() + contentTranslationX - clipShift
154             val top = widgetState.y.toInt() + contentTranslationY
155             val clipMode = clipModeShift != null
156             val boundsWidth = if (clipMode) widgetState.measureWidth else widgetState.width
157             val boundsHeight = if (clipMode) widgetState.measureHeight else widgetState.height
158             child.setLeftTopRightBottom(left, top, left + boundsWidth, top + boundsHeight)
159             child.scaleX = widgetState.scale
160             child.scaleY = widgetState.scale
161             val clipBounds = child.clipBounds ?: Rect()
162             clipBounds.set(clipShift, 0, widgetState.width + clipShift, widgetState.height)
163             child.clipBounds = clipBounds
164             CrossFadeHelper.fadeIn(child, widgetState.alpha)
165             child.visibility = if (widgetState.gone || widgetState.alpha == 0.0f) {
166                 View.INVISIBLE
167             } else {
168                 View.VISIBLE
169             }
170         }
171         updateBounds()
172         translationX = currentState.translation.x
173         translationY = currentState.translation.y
174 
175         CrossFadeHelper.fadeIn(this, currentState.alpha)
176 
177         // CrossFadeHelper#fadeIn will change this view visibility, which overrides the transition
178         // visibility. We set the transition visibility again to make sure that this view plays well
179         // with GhostView, which sets the transition visibility and is used for activity launch
180         // animations.
181         if (transitionVisibility != View.VISIBLE) {
182             setTransitionVisibility(transitionVisibility)
183         }
184     }
185 
applyCurrentStateOnPredrawnull186     private fun applyCurrentStateOnPredraw() {
187         if (!updateScheduled) {
188             updateScheduled = true
189             if (!isPreDrawApplicatorRegistered) {
190                 viewTreeObserver.addOnPreDrawListener(preDrawApplicator)
191                 isPreDrawApplicatorRegistered = true
192             }
193         }
194     }
195 
onMeasurenull196     override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
197         if (measureAsConstraint) {
198             super.onMeasure(widthMeasureSpec, heightMeasureSpec)
199         } else {
200             for (i in 0 until childCount) {
201                 val child = getChildAt(i)
202                 val widgetState = currentState.widgetStates.get(child.id) ?: continue
203                 val measureWidthSpec = MeasureSpec.makeMeasureSpec(widgetState.measureWidth,
204                         MeasureSpec.EXACTLY)
205                 val measureHeightSpec = MeasureSpec.makeMeasureSpec(widgetState.measureHeight,
206                         MeasureSpec.EXACTLY)
207                 child.measure(measureWidthSpec, measureHeightSpec)
208             }
209             setMeasuredDimension(desiredMeasureWidth, desiredMeasureHeight)
210         }
211     }
212 
onLayoutnull213     override fun onLayout(changed: Boolean, l: Int, t: Int, r: Int, b: Int) {
214         if (measureAsConstraint) {
215             super.onLayout(changed, left, top, right, bottom)
216         } else {
217             val childCount = childCount
218             for (i in 0 until childCount) {
219                 val child = getChildAt(i)
220                 child.layout(0, 0, child.measuredWidth, child.measuredHeight)
221             }
222             // Reapply the bounds to update the background
223             applyCurrentState()
224         }
225     }
226 
dispatchDrawnull227     override fun dispatchDraw(canvas: Canvas?) {
228         canvas?.save()
229         canvas?.clipRect(boundsRect)
230         super.dispatchDraw(canvas)
231         canvas?.restore()
232     }
233 
updateBoundsnull234     private fun updateBounds() {
235         val layoutLeft = left
236         val layoutTop = top
237         setLeftTopRightBottom(layoutLeft, layoutTop, layoutLeft + currentState.width,
238                 layoutTop + currentState.height)
239         boundsRect.set(0, 0, width.toInt(), height.toInt())
240     }
241 
242     /**
243      * Calculates a view state for a given ConstraintSet and measurement, saving all positions
244      * of all widgets.
245      *
246      * @param input the measurement input this should be done with
247      * @param constraintSet the constraint set to apply
248      * @param resusableState the result that we can reuse to minimize memory impact
249      */
calculateViewStatenull250     fun calculateViewState(
251         input: MeasurementInput,
252         constraintSet: ConstraintSet,
253         existing: TransitionViewState? = null
254     ): TransitionViewState {
255 
256         val result = existing ?: TransitionViewState()
257         // Reset gone children to the original state
258         applySetToFullLayout(constraintSet)
259         val previousHeight = measuredHeight
260         val previousWidth = measuredWidth
261 
262         // Let's measure outselves as a ConstraintLayout
263         measureAsConstraint = true
264         measure(input.widthMeasureSpec, input.heightMeasureSpec)
265         val layoutLeft = left
266         val layoutTop = top
267         layout(layoutLeft, layoutTop, layoutLeft + measuredWidth, layoutTop + measuredHeight)
268         measureAsConstraint = false
269         result.initFromLayout(this)
270         ensureViewsNotGone()
271 
272         // Let's reset our layout to have the right size again
273         setMeasuredDimension(previousWidth, previousHeight)
274         applyCurrentStateOnPredraw()
275         return result
276     }
277 
applySetToFullLayoutnull278     private fun applySetToFullLayout(constraintSet: ConstraintSet) {
279         // Let's reset our views to the initial gone state of the layout, since the constraintset
280         // might only be a subset of the views. Otherwise the gone state would be calculated
281         // wrongly later if we made this invisible in the layout (during apply we make sure they
282         // are invisible instead
283         val childCount = childCount
284         for (i in 0 until childCount) {
285             val child = getChildAt(i)
286             if (originalGoneChildrenSet.contains(child.id)) {
287                 child.visibility = View.GONE
288             }
289             // Reset the alphas, to only have the alphas present from the constraintset
290             child.alpha = originalViewAlphas[child.id] ?: 1.0f
291         }
292         // Let's now apply the constraintSet to get the full state
293         constraintSet.applyTo(this)
294     }
295 
296     /**
297      * Ensures that our views are never gone but invisible instead, this allows us to animate them
298      * without remeasuring.
299      */
ensureViewsNotGonenull300     private fun ensureViewsNotGone() {
301         val childCount = childCount
302         for (i in 0 until childCount) {
303             val child = getChildAt(i)
304             val widgetState = currentState.widgetStates.get(child.id)
305             child.visibility = if (widgetState?.gone != false) View.INVISIBLE else View.VISIBLE
306         }
307     }
308 
309     /**
310      * Set the state that should be applied to this View
311      *
312      */
setStatenull313     fun setState(state: TransitionViewState) {
314         currentState = state
315         applyCurrentState()
316     }
317 }
318 
319 class TransitionViewState {
320     var widgetStates: MutableMap<Int, WidgetState> = mutableMapOf()
321     var width: Int = 0
322     var height: Int = 0
323     var alpha: Float = 1.0f
324     val translation = PointF()
325     val contentTranslation = PointF()
copynull326     fun copy(reusedState: TransitionViewState? = null): TransitionViewState {
327         // we need a deep copy of this, so we can't use a data class
328         val copy = reusedState ?: TransitionViewState()
329         copy.width = width
330         copy.height = height
331         copy.alpha = alpha
332         copy.translation.set(translation.x, translation.y)
333         copy.contentTranslation.set(contentTranslation.x, contentTranslation.y)
334         for (entry in widgetStates) {
335             copy.widgetStates[entry.key] = entry.value.copy()
336         }
337         return copy
338     }
339 
initFromLayoutnull340     fun initFromLayout(transitionLayout: TransitionLayout) {
341         val childCount = transitionLayout.childCount
342         for (i in 0 until childCount) {
343             val child = transitionLayout.getChildAt(i)
344             val widgetState = widgetStates.getOrPut(child.id, {
345                 WidgetState(0.0f, 0.0f, 0, 0, 0, 0, 0.0f)
346             })
347             widgetState.initFromLayout(child)
348         }
349         width = transitionLayout.measuredWidth
350         height = transitionLayout.measuredHeight
351         translation.set(0.0f, 0.0f)
352         contentTranslation.set(0.0f, 0.0f)
353         alpha = 1.0f
354     }
355 }
356 
357 data class WidgetState(
358     var x: Float = 0.0f,
359     var y: Float = 0.0f,
360     var width: Int = 0,
361     var height: Int = 0,
362     var measureWidth: Int = 0,
363     var measureHeight: Int = 0,
364     var alpha: Float = 1.0f,
365     var scale: Float = 1.0f,
366     var gone: Boolean = false
367 ) {
initFromLayoutnull368     fun initFromLayout(view: View) {
369         gone = view.visibility == View.GONE
370         if (gone) {
371             val layoutParams = view.layoutParams as ConstraintLayout.LayoutParams
372             x = layoutParams.constraintWidget.left.toFloat()
373             y = layoutParams.constraintWidget.top.toFloat()
374             width = layoutParams.constraintWidget.width
375             height = layoutParams.constraintWidget.height
376             measureHeight = height
377             measureWidth = width
378             alpha = 0.0f
379             scale = 0.0f
380         } else {
381             x = view.left.toFloat()
382             y = view.top.toFloat()
383             width = view.width
384             height = view.height
385             measureWidth = width
386             measureHeight = height
387             gone = view.visibility == View.GONE
388             alpha = view.alpha
389             // No scale by default. Only during transitions!
390             scale = 1.0f
391         }
392     }
393 }
394