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