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 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             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