亚洲乱码中文字幕综合,中国熟女仑乱hd,亚洲精品乱拍国产一区二区三区,一本大道卡一卡二卡三乱码全集资源,又粗又黄又硬又爽的免费视频

Android自定義PhotoView使用教程

 更新時(shí)間:2023年04月06日 09:37:35   作者:別偷我的豬_09  
自定義 PhotoView 繼承(extends)自 View。并在最中間顯示后面操作的圖片。繪制圖片可以重寫(xiě) onDraw()方法,并在里面通過(guò)Canvas.drawBitmap()來(lái)要繪制圖片

準(zhǔn)備工作

自定義PhotoView

自定義 PhotoView 繼承(extends)自 View。并在最中間顯示后面操作的圖片。繪制圖片可以重寫(xiě) onDraw()方法,并在里面通過(guò)Canvas.drawBitmap()來(lái)要繪制圖片。

drawBitmap()的四個(gè)參數(shù):

  • bitmap: 要在 Canvas 中繪制的位圖
  • letf:正在繪制的位圖左側(cè)的位置
  • top:正在繪制的位圖頂部的位置
  • paint: 畫(huà)筆

其中 (left, top) 是要繪制圖片的起始坐標(biāo)。要將圖片繪制在中間,我們就需要計(jì)算 left/top 的位置。我們重寫(xiě) onSizeChanged() 函數(shù),該函數(shù)在onDraw之前調(diào)用,且尺寸改變時(shí)也要調(diào)用。

其中:(下面代碼中是用 originalOffsetX/originalOffsetY 來(lái)代替的)

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; // 畫(huà)筆
    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)用,且尺寸改變時(shí)也要調(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;
    }
    /**
     * 畫(huà)出圖片
     * @param canvas 畫(huà)布
     */
    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        canvas.drawBitmap(bitmap, originalOffsetX, originalOffsetY, paint);
    }
}
  • xml 布局

xml 布局中最外層是 FragmeLayout,里面只有一個(gè)自定義的 PhotoView 用來(lái)展示圖片。

<?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 工具類(lèi)

Utils 工具類(lèi)里主要有兩個(gè)函數(shù)。dpToPixel() 將 dp 轉(zhuǎn)換為像素;getPhot() 加載 Drawable 下的圖片,并返回為 bitmap 類(lèi)型。

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),即圖片左右兩邊貼進(jìn)屏幕;右邊是大放大(bigScale),即圖片沾滿整個(gè)屏幕。

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

