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

  • 相关阅读:
    PAT (Advanced Level) 1080. Graduate Admission (30)
    PAT (Advanced Level) 1079. Total Sales of Supply Chain (25)
    PAT (Advanced Level) 1078. Hashing (25)
    PAT (Advanced Level) 1077. Kuchiguse (20)
    PAT (Advanced Level) 1076. Forwards on Weibo (30)
    PAT (Advanced Level) 1075. PAT Judge (25)
    PAT (Advanced Level) 1074. Reversing Linked List (25)
    PAT (Advanced Level) 1073. Scientific Notation (20)
    PAT (Advanced Level) 1072. Gas Station (30)
    PAT (Advanced Level) 1071. Speech Patterns (25)
  • 原文地址:https://www.cnblogs.com/hankzhouAndroid/p/9647921.html
Copyright © 2011-2022 走看看