Header

        看见一些loading,有时候对于刚入门的开发者来说,当产品中有一些效果需求时,一般会找找别人写的,那么就分享下关于loading和progressbar的自定义,看完就会,很简单,也就不再需要用别人的了,看效果吧

关于自定义控件的一些知识点网上可以搜搜。


android 引用不了自定义控件 android自定义loading_ci

CircleProgressBar

android 引用不了自定义控件 android自定义loading_android 引用不了自定义控件_02

TextProgressBar

android 引用不了自定义控件 android自定义loading_android 引用不了自定义控件_03

WaterRippleProgressBar

 


android 引用不了自定义控件 android自定义loading_ci_04

RoundBallProgressBar

android 引用不了自定义控件 android自定义loading_Data_05

FlyLoadingView

android 引用不了自定义控件 android自定义loading_Data_06

HeartBeatLoadingView

 

Body

    六个效果中,其实所用到的东西都大同小异,大致说一下吧。

  CircleProgressBar

1.绘制灰色背景

2.绘制蓝色圆弧,弧度要使用动态数据

3.绘制中间字体,数据取自动态数据

看代码吧

attrs.xml

<declare-styleable name="CircleProgressBar">
        <attr name="circle_background_color" format="color"/>
        <attr name="circle_color" format="color"/>
        <attr name="circle_text_color" format="color"/>
        <attr name="circle_text_size" format="dimension"/>
    </declare-styleable>
public class CircleProgressBar extends View {

    private int border_length;

    private RectF rectF;

    private Paint background_circle_paint;

    private Paint circle_paint;

    private Paint text_paint;

    private int background_color;

    private int circle_color;

    private int text_color;

    private int text_size = 23;

    private int currentData = 0;

    private int maxData = 100;

    public CircleProgressBar(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
        TypedArray typedArray = getContext().obtainStyledAttributes(attrs,R.styleable.CircleProgressBar);
        background_color = typedArray.getColor(R.styleable.CircleProgressBar_circle_background_color, Color.parseColor("#cccccc"));
        circle_color = typedArray.getColor(R.styleable.CircleProgressBar_circle_color,Color.parseColor("#3333ff"));
        text_color = typedArray.getColor(R.styleable.CircleProgressBar_circle_text_color,Color.parseColor("#3333ff"));
        text_size = (int) typedArray.getDimension(R.styleable.CircleProgressBar_circle_text_size,23);

        initView();
    }

    public CircleProgressBar(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }

    private void initView(){
        background_circle_paint = new Paint();
        background_circle_paint.setStyle(Paint.Style.STROKE);
        background_circle_paint.setColor(background_color);
        background_circle_paint.setAntiAlias(true);

        circle_paint = new Paint();
        circle_paint.setStyle(Paint.Style.STROKE);
        circle_paint.setColor(circle_color);
        circle_paint.setAntiAlias(true);
        circle_paint.setStrokeCap(Paint.Cap.ROUND);

        text_paint = new Paint();
        text_paint.setColor(text_color);
        text_paint.setTextSize(dp2px(text_size));
        text_paint.setAntiAlias(true);
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        int width = MeasureSpec.getSize(widthMeasureSpec);
        int height = MeasureSpec.getSize(heightMeasureSpec);

        border_length = Math.min(width,height);

        rectF = new RectF(border_length * (float)0.1,border_length * (float)0.1,border_length * (float)0.9,border_length * (float)0.9);
        background_circle_paint.setStrokeWidth(border_length * (float)0.05);
        circle_paint.setStrokeWidth(border_length * (float)0.05);

        setMeasuredDimension(border_length,border_length);
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        drawBackgroundCircle(canvas);
        drawCircle(canvas);
        drawText(canvas);
    }

    private void drawBackgroundCircle(Canvas canvas){

        canvas.drawArc(rectF,-90,360,false,background_circle_paint);
        canvas.save();
    }

