每周学一个小轮子之 可以缩放的ScalableView

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接: https://blog.csdn.net/rikkatheworld/article/details/97885333

开一个每周学一篇小轮子的blog,督促自己掌握一些别人写的好的轮子。希望自己再忙也要来坚持写(至少两周写一篇对自己来说有点质量的)。

demo我也放在GitHub上了,希望老哥如果学到了可以点一个star,能有一个都是我学下去的动力了呜呜呜呜呜呜呜。

GitHub的Demo地址~~~~

这周学习的是一个可以支持 单指移动、双指缩放、双击缩放的 ScalableView。

先上个效果图:
在这里插入图片描述 在这里插入图片描述

主要用到的帮助类为:

  • GestureDetectorCompat
    GestureDetector的兼容类,所以用这两个的任意一个都没有关系,当然了,既然出了兼容类,那用用也没有什么关系。
    它是手势监听类关于一些单击、双击的大管家类,一般用于onTouchEvent的拦截。
  • GestureDetector.SimpleOnGestureListener
    它是上面那个大管家类的内部类,里面有每个手势对应的方法,我们只需重写,并将其构造在大管家上,就能实现我们想要的效果。SimpleOnGestureListener是同时实现了单击和双击的手势,比我们分别去实现GestureDetector.OnDoubleTapListenerGestureDetector.OnGestureListener要简单的多。
    注意,我们需要在 onDown方法返回true,原理和onTouchEvent一样,如果不是true,就接收不到后面的事件了。
  • ScaleGestureDetector
    双指缩放的精髓类,它是Android专门用于解决双指缩放下缩放系数变化的API,它是个大管家类,用于onTouchEvent的拦截。
    这里注意一点,它也有个ScaleGestureDetectorCompat类,但是这个类和它已经是两码事了,所以说不上是兼容类。可能只是个扩充。
  • ScaleGestureDetector.OnScaleGestureListener
    缩放监听类,可以监听到缩放开始、缩放时、缩放结束的状态,我们需要重写三个类,并且在 onScaleBegin返回true,原理和onTouchEvent一样。
  • OverScroller
    回弹Scroller,它和Scroller类的区别就是它可以设置回弹边界,所以喜欢谁就用谁,因为它们的计算API都是一样的,用法上几乎没有区别。
  • postOnAnimation(Runnable action)
    配合OverScroller食用更加美味,它的作用是展现动画的下一帧,也就是说,我们在滑动图片的时候,我们需要在滑动的过程中通过 OverScroller去计算每一帧的滑动速度、坐标,同时又要让其展现出来,所以我们需要在算完每一帧的时候,通过postOnAnimation去画出来~有没有点像异步处理,Handler什么的啊哈哈哈哈

1、缩放的依据是什么?
缩放的依据是什么?就是我们根据什么来缩放,先要理清这个东西特别特别的重要。因为我们所有方法的代码都要根据这个缩放依据来进行操作(比如做动画、做画布平移),而如果选择一个不好的缩放依据,会给我们留下很多的坑。

正常的缩放依据选择有两个:

  1. 缩放比例
    缩放比例很简单,范围是 0-100%,以百分比形式呈现,0%就是原始大小,100%就是放大的最大倍数。
    这样的依据比较直观,也可行。
  2. 缩放倍数(系数)
    放大倍数,范围在 最初的放大倍数—最大的放大倍数。

我这里选择的是第二个。原因是ScaleGestureDetector.OnScaleGestureListener里的onScale是我们双指捏撑的重要方法,它的getScaleFactor()能够提供当前的缩放系数,比如说我在一个 1倍->3倍的放大过程中,这个方法能够返回能够直接和缩放倍数挂钩。

2、原始图片是怎么样的?放大后的图片又是怎么样的?
Bitmap是我们要缩放的图片,而view是这个bitmap的容器,那么Bitmap应该要在这个View居中显示更符合实际情况。
其次原始图片时怎样的?我们需要留空吗?比如一个图片可能才 300*200,你让他居中显示,它左右上下都会留白。

