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