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