图解PhotoView,从“百草园”到“三味书屋”!

0c7d3f67ab54f02eb097ed5c8a6f410b.png

/   今日科技快讯   /

近日,胡润研究院今日发布《2021胡润百富榜》。农夫山泉67岁的钟睒睒财富比去年增长250亿,即7%,以3,900亿元首次成为中国首富。抖音创始人38岁的张一鸣财富增长至去年的3倍,增长了2,300亿,超过马化腾和马云,以3,400亿元跃居第二。 

/   作者简介   /

本篇文章来自android超级兵的投稿,文章主要分享了如何实现自定义PhotoView,相信会对大家有所帮助!同时也感谢作者贡献的精彩文章。

android超级兵的博客地址:

https://blog.csdn.net/weixin_44819566

/   正文   /

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

先来看看今天的效果图:

1d0bdef969b49581cebc9d4dddcbd54a.gif

需求

图片

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

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

双击

放大/缩小,放大后可单指移动

双指

放大

最小缩小不能小于初始图片,最大方法不能大于图片的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;
    }
}

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

68f0fee961de03c794b247c84b674200.png

图片居中

众所周知,在自定义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);
    }

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

36ff98ad0a456bcea803737d3373de92.png

当前的效果:

6888441e892eeeea4d4e83c3bafe84bf.png

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

图片放大

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

需求一

图片

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

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

需求一辅助图:

59d9be2364dbcdaae4fa67d4287f850d.png

d4e366f0e34f5611c02ad6433d2c07da.png

先来看代码:

#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还不懂是什么?以横向图片为例,带入参数,一张图搞懂!

86a631445f02b0a95742a667314b6c5e.png

  • 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是为了防止图片过小从而没有超出整个屏幕。

当前的效果

ff5d9499264d611063eb807f17012ef0.png

双击放大

提到双击放大,就不得不提到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);
}

当前的效果

0ad8f5f2f0d6bc3a6f768cab137b6043.gif

双击放大添加动画

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

#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();
    }

当前的效果

a93cbbfd8f07541189a030592eb97853.gif

放大后图片滑动

这里为了代码规范,行,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轴举例:

0101f42414ce6abc0bd433d601178a7a.png

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

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);
}

效果

9d71b1b07cb664dad347df9bb7f8c69e.png

得出结论

想要图片想左移动,设置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.getX(), moveOffset.getY());
        .... 其余代码...

    }

当前的效果

3c297e93e5161c341066d10c8cdfb464.gif

图片放大状态操作

先来看看我说的这句图片放大状态操作是什么意思,直接看图。

0a19a9c551bedcc8e20ea7f86a99bf92.gif

其实就是放大状态,禁止出现白边,使得用户体验更高!

98681ce4e38473a9e8399f494696d5ca.png

来看看代码吧:

#PohtoView2.java

 public void fixOffset() {
        // 当前图片放大后的宽
        float currentWidth = mBitMap.getWidth() * bigScale;
        // 当前图片放大后的高
        float currentHeight = mBitMap.getHeight() * bigScale;

        // 右侧限制
        moveOffset.setX(Math.max(moveOffset.getX(), -(currentWidth - getWidth()) / 2));

        // 左侧限制 [左侧moveOffset.getX()为负数]
        moveOffset.setX(Math.min(moveOffset.getX(), (currentWidth - getWidth()) / 2));

        // 下侧限制
        moveOffset.setY(Math.max(moveOffset.getY(), -(currentHeight - getHeight()) / 2));

        // 上侧限制 [上侧moveOffset.getY()为负数]
        moveOffset.setY(Math.min(moveOffset.getY(), (currentHeight - getHeight()) / 2));
    }

在滑动过程中onScroll()限制一下即可!

#PhotoGestureListener.java

         // 滑动时候触发 类似 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);
                // moveOffset.x -= distanceX;
                // moveOffset.y -= distanceY;

                // 禁止图片滑动到屏幕外面
                fixOffset();

                invalidate();
            }

            return super.onScroll(e1, e2, distanceX, distanceY);
        }

这段代码需要细细品味一下!

当前的效果

ac3c3efecfaa423ae6cc48c0e284e3f3.gif

双击放大到具体位置

名字很抽象,先来看看效果图:

a152b55a339de40cecb6c9fd47b9cf4c.gif

辅助图

fae07cf40aa89fb33033b80d967fcb5f.png

实现思路

当是小图片时候需要双击放大,只需要求出双击的位置到双击放大后对应的位置的距离,然后在平移过去即可。由上图可知,缩小状态下双击的位置到辅助线A的距离等于e.getX()减去getWidth()的二分之一。由于大图的宽等于getWidth()乘以bigScale。

同理,所以大图对应小图点击的位置就是(e.getX() - getWidth() / 2) * bigSale。那么缩小状态下双击的位置到放大后对应的位置等于e.getX() - getWidth() / 2 - (e.getX() - getWidth() / 2) * bigSale。在双击放大时候进行移动即可:

#PhotoGestureListener.java

        @Override
        public boolean onDoubleTap(MotionEvent e) {
            isDoubleClick = !isDoubleClick;
            if (isDoubleClick) {
                float currentX = e.getX() - (float) getWidth() / 2f;
                float currentY = e.getY() - (float) getHeight() / 2f;

                moveOffset.setX(currentX - currentX * bigScale);
                moveOffset.setY(currentY - currentY * bigScale);

                // 重新计算,禁止放大后出现白边[具体实现在[图片放大状态操作]中娓娓道来过]
                fixOffset();

                scaleAnimation(currentScale, bigScale).start();
            } else {
                scaleAnimation(bigScale, smallScale).start();
            }
            return super.onDoubleTap(e);
        }

来看看效果

4d34db7f7c9070d729293afd89365755.gif

shit,这…是什么…好像从某种意义上来说也是对的,最起码点击时候,平移是正确的,冷静分析,看看是什么问题…

经过长达30分钟的思考,终于知道是为什么了。问题就在于,直接双击的时候,直接就计算出了小图与大图之前的距离,然后底下还有一个缩放的动画,所以导致这种情况发生,只要让moveOffset也是跟随着缩放动画来改变即可!

目前知道的双击放大缩小条件有:

  • 双击放大是从currentScale -> bigScale的改变

  • 双击缩小是从bigScale -> smallScale的改变

这里引出了一个小算法:

float a = (currentScale - smallScale) / (bigScale - smallScale);

假设当前是从小图缩放到大图 也就是从currentScale -> bigScale的改变。当currentScale等于bigScale时候证明已经放大最大。所以(currentScale - smallScale) / (bigScale - smallScale)等于1。

否则的话:

双击放大

(currentScale - smallScale) / (bigScale - smallScale)就是从0 - 1的状态改变

双击缩小

(currentScale - smallScale) / (bigScale - smallScale) 就是从1 - 0的状态改变

来看看代码如何写:

#PhotoView2.java

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

        ....省略了好多代码....

}

这段代码需要细细品。

当前的效果

412cec08464d4c499b7da92e89a4f96f.gif

图片放大状态加入Fling效果

先来看看要实现的效果:

9b473e1b6829afdfb70c5fab12fe9a14.gif

既然说到fling,那就得借助android中fling的类OverScroller。使用很简单,纯调api的代码。

#PhotoView2.java

    // 惯性滑动
    private final OverScroller mOverScroller;
    @SuppressLint("CustomViewStyleable")
    public PhotoView2(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);

        //  惯性滑动
        mOverScroller = new OverScroller(context);
    }

在onFling事件中调用:

#PhotoGestureListener.java

         // 滑翔/飞翔 [惯性滑动]
        @Override
        public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
            /*
             * int startX       滑动x
             * int startY,      滑动y
             * int velocityX,   每秒像素为单位[x轴]
             * int velocityY,   每秒像素为单位[y轴]
             * int minX,        宽最小值
             * int maxX,        宽最大值
             * int minY,        高最小值
             * int maxY,        高最大值
             * int overX,       溢出x的距离
             * int overY        溢出y的距离
             * 
             * 这里可以理解为吧滑动距离保存在了mOverScroller.fling()中
             */
            mOverScroller.fling(
                    (int) moveOffset.getX(),
                    (int) moveOffset.getY(),
                    (int) velocityX,
                    (int) velocityY,
                    (int) (-(mBitMap.getWidth() * bigScale - getWidth()) / 2),
                    (int) ((mBitMap.getHeight() * bigScale - getWidth()) / 2),
                    (int) (-(mBitMap.getHeight() * bigScale - getHeight()) / 2),
                    (int) ((mBitMap.getHeight() * bigScale - getHeight()) / 2),
                    300,
                    300
            );
            return super.onFling(e1, e2, velocityX, velocityY);
        }

来看看效果

7ad99fa0edd833cbb8389a445169cbc7.gif

这…好像有点拉,并没有实现想要的效果…通过打印log知道,onFling()过程中他只会执行一次。所以需要吧保存在mOverScroller.fling()中的值取出来。

#PhotoView2.java

    // 惯性滑动辅助
    class FlingRunner implements Runnable {

        @Override
        public void run() {
            // 判断当前是否是执行
            if (mOverScroller.computeScrollOffset()) {
                // 设置fling的值
                moveOffset.setX(mOverScroller.getCurrX());
                moveOffset.setY(mOverScroller.getCurrY());
                Log.i("szjFlingRunner", "X:" + mOverScroller.getCurrX() + "\tY:" + mOverScroller.getCurrY());

                // 继续执行FlingRunner.run
                postOnAnimation(this);
                // 刷新
                invalidate();
            }
        }
    }

