ImageView 實現縮放,平移,Fling

Imageview自適應顯示圖片(不管任何圖片任何尺寸都自適應View大小顯示)
主要使用了一下知識點:
Matrix:矩陣變換(縮放,平移)
ScaleGestureDetector:縮放手勢檢測
GestureDetector:手勢檢測

- ImageView 縮放功能
縮放實現重寫:

//如果返回true,則會重置detector對放大比例的計算。默認爲1.0
//如果返回false,則持續計算放大比例
public boolean onScale(ScaleGestureDetector detector);

實現步驟:

  1. 獲取Image本身的縮放比例
//因爲是等比縮放,所以X軸的縮放等於y軸縮放
private float getScale(){
		Matrix matrix = getImageMatrix();
        matrix.getValues(matrixValues);
        return matrixValues[Matrix.MSCALE_X];
    }
  1. 獲取當前用戶操作的縮放比例的增量
float scaleFactor = detector.getScaleFactor();
scaleFactor = scaleFactor  - 1.0f;
  1. 使用Matrix進行縮放
float now_scale = getScale();
//當前放大倍數+當次放大的比例,放大倍數不能爲負數,負數的話則圖片上下顛倒了
now_scale = now_scale + (scaleFactor - 1.0f);
now_scale = Math.abs(now_scale);

Matrix matrix = new Matrix();
matrix.postScale(now_scale,now_scale,detector.getFocusX(),detector.getFocusY());
setImageMatrix(matrix);

- ImageView 平移功能
平移重寫:

public boolean onTouch(View v, MotionEvent event);

實現步驟:

  1. OnTouch 處理ACTION_MOVE事件,計算Move的偏移量
float dx = event.getX() - mLastX;
float dy = event.getY() - mLastY;
//計算完之後更新最後的X,Y位置
mLastX = event.getX();
mLastY = event.getY();
  1. 獲取Img已經發生的平移量
private float getTranclateX(){
        Matrix matrix = getImageMatrix();
        matrix.getValues(matrixValues);
        return matrixValues[Matrix.MTRANS_X];
    }

private float getTranclateY(){
        Matrix matrix = getImageMatrix();
        matrix.getValues(matrixValues);
        return matrixValues[Matrix.MTRANS_Y];
    }
  1. 使用Matrix進行平移
Matrix matrix = new Matrix();
matrix.postTranslate(dx, dy);
setImageMatrix(matrix);

- 平移+縮放綜合處理
上述只是單獨的實現了縮放或者平移某一功能,setImageMatrix的Matrix是New的新對象會重置Scale或Translate因子。但是圖片縮放和平移可能是同步進行的,所以我們需要同時處理Scale和Translate。

例如:圖片目前平移量是(-150,-200),放大比例是(2.0,2.0);如果我們將圖片平移到(-300,-400),那麼圖片的放大比例依然是(2.0,2.0)。如果我們將圖片放大到(3.0,3.0),因爲圖片放大了,所以尺寸發生變化導致平移量也將發生變化,不在保持爲(-300,-400)。具體偏移量的更新算法,可以參考Matrix 變換原理

所以平移的時候我們需要保持縮放因子

Matrix matrix = new Matrix();
matrix.preScale(getScale(),getScale());
matrix.postTranslate(dx, dy);
setImageMatrix(matrix);

縮放的時候需要更新平移量爲縮放之後的平移量

//先進行縮放
Matrix matrix = new Matrix();
matrix.postScale(now_scale,now_scale,detector.getFocusX(),detector.getFocusY());
setImageMatrix(matrix);
//縮放成功之後,獲取最新平移量,更新平移量
matrix = new Matrix();
matrix.postScale(getScale(),getScale());
matrix.postTranslate(getTranclateX(),getTranclateY());
setImageMatrix(matrix);

此時就可以對圖片進行正常的縮放和平移了。

爲什麼我們不使用getImageMatrix來獲取image的Matrix,然後在通過Matrix來進行變換?

因爲矩陣變換的算法很複雜,如果操作不當,那麼結果就和我們預期的有差別了。所以爲了簡單計算,控制變量因子,每次都重置Matrix,保證Matrix是我們想象中的模樣。

