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 */ 16package android.support.wear.widget; 17 18import android.content.Context; 19import android.content.res.Resources; 20import android.content.res.TypedArray; 21import android.graphics.Rect; 22import android.graphics.drawable.Drawable; 23import android.os.Build; 24import android.support.annotation.IntDef; 25import android.support.annotation.NonNull; 26import android.support.annotation.Nullable; 27import android.support.annotation.RestrictTo; 28import android.support.annotation.StyleRes; 29import android.support.annotation.UiThread; 30import android.support.wear.R; 31import android.util.AttributeSet; 32import android.view.Gravity; 33import android.view.View; 34import android.view.ViewGroup; 35import android.view.WindowInsets; 36import android.widget.FrameLayout; 37 38import java.lang.annotation.Retention; 39import java.lang.annotation.RetentionPolicy; 40 41/** 42 * BoxInsetLayout is a screen shape-aware ViewGroup that can box its children in the center 43 * square of a round screen by using the {@code boxedEdges} attribute. The values for this attribute 44 * specify the child's edges to be boxed in: {@code left|top|right|bottom} or {@code all}. The 45 * {@code boxedEdges} attribute is ignored on a device with a rectangular screen. 46 */ 47@UiThread 48public class BoxInsetLayout extends ViewGroup { 49 50 private static final float FACTOR = 0.146467f; //(1 - sqrt(2)/2)/2 51 private static final int DEFAULT_CHILD_GRAVITY = Gravity.TOP | Gravity.START; 52 53 private final int mScreenHeight; 54 private final int mScreenWidth; 55 56 private boolean mIsRound; 57 private Rect mForegroundPadding; 58 private Rect mInsets; 59 private Drawable mForegroundDrawable; 60 61 /** 62 * Simple constructor to use when creating a view from code. 63 * 64 * @param context The {@link Context} the view is running in, through which it can access 65 * the current theme, resources, etc. 66 */ 67 public BoxInsetLayout(@NonNull Context context) { 68 this(context, null); 69 } 70 71 /** 72 * Constructor that is called when inflating a view from XML. This is called when a view is 73 * being constructed from an XML file, supplying attributes that were specified in the XML 74 * file. This version uses a default style of 0, so the only attribute values applied are those 75 * in the Context's Theme and the given AttributeSet. 76 * <p> 77 * <p> 78 * The method onFinishInflate() will be called after all children have been added. 79 * 80 * @param context The {@link Context} the view is running in, through which it can access 81 * the current theme, resources, etc. 82 * @param attrs The attributes of the XML tag that is inflating the view. 83 */ 84 public BoxInsetLayout(@NonNull Context context, @Nullable AttributeSet attrs) { 85 this(context, attrs, 0); 86 } 87 88 /** 89 * Perform inflation from XML and apply a class-specific base style from a theme attribute. 90 * This constructor allows subclasses to use their own base style when they are inflating. 91 * 92 * @param context The {@link Context} the view is running in, through which it can 93 * access the current theme, resources, etc. 94 * @param attrs The attributes of the XML tag that is inflating the view. 95 * @param defStyle An attribute in the current theme that contains a reference to a style 96 * resource that supplies default values for the view. Can be 0 to not look for 97 * defaults. 98 */ 99 public BoxInsetLayout(@NonNull Context context, @Nullable AttributeSet attrs, @StyleRes int 100 defStyle) { 101 super(context, attrs, defStyle); 102 // make sure we have a foreground padding object 103 if (mForegroundPadding == null) { 104 mForegroundPadding = new Rect(); 105 } 106 if (mInsets == null) { 107 mInsets = new Rect(); 108 } 109 mScreenHeight = Resources.getSystem().getDisplayMetrics().heightPixels; 110 mScreenWidth = Resources.getSystem().getDisplayMetrics().widthPixels; 111 } 112 113 @Override 114 public WindowInsets onApplyWindowInsets(WindowInsets insets) { 115 insets = super.onApplyWindowInsets(insets); 116 if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) { 117 final boolean round = insets.isRound(); 118 if (round != mIsRound) { 119 mIsRound = round; 120 requestLayout(); 121 } 122 mInsets.set(insets.getSystemWindowInsetLeft(), insets.getSystemWindowInsetTop(), 123 insets.getSystemWindowInsetRight(), insets.getSystemWindowInsetBottom()); 124 } 125 return insets; 126 } 127 128 @Override 129 public void setForeground(Drawable drawable) { 130 super.setForeground(drawable); 131 mForegroundDrawable = drawable; 132 if (mForegroundPadding == null) { 133 mForegroundPadding = new Rect(); 134 } 135 if (mForegroundDrawable != null) { 136 drawable.getPadding(mForegroundPadding); 137 } 138 } 139 140 @Override 141 public LayoutParams generateLayoutParams(AttributeSet attrs) { 142 return new BoxInsetLayout.LayoutParams(getContext(), attrs); 143 } 144 145 @Override 146 protected void onAttachedToWindow() { 147 super.onAttachedToWindow(); 148 if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) { 149 requestApplyInsets(); 150 } else { 151 mIsRound = getResources().getConfiguration().isScreenRound(); 152 WindowInsets insets = getRootWindowInsets(); 153 mInsets.set(insets.getSystemWindowInsetLeft(), insets.getSystemWindowInsetTop(), 154 insets.getSystemWindowInsetRight(), insets.getSystemWindowInsetBottom()); 155 } 156 } 157 158 @Override 159 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 160 int count = getChildCount(); 161 // find max size 162 int maxWidth = 0; 163 int maxHeight = 0; 164 int childState = 0; 165 for (int i = 0; i < count; i++) { 166 final View child = getChildAt(i); 167 if (child.getVisibility() != GONE) { 168 LayoutParams lp = (BoxInsetLayout.LayoutParams) child.getLayoutParams(); 169 int marginLeft = 0; 170 int marginRight = 0; 171 int marginTop = 0; 172 int marginBottom = 0; 173 if (mIsRound) { 174 // round screen, check boxed, don't use margins on boxed 175 if ((lp.boxedEdges & LayoutParams.BOX_LEFT) == 0) { 176 marginLeft = lp.leftMargin; 177 } 178 if ((lp.boxedEdges & LayoutParams.BOX_RIGHT) == 0) { 179 marginRight = lp.rightMargin; 180 } 181 if ((lp.boxedEdges & LayoutParams.BOX_TOP) == 0) { 182 marginTop = lp.topMargin; 183 } 184 if ((lp.boxedEdges & LayoutParams.BOX_BOTTOM) == 0) { 185 marginBottom = lp.bottomMargin; 186 } 187 } else { 188 // rectangular, ignore boxed, use margins 189 marginLeft = lp.leftMargin; 190 marginTop = lp.topMargin; 191 marginRight = lp.rightMargin; 192 marginBottom = lp.bottomMargin; 193 } 194 measureChildWithMargins(child, widthMeasureSpec, 0, heightMeasureSpec, 0); 195 maxWidth = Math.max(maxWidth, child.getMeasuredWidth() + marginLeft + marginRight); 196 maxHeight = Math.max(maxHeight, 197 child.getMeasuredHeight() + marginTop + marginBottom); 198 childState = combineMeasuredStates(childState, child.getMeasuredState()); 199 } 200 } 201 // Account for padding too 202 maxWidth += getPaddingLeft() + mForegroundPadding.left + getPaddingRight() 203 + mForegroundPadding.right; 204 maxHeight += getPaddingTop() + mForegroundPadding.top + getPaddingBottom() 205 + mForegroundPadding.bottom; 206 207 // Check against our minimum height and width 208 maxHeight = Math.max(maxHeight, getSuggestedMinimumHeight()); 209 maxWidth = Math.max(maxWidth, getSuggestedMinimumWidth()); 210 211 // Check against our foreground's minimum height and width 212 if (mForegroundDrawable != null) { 213 maxHeight = Math.max(maxHeight, mForegroundDrawable.getMinimumHeight()); 214 maxWidth = Math.max(maxWidth, mForegroundDrawable.getMinimumWidth()); 215 } 216 217 int measuredWidth = resolveSizeAndState(maxWidth, widthMeasureSpec, childState); 218 int measuredHeight = resolveSizeAndState(maxHeight, heightMeasureSpec, 219 childState << MEASURED_HEIGHT_STATE_SHIFT); 220 setMeasuredDimension(measuredWidth, measuredHeight); 221 222 // determine boxed inset 223 int boxInset = calculateInset(measuredWidth, measuredHeight); 224 // adjust the the children measures, if necessary 225 for (int i = 0; i < count; i++) { 226 measureChild(widthMeasureSpec, heightMeasureSpec, boxInset, i); 227 } 228 } 229 230 @Override 231 protected void onLayout(boolean changed, int left, int top, int right, int bottom) { 232 final int count = getChildCount(); 233 234 final int parentLeft = getPaddingLeft() + mForegroundPadding.left; 235 final int parentRight = right - left - getPaddingRight() - mForegroundPadding.right; 236 237 final int parentTop = getPaddingTop() + mForegroundPadding.top; 238 final int parentBottom = bottom - top - getPaddingBottom() - mForegroundPadding.bottom; 239 240 for (int i = 0; i < count; i++) { 241 final View child = getChildAt(i); 242 if (child.getVisibility() != GONE) { 243 final LayoutParams lp = (LayoutParams) child.getLayoutParams(); 244 245 final int width = child.getMeasuredWidth(); 246 final int height = child.getMeasuredHeight(); 247 248 int childLeft; 249 int childTop; 250 251 int gravity = lp.gravity; 252 if (gravity == -1) { 253 gravity = DEFAULT_CHILD_GRAVITY; 254 } 255 256 final int layoutDirection = getLayoutDirection(); 257 final int absoluteGravity = Gravity.getAbsoluteGravity(gravity, layoutDirection); 258 final int verticalGravity = gravity & Gravity.VERTICAL_GRAVITY_MASK; 259 final int horizontalGravity = gravity & Gravity.HORIZONTAL_GRAVITY_MASK; 260 int desiredInset = calculateInset(getMeasuredWidth(), getMeasuredHeight()); 261 262 // If the child's width is match_parent then we can ignore gravity. 263 int leftChildMargin = calculateChildLeftMargin(lp, horizontalGravity, desiredInset); 264 int rightChildMargin = calculateChildRightMargin(lp, horizontalGravity, 265 desiredInset); 266 if (lp.width == LayoutParams.MATCH_PARENT) { 267 childLeft = parentLeft + leftChildMargin; 268 } else { 269 switch (absoluteGravity & Gravity.HORIZONTAL_GRAVITY_MASK) { 270 case Gravity.CENTER_HORIZONTAL: 271 childLeft = parentLeft + (parentRight - parentLeft - width) / 2 272 + leftChildMargin - rightChildMargin; 273 break; 274 case Gravity.RIGHT: 275 childLeft = parentRight - width - rightChildMargin; 276 break; 277 case Gravity.LEFT: 278 default: 279 childLeft = parentLeft + leftChildMargin; 280 } 281 } 282 283 // If the child's height is match_parent then we can ignore gravity. 284 int topChildMargin = calculateChildTopMargin(lp, verticalGravity, desiredInset); 285 int bottomChildMargin = calculateChildBottomMargin(lp, verticalGravity, 286 desiredInset); 287 if (lp.height == LayoutParams.MATCH_PARENT) { 288 childTop = parentTop + topChildMargin; 289 } else { 290 switch (verticalGravity) { 291 case Gravity.CENTER_VERTICAL: 292 childTop = parentTop + (parentBottom - parentTop - height) / 2 293 + topChildMargin - bottomChildMargin; 294 break; 295 case Gravity.BOTTOM: 296 childTop = parentBottom - height - bottomChildMargin; 297 break; 298 case Gravity.TOP: 299 default: 300 childTop = parentTop + topChildMargin; 301 } 302 } 303 child.layout(childLeft, childTop, childLeft + width, childTop + height); 304 } 305 } 306 } 307 308 @Override 309 protected boolean checkLayoutParams(ViewGroup.LayoutParams p) { 310 return p instanceof LayoutParams; 311 } 312 313 @Override 314 protected ViewGroup.LayoutParams generateLayoutParams(ViewGroup.LayoutParams p) { 315 return new LayoutParams(p); 316 } 317 318 private void measureChild(int widthMeasureSpec, int heightMeasureSpec, int desiredMinInset, 319 int i) { 320 final View child = getChildAt(i); 321 final LayoutParams childLayoutParams = (LayoutParams) child.getLayoutParams(); 322 323 int gravity = childLayoutParams.gravity; 324 if (gravity == -1) { 325 gravity = DEFAULT_CHILD_GRAVITY; 326 } 327 final int verticalGravity = gravity & Gravity.VERTICAL_GRAVITY_MASK; 328 final int horizontalGravity = gravity & Gravity.HORIZONTAL_GRAVITY_MASK; 329 330 int childWidthMeasureSpec; 331 int childHeightMeasureSpec; 332 333 int leftParentPadding = getPaddingLeft() + mForegroundPadding.left; 334 int rightParentPadding = getPaddingRight() + mForegroundPadding.right; 335 int topParentPadding = getPaddingTop() + mForegroundPadding.top; 336 int bottomParentPadding = getPaddingBottom() + mForegroundPadding.bottom; 337 338 // adjust width 339 int totalWidthMargin = leftParentPadding + rightParentPadding + calculateChildLeftMargin( 340 childLayoutParams, horizontalGravity, desiredMinInset) + calculateChildRightMargin( 341 childLayoutParams, horizontalGravity, desiredMinInset); 342 343 // adjust height 344 int totalHeightMargin = topParentPadding + bottomParentPadding + calculateChildTopMargin( 345 childLayoutParams, verticalGravity, desiredMinInset) + calculateChildBottomMargin( 346 childLayoutParams, verticalGravity, desiredMinInset); 347 348 childWidthMeasureSpec = getChildMeasureSpec(widthMeasureSpec, totalWidthMargin, 349 childLayoutParams.width); 350 childHeightMeasureSpec = getChildMeasureSpec(heightMeasureSpec, totalHeightMargin, 351 childLayoutParams.height); 352 353 int maxAllowedWidth = getMeasuredWidth() - totalWidthMargin; 354 int maxAllowedHeight = getMeasuredHeight() - totalHeightMargin; 355 if (child.getMeasuredWidth() > maxAllowedWidth 356 || child.getMeasuredHeight() > maxAllowedHeight) { 357 child.measure(childWidthMeasureSpec, childHeightMeasureSpec); 358 } 359 } 360 361 private int calculateChildLeftMargin(LayoutParams lp, int horizontalGravity, int 362 desiredMinInset) { 363 if (mIsRound && ((lp.boxedEdges & LayoutParams.BOX_LEFT) != 0)) { 364 if (lp.width == LayoutParams.MATCH_PARENT || horizontalGravity == Gravity.LEFT) { 365 return lp.leftMargin + desiredMinInset; 366 } 367 } 368 return lp.leftMargin; 369 } 370 371 private int calculateChildRightMargin(LayoutParams lp, int horizontalGravity, int 372 desiredMinInset) { 373 if (mIsRound && ((lp.boxedEdges & LayoutParams.BOX_RIGHT) != 0)) { 374 if (lp.width == LayoutParams.MATCH_PARENT || horizontalGravity == Gravity.RIGHT) { 375 return lp.rightMargin + desiredMinInset; 376 } 377 } 378 return lp.rightMargin; 379 } 380 381 private int calculateChildTopMargin(LayoutParams lp, int verticalGravity, int desiredMinInset) { 382 if (mIsRound && ((lp.boxedEdges & LayoutParams.BOX_TOP) != 0)) { 383 if (lp.height == LayoutParams.MATCH_PARENT || verticalGravity == Gravity.TOP) { 384 return lp.topMargin + desiredMinInset; 385 } 386 } 387 return lp.topMargin; 388 } 389 390 private int calculateChildBottomMargin(LayoutParams lp, int verticalGravity, int 391 desiredMinInset) { 392 if (mIsRound && ((lp.boxedEdges & LayoutParams.BOX_BOTTOM) != 0)) { 393 if (lp.height == LayoutParams.MATCH_PARENT || verticalGravity == Gravity.BOTTOM) { 394 return lp.bottomMargin + desiredMinInset; 395 } 396 } 397 return lp.bottomMargin; 398 } 399 400 private int calculateInset(int measuredWidth, int measuredHeight) { 401 int rightEdge = Math.min(measuredWidth, mScreenWidth); 402 int bottomEdge = Math.min(measuredHeight, mScreenHeight); 403 return (int) (FACTOR * Math.max(rightEdge, bottomEdge)); 404 } 405 406 /** 407 * Per-child layout information for layouts that support margins, gravity and boxedEdges. 408 * See {@link R.styleable#BoxInsetLayout_Layout BoxInsetLayout Layout Attributes} for a list 409 * of all child view attributes that this class supports. 410 * 411 * @attr ref R.styleable#BoxInsetLayout_Layout_boxedEdges 412 */ 413 public static class LayoutParams extends FrameLayout.LayoutParams { 414 415 /** @hide */ 416 @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) 417 @IntDef({BOX_NONE, BOX_LEFT, BOX_TOP, BOX_RIGHT, BOX_BOTTOM, BOX_ALL}) 418 @Retention(RetentionPolicy.SOURCE) 419 public @interface BoxedEdges {} 420 421 /** Default boxing setting. There are no insets forced on the child views. */ 422 public static final int BOX_NONE = 0x0; 423 /** The view will force an inset on the left edge of the children. */ 424 public static final int BOX_LEFT = 0x01; 425 /** The view will force an inset on the top edge of the children. */ 426 public static final int BOX_TOP = 0x02; 427 /** The view will force an inset on the right edge of the children. */ 428 public static final int BOX_RIGHT = 0x04; 429 /** The view will force an inset on the bottom edge of the children. */ 430 public static final int BOX_BOTTOM = 0x08; 431 /** The view will force an inset on all of the edges of the children. */ 432 public static final int BOX_ALL = 0x0F; 433 434 /** Specifies the screen-specific insets for each of the child edges. */ 435 @BoxedEdges 436 public int boxedEdges = BOX_NONE; 437 438 /** 439 * Creates a new set of layout parameters. The values are extracted from the supplied 440 * attributes set and context. 441 * 442 * @param context the application environment 443 * @param attrs the set of attributes from which to extract the layout parameters' values 444 */ 445 @SuppressWarnings("ResourceType") 446 public LayoutParams(@NonNull Context context, @Nullable AttributeSet attrs) { 447 super(context, attrs); 448 TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.BoxInsetLayout_Layout, 449 0, 0); 450 boxedEdges = a.getInt(R.styleable.BoxInsetLayout_Layout_boxedEdges, BOX_NONE); 451 a.recycle(); 452 } 453 454 /** 455 * Creates a new set of layout parameters with the specified width and height. 456 * 457 * @param width the width, either {@link #MATCH_PARENT}, 458 * {@link #WRAP_CONTENT} or a fixed size in pixels 459 * @param height the height, either {@link #MATCH_PARENT}, 460 * {@link #WRAP_CONTENT} or a fixed size in pixelsy 461 */ 462 public LayoutParams(int width, int height) { 463 super(width, height); 464 } 465 466 /** 467 * Creates a new set of layout parameters with the specified width, height 468 * and gravity. 469 * 470 * @param width the width, either {@link #MATCH_PARENT}, 471 * {@link #WRAP_CONTENT} or a fixed size in pixels 472 * @param height the height, either {@link #MATCH_PARENT}, 473 * {@link #WRAP_CONTENT} or a fixed size in pixels 474 * @param gravity the gravity 475 * 476 * @see android.view.Gravity 477 */ 478 public LayoutParams(int width, int height, int gravity) { 479 super(width, height, gravity); 480 } 481 482 483 public LayoutParams(int width, int height, int gravity, @BoxedEdges int boxed) { 484 super(width, height, gravity); 485 boxedEdges = boxed; 486 } 487 488 /** 489 * Copy constructor. Clones the width and height of the source. 490 * 491 * @param source The layout params to copy from. 492 */ 493 public LayoutParams(@NonNull ViewGroup.LayoutParams source) { 494 super(source); 495 } 496 497 /** 498 * Copy constructor. Clones the width, height and margin values. 499 * 500 * @param source The layout params to copy from. 501 */ 502 public LayoutParams(@NonNull ViewGroup.MarginLayoutParams source) { 503 super(source); 504 } 505 506 /** 507 * Copy constructor. Clones the width, height, margin values, and 508 * gravity of the source. 509 * 510 * @param source The layout params to copy from. 511 */ 512 public LayoutParams(@NonNull FrameLayout.LayoutParams source) { 513 super(source); 514 } 515 516 /** 517 * Copy constructor. Clones the width, height, margin values, boxedEdges and 518 * gravity of the source. 519 * 520 * @param source The layout params to copy from. 521 */ 522 public LayoutParams(@NonNull LayoutParams source) { 523 super(source); 524 this.boxedEdges = source.boxedEdges; 525 this.gravity = source.gravity; 526 } 527 } 528} 529