Android開發實戰——自定義view之PhotoView圖片查看器


 

為了方便代碼的閱讀,我將類都寫成了內部類,下面的代碼拿了直接可以使用,換一下bitmap就行了。注釋也是比較詳細的,認真看再結合使用,應該很容易理解。

PhotoView.java

public class PhotoView extends View {
    private static final float IMAGE_WIDTH = Utils.dpToPixel(300);
    private Bitmap bitmap;
    private Paint paint;

    private float originalOffsetX; // X軸偏移 這里主要用於圖片初始化時設置居中
    private float originalOffsetY; // Y軸偏移 這里主要用於圖片初始化時設置居中

    private float smallScale;
    private float bigScale;
    private float currentScale;

    private GestureDetector gestureDetector;

    /*
     * 標志位,用於判斷當前圖片是smallScale還是bigScale
     * boolean值默認為false PhotoView默認也是smallScale
     * 所以當圖片為smallScale時isFlag=false  當圖片為bigScale時isFlag=true 值在onDoubleTap方法里面進行修改
     * */
    private boolean isFlag;

    private ObjectAnimator animator;// 雙擊圖片時的動畫

    private float offsetX; // 拖動圖片時X軸偏移量
    private float offsetY; // 拖動圖片時Y軸偏移量

    private OverScroller scroller; // 慣性滑動

    private ScaleGestureDetector scaleGestureDetector;


