1 /* 2 * Copyright (C) 2016 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.google.android.setupdesign.view; 18 19 import static java.lang.Math.min; 20 21 import android.annotation.TargetApi; 22 import android.content.Context; 23 import android.content.res.TypedArray; 24 import android.graphics.Rect; 25 import android.os.Build; 26 import android.os.Build.VERSION_CODES; 27 import android.util.AttributeSet; 28 import android.util.DisplayMetrics; 29 import android.view.Display; 30 import android.view.ViewGroup; 31 import android.view.WindowInsets; 32 import android.view.WindowManager; 33 import android.view.WindowMetrics; 34 import android.widget.FrameLayout; 35 import androidx.annotation.VisibleForTesting; 36 import com.google.android.setupcompat.partnerconfig.PartnerConfig; 37 import com.google.android.setupcompat.partnerconfig.PartnerConfigHelper; 38 import com.google.android.setupcompat.util.BuildCompatUtils; 39 import com.google.android.setupdesign.R; 40 41 /** 42 * A FrameLayout subclass that has an "intrinsic size", which is the size it wants to be if that is 43 * within the constraints given by the parent. The intrinsic size can be set with the {@code 44 * android:width} and {@code android:height} attributes in XML. 45 * 46 * <p>Note that for the intrinsic size to be meaningful, {@code android:layout_width} and/or {@code 47 * android:layout_height} will need to be {@code wrap_content}. 48 */ 49 public class IntrinsicSizeFrameLayout extends FrameLayout { 50 51 private int intrinsicHeight = 0; 52 private int intrinsicWidth = 0; 53 private Object lastInsets; // Use generic Object type for compatibility 54 55 // Define here to avoid allocating resource during layout/draw operation. 56 private final Rect windowVisibleDisplayRect = new Rect(); 57 IntrinsicSizeFrameLayout(Context context)58 public IntrinsicSizeFrameLayout(Context context) { 59 super(context); 60 init(context, null, 0); 61 } 62 IntrinsicSizeFrameLayout(Context context, AttributeSet attrs)63 public IntrinsicSizeFrameLayout(Context context, AttributeSet attrs) { 64 super(context, attrs); 65 init(context, attrs, 0); 66 } 67 68 @TargetApi(VERSION_CODES.HONEYCOMB) IntrinsicSizeFrameLayout(Context context, AttributeSet attrs, int defStyleAttr)69 public IntrinsicSizeFrameLayout(Context context, AttributeSet attrs, int defStyleAttr) { 70 super(context, attrs, defStyleAttr); 71 init(context, attrs, defStyleAttr); 72 } 73 init(Context context, AttributeSet attrs, int defStyleAttr)74 private void init(Context context, AttributeSet attrs, int defStyleAttr) { 75 if (isInEditMode()) { 76 return; 77 } 78 79 final TypedArray a = 80 context.obtainStyledAttributes( 81 attrs, R.styleable.SudIntrinsicSizeFrameLayout, defStyleAttr, 0); 82 intrinsicHeight = 83 a.getDimensionPixelSize(R.styleable.SudIntrinsicSizeFrameLayout_android_height, 0); 84 intrinsicWidth = 85 a.getDimensionPixelSize(R.styleable.SudIntrinsicSizeFrameLayout_android_width, 0); 86 a.recycle(); 87 88 if (BuildCompatUtils.isAtLeastS()) { 89 if (PartnerConfigHelper.get(context) 90 .isPartnerConfigAvailable(PartnerConfig.CONFIG_CARD_VIEW_INTRINSIC_HEIGHT)) { 91 intrinsicHeight = 92 (int) 93 PartnerConfigHelper.get(context) 94 .getDimension(context, PartnerConfig.CONFIG_CARD_VIEW_INTRINSIC_HEIGHT); 95 } 96 if (PartnerConfigHelper.get(context) 97 .isPartnerConfigAvailable(PartnerConfig.CONFIG_CARD_VIEW_INTRINSIC_WIDTH)) { 98 intrinsicWidth = 99 (int) 100 PartnerConfigHelper.get(context) 101 .getDimension(context, PartnerConfig.CONFIG_CARD_VIEW_INTRINSIC_WIDTH); 102 } 103 } 104 } 105 106 @Override setLayoutParams(ViewGroup.LayoutParams params)107 public void setLayoutParams(ViewGroup.LayoutParams params) { 108 if (BuildCompatUtils.isAtLeastS()) { 109 // When both intrinsic height and width are 0, the card view style would be removed from 110 // foldable/tablet layout. It must set the layout width and height to MATCH_PARENT and then it 111 // can ignore the IntrinsicSizeFrameLayout from the foldable/tablet layout. 112 if (intrinsicHeight == 0 && intrinsicWidth == 0) { 113 params.width = ViewGroup.LayoutParams.MATCH_PARENT; 114 params.height = ViewGroup.LayoutParams.MATCH_PARENT; 115 } 116 } 117 super.setLayoutParams(params); 118 } 119 120 @Override onMeasure(int widthMeasureSpec, int heightMeasureSpec)121 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 122 int measureWidth; 123 124 // The the content may be truncated if the layout show in multi-window mode or two pane mode, 125 // because the given width is fixed size which based on the display to compute. So the width 126 // the content may be truncated. Make the layout width align window while window width smaller 127 // than display size. 128 if (isWindowSizeSmallerThanDisplaySize()) { 129 getWindowVisibleDisplayFrame(windowVisibleDisplayRect); 130 131 measureWidth = 132 MeasureSpec.makeMeasureSpec(windowVisibleDisplayRect.width(), MeasureSpec.EXACTLY); 133 } else { 134 measureWidth = getIntrinsicMeasureSpec(widthMeasureSpec, intrinsicWidth); 135 } 136 137 super.onMeasure(measureWidth, getIntrinsicMeasureSpec(heightMeasureSpec, intrinsicHeight)); 138 } 139 140 @VisibleForTesting isWindowSizeSmallerThanDisplaySize()141 boolean isWindowSizeSmallerThanDisplaySize() { 142 boolean result = false; 143 /// NOMUTANTS--reason(b/221151816): Support setCurrentWindowMetrics from ShadowWindowManagerImpl 144 // to verify this case 145 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { 146 WindowManager windowManager = getContext().getSystemService(WindowManager.class); 147 WindowMetrics windowMetrics = windowManager.getCurrentWindowMetrics(); 148 windowVisibleDisplayRect.set(windowMetrics.getBounds()); 149 150 Display display = getDisplay(); 151 if (display != null) { 152 DisplayMetrics displayMetrics = new DisplayMetrics(); 153 display.getRealMetrics(displayMetrics); 154 155 if (windowVisibleDisplayRect.width() > 0 156 && windowVisibleDisplayRect.width() < displayMetrics.widthPixels) { 157 result = true; 158 } 159 } 160 } 161 162 return result; 163 } 164 getIntrinsicMeasureSpec(int measureSpec, int intrinsicSize)165 private int getIntrinsicMeasureSpec(int measureSpec, int intrinsicSize) { 166 if (intrinsicSize <= 0) { 167 // Intrinsic size is not set, just return the original spec 168 return measureSpec; 169 } 170 final int mode = MeasureSpec.getMode(measureSpec); 171 final int size = MeasureSpec.getSize(measureSpec); 172 if (mode == MeasureSpec.UNSPECIFIED) { 173 // Parent did not give any constraint, so we'll be the intrinsic size 174 return MeasureSpec.makeMeasureSpec(intrinsicHeight, MeasureSpec.EXACTLY); 175 } else if (mode == MeasureSpec.AT_MOST) { 176 // If intrinsic size is within parents constraint, take the intrinsic size. 177 // Otherwise take the parents size because that's closest to the intrinsic size. 178 return MeasureSpec.makeMeasureSpec(min(size, intrinsicHeight), MeasureSpec.EXACTLY); 179 } 180 // Parent specified EXACTLY, or in all other cases, just return the original spec 181 return measureSpec; 182 } 183 184 @Override onAttachedToWindow()185 protected void onAttachedToWindow() { 186 super.onAttachedToWindow(); 187 if (Build.VERSION.SDK_INT >= VERSION_CODES.LOLLIPOP) { 188 if (lastInsets == null) { 189 requestApplyInsets(); 190 } 191 } 192 } 193 194 @Override onApplyWindowInsets(WindowInsets insets)195 public WindowInsets onApplyWindowInsets(WindowInsets insets) { 196 lastInsets = insets; 197 return super.onApplyWindowInsets(insets); 198 } 199 } 200