package com.dmsys.airdiskpro.ui.imagereader; |
| |
| import android.annotation.TargetApi; |
| import android.content.Context; |
| import android.graphics.Bitmap; |
| import android.graphics.Canvas; |
| import android.graphics.Matrix; |
| import android.graphics.Matrix.ScaleToFit; |
| import android.graphics.RectF; |
| import android.graphics.drawable.Drawable; |
| import android.net.Uri; |
| import android.os.Build.VERSION; |
| import android.os.Build.VERSION_CODES; |
| import android.util.AttributeSet; |
| import android.view.GestureDetector; |
| import android.view.MotionEvent; |
| import android.view.ScaleGestureDetector; |
| import android.view.ScaleGestureDetector.OnScaleGestureListener; |
| import android.view.VelocityTracker; |
| import android.view.View; |
| import android.view.ViewConfiguration; |
| import android.view.ViewTreeObserver; |
| import android.view.animation.Interpolator; |
| import android.view.animation.LinearInterpolator; |
| import android.widget.ImageView; |
| import android.widget.OverScroller; |
| import android.widget.Scroller; |
| |
| public class ZoomView extends ImageView implements View.OnTouchListener, |
| ViewTreeObserver.OnGlobalLayoutListener { |
| |
| /** |
| * Interface definition for a callback to be invoked when the Photo is |
| * tapped with a single tap. |
| * |
| * @author tomasz.zawada@gmail.com |
| */ |
| public static interface OnPhotoTapListener { |
| /** |
| * A callback to receive where the user taps on a photo. You will only |
| * receive a callback if the user taps on the actual photo, tapping on |
| * 'whitespace' will be ignored. |
| * |
| * @param view |
| * - View the user tapped. |
| * @param x |
| * - where the user tapped from the of the Drawable, as |
| * percentage of the Drawable width. |
| * @param y |
| * - where the user tapped from the top of the Drawable, as |
| * percentage of the Drawable height. |
| */ |
| public void onPhotoTap(View view, float x, float y); |
| } |
| |
| /** |
| * Interface definition for a callback to be invoked when the ImageView is |
| * tapped with a single tap. |
| * |
| * @author tomasz.zawada@gmail.com |
| */ |
| public static interface OnViewTapListener { |
| /** |
| * A callback to receive where the user taps on a ImageView. You will |
| * receive a callback if the user taps anywhere on the view, tapping on |
| * 'whitespace' will not be ignored. |
| * |
| * @param view |
| * - View the user tapped. |
| * @param x |
| * - where the user tapped from the left of the View. |
| * @param y |
| * - where the user tapped from the top of the View. |
| */ |
| public void onViewTap(View view, float x, float y); |
| } |
| public interface SingleTapConfirmedListener { |
| public void onSingleTapConfirmed(); |
| } |
| |
| /** |
| * |
| * The MultiGestureDetector manages the multi-finger pinch zoom, pan and tap |
| * |
| * @author tomasz.zawada@gmail.com |
| * |
| */ |
| private class MultiGestureDetector extends |
| GestureDetector.SimpleOnGestureListener implements |
| OnScaleGestureListener { |
| private final ScaleGestureDetector scaleGestureDetector; |
| private final GestureDetector gestureDetector; |
| |
| private VelocityTracker velocityTracker; |
| private boolean isDragging; |
| |
| private float lastTouchX; |
| private float lastTouchY; |
| private float lastPointerCount; |
| private final float scaledTouchSlop; |
| private final float scaledMinimumFlingVelocity; |
| |
| private final Matrix doubleTapMatrix = new Matrix(); |
| |
| private Float lastRotation;//上一次的角度 |
| private float startRotation;//開始時的角度 |
| private float startScale;//開始時的縮放 |
| private boolean isRepeatPointerDown = false;//第二個手指是否是抬起後重新按下 |
| |
| public MultiGestureDetector(Context context) { |
| scaleGestureDetector = new ScaleGestureDetector(context, this); |
| |
| gestureDetector = new GestureDetector(context, this); |
| gestureDetector.setOnDoubleTapListener(this); |
| final ViewConfiguration configuration = ViewConfiguration |
| .get(context); |
| scaledMinimumFlingVelocity = configuration |
| .getScaledMinimumFlingVelocity(); |
| scaledTouchSlop = configuration.getScaledTouchSlop(); |
| } |
| |
| |
| public boolean isScaling() { |
| return scaleGestureDetector.isInProgress(); |
| } |
| |
| public boolean onTouchEvent(MotionEvent event) { |
| |
| if (gestureDetector.onTouchEvent(event)) { |
| return true; |
| } |
| scaleGestureDetector.onTouchEvent(event); |
| if(isZoomEnabled){ |
| /* |
| * Get the center x, y of all the pointers |
| */ |
| float x = 0, y = 0; |
| final int pointerCount = event.getPointerCount(); |
| for (int i = 0; i < pointerCount; i++) { |
| x += event.getX(i); |
| y += event.getY(i); |
| } |
| x = x / pointerCount; |
| y = y / pointerCount; |
| |
| /* |
| * If the pointer count has changed cancel the drag |
| */ |
| if (pointerCount != lastPointerCount) { |
| isDragging = false; |
| if (velocityTracker != null) { |
| velocityTracker.clear(); |
| } |
| lastTouchX = x; |
| lastTouchY = y; |
| } |
| lastPointerCount = pointerCount; |
| switch (event.getAction() & MotionEvent.ACTION_MASK) { |
| case MotionEvent.ACTION_DOWN: |
| if (velocityTracker == null) { |
| velocityTracker = VelocityTracker.obtain(); |
| } else { |
| velocityTracker.clear(); |
| } |
| velocityTracker.addMovement(event); |
| |
| lastTouchX = x; |
| lastTouchY = y; |
| lastRotation = null; |
| isDragging = false; |
| break; |
| case MotionEvent.ACTION_POINTER_DOWN: |
| startRotation = rotation(event); |
| isDragging = false; |
| break; |
| case MotionEvent.ACTION_MOVE: { |
| final float dx = x - lastTouchX, dy = y - lastTouchY; |
| //只有一個手指的時候才可以拖拽 |
| if (isDragging == false && event.getPointerCount() == 1) { |
| // Use Pythagoras to see if drag length is larger than |
| // touch slop |
| isDragging = Math.sqrt((dx * dx) + (dy * dy)) >= scaledTouchSlop; |
| } |
| |
| if(event.getPointerCount() > 1){ |
| float rotation = rotation(event); |
| if(scaleNoRotation && isNewGesture){//如果只能縮放的話 判斷是否進入旋轉狀態 |
| if(Math.abs(rotation - startRotation) > 10){ |
| scaleNoRotation = false; |
| isNewGesture = false; |
| startRotationMatrix.set(getImageMatrix()); |
| } |
| } |
| if(!scaleNoRotation){ |
| if(lastRotation != null){ |
| if(isRepeatPointerDown){ |
| lastRotation = rotation; |
| isRepeatPointerDown = false; |
| }else{ |
| float deltaRotation = rotation - lastRotation; |
| if(Math.abs(deltaRotation) > 1){ |
| if(getDrawable() != null){ |
| deltaDegree += deltaRotation; |
| showMatrix.set(getImageMatrix()); |
| showMatrix.postRotate(deltaRotation, getWidth()/2,getHeight()/2); |
| setImageMatrix(showMatrix); |
| } |
| lastRotation = rotation; |
| } |
| } |
| }else{ |
| lastRotation = rotation; |
| } |
| } |
| } |
| |
| if (isDragging) { |
| if (getDrawable() != null) { |
| showMatrix.set(getImageMatrix()); |
| showMatrix.postTranslate(dx, dy); |
| backMatrix.reset(); |
| checkMatrixBounds(showMatrix); |
| showMatrix.postConcat(backMatrix); |
| setImageMatrix(showMatrix); |
| |
| /** |
| * Here we decide whether to let the ImageView's parent |
| * to start taking over the touch event. |
| * |
| * First we check whether this function is enabled. We |
| * never want the parent to take over if we're scaling. |
| * We then check the edge we're on, and the direction of |
| * the scroll (i.e. if we're pulling against the edge, |
| * aka 'overscrolling', let the parent take over). |
| */ |
| if (allowParentInterceptOnEdge |
| && !multiGestureDetector.isScaling()) { |
| if ((scrollEdge == EDGE_BOTH) |
| || ((scrollEdge == EDGE_LEFT) && (dx >= 1f)) |
| || ((scrollEdge == EDGE_RIGHT) && (dx <= -1f))) { |
| |
| if (getParent() != null) { |
| getParent() |
| .requestDisallowInterceptTouchEvent( |
| false); |
| } |
| } |
| } |
| } |
| |
| lastTouchX = x; |
| lastTouchY = y; |
| |
| if (velocityTracker != null) { |
| velocityTracker.addMovement(event); |
| } |
| } |
| break; |
| } |
| case MotionEvent.ACTION_POINTER_UP: |
| isDragging = true; |
| isRepeatPointerDown = true; |
| break; |
| case MotionEvent.ACTION_UP: { |
| isRepeatPointerDown = false; |
| if (isDragging) { |
| lastTouchX = x; |
| lastTouchY = y; |
| |
| // Compute velocity within the last 1000ms |
| if (velocityTracker != null) { |
| velocityTracker.addMovement(event); |
| velocityTracker.computeCurrentVelocity(1000); |
| final float vX = velocityTracker.getXVelocity(), vY = velocityTracker |
| .getYVelocity(); |
| |
| // If the velocity is greater than minVelocity perform |
| // a fling |
| if ((Math.max(Math.abs(vX), Math.abs(vY)) >= scaledMinimumFlingVelocity) |
| && (getDrawable() != null)) { |
| currentFlingRunnable = new FlingRunnable(getContext()); |
| currentFlingRunnable.fling(getWidth(), getHeight(), |
| (int) -vX, (int) -vY); |
| post(currentFlingRunnable); |
| } |
| } |
| |
| } |
| } |
| case MotionEvent.ACTION_CANCEL: |
| lastPointerCount = 0; |
| if (velocityTracker != null) { |
| velocityTracker.recycle(); |
| velocityTracker = null; |
| } |
| break; |
| } |
| } |
| return true; |
| } |
| |
| @Override |
| public boolean onScale(ScaleGestureDetector detector) { |
| if(canScale){ |
| float scale = getScaleWithoutRotation(); |
| float scaleFactor = detector.getScaleFactor(); |
| float maxScale = horizontalMaxScale; |
| float minScale = horizontalMinScale; |
| float curD = changeDegreeIn360(curDegree); |
| if(!isDegreeHorizontal(curD)){ |
| maxScale = verticalMaxScale; |
| minScale = verticalMinScale; |
| } |
| if(scaleNoRotation && getDrawable() != null && !((( scale >= maxScale) && (scaleFactor > 1f)) || ((scale < minScale) && (scaleFactor <1f)))){ |
| if(isNewGesture){ |
| //如果縮放超過開始的十分之一就算這次手勢是縮放,不能旋轉 |
| if(Math.abs(scale - startScale) > startScale * 0.1 ){ |
| isNewGesture = false; |
| } |
| } |
| showMatrix.set(getImageMatrix()); |
| showMatrix.postScale(scaleFactor, scaleFactor, detector.getFocusX(), detector.getFocusY()); |
| backMatrix.reset(); |
| checkMatrixBounds(showMatrix); |
| setImageMatrix(showMatrix); |
| } |
| } |
| return true; |
| } |
| |
| @Override |
| public boolean onScaleBegin(ScaleGestureDetector detector) { |
| startScale = getScaleWithoutRotation(); |
| return true; |
| } |
| |
| @Override |
| public void onScaleEnd(ScaleGestureDetector detector) { |
| } |
| |
| @Override |
| public boolean onDoubleTap(MotionEvent event) { |
| if(canDoubleTap){ |
| try { |
| float scale = getScaleWithoutRotation(); |
| float x = event.getX(); |
| float y = event.getY(); |
| |
| float midScale = horizontalMidScale; |
| float maxScale = horizontalMaxScale; |
| float minScale = horizontalNormalScale; |
| if(!isTargetDegreeHorizontal(startDegree)){ |
| midScale = verticalMidScale; |
| maxScale = verticalMaxScale; |
| minScale = verticalNormalScale; |
| } |
| RectF rect = getDisplayRect(); |
| doubleTapMatrix.reset(); |
| float[] point = new float[]{rect.centerX()-x, rect.centerY()-y}; |
| if(scale < midScale){ |
| doubleTapMatrix.postScale(midScale/scale, midScale/scale); |
| doubleTapMatrix.mapPoints(point); |
| // ScrollState start = new ScrollState(rect.centerX(), rect.centerY(), scale, scale, curDegree); |
| // ScrollState end = new ScrollState(point[0]+x, point[1]+y, midScale, midScale, startDegree); |
| // ImageScroller scroller = new ImageScroller(start, end, 300, getDrawable(), this, bitmapChangeListener); |
| post(new BackRunnable(startDegree, curDegree, midScale, scale, rect.centerX(), rect.centerY(),point[0]+x, point[1]+y)); |
| }else if( scale >= midScale && scale < maxScale){ |
| doubleTapMatrix.postScale(maxScale/scale, maxScale/scale); |
| doubleTapMatrix.mapPoints(point); |
| post(new BackRunnable(startDegree, curDegree, maxScale, scale, rect.centerX(), rect.centerY(),point[0]+x, point[1]+y)); |
| }else{ |
| doubleTapMatrix.postScale(minScale/scale, minScale/scale); |
| doubleTapMatrix.mapPoints(point); |
| post(new BackRunnable(startDegree, curDegree, minScale, scale, rect.centerX(), rect.centerY(),point[0]+x, point[1]+y)); |
| } |
| } catch (Exception e) { |
| } |
| } |
| return true; |
| } |
| |
| @Override |
| public boolean onDoubleTapEvent(MotionEvent event) { |
| return false; |
| } |
| |
| @Override |
| public boolean onSingleTapConfirmed(MotionEvent event) { |
| if(mSingleTapConfirmedListener != null) |
| mSingleTapConfirmedListener.onSingleTapConfirmed(); |
| if(isFling){ |
| |
| return false; |
| } |
| if(canSingleTap){ |
| if (photoTapListener != null) { |
| final RectF displayRect = getDisplayRect(); |
| |
| if (null != displayRect) { |
| final float x = event.getX(), y = event.getY(); |
| |
| // Check to see if the user tapped on the photo |
| if (displayRect.contains(x, y)) { |
| float xResult = (x - displayRect.left) |
| / displayRect.width(); |
| float yResult = (y - displayRect.top) |
| / displayRect.height(); |
| |
| photoTapListener.onPhotoTap(ZoomView.this, xResult, |
| yResult); |
| return true; |
| } |
| } |
| } |
| if (viewTapListener != null) { |
| viewTapListener.onViewTap(ZoomView.this, event.getX(), |
| event.getY()); |
| } |
| } |
| return false; |
| } |
| |
| @Override |
| public void onLongPress(MotionEvent e) { |
| if (longClickListener != null) { |
| longClickListener.onLongClick(ZoomView.this); |
| } |
| } |
| } |
| |
| /** |
| * |
| * The ScrollerProxy encapsulates the Scroller and OverScroller classes. |
| * OverScroller is available since API 9. |
| * |
| * @author tomasz.zawada@gmail.com |
| * |
| */ |
| @TargetApi(VERSION_CODES.GINGERBREAD) |
| private class ScrollerProxy { |
| |
| private boolean isOld; |
| private Object scroller; |
| |
| public ScrollerProxy(Context context) { |
| if (VERSION.SDK_INT < VERSION_CODES.GINGERBREAD) { |
| isOld = true; |
| scroller = new Scroller(context); |
| } else { |
| isOld = false; |
| scroller = new OverScroller(context); |
| } |
| } |
| |
| public boolean computeScrollOffset() { |
| return isOld ? ((Scroller) scroller).computeScrollOffset() |
| : ((OverScroller) scroller).computeScrollOffset(); |
| } |
| |
| public void fling(int startX, int startY, int velocityX, int velocityY, |
| int minX, int maxX, int minY, int maxY, int overX, int overY) { |
| |
| if (isOld) { |
| ((Scroller) scroller).fling(startX, startY, velocityX, |
| velocityY, minX, maxX, minY, maxY); |
| } else { |
| ((OverScroller) scroller).fling(startX, startY, velocityX, |
| velocityY, minX, maxX, minY, maxY, overX, overY); |
| } |
| } |
| |
| public void forceFinished(boolean finished) { |
| if (isOld) { |
| ((Scroller) scroller).forceFinished(finished); |
| } else { |
| ((OverScroller) scroller).forceFinished(finished); |
| } |
| } |
| |
| public int getCurrX() { |
| return isOld ? ((Scroller) scroller).getCurrX() |
| : ((OverScroller) scroller).getCurrX(); |
| } |
| |
| public int getCurrY() { |
| return isOld ? ((Scroller) scroller).getCurrY() |
| : ((OverScroller) scroller).getCurrY(); |
| } |
| } |
| |
| private static final int EDGE_NONE = -1; |
| private static final int EDGE_LEFT = 0; |
| private static final int EDGE_RIGHT = 1; |
| private static final int EDGE_BOTH = 2; |
| |
| /** |
| * 縮放比例,這裡的比例並不是以圖片自身大小為基準,而是以圖片fill_center時的大小為基準。 |
| * 這裡只是一個比例係數 |
| */ |
| public static float DEFAULT_MAX_SCALE = 3.0f;//最大比例 |
| public static float DEFAULT_MID_SCALE = 1.75f;//中間比例 |
| public static float DEFAULT_NORMAL_SCALE = 1f;//正常顯示的比例,也是回彈時的比例,也是顯示的最小比例 |
| public static float DEFAULT_MIN_SCALE = 0.75f;//最小縮放比例,手勢操作時的最小縮小比例 |
| |
| |
| //圖片fill_center時的縮放比例,是其他比例的基準 |
| private float horizontalBaseScale = 0; |
| private float verticalBaseScale = 0; |
| |
| private float horizontalNormalScale = DEFAULT_NORMAL_SCALE;//顯示時的最小大小 |
| private float horizontalMidScale = DEFAULT_MID_SCALE;//顯示時的中等大小 |
| private float horizontalMaxScale = DEFAULT_MAX_SCALE;//顯示時的最大大小 |
| private float horizontalMinScale = DEFAULT_MIN_SCALE;//縮放時的最小縮小大小 |
| private float verticalNormalScale = DEFAULT_NORMAL_SCALE; |
| private float verticalMidScale = DEFAULT_MID_SCALE; |
| private float verticalMaxScale = DEFAULT_MAX_SCALE; |
| private float verticalMinScale = DEFAULT_MIN_SCALE; |
| |
| private boolean allowParentInterceptOnEdge = true; |
| |
| private MultiGestureDetector multiGestureDetector; |
| |
| // These are set so we don't keep allocating them on the heap |
| private final Matrix baseMatrix = new Matrix(); |
| private final RectF displayRect = new RectF(); |
| private final float[] matrixValues = new float[9]; |
| private final Matrix showMatrix = new Matrix(); |
| private final Matrix startRotationMatrix = new Matrix();//開始旋轉時的矩陣 主要用來結束回彈時使用 |
| //作用是根據變換後的matrix計算出變換後圖片的偏移情況, |
| //如果圖片大於控制元件,則拖拽時保證圖片不會小於邊界 |
| //如果圖片小於控制元件,則縮放移動時不會離開中心 |
| //需要將這個matrix後乘到變換matrix上 |
| private final Matrix backMatrix = new Matrix(); |
| |
| private float floatDeviation = 0.001f;//比較2個float時的誤差,大於這個誤差才算大,否則算是float的誤差 |
| |
| //是否是新的一次手勢 |
| private boolean isNewGesture; |
| private boolean horizontalImage;//圖片是橫圖還是豎圖 |
| private boolean horizontalScaleHorizontal;//水平時,是否以水平方向為縮放基準 |
| private boolean verticalScaleHorizontal;//豎直時,是否以水平方向為縮放基準 |
| |
| private float curDegree;//當前圖片的旋轉角度 |
| private float deltaDegree;//這次手勢操作中旋轉的總角度 |
| private int startDegree;//手勢開始時的圖片旋轉角度 |
| |
| //旋轉和縮放是互斥的,所以這裡用一個變數表示是旋轉還是縮放 |
| private boolean scaleNoRotation; |
| |
| // Listeners |
| private OnPhotoTapListener photoTapListener; |
| private OnViewTapListener viewTapListener; |
| private OnLongClickListener longClickListener; |
| |
| private int top, right, bottom, left; |
| private FlingRunnable currentFlingRunnable; |
| private int scrollEdge = EDGE_BOTH; |
| |
| private boolean isZoomEnabled; |
| private ScaleType scaleType = ScaleType.FIT_CENTER; |
| |
| private boolean canSingleTap = true; |
| private boolean canDoubleTap = true; |
| private boolean canScale = true; |
| |
| private SingleTapConfirmedListener mSingleTapConfirmedListener; |
| |
| // ScrollState startScroll; |
| // ScrollState endScroll; |
| |
| public boolean isCanSingleTap() { |
| return canSingleTap; |
| } |
| |
| public void setCanSingleTap(boolean canSingleTap) { |
| this.canSingleTap = canSingleTap; |
| } |
| |
| public boolean isCanDoubleTap() { |
| return canDoubleTap; |
| } |
| |
| public void setCanDoubleTap(boolean canDoubleTap) { |
| this.canDoubleTap = canDoubleTap; |
| } |
| |
| public boolean isCanScale() { |
| return canScale; |
| } |
| |
| public void setCanScale(boolean canScale) { |
| this.canScale = canScale; |
| } |
| |
| public ZoomView(Context context) { |
| this(context, null); |
| } |
| |
| public ZoomView(Context context, AttributeSet attr) { |
| this(context, attr, 0); |
| } |
| |
| public ZoomView(Context context, AttributeSet attr, int defStyle) { |
| super(context, attr, defStyle); |
| |
| super.setScaleType(ScaleType.MATRIX); |
| |
| setOnTouchListener(this); |
| |
| multiGestureDetector = new MultiGestureDetector(context); |
| |
| setIsZoomEnabled(true); |
| } |
| |
| public float getCurRotation(){ |
| return curDegree; |
| } |
| |
| public float getStartRotation(){ |
| return startDegree; |
| } |
| |
| /** |
| * Gets the Display Rectangle of the currently displayed Drawable. The |
| * Rectangle is relative to this View and includes all scaling and |
| * translations. |
| * |
| * @return - RectF of Displayed Drawable |
| */ |
| public final RectF getDisplayRect() { |
| checkMatrixBounds(getImageMatrix()); |
| return getDisplayRect(getDisplayMatrix()); |
| } |
| |
| /** |
| * 獲取圖片水平時的基準縮放大小 |
| * @return |
| */ |
| public float getHorizontalBaseScaleWithoutRotation(){ |
| return horizontalBaseScale; |
| } |
| |
| /** |
| * 獲取圖片垂直時的基準縮放大小 |
| * @return |
| */ |
| public float getVerticalBaseScaleWithoutRotation(){ |
| return verticalBaseScale; |
| } |
| |
| /** |
| * Return the current scale type in use by the ImageView. |
| */ |
| @Override |
| public final ScaleType getScaleType() { |
| return scaleType; |
| } |
| |
| /** |
| * Controls how the image should be resized or moved to match the size of |
| * the ImageView. Any scaling or panning will happen within the confines of |
| * this {@link ScaleType}. |
| * |
| * @param scaleType |
| * - The desired scaling mode. |
| */ |
| @Override |
| public final void setScaleType(ScaleType scaleType) { |
| if (scaleType == ScaleType.MATRIX) { |
| throw new IllegalArgumentException(scaleType.name() |
| + " is not supported in ZoomImageView"); |
| } |
| |
| if (scaleType != this.scaleType) { |
| this.scaleType = scaleType; |
| update(); |
| } |
| } |
| |
| /** |
| * Returns true if the ZoomImageView is set to allow zooming of Photos. |
| * |
| * @return true if the ZoomImageView allows zooming. |
| */ |
| public final boolean isZoomEnabled() { |
| return isZoomEnabled; |
| } |
| |
| /** |
| * Allows you to enable/disable the zoom functionality on the ImageView. |
| * When disable the ImageView reverts to using the FIT_CENTER matrix. |
| * |
| * @param isZoomEnabled |
| * - Whether the zoom functionality is enabled. |
| */ |
| public final void setIsZoomEnabled(boolean isZoomEnabled) { |
| this.isZoomEnabled = isZoomEnabled; |
| update(); |
| } |
| |
| /** |
| * Whether to allow the ImageView's parent to intercept the touch event when |
| * the photo is scroll to it's horizontal edge. |
| */ |
| public void setAllowParentInterceptOnEdge(boolean allowParentInterceptOnEdge) { |
| this.allowParentInterceptOnEdge = allowParentInterceptOnEdge; |
| } |
| |
| @Override |
| public void setImageBitmap(Bitmap bitmap) { |
| super.setImageBitmap(bitmap); |
| update(); |
| } |
| |
| @Override |
| public void setImageDrawable(Drawable drawable) { |
| super.setImageDrawable(drawable); |
| update(); |
| } |
| |
| @Override |
| public void setImageResource(int resId) { |
| super.setImageResource(resId); |
| update(); |
| } |
| |
| @Override |
| public void setImageURI(Uri uri) { |
| super.setImageURI(uri); |
| update(); |
| } |
| |
| /** |
| * Register a callback to be invoked when the Photo displayed by this view |
| * is long-pressed. |
| * |
| * @param listener |
| * - Listener to be registered. |
| */ |
| @Override |
| public final void setOnLongClickListener(OnLongClickListener listener) { |
| longClickListener = listener; |
| } |
| |
| /** |
| * Register a callback to be invoked when the Photo displayed by this View |
| * is tapped with a single tap. |
| * |
| * @param listener |
| * - Listener to be registered. |
| */ |
| public final void setOnPhotoTapListener(OnPhotoTapListener listener) { |
| photoTapListener = listener; |
| } |
| public void setOnSingleTapConfirmedListener(SingleTapConfirmedListener l) { |
| this.mSingleTapConfirmedListener = l; |
| } |
| |
| /** |
| * Register a callback to be invoked when the View is tapped with a single |
| * tap. |
| * |
| * @param listener |
| * - Listener to be registered. |
| */ |
| public final void setOnViewTapListener(OnViewTapListener listener) { |
| viewTapListener = listener; |
| } |
| |
| @Override |
| public final void onGlobalLayout() { |
| if (isZoomEnabled) { |
| final int top = getTop(); |
| final int right = getRight(); |
| final int bottom = getBottom(); |
| final int left = getLeft(); |
| |
| /** |
| * We need to check whether the ImageView's bounds have changed. |
| * This would be easier if we targeted API 11+ as we could just use |
| * View.OnLayoutChangeListener. Instead we have to replicate the |
| * work, keeping track of the ImageView's bounds and then checking |
| * if the values change. |
| */ |
| if ((top != this.top) || (bottom != this.bottom) |
| || (left != this.left) || (right != this.right)) { |
| // Update our base matrix, as the bounds have changed |
| updateBaseMatrix(getDrawable()); |
| |
| // Update values as something has changed |
| this.top = top; |
| this.right = right; |
| this.bottom = bottom; |
| this.left = left; |
| } |
| } |
| } |
| |
| boolean backing = false;//是否正在回彈 |
| boolean isIgnore = false;//是否忽略手勢,如果正在回彈時,觸發手勢會把這次手勢完全遮蔽掉 |
| |
| @Override |
| public final boolean onTouch(View v, MotionEvent ev) { |
| boolean handled = false; |
| if(backing){//防止圖片回彈時的手勢 |
| switch (ev.getAction()) { |
| case MotionEvent.ACTION_CANCEL: |
| case MotionEvent.ACTION_UP: |
| isIgnore = false; |
| break; |
| default : |
| isIgnore = true; |
| } |
| if (v.getParent() != null) { |
| v.getParent().requestDisallowInterceptTouchEvent(true); |
| } |
| return true; |
| } |
| if(isIgnore){//如果正在回彈時觸發手勢,回彈結束後之後的手勢也需要遮蔽防止造成圖片角度突然變化不連貫。之後再手指抬起時才結束遮蔽 |
| switch (ev.getAction()) { |
| case MotionEvent.ACTION_CANCEL: |
| case MotionEvent.ACTION_UP: |
| isIgnore = false; |
| } |
| return true; |
| } |
| |
| if (isZoomEnabled) { |
| switch (ev.getAction() & MotionEvent.ACTION_MASK) { |
| case MotionEvent.ACTION_DOWN: |
| |
| // First, disable the Parent from intercepting the touch |
| // event |
| if (v.getParent() != null) { |
| v.getParent().requestDisallowInterceptTouchEvent(true); |
| } |
| |
| // If we're flinging, and the user presses down, cancel |
| // fling |
| if (currentFlingRunnable != null) { |
| currentFlingRunnable.cancelFling(); |
| currentFlingRunnable = null; |
| } |
| curDegree = startDegree; |
| scaleNoRotation = true; |
| isNewGesture = true; |
| deltaDegree = 0; |
| break; |
| case MotionEvent.ACTION_MOVE: |
| curDegree = startDegree + deltaDegree; |
| break; |
| case MotionEvent.ACTION_CANCEL: |
| case MotionEvent.ACTION_UP: |
| float curScale = getScaleWithoutRotation(); |
| float targetScale = horizontalNormalScale; |
| int targetDegree = getTargetDegree(); |
| targetDegree = (int)changeDegreeIn360(targetDegree); |
| targetDegree = getAbsDegree(targetDegree); |
| curDegree = changeDegreeIn360(curDegree); |
| if(targetDegree == 0 && curDegree > 180){ |
| targetDegree = 360; |
| } |
| boolean isHorizontal = isTargetDegreeHorizontal(targetDegree); |
| if(!isHorizontal){ |
| targetScale = verticalNormalScale; |
| } |
| RectF currentRect = getDisplayRect(); |
| if(currentRect == null){ |
| deltaDegree = 0; |
| break; |
| } |
| float centerFromX = currentRect.centerX(); |
| float centerFromY = currentRect.centerY(); |
| //如果縮放大小小於最小縮放值回彈到中心 |
| if(targetScale - curScale > floatDeviation){ |
| //角度太小 完全回彈 |
| v.post(new BackRunnable(targetDegree, curDegree, targetScale, curScale, centerFromX, centerFromY)); |
| handled = true; |
| deltaDegree = 0; |
| break; |
| } |
| float center2X = centerFromX; |
| float center2Y = centerFromY; |
| int viewWidth = getWidth(); |
| int viewHeight = getHeight(); |
| //當前旋轉角度和目標角度不同 |
| if(Float.compare(targetDegree, curDegree)!=0){ |
| //如果旋轉後回彈方向沒變則大小不變 |
| if(Math.abs(changeDegreeIn360(targetDegree - startDegree)) < 45 ){ |
| targetScale = curScale; |
| RectF backRect = getDisplayRect(startRotationMatrix); |
| center2X = backRect.centerX(); |
| center2Y = backRect.centerY(); |
| if(isHorizontal){//如果返回的是水平狀態 |
| if(backRect.width() > viewWidth){ |
| if(backRect.left > 0){ |
| center2X -= backRect.left; |
| } |
| if(backRect.right < viewWidth){ |
| center2X += (viewWidth - backRect.right); |
| } |
| }else{ |
| center2X = 1f * viewWidth / 2; |
| } |
| if(backRect.height() > viewHeight){ |
| if(backRect.top > 0){ |
| center2Y -= backRect.top; |
| } |
| if(backRect.bottom < viewHeight){ |
| center2Y += (viewHeight - backRect.bottom); |
| } |
| }else{ |
| center2Y = 1f*viewHeight/2; |
| } |
| }else{//如果返回的是豎直狀態 |
| if(backRect.width() > viewWidth){ |
| if(backRect.left > 0){ |
| center2X -= backRect.left; |
| } |
| if(backRect.right < viewWidth){ |
| center2X += (viewWidth - backRect.right); |
| } |
| }else{ |
| center2X = 1f * viewWidth / 2; |
| } |
| if(backRect.height() > viewHeight){ |
| if(backRect.top > 0){ |
| center2Y -= backRect.top; |
| } |
| if(backRect.bottom < viewHeight){ |
| center2Y += (viewHeight - backRect.bottom); |
| } |
| }else{ |
| center2Y = 1f * viewHeight / 2; |
| } |
| } |
| }else{//如果方向需要改變,中心變為view中心 |
| center2Y = 1f * viewHeight / 2; |
| center2X = 1f * viewWidth / 2; |
| } |
| v.post(new BackRunnable(targetDegree, curDegree, targetScale, curScale, centerFromX, centerFromY, center2X, center2Y, false)); |
| handled = true; |
| deltaDegree = 0; |
| break; |
| } |
| //當前角度沒變 並且縮放大小大於最小縮放大小,則縮放大小不變 |
| targetScale = curScale; |
| boolean needBack = false; |
| if(isHorizontal){//如果當前是水平顯示 |
| if(horizontalScaleHorizontal){//如果是水平縮放 |
| if(currentRect.left > 1){ |
| needBack = true; |
| center2X = centerFromX - currentRect.left; |
| } |
| if(viewWidth - currentRect.right > 1 ){ |
| needBack = true; |
| center2X = centerFromX + (viewWidth - currentRect.right); |
| } |
| if(currentRect.height() > viewHeight){ |
| if(currentRect.top > 0){ |
| center2Y -= currentRect.top; |
| needBack = true; |
| } |
| if(currentRect.bottom < viewHeight){ |
| center2Y += (viewHeight - currentRect.bottom); |
| needBack = true; |
| } |
| }else{ |
| center2Y = 1f*viewHeight/2; |
| if(Math.abs(center2Y - centerFromY) > 1){ |
| needBack = true; |
| } |
| } |
| }else{//如果是豎直為基準縮放 |
| if(currentRect.top > 1){ |
| needBack = true; |
| center2Y = centerFromY - currentRect.top; |
| } |
| if(viewHeight - currentRect.bottom > 1 ){ |
| needBack = true; |
| center2Y = centerFromY + (viewHeight - currentRect.bottom); |
| } |
| if(currentRect.width() > viewWidth){ |
| if(currentRect.left > 0){ |
| center2X -= currentRect.left; |
| needBack = true; |
| } |
| if(currentRect.right < viewWidth){ |
| center2X += (viewWidth - currentRect.right); |
| needBack = true; |
| } |
| }else{ |
| center2X = 1f*viewWidth/2; |
| if(Math.abs(center2X - centerFromX) > 1){ |
| needBack = true; |
| } |
| } |
| } |
| }else{//如果是豎直顯示 |
| if(verticalScaleHorizontal){//如果是水平縮放 |
| if(currentRect.left > 1){ |
| needBack = true; |
| center2X -= currentRect.left; |
| } |
| if(viewWidth - currentRect.right > 1){ |
| needBack = true; |
| center2X += (viewWidth - currentRect.right); |
| } |
| if(currentRect.height() > viewHeight){ |
| if(currentRect.top > 0){ |
| center2Y -= currentRect.top; |
| needBack = true; |
| } |
| if(currentRect.bottom < viewHeight){ |
| center2Y += (viewHeight - currentRect.bottom); |
| needBack = true; |
| } |
| }else{ |
| center2Y = 1f*viewHeight/2; |
| if(Math.abs(center2Y - centerFromY) > 1){ |
| needBack = true; |
| } |
| } |
| }else{//如果是以豎直為基準 |
| if(currentRect.top > 1){ |
| needBack = true; |
| center2Y -= currentRect.top; |
| } |
| if(viewHeight - currentRect.bottom > 1){ |
| needBack = true; |
| center2Y += (viewHeight - currentRect.bottom); |
| } |
| if(currentRect.width() > viewWidth){ |
| if(currentRect.left > 0){ |
| center2X -= currentRect.left; |
| needBack = true; |
| } |
| if(currentRect.right < viewWidth){ |
| center2X += (viewWidth - currentRect.right); |
| needBack = true; |
| } |
| }else{ |
| center2X = 1f*viewWidth/2; |
| if(Math.abs(center2X - centerFromX) > 1){ |
| needBack = true; |
| } |
| } |
| } |
| } |
| if(needBack){ |
| v.post(new BackRunnable(targetDegree, curDegree, targetScale, curScale, currentRect.centerX(), currentRect.centerY(),center2X, center2Y, false)); |
| handled = true; |
| } |
| //清理手勢旋轉角度 |
| deltaDegree = 0; |
| break; |
| } |
| |
| // // Finally, try the scale/drag/tap detector |
| // if ((multiGestureDetector != null) |
| // && multiGestureDetector.onTouchEvent(ev)) { |
| // handled = true; |
| // } |
| } |
| // Finally, try the scale/drag/tap detector |
| if ((multiGestureDetector != null) |
| && multiGestureDetector.onTouchEvent(ev)) { |
| handled = true; |
| } |
| // } |
| |
| return handled; |
| } |
| |
| /** |
| * 判斷目標角度是否是水平的 |
| * @param degreeIn360 |
| * @return |
| */ |
| private boolean isTargetDegreeHorizontal(int degreeIn360){ |
| if(degreeIn360 == 0 || degreeIn360 == 180 || degreeIn360 == 360){ |
| return true; |
| }else{ |
| return false; |
| } |
| } |
| |
| /** |
| * 判斷目標角度是否是在水平範圍內 |
| * @param degreeIn360 |
| * @return |
| */ |
| private boolean isDegreeHorizontal(float degreeIn360){ |
| if(degreeIn360 >=0 && degreeIn360 < 45){ |
| return true; |
| } |
| if(degreeIn360 >= 135 && degreeIn360 < 225){ |
| return true; |
| } |
| if(degreeIn360 >= 315){ |
| return true; |
| } |
| return false; |
| } |
| |
| /** |
| * 獲取當前真實的縮放大小,排除旋轉對縮放的影響 |
| * @return |
| */ |
| public float getScaleWithoutRotation(){ |
| showMatrix.set(getImageMatrix()); |
| showMatrix.postRotate(-curDegree);//旋轉需要還原不然的話縮放不對 |
| showMatrix.getValues(matrixValues); |
| return matrixValues[Matrix.MSCALE_X]; |
| } |
| |
| /** |
| * 根據開始時的角度和手勢中旋轉角度獲取回彈時的目標角度 |
| * 比如開始時是90度,順時針移動超過45度則返回180度,少於45度返回90度 |
| * @return |
| */ |
| private int getTargetDegree(){ |
| int targetDegree = 0; |
| int num = (int)Math.abs(deltaDegree) / 45; |
| if(num % 2 == 0){//如果是雙數,表示沒有超過一半 需要退回當前前一個位置 |
| if(deltaDegree > 0){ |
| targetDegree = startDegree + 45 * num; |
| }else{ |
| targetDegree = startDegree - 45 * num; |
| } |
| }else{//表示超過一半需要到下一個位置 |
| if(deltaDegree > 0){ |
| targetDegree = startDegree + 45 * (num + 1); |
| }else{ |
| targetDegree = startDegree - 45 * (num + 1); |
| } |
| } |
| return targetDegree; |
| } |
| |
| |
| |
| /** |
| * 將角度換算到360度之內,不包括360度 |
| * 比如-90度變為270度 |
| * @param degree |
| * @return |
| */ |
| public float changeDegreeIn360(float degree){ |
| while(degree < 0){ |
| degree += 360; |
| } |
| while(degree >= 360){ |
| degree -= 360; |
| } |
| return degree; |
| } |
| |
| /** |
| * 將目標角度變成具體的0,90,180,270四個中的一個,防止出現偏差 沒有360 |
| * 目標角度是一個float,為了保證準確,將該值改為特定的int值 |
| * @param degreeIn360 |
| * @return |
| */ |
| private int getAbsDegree(float degreeIn360){ |
| if(Math.abs(degreeIn360 - 0) < 10){ |
| return 0; |
| } |
| if(Math.abs(degreeIn360 - 90) < 10){ |
| return 90; |
| } |
| if(Math.abs(degreeIn360 - 180) < 10){ |
| return 180; |
| } |
| if(Math.abs(degreeIn360 - 270) < 10){ |
| return 270; |
| } |
| if(Math.abs(degreeIn360 - 360) < 10){ |
| return 360; |
| } |
| return (int)degreeIn360; |
| } |
| |
| |
| // 取旋轉角度 |
| private float rotation(MotionEvent event) { |
| double delta_x = (event.getX(0) - event.getX(1)); |
| double delta_y = (event.getY(0) - event.getY(1)); |
| double radians = Math.atan2(delta_y, delta_x); |
| return (float) Math.toDegrees(radians); |
| } |
| |
| @Override |
| protected void onAttachedToWindow() { |
| super.onAttachedToWindow(); |
| |
| getViewTreeObserver().addOnGlobalLayoutListener(this); |
| } |
| |
| @SuppressWarnings("deprecation") |
| @Override |
| protected void onDetachedFromWindow() { |
| super.onDetachedFromWindow(); |
| |
| getViewTreeObserver().removeGlobalOnLayoutListener(this); |
| } |
| |
| protected Matrix getDisplayMatrix() { |
| showMatrix.set(getImageMatrix()); |
| return showMatrix; |
| } |
| |
| |
| private final void update() { |
| if (isZoomEnabled) { |
| super.setScaleType(ScaleType.MATRIX); |
| updateBaseMatrix(getDrawable()); |
| } else { |
| updateBaseMatrix(getDrawable()); |
| // resetMatrix(); |
| } |
| } |
| |
| |
| /** |
| * 這裡的作用主要是: |
| * 經過給定的Matrix變換後 |
| * 判斷當前圖片狀態是否已經達到邊界 |
| * 如果圖片大於控制元件,則拖拽時保證圖片不會小於邊界(通過這裡設定偏移matrix實現,給定matrix.postConcat(偏移matrix)) |
| * 如果圖片小於控制元件,則縮放移動時不會離開中心(通過這裡設定偏移matrix實現,給定matrix.postConcat(偏移matrix)) |
| */ |
| private void checkMatrixBounds(Matrix matrix) { |
| final RectF rect = getDisplayRect(matrix); |
| if (null == rect) { |
| return; |
| } |
| |
| final float height = rect.height(), width = rect.width(); |
| float deltaX = 0, deltaY = 0; |
| |
| final int viewHeight = getHeight(); |
| if (height <= viewHeight) { |
| switch (scaleType) { |
| case FIT_START: |
| deltaY = -rect.top; |
| break; |
| case FIT_END: |
| deltaY = viewHeight - height - rect.top; |
| break; |
| default: |
| deltaY = ((viewHeight - height) / 2) - rect.top; |
| break; |
| } |
| } else if (rect.top > 0) { |
| deltaY = -rect.top; |
| } else if (rect.bottom < viewHeight) { |
| deltaY = viewHeight - rect.bottom; |
| } |
| |
| final int viewWidth = getWidth(); |
| if (width <= viewWidth) { |
| switch (scaleType) { |
| case FIT_START: |
| deltaX = -rect.left; |
| break; |
| case FIT_END: |
| deltaX = viewWidth - width - rect.left; |
| break; |
| default: |
| deltaX = ((viewWidth - width) / 2) - rect.left; |
| break; |
| } |
| scrollEdge = EDGE_BOTH; |
| } else if (rect.left > 0) { |
| scrollEdge = EDGE_LEFT; |
| deltaX = -rect.left; |
| } else if (rect.right < viewWidth) { |
| deltaX = viewWidth - rect.right; |
| scrollEdge = EDGE_RIGHT; |
| } else { |
| scrollEdge = EDGE_NONE; |
| } |
| |
| // Finally actually translate the matrix |
| backMatrix.postTranslate(deltaX, deltaY); |
| } |
| |
| /** |
| * Helper method that maps the supplied Matrix to the current Drawable |
| * |
| * @param matrix |
| * - Matrix to map Drawable against |
| * @return RectF - Displayed Rectangle |
| */ |
| private RectF getDisplayRect(Matrix matrix) { |
| Drawable d = getDrawable(); |
| if (null != d) { |
| displayRect |
| .set(0, 0, d.getIntrinsicWidth(), d.getIntrinsicHeight()); |
| matrix.mapRect(displayRect); |
| return displayRect; |
| } |
| |
| return null; |
| } |
| |
| /** |
| * Resets the Matrix back to FIT_CENTER, and then displays it.s |
| */ |
| private void resetMatrix() { |
| setImageMatrix(baseMatrix); |
| checkMatrixBounds(baseMatrix); |
| } |
| |
| /** |
| * Calculate Matrix for FIT_CENTER |
| * |
| * @param d |
| * - Drawable being displayed |
| */ |
| private void updateBaseMatrix(Drawable d) { |
| if (null == d) { |
| return; |
| } |
| startDegree = 0; |
| curDegree = 0; |
| final float viewWidth = getWidth(); |
| final float viewHeight = getHeight(); |
| //這裡的作用主要是當設定圖片的時候當前view還沒有計算出寬高,所以需要等他寬高計算出來後才能計算他的matrix |
| if(viewWidth == 0 || viewHeight == 0){ |
| ViewTreeObserver vto = getViewTreeObserver(); |
| vto.addOnPreDrawListener(new MyPreDrawFilter(this) { |
| |
| @Override |
| public void doBeforeDraw() { |
| updateBaseMatrix(getDrawable()); |
| } |
| }); |
| return ; |
| } |
| final int drawableWidth = d.getIntrinsicWidth(); |
| final int drawableHeight = d.getIntrinsicHeight(); |
| if(drawableHeight > drawableWidth){ |
| horizontalImage = false; |
| }else{ |
| horizontalImage = true; |
| } |
| mPicCenterX = 1f * drawableWidth/2; |
| mPicCenterY = 1f * drawableHeight/2; |
| baseMatrix.reset(); |
| |
| final float widthScale = viewWidth / drawableWidth; |
| final float heightScale = viewHeight / drawableHeight; |
| DEFAULT_MAX_SCALE = 3.0f;//最大比例 |
| if(widthScale > heightScale){ |
| horizontalScaleHorizontal = false; |
| horizontalBaseScale = heightScale; |
| if(horizontalBaseScale > 0){ |
| float maxScale = widthScale/horizontalBaseScale; |
| if(maxScale > DEFAULT_MAX_SCALE){ |
| DEFAULT_MAX_SCALE = maxScale; |
| } |
| } |
| }else{ |
| horizontalScaleHorizontal = true; |
| horizontalBaseScale = widthScale; |
| if(horizontalBaseScale > 0){ |
| float maxScale = heightScale/horizontalBaseScale; |
| if(maxScale > DEFAULT_MAX_SCALE){ |
| DEFAULT_MAX_SCALE = maxScale; |
| } |
| } |
| } |
| horizontalNormalScale = DEFAULT_NORMAL_SCALE * horizontalBaseScale; |
| horizontalMidScale = DEFAULT_MID_SCALE * horizontalBaseScale; |
| horizontalMaxScale = DEFAULT_MAX_SCALE * horizontalBaseScale; |
| horizontalMinScale = DEFAULT_MIN_SCALE * horizontalBaseScale; |
| final float verticalWidthScale = viewHeight / drawableWidth; |
| final float verticalHeightScale = viewWidth / drawableHeight; |
| if(verticalWidthScale > verticalHeightScale){ |
| verticalScaleHorizontal = true; |
| verticalBaseScale = verticalHeightScale; |
| }else{ |
| verticalScaleHorizontal = false; |
| verticalBaseScale = verticalWidthScale; |
| } |
| verticalNormalScale = DEFAULT_NORMAL_SCALE * verticalBaseScale; |
| verticalMidScale = DEFAULT_MID_SCALE * verticalBaseScale; |
| verticalMaxScale = DEFAULT_MAX_SCALE * verticalBaseScale; |
| verticalMinScale = DEFAULT_MIN_SCALE * verticalBaseScale; |
| if (scaleType == ScaleType.CENTER) { |
| baseMatrix.postTranslate((viewWidth - drawableWidth) / 2F, |
| (viewHeight - drawableHeight) / 2F); |
| |
| } else if (scaleType == ScaleType.CENTER_CROP) { |
| float scale = Math.max(widthScale, heightScale); |
| baseMatrix.postScale(scale, scale); |
| baseMatrix.postTranslate( |
| (viewWidth - (drawableWidth * scale)) / 2F, |
| (viewHeight - (drawableHeight * scale)) / 2F); |
| |
| } else if (scaleType == ScaleType.CENTER_INSIDE) { |
| float scale = Math.min(1.0f, Math.min(widthScale, heightScale)); |
| baseMatrix.postScale(scale, scale); |
| baseMatrix.postTranslate( |
| (viewWidth - (drawableWidth * scale)) / 2F, |
| (viewHeight - (drawableHeight * scale)) / 2F); |
| |
| } else { |
| RectF mTempSrc = new RectF(0, 0, drawableWidth, drawableHeight); |
| RectF mTempDst = new RectF(0, 0, viewWidth, viewHeight); |
| |
| switch (scaleType) { |
| case FIT_CENTER: |
| baseMatrix.setRectToRect(mTempSrc, mTempDst, ScaleToFit.CENTER); |
| break; |
| |
| case FIT_START: |
| baseMatrix.setRectToRect(mTempSrc, mTempDst, ScaleToFit.START); |
| break; |
| |
| case FIT_END: |
| baseMatrix.setRectToRect(mTempSrc, mTempDst, ScaleToFit.END); |
| break; |
| |
| case FIT_XY: |
| baseMatrix.setRectToRect(mTempSrc, mTempDst, ScaleToFit.FILL); |
| break; |
| |
| default: |
| break; |
| } |
| } |
| |
| resetMatrix(); |
| } |
| |
| @Override |
| protected void onDraw(Canvas canvas) { |
| super.onDraw(canvas); |
| } |
| |
| @TargetApi(VERSION_CODES.JELLY_BEAN) |
| private void postOnAnimation(View view, Runnable runnable) { |
| if (VERSION.SDK_INT >= VERSION_CODES.JELLY_BEAN) { |
| view.postOnAnimation(runnable); |
| } else { |
| view.postDelayed(runnable, 16); |
| } |
| } |
| |
| |
| // //如果需要新增獲取縮放大小和設定縮放大小的方法時需使用這個方法來進行校驗 |
| private void checkZoomLevels(float minZoom, float normal, float midZoom, float maxZoom) { |
| if (minZoom >= normal) { |
| throw new IllegalArgumentException( |
| "MinZoom should be less than normal"); |
| }else if (normal >= midZoom) { |
| throw new IllegalArgumentException( |
| "normal should be less than midZoom"); |
| }else if (midZoom >= maxZoom) { |
| throw new IllegalArgumentException( |
| "MidZoom should be less than MaxZoom"); |
| } |
| } |
| |
| public ZoomState getZoomState(){ |
| ZoomState state = new ZoomState(); |
| state.setCurDegree(curDegree); |
| state.setDeltaDegree(deltaDegree); |
| state.setStartDegree(startDegree); |
| return state; |
| } |
| public void resetZoomState(ZoomState zoomState){ |
| this.startDegree = zoomState.getStartDegree(); |
| this.curDegree = zoomState.getCurDegree(); |
| this.deltaDegree = zoomState.getDeltaDegree(); |
| } |
| |
| public class ZoomState{ |
| private int startDegree; |
| private float curDegree;//當前圖片的旋轉角度 |
| private float deltaDegree;//這次手勢操作中旋轉的總角度 |
| |
| public boolean isOrignal(){ |
| return startDegree == 0 && Float.compare(curDegree, 0) == 0 && Float.compare(deltaDegree, 0) == 0; |
| } |
| public int getStartDegree() { |
| return startDegree; |
| } |
| public void setStartDegree(int startDegree) { |
| this.startDegree = startDegree; |
| } |
| public float getCurDegree() { |
| return curDegree; |
| } |
| public void setCurDegree(float curDegree) { |
| this.curDegree = curDegree; |
| } |
| public float getDeltaDegree() { |
| return deltaDegree; |
| } |
| public void setDeltaDegree(float deltaDegree) { |
| this.deltaDegree = deltaDegree; |
| } |
| } |
| |
| /** |
| * 原始圖片的中心點 |
| */ |
| private float mPicCenterX; |
| private float mPicCenterY; |
| |
| /** |
| * 回彈類 |
| * 作用是將圖片進行回彈 |
| * 這裡的centerX,centerY表示的是圖片中心點的位置。這裡是根據圖片的中心點位置,以及旋轉和縮放來進行回彈的。 |
| * 這裡的縮放是在沒有旋轉下的縮放,即水平狀態下的縮放 |
| * @author cx |
| * |
| */ |
| private class BackRunnable implements Runnable { |
| |
| private float targetRotation; |
| private float targetScale; |
| private float fromRotation; |
| private float fromScale; |
| private float centerFromX;//圖片中心點的起始位置 |
| private float centerFromY;//圖片中心點的起始位置 |
| private long mDuration;//回彈時間 單位毫秒 |
| private boolean isStop;//是否結束 |
| private long mStartTime = -1;//開始時間 |
| private Interpolator mInterpolator = new LinearInterpolator();//回彈速率 |
| private float center2X;//圖片中心點的目標位置 |
| private float center2Y;//圖片中心點的目標位置 |
| |
| //圖片回彈時是否保持圖片的寬或高不離開邊界,如果為true,則如果回彈開始時圖片 |
| //中心不在view中心,並且圖片脫離邊界,則會直接從中心開始回彈,即從當前位置 |
| //消失,然後從中心顯示然後開始回彈,會有一些不連貫,回彈最終結果保持圖片不離開 |
| //邊界,如果設定的目標超出邊界,則真正回彈的目標點不會是設定的點,會在設定的目標點 |
| //上進行相應的移動變換。如果為false則平滑從當前位置回彈,最終到達目標點,可能會超出邊界 |
| private boolean limitBounds; |
| |
| public BackRunnable(float targetRotation, float fromRotation, float targetScale, float fromScale, float centerFromX, float centerFromY){ |
| this.targetRotation = targetRotation; |
| this.targetScale = targetScale; |
| this.fromRotation = fromRotation; |
| this.fromScale = fromScale; |
| this.centerFromX = centerFromX; |
| this.centerFromY = centerFromY; |
| mDuration = 200; |
| center2X = getWidth()/2; |
| center2Y = getHeight()/2; |
| limitBounds = false; |
| } |
| |
| public BackRunnable(float targetRotation, float fromRotation, float targetScale, float fromScale, float centerFromX, float centerFromY, float center2X, float center2Y){ |
| this.targetRotation = targetRotation; |
| this.targetScale = targetScale; |
| this.fromRotation = fromRotation; |
| this.fromScale = fromScale; |
| this.centerFromX = centerFromX; |
| this.centerFromY = centerFromY; |
| mDuration = 300; |
| this.center2X = center2X; |
| this.center2Y = center2Y; |
| limitBounds = true; |
| } |
| |
| public BackRunnable(float targetRotation, float fromRotation, float targetScale, float fromScale, float centerFromX, float centerFromY, float center2X, float center2Y, boolean limitBounds){ |
| this.targetRotation = targetRotation; |
| this.targetScale = targetScale; |
| this.fromRotation = fromRotation; |
| this.fromScale = fromScale; |
| this.centerFromX = centerFromX; |
| this.centerFromY = centerFromY; |
| mDuration = 300; |
| this.center2X = center2X; |
| this.center2Y = center2Y; |
| this.limitBounds = limitBounds; |
| } |
| |
| |
| |
| public void run() { |
| if(!backing){ |
| backing = true; |
| } |
| Matrix changeMatrix = null; |
| float percent = 0; |
| if(mStartTime == -1){//剛開始,初始化時間 |
| mStartTime = System.currentTimeMillis(); |
| }else{ |
| long normalizedTime = (1000 * (System.currentTimeMillis() - mStartTime)) / mDuration; |
| normalizedTime = Math.max(Math.min(normalizedTime, 1000), 0); |
| percent = mInterpolator.getInterpolation(normalizedTime / 1000f); |
| final float deltaX = (center2X - centerFromX) * percent; |
| final float deltaY = (center2Y - centerFromY) * percent; |
| //縮放是相對於原始圖片進行的,所以需要加上初始值 |
| float deltaScale =fromScale +(targetScale - fromScale)*percent; |
| final float deltaRotation = fromRotation + (targetRotation - fromRotation)*percent; |
| changeMatrix = new Matrix();//必須new 不然縮放會在當前基礎上進行 |
| //縮放到目標大小 |
| changeMatrix.postTranslate(- mPicCenterX, - mPicCenterY);//將圖片中心移動到縮放原點 為了防止在原地進行縮放導致圖片中心縮放時發生偏移 |
| changeMatrix.postScale(deltaScale, deltaScale, 0, 0);//進行縮放 |
| changeMatrix.postRotate(deltaRotation, 0, 0);//進行旋轉 |
| changeMatrix.postTranslate(mPicCenterX, mPicCenterY);//將縮放後的圖片移動回原來的位置 |
| //將圖片移動到任務起始位置 |
| changeMatrix.postTranslate(centerFromX - mPicCenterX, centerFromY - mPicCenterY);//將圖片中心移動到起始點 |
| changeMatrix.postTranslate(deltaX, deltaY); |
| backMatrix.reset(); |
| checkMatrixBounds(changeMatrix); |
| if(limitBounds){ |
| changeMatrix.postConcat(backMatrix); |
| } |
| setImageMatrix(changeMatrix); |
| curDegree = deltaRotation; |
| } |
| |
| if (!isStop && percent != 1) { |
| post(this); |
| }else{ |
| backing = false; |
| startDegree = getAbsDegree(targetRotation); |
| curDegree = startDegree; |
| isStop = true; |
| } |
| } |
| } |
| boolean isFling = false; |
| |
| private class FlingRunnable implements Runnable { |
| private final ScrollerProxy scroller; |
| private int currentX, currentY; |
| |
| public FlingRunnable(Context context) { |
| scroller = new ScrollerProxy(context); |
| } |
| |
| public void cancelFling() { |
| scroller.forceFinished(true); |
| } |
| |
| public void fling(int viewWidth, int viewHeight, int velocityX, |
| int velocityY) { |
| final RectF rect = getDisplayRect(); |
| if (null == rect) { |
| return; |
| } |
| |
| final int startX = Math.round(-rect.left); |
| final int minX, maxX, minY, maxY; |
| |
| if (viewWidth < rect.width()) { |
| minX = 0; |
| maxX = Math.round(rect.width() - viewWidth); |
| } else { |
| minX = maxX = startX; |
| } |
| |
| final int startY = Math.round(-rect.top); |
| if (viewHeight < rect.height()) { |
| minY = 0; |
| maxY = Math.round(rect.height() - viewHeight); |
| } else { |
| minY = maxY = startY; |
| } |
| |
| currentX = startX; |
| currentY = startY; |
| |
| // If we actually can move, fling the scroller |
| if ((startX != maxX) || (startY != maxY)) { |
| scroller.fling(startX, startY, velocityX, velocityY, minX, |
| maxX, minY, maxY, 0, 0); |
| } |
| } |
| |
| @Override |
| public void run() { |
| |
| if (scroller.computeScrollOffset()) { |
| final int newX = scroller.getCurrX(); |
| final int newY = scroller.getCurrY(); |
| showMatrix.set(getImageMatrix()); |
| showMatrix.postTranslate(currentX - newX, currentY - newY); |
| setImageMatrix(showMatrix); |
| currentX = newX; |
| currentY = newY; |
| isFling = true; |
| // Post On animation |
| postOnAnimation(ZoomView.this, this); |
| }else{ |
| isFling = false; |
| } |
| } |
| } |
| } |