    private void drawCircle(Canvas canvas){
        float sweepAngle = (currentData/(float)maxData) * (float)360;
        canvas.drawArc(rectF,-90,sweepAngle,false,circle_paint);
        canvas.save();
    }

    private void drawText(Canvas canvas){
        Rect rect = new Rect();
        text_paint.getTextBounds(currentData + "%",0,(currentData + "%").length(),rect);
        canvas.drawText(currentData + "%",getWidth() / 2 - rect.width() / 2,getHeight() / 2 + rect.height() / 2,text_paint);
        canvas.save();
    }

    public void progress(int data){
        if (data < 0){
            data = 0;
        }
        if (data > maxData){
            data = maxData;
        }
        currentData = data;
        invalidate();
    }

    public void goToProgress(int data){
        if (data < 0){
            data = 0;
        }
        if (data > maxData){
            data = maxData;
        }
        final ValueAnimator animator = ValueAnimator.ofFloat(currentData,data);
        animator.setDuration((long) Math.abs(currentData - data) * 30);
        animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator animation) {
                float value = (float)animator.getAnimatedValue();
                currentData = (Math.round(value*10))/10;
                invalidate();
            }
        });
        animator.start();
    }

    public int getCurrentData(){
        return currentData;
    }

    public int getMaxData(){
        return maxData;
    }

    public void setMaxData(int maxData){
        this.maxData = maxData;
    }

    /**
     * px dp 转换
     * @param dpval
     * @return
     */
    protected int dp2px(int dpval){
        return (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP,dpval,getResources().getDisplayMetrics());
    }

}

TextProgressBar

1.绘制灰色直线

2.绘制蓝色动态直线

3.绘制字体

4.绘制一条白线,先于第三步,位置动态处于字体下方,以保证字体不会被灰色直线混淆

attrs.xml

<declare-styleable name="TextProgressBar">
        <attr name="bar_background_color" format="color"/>
        <attr name="bar_color" format="color"/>
        <attr name="text_color" format="color"/>
        <attr name="text_size" format="dimension"/>
        <attr name="unit_text" format="string"/>
    </declare-styleable>
public class TextProgressBar extends View {

    /**
     * bar 背景颜色
     */
    private int bar_background_color = Color.parseColor("#cccccc");
    /**
     * bar 颜色
     */
    private int bar_color = Color.parseColor("#3333FF");
    /**
     * 文字颜色
     */
    private int text_color = Color.parseColor("#3333FF");
    /**
     *文字字体大小
     */
    private int text_size = 18;
    /**
     * 单位内容
     */
    private String unit_text = "%";
    /**
     * bar 背景画笔
     */
    private Paint barBgPaint;
    /**
     * bar 画笔
     */
    private Paint barPaint;
    /**
     * 文字画笔
     */
    private Paint textPaint;
    /**
     * 文字背景画笔
     */
    private Paint textBgPaint;
    /**
     * 当前数据
     */
    private float currentData = 0;
    /**
     * 总数据
     */
    private float maxData = 100;
    /**
     * 文字矩形范围
     */
    private Rect text_rect;

    public TextProgressBar(Context context) {
        super(context,null);
    }

