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