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