    public TextProgressBar(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs,0);
        /**
         * xml 属性设置获取
         */
        TypedArray typedArray = getContext().obtainStyledAttributes(attrs,R.styleable.TextProgressBar);
        bar_background_color = typedArray.getColor(R.styleable.TextProgressBar_bar_background_color,Color.parseColor("#cccccc"));
        bar_color = typedArray.getColor(R.styleable.TextProgressBar_bar_color,Color.parseColor("#3333FF"));
        text_color = typedArray.getColor(R.styleable.TextProgressBar_text_color,Color.parseColor("#3333FF"));
        text_size = (int) typedArray.getDimension(R.styleable.TextProgressBar_text_size,18);
        if (!TextUtils.isEmpty(typedArray.getString(R.styleable.TextProgressBar_unit_text))){
            unit_text = typedArray.getString(R.styleable.TextProgressBar_unit_text);
        }
        initView();
    }

    public TextProgressBar(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);



    }

    /**
     * 画笔初始化
     */
    private void initView(){
        barBgPaint = new Paint();
        barBgPaint.setColor(bar_background_color);
        barBgPaint.setStrokeWidth(dp2px(2));
        barBgPaint.setAntiAlias(true);
        barBgPaint.setStyle(Paint.Style.FILL_AND_STROKE);

        barPaint = new Paint();
        barPaint.setColor(bar_color);
        barPaint.setStrokeWidth(dp2px(2));
        barPaint.setAntiAlias(true);
        barPaint.setStyle(Paint.Style.FILL_AND_STROKE);

        textPaint = new Paint();
        textPaint.setColor(text_color);
        textPaint.setStrokeWidth(dp2px(1));
        textPaint.setTextSize(text_size);
        textPaint.setAntiAlias(true);
        textPaint.setStyle(Paint.Style.FILL_AND_STROKE);
        text_rect = new Rect();
        textPaint.getTextBounds(currentData + unit_text,0,(currentData + unit_text).length(),text_rect);

        textBgPaint = new Paint();
        textBgPaint.setColor(Color.parseColor("#ffffff"));
        textBgPaint.setStrokeWidth(dp2px(2));
        textBgPaint.setAntiAlias(true);
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        /**
         * 设置画布宽高为:宽度为设置宽度,高度为文字高度的2倍
         */
        setMeasuredDimension(MeasureSpec.getSize(widthMeasureSpec),text_rect.height() * 2);
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        drawBarBackground(canvas);
        drawBar(canvas);
        drawText(canvas);
    }

    /**
     * 绘制进度条背景
     * @param canvas
     */
    private void drawBarBackground(Canvas canvas){

        canvas.drawLine(0,getHeight()/2,getWidth(),getHeight()/2,barBgPaint);
        canvas.save();
    }

    /**
     * 绘制进度条bar
     * @param canvas
     */
    private void drawBar(Canvas canvas){
        int scale = (int)((float)currentData * getWidth() / maxData);
        canvas.drawLine(0,getHeight()/2,scale,getHeight()/2,barPaint);
        canvas.save();
    }

    /**
     * 绘制进度文字
     * @param canvas
     */
    private void drawText(Canvas canvas){
        text_rect = new Rect();
        textPaint.getTextBounds(currentData + unit_text,0,(currentData + unit_text).length(),text_rect);
        int currentX = (int) ((getWidth() - text_rect.width()) * currentData / maxData) ;
        canvas.drawLine(currentX - 2 ,getHeight() / 2,currentX + text_rect.width() + 2,getHeight() / 2,textBgPaint);
        canvas.save();
        canvas.drawText(currentData + unit_text,(int)((float)currentData * (getWidth() - text_rect.width()) / maxData),(getHeight() * 3) / 4,textPaint);
        canvas.save();
    }

    /**
     * px dp 转换
     * @param dpval
     * @return
     */
    protected int dp2px(int dpval){
        return (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP,dpval,getResources().getDisplayMetrics());
    }

    /**
     * 获取当前数值
     * @return
     */
    public float getCurrentData(){
        return currentData;
    }

    /**
     * 获取最大数值
     * @return
     */
    public float getMaxData(){
        return maxData;
    }

    /**
     * 设置最大数值
     * @param maxData
     */
    public void setMaxData(int maxData){
        this.maxData = maxData;
    }

    /**
     * 设置单位文字
     * @param text
     */
    public void setUnitText(String text){
        unit_text = text;
    }

    /**
     * 静态显示进度
     * @param i
     */
    public void progress(float i){
        if (i > maxData){
            i = maxData;
        }
        if (i < 0){
            i = 0;
        }
        currentData = i;
        invalidate();
    }

    /**
     * 动态显示进度
     * @param i
     * @param interpolator
     */
    public void goToProgress(float i, TimeInterpolator interpolator){
        if (i > maxData){
            i = maxData;
        }
        if (i < 0){
            i = 0;
        }
        final ValueAnimator animator = ValueAnimator.ofFloat(currentData,i);
        animator.setDuration((long) Math.abs(currentData - i) * 30);
        animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator animation) {
                float value = (float)animator.getAnimatedValue();
                currentData = (float)(Math.round(value*10))/10;
                invalidate();
            }
        });
        if (null != interpolator){
            animator.setInterpolator(interpolator);
        }
        animator.start();
    }
}

