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