android高级UI之PathMeasure<三>--Path测量实战(笑脸loading效果实现、划船效果实现)

接着上一次android高级UI之PathMeasure<二>--Path测量实战(各种Loading效果)的PathMeasure学习继续,这里将对PathMeasure的学习进行收尾。

笑脸loading效果实现:

效果:

具体实现: 

1、新建View:

2、画左、右边眼睛:

由于左右眼睛就是两个实心圆,绘制比较简单:

package com.cexo.pathmeasurestudy;

import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.util.AttributeSet;
import android.view.View;

/**
 * Loading效果四:笑脸
 */
public class LoadingView4 extends View {

    //constants
    /**
     * 左眼距离左边的距离(控件宽度*EYE_PERCENT_W),
     * 右眼距离右边的距离(控件宽度*EYE_PERCENT_W)
     */
    private static final float EYE_PERCENT_W = 0.35F;
    /**
     * 眼睛距离top的距离(控件的高度*EYE_PERCENT_H)
     */
    private static final float EYE_PERCENT_H = 0.38F;

    //variables
    private Paint paint;
    private float eyesH = EYE_PERCENT_H;
    private float radius;

    public LoadingView4(Context context) {
        this(context, null);
    }

    public LoadingView4(Context context, AttributeSet attrs) {
        this(context, attrs, -1);
    }

    public LoadingView4(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        init();
    }

    private void init() {
        paint = new Paint();
        paint.setColor(Color.GRAY);
    }

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

    private void drawFace(Canvas canvas) {
        paint.setStyle(Paint.Style.FILL);
        //画左边的眼睛
        canvas.drawCircle(getWidth() * EYE_PERCENT_W, getHeight() * eyesH - radius, radius, paint);
        //画右边的眼睛
        canvas.drawCircle(getWidth() * (1 - EYE_PERCENT_W), getHeight() * eyesH - radius, radius, paint);
    }

    @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        super.onSizeChanged(w, h, oldw, oldh);
        radius = getWidth() / 7F / 2;
    }
}

运行:

3、画嘴巴:

对于嘴巴是一条曲线,很明显需要使用到贝塞尔线来进行绘制,首先将path需要移动到眼睛的下面:

其中涉及到几个变量:

然后利用贝塞尔曲线来绘制一条曲线【关于这块如不熟,可以参考android高级UI之贝塞尔曲线<上>---基本概念、德卡斯特里奥算法】:

其中又涉及到变量:

此时运行看一下效果:

嗯,嘴巴有了,不过感觉线条太细了,加粗一点:

再运行:

4、画大脑的轮廓:

接下来则来画大脑的轮廓了,这块就是绘制一个椭圆的路径,所以先来定义path:

其中构建椭圆的路径用的是addRoundRect api,对于第一个参数比较好理解,是一个path的左上右下的位置,而第二个和第三个参数:

也就是用这俩参数来控制圆角的大小的,下面运行看一下:

5、眼睛跟嘴巴动效实现:

要实现让嘴和眼睛来进行上下动,其实就是需要控制这三个值:

而如何来控制呢?由于是无限循环进行变动,所以这里用一个动画进行控制是最合适的,如下:

这种动画的用法有啥用呢?下面看一下日志打印就知道了:

等于是从0~1之间进行数值的变化的,那么,就可以用这个百分比来控制上下滚动的幅度啦,如下:

此时再运行你会发现有个bug:

原因是需要加这么一句话:

再运行就如最初看到的效果一样啦,但是你会发现貌似木有用到PathMeasure这个东东对吧,目前这效果确实没用上,下面划船的就用到啦。 

划船效果实现:

效果:

其实它已经在github上进行开源了,GitHub - webor2006/UI2018: 安卓高级UI代码,配合博客详细讲解知识点

这个开源项目里面有挺多UI效果的,想学学UI相关的可以瞅瞅它,上面则跟着源码走一遍流程,既使是抄一遍其收获也是有的~~

具体实现: 

1、新建View:

然后搭建主框架:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical">

    <com.cexo.pathmeasurestudy.BoatWaveView
        android:id="@+id/boat_wave_view"
        android:layout_width="match_parent"
        android:layout_height="0dp"
        android:layout_weight="1" />

    <TextView
        style="@style/textview_button"
        android:onClick="start"
        android:text="开始" />

    <TextView
        style="@style/textview_button"
        android:onClick="stop"
        android:text="停止" />