WaterRippleProgressBar

这个控件,其实就是用到了正弦函数  

y = Asin(wx+b)+h ,这个公式里:w影响周期,A影响振幅,h影响y位置,b为初相;

还用到了 canvas.clipPath,裁剪

attrs.xml

<declare-styleable name="WaterRippleProgressBar">
        <attr name="water_color" format="color"/>
        <attr name="water_text_color" format="color"/>
        <attr name="water_text_size" format="dimension"/>
    </declare-styleable>
public class WaterRippleProgressBar extends View {

    // 影响三角函数的初相
    private float move;

    // 正方形的宽高
    private int len;

    // 存放第一条水波Y值
    private float[] firstWaterLine;
    // 第二条
    private float[] secondWaterLine;

    // 水球的增长值
    int up = 0;

    // 画水球的画笔
    private Paint waterPaint;

    //颜色
    private int color;

    // 剪切圆的半径
    private int clipRadius;

    //当前进度值
    private float targetData = 0;

    //总进度值
    private float MaxData = 100;

    //文字画笔
    private Paint textPaint;

    //字体大小
    private int text_size = 20;

    //字体颜色
    private int text_color = Color.parseColor("#333333");

    public WaterRippleProgressBar(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
        TypedArray typedArray = getContext().obtainStyledAttributes(attrs,R.styleable.WaterRippleProgressBar);
        color = typedArray.getColor(R.styleable.WaterRippleProgressBar_water_color,Color.parseColor("#6633ff"));
        text_color = typedArray.getColor(R.styleable.WaterRippleProgressBar_water_text_color,Color.parseColor("#333333"));
        text_size = (int) typedArray.getDimension(R.styleable.WaterRippleProgressBar_water_text_size,20);
        initView();
    }

