引子
上网逛技术贴的时候,偶尔看到了这种特效;
想来应该也不是很难,偶有闲暇,研究一下,最后成功之后的效果如下,
并不完全相同。
本来还想继续研究,项目来了,没办法,只能放后面再说;
实现思路,我在项目代码里面会有详细解释;
本文,查阅了很多资料; 主要感谢 这位大佬的神贴: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