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