android 图解 PhotoView,从‘百草园’到‘三味书屋’!

Posted android超级兵

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了android 图解 PhotoView,从‘百草园’到‘三味书屋’!相关的知识,希望对你有一定的参考价值。

⚠️:考虑到部分java开发者不熟悉kt,本篇采用java语言来编写! 底部附kotlin/java版源码

先来看看今天的效果图:

横向图片纵向图片

需求:

  • 图片
    • 横向图片 默认左右靠边 上下留白
    • 总想图片 默认上下靠边 左右留白
  • 双击 放大/缩小,放大后可单指移动
  • 双指 放大
  • 最小缩小不能小于初始图片,最大方法不能大于图片的1.5倍

最基础,绘制一张图片!

public class PhotoView2 extends View {
	 // 需要操作的图片
    private Bitmap mBitMap;

    // 画笔
    Paint mPaint = new Paint();	

    public PhotoView2(Context context) {
        this(context, null);
    }

   public PhotoView2(Context context, @Nullable AttributeSet attrs) {
        this(context, attrs, 0);
    }

    @SuppressLint("CustomViewStyleable")
    public PhotoView2(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
		 TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.PhotoView);

        Drawable drawable = typedArray.getDrawable(R.styleable.PhotoView_android_src);
        if (drawable == null)
            mBitMap = BitmapFactory.decodeResource(context.getResources(), R.mipmap.error);
        else
            mBitMap = toBitMap(drawable, 800, 800);

        // 回收 避免内存泄漏
        typedArray.recycle();
	}
	
	 @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        // 绘制一张图片 其实位置为0,0
        canvas.drawBitmap(mBitMap, 0, 0, mPaint);
    }


	// drawable -> bitmap
	private Bitmap toBitMap(Drawable drawable, int width, int height) {
        Bitmap bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
        Canvas canvas = new Canvas(bitmap);
        drawable.setBounds(0, 0, canvas.getWidth(), canvas.getHeight());
        drawable.draw(canvas);
        return bitmap;
    }
}

这部分代码比较简单,过一下就完事了!

图片居中

众所周知,在自定义View时候

View的执行流程为-> 构造方法 -> onMeasure() -> onSizeChanged() -> onDraw()

在绘制(onDraw)之前获取到偏移量即可

#PhotoView2.java

	 // 将图片移动到View中心
    float offsetWidth = 0f;
    float offsetHeight = 0f;
    
	@Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        super.onSizeChanged(w, h, oldw, oldh);
 		offsetWidth = getWidth() / 2f - mBitMap.getWidth() / 2f;
        offsetHeight = getHeight() / 2f - mBitMap.getHeight() / 2f;
}

	 @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        // 参数一:图片
        // 参数二:图片x位置
        // 参数三:图片y的位置
        // 参数四:画笔
        canvas.drawBitmap(mBitMap, offsetWidth, offsetHeight, mPaint);
    }

看不懂没关系,来张图就一目了然了!

当前的效果


这块还是比较基础的东西!接下来要提高难度了…

图片放大

为了满足需求一,先将图片放大到合适的位置

需求一:

  • 图片
    - 横向图片 默认左右靠边 上下留白
    - 纵向图片 默认上下靠边 左右留白

需求一辅助图:

纵向图片横向图片

先来看代码:

#PhotoView2.java

// 缩放前图片比例
    float smallScale = 0f;
    
    // 缩放后图片
    float bigScale = 0f;

    // 当前比例
    float currentScale = 0f;

    // 缩放倍数
    private static final float ZOOM_SCALE = 1.5f;
    
 @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        super.onSizeChanged(w, h, oldw, oldh);

     	// view比例
        float viewScale = (float) getWidth() / (float) getHeight();
        
        // 图片比例
        float bitScale = (float) mBitMap.getWidth() / (float) mBitMap.getHeight();

        // 如果图片比例大于view比例
        if (bitScale > viewScale) {
            // 横向图片
            smallScale = (float) getWidth() / (float) mBitMap.getWidth();
            bigScale = (float) getHeight() / (float) mBitMap.getHeight() * ZOOM_SCALE;
        } else {
            // 纵向图片
            smallScale = (float) getHeight() / (float) mBitMap.getHeight();
            bigScale = (float) getWidth() / (float) mBitMap.getWidth() * ZOOM_SCALE;
        }

        // 当前缩放比例 = 缩放前的比例
        currentScale = smallScale;
    }

	 @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        /*
         *  参数一: x 缩放比例
         *  参数二: y 缩放比例
         *  参数三: x 轴位置
         *  参数四: y 轴位置
         *  
         *  这里为了简单起见,所以x,y缩放比例使用的同一个值[currentScale]
         */
        canvas.scale(currentScale, currentScale, getWidth() / 2f, getHeight() / 2f);

        canvas.drawBitmap(mBitMap, offsetWidth, offsetHeight, mPaint);
    }