通过查看多个App的情况,缩小的状态其实就是 大的那一边填充,放大状态的就是小的那一边填充:

  1. 如果图片比例是 宽大于高的
    那么缩小状态应该是左右填充屏幕,放大状态应该是上下边填充屏幕
    这样的话,最小缩放系数就是 view.getWidth()/bitmap.getWidth(),最大缩放系数就是 getHeight() / mBitmap.getHeight()
  2. 如果图片比例是 高大于宽的
    那么缩小状态应该是上下填充屏幕,放大状态应该是左右填充屏幕
    缩小/放大系数反之。

其实这个解释不是很难,大家找幅图,然后拿着图片去Activity里面试一下,再和别的App的比较一下就Ok了。

自定义流程

OK,有了这些我们可以去开始做我们的ScalableView了。

第一步、继承自View,初始化所有变量
为了去除干扰因数,我选择继承自View而不是 ImageView。
下面是用到的所有变量,变量是边做边产生的:

public class RikkaScalableView extends View {
    //初始化Bimap的宽度
    private float imageWidth = Utils.dpToPixel(300);

    //Bitmap图片
    private Bitmap mBitmap;

    //画笔
    private Paint mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);

    //初始化bitmap偏移量X、Y值,让bitmap居中的偏移量
    float originalOffsetX, originalOffsetY;

    //偏移量 X、Y值
    float offsetX, offsetY;

    //旧的放大系数,和currentScale对应,用于放大前保存当前放大系数
    float oldScale;

    //放大倍数、缩小倍数
    float bigScale, smallScale;

    //放大的倍数还要再大个1.5倍
    private float bigScaleMore = 1.5f;

    //用currentScale表示当前放大倍数,而且之后用到的缩放、动画的差值都是这个以这个值为标准
    private float currentScale;

    //当前是否是放大状态
    private boolean isBig = false;

    //放大和缩小的动画,因为缩小动画就是放大动画的镜像,所以可以直接用放大动画的reverse来做
    private ObjectAnimator bigAnimator, smallAnimator;

    //手势缩放对象
    private GestureDetectorCompat gestureDetector;

    //自定义手势监听器
    private RikkaGestureListener rikkaGestureListener = new RikkaGestureListener();

    //手势缩放对象
    private ScaleGestureDetector scaleDetector;

    //自定义手势缩放监听器
    private RikkaScaleListener rikkaScaleListener = new RikkaScaleListener();

    //用OverScroller去计算滑动值,可以设置回弹动画
    private OverScroller scroller;

    //postOnAnimation里的Runnable
    private RikkaRunnable rikkaRunnable;
}

在上面我们自定义了监听器。

第二步:将图片居中显示,求出最小缩放和最大缩放系数
图片就是我们的bitmap,居中指的是在View里面居中,我们需要在View测量好自己的大小之后,获取宽高来得到正中间的位置。我们可以在 layout()或者onSzieChanged()方法中获取,这个时候view的大小已经测量好了。
同时,我们也可以在这个时候算出:最小缩放系数和最大缩放系数。
算出来后,我们通过 canva.scale()来进行缩放。

 //onSizeChanged表示view已经初始化完了,我们在这里初始化一些比例
    @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        super.onSizeChanged(w, h, oldw, oldh);

        //计算初始偏移量,让图片居中显示,这也表明了,我们将缩放的坐标原点,放在了View的正中心
        //偏移是以bitmap的左上角为点
        originalOffsetX = (getWidth() - mBitmap.getWidth()) / 2;
        originalOffsetY = (getHeight() - mBitmap.getHeight()) / 2;

        //计算bigScale,smallScale
        //smallScale是初始状态,要求图片:(1)如果图片比例宽大于高的话就是左右贴屏幕的边,(2)如果图片比例高大于宽的话就要上下贴屏幕的边
        //bigScale是放大后的状态,要求图片:(1)如果图片比例宽大于高的话就是上下贴屏幕的边,(2)如果图片比例高大于宽的话就要左右贴屏幕的边
        if ((float) mBitmap.getWidth() / mBitmap.getHeight() > (float) getWidth() / getHeight()) {
            //表示当前图片比例宽大于高
            smallScale = (float) getWidth() / mBitmap.getWidth();
            bigScale = (float) getHeight() / mBitmap.getHeight() * bigScaleMore;
        } else {
            //表示当前图片比例高大于宽
            smallScale = (float) getHeight() / mBitmap.getHeight();
            bigScale = (float) getWidth() / mBitmap.getWidth() * bigScaleMore;
        }
        //初始放大倍数等于smallScale(最小)
        currentScale = smallScale;
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        
        //这里用scale()方法来控制画布放大还是缩小
        canvas.scale(currentScale, currentScale, getWidth() / 2f, getHeight() / 2f);
        canvas.drawBitmap(mBitmap, originalOffsetX, originalOffsetY, mPaint);
    }

