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 import com.android.gallery3d.glrenderer.GLCanvas;
27 
28 import 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 //
45 public 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 {
onClick(GLView v)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 
startAnimation(CanvasAnimation animation)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).
setVisibility(int visibility)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
getVisibility()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).
attachToRoot(GLRoot root)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).
detachFromRoot()121     public void detachFromRoot() {
122         Utils.assertTrue(mParent == null && mRoot != null);
123         onDetachFromRoot();
124     }
125 
126     // Returns the number of children of the GLView.
getComponentCount()127     public int getComponentCount() {
128         return mComponents == null ? 0 : mComponents.size();
129     }
130 
131     // Returns the children for the given index.
getComponent(int 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.
addComponent(GLView component)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.
removeComponent(GLView component)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.
removeAllComponents()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 
removeOneComponent(GLView component)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 
bounds()187     public Rect bounds() {
188         return mBounds;
189     }
190 
getWidth()191     public int getWidth() {
192         return mBounds.right - mBounds.left;
193     }
194 
getHeight()195     public int getHeight() {
196         return mBounds.bottom - mBounds.top;
197     }
198 
getGLRoot()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.
invalidate()205     public void invalidate() {
206         GLRoot root = getGLRoot();
207         if (root != null) root.requestRender();
208     }
209 
210     // Request re-layout of the view hierarchy.
requestLayout()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 
render(GLCanvas canvas)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 
setIntroAnimation(StateTransitionAnimation intro)244     public void setIntroAnimation(StateTransitionAnimation intro) {
245         mTransition = intro;
246         if (mTransition != null) mTransition.start();
247     }
248 
getBackgroundColor()249     public float [] getBackgroundColor() {
250         return mBackgroundColor;
251     }
252 
setBackgroundColor(float [] color)253     public void setBackgroundColor(float [] color) {
254         mBackgroundColor = color;
255     }
256 
renderBackground(GLCanvas view)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 
renderChild(GLCanvas canvas, GLView component)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 
onTouch(MotionEvent event)291     protected boolean onTouch(MotionEvent event) {
292         return false;
293     }
294 
dispatchTouchEvent(MotionEvent event, int x, int y, GLView component, boolean checkBounds)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 
dispatchTouchEvent(MotionEvent event)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 
getPaddings()344     public Rect getPaddings() {
345         return mPaddings;
346     }
347 
layout(int left, int top, int right, int bottom)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 
setBounds(int left, int top, int right, int bottom)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 
measure(int widthSpec, int heightSpec)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 
onMeasure(int widthSpec, int heightSpec)382     protected void onMeasure(int widthSpec, int heightSpec) {
383     }
384 
setMeasuredSize(int width, int height)385     protected void setMeasuredSize(int width, int height) {
386         mViewFlags |= FLAG_SET_MEASURED_SIZE;
387         mMeasuredWidth = width;
388         mMeasuredHeight = height;
389     }
390 
getMeasuredWidth()391     public int getMeasuredWidth() {
392         return mMeasuredWidth;
393     }
394 
getMeasuredHeight()395     public int getMeasuredHeight() {
396         return mMeasuredHeight;
397     }
398 
onLayout( boolean changeSize, int left, int top, int right, int bottom)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      */
getBoundsOf(GLView descendant, Rect out)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 
onVisibilityChanged(int visibility)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 
onAttachToRoot(GLRoot root)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 
onDetachFromRoot()438     protected void onDetachFromRoot() {
439         for (int i = 0, n = getComponentCount(); i < n; ++i) {
440             getComponent(i).onDetachFromRoot();
441         }
442         mRoot = null;
443     }
444 
lockRendering()445     public void lockRendering() {
446         if (mRoot != null) {
447             mRoot.lockRenderThread();
448         }
449     }
450 
unlockRendering()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.
dumpTree(String prefix)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