一看就懂的自定义View -- 水波纹圆形进度球

在开发过程中,为了丰富我们的软件功能,我们常常需要很多控件、视图,虽然android为我们提供了很多控件,但是都很普通,功能也不够强大,所以,一个自定义的view便成为了我们在开发过程中必须要掌握的。
看完这篇文章,我相信大家会对自定义view有一个初步的认识,也能够打开自定义view的大门。
本文从自定义view的应用出发,对原理不作深究。
本文解释详尽,保证一看就懂,不懂你来找我。
首先看下最终的效果图:
这里写图片描述
由于手机系统版本太低了,录不了屏,想看动图可以导入我放在github上的源码查看,地址在最后。

一. 自定义View一般步骤

  • 继承View或ViewGroup,然后重写里面的三个基本方法

1.onMeasure() // 用来测量自定义View的长宽
2.onLayout() // 用来计算自定义View的位置
3.onDraw() // 用来绘制自定义控件

这是三个基本的方法,必须要重写的,我们将要学习的这个自定义控件最主要用到代码都在onDraw里面,我们会用画布和画笔一点点绘制这个动态的水波进度球。

4.自定义View类的构造方法 // 分别是带1、2、3、4个参数的构造方法,第一个和第二个构造方法很必要,不然运行会报错的

  • 定义自定义View属性
    就像我们在使用android官方的控件时一样,View一般会有自己的专有属性,而这个属性就是定义在style文件中的
<declare-styleable name="自定义View类名">
        <attr name="属性名" format="属性值接受的类型"/>
</declare-styleable>
  • 这些都做完就可以直接在布局文件中使用了
<自定义View所在完整包名.自定义View类名
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_margin="30dp"
        app:color="#2a70e9"
        app:defalt_level="400"
        app:radius="300"
        app:text="66%"/>

可以看到上面带有app命名空间的属性就是我们自定义的属性,需要在根节点中声明xmls,android studio中会自动生成,快捷键Alt+Enter

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    xmlns:app="http://schemas.android.com/apk/res-auto" // 就是这一行
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    tools:context="com.demo.sisyphus.myapplication.MainActivity">

这就是最简单的自定义View过程,下面我们就按照上面这个流程,图文并茂地学习这个自定义View。

二. 自定义水波纹进度球

  • 准备工作

    有过开发经验的都知道,android和html一样,屏幕左上角为原点,向下为Y正轴,向右为X正轴
    这里写图片描述
    把手机屏幕看作是数学坐标系里的第一象限,这对理解下面的原理很重要。
    从上面的效果图我们可以分析出水波顶部抽象出来就是一条正弦图像,而运动中的水波就是不断地去平移这条正弦图像,如下:
    这里写图片描述
    正弦公式为:y = Asin(Bx+C)+D;

A – 振幅
B – 周期
C – 左右偏移量,左右平移就不断自加这个数
D –上下偏移量,上下移动就自加它

得到这条正弦图像的y坐标值,将它存放在一个数组中,待会儿用canvas画图用。代码如下:

period = (float) (2 * Math.PI / radius); // 周期
swing = radius/10; // 振幅
pointOneY = new float[2 * radius]; // 波浪1,Y坐标
pointTwoY = new float[2 * radius]; // 波浪2,Y坐标

for (int i = 0; i < 2 * radius; i++){
     float oneY = (float) (swing * Math.sin(period * i + change) + waterLevel);
     float twoY = (float) ((swing - 5) * Math.sin(period * i + change + 5) + waterLevel);
     pointTwoY[i] = twoY;
     pointTwoY[i] = twoY;
}

当然,只是这个正弦的点是不够的,因为从效果图我们可以看出,这个水波只会显示在圆球的范围内,所以,我们还需要结合圆球的y坐标集,示意图如下:
这里写图片描述
我们可以这样判断,x取值(0,2r),得到对应的圆和正弦y坐标,但是我们只取y坐标相对较大的那一个,于是就得到了上图红色的部分;很快我们又会发现,当水波的高度在圆心以下时,我们就不能取较大的那一个了,而是要取较小的那一个,也就是蓝色部分。所以我们改进得到代码如下:

period = (float) (2 * Math.PI / radius); // 周期
swing = radius/10; // 振幅
pointOneY = new float[2 * radius]; // 波浪1,Y坐标
pointTwoY = new float[2 * radius]; // 波浪2,Y坐标

circleY = new float[2 * radius]; // 圆球Y坐标
int x = radius; // 圆球圆心X坐标
int y = x; // 圆球圆心坐标

