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