第三步:实现双击缩放,并且可以进行单指移动画布
我们首先要去实现自定义的监听器,

    ...
     gestureDetector = new GestureDetectorCompat(context, rikkaGestureListener);
     ....
     
    //继承自SimpleOnGestureListener,这里面做了所有的单手势监听
    class RikkaGestureListener extends GestureDetector.SimpleOnGestureListener {
        @Override
        public boolean onDown(MotionEvent e) {
            return true;
        }

        //双击放大、缩小
        @Override
        public boolean onDoubleTap(MotionEvent e) {
            isBig = !isBig;
            if (isBig && currentScale < bigScale) {
                //记录双击的位置,在这个点上放大,要把原点放在View的正中心,因为我们的图片时居中显示的。
                offsetX = (e.getX() - getWidth() / 2) - ((e.getX() - getWidth() / 2) * bigScale / smallScale);
                offsetY = (e.getY() - getHeight() / 2) - ((e.getY() - getHeight() / 2) * bigScale / smallScale);
                notOutBound();
                //如果当前是放大状态,就做放大动画
                getBigAnimator().start();
            } else {
                //如果是缩小状态,就做缩小动画
                getSmallAnimator().start();
            }
            return false;
        }
    }

    //offsetX、offsetY不能超出边界
    private void notOutBound() {
        offsetX = Math.max(offsetX, -((mBitmap.getWidth() * bigScale - getWidth()) / 2f));
        offsetX = Math.min(offsetX, (mBitmap.getWidth() * bigScale - getWidth()) / 2f);
        offsetY = Math.max(offsetY, -((mBitmap.getHeight() * bigScale - getHeight()) / 2f));
        offsetY = Math.min(offsetY, (mBitmap.getHeight() * bigScale - getHeight()) / 2f);
    }

然后设置放大缩小动画,因为我们是根据currentScale来缩放的,所以我们的属性动画也是根据这个属性来做,动画每次设置一次currentScale,我们就需要invalidate()一次,并且将画布平移到 offsetX,offsetY的位置。
在每次做缩小动画的时候,需要重置一下offsetX/Y,否则下次双击,会导致放大到上一次的偏移位置上了。


  @Override
    protected void onDraw(Canvas canvas) {
        ...
        //scalingFraction为放大倍数的百分比
        float scalingFraction = (currentScale - smallScale) / (bigScale - smallScale);
        canvas.translate(offsetX * scalingFraction, offsetY * scalingFraction);
        ...
    }
   public float getCurrentScale() {
        return currentScale;
    }

    public void setCurrentScale(float currentScale) {
        this.currentScale = currentScale;
        //变化的时候要重绘
        invalidate();
    }

    //放大动画
    public ObjectAnimator getBigAnimator() {
        if (bigAnimator == null) {
            //懒加载缩放动画,用currentScale缩小到放大,值域在smallScale -> bigScale之间
            bigAnimator = ObjectAnimator.ofFloat(this, "currentScale", currentScale, bigScale);
        }
        //因为每次的currentScale都会变化,每次拿都要重新设置一遍currentScale
        bigAnimator.setFloatValues(currentScale, bigScale);
        return bigAnimator;
    }

    //缩小动画
    public ObjectAnimator getSmallAnimator() {
        if (smallAnimator == null) {
            //懒加载缩放动画,用currentScale缩小到放大,值域在smallScale -> bigScale之间
            smallAnimator = ObjectAnimator.ofFloat(this, "currentScale", currentScale, smallScale);
            smallAnimator.addListener(new Animator.AnimatorListener() {
                @Override
                public void onAnimationStart(Animator animation) {

                }

                @Override
                public void onAnimationEnd(Animator animation) {
                    //每次动画结束的时候要将offsetX/Y置为初始值
                    offsetX = 0;
                    offsetY = 0;
                }

                @Override
                public void onAnimationCancel(Animator animation) {

                }

                @Override
                public void onAnimationRepeat(Animator animation) {

                }
            });
        }
        smallAnimator.setFloatValues(currentScale, smallScale);
        return smallAnimator;
    }

