美文网首页
Android从零开始实现可以双指缩放,自由移动,双击放大的自定

Android从零开始实现可以双指缩放,自由移动,双击放大的自定

作者: Destroyer716 | 来源:发表于2020-03-29 22:57 被阅读0次

    Android从零开始实现可以双指缩放,自由移动,双击放大的自定义ImageView

    之前一直都是用别人的轮子,总感觉心里不踏实,所以自己也尝试这造一些别人造过的轮子,提升自己,也可以在平时开发中工作中更熟练的使用。

    这一次从零开始,撸一个可单指移动,双击放大,双指缩放,双指移动的ImageView

    这篇博文较长,如果向直接看完整代码,请直接拉到最后。

    我们先来看看效果吧


    20200326_221007.gif

    第一步:继承ImageView,并让图片能等比缩放后居中显示

    
        public class ZoomImageView extends ImageView {
        
        
            private Matrix matrix;
        
            public ZoomImageView(Context context) {
                super(context);
                init();
            }
        
            public ZoomImageView(Context context, @Nullable AttributeSet attrs) {
                super(context, attrs);
                init();
            }
        
            public ZoomImageView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
                super(context, attrs, defStyleAttr);
                init();
            }
        
            private void init(){
                setScaleType(ScaleType.MATRIX);
                matrix = new Matrix();
            }
        }
    
    

    布局时这样的

    <?xml version="1.0" encoding="utf-8"?>
    <androidx.constraintlayout.widget.ConstraintLayout 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.zoomimagetest.view.ZoomImageView
            android:id="@+id/my_image_view"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintLeft_toLeftOf="parent"
            app:layout_constraintRight_toRightOf="parent"
            app:layout_constraintTop_toTopOf="parent"
            android:src="@drawable/test5"/>
    
    </androidx.constraintlayout.widget.ConstraintLayout>
    

    运行后是这样的


    image

    图片没有显示全,那么接下来就让图片默认等比缩放然后居中显示

    如果要实现等比缩放,需要知道ImageView 的宽高,以及图片的原始宽高,ImageView 的宽高这些信息可以再onMeasure方法中获取,而图片的宽高可以通过Drawable这个类拿到,下面代码中,获取图片的宽高时还有一个坑,这个后面再说。

        //imageView的大小
        private PointF viewSize;
        //图片的大小
        private PointF imageSize;
    
        @Override
        protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
            super.onMeasure(widthMeasureSpec, heightMeasureSpec);
            int width = MeasureSpec.getSize(widthMeasureSpec);
            int height = MeasureSpec.getSize(heightMeasureSpec);
            viewSize = new PointF(width,height);
            
            Drawable drawable = getDrawable();
            imageSize = new PointF(drawable.getMinimumWidth(),drawable.getMinimumHeight());
        }
    

    然后我们ImageView的宽与图片的宽的比例,以及ImageView的高与图片的高的比例,对比宽和高的缩放比例,取较小的那个做为图片的缩放比例。

    这里解释下为什么要用较小的那个做为图片的缩放比例,因为我们需要图片等比缩放,并且显示全,这里在同一部手机上,相同的这个自定义ZoomImageView中,可以认为viewSize的x,y是固定的,也就是在下面的式子中,分子是固定的,所以,如果整个值就越小,说明分母就越大,也就说明图片的这条边与这个view的这条边的差距越大,所以按照相差较大的这条边的缩放比例来缩放,肯定能保证在view中显示全。

        float scalex = viewSize.x/imageSize.x;
        float scaley = viewSize.y/imageSize.y;
        float scale = scalex<scaley?scalex:scaley;
    

    得到我们的缩放比例后就可以对图片进行缩放了

        //缩放后图片的大小
        private PointF scaleSize = new PointF();
        public void scaleImage(PointF scaleXY){
            matrix.setScale(scaleXY.x,scaleXY.y);
            //这里将缩放后的图片大小保存一下
            scaleSize.set(scaleXY.x * imageSize.x,scaleXY.y * imageSize.y);
            setImageMatrix(matrix);
        }
    

    到这一步的完整代码如下

    @SuppressLint("AppCompatCustomView")
    public class ZoomImageView extends ImageView {
    
    
        private Matrix matrix;
        //imageView的大小
        private PointF viewSize;
        //图片的大小
        private PointF imageSize;
        //缩放后图片的大小
        private PointF scaleSize = new PointF();
        //最初的宽高的缩放比例
        private PointF originScale = new PointF();
    
    
        public ZoomImageView(Context context) {
            super(context);
            init();
        }
    
        public ZoomImageView(Context context, @Nullable AttributeSet attrs) {
            super(context, attrs);
            init();
        }
    
        public ZoomImageView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
            super(context, attrs, defStyleAttr);
            init();
        }
    
        private void init(){
            setScaleType(ScaleType.MATRIX);
            matrix = new Matrix();
        }
    
    
        @Override
        protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
            super.onMeasure(widthMeasureSpec, heightMeasureSpec);
            int width = MeasureSpec.getSize(widthMeasureSpec);
            int height = MeasureSpec.getSize(heightMeasureSpec);
            viewSize = new PointF(width,height);
    
            Drawable drawable = getDrawable();
            imageSize = new PointF(drawable.getMinimumWidth(),drawable.getMinimumHeight());
    
            showCenter();
        }
    
        /**
         * 设置图片居中等比显示
         */
        private void showCenter(){
            float scalex = viewSize.x/imageSize.x;
            float scaley = viewSize.y/imageSize.y;
    
            float scale = scalex<scaley?scalex:scaley;
            scaleImage(new PointF(scale,scale));
        }
    
    
        /**
         * 将图片按照比例缩放,这里宽高缩放比例相等,所以PoinF 里面的x,y是一样的
         * @param scaleXY
         */
        public void scaleImage(PointF scaleXY){
            matrix.setScale(scaleXY.x,scaleXY.y);
            scaleSize.set(scaleXY.x * imageSize.x,scaleXY.y * imageSize.y);
            setImageMatrix(matrix);
        }
    

    运行效果如下

    image

    到这里,图片是显示全了,但是并没有居中显示,那么接下来就来处理图片居中显示的问题。

        /**
         * 对图片进行x和y轴方向的平移
         * @param pointF
         */
        public void translationImage(PointF pointF){
            matrix.postTranslate(pointF.x,pointF.y);
            setImageMatrix(matrix);
        }
    

    上面是对图片进行平移的方法

    接下来需要计算,如果要图片在ImageView中居中显示,需要的平移量

        if (scalex<scaley){
             translationImage(new PointF(0,viewSize.y/2 - scaleSize.y/2));
         }else {
             translationImage(new PointF(viewSize.x/2 - scaleSize.x/2,0));
         }
    

    前面讲过,两个方向的缩放值越小,说明那个方向的view的值与图片的值差距就越大,按照这个较小缩放值,缩放后,这个方向就正好充满view,那么此方向就不需要平移,平移另外一个方向即可。这里如果没有理解的,可以自己在纸上画个图。

    那么到这里,第一步就等比缩放,居中显示就完成了。此时的全部代码如下

    @SuppressLint("AppCompatCustomView")
    public class ZoomImageView extends ImageView {
    
    
        private Matrix matrix;
        //imageView的大小
        private PointF viewSize;
        //图片的大小
        private PointF imageSize;
        //缩放后图片的大小
        private PointF scaleSize = new PointF();
        //最初的宽高的缩放比例
        private PointF originScale = new PointF();
        //imageview中bitmap的xy实时坐标
        private PointF bitmapOriginPoint = new PointF();
    
    
        public ZoomImageView(Context context) {
            super(context);
            init();
        }
    
        public ZoomImageView(Context context, @Nullable AttributeSet attrs) {
            super(context, attrs);
            init();
        }
    
        public ZoomImageView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
            super(context, attrs, defStyleAttr);
            init();
        }
    
        private void init(){
            setScaleType(ScaleType.MATRIX);
            matrix = new Matrix();
        }
    
    
        @Override
        protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
            super.onMeasure(widthMeasureSpec, heightMeasureSpec);
            int width = MeasureSpec.getSize(widthMeasureSpec);
            int height = MeasureSpec.getSize(heightMeasureSpec);
            viewSize = new PointF(width,height);
    
            Drawable drawable = getDrawable();
            imageSize = new PointF(drawable.getMinimumWidth(),drawable.getMinimumHeight());
    
            showCenter();
        }
    
        /**
         * 设置图片居中等比显示
         */
        private void showCenter(){
            float scalex = viewSize.x/imageSize.x;
            float scaley = viewSize.y/imageSize.y;
    
            float scale = scalex<scaley?scalex:scaley;
            scaleImage(new PointF(scale,scale));
    
            //移动图片,并保存最初的图片左上角(即原点)所在坐标
            if (scalex<scaley){
                translationImage(new PointF(0,viewSize.y/2 - scaleSize.y/2));
                bitmapOriginPoint.x = 0;
                bitmapOriginPoint.y = viewSize.y/2 - scaleSize.y/2;
            }else {
                translationImage(new PointF(viewSize.x/2 - scaleSize.x/2,0));
                bitmapOriginPoint.x = viewSize.x/2 - scaleSize.x/2;
                bitmapOriginPoint.y = 0;
            }
            //保存下最初的缩放比例
            originScale.set(scale,scale);
        }
    
    
    
        public void scaleImage(PointF scaleXY){
            matrix.setScale(scaleXY.x,scaleXY.y);
            scaleSize.set(scaleXY.x * imageSize.x,scaleXY.y * imageSize.y);
            setImageMatrix(matrix);
        }
    
        /**
         * 对图片进行x和y轴方向的平移
         * @param pointF
         */
        public void translationImage(PointF pointF){
            matrix.postTranslate(pointF.x,pointF.y);
            setImageMatrix(matrix);
        }
    

    效果图如下

    image

    第二步:双击缩放

    具体需要实现的功能是,双击图片放大一倍,在放大的时候再双击,就回到最初的大小,并且时点哪里放大哪里,即放大后,双击的点,位置坐标不变。

    那么接下来就会要实现OnTouchListener 实现onTouch方法,并做一些事件的判断了

    @Override
        public boolean onTouch(View v, MotionEvent event) {
    
            switch (event.getAction() & MotionEvent.ACTION_MASK) {
                case MotionEvent.ACTION_DOWN:
                    //手指按下事件
                    break;
                case MotionEvent.ACTION_POINTER_DOWN:
                    //屏幕上已经有一个点被按住了 第二个点被按下时触发该事件
    
                    break;
                case MotionEvent.ACTION_POINTER_UP:
                    //屏幕上已经有两个点按住 再松开一个点时触发该事件
    
                    break;
                case MotionEvent.ACTION_MOVE:
                    //手指移动时触发事件
    
                    break;
                case MotionEvent.ACTION_UP:
                    //手指松开时触发事件
    
                    break;
            }
            return true;
        }
    

    在正式处理事件之前,我们先定义几个变量,并定义双击能放大一倍还有缩放的三个状态

        //缩放的三个状态
        public class ZoomMode{
            public  final  static  int Ordinary=0;//普通
            public  final  static  int  ZoomIn=1;//双击放大
            public final static int TowFingerZoom = 2;//双指缩放
        }
    
    
        //点击的点
        private PointF clickPoint = new PointF();
        //设置的双击检查时间间隔
        private long doubleClickTimeSpan = 250;
        //上次点击的时间
        private long lastClickTime = 0;
        //双击放大的倍数
        private int doubleClickZoom = 2;
        //当前缩放的模式
        private int zoomInMode = ZoomMode.Ordinary;
        //临时坐标比例数据
        private PointF tempPoint = new PointF();
    

    下面是屏幕被按下事件的处理

        case MotionEvent.ACTION_DOWN:
            //手指按下事件
            //记录被点击的点的坐标
            clickPoint.set(event.getX(),event.getY());
            //判断屏幕上此时被按住的点的个数,当前屏幕只有一个点被点击的时候触发
            if (event.getPointerCount() == 1) {
                //设置一个点击的间隔时长,来判断是不是双击
                if (System.currentTimeMillis() - lastClickTime <= doubleClickTimeSpan) {
                    //如果图片此时缩放模式是普通模式,就触发双击放大
                    if (zoomInMode == ZoomMode.Ordinary) {
                        //分别记录被点击的点到图片左上角x,y轴的距离与图片x,y轴边长的比例,方便在进行缩放后,算出这                  个点对应的坐标点
                        tempPoint.set((clickPoint.x - bitmapOriginPoint.x) / scaleSize.x, (clickPoint.y -                   bitmapOriginPoint.y) / scaleSize.y);
                        //进行缩放
                        scaleImage(new PointF(originScale.x * doubleClickZoom, originScale.y *                              doubleClickZoom));
                        //获取缩放后,图片左上角的xy坐标
                        getBitmapOffset();
                        //平移图片,使得被点击的点的位置不变。这里是计算缩放后被点击的xy坐标,与原始点击的位置的xy                  坐标值,计算出差值,然后做平移动作
                        translationImage(new PointF(clickPoint.x - (bitmapOriginPoint.x + tempPoint.x *                    scaleSize.x), clickPoint.y - (bitmapOriginPoint.y + tempPoint.y * scaleSize.y)));
                        zoomInMode = ZoomMode.ZoomIn;
                    } else {
                        //双击还原
                        showCenter();
                        zoomInMode = ZoomMode.Ordinary;
                    }
                } else {
                    lastClickTime = System.currentTimeMillis();
                }
            }
            break;
    
       /**
         * 获取view中bitmap的坐标点
         */
        public void getBitmapOffset(){
            float[] value = new float[9];
            float[] offset = new float[2];
            Matrix imageMatrix = getImageMatrix();
            imageMatrix.getValues(value);
            offset[0] = value[2];
            offset[1] = value[5];
            bitmapOriginPoint.set(offset[0],offset[1]);
        }
    

    运行效果如下,也就实现了双击哪里放大哪里的效果

    image

    第三步:双指缩放

    双指缩放其实和双击放大的原理是一样的,只是由一个点的操作,变成两个点的操作,并且缩放的倍数也是任意变化的。

    先来看第一个问题,如果将两个手指的缩放操作向一个点的操作转变。这个其实很简单,我们只需要算出两个手指的之间的中点,来当作实际缩放的那个点,就像双击放大的那个点一样。

    再来看第二个问题,缩放的倍数。这个也简单,只需要获取两个手指之间最开始是相聚多少,然后监测两个手指间变化后的距离占最开始的距离的百分比就可以了,都不需要单独判断是缩小还是放大。

    下面看实际代码

        //最大缩放比例
        private float maxScrole = 4;
        //两点之间的距离
        private float doublePointDistance = 0;
        //双指缩放时候的中心点
        private PointF doublePointCenter = new PointF();
        //两指缩放的比例
        private float doubleFingerScrole = 0;
    
        @Override
        public boolean onTouch(View v, MotionEvent event) {
            switch (event.getAction() & MotionEvent.ACTION_MASK) {
                case MotionEvent.ACTION_DOWN:
                    //手指按下事件
                    //这里省略上面实现的双击放大的代码
                    ...
                        
                        
                    //并在双击放大后记录缩放比例
                    doubleFingerScrole = originScale.x*doubleClickZoom;
                    break;
                case MotionEvent.ACTION_POINTER_DOWN:
                    //屏幕上已经有一个点按住 再按下一点时触发该事件
                    //计算最初的两个手指之间的距离
                    doublePointDistance = getDoubleFingerDistance(event);
                    break;
                case MotionEvent.ACTION_POINTER_UP:
                    //屏幕上已经有两个点按住 再松开一点时触发该事件
                    //当有一个手指离开屏幕后,就修改状态,这样如果双击屏幕就能恢复到初始大小
                    zoomInMode = ZoomMode.ZoomIn;
                    //记录此时的双指缩放比例
                    doubleFingerScrole =scaleSize.x/imageSize.x;
                    break;
                case MotionEvent.ACTION_MOVE:
                    //手指移动时触发事件
                    /**************************************缩放                                                         *******************************************/
                    //判断当前是两个手指接触到屏幕才处理缩放事件
                    if (event.getPointerCount() == 2){
                        //如果此时缩放后的大小,大于等于了设置的最大缩放的大小,就不处理
                        if ((scaleSize.x/imageSize.x >= originScale.x * maxScrole 
                             || scaleSize.y/imageSize.y >= originScale.y * maxScrole) 
                            && getDoubleFingerDistance(event) - doublePointDistance > 0){
                            break;
                        }
                        //这里设置当双指缩放的的距离变化量大于50,并且当前不是在双指缩放状态下,
                        //就计算中心点,等一些操作
                        if (Math.abs(getDoubleFingerDistance(event) - doublePointDistance) > 50 
                            && zoomInMode != ZoomMode.TowFingerZoom){
                            //计算两个手指之间的中心点,当作放大的中心点
                            doublePointCenter.set((event.getX(0) + event.getX(1))/2,
                                                  (event.getY(0) + event.getY(1))/2);
                            /将双指的中心点就假设为点击的点
                            clickPoint.set(doublePointCenter);
                            //下面就和双击放大基本一样
                            getBitmapOffset();
                            //分别记录被点击的点到图片左上角x,y轴的距离与图片x,y轴边长的比例,
                            //方便在进行缩放后,算出这个点对应的坐标点
                            tempPoint.set((clickPoint.x - bitmapOriginPoint.x)/scaleSize.x,
                                          (clickPoint.y - bitmapOriginPoint.y)/scaleSize.y);
                            //设置进入双指缩放状态
                            zoomInMode = ZoomMode.TowFingerZoom;
                        }
                        //如果已经进入双指缩放状态,就直接计算缩放的比例,并进行位移
                        if (zoomInMode == ZoomMode.TowFingerZoom){
                            //用当前的缩放比例与此时双指间距离的缩放比例相乘,就得到对应的图片应该缩放的比例
                            float scrole = 
                                doubleFingerScrole*getDoubleFingerDistance(event)/doublePointDistance;
                            //这里也是和双击放大时一样的
                            scaleImage(new PointF(scrole,scrole));
                            getBitmapOffset();
                                  +bitmapOriginPoint);
                            translationImage(
                               new PointF(clickPoint.x - (bitmapOriginPoint.x + tempPoint.x*scaleSize.x),
                                         clickPoint.y - (bitmapOriginPoint.y + tempPoint.y*scaleSize.y))
                            );
                        }
                    }
                    break;
                case MotionEvent.ACTION_UP:
                    //手指松开时触发事件
                    Log.e("kzg","***********************ACTION_UP");
                    break;
            }
            return true;
        }
    
    
        /**
         * 计算零个手指间的距离
         * @param event
         * @return
         */
        public static float  getDoubleFingerDistance(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) ;
        }
    

    此外还需要再showCenter 方法中将最初的缩放比例赋值给doubleFingerScrole

    doubleFingerScrole = scale;
    

    好啦,来看看效果吧

    image

    第四步:单指或者双指移动图片

    这里也时比较简单的,单指移动图片就不用多说,双指移动就算出两指之间的中点,后面就和单指移动一样了

        //上次触碰的手指数量
        private int lastFingerNum = 0;
    
        @Override
        public boolean onTouch(View v, MotionEvent event) {
            switch (event.getAction() & MotionEvent.ACTION_MASK) {
                case MotionEvent.ACTION_DOWN:
                    //手指按下事件
                    //这里省略上面实现的双击放大的代码
                    ...
                    break;
                case MotionEvent.ACTION_POINTER_DOWN:
                    //屏幕上已经有一个点按住 再按下一点时触发该事件
                    //计算最初的两个手指之间的距离
                    doublePointDistance = getDoubleFingerDistance(event);
                    break;
                case MotionEvent.ACTION_POINTER_UP:
                    //屏幕上已经有两个点按住 再松开一点时触发该事件
                    ...
                    //记录此时屏幕触碰的点的数量
                    lastFingerNum = 1;
                    break;
                case MotionEvent.ACTION_MOVE:
                    //手指移动时触发事件
                    /**************************************移动
                    *******************************************/
                    if (zoomInMode != ZoomMode.Ordinary) {
                        //如果是多指,计算中心点为假设的点击的点
                        float currentX = 0;
                        float currentY = 0;
                        //获取此时屏幕上被触碰的点有多少个
                        int pointCount = event.getPointerCount();
                      //计算出中间点所在的坐标
                        for (int i = 0; i < pointCount; i++) {
                            currentX += event.getX(i);
                            currentY += event.getY(i);
                        }
                        currentX /= pointCount;
                        currentY /= pointCount;
                        //当屏幕被触碰的点的数量变化时,将最新算出来的中心点看作是被点击的点
                        if (lastFingerNum != event.getPointerCount()) {
                            clickPoint.x = currentX;
                            clickPoint.y = currentY;
                            lastFingerNum = event.getPointerCount();
                        }
                        //将移动手指时,实时计算出来的中心点坐标,减去被点击点的坐标就得到了需要移动的距离
                        float moveX = currentX - clickPoint.x;
                        float moveY = currentY - clickPoint.y;
                      //计算边界,使得不能已出边界,但是如果是双指缩放时移动,因为存在缩放效果,
                        //所以此时的边界判断无效
                        float[] moveFloat = moveBorderDistance(moveX, moveY);
                        //处理移动图片的事件
                        translationImage(new PointF(moveFloat[0], moveFloat[1]));
                        clickPoint.set(currentX, currentY);
                    }
                    
                    /**************************************缩放                                                         *******************************************/
                    //下面省略之前的缩放的代码
                    ...
                    break;
                case MotionEvent.ACTION_UP:
                    //手指松开时触发事件
                    lastFingerNum = 0;
                    break;
            }
            return true;
        }
    
        /**
         * 防止移动图片超过边界,计算边界情况
         * @param moveX
         * @param moveY
         * @return
         */
        public float[] moveBorderDistance(float moveX,float moveY){
            //计算bitmap的左上角坐标
            getBitmapOffset();
            //计算bitmap的右下角坐标
            float bitmapRightBottomX = bitmapOriginPoint.x + scaleSize.x;
            float bitmapRightBottomY = bitmapOriginPoint.y + scaleSize.y;
    
            if (moveY > 0){
                //向下滑
                if (bitmapOriginPoint.y + moveY > 0){
                    if (bitmapOriginPoint.y < 0){
                        moveY = -bitmapOriginPoint.y;
                    }else {
                        moveY = 0;
                    }
                }
            }else if (moveY < 0){
                //向上滑
                if (bitmapRightBottomY + moveY < viewSize.y){
                    if (bitmapRightBottomY > viewSize.y){
                        moveY = -(bitmapRightBottomY - viewSize.y);
                    }else {
                        moveY = 0;
                    }
                }
            }
    
            if (moveX > 0){
                //向右滑
                if (bitmapOriginPoint.x + moveX > 0){
                    if (bitmapOriginPoint.x < 0){
                        moveX = -bitmapOriginPoint.x;
                    }else {
                        moveX = 0;
                    }
                }
            }else if (moveX < 0){
                //向左滑
                if (bitmapRightBottomX + moveX < viewSize.x){
                    if (bitmapRightBottomX > viewSize.x){
                        moveX = -(bitmapRightBottomX - viewSize.x);
                    }else {
                        moveX = 0;
                    }
                }
            }
            return new float[]{moveX,moveY};
        }
    

    再来看看效果吧

    image

    到这里还差最后一个小功能,当图片缩放的比例比最开始的比例要小时,让其自动恢复到普通的缩放比例

    代码很简单,在onTouch方法的MotionEvent.ACTION_POINTER_UP分支中添加

        //判断缩放后的比例,如果小于最初的那个比例,就恢复到最初的大小
        if (scaleSize.x<viewSize.x && scaleSize.y<viewSize.y){
            zoomInMode = ZoomMode.Ordinary;
            showCenter();
        }
    

    到这里功能都已经完成了,但是还记得我一开始说的一个坑吗?那就是在onMeasure方法中调用 getDrawable() 来获取图片的Drawable对象,这里一不小心就会获取到空的Drawable,我们这里没有问题时因为我们直接在布局中就将要显示的图片赋值了,而实际开发中很有可能是代码运行后动态获取的,这个时候这个自定义的View一加载就会报错了。

    这里解决起来也很简单,加一个判空就可以了,如果不为空才执行后面的操作。我就不单独贴代码了,直接将全部代码贴出来

    @SuppressLint("AppCompatCustomView")
    public class ZoomImageView extends ImageView implements View.OnTouchListener {
    
        public class ZoomMode{
            public  final  static  int Ordinary=0;
            public  final  static  int  ZoomIn=1;
            public final static int TowFingerZoom = 2;
        }
    
    
    
        private Matrix matrix;
        //imageView的大小
        private PointF viewSize;
        //图片的大小
        private PointF imageSize;
        //缩放后图片的大小
        private PointF scaleSize = new PointF();
        //最初的宽高的缩放比例
        private PointF originScale = new PointF();
        //imageview中bitmap的xy实时坐标
        private PointF bitmapOriginPoint = new PointF();
        //点击的点
        private PointF clickPoint = new PointF();
        //设置的双击检查时间限制
        private long doubleClickTimeSpan = 250;
        //上次点击的时间
        private long lastClickTime = 0;
        //双击放大的倍数
        private int doubleClickZoom = 2;
        //当前缩放的模式
        private int zoomInMode = ZoomMode.Ordinary;
        //临时坐标比例数据
        private PointF tempPoint = new PointF();
        //最大缩放比例
        private float maxScrole = 4;
        //两点之间的距离
        private float doublePointDistance = 0;
        //双指缩放时候的中心点
        private PointF doublePointCenter = new PointF();
        //两指缩放的比例
        private float doubleFingerScrole = 0;
        //上次触碰的手指数量
        private int lastFingerNum = 0;
    
    
        public ZoomImageView(Context context) {
            super(context);
            init();
        }
    
        public ZoomImageView(Context context, @Nullable AttributeSet attrs) {
            super(context, attrs);
            init();
        }
    
        public ZoomImageView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
            super(context, attrs, defStyleAttr);
            init();
        }
    
        private void init(){
            setOnTouchListener(this);
            setScaleType(ScaleType.MATRIX);
            matrix = new Matrix();
        }
    
    
        @Override
        protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
            super.onMeasure(widthMeasureSpec, heightMeasureSpec);
            int width = MeasureSpec.getSize(widthMeasureSpec);
            int height = MeasureSpec.getSize(heightMeasureSpec);
            viewSize = new PointF(width,height);
    
            Drawable drawable = getDrawable();
            if (drawable != null){
                imageSize = new PointF(drawable.getMinimumWidth(),drawable.getMinimumHeight());
                showCenter();
            }
        }
    
        /**
         * 设置图片居中等比显示
         */
        private void showCenter(){
            float scalex = viewSize.x/imageSize.x;
            float scaley = viewSize.y/imageSize.y;
    
            float scale = scalex<scaley?scalex:scaley;
            scaleImage(new PointF(scale,scale));
    
            //移动图片,并保存最初的图片左上角(即原点)所在坐标
            if (scalex<scaley){
                translationImage(new PointF(0,viewSize.y/2 - scaleSize.y/2));
                bitmapOriginPoint.x = 0;
                bitmapOriginPoint.y = viewSize.y/2 - scaleSize.y/2;
            }else {
                translationImage(new PointF(viewSize.x/2 - scaleSize.x/2,0));
                bitmapOriginPoint.x = viewSize.x/2 - scaleSize.x/2;
                bitmapOriginPoint.y = 0;
            }
            //保存下最初的缩放比例
            originScale.set(scale,scale);
            doubleFingerScrole = scale;
        }
    
    
    
        @Override
        public boolean onTouch(View v, MotionEvent event) {
            switch (event.getAction() & MotionEvent.ACTION_MASK) {
                case MotionEvent.ACTION_DOWN:
                    //手指按下事件
                    //记录被点击的点的坐标
                    clickPoint.set(event.getX(),event.getY());
                    //判断屏幕上此时被按住的点的个数,当前屏幕只有一个点被点击的时候触发
                    if (event.getPointerCount() == 1) {
                        //设置一个点击的间隔时长,来判断是不是双击
                        if (System.currentTimeMillis() - lastClickTime <= doubleClickTimeSpan) {
                            //如果图片此时缩放模式是普通模式,就触发双击放大
                            if (zoomInMode == ZoomMode.Ordinary) {
                                //分别记录被点击的点到图片左上角x,y轴的距离与图片x,y轴边长的比例,
                                //方便在进行缩放后,算出这个点对应的坐标点
                                tempPoint.set((clickPoint.x - bitmapOriginPoint.x) / scaleSize.x, 
                                              (clickPoint.y - bitmapOriginPoint.y) / scaleSize.y);
                                //进行缩放
                                scaleImage(new PointF(originScale.x * doubleClickZoom, 
                                                      originScale.y * doubleClickZoom));
                                //获取缩放后,图片左上角的xy坐标
                                getBitmapOffset();
                               
                                //平移图片,使得被点击的点的位置不变。这里是计算缩放后被点击的xy坐标,
                                //与原始点击的位置的xy坐标值,计算出差值,然后做平移动作
                                translationImage(
                                    new PointF(
                                        clickPoint.x - (bitmapOriginPoint.x + tempPoint.x * scaleSize.x),
                                        clickPoint.y - (bitmapOriginPoint.y + tempPoint.y * scaleSize.y))
                                );
                                zoomInMode = ZoomMode.ZoomIn;
                                doubleFingerScrole = originScale.x*doubleClickZoom;
                            } else {
                                //双击还原
                                showCenter();
                                zoomInMode = ZoomMode.Ordinary;
                            }
                        } else {
                            lastClickTime = System.currentTimeMillis();
                        }
                    }
                    break;
                case MotionEvent.ACTION_POINTER_DOWN:
                    //屏幕上已经有一个点按住 再按下一点时触发该事件
                    //计算最初的两个手指之间的距离
                    doublePointDistance = getDoubleFingerDistance(event);
                    break;
                case MotionEvent.ACTION_POINTER_UP:
                    //屏幕上已经有两个点按住 再松开一点时触发该事件
                    //当有一个手指离开屏幕后,就修改状态,这样如果双击屏幕就能恢复到初始大小
                    zoomInMode = ZoomMode.ZoomIn;
                    //记录此时的双指缩放比例
                    doubleFingerScrole =scaleSize.x/imageSize.x;
                    //记录此时屏幕触碰的点的数量
                    lastFingerNum = 1;
                    //判断缩放后的比例,如果小于最初的那个比例,就恢复到最初的大小
                    if (scaleSize.x<viewSize.x && scaleSize.y<viewSize.y){
                        zoomInMode = ZoomMode.Ordinary;
                        showCenter();
                    }
                    break;
                case MotionEvent.ACTION_MOVE:
                    //手指移动时触发事件
                    /**************************************移动
                    *******************************************/
                    if (zoomInMode != ZoomMode.Ordinary) {
                        //如果是多指,计算中心点为假设的点击的点
                        float currentX = 0;
                        float currentY = 0;
                        //获取此时屏幕上被触碰的点有多少个
                        int pointCount = event.getPointerCount();
                        //计算出中间点所在的坐标
                        for (int i = 0; i < pointCount; i++) {
                            currentX += event.getX(i);
                            currentY += event.getY(i);
                        }
                        currentX /= pointCount;
                        currentY /= pointCount;
                        //当屏幕被触碰的点的数量变化时,将最新算出来的中心点看作是被点击的点
                        if (lastFingerNum != event.getPointerCount()) {
                            clickPoint.x = currentX;
                            clickPoint.y = currentY;
                            lastFingerNum = event.getPointerCount();
                        }
                        //将移动手指时,实时计算出来的中心点坐标,减去被点击点的坐标就得到了需要移动的距离
                        float moveX = currentX - clickPoint.x;
                        float moveY = currentY - clickPoint.y;
                        //计算边界,使得不能已出边界,但是如果是双指缩放时移动,因为存在缩放效果,
                        //所以此时的边界判断无效
                        float[] moveFloat = moveBorderDistance(moveX, moveY);
                        //处理移动图片的事件
                        translationImage(new PointF(moveFloat[0], moveFloat[1]));
                        clickPoint.set(currentX, currentY);
                    }
                    /**************************************缩放
                    *******************************************/
                    //判断当前是两个手指接触到屏幕才处理缩放事件
                    if (event.getPointerCount() == 2){
                        //如果此时缩放后的大小,大于等于了设置的最大缩放的大小,就不处理
                        if ((scaleSize.x/imageSize.x >= originScale.x * maxScrole 
                             || scaleSize.y/imageSize.y >= originScale.y * maxScrole) 
                            && getDoubleFingerDistance(event) - doublePointDistance > 0){
                            break;
                        }
                        //这里设置当双指缩放的的距离变化量大于50,并且当前不是在双指缩放状态下,就计算中心点,等一些操作
                        if (Math.abs(getDoubleFingerDistance(event) - doublePointDistance) > 50 
                            && zoomInMode != ZoomMode.TowFingerZoom){
                            //计算两个手指之间的中心点,当作放大的中心点
                            doublePointCenter.set((event.getX(0) + event.getX(1))/2,
                                                  (event.getY(0) + event.getY(1))/2);
                            //将双指的中心点就假设为点击的点
                            clickPoint.set(doublePointCenter);
                            //下面就和双击放大基本一样
                            getBitmapOffset();
                            //分别记录被点击的点到图片左上角x,y轴的距离与图片x,y轴边长的比例,
                            //方便在进行缩放后,算出这个点对应的坐标点
                            tempPoint.set((clickPoint.x - bitmapOriginPoint.x)/scaleSize.x,
                                          (clickPoint.y - bitmapOriginPoint.y)/scaleSize.y);
                            //设置进入双指缩放状态
                            zoomInMode = ZoomMode.TowFingerZoom;
                        }
                        //如果已经进入双指缩放状态,就直接计算缩放的比例,并进行位移
                        if (zoomInMode == ZoomMode.TowFingerZoom){
                            //用当前的缩放比例与此时双指间距离的缩放比例相乘,就得到对应的图片应该缩放的比例
                            float scrole = 
                                doubleFingerScrole*getDoubleFingerDistance(event)/doublePointDistance;
                            //这里也是和双击放大时一样的
                            scaleImage(new PointF(scrole,scrole));
                            getBitmapOffset();
                            translationImage(
                                new PointF(
                                    clickPoint.x - (bitmapOriginPoint.x + tempPoint.x*scaleSize.x),
                                    clickPoint.y - (bitmapOriginPoint.y + tempPoint.y*scaleSize.y))
                            );
                        }
                    }
                    break;
                case MotionEvent.ACTION_UP:
                    //手指松开时触发事件
                    Log.e("kzg","***********************ACTION_UP");
                    lastFingerNum = 0;
                    break;
            }
            return true;
        }
    
    
    
        public void scaleImage(PointF scaleXY){
            matrix.setScale(scaleXY.x,scaleXY.y);
            scaleSize.set(scaleXY.x * imageSize.x,scaleXY.y * imageSize.y);
            setImageMatrix(matrix);
        }
    
        /**
         * 对图片进行x和y轴方向的平移
         * @param pointF
         */
        public void translationImage(PointF pointF){
            matrix.postTranslate(pointF.x,pointF.y);
            setImageMatrix(matrix);
        }
    
    
        /**
         * 防止移动图片超过边界,计算边界情况
         * @param moveX
         * @param moveY
         * @return
         */
        public float[] moveBorderDistance(float moveX,float moveY){
            //计算bitmap的左上角坐标
            getBitmapOffset();
            Log.e("kzg","**********************moveBorderDistance--
                  bitmapOriginPoint:"+bitmapOriginPoint);
            //计算bitmap的右下角坐标
            float bitmapRightBottomX = bitmapOriginPoint.x + scaleSize.x;
            float bitmapRightBottomY = bitmapOriginPoint.y + scaleSize.y;
    
            if (moveY > 0){
                //向下滑
                if (bitmapOriginPoint.y + moveY > 0){
                    if (bitmapOriginPoint.y < 0){
                        moveY = -bitmapOriginPoint.y;
                    }else {
                        moveY = 0;
                    }
                }
            }else if (moveY < 0){
                //向上滑
                if (bitmapRightBottomY + moveY < viewSize.y){
                    if (bitmapRightBottomY > viewSize.y){
                        moveY = -(bitmapRightBottomY - viewSize.y);
                    }else {
                        moveY = 0;
                    }
                }
            }
    
            if (moveX > 0){
                //向右滑
                if (bitmapOriginPoint.x + moveX > 0){
                    if (bitmapOriginPoint.x < 0){
                        moveX = -bitmapOriginPoint.x;
                    }else {
                        moveX = 0;
                    }
                }
            }else if (moveX < 0){
                //向左滑
                if (bitmapRightBottomX + moveX < viewSize.x){
                    if (bitmapRightBottomX > viewSize.x){
                        moveX = -(bitmapRightBottomX - viewSize.x);
                    }else {
                        moveX = 0;
                    }
                }
            }
            return new float[]{moveX,moveY};
        }
    
        /**
         * 获取view中bitmap的坐标点
         */
        public void getBitmapOffset(){
            float[] value = new float[9];
            float[] offset = new float[2];
            Matrix imageMatrix = getImageMatrix();
            imageMatrix.getValues(value);
            offset[0] = value[2];
            offset[1] = value[5];
            bitmapOriginPoint.set(offset[0],offset[1]);
        }
    
    
        /**
         * 计算零个手指间的距离
         * @param event
         * @return
         */
        public static float  getDoubleFingerDistance(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) ;
        }
    }
    

    至此这个简单的自定义View就完成了,文中很多实现都不是最优的实现思路,但是是普通人最容易理解的思路,在此欢迎大家指出文中的不足,如果有更优的方案欢迎交流。

    本文GitHub地址 https://github.com/Destroyer716/ZoomImageTest

    相关文章

      网友评论

          本文标题:Android从零开始实现可以双指缩放,自由移动,双击放大的自定

          本文链接:https://www.haomeiwen.com/subject/rvmpuhtx.html