    public WaterRippleProgressBar(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);

    }

    //初始化画笔,颜色,水波纹动态等
    private void initView() {

        waterPaint = new Paint();
        waterPaint.setAntiAlias(true);

        textPaint = new Paint();
        textPaint.setColor(text_color);
        textPaint.setAntiAlias(true);

        color = Color.parseColor("#6633ff");

        moveWaterLine();
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        // 通过测量规则获得宽和高
        int width = MeasureSpec.getSize(widthMeasureSpec);
        int height = MeasureSpec.getSize(heightMeasureSpec);
        // 取出最小值
        len = Math.min(width, height);

        Log.e("width", "width = " + len);

        firstWaterLine = new float[len];
        secondWaterLine = new float[len];

        clipRadius = (len / 2) - 45;

//        text_size = len / 15;
        textPaint.setTextSize(dp2px(text_size));

        setMeasuredDimension(len, len);
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        drawWaterView(canvas);
        drawText(canvas);
        drawCircle(canvas);
    }

    //动态水波纹
    private void moveWaterLine() {
        final Timer timer = new Timer();
        timer.schedule(new TimerTask() {

            @Override
            public void run() {
                move += 0.1;
                postInvalidate();
            }
        }, 200, 20);
    }



    /**
     * 画水球的功能
     *
     * @param canvas
     */
    private void drawWaterView(Canvas canvas) {
        // y = Asin(wx+b)+h ,这个公式里:w影响周期,A影响振幅,h影响y位置,b为初相;
        // 将周期定为view总宽度
        float mCycleFactorW = (float) (2 * Math.PI / len);

        // 得到第一条波的y值
        for (int i = 0; i < len; i++) {
            firstWaterLine[i] = (float) (7.5 * Math
                    .sin(mCycleFactorW * i + move) - up);
        }
        // 得到第一条波的y值
        for (int i = 0; i < len; i++) {
            secondWaterLine[i] = (float) (11.25 * Math.sin(mCycleFactorW * i
                    + move + 10) - up);
        }

        canvas.save();

        // 裁剪成圆形区域
        Path path = new Path();
        waterPaint.setColor(color);
        path.reset();
        canvas.clipPath(path);

        path.addCircle(len / 2, len / 2, clipRadius, Path.Direction.CCW);
        canvas.clipPath(path, android.graphics.Region.Op.REPLACE);
        // 将坐标系移到底部
        canvas.translate(0, len / 2 + clipRadius);

        for (int i = 0; i < len; i++) {
            canvas.drawLine(i, firstWaterLine[i], i, len, waterPaint);
        }
        for (int i = 0; i < len; i++) {
            canvas.drawLine(i, secondWaterLine[i], i, len, waterPaint);
        }
        canvas.restore();
    }

    private void drawCircle(Canvas canvas){
        Paint paint = new Paint();
        paint.setStyle(Paint.Style.STROKE);
        paint.setColor(color);
        paint.setStrokeWidth(dp2px(1));
        paint.setAntiAlias(true);
        canvas.drawCircle(getWidth()/2,getHeight()/2,clipRadius,paint);
        canvas.save();
    }

    /**
     * 绘制中间文字
     * @param canvas
     */
    private void drawText(Canvas canvas){
        Rect rect = new Rect();
        textPaint.getTextBounds(targetData + "%",0,(targetData + "%").length(),rect);
        canvas.drawText(targetData + "%",getWidth() / 2 - rect.width() / 2,getHeight() / 2 + rect.height() / 2,textPaint);
        canvas.save();
    }

    /**
     * 动态水波纹
     * @param trueData
     */
    public void goToProgress(float trueData) {
        if (trueData > MaxData){
            trueData = MaxData;
        }
        if (trueData < 0){
            trueData = 0;
        }

        final ValueAnimator animator = ValueAnimator.ofFloat(targetData,trueData);
        animator.setDuration((long) Math.abs(targetData - trueData) * 30);
        animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator animation) {
                float value = (float)animator.getAnimatedValue();
                targetData = (float)(Math.round(value*10))/10;
                up = (int) (targetData / MaxData * clipRadius * 2);
                postInvalidate();
            }
        });
        postDelayed(new Runnable() {
            @Override
            public void run() {
                animator.start();
            }
        },300);


    }

    public float getCurrentData(){
        return targetData;
    }

    public float getMaxData(){
        return MaxData;
    }

    public void setMaxData(float maxData){
        this.MaxData = maxData;
    }

    /**
     * px dp 转换
     * @param dpval
     * @return
     */
    protected int dp2px(int dpval){
        return (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP,dpval,getResources().getDisplayMetrics());
    }
}

RoundBallProgressBar

这个比较简单,就是起线程或者Timer,动态修改每个圆点的半径值就可以了

public class RoundBallProgressBar extends View {

    private double cosX;

    private Paint ball_paint;

    private int ball_color = Color.parseColor("#3333ff");

    private float[] radius = {(float)0.01,(float)0.02,(float)0.03,(float)0.04,(float)0.03,(float)0.002,(float)0.001,(float)0.000};

    private int state = 2;

    private int speed = 100;

    public enum speeds{
        slow,
        normal,
        fast
    }

    private speeds speedss = speeds.normal;

    public RoundBallProgressBar(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
        TypedArray typedArray = getContext().obtainStyledAttributes(attrs,R.styleable.RoundBallProgressBar);
        ball_color = typedArray.getColor(R.styleable.RoundBallProgressBar_ball_color,Color.parseColor("#3333ff"));
        state = typedArray.getInt(R.styleable.RoundBallProgressBar_loading_speed,2);
        if (state == 1){
            speed = 200;
            speedss = speeds.slow;
        }else if (state == 2){
            speed = 100;
            speedss = speeds.normal;
        }else{
            speed = 50;
            speedss = speeds.fast;
        }
        initView();
    }

