Android自定義PhotoView使用教程
準備工作
自定義PhotoView
自定義 PhotoView 繼承(extends)自 View。并在最中間顯示后面操作的圖片。繪制圖片可以重寫 onDraw()方法,并在里面通過Canvas.drawBitmap()來要繪制圖片。
drawBitmap()的四個參數(shù):
- bitmap: 要在 Canvas 中繪制的位圖
- letf:正在繪制的位圖左側(cè)的位置
- top:正在繪制的位圖頂部的位置
- paint: 畫筆
其中 (left, top) 是要繪制圖片的起始坐標。要將圖片繪制在中間,我們就需要計算 left/top 的位置。我們重寫 onSizeChanged() 函數(shù),該函數(shù)在onDraw之前調(diào)用,且尺寸改變時也要調(diào)用。
其中:(下面代碼中是用 originalOffsetX/originalOffsetY 來代替的)
left = (getWidth() - bitmap.getWidth()) / 2;
top =(getHeight() - bitmap.getHeight()) / 2;

public class PhotoView extends View {
private static final float IMAGE_WIDTH = Utils.dpToPixel(300);
private Bitmap bitmap;
private Paint paint; // 畫筆
private float originalOffsetX;
private float originalOffsetY;
public PhotoView(Context context) {
this(context, null);
}
public PhotoView(Context context, @Nullable AttributeSet attrs) {
this(context, attrs, 0);
}
public PhotoView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init();
}
/**
* 初始化操作
*/
private void init() {
bitmap = Utils.getPhoto(getResources(), (int) IMAGE_WIDTH); // 獲取到圖片
paint = new Paint();
}
/**
* TODO 在onDraw之前調(diào)用,且尺寸改變時也要調(diào)用
*/
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
originalOffsetX = (getWidth() - bitmap.getWidth()) / 2f;
originalOffsetY = (getHeight() - bitmap.getHeight()) / 2f;
}
/**
* 畫出圖片
* @param canvas 畫布
*/
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
canvas.drawBitmap(bitmap, originalOffsetX, originalOffsetY, paint);
}
}- xml 布局
xml 布局中最外層是 FragmeLayout,里面只有一個自定義的 PhotoView 用來展示圖片。
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<com.example.photoview2.PhotoView
android:layout_width="match_parent"
android:layout_height="match_parent"/>
</FrameLayout>- Utils 工具類
Utils 工具類里主要有兩個函數(shù)。dpToPixel() 將 dp 轉(zhuǎn)換為像素;getPhot() 加載 Drawable 下的圖片,并返回為 bitmap 類型。
public class Utils {
public static float dpToPixel(float dp) {
return TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dp,
Resources.getSystem().getDisplayMetrics());
}
public static Bitmap getPhoto(Resources res, int width) {
BitmapFactory.Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = true;
BitmapFactory.decodeResource(res, R.drawable.photo, options);
options.inJustDecodeBounds = false;
options.inDensity = options.outWidth;
options.inTargetDensity = width;
return BitmapFactory.decodeResource(res, R.drawable.photo, options);
}
}1、雙擊放大和縮小

- 設(shè)置圖片的縮放比例
如下圖的三種情況,左邊的是原圖;中間是小放大(smallScale),即圖片左右兩邊貼進屏幕;右邊是大放大(bigScale),即圖片沾滿整個屏幕。

根據(jù)上面的描述,設(shè)置兩個變量即 smallScale 和 bigScale 分別代表上圖"中"和“右”的縮放比例,smallScale 是初始樣式,bigSmall 是雙擊后的樣式。將 smallScale 和 bigScale 的設(shè)置放在 onSizeChanged() 函數(shù)里設(shè)值。如下圖所示

