Android自定義控件之可平移、縮放、旋轉(zhuǎn)圖片控件

先上效果圖

效果圖.gif

源碼

一、需求分析

單點(diǎn)拖動(dòng)圖片對(duì)圖片進(jìn)行平移操作。雙手縮放圖片大小和旋轉(zhuǎn)圖片到一定的角度。圖片縮放的時(shí)候 不能大于最大的縮放因子和小于最小的縮放因子。大于最大縮放因子或者小于最小縮放因子需要對(duì)圖像進(jìn)行回彈。圖片旋轉(zhuǎn)的角度只能為90度的倍數(shù),不滿足90度要進(jìn)行回彈。圖片回彈要一個(gè)漸變的效果。

二、 思路分析

大體思路:首先,Android中提供了Matrix類可以對(duì)圖像進(jìn)行處理。其次,要顯示一張圖片最容易想到的就是ImageView?;貜椧鬂u變的過程,可以通過屬性動(dòng)畫進(jìn)行設(shè)置。所以大體的思路是:繼承ImageView,重寫onTouchEvent()方法,判斷事件類型,在對(duì)應(yīng)的事件使用Matrix對(duì)圖像進(jìn)行變換。
Matrix是一個(gè)已經(jīng)封裝好的矩陣,最重要的作用就是對(duì)坐標(biāo)點(diǎn)進(jìn)行變換。
舉個(gè)栗子:
1.某個(gè)點(diǎn)(x0,y0,1)通過單位矩陣E映射得到的點(diǎn)還是(x0,y0,1)。


2.調(diào)用單位矩陣E的postTranslate(dx,dy)。單位矩陣轉(zhuǎn)換成矩陣T

3.點(diǎn)(x0,y0,1)通過矩陣T映射得到的點(diǎn)就會(huì)做如下的變換


可以看到點(diǎn)(x0,y0,1)經(jīng)過T矩陣在x軸方向上平移了dx,在y軸方向上平移了dy。

通過以上的變換可以得到具體的思路:我們維護(hù)一個(gè)圖像對(duì)應(yīng)的矩陣mCurrentMatrix,該矩陣主要是對(duì)ImageView中的圖像的各個(gè)點(diǎn)進(jìn)行映射。ImageView在容器位置擺放完成之后,置mCurrentMatrix矩陣為單位矩陣。當(dāng)onTouchEvent()方法中觸發(fā)單點(diǎn)觸控并且手指進(jìn)行平移的時(shí)候,調(diào)用矩陣mCurrentMatrix的postTranslate(dx,dy),對(duì)mCurrentMatrix進(jìn)行變換。當(dāng)手指抬起,利用變換結(jié)束后的矩陣對(duì)圖像的各個(gè)點(diǎn)進(jìn)行映射,從而得到平移變換后的圖像。同理可得,在兩只手指進(jìn)行縮放旋轉(zhuǎn)的時(shí)候,我們對(duì)矩陣mCurrentMatrix進(jìn)行各種變換,當(dāng)縮放旋轉(zhuǎn)的事件結(jié)束再利用變換完的矩陣去映射圖像的各個(gè)點(diǎn),從而得到縮放、旋轉(zhuǎn)后的圖像。

三、Matrix

安卓自定義View進(jìn)階 - Matrix原理
安卓自定義View進(jìn)階 - Matrix詳解

  • 對(duì)于Matrix的使用以上兩篇博客中有詳細(xì)的描述Matrix的使用方法以及原理。例如矩陣的第一行第三列的浮點(diǎn)數(shù)以及第二行第三列的浮點(diǎn)數(shù)是用來映射點(diǎn)的平移。

  • 注意調(diào)用Matrix的setXxx方法,會(huì)將Matrix之前的變換都清除。

四、主要方法