/**
     * TODO 在onDraw之前調(diào)用,且尺寸改變時(shí)也要調(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 是扁的還是長(zhǎng)的
        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 里的判斷條件,判斷圖片是扁平還是長(zhǎng)的。如下圖理解,當(dāng)然我們這里用的圖是扁平的。currentScale 是當(dāng)前的縮放比例,smallScale <= currentScale <= bigScale 。

最后設(shè)置了 smallScale 和 bigScale 后,我們還要在 onDraw 里將 smallScale 放大的圖片繪制出來(lái)。這里用 currentScale ,因?yàn)樵?onSizeChanged 函數(shù)里,我們將 smallScale 賦值給了 currentScale 。使用 Canvas.scale 函數(shù)進(jìn)行縮放。

// TODO 圖片放大,
// 第1,2個(gè)參數(shù)是放大比例,第3,4個(gè)參數(shù)是縮放的起始點(diǎn),默認(rèn)是(0,0)
canvas.scale(currentScale, currentScale, getWidth() / 2f, getHeight() / 2f);
  • 雙擊擊縮放

Android 為我們提供了一個(gè) GestureDetector 類(lèi)來(lái)實(shí)現(xiàn)雙擊、單擊、滑動(dòng)、慣性滑動(dòng)等。在 init 函數(shù)里添加如下代碼,初始化 GestureDetector。gestureDectector 是一個(gè)全局變量。

gestureDetector = new GestureDetector(context, new photoGestureListener());

GestureDetector 的第二個(gè)參數(shù)是一個(gè) Listener ,所以我們寫(xiě)了個(gè)內(nèi)部類(lèi) photoGestureListener 繼承GestureDetector.SimpleOnGestureListener。SimpleOnGestureListener 是一個(gè) interface, 所以我們重寫(xiě)里面的方法,其中onDoubleTap() 就是實(shí)現(xiàn)寫(xiě)雙擊縮放的。

注意:onDown() 方法要返回 true 才能響應(yīng)到雙擊事件

/**
     * TODO 單擊/雙擊/慣性滑動(dòng)的監(jiān)聽(tīng)
     */
    class photoGestureListener extends GestureDetector.SimpleOnGestureListener{
        // up 時(shí)觸發(fā),單擊或者雙擊的第一次會(huì)觸發(fā) --- up時(shí),如果不是雙擊的得二次點(diǎn)擊,不是長(zhǎng)按,則觸發(fā)
        @Override
        public boolean onSingleTapUp(MotionEvent e) {
            return super.onSingleTapUp(e);
        }
        // 長(zhǎng)按 默認(rèn)300ms后觸發(fā)
        @Override
        public void onLongPress(MotionEvent e) {
            super.onLongPress(e);
        }
        /**
         * 滾動(dòng) --move
         * @param e1 手指按下
         * @param e2 當(dāng)前動(dòng)作
         * @param distanceX 就位置 - 新位置
         * @param distanceY
         * @return
         */
        @Override
        public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
            return super.onScroll(e1, e2, distanceX, distanceY);
        }
        /**
         * 慣性滑動(dòng)
         * @param velocityX X軸方向運(yùn)動(dòng)速度 像素/s
         * @param velocityY Y軸方向運(yùn)動(dòng)速度 像素/s
         * @return
         */
        @Override
        public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
            return super.onFling(e1, e2, velocityX, velocityY);
        }
        // 處理點(diǎn)擊效果 --延時(shí) 100ms 觸發(fā)
        @Override
        public void onShowPress(MotionEvent e) {
            super.onShowPress(e);
        }
        // 只需要關(guān)注 onDown 的返回值,默認(rèn)返回 false
        @Override
        public boolean onDown(MotionEvent e) {
            return true;
        }
        // 雙擊的第二次點(diǎn)擊 down 時(shí)觸發(fā) 雙擊 40ms -- 300ms 之間
        @Override
        public boolean onDoubleTap(MotionEvent e) {
//            // TODO 第一版,這種直接放大/縮小有點(diǎn)深硬,不平滑
//            isEnlarge = !isEnlarge;
//            if (isEnlarge) {
//                currentScale = bigScale; // 雙擊放大
//            }else {
//                currentScale = smallScale; // 再雙擊時(shí)放小
//            }
//            invalidate(); // 刷新
            //TODO 第二版,借助屬性動(dòng)畫(huà)實(shí)現(xiàn)
            isEnlarge = !isEnlarge;
            if (isEnlarge) {
                // TODO 雙擊時(shí)計(jì)算偏移,雙擊那個(gè)位置,就放大那個(gè)位置 / (e.getX(), e.getY()) 當(dāng)前點(diǎn)擊的位置
                offsetX = (e.getX() - getWidth() / 2f)
                        - (e.getX() - getWidth() / 2f) * bigScale / smallScale;
                offsetY = (e.getY() - getHeight() / 2f)
                        - (e.getY() - getHeight() / 2f) * bigScale / smallScale;
                fitOffsets(); // 解決點(diǎn)擊圖片外時(shí)放大空白部分
                getScaleAnimator().start();
            }else {
                getScaleAnimator().reverse();
            }
            return super.onDoubleTap(e);
        }
        // 雙擊的第二次down, move, up 都觸發(fā)
        @Override
        public boolean onDoubleTapEvent(MotionEvent e) {
            return super.onDoubleTapEvent(e);
        }
        // 單擊按下時(shí)觸發(fā),雙擊時(shí)不觸發(fā)/ down, up時(shí)都可能觸發(fā)(不會(huì)同時(shí)觸發(fā))
        // 延時(shí)300ms觸發(fā)TAP事件
        // 300ms 以內(nèi)抬手  -- 才會(huì)觸發(fā)TAP -- onSingleTapConfirmed
        // 300ms 以后抬手 -- 不是雙擊或長(zhǎng)按,則觸發(fā)
        @Override
        public boolean onSingleTapConfirmed(MotionEvent e) {
            return super.onSingleTapConfirmed(e);
        }
    }

