图片浏览(点击放大缩小支持多张图片浏览)
大神写的,我就参考参考啦!
从主布局开始了
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="fill_parent" android:layout_height="fill_parent" android:background="#ff171b19" android:orientation="vertical" > <RelativeLayout android:layout_width="fill_parent" android:layout_height="fill_parent" android:layout_weight="1" > <com.app.widget.MyViewPager android:id="@+id/viewPager" android:background="#ff171b19" android:layout_width="match_parent" android:layout_height="match_parent"></com.app.widget.MyViewPager> </RelativeLayout> <LinearLayout android:layout_width="fill_parent" android:layout_height="80dip" android:paddingLeft="15dip" android:paddingRight="15dip" > <ImageView android:id="@+id/imagebrowser_iv_download" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_gravity="center_vertical" android:src="@drawable/ic_topbar_save" android:visibility="gone" /> <TextView android:id="@+id/imagebrowser_ptv_page" android:layout_width="wrap_content" android:layout_height="fill_parent" android:layout_weight="1" android:gravity="right|center_vertical" android:textColor="#ffcdcdcd" android:textSize="34sp" /> </LinearLayout> </LinearLayo
根据界面,下面是代码
@ContentView(R.layout.activity_imagebrowser) public class ImageBrowserActivity extends BaseAppActivity implements OnPageChangeListener, OnClickListener { @ViewInject(R.id.viewPager) private MyViewPager viewPager;//一、自定义的viewpager @ViewInject(R.id.imagebrowser_ptv_page) private TextView mPtvPage; @ViewInject(R.id.imagebrowser_iv_download) private ImageView mIvDownload; private ImageBrowserAdapter mAdapter; private String mType; private int mPosition; private int mTotal; private List<String> list; public static final String PATH_TYPE = "pathType";//1全路径 public static final int TYPE_FULL = 1;//1全路径 public static final int TYPE_PART = 0;//0部分路径 public static final String IMAGE_TYPE = "image_type"; public static final String TYPE_ALBUM = "image_album"; public static final String TYPE_PHOTO = "image_photo"; private int pathType; @Override protected void init() { viewPager.setOnPageChangeListener(this); mIvDownload.setOnClickListener(this); Bundle bundle =getIntent().getExtras();//二、是从其他类中传过来的 if (bundle != null) { mType = bundle.getString(IMAGE_TYPE); pathType=bundle.getInt(PATH_TYPE,0);//默认是部分路径 } if (TYPE_ALBUM.equals(mType)) { mPosition = bundle.getInt("position", 0); list = (ArrayList)bundle.getSerializable("images"); mTotal = list.size(); if (mPosition > mTotal) { mPosition = mTotal - 1; } if (mTotal > 1) { mPosition += 1000 * mTotal; mPtvPage.setText((mPosition % mTotal) + 1 + "/" + mTotal); final ImageView []mImageView=new ImageView[list.size()]; PhotoAdapter adapter=new PhotoAdapter(list,this); viewPager.setAdapter(adapter); } } else if (TYPE_PHOTO.equals(mType)) { final String path = bundle.getString("path"); list = new ArrayList<>(); list.add(path); mPtvPage.setText("1/1"); PhotoAdapter adapter=new PhotoAdapter(list,this); viewPager.setAdapter(adapter); } } @Override public void onPageScrollStateChanged(int arg0) { } @Override public void onPageScrolled(int arg0, float arg1, int arg2) { } @Override public void onPageSelected(int arg0) { mPosition = arg0; mPtvPage.setText((mPosition % mTotal) + 1 + "/" + mTotal); } @Override public void onClick(View arg0) { showCustomToast("图片已保存到本地"); } //adapter class PhotoAdapter extends PagerAdapter{ private List<String> datas; private List<View> views; private Context context; private PhotoAdapter(List<String>datas,Context context){ this.datas=datas; this.context=context; views=new ArrayList<>(); if (datas!=null){ for (Object o:datas){ View view=View.inflate(context,R.layout.vp,null);//三、图片自定义view views.add(view); } } } @Override public int getCount() { return views==null?0:views.size(); } @Override public Object instantiateItem(ViewGroup container, int position) { View view=views.get(position); PhotoView photoView= (PhotoView)view.findViewById(R.id.photoView);// ImageLoader.getInstance().displayImage(UrlConfig.BASE_IMAGE_URL+datas.get(position),photoView); container.addView(view); return view; } @Override public void destroyItem(ViewGroup container, int position, Object object) { container.removeView(views.get(position)); } @Override public boolean isViewFromObject(View view, Object object) { return view==object; } } }
从一到三我们开始吧!
一、MyViewPager
public class MyViewPager extends ViewPager { public MyViewPager(Context context) { super(context); } public MyViewPager(Context context, AttributeSet attrs) { super(context, attrs); } @Override public boolean onTouchEvent(MotionEvent ev) { try { return super.onTouchEvent(ev); } catch (IllegalArgumentException ex) { ex.printStackTrace(); } return false; } @Override public boolean onInterceptTouchEvent(MotionEvent ev) { try { return super.onInterceptTouchEvent(ev); } catch (IllegalArgumentException ex) { ex.printStackTrace(); } return false; } }
二、是从其他类中传过来的
我们看怎么传的
//list为取得数据list final JSONObject object = (JSONObject) list.get(i); pathlist = new ArrayList<>(); for (int j = 0; j < list.size(); j++) { JSONObject object1 = (JSONObject) list.get(j); pathlist.add(object1.getString("path")); } ImageUtil.ShowIamge(holder.pic, UrlConfig.BASE_IMAGE_URL + object.getString("path")); holder.itemView.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { Bundle bundle = new Bundle(); if (list.size() == 1) { String str = object.getString("path").toString(); bundle.putString(ImageBrowserActivity.IMAGE_TYPE, ImageBrowserActivity.TYPE_PHOTO); bundle.putString("path", str); } else { bundle.putString(ImageBrowserActivity.IMAGE_TYPE, ImageBrowserActivity.TYPE_ALBUM); bundle.putInt("position", i); bundle.putSerializable("images", (Serializable) pathlist); } startActivity(ImageBrowserActivity.class, "图片浏览", bundle); } });
//三、图片自定义view R.layout.vp
1 <?xml version="1.0" encoding="utf-8"?> 2 <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" 3 android:layout_width="match_parent" 4 android:layout_height="match_parent" 5 android:orientation="vertical"> 6 7 <com.app.widget.photoview.PhotoView//自定义View 8 android:id="@+id/photoView" 9 android:layout_width="match_parent" 10 android:layout_height="match_parent" /> 11 12 </LinearLayout>
现在开始写PhotoView啦!
1 public class PhotoView extends ImageView implements IPhotoView { 2 3 private final PhotoViewAttacher mAttacher; 4 5 private ScaleType mPendingScaleType; 6 7 public PhotoView(Context context) { 8 this(context, null); 9 } 10 11 public PhotoView(Context context, AttributeSet attr) { 12 this(context, attr, 0); 13 } 14 15 public PhotoView(Context context, AttributeSet attr, int defStyle) { 16 super(context, attr, defStyle); 17 super.setScaleType(ScaleType.MATRIX); 18 mAttacher = new PhotoViewAttacher(this); 19 20 if (null != mPendingScaleType) { 21 setScaleType(mPendingScaleType); 22 mPendingScaleType = null; 23 } 24 } 25 26 @Override 27 public boolean canZoom() { 28 return mAttacher.canZoom(); 29 } 30 31 @Override 32 public RectF getDisplayRect() { 33 return mAttacher.getDisplayRect(); 34 } 35 36 @Override 37 public float getMinScale() { 38 return mAttacher.getMinScale(); 39 } 40 41 @Override 42 public float getMidScale() { 43 return mAttacher.getMidScale(); 44 } 45 46 @Override 47 public float getMaxScale() { 48 return mAttacher.getMaxScale(); 49 } 50 51 @Override 52 public float getScale() { 53 return mAttacher.getScale(); 54 } 55 56 @Override 57 public ScaleType getScaleType() { 58 return mAttacher.getScaleType(); 59 } 60 61 @Override 62 public void setAllowParentInterceptOnEdge(boolean allow) { 63 mAttacher.setAllowParentInterceptOnEdge(allow); 64 } 65 66 @Override 67 public void setMinScale(float minScale) { 68 mAttacher.setMinScale(minScale); 69 } 70 71 @Override 72 public void setMidScale(float midScale) { 73 mAttacher.setMidScale(midScale); 74 } 75 76 @Override 77 public void setMaxScale(float maxScale) { 78 mAttacher.setMaxScale(maxScale); 79 } 80 81 @Override 82 // setImageBitmap calls through to this method 83 public void setImageDrawable(Drawable drawable) { 84 super.setImageDrawable(drawable); 85 if (null != mAttacher) { 86 mAttacher.update(); 87 } 88 } 89 90 @Override 91 public void setImageResource(int resId) { 92 super.setImageResource(resId); 93 if (null != mAttacher) { 94 mAttacher.update(); 95 } 96 } 97 98 @Override 99 public void setImageURI(Uri uri) { 100 super.setImageURI(uri); 101 if (null != mAttacher) { 102 mAttacher.update(); 103 } 104 } 105 106 @Override 107 public void setOnMatrixChangeListener(PhotoViewAttacher.OnMatrixChangedListener listener) { 108 mAttacher.setOnMatrixChangeListener(listener); 109 } 110 111 @Override 112 public void setOnLongClickListener(OnLongClickListener l) { 113 mAttacher.setOnLongClickListener(l); 114 } 115 116 @Override 117 public void setOnPhotoTapListener(PhotoViewAttacher.OnPhotoTapListener listener) { 118 mAttacher.setOnPhotoTapListener(listener); 119 } 120 121 @Override 122 public void setOnViewTapListener(PhotoViewAttacher.OnViewTapListener listener) { 123 mAttacher.setOnViewTapListener(listener); 124 } 125 126 @Override 127 public void setScaleType(ScaleType scaleType) { 128 if (null != mAttacher) { 129 mAttacher.setScaleType(scaleType); 130 } else { 131 mPendingScaleType = scaleType; 132 } 133 } 134 135 @Override 136 public void setZoomable(boolean zoomable) { 137 mAttacher.setZoomable(zoomable); 138 } 139 140 @Override 141 public void zoomTo(float scale, float focalX, float focalY) { 142 mAttacher.zoomTo(scale, focalX, focalY); 143 } 144 145 @Override 146 protected void onDetachedFromWindow() { 147 mAttacher.cleanup(); 148 super.onDetachedFromWindow(); 149 } 150 151 }
IPhotoView
1 public interface IPhotoView { 2 /** 3 * Returns true if the PhotoView is set to allow zooming of Photos. 4 * 5 * @return true if the PhotoView allows zooming. 6 */ 7 boolean canZoom(); 8 9 /** 10 * Gets the Display Rectangle of the currently displayed Drawable. The 11 * Rectangle is relative to this View and includes all scaling and 12 * translations. 13 * 14 * @return - RectF of Displayed Drawable 15 */ 16 RectF getDisplayRect(); 17 18 /** 19 * @return The current minimum scale level. What this value represents depends on the current {@link ImageView.ScaleType}. 20 */ 21 float getMinScale(); 22 23 /** 24 * @return The current middle scale level. What this value represents depends on the current {@link ImageView.ScaleType}. 25 */ 26 float getMidScale(); 27 28 /** 29 * @return The current maximum scale level. What this value represents depends on the current {@link ImageView.ScaleType}. 30 */ 31 float getMaxScale(); 32 33 /** 34 * Returns the current scale value 35 * 36 * @return float - current scale value 37 */ 38 float getScale(); 39 40 /** 41 * Return the current scale type in use by the ImageView. 42 */ 43 ImageView.ScaleType getScaleType(); 44 45 /** 46 * Whether to allow the ImageView's parent to intercept the touch event when the photo is scroll to it's horizontal edge. 47 */ 48 void setAllowParentInterceptOnEdge(boolean allow); 49 50 /** 51 * Sets the minimum scale level. What this value represents depends on the current {@link ImageView.ScaleType}. 52 */ 53 void setMinScale(float minScale); 54 55 /** 56 * Sets the middle scale level. What this value represents depends on the current {@link ImageView.ScaleType}. 57 */ 58 void setMidScale(float midScale); 59 60 /** 61 * Sets the maximum scale level. What this value represents depends on the current {@link ImageView.ScaleType}. 62 */ 63 void setMaxScale(float maxScale); 64 65 /** 66 * Register a callback to be invoked when the Photo displayed by this view is long-pressed. 67 * 68 * @param listener - Listener to be registered. 69 */ 70 void setOnLongClickListener(View.OnLongClickListener listener); 71 72 /** 73 * Register a callback to be invoked when the Matrix has changed for this 74 * View. An example would be the user panning or scaling the Photo. 75 * 76 * @param listener - Listener to be registered. 77 */ 78 void setOnMatrixChangeListener(PhotoViewAttacher.OnMatrixChangedListener listener); 79 80 /** 81 * Register a callback to be invoked when the Photo displayed by this View 82 * is tapped with a single tap. 83 * 84 * @param listener - Listener to be registered. 85 */ 86 void setOnPhotoTapListener(PhotoViewAttacher.OnPhotoTapListener listener); 87 88 /** 89 * Register a callback to be invoked when the View is tapped with a single 90 * tap. 91 * 92 * @param listener - Listener to be registered. 93 */ 94 void setOnViewTapListener(PhotoViewAttacher.OnViewTapListener listener); 95 96 /** 97 * Controls how the image should be resized or moved to match the size of 98 * the ImageView. Any scaling or panning will happen within the confines of 99 * this {@link ImageView.ScaleType}. 100 * 101 * @param scaleType - The desired scaling mode. 102 */ 103 void setScaleType(ImageView.ScaleType scaleType); 104 105 /** 106 * Allows you to enable/disable the zoom functionality on the ImageView. 107 * When disable the ImageView reverts to using the FIT_CENTER matrix. 108 * 109 * @param zoomable - Whether the zoom functionality is enabled. 110 */ 111 void setZoomable(boolean zoomable); 112 113 /** 114 * Zooms to the specified scale, around the focal point given. 115 * 116 * @param scale - Scale to zoom to 117 * @param focalX - X Focus Point 118 * @param focalY - Y Focus Point 119 */ 120 void zoomTo(float scale, float focalX, float focalY); 121 }
PhotoViewAttacher
1 public class PhotoViewAttacher implements IPhotoView, View.OnTouchListener, VersionedGestureDetector.OnGestureListener, 2 GestureDetector.OnDoubleTapListener, ViewTreeObserver.OnGlobalLayoutListener { 3 4 static final String LOG_TAG = "PhotoViewAttacher"; 5 6 // let debug flag be dynamic, but still Proguard can be used to remove from release builds 7 static final boolean DEBUG = Log.isLoggable(LOG_TAG, Log.DEBUG); 8 9 static final int EDGE_NONE = -1; 10 static final int EDGE_LEFT = 0; 11 static final int EDGE_RIGHT = 1; 12 static final int EDGE_BOTH = 2; 13 14 public static final float DEFAULT_MAX_SCALE = 3.0f; 15 public static final float DEFAULT_MID_SCALE = 1.75f; 16 public static final float DEFAULT_MIN_SCALE = 1.0f; 17 18 private float mMinScale = DEFAULT_MIN_SCALE; 19 private float mMidScale = DEFAULT_MID_SCALE; 20 private float mMaxScale = DEFAULT_MAX_SCALE; 21 22 private boolean mAllowParentInterceptOnEdge = true; 23 24 private static void checkZoomLevels(float minZoom, float midZoom, float maxZoom) { 25 if (minZoom >= midZoom) { 26 throw new IllegalArgumentException("MinZoom should be less than MidZoom"); 27 } else if (midZoom >= maxZoom) { 28 throw new IllegalArgumentException("MidZoom should be less than MaxZoom"); 29 } 30 } 31 32 /** 33 * @return true if the ImageView exists, and it's Drawable existss 34 */ 35 private static boolean hasDrawable(ImageView imageView) { 36 return null != imageView && null != imageView.getDrawable(); 37 } 38 39 /** 40 * @return true if the ScaleType is supported. 41 */ 42 private static boolean isSupportedScaleType(final ScaleType scaleType) { 43 if (null == scaleType) { 44 return false; 45 } 46 47 switch (scaleType) { 48 case MATRIX: 49 throw new IllegalArgumentException(scaleType.name() + " is not supported in PhotoView"); 50 51 default: 52 return true; 53 } 54 } 55 56 /** 57 * Set's the ImageView's ScaleType to Matrix. 58 */ 59 private static void setImageViewScaleTypeMatrix(ImageView imageView) { 60 if (null != imageView) { 61 if (imageView instanceof PhotoView) { 62 /** 63 * PhotoView sets it's own ScaleType to Matrix, then diverts all 64 * calls setScaleType to this.setScaleType. Basically we don't 65 * need to do anything here 66 */ 67 } else { 68 imageView.setScaleType(ScaleType.MATRIX); 69 } 70 } 71 } 72 73 private WeakReference<ImageView> mImageView; 74 private ViewTreeObserver mViewTreeObserver; 75 76 // Gesture Detectors 77 private GestureDetector mGestureDetector; 78 private VersionedGestureDetector mScaleDragDetector; 79 80 // These are set so we don't keep allocating them on the heap 81 private final Matrix mBaseMatrix = new Matrix(); 82 private final Matrix mDrawMatrix = new Matrix(); 83 private final Matrix mSuppMatrix = new Matrix(); 84 private final RectF mDisplayRect = new RectF(); 85 private final float[] mMatrixValues = new float[9]; 86 87 // Listeners 88 private OnMatrixChangedListener mMatrixChangeListener; 89 private OnPhotoTapListener mPhotoTapListener; 90 private OnViewTapListener mViewTapListener; 91 private OnLongClickListener mLongClickListener; 92 93 private int mIvTop, mIvRight, mIvBottom, mIvLeft; 94 private FlingRunnable mCurrentFlingRunnable; 95 private int mScrollEdge = EDGE_BOTH; 96 97 private boolean mZoomEnabled; 98 private ScaleType mScaleType = ScaleType.FIT_CENTER; 99 100 public PhotoViewAttacher(ImageView imageView) { 101 mImageView = new WeakReference<ImageView>(imageView); 102 103 imageView.setOnTouchListener(this); 104 105 mViewTreeObserver = imageView.getViewTreeObserver(); 106 mViewTreeObserver.addOnGlobalLayoutListener(this); 107 108 // Make sure we using MATRIX Scale Type 109 setImageViewScaleTypeMatrix(imageView); 110 111 if (!imageView.isInEditMode()) { 112 // Create Gesture Detectors... 113 mScaleDragDetector = VersionedGestureDetector.newInstance(imageView.getContext(), this); 114 115 mGestureDetector = new GestureDetector(imageView.getContext(), 116 new GestureDetector.SimpleOnGestureListener() { 117 118 // forward long click listener 119 @Override 120 public void onLongPress(MotionEvent e) { 121 if(null != mLongClickListener) { 122 mLongClickListener.onLongClick(mImageView.get()); 123 } 124 }}); 125 126 mGestureDetector.setOnDoubleTapListener(this); 127 128 // Finally, update the UI so that we're zoomable 129 setZoomable(true); 130 } 131 } 132 133 @Override 134 public final boolean canZoom() { 135 return mZoomEnabled; 136 } 137 138 /** 139 * Clean-up the resources attached to this object. This needs to be called 140 * when the ImageView is no longer used. A good example is from 141 * {@link View#onDetachedFromWindow()} or from {@link android.app.Activity#onDestroy()}. 142 * This is automatically called if you are using {@link uk.co.senab.photoview.PhotoView}. 143 */ 144 @SuppressWarnings("deprecation") 145 public final void cleanup() { 146 // if (null != mImageView) { 147 // mImageView.get().getViewTreeObserver().removeGlobalOnLayoutListener(this); 148 // } 149 // mViewTreeObserver = null; 150 // 151 // // Clear listeners too 152 // mMatrixChangeListener = null; 153 // mPhotoTapListener = null; 154 // mViewTapListener = null; 155 // 156 // // Finally, clear ImageView 157 // mImageView = null; 158 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) { 159 if (null != mImageView) { 160 mImageView.get().getViewTreeObserver() 161 .removeOnGlobalLayoutListener(this); 162 } 163 164 if (null != mViewTreeObserver && mViewTreeObserver.isAlive()) { 165 mViewTreeObserver.removeOnGlobalLayoutListener(this); 166 167 mViewTreeObserver = null; 168 169 // Clear listeners too 170 mMatrixChangeListener = null; 171 mPhotoTapListener = null; 172 mViewTapListener = null; 173 // Finally, clear ImageView 174 mImageView = null; 175 } 176 177 } else { 178 if (null != mImageView) { 179 mImageView.get().getViewTreeObserver() 180 .removeGlobalOnLayoutListener(this); 181 } 182 183 if (null != mViewTreeObserver && mViewTreeObserver.isAlive()) { 184 mViewTreeObserver.removeGlobalOnLayoutListener(this); 185 186 mViewTreeObserver = null; 187 188 // Clear listeners too 189 mMatrixChangeListener = null; 190 mPhotoTapListener = null; 191 mViewTapListener = null; 192 // Finally, clear ImageView 193 mImageView = null; 194 } 195 } 196 } 197 198 @Override 199 public final RectF getDisplayRect() { 200 checkMatrixBounds(); 201 return getDisplayRect(getDisplayMatrix()); 202 } 203 204 public final ImageView getImageView() { 205 ImageView imageView = null; 206 207 if (null != mImageView) { 208 imageView = mImageView.get(); 209 } 210 211 // If we don't have an ImageView, call cleanup() 212 if (null == imageView) { 213 cleanup(); 214 throw new IllegalStateException( 215 "ImageView no longer exists. You should not use this PhotoViewAttacher any more."); 216 } 217 218 return imageView; 219 } 220 221 @Override 222 public float getMinScale() { 223 return mMinScale; 224 } 225 226 @Override 227 public float getMidScale() { 228 return mMidScale; 229 } 230 231 @Override 232 public float getMaxScale() { 233 return mMaxScale; 234 } 235 236 @Override 237 public final float getScale() { 238 return getValue(mSuppMatrix, Matrix.MSCALE_X); 239 } 240 241 @Override 242 public final ScaleType getScaleType() { 243 return mScaleType; 244 } 245 246 public final boolean onDoubleTap(MotionEvent ev) { 247 try { 248 float scale = getScale(); 249 float x = ev.getX(); 250 float y = ev.getY(); 251 252 if (scale < mMidScale) { 253 zoomTo(mMidScale, x, y); 254 } else if (scale >= mMidScale && scale < mMaxScale) { 255 zoomTo(mMaxScale, x, y); 256 } else { 257 zoomTo(mMinScale, x, y); 258 } 259 } catch (ArrayIndexOutOfBoundsException e) { 260 // Can sometimes happen when getX() and getY() is called 261 } 262 263 return true; 264 } 265 266 public final boolean onDoubleTapEvent(MotionEvent e) { 267 // Wait for the confirmed onDoubleTap() instead 268 return false; 269 } 270 271 public final void onDrag(float dx, float dy) { 272 if (DEBUG) { 273 Log.d(LOG_TAG, String.format("onDrag: dx: %.2f. dy: %.2f", dx, dy)); 274 } 275 276 ImageView imageView = getImageView(); 277 278 if (null != imageView && hasDrawable(imageView)) { 279 mSuppMatrix.postTranslate(dx, dy); 280 checkAndDisplayMatrix(); 281 282 /** 283 * Here we decide whether to let the ImageView's parent to start 284 * taking over the touch event. 285 * 286 * First we check whether this function is enabled. We never want the 287 * parent to take over if we're scaling. We then check the edge we're 288 * on, and the direction of the scroll (i.e. if we're pulling against 289 * the edge, aka 'overscrolling', let the parent take over). 290 */ 291 if (mAllowParentInterceptOnEdge && !mScaleDragDetector.isScaling()) { 292 if (mScrollEdge == EDGE_BOTH || (mScrollEdge == EDGE_LEFT && dx >= 1f) 293 || (mScrollEdge == EDGE_RIGHT && dx <= -1f)) { 294 imageView.getParent().requestDisallowInterceptTouchEvent(false); 295 } 296 } 297 } 298 } 299 300 @Override 301 public final void onFling(float startX, float startY, float velocityX, float velocityY) { 302 if (DEBUG) { 303 Log.d(LOG_TAG, "onFling. sX: " + startX + " sY: " + startY + " Vx: " + velocityX + " Vy: " + velocityY); 304 } 305 306 ImageView imageView = getImageView(); 307 if (hasDrawable(imageView)) { 308 mCurrentFlingRunnable = new FlingRunnable(imageView.getContext()); 309 mCurrentFlingRunnable.fling(imageView.getWidth(), imageView.getHeight(), (int) velocityX, (int) velocityY); 310 imageView.post(mCurrentFlingRunnable); 311 } 312 } 313 314 @Override 315 public final void onGlobalLayout() { 316 ImageView imageView = getImageView(); 317 318 if (null != imageView && mZoomEnabled) { 319 final int top = imageView.getTop(); 320 final int right = imageView.getRight(); 321 final int bottom = imageView.getBottom(); 322 final int left = imageView.getLeft(); 323 324 /** 325 * We need to check whether the ImageView's bounds have changed. 326 * This would be easier if we targeted API 11+ as we could just use 327 * View.OnLayoutChangeListener. Instead we have to replicate the 328 * work, keeping track of the ImageView's bounds and then checking 329 * if the values change. 330 */ 331 if (top != mIvTop || bottom != mIvBottom || left != mIvLeft || right != mIvRight) { 332 // Update our base matrix, as the bounds have changed 333 updateBaseMatrix(imageView.getDrawable()); 334 335 // Update values as something has changed 336 mIvTop = top; 337 mIvRight = right; 338 mIvBottom = bottom; 339 mIvLeft = left; 340 } 341 } 342 } 343 344 public final void onScale(float scaleFactor, float focusX, float focusY) { 345 if (DEBUG) { 346 Log.d(LOG_TAG, String.format("onScale: scale: %.2f. fX: %.2f. fY: %.2f", scaleFactor, focusX, focusY)); 347 } 348 349 if (hasDrawable(getImageView()) && (getScale() < mMaxScale || scaleFactor < 1f)) { 350 mSuppMatrix.postScale(scaleFactor, scaleFactor, focusX, focusY); 351 checkAndDisplayMatrix(); 352 } 353 } 354 355 public final boolean onSingleTapConfirmed(MotionEvent e) { 356 ImageView imageView = getImageView(); 357 358 if (null != imageView) { 359 if (null != mPhotoTapListener) { 360 final RectF displayRect = getDisplayRect(); 361 362 if (null != displayRect) { 363 final float x = e.getX(), y = e.getY(); 364 365 // Check to see if the user tapped on the photo 366 if (displayRect.contains(x, y)) { 367 368 float xResult = (x - displayRect.left) / displayRect.width(); 369 float yResult = (y - displayRect.top) / displayRect.height(); 370 371 mPhotoTapListener.onPhotoTap(imageView, xResult, yResult); 372 return true; 373 } 374 } 375 } 376 if (null != mViewTapListener) { 377 mViewTapListener.onViewTap(imageView, e.getX(), e.getY()); 378 } 379 } 380 381 return false; 382 } 383 384 @Override 385 public final boolean onTouch(View v, MotionEvent ev) { 386 boolean handled = false; 387 388 if (mZoomEnabled) { 389 switch (ev.getAction()) { 390 case MotionEvent.ACTION_DOWN: 391 // First, disable the Parent from intercepting the touch 392 // event 393 v.getParent().requestDisallowInterceptTouchEvent(true); 394 395 // If we're flinging, and the user presses down, cancel 396 // fling 397 cancelFling(); 398 break; 399 400 case MotionEvent.ACTION_CANCEL: 401 case MotionEvent.ACTION_UP: 402 // If the user has zoomed less than min scale, zoom back 403 // to min scale 404 if (getScale() < mMinScale) { 405 RectF rect = getDisplayRect(); 406 if (null != rect) { 407 v.post(new AnimatedZoomRunnable(getScale(), mMinScale, rect.centerX(), rect.centerY())); 408 handled = true; 409 } 410 } 411 break; 412 } 413 414 // Check to see if the user double tapped 415 if (null != mGestureDetector && mGestureDetector.onTouchEvent(ev)) { 416 handled = true; 417 } 418 419 // Finally, try the Scale/Drag detector 420 if (null != mScaleDragDetector && mScaleDragDetector.onTouchEvent(ev)) { 421 handled = true; 422 } 423 } 424 425 return handled; 426 } 427 428 @Override 429 public void setAllowParentInterceptOnEdge(boolean allow) { 430 mAllowParentInterceptOnEdge = allow; 431 } 432 433 @Override 434 public void setMinScale(float minScale) { 435 checkZoomLevels(minScale, mMidScale, mMaxScale); 436 mMinScale = minScale; 437 } 438 439 @Override 440 public void setMidScale(float midScale) { 441 checkZoomLevels(mMinScale, midScale, mMaxScale); 442 mMidScale = midScale; 443 } 444 445 @Override 446 public void setMaxScale(float maxScale) { 447 checkZoomLevels(mMinScale, mMidScale, maxScale); 448 mMaxScale = maxScale; 449 } 450 451 @Override 452 public final void setOnLongClickListener(OnLongClickListener listener) { 453 mLongClickListener = listener; 454 } 455 456 @Override 457 public final void setOnMatrixChangeListener(OnMatrixChangedListener listener) { 458 mMatrixChangeListener = listener; 459 } 460 461 @Override 462 public final void setOnPhotoTapListener(OnPhotoTapListener listener) { 463 mPhotoTapListener = listener; 464 } 465 466 @Override 467 public final void setOnViewTapListener(OnViewTapListener listener) { 468 mViewTapListener = listener; 469 } 470 471 @Override 472 public final void setScaleType(ScaleType scaleType) { 473 if (isSupportedScaleType(scaleType) && scaleType != mScaleType) { 474 mScaleType = scaleType; 475 476 // Finally update 477 update(); 478 } 479 } 480 481 @Override 482 public final void setZoomable(boolean zoomable) { 483 mZoomEnabled = zoomable; 484 update(); 485 } 486 487 public final void update() { 488 ImageView imageView = getImageView(); 489 490 if (null != imageView) { 491 if (mZoomEnabled) { 492 // Make sure we using MATRIX Scale Type 493 setImageViewScaleTypeMatrix(imageView); 494 495 // Update the base matrix using the current drawable 496 updateBaseMatrix(imageView.getDrawable()); 497 } else { 498 // Reset the Matrix... 499 resetMatrix(); 500 } 501 } 502 } 503 504 @Override 505 public final void zoomTo(float scale, float focalX, float focalY) { 506 ImageView imageView = getImageView(); 507 508 if (null != imageView) { 509 imageView.post(new AnimatedZoomRunnable(getScale(), scale, focalX, focalY)); 510 } 511 } 512 513 protected Matrix getDisplayMatrix() { 514 mDrawMatrix.set(mBaseMatrix); 515 mDrawMatrix.postConcat(mSuppMatrix); 516 return mDrawMatrix; 517 } 518 519 private void cancelFling() { 520 if (null != mCurrentFlingRunnable) { 521 mCurrentFlingRunnable.cancelFling(); 522 mCurrentFlingRunnable = null; 523 } 524 } 525 526 /** 527 * Helper method that simply checks the Matrix, and then displays the result 528 */ 529 private void checkAndDisplayMatrix() { 530 checkMatrixBounds(); 531 setImageViewMatrix(getDisplayMatrix()); 532 } 533 534 private void checkImageViewScaleType() { 535 ImageView imageView = getImageView(); 536 537 /** 538 * PhotoView's getScaleType() will just divert to this.getScaleType() so 539 * only call if we're not attached to a PhotoView. 540 */ 541 if (null != imageView && !(imageView instanceof PhotoView)) { 542 if (imageView.getScaleType() != ScaleType.MATRIX) { 543 throw new IllegalStateException( 544 "The ImageView's ScaleType has been changed since attaching a PhotoViewAttacher"); 545 } 546 } 547 } 548 549 private void checkMatrixBounds() { 550 final ImageView imageView = getImageView(); 551 if (null == imageView) { 552 return; 553 } 554 555 final RectF rect = getDisplayRect(getDisplayMatrix()); 556 if (null == rect) { 557 return; 558 } 559 560 final float height = rect.height(), width = rect.width(); 561 float deltaX = 0, deltaY = 0; 562 563 final int viewHeight = imageView.getHeight(); 564 if (height <= viewHeight) { 565 switch (mScaleType) { 566 case FIT_START: 567 deltaY = -rect.top; 568 break; 569 case FIT_END: 570 deltaY = viewHeight - height - rect.top; 571 break; 572 default: 573 deltaY = (viewHeight - height) / 2 - rect.top; 574 break; 575 } 576 } else if (rect.top > 0) { 577 deltaY = -rect.top; 578 } else if (rect.bottom < viewHeight) { 579 deltaY = viewHeight - rect.bottom; 580 } 581 582 final int viewWidth = imageView.getWidth(); 583 if (width <= viewWidth) { 584 switch (mScaleType) { 585 case FIT_START: 586 deltaX = -rect.left; 587 break; 588 case FIT_END: 589 deltaX = viewWidth - width - rect.left; 590 break; 591 default: 592 deltaX = (viewWidth - width) / 2 - rect.left; 593 break; 594 } 595 mScrollEdge = EDGE_BOTH; 596 } else if (rect.left > 0) { 597 mScrollEdge = EDGE_LEFT; 598 deltaX = -rect.left; 599 } else if (rect.right < viewWidth) { 600 deltaX = viewWidth - rect.right; 601 mScrollEdge = EDGE_RIGHT; 602 } else { 603 mScrollEdge = EDGE_NONE; 604 } 605 606 // Finally actually translate the matrix 607 mSuppMatrix.postTranslate(deltaX, deltaY); 608 } 609 610 /** 611 * Helper method that maps the supplied Matrix to the current Drawable 612 * 613 * @param matrix - Matrix to map Drawable against 614 * @return RectF - Displayed Rectangle 615 */ 616 private RectF getDisplayRect(Matrix matrix) { 617 ImageView imageView = getImageView(); 618 619 if (null != imageView) { 620 Drawable d = imageView.getDrawable(); 621 if (null != d) { 622 mDisplayRect.set(0, 0, d.getIntrinsicWidth(), d.getIntrinsicHeight()); 623 matrix.mapRect(mDisplayRect); 624 return mDisplayRect; 625 } 626 } 627 return null; 628 } 629 630 /** 631 * Helper method that 'unpacks' a Matrix and returns the required value 632 * 633 * @param matrix - Matrix to unpack 634 * @param whichValue - Which value from Matrix.M* to return 635 * @return float - returned value 636 */ 637 private float getValue(Matrix matrix, int whichValue) { 638 matrix.getValues(mMatrixValues); 639 return mMatrixValues[whichValue]; 640 } 641 642 /** 643 * Resets the Matrix back to FIT_CENTER, and then displays it.s 644 */ 645 private void resetMatrix() { 646 mSuppMatrix.reset(); 647 setImageViewMatrix(getDisplayMatrix()); 648 checkMatrixBounds(); 649 } 650 651 private void setImageViewMatrix(Matrix matrix) { 652 ImageView imageView = getImageView(); 653 if (null != imageView) { 654 655 checkImageViewScaleType(); 656 imageView.setImageMatrix(matrix); 657 658 // Call MatrixChangedListener if needed 659 if (null != mMatrixChangeListener) { 660 RectF displayRect = getDisplayRect(matrix); 661 if (null != displayRect) { 662 mMatrixChangeListener.onMatrixChanged(displayRect); 663 } 664 } 665 } 666 } 667 668 /** 669 * Calculate Matrix for FIT_CENTER 670 * 671 * @param d - Drawable being displayed 672 */ 673 private void updateBaseMatrix(Drawable d) { 674 ImageView imageView = getImageView(); 675 if (null == imageView || null == d) { 676 return; 677 } 678 679 final float viewWidth = imageView.getWidth(); 680 final float viewHeight = imageView.getHeight(); 681 final int drawableWidth = d.getIntrinsicWidth(); 682 final int drawableHeight = d.getIntrinsicHeight(); 683 684 mBaseMatrix.reset(); 685 686 final float widthScale = viewWidth / drawableWidth; 687 final float heightScale = viewHeight / drawableHeight; 688 689 if (mScaleType == ScaleType.CENTER) { 690 mBaseMatrix.postTranslate((viewWidth - drawableWidth) / 2F, (viewHeight - drawableHeight) / 2F); 691 692 } else if (mScaleType == ScaleType.CENTER_CROP) { 693 float scale = Math.max(widthScale, heightScale); 694 mBaseMatrix.postScale(scale, scale); 695 mBaseMatrix.postTranslate((viewWidth - drawableWidth * scale) / 2F, 696 (viewHeight - drawableHeight * scale) / 2F); 697 698 } else if (mScaleType == ScaleType.CENTER_INSIDE) { 699 float scale = Math.min(1.0f, Math.min(widthScale, heightScale)); 700 mBaseMatrix.postScale(scale, scale); 701 mBaseMatrix.postTranslate((viewWidth - drawableWidth * scale) / 2F, 702 (viewHeight - drawableHeight * scale) / 2F); 703 704 } else { 705 RectF mTempSrc = new RectF(0, 0, drawableWidth, drawableHeight); 706 RectF mTempDst = new RectF(0, 0, viewWidth, viewHeight); 707 708 switch (mScaleType) { 709 case FIT_CENTER: 710 mBaseMatrix.setRectToRect(mTempSrc, mTempDst, ScaleToFit.CENTER); 711 break; 712 713 case FIT_START: 714 mBaseMatrix.setRectToRect(mTempSrc, mTempDst, ScaleToFit.START); 715 break; 716 717 case FIT_END: 718 mBaseMatrix.setRectToRect(mTempSrc, mTempDst, ScaleToFit.END); 719 break; 720 721 case FIT_XY: 722 mBaseMatrix.setRectToRect(mTempSrc, mTempDst, ScaleToFit.FILL); 723 break; 724 725 default: 726 break; 727 } 728 } 729 730 resetMatrix(); 731 } 732 733 /** 734 * Interface definition for a callback to be invoked when the internal 735 * Matrix has changed for this View. 736 * 737 * @author Chris Banes 738 */ 739 public static interface OnMatrixChangedListener { 740 /** 741 * Callback for when the Matrix displaying the Drawable has changed. 742 * This could be because the View's bounds have changed, or the user has 743 * zoomed. 744 * 745 * @param rect - Rectangle displaying the Drawable's new bounds. 746 */ 747 void onMatrixChanged(RectF rect); 748 } 749 750 /** 751 * Interface definition for a callback to be invoked when the Photo is 752 * tapped with a single tap. 753 * 754 * @author Chris Banes 755 */ 756 public static interface OnPhotoTapListener { 757 758 /** 759 * A callback to receive where the user taps on a photo. You will only 760 * receive a callback if the user taps on the actual photo, tapping on 761 * 'whitespace' will be ignored. 762 * 763 * @param view - View the user tapped. 764 * @param x - where the user tapped from the of the Drawable, as 765 * percentage of the Drawable width. 766 * @param y - where the user tapped from the top of the Drawable, as 767 * percentage of the Drawable height. 768 */ 769 void onPhotoTap(View view, float x, float y); 770 } 771 772 /** 773 * Interface definition for a callback to be invoked when the ImageView is 774 * tapped with a single tap. 775 * 776 * @author Chris Banes 777 */ 778 public static interface OnViewTapListener { 779 780 /** 781 * A callback to receive where the user taps on a ImageView. You will 782 * receive a callback if the user taps anywhere on the view, tapping on 783 * 'whitespace' will not be ignored. 784 * 785 * @param view - View the user tapped. 786 * @param x - where the user tapped from the left of the View. 787 * @param y - where the user tapped from the top of the View. 788 */ 789 void onViewTap(View view, float x, float y); 790 } 791 792 private class AnimatedZoomRunnable implements Runnable { 793 794 // These are 'postScale' values, means they're compounded each iteration 795 static final float ANIMATION_SCALE_PER_ITERATION_IN = 1.07f; 796 static final float ANIMATION_SCALE_PER_ITERATION_OUT = 0.93f; 797 798 private final float mFocalX, mFocalY; 799 private final float mTargetZoom; 800 private final float mDeltaScale; 801 802 public AnimatedZoomRunnable(final float currentZoom, final float targetZoom, final float focalX, 803 final float focalY) { 804 mTargetZoom = targetZoom; 805 mFocalX = focalX; 806 mFocalY = focalY; 807 808 if (currentZoom < targetZoom) { 809 mDeltaScale = ANIMATION_SCALE_PER_ITERATION_IN; 810 } else { 811 mDeltaScale = ANIMATION_SCALE_PER_ITERATION_OUT; 812 } 813 } 814 815 public void run() { 816 ImageView imageView = getImageView(); 817 818 if (null != imageView) { 819 mSuppMatrix.postScale(mDeltaScale, mDeltaScale, mFocalX, mFocalY); 820 checkAndDisplayMatrix(); 821 822 final float currentScale = getScale(); 823 824 if ((mDeltaScale > 1f && currentScale < mTargetZoom) 825 || (mDeltaScale < 1f && mTargetZoom < currentScale)) { 826 // We haven't hit our target scale yet, so post ourselves 827 // again 828 Compat.postOnAnimation(imageView, this); 829 830 } else { 831 // We've scaled past our target zoom, so calculate the 832 // necessary scale so we're back at target zoom 833 final float delta = mTargetZoom / currentScale; 834 mSuppMatrix.postScale(delta, delta, mFocalX, mFocalY); 835 checkAndDisplayMatrix(); 836 } 837 } 838 } 839 } 840 841 private class FlingRunnable implements Runnable { 842 843 private final ScrollerProxy mScroller; 844 private int mCurrentX, mCurrentY; 845 846 public FlingRunnable(Context context) { 847 mScroller = ScrollerProxy.getScroller(context); 848 } 849 850 public void cancelFling() { 851 if (DEBUG) { 852 Log.d(LOG_TAG, "Cancel Fling"); 853 } 854 mScroller.forceFinished(true); 855 } 856 857 public void fling(int viewWidth, int viewHeight, int velocityX, int velocityY) { 858 final RectF rect = getDisplayRect(); 859 if (null == rect) { 860 return; 861 } 862 863 final int startX = Math.round(-rect.left); 864 final int minX, maxX, minY, maxY; 865 866 if (viewWidth < rect.width()) { 867 minX = 0; 868 maxX = Math.round(rect.width() - viewWidth); 869 } else { 870 minX = maxX = startX; 871 } 872 873 final int startY = Math.round(-rect.top); 874 if (viewHeight < rect.height()) { 875 minY = 0; 876 maxY = Math.round(rect.height() - viewHeight); 877 } else { 878 minY = maxY = startY; 879 } 880 881 mCurrentX = startX; 882 mCurrentY = startY; 883 884 if (DEBUG) { 885 Log.d(LOG_TAG, "fling. StartX:" + startX + " StartY:" + startY + " MaxX:" + maxX + " MaxY:" + maxY); 886 } 887 888 // If we actually can move, fling the scroller 889 if (startX != maxX || startY != maxY) { 890 mScroller.fling(startX, startY, velocityX, velocityY, minX, maxX, minY, maxY, 0, 0); 891 } 892 } 893 894 @Override 895 public void run() { 896 ImageView imageView = getImageView(); 897 if (null != imageView && mScroller.computeScrollOffset()) { 898 899 final int newX = mScroller.getCurrX(); 900 final int newY = mScroller.getCurrY(); 901 902 if (DEBUG) { 903 Log.d(LOG_TAG, "fling run(). CurrentX:" + mCurrentX + " CurrentY:" + mCurrentY + " NewX:" + newX 904 + " NewY:" + newY); 905 } 906 907 mSuppMatrix.postTranslate(mCurrentX - newX, mCurrentY - newY); 908 setImageViewMatrix(getDisplayMatrix()); 909 910 mCurrentX = newX; 911 mCurrentY = newY; 912 913 // Post On animation 914 Compat.postOnAnimation(imageView, this); 915 } 916 } 917 } 918 }
1 public abstract class VersionedGestureDetector { 2 static final String LOG_TAG = "VersionedGestureDetector"; 3 OnGestureListener mListener; 4 5 public static VersionedGestureDetector newInstance(Context context, OnGestureListener listener) { 6 final int sdkVersion = Build.VERSION.SDK_INT; 7 VersionedGestureDetector detector = null; 8 9 if (sdkVersion < Build.VERSION_CODES.ECLAIR) { 10 detector = new CupcakeDetector(context); 11 } else if (sdkVersion < Build.VERSION_CODES.FROYO) { 12 detector = new EclairDetector(context); 13 } else { 14 detector = new FroyoDetector(context); 15 } 16 17 detector.mListener = listener; 18 19 return detector; 20 } 21 22 public abstract boolean onTouchEvent(MotionEvent ev); 23 24 public abstract boolean isScaling(); 25 26 public static interface OnGestureListener { 27 public void onDrag(float dx, float dy); 28 29 public void onFling(float startX, float startY, float velocityX, float velocityY); 30 31 public void onScale(float scaleFactor, float focusX, float focusY); 32 } 33 34 private static class CupcakeDetector extends VersionedGestureDetector { 35 36 float mLastTouchX; 37 float mLastTouchY; 38 final float mTouchSlop; 39 final float mMinimumVelocity; 40 41 public CupcakeDetector(Context context) { 42 final ViewConfiguration configuration = ViewConfiguration.get(context); 43 mMinimumVelocity = configuration.getScaledMinimumFlingVelocity(); 44 mTouchSlop = configuration.getScaledTouchSlop(); 45 } 46 47 private VelocityTracker mVelocityTracker; 48 private boolean mIsDragging; 49 50 float getActiveX(MotionEvent ev) { 51 return ev.getX(); 52 } 53 54 float getActiveY(MotionEvent ev) { 55 return ev.getY(); 56 } 57 58 public boolean isScaling() { 59 return false; 60 } 61 62 @Override 63 public boolean onTouchEvent(MotionEvent ev) { 64 switch (ev.getAction()) { 65 case MotionEvent.ACTION_DOWN: { 66 mVelocityTracker = VelocityTracker.obtain(); 67 mVelocityTracker.addMovement(ev); 68 69 mLastTouchX = getActiveX(ev); 70 mLastTouchY = getActiveY(ev); 71 mIsDragging = false; 72 break; 73 } 74 75 case MotionEvent.ACTION_MOVE: { 76 final float x = getActiveX(ev); 77 final float y = getActiveY(ev); 78 final float dx = x - mLastTouchX, dy = y - mLastTouchY; 79 80 if (!mIsDragging) { 81 // Use Pythagoras to see if drag length is larger than 82 // touch slop 83 mIsDragging = Math.sqrt((dx * dx) + (dy * dy)) >= mTouchSlop; 84 } 85 86 if (mIsDragging) { 87 mListener.onDrag(dx, dy); 88 mLastTouchX = x; 89 mLastTouchY = y; 90 91 if (null != mVelocityTracker) { 92 mVelocityTracker.addMovement(ev); 93 } 94 } 95 break; 96 } 97 98 case MotionEvent.ACTION_CANCEL: { 99 // Recycle Velocity Tracker 100 if (null != mVelocityTracker) { 101 mVelocityTracker.recycle(); 102 mVelocityTracker = null; 103 } 104 break; 105 } 106 107 case MotionEvent.ACTION_UP: { 108 if (mIsDragging) { 109 if (null != mVelocityTracker) { 110 mLastTouchX = getActiveX(ev); 111 mLastTouchY = getActiveY(ev); 112 113 // Compute velocity within the last 1000ms 114 mVelocityTracker.addMovement(ev); 115 mVelocityTracker.computeCurrentVelocity(1000); 116 117 final float vX = mVelocityTracker.getXVelocity(), vY = mVelocityTracker.getYVelocity(); 118 119 // If the velocity is greater than minVelocity, call 120 // listener 121 if (Math.max(Math.abs(vX), Math.abs(vY)) >= mMinimumVelocity) { 122 mListener.onFling(mLastTouchX, mLastTouchY, -vX, -vY); 123 } 124 } 125 } 126 127 // Recycle Velocity Tracker 128 if (null != mVelocityTracker) { 129 mVelocityTracker.recycle(); 130 mVelocityTracker = null; 131 } 132 break; 133 } 134 } 135 136 return true; 137 } 138 } 139 140 @TargetApi(5) 141 private static class EclairDetector extends CupcakeDetector { 142 private static final int INVALID_POINTER_ID = -1; 143 private int mActivePointerId = INVALID_POINTER_ID; 144 private int mActivePointerIndex = 0; 145 146 public EclairDetector(Context context) { 147 super(context); 148 } 149 150 @Override 151 float getActiveX(MotionEvent ev) { 152 try { 153 return ev.getX(mActivePointerIndex); 154 } catch (Exception e) { 155 return ev.getX(); 156 } 157 } 158 159 @Override 160 float getActiveY(MotionEvent ev) { 161 try { 162 return ev.getY(mActivePointerIndex); 163 } catch (Exception e) { 164 return ev.getY(); 165 } 166 } 167 168 @Override 169 public boolean onTouchEvent(MotionEvent ev) { 170 final int action = ev.getAction(); 171 switch (action & MotionEvent.ACTION_MASK) { 172 case MotionEvent.ACTION_DOWN: 173 mActivePointerId = ev.getPointerId(0); 174 break; 175 case MotionEvent.ACTION_CANCEL: 176 case MotionEvent.ACTION_UP: 177 mActivePointerId = INVALID_POINTER_ID; 178 break; 179 case MotionEvent.ACTION_POINTER_UP: 180 final int pointerIndex = (ev.getAction() & MotionEvent.ACTION_POINTER_INDEX_MASK) >> MotionEvent.ACTION_POINTER_INDEX_SHIFT; 181 final int pointerId = ev.getPointerId(pointerIndex); 182 if (pointerId == mActivePointerId) { 183 // This was our active pointer going up. Choose a new 184 // active pointer and adjust accordingly. 185 final int newPointerIndex = pointerIndex == 0 ? 1 : 0; 186 mActivePointerId = ev.getPointerId(newPointerIndex); 187 mLastTouchX = ev.getX(newPointerIndex); 188 mLastTouchY = ev.getY(newPointerIndex); 189 } 190 break; 191 } 192 193 mActivePointerIndex = ev.findPointerIndex(mActivePointerId != INVALID_POINTER_ID ? mActivePointerId : 0); 194 return super.onTouchEvent(ev); 195 } 196 } 197 198 @TargetApi(8) 199 private static class FroyoDetector extends EclairDetector { 200 201 private final ScaleGestureDetector mDetector; 202 203 // Needs to be an inner class so that we don't hit 204 // VerifyError's on API 4. 205 private final OnScaleGestureListener mScaleListener = new OnScaleGestureListener() { 206 207 @Override 208 public boolean onScale(ScaleGestureDetector detector) { 209 mListener.onScale(detector.getScaleFactor(), detector.getFocusX(), detector.getFocusY()); 210 return true; 211 } 212 213 @Override 214 public boolean onScaleBegin(ScaleGestureDetector detector) { 215 return true; 216 } 217 218 @Override 219 public void onScaleEnd(ScaleGestureDetector detector) { 220 // NO-OP 221 } 222 }; 223 224 public FroyoDetector(Context context) { 225 super(context); 226 mDetector = new ScaleGestureDetector(context, mScaleListener); 227 } 228 229 @Override 230 public boolean isScaling() { 231 return mDetector.isInProgress(); 232 } 233 234 @Override 235 public boolean onTouchEvent(MotionEvent ev) { 236 mDetector.onTouchEvent(ev); 237 return super.onTouchEvent(ev); 238 } 239 240 }