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

贝塞尔曲线应用及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

  • 8
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值