blob: e5b126d7ff7f26ae8ca834ac33aed766c4423b65 [file] [log] [blame]
Selim Cinek2de5ebb2020-05-20 15:39:03 -07001/*
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
17package com.android.systemui.util.animation
18
19import android.content.Context
Lucas Dupin58158512020-06-16 19:45:02 -070020import android.graphics.Canvas
Lucas Dupin84f5a0e2020-06-08 19:55:33 -070021import android.graphics.PointF
Selim Cinek2de5ebb2020-05-20 15:39:03 -070022import android.graphics.Rect
23import android.util.AttributeSet
24import android.view.View
25import android.view.ViewTreeObserver
26import androidx.constraintlayout.widget.ConstraintLayout
27import androidx.constraintlayout.widget.ConstraintSet
28import com.android.systemui.statusbar.CrossFadeHelper
29
30/**
31 * A view that handles displaying of children and transitions of them in an optimized way,
32 * minimizing the number of measure passes, while allowing for maximum flexibility
33 * and interruptibility.
34 */
35class TransitionLayout @JvmOverloads constructor(
36 context: Context,
37 attrs: AttributeSet? = null,
38 defStyleAttr: Int = 0
39) : ConstraintLayout(context, attrs, defStyleAttr) {
40
Lucas Dupin58158512020-06-16 19:45:02 -070041 private val boundsRect = Rect()
Selim Cinek2de5ebb2020-05-20 15:39:03 -070042 private val originalGoneChildrenSet: MutableSet<Int> = mutableSetOf()
Selim Cinekeb70e132020-06-12 19:04:42 -070043 private val originalViewAlphas: MutableMap<Int, Float> = mutableMapOf()
Selim Cinek2de5ebb2020-05-20 15:39:03 -070044 private var measureAsConstraint: Boolean = false
45 private var currentState: TransitionViewState = TransitionViewState()
46 private var updateScheduled = false
47
48 /**
49 * The measured state of this view which is the one we will lay ourselves out with. This
50 * may differ from the currentState if there is an external animation or transition running.
51 * This state will not be used to measure the widgets, where the current state is preferred.
52 */
53 var measureState: TransitionViewState = TransitionViewState()
54 private val preDrawApplicator = object : ViewTreeObserver.OnPreDrawListener {
55 override fun onPreDraw(): Boolean {
56 updateScheduled = false
57 viewTreeObserver.removeOnPreDrawListener(this)
58 applyCurrentState()
59 return true
60 }
61 }
62
63 override fun onFinishInflate() {
64 super.onFinishInflate()
65 val childCount = childCount
66 for (i in 0 until childCount) {
67 val child = getChildAt(i)
68 if (child.id == View.NO_ID) {
69 child.id = i
70 }
71 if (child.visibility == GONE) {
72 originalGoneChildrenSet.add(child.id)
73 }
Selim Cinekeb70e132020-06-12 19:04:42 -070074 originalViewAlphas[child.id] = child.alpha
Selim Cinek2de5ebb2020-05-20 15:39:03 -070075 }
76 }
77
78 /**
79 * Apply the current state to the view and its widgets
80 */
81 private fun applyCurrentState() {
82 val childCount = childCount
83 for (i in 0 until childCount) {
84 val child = getChildAt(i)
85 val widgetState = currentState.widgetStates.get(child.id) ?: continue
86 if (child.measuredWidth != widgetState.measureWidth ||
87 child.measuredHeight != widgetState.measureHeight) {
88 val measureWidthSpec = MeasureSpec.makeMeasureSpec(widgetState.measureWidth,
89 MeasureSpec.EXACTLY)
90 val measureHeightSpec = MeasureSpec.makeMeasureSpec(widgetState.measureHeight,
91 MeasureSpec.EXACTLY)
92 child.measure(measureWidthSpec, measureHeightSpec)
93 child.layout(0, 0, child.measuredWidth, child.measuredHeight)
94 }
95 val left = widgetState.x.toInt()
96 val top = widgetState.y.toInt()
97 child.setLeftTopRightBottom(left, top, left + widgetState.width,
98 top + widgetState.height)
99 child.scaleX = widgetState.scale
100 child.scaleY = widgetState.scale
101 val clipBounds = child.clipBounds ?: Rect()
102 clipBounds.set(0, 0, widgetState.width, widgetState.height)
103 child.clipBounds = clipBounds
104 CrossFadeHelper.fadeIn(child, widgetState.alpha)
105 child.visibility = if (widgetState.gone || widgetState.alpha == 0.0f) {
106 View.INVISIBLE
107 } else {
108 View.VISIBLE
109 }
110 }
111 updateBounds()
112 }
113
114 private fun applyCurrentStateOnPredraw() {
115 if (!updateScheduled) {
116 updateScheduled = true
117 viewTreeObserver.addOnPreDrawListener(preDrawApplicator)
118 }
119 }
120
121 override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
122 if (measureAsConstraint) {
123 super.onMeasure(widthMeasureSpec, heightMeasureSpec)
124 } else {
125 for (i in 0 until childCount) {
126 val child = getChildAt(i)
127 val widgetState = currentState.widgetStates.get(child.id) ?: continue
128 val measureWidthSpec = MeasureSpec.makeMeasureSpec(widgetState.measureWidth,
129 MeasureSpec.EXACTLY)
130 val measureHeightSpec = MeasureSpec.makeMeasureSpec(widgetState.measureHeight,
131 MeasureSpec.EXACTLY)
132 child.measure(measureWidthSpec, measureHeightSpec)
133 }
134 setMeasuredDimension(measureState.width, measureState.height)
135 }
136 }
137
138 override fun onLayout(changed: Boolean, l: Int, t: Int, r: Int, b: Int) {
139 if (measureAsConstraint) {
140 super.onLayout(changed, left, top, right, bottom)
141 } else {
142 val childCount = childCount
143 for (i in 0 until childCount) {
144 val child = getChildAt(i)
145 child.layout(0, 0, child.measuredWidth, child.measuredHeight)
146 }
147 // Reapply the bounds to update the background
148 applyCurrentState()
149 }
150 }
151
Lucas Dupin58158512020-06-16 19:45:02 -0700152 override fun dispatchDraw(canvas: Canvas?) {
153 val clip = !boundsRect.isEmpty
154 if (clip) {
155 canvas?.save()
156 canvas?.clipRect(boundsRect)
157 }
158 super.dispatchDraw(canvas)
159 if (clip) {
160 canvas?.restore()
161 }
162 }
163
Selim Cinek2de5ebb2020-05-20 15:39:03 -0700164 private fun updateBounds() {
165 val layoutLeft = left
166 val layoutTop = top
167 setLeftTopRightBottom(layoutLeft, layoutTop, layoutLeft + currentState.width,
168 layoutTop + currentState.height)
Lucas Dupin84f5a0e2020-06-08 19:55:33 -0700169 translationX = currentState.translation.x
170 translationY = currentState.translation.y
Lucas Dupin58158512020-06-16 19:45:02 -0700171 boundsRect.set(0, 0, (width + translationX).toInt(), (height + translationY).toInt())
Selim Cinek2de5ebb2020-05-20 15:39:03 -0700172 }
173
174 /**
175 * Calculates a view state for a given ConstraintSet and measurement, saving all positions
176 * of all widgets.
177 *
178 * @param input the measurement input this should be done with
179 * @param constraintSet the constraint set to apply
180 * @param resusableState the result that we can reuse to minimize memory impact
181 */
182 fun calculateViewState(
183 input: MeasurementInput,
184 constraintSet: ConstraintSet,
185 existing: TransitionViewState? = null
186 ): TransitionViewState {
187
188 val result = existing ?: TransitionViewState()
189 // Reset gone children to the original state
190 applySetToFullLayout(constraintSet)
191 val previousHeight = measuredHeight
192 val previousWidth = measuredWidth
193
194 // Let's measure outselves as a ConstraintLayout
195 measureAsConstraint = true
196 measure(input.widthMeasureSpec, input.heightMeasureSpec)
197 val layoutLeft = left
198 val layoutTop = top
199 layout(layoutLeft, layoutTop, layoutLeft + measuredWidth, layoutTop + measuredHeight)
200 measureAsConstraint = false
201 result.initFromLayout(this)
202 ensureViewsNotGone()
203
204 // Let's reset our layout to have the right size again
205 setMeasuredDimension(previousWidth, previousHeight)
206 applyCurrentStateOnPredraw()
207 return result
208 }
209
210 private fun applySetToFullLayout(constraintSet: ConstraintSet) {
211 // Let's reset our views to the initial gone state of the layout, since the constraintset
212 // might only be a subset of the views. Otherwise the gone state would be calculated
213 // wrongly later if we made this invisible in the layout (during apply we make sure they
214 // are invisible instead
215 val childCount = childCount
216 for (i in 0 until childCount) {
217 val child = getChildAt(i)
218 if (originalGoneChildrenSet.contains(child.id)) {
219 child.visibility = View.GONE
220 }
Selim Cinekeb70e132020-06-12 19:04:42 -0700221 // Reset the alphas, to only have the alphas present from the constraintset
222 child.alpha = originalViewAlphas[child.id] ?: 1.0f
Selim Cinek2de5ebb2020-05-20 15:39:03 -0700223 }
224 // Let's now apply the constraintSet to get the full state
225 constraintSet.applyTo(this)
226 }
227
228 /**
229 * Ensures that our views are never gone but invisible instead, this allows us to animate them
230 * without remeasuring.
231 */
232 private fun ensureViewsNotGone() {
233 val childCount = childCount
234 for (i in 0 until childCount) {
235 val child = getChildAt(i)
236 val widgetState = currentState.widgetStates.get(child.id)
237 child.visibility = if (widgetState?.gone != false) View.INVISIBLE else View.VISIBLE
238 }
239 }
240
241 /**
242 * Set the state that should be applied to this View
243 *
244 */
245 fun setState(state: TransitionViewState) {
246 currentState = state
247 applyCurrentState()
248 }
249}
250
251class TransitionViewState {
252 var widgetStates: MutableMap<Int, WidgetState> = mutableMapOf()
253 var width: Int = 0
254 var height: Int = 0
Lucas Dupin84f5a0e2020-06-08 19:55:33 -0700255 val translation = PointF()
Selim Cinek2de5ebb2020-05-20 15:39:03 -0700256 fun copy(reusedState: TransitionViewState? = null): TransitionViewState {
257 // we need a deep copy of this, so we can't use a data class
258 val copy = reusedState ?: TransitionViewState()
259 copy.width = width
260 copy.height = height
Lucas Dupin84f5a0e2020-06-08 19:55:33 -0700261 copy.translation.set(translation.x, translation.y)
Selim Cinek2de5ebb2020-05-20 15:39:03 -0700262 for (entry in widgetStates) {
263 copy.widgetStates[entry.key] = entry.value.copy()
264 }
265 return copy
266 }
267
268 fun initFromLayout(transitionLayout: TransitionLayout) {
269 val childCount = transitionLayout.childCount
270 for (i in 0 until childCount) {
271 val child = transitionLayout.getChildAt(i)
272 val widgetState = widgetStates.getOrPut(child.id, {
273 WidgetState(0.0f, 0.0f, 0, 0, 0, 0, 0.0f)
274 })
275 widgetState.initFromLayout(child)
276 }
277 width = transitionLayout.measuredWidth
278 height = transitionLayout.measuredHeight
Lucas Dupin84f5a0e2020-06-08 19:55:33 -0700279 translation.set(0.0f, 0.0f)
Selim Cinek2de5ebb2020-05-20 15:39:03 -0700280 }
281}
282
283data class WidgetState(
284 var x: Float = 0.0f,
285 var y: Float = 0.0f,
286 var width: Int = 0,
287 var height: Int = 0,
288 var measureWidth: Int = 0,
289 var measureHeight: Int = 0,
290 var alpha: Float = 1.0f,
291 var scale: Float = 1.0f,
292 var gone: Boolean = false
293) {
294 fun initFromLayout(view: View) {
295 gone = view.visibility == View.GONE
296 if (gone) {
297 val layoutParams = view.layoutParams as ConstraintLayout.LayoutParams
298 x = layoutParams.constraintWidget.left.toFloat()
299 y = layoutParams.constraintWidget.top.toFloat()
300 width = layoutParams.constraintWidget.width
301 height = layoutParams.constraintWidget.height
302 measureHeight = height
303 measureWidth = width
304 alpha = 0.0f
305 scale = 0.0f
306 } else {
307 x = view.left.toFloat()
308 y = view.top.toFloat()
309 width = view.width
310 height = view.height
311 measureWidth = width
312 measureHeight = height
313 gone = view.visibility == View.GONE
314 alpha = view.alpha
315 // No scale by default. Only during transitions!
316 scale = 1.0f
317 }
318 }
319}