准备工作
自定义PhotoView
自定义 PhotoView 继承(extends)自 View。并在最中间显示后面操作的图片。绘制图片可以重写 onDraw()方法,并在里面通过Canvas.drawBitmap()来要绘制图片。
drawBitmap()的四个参数:
- bitmap: 要在 Canvas 中绘制的位图
- letf:正在绘制的位图左侧的位置
- top:正在绘制的位图顶部的位置
- paint: 画笔
其中 (left, top) 是要绘制图片的起始坐标。要将图片绘制在中间,我们就需要计算 left/top 的位置。我们重写 onSizeChanged() 函数,该函数在onDraw之前调用,且尺寸改变时也要调用。
其中:(下面代码中是用 originalOffsetX/originalOffsetY 来代替的)
left = (getWidth() - bitmap.getWidth()) / 2;
top =(getHeight() - bitmap.getHeight()) / 2;
public class PhotoView extends View { private static final float IMAGE_WIDTH = Utils.dpToPixel(300); private Bitmap bitmap; private Paint paint; // 画笔 private float originalOffsetX; private float originalOffsetY; public PhotoView(Context context) { this(context, null); } public PhotoView(Context context, @Nullable AttributeSet attrs) { this(context, attrs, 0); } public PhotoView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); init(); } /** * 初始化操作 */ private void init() { bitmap = Utils.getPhoto(getResources(), (int) IMAGE_WIDTH); // 获取到图片 paint = new Paint(); } /** * TODO 在onDraw之前调用,且尺寸改变时也要调用 */ @Override protected void onSizeChanged(int w, int h, int oldw, int oldh) { super.onSizeChanged(w, h, oldw, oldh); originalOffsetX = (getWidth() - bitmap.getWidth()) / 2f; originalOffsetY = (getHeight() - bitmap.getHeight()) / 2f; } /** * 画出图片 * @param canvas 画布 */ @Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); canvas.drawBitmap(bitmap, originalOffsetX, originalOffsetY, paint); } }
- xml 布局
xml 布局中最外层是 FragmeLayout,里面只有一个自定义的 PhotoView 用来展示图片。
- Utils 工具类
Utils 工具类里主要有两个函数。dpToPixel() 将 dp 转换为像素;getPhot() 加载 Drawable 下的图片,并返回为 bitmap 类型。
public class Utils { public static float dpToPixel(float dp) { return TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dp, Resources.getSystem().getDisplayMetrics()); } public static Bitmap getPhoto(Resources res, int width) { BitmapFactory.Options options = new BitmapFactory.Options(); options.inJustDecodeBounds = true; BitmapFactory.decodeResource(res, R.drawable.photo, options); options.inJustDecodeBounds = false; options.inDensity = options.outWidth; options.inTargetDensity = width; return BitmapFactory.decodeResource(res, R.drawable.photo, options); } }
1、双击放大和缩小
- 设置图片的缩放比例
如下图的三种情况,左边的是原图;中间是小放大(smallScale),即图片左右两边贴进屏幕;右边是大放大(bigScale),即图片沾满整个屏幕。
根据上面的描述,设置两个变量即 smallScale 和 bigScale 分别代表上图"中"和“右”的缩放比例,smallScale 是初始样式,bigSmall 是双击后的样式。将 smallScale 和 bigScale 的设置放在 onSizeChanged() 函数里设值。如下图所示
/** * TODO 在onDraw之前调用,且尺寸改变时也要调用 */ @Override protected void onSizeChanged(int w, int h, int oldw, int oldh) { super.onSizeChanged(w, h, oldw, oldh); originalOffsetX = (getWidth() - bitmap.getWidth()) / 2f; originalOffsetY = (getHeight() - bitmap.getHeight()) / 2f; // TODO 判断 bitmap 是扁的还是长的 if ((float)bitmap.getWidth() / bitmap.getHeight() > (float) getWidth() / getHeight()) { // bitmap 的 width > height smallScale = (float) getWidth() / bitmap.getWidth(); bigScale = (float) getHeight() / bitmap.getHeight() * OVER_SCALE_FACTOR; }else { // bitmap 的 height > width smallScale = (float) getHeight() / bitmap.getHeight(); bigScale = (float) getWidth() / bitmap.getWidth() * OVER_SCALE_FACTOR; } currentScale = smallScale; }
注意 if 里的判断条件,判断图片是扁平还是长的。如下图理解,当然我们这里用的图是扁平的。currentScale 是当前的缩放比例,smallScale <= currentScale <= bigScale 。
最后设置了 smallScale 和 bigScale 后,我们还要在 onDraw 里将 smallScale 放大的图片绘制出来。这里用 currentScale ,因为在 onSizeChanged 函数里,我们将 smallScale 赋值给了 currentScale 。使用 Canvas.scale 函数进行缩放。
// TODO 图片放大, // 第1,2个参数是放大比例,第3,4个参数是缩放的起始点,默认是(0,0) canvas.scale(currentScale, currentScale, getWidth() / 2f, getHeight() / 2f);
- 双击击缩放
Android 为我们提供了一个 GestureDetector 类来实现双击、单击、滑动、惯性滑动等。在 init 函数里添加如下代码,初始化 GestureDetector。gestureDectector 是一个全局变量。
gestureDetector = new GestureDetector(context, new photoGestureListener());
GestureDetector 的第二个参数是一个 Listener ,所以我们写了个内部类 photoGestureListener 继承GestureDetector.SimpleOnGestureListener。SimpleOnGestureListener 是一个 interface, 所以我们重写里面的方法,其中onDoubleTap() 就是实现写双击缩放的。
注意:onDown() 方法要返回 true 才能响应到双击事件
/** * TODO 单击/双击/惯性滑动的监听 */ class photoGestureListener extends GestureDetector.SimpleOnGestureListener{ // up 时触发,单击或者双击的第一次会触发 --- up时,如果不是双击的得二次点击,不是长按,则触发 @Override public boolean onSingleTapUp(MotionEvent e) { return super.onSingleTapUp(e); } // 长按 默认300ms后触发 @Override public void onLongPress(MotionEvent e) { super.onLongPress(e); } /** * 滚动 --move * @param e1 手指按下 * @param e2 当前动作 * @param distanceX 就位置 - 新位置 * @param distanceY * @return */ @Override public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) { return super.onScroll(e1, e2, distanceX, distanceY); } /** * 惯性滑动 * @param velocityX X轴方向运动速度 像素/s * @param velocityY Y轴方向运动速度 像素/s * @return */ @Override public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) { return super.onFling(e1, e2, velocityX, velocityY); } // 处理点击效果 --延时 100ms 触发 @Override public void onShowPress(MotionEvent e) { super.onShowPress(e); } // 只需要关注 onDown 的返回值,默认返回 false @Override public boolean onDown(MotionEvent e) { return true; } // 双击的第二次点击 down 时触发 双击 40ms -- 300ms 之间 @Override public boolean onDoubleTap(MotionEvent e) { // // TODO 第一版,这种直接放大/缩小有点深硬,不平滑 // isEnlarge = !isEnlarge; // if (isEnlarge) { // currentScale = bigScale; // 双击放大 // }else { // currentScale = smallScale; // 再双击时放小 // } // invalidate(); // 刷新 //TODO 第二版,借助属性动画实现 isEnlarge = !isEnlarge; if (isEnlarge) { // TODO 双击时计算偏移,双击那个位置,就放大那个位置 / (e.getX(), e.getY()) 当前点击的位置 offsetX = (e.getX() - getWidth() / 2f) - (e.getX() - getWidth() / 2f) * bigScale / smallScale; offsetY = (e.getY() - getHeight() / 2f) - (e.getY() - getHeight() / 2f) * bigScale / smallScale; fitOffsets(); // 解决点击图片外时放大空白部分 getScaleAnimator().start(); }else { getScaleAnimator().reverse(); } return super.onDoubleTap(e); } // 双击的第二次down, move, up 都触发 @Override public boolean onDoubleTapEvent(MotionEvent e) { return super.onDoubleTapEvent(e); } // 单击按下时触发,双击时不触发/ down, up时都可能触发(不会同时触发) // 延时300ms触发TAP事件 // 300ms 以内抬手 -- 才会触发TAP -- onSingleTapConfirmed // 300ms 以后抬手 -- 不是双击或长按,则触发 @Override public boolean onSingleTapConfirmed(MotionEvent e) { return super.onSingleTapConfirmed(e); } }
onDoubleTap() 里的第一版代码里 currentScale 直接由 smallScale 变到 bigscale,一下子就放大了,就很生硬不平滑。为了实现平滑的效果,我们使用 属性动画(ObjectAnimator),使得currentScale 由 smallScale 逐步变化到 bigScale,即 currentScale
(smallScale, bigScale)
private ObjectAnimator getScaleAnimator(){ if (scaleAnimator == null) { scaleAnimator = ObjectAnimator.ofFloat(this, "currentScale", 0); } // TODO 平滑的范围,从 smallScale --> bigScale scaleAnimator.setFloatValues(smallScale, bigScale); return scaleAnimator; } public float getCurrentScale() { return currentScale; } public void setCurrentScale(float currentScale) { this.currentScale = currentScale; // 每一次在 smallScale -- bigScale 直接变化时都刷新 invalidate(); }
注意:上面代码里的 offsetX / offsetY 两个变量这里没讲,是因为它们是滑动里用到的变量,所以我们放到下一小节里讲,这里用它们是为了实现双击那个位置,就放大那个位置。如果把下面两句代码注释掉,会发现双击的时候永远是从中间位置放大。实现原理就是 offsetX / offsetY 是两个偏移量,我们从中间放大后再移到 offsetX / offsetY 的位置,就实现了点击哪里就放大哪里。
offsetX = (e.getX() - getWidth() / 2f) - (e.getX() - getWidth() / 2f) * bigScale / smallScale; offsetY = (e.getY() - getHeight() / 2f) - (e.getY() - getHeight() / 2f) * bigScale / smallScale; fitOffsets(); // 解决点击图片外时放大空白部分
完成上面的代码,当我们运行程序然后双击屏幕时发现图片并没有放大,为什么?因为我们双击的时候触发的是 photoView 的 onTouchEvent(),而双击时需要触发 GestureDetector 的 onToucEvent()才能实现效果,所以我们再 photoView 里重写 onTouchEvent ,并用 GestureDetector 的 onTouchEvent() 来强制接管。
/** TODO 我们点击图片时,触发的是 PhotoView 里的 onTouchEvent, * TODO 并没有触发 GestureDetector 里的onTouchEvent, 所以才需要强制接管 */ @Override public boolean onTouchEvent(MotionEvent event) { return gestureDetector.onTouchEvent(event); //return super.onTouchEvent(event); }
2、滑动和惯性滑动
当我们双击放大图片后,可以通过手指滑动查看屏幕外面的内容,或者用力往某个方向滑动,实现惯性滑动的效果。
- 手指滑动
在上面一节提到的 SimpleOnGestureListener 接口,里面的 onScroll 函数实现滑动。offsetX offsetY 是滑动的偏移量,即滑动到了图片的那个位置,在绘制的时候才能把滑动到的位置的图片绘制出来。
/** * 滚动 --move * @param e1 手指按下 * @param e2 当前动作 * @param distanceX 就位置 - 新位置 * @param distanceY * @return */ @Override public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) { // 图片放大时,才可以滑动,即改变 offsetX offsetY if (isEnlarge) { offsetX -= distanceX; offsetY -= distanceY; fitOffsets(); invalidate(); } return super.onScroll(e1, e2, distanceX, distanceY); }
if 里的判断条件是确保在图片放大的情况下才进行滑动。fitOffsets() 是一个功能函数,计算图片滑动到边界的情况,放大后图片的边界滑动到屏幕边界时就滑不动了。
/** * 计算图片滑动的边界情况 * TODO 当往某个方向滑动图片时,放大后的图片边界与手机屏幕边界重合时,就不能滑动了 */ private void fitOffsets(){ offsetX = Math.min(offsetX, (bitmap.getWidth() * bigScale - getWidth()) / 2); offsetX = Math.max(offsetX, -(bitmap.getWidth() * bigScale - getWidth()) / 2); offsetY = Math.min(offsetY, (bitmap.getHeight() * bigScale - getHeight()) / 2); offsetY = Math.max(offsetY, -(bitmap.getHeight() * bigScale - getHeight()) / 2); }
对 offsetX 取值用 Math.min()和 Math.max() 的情况可以如下图理解。offsetY 同理。
设置好了 onScroll() 函数后,我们还要将滑动的图片绘制出来,所以我们还要在 onDraw 函数里调用 Canvas.translate(), 将滑动的偏移 offsetX / offsetY 设置进去。
// TODO 图片滑动查看隐藏部分 canvas.translate(offsetX, offsetY);
惯性滑动
SimpleOnGestureListener 接口里的 onFling 函数实现惯性滑动。通过 OverScroll.fling() 来实现,filing 函数的最后两个参数表示当滑动到边界时,如果还有速度,则会将边界外的空白部分拉出200像素,然后立马回弹回去的那种效果。可以尝试将这两个参数去掉对比两种情况的效果。
/** * 惯性滑动 * @param velocityX X轴方向运动速度 像素/s * @param velocityY Y轴方向运动速度 像素/s * @return */ @Override public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) { if (isEnlarge) { overScroller.fling((int) offsetX, (int) offsetY, (int) velocityX, (int) velocityY, -(int) (bitmap.getWidth() * bigScale - getWidth()) / 2, (int) (bitmap.getWidth() * bigScale - getWidth()) /2, -(int) (bitmap.getHeight() * bigScale - getHeight()) / 2, (int) (bitmap.getHeight() * bigScale - getHeight()) /2, 200, 200); // TODO 我们要不断的刷新界面,不断的改变 offsetX, offsetY, 参数:Runnable接口 // postOnAnimation 下一帧动画的时候执行 postOnAnimation(new flingRunner()); } return super.onFling(e1, e2, velocityX, velocityY); }
我们在惯性滑动时要不断的刷新界面,不断改变 offsetX / offsetY 。我们使用 postOnAnimation(),里面传入一个 filingRunner 接口,继承自Runnable 。然后在filingRunner 里再调用postOnAnimation() 实现循环的效果。用 overScroller.computeScrollOffset() 函数计算当前的偏移并赋值给 offsetX/offsetY,实现不断改变它的功能。当computeScrollOffset() 返回 false,则表明当前的惯性速度为0,惯性滑动就结束,则结束循环。
class flingRunner implements Runnable{ @Override public void run() { // TODO 用 overScroller 计算当前的偏移,并赋值给offsetX, offsetY if (overScroller.computeScrollOffset()) { // computeScrollOffset()会返回一个boolean值,为true, 说明动作还没完成,以此来作为循环结束条件 offsetX = overScroller.getCurrX(); offsetY = overScroller.getCurrY(); invalidate(); //在上面的onFling 方法里面,postOnAnimation 只会调用一次,所以我们这里再调用,参数:自己(flingRunner) //TODO postOnAnimation 下一帧动画的时候执行 postOnAnimation(this); } } }
注意:写到这里,就有了一个小 bug ,就是当我们滑动了图片后再双击放小,会发现图片不会显示在正中间了,只需在 onDraw() 函数里做如下修改:我们在 offsetX / offsetY 上乘以一个平移因子,当双击缩小的时候,currentScale == smallScale ,则 scaleFaction == 0 --> offsetX / offsetY ==0 ,就相当于没有平移了,所以双击缩小时就能显示在原位置。
// 解决:当位置移动后,双击缩小,让图片显示在最初的位置 // 双击缩小时,currentScale = smallScale, 所以 scaleFunction = 0, 所以 translate就相当于没有平移 float scaleFaction = (currentScale - smallScale) / (bigScale - smallScale); // TODO 图片滑动查看隐藏部分 canvas.translate(offsetX * scaleFaction, offsetY * scaleFaction);
3、双指放大和缩小
Android 为我们提供了一个 ScaleGestureDetector 类来实现双指缩放功能。在 init() 函数里初始化。
scaleGestureDetector = new ScaleGestureDetector(context, new photoScaleGestureListener());
photoScaleGestureListener() 实现了ScaleGestureDetector.onScaleGestureListener 接口,实现里面的三个方法。
- onScale:处理正在缩放
- onScaleBegin: 开始缩放
- onScaleEnd: 结束缩放
/** * TODO 双指缩放大的监听 */ class photoScaleGestureListener implements ScaleGestureDetector.OnScaleGestureListener{ float initScale; // 处理正在缩放 @Override public boolean onScale(ScaleGestureDetector detector) { if (currentScale >= smallScale && !isEnlarge) { isEnlarge = !isEnlarge; } // 缩放因子 缩放后 / 缩放前 // eg 放大后=10,放大前=5, 缩放因子 == 10 / 5 == 2 currentScale = initScale * detector.getScaleFactor(); invalidate(); return false; } // 开始缩放 @Override public boolean onScaleBegin(ScaleGestureDetector detector) { initScale = currentScale; return true; } //结束缩放 @Override public void onScaleEnd(ScaleGestureDetector detector) { } }
同理,ScaleGestureDetector 的触发也需要在 photoView 里的 onTouchEvent 里强制接管,所以修改 onTouchEvnet() 里的代码如下:
/** TODO 我们点击图片时,触发的是 PhotoView 里的 onTouchEvent, * TODO 并没有触发 GestureDetector 里的onTouchEvent, 所以才需要强制接管 * TODO 同理,ScaleGestureDetector 也需要接管 */ @Override public boolean onTouchEvent(MotionEvent event) { // TODO 响应事件以双指缩放优先 boolean result = scaleGestureDetector.onTouchEvent(event); if(!scaleGestureDetector.isInProgress()){ // TODO 不是双指缩放,则用 GestureDetector 的 onTouchEvent 强制接管 result = gestureDetector.onTouchEvent(event); } return result; //return super.onTouchEvent(event); }
4、完整DEMO
完整的 photoView 代码(MainActivity里没写什么)
package com.example.photoview; import android.animation.ObjectAnimator; import android.content.Context; import android.graphics.Bitmap; import android.graphics.Canvas; import android.graphics.Paint; import android.util.AttributeSet; import android.view.GestureDetector; import android.view.MotionEvent; import android.view.ScaleGestureDetector; import android.view.View; import android.widget.OverScroller; import androidx.annotation.Nullable; public class PhotoView extends View { private static final float IMAGE_WIDTH = Utils.dpToPixel(300); private Bitmap bitmap; private Paint paint; float originalOffsetX; float originalOffsetY; private float smallScale; private float bigScale; private float currentScale; //当前缩放值 private float OVER_SCALE_FACTOR = 1.5f; private boolean isEnlarge = false; //双击时放大/缩小的标志位 private ObjectAnimator scaleAnimator; // 双击放大/缩小时,通过属性动画做出平滑的效果 private GestureDetector gestureDetector; // android 提高的手势探测器,TODO 判断是单价还是双击 private ScaleGestureDetector scaleGestureDetector; // TODO 实现双指缩放 private float offsetX; // 图片放大后,手指滑动图片查看隐藏部分 private float offsetY; private OverScroller overScroller; // TODO 实现惯性滑动 public PhotoView(Context context) { this(context, null); } public PhotoView(Context context, @Nullable AttributeSet attrs) { this(context, attrs, 0); } public PhotoView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); init(context); } private void init(Context context){ bitmap = Utils.getPhoto(getResources(), (int) IMAGE_WIDTH); paint = new Paint(Paint.ANTI_ALIAS_FLAG); gestureDetector = new GestureDetector(context, new photoGestureListener()); scaleGestureDetector = new ScaleGestureDetector(context, new photoScaleGestureListener()); // 设置长按响应,false--关闭 //gestureDetector.setIsLongpressEnabled(false); overScroller = new OverScroller(context); } @Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); // 解决:当位置移动后,双击缩小,让图片显示在最初的位置 // 双击缩小时,currentScale = smallScale, 所以 scaleFunction = 0, 所以 translate就相当于没有平移 float scaleFaction = (currentScale - smallScale) / (bigScale - smallScale); // TODO 图片滑动查看隐藏部分 canvas.translate(offsetX * scaleFaction, offsetY * scaleFaction); // TODO 图片放大, // 第1,2个参数是放大比例,第3,4个参数是缩放的起始点,默认是(0,0) canvas.scale(currentScale, currentScale, getWidth() / 2f, getHeight() / 2f); // drawBitmap(); 第2,3个参数是画bitmap的起始坐标点 canvas.drawBitmap(bitmap, originalOffsetX, originalOffsetY, paint); } /** * TODO 在onDraw之前调用,且尺寸改变时也要调用 */ @Override protected void onSizeChanged(int w, int h, int oldw, int oldh) { super.onSizeChanged(w, h, oldw, oldh); originalOffsetX = (getWidth() - bitmap.getWidth()) / 2f; originalOffsetY = (getHeight() - bitmap.getHeight()) / 2f; // TODO 判断 bitmap 是扁的还是长的 if ((float)bitmap.getWidth() / bitmap.getHeight() > (float) getWidth() / getHeight()) { // bitmap 的 width > height smallScale = (float) getWidth() / bitmap.getWidth(); bigScale = (float) getHeight() / bitmap.getHeight() * OVER_SCALE_FACTOR; }else { // bitmap 的 height > width smallScale = (float) getHeight() / bitmap.getHeight(); bigScale = (float) getWidth() / bitmap.getWidth() * OVER_SCALE_FACTOR; } currentScale = smallScale; } /** TODO 我们点击图片时,触发的是 PhotoView 里的 onTouchEvent, * TODO 并没有触发 GestureDetector 里的onTouchEvent, 所以才需要强制接管 * TODO 同理,ScaleGestureDetector 也需要接管 */ @Override public boolean onTouchEvent(MotionEvent event) { // TODO 响应事件以双指缩放优先 boolean result = scaleGestureDetector.onTouchEvent(event); if(!scaleGestureDetector.isInProgress()){ // TODO 不是双指缩放,则用 GestureDetector 的 onTouchEvent 强制接管 result = gestureDetector.onTouchEvent(event); } return result; //return super.onTouchEvent(event); } /** * TODO 单击/双击/惯性滑动的监听 */ class photoGestureListener extends GestureDetector.SimpleOnGestureListener{ // up 时触发,单击或者双击的第一次会触发 --- up时,如果不是双击的得二次点击,不是长按,则触发 @Override public boolean onSingleTapUp(MotionEvent e) { return super.onSingleTapUp(e); } // 长按 默认300ms后触发 @Override public void onLongPress(MotionEvent e) { super.onLongPress(e); } /** * 滚动 --move * @param e1 手指按下 * @param e2 当前动作 * @param distanceX 就位置 - 新位置 * @param distanceY * @return */ @Override public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) { // 图片放大时,才可以滑动,即改变 offsetX offsetY if (isEnlarge) { offsetX -= distanceX; offsetY -= distanceY; fitOffsets(); invalidate(); } return super.onScroll(e1, e2, distanceX, distanceY); } /** * 惯性滑动 * @param velocityX X轴方向运动速度 像素/s * @param velocityY Y轴方向运动速度 像素/s * @return */ @Override public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) { if (isEnlarge) { overScroller.fling((int) offsetX, (int) offsetY, (int) velocityX, (int) velocityY, -(int) (bitmap.getWidth() * bigScale - getWidth()) / 2, (int) (bitmap.getWidth() * bigScale - getWidth()) /2, -(int) (bitmap.getHeight() * bigScale - getHeight()) / 2, (int) (bitmap.getHeight() * bigScale - getHeight()) /2, 200, 200); // TODO 我们要不断的刷新界面,不断的改变 offsetX, offsetY, 参数:Runnable接口 // postOnAnimation 下一帧动画的时候执行 postOnAnimation(new flingRunner()); } return super.onFling(e1, e2, velocityX, velocityY); } // 处理点击效果 --延时 100ms 触发 @Override public void onShowPress(MotionEvent e) { super.onShowPress(e); } // 只需要关注 onDown 的返回值,默认返回 false @Override public boolean onDown(MotionEvent e) { return true; } // 双击的第二次点击 down 时触发 双击 40ms -- 300ms 之间 @Override public boolean onDoubleTap(MotionEvent e) { // // TODO 第一版,这种直接放大/缩小有点深硬,不平滑 // isEnlarge = !isEnlarge; // if (isEnlarge) { // currentScale = bigScale; // 双击放大 // }else { // currentScale = smallScale; // 再双击时放小 // } // invalidate(); // 刷新 //TODO 第二版,借助属性动画实现 isEnlarge = !isEnlarge; if (isEnlarge) { // TODO 双击时计算偏移,双击那个位置,就放大那个位置 / (e.getX(), e.getY()) 当前点击的位置 offsetX = (e.getX() - getWidth() / 2f) - (e.getX() - getWidth() / 2f) * bigScale / smallScale; offsetY = (e.getY() - getHeight() / 2f) - (e.getY() - getHeight() / 2f) * bigScale / smallScale; fitOffsets(); // 解决点击图片外时放大空白部分 getScaleAnimator().start(); }else { getScaleAnimator().reverse(); } return super.onDoubleTap(e); } // 双击的第二次down, move, up 都触发 @Override public boolean onDoubleTapEvent(MotionEvent e) { return super.onDoubleTapEvent(e); } // 单击按下时触发,双击时不触发/ down, up时都可能触发(不会同时触发) // 延时300ms触发TAP事件 // 300ms 以内抬手 -- 才会触发TAP -- onSingleTapConfirmed // 300ms 以后抬手 -- 不是双击或长按,则触发 @Override public boolean onSingleTapConfirmed(MotionEvent e) { return super.onSingleTapConfirmed(e); } } /** * TODO 双指缩放大的监听 */ class photoScaleGestureListener implements ScaleGestureDetector.OnScaleGestureListener{ float initScale; // 处理正在缩放 @Override public boolean onScale(ScaleGestureDetector detector) { if (currentScale >= smallScale && !isEnlarge) { isEnlarge = !isEnlarge; } // 缩放因子 缩放后 / 缩放前 // eg 放大后=10,放大前=5, 缩放因子 == 10 / 5 == 2 currentScale = initScale * detector.getScaleFactor(); invalidate(); return false; } // 开始缩放 @Override public boolean onScaleBegin(ScaleGestureDetector detector) { initScale = currentScale; return true; } //结束缩放 @Override public void onScaleEnd(ScaleGestureDetector detector) { } } class flingRunner implements Runnable{ @Override public void run() { // TODO 用 overScroller 计算当前的偏移,并赋值给offsetX, offsetY if (overScroller.computeScrollOffset()) { // computeScrollOffset()会返回一个boolean值,为true, 说明动作还没完成,以此来作为循环结束条件 offsetX = overScroller.getCurrX(); offsetY = overScroller.getCurrY(); invalidate(); //在上面的onFling 方法里面,postOnAnimation 只会调用一次,所以我们这里再调用,参数:自己(flingRunner) //TODO postOnAnimation 下一帧动画的时候执行 postOnAnimation(this); } } } /** * 计算图片滑动的边界情况 * TODO 当往某个方向滑动图片时,放大后的图片边界与手机屏幕边界重合时,就不能滑动了 */ private void fitOffsets(){ offsetX = Math.min(offsetX, (bitmap.getWidth() * bigScale - getWidth()) / 2); offsetX = Math.max(offsetX, -(bitmap.getWidth() * bigScale - getWidth()) / 2); offsetY = Math.min(offsetY, (bitmap.getHeight() * bigScale - getHeight()) / 2); offsetY = Math.max(offsetY, -(bitmap.getHeight() * bigScale - getHeight()) / 2); } private ObjectAnimator getScaleAnimator(){ if (scaleAnimator == null) { scaleAnimator = ObjectAnimator.ofFloat(this, "currentScale", 0); } // TODO 平滑的范围,从 smallScale --> bigScale scaleAnimator.setFloatValues(smallScale, bigScale); return scaleAnimator; } public float getCurrentScale() { return currentScale; } public void setCurrentScale(float currentScale) { this.currentScale = currentScale; // 每一次在 smallScale -- bigScale 直接变化时都刷新 invalidate(); } }
到此这篇关于Android自定义PhotoView使用教程的文章就介绍到这了,更多相关Android PhotoView内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!