/**
* TODO 在onDraw之前調(diào)用,且尺寸改變時也要調(diào)用
*/
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
originalOffsetX = (getWidth() - bitmap.getWidth()) / 2f;
originalOffsetY = (getHeight() - bitmap.getHeight()) / 2f;
// TODO 判斷 bitmap 是扁的還是長的
if ((float)bitmap.getWidth() / bitmap.getHeight() > (float) getWidth() / getHeight()) {
// bitmap 的 width > height
smallScale = (float) getWidth() / bitmap.getWidth();
bigScale = (float) getHeight() / bitmap.getHeight() * OVER_SCALE_FACTOR;
}else {
// bitmap 的 height > width
smallScale = (float) getHeight() / bitmap.getHeight();
bigScale = (float) getWidth() / bitmap.getWidth() * OVER_SCALE_FACTOR;
}
currentScale = smallScale;
}注意 if 里的判斷條件,判斷圖片是扁平還是長的。如下圖理解,當然我們這里用的圖是扁平的。currentScale 是當前的縮放比例,smallScale <= currentScale <= bigScale 。

最后設(shè)置了 smallScale 和 bigScale 后,我們還要在 onDraw 里將 smallScale 放大的圖片繪制出來。這里用 currentScale ,因為在 onSizeChanged 函數(shù)里,我們將 smallScale 賦值給了 currentScale 。使用 Canvas.scale 函數(shù)進行縮放。
// TODO 圖片放大, // 第1,2個參數(shù)是放大比例,第3,4個參數(shù)是縮放的起始點,默認是(0,0) canvas.scale(currentScale, currentScale, getWidth() / 2f, getHeight() / 2f);
- 雙擊擊縮放
Android 為我們提供了一個 GestureDetector 類來實現(xiàn)雙擊、單擊、滑動、慣性滑動等。在 init 函數(shù)里添加如下代碼,初始化 GestureDetector。gestureDectector 是一個全局變量。
gestureDetector = new GestureDetector(context, new photoGestureListener());
GestureDetector 的第二個參數(shù)是一個 Listener ,所以我們寫了個內(nèi)部類 photoGestureListener 繼承GestureDetector.SimpleOnGestureListener。SimpleOnGestureListener 是一個 interface, 所以我們重寫里面的方法,其中onDoubleTap() 就是實現(xiàn)寫雙擊縮放的。
注意:onDown() 方法要返回 true 才能響應(yīng)到雙擊事件
/**
* TODO 單擊/雙擊/慣性滑動的監(jiān)聽
*/
class photoGestureListener extends GestureDetector.SimpleOnGestureListener{
// up 時觸發(fā),單擊或者雙擊的第一次會觸發(fā) --- up時,如果不是雙擊的得二次點擊,不是長按,則觸發(fā)
@Override
public boolean onSingleTapUp(MotionEvent e) {
return super.onSingleTapUp(e);
}
// 長按 默認300ms后觸發(fā)
@Override
public void onLongPress(MotionEvent e) {
super.onLongPress(e);
}
/**
* 滾動 --move
* @param e1 手指按下
* @param e2 當前動作
* @param distanceX 就位置 - 新位置
* @param distanceY
* @return
*/
@Override
public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
return super.onScroll(e1, e2, distanceX, distanceY);
}
/**
* 慣性滑動
* @param velocityX X軸方向運動速度 像素/s
* @param velocityY Y軸方向運動速度 像素/s
* @return
*/
@Override
public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
return super.onFling(e1, e2, velocityX, velocityY);
}
// 處理點擊效果 --延時 100ms 觸發(fā)
@Override
public void onShowPress(MotionEvent e) {
super.onShowPress(e);
}
// 只需要關(guān)注 onDown 的返回值,默認返回 false
@Override
public boolean onDown(MotionEvent e) {
return true;
}
// 雙擊的第二次點擊 down 時觸發(fā) 雙擊 40ms -- 300ms 之間
@Override
public boolean onDoubleTap(MotionEvent e) {
// // TODO 第一版,這種直接放大/縮小有點深硬,不平滑
// isEnlarge = !isEnlarge;
// if (isEnlarge) {
// currentScale = bigScale; // 雙擊放大
// }else {
// currentScale = smallScale; // 再雙擊時放小
// }
// invalidate(); // 刷新
//TODO 第二版,借助屬性動畫實現(xiàn)
isEnlarge = !isEnlarge;
if (isEnlarge) {
// TODO 雙擊時計算偏移,雙擊那個位置,就放大那個位置 / (e.getX(), e.getY()) 當前點擊的位置
offsetX = (e.getX() - getWidth() / 2f)
- (e.getX() - getWidth() / 2f) * bigScale / smallScale;
offsetY = (e.getY() - getHeight() / 2f)
- (e.getY() - getHeight() / 2f) * bigScale / smallScale;
fitOffsets(); // 解決點擊圖片外時放大空白部分
getScaleAnimator().start();
}else {
getScaleAnimator().reverse();
}
return super.onDoubleTap(e);
}
// 雙擊的第二次down, move, up 都觸發(fā)
@Override
public boolean onDoubleTapEvent(MotionEvent e) {
return super.onDoubleTapEvent(e);
}
// 單擊按下時觸發(fā),雙擊時不觸發(fā)/ down, up時都可能觸發(fā)(不會同時觸發(fā))
// 延時300ms觸發(fā)TAP事件
// 300ms 以內(nèi)抬手 -- 才會觸發(fā)TAP -- onSingleTapConfirmed
// 300ms 以后抬手 -- 不是雙擊或長按,則觸發(fā)
@Override
public boolean onSingleTapConfirmed(MotionEvent e) {
return super.onSingleTapConfirmed(e);
}
}onDoubleTap() 里的第一版代碼里 currentScale 直接由 smallScale 變到 bigscale,一下子就放大了,就很生硬不平滑。為了實現(xiàn)平滑的效果,我們使用 屬性動畫(ObjectAnimator),使得currentScale 由 smallScale 逐步變化到 bigScale,即 currentScale
(smallScale, bigScale)
private ObjectAnimator getScaleAnimator(){
if (scaleAnimator == null) {
scaleAnimator = ObjectAnimator.ofFloat(this, "currentScale", 0);
}
// TODO 平滑的范圍,從 smallScale --> bigScale
scaleAnimator.setFloatValues(smallScale, bigScale);
return scaleAnimator;
}
public float getCurrentScale() {
return currentScale;
}
public void setCurrentScale(float currentScale) {
this.currentScale = currentScale;
// 每一次在 smallScale -- bigScale 直接變化時都刷新
invalidate();
}注意:上面代碼里的 offsetX / offsetY 兩個變量這里沒講,是因為它們是滑動里用到的變量,所以我們放到下一小節(jié)里講,這里用它們是為了實現(xiàn)雙擊那個位置,就放大那個位置。如果把下面兩句代碼注釋掉,會發(fā)現(xiàn)雙擊的時候永遠是從中間位置放大。實現(xiàn)原理就是 offsetX / offsetY 是兩個偏移量,我們從中間放大后再移到 offsetX / offsetY 的位置,就實現(xiàn)了點擊哪里就放大哪里。
offsetX = (e.getX() - getWidth() / 2f)
- (e.getX() - getWidth() / 2f) * bigScale / smallScale;
offsetY = (e.getY() - getHeight() / 2f)
- (e.getY() - getHeight() / 2f) * bigScale / smallScale;
fitOffsets(); // 解決點擊圖片外時放大空白部分完成上面的代碼,當我們運行程序然后雙擊屏幕時發(fā)現(xiàn)圖片并沒有放大,為什么?因為我們雙擊的時候觸發(fā)的是 photoView 的 onTouchEvent(),而雙擊時需要觸發(fā) GestureDetector 的 onToucEvent()才能實現(xiàn)效果,所以我們再 photoView 里重寫 onTouchEvent ,并用 GestureDetector 的 onTouchEvent() 來強制接管。
/** TODO 我們點擊圖片時,觸發(fā)的是 PhotoView 里的 onTouchEvent,
* TODO 并沒有觸發(fā) GestureDetector 里的onTouchEvent, 所以才需要強制接管
*/
@Override
public boolean onTouchEvent(MotionEvent event) {
return gestureDetector.onTouchEvent(event);
//return super.onTouchEvent(event);
}2、滑動和慣性滑動

