Owen Lin | a2fba68 | 2011-08-17 22:07:43 +0800 | [diff] [blame] | 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 | |
Owen Lin | a2fba68 | 2011-08-17 22:07:43 +0800 | [diff] [blame] | 19 | import android.graphics.Rect; |
| 20 | import android.os.SystemClock; |
| 21 | import android.view.MotionEvent; |
| 22 | |
Owen Lin | 73a04ff | 2012-03-14 17:27:24 +0800 | [diff] [blame] | 23 | import com.android.gallery3d.anim.CanvasAnimation; |
| 24 | import com.android.gallery3d.common.Utils; |
| 25 | |
Owen Lin | a2fba68 | 2011-08-17 22:07:43 +0800 | [diff] [blame] | 26 | import 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 | // |
| 43 | public 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(); |
Owen Lin | a2fba68 | 2011-08-17 22:07:43 +0800 | [diff] [blame] | 79 | mAnimation = animation; |
Owen Lin | ace280a | 2011-08-30 10:38:59 +0800 | [diff] [blame] | 80 | if (mAnimation != null) { |
| 81 | mAnimation.start(); |
| 82 | root.registerLaunchedAnimation(mAnimation); |
| 83 | } |
Owen Lin | a2fba68 | 2011-08-17 22:07:43 +0800 | [diff] [blame] | 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 | |
Chih-Chung Chang | 3f43ecb | 2012-02-16 07:27:03 +0800 | [diff] [blame] | 232 | canvas.translate(xoffset, yoffset); |
Owen Lin | a2fba68 | 2011-08-17 22:07:43 +0800 | [diff] [blame] | 233 | |
| 234 | CanvasAnimation anim = component.mAnimation; |
| 235 | if (anim != null) { |
| 236 | canvas.save(anim.getCanvasSaveFlags()); |
Chih-Chung Chang | b3d0196 | 2012-02-17 10:02:27 +0800 | [diff] [blame] | 237 | if (anim.calculate(AnimationTime.get())) { |
Owen Lin | a2fba68 | 2011-08-17 22:07:43 +0800 | [diff] [blame] | 238 | invalidate(); |
| 239 | } else { |
| 240 | component.mAnimation = null; |
| 241 | } |
| 242 | anim.apply(canvas); |
| 243 | } |
| 244 | component.render(canvas); |
| 245 | if (anim != null) canvas.restore(); |
Chih-Chung Chang | 3f43ecb | 2012-02-16 07:27:03 +0800 | [diff] [blame] | 246 | canvas.translate(-xoffset, -yoffset); |
Owen Lin | a2fba68 | 2011-08-17 22:07:43 +0800 | [diff] [blame] | 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 | |
Owen Lin | a2fba68 | 2011-08-17 22:07:43 +0800 | [diff] [blame] | 306 | public void layout(int left, int top, int right, int bottom) { |
| 307 | boolean sizeChanged = setBounds(left, top, right, bottom); |
| 308 | if (sizeChanged) { |
| 309 | mViewFlags &= ~FLAG_LAYOUT_REQUESTED; |
| 310 | onLayout(true, left, top, right, bottom); |
| 311 | } else if ((mViewFlags & FLAG_LAYOUT_REQUESTED)!= 0) { |
| 312 | mViewFlags &= ~FLAG_LAYOUT_REQUESTED; |
| 313 | onLayout(false, left, top, right, bottom); |
| 314 | } |
| 315 | } |
| 316 | |
| 317 | private boolean setBounds(int left, int top, int right, int bottom) { |
| 318 | boolean sizeChanged = (right - left) != (mBounds.right - mBounds.left) |
| 319 | || (bottom - top) != (mBounds.bottom - mBounds.top); |
| 320 | mBounds.set(left, top, right, bottom); |
| 321 | return sizeChanged; |
| 322 | } |
| 323 | |
| 324 | public void measure(int widthSpec, int heightSpec) { |
| 325 | if (widthSpec == mLastWidthSpec && heightSpec == mLastHeightSpec |
| 326 | && (mViewFlags & FLAG_LAYOUT_REQUESTED) == 0) { |
| 327 | return; |
| 328 | } |
| 329 | |
| 330 | mLastWidthSpec = widthSpec; |
| 331 | mLastHeightSpec = heightSpec; |
| 332 | |
| 333 | mViewFlags &= ~FLAG_SET_MEASURED_SIZE; |
| 334 | onMeasure(widthSpec, heightSpec); |
| 335 | if ((mViewFlags & FLAG_SET_MEASURED_SIZE) == 0) { |
| 336 | throw new IllegalStateException(getClass().getName() |
| 337 | + " should call setMeasuredSize() in onMeasure()"); |
| 338 | } |
| 339 | } |
| 340 | |
| 341 | protected void onMeasure(int widthSpec, int heightSpec) { |
| 342 | } |
| 343 | |
| 344 | protected void setMeasuredSize(int width, int height) { |
| 345 | mViewFlags |= FLAG_SET_MEASURED_SIZE; |
| 346 | mMeasuredWidth = width; |
| 347 | mMeasuredHeight = height; |
| 348 | } |
| 349 | |
| 350 | public int getMeasuredWidth() { |
| 351 | return mMeasuredWidth; |
| 352 | } |
| 353 | |
| 354 | public int getMeasuredHeight() { |
| 355 | return mMeasuredHeight; |
| 356 | } |
| 357 | |
| 358 | protected void onLayout( |
| 359 | boolean changeSize, int left, int top, int right, int bottom) { |
| 360 | } |
| 361 | |
| 362 | /** |
| 363 | * Gets the bounds of the given descendant that relative to this view. |
| 364 | */ |
| 365 | public boolean getBoundsOf(GLView descendant, Rect out) { |
| 366 | int xoffset = 0; |
| 367 | int yoffset = 0; |
| 368 | GLView view = descendant; |
| 369 | while (view != this) { |
| 370 | if (view == null) return false; |
| 371 | Rect bounds = view.mBounds; |
| 372 | xoffset += bounds.left; |
| 373 | yoffset += bounds.top; |
| 374 | view = view.mParent; |
| 375 | } |
| 376 | out.set(xoffset, yoffset, xoffset + descendant.getWidth(), |
| 377 | yoffset + descendant.getHeight()); |
| 378 | return true; |
| 379 | } |
| 380 | |
| 381 | protected void onVisibilityChanged(int visibility) { |
| 382 | for (int i = 0, n = getComponentCount(); i < n; ++i) { |
| 383 | GLView child = getComponent(i); |
| 384 | if (child.getVisibility() == GLView.VISIBLE) { |
| 385 | child.onVisibilityChanged(visibility); |
| 386 | } |
| 387 | } |
| 388 | } |
| 389 | |
| 390 | protected void onAttachToRoot(GLRoot root) { |
| 391 | mRoot = root; |
| 392 | for (int i = 0, n = getComponentCount(); i < n; ++i) { |
| 393 | getComponent(i).onAttachToRoot(root); |
| 394 | } |
| 395 | } |
| 396 | |
| 397 | protected void onDetachFromRoot() { |
| 398 | for (int i = 0, n = getComponentCount(); i < n; ++i) { |
| 399 | getComponent(i).onDetachFromRoot(); |
| 400 | } |
| 401 | mRoot = null; |
| 402 | } |
| 403 | |
| 404 | public void lockRendering() { |
| 405 | if (mRoot != null) { |
| 406 | mRoot.lockRenderThread(); |
| 407 | } |
| 408 | } |
| 409 | |
| 410 | public void unlockRendering() { |
| 411 | if (mRoot != null) { |
| 412 | mRoot.unlockRenderThread(); |
| 413 | } |
| 414 | } |
| 415 | |
| 416 | // This is for debugging only. |
| 417 | // Dump the view hierarchy into log. |
| 418 | void dumpTree(String prefix) { |
| 419 | Log.d(TAG, prefix + getClass().getSimpleName()); |
| 420 | for (int i = 0, n = getComponentCount(); i < n; ++i) { |
| 421 | getComponent(i).dumpTree(prefix + "...."); |
| 422 | } |
| 423 | } |
| 424 | } |