1 /*
2  * Copyright (C) 2015 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.tv.tuner.layout;
18 
19 import android.content.Context;
20 import android.content.res.TypedArray;
21 import android.graphics.Point;
22 import android.graphics.Rect;
23 import android.hardware.display.DisplayManager;
24 import android.util.AttributeSet;
25 import android.util.Log;
26 import android.view.Display;
27 import android.view.View;
28 import android.view.ViewGroup;
29 import com.android.tv.tuner.R;
30 import java.util.Arrays;
31 import java.util.Comparator;
32 
33 /** A layout that scales its children using the given percentage value. */
34 public class ScaledLayout extends ViewGroup {
35     private static final String TAG = "ScaledLayout";
36     private static final boolean DEBUG = false;
37     private static final Comparator<Rect> mRectTopLeftSorter =
38             (Rect lhs, Rect rhs) -> {
39                 if (lhs.top != rhs.top) {
40                     return lhs.top - rhs.top;
41                 } else {
42                     return lhs.left - rhs.left;
43                 }
44             };
45 
46     private Rect[] mRectArray;
47     private final int mMaxWidth;
48     private final int mMaxHeight;
49 
ScaledLayout(Context context)50     public ScaledLayout(Context context) {
51         this(context, null);
52     }
53 
ScaledLayout(Context context, AttributeSet attrs)54     public ScaledLayout(Context context, AttributeSet attrs) {
55         this(context, attrs, 0);
56     }
57 
ScaledLayout(Context context, AttributeSet attrs, int defStyle)58     public ScaledLayout(Context context, AttributeSet attrs, int defStyle) {
59         super(context, attrs, defStyle);
60         Point size = new Point();
61         DisplayManager displayManager =
62                 (DisplayManager) getContext().getSystemService(Context.DISPLAY_SERVICE);
63         Display display = displayManager.getDisplay(Display.DEFAULT_DISPLAY);
64         display.getRealSize(size);
65         mMaxWidth = size.x;
66         mMaxHeight = size.y;
67     }
68 
69     /**
70      * ScaledLayoutParams stores the four scale factors. <br>
71      * Vertical coordinate system: ({@code scaleStartRow} * 100) % ~ ({@code scaleEndRow} * 100) %
72      * Horizontal coordinate system: ({@code scaleStartCol} * 100) % ~ ({@code scaleEndCol} * 100) %
73      * <br>
74      * In XML, for example,
75      *
76      * <pre>{@code
77      * <View
78      *     app:layout_scaleStartRow="0.1"
79      *     app:layout_scaleEndRow="0.5"
80      *     app:layout_scaleStartCol="0.4"
81      *     app:layout_scaleEndCol="1" />
82      * }</pre>
83      */
84     public static class ScaledLayoutParams extends ViewGroup.LayoutParams {
85         public static final float SCALE_UNSPECIFIED = -1;
86         public final float scaleStartRow;
87         public final float scaleEndRow;
88         public final float scaleStartCol;
89         public final float scaleEndCol;
90 
ScaledLayoutParams( float scaleStartRow, float scaleEndRow, float scaleStartCol, float scaleEndCol)91         public ScaledLayoutParams(
92                 float scaleStartRow, float scaleEndRow, float scaleStartCol, float scaleEndCol) {
93             super(MATCH_PARENT, MATCH_PARENT);
94             this.scaleStartRow = scaleStartRow;
95             this.scaleEndRow = scaleEndRow;
96             this.scaleStartCol = scaleStartCol;
97             this.scaleEndCol = scaleEndCol;
98         }
99 
ScaledLayoutParams(Context context, AttributeSet attrs)100         public ScaledLayoutParams(Context context, AttributeSet attrs) {
101             super(MATCH_PARENT, MATCH_PARENT);
102             TypedArray array = context.obtainStyledAttributes(attrs, R.styleable.utScaledLayout);
103             scaleStartRow =
104                     array.getFloat(
105                             R.styleable.utScaledLayout_layout_scaleStartRow, SCALE_UNSPECIFIED);
106             scaleEndRow =
107                     array.getFloat(
108                             R.styleable.utScaledLayout_layout_scaleEndRow, SCALE_UNSPECIFIED);
109             scaleStartCol =
110                     array.getFloat(
111                             R.styleable.utScaledLayout_layout_scaleStartCol, SCALE_UNSPECIFIED);
112             scaleEndCol =
113                     array.getFloat(
114                             R.styleable.utScaledLayout_layout_scaleEndCol, SCALE_UNSPECIFIED);
115             array.recycle();
116         }
117     }
118 
119     @Override
generateLayoutParams(AttributeSet attrs)120     public LayoutParams generateLayoutParams(AttributeSet attrs) {
121         return new ScaledLayoutParams(getContext(), attrs);
122     }
123 
124     @Override
checkLayoutParams(LayoutParams p)125     protected boolean checkLayoutParams(LayoutParams p) {
126         return (p instanceof ScaledLayoutParams);
127     }
128 
129     @Override
onMeasure(int widthMeasureSpec, int heightMeasureSpec)130     protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
131         int widthSpecSize = MeasureSpec.getSize(widthMeasureSpec);
132         int heightSpecSize = MeasureSpec.getSize(heightMeasureSpec);
133         int width = widthSpecSize - getPaddingLeft() - getPaddingRight();
134         int height = heightSpecSize - getPaddingTop() - getPaddingBottom();
135         if (DEBUG) {
136             Log.d(TAG, String.format("onMeasure width: %d, height: %d", width, height));
137         }
138         int count = getChildCount();
139         mRectArray = new Rect[count];
140         for (int i = 0; i < count; ++i) {
141             View child = getChildAt(i);
142             ViewGroup.LayoutParams params = child.getLayoutParams();
143             float scaleStartRow, scaleEndRow, scaleStartCol, scaleEndCol;
144             if (!(params instanceof ScaledLayoutParams)) {
145                 throw new RuntimeException(
146                         "A child of ScaledLayout cannot have the UNSPECIFIED scale factors");
147             }
148             scaleStartRow = ((ScaledLayoutParams) params).scaleStartRow;
149             scaleEndRow = ((ScaledLayoutParams) params).scaleEndRow;
150             scaleStartCol = ((ScaledLayoutParams) params).scaleStartCol;
151             scaleEndCol = ((ScaledLayoutParams) params).scaleEndCol;
152             if (scaleStartRow < 0 || scaleStartRow > 1) {
153                 throw new RuntimeException(
154                         "A child of ScaledLayout should have a range of "
155                                 + "scaleStartRow between 0 and 1");
156             }
157             if (scaleEndRow < scaleStartRow || scaleStartRow > 1) {
158                 throw new RuntimeException(
159                         "A child of ScaledLayout should have a range of "
160                                 + "scaleEndRow between scaleStartRow and 1");
161             }
162             if (scaleEndCol < 0 || scaleEndCol > 1) {
163                 throw new RuntimeException(
164                         "A child of ScaledLayout should have a range of "
165                                 + "scaleStartCol between 0 and 1");
166             }
167             if (scaleEndCol < scaleStartCol || scaleEndCol > 1) {
168                 throw new RuntimeException(
169                         "A child of ScaledLayout should have a range of "
170                                 + "scaleEndCol between scaleStartCol and 1");
171             }
172             if (DEBUG) {
173                 Log.d(
174                         TAG,
175                         String.format(
176                                 "onMeasure child scaleStartRow: %f scaleEndRow: %f "
177                                         + "scaleStartCol: %f scaleEndCol: %f",
178                                 scaleStartRow, scaleEndRow, scaleStartCol, scaleEndCol));
179             }
180             mRectArray[i] =
181                     new Rect(
182                             (int) (scaleStartCol * width),
183                             (int) (scaleStartRow * height),
184                             (int) (scaleEndCol * width),
185                             (int) (scaleEndRow * height));
186             int scaleWidth = (int) (width * (scaleEndCol - scaleStartCol));
187             int childWidthSpec =
188                     MeasureSpec.makeMeasureSpec(
189                             scaleWidth > mMaxWidth ? mMaxWidth : scaleWidth, MeasureSpec.EXACTLY);
190             int childHeightSpec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED);
191             child.measure(childWidthSpec, childHeightSpec);
192 
193             // If the height of the measured child view is bigger than the height of the calculated
194             // region by the given ScaleLayoutParams, the height of the region should be increased
195             // to fit the size of the child view.
196             if (child.getMeasuredHeight() > mRectArray[i].height()) {
197                 int overflowedHeight = child.getMeasuredHeight() - mRectArray[i].height();
198                 overflowedHeight = (overflowedHeight + 1) / 2;
199                 mRectArray[i].bottom += overflowedHeight;
200                 mRectArray[i].top -= overflowedHeight;
201                 if (mRectArray[i].top < 0) {
202                     mRectArray[i].bottom -= mRectArray[i].top;
203                     mRectArray[i].top = 0;
204                 }
205                 if (mRectArray[i].bottom > height) {
206                     mRectArray[i].top -= mRectArray[i].bottom - height;
207                     mRectArray[i].bottom = height;
208                 }
209             }
210             int scaleHeight = (int) (height * (scaleEndRow - scaleStartRow));
211             childHeightSpec =
212                     MeasureSpec.makeMeasureSpec(
213                             scaleHeight > mMaxHeight ? mMaxHeight : scaleHeight,
214                             MeasureSpec.EXACTLY);
215             child.measure(childWidthSpec, childHeightSpec);
216         }
217 
218         // Avoid overlapping rectangles.
219         // Step 1. Sort rectangles by position (top-left).
220         int visibleRectCount = 0;
221         int[] visibleRectGroup = new int[count];
222         Rect[] visibleRectArray = new Rect[count];
223         for (int i = 0; i < count; ++i) {
224             if (getChildAt(i).getVisibility() == View.VISIBLE) {
225                 visibleRectGroup[visibleRectCount] = visibleRectCount;
226                 visibleRectArray[visibleRectCount] = mRectArray[i];
227                 ++visibleRectCount;
228             }
229         }
230         Arrays.sort(visibleRectArray, 0, visibleRectCount, mRectTopLeftSorter);
231 
232         // Step 2. Move down if there are overlapping rectangles.
233         for (int i = 0; i < visibleRectCount - 1; ++i) {
234             for (int j = i + 1; j < visibleRectCount; ++j) {
235                 if (Rect.intersects(visibleRectArray[i], visibleRectArray[j])) {
236                     visibleRectGroup[j] = visibleRectGroup[i];
237                     visibleRectArray[j].set(
238                             visibleRectArray[j].left,
239                             visibleRectArray[i].bottom,
240                             visibleRectArray[j].right,
241                             visibleRectArray[i].bottom + visibleRectArray[j].height());
242                 }
243             }
244         }
245 
246         // Step 3. Move up if there is any overflowed rectangle.
247         for (int i = visibleRectCount - 1; i >= 0; --i) {
248             if (visibleRectArray[i].bottom > height) {
249                 int overflowedHeight = visibleRectArray[i].bottom - height;
250                 for (int j = 0; j <= i; ++j) {
251                     if (visibleRectGroup[i] == visibleRectGroup[j]) {
252                         visibleRectArray[j].set(
253                                 visibleRectArray[j].left,
254                                 visibleRectArray[j].top - overflowedHeight,
255                                 visibleRectArray[j].right,
256                                 visibleRectArray[j].bottom - overflowedHeight);
257                     }
258                 }
259             }
260         }
261         setMeasuredDimension(widthSpecSize, heightSpecSize);
262     }
263 
264     @Override
onLayout(boolean changed, int l, int t, int r, int b)265     protected void onLayout(boolean changed, int l, int t, int r, int b) {
266         int paddingLeft = getPaddingLeft();
267         int paddingTop = getPaddingTop();
268         int count = getChildCount();
269         for (int i = 0; i < count; ++i) {
270             View child = getChildAt(i);
271             if (child.getVisibility() != GONE) {
272                 int childLeft = paddingLeft + mRectArray[i].left;
273                 int childTop = paddingTop + mRectArray[i].top;
274                 int childBottom = paddingLeft + mRectArray[i].bottom;
275                 int childRight = paddingTop + mRectArray[i].right;
276                 if (DEBUG) {
277                     Log.d(
278                             TAG,
279                             String.format(
280                                     "layoutChild bottom: %d left: %d right: %d top: %d",
281                                     childBottom, childLeft, childRight, childTop));
282                 }
283                 child.layout(childLeft, childTop, childRight, childBottom);
284             }
285         }
286     }
287 }
288