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


    接着上一次https://www.cnblogs.com/webor2006/p/15605936.html的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需要移动到眼睛的下面:

    其中涉及到几个变量:

    然后利用贝塞尔曲线来绘制一条曲线【关于这块如不熟,可以参考https://www.cnblogs.com/webor2006/p/12901271.html】:

    其中又涉及到变量:

    此时运行看一下效果:

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

    再运行:

    4、画大脑的轮廓:

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

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

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

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

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

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

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

     

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

     

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

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

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

    划船效果实现:

    效果:

     

    其实它已经在github上进行开源了,https://github.com/webor2006/UI2018

    这个开源项目里面有挺多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来进行绘制了?

    这块如还不太清楚的可以参考https://www.cnblogs.com/webor2006/p/15605936.html,接下来则需要定义小船行走的轨迹,先来定义path:

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

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

    运行效果:

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

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

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

    关于它,我一直木有能理解透,好在搜到这么一篇大佬的文章https://blog.csdn.net/harvic880925/article/details/50995587才搞明白:

    先来用它实现上面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;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 + ";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()有啥作用呢? 可以参考https://blog.csdn.net/programchangesworld/article/details/49078387,接下来运行看一下效果:

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

    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 + ";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()有一定的了解了,这块的基础知识可以参考https://www.cnblogs.com/webor2006/p/15488224.html,这里直接给出代码了:

    而对于path它有lineTo和rLineTo两个类似的api,此时就需要对这俩进行一个区别的了解,可以参考https://blog.csdn.net/wzping435/article/details/78583555,也就是构建这么一个区域可以达到闭环:

    如果看不太懂,可以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端的不思进取,把自己固守在自己的领域都不愿往后台搞一搞,好吧,算是逼自己换学习计划了,也挺好,接下来有时间就学它~~

  • 相关阅读:
    操作系统笔记(六)页面置换算法 FIFO法 LRU最近最久未使用法 CLOCK法 二次机会法
    文加图, 理解Http请求与响应
    Android Retrofit 2.0使用
    Java 注解 (Annotation)你可以这样学
    MySQL 基本语句
    Java数据类型和MySql数据类型对应表
    Android Library 打造自己的 SDK,并 Maven 发布
    Java并发——线程同步Volatile与Synchronized详解
    Android getScrollX()详解
    图解Android View的scrollTo(),scrollBy(),getScrollX(), getScrollY()
  • 原文地址:https://www.cnblogs.com/webor2006/p/15841606.html
Copyright © 2020-2023  润新知