首先理清事件的邏輯:

  • 單只手指按下的時(shí)候是可以進(jìn)行平移操作的,多點(diǎn)觸控的時(shí)候不能進(jìn)行平移操作。所以維護(hù)一個(gè)boolean變量mCanTranslate,在ACTION_DOWN的時(shí)候置位true,ACTION_POINTER_DOWN置為false。而平移操作的位移量需要手指滑動(dòng)時(shí)與上一次點(diǎn)的坐標(biāo)進(jìn)行相比較,所以維護(hù)一個(gè)mLastSinglePoint記錄手指按下以及手指滑動(dòng)時(shí)上一次點(diǎn)的坐標(biāo)。

  • 當(dāng)兩只手指按下的時(shí)候是可以進(jìn)行旋轉(zhuǎn)和縮放操作的,維護(hù)兩個(gè)boolean變量:mCanRotate、mCanScale在ACTION_POINTER_DOWN置為true,ACTION_POINTER_UP置為false。

  • 圖像的縮放比例因子采取的是前后兩個(gè)手指間距離的比值。所以維護(hù)mLastDist,在ACTION_POINTER_DOWN的時(shí)候算出兩只手指間的歐氏距離。在ACTION_MOVE的時(shí)候,算出新的手指間的距離,并和上一次手指間距離的運(yùn)算求出比例,更新mLastDist為本次手指間的距離。

  • 圖像的旋轉(zhuǎn)角度采取的是兩只手構(gòu)成的向量轉(zhuǎn)過的角度。維護(hù)一個(gè)mLastVector。在ACTION_POINTER_DOWN的時(shí)候記錄兩只手指構(gòu)成的向量。在ACTION_MOVE的時(shí)候,記錄新的兩只手指構(gòu)成的向量,求出轉(zhuǎn)過的角度,最后更新mLastVector為本次兩手指構(gòu)成的向量。

初始化圖像大小和位置

縮放圖像大小和控件大小自適應(yīng),平移圖像中心和控件中心重合

 private void init() {
        mCurrentMatrix.reset();
        upDateBoundRectF();
        float scaleFactor = Math.min(getWidth() / mBoundRectF.width(), getHeight() / mBoundRectF.height());
        mInitialScaleFactor = scaleFactor;
        mTotalScaleFactor *= scaleFactor;
        //以圖片的中心點(diǎn)進(jìn)行縮放,縮放圖片大小和控件大小適應(yīng)
        mCurrentMatrix.postScale(scaleFactor, scaleFactor, mBoundRectF.centerX(), mBoundRectF.centerY());
        //將圖片中心點(diǎn)平移到和控件中心點(diǎn)重合
        mCurrentMatrix.postTranslate(getPivotX() - mBoundRectF.centerX(), getPivotY() - mBoundRectF.centerY());
        //對(duì)圖片進(jìn)行變換,并更新圖片的邊界矩形
        transform();
    }

