/* * 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 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 container) { dispatchSaveInstanceState(container); } protected void dispatchSaveInstanceState(SparseArray 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 container) { dispatchRestoreInstanceState(container); } protected void dispatchRestoreInstanceState(SparseArray 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: *
*
UNSPECIFIED
*
* The parent has not imposed any constraint on the child. It can be whatever size * it wants. *
* *
EXACTLY
*
* 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. *
* *
AT_MOST
*
* The child can be as large as it wants up to the specified size. *
*
* * MeasureSpecs are implemented as ints to reduce object allocation. This class * is provided to pack and unpack the <size, mode> 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: *
    *
  • {@link #UNSPECIFIED}
  • *
  • {@link #EXACTLY}
  • *
  • {@link #AT_MOST}
  • *
* *

Note: 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.

* * @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); } } }