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 | package github.xuqk.kdimageviewer.photoview; 17 | 18 | import android.content.Context; 19 | import android.graphics.Matrix; 20 | import android.graphics.RectF; 21 | import android.graphics.drawable.Drawable; 22 | import android.net.Uri; 23 | import android.util.AttributeSet; 24 | import android.view.GestureDetector; 25 | 26 | import androidx.appcompat.widget.AppCompatImageView; 27 | 28 | ; 29 | 30 | /** 31 | * A modified version for https://github.com/chrisbanes/PhotoView. 32 | */ 33 | @SuppressWarnings("unused") 34 | public class PhotoView extends AppCompatImageView { 35 | 36 | public PhotoViewAttacher attacher; 37 | private ScaleType pendingScaleType; 38 | 39 | public PhotoView(Context context) { 40 | this(context, null); 41 | } 42 | 43 | public PhotoView(Context context, AttributeSet attr) { 44 | this(context, attr, 0); 45 | } 46 | 47 | public PhotoView(Context context, AttributeSet attr, int defStyle) { 48 | super(context, attr, defStyle); 49 | init(); 50 | } 51 | 52 | private void init() { 53 | attacher = new PhotoViewAttacher(this); 54 | //We always pose as a Matrix scale type, though we can change to another scale type 55 | //via the attacher 56 | super.setScaleType(ScaleType.MATRIX); 57 | //apply the previously applied scale type 58 | if (pendingScaleType != null) { 59 | setScaleType(pendingScaleType); 60 | pendingScaleType = null; 61 | } 62 | } 63 | 64 | /** 65 | * Get the current {@link PhotoViewAttacher} for this view. Be wary of holding on to references 66 | * to this attacher, as it has a reference to this view, which, if a reference is held in the 67 | * wrong place, can cause memory leaks. 68 | * 69 | * @return the attacher. 70 | */ 71 | public PhotoViewAttacher getAttacher() { 72 | return attacher; 73 | } 74 | 75 | @Override 76 | public ScaleType getScaleType() { 77 | return attacher.getScaleType(); 78 | } 79 | 80 | @Override 81 | public Matrix getImageMatrix() { 82 | return attacher.getImageMatrix(); 83 | } 84 | 85 | @Override 86 | public void setOnLongClickListener(OnLongClickListener l) { 87 | attacher.setOnLongClickListener(l); 88 | } 89 | 90 | @Override 91 | public void setOnClickListener(OnClickListener l) { 92 | attacher.setOnClickListener(l); 93 | } 94 | 95 | @Override 96 | public void setScaleType(ScaleType scaleType) { 97 | if (attacher == null) { 98 | pendingScaleType = scaleType; 99 | } else { 100 | attacher.setScaleType(scaleType); 101 | } 102 | } 103 | 104 | @Override 105 | public void setImageDrawable(Drawable drawable) { 106 | super.setImageDrawable(drawable); 107 | // setImageBitmap calls through to this method 108 | if (attacher != null) { 109 | attacher.update(); 110 | } 111 | } 112 | 113 | @Override 114 | public void setImageResource(int resId) { 115 | super.setImageResource(resId); 116 | if (attacher != null) { 117 | attacher.update(); 118 | } 119 | } 120 | 121 | @Override 122 | public void setImageURI(Uri uri) { 123 | super.setImageURI(uri); 124 | if (attacher != null) { 125 | attacher.update(); 126 | } 127 | } 128 | 129 | @Override 130 | protected boolean setFrame(int l, int t, int r, int b) { 131 | boolean changed = super.setFrame(l, t, r, b); 132 | if (changed) { 133 | attacher.update(); 134 | } 135 | return changed; 136 | } 137 | 138 | public void setRotationTo(float rotationDegree) { 139 | attacher.setRotationTo(rotationDegree); 140 | } 141 | 142 | public void setRotationBy(float rotationDegree) { 143 | attacher.setRotationBy(rotationDegree); 144 | } 145 | 146 | public boolean isZoomable() { 147 | return attacher.isZoomable(); 148 | } 149 | 150 | public void setZoomable(boolean zoomable) { 151 | attacher.setZoomable(zoomable); 152 | } 153 | 154 | public RectF getDisplayRect() { 155 | return attacher.getDisplayRect(); 156 | } 157 | 158 | public void getDisplayMatrix(Matrix matrix) { 159 | attacher.getDisplayMatrix(matrix); 160 | } 161 | 162 | @SuppressWarnings("UnusedReturnValue") public boolean setDisplayMatrix(Matrix finalRectangle) { 163 | return attacher.setDisplayMatrix(finalRectangle); 164 | } 165 | 166 | public void getSuppMatrix(Matrix matrix) { 167 | attacher.getSuppMatrix(matrix); 168 | } 169 | 170 | public boolean setSuppMatrix(Matrix matrix) { 171 | return attacher.setDisplayMatrix(matrix); 172 | } 173 | 174 | public float getMinimumScale() { 175 | return attacher.getMinimumScale(); 176 | } 177 | 178 | public float getMediumScale() { 179 | return attacher.getMediumScale(); 180 | } 181 | 182 | public float getMaximumScale() { 183 | return attacher.getMaximumScale(); 184 | } 185 | 186 | public float getScale() { 187 | return attacher.getScale(); 188 | } 189 | 190 | public void setAllowParentInterceptOnEdge(boolean allow) { 191 | attacher.setAllowParentInterceptOnEdge(allow); 192 | } 193 | 194 | public void setMinimumScale(float minimumScale) { 195 | attacher.setMinimumScale(minimumScale); 196 | } 197 | 198 | public void setMediumScale(float mediumScale) { 199 | attacher.setMediumScale(mediumScale); 200 | } 201 | 202 | public void setMaximumScale(float maximumScale) { 203 | attacher.setMaximumScale(maximumScale); 204 | } 205 | 206 | public void setScaleLevels(float minimumScale, float mediumScale, float maximumScale) { 207 | attacher.setScaleLevels(minimumScale, mediumScale, maximumScale); 208 | } 209 | 210 | public void setOnMatrixChangeListener(OnMatrixChangedListener listener) { 211 | attacher.setOnMatrixChangeListener(listener); 212 | } 213 | 214 | public void setOnPhotoTapListener(OnPhotoTapListener listener) { 215 | attacher.setOnPhotoTapListener(listener); 216 | } 217 | 218 | public void setOnOutsidePhotoTapListener(OnOutsidePhotoTapListener listener) { 219 | attacher.setOnOutsidePhotoTapListener(listener); 220 | } 221 | 222 | public void setOnViewTapListener(OnViewTapListener listener) { 223 | attacher.setOnViewTapListener(listener); 224 | } 225 | 226 | public void setOnViewDragListener(OnViewDragListener listener) { 227 | attacher.setOnViewDragListener(listener); 228 | } 229 | 230 | public void setScale(float scale) { 231 | attacher.setScale(scale); 232 | } 233 | 234 | public void setScale(float scale, boolean animate) { 235 | attacher.setScale(scale, animate); 236 | } 237 | 238 | public void setScale(float scale, float focalX, float focalY, boolean animate) { 239 | attacher.setScale(scale, focalX, focalY, animate); 240 | } 241 | 242 | public void setZoomTransitionDuration(int milliseconds) { 243 | attacher.setZoomTransitionDuration(milliseconds); 244 | } 245 | 246 | public void setOnDoubleTapListener(GestureDetector.OnDoubleTapListener onDoubleTapListener) { 247 | attacher.setOnDoubleTapListener(onDoubleTapListener); 248 | } 249 | 250 | public void setOnScaleChangeListener(OnScaleChangedListener onScaleChangedListener) { 251 | attacher.setOnScaleChangeListener(onScaleChangedListener); 252 | } 253 | 254 | public void setOnSingleFlingListener(OnSingleFlingListener onSingleFlingListener) { 255 | attacher.setOnSingleFlingListener(onSingleFlingListener); 256 | } 257 | } 258 | -------------------------------------------------------------------------------- /kdimageviewer/src/main/java/github/xuqk/kdimageviewer/photoview/PhotoViewAttacher.java: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2011, 2012 Chris Banes. 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 | package github.xuqk.kdimageviewer.photoview; 17 | 18 | import android.content.Context; 19 | import android.graphics.Matrix; 20 | import android.graphics.Matrix.ScaleToFit; 21 | import android.graphics.RectF; 22 | import android.graphics.drawable.Drawable; 23 | import android.view.GestureDetector; 24 | import android.view.MotionEvent; 25 | import android.view.View; 26 | import android.view.View.OnLongClickListener; 27 | import android.view.ViewParent; 28 | import android.view.animation.AccelerateDecelerateInterpolator; 29 | import android.view.animation.Interpolator; 30 | import android.widget.ImageView; 31 | import android.widget.ImageView.ScaleType; 32 | import android.widget.OverScroller; 33 | 34 | 35 | /** 36 | * The component of which does the work allowing for zooming, scaling, panning, etc. 37 | * It is made public in case you need to subclass something other than AppCompatImageView and still 38 | * gain the functionality that {@link PhotoView} offers 39 | */ 40 | public class PhotoViewAttacher implements View.OnTouchListener, 41 | View.OnLayoutChangeListener { 42 | 43 | private static float DEFAULT_MAX_SCALE = 4.0f; 44 | private static float DEFAULT_MID_SCALE = 2.5f; 45 | private static float DEFAULT_MIN_SCALE = 1.0f; 46 | private static int DEFAULT_ZOOM_DURATION = 200; 47 | 48 | private static final int HORIZONTAL_EDGE_NONE = -1; 49 | private static final int HORIZONTAL_EDGE_LEFT = 0; 50 | private static final int HORIZONTAL_EDGE_RIGHT = 1; 51 | private static final int HORIZONTAL_EDGE_BOTH = 2; 52 | private static final int VERTICAL_EDGE_NONE = -1; 53 | private static final int VERTICAL_EDGE_TOP = 0; 54 | private static final int VERTICAL_EDGE_BOTTOM = 1; 55 | private static final int VERTICAL_EDGE_BOTH = 2; 56 | private static int SINGLE_TOUCH = 1; 57 | 58 | private Interpolator mInterpolator = new AccelerateDecelerateInterpolator(); 59 | private int mZoomDuration = DEFAULT_ZOOM_DURATION; 60 | private float mMinScale = DEFAULT_MIN_SCALE; 61 | private float mMidScale = DEFAULT_MID_SCALE; 62 | private float mMaxScale = DEFAULT_MAX_SCALE; 63 | 64 | private boolean mAllowParentInterceptOnEdge = true; 65 | private boolean mBlockParentIntercept = false; 66 | 67 | private ImageView mImageView; 68 | 69 | // Gesture Detectors 70 | private GestureDetector mGestureDetector; 71 | private CustomGestureDetector mScaleDragDetector; 72 | 73 | // These are set so we don't keep allocating them on the heap 74 | private final Matrix mBaseMatrix = new Matrix(); 75 | private final Matrix mDrawMatrix = new Matrix(); 76 | private final Matrix mSuppMatrix = new Matrix(); 77 | private final RectF mDisplayRect = new RectF(); 78 | private final float[] mMatrixValues = new float[9]; 79 | 80 | // Listeners 81 | private OnMatrixChangedListener mMatrixChangeListener; 82 | private OnPhotoTapListener mPhotoTapListener; 83 | private OnOutsidePhotoTapListener mOutsidePhotoTapListener; 84 | private OnViewTapListener mViewTapListener; 85 | private View.OnClickListener mOnClickListener; 86 | private OnLongClickListener mLongClickListener; 87 | private OnScaleChangedListener mScaleChangeListener; 88 | private OnSingleFlingListener mSingleFlingListener; 89 | private OnViewDragListener mOnViewDragListener; 90 | 91 | private FlingRunnable mCurrentFlingRunnable; 92 | private int mHorizontalScrollEdge = HORIZONTAL_EDGE_BOTH; 93 | private int mVerticalScrollEdge = VERTICAL_EDGE_BOTH; 94 | private float mBaseRotation; 95 | public boolean isTopEnd, isBottomEnd, isLeftEnd, isRightEnd = false; 96 | public boolean isVertical, isHorizontal; 97 | private boolean mZoomEnabled = true; 98 | private boolean isLongImage = false;//是否是长图 99 | private ScaleType mScaleType = ScaleType.FIT_CENTER; 100 | private OnGestureListener onGestureListener = new OnGestureListener() { 101 | @Override 102 | public void onDrag(float dx, float dy) { 103 | if (mScaleDragDetector.isScaling()) { 104 | return; // Do not drag if we are already scaling 105 | } 106 | if (mOnViewDragListener != null) { 107 | mOnViewDragListener.onDrag(dx, dy); 108 | } 109 | mSuppMatrix.postTranslate(dx, dy); 110 | checkAndDisplayMatrix(); 111 | isTopEnd = (mVerticalScrollEdge == VERTICAL_EDGE_TOP) && getScale() != 1f; 112 | isBottomEnd = (mVerticalScrollEdge == VERTICAL_EDGE_BOTTOM) && getScale() != 1f; 113 | isLeftEnd = (mHorizontalScrollEdge == HORIZONTAL_EDGE_LEFT) && getScale() != 1f; 114 | isRightEnd = (mHorizontalScrollEdge == HORIZONTAL_EDGE_RIGHT) && getScale() != 1f; 115 | 116 | ViewParent parent = mImageView.getParent(); 117 | if (parent == null) return; 118 | if (mAllowParentInterceptOnEdge && !mScaleDragDetector.isScaling() && !mBlockParentIntercept) { 119 | if ((mHorizontalScrollEdge == HORIZONTAL_EDGE_BOTH && !isLongImage) 120 | || (mHorizontalScrollEdge == HORIZONTAL_EDGE_LEFT && dx >= 0f && isHorizontal) 121 | || (mHorizontalScrollEdge == HORIZONTAL_EDGE_RIGHT && dx <= -0f && isHorizontal) 122 | // || (mVerticalScrollEdge == VERTICAL_EDGE_TOP && dy >= 1f) 123 | // || (mVerticalScrollEdge == VERTICAL_EDGE_BOTTOM && dy <= -1f) 124 | ) { 125 | parent.requestDisallowInterceptTouchEvent(false); 126 | } else if ((mVerticalScrollEdge == VERTICAL_EDGE_BOTH && isVertical) 127 | || (isTopEnd && dy > 0 && isVertical) 128 | || (isBottomEnd && dy < 0 && isVertical)) { 129 | parent.requestDisallowInterceptTouchEvent(false); 130 | } else if (isLongImage) { 131 | //长图特殊上下滑动 132 | if ((mVerticalScrollEdge == VERTICAL_EDGE_TOP && dy > 0 && isVertical) 133 | || (mVerticalScrollEdge == VERTICAL_EDGE_BOTTOM && dy < 0 && isVertical)) { 134 | parent.requestDisallowInterceptTouchEvent(false); 135 | } 136 | } 137 | 138 | } else { 139 | if (mHorizontalScrollEdge == HORIZONTAL_EDGE_BOTH && isLongImage && isHorizontal) { 140 | //长图左右滑动 141 | parent.requestDisallowInterceptTouchEvent(false); 142 | }else{ 143 | parent.requestDisallowInterceptTouchEvent(true); 144 | } 145 | } 146 | } 147 | 148 | @Override 149 | public void onFling(float startX, float startY, float velocityX, float velocityY) { 150 | mCurrentFlingRunnable = new FlingRunnable(mImageView.getContext()); 151 | mCurrentFlingRunnable.fling(getImageViewWidth(mImageView), 152 | getImageViewHeight(mImageView), (int) velocityX, (int) velocityY); 153 | mImageView.post(mCurrentFlingRunnable); 154 | } 155 | 156 | @Override 157 | public void onScale(float scaleFactor, float focusX, float focusY) { 158 | if (getScale() < mMaxScale || scaleFactor < 1f) { 159 | if (mScaleChangeListener != null) { 160 | mScaleChangeListener.onScaleChange(scaleFactor, focusX, focusY); 161 | } 162 | mSuppMatrix.postScale(scaleFactor, scaleFactor, focusX, focusY); 163 | checkAndDisplayMatrix(); 164 | } 165 | } 166 | }; 167 | 168 | public PhotoViewAttacher(ImageView imageView) { 169 | mImageView = imageView; 170 | imageView.setOnTouchListener(this); 171 | imageView.addOnLayoutChangeListener(this); 172 | if (imageView.isInEditMode()) { 173 | return; 174 | } 175 | mBaseRotation = 0.0f; 176 | // Create Gesture Detectors... 177 | mScaleDragDetector = new CustomGestureDetector(imageView.getContext(), onGestureListener); 178 | mGestureDetector = new GestureDetector(imageView.getContext(), new GestureDetector.SimpleOnGestureListener() { 179 | // forward long click listener 180 | @Override 181 | public void onLongPress(MotionEvent e) { 182 | if (mLongClickListener != null) { 183 | mLongClickListener.onLongClick(mImageView); 184 | } 185 | } 186 | 187 | @Override 188 | public boolean onFling(MotionEvent e1, MotionEvent e2, 189 | float velocityX, float velocityY) { 190 | if (mSingleFlingListener != null) { 191 | if (getScale() > DEFAULT_MIN_SCALE) { 192 | return false; 193 | } 194 | if (e1.getPointerCount() > SINGLE_TOUCH 195 | || e2.getPointerCount() > SINGLE_TOUCH) { 196 | return false; 197 | } 198 | return mSingleFlingListener.onFling(e1, e2, velocityX, velocityY); 199 | } 200 | return false; 201 | } 202 | }); 203 | mGestureDetector.setOnDoubleTapListener(new GestureDetector.OnDoubleTapListener() { 204 | @Override 205 | public boolean onSingleTapConfirmed(MotionEvent e) { 206 | if (mOnClickListener != null) { 207 | mOnClickListener.onClick(mImageView); 208 | } 209 | final RectF displayRect = getDisplayRect(); 210 | final float x = e.getX(), y = e.getY(); 211 | if (mViewTapListener != null) { 212 | mViewTapListener.onViewTap(mImageView, x, y); 213 | } 214 | if (displayRect != null) { 215 | // Check to see if the user tapped on the photo 216 | if (displayRect.contains(x, y)) { 217 | float xResult = (x - displayRect.left) 218 | / displayRect.width(); 219 | float yResult = (y - displayRect.top) 220 | / displayRect.height(); 221 | if (mPhotoTapListener != null) { 222 | mPhotoTapListener.onPhotoTap(mImageView, xResult, yResult); 223 | } 224 | return true; 225 | } else { 226 | if (mOutsidePhotoTapListener != null) { 227 | mOutsidePhotoTapListener.onOutsidePhotoTap(mImageView); 228 | } 229 | } 230 | } 231 | return false; 232 | } 233 | 234 | @Override 235 | public boolean onDoubleTap(MotionEvent ev) { 236 | try { 237 | float scale = getScale(); 238 | float x = ev.getX(); 239 | float y = ev.getY(); 240 | if (scale < getMediumScale()) { 241 | setScale(getMediumScale(), x, y, true); 242 | } else if (scale >= getMediumScale() && scale < getMaximumScale()) { 243 | setScale(getMaximumScale(), x, y, true); 244 | } else { 245 | setScale(getMinimumScale(), x, y, true); 246 | } 247 | } catch (ArrayIndexOutOfBoundsException e) { 248 | // Can sometimes happen when getX() and getY() is called 249 | } 250 | return true; 251 | } 252 | 253 | @Override 254 | public boolean onDoubleTapEvent(MotionEvent e) { 255 | // Wait for the confirmed onDoubleTap() instead 256 | return true; 257 | } 258 | }); 259 | } 260 | 261 | public void setOnDoubleTapListener(GestureDetector.OnDoubleTapListener newOnDoubleTapListener) { 262 | this.mGestureDetector.setOnDoubleTapListener(newOnDoubleTapListener); 263 | } 264 | 265 | public void setOnScaleChangeListener(OnScaleChangedListener onScaleChangeListener) { 266 | this.mScaleChangeListener = onScaleChangeListener; 267 | } 268 | 269 | public void setOnSingleFlingListener(OnSingleFlingListener onSingleFlingListener) { 270 | this.mSingleFlingListener = onSingleFlingListener; 271 | } 272 | 273 | @Deprecated 274 | public boolean isZoomEnabled() { 275 | return mZoomEnabled; 276 | } 277 | 278 | public RectF getDisplayRect() { 279 | checkMatrixBounds(); 280 | return getDisplayRect(getDrawMatrix()); 281 | } 282 | 283 | public boolean setDisplayMatrix(Matrix finalMatrix) { 284 | if (finalMatrix == null) { 285 | throw new IllegalArgumentException("Matrix cannot be null"); 286 | } 287 | if (mImageView.getDrawable() == null) { 288 | return false; 289 | } 290 | mSuppMatrix.set(finalMatrix); 291 | checkAndDisplayMatrix(); 292 | return true; 293 | } 294 | 295 | public void setBaseRotation(final float degrees) { 296 | mBaseRotation = degrees % 360; 297 | update(); 298 | setRotationBy(mBaseRotation); 299 | checkAndDisplayMatrix(); 300 | } 301 | 302 | public void setRotationTo(float degrees) { 303 | mSuppMatrix.setRotate(degrees % 360); 304 | checkAndDisplayMatrix(); 305 | } 306 | 307 | public void setRotationBy(float degrees) { 308 | mSuppMatrix.postRotate(degrees % 360); 309 | checkAndDisplayMatrix(); 310 | } 311 | 312 | public float getMinimumScale() { 313 | return mMinScale; 314 | } 315 | 316 | public float getMediumScale() { 317 | return mMidScale; 318 | } 319 | 320 | public float getMaximumScale() { 321 | return mMaxScale; 322 | } 323 | 324 | public float getScale() { 325 | return (float) Math.sqrt((float) Math.pow(getValue(mSuppMatrix, Matrix.MSCALE_X), 2) + (float) Math.pow 326 | (getValue(mSuppMatrix, Matrix.MSKEW_Y), 2)); 327 | } 328 | 329 | public ScaleType getScaleType() { 330 | return mScaleType; 331 | } 332 | 333 | @Override 334 | public void onLayoutChange(View v, int left, int top, int right, int bottom, int oldLeft, int oldTop, int 335 | oldRight, int oldBottom) { 336 | // Update our base matrix, as the bounds have changed 337 | if (left != oldLeft || top != oldTop || right != oldRight || bottom != oldBottom) { 338 | updateBaseMatrix(mImageView.getDrawable()); 339 | } 340 | } 341 | 342 | float x, y; 343 | 344 | @Override 345 | public boolean onTouch(View v, MotionEvent ev) { 346 | boolean handled = false; 347 | if (mZoomEnabled && Util.hasDrawable((ImageView) v)) { 348 | switch (ev.getAction()) { 349 | case MotionEvent.ACTION_DOWN: 350 | x = ev.getX(); 351 | y = ev.getY(); 352 | ViewParent parent = v.getParent(); 353 | // First, disable the Parent from intercepting the touch 354 | // event 355 | // If we're flinging, and the user presses down, cancel 356 | // fling 357 | cancelFling(); 358 | if (parent != null) { 359 | parent.requestDisallowInterceptTouchEvent(true); 360 | } 361 | 362 | break; 363 | case MotionEvent.ACTION_CANCEL: 364 | case MotionEvent.ACTION_UP: 365 | isTopEnd = false; 366 | // If the user has zoomed less than min scale, zoom back 367 | // to min scale 368 | if (getScale() < mMinScale) { 369 | RectF rect = getDisplayRect(); 370 | if (rect != null) { 371 | v.post(new AnimatedZoomRunnable(getScale(), mMinScale, 372 | rect.centerX(), rect.centerY())); 373 | handled = true; 374 | } 375 | } else if (getScale() > mMaxScale) { 376 | RectF rect = getDisplayRect(); 377 | if (rect != null) { 378 | v.post(new AnimatedZoomRunnable(getScale(), mMaxScale, 379 | rect.centerX(), rect.centerY())); 380 | handled = true; 381 | } 382 | } 383 | break; 384 | case MotionEvent.ACTION_MOVE: 385 | float dx = Math.abs(ev.getX() - x); 386 | float dy = Math.abs(ev.getY() - y); 387 | if(isLongImage){ 388 | isVertical = dy > dx; 389 | isHorizontal = dx > dy * 2; 390 | }else { 391 | isVertical = (getScale() != 1.0 && dy > dx); 392 | isHorizontal = (getScale() != 1.0 && dx > dy * 2); 393 | } 394 | break; 395 | } 396 | // Try the Scale/Drag detector 397 | if (mScaleDragDetector != null) { 398 | boolean wasScaling = mScaleDragDetector.isScaling(); 399 | boolean wasDragging = mScaleDragDetector.isDragging(); 400 | handled = mScaleDragDetector.onTouchEvent(ev); 401 | boolean didntScale = !wasScaling && !mScaleDragDetector.isScaling(); 402 | boolean didntDrag = !wasDragging && !mScaleDragDetector.isDragging(); 403 | mBlockParentIntercept = didntScale && didntDrag; 404 | } 405 | // Check to see if the user double tapped 406 | if (mGestureDetector != null && mGestureDetector.onTouchEvent(ev)) { 407 | handled = true; 408 | } 409 | 410 | } 411 | return handled; 412 | } 413 | 414 | public void setAllowParentInterceptOnEdge(boolean allow) { 415 | mAllowParentInterceptOnEdge = allow; 416 | } 417 | 418 | public void setMinimumScale(float minimumScale) { 419 | Util.checkZoomLevels(minimumScale, mMidScale, mMaxScale); 420 | mMinScale = minimumScale; 421 | } 422 | 423 | public void setMediumScale(float mediumScale) { 424 | Util.checkZoomLevels(mMinScale, mediumScale, mMaxScale); 425 | mMidScale = mediumScale; 426 | } 427 | 428 | public void setMaximumScale(float maximumScale) { 429 | Util.checkZoomLevels(mMinScale, mMidScale, maximumScale); 430 | mMaxScale = maximumScale; 431 | } 432 | 433 | public void setScaleLevels(float minimumScale, float mediumScale, float maximumScale) { 434 | Util.checkZoomLevels(minimumScale, mediumScale, maximumScale); 435 | mMinScale = minimumScale; 436 | mMidScale = mediumScale; 437 | mMaxScale = maximumScale; 438 | } 439 | 440 | public void setOnLongClickListener(OnLongClickListener listener) { 441 | mLongClickListener = listener; 442 | } 443 | 444 | public void setOnClickListener(View.OnClickListener listener) { 445 | mOnClickListener = listener; 446 | } 447 | 448 | public void setOnMatrixChangeListener(OnMatrixChangedListener listener) { 449 | mMatrixChangeListener = listener; 450 | } 451 | 452 | public void setOnPhotoTapListener(OnPhotoTapListener listener) { 453 | mPhotoTapListener = listener; 454 | } 455 | 456 | public void setOnOutsidePhotoTapListener(OnOutsidePhotoTapListener mOutsidePhotoTapListener) { 457 | this.mOutsidePhotoTapListener = mOutsidePhotoTapListener; 458 | } 459 | 460 | public void setOnViewTapListener(OnViewTapListener listener) { 461 | mViewTapListener = listener; 462 | } 463 | 464 | public void setOnViewDragListener(OnViewDragListener listener) { 465 | mOnViewDragListener = listener; 466 | } 467 | 468 | public void setScale(float scale) { 469 | setScale(scale, false); 470 | } 471 | 472 | public void setScale(float scale, boolean animate) { 473 | setScale(scale, (mImageView.getRight()) / 2, (mImageView.getBottom()) / 2, animate); 474 | } 475 | 476 | public void setScale(float scale, float focalX, float focalY, 477 | boolean animate) { 478 | // Check to see if the scale is within bounds 479 | // if (scale < mMinScale || scale > mMaxScale) { 480 | // throw new IllegalArgumentException("Scale must be within the range of minScale and maxScale"); 481 | // } 482 | if (animate) { 483 | mImageView.post(new AnimatedZoomRunnable(getScale(), scale, 484 | focalX, focalY)); 485 | } else { 486 | mSuppMatrix.setScale(scale, scale, focalX, focalY); 487 | checkAndDisplayMatrix(); 488 | } 489 | } 490 | 491 | /** 492 | * Set the zoom interpolator 493 | * 494 | * @param interpolator the zoom interpolator 495 | */ 496 | public void setZoomInterpolator(Interpolator interpolator) { 497 | mInterpolator = interpolator; 498 | } 499 | 500 | public void setScaleType(ScaleType scaleType) { 501 | if (Util.isSupportedScaleType(scaleType) && scaleType != mScaleType) { 502 | mScaleType = scaleType; 503 | update(); 504 | } 505 | } 506 | 507 | public boolean isZoomable() { 508 | return mZoomEnabled; 509 | } 510 | 511 | public void setZoomable(boolean zoomable) { 512 | mZoomEnabled = zoomable; 513 | update(); 514 | } 515 | 516 | public void update() { 517 | if (mZoomEnabled) { 518 | // Update the base matrix using the current drawable 519 | updateBaseMatrix(mImageView.getDrawable()); 520 | } else { 521 | // Reset the Matrix... 522 | resetMatrix(); 523 | } 524 | } 525 | 526 | /** 527 | * Get the display matrix 528 | * 529 | * @param matrix target matrix to copy to 530 | */ 531 | public void getDisplayMatrix(Matrix matrix) { 532 | matrix.set(getDrawMatrix()); 533 | } 534 | 535 | /** 536 | * Get the current support matrix 537 | */ 538 | public void getSuppMatrix(Matrix matrix) { 539 | matrix.set(mSuppMatrix); 540 | } 541 | 542 | private Matrix getDrawMatrix() { 543 | mDrawMatrix.set(mBaseMatrix); 544 | mDrawMatrix.postConcat(mSuppMatrix); 545 | return mDrawMatrix; 546 | } 547 | 548 | public Matrix getImageMatrix() { 549 | return mDrawMatrix; 550 | } 551 | 552 | public void setZoomTransitionDuration(int milliseconds) { 553 | this.mZoomDuration = milliseconds; 554 | } 555 | 556 | /** 557 | * Helper method that 'unpacks' a Matrix and returns the required value 558 | * 559 | * @param matrix Matrix to unpack 560 | * @param whichValue Which value from Matrix.M* to return 561 | * @return returned value 562 | */ 563 | public float getValue(Matrix matrix, int whichValue) { 564 | matrix.getValues(mMatrixValues); 565 | return mMatrixValues[whichValue]; 566 | } 567 | 568 | /** 569 | * Resets the Matrix back to FIT_CENTER, and then displays its contents 570 | */ 571 | private void resetMatrix() { 572 | mSuppMatrix.reset(); 573 | setRotationBy(mBaseRotation); 574 | setImageViewMatrix(getDrawMatrix()); 575 | checkMatrixBounds(); 576 | } 577 | 578 | private void setImageViewMatrix(Matrix matrix) { 579 | mImageView.setImageMatrix(matrix); 580 | // Call MatrixChangedListener if needed 581 | if (mMatrixChangeListener != null) { 582 | RectF displayRect = getDisplayRect(matrix); 583 | if (displayRect != null) { 584 | mMatrixChangeListener.onMatrixChanged(displayRect); 585 | } 586 | } 587 | } 588 | 589 | /** 590 | * Helper method that simply checks the Matrix, and then displays the result 591 | */ 592 | private void checkAndDisplayMatrix() { 593 | if (checkMatrixBounds()) { 594 | setImageViewMatrix(getDrawMatrix()); 595 | } 596 | } 597 | 598 | /** 599 | * Helper method that maps the supplied Matrix to the current Drawable 600 | * 601 | * @param matrix - Matrix to map Drawable against 602 | * @return RectF - Displayed Rectangle 603 | */ 604 | private RectF getDisplayRect(Matrix matrix) { 605 | Drawable d = mImageView.getDrawable(); 606 | if (d != null) { 607 | mDisplayRect.set(0, 0, d.getIntrinsicWidth(), 608 | d.getIntrinsicHeight()); 609 | matrix.mapRect(mDisplayRect); 610 | return mDisplayRect; 611 | } 612 | return null; 613 | } 614 | 615 | /** 616 | * Calculate Matrix for FIT_CENTER 617 | * 618 | * @param drawable - Drawable being displayed 619 | */ 620 | private void updateBaseMatrix(Drawable drawable) { 621 | if (drawable == null) { 622 | return; 623 | } 624 | final float viewWidth = getImageViewWidth(mImageView); 625 | final float viewHeight = getImageViewHeight(mImageView); 626 | final int drawableWidth = drawable.getIntrinsicWidth(); 627 | final int drawableHeight = drawable.getIntrinsicHeight(); 628 | mBaseMatrix.reset(); 629 | final float widthScale = viewWidth / drawableWidth; 630 | final float heightScale = viewHeight / drawableHeight; 631 | if (mScaleType == ScaleType.CENTER) { 632 | mBaseMatrix.postTranslate((viewWidth - drawableWidth) / 2F, 633 | (viewHeight - drawableHeight) / 2F); 634 | 635 | } else if (mScaleType == ScaleType.CENTER_CROP) { 636 | float scale = Math.max(widthScale, heightScale); 637 | mBaseMatrix.postScale(scale, scale); 638 | mBaseMatrix.postTranslate((viewWidth - drawableWidth * scale) / 2F, 639 | (viewHeight - drawableHeight * scale) / 2F); 640 | 641 | } else if (mScaleType == ScaleType.CENTER_INSIDE) { 642 | float scale = Math.min(1.0f, Math.min(widthScale, heightScale)); 643 | mBaseMatrix.postScale(scale, scale); 644 | mBaseMatrix.postTranslate((viewWidth - drawableWidth * scale) / 2F, 645 | (viewHeight - drawableHeight * scale) / 2F); 646 | 647 | } else { 648 | RectF mTempSrc = new RectF(0, 0, drawableWidth, drawableHeight); 649 | RectF mTempDst = new RectF(0, 0, viewWidth, viewHeight); 650 | if ((int) mBaseRotation % 180 != 0) { 651 | mTempSrc = new RectF(0, 0, drawableHeight, drawableWidth); 652 | } 653 | switch (mScaleType) { 654 | case FIT_CENTER: 655 | // for long image, 图片高>view高,比例也大于view的高/宽,则认为是长图 656 | if (drawableHeight > viewHeight && drawableHeight * 1f / drawableWidth > viewHeight * 1f / viewWidth) { 657 | // mBaseMatrix.postScale(widthScale, widthScale); 658 | // setScale(widthScale); 659 | //长图特殊处理,宽度撑满屏幕,并且顶部对齐 660 | isLongImage = true; 661 | mBaseMatrix.setRectToRect(mTempSrc, new RectF(0, 0, viewWidth, drawableHeight * widthScale), ScaleToFit.START); 662 | } else { 663 | mBaseMatrix.setRectToRect(mTempSrc, mTempDst, ScaleToFit.CENTER); 664 | } 665 | break; 666 | case FIT_START: 667 | mBaseMatrix.setRectToRect(mTempSrc, mTempDst, ScaleToFit.START); 668 | break; 669 | case FIT_END: 670 | mBaseMatrix.setRectToRect(mTempSrc, mTempDst, ScaleToFit.END); 671 | break; 672 | case FIT_XY: 673 | mBaseMatrix.setRectToRect(mTempSrc, mTempDst, ScaleToFit.FILL); 674 | break; 675 | default: 676 | break; 677 | } 678 | } 679 | resetMatrix(); 680 | } 681 | 682 | private boolean checkMatrixBounds() { 683 | final RectF rect = getDisplayRect(getDrawMatrix()); 684 | if (rect == null) { 685 | return false; 686 | } 687 | final float height = rect.height(), width = rect.width(); 688 | float deltaX = 0, deltaY = 0; 689 | final int viewHeight = getImageViewHeight(mImageView); 690 | if (height <= viewHeight && rect.top >= 0) { 691 | switch (mScaleType) { 692 | case FIT_START: 693 | deltaY = -rect.top; 694 | break; 695 | case FIT_END: 696 | deltaY = viewHeight - height - rect.top; 697 | break; 698 | default: 699 | deltaY = (viewHeight - height) / 2 - rect.top; 700 | break; 701 | } 702 | mVerticalScrollEdge = VERTICAL_EDGE_BOTH; 703 | } else if (rect.top >= 0) { 704 | mVerticalScrollEdge = VERTICAL_EDGE_TOP; 705 | deltaY = -rect.top; 706 | } else if (rect.bottom <= viewHeight) { 707 | mVerticalScrollEdge = VERTICAL_EDGE_BOTTOM; 708 | deltaY = viewHeight - rect.bottom; 709 | } else { 710 | mVerticalScrollEdge = VERTICAL_EDGE_NONE; 711 | } 712 | final int viewWidth = getImageViewWidth(mImageView); 713 | // Log.e("tag", "rect: " + rect.toShortString() + " viewWidth: " + viewWidth + " viewHeight: " + viewHeight 714 | // + " recLeft: " + rect.left + " recRight: " + rect.right + " mHorizontalScrollEdge: " + mHorizontalScrollEdge); 715 | if (width <= viewWidth && rect.left >= 0) { 716 | switch (mScaleType) { 717 | case FIT_START: 718 | deltaX = -rect.left; 719 | break; 720 | case FIT_END: 721 | deltaX = viewWidth - width - rect.left; 722 | break; 723 | default: 724 | deltaX = (viewWidth - width) / 2 - rect.left; 725 | break; 726 | } 727 | mHorizontalScrollEdge = HORIZONTAL_EDGE_BOTH; 728 | } else if (rect.left >= 0) { 729 | mHorizontalScrollEdge = HORIZONTAL_EDGE_LEFT; 730 | deltaX = -rect.left; 731 | } else if (rect.right <= viewWidth) { 732 | deltaX = viewWidth - rect.right; 733 | mHorizontalScrollEdge = HORIZONTAL_EDGE_RIGHT; 734 | } else { 735 | mHorizontalScrollEdge = HORIZONTAL_EDGE_NONE; 736 | } 737 | // Finally actually translate the matrix 738 | mSuppMatrix.postTranslate(deltaX, deltaY); 739 | return true; 740 | } 741 | 742 | private int getImageViewWidth(ImageView imageView) { 743 | return imageView.getWidth() - imageView.getPaddingLeft() - imageView.getPaddingRight(); 744 | } 745 | 746 | private int getImageViewHeight(ImageView imageView) { 747 | return imageView.getHeight() - imageView.getPaddingTop() - imageView.getPaddingBottom(); 748 | } 749 | 750 | private void cancelFling() { 751 | if (mCurrentFlingRunnable != null) { 752 | mCurrentFlingRunnable.cancelFling(); 753 | mCurrentFlingRunnable = null; 754 | } 755 | } 756 | 757 | private class AnimatedZoomRunnable implements Runnable { 758 | 759 | private final float mFocalX, mFocalY; 760 | private final long mStartTime; 761 | private final float mZoomStart, mZoomEnd; 762 | 763 | public AnimatedZoomRunnable(final float currentZoom, final float targetZoom, 764 | final float focalX, final float focalY) { 765 | mFocalX = focalX; 766 | mFocalY = focalY; 767 | mStartTime = System.currentTimeMillis(); 768 | mZoomStart = currentZoom; 769 | mZoomEnd = targetZoom; 770 | } 771 | 772 | @Override 773 | public void run() { 774 | float t = interpolate(); 775 | float scale = mZoomStart + t * (mZoomEnd - mZoomStart); 776 | float deltaScale = scale / getScale(); 777 | onGestureListener.onScale(deltaScale, mFocalX, mFocalY); 778 | // We haven't hit our target scale yet, so post ourselves again 779 | if (t < 1f) { 780 | Compat.postOnAnimation(mImageView, this); 781 | } 782 | } 783 | 784 | private float interpolate() { 785 | float t = 1f * (System.currentTimeMillis() - mStartTime) / mZoomDuration; 786 | t = Math.min(1f, t); 787 | t = mInterpolator.getInterpolation(t); 788 | return t; 789 | } 790 | } 791 | 792 | private class FlingRunnable implements Runnable { 793 | 794 | private final OverScroller mScroller; 795 | private int mCurrentX, mCurrentY; 796 | 797 | public FlingRunnable(Context context) { 798 | mScroller = new OverScroller(context); 799 | } 800 | 801 | public void cancelFling() { 802 | mScroller.forceFinished(true); 803 | } 804 | 805 | public void fling(int viewWidth, int viewHeight, int velocityX, 806 | int velocityY) { 807 | final RectF rect = getDisplayRect(); 808 | if (rect == null) { 809 | return; 810 | } 811 | final int startX = Math.round(-rect.left); 812 | final int minX, maxX, minY, maxY; 813 | if (viewWidth < rect.width()) { 814 | minX = 0; 815 | maxX = Math.round(rect.width() - viewWidth); 816 | } else { 817 | minX = maxX = startX; 818 | } 819 | final int startY = Math.round(-rect.top); 820 | if (viewHeight < rect.height()) { 821 | minY = 0; 822 | maxY = Math.round(rect.height() - viewHeight); 823 | } else { 824 | minY = maxY = startY; 825 | } 826 | mCurrentX = startX; 827 | mCurrentY = startY; 828 | // If we actually can move, fling the scroller 829 | if (startX != maxX || startY != maxY) { 830 | mScroller.fling(startX, startY, velocityX, velocityY, minX, 831 | maxX, minY, maxY, 0, 0); 832 | } 833 | } 834 | 835 | @Override 836 | public void run() { 837 | if (mScroller.isFinished()) { 838 | return; // remaining post that should not be handled 839 | } 840 | if (mScroller.computeScrollOffset()) { 841 | final int newX = mScroller.getCurrX(); 842 | final int newY = mScroller.getCurrY(); 843 | mSuppMatrix.postTranslate(mCurrentX - newX, mCurrentY - newY); 844 | checkAndDisplayMatrix(); 845 | mCurrentX = newX; 846 | mCurrentY = newY; 847 | // Post On animation 848 | Compat.postOnAnimation(mImageView, this); 849 | } 850 | } 851 | } 852 | } 853 | -------------------------------------------------------------------------------- /kdimageviewer/src/main/java/github/xuqk/kdimageviewer/photoview/Util.java: -------------------------------------------------------------------------------- 1 | package github.xuqk.kdimageviewer.photoview; 2 | 3 | import android.view.MotionEvent; 4 | import android.widget.ImageView; 5 | 6 | class Util { 7 | 8 | static void checkZoomLevels(float minZoom, float midZoom, 9 | float maxZoom) { 10 | if (minZoom >= midZoom) { 11 | throw new IllegalArgumentException( 12 | "Minimum zoom has to be less than Medium zoom. Call setMinimumZoom() with a more appropriate value"); 13 | } else if (midZoom >= maxZoom) { 14 | throw new IllegalArgumentException( 15 | "Medium zoom has to be less than Maximum zoom. Call setMaximumZoom() with a more appropriate value"); 16 | } 17 | } 18 | 19 | static boolean hasDrawable(ImageView imageView) { 20 | return imageView.getDrawable() != null; 21 | } 22 | 23 | static boolean isSupportedScaleType(final ImageView.ScaleType scaleType) { 24 | if (scaleType == null) { 25 | return false; 26 | } 27 | switch (scaleType) { 28 | case MATRIX: 29 | // throw new IllegalStateException("Matrix scale type is not supported"); 30 | return false; 31 | } 32 | return true; 33 | } 34 | 35 | static int getPointerIndex(int action) { 36 | return (action & MotionEvent.ACTION_POINTER_INDEX_MASK) >> MotionEvent.ACTION_POINTER_INDEX_SHIFT; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | include ':app', ':kdimageviewer' 2 | rootProject.name='Demo' 3 | --------------------------------------------------------------------------------