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 { 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