效果图

效果图中我们实现了一个简单的随手指滑动的二阶贝塞尔曲线,还有一个复杂点的,穿越所有已知点的贝塞尔曲线。学会使用贝塞尔曲线后可以实现例如QQ红点滑动删除啦,360动态球啦,bulabulabula~

什么是贝塞尔曲线?

贝赛尔曲线(Bézier曲线)是电脑图形学中相当重要的参数曲线。更高维度的广泛化贝塞尔曲线就称作贝塞尔曲面,其中贝塞尔三角是一种特殊的实例。贝塞尔曲线于1962年,由法国工程师皮埃尔·贝塞尔(Pierre Bézier)所广泛发表,他运用贝塞尔曲线来为汽车的主体进行设计。贝塞尔曲线最初由Paul de Casteljau于1959年运用de Casteljau算法开发,以稳定数值的方法求出贝塞尔曲线。

读完上述贝塞尔曲线简介我还是一头雾水,来个示例呗。

示例

线性贝塞尔曲线

给定点P0、P1,线性贝塞尔曲线只是一条两点之间的直线。这条线由下式给出: 
 

二次方贝塞尔曲线

二次方贝塞尔曲线的路径由给定点P0、P1、P2的函数B(t)追踪: 
 
 

三次方贝塞尔曲线

P0、P1、P2、P3四个点在平面或在三维空间中定义了三次方贝塞尔曲线。曲线起始于P0走向P1,并从P2的方向来到P3。一般不会经过P1或P2;公式如下: 
 
 

N次方贝塞尔曲线

身为三维生物超出三维我很方,这里只给示例图。想具体了解的同学请左转度娘。 
 

就当没看过上面

Android在API=1的时候就提供了贝塞尔曲线的画法,只是隐藏在Path#quadTo()和Path#cubicTo()方法中,一个是二阶贝塞尔曲线,一个是三阶贝塞尔曲线。当然,如果你想自己写个方法,依照上面贝塞尔的表达式也是可以的。不过一般没有必要,因为Android已经在native层为我们封装好了二阶和三阶的函数。

从一个二阶贝塞尔开始

自定义一个BezierView

初始化各个参数,花3s扫一下即可。

    private Paint mPaint;
private Path mPath;
private Point startPoint;
private Point endPoint;
// 辅助点
private Point assistPoint;
public BezierView(Context context) {
this(context, null);
} public BezierView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
} public BezierView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init(context);
} private void init(Context context) {
mPaint = new Paint();
mPath = new Path();
startPoint = new Point(300, 600);
endPoint = new Point(900, 600);
assistPoint = new Point(600, 900);
// 抗锯齿
mPaint.setAntiAlias(true);
// 防抖动
mPaint.setDither(true);
}

在onDraw中画二阶贝塞尔

        // 画笔颜色
mPaint.setColor(Color.BLACK);
// 笔宽
mPaint.setStrokeWidth(POINTWIDTH);
// 空心
mPaint.setStyle(Paint.Style.STROKE);
// 重置路径
mPath.reset();
// 起点
mPath.moveTo(startPoint.x, startPoint.y);
// 重要的就是这句
mPath.quadTo(assistPoint.x, assistPoint.y, endPoint.x, endPoint.y);
// 画路径
canvas.drawPath(mPath, mPaint);
// 画辅助点
canvas.drawPoint(assistPoint.x, assistPoint.y, mPaint);

上面注释很清晰就不赘述了。示例中贝塞尔是可以跟着手指的滑动而变化,我一拍榴莲,肯定是复写了onTouchEvent()!

    @Override
public boolean onTouchEvent(MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
case MotionEvent.ACTION_MOVE:
assistPoint.x = (int) event.getX();
assistPoint.y = (int) event.getY();
Log.i(TAG, "assistPoint.x = " + assistPoint.x);
Log.i(TAG, "assistPoint.Y = " + assistPoint.y);
invalidate();
break;
}
return true;
}

最后将我们自定义的BezierView添加到布局文件中。至此一个简单的二阶贝塞尔曲线就完成了。假设一下,在向下拉动的过程中,在曲线上增加一个“小超人”,360动态清理是不是就出来了呢?有兴趣的可以自己拓展下。

以一个三阶贝塞尔结束

天气预报曲线图示例

(图一) 
 
(图二) 

概述

要想得到上图的效果,需要二阶贝塞尔和三阶贝塞尔配合。具体表现为,第一段和最后一段曲线为二阶贝塞尔,中间N段都为三阶贝塞尔曲线。

思路

先根据相邻点(P1,P2, P3)计算出相邻点的中点(P4, P5),然后再计算相邻中点的中点(P6)。然后将(P4,P6, P5)组成的线段平移到经过P2的直线(P8,P2,P7)上。接着根据(P4,P6,P5,P2)的坐标计算出(P7,P8)的坐标。最后根据P7,P8等控制点画出三阶贝塞尔曲线。

点和线的解释

  1. 黑色点:要经过的点,例如温度
  2. 蓝色点:两个黑色点构成线段的中点
  3. 黄色点:两个蓝色点构成线段的中点
  4. 灰色点:贝塞尔曲线的控制点
  5. 红色线:黑色点的折线图
  6. 黑色线:黑色点的贝塞尔曲线,也是我们最终想要的效果

声明

为了方便讲解以及读者的理解。本篇以图一效果为例进行讲解。BezierView坐标都是根据屏幕动态生成的,想要图二的效果只需修改初始坐标,不用对代码做很大的修改即可实现。

那么,开始吧!

初始化参数

    private static final String TAG = "BIZIER";
private static final int LINEWIDTH = 5;
private static final int POINTWIDTH = 10; private Context mContext;
/** 即将要穿越的点集合 */
private List<Point> mPoints = new ArrayList<>();
/** 中点集合 */
private List<Point> mMidPoints = new ArrayList<>();
/** 中点的中点集合 */
private List<Point> mMidMidPoints = new ArrayList<>();
/** 移动后的点集合(控制点) */
private List<Point> mControlPoints = new ArrayList<>(); private int mScreenWidth;
private int mScreenHeight;
private void init(Context context) {
mPaint = new Paint();
mPath = new Path();
// 抗锯齿
mPaint.setAntiAlias(true);
// 防抖动
mPaint.setDither(true); mContext = context;
getScreenParams();
initPoints();
initMidPoints(this.mPoints);
initMidMidPoints(this.mMidPoints);
initControlPoints(this.mPoints, this.mMidPoints , this.mMidMidPoints); }

第一个函数获取屏幕宽高就不说了。紧接着初始化了初始点、中点、中点的中点、控制点。我们一个个的跟进。首先是初始点。

    /** 添加即将要穿越的点 */
private void initPoints() {
int pointWidthSpace = mScreenWidth / 5;
int pointHeightSpace = 100;
for (int i = 0; i < 5; i++) {
Point point;
// 一高一低五个点
if (i%2 != 0) {
point = new Point((int) (pointWidthSpace*(i + 0.5)), mScreenHeight/2 - pointHeightSpace);
} else {
point = new Point((int) (pointWidthSpace*(i + 0.5)), mScreenHeight/2);
}
mPoints.add(point);
}
}

这里循环创建了一高一低五个点,并添加到List<Point> mPoints中。上文说道图一到图二只需修改这里的初始点即可。

    /** 初始化中点集合 */
private void initMidPoints(List<Point> points) {
for (int i = 0; i < points.size(); i++) {
Point midPoint = null;
if (i == points.size()-1){
return;
}else {
midPoint = new Point((points.get(i).x + points.get(i + 1).x)/2, (points.get(i).y + points.get(i + 1).y)/2);
}
mMidPoints.add(midPoint);
}
} /** 初始化中点的中点集合 */
private void initMidMidPoints(List<Point> midPoints){
for (int i = 0; i < midPoints.size(); i++) {
Point midMidPoint = null;
if (i == midPoints.size()-1){
return;
}else {
midMidPoint = new Point((midPoints.get(i).x + midPoints.get(i + 1).x)/2, (midPoints.get(i).y + midPoints.get(i + 1).y)/2);
}
mMidMidPoints.add(midMidPoint);
}
}

这里算出中点集合以及中点的中点集合,小学数学题没什么好说的。唯一需要注意的是他们数量的差别。

    /** 初始化控制点集合 */
private void initControlPoints(List<Point> points, List<Point> midPoints, List<Point> midMidPoints){
for (int i = 0; i < points.size(); i ++){
if (i ==0 || i == points.size()-1){
continue;
}else{
Point before = new Point();
Point after = new Point();
before.x = points.get(i).x - midMidPoints.get(i - 1).x + midPoints.get(i - 1).x;
before.y = points.get(i).y - midMidPoints.get(i - 1).y + midPoints.get(i - 1).y;
after.x = points.get(i).x - midMidPoints.get(i - 1).x + midPoints.get(i).x;
after.y = points.get(i).y - midMidPoints.get(i - 1).y + midPoints.get(i).y;
mControlPoints.add(before);
mControlPoints.add(after);
}
}
}

大家需要注意下这个方法的计算过程。以图一(P2,P4, P6,P8)为例。现在P2、P4、P6的坐标是已知的。根据由于(P8, P2)线段由(P4, P6)线段平移而来,所以可得如下结论:P2 - P6 = P8 - P4 。即P8 = P2 - P6 + P4。其余同理。

画辅助点以及对比折线图

    @Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
// ***********************************************************
// ************* 贝塞尔进阶--曲滑穿越已知点 **********************
// *********************************************************** // 画原始点
drawPoints(canvas);
// 画穿越原始点的折线
drawCrossPointsBrokenLine(canvas);
// 画中间点
drawMidPoints(canvas);
// 画中间点的中间点
drawMidMidPoints(canvas);
// 画控制点
drawControlPoints(canvas);
// 画贝塞尔曲线
drawBezier(canvas); }

可以看到,在画贝塞尔曲线之前我们画了一系列的辅助点,还有和贝塞尔曲线作对比的折线图。效果如图一。辅助点的坐标全都得到了,基本的画画就比较简单了。有能力的可跳过下面这段,直接进入drawBezier(canvas)方法。基本的画画这里只贴代码,如有疑问可评论或者私信。

    /** 画原始点 */
private void drawPoints(Canvas canvas) {
mPaint.setStrokeWidth(POINTWIDTH);
for (int i = 0; i < mPoints.size(); i++) {
canvas.drawPoint(mPoints.get(i).x, mPoints.get(i).y, mPaint);
}
} /** 画穿越原始点的折线 */
private void drawCrossPointsBrokenLine(Canvas canvas) {
mPaint.setStrokeWidth(LINEWIDTH);
mPaint.setColor(Color.RED);
// 重置路径
mPath.reset();
// 画穿越原始点的折线
mPath.moveTo(mPoints.get(0).x, mPoints.get(0).y);
for (int i = 0; i < mPoints.size(); i++) {
mPath.lineTo(mPoints.get(i).x, mPoints.get(i).y);
}
canvas.drawPath(mPath, mPaint);
} /** 画中间点 */
private void drawMidPoints(Canvas canvas) {
mPaint.setStrokeWidth(POINTWIDTH);
mPaint.setColor(Color.BLUE);
for (int i = 0; i < mMidPoints.size(); i++) {
canvas.drawPoint(mMidPoints.get(i).x, mMidPoints.get(i).y, mPaint);
}
} /** 画中间点的中间点 */
private void drawMidMidPoints(Canvas canvas) {
mPaint.setColor(Color.YELLOW);
for (int i = 0; i < mMidMidPoints.size(); i++) {
canvas.drawPoint(mMidMidPoints.get(i).x, mMidMidPoints.get(i).y, mPaint);
} } /** 画控制点 */
private void drawControlPoints(Canvas canvas) {
mPaint.setColor(Color.GRAY);
// 画控制点
for (int i = 0; i < mControlPoints.size(); i++) {
canvas.drawPoint(mControlPoints.get(i).x, mControlPoints.get(i).y, mPaint);
}
}

画贝塞尔曲线

    /** 画贝塞尔曲线 */
private void drawBezier(Canvas canvas) {
mPaint.setStrokeWidth(LINEWIDTH);
mPaint.setColor(Color.BLACK);
// 重置路径
mPath.reset();
for (int i = 0; i < mPoints.size(); i++){
if (i == 0){// 第一条为二阶贝塞尔
mPath.moveTo(mPoints.get(i).x, mPoints.get(i).y);// 起点
mPath.quadTo(mControlPoints.get(i).x, mControlPoints.get(i).y,// 控制点
mPoints.get(i + 1).x,mPoints.get(i + 1).y);
}else if(i < mPoints.size() - 2){// 三阶贝塞尔
mPath.cubicTo(mControlPoints.get(2*i-1).x,mControlPoints.get(2*i-1).y,// 控制点
mControlPoints.get(2*i).x,mControlPoints.get(2*i).y,// 控制点
mPoints.get(i+1).x,mPoints.get(i+1).y);// 终点
}else if(i == mPoints.size() - 2){// 最后一条为二阶贝塞尔
mPath.moveTo(mPoints.get(i).x, mPoints.get(i).y);// 起点
mPath.quadTo(mControlPoints.get(mControlPoints.size()-1).x,mControlPoints.get(mControlPoints.size()-1).y,
mPoints.get(i+1).x,mPoints.get(i+1).y);// 终点
}
}
canvas.drawPath(mPath,mPaint);
}

注释太详细,都没什么好写的了。不过这里需要注意判断里面的条件,对起点和终点的判断一定要理解。要不然很可能会送你一个ArrayIndexOutOfBoundsException。

结束

贝塞尔曲线可以实现很多绚丽的效果,难的不是贝塞尔,而是good idea。

BezierView源码下载:http://download.csdn.net/detail/qq_17250009/9478018

Android 自定义View高级特效,神奇的贝塞尔曲线的更多相关文章

  1. 简单说说Android自定义view学习推荐的方式

    这几天比较受关注,挺开心的,嘿嘿. 这里给大家总结一下学习自定义view的一些技巧.  以后写自定义view可能不会写博客了,但是可以开源的我会把源码丢到github上我的地址:https://git ...

  2. Android 自定义 View 绘制

    在 Android 自定义View 里面,介绍了自定义的View的基本概念.同时在 Android 控件架构及View.ViewGroup的测量 里面介绍了 Android 的坐标系 View.Vie ...

  3. Android自定义View

    转载请标明出处:http://blog.csdn.net/lmj623565791/article/details/24252901 很多的Android入门程序猿来说对于Android自定义View ...

  4. Android 自定义 View 圆形进度条总结

    Android 自定义圆形进度条总结 版权声明:本文为博主原创文章,未经博主允许不得转载. 微博:厉圣杰 微信公众号:牙锅子 源码:CircleProgress 文中如有纰漏,欢迎大家留言指出. 最近 ...

  5. android 自定义view 前的基础知识

    本篇文章是自己自学自定义view前的准备,具体参考资料来自 Android LayoutInflater原理分析,带你一步步深入了解View(一) Android视图绘制流程完全解析,带你一步步深入了 ...

  6. Android自定义View 画弧形,文字,并增加动画效果

    一个简单的Android自定义View的demo,画弧形,文字,开启一个多线程更新ui界面,在子线程更新ui是不允许的,但是View提供了方法,让我们来了解下吧. 1.封装一个抽象的View类   B ...

  7. (转)[原] Android 自定义View 密码框 例子

    遵从准则 暴露您view中所有影响可见外观的属性或者行为. 通过XML添加和设置样式 通过元素的属性来控制其外观和行为,支持和重要事件交流的事件监听器 详细步骤见:Android 自定义View步骤 ...

  8. Android 自定义View合集

    自定义控件学习 https://github.com/GcsSloop/AndroidNote/tree/master/CustomView 小良自定义控件合集 https://github.com/ ...

  9. Android 自定义View (五)——实践

    前言: 前面已经介绍了<Android 自定义 view(四)-- onMeasure 方法理解>,那么这次我们就来小实践下吧 任务: 公司现有两个任务需要我完成 (1)监测液化天然气液压 ...

随机推荐

  1. C语言的指针

    指针是C语言中非常重要的数据类型,那么什么是指针呢? 指针类型就是用来用来存放变量地址的变量,指向某个变量. 指针的一般形式:*指针变量名 int *p; float *p1; “*”是用来说明这个变 ...

  2. bzoj2011: [Ceoi2010]Mp3 Player

    Description Georg有个MP3 Player,没有任何操作T秒钟就会锁定,这时按下任意一个键就会变回没锁定的状态,但不会改变频道.只有在没锁定的状态下按键才有可能改变频道. MP3的频道 ...

  3. php的几个版本的区别?

    1. VC6与VC9的区别:VC6版本是使用Visual Studio 6编译器编译的,如果你的PHP是用Apache来架设的,那你就选择VC6版本.VC9版本是使用Visual Studio 200 ...

  4. ibatis错误

    java.lang.IllegalArgumentException: Mapped Statement collection already contains value for com.regin ...

  5. jQuery分页插件jBootstrapPage,一个Bootstrap风格的分页插件

    一个Bootstrap风格的分页控件,对于喜欢Bootstrap简洁美观和扁平化的同学可以关注jBootstrapPage, 目前jBootstrapPage最新版为V0.1,后续还有更多功能需要完善 ...

  6. Flyer

    hdu4768:http://acm.hdu.edu.cn/showproblem.php?pid=4768 题意:给你1--2^32个位置,然后有m个操作,每次操作给你3个数 a,b,c,然后在a, ...

  7. Watch gcc at ubuntu 12,See ELF file header

    first write article at my ubuntu 12. ELF is very important file format.

  8. RSA算法原理(二)

    上一次,我介绍了一些数论知识. 有了这些知识,我们就可以看懂RSA算法.这是目前地球上最重要的加密算法. 六.密钥生成的步骤 我们通过一个例子,来理解RSA算法.假设爱丽丝要与鲍勃进行加密通信,她该怎 ...

  9. 【POJ】2886 Who Gets the Most Candies?

    移动题目相当麻烦. #include <stdio.h> #include <string.h> #define MAXN 500005 #define lson l, mid ...

  10. 【转】android: 长按删除listview的item

    原文网址:http://www.cnblogs.com/nuistlr/archive/2012/09/07/2675649.html 首先要继承OnItemLongClickListener pub ...