- 實現圖片的Fling功能
基本監聽GestureDetector的onFling中的垂直方向投遞速度,然後在定時器裏面進行變速移動
當前實現最基本的算法。
1.投擲之後再1s中內完成垂直平移。
2.因爲投擲之後是變速的先快後慢,所以打算在1s中完成50次的變速移動
3.變速算法使用垂直速度(velocityY)/5000獲得單步移動量。然後50次的移動中,第一次移動50個單位,第二次移動49個單位,逐次遞減,直至最後一次移動一個單位,完成Fling過程。
4.如果在移動的過程中用戶點擊了屏幕,此時應該停止移動。
(以上算法僅僅爲最基本的模擬算法,用戶可以實現自己的更完善的算法)

 mGestureDetector = new GestureDetector(context,new GestureDetector.SimpleOnGestureListener(){
            @Override
            public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, final float velocityY) {
                Log.i(TAG, "onFling: y0:"+e1.getRawY()+",y1:"+e2.getRawY()+",velocityY:"+velocityY);
				
				 //投擲距離過短或者速度很慢不作爲Fling處理
                if( Math.abs(e1.getY() - e2.getY()) < 30 || Math.abs(velocityY) < 5000)
                    return false;
               //啓動定時器20ms移動一次
               try {
               	mTimer = new Timer();
                mTimerCount = 0;
                mTimer.schedule(new TimerTask() {
                @Override
                public void run() {
                	Message message = new Message();
                    message.what = 1;
                    message.arg1 = (int) velocityY;
                    mHandler.sendMessage(message);
                    }
               	},0,20);
             }
             catch (Exception e){
             	e.printStackTrace();
             }
             return super.onFling(e1, e2, velocityX, velocityY);
            }
        });

		//處理定時器
		mHandler = new Handler(){
            @Override
            public void handleMessage(@NonNull Message msg) {
                switch (msg.what) {
                    case 1: {
                        Log.i(TAG, "handleMessage: arg1:"+msg.arg1);

                        float fStep = Math.max(1,Math.abs(msg.arg1)/5000.0f);
                        float fOffset = fStep * (MAX_COUNT - mTimerCount);
                        if(msg.arg1 < 0)
                            fOffset = -fOffset;
                        ++mTimerCount;
                        //如果滑動結束
                        if(scrollByY(fOffset) ||mTimerCount >= MAX_COUNT){
                            mTimer.cancel();
                            mTimer = null;
                        }
                        break;  
                    }
                        
                }
                super.handleMessage(msg);
            }
        };

private boolean scrollByY(float dOffset){
	 float dy = getTranclateY();
     dy += dOffset;
	
	Matrix matrix = new Matrix();
    matrix.preScale(getScale(),getScale());
    matrix.postTranslate(getTranclateX(), dy);
    setImageMatrix(matrix);
	return false;
}

  • 優化
    1.圖片如果已經可以完全展示在視圖內,則應該不允許在縮小,而且上下居中顯示
 Drawable d = getDrawable();
 if (null == d) return;
 //計算最小的縮放比例
mMinScale = getWidth()/d.getIntrinsicWidth();

ow_scale = now_scale + (scaleFactor - 1.0f);
now_scale = Math.abs(now_scale);
now_scale = Math.max(mMinScale,now_scale);

if(now_scale < SCALE_MAX ){
            Matrix matrix = new Matrix();
            matrix.postScale(now_scale,now_scale,detector.getFocusX(),detector.getFocusY());
            setImageMatrix(matrix);

            //縮放之後TranslX和TranslateY會發生變化,如果縮放之後圖片小於視圖則居中顯示
            RectF rtMatrixf = getMatrixRectF();
            //檢查上下左右邊界
            float dx =Math.min(0, getTranclateX()),dy = Math.min(0,getTranclateY());
            //如果圖片小於視圖則左右居中顯示
            if(rtMatrixf.right - rtMatrixf.left <= getWidth()){
                dx = (getWidth() - (rtMatrixf.right - rtMatrixf.left))/2;
            }
            //製圖片右邊界顯示在視圖內,導致白邊
            else if(dx + (rtMatrixf.right - rtMatrixf.left) < getWidth()){
                dx = getWidth() - (rtMatrixf.right - rtMatrixf.left);
            }

            //如果圖片小於視圖則垂直居中顯示
            if(rtMatrixf.bottom - rtMatrixf.top <= getHeight()){
                dy = (getHeight() - (rtMatrixf.bottom - rtMatrixf.top))/2;
            }
            //控制圖片下邊界顯示在視圖內,導致白邊
            else if(dy + (rtMatrixf.bottom - rtMatrixf.top) < getHeight()){
                dy = getHeight() - (rtMatrixf.bottom - rtMatrixf.top);
            }

            matrix = new Matrix();
            matrix.postScale(getScale(),getScale());
            matrix.postTranslate(dx,dy);
            setImageMatrix(matrix);
     }

