专栏名称: 安卓开发精选
伯乐在线旗下账号,分享安卓应用相关内容,包括:安卓应用开发、设计和动态等。
目录
相关文章推荐
鸿洋  ·  Google 为何设计了如此难用的 ... ·  19 小时前  
stormzhang  ·  真正该刺激的是收入 ·  2 天前  
鸿洋  ·  一款高效的HarmonyOS工具包 ·  2 天前  
鸿洋  ·  Android主线程锁监控的一种方案 ·  6 天前  
stormzhang  ·  打工人维权,难吗? ·  1 周前  
51好读  ›  专栏  ›  安卓开发精选

自定义View实现圆形水波进度条(下)

安卓开发精选  · 公众号  · android  · 2016-09-23 08:37

正文

(点击上方公众号,可快速关注)


来源:伯乐在线专栏作者 - Code4Android

链接:http://android.jobbole.com/84776/

点击 → 了解如何加入专栏作者


接上文


通过效果图,我们看到实现此效果就是不断的更新进度值,然后重绘,,那么我们只需开启一个线程实现更新进度值,为了更好的控制我们再加点击事件,当单机时开始增大进度,双击时暂停进度,并弹出Snackbar,其中有一个重置按钮,点击重置时将进度设置为0,重绘界面。


响应点击事件


因为要实现双击事件,我们可以直接用GestureDetector(手势检测),通过这个类我们可以识别很多的手势,主要是通过他的onTouchEvent(event)方法完成了不同手势的识别GestureDetector里有一个内部类 SimpleOnGestureListener。SimpleOnGestureListener类是GestureDetector提供给我们的一个更方便的响应不同手势的类,这个类实现了上述两个接口(OnGestureListener, OnDoubleTapListener,但是所有的方法体都是空的),该类是static class,也就是说它实际上是一个外部类。程序员可以在外部继承这个类,重写里面的手势处理方法


public static class SimpleOnGestureListener implements OnGestureListener, OnDoubleTapListener,

            OnContextClickListener {

//单击抬起

        public boolean onSingleTapUp(MotionEvent e) {

            return false;

        }

//长按

        public void onLongPress(MotionEvent e) {

        }

//滚动

        public boolean onScroll(MotionEvent e1, MotionEvent e2,

                float distanceX, float distanceY) {

            return false;

        }

//快速滑动

        public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX,

                float velocityY) {

            return false;

        }

//

        public void onShowPress(MotionEvent e) {

        }

 

        public boolean onDown(MotionEvent e) {

            return false;

        }

 

        public boolean onDoubleTap(MotionEvent e) {

            return false;

        }

 

        public boolean onDoubleTapEvent(MotionEvent e) {

            return false;

        }

 

        public boolean onSingleTapConfirmed(MotionEvent e) {

            return false;

        }

 

        public boolean onContextClick(MotionEvent e) {

            return false;

        }

    }


下面是我们自定继承SimpleOnGestureListener,由于我们只要响应单击和双击事件,那么我们只需要重写onDoubleTap双击(),onSingleTapConfirmed(单击)方法即可,


public class MyGestureDetector extends GestureDetector.SimpleOnGestureListener {

 

        @Override

        public boolean onDoubleTap(MotionEvent e) {

            getHandler().removeCallbacks(singleTapThread);

            singleTapThread=null;

            Snackbar.make(CustomBall.this, "暂停进度,是否重置进度?", Snackbar.LENGTH_LONG).setAction("重置", new OnClickListener() {

                @Override

                public void onClick(View v) {

                    currentProgress=0;

                    invalidate();

                }

            }).show();

            return super.onDoubleTap(e);

        }

 

        @Override

        public boolean onSingleTapConfirmed(MotionEvent e) {

            Snackbar.make(CustomBall.this, "单机了", Snackbar.LENGTH_LONG).setAction("Action", null).show();

            startProgressAnimation();

            return super.onSingleTapConfirmed(e);

        }

    }


当点击时Snackbar做个提醒单击了View,然后调用startProgressAnimation()方法初始化一个线程,通过postDelayed将线程加入的消息队列,延迟100ms执行,通过singleTapThread == null判断条件,避免过多的创建对象


private void startProgressAnimation() {

        if (singleTapThread == null) {

            singleTapThread = new SingleTapThread();

            getHandler().postDelayed(singleTapThread, 100);

        }

    }


我们将SingleTapThread 实现Runnable接口,在run方法里书写我们的处理逻辑,其实很简单,先判断当前进度值是不是大于最大进度(100),如果小于最大的值,我们就将currentProgress(当前进度值)加1的操作,然后调用invalidate()方法重绘界面,之后还需要再次将线程加入消息队列,依然延迟100ms执行。对于当如果当前进度已经加载到100%,此时我们将此线程从消息队列移除。


 private class SingleTapThread implements Runnable {

        @Override

        public void run() {

            if (currentProgress maxProgress) {

                currentProgress++;

                invalidate();

                getHandler().postDelayed(singleTapThread, 100);

 

            } else {

                getHandler().removeCallbacks(singleTapThread);

            }

        }

    }


接下来还需要注册事件,我们可以在onDraw()方法中通过GestureDetector的构造方法可以将自定义的MyGestureDetector对象传递进去,然后通setOnTouchListener设置监听器,这样GestureDetector能处理不同的手势了


