GLView.java revision 4831c7494373c360062a7becac820eba3bc39287
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
17package com.android.gallery3d.ui;
18
19import android.graphics.Rect;
20import android.os.SystemClock;
21import android.view.MotionEvent;
22
23import com.android.gallery3d.anim.CanvasAnimation;
24import com.android.gallery3d.anim.StateTransitionAnimation;
25import com.android.gallery3d.common.Utils;
26
27import 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//
44public 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