Home | History | Annotate | Download | only in layout
      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 
     30 import com.android.tv.tuner.R;
     31 
     32 import java.util.Arrays;
     33 import java.util.Comparator;
     34 
     35 /**
     36  * A layout that scales its children using the given percentage value.
     37  */
     38 public 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