1 /* 2 * Copyright (C) 2014 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.cts.verifier; 18 19 import android.annotation.TargetApi; 20 import android.os.Build; 21 import android.content.Context; 22 import android.content.res.TypedArray; 23 import android.graphics.Rect; 24 import android.graphics.drawable.Drawable; 25 import android.util.AttributeSet; 26 import android.view.Gravity; 27 import android.view.View; 28 import android.view.ViewGroup; 29 import android.view.WindowInsets; 30 import android.widget.FrameLayout; 31 32 /** 33 * BoxInsetLayout is a screen shape-aware FrameLayout that can box its children 34 * in the center square of a round screen by using the 35 * {@code ctsv_layout_box} attribute. The values for this attribute specify the 36 * child's edges to be boxed in: 37 * {@code left|top|right|bottom} or {@code all}. 38 * The {@code ctsv_layout_box} attribute is ignored on a device with a rectangular 39 * screen. 40 */ 41 @TargetApi(Build.VERSION_CODES.KITKAT_WATCH) 42 public class BoxInsetLayout extends FrameLayout { 43 44 private static float FACTOR = 0.146467f; //(1 - sqrt(2)/2)/2 45 private static final int DEFAULT_CHILD_GRAVITY = Gravity.TOP | Gravity.START; 46 47 private Rect mForegroundPadding; 48 private boolean mLastKnownRound; 49 private Rect mInsets; 50 BoxInsetLayout(Context context)51 public BoxInsetLayout(Context context) { 52 this(context, null); 53 } 54 BoxInsetLayout(Context context, AttributeSet attrs)55 public BoxInsetLayout(Context context, AttributeSet attrs) { 56 this(context, attrs, 0); 57 } 58 BoxInsetLayout(Context context, AttributeSet attrs, int defStyle)59 public BoxInsetLayout(Context context, AttributeSet attrs, int defStyle) { 60 super(context, attrs, defStyle); 61 // make sure we have foreground padding object 62 if (mForegroundPadding == null) { 63 mForegroundPadding = new Rect(); 64 } 65 if (mInsets == null) { 66 mInsets = new Rect(); 67 } 68 } 69 70 @Override onAttachedToWindow()71 protected void onAttachedToWindow() { 72 super.onAttachedToWindow(); 73 requestApplyInsets(); 74 } 75 76 @Override onApplyWindowInsets(WindowInsets insets)77 public WindowInsets onApplyWindowInsets(WindowInsets insets) { 78 insets = super.onApplyWindowInsets(insets); 79 final boolean round = insets.isRound(); 80 if (round != mLastKnownRound) { 81 mLastKnownRound = round; 82 requestLayout(); 83 } 84 mInsets.set( 85 insets.getSystemWindowInsetLeft(), 86 insets.getSystemWindowInsetTop(), 87 insets.getSystemWindowInsetRight(), 88 insets.getSystemWindowInsetBottom()); 89 return insets; 90 } 91 92 /** 93 * determine screen shape 94 * @return true if on a round screen 95 */ isRound()96 public boolean isRound() { 97 return mLastKnownRound; 98 } 99 100 /** 101 * @return the system window insets Rect 102 */ getInsets()103 public Rect getInsets() { 104 return mInsets; 105 } 106 107 @Override onMeasure(int widthMeasureSpec, int heightMeasureSpec)108 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 109 int count = getChildCount(); 110 // find max size 111 int maxWidth = 0; 112 int maxHeight = 0; 113 int childState = 0; 114 for (int i = 0; i < count; i++) { 115 final View child = getChildAt(i); 116 if (child.getVisibility() != GONE) { 117 LayoutParams lp = (BoxInsetLayout.LayoutParams) child.getLayoutParams(); 118 int marginLeft = 0; 119 int marginRight = 0; 120 int marginTop = 0; 121 int marginBottom = 0; 122 if (mLastKnownRound) { 123 // round screen, check boxed, don't use margins on boxed 124 if ((lp.boxedEdges & LayoutParams.BOX_LEFT) == 0) { 125 marginLeft = lp.leftMargin; 126 } 127 if ((lp.boxedEdges & LayoutParams.BOX_RIGHT) == 0) { 128 marginRight = lp.rightMargin; 129 } 130 if ((lp.boxedEdges & LayoutParams.BOX_TOP) == 0) { 131 marginTop = lp.topMargin; 132 } 133 if ((lp.boxedEdges & LayoutParams.BOX_BOTTOM) == 0) { 134 marginBottom = lp.bottomMargin; 135 } 136 } else { 137 // rectangular, ignore boxed, use margins 138 marginLeft = lp.leftMargin; 139 marginTop = lp.topMargin; 140 marginRight = lp.rightMargin; 141 marginBottom = lp.bottomMargin; 142 } 143 measureChildWithMargins(child, widthMeasureSpec, 0, heightMeasureSpec, 0); 144 maxWidth = Math.max(maxWidth, 145 child.getMeasuredWidth() + marginLeft + marginRight); 146 maxHeight = Math.max(maxHeight, 147 child.getMeasuredHeight() + marginTop + marginBottom); 148 childState = combineMeasuredStates(childState, child.getMeasuredState()); 149 } 150 } 151 // Account for padding too 152 maxWidth += getPaddingLeft() + mForegroundPadding.left 153 + getPaddingRight() + mForegroundPadding.right; 154 maxHeight += getPaddingTop() + mForegroundPadding.top 155 + getPaddingBottom() + mForegroundPadding.bottom; 156 157 // Check against our minimum height and width 158 maxHeight = Math.max(maxHeight, getSuggestedMinimumHeight()); 159 maxWidth = Math.max(maxWidth, getSuggestedMinimumWidth()); 160 161 // Check against our foreground's minimum height and width 162 final Drawable drawable = getForeground(); 163 if (drawable != null) { 164 maxHeight = Math.max(maxHeight, drawable.getMinimumHeight()); 165 maxWidth = Math.max(maxWidth, drawable.getMinimumWidth()); 166 } 167 168 setMeasuredDimension(resolveSizeAndState(maxWidth, widthMeasureSpec, childState), 169 resolveSizeAndState(maxHeight, heightMeasureSpec, 170 childState << MEASURED_HEIGHT_STATE_SHIFT)); 171 172 // determine boxed inset 173 int boxInset = (int) (FACTOR * Math.max(getMeasuredWidth(), getMeasuredHeight())); 174 // adjust the match parent children 175 for (int i = 0; i < count; i++) { 176 final View child = getChildAt(i); 177 178 final LayoutParams lp = (BoxInsetLayout.LayoutParams) child.getLayoutParams(); 179 int childWidthMeasureSpec; 180 int childHeightMeasureSpec; 181 int plwf = getPaddingLeft() + mForegroundPadding.left; 182 int prwf = getPaddingRight() + mForegroundPadding.right; 183 int ptwf = getPaddingTop() + mForegroundPadding.top; 184 int pbwf = getPaddingBottom() + mForegroundPadding.bottom; 185 186 // adjust width 187 int totalPadding = 0; 188 int totalMargin = 0; 189 // BoxInset is a padding. Ignore margin when we want to do BoxInset. 190 if (mLastKnownRound && ((lp.boxedEdges & LayoutParams.BOX_LEFT) != 0)) { 191 totalPadding += boxInset; 192 } else { 193 totalMargin += plwf + lp.leftMargin; 194 } 195 if (mLastKnownRound && ((lp.boxedEdges & LayoutParams.BOX_RIGHT) != 0)) { 196 totalPadding += boxInset; 197 } else { 198 totalMargin += prwf + lp.rightMargin; 199 } 200 if (lp.width == LayoutParams.MATCH_PARENT) { 201 // Only subtract margin from the actual width, leave the padding in. 202 childWidthMeasureSpec = MeasureSpec.makeMeasureSpec( 203 getMeasuredWidth() - totalMargin, MeasureSpec.EXACTLY); 204 } else { 205 childWidthMeasureSpec = getChildMeasureSpec(widthMeasureSpec, 206 totalPadding + totalMargin, lp.width); 207 } 208 209 // adjust height 210 totalPadding = 0; 211 totalMargin = 0; 212 if (mLastKnownRound && ((lp.boxedEdges & LayoutParams.BOX_TOP) != 0)) { 213 totalPadding += boxInset; 214 } else { 215 totalMargin += ptwf + lp.topMargin; 216 } 217 if (mLastKnownRound && ((lp.boxedEdges & LayoutParams.BOX_BOTTOM) != 0)) { 218 totalPadding += boxInset; 219 } else { 220 totalMargin += pbwf + lp.bottomMargin; 221 } 222 223 if (lp.height == LayoutParams.MATCH_PARENT) { 224 childHeightMeasureSpec = MeasureSpec.makeMeasureSpec( 225 getMeasuredHeight() - totalMargin, MeasureSpec.EXACTLY); 226 } else { 227 childHeightMeasureSpec = getChildMeasureSpec(heightMeasureSpec, 228 totalPadding + totalMargin, lp.height); 229 } 230 231 child.measure(childWidthMeasureSpec, childHeightMeasureSpec); 232 } 233 } 234 235 236 @Override onLayout(boolean changed, int left, int top, int right, int bottom)237 protected void onLayout(boolean changed, int left, int top, int right, int bottom) { 238 layoutBoxChildren(left, top, right, bottom, false /* no force left gravity */); 239 } 240 layoutBoxChildren(int left, int top, int right, int bottom, boolean forceLeftGravity)241 private void layoutBoxChildren(int left, int top, int right, int bottom, 242 boolean forceLeftGravity) { 243 final int count = getChildCount(); 244 int boxInset = (int)(FACTOR * Math.max(right - left, bottom - top)); 245 246 final int parentLeft = getPaddingLeft() + mForegroundPadding.left; 247 final int parentRight = right - left - getPaddingRight() - mForegroundPadding.right; 248 249 final int parentTop = getPaddingTop() + mForegroundPadding.top; 250 final int parentBottom = bottom - top - getPaddingBottom() - mForegroundPadding.bottom; 251 252 for (int i = 0; i < count; i++) { 253 final View child = getChildAt(i); 254 if (child.getVisibility() != GONE) { 255 final LayoutParams lp = (LayoutParams) child.getLayoutParams(); 256 257 final int width = child.getMeasuredWidth(); 258 final int height = child.getMeasuredHeight(); 259 260 int childLeft; 261 int childTop; 262 263 int gravity = lp.gravity; 264 if (gravity == -1) { 265 gravity = DEFAULT_CHILD_GRAVITY; 266 } 267 268 final int layoutDirection = getLayoutDirection(); 269 final int absoluteGravity = Gravity.getAbsoluteGravity(gravity, layoutDirection); 270 final int verticalGravity = gravity & Gravity.VERTICAL_GRAVITY_MASK; 271 272 // These values are replaced with boxInset below as necessary. 273 int paddingLeft = child.getPaddingLeft(); 274 int paddingRight = child.getPaddingRight(); 275 int paddingTop = child.getPaddingTop(); 276 int paddingBottom = child.getPaddingBottom(); 277 278 // If the child's width is match_parent, we ignore gravity and set boxInset padding 279 // on both sides, with a left position of parentLeft + the child's left margin. 280 if (lp.width == LayoutParams.MATCH_PARENT) { 281 if (mLastKnownRound && ((lp.boxedEdges & LayoutParams.BOX_LEFT) != 0)) { 282 paddingLeft = boxInset; 283 } 284 if (mLastKnownRound && ((lp.boxedEdges & LayoutParams.BOX_RIGHT) != 0)) { 285 paddingRight = boxInset; 286 } 287 childLeft = parentLeft + lp.leftMargin; 288 } else { 289 switch (absoluteGravity & Gravity.HORIZONTAL_GRAVITY_MASK) { 290 case Gravity.CENTER_HORIZONTAL: 291 childLeft = parentLeft + (parentRight - parentLeft - width) / 2 + 292 lp.leftMargin - lp.rightMargin; 293 break; 294 case Gravity.RIGHT: 295 if (!forceLeftGravity) { 296 if (mLastKnownRound 297 && ((lp.boxedEdges & LayoutParams.BOX_RIGHT) != 0)) { 298 paddingRight = boxInset; 299 childLeft = right - left - width; 300 } else { 301 childLeft = parentRight - width - lp.rightMargin; 302 } 303 break; 304 } 305 case Gravity.LEFT: 306 default: 307 if (mLastKnownRound && ((lp.boxedEdges & LayoutParams.BOX_LEFT) != 0)) { 308 paddingLeft = boxInset; 309 childLeft = 0; 310 } else { 311 childLeft = parentLeft + lp.leftMargin; 312 } 313 } 314 } 315 316 // If the child's height is match_parent, we ignore gravity and set boxInset padding 317 // on both top and bottom, with a top position of parentTop + the child's top 318 // margin. 319 if (lp.height == LayoutParams.MATCH_PARENT) { 320 if (mLastKnownRound && ((lp.boxedEdges & LayoutParams.BOX_TOP) != 0)) { 321 paddingTop = boxInset; 322 } 323 if (mLastKnownRound && ((lp.boxedEdges & LayoutParams.BOX_BOTTOM) != 0)) { 324 paddingBottom = boxInset; 325 } 326 childTop = parentTop + lp.topMargin; 327 } else { 328 switch (verticalGravity) { 329 case Gravity.TOP: 330 if (mLastKnownRound && ((lp.boxedEdges & LayoutParams.BOX_TOP) != 0)) { 331 paddingTop = boxInset; 332 childTop = 0; 333 } else { 334 childTop = parentTop + lp.topMargin; 335 } 336 break; 337 case Gravity.CENTER_VERTICAL: 338 childTop = parentTop + (parentBottom - parentTop - height) / 2 + 339 lp.topMargin - lp.bottomMargin; 340 break; 341 case Gravity.BOTTOM: 342 if (mLastKnownRound && ((lp.boxedEdges & LayoutParams.BOX_BOTTOM) != 0)) { 343 paddingBottom = boxInset; 344 childTop = bottom - top - height; 345 } else { 346 childTop = parentBottom - height - lp.bottomMargin; 347 } 348 break; 349 default: 350 childTop = parentTop + lp.topMargin; 351 } 352 } 353 354 child.setPadding(paddingLeft, paddingTop, paddingRight, paddingBottom); 355 child.layout(childLeft, childTop, childLeft + width, childTop + height); 356 } 357 } 358 } 359 setForeground(Drawable drawable)360 public void setForeground(Drawable drawable) { 361 super.setForeground(drawable); 362 if (mForegroundPadding == null) { 363 mForegroundPadding = new Rect(); 364 } 365 drawable.getPadding(mForegroundPadding); 366 } 367 368 @Override checkLayoutParams(ViewGroup.LayoutParams p)369 protected boolean checkLayoutParams(ViewGroup.LayoutParams p) { 370 return p instanceof LayoutParams; 371 } 372 373 @Override generateLayoutParams(ViewGroup.LayoutParams p)374 protected ViewGroup.LayoutParams generateLayoutParams(ViewGroup.LayoutParams p) { 375 return new LayoutParams(p); 376 } 377 378 @Override generateLayoutParams(AttributeSet attrs)379 public LayoutParams generateLayoutParams(AttributeSet attrs) { 380 return new BoxInsetLayout.LayoutParams(getContext(), attrs); 381 } 382 383 /** 384 * adds {@code ctsv_layout_box} attribute to layout parameters 385 */ 386 public static class LayoutParams extends FrameLayout.LayoutParams { 387 388 public static final int BOX_NONE = 0x0; 389 public static final int BOX_LEFT = 0x01; 390 public static final int BOX_TOP = 0x02; 391 public static final int BOX_RIGHT = 0x04; 392 public static final int BOX_BOTTOM = 0x08; 393 public static final int BOX_ALL = 0x0F; 394 395 public int boxedEdges = BOX_NONE; 396 LayoutParams(Context context, AttributeSet attrs)397 public LayoutParams(Context context, AttributeSet attrs) { 398 super(context, attrs); 399 TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.BoxInsetLayout_Layout, 0, 0); 400 boxedEdges = a.getInt(R.styleable.BoxInsetLayout_Layout_ctsv_layout_box, BOX_NONE); 401 a.recycle(); 402 } 403 LayoutParams(int width, int height)404 public LayoutParams(int width, int height) { 405 super(width, height); 406 } 407 LayoutParams(int width, int height, int gravity)408 public LayoutParams(int width, int height, int gravity) { 409 super(width, height, gravity); 410 } 411 LayoutParams(int width, int height, int gravity, int boxed)412 public LayoutParams(int width, int height, int gravity, int boxed) { 413 super(width, height, gravity); 414 boxedEdges = boxed; 415 } 416 LayoutParams(ViewGroup.LayoutParams source)417 public LayoutParams(ViewGroup.LayoutParams source) { 418 super(source); 419 } 420 LayoutParams(ViewGroup.MarginLayoutParams source)421 public LayoutParams(ViewGroup.MarginLayoutParams source) { 422 super(source); 423 } 424 LayoutParams(FrameLayout.LayoutParams source)425 public LayoutParams(FrameLayout.LayoutParams source) { 426 super(source); 427 } 428 LayoutParams(LayoutParams source)429 public LayoutParams(LayoutParams source) { 430 super(source); 431 this.boxedEdges = source.boxedEdges; 432 this.gravity = source.gravity; 433 } 434 435 } 436 } 437