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 27 import 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 // 44 public 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