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