[Android]贝塞尔曲线应用及QQ气泡拖动原理实践

来源:互联网 发布:mac常用软件2017 编辑:程序博客网 时间:2024/05/22 17:43

贝塞尔曲线应用及QQ气泡拖动原理实践


绘线API介绍

  1. moveTo
    控制Path的起点位置
  2. lineTo
    从起点连接到某点位置
  3. quadTo
    绘制贝塞尔曲线
  4. cubicTo
    同样是绘制贝塞尔曲线,多一个起点坐标参数,比起quadTo省去了一个moveTo步骤
  5. 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

4 0
原创粉丝点击
热门问题 老师的惩罚 人脸识别 我在镇武司摸鱼那些年 重生之率土为王 我在大康的咸鱼生活 盘龙之生命进化 天生仙种 凡人之先天五行 春回大明朝 姑娘不必设防,我是瞎子 台达伺服报警009怎么办 台达plc禁止上传怎么办? 潜水泵电机启动绕组断线了怎么办? 永磁电机磁没了怎么办 热水器热水管坏了怎么办 松下伺服驱动器坏了怎么办 步进电机功率小了怎么办 电三轮电机坏了怎么办 电动车钢圈撞变形了怎么办 电动车后轮钢圈变形了怎么办 软油管接头渗油怎么办 一只单眼皮一只双眼皮怎么办 儿童轻轻泥干了怎么办 5d轻轻泥干了怎么办 手机炉石一直闪退怎么办 鸟之羽任务失败怎么办 巫师3没血没药怎么办 荣威rx5灯光不亮怎么办 点滴打没了回血怎么办 加了低标号的油怎么办 别克车钥匙丢了怎么办 霜子哀伤断了怎么办 侧车窗外有雨水怎么办 昂科威15t变速箱异响怎么办 别克昂科拉一公里9毛怎么办? 雷诺科雷傲车钥匙丢了怎么办 奥迪a6l烧机油了怎么办 卡地亚手镯刮花怎么办 卡地亚戒指花了怎么办 手表摔了不走了怎么办 ck手表表扣很难打开怎么办 小天才泡了水怎么办 小天才手表掉水里了怎么办 小天才电话手表进水了怎么办 小天才手表进水了怎么办 小天才电话手表丢了怎么办 小天才电话手表黑屏怎么办 安全守护注册码忘记了怎么办 儿童安全锁的门打不开怎么办 守护宝老年机打不开了怎么办 小米电话手表坏了怎么办