Android多点触控技术,实现对图片的放大缩小平移,惯性滑动等功能

首先推荐一下鸿洋大大的打造个性的图片预览与多点触控视频教程,这套教程教我们一步一步实现了多点触控实现对图片的平移和缩放的功能。这篇文章我将在鸿洋大大的基础之上做了一些扩展功能:
1.图片的惯性滑动
2.图片缩放小于正常比例时,松手会自己主动回弹成正常比例
3.图片缩放大于最大比例时,松手会自己主动回弹成最大比例

这里写图片描写叙述

实现图片的缩放,平移,双击缩放等基本功能的代码例如以下,每一行代码我都做了具体的凝视

public class ZoomImageView extends ImageView implements ScaleGestureDetector.OnScaleGestureListener,
        View.OnTouchListener , ViewTreeObserver.OnGlobalLayoutListener{
    /**
     * 缩放手势的监測
     */
    private ScaleGestureDetector mScaleGestureDetector;
    /**
     * 监听手势
     */
    private GestureDetector mGestureDetector;
    /**
     * 对图片进行缩放平移的Matrix
     */
    private Matrix mScaleMatrix;
    /**
     * 第一次载入图片时调整图片缩放比例。使图片的宽或者高充满屏幕
     */
    private boolean mFirst;
    /**
     * 图片的初始化比例
     */
    private float mInitScale;
    /**
     * 图片的最大比例
     */
    private float mMaxScale;
    /**
     * 双击图片放大的比例
     */
    private float mMidScale;

    /**
     * 是否正在自己主动放大或者缩小
     */
    private boolean isAutoScale;

    //-----------------------------------------------
    /**
     * 上一次触控点的数量
     */
    private int mLastPointerCount;
    /**
     * 能否够拖动
     */
    private boolean isCanDrag;
    /**
     * 上一次滑动的x和y坐标
     */
    private float mLastX;
    private float mLastY;
    /**
     * 可滑动的临界值
     */
    private int mTouchSlop;
    /**
     * 是否用检查左右边界
     */
    private boolean isCheckLeftAndRight;
    /**
     * 是否用检查上下边界
     */
    private boolean isCheckTopAndBottom;


    public ZoomImageView(Context context) {
        this(context, null, 0);
    }

    public ZoomImageView(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public ZoomImageView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        //一定要将图片的ScaleType设置成Matrix类型的
        setScaleType(ScaleType.MATRIX);
        //初始化缩放手势监听器
        mScaleGestureDetector = new ScaleGestureDetector(context,this);
        //初始化矩阵
        mScaleMatrix = new Matrix();
        setOnTouchListener(this);
        mTouchSlop = ViewConfiguration.get(context).getScaledTouchSlop();
        //初始化手势检測器,监听双击事件
        mGestureDetector = new GestureDetector(context,new GestureDetector.SimpleOnGestureListener(){
            @Override
            public boolean onDoubleTap(MotionEvent e) {
                //假设是正在自己主动缩放。则直接返回,不进行处理
                if (isAutoScale) return true;
                //得到点击的坐标
                float x = e.getX();
                float y = e.getY();
                //假设当前图片的缩放值小于指定的双击缩放值
                if (getScale() < mMidScale){
                    //进行自己主动放大
                    post(new AutoScaleRunnable(mMidScale,x,y));
                }else{
                    //当前图片的缩放值大于初试缩放值,则自己主动缩小
                    post(new AutoScaleRunnable(mInitScale,x,y));
                }
                return true;
            }
        });



    }

    /**
     * 当view加入到window时调用,早于onGlobalLayout,因此能够在这里注冊监听器
     */
    @Override
    protected void onAttachedToWindow() {
        super.onAttachedToWindow();
        getViewTreeObserver().addOnGlobalLayoutListener(this);
    }

    /**
     * 当view从window上移除时调用,因此能够在这里移除监听器
     */
    @Override
    protected void onDetachedFromWindow() {
        super.onDetachedFromWindow();
        getViewTreeObserver().removeGlobalOnLayoutListener(this);
    }

    /**
     * 当布局树发生变化时会调用此方法。我们能够在此方法中获得控件的宽和高
     */
    @Override
    public void onGlobalLayout() {
        //仅仅有当第一次载入图片的时候才会进行初始化,用一个变量mFirst控制
        if (!mFirst){
            mFirst = true;
            //得到控件的宽和高
            int width = getWidth();
            int height = getHeight();
            //得到当前ImageView中载入的图片
            Drawable d = getDrawable();
            if(d == null){//假设没有图片,则直接返回
                return;
            }
            //得到当前图片的宽和高,图片的宽和高不一定等于控件的宽和高
            //因此我们须要将图片的宽和高与控件宽和高进行推断
            //将图片完整的显示在屏幕中
            int dw = d.getIntrinsicWidth();
            int dh = d.getIntrinsicHeight();
            //我们定义一个暂时变量,依据图片与控件的宽高比例。来确定这个终于缩放值
            float scale = 1.0f;
            //假设图片宽度大于控件宽度,图片高度小于控件高度
            if (dw>width && dh<height){
                //我们须要将图片宽度缩小。缩小至控件的宽度
                //至于为什么要这样计算,我们能够这样想
                //我们调用matrix.postScale(scale,scale)时,宽和高都要乘以scale的
                //当前我们的图片宽度是dw,dw*scale=dw*(width/dw)=width,这样就等于控件宽度了
                //我们的高度同一时候也乘以scale,这样能够保证图片的宽高比不改变,图片不变形
                scale = width * 1.0f / dw;

            }
            //假设图片的宽度小于控件宽度。图片高度大于控件高度
            if (dw<width && dh>height){
                //我们就应该将图片的高度缩小,缩小至控件的高度,计算方法同上
                scale = height * 1.0f / dh;
            }
            //假设图片的宽度小于控件宽度。高度小于控件高度时。我们应该将图片放大
            //比方图片宽度是控件宽度的1/2 。图片高度是控件高度的1/4
            //假设我们将图片放大4倍,则图片的高度是和控件高度一样了,可是图片宽度就超出控件宽度了
            //因此我们应该选择一个最小值,那就是将图片放大2倍,此时图片宽度等于控件宽度
            //同理,假设图片宽度大于控件宽度。图片高度大于控件高度,我们应该将图片缩小
            //缩小的倍数也应该为那个最小值
            if ((dw < width && dh < height) || (dw > width && dh > height)){
                scale = Math.min(width * 1.0f / dw , height * 1.0f / dh);
            }

            //我们还应该对图片进行平移操作,将图片移动到屏幕的居中位置
            //控件宽度的一半减去图片宽度的一半即为图片须要水平移动的距离
            //高度同理,大家能够画个图看一看
            int dx = width/2 - dw/2;
            int dy = height/2 - dh/2;
            //对图片进行平移。dx和dy分别表示水平和竖直移动的距离
            mScaleMatrix.postTranslate(dx, dy);
            //对图片进行缩放。scale为缩放的比例。后两个參数为缩放的中心点
            mScaleMatrix.postScale(scale, scale, width / 2, height / 2);
            //将矩阵作用于我们的图片上。图片真正得到了平移和缩放
            setImageMatrix(mScaleMatrix);

            //初始化一下我们的几个缩放的边界值
            mInitScale = scale;
            //最大比例为初始比例的4倍
            mMaxScale = mInitScale * 4;
            //双击放大比例为初始化比例的2倍
            mMidScale = mInitScale * 2;
        }
    }

    /**
     * 获得图片当前的缩放比例值
     */
    private float getScale(){
        //Matrix为一个3*3的矩阵。一共9个值
        float[] values = new float[9];
        //将Matrix的9个值映射到values数组中
        mScaleMatrix.getValues(values);
        //拿到Matrix中的MSCALE_X的值,这个值为图片宽度的缩放比例,由于图片高度
        //的缩放比例和宽度的缩放比例一致,我们取一个就能够了
        //我们还能够 return values[Matrix.MSCALE_Y];
        return values[Matrix.MSCALE_X];
    }

    /**
     * 获得缩放后图片的上下左右坐标以及宽高
     */
    private RectF getMatrixRectF(){
        //获得当钱图片的矩阵
        Matrix matrix = mScaleMatrix;
        //创建一个浮点类型的矩形
        RectF rectF = new RectF();
        //得到当前的图片
        Drawable d = getDrawable();
        if (d != null){
            //使这个矩形的宽和高同当前图片一致
            rectF.set(0,0,d.getIntrinsicWidth(),d.getIntrinsicHeight());
            //将矩阵映射到矩形上面,之后我们能够通过获取到矩阵的上下左右坐标以及宽高
            //来得到缩放后图片的上下左右坐标和宽高
            matrix.mapRect(rectF);
        }
        return rectF;
    }

    /**
     * 当缩放时检查边界而且使图片居中
     */
    private void checkBorderAndCenterWhenScale(){
        if (getDrawable() == null){
            return;
        }
        //初始化水平和竖直方向的偏移量
        float deltaX = 0.0f;
        float deltaY = 0.0f;
        //得到控件的宽和高
        int width = getWidth();
        int height = getHeight();
        //拿到当前图片相应的矩阵
        RectF rectF = getMatrixRectF();
        //假设当前图片的宽度大于控件宽度。当前图片处于放大状态
        if (rectF.width() >= width){
            //假设图片左边坐标是大于0的,说明图片左边离控件左边有一定距离,
            //左边会出现一个小白边
            if (rectF.left > 0){
                //我们将图片向左边移动
                deltaX = -rectF.left;
            }
            //假设图片右边坐标小于控件宽度。说明图片右边离控件右边有一定距离。
            //右边会出现一个小白边
            if (rectF.right <width){
                //我们将图片向右边移动
                deltaX = width - rectF.right;
            }
        }
        //上面是调整宽度。这是调整高度
        if (rectF.height() >= height){
            //假设上面出现小白边。则向上移动
            if (rectF.top > 0){
                deltaY = -rectF.top;
            }
            //假设以下出现小白边,则向下移动
            if (rectF.bottom < height){
                deltaY = height - rectF.bottom;
            }
        }
        //假设图片的宽度小于控件的宽度,我们要对图片做一个水平的居中
        if (rectF.width() < width){
            deltaX = width/2f - rectF.right + rectF.width()/2f;
        }

        //假设图片的高度小于控件的高度。我们要对图片做一个竖直方向的居中
        if (rectF.height() < height){
            deltaY = height/2f - rectF.bottom + rectF.height()/2f;
        }
        //将平移的偏移量作用到矩阵上
        mScaleMatrix.postTranslate(deltaX, deltaY);
    }

    /**
     * 平移时检查上下左右边界
     */
    private void checkBorderWhenTranslate() {
        //获得缩放后图片的相应矩形
        RectF rectF = getMatrixRectF();
        //初始化水平和竖直方向的偏移量
        float deltaX = 0.0f;
        float deltaY = 0.0f;
        //得到控件的宽度
        int width = getWidth();
        //得到控件的高度
        int height = getHeight();
        //假设是须要检查左和右边界
        if (isCheckLeftAndRight){
            //假设左边出现的白边
            if (rectF.left > 0){
                //向左偏移
                deltaX = -rectF.left;
            }
            //假设右边出现的白边
            if (rectF.right < width){
                //向右偏移
                deltaX = width - rectF.right;
            }
        }
        //假设是须要检查上和下边界
        if (isCheckTopAndBottom){
            //假设上面出现白边
            if (rectF.top > 0){
                //向上偏移
                deltaY = -rectF.top;
            }
            //假设以下出现白边
            if (rectF.bottom < height){
                //向下偏移
                deltaY = height - rectF.bottom;
            }
        }

        mScaleMatrix.postTranslate(deltaX,deltaY);
    }


    /**
     * 自己主动放大缩小。自己主动缩放的原理是使用View.postDelay()方法,每隔16ms调用一次
     * run方法,给人视觉上形成一种动画的效果
     */
    private class AutoScaleRunnable implements Runnable{
        //放大或者缩小的目标比例
        private float mTargetScale;
        //可能是BIGGER,也可能是SMALLER
        private float tempScale;
        //放大缩小的中心点
        private float x;
        private float y;
        //比1略微大一点,用于放大
        private final float BIGGER = 1.07f;
        //比1略微小一点。用于缩小
        private final float SMALLER = 0.93f;
        //构造方法,将目标比例。缩放中心点传入。而且推断是要放大还是缩小
        public AutoScaleRunnable(float targetScale , float x , float y){
            this.mTargetScale = targetScale;
            this.x = x;
            this.y = y;
            //假设当前缩放比例小于目标比例,说明要自己主动放大
            if (getScale() < mTargetScale){
                //设置为Bigger
                tempScale = BIGGER;
            }
            //假设当前缩放比例大于目标比例。说明要自己主动缩小
            if (getScale() > mTargetScale){
                //设置为Smaller
                tempScale = SMALLER;
            }
        }
        @Override
        public void run() {
            //这里缩放的比例非常小,仅仅是略微比1大一点或者比1小一点的倍数
            //可是当每16ms都放大或者缩小一点点的时候,动画效果就出来了
            mScaleMatrix.postScale(tempScale, tempScale, x, y);
            //每次将矩阵作用到图片之前,都检查一下边界
            checkBorderAndCenterWhenScale();
            //将矩阵作用到图片上
            setImageMatrix(mScaleMatrix);
            //得到当前图片的缩放值
            float currentScale = getScale();
            //假设当前想要放大,而且当前缩放值小于目标缩放值
            //或者  当前想要缩小,而且当前缩放值大于目标缩放值
            if ((tempScale > 1.0f) && currentScale < mTargetScale
                    ||(tempScale < 1.0f) && currentScale > mTargetScale){
                //每隔16ms就调用一次run方法
                postDelayed(this,16);
            }else {
                //current*scale=current*(mTargetScale/currentScale)=mTargetScale
                //保证图片终于的缩放值和目标缩放值一致
                float scale = mTargetScale / currentScale;
                mScaleMatrix.postScale(scale, scale, x, y);
                checkBorderAndCenterWhenScale();
                setImageMatrix(mScaleMatrix);
                //自己主动缩放结束。置为false
                isAutoScale = false;
            }
        }
    }

    /**
     * 这个是OnScaleGestureListener中的方法,在这种方法中我们能够对图片进行放大缩小
     */
    @Override
    public boolean onScale(ScaleGestureDetector detector) {
        //当我们两个手指进行分开操作时,说明我们想要放大,这个scaleFactor是一个略微大于1的数值
        //当我们两个手指进行闭合操作时,说明我们想要缩小。这个scaleFactor是一个略微小于1的数值
        float scaleFactor = detector.getScaleFactor();
        //获得我们图片当前的缩放值
        float scale = getScale();
        //假设当前没有图片,则直接返回
        if (getDrawable() == null){
            return true;
        }
        //假设scaleFactor大于1,说明想放大,当前的缩放比例乘以scaleFactor之后小于
        //最大的缩放比例时,同意放大
        //假设scaleFactor小于1,说明想缩小,当前的缩放比例乘以scaleFactor之后大于
        //最小的缩放比例时。同意缩小
        if ((scaleFactor > 1.0f && scale * scaleFactor < mMaxScale)
                || scaleFactor < 1.0f && scale * scaleFactor > mInitScale){
            //边界控制,假设当前缩放比例乘以scaleFactor之后大于了最大的缩放比例
            if (scale * scaleFactor > mMaxScale + 0.01f){
                //则将scaleFactor设置成mMaxScale/scale
                //当再进行matrix.postScale时
                //scale*scaleFactor=scale*(mMaxScale/scale)=mMaxScale
                //最后图片就会放大至mMaxScale缩放比例的大小
                scaleFactor = mMaxScale / scale;
            }
            //边界控制,假设当前缩放比例乘以scaleFactor之后小于了最小的缩放比例
            //我们不同意再缩小
            if (scale * scaleFactor < mInitScale + 0.01f){
                //计算方法同上
                scaleFactor = mInitScale / scale;

            }
            //前两个參数是缩放的比例,是一个略微大于1或者略微小于1的数,形成一个随着手指放大
            //或者缩小的效果
            //detector.getFocusX()和detector.getFocusY()得到的是多点触控的中点
            //这样就能实现我们在图片的某一处局部放大的效果
            mScaleMatrix.postScale(scaleFactor, scaleFactor, detector.getFocusX(), detector.getFocusY());
            //由于图片的缩放点不是图片的中心点了,所以图片会出现偏移的现象,所以进行一次边界的检查和居中操作
            checkBorderAndCenterWhenScale();
            //将矩阵作用到图片上
            setImageMatrix(mScaleMatrix);
        }
        return true;
    }

    /**
     * 一定要返回true
     */
    @Override
    public boolean onScaleBegin(ScaleGestureDetector detector) {
        return true;
    }

    @Override
    public void onScaleEnd(ScaleGestureDetector detector) {

    }

    @Override
    public boolean onTouch(View v, MotionEvent event) {
        //当双击操作时,不同意移动图片,直接返回true
        if (mGestureDetector.onTouchEvent(event)){
            return true;
        }
        //将事件传递给ScaleGestureDetector
        mScaleGestureDetector.onTouchEvent(event);
        //用于存储多点触控产生的坐标
        float x = 0.0f;
        float y = 0.0f;
        //得到多点触控的个数
        int pointerCount = event.getPointerCount();
        //将全部触控点的坐标累加起来
        for(int i=0 ; i<pointerCount ; i++){
            x += event.getX(i);
            y += event.getY(i);
        }
        //取平均值,得到的就是多点触控后产生的那个点的坐标
        x /= pointerCount;
        y /= pointerCount;
        //假设触控点的数量变了,则置为不可滑动
        if (mLastPointerCount != pointerCount){
            isCanDrag = false;
            mLastX = x;
            mLastY = y;
        }
        mLastPointerCount = pointerCount;
        RectF rectF = getMatrixRectF();
        switch (event.getAction()){
            case MotionEvent.ACTION_DOWN:
                isCanDrag = false;
                //当图片处于放大状态时。禁止ViewPager拦截事件,将事件传递给图片,进行拖动
                if (rectF.width() > getWidth() + 0.01f || rectF.height() > getHeight() + 0.01f){
                    if (getParent() instanceof ViewPager){
                        getParent().requestDisallowInterceptTouchEvent(true);
                    }
                }
                break;
            case MotionEvent.ACTION_MOVE:
                //当图片处于放大状态时,禁止ViewPager拦截事件。将事件传递给图片。进行拖动
                if (rectF.width() > getWidth() + 0.01f || rectF.height() > getHeight() + 0.01f){
                    if (getParent() instanceof ViewPager){
                        getParent().requestDisallowInterceptTouchEvent(true);
                    }
                }
                //得到水平和竖直方向的偏移量
                float dx = x - mLastX;
                float dy = y - mLastY;
                //假设当前是不可滑动的状态。推断一下是否是滑动的操作
                if (!isCanDrag){
                    isCanDrag = isMoveAction(dx,dy);
                }
                //假设可滑动
                if (isCanDrag){
                    if (getDrawable() != null){
                        isCheckLeftAndRight = true;
                        isCheckTopAndBottom = true;
                        //假设图片宽度小于控件宽度
                        if (rectF.width() < getWidth()){
                            //左右不可滑动
                            dx = 0;
                            //左右不可滑动。也就不用检查左右的边界了
                            isCheckLeftAndRight = false;
                        }
                        //假设图片的高度小于控件的高度
                        if (rectF.height() < getHeight()){
                            //上下不可滑动
                            dy = 0;
                            //上下不可滑动,也就不用检查上下边界了
                            isCheckTopAndBottom = false;
                        }
                    }
                    mScaleMatrix.postTranslate(dx,dy);
                    //当平移时。检查上下左右边界
                    checkBorderWhenTranslate();
                    setImageMatrix(mScaleMatrix);
                }
                mLastX = x;
                mLastY = y;
                break;
            case MotionEvent.ACTION_UP:
                //当手指抬起时。将mLastPointerCount置0。停止滑动
                mLastPointerCount = 0;
                break;
            case MotionEvent.ACTION_CANCEL:
                break;
        }
        return true;
    }


    /**
     * 推断是否是移动的操作
     */
    private boolean isMoveAction(float dx , float dy){
        //勾股定理,推断斜边是否大于可滑动的一个临界值
        return Math.sqrt(dx*dx + dy*dy) > mTouchSlop;
    }
}