當我們雙擊放大圖片后,可以通過手指滑動查看屏幕外面的內(nèi)容,或者用力往某個方向滑動,實現(xiàn)慣性滑動的效果。
- 手指滑動
在上面一節(jié)提到的 SimpleOnGestureListener 接口,里面的 onScroll 函數(shù)實現(xiàn)滑動。offsetX offsetY 是滑動的偏移量,即滑動到了圖片的那個位置,在繪制的時候才能把滑動到的位置的圖片繪制出來。
/**
* 滾動 --move
* @param e1 手指按下
* @param e2 當前動作
* @param distanceX 就位置 - 新位置
* @param distanceY
* @return
*/
@Override
public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
// 圖片放大時,才可以滑動,即改變 offsetX offsetY
if (isEnlarge) {
offsetX -= distanceX;
offsetY -= distanceY;
fitOffsets();
invalidate();
}
return super.onScroll(e1, e2, distanceX, distanceY);
}if 里的判斷條件是確保在圖片放大的情況下才進行滑動。fitOffsets() 是一個功能函數(shù),計算圖片滑動到邊界的情況,放大后圖片的邊界滑動到屏幕邊界時就滑不動了。
/**
* 計算圖片滑動的邊界情況
* TODO 當往某個方向滑動圖片時,放大后的圖片邊界與手機屏幕邊界重合時,就不能滑動了
*/
private void fitOffsets(){
offsetX = Math.min(offsetX, (bitmap.getWidth() * bigScale - getWidth()) / 2);
offsetX = Math.max(offsetX, -(bitmap.getWidth() * bigScale - getWidth()) / 2);
offsetY = Math.min(offsetY, (bitmap.getHeight() * bigScale - getHeight()) / 2);
offsetY = Math.max(offsetY, -(bitmap.getHeight() * bigScale - getHeight()) / 2);
}對 offsetX 取值用 Math.min()和 Math.max() 的情況可以如下圖理解。offsetY 同理。