这里算是实现了双击放大缩小了,但是还不能进行平移,因为我们没有设置 onScroll和onFling,我们再去监听器里面实现:

    //继承自SimpleOnGestureListener,这里面做了所有的单手势监听
    class RikkaGestureListener extends GestureDetector.SimpleOnGestureListener {
        ...
        //滑动的时候通过 dX,dY去计算 偏移值offsetX和offsetY
        //而且只有在放大的状态才能滑动,不然缩小状态滑动会超出边界
        @Override
        public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
            offsetX -= distanceX;
            offsetY -= distanceY;
            notOutBound();
            invalidate();
            return false;
        }

        //惯性滑动,让图片滑动不要那么的僵硬,使用OverScroll去计算滑动值,postOnAnimation去更新滑动动画,滑动速度由比例决定
        @Override
        public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
            scroller.fling((int) offsetX, (int) offsetY, (int) velocityX, (int) velocityY,
                    (int) -(mBitmap.getWidth() * bigScale - getWidth()) / 2,
                    (int) (mBitmap.getWidth() * bigScale - getWidth()) / 2,
                    (int) -(mBitmap.getHeight() * bigScale - getHeight()) / 2,
                    (int) (mBitmap.getHeight() * bigScale - getHeight()) / 2);
            postOnAnimation(rikkaRunnable);
            return false;
        }
    }

    //用于 postOnAnimation的runnable
    class RikkaRunnable implements Runnable {
        @Override
        public void run() {
            if (scroller.computeScrollOffset()) {
                offsetX = scroller.getCurrX();
                offsetY = scroller.getCurrY();
                invalidate();
                //继续计算下一帧,因为这个Rnnable只能计算一帧
                postOnAnimation(rikkaRunnable);
            }
        }
    }

到这里,双击缩放、单指滑动画布的效果就实现了。

第四步,实现双指滑动
我们先要去实现以下自定义的缩放监听器:

    ....
    scaleDetector = new ScaleGestureDetector(context, rikkaScaleListener);
    ...

    //自定义的缩放监听器
    class RikkaScaleListener implements ScaleGestureDetector.OnScaleGestureListener {

        @Override
        public boolean onScale(ScaleGestureDetector detector) {
            currentScale = oldScale * detector.getScaleFactor();
            currentScale = Math.max(smallScale, currentScale);
            currentScale = Math.min(currentScale, bigScale * 2);
            invalidate();
            return false;
        }

        @Override
        public boolean onScaleBegin(ScaleGestureDetector detector) {
            //这里要记录缩放之前的放大系数,不然后面的乘法就会呈指数倍增长
            oldScale = currentScale;
            //这里一定要设置为true,它和down一样
            return true;
        }

        @Override
        public void onScaleEnd(ScaleGestureDetector detector) {

        }
    }

缩放的API其实已经很完善了,所以我们只要这样做,就已经完全实现了双指缩放了哈哈哈啊哈哈哈

第五步、在onTouchEvent中进行拦截
因为我们有两个 手势,一个是GestureDetector,另一个是ScaleGestureDetector,我们要怎么去同时使用他们呢?
ScaleGestureDetector提供了这么一个方法isInProgress()表示是否正在使用双指缩放,所以,当不是的时候,我们就使用双击缩放,代码如下:

  @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);

        //scalingFraction为放大倍数的百分比
        float scalingFraction = (currentScale - smallScale) / (bigScale - smallScale);
        canvas.translate(offsetX * scalingFraction, offsetY * scalingFraction);
        //这里用scale()方法来控制画布放大还是缩小
        canvas.scale(currentScale, currentScale, getWidth() / 2f, getHeight() / 2f);
        canvas.drawBitmap(mBitmap, originalOffsetX, originalOffsetY, mPaint);
    }

Ok,到这里就已经算是做出来一个小Demo啦,(本来就是为了公司的新需求学的,这个地方我虽然之前有了解过,但是做起来还是挺绕来绕去的,真的,有时候写着写着,就在骂自己数学为什么这么差!)

猜你喜欢

转载自blog.csdn.net/rikkatheworld/article/details/97885333