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