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