Android 自定义View高级特效,神奇的贝塞尔曲线
效果图
效果图中我们实现了一个简单的随手指滑动的二阶贝塞尔曲线,还有一个复杂点的,穿越所有已知点的贝塞尔曲线。学会使用贝塞尔曲线后可以实现例如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等控制点画出三阶贝塞尔曲线。
点和线的解释
- 黑色点:要经过的点,例如温度
- 蓝色点:两个黑色点构成线段的中点
- 黄色点:两个蓝色点构成线段的中点
- 灰色点:贝塞尔曲线的控制点
- 红色线:黑色点的折线图
- 黑色线:黑色点的贝塞尔曲线,也是我们最终想要的效果
声明
为了方便讲解以及读者的理解。本篇以图一效果为例进行讲解。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高级特效,神奇的贝塞尔曲线的更多相关文章
- 简单说说Android自定义view学习推荐的方式
这几天比较受关注,挺开心的,嘿嘿. 这里给大家总结一下学习自定义view的一些技巧. 以后写自定义view可能不会写博客了,但是可以开源的我会把源码丢到github上我的地址:https://git ...
- Android 自定义 View 绘制
在 Android 自定义View 里面,介绍了自定义的View的基本概念.同时在 Android 控件架构及View.ViewGroup的测量 里面介绍了 Android 的坐标系 View.Vie ...
- Android自定义View
转载请标明出处:http://blog.csdn.net/lmj623565791/article/details/24252901 很多的Android入门程序猿来说对于Android自定义View ...
- Android 自定义 View 圆形进度条总结
Android 自定义圆形进度条总结 版权声明:本文为博主原创文章,未经博主允许不得转载. 微博:厉圣杰 微信公众号:牙锅子 源码:CircleProgress 文中如有纰漏,欢迎大家留言指出. 最近 ...
- android 自定义view 前的基础知识
本篇文章是自己自学自定义view前的准备,具体参考资料来自 Android LayoutInflater原理分析,带你一步步深入了解View(一) Android视图绘制流程完全解析,带你一步步深入了 ...
- Android自定义View 画弧形,文字,并增加动画效果
一个简单的Android自定义View的demo,画弧形,文字,开启一个多线程更新ui界面,在子线程更新ui是不允许的,但是View提供了方法,让我们来了解下吧. 1.封装一个抽象的View类 B ...
- (转)[原] Android 自定义View 密码框 例子
遵从准则 暴露您view中所有影响可见外观的属性或者行为. 通过XML添加和设置样式 通过元素的属性来控制其外观和行为,支持和重要事件交流的事件监听器 详细步骤见:Android 自定义View步骤 ...
- Android 自定义View合集
自定义控件学习 https://github.com/GcsSloop/AndroidNote/tree/master/CustomView 小良自定义控件合集 https://github.com/ ...
- Android 自定义View (五)——实践
前言: 前面已经介绍了<Android 自定义 view(四)-- onMeasure 方法理解>,那么这次我们就来小实践下吧 任务: 公司现有两个任务需要我完成 (1)监测液化天然气液压 ...
随机推荐
- 《C和指针》章节后编程练习解答参考——6.6
<C和指针>——6.6 题目: 在指定的下限.上限之间使用数组方法查找质数,并将质数提取出来. 要求: 略 解答代码: #include <stdio.h> #define U ...
- 2016030204 - git和github结合
1.下载和安装git客户端 参考:http://www.cnblogs.com/zhtzyh2012/p/5232291.html 2.github上创建项目 参考:http://www.cnblog ...
- C 字符串倒转,XCode中编译
正在学习ios开发,在前期学习c时,常规方法直接倒转数组的值,只能用于非中文字符,否则出现乱码, 在网上找了点资料,可能是 IDE不一致,一直得不到自己想要的值.花时间自己改了一下,正常通过 //字符 ...
- CocoaPods ADD private Spec Repo
Private Pods CocoaPods is a great tool not only for adding open source code to your project, but als ...
- css3:user-select属性
一.user-select简介 这是在css3 UI规范中新增的一个功能,用来控制内容的可选择性 二.user-select:值 auto——默认值,用户可以选中元素中的内容 none——用户不能选择 ...
- Chaperon简介
Chaperon是一个可以把有结构的Text转换成XML.它包括一个强大的LALR(1)解析器来解析Text和一个可以用来创建XML文档的Tree builder. http://chaperon.s ...
- Linq 数据合并,比较,连接,交叉 维恩图工具
Except 返回包含两个不同之处的linq结果集 Intersect 返回两个容器中共同的数据项 Union 返回所有成员,相同的成员出现多次,将只返回一个 Concat 返回所有数据项
- Dataguard配置前提条件
Data Guard配置前提条件 配置Data Guard必须保证以下前提条件: 1.Data Guard是Oracle企业版的组件.Oracle标准版里没有这个控件.所以Data Guard配置所使 ...
- 30+最佳Ajax jQuery的自动完成插件的例子
在这篇文章中,我们将介绍35个jQuery AJAX的自动完成提示例子. jQuery 的自动完成功能,使用户快速找到并选择一定的价值.每个人都想要快速和即时搜索输入栏位,因为这个原因,许 流行的搜索 ...
- 修改Delphi工具控件的默认字体
修改Delphi工具控件的默认字体: 注册表: Delphi 6: HKEY_CURRENT_USER\Software\Borland\Delphi\6.0Delphi 7: HKEY_ ...