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