Android自定义View 水波气泡

前言:公司在作的一个项目,要求在地图上以水波气泡的形式来显示站点,而且气泡要有水波的动态效果。好吧!既然有这样的需求,那就手撸一款水波气泡吧!java

效果图预览

  最后完成的效果图以下git

不想看文章的话,能够点击这里,直接获取源码。github

实现方式

步骤拆解

  在须要自定义view的时候,我首先要作的就是将最后要实现的效果来进行拆分,拆分红许多小的步骤,而后一步步的来实现,最终达到想要的效果。canvas

  能够将文章开始的时候的效果图拆分红如下几部分:markdown

  1. 画出气泡后面的白色背景。
  2. 画内部的紫色气泡。
  3. 用贝塞尔曲线让内部的紫色气泡动起来。

拆解以后,就能够按照拆解的步骤来一步步实现了。ide

画白色背景

  这里画白色背景有如下两种方式:oop

  1. path直接描述一个白色背景的形状。
  2. path描述一个三角形,而后在画出一个圆形,即成最终的白色背景了。

第一种方式以下图的左图,用path直接描述出了白色背景,这种方式能够用path.addArc()来画上部弧形,而后用path.moveTo()path.lineTo()方法描述出下部分的尖角。post

第二种实现的方式以下图的右图,直接画出一个圆,再用path.moveTo()path.lineTo()方法来描述出下部分的尖角。动画

本文采用的是第二种方式来实现的,具体代码以下ui

//此处代码是下部尖角的path
mBackgroundPath.moveTo(mResultWidth / 2 - mOutRadius / 2, mResultWidth / 2 + mOutRadius / 2);
        mBackgroundPath.lineTo(mResultWidth / 2, mResultWidth / 2 + mOutRadius + mOutRadius / 4);
        mBackgroundPath.lineTo(mResultWidth / 2 + mOutRadius / 2, mResultWidth / 2 + mOutRadius / 2);

 //画外部背景
        canvas.drawPath(mBackgroundPath, mBackgroundPaint);
        canvas.drawCircle(mResultWidth / 2, mResultWidth / 2, mOutRadius, mBackgroundPaint);
复制代码

画内部的气泡

  内部的气泡的形状其实就是缩小的外部背景,具体的代码以下

//内部气泡的尖角 
mBubblesPath.moveTo(mResultWidth / 2 - mOutRadius / 2, mResultWidth / 2 + mOutRadius / 2 - dp2px(getContext(), 5));
        mBubblesPath.lineTo(mResultWidth / 2, mResultWidth / 2 + mOutRadius + mOutRadius / 4 - dp2px(getContext(), 5));
        mBubblesPath.lineTo(mResultWidth / 2 + mOutRadius / 2, mResultWidth / 2 + mOutRadius / 2 - dp2px(getContext(), 5));
//画圆
 mBubblesPath.addCircle(mResultWidth / 2, mResultWidth / 2, mInnerRadius, Path.Direction.CCW);
复制代码

到这里已经将气泡的基本形状画出来了,见下图

咱们会发现气泡内部的颜色是渐变色,那渐变色是怎么设置的呢?其实自定义view就是将想要的效果经过画笔画在画布上,实现颜色的渐变确定就是经过设置画笔的属性来实现的了,设置渐变色的代码以下

//设置渐变色
        Shader shader = new LinearGradient(mResultWidth / 2, mResultWidth / 2 - mInnerRadius, mResultWidth / 2, mResultWidth / 2 + mInnerRadius, Color.parseColor("#9592FB"),
                Color.parseColor("#3831D4"), Shader.TileMode.CLAMP);
        mBubblesPaint.setShader(shader);
复制代码

LinearGradient(float x0, float y0, float x1, float y1, @ColorInt int color0, @ColorInt int color1, @NonNull TileMode tile)

