Files
EhViewer/app/src/main/java/com/hippo/gl/view/GLView.java
T
2016-02-10 16:04:50 +08:00

1267 lines
40 KiB
Java

/*
* Copyright (C) 2010 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.hippo.gl.view;
import android.graphics.Color;
import android.graphics.Rect;
import android.os.Parcelable;
import android.os.SystemClock;
import android.util.Log;
import android.util.SparseArray;
import android.view.MotionEvent;
import com.hippo.gl.anim.CanvasAnimation;
import com.hippo.gl.glrenderer.GLCanvas;
import com.hippo.gl.glrenderer.GLPaint;
import com.hippo.yorozuya.AssertUtils;
import java.util.ArrayList;
// GLView is a UI component. It can render to a GLCanvas and accept touch
// events. A GLView may have zero or more child GLView and they form a tree
// structure. The rendering and event handling will pass through the tree
// structure.
//
// A GLView tree should be attached to a GLRoot before event dispatching and
// rendering happens. GLView asks GLRoot to re-render or re-layout the
// GLView hierarchy using requestRender() and requestLayoutContentPane().
//
// The render() method is called in a separate thread. Before calling
// dispatchTouchEvent() and layout(), GLRoot acquires a lock to avoid the
// rendering thread running at the same time. If there are other entry points
// from main thread (like a Handler) in your GLView, you need to call
// lockRendering() if the rendering thread should not run at the same time.
//
public class GLView implements TouchOwner {
private static final String TAG = "GLView";
private static final boolean DEBUG_DRAW_BOUNDS = false;
public static final int VISIBLE = 0;
public static final int INVISIBLE = 1;
private static final int FLAG_INVISIBLE = 1;
private static final int FLAG_SET_MEASURED_SIZE = 2;
private static final int FLAG_LAYOUT_REQUESTED = 4;
/**
* Used to mark a GlView that has no ID.
*/
public static final int NO_ID = -1;
public interface OnClickListener {
boolean onClick(GLView v);
}
public interface OnLongClickListener {
boolean onLongClick(GLView v);
}
protected final static GLPaint mDrawBoundsPaint;
static {
mDrawBoundsPaint = new GLPaint();
mDrawBoundsPaint.setColor(Color.RED);
mDrawBoundsPaint.setLineWidth(2);
}
/**
* Position in parent, just like left, top, right, bottom in {@link android.view.View}
*/
protected final Rect mBounds = new Rect();
/**
* Position in root
*/
private final int[] mPosition = new int[2];
protected final Rect mPaddings = new Rect();
private GLRoot mRoot;
protected GLView mParent;
private ArrayList<GLView> mComponents;
private GLView mMotionTarget;
private CanvasAnimation mAnimation;
private int mViewFlags = 0;
protected int mMeasuredWidth = 0;
protected int mMeasuredHeight = 0;
private int mLastWidthSpec = -1;
private int mLastHeightSpec = -1;
protected int mScrollY = 0;
protected int mScrollX = 0;
protected int mScrollHeight = 0;
protected int mScrollWidth = 0;
/**
* The GlView's identifier.
*
* @see #setId(int)
* @see #getId()
*/
private int mID = NO_ID;
/**
* The minimum height of the view. We'll try our best to have the height
* of this view to at least this amount.
*/
private int mMinHeight;
/**
* The minimum width of the view. We'll try our best to have the width
* of this view to at least this amount.
*/
private int mMinWidth;
private LayoutParams mLayoutParams;
private int mBackgroundColor = Color.TRANSPARENT;
private TouchHelper mTouchHelper;
private float mHotspotX;
private float mHotspotY;
private boolean mPressed;
private OnClickListener mOnClickListener;
private OnLongClickListener mOnLongClickListener;
// Sets the visibility of this GLView (either GLView.VISIBLE or
// GLView.INVISIBLE).
public void setVisibility(int visibility) {
if (visibility == getVisibility()) return;
if (visibility == VISIBLE) {
mViewFlags &= ~FLAG_INVISIBLE;
} else {
mViewFlags |= FLAG_INVISIBLE;
}
onVisibilityChanged(visibility);
invalidate();
}
// Returns GLView.VISIBLE or GLView.INVISIBLE
public int getVisibility() {
return (mViewFlags & FLAG_INVISIBLE) == 0 ? VISIBLE : INVISIBLE;
}
// This should only be called on the content pane (the topmost GLView).
public void attachToRoot(GLRoot root) {
AssertUtils.assertTrue(mParent == null && mRoot == null);
onAttachToRoot(root);
}
// This should only be called on the content pane (the topmost GLView).
void detachFromRoot() {
AssertUtils.assertTrue(mParent == null && mRoot != null);
onDetachFromRoot();
}
// Returns the number of children of the GLView.
public int getComponentCount() {
return mComponents == null ? 0 : mComponents.size();
}
// Returns the children for the given index.
public GLView getComponent(int index) {
if (mComponents == null) {
throw new ArrayIndexOutOfBoundsException(index);
}
return mComponents.get(index);
}
// Adds a child to this GLView.
public void addComponent(GLView component) {
addComponent(component, -1, null);
}
public void addComponent(GLView component, int index) {
addComponent(component, index, null);
}
public void addComponent(GLView component, LayoutParams params) {
addComponent(component, -1, params);
}
public void addComponent(GLView component, int index, LayoutParams params) {
// Make component is not null
if (component == null) {
throw new IllegalArgumentException("Cannot add a null child view to a ViewGroup");
}
// Make sure the component doesn't have a parent currently.
if (component.mParent != null) {
throw new IllegalStateException(
"Component " + this + " being added, but it already has a parent");
}
// Ensure index is valid
if (index < 0) {
index = getComponentCount();
}
// Ensure params is valid
if (params == null) {
params = generateDefaultLayoutParams();
} else if (!checkLayoutParams(params)) {
params = generateLayoutParams(params);
}
// Build parent-child links
if (mComponents == null) {
mComponents = new ArrayList<>();
}
mComponents.add(index, component);
component.mParent = this;
component.setLayoutParams(params);
// If this is added after we have a root, tell the component.
if (mRoot != null) {
component.onAttachToRoot(mRoot);
}
}
// Removes a child from this GLView.
public boolean removeComponent(GLView component) {
if (mComponents == null) return false;
if (mComponents.remove(component)) {
removeOneComponent(component);
return true;
}
return false;
}
public boolean removeComponentAt(int index) {
if (mComponents == null) {
return false;
}
if (index >= 0 && index < getComponentCount()) {
GLView component = mComponents.remove(index);
removeOneComponent(component);
return true;
} else {
return false;
}
}
// Removes all children of this GLView.
public void removeAllComponents() {
for (int i = 0, n = mComponents.size(); i < n; ++i) {
removeOneComponent(mComponents.get(i));
}
mComponents.clear();
}
private void removeOneComponent(GLView component) {
if (mMotionTarget == component) {
long now = SystemClock.uptimeMillis();
MotionEvent cancelEvent = MotionEvent.obtain(
now, now, MotionEvent.ACTION_CANCEL, 0, 0, 0);
dispatchTouchEvent(cancelEvent);
cancelEvent.recycle();
}
component.onDetachFromRoot();
component.mParent = null;
}
/**
* Sets the identifier for this GlView. The identifier does not have to be
* unique in this GlView's hierarchy. The identifier should be a positive
* number.
*
* @param id a number used to identify the view
*/
public void setId(int id) {
mID = id;
}
/**
* Returns this GlView's identifier.
*
* @return a positive integer used to identify the view or {@link #NO_ID}
* if the view has no ID
*/
public int getId() {
return mID;
}
public Rect bounds() {
return mBounds;
}
@Override
public void setHotspot(float x, float y) {
mHotspotX = x;
mHotspotY = y;
}
public float getHotspotX() {
return mHotspotX;
}
public float getHotspotY() {
return mHotspotY;
}
@Override
public boolean isEnabled() {
return true; // TODO
}
@Override
public boolean isPressed() {
return mPressed;
}
@Override
public void setPressed(boolean pressed) {
mPressed = pressed;
}
@Override
public boolean isClickable() {
return mOnClickListener != null;
}
@Override
public boolean isLongClickable() {
return mOnLongClickListener != null;
}
@Override
public boolean performClick() {
if (mOnClickListener != null) {
return mOnClickListener.onClick(this);
} else {
return false;
}
}
@Override
public boolean performLongClick() {
if (mOnLongClickListener != null) {
return mOnLongClickListener.onLongClick(this);
} else {
return false;
}
}
public void setOnClickListener(OnClickListener listener) {
mOnClickListener = listener;
}
public void setOnLongClickListener(OnLongClickListener listener) {
mOnLongClickListener = listener;
}
@Override
public int getWidth() {
return mBounds.right - mBounds.left;
}
@Override
public int getHeight() {
return mBounds.bottom - mBounds.top;
}
/**
* Offset this GLView's vertical location by the specified number of pixels.
*
* @param offset the number of pixels to offset the view by
*/
public void offsetTopAndBottom(int offset) {
if (offset != 0){
mBounds.offset(0, offset);
invalidate();
}
}
/**
* Offset this GLView's horizontal location by the specified amount of pixels.
*
* @param offset the number of pixels to offset the view by
*/
public void offsetLeftAndRight(int offset) {
if (offset != 0){
mBounds.offset(offset, 0);
invalidate();
}
}
public GLRoot getGLRoot() {
return mRoot;
}
// Request re-rendering of the view hierarchy.
// This is used for animation or when the contents changed.
public void invalidate() {
GLRoot root = getGLRoot();
if (root != null) root.requestRender();
}
// Request re-layout of the view hierarchy.
public void requestLayout() {
mViewFlags |= FLAG_LAYOUT_REQUESTED;
mLastWidthSpec = -1;
mLastHeightSpec = -1;
if (mParent != null) {
mParent.requestLayout();
} else {
// Is this a content pane ?
GLRoot root = getGLRoot();
if (root != null) root.requestLayoutContentPane();
}
}
protected void render(GLCanvas canvas) {
// render background color
if (Color.alpha(mBackgroundColor) != 0) {
canvas.fillRect(0, 0, getWidth(), getHeight(), mBackgroundColor);
}
// render content
onRender(canvas);
// render child
canvas.save();
for (int i = 0, n = getComponentCount(); i < n; ++i) {
renderChild(canvas, getComponent(i));
}
canvas.restore();
// render bounds
if (DEBUG_DRAW_BOUNDS) {
canvas.drawRect(0, 0, getWidth(), getHeight(), mDrawBoundsPaint);
}
}
public void onRender(GLCanvas canvas) {
}
public void setBackgroundColor(int color) {
mBackgroundColor = color;
}
protected void renderChild(GLCanvas canvas, GLView component) {
if (component.getVisibility() != GLView.VISIBLE
&& component.mAnimation == null) return;
int xOffset = component.mBounds.left - mScrollX;
int yOffset = component.mBounds.top - mScrollY;
canvas.translate(xOffset, yOffset);
CanvasAnimation anim = component.mAnimation;
if (anim != null) {
canvas.save(anim.getCanvasSaveFlags());
if (anim.calculate(AnimationTime.get())) {
invalidate();
} else {
component.mAnimation = null;
}
anim.apply(canvas);
}
component.render(canvas);
if (anim != null) canvas.restore();
canvas.translate(-xOffset, -yOffset);
}
protected boolean onTouch(MotionEvent event) {
if (mTouchHelper == null) {
mTouchHelper = new TouchHelper(this);
}
return mTouchHelper.onTouch(event);
}
protected boolean dispatchTouchEvent(MotionEvent event,
int x, int y, GLView component, boolean checkBounds) {
Rect rect = component.mBounds;
int left = rect.left;
int top = rect.top;
if (!checkBounds || rect.contains(x, y)) {
event.offsetLocation(-left, -top);
if (component.dispatchTouchEvent(event)) {
event.offsetLocation(left, top);
return true;
}
event.offsetLocation(left, top);
}
return false;
}
protected boolean dispatchTouchEvent(MotionEvent event) {
int x = (int) event.getX();
int y = (int) event.getY();
int action = event.getAction();
if (mMotionTarget != null) {
if (action == MotionEvent.ACTION_DOWN) {
MotionEvent cancel = MotionEvent.obtain(event);
cancel.setAction(MotionEvent.ACTION_CANCEL);
dispatchTouchEvent(cancel, x, y, mMotionTarget, false);
mMotionTarget = null;
} else {
dispatchTouchEvent(event, x, y, mMotionTarget, false);
if (action == MotionEvent.ACTION_CANCEL
|| action == MotionEvent.ACTION_UP) {
mMotionTarget = null;
}
return true;
}
}
if (action == MotionEvent.ACTION_DOWN) {
// in the reverse rendering order
for (int i = getComponentCount() - 1; i >= 0; --i) {
GLView component = getComponent(i);
if (component.getVisibility() != GLView.VISIBLE) continue;
if (dispatchTouchEvent(event, x, y, component, true)) {
mMotionTarget = component;
return true;
}
}
}
return onTouch(event);
}
/**
* Called by {@link GLRootView#dispatchSaveInstanceState(SparseArray)} to
* store this GlView hierarchy's frozen state into the given container.
*
* @param container The SparseArray in which to save the view's state.
*/
public void saveHierarchyState(SparseArray<Parcelable> container) {
dispatchSaveInstanceState(container);
}
protected void dispatchSaveInstanceState(SparseArray<Parcelable> container) {
if (mID != NO_ID) {
Parcelable state = onSaveInstanceState();
if (state != null) {
container.put(mID, state);
}
}
for (int i = 0, n = getComponentCount(); i < n; ++i) {
getComponent(i).dispatchSaveInstanceState(container);
}
}
protected Parcelable onSaveInstanceState() {
return null;
}
/**
* Called by {@link GLRootView#dispatchRestoreInstanceState(SparseArray)} to
* restore this view hierarchy's frozen state from the given container.
*
* @param container The SparseArray which holds previously frozen states.
*/
public void restoreHierarchyState(SparseArray<Parcelable> container) {
dispatchRestoreInstanceState(container);
}
protected void dispatchRestoreInstanceState(SparseArray<Parcelable> container) {
if (mID != NO_ID) {
Parcelable state = container.get(mID);
if (state != null) {
onRestoreInstanceState(state);
}
}
for (int i = 0, n = getComponentCount(); i < n; ++i) {
getComponent(i).dispatchRestoreInstanceState(container);
}
}
protected void onRestoreInstanceState(Parcelable state) {
if (state != null) {
throw new IllegalArgumentException("Please override onRestoreInstanceState");
}
}
public void setPaddings(int paddingLeft, int paddingTop, int paddingRight, int paddingBottom) {
mPaddings.set(paddingLeft, paddingTop, paddingRight, paddingBottom);
}
public void setPaddingLeft(int paddingLeft) {
mPaddings.left = paddingLeft;
}
public void setPaddingTop(int paddingTop) {
mPaddings.top = paddingTop;
}
public void setPaddingRight(int paddingRight) {
mPaddings.right = paddingRight;
}
public void setPaddingBottom(int paddingBottom) {
mPaddings.bottom = paddingBottom;
}
public Rect getPaddings() {
return mPaddings;
}
// There is a little different between GLView.layout() and View.layout().
// onMeasure() is not called in GLView.layout().
// For the content view of GLRootView, GLView.measure() is called before GLView.layout().
// For component, GLView.measure() in called in parent's GLView.onMeasure().
// So it is OK.
public void layout(int left, int top, int right, int bottom) {
boolean sizeChanged = setBounds(left, top, right, bottom);
final boolean forceLayout = (mViewFlags & FLAG_LAYOUT_REQUESTED) == FLAG_LAYOUT_REQUESTED;
if (sizeChanged || forceLayout) {
onLayout(sizeChanged, left, top, right, bottom);
}
mViewFlags &= ~FLAG_LAYOUT_REQUESTED;
}
private boolean setBounds(int left, int top, int right, int bottom) {
Rect bounds = mBounds;
int oldW = bounds.right - bounds.left;
int oldH = bounds.bottom - bounds.top;
int newW = right - left;
int newH = bottom - top;
boolean sizeChanged = oldW != newW || oldH != newH;
bounds.set(left, top, right, bottom);
if (sizeChanged) {
onSizeChanged(newW, newH, oldW, oldH);
}
// Update position in root
int[] position = mPosition;
int oldX = position[0];
int oldY = position[1];
if (mParent == null) {
position[0] = 0;
position[1] = 0;
} else {
mParent.getPositionInRoot(position);
// Apply offset in parent
position[0] += left;
position[1] += top;
}
int x = position[0];
int y = position[1];
if (x != oldX || y != oldY) {
onPositionInRootChanged(x, y, oldX, oldY);
}
return sizeChanged;
}
protected void onSizeChanged(int newW, int newH, int oldW, int oldH) {
}
protected void onPositionInRootChanged(int x, int y, int oldX, int oldY) {
}
public void getPositionInRoot(int[] position) {
AssertUtils.assertEquals("position should be 2 length int array", position.length, 2);
position[0] = mPosition[0];
position[1] = mPosition[1];
}
/**
* Get the rect of the view clipped by root rect
* @param rect the result
*/
public void getValidRect(Rect rect) {
GLRoot root = mRoot;
if (root == null) {
rect.setEmpty();
return;
}
int x = mPosition[0];
int y = mPosition[1];
rect.set(x, y, x + getWidth(), y + getHeight());
if (!rect.intersect(0, 0, root.getWidth(), root.getHeight())) {
rect.setEmpty();
return;
}
rect.offset(-x, -y);
}
public void measure(int widthSpec, int heightSpec) {
final boolean forceLayout = (mViewFlags & FLAG_LAYOUT_REQUESTED) == FLAG_LAYOUT_REQUESTED;
final boolean isExactly = MeasureSpec.getMode(widthSpec) == MeasureSpec.EXACTLY &&
MeasureSpec.getMode(heightSpec) == MeasureSpec.EXACTLY;
final boolean matchingSize = isExactly &&
getMeasuredWidth() == MeasureSpec.getSize(widthSpec) &&
getMeasuredHeight() == MeasureSpec.getSize(heightSpec);
if (forceLayout || !matchingSize &&
(widthSpec != mLastWidthSpec ||
heightSpec != mLastHeightSpec)) {
mLastWidthSpec = widthSpec;
mLastHeightSpec = heightSpec;
mViewFlags &= ~FLAG_SET_MEASURED_SIZE;
onMeasure(widthSpec, heightSpec);
if ((mViewFlags & FLAG_SET_MEASURED_SIZE) == 0) {
throw new IllegalStateException(getClass().getName()
+ " should call setMeasuredSize() in onMeasure()");
}
}
}
protected void onMeasure(int widthSpec, int heightSpec) {
setMeasuredSize(getDefaultSize(getSuggestedMinimumWidth(), widthSpec),
getDefaultSize(getSuggestedMinimumHeight(), heightSpec));
}
protected int getSuggestedMinimumHeight() {
return getMinimumHeight() + mPaddings.top + mPaddings.bottom;
}
protected int getSuggestedMinimumWidth() {
return getMinimumWidth() + mPaddings.left + mPaddings.right;
}
/**
* Returns the minimum height of the view.
*
* @return the minimum height the view will try to be.
*
* @see #setMinimumHeight(int)
*/
public int getMinimumHeight() {
return mMinHeight;
}
/**
* Sets the minimum height of the view. It is not guaranteed the view will
* be able to achieve this minimum height (for example, if its parent layout
* constrains it with less available height).
*
* @param minHeight The minimum height the view will try to be.
*
* @see #getMinimumHeight()
*/
public void setMinimumHeight(int minHeight) {
mMinHeight = minHeight;
requestLayout();
}
/**
* Returns the minimum width of the view.
*
* @return the minimum width the view will try to be.
*
* @see #setMinimumWidth(int)
*/
public int getMinimumWidth() {
return mMinWidth;
}
/**
* Sets the minimum width of the view. It is not guaranteed the view will
* be able to achieve this minimum width (for example, if its parent layout
* constrains it with less available width).
*
* @param minWidth The minimum width the view will try to be.
*
* @see #getMinimumWidth()
*/
public void setMinimumWidth(int minWidth) {
mMinWidth = minWidth;
requestLayout();
}
protected void setMeasuredSize(int width, int height) {
mViewFlags |= FLAG_SET_MEASURED_SIZE;
mMeasuredWidth = width;
mMeasuredHeight = height;
}
public int getMeasuredWidth() {
return mMeasuredWidth;
}
public int getMeasuredHeight() {
return mMeasuredHeight;
}
protected void onLayout(
boolean changeSize, int left, int top, int right, int bottom) {
}
/**
* Utility to return a default begin. Uses the supplied number as left or top.
*
* @param size the parent size
* @param specSize the child size
* @param paddingBeign the padding begin
* @param paddingFinish the padding finish
* @param position the {@link Gravity#POSITION_BEGIN} or {@link Gravity#POSITION_CENTER} or
* {@link Gravity#POSITION_FINISH}
* @return the begin size
*/
public static int getDefaultBegin(int size, int specSize, int paddingBeign, int paddingFinish,
@Gravity.PositionMode int position) {
if (position == Gravity.POSITION_FINISH) {
return size - paddingFinish - specSize;
} else if (position == Gravity.POSITION_CENTER) {
return ((size - paddingBeign - paddingFinish) / 2) - (specSize / 2) + paddingBeign;
} else {
return paddingBeign;
}
}
/**
* Utility to return a default size. Uses the supplied size if the
* MeasureSpec imposed no constraints. Will get larger if allowed
* by the MeasureSpec.
*
* It works differently from {@link android.view.View#getDefaultSize(int, int)}.
* For {@link MeasureSpec#AT_MOST}, size is suggestion size
* if it is small then spec size or it is not zero,
* otherwise spec size.
*
* @param size Default size for this view
* @param measureSpec Constraints imposed by the parent
* @return The size this view should be.
*/
public static int getDefaultSize(int size, int measureSpec) {
int result = size;
int specMode = MeasureSpec.getMode(measureSpec);
int specSize = MeasureSpec.getSize(measureSpec);
switch (specMode) {
case MeasureSpec.UNSPECIFIED:
result = size;
break;
case MeasureSpec.EXACTLY:
result = specSize;
break;
case MeasureSpec.AT_MOST:
return size == 0 ? specSize : Math.min(size, specSize);
}
return result;
}
/**
* Utility to return a max size. Uses the supplied size if the
* MeasureSpec imposed no constraints. Will get larger if allowed
* by the MeasureSpec.
*
* It works the same as {@link android.view.View#getDefaultSize(int, int)}.
*
* @param size Default size for this view
* @param measureSpec Constraints imposed by the parent
* @return The size this view should be.
*/
public static int getMaxSize(int size, int measureSpec) {
int result = size;
int specMode = MeasureSpec.getMode(measureSpec);
int specSize = MeasureSpec.getSize(measureSpec);
switch (specMode) {
case MeasureSpec.UNSPECIFIED:
result = size;
break;
case MeasureSpec.EXACTLY:
case MeasureSpec.AT_MOST:
result = specSize;
break;
}
return result;
}
public static int getComponentSpec(int spec, int childSize) {
int specMode = MeasureSpec.getMode(spec);
int specSize = MeasureSpec.getSize(spec);
int size = Math.max(0, specSize);
int resultSize = 0;
int resultMode = 0;
switch (specMode) {
// Parent has imposed an exact size on us
case MeasureSpec.EXACTLY:
if (childSize >= 0) {
resultSize = childSize;
resultMode = MeasureSpec.EXACTLY;
} else if (childSize == LayoutParams.MATCH_PARENT) {
// Child wants to be our size. So be it.
resultSize = size;
resultMode = MeasureSpec.EXACTLY;
} else if (childSize == LayoutParams.WRAP_CONTENT) {
// Child wants to determine its own size. It can't be
// bigger than us.
resultSize = size;
resultMode = MeasureSpec.AT_MOST;
}
break;
// Parent has imposed a maximum size on us
case MeasureSpec.AT_MOST:
if (childSize >= 0) {
// Child wants a specific size... so be it
resultSize = childSize;
resultMode = MeasureSpec.EXACTLY;
} else if (childSize == LayoutParams.MATCH_PARENT) {
// Child wants to be our size, but our size is not fixed.
// Constrain child to not be bigger than us.
resultSize = size;
resultMode = MeasureSpec.AT_MOST;
} else if (childSize == LayoutParams.WRAP_CONTENT) {
// Child wants to determine its own size. It can't be
// bigger than us.
resultSize = size;
resultMode = MeasureSpec.AT_MOST;
}
break;
// Parent asked to see how big we want to be
case MeasureSpec.UNSPECIFIED:
if (childSize >= 0) {
// Child wants a specific size... let him have it
resultSize = childSize;
resultMode = MeasureSpec.EXACTLY;
} else if (childSize == LayoutParams.MATCH_PARENT) {
// Child wants to be our size... find out how big it should
// be
resultSize = 0;
resultMode = MeasureSpec.UNSPECIFIED;
} else if (childSize == LayoutParams.WRAP_CONTENT) {
// Child wants to determine its own size.... find out how
// big it should be
resultSize = 0;
resultMode = MeasureSpec.UNSPECIFIED;
}
break;
}
return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
}
public static void measureComponent(GLView component, int widthSpec, int heightSpec) {
final LayoutParams lp = component.getLayoutParams();
final int componentWidthSpec = getComponentSpec(widthSpec, lp.width);
final int componentHeightSpec = getComponentSpec(heightSpec, lp.height);
component.measure(componentWidthSpec, componentHeightSpec);
}
public static void measureAllComponents(GLView parent, int widthSpec, int heightSpec) {
for (int i = 0, n = parent.getComponentCount(); i < n; i++) {
GLView component = parent.getComponent(i);
measureComponent(component, widthSpec, heightSpec);
}
}
public LayoutParams getLayoutParams() {
return mLayoutParams;
}
public void setLayoutParams(LayoutParams params) {
if (params == null) {
if (mParent == null) {
throw new NullPointerException("Layout parameters cannot be null");
} else {
params = mParent.generateDefaultLayoutParams();
}
}
mLayoutParams = params;
requestLayout();
}
protected boolean checkLayoutParams(LayoutParams p) {
return p != null;
}
protected LayoutParams generateDefaultLayoutParams() {
return new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);
}
protected LayoutParams generateLayoutParams(LayoutParams p) {
return new LayoutParams(p);
}
/**
* Gets the bounds of the given descendant that relative to this view.
*/
public boolean getBoundsOf(GLView descendant, Rect out) {
int xOffset = 0;
int yOffset = 0;
GLView view = descendant;
while (view != this) {
if (view == null) return false;
Rect bounds = view.mBounds;
xOffset += bounds.left;
yOffset += bounds.top;
view = view.mParent;
}
out.set(xOffset, yOffset, xOffset + descendant.getWidth(),
yOffset + descendant.getHeight());
return true;
}
protected void onVisibilityChanged(int visibility) {
for (int i = 0, n = getComponentCount(); i < n; ++i) {
GLView child = getComponent(i);
if (child.getVisibility() == GLView.VISIBLE) {
child.onVisibilityChanged(visibility);
}
}
}
public void onAttachToRoot(GLRoot root) {
mRoot = root;
for (int i = 0, n = getComponentCount(); i < n; ++i) {
getComponent(i).onAttachToRoot(root);
}
}
public void onDetachFromRoot() {
for (int i = 0, n = getComponentCount(); i < n; ++i) {
getComponent(i).onDetachFromRoot();
}
mRoot = null;
}
public void lockRendering() {
if (mRoot != null) {
mRoot.lockRenderThread();
}
}
public void unlockRendering() {
if (mRoot != null) {
mRoot.unlockRenderThread();
}
}
// This is for debugging only.
// Dump the view hierarchy into log.
void dumpTree(String prefix) {
Log.d(TAG, prefix + getClass().getSimpleName());
for (int i = 0, n = getComponentCount(); i < n; ++i) {
getComponent(i).dumpTree(prefix + "....");
}
}
/**
* A MeasureSpec encapsulates the layout requirements passed from parent to child.
* Each MeasureSpec represents a requirement for either the width or the height.
* A MeasureSpec is comprised of a size and a mode. There are three possible
* modes:
* <dl>
* <dt>UNSPECIFIED</dt>
* <dd>
* The parent has not imposed any constraint on the child. It can be whatever size
* it wants.
* </dd>
*
* <dt>EXACTLY</dt>
* <dd>
* The parent has determined an exact size for the child. The child is going to be
* given those bounds regardless of how big it wants to be.
* </dd>
*
* <dt>AT_MOST</dt>
* <dd>
* The child can be as large as it wants up to the specified size.
* </dd>
* </dl>
*
* MeasureSpecs are implemented as ints to reduce object allocation. This class
* is provided to pack and unpack the &lt;size, mode&gt; tuple into the int.
*/
public static class MeasureSpec {
private static final int MODE_SHIFT = 30;
private static final int MODE_MASK = 0x3 << MODE_SHIFT;
/**
* Measure specification mode: The parent has not imposed any constraint
* on the child. It can be whatever size it wants.
*/
public static final int UNSPECIFIED = 0 << MODE_SHIFT;
/**
* Measure specification mode: The parent has determined an exact size
* for the child. The child is going to be given those bounds regardless
* of how big it wants to be.
*/
public static final int EXACTLY = 1 << MODE_SHIFT;
/**
* Measure specification mode: The child can be as large as it wants up
* to the specified size.
*/
public static final int AT_MOST = 2 << MODE_SHIFT;
/**
* The max size allowed
*/
public static final int MAX_SIZE = ~MODE_MASK;
/**
* Creates a measure specification based on the supplied size and mode.
*
* The mode must always be one of the following:
* <ul>
* <li>{@link #UNSPECIFIED}</li>
* <li>{@link #EXACTLY}</li>
* <li>{@link #AT_MOST}</li>
* </ul>
*
* <p><strong>Note:</strong> On API level 17 and lower, makeMeasureSpec's
* implementation was such that the order of arguments did not matter
* and overflow in either value could impact the resulting MeasureSpec.
* {@link android.widget.RelativeLayout} was affected by this bug.
* Apps targeting API levels greater than 17 will get the fixed, more strict
* behavior.</p>
*
* @param size the size of the measure specification
* @param mode the mode of the measure specification
* @return the measure specification based on size and mode
*/
public static int makeMeasureSpec(int size, int mode) {
return (size & ~MODE_MASK) | (mode & MODE_MASK);
}
/**
* Extracts the mode from the supplied measure specification.
*
* @param measureSpec the measure specification to extract the mode from
* @return {@link #UNSPECIFIED},
* {@link #AT_MOST} or
* {@link #EXACTLY}
*/
public static int getMode(int measureSpec) {
return (measureSpec & MODE_MASK);
}
/**
* Extracts the size from the supplied measure specification.
*
* @param measureSpec the measure specification to extract the size from
* @return the size in pixels defined in the supplied measure specification
*/
public static int getSize(int measureSpec) {
return (measureSpec & ~MODE_MASK);
}
/**
* Returns a String representation of the specified measure
* specification.
*
* @param measureSpec the measure specification to convert to a String
* @return a String with the following format: "MeasureSpec: MODE SIZE"
*/
public static String toString(int measureSpec) {
int mode = getMode(measureSpec);
int size = getSize(measureSpec);
StringBuilder sb = new StringBuilder("MeasureSpec: ");
if (mode == UNSPECIFIED)
sb.append("UNSPECIFIED ");
else if (mode == EXACTLY)
sb.append("EXACTLY ");
else if (mode == AT_MOST)
sb.append("AT_MOST ");
else
sb.append(mode).append(" ");
sb.append(size);
return sb.toString();
}
}
/**
* The LayoutParams look like {@link android.view.ViewGroup.LayoutParams},
* and work like it.
*/
public static class LayoutParams {
/**
* Special value for the height or width requested by a View.
* MATCH_PARENT means that the view wants to be as big as its parent,
* minus the parent's padding, if any. Introduced in API Level 8.
*/
public static final int MATCH_PARENT = -1;
/**
* Special value for the height or width requested by a View.
* WRAP_CONTENT means that the view wants to be just large enough to fit
* its own internal content, taking its own padding into account.
*/
public static final int WRAP_CONTENT = -2;
/**
* Information about how wide the view wants to be. Can be one of the
* constants {@link #MATCH_PARENT} or {@link #WRAP_CONTENT} or an exact size.
*/
public int width;
/**
* Information about how tall the view wants to be. Can be one of the
* constants {@link #MATCH_PARENT} or {@link #WRAP_CONTENT} or an exact size.
*/
public int height;
/**
* Creates a new set of layout parameters with the specified width
* and height.
*
* @param width the width, either {@link #WRAP_CONTENT},
* {@link #MATCH_PARENT}, or a fixed size in pixels
* @param height the height, either {@link #WRAP_CONTENT},
* {@link #MATCH_PARENT}, or a fixed size in pixels
*/
public LayoutParams(int width, int height) {
this.width = width;
this.height = height;
}
/**
* Copy constructor. Clones the width and height values of the source.
*
* @param source The layout params to copy from.
*/
public LayoutParams(LayoutParams source) {
this.width = source.width;
this.height = source.height;
}
}
/**
* LayoutParams with gravity inside
*/
public static class GravityLayoutParams extends LayoutParams {
public int gravity = Gravity.NO_GRAVITY;
public GravityLayoutParams(GLView.LayoutParams source) {
super(source);
if (source instanceof GravityLayoutParams) {
gravity = ((GravityLayoutParams) source).gravity;
}
}
public GravityLayoutParams(int width, int height) {
super(width, height);
}
}
}