if (detector==null){

            detector = new GestureDetector(new MyGestureDetector());

            setOnTouchListener(new OnTouchListener() {

                @Override

                public boolean onTouch(View v, MotionEvent event) {

                    return detector.onTouchEvent(event);

                }

            });

 

        }


还有最重要的一点是,View默认是不可点击的,所以我们需要 setClickable(true)设置View可点击的,OK,到这里我们就完成的中心进度值得更新,接下来就开始绘制里面的波浪形状,效果图如下


实现水波浪效果


水波纹效果是通过二阶贝塞尔曲线实现的,先简单看下什么是贝塞尔曲线


在数学的数值分析领域中,贝塞尔曲线(英语:Bézier curve)是电脑图形学中相当重要的参数曲线。更高维度的广泛化贝塞尔曲线就称作贝塞尔曲面,其中贝塞尔三角是一种特殊的实例。

贝塞尔曲线于1962年,由法国工程师皮埃尔·贝塞尔(Pierre Bézier)所广泛发表,他运用贝塞尔曲线来为汽车的主体进行设计。贝塞尔曲线最初由Paul de Casteljau于1959年运用de Casteljau算法开发,以稳定数值的方法求出贝塞尔曲线 – – – – -维基百科


  • 线性贝塞尔曲线


给定点P0、P1,线性贝塞尔曲线只是一条两点之间的直线。这条线由下式给出:



绘制效果为



  • 二次方贝塞尔曲线


二次方贝塞尔曲线的路径由给定点P0、P1、P2的函数B(t)追踪:



  • 三次方贝塞尔曲线


P0、P1、P2、P3四个点在平面或在三维空间中定义了三次方贝塞尔曲线。曲线起始于P0走向P1,并从P2的方向来到P3。一般不会经过P1或P2;这两个点只是在那里提供方向资讯。P0和P1之间的间距,决定了曲线在转而趋进P2之前,走向P1方向的“长度有多长”。

曲线的参数形式为:



当然贝塞尔曲线是一个很复杂的东西,他可以延伸N阶贝塞尔曲线,如果想要真正搞明白,想自定义比较复杂或者比较酷炫的动画,那高等数学知识必须要搞明白,很多时候,我们只需要了解二次贝塞尔曲线就可以了,或者说,即使贝塞尔曲线不是那么熟悉,也不用怕,android API 封装了常用的贝塞尔曲线,我们只需要传入坐标就可以实现很多动画。


首先我们需要初始化贝塞尔曲线区域的画笔设置。其中重要的一点就是setXfermode()方法,此方法可以设置与其他绘制图形的交集,合集,补集等运算,在这个项目中,我们使用了交集(绘制贝塞尔曲线区域和圆区域的交集)


 progressPaint = new Paint();

        progressPaint.setAntiAlias(true);

        progressPaint.setColor(progressColor);

        //取两层绘制交集。显示上层

        progressPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC_IN));


初始化画笔后,就开始绘制我们的图形,先初始化一个

宽和高都为radius * 2的正方形画布作为缓冲区画布,我们可以先在缓冲区画布绘制,绘制完成后一次再绘制到画布上。


bitmap = Bitmap.createBitmap((int) radius * 2, (int) radius * 2, Bitmap.Config.ARGB_8888);

 

bitmapCanvas = new Canvas(bitmap);


然后绘制圆心(width / 2, height / 2)半径为radius的圆


bitmapCanvas.drawCircle(width / 2, height / 2, radius, roundPaint);


水波从圆的最下方开始(进度为0),到最上方(进度最大值maxProgress)结束,那么我们需要根据当前进度值动态计算水波的高度


float y = (1 - (float) currentProgress / maxProgress) * radius * 2


如图,我们就可以先将path.lineTo将每个点连起来,可以先从(width,y)绘制,那么需要调用path.moveTo(width, y);方法将操作点移动到该坐标上,接下下就开始依次连接其余三个点(width,height),(0,height),(0,y)。由于我们之前画笔设置的是取交集(progressPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC_IN))),所以此时会绘制与圆相交的部分,也就是圆内的部分。



下面就是绘制贝塞尔曲线


path.rQuadTo(space, -d, space * 2, 0);

path.rQuadTo(space, d, space * 2, 0);


第一个是绘制向下弯曲,第二个是绘制向上弯曲。为了从左到右都绘制曲线,我们根据圆的直径计算一下,需要几次才能平铺,然后循环执行上面两句,直到平铺圆形区域,为了展示当进度增大时将波纹幅度降低的效果(直到进度为100%,幅度降为0)我们根据当前进度值动态计算了幅度值,计算方法如下


float d = (1 - (float) currentProgress / maxProgress) *space;


由于我们需要以实心的方式绘制区域,那么我们调用

path.close();将所画区域封闭,也就是实心的效果。


path.close();

  bitmapCanvas.drawPath(path, progressPaint);


Ok,到这里,自定义的水波形状的进度条就完成了,再次上效果图

(注:此水波左右移动是后来加的效果,具体实现点击代码查看)



由于本人目前水平有限,文字若有不足的地方,欢迎指正,谢谢。


关注「安卓开发精选」
看更多精选安卓技术文章
↓↓↓