Android使用ImageView實現支持手勢縮放效果
TouchImageView繼承自ImageView具有ImageView的所有功能;除此之外,還有縮放、拖拽、雙擊放大等功能,支持viewpager和scaletype,并伴有動畫效果。
sharedConstructing private void sharedConstructing(Context context) { super.setClickable(true); this.context = context; mScaleDetector = new ScaleGestureDetector(context, new ScaleListener()); mGestureDetector = new GestureDetector(context, new GestureListener()); matrix = new Matrix(); prevMatrix = new Matrix(); m = new float[9]; normalizedScale = 1; if (mScaleType == null) { mScaleType = ScaleType.FIT_CENTER; } minScale = 1; maxScale = 3; superMinScale = SUPER_MIN_MULTIPLIER * minScale; superMaxScale = SUPER_MAX_MULTIPLIER * maxScale; setImageMatrix(matrix); setScaleType(ScaleType.MATRIX); setState(State.NONE); onDrawReady = false; super.setOnTouchListener(new PrivateOnTouchListener()); }
初始化,設置ScaleGestureDetector的監(jiān)聽器為ScaleListener,這是用來處理縮放手勢的,設置GestureDetector的監(jiān)聽器為GestureListener,這是用來處理雙擊和fling手勢的,前兩個手勢都會引起圖片的縮放,而fling會引起圖片的移動。
mScaleDetector = new ScaleGestureDetector(context, new ScaleListener()); mGestureDetector = new GestureDetector(context, new GestureListener());
最后設置自定義View的touch事件監(jiān)聽器為PrivateOnTouchListener,這是touch事件的入口。
super.setOnTouchListener(new PrivateOnTouchListener()); PrivateOnTouchListener private class PrivateOnTouchListener implements OnTouchListener { // // Remember last point position for dragging // private PointF last = new PointF(); @Override public boolean onTouch(View v, MotionEvent event) { mScaleDetector.onTouchEvent(event); mGestureDetector.onTouchEvent(event); PointF curr = new PointF(event.getX(), event.getY()); if (state == State.NONE || state == State.DRAG || state == State.FLING) { switch (event.getAction()) { case MotionEvent.ACTION_DOWN: last.set(curr); if (fling != null) fling.cancelFling(); setState(State.DRAG); break; case MotionEvent.ACTION_MOVE: if (state == State.DRAG) { float deltaX = curr.x - last.x; float deltaY = curr.y - last.y; float fixTransX = getFixDragTrans(deltaX, viewWidth, getImageWidth()); float fixTransY = getFixDragTrans(deltaY, viewHeight, getImageHeight()); matrix.postTranslate(fixTransX, fixTransY); fixTrans(); last.set(curr.x, curr.y); } break; case MotionEvent.ACTION_UP: case MotionEvent.ACTION_POINTER_UP: setState(State.NONE); break; } } setImageMatrix(matrix); // // User-defined OnTouchListener // if(userTouchListener != null) { userTouchListener.onTouch(v, event); } // // OnTouchImageViewListener is set: TouchImageView dragged by user. // if (touchImageViewListener != null) { touchImageViewListener.onMove(); } // // indicate event was handled // return true; } }
觸摸時會走到PrivateOnTouchListener的onTouch,它又會將捕捉到的MotionEvent交給mScaleDetector和mGestureDetector來分析是否有合適的callback函數來處理用戶的手勢。
mScaleDetector.onTouchEvent(event); mGestureDetector.onTouchEvent(event);
同時在當前狀態(tài)是DRAG時將X、Y移動的距離賦值給變換矩陣
matrix.postTranslate(fixTransX, fixTransY);
給ImageView設置矩陣,完成X、Y的移動,即實現單指拖拽動作
setImageMatrix(matrix);
ScaleListener
private class ScaleListener extends ScaleGestureDetector.SimpleOnScaleGestureListener { @Override public boolean onScaleBegin(ScaleGestureDetector detector) { setState(State.ZOOM); return true; } @Override public boolean onScale(ScaleGestureDetector detector) { scaleImage(detector.getScaleFactor(), detector.getFocusX(), detector.getFocusY(), true); // // OnTouchImageViewListener is set: TouchImageView pinch zoomed by user. // if (touchImageViewListener != null) { touchImageViewListener.onMove(); } return true; } @Override public void onScaleEnd(ScaleGestureDetector detector) { super.onScaleEnd(detector); setState(State.NONE); boolean animateToZoomBoundary = false; float targetZoom = normalizedScale; if (normalizedScale > maxScale) { targetZoom = maxScale; animateToZoomBoundary = true; } else if (normalizedScale < minScale) { targetZoom = minScale; animateToZoomBoundary = true; } if (animateToZoomBoundary) { DoubleTapZoom doubleTap = new DoubleTapZoom(targetZoom, viewWidth / 2, viewHeight / 2, true); compatPostOnAnimation(doubleTap); } } }
兩指縮放動作會走到ScaleListener的回調,在它的onScale回調中會處理圖片的縮放
scaleImage(detector.getScaleFactor(), detector.getFocusX(), detector.getFocusY(), true);
scaleImage
private void scaleImage(double deltaScale, float focusX, float focusY, boolean stretchImageToSuper) { float lowerScale, upperScale; if (stretchImageToSuper) { lowerScale = superMinScale; upperScale = superMaxScale; } else { lowerScale = minScale; upperScale = maxScale; } float origScale = normalizedScale; normalizedScale *= deltaScale; if (normalizedScale > upperScale) { normalizedScale = upperScale; deltaScale = upperScale / origScale; } else if (normalizedScale < lowerScale) { normalizedScale = lowerScale; deltaScale = lowerScale / origScale; } matrix.postScale((float) deltaScale, (float) deltaScale, focusX, focusY); fixScaleTrans(); }
這里會將多次縮放的縮放比累積,并設置有最大和最小縮放比,不能超出范圍
normalizedScale *= deltaScale;
最后把X、Y的縮放比和焦點傳給變換矩陣,通過矩陣關聯到ImageView,完成縮放動作
matrix.postScale((float) deltaScale, (float) deltaScale, focusX, focusY);
在onScaleEnd回調中,我們會判斷是否當前縮放比超出最大縮放比或者小于最小縮放比,如果是,會有一個動畫回到最大或最小縮放比狀態(tài)
DoubleTapZoom doubleTap = new DoubleTapZoom(targetZoom, viewWidth / 2, viewHeight / 2, true); compatPostOnAnimation(doubleTap);
這里的動畫DoubleTapZoom就是雙擊動畫,關于DoubleTapZoom我們下面會講到。至此兩指縮放動作就完成了,下面繼續(xù)看雙擊縮放動作。
GestureListener
private class GestureListener extends GestureDetector.SimpleOnGestureListener { @Override public boolean onSingleTapConfirmed(MotionEvent e) { if(doubleTapListener != null) { return doubleTapListener.onSingleTapConfirmed(e); } return performClick(); } @Override public void onLongPress(MotionEvent e) { performLongClick(); } @Override public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) { if (fling != null) { // // If a previous fling is still active, it should be cancelled so that two flings // are not run simultaenously. // fling.cancelFling(); } fling = new Fling((int) velocityX, (int) velocityY); compatPostOnAnimation(fling); return super.onFling(e1, e2, velocityX, velocityY); } @Override public boolean onDoubleTap(MotionEvent e) { boolean consumed = false; if(doubleTapListener != null) { consumed = doubleTapListener.onDoubleTap(e); } if (state == State.NONE) { float targetZoom = (normalizedScale == minScale) ? maxScale : minScale; DoubleTapZoom doubleTap = new DoubleTapZoom(targetZoom, e.getX(), e.getY(), false); compatPostOnAnimation(doubleTap); consumed = true; } return consumed; } @Override public boolean onDoubleTapEvent(MotionEvent e) { if(doubleTapListener != null) { return doubleTapListener.onDoubleTapEvent(e); } return false; } }
在onDoubleTap回調中,設置雙擊縮放比,如果當前無縮放,則設置縮放比為最大值,如果已經是最大值,則設置為無縮放
float targetZoom = (normalizedScale == minScale) ? maxScale : minScale;
然后將當前點擊坐標做為縮放中心,連同縮放比一起交給DoubleTapZoom,完成縮放動畫
DoubleTapZoom doubleTap = new DoubleTapZoom(targetZoom, e.getX(), e.getY(), false); compatPostOnAnimation(doubleTap);
DoubleTapZoom
private class DoubleTapZoom implements Runnable { private long startTime; private static final float ZOOM_TIME = 500; private float startZoom, targetZoom; private float bitmapX, bitmapY; private boolean stretchImageToSuper; private AccelerateDecelerateInterpolator interpolator = new AccelerateDecelerateInterpolator(); private PointF startTouch; private PointF endTouch; DoubleTapZoom(float targetZoom, float focusX, float focusY, boolean stretchImageToSuper) { setState(State.ANIMATE_ZOOM); startTime = System.currentTimeMillis(); this.startZoom = normalizedScale; this.targetZoom = targetZoom; this.stretchImageToSuper = stretchImageToSuper; PointF bitmapPoint = transformCoordTouchToBitmap(focusX, focusY, false); this.bitmapX = bitmapPoint.x; this.bitmapY = bitmapPoint.y; // // Used for translating image during scaling // startTouch = transformCoordBitmapToTouch(bitmapX, bitmapY); endTouch = new PointF(viewWidth / 2, viewHeight / 2); } @Override public void run() { float t = interpolate(); double deltaScale = calculateDeltaScale(t); scaleImage(deltaScale, bitmapX, bitmapY, stretchImageToSuper); translateImageToCenterTouchPosition(t); fixScaleTrans(); setImageMatrix(matrix); // // OnTouchImageViewListener is set: double tap runnable updates listener // with every frame. // if (touchImageViewListener != null) { touchImageViewListener.onMove(); } if (t < 1f) { // // We haven't finished zooming // compatPostOnAnimation(this); } else { // // Finished zooming // setState(State.NONE); } } /** * Interpolate between where the image should start and end in order to translate * the image so that the point that is touched is what ends up centered at the end * of the zoom. * @param t */ private void translateImageToCenterTouchPosition(float t) { float targetX = startTouch.x + t * (endTouch.x - startTouch.x); float targetY = startTouch.y + t * (endTouch.y - startTouch.y); PointF curr = transformCoordBitmapToTouch(bitmapX, bitmapY); matrix.postTranslate(targetX - curr.x, targetY - curr.y); } /** * Use interpolator to get t * @return */ private float interpolate() { long currTime = System.currentTimeMillis(); float elapsed = (currTime - startTime) / ZOOM_TIME; elapsed = Math.min(1f, elapsed); return interpolator.getInterpolation(elapsed); } /** * Interpolate the current targeted zoom and get the delta * from the current zoom. * @param t * @return */ private double calculateDeltaScale(float t) { double zoom = startZoom + t * (targetZoom - startZoom); return zoom / normalizedScale; } }
DoubleTapZoom其實是一個線程,實現了Runnable,我們直接看它的Run方法吧,這里定義了一個時間t
float t = interpolate();
其實t在500ms內通過一個加速差值器從0到1加速增長
private float interpolate() { long currTime = System.currentTimeMillis(); float elapsed = (currTime - startTime) / ZOOM_TIME; elapsed = Math.min(1f, elapsed); return interpolator.getInterpolation(elapsed); }
通過t計算出當前縮放比
double deltaScale = calculateDeltaScale(t);
實現縮放
scaleImage(deltaScale, bitmapX, bitmapY, stretchImageToSuper);
然后根據當前t的值判斷動畫是否結束,如果t小于1,表示動畫還未結束,重新執(zhí)行本線程,否則設置狀態(tài)完成。這里就是通過在這500ms內多次執(zhí)行線程,多次重繪ImageView實現動畫效果的。
if (t < 1f) { compatPostOnAnimation(this); } else { setState(State.NONE); }
同時在GestureListener的onFling回調中,設置Fling的X、Y速度,然后執(zhí)行Fling的位移動畫
fling = new Fling((int) velocityX, (int) velocityY); compatPostOnAnimation(fling);
Fling
private class Fling implements Runnable { CompatScroller scroller; int currX, currY; Fling(int velocityX, int velocityY) { setState(State.FLING); scroller = new CompatScroller(context); matrix.getValues(m); int startX = (int) m[Matrix.MTRANS_X]; int startY = (int) m[Matrix.MTRANS_Y]; int minX, maxX, minY, maxY; if (getImageWidth() > viewWidth) { minX = viewWidth - (int) getImageWidth(); maxX = 0; } else { minX = maxX = startX; } if (getImageHeight() > viewHeight) { minY = viewHeight - (int) getImageHeight(); maxY = 0; } else { minY = maxY = startY; } scroller.fling(startX, startY, (int) velocityX, (int) velocityY, minX, maxX, minY, maxY); currX = startX; currY = startY; } public void cancelFling() { if (scroller != null) { setState(State.NONE); scroller.forceFinished(true); } } @Override public void run() { // // OnTouchImageViewListener is set: TouchImageView listener has been flung by user. // Listener runnable updated with each frame of fling animation. // if (touchImageViewListener != null) { touchImageViewListener.onMove(); } if (scroller.isFinished()) { scroller = null; return; } if (scroller.computeScrollOffset()) { int newX = scroller.getCurrX(); int newY = scroller.getCurrY(); int transX = newX - currX; int transY = newY - currY; currX = newX; currY = newY; matrix.postTranslate(transX, transY); fixTrans(); setImageMatrix(matrix); compatPostOnAnimation(this); } } }
Fling其實也是一個線程,實現了Runnable,根據Fling手勢的X、Y速度我們會執(zhí)行Scroller的fling函數,并且將當前位置設置為起始位置
scroller.fling(startX, startY, (int) velocityX, (int) velocityY, minX,maxX, minY, maxY); currX = startX; currY = startY;
再來看看Run函數,根據scroller當前滾動位置計算出新的位置信息,與舊位置相減得出在X、Y軸平移距離,實現平移
if (scroller.computeScrollOffset()) { int newX = scroller.getCurrX(); int newY = scroller.getCurrY(); int transX = newX - currX; int transY = newY - currY; currX = newX; currY = newY; matrix.postTranslate(transX, transY); fixTrans(); setImageMatrix(matrix); compatPostOnAnimation(this); }
最后延時一段時間再次調用線程完成新的平移繪圖,如此往復,直到scroller停止?jié)L動,多次重繪ImageView實現了fling動畫效果。
private void compatPostOnAnimation(Runnable runnable) { if (VERSION.SDK_INT >= VERSION_CODES.JELLY_BEAN) { postOnAnimation(runnable); } else { postDelayed(runnable, 1000/60); } }
下面看一看顯示效果吧:
單個圖片
圖片加載到ViewPager中
鏡像圖片
點擊可改變圖片
點擊可改變ScaleType
以上所述是小編給大家介紹的Android使用ImageView實現支持手勢縮放效果,希望對大家有所幫助,如果大家有任何疑問請給我留言,小編會及時回復大家的。在此也非常感謝大家對腳本之家網站的支持!
相關文章
Android 讓自定義TextView的drawableLeft與文本一起居中
本文主要介紹Android 自定義控件TextView顯示居中問題,在開發(fā)過程中經常會遇到控件的重寫,這里主要介紹TextView的drawableLeft與文本一起居中的問題2016-07-07android scrollview 滑動到頂端或者指定位置的實現方法
下面小編就為大家?guī)硪黄猘ndroid scrollview 滑動到頂端或者指定位置的實現方法。小編覺得挺不錯的,現在就分享給大家,也給大家做個參考。一起跟隨小編過來看看吧2017-04-04Android基礎總結篇之三:Activity的task相關介紹
這篇文章主要介紹了Android基礎總結篇之三:Activity的task相關介紹,具有一定的參考價值,有需要的可以了解一下。2016-11-11Android自定義ScrollView實現放大回彈效果實例代碼
本篇文章主要介紹了Android自定義ScrollView實現放大回彈效果實例代碼,具有一定的參考價值,有興趣的可以了解一下。2017-03-03