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