設(shè)置好了 onScroll() 函數(shù)后,我們還要將滑動的圖片繪制出來,所以我們還要在 onDraw 函數(shù)里調(diào)用 Canvas.translate(), 將滑動的偏移 offsetX / offsetY 設(shè)置進去。
// TODO 圖片滑動查看隱藏部分 canvas.translate(offsetX, offsetY);
慣性滑動
SimpleOnGestureListener 接口里的 onFling 函數(shù)實現(xiàn)慣性滑動。通過 OverScroll.fling() 來實現(xiàn),filing 函數(shù)的最后兩個參數(shù)表示當滑動到邊界時,如果還有速度,則會將邊界外的空白部分拉出200像素,然后立馬回彈回去的那種效果??梢試L試將這兩個參數(shù)去掉對比兩種情況的效果。
/**
* 慣性滑動
* @param velocityX X軸方向運動速度 像素/s
* @param velocityY Y軸方向運動速度 像素/s
* @return
*/
@Override
public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
if (isEnlarge) {
overScroller.fling((int) offsetX, (int) offsetY, (int) velocityX, (int) velocityY,
-(int) (bitmap.getWidth() * bigScale - getWidth()) / 2,
(int) (bitmap.getWidth() * bigScale - getWidth()) /2,
-(int) (bitmap.getHeight() * bigScale - getHeight()) / 2,
(int) (bitmap.getHeight() * bigScale - getHeight()) /2,
200, 200);
// TODO 我們要不斷的刷新界面,不斷的改變 offsetX, offsetY, 參數(shù):Runnable接口
// postOnAnimation 下一幀動畫的時候執(zhí)行
postOnAnimation(new flingRunner());
}
return super.onFling(e1, e2, velocityX, velocityY);
}我們在慣性滑動時要不斷的刷新界面,不斷改變 offsetX / offsetY 。我們使用 postOnAnimation(),里面?zhèn)魅胍粋€ filingRunner 接口,繼承自Runnable 。然后在filingRunner 里再調(diào)用postOnAnimation() 實現(xiàn)循環(huán)的效果。用 overScroller.computeScrollOffset() 函數(shù)計算當前的偏移并賦值給 offsetX/offsetY,實現(xiàn)不斷改變它的功能。當computeScrollOffset() 返回 false,則表明當前的慣性速度為0,慣性滑動就結(jié)束,則結(jié)束循環(huán)。
class flingRunner implements Runnable{
@Override
public void run() {
// TODO 用 overScroller 計算當前的偏移,并賦值給offsetX, offsetY
if (overScroller.computeScrollOffset()) {
// computeScrollOffset()會返回一個boolean值,為true, 說明動作還沒完成,以此來作為循環(huán)結(jié)束條件
offsetX = overScroller.getCurrX();
offsetY = overScroller.getCurrY();
invalidate();
//在上面的onFling 方法里面,postOnAnimation 只會調(diào)用一次,所以我們這里再調(diào)用,參數(shù):自己(flingRunner)
//TODO postOnAnimation 下一幀動畫的時候執(zhí)行
postOnAnimation(this);
}
}
}注意:寫到這里,就有了一個小 bug ,就是當我們滑動了圖片后再雙擊放小,會發(fā)現(xiàn)圖片不會顯示在正中間了,只需在 onDraw() 函數(shù)里做如下修改:我們在 offsetX / offsetY 上乘以一個平移因子,當雙擊縮小的時候,currentScale == smallScale ,則 scaleFaction == 0 --> offsetX / offsetY ==0 ,就相當于沒有平移了,所以雙擊縮小時就能顯示在原位置。
// 解決:當位置移動后,雙擊縮小,讓圖片顯示在最初的位置
// 雙擊縮小時,currentScale = smallScale, 所以 scaleFunction = 0, 所以 translate就相當于沒有平移
float scaleFaction = (currentScale - smallScale) / (bigScale - smallScale);
// TODO 圖片滑動查看隱藏部分
canvas.translate(offsetX * scaleFaction, offsetY * scaleFaction);3、雙指放大和縮小