    private void initView(){
        ball_paint = new Paint();
        ball_paint.setColor(ball_color);
        ball_paint.setAntiAlias(true);
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        int width = MeasureSpec.getSize(widthMeasureSpec);
        int height = MeasureSpec.getSize(heightMeasureSpec);

        int len = Math.min(width,height);

//        rectF = new RectF(len * (float)0.1,len * (float)0.1,len * (float) 0.9,len * (float)0.9);

        cosX = Math.sqrt(Math.pow(len * 0.3,2)/2);

        setMeasuredDimension(len,len);
    }

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

    private void drawRoundBall(Canvas canvas){

        canvas.drawCircle(getWidth()/2,getHeight() * (float)0.2,getWidth() * radius[0],ball_paint);

        canvas.drawCircle(getWidth()/2 + (float)cosX,getHeight()/2-(float)cosX,getWidth() * radius[1],ball_paint);

        canvas.drawCircle(getWidth()/2 + getWidth() * (float)0.3,getHeight()/2,getWidth() * radius[2],ball_paint);

        canvas.drawCircle(getWidth()/2 + (float)cosX,getHeight()/2 + (float)cosX,getWidth() * radius[3],ball_paint);

        canvas.drawCircle(getWidth()/2,getHeight()/2 + getHeight() * (float)0.3,getWidth() * radius[4],ball_paint);

        canvas.drawCircle(getWidth()/2 - (float)cosX,getHeight()/2 + (float)cosX,getWidth() * radius[5],ball_paint);

        canvas.drawCircle(getWidth()/2 - getWidth() * (float)0.3,getHeight()/2,getWidth() * radius[6],ball_paint);

        canvas.drawCircle(getWidth()/2 - (float)cosX,getWidth()/2 - (float)cosX,getWidth() * radius[7],ball_paint);

        canvas.save();
    }

    public void setSpeed(speeds speed1){
        speedss = speed1;
        if (speedss == speeds.fast){
            speed = 50;
            state = 3;
        }else if (speedss == speeds.normal){
            speed = 100;
            state = 2;
        }else{
            speed = 200;
            state = 1;
        }
        postInvalidate();
    }

    public void startLoading(){
        Timer timer = new Timer();
        timer.schedule(new TimerTask() {
            @Override
            public void run() {
                changeFloat();
                postInvalidate();
            }
        },300,speed);
    }

    private void changeFloat(){
        float temp = radius[radius.length - 1];
        for (int i = radius.length - 1;i > 0 ; i --){
            radius[i] = radius[i - 1];
        }
        radius[0] = temp;
    }
}

FlyLoadingView

这个其实也用到了正弦函数,不过是取绝对值,这样云的效果就出来了,然后飞机的抖动其实也是取函数某个点的高度,加上飞机静态高度,就可以让飞机随着云的变化而变化

public class FlyLoadingView extends View {

    private Paint bacPaint;

    private Paint cloudPaint1;

    private Paint cloudPaint2;

    // 存放第一条水波Y值
    private float[] firstWaterLine;
    // 第二条
    private float[] secondWaterLine;

    // 影响三角函数的初相
    private float move;

    // 剪切圆的半径
    private int clipRadius;

    public FlyLoadingView(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
        initView();
    }

