blob: 379ea70e26a17abcda2e3275d6067bfcd77cfbf4 [file] [log] [blame]
/*
* Copyright (C) 2015 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.tv.tuner.layout;
import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Point;
import android.graphics.Rect;
import android.hardware.display.DisplayManager;
import android.util.AttributeSet;
import android.util.Log;
import android.view.Display;
import android.view.View;
import android.view.ViewGroup;
import com.android.tv.tuner.R;
import java.util.Arrays;
import java.util.Comparator;
/**
* A layout that scales its children using the given percentage value.
*/
public class ScaledLayout extends ViewGroup {
private static final String TAG = "ScaledLayout";
private static final boolean DEBUG = false;
private static final Comparator<Rect> mRectTopLeftSorter = new Comparator<Rect>() {
@Override
public int compare(Rect lhs, Rect rhs) {
if (lhs.top != rhs.top) {
return lhs.top - rhs.top;
} else {
return lhs.left - rhs.left;
}
}
};
private Rect[] mRectArray;
private final int mMaxWidth;
private final int mMaxHeight;
public ScaledLayout(Context context) {
this(context, null);
}
public ScaledLayout(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public ScaledLayout(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
Point size = new Point();
DisplayManager displayManager = (DisplayManager) getContext()
.getSystemService(Context.DISPLAY_SERVICE);
Display display = displayManager.getDisplay(Display.DEFAULT_DISPLAY);
display.getRealSize(size);
mMaxWidth = size.x;
mMaxHeight = size.y;
}
/**
* ScaledLayoutParams stores the four scale factors.
* <br>
* Vertical coordinate system: ({@code scaleStartRow} * 100) % ~ ({@code scaleEndRow} * 100) %
* Horizontal coordinate system: ({@code scaleStartCol} * 100) % ~ ({@code scaleEndCol} * 100) %
* <br>
* In XML, for example,
* <pre>
* {@code
* <View
* app:layout_scaleStartRow="0.1"
* app:layout_scaleEndRow="0.5"
* app:layout_scaleStartCol="0.4"
* app:layout_scaleEndCol="1" />
* }
* </pre>
*/
public static class ScaledLayoutParams extends ViewGroup.LayoutParams {
public static final float SCALE_UNSPECIFIED = -1;
public final float scaleStartRow;
public final float scaleEndRow;
public final float scaleStartCol;
public final float scaleEndCol;
public ScaledLayoutParams(float scaleStartRow, float scaleEndRow,
float scaleStartCol, float scaleEndCol) {
super(MATCH_PARENT, MATCH_PARENT);
this.scaleStartRow = scaleStartRow;
this.scaleEndRow = scaleEndRow;
this.scaleStartCol = scaleStartCol;
this.scaleEndCol = scaleEndCol;
}
public ScaledLayoutParams(Context context, AttributeSet attrs) {
super(MATCH_PARENT, MATCH_PARENT);
TypedArray array =
context.obtainStyledAttributes(attrs, R.styleable.utScaledLayout);
scaleStartRow =
array.getFloat(R.styleable.utScaledLayout_layout_scaleStartRow, SCALE_UNSPECIFIED);
scaleEndRow =
array.getFloat(R.styleable.utScaledLayout_layout_scaleEndRow, SCALE_UNSPECIFIED);
scaleStartCol =
array.getFloat(R.styleable.utScaledLayout_layout_scaleStartCol, SCALE_UNSPECIFIED);
scaleEndCol =
array.getFloat(R.styleable.utScaledLayout_layout_scaleEndCol, SCALE_UNSPECIFIED);
array.recycle();
}
}
@Override
public LayoutParams generateLayoutParams(AttributeSet attrs) {
return new ScaledLayoutParams(getContext(), attrs);
}
@Override
protected boolean checkLayoutParams(LayoutParams p) {
return (p instanceof ScaledLayoutParams);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int widthSpecSize = MeasureSpec.getSize(widthMeasureSpec);
int heightSpecSize = MeasureSpec.getSize(heightMeasureSpec);
int width = widthSpecSize - getPaddingLeft() - getPaddingRight();
int height = heightSpecSize - getPaddingTop() - getPaddingBottom();
if (DEBUG) {
Log.d(TAG, String.format("onMeasure width: %d, height: %d", width, height));
}
int count = getChildCount();
mRectArray = new Rect[count];
for (int i = 0; i < count; ++i) {
View child = getChildAt(i);
ViewGroup.LayoutParams params = child.getLayoutParams();
float scaleStartRow, scaleEndRow, scaleStartCol, scaleEndCol;
if (!(params instanceof ScaledLayoutParams)) {
throw new RuntimeException(
"A child of ScaledLayout cannot have the UNSPECIFIED scale factors");
}
scaleStartRow = ((ScaledLayoutParams) params).scaleStartRow;
scaleEndRow = ((ScaledLayoutParams) params).scaleEndRow;
scaleStartCol = ((ScaledLayoutParams) params).scaleStartCol;
scaleEndCol = ((ScaledLayoutParams) params).scaleEndCol;
if (scaleStartRow < 0 || scaleStartRow > 1) {
throw new RuntimeException("A child of ScaledLayout should have a range of "
+ "scaleStartRow between 0 and 1");
}
if (scaleEndRow < scaleStartRow || scaleStartRow > 1) {
throw new RuntimeException("A child of ScaledLayout should have a range of "
+ "scaleEndRow between scaleStartRow and 1");
}
if (scaleEndCol < 0 || scaleEndCol > 1) {
throw new RuntimeException("A child of ScaledLayout should have a range of "
+ "scaleStartCol between 0 and 1");
}
if (scaleEndCol < scaleStartCol || scaleEndCol > 1) {
throw new RuntimeException("A child of ScaledLayout should have a range of "
+ "scaleEndCol between scaleStartCol and 1");
}
if (DEBUG) {
Log.d(TAG, String.format("onMeasure child scaleStartRow: %f scaleEndRow: %f "
+ "scaleStartCol: %f scaleEndCol: %f",
scaleStartRow, scaleEndRow, scaleStartCol, scaleEndCol));
}
mRectArray[i] = new Rect((int) (scaleStartCol * width), (int) (scaleStartRow * height),
(int) (scaleEndCol * width), (int) (scaleEndRow * height));
int scaleWidth = (int) (width * (scaleEndCol - scaleStartCol));
int childWidthSpec = MeasureSpec.makeMeasureSpec(
scaleWidth > mMaxWidth ? mMaxWidth : scaleWidth, MeasureSpec.EXACTLY);
int childHeightSpec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED);
child.measure(childWidthSpec, childHeightSpec);
// If the height of the measured child view is bigger than the height of the calculated
// region by the given ScaleLayoutParams, the height of the region should be increased
// to fit the size of the child view.
if (child.getMeasuredHeight() > mRectArray[i].height()) {
int overflowedHeight = child.getMeasuredHeight() - mRectArray[i].height();
overflowedHeight = (overflowedHeight + 1) / 2;
mRectArray[i].bottom += overflowedHeight;
mRectArray[i].top -= overflowedHeight;
if (mRectArray[i].top < 0) {
mRectArray[i].bottom -= mRectArray[i].top;
mRectArray[i].top = 0;
}
if (mRectArray[i].bottom > height) {
mRectArray[i].top -= mRectArray[i].bottom - height;
mRectArray[i].bottom = height;
}
}
int scaleHeight = (int) (height * (scaleEndRow - scaleStartRow));
childHeightSpec = MeasureSpec.makeMeasureSpec(
scaleHeight > mMaxHeight ? mMaxHeight : scaleHeight, MeasureSpec.EXACTLY);
child.measure(childWidthSpec, childHeightSpec);
}
// Avoid overlapping rectangles.
// Step 1. Sort rectangles by position (top-left).
int visibleRectCount = 0;
int[] visibleRectGroup = new int[count];
Rect[] visibleRectArray = new Rect[count];
for (int i = 0; i < count; ++i) {
if (getChildAt(i).getVisibility() == View.VISIBLE) {
visibleRectGroup[visibleRectCount] = visibleRectCount;
visibleRectArray[visibleRectCount] = mRectArray[i];
++visibleRectCount;
}
}
Arrays.sort(visibleRectArray, 0, visibleRectCount, mRectTopLeftSorter);
// Step 2. Move down if there are overlapping rectangles.
for (int i = 0; i < visibleRectCount - 1; ++i) {
for (int j = i + 1; j < visibleRectCount; ++j) {
if (Rect.intersects(visibleRectArray[i], visibleRectArray[j])) {
visibleRectGroup[j] = visibleRectGroup[i];
visibleRectArray[j].set(visibleRectArray[j].left,
visibleRectArray[i].bottom,
visibleRectArray[j].right,
visibleRectArray[i].bottom + visibleRectArray[j].height());
}
}
}
// Step 3. Move up if there is any overflowed rectangle.
for (int i = visibleRectCount - 1; i >= 0; --i) {
if (visibleRectArray[i].bottom > height) {
int overflowedHeight = visibleRectArray[i].bottom - height;
for (int j = 0; j <= i; ++j) {
if (visibleRectGroup[i] == visibleRectGroup[j]) {
visibleRectArray[j].set(visibleRectArray[j].left,
visibleRectArray[j].top - overflowedHeight,
visibleRectArray[j].right,
visibleRectArray[j].bottom - overflowedHeight);
}
}
}
}
setMeasuredDimension(widthSpecSize, heightSpecSize);
}
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
int paddingLeft = getPaddingLeft();
int paddingTop = getPaddingTop();
int count = getChildCount();
for (int i = 0; i < count; ++i) {
View child = getChildAt(i);
if (child.getVisibility() != GONE) {
int childLeft = paddingLeft + mRectArray[i].left;
int childTop = paddingTop + mRectArray[i].top;
int childBottom = paddingLeft + mRectArray[i].bottom;
int childRight = paddingTop + mRectArray[i].right;
if (DEBUG) {
Log.d(TAG, String.format("layoutChild bottom: %d left: %d right: %d top: %d",
childBottom, childLeft,
childRight, childTop));
}
child.layout(childLeft, childTop, childRight, childBottom);
}
}
}
}