[Android]贝塞尔曲线应用及QQ气泡拖动原理实践
来源:互联网 发布:mac常用软件2017 编辑:程序博客网 时间:2024/05/22 17:43
贝塞尔曲线应用及QQ气泡拖动原理实践
绘线API介绍
- moveTo
控制Path的起点位置 - lineTo
从起点连接到某点位置 - quadTo
绘制贝塞尔曲线 - cubicTo
同样是绘制贝塞尔曲线,多一个起点坐标参数,比起quadTo省去了一个moveTo步骤 - arcTo
截取圆弧的一部分角度
这部分读者可以直接参考别人的实例讲解
我在这儿主要介绍贝塞尔曲线的应用,虽然有很多人已经解释过什么是贝塞尔曲线,但大部分都没有说清楚公式转化的那部分,所以我还是在这里记录下来。
首先是一阶的贝塞尔曲线:
其对应的公式
然后是二阶的贝塞尔曲线:
其对应的公式
二阶函数公式对应的曲线为什么是这样的呢,这里可以把公式拆开转化,依次如下:
和一阶的公式联系起来看,继续转化:
用B0和B1代替和一阶公式对应的部分
B0和B1分别是P0到P1和P1到P2的1阶贝塞尔曲线。而2阶贝塞尔曲线B就是B0到B1的1阶贝塞尔曲线。
看着最后的这个公式,回去看看那个二阶的动态图,有木有瞬间明白了!
那么如果想问还有三阶、四阶什么的呵呵嗒,我这里转两个图读者自己去转化吧=。=#
三阶:
四阶:
QQ 气泡拖动原理解析及相关Demo
很早就看了QQ消息气泡的功能,也有很多人做过相关实现,类似于下面的,都是用贝塞尔曲线实现的:
于是自己也去实现了一个简单的Demo,主要也是想学习实现的原理,写这篇博客即是想记录,也是想分享。上面的Demo最后是用帧图片去做的,我就不去找图片了,改成了回弹的效果。
效果是这样的:
代码不多,直接铺出来:
import android.animation.ValueAnimator;import android.content.Context;import android.graphics.Canvas;import android.graphics.Color;import android.graphics.Paint;import android.graphics.Path;import android.util.AttributeSet;import android.view.MotionEvent;import android.view.View;/** * Created by Yellow5A5 on 15/12/18. */public class ElasticRoundView extends View { //变化因子,用于设置拖动距离与半径变化的关系 private final int CHANGE_FACTOR = 8; private int density; private int displayWidth; private int displayHeight; //中心坐标 private float mCenterX; private float mCenterY; //移动的圆中心坐标 private float mMovingX; private float mMovingY; //初始半径记录 private float mStartRadius; //中心的圆半径 private float mCenterRadius; //移动的圆半径 private float mMovingRadius; //限制拖动范围 private float mLimit; //标记最后ACTION_UP的坐标 private float mEndX, mEndY; private Path mPath; private Paint mPaint; private ValueAnimator animator; public ElasticRoundView(Context context) { this(context, null); } public ElasticRoundView(Context context, AttributeSet attrs) { this(context, attrs, 0); } public ElasticRoundView(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); init(); } private void init() { density = (int) getResources().getDisplayMetrics().density; displayWidth = getResources().getDisplayMetrics().widthPixels; displayHeight = getResources().getDisplayMetrics().heightPixels; mCenterX = displayWidth / 2; mCenterY = displayHeight / 2; mCenterRadius = density * 25; mStartRadius = mCenterRadius; mPath = new Path(); mPaint = new Paint(); mPaint.setColor(Color.parseColor("#ff5777")); mPaint.setAntiAlias(true);//去除锯齿 mPaint.setStyle(Paint.Style.FILL); mMovingX = mCenterX; mMovingY = mCenterY; mMovingRadius = mCenterRadius; mLimit = 7 * mCenterRadius; initAnim(); updatePath(); } //设置回归动画 private void initAnim() { animator = ValueAnimator.ofFloat(1f, 0f).setDuration(1500); animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { @Override public void onAnimationUpdate(ValueAnimator animation) { mMovingX = mCenterX + ((mEndX - mCenterX) * (float) animation.getAnimatedValue()); mMovingY = mCenterY + ((mEndY - mCenterY) * (float) animation.getAnimatedValue()); mCenterRadius = mStartRadius - vectorToPoint(mCenterX, mCenterY, mMovingX, mMovingY) / CHANGE_FACTOR; mMovingRadius = mStartRadius - mCenterRadius; updatePath(); invalidate(); } }); } //更新路径参数 private void updatePath() { if (mMovingY == mCenterY || mMovingX == mCenterX) return; double corners = Math.atan((mMovingY - mCenterY) / (mMovingX - mCenterX)); float offsetX1 = (float) (mCenterRadius * Math.sin(corners)); float offsetY1 = (float) (mCenterRadius * Math.cos(corners)); float offsetX2 = (float) (mMovingRadius * Math.sin(corners)); float offsetY2 = (float) (mMovingRadius * Math.cos(corners)); float x1 = mCenterX - offsetX1; float y1 = mCenterY + offsetY1; float x2 = mMovingX - offsetX2; float y2 = mMovingY + offsetY2; float x3 = mMovingX + offsetX2; float y3 = mMovingY - offsetY2; float x4 = mCenterX + offsetX1; float y4 = mCenterY - offsetY1; float midpointX = (mCenterX + mMovingX) / 2; float midpointY = (mCenterY + mMovingY) / 2; mPath.reset(); mPath.moveTo(x1, y1); mPath.quadTo(midpointX, midpointY, x2, y2); mPath.lineTo(x3, y3); mPath.quadTo(midpointX, midpointY, x4, y4); mPath.lineTo(x1, y1); } @Override public boolean onTouchEvent(MotionEvent event) { int eventAction = event.getAction(); int x = (int) event.getX(); int y = (int) event.getY(); float temp = 0; switch (eventAction) { case MotionEvent.ACTION_DOWN: if (x < mCenterX - mCenterRadius || x > mCenterX + mCenterRadius || y < mCenterY - mCenterRadius || y > mCenterY + mCenterRadius) { return false; } case MotionEvent.ACTION_MOVE: mMovingX = x; mMovingY = y; temp = vectorToPoint(mCenterX, mCenterY, mMovingX, mMovingY); if (temp > mLimit) {//限制拖动长度。 float multiple = mLimit / temp; mMovingX = (mMovingX - mCenterX) * multiple + mCenterX; mMovingY = (mMovingY - mCenterY) * multiple + mCenterY; temp = mLimit; } mCenterRadius = mStartRadius - temp / CHANGE_FACTOR; mMovingRadius = mStartRadius - mCenterRadius; updatePath(); invalidate(); return true; case MotionEvent.ACTION_UP: //限制拖动长度。 mEndX = mMovingX; mEndY = mMovingY; animator.start(); } return true; } @Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); canvas.drawCircle(mCenterX, mCenterY, mCenterRadius, mPaint); canvas.drawCircle(mMovingX, mMovingY, mMovingRadius, mPaint); canvas.drawPath(mPath, mPaint); } /** * 计算两点之间的距离 * @return 两点之间的距离 */ private float vectorToPoint(float X1, float Y1, float X2, float Y2) { return (float) Math.sqrt(Math.pow(Math.abs(X2 - X1), 2) + Math.pow(Math.abs(Y2 - Y1), 2)); }}
这就是整个类了,复制即可用。
讲解部分:
首先看看onDraw的代码:
@Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); canvas.drawCircle(mCenterX, mCenterY, mCenterRadius, mPaint); canvas.drawCircle(mMovingX, mMovingY, mMovingRadius, mPaint); canvas.drawPath(mPath, mPaint); }
首先绘制了两个圆,分别是拖动的圆和中心的圆。然后再绘制mPath。绘制mPath事实上是填充模式去绘制两个圆连接的部分,通过6个点的坐标绘制2条直线和2条贝塞尔曲线围成的。
下面看看mPath的曲线绘制部分:
//更新路径参数 private void updatePath() { if (mMovingY == mCenterY || mMovingX == mCenterX) return; double corners = Math.atan((mMovingY - mCenterY) / (mMovingX - mCenterX)); float offsetX1 = (float) (mCenterRadius * Math.sin(corners)); float offsetY1 = (float) (mCenterRadius * Math.cos(corners)); float offsetX2 = (float) (mMovingRadius * Math.sin(corners)); float offsetY2 = (float) (mMovingRadius * Math.cos(corners)); float x1 = mCenterX - offsetX1; float y1 = mCenterY + offsetY1; float x2 = mMovingX - offsetX2; float y2 = mMovingY + offsetY2; float x3 = mMovingX + offsetX2; float y3 = mMovingY - offsetY2; float x4 = mCenterX + offsetX1; float y4 = mCenterY - offsetY1; float midpointX = (mCenterX + mMovingX) / 2; float midpointY = (mCenterY + mMovingY) / 2; mPath.reset(); mPath.moveTo(x1, y1); mPath.quadTo(midpointX, midpointY, x2, y2); mPath.lineTo(x3, y3); mPath.quadTo(midpointX, midpointY, x4, y4); mPath.lineTo(x1, y1); }
先通过角度计算,获得两个园中心的坐标角度,这个需要在图上画一画比较清晰,然后通过这个角度去获得四个角上的点坐标,然后再去取连接两圆圆心的线的中点位置midpoint。用这个midpoint来作为贝塞尔曲线坐标的控制点,进而开始绘制。
另外我设置了一个CHANGE_FACTOR值,通过改变这个值来设置拖动距离和半径变化的关系。
接下来我看看touchEvent里面ACTION_MOVE的代码:
case MotionEvent.ACTION_MOVE: mMovingX = x; mMovingY = y; temp = vectorToPoint(mCenterX, mCenterY, mMovingX, mMovingY); if (temp > mLimit) {//限制拖动长度。 float multiple = mLimit / temp; mMovingX = (mMovingX - mCenterX) * multiple + mCenterX; mMovingY = (mMovingY - mCenterY) * multiple + mCenterY; temp = mLimit; } mCenterRadius = mStartRadius - temp / CHANGE_FACTOR; mMovingRadius = mStartRadius - mCenterRadius; updatePath(); invalidate(); return true;
这部分的逻辑负责更新移动中的圆的坐标及两圆半径。这部分坐标的更新先需要通过vectorToPoint方法来计算两圆距离,并是否超出限制,进而符合逻辑的更新绘制。
根据这样的效果实现,明白了实现原理,对于那些看起来很像特别酷炫的效果,像下面这个Demo,你是不是也应该有了头绪?
文章到此结束,谢谢阅读!
附参考文章:
http://segmentfault.com/a/1190000000721127
http://blog.csdn.net/zhongkejingwang/article/details/38556891
http://www.cnblogs.com/tianzhijiexian/p/4301113.html
- [Android]贝塞尔曲线应用及QQ气泡拖动原理实践
- 贝塞尔曲线原理及应用
- 贝塞尔曲线实践--拖拽气泡
- Android 仿QQ 5.0 气泡提示 拖动爆炸消除
- QQ聊天气泡拖动效果实现
- 类似QQ拖动气泡删除消息的气泡实现
- Android-贝塞尔曲线应用
- 贝塞尔曲线原理以及在android中的应用
- 贝塞尔曲线实现QQ未读消息气泡拖拽效果
- 贝塞尔曲线初探及原理
- 仿QQ拖动删除未读消息个数气泡
- Android之QQ聊天气泡对话实现
- Kubernetes初探:原理及实践应用
- Kubernetes初探:原理及实践应用
- Kubernetes初探:原理及实践应用
- Kubernetes初探:原理及实践应用
- Android 贝塞尔曲线实现QQ拖拽清除效果
- android 仿qq5.0 消息气泡拖动删除效果
- BZOJ 3207 花神的嘲讽计划Ⅰ 可持久化线段树
- 树中点对距离
- SQLite数据库存储
- (java多线程并发)控制并发线程数的Semaphore
- android 图片操作之色彩变换
- [Android]贝塞尔曲线应用及QQ气泡拖动原理实践
- afn https网络访问
- 【总结】初识C#变量
- visualbox ubantu 环境问题记录
- git中文教程
- android studio中导入Slidingmenu侧边栏
- 调用系列函数的方式
- Vanya and Cards
- Android 环境搭建