for (int i = 0; i < 2 * radius; i++){
     float oneY = (float) (swing * Math.sin(period * i + change) + waterLevel);
     float twoY = (float) ((swing - 5) * Math.sin(period * i + change + 5) + waterLevel);
     if (waterLevel < radius) {
         circleY[i] = (float) (-Math.sqrt(radius*radius - (i-x)*(i-x)) + y);
         pointOneY[i] = circleY[i] >= oneY ? circleY[i] : oneY;
         pointTwoY[i] = circleY[i] >= twoY ? circleY[i] : twoY;
     }else {
            circleY[i] = (float) (Math.sqrt(radius*radius - (i-x)*(i-x)) + y);
            pointOneY[i] = circleY[i] <= oneY ? circleY[i] : oneY;
            pointTwoY[i] = circleY[i] <= twoY ? circleY[i] : twoY;
     }
}

画图的点准备好了,我们可以开始画图了:

private void drawWater(Canvas cavas){
        getWaterLine();
        paint.setAlpha(80);
        paint.setAntiAlias(true);
        float temp = 0;

        for (int i = 0; i < 2 * radius; i++){
            if (waterLevel < radius){
                temp = 2*radius-circleY[i];
            }else {
                temp = circleY[i];
            }
            // 画波浪1
            cavas.drawLine(i, pointOneY[i], i, temp, paint);
            // 画波浪2
            paint.setAlpha(90);
            cavas.drawLine(i, pointTwoY[i], i, temp, paint);
        }
    }

这段代码就是利用canvas的drawLine方法,画一条一条的线,从点(x1,y1)画到点(x2,y2),上上面的代码我们解决了x1、y1、x2的值,但是我们还没有得到进度球底部的y轴坐标,也就是y2。而上面这段代码中的其中一段代码就是解决这个问题的:

if (waterLevel < radius){
    temp = 2*radius-circleY[i];
}else {
    temp = circleY[i];
}

结合示意图,这段代码很好理解。
然后我们来绘制进度球中间的文本,代码如下:

private void drawText(Canvas canvas){
        Paint p = new Paint();
        p.setColor(textColor);
        p.setTextSize(textSize);
        // 获取所要画的文本所占像素宽度,下面计算中间位置备用
        float length = p.measureText(text); 
        // 将文本绘制在圆球正中间的位置
        canvas.drawText(text, radius - (length / 2), radius+textSize/2, p);
    }

有注释,我就不做过多的说明了。
整个水波绘制完成了,但还不是我们最终想要的结果,我们还要让他动起来。细心的朋友可以发现,我们在上面计算y坐标的函数中有一个变量change,他就起到了上面我们所说的偏移量的左右,加减它可以让我们的正弦左右移动,起到水波动起来的作用。我们激活一个线程去执行改变change,代码如下:

public void startAnimation(){
        new Thread(){
            int change = 0;
            @Override
            public void run() {
                super.run();
                while (change <= radius){
                    if (change == radius){
                        change = 0;
                    }
                    change += 1;
                    setChange(change);
                    postInvalidate();
                    try {
                        Thread.sleep(200);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        }.start();
    }

可以修改这个线程中的参数,让水波运动得更流畅。
最后要注意的是,这里面所涉及的水波颜色,圆球半径,初始水位高度,文字等等都是我们的自定义View属性,我们需要在构造函数中获取一下用户填入的值:

public MyLoader(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
        TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.MyLoader);
        radius = typedArray.getInt(R.styleable.MyLoader_radius, 50);
        waterColor = typedArray.getColor(R.styleable.MyLoader_color, Color.BLUE);
        waterLevel = (2 * radius) - typedArray.getInt(R.styleable.MyLoader_defalt_level, radius/2);
        text = typedArray.getString(R.styleable.MyLoader_text);
        textSize = typedArray.getDimensionPixelSize(R.styleable.MyLoader_text_size, radius/4);
        textColor = typedArray.getColor(R.styleable.MyLoader_text_color, Color.GRAY);
        typedArray.recycle(); // 最后记得把这个对象回收了,避免造成溢出
    }

获取用户设置的属性值可以用上面的TypedArray对象,也可以直接用传入的参数attr,循环遍历xml配置文件得到。当然要让用户能够设置这些属性值我们需要在style中声明这些属性,也就是自定义View中的第二步:

<declare-styleable name="MyLoader">
        <attr name="radius" format="integer"/>
        <attr name="color" format="color"/>
        <attr name="defalt_level" format="integer"/>
        <attr name="text_size" format="dimension"/>
        <attr name="text_color" format="integer"/>
        <attr name="text" format="string"/>
</declare-styleable>

最后的最后我们就可以在布局文件中畅快地使用了。
终于写完了,你也终于看完了,第一遍没懂没关系,结合示意图,慢慢消化一下,开启你更多酷炫自定义View的第一步。
想查看完整项目,请使劲儿戳我,下载源码。
如果觉得写得还行,欢迎star。

猜你喜欢

转载自blog.csdn.net/qq_26525715/article/details/65630371
今日推荐