blob: 3c0a23aa2eca58925c76cd6be1fdc9d273fb7e7d [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?) {
Selim Cinek52302242020-06-22 19:31:45 -0700153 canvas?.save()
154 canvas?.clipRect(boundsRect)
Lucas Dupin58158512020-06-16 19:45:02 -0700155 super.dispatchDraw(canvas)
Selim Cinek52302242020-06-22 19:31:45 -0700156 canvas?.restore()
Lucas Dupin58158512020-06-16 19:45:02 -0700157 }
158
Selim Cinek2de5ebb2020-05-20 15:39:03 -0700159 private fun updateBounds() {
160 val layoutLeft = left
161 val layoutTop = top
162 setLeftTopRightBottom(layoutLeft, layoutTop, layoutLeft + currentState.width,
163 layoutTop + currentState.height)
Lucas Dupin84f5a0e2020-06-08 19:55:33 -0700164 translationX = currentState.translation.x
165 translationY = currentState.translation.y
Lucas Dupin58158512020-06-16 19:45:02 -0700166 boundsRect.set(0, 0, (width + translationX).toInt(), (height + translationY).toInt())
Selim Cinek2de5ebb2020-05-20 15:39:03 -0700167 }
168
169 /**
170 * Calculates a view state for a given ConstraintSet and measurement, saving all positions
171 * of all widgets.
172 *
173 * @param input the measurement input this should be done with
174 * @param constraintSet the constraint set to apply
175 * @param resusableState the result that we can reuse to minimize memory impact
176 */
177 fun calculateViewState(
178 input: MeasurementInput,
179 constraintSet: ConstraintSet,
180 existing: TransitionViewState? = null
181 ): TransitionViewState {
182
183 val result = existing ?: TransitionViewState()
184 // Reset gone children to the original state
185 applySetToFullLayout(constraintSet)
186 val previousHeight = measuredHeight
187 val previousWidth = measuredWidth
188
189 // Let's measure outselves as a ConstraintLayout
190 measureAsConstraint = true
191 measure(input.widthMeasureSpec, input.heightMeasureSpec)
192 val layoutLeft = left
193 val layoutTop = top
194 layout(layoutLeft, layoutTop, layoutLeft + measuredWidth, layoutTop + measuredHeight)
195 measureAsConstraint = false
196 result.initFromLayout(this)
197 ensureViewsNotGone()
198
199 // Let's reset our layout to have the right size again
200 setMeasuredDimension(previousWidth, previousHeight)
201 applyCurrentStateOnPredraw()
202 return result
203 }
204
205 private fun applySetToFullLayout(constraintSet: ConstraintSet) {
206 // Let's reset our views to the initial gone state of the layout, since the constraintset
207 // might only be a subset of the views. Otherwise the gone state would be calculated
208 // wrongly later if we made this invisible in the layout (during apply we make sure they
209 // are invisible instead
210 val childCount = childCount
211 for (i in 0 until childCount) {
212 val child = getChildAt(i)
213 if (originalGoneChildrenSet.contains(child.id)) {
214 child.visibility = View.GONE
215 }
Selim Cinekeb70e132020-06-12 19:04:42 -0700216 // Reset the alphas, to only have the alphas present from the constraintset
217 child.alpha = originalViewAlphas[child.id] ?: 1.0f
Selim Cinek2de5ebb2020-05-20 15:39:03 -0700218 }
219 // Let's now apply the constraintSet to get the full state
220 constraintSet.applyTo(this)
221 }
222
223 /**
224 * Ensures that our views are never gone but invisible instead, this allows us to animate them
225 * without remeasuring.
226 */
227 private fun ensureViewsNotGone() {
228 val childCount = childCount
229 for (i in 0 until childCount) {
230 val child = getChildAt(i)
231 val widgetState = currentState.widgetStates.get(child.id)
232 child.visibility = if (widgetState?.gone != false) View.INVISIBLE else View.VISIBLE
233 }
234 }
235
236 /**
237 * Set the state that should be applied to this View
238 *
239 */
240 fun setState(state: TransitionViewState) {
241 currentState = state
242 applyCurrentState()
243 }
244}
245
246class TransitionViewState {
247 var widgetStates: MutableMap<Int, WidgetState> = mutableMapOf()
248 var width: Int = 0
249 var height: Int = 0
Lucas Dupin84f5a0e2020-06-08 19:55:33 -0700250 val translation = PointF()
Selim Cinek2de5ebb2020-05-20 15:39:03 -0700251 fun copy(reusedState: TransitionViewState? = null): TransitionViewState {
252 // we need a deep copy of this, so we can't use a data class
253 val copy = reusedState ?: TransitionViewState()
254 copy.width = width
255 copy.height = height
Lucas Dupin84f5a0e2020-06-08 19:55:33 -0700256 copy.translation.set(translation.x, translation.y)
Selim Cinek2de5ebb2020-05-20 15:39:03 -0700257 for (entry in widgetStates) {
258 copy.widgetStates[entry.key] = entry.value.copy()
259 }
260 return copy
261 }
262
263 fun initFromLayout(transitionLayout: TransitionLayout) {
264 val childCount = transitionLayout.childCount
265 for (i in 0 until childCount) {
266 val child = transitionLayout.getChildAt(i)
267 val widgetState = widgetStates.getOrPut(child.id, {
268 WidgetState(0.0f, 0.0f, 0, 0, 0, 0, 0.0f)
269 })
270 widgetState.initFromLayout(child)
271 }
272 width = transitionLayout.measuredWidth
273 height = transitionLayout.measuredHeight
Lucas Dupin84f5a0e2020-06-08 19:55:33 -0700274 translation.set(0.0f, 0.0f)
Selim Cinek2de5ebb2020-05-20 15:39:03 -0700275 }
276}
277
278data class WidgetState(
279 var x: Float = 0.0f,
280 var y: Float = 0.0f,
281 var width: Int = 0,
282 var height: Int = 0,
283 var measureWidth: Int = 0,
284 var measureHeight: Int = 0,
285 var alpha: Float = 1.0f,
286 var scale: Float = 1.0f,
287 var gone: Boolean = false
288) {
289 fun initFromLayout(view: View) {
290 gone = view.visibility == View.GONE
291 if (gone) {
292 val layoutParams = view.layoutParams as ConstraintLayout.LayoutParams
293 x = layoutParams.constraintWidget.left.toFloat()
294 y = layoutParams.constraintWidget.top.toFloat()
295 width = layoutParams.constraintWidget.width
296 height = layoutParams.constraintWidget.height
297 measureHeight = height
298 measureWidth = width
299 alpha = 0.0f
300 scale = 0.0f
301 } else {
302 x = view.left.toFloat()
303 y = view.top.toFloat()
304 width = view.width
305 height = view.height
306 measureWidth = width
307 measureHeight = height
308 gone = view.visibility == View.GONE
309 alpha = view.alpha
310 // No scale by default. Only during transitions!
311 scale = 1.0f
312 }
313 }
314}