x0 y0 x1 y1:渐变的两个端点的位置  color0 color1 是端点的颜色  tile:端点范围以外的着色规则,类型是 TileModeTileMode 一共有 3 个值可选: CLAMPMIRROR和 REPEAT。通常用 CLAMP就能够了。

让内部气泡动起来

  气泡内部的动画是水波的形式,这里画水波用的是二阶贝塞尔曲线,关于Android中贝塞尔曲线的知识能够参考这里。实现气泡内部水波效果的代码以下

/** * 核心代码,计算path * * @return */
    private Path getPath() {
        int itemWidth = waveWidth / 2;//半个波长
        Path mPath = new Path();
        mPath.moveTo(-itemWidth * 3, baseLine);//起始坐标
        Log.d(TAG, "getPath: " + baseLine);

        //核心的代码就是这里
        for (int i = -3; i < 2; i++) {
            int startX = i * itemWidth;
            mPath.quadTo(
                    startX + itemWidth / 2 + offset,//控制点的X,(起始点X + itemWidth/2 + offset)
                    getWaveHeight(i),//控制点的Y
                    startX + itemWidth + offset,//结束点的X
                    baseLine//结束点的Y
            );//只须要处理完半个波长,剩下的有for循环自已就添加了。
        }
        Log.d(TAG, "getPath: ");
        //下面这三句话是行程封闭的效果,不明白能够将下面3句代码注释看下效果的变化
        mPath.lineTo(width, height);
        mPath.lineTo(0, height);
        mPath.close();
        return mPath;
    }

//奇数峰值是正的,偶数峰值是负数
    private float getWaveHeight(int num) {
        if (num % 2 == 0) {
            return baseLine + waveHeight;
        }
        return baseLine - waveHeight;
    }
复制代码

上面的代码画出的水波以下图

到这里已经画出了水波,但如今水波仍是静止的,要让水波不停的移动,就要添加属性动画,添加动画的代码以下

/** * 不断的更新偏移量,而且循环。 */
    public void updateXControl() {
        //设置一个波长的偏移
        ValueAnimator mAnimator = ValueAnimator.ofFloat(0, waveWidth);
        mAnimator.setInterpolator(new LinearInterpolator());
        mAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {

            @Override
            public void onAnimationUpdate(ValueAnimator animation) {
                float animatorValue = (float) animation.getAnimatedValue();
                offset = animatorValue;//不断的设置偏移量,并重画
                postInvalidate();
            }
        });
        mAnimator.setDuration(1800);
        mAnimator.setRepeatCount(ValueAnimator.INFINITE);
        mAnimator.start();

    }
复制代码

修改一下onDraw中的代码,以下

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

        //设置渐变色
        Shader shader = new LinearGradient(mResultWidth / 2, mResultWidth / 2 - mInnerRadius, mResultWidth / 2, mResultWidth / 2 + mInnerRadius, Color.parseColor("#9592FB"),
                Color.parseColor("#3831D4"), Shader.TileMode.CLAMP);
        mBubblesPaint.setShader(shader);

        //此处代码是下部尖角的path
        mBackgroundPath.moveTo(mResultWidth / 2 - mOutRadius / 2, mResultWidth / 2 + mOutRadius / 2);
        mBackgroundPath.lineTo(mResultWidth / 2, mResultWidth / 2 + mOutRadius + mOutRadius / 4);
        mBackgroundPath.lineTo(mResultWidth / 2 + mOutRadius / 2, mResultWidth / 2 + mOutRadius / 2);


        //内部气泡的尖角
        mBubblesPath.moveTo(mResultWidth / 2 - mOutRadius / 2, mResultWidth / 2 + mOutRadius / 2 - dp2px(getContext(), 5));
        mBubblesPath.lineTo(mResultWidth / 2, mResultWidth / 2 + mOutRadius + mOutRadius / 4 - dp2px(getContext(), 5));
        mBubblesPath.lineTo(mResultWidth / 2 + mOutRadius / 2, mResultWidth / 2 + mOutRadius / 2 - dp2px(getContext(), 5));
        //画外部背景
        canvas.drawPath(mBackgroundPath, mBackgroundPaint);
        canvas.drawCircle(mResultWidth / 2, mResultWidth / 2, mOutRadius, mBackgroundPaint);
        Log.d(TAG, "cx: " + mResultWidth / 2);
        //画水波
        mBubblesPath.addCircle(mResultWidth / 2, mResultWidth / 2, mInnerRadius, Path.Direction.CCW);

        canvas.drawPath(getPath(), mBubblesPaint);

    }