    public void initView(){
        bacPaint = new Paint();
        bacPaint.setColor(Color.parseColor("#ff51add8"));
        bacPaint.setAntiAlias(true);
        bacPaint.setStrokeWidth(1);
        bacPaint.setStyle(Paint.Style.FILL_AND_STROKE);

        cloudPaint1 = new Paint();
        cloudPaint1.setColor(Color.parseColor("#ffffffff"));
        cloudPaint1.setAntiAlias(true);
        cloudPaint1.setStrokeWidth(1);

        cloudPaint2 = new Paint();
        cloudPaint2.setColor(Color.parseColor("#ffffffff"));
        cloudPaint2.setAntiAlias(true);
        cloudPaint2.setStrokeWidth(1);

        moveWaterLine();
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        int width = MeasureSpec.getSize(widthMeasureSpec);
        int height = MeasureSpec.getSize(heightMeasureSpec);

        int len = Math.min(width,height);

        firstWaterLine = new float[len];
        secondWaterLine = new float[len];

        clipRadius = (int) ((len / 2) * 0.8);

        setMeasuredDimension(len,len);
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        drawBac(canvas);
        drawWaterView(canvas);
        drawPlane(canvas);
    }

    private void drawBac(Canvas canvas){
        RectF rectF = new RectF(getWidth() * (float)0.1,getWidth() * (float)0.1,getWidth() * (float)0.9,getWidth() * (float)0.9);
        canvas.drawArc(rectF,0,360,false,bacPaint);
        canvas.save();
    }

    private void drawWaterView(Canvas canvas) {
        // y = Asin(wx+b)+h ,这个公式里:w影响周期,A影响振幅,h影响y位置,b为初相;
        // 将周期定为view总宽度
//        float mCycleFactorW = (float) (2 * Math.PI / getWidth());
        float mCycleFactorW = (float) 0.05;

        // 得到第一条波的y值
        for (int i = 0; i < getWidth(); i++) {
            firstWaterLine[i] = (float) (13 * -Math.abs(Math
                    .sin(mCycleFactorW * i + move)) - (getWidth() / 6) - 10);
        }
        // 得到第一条波的y值
        for (int i = 0; i < getWidth(); i++) {
            secondWaterLine[i] = (float) (13 * -Math.abs(Math.sin(mCycleFactorW * i
                    + move + 15)) - ((getWidth() * 2) / 9) - 13);
        }

        canvas.save();

        // 裁剪成圆形区域
        Path path = new Path();
        path.reset();
        canvas.clipPath(path);

        path.addCircle(getWidth() / 2, getWidth() / 2, clipRadius, Path.Direction.CCW);
        canvas.clipPath(path, Region.Op.REPLACE);
        // 将坐标系移到底部
        canvas.translate(0, getWidth() / 2 + clipRadius);

        for (int i = 0; i < getWidth(); i++) {
            canvas.drawLine(i, firstWaterLine[i], i, getWidth(), cloudPaint1);
        }
        for (int i = 0; i < getWidth(); i++) {
            canvas.drawLine(i, secondWaterLine[i], i, getWidth(), cloudPaint2);
        }
        canvas.restore();
        canvas.save();
    }

    //动态水波纹
    private void moveWaterLine() {
        final Timer timer = new Timer();
        timer.schedule(new TimerTask() {

            @Override
            public void run() {
                move += 0.1;
                postInvalidate();
            }
        }, 200, 15);
    }

    private void drawPlane(Canvas canvas){

        Bitmap bitmap = BitmapFactory.decodeResource(getResources(),R.mipmap.plane);
        int newWidth = (int) (getWidth() * 0.4);
        float scale = ((float)newWidth)/bitmap.getWidth();
        Matrix matrix = new Matrix();
        matrix.setRotate(12,bitmap.getWidth()/2,bitmap.getHeight()/2);
        matrix.postScale(scale,scale);
        Bitmap newbitmap = bitmap.createBitmap(bitmap,0,0,bitmap.getWidth(),bitmap.getHeight(),matrix,true);
        canvas.drawBitmap(newbitmap,getWidth()/2 - newbitmap.getWidth()/2,getHeight()/2 - newbitmap.getHeight() *2/3 + firstWaterLine[0] + (getWidth() / 6) + 20,bacPaint);
        bitmap.recycle();
        newbitmap.recycle();
    }

}

HeartBeatLoadingView

这个控件,用到了 canvas.clipPath

public class HeartBeatLoadingView extends View {

