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