onTouchEvent()函數(shù)

 /**
     * 當(dāng)單點(diǎn)觸控的時(shí)候可以進(jìn)行平移操作
     * 當(dāng)多點(diǎn)觸控的時(shí)候:可以進(jìn)行圖片的縮放、旋轉(zhuǎn)
     * ACTION_DOWN:標(biāo)記能平移、不能旋轉(zhuǎn)、不能縮放
     * ACTION_POINTER_DOWN:如果手指?jìng)€(gè)數(shù)為2,標(biāo)記不能平移、能旋轉(zhuǎn)、能縮放
     * 記錄平移開始時(shí)兩手指的中點(diǎn)、兩只手指形成的向量、兩只手指間的距離
     * ACTION_MOVE:進(jìn)行平移、旋轉(zhuǎn)、縮放的操作。
     * ACTION_POINTER_UP:有一只手指抬起的時(shí)候,設(shè)置圖片不能旋轉(zhuǎn)、不能縮放,可以平移
     *
     * @param event 點(diǎn)擊事件
     * @return
     */
    @Override
    public boolean onTouchEvent(MotionEvent event) {
        switch (event.getActionMasked()) {
            //單點(diǎn)觸控,設(shè)置圖片可以平移、不能旋轉(zhuǎn)和縮放
            case MotionEvent.ACTION_DOWN:
                mCanTranslate = true;
                mCanRotate = false;
                mCanScale = false;
                //記錄單點(diǎn)觸控的上一個(gè)單點(diǎn)的坐標(biāo)
                mLastSinglePoint.set(event.getX(), event.getY());
                break;
            case MotionEvent.ACTION_POINTER_DOWN:
                animator.cancel();
                //多點(diǎn)觸控,設(shè)置圖片不能平移
                mCanTranslate = false;
                //當(dāng)手指?jìng)€(gè)數(shù)為兩個(gè)的時(shí)候,設(shè)置圖片能夠旋轉(zhuǎn)和縮放
                if (event.getPointerCount() == 2) {
                    mCanRotate = true;
                    mCanScale = true;
                    //記錄兩手指的中點(diǎn)
                    PointF pointF = midPoint(event);
                    //記錄開始滑動(dòng)前兩手指中點(diǎn)的坐標(biāo)
                    mLastMidPoint.set(pointF.x, pointF.y);
                    //記錄開始滑動(dòng)前兩個(gè)手指之間的距離
                    mLastDist = distance(event);
                    //設(shè)置向量,以便于計(jì)算角度
                    mLastVector.set(event.getX(0) - event.getX(1), event.getY(0) - event.getY(1));
                }
                break;
            case MotionEvent.ACTION_MOVE:
                //判斷能否平移操作
                if (mCanTranslate) {
                    float dx = event.getX() - mLastSinglePoint.x;
                    float dy = event.getY() - mLastSinglePoint.y;
                    //平移操作
                    translation(dx, dy);
                    //重置上一個(gè)單點(diǎn)的坐標(biāo)
                    mLastSinglePoint.set(event.getX(), event.getY());
                }
                //判斷能否縮放操作
                if (mCanScale) {
                    float scaleFactor = distance(event) / mLastDist;
                    scale(scaleFactor);
                    //重置mLastDist,讓下次縮放在此基礎(chǔ)上進(jìn)行
                    mLastDist = distance(event);
                }
                //判斷能否旋轉(zhuǎn)操作
                if (mCanRotate) {
                    //當(dāng)前兩只手指構(gòu)成的向量
                    PointF vector = new PointF(event.getX(0) - event.getX(1), event.getY(0) - event.getY(1));
                    //計(jì)算本次向量和上一次向量之間的夾角
                    float degree = calculateDeltaDegree(mLastVector, vector);
                    rotation(degree);
                    //更新mLastVector,以便下次旋轉(zhuǎn)計(jì)算旋轉(zhuǎn)過的角度
                    mLastVector.set(vector.x, vector.y);
                }
                //圖像變換
                transform();
                break;
            case MotionEvent.ACTION_POINTER_UP:
                //當(dāng)兩只手指有一只抬起的時(shí)候,設(shè)置圖片不能縮放和選擇,能夠進(jìn)行平移
                if (event.getPointerCount() == 2) {
                    mCanScale = false;
                    mCanRotate = false;
                    mCanTranslate = true;
                    //重置旋轉(zhuǎn)和縮放使用到的中點(diǎn)坐標(biāo)
                    mLastMidPoint.set(0f, 0f);
                    //重置兩只手指的距離
                    mLastDist = 0f;
                    //重置兩只手指形成的向量
                    mLastVector.set(0f, 0f);
                }
                //獲得開始動(dòng)畫之前的矩陣
                mCurrentMatrix.getValues(mBeginMatrixValues);
                //縮放回彈
                backScale();
                upDateBoundRectF();
                //旋轉(zhuǎn)回彈
                backRotation();
                upDateBoundRectF();
                //獲得動(dòng)畫結(jié)束之后的矩陣
                mCurrentMatrix.getValues(mEndMatrixValues);
                animator.start();
                break;
            case MotionEvent.ACTION_CANCEL:
            case MotionEvent.ACTION_UP:
                backTranslation();
                upDateBoundRectF();
                mLastSinglePoint.set(0f, 0f);
                mCanTranslate = false;
                mCanScale = false;
                mCanRotate = false;
                break;
        }
        return true;
    }

平移操作

將圖像對(duì)應(yīng)的矩陣進(jìn)行變換。

  protected void translation(float dx, float dy) {
        //檢查圖片邊界的平移是否超過控件的邊界
        if (mBoundRectF.left + dx > getWidth() - 20 || mBoundRectF.right + dx < 20
                || mBoundRectF.top + dy > getHeight() - 20 || mBoundRectF.bottom + dy < 20) {
            return;
        }
        mCurrentMatrix.postTranslate(dx, dy);
    }

縮放操作

