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