Selim Cinek | 5480962 | 2020-04-30 19:04:44 -0700 | [diff] [blame] | 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 |
Selim Cinek | 2de5ebb | 2020-05-20 15:39:03 -0700 | [diff] [blame] | 22 | import android.view.ViewGroup |
Selim Cinek | 5480962 | 2020-04-30 19:04:44 -0700 | [diff] [blame] | 23 | import android.widget.FrameLayout |
Selim Cinek | 2de5ebb | 2020-05-20 15:39:03 -0700 | [diff] [blame] | 24 | import com.android.systemui.R |
Selim Cinek | 5480962 | 2020-04-30 19:04:44 -0700 | [diff] [blame] | 25 | |
| 26 | /** |
Selim Cinek | f418bb0 | 2020-05-04 17:16:58 -0700 | [diff] [blame] | 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. |
Selim Cinek | 5480962 | 2020-04-30 19:04:44 -0700 | [diff] [blame] | 35 | */ |
| 36 | class UniqueObjectHostView( |
| 37 | context: Context |
| 38 | ) : FrameLayout(context) { |
Selim Cinek | 2de5ebb | 2020-05-20 15:39:03 -0700 | [diff] [blame] | 39 | lateinit var measurementManager: MeasurementManager |
Selim Cinek | 5480962 | 2020-04-30 19:04:44 -0700 | [diff] [blame] | 40 | |
| 41 | @SuppressLint("DrawAllocation") |
| 42 | override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) { |
Selim Cinek | 5480962 | 2020-04-30 19:04:44 -0700 | [diff] [blame] | 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)) |
Selim Cinek | 2de5ebb | 2020-05-20 15:39:03 -0700 | [diff] [blame] | 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 | |
Selim Cinek | b28ec0a | 2020-05-01 15:07:42 -0700 | [diff] [blame] | 56 | if (!isCurrentHost()) { |
Selim Cinek | 2de5ebb | 2020-05-20 15:39:03 -0700 | [diff] [blame] | 57 | // We're not currently the host, let's use the dimension from our cache |
Selim Cinek | f418bb0 | 2020-05-04 17:16:58 -0700 | [diff] [blame] | 58 | // The goal here is that the view will always have a consistent measuring, regardless |
| 59 | // if it's attached or not. |
| 60 | // The behavior is therefore very similar to the view being persistently attached to |
| 61 | // this host, which can prevent flickers. It also makes sure that we always know |
| 62 | // the size of the view during transitions even if it has never been attached here |
| 63 | // before. |
Selim Cinek | 5480962 | 2020-04-30 19:04:44 -0700 | [diff] [blame] | 64 | setMeasuredDimension(cachedWidth + paddingHorizontal, cachedHeight + paddingVertical) |
| 65 | } else { |
Selim Cinek | b28ec0a | 2020-05-01 15:07:42 -0700 | [diff] [blame] | 66 | super.onMeasure(widthMeasureSpec, heightMeasureSpec) |
| 67 | // Let's update our cache |
Selim Cinek | 2de5ebb | 2020-05-20 15:39:03 -0700 | [diff] [blame] | 68 | getChildAt(0)?.requiresRemeasuring = false |
Selim Cinek | 5480962 | 2020-04-30 19:04:44 -0700 | [diff] [blame] | 69 | } |
| 70 | } |
| 71 | |
Selim Cinek | 2de5ebb | 2020-05-20 15:39:03 -0700 | [diff] [blame] | 72 | override fun addView(child: View?, index: Int, params: ViewGroup.LayoutParams?) { |
Jeff DeCew | afec78f | 2020-06-12 13:57:23 -0400 | [diff] [blame] | 73 | if (child == null) { |
| 74 | throw IllegalArgumentException("child must be non-null") |
| 75 | } |
| 76 | if (child.measuredWidth == 0 || measuredWidth == 0 || child.requiresRemeasuring == true) { |
Selim Cinek | 2de5ebb | 2020-05-20 15:39:03 -0700 | [diff] [blame] | 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 */) |
Jeff DeCew | afec78f | 2020-06-12 13:57:23 -0400 | [diff] [blame] | 84 | // RTL properties are normally resolved in onMeasure(), which we are intentionally skipping |
| 85 | child.resolveRtlPropertiesIfNeeded() |
Selim Cinek | 2de5ebb | 2020-05-20 15:39:03 -0700 | [diff] [blame] | 86 | val left = paddingLeft |
| 87 | val top = paddingTop |
| 88 | val paddingHorizontal = paddingStart + paddingEnd |
| 89 | val paddingVertical = paddingTop + paddingBottom |
Jeff DeCew | afec78f | 2020-06-12 13:57:23 -0400 | [diff] [blame] | 90 | child.layout(left, |
Selim Cinek | 2de5ebb | 2020-05-20 15:39:03 -0700 | [diff] [blame] | 91 | top, |
| 92 | left + measuredWidth - paddingHorizontal, |
| 93 | top + measuredHeight - paddingVertical) |
| 94 | } |
| 95 | |
Selim Cinek | 5480962 | 2020-04-30 19:04:44 -0700 | [diff] [blame] | 96 | private fun isCurrentHost() = childCount != 0 |
Selim Cinek | 5480962 | 2020-04-30 19:04:44 -0700 | [diff] [blame] | 97 | |
Selim Cinek | 2de5ebb | 2020-05-20 15:39:03 -0700 | [diff] [blame] | 98 | interface MeasurementManager { |
| 99 | fun onMeasure(input: MeasurementInput): MeasurementOutput |
Selim Cinek | 5480962 | 2020-04-30 19:04:44 -0700 | [diff] [blame] | 100 | } |
Selim Cinek | 5480962 | 2020-04-30 19:04:44 -0700 | [diff] [blame] | 101 | } |
| 102 | |
| 103 | /** |
Selim Cinek | 2de5ebb | 2020-05-20 15:39:03 -0700 | [diff] [blame] | 104 | * Does this view require remeasuring currently outside of the regular measure flow? |
Selim Cinek | 5480962 | 2020-04-30 19:04:44 -0700 | [diff] [blame] | 105 | */ |
Selim Cinek | 2de5ebb | 2020-05-20 15:39:03 -0700 | [diff] [blame] | 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 | } |