mBoundRectF為記錄圖像邊界的矩形??s放的時(shí)候選取圖像的中心進(jìn)行縮放。

  private void scale(float scaleFactor) {
        //累乘得到總的的縮放因子
        mTotalScaleFactor *= scaleFactor;
        mCurrentMatrix.postScale(scaleFactor, scaleFactor, mBoundRectF.centerX(), mBoundRectF.centerY());
    }

旋轉(zhuǎn)操作

旋轉(zhuǎn)的時(shí)候旋轉(zhuǎn)的旋轉(zhuǎn)中心也是圖像的中心

    private void rotation(float degree) {
        //旋轉(zhuǎn)變換
        mCurrentMatrix.postRotate(degree, mBoundRectF.centerX(), mBoundRectF.centerY());

    }

圖像中各個(gè)點(diǎn)的映射

調(diào)用ImageView的setImageMatrix(Matrix matrix)會(huì)讓ImageView根據(jù)設(shè)置的matrix去重新繪制圖像。

   private void transform() {
        setImageMatrix(mCurrentMatrix);
        upDateBoundRectF();
    }

更新圖像的矩形邊界

獲得圖像的矩形,并根據(jù)矩陣映射矩形各個(gè)點(diǎn)的坐標(biāo)。

 private void upDateBoundRectF() {
        if (getDrawable() != null) {
            mBoundRectF.set(getDrawable().getBounds());
            mCurrentMatrix.mapRect(mBoundRectF);
        }
    }

縮放回彈

    private void backScale() {
        float scaleFactor = 1.0f;
        //如果總的縮放比例因子比初始化的縮放因子還小,進(jìn)行回彈
        if (mTotalScaleFactor / mInitialScaleFactor < mMinScaleFactor) {
            //1除以總的縮放因子再乘初始化的縮放因子,求得回彈的縮放因子
            scaleFactor = mInitialScaleFactor / mTotalScaleFactor * mMinScaleFactor;
            //更新總的縮放因子,以便下次在此縮放比例的基礎(chǔ)上進(jìn)行縮放
            mTotalScaleFactor = mInitialScaleFactor * mMinScaleFactor;
        }
        //如果總的縮放比例因子大于最大值,讓圖片放大到最大倍數(shù)
        else if (mTotalScaleFactor / mInitialScaleFactor > mMaxScaleFactor) {
            //求放大到最大倍數(shù),需要的比例因子
            scaleFactor = mInitialScaleFactor / mTotalScaleFactor * mMaxScaleFactor;
            //更新總的縮放因子,以便下次在此縮放比例的基礎(chǔ)上進(jìn)行縮放
            mTotalScaleFactor = mInitialScaleFactor * mMaxScaleFactor;
        }
        mCurrentMatrix.postScale(scaleFactor, scaleFactor, mBoundRectF.centerX(), mBoundRectF.centerY());
    }

旋轉(zhuǎn)回彈

private void backRotation() {
        //x軸方向的單位向量,在極坐標(biāo)中,角度為0
        float[] x_vector = new float[]{1.0f, 0.0f};
        //映射向量
        mCurrentMatrix.mapVectors(x_vector);
        //計(jì)算x軸方向的單位向量轉(zhuǎn)過的角度
        float totalDegree = (float) Math.toDegrees((float) Math.atan2(x_vector[1], x_vector[0]));
        float degree = totalDegree;
        degree = Math.abs(degree);
        //如果旋轉(zhuǎn)角度的絕對(duì)值在45-135度之間,讓其旋轉(zhuǎn)角度為90度
        if (degree > 45 && degree <= 135) {
            degree = 90;
        } //如果旋轉(zhuǎn)角度的絕對(duì)值在135-225之間,讓其旋轉(zhuǎn)角度為180度
        else if (degree > 135 && degree <= 225) {
            degree = 180;
        } //如果旋轉(zhuǎn)角度的絕對(duì)值在225-315之間,讓其旋轉(zhuǎn)角度為270度
        else if (degree > 225 && degree <= 315) {
            degree = 270;
        }//如果旋轉(zhuǎn)角度的絕對(duì)值在315-360之間,讓其旋轉(zhuǎn)角度為0度
        else {
            degree = 0;
        }
        degree = totalDegree < 0 ? -degree : degree;
        //degree-totalDegree計(jì)算達(dá)到90的倍數(shù)角,所需的差值
        mCurrentMatrix.postRotate(degree - totalDegree, mBoundRectF.centerX(), mBoundRectF.centerY());
    }

