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