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.camera.ui; 18 19 import static android.view.View.MeasureSpec.makeMeasureSpec; 20 21 import android.content.Context; 22 import android.graphics.Rect; 23 import android.os.Handler; 24 import android.os.Message; 25 import android.view.GestureDetector; 26 import android.view.MotionEvent; 27 import android.view.View.MeasureSpec; 28 import android.view.animation.AlphaAnimation; 29 import android.view.animation.Animation; 30 import android.view.animation.Transformation; 31 import android.widget.Scroller; 32 33 import com.android.camera.Util; 34 35 import javax.microedition.khronos.opengles.GL11; 36 37 class GLListView extends GLView { 38 @SuppressWarnings("unused") 39 private static final String TAG = "GLListView"; 40 private static final int INDEX_NONE = -1; 41 private static final int SCROLL_BAR_TIMEOUT = 2500; 42 43 private static final int HIDE_SCROLL_BAR = 1; 44 45 private Model mModel; 46 private Handler mHandler; 47 48 private int mHighlightIndex = INDEX_NONE; 49 private GLView mHighlightView; 50 51 private Texture mHighLight; 52 private NinePatchTexture mScrollbar; 53 54 private int mVisibleStart = 0; // inclusive 55 private int mVisibleEnd = 0; // exclusive 56 57 private boolean mHasMeasured = false; 58 59 private boolean mScrollBarVisible = false; 60 private Animation mScrollBarAnimation; 61 private OnItemSelectedListener mOnItemSelectedListener; 62 63 private GestureDetector mGestureDetector; 64 private final Scroller mScroller; 65 private boolean mScrollable; 66 private boolean mIsPressed = false; 67 68 static public interface Model { 69 public int size(); 70 public GLView getView(int index); 71 public boolean isSelectable(int index); 72 } 73 74 static public interface OnItemSelectedListener { 75 public void onItemSelected(GLView view, int position); 76 } 77 78 public GLListView(Context context) { 79 mScroller = new Scroller(context); 80 mHandler = new Handler() { 81 @Override 82 public void handleMessage(Message msg) { 83 GLRootView root = getGLRootView(); 84 if (root != null) { 85 synchronized (root) { 86 handleMessageLocked(msg); 87 } 88 } else { 89 handleMessageLocked(msg); 90 } 91 } 92 93 private void handleMessageLocked(Message msg) { 94 switch(msg.what) { 95 case HIDE_SCROLL_BAR: 96 setScrollBarVisible(false); 97 break; 98 } 99 } 100 }; 101 mGestureDetector = new GestureDetector( 102 context, new MyGestureListener(), mHandler); 103 } 104 105 @Override 106 protected void onVisibilityChanged(int visibility) { 107 super.onVisibilityChanged(visibility); 108 if (visibility == GLView.VISIBLE && mScrollHeight > getHeight()) { 109 setScrollBarVisible(true); 110 mHandler.sendEmptyMessageDelayed( 111 HIDE_SCROLL_BAR, SCROLL_BAR_TIMEOUT); 112 } 113 } 114 115 private void setScrollBarVisible(boolean visible) { 116 if (mScrollBarVisible == visible || mScrollbar == null) return; 117 mScrollBarVisible = visible; 118 if (!visible) { 119 mScrollBarAnimation = new AlphaAnimation(1, 0); 120 mScrollBarAnimation.setDuration(300); 121 mScrollBarAnimation.start(); 122 } else { 123 mScrollBarAnimation = null; 124 } 125 invalidate(); 126 } 127 128 public void setHighLight(Texture highLight) { 129 mHighLight = highLight; 130 } 131 132 public void setDataModel(Model model) { 133 mModel = model; 134 mScrollY = 0; 135 requestLayout(); 136 } 137 138 public void setOnItemSelectedListener(OnItemSelectedListener l) { 139 mOnItemSelectedListener = l; 140 } 141 142 private boolean drawWithAnimation(GLRootView root, 143 Texture texture, int x, int y, int w, int h, Animation anim) { 144 long now = root.currentAnimationTimeMillis(); 145 Transformation temp = root.obtainTransformation(); 146 boolean more = anim.getTransformation(now, temp); 147 Transformation transformation = root.pushTransform(); 148 transformation.compose(temp); 149 texture.draw(root, x, y, w, h); 150 invalidate(); 151 root.popTransform(); 152 return more; 153 } 154 155 @Override 156 protected void render(GLRootView root, GL11 gl) { 157 root.clipRect(0, 0, getWidth(), getHeight()); 158 if (mHighlightIndex != INDEX_NONE) { 159 GLView view = mModel.getView(mHighlightIndex); 160 Rect bounds = view.bounds(); 161 if (mHighLight != null) { 162 int width = bounds.width(); 163 int height = bounds.height(); 164 mHighLight.draw(root, 165 bounds.left - mScrollX, bounds.top - mScrollY, 166 width, height); 167 } 168 } 169 super.render(root, gl); 170 root.clearClip(); 171 172 if (mScrollBarAnimation != null || mScrollBarVisible) { 173 int width = mScrollbar.getWidth(); 174 int height = getHeight() * getHeight() / mScrollHeight; 175 int yoffset = mScrollY * getHeight() / mScrollHeight; 176 if (mScrollBarAnimation != null) { 177 if (!drawWithAnimation( 178 root, mScrollbar, getWidth() - width, yoffset, 179 width, height, mScrollBarAnimation)) { 180 mScrollBarAnimation = null; 181 } 182 } else { 183 mScrollbar.draw( 184 root, getWidth() - width, yoffset, width, height); 185 } 186 } 187 if (mScroller.computeScrollOffset()) { 188 setScrollPosition(mScroller.getCurrY(), false); 189 } 190 } 191 192 @Override 193 protected void onMeasure(int widthSpec, int heightSpec) { 194 // first get the total height 195 int height = 0; 196 int maxWidth = 0; 197 for (int i = 0, n = mModel.size(); i < n; ++i) { 198 GLView view = mModel.getView(i); 199 view.measure(widthSpec, MeasureSpec.UNSPECIFIED); 200 height += view.getMeasuredHeight(); 201 maxWidth = Math.max(maxWidth, view.getMeasuredWidth()); 202 } 203 mScrollHeight = height; 204 mHasMeasured = true; 205 new MeasureHelper(this) 206 .setPreferredContentSize(maxWidth, height) 207 .measure(widthSpec, heightSpec); 208 } 209 210 @Override 211 public int getComponentCount() { 212 return mVisibleEnd - mVisibleStart; 213 } 214 215 @Override 216 public GLView getComponent(int index) { 217 if (index < 0 || index >= mVisibleEnd - mVisibleStart) { 218 throw new ArrayIndexOutOfBoundsException(index); 219 } 220 return mModel.getView(mVisibleStart + index); 221 } 222 223 @Override 224 public void requestLayout() { 225 mHasMeasured = false; 226 super.requestLayout(); 227 } 228 229 @Override 230 protected void onLayout( 231 boolean change, int left, int top, int right, int bottom) { 232 233 if (!mHasMeasured || mMeasuredWidth != (right - left)) { 234 measure(makeMeasureSpec(right - left, MeasureSpec.EXACTLY), 235 makeMeasureSpec(bottom - top, MeasureSpec.EXACTLY)); 236 } 237 238 mScrollable = mScrollHeight > (bottom - top); 239 int width = right - left; 240 int yoffset = 0; 241 242 for (int i = 0, n = mModel.size(); i < n; ++i) { 243 GLView item = mModel.getView(i); 244 item.onAddToParent(this); 245 int nextOffset = yoffset + item.getMeasuredHeight(); 246 item.layout(0, yoffset, width, nextOffset); 247 yoffset = nextOffset; 248 } 249 setScrollPosition(mScrollY, true); 250 } 251 252 private void setScrollPosition(int position, boolean force) { 253 int height = getHeight(); 254 255 position = Util.clamp(position, 0, mScrollHeight - height); 256 257 if (!force && position == mScrollY) return; 258 mScrollY = position; 259 260 int n = mModel.size(); 261 262 int start = 0; 263 int end = 0; 264 for (start = 0; start < n; ++start) { 265 if (position < mModel.getView(start).mBounds.bottom) break; 266 } 267 268 int bottom = position + height; 269 for (end = start; end < n; ++ end) { 270 if (bottom <= mModel.getView(end).mBounds.top) break; 271 } 272 setVisibleRange(start , end); 273 invalidate(); 274 } 275 276 private void setVisibleRange(int start, int end) { 277 if (start == mVisibleStart && end == mVisibleEnd) return; 278 mVisibleStart = start; 279 mVisibleEnd = end; 280 } 281 282 @Override 283 protected boolean dispatchTouchEvent(MotionEvent event) { 284 return onTouch(event); 285 } 286 287 @Override @SuppressWarnings("fallthrough") 288 protected boolean onTouch(MotionEvent event) { 289 290 mGestureDetector.onTouchEvent(event); 291 292 switch (event.getAction()) { 293 case MotionEvent.ACTION_DOWN: 294 mIsPressed = true; 295 mHandler.removeMessages(HIDE_SCROLL_BAR); 296 setScrollBarVisible(mScrollHeight > getHeight()); 297 298 // fallthrough: we need to highlight the item which is pressed 299 case MotionEvent.ACTION_MOVE: 300 if (!mScrollable) { 301 findAndSetHighlightItem((int) event.getY()); 302 } 303 break; 304 case MotionEvent.ACTION_UP: 305 mIsPressed = false; 306 if (mScrollBarVisible) { 307 mHandler.removeMessages(HIDE_SCROLL_BAR); 308 mHandler.sendEmptyMessageDelayed( 309 HIDE_SCROLL_BAR, SCROLL_BAR_TIMEOUT); 310 } 311 if (!mScrollable && mOnItemSelectedListener != null 312 && mHighlightView != null) { 313 mOnItemSelectedListener 314 .onItemSelected(mHighlightView, mHighlightIndex); 315 } 316 case MotionEvent.ACTION_CANCEL: 317 case MotionEvent.ACTION_OUTSIDE: 318 setHighlightItem(null, INDEX_NONE); 319 } 320 return true; 321 } 322 323 private void findAndSetHighlightItem(int y) { 324 int position = y + mScrollY; 325 for (int i = mVisibleStart, n = mVisibleEnd; i < n; ++i) { 326 GLView child = mModel.getView(i); 327 if (child.mBounds.bottom > position) { 328 if (mModel.isSelectable(i)) { 329 setHighlightItem(child, i); 330 return; 331 } 332 break; 333 } 334 } 335 setHighlightItem(null, INDEX_NONE); 336 } 337 338 private void setHighlightItem(GLView view, int index) { 339 if (index == mHighlightIndex) return; 340 mHighlightIndex = index; 341 mHighlightView = view; 342 if (mHighLight != null) invalidate(); 343 } 344 345 public void setScroller(NinePatchTexture scrollbar) { 346 this.mScrollbar = scrollbar; 347 requestLayout(); 348 } 349 350 private class MyGestureListener 351 extends GestureDetector.SimpleOnGestureListener { 352 353 @Override 354 public boolean onFling(MotionEvent e1, 355 MotionEvent e2, float velocityX, float velocityY) { 356 if (!mScrollable) return false; 357 mScroller.fling(0, mScrollY, 358 0, -(int) velocityY, 0, 0, 0, mScrollHeight - getHeight()); 359 invalidate(); 360 return true; 361 } 362 363 @Override 364 public boolean onScroll(MotionEvent e1, 365 MotionEvent e2, float distanceX, float distanceY) { 366 if (!mScrollable) return false; 367 setHighlightItem(null, INDEX_NONE); 368 setScrollPosition(mScrollY + (int) distanceY, false); 369 return true; 370 } 371 372 @Override 373 public void onShowPress(MotionEvent e) { 374 if (!mScrollable || !mIsPressed) return; 375 findAndSetHighlightItem((int) e.getY()); 376 } 377 378 @Override 379 public boolean onSingleTapUp(MotionEvent e) { 380 if (!mScrollable) return false; 381 findAndSetHighlightItem((int) e.getY()); 382 if (mOnItemSelectedListener != null && mHighlightView != null) { 383 mOnItemSelectedListener 384 .onItemSelected(mHighlightView, mHighlightIndex); 385 } 386 setHighlightItem(null, INDEX_NONE); 387 return true; 388 } 389 } 390 } 391