一些計(jì)算方法

    /**
     * 計(jì)算兩個(gè)手指頭之間的中心點(diǎn)的位置
     * x = (x1+x2)/2;
     * y = (y1+y2)/2;
     *
     * @param event 觸摸事件
     * @return 返回中心點(diǎn)的坐標(biāo)
     */
    private PointF midPoint(MotionEvent event) {
        float x = (event.getX(0) + event.getX(1)) / 2;
        float y = (event.getY(0) + event.getY(1)) / 2;
        return new PointF(x, y);
    }
    /**
     * 計(jì)算兩個(gè)手指間的距離
     *
     * @param event 觸摸事件
     * @return 放回兩個(gè)手指之間的距離
     */
    private float distance(MotionEvent event) {
        float x = event.getX(0) - event.getX(1);
        float y = event.getY(0) - event.getY(1);
        return (float) Math.sqrt(x * x + y * y);//兩點(diǎn)間距離公式
    }
    /**
     * 計(jì)算兩個(gè)向量之間的夾角
     *
     * @param lastVector 上一次兩只手指形成的向量
     * @param vector     本次兩只手指形成的向量
     * @return 返回手指旋轉(zhuǎn)過的角度
     */
    private float calculateDeltaDegree(PointF lastVector, PointF vector) {
        float lastDegree = (float) Math.atan2(lastVector.y, lastVector.x);
        float degree = (float) Math.atan2(vector.y, vector.x);
        float deltaDegree = degree - lastDegree;
        return (float) Math.toDegrees(deltaDegree);
    }

五、動(dòng)畫

要求圖像的變換是一個(gè)漸變的過程,很容易想到的就是屬性動(dòng)畫。因?yàn)閷傩詣?dòng)畫本身就是對(duì)值進(jìn)行不斷set的過程。而我們維護(hù)的矩陣也是一個(gè)值,所以很自然可以想到,如果得到回彈之前的矩陣的值以及回彈之后矩陣的值,就可以根據(jù)動(dòng)畫監(jiān)聽器中動(dòng)畫當(dāng)前的系數(shù)值去改變矩陣的值。

/**
     * 動(dòng)畫監(jiān)聽器
     */
    private ValueAnimator.AnimatorUpdateListener animatorUpdateListener = new ValueAnimator.AnimatorUpdateListener() {
        @Override
        public void onAnimationUpdate(ValueAnimator animation) {
            //獲得動(dòng)畫過程當(dāng)前的系數(shù)值
            float animatedValue = (float) animation.getAnimatedValue();
            for (int i = 0; i < 9; i++) {
                //使用漸變過程中的系數(shù)值去變換矩陣
                mTransformMatrixValues[i] = mBeginMatrixValues[i] + (mEndMatrixValues[i] - mBeginMatrixValues[i]) * animatedValue;
            }
            //動(dòng)態(tài)更新矩陣中的值
            mCurrentMatrix.setValues(mTransformMatrixValues);
            //圖像變化
            transform();
        }
    };

    /**
     * 動(dòng)畫監(jiān)聽器
     */
    private Animator.AnimatorListener animatorListener = new AnimatorListenerAdapter() {
        @Override
        public void onAnimationEnd(Animator animation) {
            mCurrentMatrix.setValues(mEndMatrixValues);
            transform();
        }
    };

對(duì)animator對(duì)象設(shè)置完監(jiān)聽器之后,就可以在手指抬起的時(shí)候調(diào)用屬性動(dòng)畫的start()方法開啟動(dòng)畫。

六、總結(jié)

自定義可平移、縮放、旋轉(zhuǎn)的控件主要點(diǎn)有兩個(gè)方面:一是onTouchEvent()中判斷平移、旋轉(zhuǎn)、縮放的觸發(fā)條件,平移位移量、縮放比例因子、旋轉(zhuǎn)角度的計(jì)算。二是Matrix矩陣的應(yīng)用。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容