android-贝塞尔bezier曲线应用

引子

上网逛技术贴的时候,偶尔看到了这种特效;

想来应该也不是很难,偶有闲暇,研究一下,最后成功之后的效果如下,

并不完全相同。

本来还想继续研究,项目来了,没办法,只能放后面再说;

实现思路,我在项目代码里面会有详细解释;

本文,查阅了很多资料; 主要感谢 这位大佬的神贴:https://blog.csdn.net/tianjian4592/article/details/54087913;借鉴思路,最终做成了一个半成品。。。╮( ̄▽ ̄")╭···好尴尬。。

如果你也想看他的代码,然后自己做一个,必须提醒一下: 我做的时候,最花时间的就是读他的算法,计算坐标,其中涉及到了大量的数学计算,看得人很蛋疼。。。中文注释很少

不由的感悟:

复杂控件,复杂在哪里?

两点:

1)层出不穷的各种功能API,,用了之后就记得,没自己去用,只是看看的话,永远学不会;

2)复杂图形,动画,很多都涉及数学概念,数学模型,公式计算,所以不得不说, 数学没学好,制约了我的想象力```

废话不多说,看代码

源码

MyBottleView.java

  1 package com.example.complex_animation;
  2 
  3 import android.content.Context;
  4 import android.graphics.Camera;
  5 import android.graphics.Canvas;
  6 import android.graphics.Color;
  7 import android.graphics.CornerPathEffect;
  8 import android.graphics.Matrix;
  9 import android.graphics.Paint;
 10 import android.graphics.Path;
 11 import android.graphics.PathMeasure;
 12 import android.graphics.RectF;
 13 import android.support.annotation.Nullable;
 14 import android.util.AttributeSet;
 15 import android.util.Log;
 16 import android.view.View;
 17 
 18 /**
 19  * 最后时间 2018年9月14日 17:06:24
 20  * <p>
 21  * 控件类:装水的瓶,水会动;
 22  * <p>
 23  * 实现思路:
 24  * 1)画瓶身
 25  * 左半边
 26  * 1- 瓶嘴 2- 瓶颈  3- 瓶身  4- 瓶底
 27  * 右半边:使用矩阵变换,复制左边部分的path
 28  * <p>
 29  * 2)画瓶中的水.采用逆时针绘制顺序
 30  * 1-左边的弧形
 31  * 2-瓶底直线
 32  * 3-右边弧形
 33  * 4-右边的小段二阶贝塞尔曲线
 34  * 5-中间的大段三阶贝塞尔曲线
 35  * 6-左边的小段二阶贝塞尔曲线
 36  *
 37  * 主要技术点:
 38  * 1)Path类的应用,包括绝对坐标定位,相对坐标定位添加 contour,
 39  * 2)PathMeasure类的应用,计算当前path对象的上某个点的坐标
 40  * 3)贝塞尔曲线的应用
 41  *
 42  * 主要难点:
 43  * 1)画波浪的时候,三段贝塞尔曲线的控制点的确定,多段贝塞尔曲线的完美相切
 44  * 2) 画瓶身的时候,矩阵变换 实现path的翻转复制;
 45  * 3) 三角函数的应用,····其实不是难,是老了,这些东西不记得了,而且反应慢 ,囧~~~
 46  *
 47  * emmm···其他的,想不起来了,应该没了吧;所有技术点,难点,可以在我的代码中找到解决方案
 48  */
 49 public class MyBottleView extends View {
 50 
 51     private Context mContext;
 52 
 53     public MyBottleView(Context context) {
 54         this(context, null);
 55     }
 56 
 57     public MyBottleView(Context context, @Nullable AttributeSet attrs) {
 58         this(context, attrs, 0);
 59     }
 60 
 61     public MyBottleView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
 62         super(context, attrs, defStyleAttr);
 63         mContext = context;
 64     }
 65 
 66     private Paint mBottlePaint, mWaterPaint, mPointPaint;//三支画笔
 67     private Path mBottlePath, mWaterPath;//两条path,一个画瓶身,一个画瓶中的水
 68 
 69     private static final int DEFAULT_WATER_COLOR = 0XFF41EDFA;//水的颜色
 70     private static final int DEFAULT_BOTTLE_COLOR = 0XFFCEFCFF;//瓶身颜色
 71 
 72     private float startX, startY;//绘制起点的X,Y
 73 
 74     //尺寸变量
 75     private float paintWidth;//画笔的宽度
 76     private float bottleMouthRadius;//瓶嘴小弯曲的直径
 77     private float bottleMouthOffSetX;//瓶嘴小弯曲的X轴矫正
 78     private float bottleBodyArcRadius;//瓶身弧形半径
 79     private float bottleNeckWidth;//瓶颈宽度
 80     private float bottleMouthConnectLineX;//瓶嘴和瓶颈连接处的小短线 X偏移量
 81     private float bottleMouthConnectLineY;//瓶嘴和瓶颈连接处的小短线 Y偏移量
 82     private float bottleNeckHeight;// 瓶颈高度
 83 
 84     //尺寸变量 相对于 参照值的半分比
 85     private float paintWidthPercent;//画笔的宽度
 86     private float bottleMouthRadiusPercent;//瓶嘴小弯曲的直径(占比)
 87     private float bottleMouthOffSetXPercent;//瓶嘴小弯曲的X轴矫正(占比)
 88     private float bottleMouthConnectLineXPercent;//瓶嘴和瓶颈连接处的小短线 X偏移量(占比)
 89     private float bottleMouthConnectLineYPercent;//瓶嘴和瓶颈连接处的小短线 Y偏移量(占比)
 90     private float bottleNeckWidthPercent;//瓶颈宽度(占比)
 91     private float bottleNeckHeightPercent;// 瓶颈高度(占比)
 92     private float bottleBodyArcRadiusPercent;//瓶身弧形半径(占比)
 93 
 94     private float referenceValue = 300;//参照值,因为我画图形原型的时候,是用300dp的宽高做的参照
 95 
 96     //角度,角度不需要适配
 97     private float bottleMouthStartAngle;// 瓶嘴弧形的开始角度值
 98     private float bottleMouthSweepAngle;// 瓶嘴弧形横扫角度
 99     private float bottleBodyStartAngle;// 瓶身弧形的开始角度值
100     private float bottleBodySweepAngle;// 瓶身弧形横扫角度
101 
102     int mWidth, mHeight;//控件的宽高
103     //保存 瓶身矩形的左上角右下角坐标
104     float bottleBodyArcLeft;
105     float bottleBodyArcTop;
106     float bottleBodyArcRight;
107     float bottleBodyArcBottom;
108     private double bottleBottomSomeContour;//瓶底,除了瓶颈宽度之外的2个小段的长度
109 
110     //按比例划分中间的波浪形态
111     private float rightQuadLengthRatio = 0.02f;//右边二阶曲线的长度比例
112     private float midCubicLengthRatio = 0.96f;//中间三阶曲线的长度比例
113     private float leftQuadLengthRatio = 0.02f;//左边二阶曲线的长度比例
114 
115     //由于 左右两个二阶曲线的Y轴控制点是要变化的(为了让波浪两端显得更加柔和),所以用全局变量保存偏移量
116     private float rightQuadControlPointOffsetY;
117     private float leftQuadControlPointOffsetY;
118 
119     private float centerCubicControlX_1 = 0.225f;//中间三阶曲线的第一个控制点X,
120     private float centerCubicControlY_1 = -0.3f;//中间三阶曲线的第一个控制点X,
121     private float centerCubicControlX_2 = 0.675f;//中间三阶曲线的第一个控制点X,
122     private float centerCubicControlY_2 = 0.3f;//中间三阶曲线的第一个控制点X,
123     private float waterLeftRatio = 0.15f;//水面抬高的比例,要让动画变得柔和,就要把水面稍微抬高一点点
124     private float paramDelta = 0.005f;//每次刷新水面时的 参数变动值,用来控制动画的频率
125 
126     private boolean ifShowSupportPoints = true;//是否要开启辅助点
127 
128     /**
129      * 画笔初始化
130      */
131     private void initPaint() {
132         mBottlePaint = new Paint();
133         mBottlePaint.setAntiAlias(true);
134         mBottlePaint.setStyle(Paint.Style.STROKE);
135         mBottlePaint.setColor(DEFAULT_BOTTLE_COLOR);
136         //柔和的特殊处理
137         mBottlePaint.setStrokeCap(Paint.Cap.ROUND);//画直线的时候,头部变成圆角
138         CornerPathEffect mBottleCornerPathEffect = new CornerPathEffect(paintWidth);//在直线和直线的交界处自动用圆角处理,圆角直径20
139         mBottlePaint.setPathEffect(mBottleCornerPathEffect);
140         mBottlePaint.setStrokeWidth(paintWidth);//画笔宽度
141 
142         //画水
143         mWaterPaint = new Paint();
144         mWaterPaint.setAntiAlias(true);
145         mWaterPaint.setStyle(Paint.Style.FILL);
146         mWaterPaint.setColor(DEFAULT_WATER_COLOR);
147         mWaterPaint.setStrokeCap(Paint.Cap.ROUND);//画直线的时候,头部变成圆角
148         mWaterPaint.setPathEffect(mBottleCornerPathEffect);
149         mWaterPaint.setStrokeWidth(paintWidth);//画笔宽度
150 
151         //画辅助点
152         mPointPaint = new Paint();
153         mPointPaint.setAntiAlias(true);
154         mPointPaint.setStyle(Paint.Style.STROKE);
155         if (ifShowSupportPoints) {
156             mPointPaint.setColor(Color.YELLOW);
157         } else {
158             mPointPaint.setColor(Color.TRANSPARENT);
159         }
160         mPointPaint.setStrokeWidth(paintWidth * 1);//画笔宽度
161     }
162 
163     /**
164      * 为了做全自动适配,将我测试过程中用到的dp值,都转变成 小数百分比, 使用的时候,再根据用乘法转化成实际的dp值
165      */
166     private void initPercents() {
167 
168         paintWidthPercent = 2 / referenceValue;
169         bottleMouthRadiusPercent = 3 / referenceValue;
170         bottleMouthOffSetXPercent = 2 / referenceValue;
171         bottleMouthConnectLineXPercent = 2 / referenceValue;
172         bottleMouthConnectLineYPercent = 5 / referenceValue;
173 
174         bottleNeckWidthPercent = 30 / referenceValue;
175         bottleNeckHeightPercent = 100 / referenceValue;
176 
177         bottleBodyArcRadiusPercent = 80 / referenceValue;
178     }
179 
180 
181     /**
182      * 初始化宽高
183      */
184     private void initWH() {
185         mWidth = getWidth();
186         mHeight = getHeight();
187     }
188 
189     /**
190      * 比例值已经上一步中已经设定好了,现在将比例值,转化成实际的长度
191      */
192     private void initParams() {
193         float realValue = DpUtil.px2dp(mContext, mWidth > mHeight ? mHeight : mWidth);//以较宽高中较小的那一项为准,现在设置的值都以这个为参照,
194         bottleMouthRadius = DpUtil.dp2Px(mContext, bottleMouthRadiusPercent * realValue);//瓶嘴小弯曲的直径
195         bottleMouthOffSetX = DpUtil.dp2Px(mContext, bottleMouthOffSetXPercent * realValue);//瓶嘴小弯曲的X轴矫正
196         bottleMouthConnectLineX = DpUtil.dp2Px(mContext, bottleMouthConnectLineXPercent * realValue);//瓶嘴和瓶颈连接处的小短线 X偏移量
197         bottleMouthConnectLineY = DpUtil.dp2Px(mContext, bottleMouthConnectLineYPercent * realValue);//瓶嘴和瓶颈连接处的小短线 Y偏移量
198         bottleNeckWidth = DpUtil.dp2Px(mContext, bottleNeckWidthPercent * realValue);//瓶颈宽度
199         bottleNeckHeight = DpUtil.dp2Px(mContext, bottleNeckHeightPercent * realValue);// 瓶颈高度
200         bottleBodyArcRadius = DpUtil.dp2Px(mContext, bottleBodyArcRadiusPercent * realValue);//瓶身弧形半径
201         paintWidth = DpUtil.dp2Px(mContext, paintWidthPercent * realValue);// 画笔
202 
203         //弧形的角度
204         bottleMouthStartAngle = -90;// 瓶嘴弧形的开始角度值
205         bottleMouthSweepAngle = -120;// 瓶嘴弧形横扫角度
206 
207         bottleBodyStartAngle = -90;// 瓶身弧形的开始角度值
208         bottleBodySweepAngle = -160;// 瓶身弧形横扫角度
209 
210         startX = mWidth / 2 - bottleNeckWidth / 2; // 绘制起点的X,Y
211         startY = (mHeight - bottleNeckHeight - bottleBodyArcRadius * 2) / 2;//起点位置的Y
212 
213     }
214 
215     /**
216      * 计算瓶身path, 并且绘制出来
217      */
218     private void calculateBottlePath(Canvas canvas) {
219         if (mBottlePath == null) {
220             mBottlePath = new Path();
221         } else {
222             mBottlePath.reset();
223         }
224         addPartLeft();//左边一半
225         addPartRight();//右边一半
226 
227         canvas.drawPath(mBottlePath, mBottlePaint);//画瓶子
228     }
229 
230 
231     /**
232      * 画左边那一半,主要是用Path,add直线,add弧线,组合起来,就是一条不规则曲线
233      */
234     private void addPartLeft() {
235         mBottlePath = new Path();
236         mBottlePath.moveTo(startX, startY);//移动path到开始绘制的位置
237 
238         //先画一个弧线,瓶子最上方的小嘴
239         RectF r = new RectF();
240         r.set(startX - bottleMouthOffSetX, startY, startX - bottleMouthOffSetX + bottleMouthRadius * 2, startY + bottleMouthRadius * 2);//用矩阵定位弧形;
241         mBottlePath.addArc(r, bottleMouthStartAngle, bottleMouthSweepAngle);//瓶嘴的小弯曲,画弧形-  解释一下这里为什么是-90:弧形的绘制 角度为0的位置是X轴的正向,而我们要从Y正向开始绘制; 划过角度是-120的意思是,逆时针旋转120度。
242 
243         mBottlePath.rLineTo(bottleMouthConnectLineX, bottleMouthConnectLineY);//瓶颈和小弯曲的连接处直线
244         mBottlePath.rLineTo(0, bottleNeckHeight);//瓶颈直线
245 
246         float[] pos = new float[2];//终点的坐标,0 位置是X,1位置是Y
247         calculateLastPartOfPathEndingPos(mBottlePath, pos);//这个pos的值在执行了这一行之后已经发生了改变 , 这个pos就是结束坐标,里面存了x和y
248 
249         //然后再画瓶身
250         RectF r2 = new RectF();
251 
252         bottleBodyArcLeft = pos[0] - bottleBodyArcRadius;
253         bottleBodyArcTop = pos[1];
254         bottleBodyArcRight = pos[0] + bottleBodyArcRadius;
255         bottleBodyArcBottom = pos[1] + bottleBodyArcRadius * 2;
256 
257         r2.set(bottleBodyArcLeft, bottleBodyArcTop, bottleBodyArcRight, bottleBodyArcBottom);//原来绘制矩阵还有这个说法,先定 左上角和右下角的坐标;
258 
259         mBottlePath.addArc(r2, bottleBodyStartAngle, bottleBodySweepAngle);//弧形瓶身
260 
261         bottleBottomSomeContour = Math.sin(Math.toRadians(180 - Math.abs(bottleBodySweepAngle))) * bottleBodyArcRadius;//由于上面的弧度并没有划过180度,所以,会有剩余的角度对应着一段X方向的距离
262         // 上面的弧形画完了,下面接着弧形的这个终点,画直线
263         mBottlePath.rLineTo(bottleNeckWidth / 2 + (float) bottleBottomSomeContour * 1.2f, 0);//瓶底
264     }
265 
266     /**
267      * 右边这一半其实是左边一半的镜像,沿着左边那一半右边线,向右翻转180度,就像翻书一样
268      */
269     private void addPartRight() {
270         //由于是对称图形,所以··复制左边的mPath就行了;
271         Camera camera = new Camera();//看Camera类的注释就知道,Camera实例是用来计算3D转换,以及生成一个可用的矩阵(比如给Canvas用)
272         Matrix matrix = new Matrix();
273         camera.save();//保存当前状态,save和restore是配套使用的
274         camera.rotateY(180);//旋转180度,相当于照镜子,复制镜像,但是这里只是指定了旋转的度数,并没有指定旋转的轴,
275         // 所以我也是很疑惑,旋转中心轴是怎么弄的;属性动画的旋转轴,应该就是控件的中心线(沿着x轴旋转,就是用Y的中垂线作为轴;沿着Y轴旋转,就是用X的中垂线做轴)
276         // 这里的旋转不是在控件层面,而是在 path层面,所以,要手动指定旋转轴
277         camera.getMatrix(matrix);//计算矩阵坐标到当前转换,以及 复制它到 参数matrix对象中;
278         camera.restore();//还原状态
279 
280         //设置矩阵旋转的轴;因为我复制出来的path,是和左边那一半覆盖的,而我要将以一条竖线往右翻转180度,达到复制镜像的目的
281         float rotateX = startX + bottleNeckWidth / 2;//旋转的轴线的X坐标
282 
283         matrix.preTranslate(-rotateX, 0);//由于是Y轴方向上的旋转,而且只是想复制镜像,原来path的Y轴坐标不需要改变,所以这里dy传0就好了
284         matrix.postTranslate(rotateX, 0);//其实这里还有很多骚操作,闲的蛋疼的话可以改参数玩一下
285         //原来这个矩阵变换,是给旋转做参数的么
286         //矩阵matrix已经好了,现在把矩阵对象设置给这个path
287         Path rightBottlePath = new Path();
288         rightBottlePath.addPath(mBottlePath);//复制左边的路径;不影响参数path对象
289 
290         //这里解释一下这两个参数:
291         // 其一,rightBottlePath,它是右边那一半的路径
292         // 其二,matrix,这个是一个矩阵对象,它在本案例中的就是 控制一个旋转中心点的作用;
293         mBottlePath.addPath(rightBottlePath, matrix);
294     }
295 
296     /**
297      * 计算直线的最终坐标
298      *
299      * @param mPath
300      * @param pos
301      */
302     private void calculateLastPartOfPathEndingPos(Path mPath, float[] pos) {
303         PathMeasure pathMeasure = new PathMeasure();
304         pathMeasure.setPath(mPath, false);
305         pathMeasure.getPosTan(pathMeasure.getLength(), pos, new float[2]);//找出终点的位置
306     }
307 
308     @Override
309     protected void onDraw(Canvas canvas) {
310         super.onDraw(canvas);
311 
312         initWH();
313         initPercents();
314         initParams();
315         initPaint();
316 
317         updateWaterFlowParams();
318 
319         calculateBottlePath(canvas);
320         calculateWaterPath(canvas);
321 
322         invalidate();//不停刷新自己
323     }
324 
325 
326     /**
327      * 计算瓶中的水的path并且绘制出来
328      * <p>
329      * 思路,整个path是逆时针的添加元素的;
330      * 添加的顺序是 一段弧线arc,一段直线line,一段弧线arc,一段二阶曲线quad,一段三阶曲线cubic,一段二阶曲线quad
331      * <p>
332      * 这里我采用的是相对坐标定位,以path当前的点为基准,设定目标点的相对坐标,使用的方法都是r开头的,比如rLine,arcTo,rQuad 等
333      */
334     private void calculateWaterPath(Canvas canvas) {
335         if (mWaterPath == null)
336             mWaterPath = new Path();
337         else
338             mWaterPath.reset();
339 
340         //从瓶身左侧开始,逆时针绘制,
341         float margin = paintWidth * 3;
342         RectF leftArcRect = new RectF(bottleBodyArcLeft + margin, bottleBodyArcTop + margin, bottleBodyArcRight - margin, bottleBodyArcBottom - margin);
343         mWaterPath.arcTo(leftArcRect, -180, -70f);//左侧一个逆时针的70度圆弧
344         mWaterPath.rLineTo(((float) bottleBottomSomeContour * 2 + bottleNeckWidth), 0);//从左到右的直线
345 
346         //右侧圆弧
347         //然后是弧线;由于我先画的是右半边的弧线,所以,矩形定位要用左半边的矩形坐标来转换
348         float left = bottleBodyArcLeft + bottleNeckWidth;
349         float top = bottleBodyArcTop;
350         float right = bottleBodyArcLeft + bottleBodyArcRadius * 2 + bottleNeckWidth;//
351         float bottom = bottleBodyArcBottom;
352         RectF rightArcRect = new RectF(left + margin, top + margin, right - margin, bottom - margin);
353         mWaterPath.arcTo(rightArcRect, 70f, -70f);
354 
355         //右边弧线画完之后的坐标
356         float rightArcEndX = leftArcRect.left + leftArcRect.width() + bottleNeckWidth;
357         float rightArcEndY = leftArcRect.top + leftArcRect.height() / 2;
358 
359         float waterFaceWidth = bottleBodyArcRadius * 2 + bottleNeckWidth - margin * 2;//水面的横向长度
360 
361         // 直接用一整段3阶贝塞尔曲线,结果发现,和边界的连接点不圆滑;
362         // 替换方案:从右到左,整个曲线分为三段,第一段是二阶曲线,长度比例为0.05;第二段是三阶曲线,长度比例0.9,第三段是  二阶曲线,长度比例为0.05
363         // 1、先用一段二阶曲线,连接右边界的点,和 中间三阶曲线的起点
364         float right_endX = -waterFaceWidth * rightQuadLengthRatio;// 右边一段曲线的X横跨长度
365         float right_endY = -bottleBodyArcRadius * waterLeftRatio;//右边二阶曲线的终点位置
366 
367         float right_controlX = right_endX * 0f;//右边的二阶曲线的控制点X
368         float right_controlY = right_endY * 1;//右边的二阶曲线的控制点Y
369 
370         // 2、贝塞尔曲线的终点相对坐标
371         // 画3阶曲线作为主波浪
372         float relative_controlX1 = -waterFaceWidth * centerCubicControlX_1;//控制点的相对坐标X
373         float relative_controlY1 = -bottleBodyArcRadius * centerCubicControlY_1;//控制点的相对坐标y
374 
375         float relative_controlX2 = -waterFaceWidth * centerCubicControlX_2;//控制点的相对坐标X
376         float relative_controlY2 = -bottleBodyArcRadius * centerCubicControlY_2;//控制点的相对坐标y
377 
378         float relative_endX = -waterFaceWidth * midCubicLengthRatio;//中间三阶曲线的横向长度
379         float relative_endY = 0;
380 
381         // 3、再用一段二阶曲线来封闭图形
382         // 我还得根据那个矩形,算出起点位置
383         float leftQuadLineEndX = -waterFaceWidth * leftQuadLengthRatio;
384         float leftQuadLineEndY = bottleBodyArcRadius * waterLeftRatio;
385 
386         float left_controlX = leftQuadLineEndX * 1;//左边的二阶曲线的控制点X
387         float left_controlY = leftQuadLineEndY * 0;//左边的二阶曲线的控制点Y
388 
389         float[] pos = new float[2];//终点的坐标,0 位置是X,1位置是Y
390         calculateLastPartOfPathEndingPos(mWaterPath, pos);//这个pos的值在执行了这一行之后已经发生了改变 , 这个pos就是结束坐标,里面存了x和y
391 
392         //下面全部采用的相对坐标,都是以当前的点为基准的相对坐标
393         mWaterPath.rQuadTo(right_controlX, right_controlY + rightQuadControlPointOffsetY, right_endX, right_endY);//右边的二阶曲线
394 
395         float[] pos2 = new float[2];//终点的坐标,0 位置是X,1位置是Y
396         calculateLastPartOfPathEndingPos(mWaterPath, pos2);//这个pos的值在执行了这一行之后已经发生了改变 , 这个pos就是结束坐标,里面存了x和y
397 
398         mWaterPath.rCubicTo(relative_controlX1, relative_controlY1, relative_controlX2, relative_controlY2, relative_endX, relative_endY);
399 
400         float[] pos3 = new float[2];//终点的坐标,0 位置是X,1位置是Y
401         calculateLastPartOfPathEndingPos(mWaterPath, pos3);//这个pos的值在执行了这一行之后已经发生了改变 , 这个pos就是结束坐标,里面存了x和y
402         mWaterPath.rQuadTo(left_controlX, left_controlY - leftQuadControlPointOffsetY, leftQuadLineEndX, leftQuadLineEndY);//用绝对坐标的二阶曲线,封闭图形;
403 
404         canvas.drawPath(mWaterPath, mWaterPaint);//画瓶子内的水
405 
406         canvas.drawPoint(rightArcEndX, rightArcEndY, mPointPaint);//右边弧线画完之后的终点,同时也是右边二阶曲线的起点
407         canvas.drawPoint(pos[0] + right_endX, pos[1] + right_endY, mPointPaint);//右边弧线画完之后的终点
408 
409         canvas.drawPoint(pos2[0] + relative_controlX1, pos2[1] + relative_controlY1, mPointPaint);//三阶曲线的右边控制点
410         canvas.drawPoint(pos2[0] + relative_controlX2, pos2[1] + relative_controlY2, mPointPaint);//三阶曲线的左边控制点
411 
412         canvas.drawPoint(pos3[0] + leftQuadLineEndX, pos3[1] + leftQuadLineEndY, mPointPaint);//左边一小段二阶曲线的终点
413         canvas.drawPoint(pos3[0], pos3[1], mPointPaint);//左边一小段二阶曲线的起点
414 
415         //我的目标,就是确定一个斜率
416         float offsetX = waterFaceWidth * rightQuadLengthRatio;
417         float x1 = pos2[0] + relative_controlX1;
418         float y1 = pos2[1] + relative_controlY1;
419 
420         float x2 = pos[0] + right_endX;
421         float y2 = pos[1] + right_endY;
422 
423         rightQuadControlPointOffsetY = calControlPointOffsetY(x1, y1, x2, y2, offsetX);
424         canvas.drawPoint(pos[0] + right_controlX, pos[1] + right_controlY + calControlPointOffsetY(x1, y1, x2, y2, offsetX), mPointPaint);//右边一小段二阶曲线的控制点(是逆时针的曲线)
425 
426         //算出左边的
427         offsetX = waterFaceWidth * rightQuadLengthRatio;
428         x1 = pos2[0] + relative_controlX2;
429         y1 = pos2[1] + relative_controlY2;
430 
431         x2 = pos3[0];
432         y2 = pos3[1];
433 
434         leftQuadControlPointOffsetY = calControlPointOffsetY(x1, y1, x2, y2, offsetX);//把这两个值保存起来,下次刷新的时候用
435         canvas.drawPoint(pos3[0] + left_controlX, pos3[1] + left_controlY - calControlPointOffsetY(x1, y1, x2, y2, offsetX), mPointPaint);//左边一小段二阶曲线的控制点
436 
437     }
438 
439     /**
440      * 计算出控制点的Y轴偏移量
441      *
442      * @param x1      第一个点X
443      * @param y1      第一个点Y
444      * @param x2      第二个点X
445      * @param y2      第二个点Y
446      * @param offsetX 已知的X轴偏移量
447      * @return
448      */
449     private float calControlPointOffsetY(float x1, float y1, float x2, float y2, float offsetX) {
450         float tan = (y2 - y1) / (x2 - x1);//斜率
451         float offsetY = offsetX * tan;
452         return offsetY;
453     }
454 
455     //辅助类
456     private ParamObj obj2, obj3;//看ParamObj的注釋;
457 
458     /**
459      * 改变水流参数,来实现水面的动态效果
460      */
461     private void updateWaterFlowParams() {
462 
463         if (obj2 == null) {
464             obj2 = new ParamObj(-0.3f, false);
465         }
466         if (obj3 == null) {
467             obj3 = new ParamObj(0.3f, true);
468         }
469 
470         centerCubicControlY_1 = calParam(-0.6f, 0.6f, obj2);
471         centerCubicControlY_2 = calParam(-0.6f, 0.6f, obj3);
472     }
473 
474     /**
475      * 做一个方法,让数字在两个范围之内变化,比如,从0到100,然后100到0,然后0到100;
476      *
477      * @param min
478      * @param max
479      * @param currentObj
480      * @return
481      */
482     private float calParam(float min, float max, ParamObj currentObj) {
483         if (currentObj.param >= min && currentObj.param <= max) {//如果在范围之内,就按照原来的方向,继续变化
484             if (currentObj.ifReverse) {
485                 currentObj.param = currentObj.param + paramDelta;
486             } else {
487                 currentObj.param = currentObj.param - paramDelta;
488             }
489         } else if (currentObj.param == max) {//如果到了最大值,就变小
490             currentObj.ifReverse = true;
491         } else if (currentObj.param == min) {//如果到了最小值,就变大
492             currentObj.ifReverse = false;
493         } else if (currentObj.param > max) {
494             currentObj.param = max;
495             currentObj.ifReverse = false;
496         } else if (currentObj.param < min) {
497             currentObj.param = min;
498             currentObj.ifReverse = true;
499         }
500         Log.d("calParam", "" + currentObj.param);
501         return currentObj.param;
502     }
503 
504     class ParamObj {
505         Float param;
506         Boolean ifReverse;//是否反向(设定:true为数字递增,false为递减)
507 
508         /**
509          * @param original  初始值
510          * @param ifReverse 初始顺序
511          */
512         ParamObj(float original, boolean ifReverse) {
513             this.param = original;
514             this.ifReverse = ifReverse;
515         }
516     }
517 
518 
519 }

辅助类:DpUtil.java

 1 package com.example.complex_animation;
 2 
 3 import android.content.Context;
 4 
 5 public class DpUtil {
 6     //辅助,dp和px的转换
 7     public static int px2dp(Context context, float pxValue) {
 8         final float scale = context.getResources().getDisplayMetrics().density;
 9         return (int) (pxValue / scale + 0.5f);
10     }
11 
12     public static int dp2Px(Context context, float dipValue) {
13         final float scale = context.getResources().getDisplayMetrics().density;
14         return (int) (dipValue * scale + 0.5f);
15     }
16 }

MainActivity.java

 1 package com.example.complex_animation;
 2 
 3 import android.support.v7.app.AppCompatActivity;
 4 import android.os.Bundle;
 5 
 6 /**
 7  */
 8 public class MainActivity extends AppCompatActivity {
 9 
10     @Override
11     protected void onCreate(Bundle savedInstanceState) {
12         super.onCreate(savedInstanceState);
13         setContentView(R.layout.activity_main);
14         //先看看怎么画不规则图形。比如,一个烧杯。。原来是Path被玩出了花;
15 
16     }
17 }

布局文件 activity_main.xml

 

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="#ff191f26"
    android:gravity="center"
    tools:context=".MainActivity">

    <com.example.complex_animation.MyBottleView
        android:layout_width="300dp"
        android:layout_height="300dp" />

</LinearLayout>

最后

附上Github地址:https://github.com/18598925736/BottleWaterView

猜你喜欢

转载自www.cnblogs.com/hankzhouAndroid/p/9647921.html
今日推荐