实现图片缩小后,松手回弹的效果

实现这个功能非常easy。我们先加入一个mMinScale作为可缩小到的最小值。我们指定为初试比例的1/4

 /**
     * 最小缩放比例
     */
    private float mMinScale;

在onGlobalLayout中进行初始化

 @Override
    public void onGlobalLayout() {
    ...
    //最小缩放比例为初试比例的1/4倍
    mMinScale = mInitScale / 4;
    ...
    }

在onScale中,改动例如以下代码

 @Override
    public boolean onScale(ScaleGestureDetector detector) {
    ...
     if ((scaleFactor > 1.0f && scale * scaleFactor < mMaxScale)
                || scaleFactor < 1.0f && scale * scaleFactor > mMinScale){

            //边界控制。假设当前缩放比例乘以scaleFactor之后小于了最小的缩放比例
            //我们不同意再缩小
            if (scale * scaleFactor < mMinScale + 0.01f){

                scaleFactor = mMinScale / scale;
            }
    ...
    }

这样我们的图片最小就能够缩放到初始化比例的1/4大小了,然后我们还须要加入一个松手后回弹至初试化大小的动画效果,然后我们须要在onTouch的ACTION_UP中加入例如以下代码

 @Override
    public boolean onTouch(View v, MotionEvent event) {
    ...
     case MotionEvent.ACTION_UP:
                //当手指抬起时,将mLastPointerCount置0,停止滑动
                mLastPointerCount = 0;
                //假设当前图片大小小于初始化大小
                if (getScale() < mInitScale){
                    //自己主动放大至初始化大小
                    post(new AutoScaleRunnable(mInitScale,getWidth()/2,getHeight()/2));
                }
                break;
    ...
    }

如今我们看一下效果
这里写图片描写叙述

实现图片放大后,松手回弹效果

这个功能实现起来和上面那个功能基本一致,大家能够先试着自己写一下。


同理。我们须要先定义一个mMaxOverScale作为放大到最大值后,还能继续放大到的值。

/**
     * 最大溢出值
     */
    private float mMaxOverScale;

在onGlobalLayout中进行初始化

 @Override
    public void onGlobalLayout() {
    ...
   //最大溢出值为最大值的5倍。能够任意调
   mMaxOverScale = mMaxScale * 5;
    ...
    }

在onScale中。改动例如以下代码

 @Override
    public boolean onScale(ScaleGestureDetector detector) {
    ...
     if ((scaleFactor > 1.0f && scale * scaleFactor < mMaxOverScale)
                || scaleFactor < 1.0f && scale * scaleFactor > mMinScale){

            if (scale * scaleFactor > mMaxOverScale + 0.01f){

                scaleFactor = mMaxOverScale / scale;
            }
    ...
    }

这样当我们图片放大至最大比例后还能够继续放大。然后我们相同须要在onTouch中的ACTION_UP中加入自己主动缩小的功能

  case MotionEvent.ACTION_UP:
                //当手指抬起时,将mLastPointerCount置0,停止滑动
                mLastPointerCount = 0;
                //假设当前图片大小小于初始化大小
                if (getScale() < mInitScale){
                    //自己主动放大至初始化大小
                    post(new AutoScaleRunnable(mInitScale,getWidth()/2,getHeight()/2));
                }
                //假设当前图片大小大于最大值
                if (getScale() > mMaxScale){
                    //自己主动缩小至最大值
                    post(new AutoScaleRunnable(mMaxScale,getWidth()/2,getHeight()/2));
                }
                break;

然后我们看一下效果
这里写图片描写叙述

实现图片的惯性滑动

要实现图片的惯性滑动,我们须要借助VelocityTracker来帮我们检測当我们手指离开图片时的一个速度,然后依据这个速度以及图片的位置来调用Scroller的fling方法来计算惯性滑动过程中的x和y的坐标

 @Override
    public boolean onTouch(View v, MotionEvent event) {
    ...
     switch (event.getAction()){
            case MotionEvent.ACTION_DOWN:
                //初始化速度检測器
                mVelocityTracker = VelocityTracker.obtain();
                if (mVelocityTracker != null){
                    //将当前的事件加入到检測器中
                    mVelocityTracker.addMovement(event);
                }
                //当手指再次点击到图片时,停止图片的惯性滑动
                if (mFlingRunnable != null){
                    mFlingRunnable.cancelFling();
                    mFlingRunnable = null;
                }
                ...
     }
     ...
      case MotionEvent.ACTION_MOVE:
      ...
       //假设可滑动
                if (isCanDrag){
                    if (getDrawable() != null){

                        if (mVelocityTracker != null){
                            //将当前事件加入到检測器中
                            mVelocityTracker.addMovement(event);
                        }
                        ...
                }
                ...
     case MotionEvent.ACTION_UP:
                //当手指抬起时。将mLastPointerCount置0。停止滑动
                mLastPointerCount = 0;
                //假设当前图片大小小于初始化大小
                if (getScale() < mInitScale){
                    //自己主动放大至初始化大小
                    post(new AutoScaleRunnable(mInitScale,getWidth()/2,getHeight()/2));
                }
                //假设当前图片大小大于最大值
                if (getScale() > mMaxScale){
                    //自己主动缩小至最大值
                    post(new AutoScaleRunnable(mMaxScale,getWidth()/2,getHeight()/2));
                }
                if (isCanDrag){//假设当前能够滑动
                    if (mVelocityTracker != null){
                        //将当前事件加入到检測器中
                        mVelocityTracker.addMovement(event);
                        //计算当前的速度
                        mVelocityTracker.computeCurrentVelocity(1000);
                        //得到当前x方向速度
                        final float vX = mVelocityTracker.getXVelocity();
                        //得到当前y方向的速度
                        final float vY = mVelocityTracker.getYVelocity();
                        mFlingRunnable = new FlingRunnable(getContext());
                        //调用fling方法。传入控件宽高和当前x和y轴方向的速度
                        //这里得到的vX和vY和scroller须要的velocityX和velocityY的负号正好相反
                        //所以传入一个负值
                        mFlingRunnable.fling(getWidth(),getHeight(),(int)-vX,(int)-vY);
                        //运行run方法
                        post(mFlingRunnable);
                    }
                }
                break;
   case MotionEvent.ACTION_CANCEL:
                //释放速度检測器
                if (mVelocityTracker != null){
                    mVelocityTracker.recycle();
                    mVelocityTracker = null;
                }
                break;
/**
     * 惯性滑动
     */
    private class FlingRunnable implements Runnable{
        private Scroller mScroller;
        private int mCurrentX , mCurrentY;

        public FlingRunnable(Context context){
            mScroller = new Scroller(context);
        }

        public void cancelFling(){
            mScroller.forceFinished(true);
        }

        /**
         * 这种方法主要是从onTouch中或得到当前滑动的水平和竖直方向的速度
         * 调用scroller.fling方法。这种方法内部能够自己主动计算惯性滑动
         * 的x和y的变化率。依据这个变化率我们就能够对图片进行平移了
         */
        public void fling(int viewWidth , int viewHeight , int velocityX ,
                          int velocityY){
            RectF rectF = getMatrixRectF();
            if (rectF == null){
                return;
            }
            //startX为当前图片左边界的x坐标
            final int startX = Math.round(-rectF.left);
            final int minX , maxX , minY , maxY;
            //假设图片宽度大于控件宽度
            if (rectF.width() > viewWidth){
                //这是一个滑动范围[minX,maxX]。详情见下图
                minX = 0;
                maxX = Math.round(rectF.width() - viewWidth);
            }else{
                //假设图片宽度小于控件宽度,则不同意滑动
                minX = maxX = startX;
            }
            //假设图片高度大于控件高度,同理
            final int startY = Math.round(-rectF.top);
            if (rectF.height() > viewHeight){
                minY = 0;
                maxY = Math.round(rectF.height() - viewHeight);
            }else{
                minY = maxY = startY;
            }
            mCurrentX = startX;
            mCurrentY = startY;

            if (startX != maxX || startY != maxY){
                //调用fling方法,然后我们能够通过调用getCurX和getCurY来获得当前的x和y坐标
                //这个坐标的计算是模拟一个惯性滑动来计算出来的,我们依据这个x和y的变化能够模拟
                //出图片的惯性滑动
                mScroller.fling(startX,startY,velocityX,velocityY,minX,maxX,minY,maxY);
            }

        }

关于startX,minX,maxX做一个解释
这里写图片描写叙述
我们从图中能够看出。当前图片可滑动的一个区间就是左边多出来的那块区间,所以minX和maxX代表的是区间的最小值和最大值,startX就是屏幕左边界的坐标值,我们能够想象成是startX在区间[minX,maxX]的移动。Y轴方向同理。

如今我们看一下效果
这里写图片描写叙述

完整代码

完整代码大家能够上我的GitHub下载

原文地址:https://www.cnblogs.com/cxchanpin/p/7306644.html