onDoubleTap() 里的第一版代碼里 currentScale 直接由 smallScale 變到 bigscale,一下子就放大了,就很生硬不平滑。為了實(shí)現(xiàn)平滑的效果,我們使用 屬性動(dòng)畫(huà)(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 直接變化時(shí)都刷新
        invalidate();
    }

注意:上面代碼里的 offsetX / offsetY 兩個(gè)變量這里沒(méi)講,是因?yàn)樗鼈兪腔瑒?dòng)里用到的變量,所以我們放到下一小節(jié)里講,這里用它們是為了實(shí)現(xiàn)雙擊那個(gè)位置,就放大那個(gè)位置。如果把下面兩句代碼注釋掉,會(huì)發(fā)現(xiàn)雙擊的時(shí)候永遠(yuǎn)是從中間位置放大。實(shí)現(xiàn)原理就是 offsetX / offsetY 是兩個(gè)偏移量,我們從中間放大后再移到 offsetX / offsetY 的位置,就實(shí)現(xiàn)了點(diǎn)擊哪里就放大哪里。

offsetX = (e.getX() - getWidth() / 2f)
           - (e.getX() - getWidth() / 2f) * bigScale / smallScale;
 offsetY = (e.getY() - getHeight() / 2f)
           - (e.getY() - getHeight() / 2f) * bigScale / smallScale;
fitOffsets(); // 解決點(diǎn)擊圖片外時(shí)放大空白部分

完成上面的代碼,當(dāng)我們運(yùn)行程序然后雙擊屏幕時(shí)發(fā)現(xiàn)圖片并沒(méi)有放大,為什么?因?yàn)槲覀冸p擊的時(shí)候觸發(fā)的是 photoView 的 onTouchEvent(),而雙擊時(shí)需要觸發(fā) GestureDetector 的 onToucEvent()才能實(shí)現(xiàn)效果,所以我們?cè)?photoView 里重寫(xiě) onTouchEvent ,并用 GestureDetector 的 onTouchEvent() 來(lái)強(qiáng)制接管。

/** TODO 我們點(diǎn)擊圖片時(shí),觸發(fā)的是 PhotoView 里的 onTouchEvent,
     *  TODO 并沒(méi)有觸發(fā) GestureDetector 里的onTouchEvent, 所以才需要強(qiáng)制接管
     */
    @Override
    public boolean onTouchEvent(MotionEvent event) {
        return gestureDetector.onTouchEvent(event);
        //return super.onTouchEvent(event);
    }

2、滑動(dòng)和慣性滑動(dòng)

當(dāng)我們雙擊放大圖片后,可以通過(guò)手指滑動(dòng)查看屏幕外面的內(nèi)容,或者用力往某個(gè)方向滑動(dòng),實(shí)現(xiàn)慣性滑動(dòng)的效果。

  • 手指滑動(dòng)

在上面一節(jié)提到的 SimpleOnGestureListener 接口,里面的 onScroll 函數(shù)實(shí)現(xiàn)滑動(dòng)。offsetX offsetY 是滑動(dòng)的偏移量,即滑動(dòng)到了圖片的那個(gè)位置,在繪制的時(shí)候才能把滑動(dòng)到的位置的圖片繪制出來(lái)。

        /**
         * 滾動(dòng) --move
         * @param e1 手指按下
         * @param e2 當(dāng)前動(dòng)作
         * @param distanceX 就位置 - 新位置
         * @param distanceY
         * @return
         */
        @Override
        public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
            // 圖片放大時(shí),才可以滑動(dòng),即改變 offsetX offsetY
            if (isEnlarge) {
                offsetX -= distanceX;
                offsetY -= distanceY;
                fitOffsets();
                invalidate();
            }
            return super.onScroll(e1, e2, distanceX, distanceY);
        }

