整理自 鴻洋大神的慕課網視頻
加了很多自己理解的注注釋
package MyView;
import android.content.Context
;
import android.graphics.Matrix
;
import android.graphics.RectF
;
import android.graphics.drawable.Drawable
;
import android.support.v4.view.ViewPager
;
import android.util.AttributeSet
;
import android.view.GestureDetector
;
import android.view.MotionEvent
;
import android.view.ScaleGestureDetector
;
import android.view.View
;
import android.view.ViewConfiguration
;
import android.view.ViewParent
;
import android.view.ViewTreeObserver
;
import android.widget.ImageView
;
/**
* 圖片預覽與多點觸控ImageView
* <p>
* <p>
* Created by wjc on 2017/3/8.
*/
public class ZoomImageView
extends ImageView
implements ViewTreeObserver.OnGlobalLayoutListener
, ScaleGestureDetector.OnScaleGestureListener
, View.OnTouchListener {
/**
* 因為縮放只需在加載圖片完成后進行一次,所以需要設置一個標記
*/
private boolean mOnce;
/**
* 初始化時縮放的值,也就是根據圖片于控件寬高計算得到的縮放比例
*/
private float mInitScale;
/**
* 雙擊放大的的scale比例
*/
private float mMidScale;
/**
* 允許放大的最大縮放比例
*/
private float mMaxScale;
/**
* 縮放矩陣
*/
private Matrix
mScaleMatrix;
/**
* 可以獲取到當前用戶手勢的 縮放比例
*/
private ScaleGestureDetector
mScaleGestureDetector;
//------------------自由移動所需變量
/**
* 記錄上一次多點觸控的點的數量,因為4個手指變為2個手指的時候,
* 觸控的中心可能就瞬間發生很大的跳躍變化,如果只是根據觸控中心位置實時移動圖片,用戶體驗差
*/
private int mLastPointerCount;
private float mLastX; //記錄上次多指的中心點
private float mlastY;
private int mTouchSlop;
private boolean isCanDrag;
private RectF
matrixRectF;
private boolean isCheckLeftAndRight;
private boolean isCheckTopAndBottom;
//-----------------雙擊放大與縮小
private GestureDetector
mGestureDetector;
private boolean isAutoScale; //當前是否處在雙擊后的縮放過程當中
public ZoomImageView(Context context) {
this(context
, null)
;
}
public ZoomImageView(Context context
, AttributeSet attrs) {
this(context
, attrs
, 0)
;
}
public ZoomImageView(Context context
, AttributeSet attrs
, int defStyleAttr) {
super(context
, attrs
, defStyleAttr)
;
mScaleMatrix =
new Matrix()
;
setScaleType(ScaleType.
MATRIX)
;//覆蓋xml的scaleType 因為這里的縮放是建立在ScaleType.MATRIX的
mScaleGestureDetector =
new ScaleGestureDetector(context
, this)
;
setOnTouchListener(
this)
;
mTouchSlop = ViewConfiguration.
get(context).getScaledTouchSlop()
;
mGestureDetector =
new GestureDetector(context
, new GestureDetector.SimpleOnGestureListener() {
/**
*
* 雙擊的圖片放大縮小,注意邊界處理
* 如果雙擊直接放大到目標大小,中間是沒有變化過程的,會非常突兀,可以使用runnable,分成多次,延時post
*
*/
@Override
public boolean onDoubleTap(MotionEvent e) {
/**
* 當前仍在縮放 就直接return;
*/
if (
isAutoScale) {
return true;
}
float x = e.getX()
;
float y = e.getY()
;
float currentScale = getScale()
;
/**
* 兩個特殊情況:
* 當前為 mMidScale 會縮小為mInitScale
* 當前為 mInitScale 會放大為mMidScale
* 也就是說 不會出現雙擊后無變化的情況。mTargetScale不會等于getScale;
*/
if (currentScale <
mMidScale) {
// mScaleMatrix.postScale(mMidScale / currentScale, mMidScale / currentScale, x, y); //這是直接縮放,不可取
postDelayed(
new AutoScaleRunnable(
mMidScale, x
, y)
, 16)
;
isAutoScale =
true;
}
else {
// mScaleMatrix.postScale(mInitScale / currentScale, mInitScale / currentScale, x, y); //
postDelayed(
new AutoScaleRunnable(
mInitScale, x
, y)
, 16)
;
isAutoScale =
true;
}
return true;
}})
;
}
/**
* 自動緩慢放大與縮小
*/
private class AutoScaleRunnable
implements Runnable {
/**
* 縮放的目標值以及縮放的中心點位置(雙擊位置)
*/
private float mTargetScale;
private float x;
private float y;
private final float BIGGER =
1.07f;
private final float SMALLER =
0.93f;
private float tmpScale;
public AutoScaleRunnable(
float mTargetScale
, float x
, float y) {
this.
mTargetScale = mTargetScale
;
this.
x = x
;
this.
y = y
;
//確定當前雙擊 每次的scale系數,放大還是縮小
if (mTargetScale > getScale()) {
tmpScale =
BIGGER;
}
else {
tmpScale =
SMALLER;
}}
@Override
public void run() {
/**
* 進行縮放
*/
mScaleMatrix.postScale(
tmpScale, tmpScale, x, y)
;
checkBorderAndCenterWhenScale()
;
setImageMatrix(
mScaleMatrix)
;
float currentScale = getScale()
;
/**
* 判斷本次縮放之后如果仍未達到目標值,接著進行下一循環的縮放,判斷
*/
if (
tmpScale >
1.0f && currentScale <
mTargetScale ||
tmpScale <
1.0f && currentScale >
mTargetScale) {postDelayed(
this, 16)
;
}
else {
/**
* 臨界判定
*
* 如果本次縮放之后已經達到或者超過了目標值
* 那么直接縮放到目標值
*
*/
float scale =
mTargetScale / currentScale
;
mScaleMatrix.postScale(scale
, scale
, x, y)
;
checkBorderAndCenterWhenScale()
;
setImageMatrix(
mScaleMatrix)
;
isAutoScale =
false;
}}}
/**
* 注冊與取消注冊注冊 OnGlobalLayoutListener
*/
@Override
protected void onAttachedToWindow() {
super.onAttachedToWindow()
;
getViewTreeObserver().addOnGlobalLayoutListener(
this)
;
}
@Override
protected void onDetachedFromWindow() {
super.onDetachedFromWindow()
;
getViewTreeObserver().removeGlobalOnLayoutListener(
this)
;
}
/**
* 全局布局完成后 調用以下OnGlobalLayoutListener的實現方法
* 在這里獲取控件的寬高是比較合適的
* 獲取ImageView加載完成的圖片,并且比較圖片大小與屏幕大小 根據結果進行縮放
* 縮放之后,能居中顯示完整的圖片,并且寬或者高頂到ImageView的邊
*/
@Override
public void onGlobalLayout() {
if (!
mOnce) {
int width = getWidth()
; //獲取當前控件的寬和高
int height = getHeight()
;
Drawable d = getDrawable()
;
if (d ==
null) {
return;
}
int dw = d.getIntrinsicWidth()
; //獲得加載完成的圖片寬高屬性
int dh = d.getIntrinsicHeight()
;
float scale =
1.0f;
//四種情況
/**
* 縮放策略
*/
if (dw > width && dh < height) {scale = width *
1.0f / dw
;
}
else if (dw < width && dh > height) {scale = height *
1.0f / dh
;
}
else {scale = Math.
min(width *
1.0f / dw
, height *
1.0f / dh)
;
}
mInitScale = scale
;
mMidScale =
mInitScale *
2;
mMaxScale =
mInitScale *
4;
/**
* 將圖片移動到控件的中心
*/
int translateX = (width - dw) /
2;
int translateY = (height - dh) /
2;
mScaleMatrix.postTranslate(translateX
, translateY)
;
mScaleMatrix.postScale(
mInitScale, mInitScale, width /
2, height /
2)
;
setImageMatrix(
mScaleMatrix)
;
mOnce =
true;
}}
/**
* 獲取當前的總體縮放比例(在縮放矩陣中可以查到)
*/
public float getScale() {
float[] values =
new float[
9]
;
mScaleMatrix.getValues(values)
;
return values[Matrix.
MSCALE_X]
;
}
/**
* ScaleGestureDetector的三個要實現的方法
* 監聽 由nTouch傳遞過來的多點觸控的縮放手勢
* <p>
* 本次onScale完成后的最終總體縮放結果,就是
* 原來的總體縮放結果getScale的值乘以本次的縮放因子scaleFactor
*/
@Override
public boolean onScale(ScaleGestureDetector detector) {
float scale = getScale()
;
float scaleFactor = detector.getScaleFactor()
;//獲得當前手勢的縮放比例,可能大于1 也可能小于1
if (getDrawable() ==
null) {
return true;
}
//注意判斷在每一次調用onScare前后,總的縮放比例都應該在 mInitScale和mMaxScale 之間
if (scale <
mMaxScale && scaleFactor >
1.0f || scale >
mInitScale && scaleFactor <
1.0f) {
//在滿足前面的條件下,如果在這次縮放之后 比例超出了范圍 就要調整scaleFactor
if (scale * scaleFactor <
mInitScale) {
/**
* 修正scaleFactor使最終的縮放結果為mInitScale,不能再小了
* 理論上 使scaleFactor直接等于1也可以,也就是在本次縮放會觸及邊界的情況下,取消本次縮放
* 但是如果是修正縮放結果為剛好達到邊界,效果自然是最好的
*/
scaleFactor =
mInitScale / scale
; // 使最終的縮放結果為mInitScale,不能再小了
}
if (scale * scaleFactor >
mMaxScale) {scaleFactor =
mMaxScale / scale
; // 使最終的縮放結果為mMaxScale,不能再大了
}
/**
* 后兩個參數是縮放的中心點,將其設為當前縮放手勢的中心點
*/
mScaleMatrix.postScale(scaleFactor
, scaleFactor
, detector.getFocusX()
,
detector.getFocusY())
;
/**
*
* 如果直接設置縮放中心點為手勢點的話,幾次縮放之后不僅圖片中心會偏移,屏幕上可能還會出現留白
* 所以需要在縮放之前 即時檢測調整,并且將調整后的偏移一起post到mScaleMatrix中
*/
checkBorderAndCenterWhenScale()
;
setImageMatrix(
mScaleMatrix)
;
}
return true;
}
/**
* 在縮放的時候進行邊界控制 以及位置控制
* 總體思路是 圖片較大的時候 處理留白 , 圖片較小的時候 處理居中
*/
private void checkBorderAndCenterWhenScale() {RectF rectF = getMatrixRectF()
;
//為了修正需要偏移的值
float deltaX =
0;
float deltaY =
0;
int width = getWidth()
;
int height = getHeight()
;
/**
* 進行邊界判斷
* 當圖片寬度大于控件寬度,而控件左右有留白
* 當圖片高度大于控件高度,而控件上下有留白
* 都是需要進行偏移處理覆蓋留白的
*/
if (rectF.width() >= width) {
if (rectF.
left >
0) {deltaX = -rectF.
left;
}
if (rectF.
right < width) {deltaX = width - rectF.
right;
}}
if (rectF.height() >= height) {
if (rectF.
top >
0) {deltaY = -rectF.
top;
}
if (rectF.
bottom < height) {deltaY = height - rectF.
bottom;
}}
/**
* 如果圖片的寬度或者高度小于控件的 則進行居中
* 計算的偏移量的時候要注意此時的圖片的邊不一定與控件的左邊或者上邊重合(基本不會重合)
*/
if (rectF.width() < width) {deltaX = width /
2f - rectF.
left - rectF.width() /
2f;
}
if (rectF.height() < height) {deltaY = height /
2f - rectF.
top - rectF.height() /
2f;
}
mScaleMatrix.postTranslate(deltaX
, deltaY)
;
}
/**
* RectF:保存一個矩形的left;top;right;bottom;四個參數,且都是float
* <p>
* 獲取當前圖片的矩形坐標
*/
private RectF
getMatrixRectF() {Matrix matrix =
mScaleMatrix;
RectF rectF =
new RectF()
;
Drawable d = getDrawable()
;
if (d !=
null) {
//設定圖片在經過Matrix變換之前的Rect坐標
rectF.set(
0, 0, d.getIntrinsicWidth()
, d.getIntrinsicHeight())
;
//將rectF坐標進行矩陣變換之后的坐標重新賦值給rectF
matrix.mapRect(rectF)
;
}
return rectF
;
}
@Override
public boolean onScaleBegin(ScaleGestureDetector detector) {
return true;
}
@Override
public void onScaleEnd(ScaleGestureDetector detector) {}
/**
* 實現OnTouchListener 并且監聽onTouchListener,然后在實現方法onTouch中 將觸摸時事件交給ScaleGestureDetector來處理
* 畢竟是系統的多點觸控處理API,很專業 哈哈
*/
@Override
public boolean onTouch(View v
, MotionEvent event) {
//優先處理雙擊,以免在雙擊的時候產生縮放或者移動
if (
mGestureDetector.onTouchEvent(event)) {
return true;
}
//將多點的scale移交給mScaleGestureDetector
mScaleGestureDetector.onTouchEvent(event)
;
/**
* 以下是平移操作
*/
float x =
0;//保存多點觸控的中心點
float y =
0;
//獲取當前的觸控點數
int pointerCount = event.getPointerCount()
;
for (
int i =
0; i < pointerCount
; i++) {x += event.getX(i)
;
y += event.getY(i)
;
}x /= pointerCount
;//算出當前的中心點位置
y /= pointerCount
;
/**
* isCanDrag為false有兩種情況,一種是觸控點數發生了變化,我們只是記錄位置
* 另一種是位移太小
* 這兩種情況 我們都不會postTranslate
*
*/
if (
mLastPointerCount != pointerCount) {
isCanDrag =
false;
mLastX = x
;
mlastY = y
;
}
mLastPointerCount = pointerCount
;
RectF rect= getMatrixRectF()
;
switch (event.getAction()) {
/**
* 在 down和move的時候進行判斷 如果當前圖片已經經過人為放大了,那么就請求父控件(Viewpager)
* 不要攔截touch事件,讓圖片可以移動查看 而不會滑動到viewpager的下一張
*
* 這里加 0.01 是因為避免浮點數計算丟失精確度 確保在mInitScale的情況下可以翻到下一張
*/
case MotionEvent.
ACTION_DOWN:
if(rect.width()>getWidth()+
0.01||rect.height()>getHeight()+
0.01){
if(getParent()
instanceof ViewPager)getParent().requestDisallowInterceptTouchEvent(
true)
;
}
break;
case MotionEvent.
ACTION_MOVE:
if(rect.width()>getWidth()+
0.01||rect.height()>getHeight()+
0.01){
if(getParent()
instanceof ViewPager)getParent().requestDisallowInterceptTouchEvent(
true)
;
}
float dx = x -
mLastX;
float dy = y -
mlastY;
if (!
isCanDrag) {
isCanDrag = isMoveAction(dx
, dy)
;
}
if (
isCanDrag) {RectF rectF = getMatrixRectF()
;
if (getDrawable() !=
null) {
isCheckLeftAndRight =
isCheckTopAndBottom =
true;
/**
* 圖片寬度小于控件寬度,不允許橫向移動
* 既然不能移動,也就不需要移動時邊界控制
*/
if (rectF.width() < getWidth()) {
isCheckLeftAndRight =
false;
dx =
0;
}
//圖片高度小于控件高度,不允許縱向移動
if (rectF.height() < getHeight()) {
isCheckTopAndBottom =
false;
dy =
0;
}
mScaleMatrix.postTranslate(dx
, dy)
;
/**
* 平移的時候也是需要邊界控制的
*/
checkBorderWhenTranslate()
;
setImageMatrix(
mScaleMatrix)
;
}}
mLastX = x
;//移動過程中不斷記錄上一次的xy
mlastY = y
;
break;
case MotionEvent.
ACTION_UP:
case MotionEvent.
ACTION_CANCEL:
mLastPointerCount =
0;
break;
default:
break;
}
return true;
}
/**
* 移動時 進行邊界控制
*/
private void checkBorderWhenTranslate() {RectF rectF = getMatrixRectF()
;
float deltaX =
0;
float deltaY =
0;
int width = getWidth()
;
int height = getHeight()
;
/**
* 左側有留白,并且此時圖片寬度是大于控件寬度的
*/
if (rectF.
left >
0 &&
isCheckLeftAndRight) {deltaX = -rectF.
left;
}
if (rectF.
right < width &&
isCheckLeftAndRight) {deltaX = width - rectF.
right;
}
if (rectF.
top >
0 &&
isCheckTopAndBottom) {deltaY = -rectF.
top;
}
if (rectF.
bottom < height &&
isCheckTopAndBottom) {deltaY = height - rectF.
bottom;
}
mScaleMatrix.postTranslate(deltaX
, deltaY)
;
}
/**
* 判斷偏移量是否足以觸發move
*/
private boolean isMoveAction(
float dx
, float dy) {
return Math.
sqrt(dx * dx + dy * dy) >
mTouchSlop;
}
}
總結
以上是生活随笔為你收集整理的图片的多点触控缩放与移动的全部內容,希望文章能夠幫你解決所遇到的問題。
如果覺得生活随笔網站內容還不錯,歡迎將生活随笔推薦給好友。