    private void init(Context context) {
        // todo 步驟1 初始化
        bitmap = Utils.getPhoto(getResources(), (int) IMAGE_WIDTH);// 獲取bitmap對象
        paint = new Paint(Paint.ANTI_ALIAS_FLAG);// 使位圖抗鋸齒

        // todo 步驟3 手勢監聽
        gestureDetector = new GestureDetector(context, new PhotoGestureDetector());
        // 關閉長按響應
        // gestureDetector.setIsLongpressEnabled(false);
        scroller = new OverScroller(context);


        // todo 步驟7 雙指縮放監聽
        scaleGestureDetector = new ScaleGestureDetector(context, new PhotoOnScaleGestureListener());
    }

    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);
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        // todo 步驟9 處理圖片放大平移后 再縮小時 圖片不回到正中間的問題
        /*
         * 這個系數是如何來的可能有點繞
         * 我們需要解決步驟9的問題,首先需要偏移值offsetX、offsetY與我們的縮放因子進行綁定,縮放因子越大,偏移的值也越大
         * 我們知道,整個圖片最大的縮放因子為bigScale(終點),最小縮放因子為smallScale(起點), bigScale-smallScale得到的就是總共縮放因子的區間值(距離)
         * 那么當前縮放因子是currentScale(當前所在位置), 減去smallScale(起點),得到的是當前縮放因子距離最小縮放因子的值(當前位置-起點的位置)
         * 那么  (當前位置-起點的位置)/ 總距離   得到的就是距離比  也就是當前我完成了總路程的百分之幾
         *
         * 結合下面的公式我們可以知道當currentScale=smallScale時 scaleAction為0  此時圖片不偏移
         * 當currentScale=bigScale時  scaleAction為1此時的圖片為最大圖 那么這個時候如果移動圖片的話offsetX、offsetY該偏移多少就偏移多少
         *
         * 所以當我們這樣計算這個比例之后,當圖片處於最大和最小之間時,我們手指平移100px,那么圖片可能只會平移50px
         * 當圖片處於最大的時候,我們手指平移100px,那么圖片會平移100px
         * 當圖片處於最小的時候,我們手指平移100px,那么圖片會平移0px 也就是不平移
         * */
        float scaleAction = (currentScale - smallScale) / (bigScale - smallScale);
        /* 圖片拖動的效果 */
        canvas.translate(offsetX * scaleAction, offsetY * scaleAction);

        /* 四個參數的意思分別是:圖片X軸縮放系數、圖片Y軸縮放系數、縮放時以哪個點進行縮放(我們取的是屏幕的中心點,默認是屏幕左上角,即0,0) */
        canvas.scale(currentScale, currentScale, getWidth() / 2f, getHeight() / 2f);

        /* 居中顯示 */
        canvas.drawBitmap(bitmap, originalOffsetX, originalOffsetY, paint);
    }


    /* 在控件大小發生改變時調用 初始化時會被調用一次 后續控件大小變化時也會調用*/
    @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        super.onSizeChanged(w, h, oldw, oldh);
        // todo 步驟2 初始圖片位置

        /* 居中顯示時 X、Y的值 */
        originalOffsetX = (getWidth() - bitmap.getWidth()) / 2f;
        originalOffsetY = (getHeight() - bitmap.getHeight()) / 2f;

        /*
         * 進入界面初始化圖片時,需要滿足左右兩邊填充整個屏幕或者上下兩邊填充整個屏幕
         * 所以要判斷圖片是豎形狀的圖片 還是橫形狀的圖片
         * 這里用圖片的寬高比與屏幕的寬高比進行對比來判斷
         *
         * smallScale表示圖片縮放的比例  圖片最小是多小
         * 命中if: 圖片按照寬度比進行縮放,當圖片的寬度與屏幕的寬度相等時停止縮放,圖片上下邊界與屏幕上下邊界會有間距
         * 命中else: 圖片按照高度比進行縮放,當圖片的高度與屏幕的高度相等時停止縮放,圖片左右邊界與屏幕左右邊界會有間距
         *
         * bigScale表示圖片縮放的比例  圖片最大是多大
         * 不*1.5f的話,那么圖片最大就是窄邊與屏邊對齊   *1.5f表示圖片放大后窄邊也可以可以超出屏幕
         *
         * currentScale的值到底是什么,取決於我們的需求
         * 在這里smallScale表示縮小  bigScale表示放大
         * 當currentScale = smallScale時 雙擊圖片之后currentScale = bigScale  否則相反  這個判斷在下面的雙擊事件里面處理
         * */
        if ((float) bitmap.getWidth() / bitmap.getHeight() > (float) getWidth() / getHeight()) {
            smallScale = (float) getWidth() / bitmap.getWidth();
            bigScale = (float) getHeight() / bitmap.getHeight() * 1.5f;
        } else {
            smallScale = (float) getHeight() / bitmap.getHeight();
            bigScale = (float) getWidth() / bitmap.getWidth() * 1.5f;
        }
        currentScale = smallScale;

    }

    /*
     * 因為當view被點擊的時候,進入的是view的onTouchEvent方法進行事件分發
     * 而我們這里用到的是GestureDetector 所以view的onTouchEvent要托管給GestureDetector的onTouchEvent去執行
     * 但是同時 雙指縮放的監聽也需要用到ScaleGestureDetector的onTouchEvent 所以還需要進行判斷
     *
     * */
    @Override
    public boolean onTouchEvent(MotionEvent event) {
        // 響應事件以雙指縮放優先
        boolean result = scaleGestureDetector.onTouchEvent(event);

        // 判斷 如果不是雙指縮放 則把事件交給手勢監聽處理
        if (!scaleGestureDetector.isInProgress()) {
            result = gestureDetector.onTouchEvent(event);
        }
        return result;
    }

    /* 手勢相關監聽 */
    class PhotoGestureDetector extends GestureDetector.SimpleOnGestureListener {

        /*
         * 單擊或者雙擊的第一次up時觸發
         * 即如果不是長按、不是雙擊的第二次點擊  則在up時觸發
         * */
        @Override
        public boolean onSingleTapUp(MotionEvent e) {
            return super.onSingleTapUp(e);
        }

        /* 長按觸發 默認超過300ms時觸發 */
        @Override
        public void onLongPress(MotionEvent e) {
            super.onLongPress(e);
        }

        /**
         * 滾動時(拖動圖片)觸發 -- move
         *
         * @param e1        手指按下的事件
         * @param e2        當前的事件
         * @param distanceX 舊位置 - 新位置  所以小於0表示往右  大於0表示往左  所以計算偏移值時要取反
         * @param distanceY 同上
         * @return
         */
        @Override
        public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
            // todo 步驟5 處理拖拽
            // 當圖片為放大模式時才允許拖動圖片
            // distanceX的值並不是起始位置與終點位置的差值,而是期間若干個小點匯聚而成的
            // 比如當從0px滑動到100px時,distanceX在1px時會出現,此時distanceX就是-1,然后可能又在2px時出現,
            // 在整個滑動過程中distanceX的值一直在變動,所以offsetX要一直計算   offsetY同理
            if (isFlag) {
                offsetX = offsetX - distanceX;
                offsetY = offsetY - distanceY;

                // 計算圖片可拖動的范圍
                fixOffset();
                // 刷新
                invalidate();
            }
            return super.onScroll(e1, e2, distanceX, distanceY);
        }

        /**
         * up時觸發 手指拖動圖片的時候 慣性滑動 -- 大於50dp/s
         *
         * @param velocityX x軸方向運動速度(像素/s)
         * @param velocityY
         * @return
         */
        @RequiresApi(api = Build.VERSION_CODES.JELLY_BEAN)
        @Override
        public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
            // todo 步驟6 處理拖拽時的慣性滑動
            // 當圖片為放大模式時才允許慣性滑動
            if (isFlag) {
                // 最后兩個參數表示 當慣性滑動超出圖片范圍多少之后回彈,這也是為什么用OverScroller而不用Scroller的原因
                scroller.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,
                        300, 300);
                postOnAnimation(new FlingRun());
            }

            return super.onFling(e1, e2, velocityX, velocityY);
        }

        /* 點擊后延時100ms觸發 -- 主要用於處理自定義的點擊效果,例如水波紋等 */
        @Override
        public void onShowPress(MotionEvent e) {
            super.onShowPress(e);
        }

        /*
         *  down時觸發 在這個需求里面,我們需要返回true 這與事件分發有關 可以看下我相關的文章
         * 這里只要知道, 當返回true的時候下面的雙擊等函數才會執行 否則直接在這里就攔截了
         * */
        @Override
        public boolean onDown(MotionEvent e) {
            return true;
        }

        /* 雙擊的第二次點擊down時觸發。雙擊的觸發時間 -- 40ms -- 300ms */
        @Override
        public boolean onDoubleTap(MotionEvent e) {
            // todo 步驟4 處理雙擊
            if (!isFlag) {
                // isFlag為false表示 取反前處於smallScale(縮小)狀態,則雙擊后要變成bigScale(放大)
                // currentScale = bigScale;
                // 點擊圖片的哪個位置 哪個位置就進行放大 不設置的話 圖片只會以中心進行放大
                // 其原理是以中心店先進行放大 再進行偏移
                offsetX = (e.getX() - getWidth() / 2f) -
                        (e.getX() - getWidth() / 2f) * bigScale / smallScale;
                offsetY = (e.getY() - getHeight() / 2f) -
                        (e.getY() - getHeight() / 2f) * bigScale / smallScale;
                fixOffset();

                // 這里直接添加動畫, 圖片從小到大  在getAnimator()方法里面我們設置了setFloatValues的值
                getAnimator(smallScale, bigScale).start();
            } else {
                // isFlag為true表示當前處於bigScale(放大)狀態,則雙擊后要變成smallScale(縮小)
                // currentScale = smallScale;
                // 這里直接添加動畫, 圖片從大到小
                // 如果沒有雙指縮放功能,直接用下面這行代碼就行了,但是在有雙指縮放的情況下,如果圖片被雙指縮放到一半的時候再進行雙擊
                // 圖片會有一個先縮小再放大的過程,這就是一個小BUG了
                // getAnimator(bigScale, smallScale).start();
                // 所以這里我們要用currentScale
                getAnimator(currentScale, smallScale).start();
            }
            // 每次雙擊后取反
            isFlag = !isFlag;
            return super.onDoubleTap(e);
        }

        /* 雙擊的第二次down、move、up 都觸發 */
        @Override
        public boolean onDoubleTapEvent(MotionEvent e) {
            return super.onDoubleTapEvent(e);
        }

        /*
         * 單擊按下時觸發,雙擊時不觸發,down,up時都可能觸發
         * 延時300ms觸發TAP事件
         * 300ms以內抬手 -- 會觸發TAP -- onSingleTapConfirmed
         * 300ms以后抬手 -- 不是雙擊不是長按,則觸發  但是300ms以后默認是長按事件
         * 因此我們可以關閉長按事件的相應  上面代碼有注釋
         * */
        @Override
        public boolean onSingleTapConfirmed(MotionEvent e) {
            return super.onSingleTapConfirmed(e);
        }
    }


    class FlingRun implements Runnable {

        @RequiresApi(api = Build.VERSION_CODES.JELLY_BEAN)
        @Override
        public void run() {
            // 判斷慣性動畫是否結束 沒結束返回true   結束了返回的是false
            if (scroller.computeScrollOffset()) {
                offsetX = scroller.getCurrX();
                offsetY = scroller.getCurrY();
                invalidate();
                // 下一幀動畫的時候執行
                postOnAnimation(this);
            }
        }
    }

    /*
     * 允許拖動圖片的邊界
     *
     * 設置圖片最大拖動的距離 如果不設置,那么拖動的距離超出圖片之后,看到的就是白色的背景
     * 設置之后,當拖動圖片到圖片邊界時,則不能繼續往該方向拖了。
     *
     * */
    private void fixOffset() {
        // 注意:offsetX為拖動的距離,offsetX = -(舊位置-新位置)
        // (bitmap.getWidth()*bigScale - getWidth())/2表示 圖片寬度的一半-屏幕寬度的一半 得到的就是可以拖動的最大距離
        // 當圖片往右時,我們的手也是往右 offsetX為正數,圖片可拖動的最大距離也為正數  此時取最小值為offsetX
        // 例如我們手指往右滑動了100px  而圖片最大只能動50px 再往右滑的話 圖片的左邊就超出圖片范圍了  因此取50px
        offsetX = Math.min(offsetX, (bitmap.getWidth() * bigScale - getWidth()) / 2);
        // 當圖片往左時,我們的手也是往左 offsetX為負數,而我們計算的圖片可拖動的距離是正數 因此這里要取反 並且取兩者最大值
        // 例如我們手指往左滑動了100px 那么offsetX = -100   而最大拖動距離為50px 取反為-50px   -100 與-50 取最大值
        offsetX = Math.max(offsetX, -(bitmap.getWidth() * bigScale - getWidth()) / 2);
        // Y軸一樣
        offsetY = Math.min(offsetY, (bitmap.getHeight() * bigScale - getHeight()) / 2);
        offsetY = Math.max(offsetY, -(bitmap.getHeight() * bigScale - getHeight()) / 2);
    }


    /* 圖片放大縮小動畫 */
    private ObjectAnimator getAnimator(float scale1, float scale2) {
        if (animator == null) {
            // 這個方法內部是通過反射 設置currentScale的值  所以currentScale必須要有get\set方法
            animator = ObjectAnimator.ofFloat(this, "currentScale", 0);
        }
        animator.setFloatValues(scale1, scale2);
        return animator;
    }

    public float getCurrentScale() {
        return currentScale;
    }

    public void setCurrentScale(float currentScale) {
        this.currentScale = currentScale;
        // 由於屬性動畫animator會不斷的調用set方法, 所以刷新放在這里
        invalidate();
    }

    /* 雙指縮放監聽類 */
    class PhotoOnScaleGestureListener implements ScaleGestureDetector.OnScaleGestureListener {

        float scale;

        /* 雙指縮放時 */
        @Override
        public boolean onScale(ScaleGestureDetector detector) {

            // todo 步驟8 處理雙指縮放以及縮放邊距
            /*
             * detector.getScaleFactor()表示兩個手指之間縮放的大小值
             * 例如兩個手指的距離縮短一半時 值為0.5  兩個手指距離增加一倍時  值為2
             *
             * scale表示初始化時的縮放因子,currentScale為最終的縮放因子
             * 這里不用currentScale直接乘的原因是 雙指縮放的動作是持續性的,
             * 因此如果用currentScale直接乘的話 縮放因子基數會一直變動,這樣取值不正確,
             * 正確的做法是要一直用縮放之前的因子 乘 detector.getScaleFactor()
             * */
            if ((currentScale >= bigScale && detector.getScaleFactor() < 1)
                    || (currentScale <= smallScale && detector.getScaleFactor() > 1)
                    || (currentScale > smallScale && currentScale < bigScale)) {

                if(scale * detector.getScaleFactor() <= smallScale){
                    // 解決雙指縮放時超過圖片最小邊界
                    currentScale = smallScale;
                    isFlag = false;
                } else if(scale * detector.getScaleFactor() >= bigScale){
                    // 解決雙指縮放時超過圖片最大邊界
                    currentScale = bigScale;
                    isFlag = true;
                } else {
                    currentScale = scale * detector.getScaleFactor();
                    isFlag = true;
                }
                invalidate();
            }

            // 解決圖片為smallScanle的時候 進行雙指放大后無法拖拽圖片的問題 因為上的else修改了isFlag的值,所以這里不用再次判斷
            /*if (currentScale >= smallScale && !isFlag) {
                isFlag = !isFlag;
            }*/

            return false;
        }

        /* 雙指縮放之前 這里要return true  與我們事件分發一樣的道理 */
        @Override
        public boolean onScaleBegin(ScaleGestureDetector detector) {
            scale = currentScale;
            return true;
        }

        /* 雙指縮放之后 這里一般不做處理 */
        @Override
        public void onScaleEnd(ScaleGestureDetector detector) {

        }
    }

}

 

Utils

public class Utils {

    public static float dpToPixel(float dp) {
        return TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dp,
                Resources.getSystem().getDisplayMetrics());
    }

    /* 獲取bitmap */
    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);
    }
}

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM