博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
贝塞尔曲线模仿练习
阅读量:5996 次
发布时间:2019-06-20

本文共 8057 字,大约阅读时间需要 26 分钟。

参考文章:

目录

注:请不要边看边复制代码,以下代码不全。建议缕清整个绘制流程后,到中去看完整代码。写这篇文章的原因是这个github上的代码注释很少,自己也琢磨了很久。为了巩固知识,加深理解,才有了这篇文章。

1. 效果图展示

2. 绘制流程

2.1 onMeasure测量大小

这一步我们直接调用父类的onMeasure(widthMeasureSpec, heightMeasureSpec)方法就好了,不需要过多的干涉。 代码如下:

protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {        super.onMeasure(widthMeasureSpec, heightMeasureSpec);    }复制代码

2.2 onLayout 摆放位置

通过图片,我们可知,现在两圆间距,圆的半径(半径可以有一个默认值,然后开放自定义属性让外界传入)都已知,但是缺少图片宽度这个重要参数。

其实,我们可以这么想,这个图片的大小,肯定是不能超过圆的半径的,而且,为了协调,它最好是个正方形。所以,我们可以让外界传入一个比例值,让使用者规定图片的大小要相对于圆的半径多大,这样,图片的宽度就可以确定了,而且也可以确保图片一定在圆圈之内。

so,图片的宽度可以这么计算:

int picWidth = scale * radius ;//scale就是传入的比例复制代码

好了,图片的宽度已知了,那么我们的摆放位置也就可以求了:

  1. 首先在onSizeChanged保存一些需要重复使用到的数值(比如:间距)
@Override    protected void onSizeChanged(int w, int h, int oldw, int oldh) {        spacing = (w - 2 * tabNum * radius) / (tabNum + 1);        startX = spacing + radius;        startY = h / 2;        super.onSizeChanged(w, h, oldw, oldh);    }复制代码

这里的startX是第一个圆的中点x坐标,startY是第一个圆的中点y坐标。 这里没有直接记录控件的宽和高,因为通过第一个圆来求其他圆的位置会容易点。 2. 在onLayout中确定各个圆的位置

private float scale = 0.5f; @Override    protected void onLayout(boolean changed, int l, int t, int r, int b) {        tabNum = getChildCount();        for (int i = 0; i < tabNum; i++) {            View child = getChildAt(i);            child.layout((int) (spacing + (1 - scale * 1) * radius + i * (spacing + 2 * radius)),                    (int) (startY - scale * radius ),                    (int) (spacing + (1 + scale * 1 ) * radius + i * (spacing + 2 * radius)),                    (int) (startY + scale * radius ));        }    }复制代码

这里如果看 源码的话,会看到,它的计算方法中还会除以一个g2变量,这个g2变量等于1.41421。 他的代码如下:

child.layout((int) (div + (1 - scale * 1 / g2) * radius + i * (div + 2 * radius)),                     (int) (startY - scale * radius / g2),                     (int) (div + (1 + scale * 1 / g2) * radius + i * (div + 2 * radius)),                    (int) (startY + scale * radius / g2));复制代码

div就是我们的spacing,也就是两圆间距。 作者这么做的原因未知,希望知道的人留言告知一下。多谢~

2.3 dispatchDraw将圆和图片绘制出来

在测量好大小,确定好位置后,我们就可以拿起我们的画笔,进行绘制图形。

  1. 初始化画笔
private float radius = 50;public MyDropIndicator(Context context) {        super(context);        init();    }public MyDropIndicator(Context context, AttributeSet attrs) {        super(context, attrs);        init();    }private void init() {        mPaintCircle = new Paint();        mPaintCircle.setColor(Color.RED);        mPaintCircle.setStyle(Paint.Style.STROKE);        mPaintCircle.setAntiAlias(true);        mPaintCircle.setStrokeWidth(3);}复制代码
  1. dispatchDraw方法绘制图形
protected void dispatchDraw(Canvas canvas) {        tabNum = getChildCount();        for (int i = 0; i < tabNum; i++) {            canvas.drawCircle(spacing + radius + i * (spacing + 2 * radius), startY, radius, mPaintCircle);        }.....}复制代码

到这一步,基本的界面应该就可以看到了。可以测试一下:

public class TestActivity extends AppCompatActivity {    @Override    protected void onCreate(@Nullable Bundle savedInstanceState) {        super.onCreate(savedInstanceState);        setContentView(R.layout.activity_test);    }}复制代码
复制代码

运行起来的界面效果如下:

2.4 绘制点击图形的波纹效果

点击后的效果如图:

这段动画的原理就是在一段时间内,不断改变圆的半径。 这个效果很容易做,难点应该是如何知道点击的是哪个圆,以及如何确定动画执行的圆的中心点。 那么如何知道是哪个圆呢?

从图中可以看出第一个圆的范围是:

spacing < x < spacing + radius * 2复制代码

依次类推,我们可以得出: 第N个圆的点击范围是:

(N-1)* spacing + radius * 2 < x< N*spacing + radius * 2复制代码

明白原理后,应该我们就可以开始写了。。。

  1. 初始化画笔
mClickPaint = new Paint();mClickPaint.setColor(Color.YELLOW);mClickPaint.setStyle(Paint.Style.STROKE);mClickPaint.setAntiAlias(true);mClickPaint.setStrokeWidth(radius / 2);复制代码
  1. onTouchEvent中处理点击事件
public boolean onTouchEvent(MotionEvent event){        float x = event.getX();        if (x > spacing + 2 * radius && x < (spacing + 2 * radius) * tabNum) {            int toPos = (int) (x / (spacing + 2 * radius));            if (toPos != currentPos && toPos <= tabNum) {                startAniTo(currentPos, toPos);            }        } else if (x > spacing && x < spacing + 2 * radius) {            if (currentPos != 0) {                startAniTo(currentPos, 0);            }        }        return super.onTouchEvent(event);    }复制代码
  1. 启动动画
private boolean startAniTo(int currentPos, int toPos) {        this.currentPos = currentPos;        this.toPos = toPos;        if (currentPos == toPos) {            return true;        }        if (animator == null) {            animator = ValueAnimator.ofFloat(0, 1.0f);            animator.setDuration(1000);            animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {                @Override                public void onAnimationUpdate(ValueAnimator animation) {                    //重点,在draw的时候会根据currentTime进行变换半径                    mCurrentTime = (float) animation.getAnimatedValue();                    invalidate();                }            });               }        animator.start();        return true;    }复制代码
  1. 根据currentTime进行绘制不同半径的圆
protected void dispatchDraw(Canvas canvas) {   //...省略 if (mCurrentTime > 0 && mCurrentTime <= 0.2) {            canvas.drawCircle(spacing + radius + (toPos) * (spacing + 2 * radius), startY, radius * 1.0f * 5 * mCurrentTime, mClickPaint);        } //....省略}复制代码

到这一步,点击效果就完成了。。

2.5 利用贝塞尔曲线绘制移动动画

现在,最难的部分来了。 先放一张最后实现的动画效果。

关于三阶贝塞尔曲线的内容,这里不进行展开,请先阅读文章: 三阶贝塞尔曲线的变化规律,用图形来表示,就是如下:

通过观察最终的效果,我们可以发现,其实圆的状态就是以下两种:

分别是状态1,和状态2。(当从左向右运动时)

要实现这两种状态,首先要了解如何通过贝塞尔曲线画圆。 用贝塞尔曲线绘制一个圆需要12个点,如图所示。

现在,我们写一个demo来测试一下,当改变点的位置以及改变m的大小,会对圆的形状造成什么影响。 首先我们用三阶贝塞尔曲线绘制出了一个圆,此时:

mc = 0.552284749831;m = 圆的半径(demo里是200) * mc;复制代码

至于为什么是0.551915024494f,请查看。 此时,我们得到的圆是这样的:

ok,现在我们改变p2的点试试看效果(p2也就是右边的点):

可以看到,我们可以通过修改p2的位置,使得图形更扁。

好了。接下来,我们修改m的大小:

可以看到,通过修改m的大小,可以改变圆两点之间的弧度,也就是m越大时,两点之间的弧度越扁平。

现在,我们一点点来分析移动过程中图形状态的变化。 首先,我们假设圆从一个点移动到另外一个点用了1s的时间。 现在,

  1. 当 0 < t < 0.2s的时候,当前的形状是:
    可以看到,其实就是把p2点慢慢向右移动,最终移动到半径的两倍的位置; 用代码表示:
if (mCurrentTime > 0 && mCurrentTime <= 0.2) {            //画布向右移动,方便进行绘制圆球            canvas.translate(startX,startY);           //p2向右移动            p2.setX(radius + 2 * 5 * mCurrentTime * radius / 2);        }复制代码
  1. 当 0.2 < t < 0.5s时,
    此时圆开始向右移动,移动的距离是多少呢?答案是:startX + (t- 0.2f) * distance / 0.7f。 那为啥是除以0.7呢?因为0到0.2没平移,0.2到0.9平移完成,0.9到1处理回弹。平移时间只有0.9-0.2=0.7,这段时间要完成一个distance的距离的平移。同时之前圆向右凸起时,p2组的点x坐标总共增加了一个radius(这个决定凸起程度)。现在要把它弄回对称椭圆,所以p1组和p3组的点要右移半个radius,同时mc调整一下使椭圆不那么尖; 用代码表示:
if (mCurrentTime > 0.2f && mCurrentTime <= 0.5f){            canvas.translate(startX + (mCurrentTime - 0.2f) * distance / 0.7f,startY);            p2.setX(2 * radius);            p1.setX(((mCurrentTime - 0.2f) * 0.5f * radius / 0.3f));            p3.setX(((mCurrentTime - 0.2f) * 0.5f * radius / 0.3f));            p2.setMc(mc + (mCurrentTime - 0.2f) * mc /4 / 0.3f);            p4.setMc(mc + (mCurrentTime - 0.2f) * mc /4 / 0.3f);        }复制代码
  1. 当0.5 < t < 0.8s时
    p1和p3的X坐标继续往右移,mc逐渐重置为原来大小,效果就是圆的最右端固定不变,左边的凸起缩回去。 用代码表示:
if (mCurrentTime > 0.5f && mCurrentTime <= 0.8f){            //开始恢复原始形状            canvas.translate(startX + (mCurrentTime - 0.2f) * distance / 0.7f,startY);            p1.setX(0.5f * radius + 0.5f * radius* (mCurrentTime - 0.5f) / 0.3f);            p3.setX(0.5f * radius + 0.5f * radius* (mCurrentTime - 0.5f) / 0.3f);            p2.setMc(1.25f * mc - 0.25f * mc * (mCurrentTime - 0.5f) / 0.3f);            p4.setMc(1.25f * mc - 0.25f * mc * (mCurrentTime - 0.5f) / 0.3f);        }复制代码
  1. 当0.8 < t < 0.9s时
    左边的p4.组点往右平移过头,圆形成凹陷。 用代码表示:
if (mCurrentTime > 0.8 && mCurrentTime <= 0.9) {            p2.setMc(mc);            p4.setMc(mc);            canvas.translate(startX + (mCurrentTime - 0.2f) * distance / 0.7f, startY);           p4.setX(-radius + 1.6f * radius * (mCurrentTime - 0.8f) / 0.1f);        } 复制代码
  1. 当0.9 < t <1s时
    这个阶段是处理回弹,p4.组点x逐渐恢复正常。表现为回弹恢复为标准圆。 用代码表示:
if (mCurrentTime > 0.9 && mCurrentTime < 1) {                p1.setX(radius);                p3.setX(radius);                canvas.translate(startX + distance, startY);                p4.setX(0.6f * radius - 0.6f * radius * (mCurrentTime - 0.9f) / 0.1f);          复制代码

注意,这里的代码都是默认球是从左向右运动的,明白了从左向右运动的规律后,从右向左其实也就不难了。为了节省代码,只粘贴了从左向右运动的。

OK,至此,本文结束。谢谢观看。

转载地址:http://dhqlx.baihongyu.com/

你可能感兴趣的文章
演示:使用IPsec+PKI来完成IP通信的安全
查看>>
Maven和Gradle对比
查看>>
C语言extern关键字用法
查看>>
我的LINUX之路----安装LINUX及远程连接
查看>>
如何提高Java并行程序性能
查看>>
数据加密到底管不管用
查看>>
面向对象程序与类
查看>>
安装vsftpd
查看>>
Linux性能分析的前60000毫秒
查看>>
Power of Three(leetcode326)
查看>>
Nginx之虚拟目录-root与alias的区别
查看>>
关于MySQL二进制日志Binlog的认识
查看>>
×××LAMP+FastCGI+xcache加速器
查看>>
华为交换机通用配置方法
查看>>
lduan server 2012 系统批量激活(三十二)
查看>>
自定义key解决zabbix端口监听取值不准确的问题
查看>>
我的友情链接
查看>>
java --枚举
查看>>
Linux 操作命令 df
查看>>
JS判断坐标点是否在给定的多边形内
查看>>