smallScale/bigScale还不懂是什么? 以横向图片为例,带入参数,一张图搞懂!

  • smallScale 缩放原来图片的1.5倍
  • bigScale 缩放原来的2.4倍

以横向图片关键代码举例:

// 横向图片
smallScale = (float) getWidth() / (float) mBitMap.getWidth();
bigScale = (float) getHeight() / (float) mBitMap.getHeight() * ZOOM_SCALE;

如果是横向图片,那么证明 height > width

所以 smallScale 缩放比例就是width / bitmap.width ,让左右不留白,上下留白

bigScale 缩放比例这里采取height的比例 * 1.5 是为了防止图片过小从而没有超出整个屏幕

当前的效果


双击放大

提到双击放大,就不得不提到android中自带的监听双击的类

#PhotoView2.java

	// 双击手势监听
    static class PhotoGestureListener extends GestureDetector.SimpleOnGestureListener {
        // 单击情况 : 抬起[ACTION_UP]时候触发
        // 双击情况 : 第二次抬起[ACTION_POINTER_UP]时候触发
        @Override
        public boolean onSingleTapUp(MotionEvent e) {
            Log.i("szjPhotoGestureListener", "抬起了 onSingleTapUp");
            return super.onSingleTapUp(e);
        }

        // 长按时触发 [300ms]
        @Override
        public void onLongPress(MotionEvent e) {
            Log.i("szjPhotoGestureListener", "长按了 onLongPress");
            super.onLongPress(e);
        }

        // 滑动时候触发 类似 ACTION_MOVE 事件
        @Override
        public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
            Log.i("szjPhotoGestureListener", "滑动了  onScroll");
            return super.onScroll(e1, e2, distanceX, distanceY);
        }

        // 滑翔/飞翔 [惯性滑动]
        @Override
        public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
            Log.i("szjPhotoGestureListener", "惯性滑动 onFling");
            return super.onFling(e1, e2, velocityX, velocityY);
        }

        // 延时触发 [100ms] -- 常用与水波纹等效果
        @Override
        public void onShowPress(MotionEvent e) {
            super.onShowPress(e);
            Log.i("szjPhotoGestureListener", "延时触发 onShowPress");
        }

        // 按下 这里必须返回true 因为所有事件都是由按下出发的
        @Override
        public boolean onDown(MotionEvent e) {
            return true;
        }

        // 双击 -- 第二次按下时候触发 (40ms - 300ms) [小于40ms是为了防止抖动]
        @Override
        public boolean onDoubleTap(MotionEvent e) {
            Log.i("szjPhotoGestureListener", "双击了 onDoubleTap");
            return super.onDoubleTap(e);
        }

        // 双击 第二次的事件处理 DOWN MOVE UP 都会执行到这里
        @Override
        public boolean onDoubleTapEvent(MotionEvent e) {
            Log.i("szjPhotoGestureListener", "双击执行了 onDoubleTapEvent");
            return super.onDoubleTapEvent(e);
        }

        // 单击时触发 双击时不触发
        @Override
        public boolean onSingleTapConfirmed(MotionEvent e) {
            Log.i("szjPhotoGestureListener", "单击了 onSingleTapConfirmed");
            return super.onSingleTapConfirmed(e);
        }
    }

这里我都打log写注释了,自己测测很容易拿捏,对于放大来说,最重要的当然是双击事件 onDoubleTap()

直接看代码

#PhotoGestureListener.java

    // 是否双击 [默认第一次点击是缩小]
    boolean isDoubleClick = false;
    
	    // 双击 -- 第二次按下时候触发 (40ms - 300ms) [小于40ms是为了防止抖动]
        @Override
        public boolean onDoubleTap(MotionEvent e) {
        	// 先改为放大,第一次点击是放大效果
            isDoubleClick = !isDoubleClick;
            if (isDoubleClick) {
                // 放大 放大到最大比例
                currentScale = bigScale;
            } else {
                // 缩小 缩小为左右留白的比例
                currentScale = smallScale;
            }
            // 刷新 onDraw
            invalidate();
          
            return super.onDoubleTap(e);
        }

记得初始化PhotoGestureListener

众所周知,单击事件(DOWN) / 触摸事件(MOVE) / 抬起事件(UP) 由onTouchEvent()可以监听到,那么作为双击事件,也是同样的道理!!

注意⚠️⚠️ :onDown() 必须返回true,因为DOWN事件是所有事件的起点

#PhotoView2.java

	// 双击操作
private final GestureDetector mPhotoGestureListener;