    private Paint bacPaint;

    private Paint whitePaint;

    private Paint redPaint;

    private float current = 0;

    public HeartBeatLoadingView(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
        initView();
    }

    private void initView(){
        bacPaint = new Paint();
        bacPaint.setColor(Color.parseColor("#555555"));
        bacPaint.setAntiAlias(true);
        bacPaint.setStrokeWidth(1);

        whitePaint = new Paint();
        whitePaint.setColor(Color.parseColor("#ffffff"));
        whitePaint.setAntiAlias(true);
        whitePaint.setStrokeWidth(1);

        redPaint = new Paint();
        redPaint.setColor(Color.RED);
        redPaint.setAntiAlias(true);
        redPaint.setStrokeWidth(1);
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        // 通过测量规则获得宽和高
        int width = MeasureSpec.getSize(widthMeasureSpec);
        int height = MeasureSpec.getSize(heightMeasureSpec);

        int len = Math.min(width,height);

        setMeasuredDimension(len,len);

    }

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

    private void drawBac(Canvas canvas){

        RectF rectF = new RectF(0,0,getWidth(),getHeight());
        canvas.drawRect(rectF,whitePaint);

        RectF rectF1 = new RectF(current - getWidth() * (float)0.3,0,current,getHeight());
        canvas.drawRect(rectF1,redPaint);

        Path path = new Path();
        path.moveTo(0,(float) getHeight()/2 - (getHeight() * (float)0.01));
        path.lineTo(getWidth() * (float)0.3 - getWidth() * (float)0.01,(float)getHeight()/2 - (getHeight() * (float)0.01));
        path.lineTo(getWidth() * (float)0.4,(float)getHeight()/2 - (getHeight() * (float)0.2) - (getHeight() * (float)0.02));//上30
//        path.lineTo(getWidth() * (float)0.4 + getWidth() * (float)0.01,(float)getHeight()/2 - (getHeight() * (float)0.2));
        path.lineTo(getWidth()* (float)0.6,(float)getHeight()/2 - (getHeight() * (float)0.02) + (getHeight() * (float)0.2));//下30
        path.lineTo(getWidth() * (float)0.7 - getWidth() * (float)0.01,(float)getHeight()/2 - (getHeight() * (float)0.01));
        path.lineTo((float) getWidth(),(float)getHeight()/2 - (getHeight() * (float)0.01));
        path.lineTo((float)getWidth(),(float)getHeight()/2 + (getHeight() * (float)0.01));
        path.lineTo(getWidth() * (float)0.7 + getWidth() * (float)0.01,(float)getHeight()/2 + (getHeight() * (float)0.01));
        path.lineTo(getWidth() * (float)0.6,(float)getHeight()/2 + (getHeight() * (float)0.03) + (getHeight() * (float)0.2));
//        path.lineTo(getWidth() * (float)0.6 - getWidth() * (float)0.01,(float)getHeight()/2 + (getHeight() * (float)0.01) + (getHeight() * (float)0.2));
        path.lineTo(getWidth() *(float)0.4,(float)getHeight()/2 + (getHeight() * (float)0.02) - (getHeight() * (float)0.2));
        path.lineTo(getWidth() * (float)0.3 + getWidth() * (float)0.01,(float)getHeight()/2 + (getHeight() * (float)0.01));
        path.lineTo(0,(float) getHeight()/2 + (getHeight() * (float)0.01));
        path.close();

        canvas.clipPath(path, Region.Op.DIFFERENCE);

        canvas.drawRect(rectF,whitePaint);
    }

    public void run(){
        Timer timer = new Timer();
        timer.schedule(new TimerTask() {
            @Override
            public void run() {
                current += (float)0.5;
                if (current > getWidth() * (1.3)){
                    current = 0 - getWidth() * (float)0.3;
                }
                postInvalidate();
            }
        },200,1);
    }


}

End

    其实,多练习练习,api熟了之后,思路就宽了,有思路了控件就不是什么难事儿了。