Android 為我們提供了一個 ScaleGestureDetector 類來實現(xiàn)雙指縮放功能。在 init() 函數(shù)里初始化。
scaleGestureDetector = new ScaleGestureDetector(context, new photoScaleGestureListener());
photoScaleGestureListener() 實現(xiàn)了ScaleGestureDetector.onScaleGestureListener 接口,實現(xiàn)里面的三個方法。
- onScale:處理正在縮放
- onScaleBegin: 開始縮放
- onScaleEnd: 結(jié)束縮放
/**
* TODO 雙指縮放大的監(jiān)聽
*/
class photoScaleGestureListener implements ScaleGestureDetector.OnScaleGestureListener{
float initScale;
// 處理正在縮放
@Override
public boolean onScale(ScaleGestureDetector detector) {
if (currentScale >= smallScale && !isEnlarge) {
isEnlarge = !isEnlarge;
}
// 縮放因子 縮放后 / 縮放前
// eg 放大后=10,放大前=5, 縮放因子 == 10 / 5 == 2
currentScale = initScale * detector.getScaleFactor();
invalidate();
return false;
}
// 開始縮放
@Override
public boolean onScaleBegin(ScaleGestureDetector detector) {
initScale = currentScale;
return true;
}
//結(jié)束縮放
@Override
public void onScaleEnd(ScaleGestureDetector detector) {
}
}同理,ScaleGestureDetector 的觸發(fā)也需要在 photoView 里的 onTouchEvent 里強制接管,所以修改 onTouchEvnet() 里的代碼如下:
/** TODO 我們點擊圖片時,觸發(fā)的是 PhotoView 里的 onTouchEvent,
* TODO 并沒有觸發(fā) GestureDetector 里的onTouchEvent, 所以才需要強制接管
* TODO 同理,ScaleGestureDetector 也需要接管
*/
@Override
public boolean onTouchEvent(MotionEvent event) {
// TODO 響應(yīng)事件以雙指縮放優(yōu)先
boolean result = scaleGestureDetector.onTouchEvent(event);
if(!scaleGestureDetector.isInProgress()){
// TODO 不是雙指縮放,則用 GestureDetector 的 onTouchEvent 強制接管
result = gestureDetector.onTouchEvent(event);
}
return result;
//return super.onTouchEvent(event);
}4、完整DEMO
完整的 photoView 代碼(MainActivity里沒寫什么)
package com.example.photoview;
import android.animation.ObjectAnimator;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.util.AttributeSet;
import android.view.GestureDetector;
import android.view.MotionEvent;
import android.view.ScaleGestureDetector;
import android.view.View;
import android.widget.OverScroller;
import androidx.annotation.Nullable;
public class PhotoView extends View {
private static final float IMAGE_WIDTH = Utils.dpToPixel(300);
private Bitmap bitmap;
private Paint paint;
float originalOffsetX;
float originalOffsetY;
private float smallScale;
private float bigScale;
private float currentScale; //當前縮放值
private float OVER_SCALE_FACTOR = 1.5f;
private boolean isEnlarge = false; //雙擊時放大/縮小的標志位
private ObjectAnimator scaleAnimator; // 雙擊放大/縮小時,通過屬性動畫做出平滑的效果
private GestureDetector gestureDetector; // android 提高的手勢探測器,TODO 判斷是單價還是雙擊
private ScaleGestureDetector scaleGestureDetector; // TODO 實現(xiàn)雙指縮放
private float offsetX; // 圖片放大后,手指滑動圖片查看隱藏部分
private float offsetY;
private OverScroller overScroller; // TODO 實現(xiàn)慣性滑動
public PhotoView(Context context) {
this(context, null);
}
public PhotoView(Context context, @Nullable AttributeSet attrs) {
this(context, attrs, 0);
}
public PhotoView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init(context);
}
private void init(Context context){
bitmap = Utils.getPhoto(getResources(), (int) IMAGE_WIDTH);
paint = new Paint(Paint.ANTI_ALIAS_FLAG);
gestureDetector = new GestureDetector(context, new photoGestureListener());
scaleGestureDetector = new ScaleGestureDetector(context, new photoScaleGestureListener());
// 設(shè)置長按響應(yīng),false--關(guān)閉
//gestureDetector.setIsLongpressEnabled(false);
overScroller = new OverScroller(context);
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
// 解決:當位置移動后,雙擊縮小,讓圖片顯示在最初的位置
// 雙擊縮小時,currentScale = smallScale, 所以 scaleFunction = 0, 所以 translate就相當于沒有平移
float scaleFaction = (currentScale - smallScale) / (bigScale - smallScale);
// TODO 圖片滑動查看隱藏部分
canvas.translate(offsetX * scaleFaction, offsetY * scaleFaction);
// TODO 圖片放大,
// 第1,2個參數(shù)是放大比例,第3,4個參數(shù)是縮放的起始點,默認是(0,0)
canvas.scale(currentScale, currentScale, getWidth() / 2f, getHeight() / 2f);
// drawBitmap(); 第2,3個參數(shù)是畫bitmap的起始坐標點
canvas.drawBitmap(bitmap, originalOffsetX, originalOffsetY, paint);
}
/**
* TODO 在onDraw之前調(diào)用,且尺寸改變時也要調(diào)用
*/
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
originalOffsetX = (getWidth() - bitmap.getWidth()) / 2f;
originalOffsetY = (getHeight() - bitmap.getHeight()) / 2f;
// TODO 判斷 bitmap 是扁的還是長的
if ((float)bitmap.getWidth() / bitmap.getHeight() > (float) getWidth() / getHeight()) {
// bitmap 的 width > height
smallScale = (float) getWidth() / bitmap.getWidth();
bigScale = (float) getHeight() / bitmap.getHeight() * OVER_SCALE_FACTOR;
}else {
// bitmap 的 height > width
smallScale = (float) getHeight() / bitmap.getHeight();
bigScale = (float) getWidth() / bitmap.getWidth() * OVER_SCALE_FACTOR;
}
currentScale = smallScale;
}
/** TODO 我們點擊圖片時,觸發(fā)的是 PhotoView 里的 onTouchEvent,
* TODO 并沒有觸發(fā) GestureDetector 里的onTouchEvent, 所以才需要強制接管
* TODO 同理,ScaleGestureDetector 也需要接管
*/
@Override
public boolean onTouchEvent(MotionEvent event) {
// TODO 響應(yīng)事件以雙指縮放優(yōu)先
boolean result = scaleGestureDetector.onTouchEvent(event);
if(!scaleGestureDetector.isInProgress()){
// TODO 不是雙指縮放,則用 GestureDetector 的 onTouchEvent 強制接管
result = gestureDetector.onTouchEvent(event);
}
return result;
//return super.onTouchEvent(event);
}
/**
* TODO 單擊/雙擊/慣性滑動的監(jiān)聽
*/
class photoGestureListener extends GestureDetector.SimpleOnGestureListener{
// up 時觸發(fā),單擊或者雙擊的第一次會觸發(fā) --- up時,如果不是雙擊的得二次點擊,不是長按,則觸發(fā)
@Override
public boolean onSingleTapUp(MotionEvent e) {
return super.onSingleTapUp(e);
}
// 長按 默認300ms后觸發(fā)
@Override
public void onLongPress(MotionEvent e) {
super.onLongPress(e);
}
/**
* 滾動 --move
* @param e1 手指按下
* @param e2 當前動作
* @param distanceX 就位置 - 新位置
* @param distanceY
* @return
*/
@Override
public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
// 圖片放大時,才可以滑動,即改變 offsetX offsetY
if (isEnlarge) {
offsetX -= distanceX;
offsetY -= distanceY;
fitOffsets();
invalidate();
}
return super.onScroll(e1, e2, distanceX, distanceY);
}
/**
* 慣性滑動
* @param velocityX X軸方向運動速度 像素/s
* @param velocityY Y軸方向運動速度 像素/s
* @return
*/
@Override
public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
if (isEnlarge) {
overScroller.fling((int) offsetX, (int) offsetY, (int) velocityX, (int) velocityY,
-(int) (bitmap.getWidth() * bigScale - getWidth()) / 2,
(int) (bitmap.getWidth() * bigScale - getWidth()) /2,
-(int) (bitmap.getHeight() * bigScale - getHeight()) / 2,
(int) (bitmap.getHeight() * bigScale - getHeight()) /2,
200, 200);
// TODO 我們要不斷的刷新界面,不斷的改變 offsetX, offsetY, 參數(shù):Runnable接口
// postOnAnimation 下一幀動畫的時候執(zhí)行
postOnAnimation(new flingRunner());
}
return super.onFling(e1, e2, velocityX, velocityY);
}
// 處理點擊效果 --延時 100ms 觸發(fā)
@Override
public void onShowPress(MotionEvent e) {
super.onShowPress(e);
}
// 只需要關(guān)注 onDown 的返回值,默認返回 false
@Override
public boolean onDown(MotionEvent e) {
return true;
}
// 雙擊的第二次點擊 down 時觸發(fā) 雙擊 40ms -- 300ms 之間
@Override
public boolean onDoubleTap(MotionEvent e) {
// // TODO 第一版,這種直接放大/縮小有點深硬,不平滑
// isEnlarge = !isEnlarge;
// if (isEnlarge) {
// currentScale = bigScale; // 雙擊放大
// }else {
// currentScale = smallScale; // 再雙擊時放小
// }
// invalidate(); // 刷新
//TODO 第二版,借助屬性動畫實現(xiàn)
isEnlarge = !isEnlarge;
if (isEnlarge) {
// TODO 雙擊時計算偏移,雙擊那個位置,就放大那個位置 / (e.getX(), e.getY()) 當前點擊的位置
offsetX = (e.getX() - getWidth() / 2f)
- (e.getX() - getWidth() / 2f) * bigScale / smallScale;
offsetY = (e.getY() - getHeight() / 2f)
- (e.getY() - getHeight() / 2f) * bigScale / smallScale;
fitOffsets(); // 解決點擊圖片外時放大空白部分
getScaleAnimator().start();
}else {
getScaleAnimator().reverse();
}
return super.onDoubleTap(e);
}
// 雙擊的第二次down, move, up 都觸發(fā)
@Override
public boolean onDoubleTapEvent(MotionEvent e) {
return super.onDoubleTapEvent(e);
}
// 單擊按下時觸發(fā),雙擊時不觸發(fā)/ down, up時都可能觸發(fā)(不會同時觸發(fā))
// 延時300ms觸發(fā)TAP事件
// 300ms 以內(nèi)抬手 -- 才會觸發(fā)TAP -- onSingleTapConfirmed
// 300ms 以后抬手 -- 不是雙擊或長按,則觸發(fā)
@Override
public boolean onSingleTapConfirmed(MotionEvent e) {
return super.onSingleTapConfirmed(e);
}
}
/**
* TODO 雙指縮放大的監(jiān)聽
*/
class photoScaleGestureListener implements ScaleGestureDetector.OnScaleGestureListener{
float initScale;
// 處理正在縮放
@Override
public boolean onScale(ScaleGestureDetector detector) {
if (currentScale >= smallScale && !isEnlarge) {
isEnlarge = !isEnlarge;
}
// 縮放因子 縮放后 / 縮放前
// eg 放大后=10,放大前=5, 縮放因子 == 10 / 5 == 2
currentScale = initScale * detector.getScaleFactor();
invalidate();
return false;
}
// 開始縮放
@Override
public boolean onScaleBegin(ScaleGestureDetector detector) {
initScale = currentScale;
return true;
}
//結(jié)束縮放
@Override
public void onScaleEnd(ScaleGestureDetector detector) {
}
}
class flingRunner implements Runnable{
@Override
public void run() {
// TODO 用 overScroller 計算當前的偏移,并賦值給offsetX, offsetY
if (overScroller.computeScrollOffset()) {
// computeScrollOffset()會返回一個boolean值,為true, 說明動作還沒完成,以此來作為循環(huán)結(jié)束條件
offsetX = overScroller.getCurrX();
offsetY = overScroller.getCurrY();
invalidate();
//在上面的onFling 方法里面,postOnAnimation 只會調(diào)用一次,所以我們這里再調(diào)用,參數(shù):自己(flingRunner)
//TODO postOnAnimation 下一幀動畫的時候執(zhí)行
postOnAnimation(this);
}
}
}
/**
* 計算圖片滑動的邊界情況
* TODO 當往某個方向滑動圖片時,放大后的圖片邊界與手機屏幕邊界重合時,就不能滑動了
*/
private void fitOffsets(){
offsetX = Math.min(offsetX, (bitmap.getWidth() * bigScale - getWidth()) / 2);
offsetX = Math.max(offsetX, -(bitmap.getWidth() * bigScale - getWidth()) / 2);
offsetY = Math.min(offsetY, (bitmap.getHeight() * bigScale - getHeight()) / 2);
offsetY = Math.max(offsetY, -(bitmap.getHeight() * bigScale - getHeight()) / 2);
}
private ObjectAnimator getScaleAnimator(){
if (scaleAnimator == null) {
scaleAnimator = ObjectAnimator.ofFloat(this, "currentScale", 0);
}
// TODO 平滑的范圍,從 smallScale --> bigScale
scaleAnimator.setFloatValues(smallScale, bigScale);
return scaleAnimator;
}
public float getCurrentScale() {
return currentScale;
}
public void setCurrentScale(float currentScale) {
this.currentScale = currentScale;
// 每一次在 smallScale -- bigScale 直接變化時都刷新
invalidate();
}
}到此這篇關(guān)于Android自定義PhotoView使用教程的文章就介紹到這了,更多相關(guān)Android PhotoView內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Android實現(xiàn)系統(tǒng)重新啟動的功能
有些Android版本沒有系統(tǒng)重啟的功能,非常不方便。需要我們自己開發(fā)一個能夠重新啟動的應(yīng)用2013-11-11
在Android中使用Anntation來代替ENUM的方法
本篇文章主要介紹了在Android中使用Anntation來代替ENUM的方法,小編覺得挺不錯的,現(xiàn)在分享給大家,也給大家做個參考。一起跟隨小編過來看看吧2018-02-02
Android入門之使用RecyclerView完美實現(xiàn)瀑布流界面詳解
網(wǎng)上充滿著不完善的基于RecyclerView的瀑布流實現(xiàn),要么根本是錯的、要么就是只知其一不知其二。本文就來用RecyclerView完美實現(xiàn)瀑布流界面,希望大家有所幫助2023-02-02
Android使用GPS獲取用戶地理位置并監(jiān)聽位置變化的方法
這篇文章主要介紹了Android使用GPS獲取用戶地理位置并監(jiān)聽位置變化的方法,實例分析了Android編程中GPS定位的實現(xiàn)與使用技巧,需要的朋友可以參考下2015-12-12
Android編程實現(xiàn)Gallery中每次滑動只顯示一頁的方法
這篇文章主要介紹了Android編程實現(xiàn)Gallery中每次滑動只顯示一頁的方法,涉及Android擴展Gallery控件實現(xiàn)翻頁效果控制的功能,涉及Android事件響應(yīng)及屬性控制的相關(guān)技巧,需要的朋友可以參考下2015-11-11