</LinearLayout>

其中按钮的样式:

  <style name="textview_button">
        <item name="android:layout_width">match_parent</item>
        <item name="android:layout_height">45dp</item>
        <item name="android:background">@drawable/selector_blue_round_5dp</item>
        <item name="android:gravity">center</item>
        <item name="android:textColor">@android:color/white</item>
        <item name="android:textSize">14sp</item>
        <item name="android:layout_margin">5dp</item>
    </style>

    <style name="textview_title">
        <item name="android:layout_width">match_parent</item>
        <item name="android:layout_height">30dp</item>
        <item name="android:textColor">#35a8ee</item>
        <item name="android:textSize">14sp</item>
        <item name="android:gravity">center</item>
    </style>

<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">

    <item android:state_pressed="true">
        <shape>
            <corners android:radius="5dp" />
            <solid android:color="#35a8ee" />
        </shape>
    </item>

    <item android:state_focused="true">
        <shape>
            <corners android:radius="5dp" />
            <solid android:color="#35a8ee" />
        </shape>
    </item>

    <item android:state_selected="true">
        <shape>
            <corners android:radius="5dp" />
            <solid android:color="#35a8ee" />
        </shape>
    </item>

    <item>
        <shape>
            <corners android:radius="5dp" />
            <solid android:color="#1296db" />
        </shape>
    </item>

</selector>

2、绘制坐标辅助线:

为了能看到绘制的坐标位置,首先来绘制一下背景的辅助线,也就是效果如下:

1、封装base:

对于坐标辅助线,可能其它View也可以使用,所以将其抽到Base当中来:

2、初始化paint:

很明显坐标网络是由三个样式组成:

所以先来初始化这三个paint:

package com.cexo.pathmeasurestudy;

import android.content.Context;
import android.content.res.Resources;
import android.graphics.Color;
import android.graphics.Paint;
import android.util.AttributeSet;
import android.view.View;

import androidx.annotation.Nullable;

public abstract class BaseView extends View {

    // 坐标画笔
    private Paint coordinatePaint;
    // 网格画笔
    private Paint gridPaint;
    // 写字画笔
    private Paint textPaint;
    // 坐标颜色
    private int coordinateColor;
    private int gridColor;
    // 坐标线宽度
    private final float coordinateLineWidth = 2.5f;
    // 网格宽度
    private final float gridLineWidth = 1f;
    // 字体大小
    private float textSize;

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

    public BaseView(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs, -1);
    }

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

    private void initCoordinate(Context context) {
        coordinateColor = Color.BLACK;
        gridColor = Color.LTGRAY;

        textSize = spToPx(10);

        coordinatePaint = new Paint();
        coordinatePaint.setAntiAlias(true);
        coordinatePaint.setColor(coordinateColor);
        coordinatePaint.setStrokeWidth(coordinateLineWidth);

        gridPaint = new Paint();
        gridPaint.setAntiAlias(true);
        gridPaint.setColor(gridColor);
        gridPaint.setStrokeWidth(gridLineWidth);

        textPaint = new Paint();
        textPaint.setAntiAlias(true);
        textPaint.setColor(coordinateColor);
        textPaint.setTextAlign(Paint.Align.CENTER);
        textPaint.setTextSize(textSize);
    }

    /**
     * 转换 sp 至 px
     */
    protected int spToPx(float spValue) {
        final float fontScale = Resources.getSystem().getDisplayMetrics().scaledDensity;
        return (int) (spValue * fontScale + 0.5f);
    }

    protected abstract void init(Context context);
}

3、画坐标和网格

由于它是一个通用的行为,很明显这个绘制应该是放在base当中,但是又并不是每个View都需要它,所以这里将绘制的逻辑只提取到base当中,而具体要不要用由子类来决定,如下:

那如何绘制呢?

1、画网格:

其中网络就是由横竖线组成的,具体绘制也不难,先将画布移到屏幕中心:

先来横着画竖线:

运行发现报错了:

空指针的原因是:

此时的效果是:

看着像竖着画对吧,其实是横着画指定高度的竖线,这里为了明白这点,可以只画一次循环,你会看到如下:

明白了吧,把代码还原,接下来再来竖着画横线:

此时的效果为:

4、画 x,y 轴:

此时运行,你会发现有问题:

这个其实原因也很简单,因为我们在绘网络时已经将画布的坐标点移到屏幕中心了:

此时再绘制坐标线时,应该将画布的中心坐标给还原,所以处理如下:

5、画刻度:

接下来则需要在x,y轴上进行刻度的绘制,跟绘制网络思路差不多,如下:

比较简单,直接看效果:

接下来则需要上面进行文字标注:

至此,坐标系效果就已经绘制完了。

3、绘制划船效果:

1、实现小船图片滑动效果:

接下来实现最核心的划船效果了,这里进行一个拆解,先来将小船图片绘制出来,然后再让它可以开始荡漾,如下:

1、首先将小船给绘制到屏幕上:

2、绘制小船行走的路径:

接下来则需要让小船进行水波荡漾的效果,此时是不是就需要改用这个api来进行绘制了?

这块如还不太清楚的可以参考android高级UI之PathMeasure<二>--Path测量实战(各种Loading效果) - cexo - 博客园,接下来则需要定义小船行走的轨迹,先来定义path:

接下来则就来构建一条浪的path,此时肯定得用到二阶贝塞尔曲线了,而二阶贝塞尔曲线在Android中已经有专门的API可供调用了,回忆一下:

下面先来用死的值构建一条曲线:

运行效果:

其中看出有辅助坐标系的作用了么?

有了坐标系,直接通过肉眼就可以知道你想要实现的效果,而关于贝塞尔曲线还有另一个API:

那它跟quadTo()有啥区别呢?先来看一下官网的解释:

关于它,我一直木有能理解透,好在搜到这么一篇大佬的文章自定义控件三部曲之绘图篇(六)——Path之贝赛尔曲线和手势轨迹、水波纹效果_启舰-CSDN博客才搞明白:

先来用它实现上面quadTo同样的效果:

其中可以算出:

控制点x坐标=上一个终点x坐标+控制点x位移=getWidth()/2-100+50 =getWidth()/2-50;

控制点y坐标=上一个终点y坐标+控制点y位移=getHeight()/2-50;

是不是就是图中的这个位置了?

而了解rQuadTo()这个API的原因是接下来绘制小船轨迹时就会用它来进行曲线的构建了,当一个扩展巩固,先来横向绘制满几个波浪:

运行效果:

对于上面这段代码是不是有点晕,这里就不详细说明了,说一下其绘制的思路,先将整个波浪的长度定为屏幕的1/3,也就是:

然后每次循环绘制一个浪,这里加一些日志你就明白其绘制的思路了:

运行日志输出:

2022-02-21 06:33:17.425 21064-21064/com.cexo.pathmeasurestudy E/cexo: x:-360;y:901;width:1080;height:1802
2022-02-21 06:33:17.425 21064-21064/com.cexo.pathmeasurestudy E/cexo: i----:-360;x:-360;y:901
2022-02-21 06:33:17.425 21064-21064/com.cexo.pathmeasurestudy E/cexo: control1:(-270,881)
2022-02-21 06:33:17.425 21064-21064/com.cexo.pathmeasurestudy E/cexo: end1:(-180,901)
2022-02-21 06:33:17.425 21064-21064/com.cexo.pathmeasurestudy E/cexo: control2:(-90,921)
2022-02-21 06:33:17.425 21064-21064/com.cexo.pathmeasurestudy E/cexo: end1:(0,901)
2022-02-21 06:33:17.425 21064-21064/com.cexo.pathmeasurestudy E/cexo: i----:0;x:0;y:901
2022-02-21 06:33:17.425 21064-21064/com.cexo.pathmeasurestudy E/cexo: control1:(90,881)
2022-02-21 06:33:17.425 21064-21064/com.cexo.pathmeasurestudy E/cexo: end1:(180,901)
2022-02-21 06:33:17.425 21064-21064/com.cexo.pathmeasurestudy E/cexo: control2:(270,921)
2022-02-21 06:33:17.425 21064-21064/com.cexo.pathmeasurestudy E/cexo: end1:(360,901)
2022-02-21 06:33:17.425 21064-21064/com.cexo.pathmeasurestudy E/cexo: i----:360;x:360;y:901
2022-02-21 06:33:17.425 21064-21064/com.cexo.pathmeasurestudy E/cexo: control1:(450,881)
2022-02-21 06:33:17.425 21064-21064/com.cexo.pathmeasurestudy E/cexo: end1:(540,901)
2022-02-21 06:33:17.425 21064-21064/com.cexo.pathmeasurestudy E/cexo: control2:(630,921)
2022-02-21 06:33:17.425 21064-21064/com.cexo.pathmeasurestudy E/cexo: end1:(720,901)
2022-02-21 06:33:17.426 21064-21064/com.cexo.pathmeasurestudy E/cexo: i----:720;x:720;y:901
2022-02-21 06:33:17.426 21064-21064/com.cexo.pathmeasurestudy E/cexo: control1:(810,881)
2022-02-21 06:33:17.426 21064-21064/com.cexo.pathmeasurestudy E/cexo: end1:(900,901)
2022-02-21 06:33:17.426 21064-21064/com.cexo.pathmeasurestudy E/cexo: control2:(990,921)
2022-02-21 06:33:17.426 21064-21064/com.cexo.pathmeasurestudy E/cexo: end1:(1080,901)
2022-02-21 06:33:17.426 21064-21064/com.cexo.pathmeasurestudy E/cexo: i----:1080;x:1080;y:901
2022-02-21 06:33:17.426 21064-21064/com.cexo.pathmeasurestudy E/cexo: control1:(1170,881)
2022-02-21 06:33:17.426 21064-21064/com.cexo.pathmeasurestudy E/cexo: end1:(1260,901)
2022-02-21 06:33:17.426 21064-21064/com.cexo.pathmeasurestudy E/cexo: control2:(1350,921)
2022-02-21 06:33:17.426 21064-21064/com.cexo.pathmeasurestudy E/cexo: end1:(1440,901)

可以看到,起始的坐标点都是已经超出屏幕了:

理解这个程序的核心一定是要知道此时画布的坐标点是在(0,0)这个位置,而非屏幕的中心哦,因为咱们在绘制完坐标系时已经将画布的平移给还原了:

也就是此时的绘制过程是从左到右进行的:

不过目前咱们这代码性能不太好,因为在onDraw()中会频繁的创建path:

所以这里将其放到onMeasure中只初始化一次:

@Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        if (!isInit) {
            isInit = true;
            width = getMeasuredWidth();
            height = getMeasuredHeight();
            waveLength = width / 3;

            //构建小船的路径
            boatPath = new Path();
            int x = -waveLength;
            int y = height / 2;
            Log.e("cexo", "x:" + x + ";y:" + y + ";width:" + width + ";height:" + height);
            boatPath.moveTo(x, y);
            int count = 0;
            for (int i = -waveLength; i < width * 1 + waveLength; i += waveLength) {
                Log.e("cexo", "i----:" + i + ";x:" + x + ";y:" + y);
                // rQuadTo 和 quadTo 区别在于
                // rQuadTo 是相对上一个点 而 quadTo是相对于画布
                int dx1 = waveLength / 4;
                int dy1 = -BOAT_WAVE_HEIGHT;
                int dx2 = waveLength / 2;
                int dy2 = 0;
                Log.e("cexo", "control1:(" + (x + dx1) + "," + (y + dy1) + ")");
                Log.e("cexo", "end1:(" + (x + dx2) + "," + (y + dy2) + ")");
                boatPath.rQuadTo(dx1, dy1, dx2, dy2);
                x = x + dx2;
                y = y + dy2;
                int dx11 = waveLength / 4;
                int dy11 = BOAT_WAVE_HEIGHT;
                int dx21 = waveLength / 2;
                int dy21 = 0;
                Log.e("cexo", "control2:(" + (x + dx11) + "," + (y + dy11) + ")");
                Log.e("cexo", "end1:(" + (x + dx21) + "," + (y + dy21) + ")");
                boatPath.rQuadTo(dx11, dy11, dx21, dy21);
                x = x + dx21;
                y = y + dy21;
            }
        }
    }

此时onDraw()中就只绘制path了:

好,有了path之后,接下来要想让小船图片随着这个path进行运行,此时PathMeasure就派上用场啦,需要对path进行测量如下:

接下来则就是绘制了,如下:

其中matrix.preTranslate()有啥作用呢? 可以参考对Matrix中preTranslate()和postTranslate()的理解_ProgramChangesWorld的专栏-CSDN博客_posttranslate,接下来运行看一下效果:

其中你会发现小船跟波浪方向是一致的,只是船体并不是完全沿着波浪线来的,这也符合物理视觉。

3、让小船动起来:

接下来让小船进行动起来就比较简单了,我们只需要来控制这个值既可:

这里还是利用ValueAnimator来实现,如下:

运行:

2、绘制小船的浪:

现在船已经动起来了,不过貌似这波浪是空心的,没有大海的感觉,应该像这样才行:

下面则来实现这样的效果:

1、 将其变成实心的浪:

这里就需要先来构建一个新的path了,目前咱们绘制的path是小船的路径:

而这个path的构建其实跟小船的构建逻辑是一样的,也就是:

然后path的构建几乎一模一样:

@Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        if (!isInit) {
            isInit = true;
            width = getMeasuredWidth();
            height = getMeasuredHeight();
            waveLength = width / 3;

            //构建小船的路径
            boatPath = new Path();
            int x = -waveLength;
            int y = height / 2;
            Log.e("cexo", "x:" + x + ";y:" + y + ";width:" + width + ";height:" + height);
            boatPath.moveTo(x, y);
            for (int i = -waveLength; i < width * 1 + waveLength; i += waveLength) {
                Log.e("cexo", "i----:" + i + ";x:" + x + ";y:" + y);
                // rQuadTo 和 quadTo 区别在于
                // rQuadTo 是相对上一个点 而 quadTo是相对于画布
                int dx1 = waveLength / 4;
                int dy1 = -BOAT_WAVE_HEIGHT;
                int dx2 = waveLength / 2;
                int dy2 = 0;
                Log.e("cexo", "control1:(" + (x + dx1) + "," + (y + dy1) + ")");
                Log.e("cexo", "end1:(" + (x + dx2) + "," + (y + dy2) + ")");
                boatPath.rQuadTo(dx1, dy1, dx2, dy2);
                x = x + dx2;
                y = y + dy2;
                int dx11 = waveLength / 4;
                int dy11 = BOAT_WAVE_HEIGHT;
                int dx21 = waveLength / 2;
                int dy21 = 0;
                Log.e("cexo", "control2:(" + (x + dx11) + "," + (y + dy11) + ")");
                Log.e("cexo", "end1:(" + (x + dx21) + "," + (y + dy21) + ")");
                boatPath.rQuadTo(dx11, dy11, dx21, dy21);
                x = x + dx21;
                y = y + dy21;
            }

            //构建小船底下的浪的路径
            boatWavePath = new Path();
            x = -waveLength;
            y = height / 2;
            boatWavePath.moveTo(x, y);
            for (int i = -waveLength; i < width * 1 + waveLength; i += waveLength) {
                Log.e("cexo", "i----:" + i + ";x:" + x + ";y:" + y);
                // rQuadTo 和 quadTo 区别在于
                // rQuadTo 是相对上一个点 而 quadTo是相对于画布
                int dx1 = waveLength / 4;
                int dy1 = -BOAT_WAVE_HEIGHT;
                int dx2 = waveLength / 2;
                int dy2 = 0;
                Log.e("cexo", "control1:(" + (x + dx1) + "," + (y + dy1) + ")");
                Log.e("cexo", "end1:(" + (x + dx2) + "," + (y + dy2) + ")");
                boatWavePath.rQuadTo(dx1, dy1, dx2, dy2);
                x = x + dx2;
                y = y + dy2;
                int dx11 = waveLength / 4;
                int dy11 = BOAT_WAVE_HEIGHT;
                int dx21 = waveLength / 2;
                int dy21 = 0;
                Log.e("cexo", "control2:(" + (x + dx11) + "," + (y + dy11) + ")");
                Log.e("cexo", "end1:(" + (x + dx21) + "," + (y + dy21) + ")");
                boatWavePath.rQuadTo(dx11, dy11, dx21, dy21);
                x = x + dx21;
                y = y + dy21;
            }

            // 让 PathMeasure 与 Path 关联
            boatPathMeasure.setPath(boatPath, false);
        }
    }

然后绘制改一下path:

此时,运行,你会发现还是跟之前一样,不是实心的,其实是咱们的paint设置没改:

此时再运行看一下,还是不如预期:

而原因就得对path.close()有一定的了解了,这块的基础知识可以参考android高级UI之PathMeasure<一>--Path测量基础(nextContour、getPosTan、getMatrix、getSegment),这里直接给出代码了:

而对于path它有lineTo和rLineTo两个类似的api,此时就需要对这俩进行一个区别的了解,可以参考lineTo和rLineTo的区别_wzping435的博客-CSDN博客,也就是构建这么一个区域可以达到闭环:

如果看不太懂,可以debug把值给打印,然后把坐标点算出来,就容易明白了,这里稍加过一下:

其实是定位在左下角的位置:

3、让浪动起来:

好,接下来浪扭动起来,咋扭动呢?这里需要用到画布平移的api了:

用一个具体代码例子来理解:

            canvas.save();//锁画布(为了保存之前的画布状态)
            canvas.translate(10, 10);//把当前画布的原点移到(10,10),后面的操作都以(10,10)作为参照点,默认原点为(0,0)
            drawScene(canvas);
            canvas.restore();//把当前画布返回(调整)到上一个save()状态之前 

所以,要想让波浪动起来,咱们只需要来让画布进行移动既可,具体如下:

然后开始移动:

此时运行你就会看到运动的效果了:

呃,新问题来了,露底了。。问题在原因在于对于小船这个浪不够“长”,所以解决起来也比较简单,将长度加大就成了,如下:

此时再运行,就木有这种露底的现象了,这里就不演示了。

4、代码抽取:

好,现在的问题就暴露了,小船的轨迹和小船的浪轨迹这俩的生成规则几乎一模一样,那。。是不是有必要封装一下?是的,所以在继续往下实现之前先来干下这事,这里细节就不过多解释了,比较简单:

3、绘制海浪:

最后,再加一个海浪,目前只有一个显得有点单调,有了上面的抽取之后,再加浪就非常简单了,如下:

其中浪的颜色值为:

<color name="color_wave_blue">#503bff</color>

运行,你会发现有bug:

海浪断层了,其实这块也很容易解决,需要这样处理:

到此,整个效果完成,完整代码如下:

package com.cexo.pathmeasurestudy;

import android.animation.ValueAnimator;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Canvas;
import android.graphics.Matrix;
import android.graphics.Paint;
import android.graphics.Path;
import android.graphics.PathMeasure;
import android.util.AttributeSet;

import androidx.annotation.Nullable;
import androidx.core.content.ContextCompat;

/**
 * 划船效果
 */
public class BoatWaveView extends BaseView {

    // 小船浪花的高度
    private static final int BOAT_WAVE_HEIGHT = 20;
    // 浪花每次的偏移量
    private final static int WAVE_OFFSET = 5;
    // 波浪高度
    private static final int WAVE_HEIGHT = 35;

    // 小船的图片
    private Bitmap boatBitmap;
    // 用于变换小船的
    private Matrix matrix;
    // 小船的路径
    public Path boatPath;
    // 小船的浪路径
    public Path boatWavePath;
    // 海浪的路径
    public Path wavePath;
    public Paint wavePaint;
    // 小船的浪色值
    private int boatBlue;
    // 浪花的色值
    private int waveBlue;
    private int width;
    private int height;
    // 浪花的宽度
    private int waveLength;
    private boolean isInit = false;
    private PathMeasure boatPathMeasure;
    private ValueAnimator animator;
    // 当前小船在path路径上的百分比位置
    float currentPosition;

    // 小船的浪花偏移量
    private int boatWaveOffset = 0;
    // 浪花当前的偏移量
    private int curWaveOffset = 0;

    public BoatWaveView(Context context) {
        super(context);
    }

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

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

    public void startAnim() {
        if (animator != null)
            animator.start();
    }

    public void stopAnim() {
        if (animator != null)
            animator.cancel();
    }

