android 图解 PhotoView,从‘百草园’到‘三味书屋’!
Posted android超级兵
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了android 图解 PhotoView,从‘百草园’到‘三味书屋’!相关的知识,希望对你有一定的参考价值。
PhotoView, android 图解 PhotoView 从0到1,从 👎 到👍
⚠️:考虑到部分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