本文是根據鴻洋,打造個性的圖片預覽與多點觸控而來,主要是熟悉里面的效果
效果
可以看到上面的效果是可以根據多指縮放,雙擊放大縮小,同時嵌套ViewPager
關于這樣的效果國外有個小伙Chris Banes寫的很好,PhotoView
具體實現步驟:
圖片加載時實現監聽
自定義控件并且繼承自ImageView,我們知道在oncreate中View.getWidth和View.getHeight無法獲得一個view的高度和寬度,這是因為View組件布局要在onResume回調后完成。所以現在需要使用getViewTreeObserver().addOnGlobalLayoutListener()來獲得寬度或者高度。這是獲得一個view的寬度和高度的方法之一。重寫onAttachedToWindow()方法
@Override
protected void onAttachedToWindow() {
super.onAttachedToWindow();
getViewTreeObserver().addOnGlobalLayoutListener(this);
}
實現方法,并且獲取圖片寬高:
@Override
public void onGlobalLayout() {
//布爾值防止多次加載
if (!once) {
//獲取屏幕的寬高
int width = getWidth();
int height = getHeight();
//獲取加載到的圖片資源
Drawable drawable = getDrawable();
//獲取圖片的寬高
int dWidth = drawable.getIntrinsicWidth();
int dHeight = drawable.getIntrinsicHeight();
//初始化的時候,我們要將圖片居中顯示
//縮放比例
float scale = 1.0f;
if (dWidth > width && dHeight < height) {
scale = width * 1.0f / dWidth;
}
if (dHeight > height && dWidth < width) {
scale = height * 1.0f / dHeight;
}
if ((dWidth > width && dHeight > height) || (dWidth < width && dHeight < height)) {
scale = Math.min(width * 1.0f / dWidth, height * 1.0f / dHeight);
}
//初始化縮放比例
mInitScale = scale;
//最大縮放比例
mMaxScale = mInitScale * 4;
//中等縮放比例
mMidScale = mInitScale * 2;
//圖片移動到中心的距離
int dx = getWidth() / 2 - dWidth / 2;
int dy = getHeight() / 2 - dHeight / 2;
//進行平移
mScaleMatrix.postTranslate(dx, dy);
//進行縮放
mScaleMatrix.postScale(mInitScale, mInitScale, width / 2, height / 2);
setImageMatrix(mScaleMatrix);
once = true;
}
}
通過上面的步驟可以設置圖片居中顯示,比例縮放到正確的位置!
接下來實現圖片縮放
多手指縮放需要用到的一個類是ScaleGestureDetector,我們在構造初始化它
//初始化Matrix
mScaleMatrix = new Matrix();
//預防在布局里沒有或者設置其他類型
super.setScaleType(ScaleType.MATRIX);
//縮放初始化
mScaleGestureDetector = new ScaleGestureDetector(context, this);
//同樣,縮放的捕獲要建立在setOnTouchListener上
setOnTouchListener(this);
這樣實現其方法:
@Override
public boolean onScale(ScaleGestureDetector detector) {
float scaleFactor = detector.getScaleFactor();
float scale = getScale();
if (getDrawable() == null) {
return true;
}
//這里是想放大和縮小
if ((scale < mMaxScale && scaleFactor > 1.0f) || (scale > mInitScale && scaleFactor < 1.0f)) {
//這里如果要縮放的值比初始化還要小的話,就按照最小可以縮放的值進行縮放
if (scale * scaleFactor < mInitScale) {
scaleFactor = mInitScale / scale;
}
//這個是放大的同理
if (scale * scaleFactor > mMaxScale) {
scaleFactor = mMaxScale / scale;
}
//detector.getFocusX(), detector.getFocusY(),是在縮放中心點進行縮放
mScaleMatrix.postScale(scaleFactor, scaleFactor, detector.getFocusX(), detector.getFocusY());
//在縮放的時候會出現圖片漏出白邊,位置出現移動,所以要另外做移動處理
checkBorderAndCenterWhenScale();
setImageMatrix(mScaleMatrix);
}
return true;
}
@Override
public boolean onScaleBegin(ScaleGestureDetector detector) {
//開始時設置為true
return true;
}
@Override
public void onScaleEnd(ScaleGestureDetector detector) {
}
檢查露出時用到的方法
private void checkBorderAndCenterWhenScale() {
RectF matrixRectF = getMatrixRectF();
float deltaX = 0;
float deltY = 0;
int width = getWidth();
int height = getHeight();
//縮放后的寬度大于屏幕
if (matrixRectF.width() >= width) {
if (matrixRectF.left > 0) {
//這就是說左邊露出了一部分,怎么辦,補上啊,補多少?
deltaX = -matrixRectF.left;
}
if (matrixRectF.right < width) {
//這就是右邊露出了
deltaX = width - matrixRectF.right;
}
}
if (matrixRectF.height() >= height) {
if (matrixRectF.top > 0) {
deltY = -matrixRectF.top;
}
if (matrixRectF.bottom < height) {
deltY = -height - matrixRectF.bottom;
}
}
//如果寬或者是高,小于屏幕的話,那就沒理由的居中就行
if (matrixRectF.width() < width) {
deltaX = width / 2f - matrixRectF.right + matrixRectF.width() / 2;
}
if (matrixRectF.height() < height) {
deltY = height / 2f - matrixRectF.bottom + matrixRectF.height() / 2;
}
mScaleMatrix.postTranslate(deltaX, deltY);
}
獲取縮放值
/**
* 獲取縮放
*
* @return
*/
private float getScale() {
float[] values = new float[9];
mScaleMatrix.getValues(values);
return values[Matrix.MSCALE_X];
}
實現圖片放大后移動查看
移動需要在OnTouch里處理:
@Override
public boolean onTouch(View v, MotionEvent event) {
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;
if (mLastPointCount != pointerCount) {
//手指變化后就不能繼續拖拽
isCanDrag = false;
//記錄最后的位置,重置
mLatX = x;
mLastY = y;
}
//記錄最后一次手指的個數
mLastPointCount = pointerCount;
RectF rectF = getMatrixRectF();
switch (event.getAction()) {
case MotionEvent.ACTION_MOVE:
//x,y移動的距離
float dx = x - mLatX;
float dy = y - mLastY;
//如果是不能拖拽,可能是因為手指變化,這時就去重新檢測看看是不是符合滑動
if (!isCanDrag) {
//反正是根據勾股定理,調用系統API
isCanDrag = isMoveAction(dx, dy);
}
if (isCanDrag) {
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);
//拖拽的時候會露出一部分空白,要補上
checkBorderAndCenterWhenTranslate();
setImageMatrix(mScaleMatrix);
}
}
mLatX = x;
mLastY = y;
break;
case MotionEvent.ACTION_UP:
case MotionEvent.ACTION_CANCEL:
mLastPointCount = 0;
break;
}
return true;
}
補上空白
private void checkBorderAndCenterWhenTranslate() {
RectF rectF = getMatrixRectF();
float deltax = 0;
float deltay = 0;
int width = getWidth();
int height = getHeight();
if (rectF.top > 0 && isCheckTopAndBottom) {
deltay = -rectF.top;
}
if (rectF.bottom < height && isCheckTopAndBottom) {
deltay = height - rectF.bottom;
}
if (rectF.left > 0 && isCheckLeftAndRight) {
deltax = -rectF.left;
}
if (rectF.right < width && isCheckLeftAndRight) {
deltax = width - rectF.right;
}
mScaleMatrix.postTranslate(deltax, deltay);
}
判斷是否是滑動
private boolean isMoveAction(float dx, float dy) {
return Math.sqrt(dx * dx + dy * dy) > mTouchSlop;
}
雙擊實現放大和縮小
雙擊需要用到系統的一個類,在構造里初始化,同樣也需要在OnTouch里進行關聯
@Override
public boolean onTouch(View v, MotionEvent event) {
//雙擊進行關聯
if (mGestureDetector.onTouchEvent(event)) {
//如果是雙擊的話就直接不向下執行了
return true;
}
//縮放進行關聯
mScaleGestureDetector.onTouchEvent(event);
...
}
在構造里進行處理雙擊監聽
public ZoomImageView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
//初始化Matrix
mScaleMatrix = new Matrix();
//預防在布局里沒有或者設置其他類型
super.setScaleType(ScaleType.MATRIX);
//縮放初始化
mScaleGestureDetector = new ScaleGestureDetector(context, this);
//同樣,縮放的捕獲要建立在setOnTouchListener上
setOnTouchListener(this);
//符合滑動的距離
mTouchSlop = ViewConfiguration.get(context).getScaledTouchSlop();
//自動縮放時需要有一個自動的過程
mGestureDetector = new GestureDetector(context, new GestureDetector.SimpleOnGestureListener() {
@Override
public boolean onDoubleTap(MotionEvent e) {
//如果再自動縮放中就不向下執行,防止多次雙擊
if (isAutoScaling) {
return true;
}
//縮放的中心點
float x = e.getX();
float y = e.getY();
if (getScale() < mMidScale) {
isAutoScaling = true;
postDelayed(new AutoScaleRunble(mMidScale, x, y), 16);
} else {
isAutoScaling = true;
postDelayed(new AutoScaleRunble(mInitScale, x, y), 16);
}
return true;
}
});
}
自動縮放處理類,實現Runnable
private class AutoScaleRunble implements Runnable {
private float mTrgetScale;
private float x;
private float y;
private float tempScale;
private float BIGGER = 1.07f;
private float SMALLER = 0.93f;
//構造傳入縮放目標值,縮放的中心點
public AutoScaleRunble(float mTrgetScale, float x, float y) {
this.mTrgetScale = mTrgetScale;
this.x = x;
this.y = y;
if (getScale() < mTrgetScale) {
tempScale = BIGGER;
}
if (getScale() > mTrgetScale) {
tempScale = SMALLER;
}
}
@Override
public void run() {
mScaleMatrix.postScale(tempScale, tempScale, x, y);
checkBorderAndCenterWhenScale();
setImageMatrix(mScaleMatrix);
float currentScale = getScale();
//如果你想放大并且當然值并沒有到達目標值,可以繼續放大,同理縮小也是一樣
if ((tempScale > 1.0f && currentScale < mTrgetScale) || (tempScale < 1.0f && currentScale > mTrgetScale)) {
postDelayed(this, 16);
} else {//此時不能再進行放大或者縮小了,要放大為目標值
float scale = mTrgetScale / currentScale;
mScaleMatrix.postScale(scale, scale, x, y);
checkBorderAndCenterWhenScale();
setImageMatrix(mScaleMatrix);
isAutoScaling = false;
}
}
}
最后嵌入到ViewPager,這里要做一個處理,在OnTouch.因為ViewPager,滑動是需要攔截時間自己處理翻頁的
case MotionEvent.ACTION_DOWN:
//當圖片放大時,這個時候左右滑動查看圖片,就請求ViewPager不攔截事件!
if (rectF.width() > getWidth() + 0.01 || rectF.height() > getHeight()) {
if (getParent() instanceof ViewPager) {
getParent().requestDisallowInterceptTouchEvent(true);
}
}
break;
case MotionEvent.ACTION_MOVE:
//x,y移動的距離
float dx = x - mLatX;
float dy = y - mLastY;
//這里的處理是,當圖片移動到最邊緣的時候,不能在移動了,此時是應該Viewpager去處理事件,翻頁
if ((dx < 0 && rectF.right <= getWidth()) || (dx > 0 && rectF.left >= 0)) {
if (getParent() instanceof ViewPager) {
//讓父類進行攔截處理
getParent().requestDisallowInterceptTouchEvent(false);
}
} else if (rectF.width() > getWidth() + 0.01 || rectF.height() > getHeight()){
if (getParent() instanceof ViewPager) {
//讓父類進行攔截處理
getParent().requestDisallowInterceptTouchEvent(true);
}
}
\MainAvtivity和ZoomImageView的鏈接
總結
雖然以上代碼可以實現我們需要的功能但是還有不完美的地方,下面看看效果就知道:
從效果可以看出,當我的圖片放大后,處于邊緣時,我如果向右滑動可以切換,確實是實現了這樣的效果,但是我當切換手指不松開,然后向反方向滑動時,會切出另外一面的pager,而不是去繼續移動大圖查看隱藏的部分,為什么會這樣呢,因為當我圖片放大正好左邊處于邊緣時,如果向右切換,這個時候是可以切換的,并且這個時候讓VIewPager接管了滑動事件處理
if ((dx < 0 && rectF.right <= getWidth()) || (dx > 0 && rectF.left >= 0)) {
if (getParent() instanceof ViewPager) {
//讓父類進行攔截處理
getParent().requestDisallowInterceptTouchEvent(false);
}
}
那么問題就來了,這個時候VIewPager切換時手指并沒有離開,事件處理依然掌握,當反方向切換時當然是會執行右邊切換!!
最好的辦法就是,當ViewPager左邊切換時,如果放棄左邊切換此時再把事件給子控件,這樣圖片又可以繼續移動查看了!
例如微信就可以實現這個效果!
本代碼中目前我還沒想到好的解決辦法,有誰知道請告訴我!!