/* * Copyright (C) 2014 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License */ package com.android.cts.verifier; import android.annotation.TargetApi; import android.os.Build; import android.content.Context; import android.content.res.TypedArray; import android.graphics.Rect; import android.graphics.drawable.Drawable; import android.util.AttributeSet; import android.view.Gravity; import android.view.View; import android.view.ViewGroup; import android.view.WindowInsets; import android.widget.FrameLayout; /** * BoxInsetLayout is a screen shape-aware FrameLayout that can box its children * in the center square of a round screen by using the * {@code ctsv_layout_box} attribute. The values for this attribute specify the * child's edges to be boxed in: * {@code left|top|right|bottom} or {@code all}. * The {@code ctsv_layout_box} attribute is ignored on a device with a rectangular * screen. */ @TargetApi(Build.VERSION_CODES.KITKAT_WATCH) public class BoxInsetLayout extends FrameLayout { private static float FACTOR = 0.146467f; //(1 - sqrt(2)/2)/2 private static final int DEFAULT_CHILD_GRAVITY = Gravity.TOP | Gravity.START; private Rect mForegroundPadding; private boolean mLastKnownRound; private Rect mInsets; public BoxInsetLayout(Context context) { this(context, null); } public BoxInsetLayout(Context context, AttributeSet attrs) { this(context, attrs, 0); } public BoxInsetLayout(Context context, AttributeSet attrs, int defStyle) { super(context, attrs, defStyle); // make sure we have foreground padding object if (mForegroundPadding == null) { mForegroundPadding = new Rect(); } if (mInsets == null) { mInsets = new Rect(); } } @Override protected void onAttachedToWindow() { super.onAttachedToWindow(); requestApplyInsets(); } @Override public WindowInsets onApplyWindowInsets(WindowInsets insets) { insets = super.onApplyWindowInsets(insets); final boolean round = insets.isRound(); if (round != mLastKnownRound) { mLastKnownRound = round; requestLayout(); } mInsets.set( insets.getSystemWindowInsetLeft(), insets.getSystemWindowInsetTop(), insets.getSystemWindowInsetRight(), insets.getSystemWindowInsetBottom()); return insets; } /** * determine screen shape * @return true if on a round screen */ public boolean isRound() { return mLastKnownRound; } /** * @return the system window insets Rect */ public Rect getInsets() { return mInsets; } @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { int count = getChildCount(); // find max size int maxWidth = 0; int maxHeight = 0; int childState = 0; for (int i = 0; i < count; i++) { final View child = getChildAt(i); if (child.getVisibility() != GONE) { LayoutParams lp = (BoxInsetLayout.LayoutParams) child.getLayoutParams(); int marginLeft = 0; int marginRight = 0; int marginTop = 0; int marginBottom = 0; if (mLastKnownRound) { // round screen, check boxed, don't use margins on boxed if ((lp.boxedEdges & LayoutParams.BOX_LEFT) == 0) { marginLeft = lp.leftMargin; } if ((lp.boxedEdges & LayoutParams.BOX_RIGHT) == 0) { marginRight = lp.rightMargin; } if ((lp.boxedEdges & LayoutParams.BOX_TOP) == 0) { marginTop = lp.topMargin; } if ((lp.boxedEdges & LayoutParams.BOX_BOTTOM) == 0) { marginBottom = lp.bottomMargin; } } else { // rectangular, ignore boxed, use margins marginLeft = lp.leftMargin; marginTop = lp.topMargin; marginRight = lp.rightMargin; marginBottom = lp.bottomMargin; } measureChildWithMargins(child, widthMeasureSpec, 0, heightMeasureSpec, 0); maxWidth = Math.max(maxWidth, child.getMeasuredWidth() + marginLeft + marginRight); maxHeight = Math.max(maxHeight, child.getMeasuredHeight() + marginTop + marginBottom); childState = combineMeasuredStates(childState, child.getMeasuredState()); } } // Account for padding too maxWidth += getPaddingLeft() + mForegroundPadding.left + getPaddingRight() + mForegroundPadding.right; maxHeight += getPaddingTop() + mForegroundPadding.top + getPaddingBottom() + mForegroundPadding.bottom; // Check against our minimum height and width maxHeight = Math.max(maxHeight, getSuggestedMinimumHeight()); maxWidth = Math.max(maxWidth, getSuggestedMinimumWidth()); // Check against our foreground's minimum height and width final Drawable drawable = getForeground(); if (drawable != null) { maxHeight = Math.max(maxHeight, drawable.getMinimumHeight()); maxWidth = Math.max(maxWidth, drawable.getMinimumWidth()); } setMeasuredDimension(resolveSizeAndState(maxWidth, widthMeasureSpec, childState), resolveSizeAndState(maxHeight, heightMeasureSpec, childState << MEASURED_HEIGHT_STATE_SHIFT)); // determine boxed inset int boxInset = (int) (FACTOR * Math.max(getMeasuredWidth(), getMeasuredHeight())); // adjust the match parent children for (int i = 0; i < count; i++) { final View child = getChildAt(i); final LayoutParams lp = (BoxInsetLayout.LayoutParams) child.getLayoutParams(); int childWidthMeasureSpec; int childHeightMeasureSpec; int plwf = getPaddingLeft() + mForegroundPadding.left; int prwf = getPaddingRight() + mForegroundPadding.right; int ptwf = getPaddingTop() + mForegroundPadding.top; int pbwf = getPaddingBottom() + mForegroundPadding.bottom; // adjust width int totalPadding = 0; int totalMargin = 0; // BoxInset is a padding. Ignore margin when we want to do BoxInset. if (mLastKnownRound && ((lp.boxedEdges & LayoutParams.BOX_LEFT) != 0)) { totalPadding += boxInset; } else { totalMargin += plwf + lp.leftMargin; } if (mLastKnownRound && ((lp.boxedEdges & LayoutParams.BOX_RIGHT) != 0)) { totalPadding += boxInset; } else { totalMargin += prwf + lp.rightMargin; } if (lp.width == LayoutParams.MATCH_PARENT) { // Only subtract margin from the actual width, leave the padding in. childWidthMeasureSpec = MeasureSpec.makeMeasureSpec( getMeasuredWidth() - totalMargin, MeasureSpec.EXACTLY); } else { childWidthMeasureSpec = getChildMeasureSpec(widthMeasureSpec, totalPadding + totalMargin, lp.width); } // adjust height totalPadding = 0; totalMargin = 0; if (mLastKnownRound && ((lp.boxedEdges & LayoutParams.BOX_TOP) != 0)) { totalPadding += boxInset; } else { totalMargin += ptwf + lp.topMargin; } if (mLastKnownRound && ((lp.boxedEdges & LayoutParams.BOX_BOTTOM) != 0)) { totalPadding += boxInset; } else { totalMargin += pbwf + lp.bottomMargin; } if (lp.height == LayoutParams.MATCH_PARENT) { childHeightMeasureSpec = MeasureSpec.makeMeasureSpec( getMeasuredHeight() - totalMargin, MeasureSpec.EXACTLY); } else { childHeightMeasureSpec = getChildMeasureSpec(heightMeasureSpec, totalPadding + totalMargin, lp.height); } child.measure(childWidthMeasureSpec, childHeightMeasureSpec); } } @Override protected void onLayout(boolean changed, int left, int top, int right, int bottom) { layoutBoxChildren(left, top, right, bottom, false /* no force left gravity */); } private void layoutBoxChildren(int left, int top, int right, int bottom, boolean forceLeftGravity) { final int count = getChildCount(); int boxInset = (int)(FACTOR * Math.max(right - left, bottom - top)); final int parentLeft = getPaddingLeft() + mForegroundPadding.left; final int parentRight = right - left - getPaddingRight() - mForegroundPadding.right; final int parentTop = getPaddingTop() + mForegroundPadding.top; final int parentBottom = bottom - top - getPaddingBottom() - mForegroundPadding.bottom; for (int i = 0; i < count; i++) { final View child = getChildAt(i); if (child.getVisibility() != GONE) { final LayoutParams lp = (LayoutParams) child.getLayoutParams(); final int width = child.getMeasuredWidth(); final int height = child.getMeasuredHeight(); int childLeft; int childTop; int gravity = lp.gravity; if (gravity == -1) { gravity = DEFAULT_CHILD_GRAVITY; } final int layoutDirection = getLayoutDirection(); final int absoluteGravity = Gravity.getAbsoluteGravity(gravity, layoutDirection); final int verticalGravity = gravity & Gravity.VERTICAL_GRAVITY_MASK; // These values are replaced with boxInset below as necessary. int paddingLeft = child.getPaddingLeft(); int paddingRight = child.getPaddingRight(); int paddingTop = child.getPaddingTop(); int paddingBottom = child.getPaddingBottom(); // If the child's width is match_parent, we ignore gravity and set boxInset padding // on both sides, with a left position of parentLeft + the child's left margin. if (lp.width == LayoutParams.MATCH_PARENT) { if (mLastKnownRound && ((lp.boxedEdges & LayoutParams.BOX_LEFT) != 0)) { paddingLeft = boxInset; } if (mLastKnownRound && ((lp.boxedEdges & LayoutParams.BOX_RIGHT) != 0)) { paddingRight = boxInset; } childLeft = parentLeft + lp.leftMargin; } else { switch (absoluteGravity & Gravity.HORIZONTAL_GRAVITY_MASK) { case Gravity.CENTER_HORIZONTAL: childLeft = parentLeft + (parentRight - parentLeft - width) / 2 + lp.leftMargin - lp.rightMargin; break; case Gravity.RIGHT: if (!forceLeftGravity) { if (mLastKnownRound && ((lp.boxedEdges & LayoutParams.BOX_RIGHT) != 0)) { paddingRight = boxInset; childLeft = right - left - width; } else { childLeft = parentRight - width - lp.rightMargin; } break; } case Gravity.LEFT: default: if (mLastKnownRound && ((lp.boxedEdges & LayoutParams.BOX_LEFT) != 0)) { paddingLeft = boxInset; childLeft = 0; } else { childLeft = parentLeft + lp.leftMargin; } } } // If the child's height is match_parent, we ignore gravity and set boxInset padding // on both top and bottom, with a top position of parentTop + the child's top // margin. if (lp.height == LayoutParams.MATCH_PARENT) { if (mLastKnownRound && ((lp.boxedEdges & LayoutParams.BOX_TOP) != 0)) { paddingTop = boxInset; } if (mLastKnownRound && ((lp.boxedEdges & LayoutParams.BOX_BOTTOM) != 0)) { paddingBottom = boxInset; } childTop = parentTop + lp.topMargin; } else { switch (verticalGravity) { case Gravity.TOP: if (mLastKnownRound && ((lp.boxedEdges & LayoutParams.BOX_TOP) != 0)) { paddingTop = boxInset; childTop = 0; } else { childTop = parentTop + lp.topMargin; } break; case Gravity.CENTER_VERTICAL: childTop = parentTop + (parentBottom - parentTop - height) / 2 + lp.topMargin - lp.bottomMargin; break; case Gravity.BOTTOM: if (mLastKnownRound && ((lp.boxedEdges & LayoutParams.BOX_BOTTOM) != 0)) { paddingBottom = boxInset; childTop = bottom - top - height; } else { childTop = parentBottom - height - lp.bottomMargin; } break; default: childTop = parentTop + lp.topMargin; } } child.setPadding(paddingLeft, paddingTop, paddingRight, paddingBottom); child.layout(childLeft, childTop, childLeft + width, childTop + height); } } } public void setForeground(Drawable drawable) { super.setForeground(drawable); if (mForegroundPadding == null) { mForegroundPadding = new Rect(); } drawable.getPadding(mForegroundPadding); } @Override protected boolean checkLayoutParams(ViewGroup.LayoutParams p) { return p instanceof LayoutParams; } @Override protected ViewGroup.LayoutParams generateLayoutParams(ViewGroup.LayoutParams p) { return new LayoutParams(p); } @Override public LayoutParams generateLayoutParams(AttributeSet attrs) { return new BoxInsetLayout.LayoutParams(getContext(), attrs); } /** * adds {@code ctsv_layout_box} attribute to layout parameters */ public static class LayoutParams extends FrameLayout.LayoutParams { public static final int BOX_NONE = 0x0; public static final int BOX_LEFT = 0x01; public static final int BOX_TOP = 0x02; public static final int BOX_RIGHT = 0x04; public static final int BOX_BOTTOM = 0x08; public static final int BOX_ALL = 0x0F; public int boxedEdges = BOX_NONE; public LayoutParams(Context context, AttributeSet attrs) { super(context, attrs); TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.BoxInsetLayout_Layout, 0, 0); boxedEdges = a.getInt(R.styleable.BoxInsetLayout_Layout_ctsv_layout_box, BOX_NONE); a.recycle(); } public LayoutParams(int width, int height) { super(width, height); } public LayoutParams(int width, int height, int gravity) { super(width, height, gravity); } public LayoutParams(int width, int height, int gravity, int boxed) { super(width, height, gravity); boxedEdges = boxed; } public LayoutParams(ViewGroup.LayoutParams source) { super(source); } public LayoutParams(ViewGroup.MarginLayoutParams source) { super(source); } public LayoutParams(FrameLayout.LayoutParams source) { super(source); } public LayoutParams(LayoutParams source) { super(source); this.boxedEdges = source.boxedEdges; this.gravity = source.gravity; } } }