zoukankan      html  css  js  c++  java
  • 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

  • 相关阅读:
    app测试点-1
    毕业5年的感悟
    关于游戏外挂
    python-unittest单元测试框架
    python-requests
    http简介
    python基础-发邮件smtp
    python-加密
    4 Python 日期和时间
    5 Python 数据类型—数字
  • 原文地址:https://www.cnblogs.com/hankzhouAndroid/p/9647921.html
Copyright © 2011-2022 走看看