if 里的判斷條件是確保在圖片放大的情況下才進(jìn)行滑動(dòng)。fitOffsets() 是一個(gè)功能函數(shù),計(jì)算圖片滑動(dòng)到邊界的情況,放大后圖片的邊界滑動(dòng)到屏幕邊界時(shí)就滑不動(dòng)了。

    /**
     * 計(jì)算圖片滑動(dòng)的邊界情況
     * TODO 當(dāng)往某個(gè)方向滑動(dòng)圖片時(shí),放大后的圖片邊界與手機(jī)屏幕邊界重合時(shí),就不能滑動(dòng)了
     */
    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);
    }

對(duì) offsetX 取值用 Math.min()和 Math.max() 的情況可以如下圖理解。offsetY 同理。

設(shè)置好了 onScroll() 函數(shù)后,我們還要將滑動(dòng)的圖片繪制出來(lái),所以我們還要在 onDraw 函數(shù)里調(diào)用 Canvas.translate(), 將滑動(dòng)的偏移 offsetX / offsetY 設(shè)置進(jìn)去。

// TODO 圖片滑動(dòng)查看隱藏部分
canvas.translate(offsetX, offsetY);

慣性滑動(dòng)

SimpleOnGestureListener 接口里的 onFling 函數(shù)實(shí)現(xiàn)慣性滑動(dòng)。通過(guò) OverScroll.fling() 來(lái)實(shí)現(xiàn),filing 函數(shù)的最后兩個(gè)參數(shù)表示當(dāng)滑動(dòng)到邊界時(shí),如果還有速度,則會(huì)將邊界外的空白部分拉出200像素,然后立馬回彈回去的那種效果??梢試L試將這兩個(gè)參數(shù)去掉對(duì)比兩種情況的效果。

        /**
         * 慣性滑動(dòng)
         * @param velocityX X軸方向運(yùn)動(dòng)速度 像素/s
         * @param velocityY Y軸方向運(yùn)動(dòng)速度 像素/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 下一幀動(dòng)畫(huà)的時(shí)候執(zhí)行
                postOnAnimation(new flingRunner());
            }
            return super.onFling(e1, e2, velocityX, velocityY);
        }

我們?cè)趹T性滑動(dòng)時(shí)要不斷的刷新界面,不斷改變 offsetX / offsetY 。我們使用 postOnAnimation(),里面?zhèn)魅胍粋€(gè) filingRunner 接口,繼承自Runnable 。然后在filingRunner 里再調(diào)用postOnAnimation() 實(shí)現(xiàn)循環(huán)的效果。用 overScroller.computeScrollOffset() 函數(shù)計(jì)算當(dāng)前的偏移并賦值給 offsetX/offsetY,實(shí)現(xiàn)不斷改變它的功能。當(dāng)computeScrollOffset() 返回 false,則表明當(dāng)前的慣性速度為0,慣性滑動(dòng)就結(jié)束,則結(jié)束循環(huán)。

class flingRunner implements Runnable{
        @Override
        public void run() {
            // TODO 用 overScroller 計(jì)算當(dāng)前的偏移,并賦值給offsetX, offsetY
            if (overScroller.computeScrollOffset()) {
                // computeScrollOffset()會(huì)返回一個(gè)boolean值,為true, 說(shuō)明動(dòng)作還沒(méi)完成,以此來(lái)作為循環(huán)結(jié)束條件
                offsetX = overScroller.getCurrX();
                offsetY = overScroller.getCurrY();
                invalidate();
                //在上面的onFling 方法里面,postOnAnimation 只會(huì)調(diào)用一次,所以我們這里再調(diào)用,參數(shù):自己(flingRunner)
                //TODO postOnAnimation 下一幀動(dòng)畫(huà)的時(shí)候執(zhí)行
                postOnAnimation(this);
            }
        }
    }

注意:寫(xiě)到這里,就有了一個(gè)小 bug ,就是當(dāng)我們滑動(dòng)了圖片后再雙擊放小,會(huì)發(fā)現(xiàn)圖片不會(huì)顯示在正中間了,只需在 onDraw() 函數(shù)里做如下修改:我們?cè)?offsetX / offsetY 上乘以一個(gè)平移因子,當(dāng)雙擊縮小的時(shí)候,currentScale == smallScale ,則 scaleFaction == 0 --> offsetX / offsetY ==0 ,就相當(dāng)于沒(méi)有平移了,所以雙擊縮小時(shí)就能顯示在原位置。

        // 解決:當(dāng)位置移動(dòng)后,雙擊縮小,讓圖片顯示在最初的位置
        // 雙擊縮小時(shí),currentScale = smallScale, 所以 scaleFunction = 0, 所以 translate就相當(dāng)于沒(méi)有平移
        float scaleFaction = (currentScale - smallScale) / (bigScale - smallScale);
        // TODO 圖片滑動(dòng)查看隱藏部分
        canvas.translate(offsetX * scaleFaction, offsetY * scaleFaction);

3、雙指放大和縮小

Android 為我們提供了一個(gè) ScaleGestureDetector 類(lèi)來(lái)實(shí)現(xiàn)雙指縮放功能。在 init() 函數(shù)里初始化。

scaleGestureDetector = new ScaleGestureDetector(context, new photoScaleGestureListener());

photoScaleGestureListener() 實(shí)現(xiàn)了ScaleGestureDetector.onScaleGestureListener 接口,實(shí)現(xiàn)里面的三個(gè)方法。

  • onScale:處理正在縮放
  • onScaleBegin: 開(kāi)始縮放
  • onScaleEnd: 結(jié)束縮放
/**
     * TODO 雙指縮放大的監(jiān)聽(tīng)
     */
    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;
        }
        // 開(kāi)始縮放
        @Override
        public boolean onScaleBegin(ScaleGestureDetector detector) {
            initScale = currentScale;
            return true;
        }
        //結(jié)束縮放
        @Override
        public void onScaleEnd(ScaleGestureDetector detector) {
 
        }
    }

