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 ScaledLayout(Context context)56 public ScaledLayout(Context context) { 57 this(context, null); 58 } 59 ScaledLayout(Context context, AttributeSet attrs)60 public ScaledLayout(Context context, AttributeSet attrs) { 61 this(context, attrs, 0); 62 } 63 ScaledLayout(Context context, AttributeSet attrs, int defStyle)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 ScaledLayoutParams(float scaleStartRow, float scaleEndRow, float scaleStartCol, float scaleEndCol)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 ScaledLayoutParams(Context context, AttributeSet attrs)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 generateLayoutParams(AttributeSet attrs)125 public LayoutParams generateLayoutParams(AttributeSet attrs) { 126 return new ScaledLayoutParams(getContext(), attrs); 127 } 128 129 @Override checkLayoutParams(LayoutParams p)130 protected boolean checkLayoutParams(LayoutParams p) { 131 return (p instanceof ScaledLayoutParams); 132 } 133 134 @Override onMeasure(int widthMeasureSpec, int heightMeasureSpec)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 onLayout(boolean changed, int l, int t, int r, int b)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