/* * 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.android.gallery3d.ui; import android.graphics.Rect; import android.os.SystemClock; import android.view.MotionEvent; import com.android.gallery3d.anim.CanvasAnimation; import com.android.gallery3d.anim.StateTransitionAnimation; import com.android.gallery3d.common.Utils; import com.android.gallery3d.glrenderer.GLCanvas; 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 { private static final String TAG = "GLView"; 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; public interface OnClickListener { void onClick(GLView v); } protected final Rect mBounds = new Rect(); 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; private float [] mBackgroundColor; private StateTransitionAnimation mTransition; public void startAnimation(CanvasAnimation animation) { GLRoot root = getGLRoot(); if (root == null) throw new IllegalStateException(); mAnimation = animation; if (mAnimation != null) { mAnimation.start(); root.registerLaunchedAnimation(mAnimation); } invalidate(); } // Sets the visiblity 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) { Utils.assertTrue(mParent == null && mRoot == null); onAttachToRoot(root); } // This should only be called on the content pane (the topmost GLView). public void detachFromRoot() { Utils.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) { // Make sure the component doesn't have a parent currently. if (component.mParent != null) throw new IllegalStateException(); // Build parent-child links if (mComponents == null) { mComponents = new ArrayList(); } mComponents.add(component); component.mParent = this; // 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; } // 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; } public Rect bounds() { return mBounds; } public int getWidth() { return mBounds.right - mBounds.left; } public int getHeight() { return mBounds.bottom - mBounds.top; } 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; mLastHeightSpec = -1; mLastWidthSpec = -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) { boolean transitionActive = false; if (mTransition != null && mTransition.calculate(AnimationTime.get())) { invalidate(); transitionActive = mTransition.isActive(); } renderBackground(canvas); canvas.save(); if (transitionActive) { mTransition.applyContentTransform(this, canvas); } for (int i = 0, n = getComponentCount(); i < n; ++i) { renderChild(canvas, getComponent(i)); } canvas.restore(); if (transitionActive) { mTransition.applyOverlay(this, canvas); } } public void setIntroAnimation(StateTransitionAnimation intro) { mTransition = intro; if (mTransition != null) mTransition.start(); } public float [] getBackgroundColor() { return mBackgroundColor; } public void setBackgroundColor(float [] color) { mBackgroundColor = color; } protected void renderBackground(GLCanvas view) { if (mBackgroundColor != null) { view.clearBuffer(mBackgroundColor); } if (mTransition != null && mTransition.isActive()) { mTransition.applyBackground(this, view); return; } } 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) { return false; } 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); } public Rect getPaddings() { return mPaddings; } public void layout(int left, int top, int right, int bottom) { boolean sizeChanged = setBounds(left, top, right, bottom); mViewFlags &= ~FLAG_LAYOUT_REQUESTED; // We call onLayout no matter sizeChanged is true or not because the // orientation may change without changing the size of the View (for // example, rotate the device by 180 degrees), and we want to handle // orientation change in onLayout. onLayout(sizeChanged, left, top, right, bottom); } private boolean setBounds(int left, int top, int right, int bottom) { boolean sizeChanged = (right - left) != (mBounds.right - mBounds.left) || (bottom - top) != (mBounds.bottom - mBounds.top); mBounds.set(left, top, right, bottom); return sizeChanged; } public void measure(int widthSpec, int heightSpec) { if (widthSpec == mLastWidthSpec && heightSpec == mLastHeightSpec && (mViewFlags & FLAG_LAYOUT_REQUESTED) == 0) { return; } 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) { } 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) { } /** * 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); } } } protected void onAttachToRoot(GLRoot root) { mRoot = root; for (int i = 0, n = getComponentCount(); i < n; ++i) { getComponent(i).onAttachToRoot(root); } } protected 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 + "...."); } } }