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