还是在构造中初始化。

#PhotoView2.java

    // 辅助惯性滑动类
    private final FlingRunner mFlingRunner;
@SuppressLint("CustomViewStyleable")
    public PhotoView2(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        ....省略了...
        // 惯性滑动辅助类
        mFlingRunner = new FlingRunner();

    }
#PhotoGestureListener.java

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

            Log.i("szjOnFling", "velocityX:" + velocityX + "\tvelocityY" + velocityY);

            /*
             * int startX       滑动x
             * int startY,      滑动y
             * int velocityX,   每秒像素为单位[x轴]
             * int velocityY,   每秒像素为单位[y轴]
             * int minX,        宽最小值
             * int maxX,        宽最大值
             * int minY,        高最小值
             * int maxY,        高最大值
             * int overX,       溢出x的距离
             * int overY        溢出y的距离
             */
            mOverScroller.fling(
                ....太长了,已被省略...
                );

            // 设置fling效果
           mFlingRunner.run();

            return super.onFling(e1, e2, velocityX, velocityY);
        }

当前的效果

2a6c0572541a76bd4a1b43ab11c71047.gif

双指操作

双指操作还是继续使用android中自带的。

class PhotoDoubleScaleGestureListener implements ScaleGestureDetector.OnScaleGestureListener {
        // 在双指操作开始时候获取当前缩放值
        private float scaleFactor = 0f;


        // 双指操作
        @Override
        public boolean onScale(ScaleGestureDetector detector) {
            // detector.getScaleFactor 缩放因子
            currentScale = scaleFactor * detector.getScaleFactor();

            // 刷新
            invalidate();
            return false;
        }

        // 双指操作开始
        @Override
        public boolean onScaleBegin(ScaleGestureDetector detector) {
            scaleFactor = currentScale;
            // 注意这里要为true 表示开始双指操作
            return true;
        }

        // 双指操作结束
        @Override
        public void onScaleEnd(ScaleGestureDetector detector) {

        }
    }

双指操作还是比较简单的,就是单纯的调调api即可。

双指操作初始化

还是在构造中初始化。

#PohtoView2.java

    // 双指操作
    private final ScaleGestureDetector scaleGestureDetector;

    @SuppressLint("CustomViewStyleable")
    public PhotoView2(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);

         // 双指头操作
        scaleGestureDetector = new ScaleGestureDetector(context, new PhotoDoubleScaleGestureListener());
}

双指操作也需要在onTouchEvent()中初始化。因为双指操作和双击操作都是一样的,都是一个事件。

@Override
    public boolean onTouchEvent(MotionEvent event) {

        // 双指操作
        boolean scaleTouchEvent = scaleGestureDetector.onTouchEvent(event);

        // 是否是双指操作
        if (scaleGestureDetector.isInProgress()) {

            return scaleTouchEvent;
        }

        // 双击操作
        return mPhotoGestureListener.onTouchEvent(event);
    }

现在默认的事件的是双指操作事件优先,其次是双击操作。

当前的效果

de49db241bbfafb53bee22e2cbc8c239.gif

最终优化双击双指操作

可以看到,基本的已经实现了,现在只需要最终限制一下就可以了!这段代码没有什么含金量,直接来看代码:

#PhotoDoubleScaleGestureListener.java

         // 双指操作结束
        @Override
        public void onScaleEnd(ScaleGestureDetector detector) {
            // 当前图片宽
            float currentWidth = mBitMap.getWidth() * currentScale;
            // 缩放前的图片宽
            float smallWidth = mBitMap.getWidth() * smallScale;
            // 缩放后的图片宽
            float bigWidth = mBitMap.getWidth() * bigScale;

            // 如果当前图片 < 缩放前的图片
            if (currentWidth < smallWidth) {
                // 图片缩小
                isDoubleClick = false;

                scaleAnimation(currentScale, smallScale).start();
            } else if (currentWidth > smallWidth) {
                // 图片缩小
                isDoubleClick = false;
            }

            // 如果当前状态 > 缩放后的图片 那么就让他改变为最大的状态
            if (currentWidth > bigWidth) {

                //  双击时 图片缩小
                scaleAnimation(currentScale, bigScale).start();
                // 双击时候 图片放大
                isDoubleClick = true;
            }
        }

最终效果

7d2881148a52b37fc6e97587acf710b7.gif

完整代码地址如下所示:

https://gitee.com/lanyangyangzzz/android_ui

推荐阅读:

我的新书,《第一行代码 第3版》已出版!

带倒计时RecyclerView的设计心路历程

Android 12上焕然一新的小组件

欢迎关注我的公众号

学习技术或投稿

89e530265fcbdb88c8f1daa6c099e087.png

1b4af9bb558f7b49c22d6bc1e3e7dcdb.png

长按上图,识别图中二维码即可关注

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值