    @Override
    protected void init(Context context) {
        BitmapFactory.Options options = new BitmapFactory.Options();
        options.inSampleSize = 1;
        boatBitmap = BitmapFactory.decodeResource(getResources(), R.drawable.boat, options);

        matrix = new Matrix();

        boatBlue = ContextCompat.getColor(context, R.color.color_boat_blue);
        waveBlue = ContextCompat.getColor(context, R.color.color_wave_blue);

        wavePaint = new Paint();
        wavePaint.setAntiAlias(true);
        wavePaint.setColor(boatBlue);
//        wavePaint.setStrokeWidth(4);
//        wavePaint.setStyle(Paint.Style.STROKE);

        boatPath = new Path();
        boatWavePath = new Path();
        wavePath = new Path();

        boatPathMeasure = new PathMeasure();
        animator = ValueAnimator.ofFloat(0, 1f);
        animator.setDuration(4000);
        animator.setRepeatCount(ValueAnimator.INFINITE);
        animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator animation) {
                currentPosition = (float) animation.getAnimatedValue();
                boatWaveOffset = (boatWaveOffset + WAVE_OFFSET / 2) % width;//小船的浪走得慢一点
                curWaveOffset = (curWaveOffset + WAVE_OFFSET) % width;//浪走得快一点
                postInvalidate();
            }
        });
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        if (!isInit) {
            isInit = true;
            width = getMeasuredWidth();
            height = getMeasuredHeight();
            waveLength = width / 3;

            //构建小船的路径
            initPath(boatPath, waveLength, BOAT_WAVE_HEIGHT, false, 1);

            //构建小船底下的浪的路径
            initPath(boatWavePath, waveLength, BOAT_WAVE_HEIGHT, true, 2);

            // 初始化 浪的路径
            initPath(wavePath, waveLength, WAVE_HEIGHT, true, 2);

            // 让 PathMeasure 与 Path 关联
            boatPathMeasure.setPath(boatPath, false);
        }
    }

    /**
     * @param path       路径
     * @param length     浪花的宽度
     * @param waveHeight 浪花的高度
     * @param isClose    是否要闭合
     * @param lengthTime 浪花长的倍数
     */
    private void initPath(Path path, int length, int waveHeight, boolean isClose, float lengthTime) {
        // 初始化 小船的路径
        path.moveTo(-length, height / 2);
        for (int i = -length; i < width * lengthTime + length; i += length) {
            // rQuadTo 和 quadTo 区别在于
            // rQuadTo 是相对上一个点 而 quadTo是相对于画布
            path.rQuadTo(length / 4,
                    -waveHeight,
                    length / 2,
                    0);
            path.rQuadTo(length / 4,
                    waveHeight,
                    length / 2,
                    0);
        }

        if (isClose) {
            path.rLineTo(0, height / 2);
            path.rLineTo(-(width * 2 + 2 * length), 0);
            path.close();
        }
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        drawCoordinate(canvas);
        float length = boatPathMeasure.getLength();
        boatPathMeasure.getMatrix(length * currentPosition,
                matrix,
                PathMeasure.POSITION_MATRIX_FLAG | PathMeasure.TANGENT_MATRIX_FLAG);
        matrix.preTranslate(-boatBitmap.getWidth() / 2, -boatBitmap.getHeight() * 5 / 6);

        //根据轨迹来绘制小船
        canvas.drawBitmap(boatBitmap, matrix, null);

        // 画船的浪花
        canvas.save();
        canvas.translate(-boatWaveOffset, 0);
        wavePaint.setColor(boatBlue);
        canvas.drawPath(boatWavePath, wavePaint);
        canvas.restore();

        // 画浪花
        canvas.save();
        canvas.translate(-curWaveOffset, 0);
        wavePaint.setColor(waveBlue);
        canvas.drawPath(wavePath, wavePaint);
        canvas.restore();
    }
}

总结:

真是不容易,年后到现在就憋出了这么一篇,堕落啦【居然整个二月0篇】,另外有一个原因其实是由于年后公司组织架构调整了,到了一个全新的项目组,然后。。为了生存不得已需要耗用全部精力来熟悉新项目,所以学习计划就被搁置了,不过这也是给自己找借口,接下来还是得按照自己的学习计划前行,今年还是把去年落的计划一步一个脚印给补上,另外就是加强服务端java后端的学习【Java后台到全栈这门】,因为,年后被老大批了,说我们做app端的不思进取,把自己固守在自己的领域都不愿往后台搞一搞,好吧,算是逼自己换学习计划了,也挺好,接下来有时间就学它~~

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

webor2006

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值