Home | History | Annotate | Download | only in ui
      1 /*
      2  * Copyright (C) 2010 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.gallery3d.ui;
     18 
     19 import android.graphics.Rect;
     20 import android.os.SystemClock;
     21 import android.view.MotionEvent;
     22 
     23 import com.android.gallery3d.anim.CanvasAnimation;
     24 import com.android.gallery3d.anim.StateTransitionAnimation;
     25 import com.android.gallery3d.common.Utils;
     26 
     27 import java.util.ArrayList;
     28 
     29 // GLView is a UI component. It can render to a GLCanvas and accept touch
     30 // events. A GLView may have zero or more child GLView and they form a tree
     31 // structure. The rendering and event handling will pass through the tree
     32 // structure.
     33 //
     34 // A GLView tree should be attached to a GLRoot before event dispatching and
     35 // rendering happens. GLView asks GLRoot to re-render or re-layout the
     36 // GLView hierarchy using requestRender() and requestLayoutContentPane().
     37 //
     38 // The render() method is called in a separate thread. Before calling
     39 // dispatchTouchEvent() and layout(), GLRoot acquires a lock to avoid the
     40 // rendering thread running at the same time. If there are other entry points
     41 // from main thread (like a Handler) in your GLView, you need to call
     42 // lockRendering() if the rendering thread should not run at the same time.
     43 //
     44 public class GLView {
     45     private static final String TAG = "GLView";
     46 
     47     public static final int VISIBLE = 0;
     48     public static final int INVISIBLE = 1;
     49 
     50     private static final int FLAG_INVISIBLE = 1;
     51     private static final int FLAG_SET_MEASURED_SIZE = 2;
     52     private static final int FLAG_LAYOUT_REQUESTED = 4;
     53 
     54     public interface OnClickListener {
     55         void onClick(GLView v);
     56     }
     57 
     58     protected final Rect mBounds = new Rect();
     59     protected final Rect mPaddings = new Rect();
     60 
     61     private GLRoot mRoot;
     62     protected GLView mParent;
     63     private ArrayList<GLView> mComponents;
     64     private GLView mMotionTarget;
     65 
     66     private CanvasAnimation mAnimation;
     67 
     68     private int mViewFlags = 0;
     69 
     70     protected int mMeasuredWidth = 0;
     71     protected int mMeasuredHeight = 0;
     72 
     73     private int mLastWidthSpec = -1;
     74     private int mLastHeightSpec = -1;
     75 
     76     protected int mScrollY = 0;
     77     protected int mScrollX = 0;
     78     protected int mScrollHeight = 0;
     79     protected int mScrollWidth = 0;
     80 
     81     private float [] mBackgroundColor;
     82     private StateTransitionAnimation mTransition;
     83 
     84     public void startAnimation(CanvasAnimation animation) {
     85         GLRoot root = getGLRoot();
     86         if (root == null) throw new IllegalStateException();
     87         mAnimation = animation;
     88         if (mAnimation != null) {
     89             mAnimation.start();
     90             root.registerLaunchedAnimation(mAnimation);
     91         }
     92         invalidate();
     93     }
     94 
     95     // Sets the visiblity of this GLView (either GLView.VISIBLE or
     96     // GLView.INVISIBLE).
     97     public void setVisibility(int visibility) {
     98         if (visibility == getVisibility()) return;
     99         if (visibility == VISIBLE) {
    100             mViewFlags &= ~FLAG_INVISIBLE;
    101         } else {
    102             mViewFlags |= FLAG_INVISIBLE;
    103         }
    104         onVisibilityChanged(visibility);
    105         invalidate();
    106     }
    107 
    108     // Returns GLView.VISIBLE or GLView.INVISIBLE
    109     public int getVisibility() {
    110         return (mViewFlags & FLAG_INVISIBLE) == 0 ? VISIBLE : INVISIBLE;
    111     }
    112 
    113     // This should only be called on the content pane (the topmost GLView).
    114     public void attachToRoot(GLRoot root) {
    115         Utils.assertTrue(mParent == null && mRoot == null);
    116         onAttachToRoot(root);
    117     }
    118 
    119     // This should only be called on the content pane (the topmost GLView).
    120     public void detachFromRoot() {
    121         Utils.assertTrue(mParent == null && mRoot != null);
    122         onDetachFromRoot();
    123     }
    124 
    125     // Returns the number of children of the GLView.
    126     public int getComponentCount() {
    127         return mComponents == null ? 0 : mComponents.size();
    128     }
    129 
    130     // Returns the children for the given index.
    131     public GLView getComponent(int index) {
    132         if (mComponents == null) {
    133             throw new ArrayIndexOutOfBoundsException(index);
    134         }
    135         return mComponents.get(index);
    136     }
    137 
    138     // Adds a child to this GLView.
    139     public void addComponent(GLView component) {
    140         // Make sure the component doesn't have a parent currently.
    141         if (component.mParent != null) throw new IllegalStateException();
    142 
    143         // Build parent-child links
    144         if (mComponents == null) {
    145             mComponents = new ArrayList<GLView>();
    146         }
    147         mComponents.add(component);
    148         component.mParent = this;
    149 
    150         // If this is added after we have a root, tell the component.
    151         if (mRoot != null) {
    152             component.onAttachToRoot(mRoot);
    153         }
    154     }
    155 
    156     // Removes a child from this GLView.
    157     public boolean removeComponent(GLView component) {
    158         if (mComponents == null) return false;
    159         if (mComponents.remove(component)) {
    160             removeOneComponent(component);
    161             return true;
    162         }
    163         return false;
    164     }
    165 
    166     // Removes all children of this GLView.
    167     public void removeAllComponents() {
    168         for (int i = 0, n = mComponents.size(); i < n; ++i) {
    169             removeOneComponent(mComponents.get(i));
    170         }
    171         mComponents.clear();
    172     }
    173 
    174     private void removeOneComponent(GLView component) {
    175         if (mMotionTarget == component) {
    176             long now = SystemClock.uptimeMillis();
    177             MotionEvent cancelEvent = MotionEvent.obtain(
    178                     now, now, MotionEvent.ACTION_CANCEL, 0, 0, 0);
    179             dispatchTouchEvent(cancelEvent);
    180             cancelEvent.recycle();
    181         }
    182         component.onDetachFromRoot();
    183         component.mParent = null;
    184     }
    185 
    186     public Rect bounds() {
    187         return mBounds;
    188     }
    189 
    190     public int getWidth() {
    191         return mBounds.right - mBounds.left;
    192     }
    193 
    194     public int getHeight() {
    195         return mBounds.bottom - mBounds.top;
    196     }
    197 
    198     public GLRoot getGLRoot() {
    199         return mRoot;
    200     }
    201 
    202     // Request re-rendering of the view hierarchy.
    203     // This is used for animation or when the contents changed.
    204     public void invalidate() {
    205         GLRoot root = getGLRoot();
    206         if (root != null) root.requestRender();
    207     }
    208 
    209     // Request re-layout of the view hierarchy.
    210     public void requestLayout() {
    211         mViewFlags |= FLAG_LAYOUT_REQUESTED;
    212         mLastHeightSpec = -1;
    213         mLastWidthSpec = -1;
    214         if (mParent != null) {
    215             mParent.requestLayout();
    216         } else {
    217             // Is this a content pane ?
    218             GLRoot root = getGLRoot();
    219             if (root != null) root.requestLayoutContentPane();
    220         }
    221     }
    222 
    223     protected void render(GLCanvas canvas) {
    224         boolean transitionActive = false;
    225         if (mTransition != null && mTransition.calculate(AnimationTime.get())) {
    226             invalidate();
    227             transitionActive = mTransition.isActive();
    228         }
    229         renderBackground(canvas);
    230         canvas.save();
    231         if (transitionActive) {
    232             mTransition.applyContentTransform(this, canvas);
    233         }
    234         for (int i = 0, n = getComponentCount(); i < n; ++i) {
    235             renderChild(canvas, getComponent(i));
    236         }
    237         canvas.restore();
    238         if (transitionActive) {
    239             mTransition.applyOverlay(this, canvas);
    240         }
    241     }
    242 
    243     public void setIntroAnimation(StateTransitionAnimation intro) {
    244         mTransition = intro;
    245         if (mTransition != null) mTransition.start();
    246     }
    247 
    248     public float [] getBackgroundColor() {
    249         return mBackgroundColor;
    250     }
    251 
    252     public void setBackgroundColor(float [] color) {
    253         mBackgroundColor = color;
    254     }
    255 
    256     protected void renderBackground(GLCanvas view) {
    257         if (mBackgroundColor != null) {
    258             view.clearBuffer(mBackgroundColor);
    259         }
    260         if (mTransition != null && mTransition.isActive()) {
    261             mTransition.applyBackground(this, view);
    262             return;
    263         }
    264     }
    265 
    266     protected void renderChild(GLCanvas canvas, GLView component) {
    267         if (component.getVisibility() != GLView.VISIBLE
    268                 && component.mAnimation == null) return;
    269 
    270         int xoffset = component.mBounds.left - mScrollX;
    271         int yoffset = component.mBounds.top - mScrollY;
    272 
    273         canvas.translate(xoffset, yoffset);
    274 
    275         CanvasAnimation anim = component.mAnimation;
    276         if (anim != null) {
    277             canvas.save(anim.getCanvasSaveFlags());
    278             if (anim.calculate(AnimationTime.get())) {
    279                 invalidate();
    280             } else {
    281                 component.mAnimation = null;
    282             }
    283             anim.apply(canvas);
    284         }
    285         component.render(canvas);
    286         if (anim != null) canvas.restore();
    287         canvas.translate(-xoffset, -yoffset);
    288     }
    289 
    290     protected boolean onTouch(MotionEvent event) {
    291         return false;
    292     }
    293 
    294     protected boolean dispatchTouchEvent(MotionEvent event,
    295             int x, int y, GLView component, boolean checkBounds) {
    296         Rect rect = component.mBounds;
    297         int left = rect.left;
    298         int top = rect.top;
    299         if (!checkBounds || rect.contains(x, y)) {
    300             event.offsetLocation(-left, -top);
    301             if (component.dispatchTouchEvent(event)) {
    302                 event.offsetLocation(left, top);
    303                 return true;
    304             }
    305             event.offsetLocation(left, top);
    306         }
    307         return false;
    308     }
    309 
    310     protected boolean dispatchTouchEvent(MotionEvent event) {
    311         int x = (int) event.getX();
    312         int y = (int) event.getY();
    313         int action = event.getAction();
    314         if (mMotionTarget != null) {
    315             if (action == MotionEvent.ACTION_DOWN) {
    316                 MotionEvent cancel = MotionEvent.obtain(event);
    317                 cancel.setAction(MotionEvent.ACTION_CANCEL);
    318                 dispatchTouchEvent(cancel, x, y, mMotionTarget, false);
    319                 mMotionTarget = null;
    320             } else {
    321                 dispatchTouchEvent(event, x, y, mMotionTarget, false);
    322                 if (action == MotionEvent.ACTION_CANCEL
    323                         || action == MotionEvent.ACTION_UP) {
    324                     mMotionTarget = null;
    325                 }
    326                 return true;
    327             }
    328         }
    329         if (action == MotionEvent.ACTION_DOWN) {
    330             // in the reverse rendering order
    331             for (int i = getComponentCount() - 1; i >= 0; --i) {
    332                 GLView component = getComponent(i);
    333                 if (component.getVisibility() != GLView.VISIBLE) continue;
    334                 if (dispatchTouchEvent(event, x, y, component, true)) {
    335                     mMotionTarget = component;
    336                     return true;
    337                 }
    338             }
    339         }
    340         return onTouch(event);
    341     }
    342 
    343     public Rect getPaddings() {
    344         return mPaddings;
    345     }
    346 
    347     public void layout(int left, int top, int right, int bottom) {
    348         boolean sizeChanged = setBounds(left, top, right, bottom);
    349         mViewFlags &= ~FLAG_LAYOUT_REQUESTED;
    350         // We call onLayout no matter sizeChanged is true or not because the
    351         // orientation may change without changing the size of the View (for
    352         // example, rotate the device by 180 degrees), and we want to handle
    353         // orientation change in onLayout.
    354         onLayout(sizeChanged, left, top, right, bottom);
    355     }
    356 
    357     private boolean setBounds(int left, int top, int right, int bottom) {
    358         boolean sizeChanged = (right - left) != (mBounds.right - mBounds.left)
    359                 || (bottom - top) != (mBounds.bottom - mBounds.top);
    360         mBounds.set(left, top, right, bottom);
    361         return sizeChanged;
    362     }
    363 
    364     public void measure(int widthSpec, int heightSpec) {
    365         if (widthSpec == mLastWidthSpec && heightSpec == mLastHeightSpec
    366                 && (mViewFlags & FLAG_LAYOUT_REQUESTED) == 0) {
    367             return;
    368         }
    369 
    370         mLastWidthSpec = widthSpec;
    371         mLastHeightSpec = heightSpec;
    372 
    373         mViewFlags &= ~FLAG_SET_MEASURED_SIZE;
    374         onMeasure(widthSpec, heightSpec);
    375         if ((mViewFlags & FLAG_SET_MEASURED_SIZE) == 0) {
    376             throw new IllegalStateException(getClass().getName()
    377                     + " should call setMeasuredSize() in onMeasure()");
    378         }
    379     }
    380 
    381     protected void onMeasure(int widthSpec, int heightSpec) {
    382     }
    383 
    384     protected void setMeasuredSize(int width, int height) {
    385         mViewFlags |= FLAG_SET_MEASURED_SIZE;
    386         mMeasuredWidth = width;
    387         mMeasuredHeight = height;
    388     }
    389 
    390     public int getMeasuredWidth() {
    391         return mMeasuredWidth;
    392     }
    393 
    394     public int getMeasuredHeight() {
    395         return mMeasuredHeight;
    396     }
    397 
    398     protected void onLayout(
    399             boolean changeSize, int left, int top, int right, int bottom) {
    400     }
    401 
    402     /**
    403      * Gets the bounds of the given descendant that relative to this view.
    404      */
    405     public boolean getBoundsOf(GLView descendant, Rect out) {
    406         int xoffset = 0;
    407         int yoffset = 0;
    408         GLView view = descendant;
    409         while (view != this) {
    410             if (view == null) return false;
    411             Rect bounds = view.mBounds;
    412             xoffset += bounds.left;
    413             yoffset += bounds.top;
    414             view = view.mParent;
    415         }
    416         out.set(xoffset, yoffset, xoffset + descendant.getWidth(),
    417                 yoffset + descendant.getHeight());
    418         return true;
    419     }
    420 
    421     protected void onVisibilityChanged(int visibility) {
    422         for (int i = 0, n = getComponentCount(); i < n; ++i) {
    423             GLView child = getComponent(i);
    424             if (child.getVisibility() == GLView.VISIBLE) {
    425                 child.onVisibilityChanged(visibility);
    426             }
    427         }
    428     }
    429 
    430     protected void onAttachToRoot(GLRoot root) {
    431         mRoot = root;
    432         for (int i = 0, n = getComponentCount(); i < n; ++i) {
    433             getComponent(i).onAttachToRoot(root);
    434         }
    435     }
    436 
    437     protected void onDetachFromRoot() {
    438         for (int i = 0, n = getComponentCount(); i < n; ++i) {
    439             getComponent(i).onDetachFromRoot();
    440         }
    441         mRoot = null;
    442     }
    443 
    444     public void lockRendering() {
    445         if (mRoot != null) {
    446             mRoot.lockRenderThread();
    447         }
    448     }
    449 
    450     public void unlockRendering() {
    451         if (mRoot != null) {
    452             mRoot.unlockRenderThread();
    453         }
    454     }
    455 
    456     // This is for debugging only.
    457     // Dump the view hierarchy into log.
    458     void dumpTree(String prefix) {
    459         Log.d(TAG, prefix + getClass().getSimpleName());
    460         for (int i = 0, n = getComponentCount(); i < n; ++i) {
    461             getComponent(i).dumpTree(prefix + "....");
    462         }
    463     }
    464 }
    465