2.圖片移動的過程中,上下左右邊框不應該出現在視圖範圍之內,造成白邊

case MotionEvent.ACTION_MOVE:{
                Drawable drawable = getDrawable();
                if(drawable == null)
                    return true;

                RectF rtMatrixf = getMatrixRectF();
                //檢查左右邊界
                float dx = 0;
                //如果圖片小於視圖則左右居中顯示
                if(rtMatrixf.right - rtMatrixf.left <= getWidth()){
                    dx = (getWidth() - (rtMatrixf.right - rtMatrixf.left))/2;
                }
                else if(rtMatrixf.right - rtMatrixf.left > getWidth()){
                    //控制圖片左邊界顯示在視圖內,導致白邊,dx只允許 < 0
                    dx = getTranclateX() + (event.getX() - mLastX);
                    dx = Math.min(0,dx);
                    //製圖片右邊界顯示在視圖內,導致白邊
                    if(dx + (rtMatrixf.right - rtMatrixf.left) < getWidth()){
                        dx = getWidth() - (rtMatrixf.right - rtMatrixf.left);
                    }
                }

                //檢查上下邊界
                float dy = 0;
                //如果圖片小於視圖則垂直居中顯示
                if(rtMatrixf.bottom - rtMatrixf.top <= getHeight()){
                    dy = (getHeight() - (rtMatrixf.bottom - rtMatrixf.top))/2;
                }
                else if(rtMatrixf.bottom - rtMatrixf.top > getHeight()){
                    //控制圖片上邊界顯示在視圖內,導致白邊,dy只允許 < 0
                    dy = getTranclateY() + (event.getY() - mLastY);
                    dy = Math.min(0,dy);
                    //控制圖片下邊界顯示在視圖內,導致白邊
                    if(dy + (rtMatrixf.bottom - rtMatrixf.top) < getHeight()){
                        dy = getHeight() - (rtMatrixf.bottom - rtMatrixf.top);
                    }
                }

                mLastX = event.getX();
                mLastY = event.getY();
                Log.i(TAG, "ACTION_MOVE,dx: "+dx + ",dy:"+dy);
                //計算邊界,
                Matrix matrix = new Matrix();
                matrix.preScale(getScale(),getScale());
                matrix.postTranslate(dx, dy);
                setImageMatrix(matrix);
                break;
            }
  1. Fling的時候,如果已經到底或者到頂部則應該結束
private boolean scrollByY(float dOffset){
        boolean bEnd = false;
        RectF rtMatrixf = getMatrixRectF();
        //檢查上下邊界
        float dy = getTranclateY();
        dy += dOffset;
        if(dy > 0) {
            dy = 0;
            bEnd = true;
        }

        if(rtMatrixf.bottom - rtMatrixf.top + dy < getHeight()){
            dy = getHeight() - (rtMatrixf.bottom - rtMatrixf.top);
            bEnd = true;
        }

        //計算邊界,
        Matrix matrix = new Matrix();
        matrix.preScale(getScale(),getScale());
        matrix.postTranslate(getTranclateX(), dy);
        setImageMatrix(matrix);
        return bEnd;
    }

Fling:
在這裏插入圖片描述
縮放和拖拽
在這裏插入圖片描述

最終源碼:

public class ZoomImageView extends ImageView implements ScaleGestureDetector.OnScaleGestureListener,
        View.OnTouchListener {
    private static final String TAG = ZoomImageView.class.getName();
    private ScaleGestureDetector mScaleGestureDetector;
    private GestureDetector mGestureDetector;
    private static final float SCALE_MAX = 4.0f;
    private final float[] matrixValues = new float[9];
    //最終點擊的位置
    private float mLastX = 0,mLastY = 0;

    //定時器滑動,滑動1s中,即50次
    private Timer mTimer = null;
    private final int MAX_COUNT = 50;
    int mTimerCount = 0;
    private Handler mHandler;

    public ZoomImageView(Context context, AttributeSet attrs)
    {
        super(context, attrs);
        super.setScaleType(ScaleType.MATRIX);
        mScaleGestureDetector = new ScaleGestureDetector(context, this);
        mGestureDetector = new GestureDetector(context,new GestureDetector.SimpleOnGestureListener(){
            @Override
            public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, final float velocityY) {
                Log.i(TAG, "onFling: y0:"+e1.getRawY()+",y1:"+e2.getRawY()+",velocityY:"+velocityY);

                //投擲距離過短或者速度很慢不作爲Fling處理
                if( Math.abs(e1.getY() - e2.getY()) < 30 || Math.abs(velocityY) < 5000)
                    return false;
                //只有當前顯示不下才可以進行Fling
                RectF rtF = getMatrixRectF();
                if(rtF.bottom - rtF.top > getHeight())
                {
                    try {
                        mTimer = new Timer();
                        mTimerCount = 0;
                        mTimer.schedule(new TimerTask() {
                            @Override
                            public void run() {
                                Message message = new Message();
                                message.what = 1;
                                message.arg1 = (int) velocityY;
                                mHandler.sendMessage(message);
                            }
                        },0,20);
                    }
                    catch (Exception e){
                        e.printStackTrace();
                    }

                }

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

        mHandler = new Handler(){
            @Override
            public void handleMessage(@NonNull Message msg) {
                switch (msg.what) {
                    case 1: {
                        Log.i(TAG, "handleMessage: arg1:"+msg.arg1);

                        float fStep = Math.max(1,Math.abs(msg.arg1)/5000.0f);
                        float fOffset = fStep * (MAX_COUNT - mTimerCount);
                        if(msg.arg1 < 0)
                            fOffset = -fOffset;
                        ++mTimerCount;
                        //如果滑動結束
                        if(scrollByY(fOffset) ||mTimerCount >= MAX_COUNT){
                            mTimer.cancel();
                            mTimer = null;
                        }
                        break;  
                    }
                        
                }
                super.handleMessage(msg);
            }
        };
    }

    //返回true會重新計算getScale的返回值,默認爲1
    public boolean onScale(ScaleGestureDetector detector){
        float now_scale = getScale();
        float scaleFactor = detector.getScaleFactor();

        Drawable d = getDrawable();
        if( d == null)
            return true;

        Log.i(TAG, "Scale: "+scaleFactor + ",Scale:"+now_scale + ",TranslateX:"+getTranclateX()+",TranslateY:"+getTranclateY()+
                ",width:"+getWidth()+",height:"+getHeight());

        //當前放大倍數+當次放大的比例,放大倍數不能爲負數,負數的話則圖片上下顛倒了
        float fMinScale = (getWidth()*1.0f)/d.getIntrinsicWidth();
        now_scale = now_scale + (scaleFactor - 1.0f);
        now_scale = Math.abs(now_scale);
        now_scale = Math.max(fMinScale,now_scale);

        if(now_scale < SCALE_MAX ){
            Matrix matrix = new Matrix();
            matrix.postScale(now_scale,now_scale,detector.getFocusX(),detector.getFocusY());
            setImageMatrix(matrix);

            //縮放之後TranslX和TranslateY會發生變化,如果縮放之後圖片小於視圖則居中顯示
            RectF rtMatrixf = getMatrixRectF();
            //檢查上下左右邊界
            float dx =Math.min(0, getTranclateX()),dy = Math.min(0,getTranclateY());
            //如果圖片小於視圖則左右居中顯示
            if(rtMatrixf.right - rtMatrixf.left <= getWidth()){
                dx = (getWidth() - (rtMatrixf.right - rtMatrixf.left))/2;
            }
            //製圖片右邊界顯示在視圖內,導致白邊
            else if(dx + (rtMatrixf.right - rtMatrixf.left) < getWidth()){
                dx = getWidth() - (rtMatrixf.right - rtMatrixf.left);
            }

            //如果圖片小於視圖則垂直居中顯示
            if(rtMatrixf.bottom - rtMatrixf.top <= getHeight()){
                dy = (getHeight() - (rtMatrixf.bottom - rtMatrixf.top))/2;
            }
            //控制圖片下邊界顯示在視圖內,導致白邊
            else if(dy + (rtMatrixf.bottom - rtMatrixf.top) < getHeight()){
                dy = getHeight() - (rtMatrixf.bottom - rtMatrixf.top);
            }

            matrix = new Matrix();
            matrix.postScale(getScale(),getScale());
            matrix.postTranslate(dx,dy);
            setImageMatrix(matrix);
        }

        return true;
    }

    @Override
    public boolean onScaleBegin(ScaleGestureDetector detector){
        return true;
    }

    @Override
    public void onScaleEnd(ScaleGestureDetector detector){
    }

    @Override
    public boolean onTouch(View v, MotionEvent event)
    {
        mGestureDetector.onTouchEvent(event);
        mScaleGestureDetector.onTouchEvent(event);
        int nTouchCount = event.getPointerCount();
        if(nTouchCount > 1)
            return true;

        switch (event.getAction()){
            case MotionEvent.ACTION_DOWN:{
                mLastX = event.getX();
                mLastY = event.getY();
               if(mTimer != null)
                   mTimer.cancel();
               mTimer = null;
                break;
            }
            case MotionEvent.ACTION_MOVE:{
                Drawable drawable = getDrawable();
                if(drawable == null)
                    return true;

                RectF rtMatrixf = getMatrixRectF();
                //檢查左右邊界
                float dx = 0;
                //如果圖片小於視圖則左右居中顯示
                if(rtMatrixf.right - rtMatrixf.left <= getWidth()){
                    dx = (getWidth() - (rtMatrixf.right - rtMatrixf.left))/2;
                }
                else if(rtMatrixf.right - rtMatrixf.left > getWidth()){
                    //控制圖片左邊界顯示在視圖內,導致白邊,dx只允許 < 0
                    dx = getTranclateX() + (event.getX() - mLastX);
                    dx = Math.min(0,dx);
                    //製圖片右邊界顯示在視圖內,導致白邊
                    if(dx + (rtMatrixf.right - rtMatrixf.left) < getWidth()){
                        dx = getWidth() - (rtMatrixf.right - rtMatrixf.left);
                    }
                }

                //檢查上下邊界
                float dy = 0;
                //如果圖片小於視圖則垂直居中顯示
                if(rtMatrixf.bottom - rtMatrixf.top <= getHeight()){
                    dy = (getHeight() - (rtMatrixf.bottom - rtMatrixf.top))/2;
                }
                else if(rtMatrixf.bottom - rtMatrixf.top > getHeight()){
                    //控制圖片上邊界顯示在視圖內,導致白邊,dy只允許 < 0
                    dy = getTranclateY() + (event.getY() - mLastY);
                    dy = Math.min(0,dy);
                    //控制圖片下邊界顯示在視圖內,導致白邊
                    if(dy + (rtMatrixf.bottom - rtMatrixf.top) < getHeight()){
                        dy = getHeight() - (rtMatrixf.bottom - rtMatrixf.top);
                    }
                }

                mLastX = event.getX();
                mLastY = event.getY();
                Log.i(TAG, "ACTION_MOVE,dx: "+dx + ",dy:"+dy);
                //計算邊界,
                Matrix matrix = new Matrix();
                matrix.preScale(getScale(),getScale());
                matrix.postTranslate(dx, dy);
                setImageMatrix(matrix);
                break;
            }
            case MotionEvent.ACTION_UP:{
                mLastX = event.getX();
                mLastY = event.getY();
            }
        }
        return true;
    }

    private float getScale(){
        Matrix matrix = getImageMatrix();
        matrix.getValues(matrixValues);
        return matrixValues[Matrix.MSCALE_X];
    }

    private float getTranclateX(){
        Matrix matrix = getImageMatrix();
        matrix.getValues(matrixValues);
        return matrixValues[Matrix.MTRANS_X];
    }

    private float getTranclateY(){
        Matrix matrix = getImageMatrix();
        matrix.getValues(matrixValues);
        return matrixValues[Matrix.MTRANS_Y];
    }

    @Override
    protected void onAttachedToWindow() {
        super.onAttachedToWindow();
    }

    @Override
    protected void onDetachedFromWindow() {
        super.onDetachedFromWindow();

        if(mTimer != null)
            mTimer.cancel();
        mTimer = null;
    }

    //獲取變換之後的圖片尺寸
    private RectF getMatrixRectF()
    {
        Matrix matrix = getImageMatrix();
        RectF rect = new RectF();
        Drawable d = getDrawable();
        if (null != d)
        {
            rect.set(0, 0, d.getIntrinsicWidth(), d.getIntrinsicHeight());
            matrix.mapRect(rect);
        }
        return rect;
    }

    private boolean scrollByY(float dOffset){
        boolean bEnd = false;
        RectF rtMatrixf = getMatrixRectF();
        //檢查上下邊界
        float dy = getTranclateY();
        dy += dOffset;
        if(dy > 0) {
            dy = 0;
            bEnd = true;
        }

        if(rtMatrixf.bottom - rtMatrixf.top + dy < getHeight()){
            dy = getHeight() - (rtMatrixf.bottom - rtMatrixf.top);
            bEnd = true;
        }

        //計算邊界,
        Matrix matrix = new Matrix();
        matrix.preScale(getScale(),getScale());
        matrix.postTranslate(getTranclateX(), dy);
        setImageMatrix(matrix);
        return bEnd;
    }
}

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章