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