public PhotoView2(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
		... 构造方法中初始化 ...
		 mPhotoGestureListener = new GestureDetector(context, new PhotoGestureListener());
}

// 双击事件传递下去
@Override
public boolean onTouchEvent(MotionEvent event) {
    return mPhotoGestureListener.onTouchEvent(event);
}

当前的效果


双击放大添加动画

现在的效果还有点《粗糙》,接下来添加一个缩放的动画

#PhotoGestureListener.java

        @Override
        public boolean onDoubleTap(MotionEvent e) {
         isDoubleClick = !isDoubleClick;
            if (isDoubleClick) {
                // 放大
//                currentScale = bigScale;
                scaleAnimation(currentScale, bigScale).start();
            } else {
                // 缩小
//                currentScale = smallScale;
                scaleAnimation(bigScale, smallScale).start();
            }
            // 不需要刷新了,在属性动画调用setCurrentScale() 的时候已经刷新了
//            invalidate();
           
            return super.onDoubleTap(e);
        }

// 缩放动画
public ObjectAnimator scaleAnimation(float start, float end) {
        ObjectAnimator animator = ObjectAnimator.ofFloat(this, "currentScale", start, end);
        // 动画时间
        animator.setDuration(500);
        return animator;
    }

    // 属性动画的关键!!  内部通过反射调用set方法来赋值
    public void setCurrentScale(float currentScale) {
        this.currentScale = currentScale;
        invalidate();
    }

当前的效果

放大后图片滑动

这里为了代码规范,行,y坐标我就写成一个OffSet类了

data class OffSet(var x: Float, var y: Float)

还是在双击手势类里面,onScroll()类似ACTION_MOVE事件,所以监听这个也是一样的[PhotoGestureListener]

#PohtoView2.java

  // 放大后手指移动位置
    private OffSet moveOffset = new OffSet(0f, 0f);

// 双击手势监听
class PhotoGestureListener extends GestureDetector.SimpleOnGestureListener {
// 滑动时候触发 类似 ACTION_MOVE 事件
        @Override
        public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {

			// 如果是放大状态才能移动
            if (isDoubleClick) {
                moveOffset.setX(moveOffset.getX() - distanceX);
                moveOffset.setY(moveOffset.getY() - distanceY);
                // kotlin 写法:
                // moveOffset.x -= distanceX
                // moveOffset.y -= distanceY
                invalidate();
            }
            return super.onScroll(e1, e2, distanceX, distanceY);
        }
}

有同学可能要问了,这里为么是减等于(累减),首先要搞清楚这个distanceX 和 distanceY是什么

因为onScroll() 相当于是MOVE事件,所以这里只要是触摸就会输出

一张图搞懂,以x轴举例:

得出结论,以按压点为中心点

  • distanceX
    • 向左滑动 正数
    • 向右滑动 负数
  • distanceY
    • 向上滑动 正数
    • 向下滑动 负数

distanceX = 新的x坐标 - 旧的x坐标
distanceY = 新的y坐标 - 旧的y坐标

接下来看看移动画布的api:

#PhotoView2.java

@Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        /*
         * 作者:android 超级兵
         * 创建时间: 10/15/21 5:17 PM
         * TODO 平移画布
         * 	参数一:x轴平移距离
         * 	参数二:y轴平移距离
         */
        canvas.translate(-300, 0);
}

效果:


得出结论:
想要图片想左移动,设置 canvas.translate();x轴(参数一),为负数,反之向右移动设置为正数

知道了distanceX和distanceY,也知道了画布移动的api,那么问题就来了,移动时候为什么是减等于呢?

#PhotoGestureListener.java

// 如果是放大状态才能移动
if (isDoubleClick) {
	 // java写法
     moveOffset.setX(moveOffset.getX() - distanceX);
     moveOffset.setY(moveOffset.getY() - distanceY);
     // kotlin 写法:
     // moveOffset.x -= distanceX
     // moveOffset.y -= distanceY
     invalidate();
}

因为向左滑动的时候,图片应该是向右移动

又因为向左滑动时候,distanceX 为正数,并且是MOVE事件触发的,所以会触发多次

所以说这里是减等于,需要吧 distanceX的坐标都累加起来

最后,记得在onDraw中绘制偏移量哦

PohtoView2.java

  @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        // 移动画布
	 	canvas.translate(moveOffset.以上是关于android 图解 PhotoView,从‘百草园’到‘三味书屋’!的主要内容,如果未能解决你的问题,请参考以下文章

Android:导入库,如 PhotoView

Android图片处理二:PhotoView源码解析

图片查看器:Android支持图片查看缩放滑动的PhotoView

如何在android studio 3中使用photoview 2.0.0

Android -- 开源库PhotoView 的基本使用

android viewpager嵌套使用photoview异常问题