复制代码

好了,如今水波已经能够移动了,看下效果

what!怎么成这个样子了呀,明显不是我想要的效果呀,确定是哪里出错了,通过我仔细的推敲,总结了出现上面问题的缘由,缘由以下图

出现上面问题的缘由就是由于下面三句代码

mPath.lineTo(width, height);
 mPath.lineTo(0, height);
 mPath.close();
复制代码

知道是这三句代码的缘由,那应该怎么修改呢?这三句代码好像不能动,否则就会出现波浪画的不完整的状况,额.....,那应该修改哪里呢?灵光一闪,不是能够裁剪画布嘛,只要将画布裁剪成想要的形状,而后在画波浪不久完美了。再修改onDraw方法,修改后的代码以下

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

        //设置渐变色
        Shader shader = new LinearGradient(mResultWidth / 2, mResultWidth / 2 - mInnerRadius, mResultWidth / 2, mResultWidth / 2 + mInnerRadius, Color.parseColor("#9592FB"),
                Color.parseColor("#3831D4"), Shader.TileMode.CLAMP);
        mBubblesPaint.setShader(shader);

        //此处代码是下部尖角的path
        mBackgroundPath.moveTo(mResultWidth / 2 - mOutRadius / 2, mResultWidth / 2 + mOutRadius / 2);
        mBackgroundPath.lineTo(mResultWidth / 2, mResultWidth / 2 + mOutRadius + mOutRadius / 4);
        mBackgroundPath.lineTo(mResultWidth / 2 + mOutRadius / 2, mResultWidth / 2 + mOutRadius / 2);


        //内部气泡的尖角
        mBubblesPath.moveTo(mResultWidth / 2 - mOutRadius / 2, mResultWidth / 2 + mOutRadius / 2 - dp2px(getContext(), 5));
        mBubblesPath.lineTo(mResultWidth / 2, mResultWidth / 2 + mOutRadius + mOutRadius / 4 - dp2px(getContext(), 5));
        mBubblesPath.lineTo(mResultWidth / 2 + mOutRadius / 2, mResultWidth / 2 + mOutRadius / 2 - dp2px(getContext(), 5));
        //画外部背景
        canvas.drawPath(mBackgroundPath, mBackgroundPaint);
        canvas.drawCircle(mResultWidth / 2, mResultWidth / 2, mOutRadius, mBackgroundPaint);
        Log.d(TAG, "cx: " + mResultWidth / 2);
        //切割画布,画水波
        canvas.save();
        mBubblesPath.addCircle(mResultWidth / 2, mResultWidth / 2, mInnerRadius, Path.Direction.CCW);
        //将画布裁剪成内部气泡的样子
        canvas.clipPath(mBubblesPath);

        canvas.drawPath(getPath(), mBubblesPaint);
        canvas.restore();

    }
复制代码

到这里已经实现了文章开始时的效果了,文章也该结束了。

结束语

  本文主要是讲解怎样实现水波气泡,并无讲到View的测量,贴出的也只是绘制气泡的代码,完整的代码能够点击这里获取。

  虽然已经撸出了这个效果,但最后项目中并无用这种动态的气泡,由于气泡多的时候是在是卡……。最后,喜欢此demo,就随手给个star吧!

ps: 历史文章中有干货哦!

本文已由公众号“AndroidShared”首发

欢迎关注个人公众号
扫码关注公众号,回复“获取资料”有惊喜
相关文章
相关标签/搜索