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.annotation.SuppressLint 20 import android.content.Context 21 import android.view.View 22 import android.view.ViewGroup 23 import android.widget.FrameLayout 24 import com.android.systemui.res.R 25 26 /** 27 * A special view that is designed to host a single "unique object". The unique object is 28 * dynamically added and removed from this view and may transition to other UniqueObjectHostViews 29 * available in the system. 30 * This is useful to share a singular instance of a view that can transition between completely 31 * independent parts of the view hierarchy. 32 * If the view currently hosts the unique object, it's measuring it normally, 33 * but if it's not attached, it will obtain the size by requesting a measure, as if it were 34 * always attached. 35 */ 36 class UniqueObjectHostView( 37 context: Context 38 ) : FrameLayout(context) { 39 lateinit var measurementManager: MeasurementManager 40 41 @SuppressLint("DrawAllocation") onMeasurenull42 override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) { 43 val paddingHorizontal = paddingStart + paddingEnd 44 val paddingVertical = paddingTop + paddingBottom 45 val width = MeasureSpec.getSize(widthMeasureSpec) - paddingHorizontal 46 val widthSpec = MeasureSpec.makeMeasureSpec(width, MeasureSpec.getMode(widthMeasureSpec)) 47 val height = MeasureSpec.getSize(heightMeasureSpec) - paddingVertical 48 val heightSpec = MeasureSpec.makeMeasureSpec(height, MeasureSpec.getMode(heightMeasureSpec)) 49 val measurementInput = MeasurementInput(widthSpec, heightSpec) 50 51 // Let's make sure the measurementManager knows about our size, to ensure that we have 52 // a value available. This might perform a measure internally if we don't have a cached 53 // size. 54 val (cachedWidth, cachedHeight) = measurementManager.onMeasure(measurementInput) 55 56 if (isCurrentHost()) { 57 super.onMeasure(widthMeasureSpec, heightMeasureSpec) 58 getChildAt(0)?.requiresRemeasuring = false 59 } 60 // The goal here is that the view will always have a consistent measuring, regardless 61 // if it's attached or not. 62 // The behavior is therefore very similar to the view being persistently attached to 63 // this host, which can prevent flickers. It also makes sure that we always know 64 // the size of the view during transitions even if it has never been attached here 65 // before. 66 // We previously still measured the size when the view was attached, but this doesn't 67 // work properly because we can set the measuredState while still attached to the 68 // old host, which will trigger an inconsistency in height 69 setMeasuredDimension(cachedWidth + paddingHorizontal, cachedHeight + paddingVertical) 70 } 71 addViewnull72 override fun addView(child: View?, index: Int, params: ViewGroup.LayoutParams?) { 73 if (child == null) { 74 throw IllegalArgumentException("child must be non-null") 75 } 76 if (child.measuredWidth == 0 || measuredWidth == 0 || child.requiresRemeasuring == true) { 77 super.addView(child, index, params) 78 return 79 } 80 // Suppress layouts when adding a view. The view should already be laid out with the 81 // right size when being attached to this view 82 invalidate() 83 addViewInLayout(child, index, params, true /* preventRequestLayout */) 84 // RTL properties are normally resolved in onMeasure(), which we are intentionally skipping 85 child.resolveRtlPropertiesIfNeeded() 86 val left = paddingLeft 87 val top = paddingTop 88 val paddingHorizontal = paddingStart + paddingEnd 89 val paddingVertical = paddingTop + paddingBottom 90 child.layout(left, 91 top, 92 left + measuredWidth - paddingHorizontal, 93 top + measuredHeight - paddingVertical) 94 } 95 isCurrentHostnull96 private fun isCurrentHost() = childCount != 0 97 98 interface MeasurementManager { 99 fun onMeasure(input: MeasurementInput): MeasurementOutput 100 } 101 } 102 103 /** 104 * Does this view require remeasuring currently outside of the regular measure flow? 105 */ 106 var View.requiresRemeasuring: Boolean 107 get() { 108 val required = getTag(R.id.requires_remeasuring) 109 return required?.equals(true) ?: false 110 } 111 set(value) { 112 setTag(R.id.requires_remeasuring, value) 113 } 114