同理,ScaleGestureDetector 的觸發(fā)也需要在 photoView 里的 onTouchEvent 里強(qiáng)制接管,所以修改 onTouchEvnet() 里的代碼如下:

    /** TODO 我們點(diǎn)擊圖片時(shí),觸發(fā)的是 PhotoView 里的 onTouchEvent,
     *  TODO 并沒(méi)有觸發(fā) GestureDetector 里的onTouchEvent, 所以才需要強(qiáng)制接管
     *  TODO 同理,ScaleGestureDetector 也需要接管
     */
    @Override
    public boolean onTouchEvent(MotionEvent event) {
        // TODO 響應(yīng)事件以雙指縮放優(yōu)先
        boolean result = scaleGestureDetector.onTouchEvent(event);
        if(!scaleGestureDetector.isInProgress()){
            // TODO 不是雙指縮放,則用 GestureDetector 的 onTouchEvent 強(qiáng)制接管
            result = gestureDetector.onTouchEvent(event);
        }
        return result;
        //return super.onTouchEvent(event);
    }

4、完整DEMO

完整的 photoView 代碼(MainActivity里沒(méi)寫(xiě)什么)

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; //當(dāng)前縮放值
    private float OVER_SCALE_FACTOR = 1.5f;
    private boolean isEnlarge = false; //雙擊時(shí)放大/縮小的標(biāo)志位
    private ObjectAnimator scaleAnimator; // 雙擊放大/縮小時(shí),通過(guò)屬性動(dòng)畫(huà)做出平滑的效果
    private GestureDetector gestureDetector; // android 提高的手勢(shì)探測(cè)器,TODO 判斷是單價(jià)還是雙擊
    private ScaleGestureDetector scaleGestureDetector; // TODO 實(shí)現(xiàn)雙指縮放
    private float offsetX; // 圖片放大后,手指滑動(dòng)圖片查看隱藏部分
    private float offsetY;
    private OverScroller overScroller; // TODO 實(shí)現(xiàn)慣性滑動(dòng)
    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è)置長(zhǎng)按響應(yīng),false--關(guān)閉
        //gestureDetector.setIsLongpressEnabled(false);
        overScroller = new OverScroller(context);
    }
    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        // 解決:當(dāng)位置移動(dòng)后,雙擊縮小,讓圖片顯示在最初的位置
        // 雙擊縮小時(shí),currentScale = smallScale, 所以 scaleFunction = 0, 所以 translate就相當(dāng)于沒(méi)有平移
        float scaleFaction = (currentScale - smallScale) / (bigScale - smallScale);
        // TODO 圖片滑動(dòng)查看隱藏部分
        canvas.translate(offsetX * scaleFaction, offsetY * scaleFaction);
        // TODO 圖片放大,
        // 第1,2個(gè)參數(shù)是放大比例,第3,4個(gè)參數(shù)是縮放的起始點(diǎn),默認(rèn)是(0,0)
        canvas.scale(currentScale, currentScale, getWidth() / 2f, getHeight() / 2f);
        // drawBitmap(); 第2,3個(gè)參數(shù)是畫(huà)bitmap的起始坐標(biāo)點(diǎn)
        canvas.drawBitmap(bitmap, originalOffsetX, originalOffsetY, paint);
    }
    /**
     * TODO 在onDraw之前調(diào)用,且尺寸改變時(shí)也要調(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 是扁的還是長(zhǎng)的
        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 我們點(diǎn)擊圖片時(shí),觸發(fā)的是 PhotoView 里的 onTouchEvent,
     *  TODO 并沒(méi)有觸發(fā) GestureDetector 里的onTouchEvent, 所以才需要強(qiáng)制接管
     *  TODO 同理,ScaleGestureDetector 也需要接管
     */
    @Override
    public boolean onTouchEvent(MotionEvent event) {
        // TODO 響應(yīng)事件以雙指縮放優(yōu)先
        boolean result = scaleGestureDetector.onTouchEvent(event);
        if(!scaleGestureDetector.isInProgress()){
            // TODO 不是雙指縮放,則用 GestureDetector 的 onTouchEvent 強(qiáng)制接管
            result = gestureDetector.onTouchEvent(event);
        }
        return result;
        //return super.onTouchEvent(event);
    }
    /**
     * TODO 單擊/雙擊/慣性滑動(dòng)的監(jiān)聽(tīng)
     */
    class photoGestureListener extends GestureDetector.SimpleOnGestureListener{
        // up 時(shí)觸發(fā),單擊或者雙擊的第一次會(huì)觸發(fā) --- up時(shí),如果不是雙擊的得二次點(diǎn)擊,不是長(zhǎng)按,則觸發(fā)
        @Override
        public boolean onSingleTapUp(MotionEvent e) {
            return super.onSingleTapUp(e);
        }
        // 長(zhǎng)按 默認(rèn)300ms后觸發(fā)
        @Override
        public void onLongPress(MotionEvent e) {
            super.onLongPress(e);
        }
        /**
         * 滾動(dòng) --move
         * @param e1 手指按下
         * @param e2 當(dāng)前動(dòng)作
         * @param distanceX 就位置 - 新位置
         * @param distanceY
         * @return
         */
        @Override
        public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
            // 圖片放大時(shí),才可以滑動(dòng),即改變 offsetX offsetY
            if (isEnlarge) {
                offsetX -= distanceX;
                offsetY -= distanceY;
                fitOffsets();
                invalidate();
            }
            return super.onScroll(e1, e2, distanceX, distanceY);
        }
        /**
         * 慣性滑動(dòng)
         * @param velocityX X軸方向運(yùn)動(dòng)速度 像素/s
         * @param velocityY Y軸方向運(yùn)動(dòng)速度 像素/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 下一幀動(dòng)畫(huà)的時(shí)候執(zhí)行
                postOnAnimation(new flingRunner());
            }
            return super.onFling(e1, e2, velocityX, velocityY);
        }
        // 處理點(diǎn)擊效果 --延時(shí) 100ms 觸發(fā)
        @Override
        public void onShowPress(MotionEvent e) {
            super.onShowPress(e);
        }
        // 只需要關(guān)注 onDown 的返回值,默認(rèn)返回 false
        @Override
        public boolean onDown(MotionEvent e) {
            return true;
        }
        // 雙擊的第二次點(diǎn)擊 down 時(shí)觸發(fā) 雙擊 40ms -- 300ms 之間
        @Override
        public boolean onDoubleTap(MotionEvent e) {
//            // TODO 第一版,這種直接放大/縮小有點(diǎn)深硬,不平滑
//            isEnlarge = !isEnlarge;
//            if (isEnlarge) {
//                currentScale = bigScale; // 雙擊放大
//            }else {
//                currentScale = smallScale; // 再雙擊時(shí)放小
//            }
//            invalidate(); // 刷新
            //TODO 第二版,借助屬性動(dòng)畫(huà)實(shí)現(xiàn)
            isEnlarge = !isEnlarge;
            if (isEnlarge) {
                // TODO 雙擊時(shí)計(jì)算偏移,雙擊那個(gè)位置,就放大那個(gè)位置 / (e.getX(), e.getY()) 當(dāng)前點(diǎn)擊的位置
                offsetX = (e.getX() - getWidth() / 2f)
                        - (e.getX() - getWidth() / 2f) * bigScale / smallScale;
                offsetY = (e.getY() - getHeight() / 2f)
                        - (e.getY() - getHeight() / 2f) * bigScale / smallScale;
                fitOffsets(); // 解決點(diǎn)擊圖片外時(shí)放大空白部分
                getScaleAnimator().start();
            }else {
                getScaleAnimator().reverse();
            }
            return super.onDoubleTap(e);
        }
        // 雙擊的第二次down, move, up 都觸發(fā)
        @Override
        public boolean onDoubleTapEvent(MotionEvent e) {
            return super.onDoubleTapEvent(e);
        }
        // 單擊按下時(shí)觸發(fā),雙擊時(shí)不觸發(fā)/ down, up時(shí)都可能觸發(fā)(不會(huì)同時(shí)觸發(fā))
        // 延時(shí)300ms觸發(fā)TAP事件
        // 300ms 以內(nèi)抬手  -- 才會(huì)觸發(fā)TAP -- onSingleTapConfirmed
        // 300ms 以后抬手 -- 不是雙擊或長(zhǎng)按,則觸發(fā)
        @Override
        public boolean onSingleTapConfirmed(MotionEvent e) {
            return super.onSingleTapConfirmed(e);
        }
    }
    /**
     * TODO 雙指縮放大的監(jiān)聽(tīng)
     */
    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;
        }
        // 開(kāi)始縮放
        @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 計(jì)算當(dāng)前的偏移,并賦值給offsetX, offsetY
            if (overScroller.computeScrollOffset()) {
                // computeScrollOffset()會(huì)返回一個(gè)boolean值,為true, 說(shuō)明動(dòng)作還沒(méi)完成,以此來(lái)作為循環(huán)結(jié)束條件
                offsetX = overScroller.getCurrX();
                offsetY = overScroller.getCurrY();
                invalidate();
                //在上面的onFling 方法里面,postOnAnimation 只會(huì)調(diào)用一次,所以我們這里再調(diào)用,參數(shù):自己(flingRunner)
                //TODO postOnAnimation 下一幀動(dòng)畫(huà)的時(shí)候執(zhí)行
                postOnAnimation(this);
            }
        }
    }
    /**
     * 計(jì)算圖片滑動(dòng)的邊界情況
     * TODO 當(dāng)往某個(gè)方向滑動(dòng)圖片時(shí),放大后的圖片邊界與手機(jī)屏幕邊界重合時(shí),就不能滑動(dòng)了
     */
    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 直接變化時(shí)都刷新
        invalidate();
    }
}

到此這篇關(guān)于Android自定義PhotoView使用教程的文章就介紹到這了,更多相關(guān)Android PhotoView內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!

相關(guān)文章

最新評(píng)論