QQ气泡效果剖析

2021年09月15日 阅读数:3
这篇文章主要向大家介绍QQ气泡效果剖析,主要内容包括基础应用、实用技巧、原理机制等方面,希望对大家有所帮助。

对于QQ汽泡效果我想不用多说了,都很是的熟悉,并且当时这效果出来简直亮瞎眼了,挺炫的,这里再来感觉下:html

而此次只实现单个汽泡的效果,并不涉及到加入Listview上的处理,一步步来,先上一下最终此次要实现的效果:web

分析:canvas

对于这么复杂的效果首先得要将它的功能进行拆解,首先先静止观察其效果:api

两端能够分为两个圆:app

而后中间链接成曲线:框架

这时再将空心的地方进行红颜色填充,以下:ide

最终是否是就达到了相似的汽泡能够拖拽的效果了:函数

  

这是对于整个动做的拆解,因此能够看出对于这个效果的实现,能够拆解成三个图形的绘制:两圆、中间链接图形:工具

而在正式编码实现以前,还得给上面图形进行一个身份明确:学习

拖拽圆:表示该圆是随手指能够去拖动的圆。

固定圆:表示该圆是固定不能随手指拖动的。

中间图形:表示须要将它与两圆进行相交合并的图形。

而当中间图形与圆进行汇合时产生的焦点也将其概念化:

因此对于中间图形就能够这样定义了:

为何要定义好这些东东呢?在代码实现的时候会比较清晰,另外变量命名按照上面的概念去写可读性也比较好,因此说这个分析颇有必要!

明确了这些概念以后下面则能够开始正式的代码编写啦,不过会细分红不少步骤一点点去实现:

绘制两个静态圆【不考虑动态效果】:

具体框架的搭建这里就省略了,直接贴上View的自定义实现,因为这一步比较简单,直接上代码:

/**
 * Goo:是粘性的意思,QQ汽泡效果
 * 绘制两个圆,此时圆是固定的,先不考虑随手指变化的问题
 */
public class GooView extends View {

    private Paint paint;
    /* 固定圆的圆心 */
    private PointF stableCenter = new PointF(200f, 200f);
    /* 固定圆的半径 */
    private float stableRadius = 20f;
    /* 拖拽圆的圆心 */
    private PointF dragCenter = new PointF(100f, 100f);
    /* 拖拽圆的半径 */
    private float dragRadius = 30f;

    public GooView(Context context) {
        this(context, null);
    }

    public GooView(Context context, @Nullable AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public GooView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        init();
    }

    private void init() {
        paint = new Paint();
        paint.setAntiAlias(true);
        paint.setColor(Color.RED);
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        //绘制两个固定圆:固定圆、拖拽圆
        canvas.drawCircle(stableCenter.x, stableCenter.y, stableRadius, paint);
        canvas.drawCircle(dragCenter.x, dragCenter.y, dragRadius, paint);
    }
}

编译运行:

绘制中间图形:

对于这个不规则的图形须要用到Path的绘制了,而有两条曲线则须要用到贝赛尔曲线,关于这方面的知识在以前已经学习过了【http://www.cnblogs.com/webor2006/p/7341697.html】,因此直接看下代码:

/**
 * Goo:是粘性的意思,QQ汽泡效果
 * 中间图形的绘制,等它绘制好了下一步就能够将它与圆进行整合
 */
public class GooView extends View {

    private Paint paint;
    /* 固定圆的圆心 */
    private PointF stableCenter = new PointF(200f, 200f);
    /* 固定圆的半径 */
    private float stableRadius = 20f;
    /* 固定圆的两个附着点 */
    private PointF[] stablePoints = new PointF[]{
            new PointF(200f, 300f),
            new PointF(200f, 350f)
    };
    /* 拖拽圆的圆心 */
    private PointF dragCenter = new PointF(100f, 100f);
    /* 拖拽圆的半径 */
    private float dragRadius = 30f;
    /* 拖拽圆的两个附着点 */
    private PointF[] dragPoints = new PointF[]{
            new PointF(100f, 300f),
            new PointF(100f, 350f)
    };
    /* 绘制中间不规则的路径 */
    private Path path;
    /* 贝塞尔曲线的控制点 */
    private PointF controlPoint = new PointF(150f, 325f);

    public GooView(Context context) {
        this(context, null);
    }

    public GooView(Context context, @Nullable AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public GooView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        init();
    }

    private void init() {
        paint = new Paint();
        paint.setAntiAlias(true);
        paint.setColor(Color.RED);

        path = new Path();
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        //绘制两个固定圆:固定圆、拖拽圆
        canvas.drawCircle(stableCenter.x, stableCenter.y, stableRadius, paint);
        canvas.drawCircle(dragCenter.x, dragCenter.y, dragRadius, paint);

        //绘制中间图形,其步骤为:
        // 一、移动到固定圆的附着点1;
        path.moveTo(stablePoints[0].x, stablePoints[0].y);
        // 二、向拖拽圆附着点1绘制贝塞尔曲线;
        path.quadTo(controlPoint.x, controlPoint.y, dragPoints[0].x, dragPoints[0].y);
        // 三、向拖拽圆的附着点2绘制直线;
        path.lineTo(dragPoints[1].x, dragPoints[1].y);
        // 四、向固定圆的附着点2绘制贝塞尔曲线;
        path.quadTo(controlPoint.x, controlPoint.y, stablePoints[1].x, stablePoints[1].y);
        // 五、闭合;
        path.close();
        canvas.drawPath(path, paint);
    }
}

编译运行:

将中间图形与两圆进行整合,实现初步的拖拽雏形【重要!】

接下来就到了一个关键步骤,上一步的附着点是用的静态定死的数据,而这一步要作的就是依据现有的两个圆来将附着点及曲线的控制点改成活的,因此这里涉及到了一些计算的工做,下面一步步来实现:

首先去掉上步骤中定死的附着点与控制点的数据:

问题的核心就回到了如何计算附着点了,只有附着点肯定了,那控制点就很容易就能够求出来了,那倒底如何计算呢?下面用图来讲明下:

用直接连着两个圆的圆心,接着再基于这条直线作垂直线,以下:

那究境得如何算出这两个附着点呢?首先根据两圆心相对水平线是有一个夹角的:

而目前已经知道的是圆心与圆的半径,要求出这两上点继续能够作出延长线,以下:

接下来就比较重要啦,其中图中的角度其实都是同样大的:

这里不论证了,从图中就能够观察出来,这时就有一个关键点啦!想办法求得这个角度的sin或cos,具体以下:

①、利用两中心点的链接能够求得斜率:已知两点坐标:(x1,y1),(x2,y2),其斜率为:k=(y2-y1)/(x2-x1);

②、有了斜率就能够求得这个角度的弧度值:radian = Math.atan(k);

③、有了弧度值,那sin和cos就能够利用Math类求得具体值啦,而参数恰好是传的弧度值。
【说明】:对于斜率及弧底的求解在以前博客中也已经有用到过。 
知道了角度的sin和cos,那两个附着点的计算就迎刃而解啦:

整个计算公式以下:

k=(y2-y1)/(x2-x1)

radian = Math.atan(k);

xOffset = (float) (Math.sin(radian) * radius);
yOffset = (float) (Math.cos(radian) * radius);

第一个拖拽圆的附着点结果为:dragPoints[0] = (dragCenter.x - xOffset, dragCenter.y + yOffset);
而同理,因为另外一个三角函数计算的xOffset和yOffset是如出一辙的:

因此另外一个附着点的结果也就出来了,结果为:dragPoints[0] = (dragCenter.x + xOffset, dragCenter.y + yOffset);
因此这里能够将这个附着点的计算封装成一个工具方法,以下:

只要理解上刚才的推导过程,对于这个工具方法就彻底可以理解啦,好了这块比较绕,可是也是实现的核心,回到正题,有了这个工具方法,那接下来就能够算得两个圆真实的附着点了,以下:

最后还差一个曲线的控制点,这个比较容易了,就不过多解释了,取的就是两点的中点,这里也将其封装成工具方法:

修改代码以下:

编译运行:


【提示】:关于附着点的计算还有另一种更加直观的计算方法,不过稍麻烦一些,可是比较好理解,具体能够参考博文:http://www.jb51.net/article/108265.htm

实现拖动效果:

首先得重写onTouchEvent():

而后去获取当前触摸的坐标点:

对于上面获取的方法须要注意:这是点击的点是距离当前自定义控件的左边缘的距离,而最终咱们会将其用到列表当中的,那时当前控件一丁点大,因此这种获取坐标的方式不可行,得用另一个api,获取距离手机屏幕的左边缘的距离,以下:

看下效果:

呃~~怎么这个样子,确实是点到哪拖拽圆就到了哪,可是以前绘制的貌似木有消失,这实际上是以前也遇到过绘制path的问题【http://www.cnblogs.com/webor2006/p/7401912.html】,由于path会记录上一次绘制的颜色,解决之道:在每次绘制完path以后则须要reset()一下,以下:

嗯~~完美!!!真的完美了么,其实仍是有一个小BUG的,看:

实际上这个差的高度就是状态栏的高度:

由于:

因此须要得到状态栏的高度,而后在rawY上将其减掉,那如何获取状态栏的高度呢?

/**
 * Goo:是粘性的意思,QQ汽泡效果
 * 处理移动效果初步:图形会随着手指的移动而移动
 */
public class GooView extends View {

    private Paint paint;
    /* 固定圆的圆心 */
    private PointF stableCenter = new PointF(200f, 200f);
    /* 固定圆的半径 */
    private float stableRadius = 20f;
    /* 固定圆的两个附着点 */
    private PointF[] stablePoints;
    /* 拖拽圆的圆心 */
    private PointF dragCenter = new PointF(100f, 100f);
    /* 拖拽圆的半径 */
    private float dragRadius = 30f;
    /* 拖拽圆的两个附着点 */
    private PointF[] dragPoints;
    /* 绘制中间不规则的路径 */
    private Path path;
    /* 贝塞尔曲线的控制点 */
    private PointF controlPoint;
    /* 状态栏高度 */
    private int statusBarHeight;

    public GooView(Context context) {
        this(context, null);
    }

    public GooView(Context context, @Nullable AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public GooView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        init();
    }

    private void init() {
        paint = new Paint();
        paint.setAntiAlias(true);
        paint.setColor(Color.RED);

        path = new Path();
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        //绘制两个固定圆:固定圆、拖拽圆
        canvas.drawCircle(stableCenter.x, stableCenter.y, stableRadius, paint);
        canvas.drawCircle(dragCenter.x, dragCenter.y, dragRadius, paint);

        //绘制中间图形,直接基于两圆来绘制,其步骤为:
        //1、基于两圆来计算真实的附着点与曲线的控制点
        float dx = dragCenter.x - stableCenter.x;
        float dy = dragCenter.y - stableCenter.y;
        double linek = 0;
        if (dx != 0) {//被除数不能为0,防止异常
            linek = dy / dx;
        }
        //获得实际的附着点
        dragPoints = GeometryUtil.getIntersectionPoints(dragCenter, dragRadius, linek);
        stablePoints = GeometryUtil.getIntersectionPoints(stableCenter, stableRadius, linek);
        controlPoint = GeometryUtil.getMiddlePoint(dragCenter, stableCenter);

        //2、开始绘制中间图形
        // 一、移动到固定圆的附着点1;
        path.moveTo(stablePoints[0].x, stablePoints[0].y);
        // 二、向拖拽圆附着点1绘制贝塞尔曲线;
        path.quadTo(controlPoint.x, controlPoint.y, dragPoints[0].x, dragPoints[0].y);
        // 三、向拖拽圆的附着点2绘制直线;
        path.lineTo(dragPoints[1].x, dragPoints[1].y);
        // 四、向固定圆的附着点2绘制贝塞尔曲线;
        path.quadTo(controlPoint.x, controlPoint.y, stablePoints[1].x, stablePoints[1].y);
        // 五、闭合;
        path.close();
        canvas.drawPath(path, paint);
        path.reset();
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                //event.getX();//点击的点距离当前自定义控件的左边缘的距离
                float rawX = event.getRawX();//点击的点距离当前手机屏幕的左边缘的距离
                float rawY = event.getRawY() - statusBarHeight;
                dragCenter.set(rawX, rawY);
                invalidate();
                break;
            case MotionEvent.ACTION_MOVE:
                break;
            case MotionEvent.ACTION_UP:
                break;
        }
        //return super.onTouchEvent(event);
        return true;//表示当前控件想要处理事件
    }

    @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        super.onSizeChanged(w, h, oldw, oldh);
        statusBarHeight = getStatusBarHeight(this);
    }

    /**
     * 获取状态栏高度
     *
     * @param view
     * @return
     */
    private int getStatusBarHeight(View view) {
        Rect rect = new Rect();
        //获取视图对应的可视范围,会把视图的左上右下的数据传入到一个矩形中
        view.getWindowVisibleDisplayFrame(rect);
        return rect.top;
    }
}

这时再看效果:

完美解决,可是还有一种更加优雅的解决之道,那就是去移动画布,而不是去让Y坐标去减状态栏的高度,以下:

/**
 * Goo:是粘性的意思,QQ汽泡效果
 * 处理移动效果初步:图形会随着手指的移动而移动
 */
public class GooView extends View {

    private Paint paint;
    /* 固定圆的圆心 */
    private PointF stableCenter = new PointF(200f, 200f);
    /* 固定圆的半径 */
    private float stableRadius = 20f;
    /* 固定圆的两个附着点 */
    private PointF[] stablePoints;
    /* 拖拽圆的圆心 */
    private PointF dragCenter = new PointF(100f, 100f);
    /* 拖拽圆的半径 */
    private float dragRadius = 30f;
    /* 拖拽圆的两个附着点 */
    private PointF[] dragPoints;
    /* 绘制中间不规则的路径 */
    private Path path;
    /* 贝塞尔曲线的控制点 */
    private PointF controlPoint;
    /* 状态栏高度 */
    private int statusBarHeight;

    public GooView(Context context) {
        this(context, null);
    }

    public GooView(Context context, @Nullable AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public GooView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        init();
    }

    private void init() {
        paint = new Paint();
        paint.setAntiAlias(true);
        paint.setColor(Color.RED);

        path = new Path();
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        canvas.save();//由于移动画布了,因此须要save()和restore()一下
        canvas.translate(0, -statusBarHeight);//优雅的解决拖拽位置不是在圆心的问题,不用在触摸事件中单独处理了

        //绘制两个固定圆:固定圆、拖拽圆
        canvas.drawCircle(stableCenter.x, stableCenter.y, stableRadius, paint);
        canvas.drawCircle(dragCenter.x, dragCenter.y, dragRadius, paint);

        //绘制中间图形,直接基于两圆来绘制,其步骤为:
        //1、基于两圆来计算真实的附着点与曲线的控制点
        float dx = dragCenter.x - stableCenter.x;
        float dy = dragCenter.y - stableCenter.y;
        double linek = 0;
        if (dx != 0) {//被除数不能为0,防止异常
            linek = dy / dx;
        }
        //获得实际的附着点
        dragPoints = GeometryUtil.getIntersectionPoints(dragCenter, dragRadius, linek);
        stablePoints = GeometryUtil.getIntersectionPoints(stableCenter, stableRadius, linek);
        controlPoint = GeometryUtil.getMiddlePoint(dragCenter, stableCenter);

        //2、开始绘制中间图形
        // 一、移动到固定圆的附着点1;
        path.moveTo(stablePoints[0].x, stablePoints[0].y);
        // 二、向拖拽圆附着点1绘制贝塞尔曲线;
        path.quadTo(controlPoint.x, controlPoint.y, dragPoints[0].x, dragPoints[0].y);
        // 三、向拖拽圆的附着点2绘制直线;
        path.lineTo(dragPoints[1].x, dragPoints[1].y);
        // 四、向固定圆的附着点2绘制贝塞尔曲线;
        path.quadTo(controlPoint.x, controlPoint.y, stablePoints[1].x, stablePoints[1].y);
        // 五、闭合;
        path.close();
        canvas.drawPath(path, paint);
        path.reset();//解决滑动时路径重叠的问题
        canvas.restore();
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                //event.getX();//点击的点距离当前自定义控件的左边缘的距离
                float rawX = event.getRawX();//点击的点距离当前手机屏幕的左边缘的距离
                float rawY = event.getRawY()/* - statusBarHeight*/;
                dragCenter.set(rawX, rawY);
                invalidate();
                break;
            case MotionEvent.ACTION_MOVE:
                break;
            case MotionEvent.ACTION_UP:
                break;
        }
        //return super.onTouchEvent(event);
        return true;//表示当前控件想要处理事件
    }

    @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        super.onSizeChanged(w, h, oldw, oldh);
        statusBarHeight = getStatusBarHeight(this);
    }

    /**
     * 获取状态栏高度
     *
     * @param view
     * @return
     */
    private int getStatusBarHeight(View view) {
        Rect rect = new Rect();
        //获取视图对应的可视范围,会把视图的左上右下的数据传入到一个矩形中
        view.getWindowVisibleDisplayFrame(rect);
        return rect.top;
    }
}

接着再来处理MOVE事件,其代码跟DOWN相似:

这时再看效果:

处理随着拖拽距离固定圆半径的变化:

接着处理当拖拽距离在变化时,固定圆的大小也随之变化的效果,也就是说固定圆的半径会进行变化了而不是像目前写死的半径同样,也就是说:

那问题的核心回到了如何去动态去获得变化的这个固定圆的半径呢?首先先拿预期的效果来分析一下:

很显示有一个最大拖拽距离,当超过这个距离以后则就会有个被拉断的效果,另外固定圆随着拖拽距离愈来愈大,最终它的缩放也有一个最小半径,因此先定义出这两上常是来:

而接着要算出两圆的距离占了移动最大距离的百分比,算出它以后,则能够用它来算出固定圆的变化半径了,因此百分比的计算首先得计算两圆心的距离,两点的距离计算公式以下:

因此能够将这个距离的求解也封装到工具类中,以下:

因此其百分比计算的代码以下:

接着根据百分比来算出临时拖拽圆的半径,其实就是从当前固定圆的半径到最小圆半径之间来根据百分比来计算,因此也能够将这个百分比计算半径的封装成工具方法,以下:

因此算临时半径以下:

而后将原来写死的固定圆的半径改为它:

编译运行:

处理超出最大拖拽范围时,有个拉断的效果:

这个实现就比较简单了,其思路是:当判断超出最大拖拽值时,则不绘制固定圆和中间图形,只剩下拖拽圆,因此说具体代码以下:

/**
 * Goo:是粘性的意思,QQ汽泡效果
 * 处理超出最大拖拽范围时,有个拉断的效果
 */
public class GooView extends View {

    /* 拖拽圆移动的最大距离 */
    private static final float MAX_DRAG_DISTANCE = 200f;
    /* 固定圆缩放的最小半径 */
    private static final float MIN_STABLE_RADIUS = 5f;

    private Paint paint;
    /* 固定圆的圆心 */
    private PointF stableCenter = new PointF(200f, 200f);
    /* 固定圆的半径 */
    private float stableRadius = 20f;
    /* 固定圆的两个附着点 */
    private PointF[] stablePoints;
    /* 拖拽圆的圆心 */
    private PointF dragCenter = new PointF(100f, 100f);
    /* 拖拽圆的半径 */
    private float dragRadius = 30f;
    /* 拖拽圆的两个附着点 */
    private PointF[] dragPoints;
    /* 绘制中间不规则的路径 */
    private Path path;
    /* 贝塞尔曲线的控制点 */
    private PointF controlPoint;
    /* 状态栏高度 */
    private int statusBarHeight;
    /* 是否拖拽已经超出最大范围了,超出则不绘制拖拽圆和中间图形实现拉断效果 */
    private boolean isOutOfRange = false;

    public GooView(Context context) {
        this(context, null);
    }

    public GooView(Context context, @Nullable AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public GooView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        init();
    }

    private void init() {
        paint = new Paint();
        paint.setAntiAlias(true);
        paint.setColor(Color.RED);

        path = new Path();
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        canvas.save();
        canvas.translate(0, -statusBarHeight);//优雅的解决拖拽位置不是在圆心的问题,不用在触摸事件中单独处理了

        //处理随着拖拽圆和固定圆的圆心距离愈来愈大,固定圆的半径愈来愈小:拖拽圆和固定圆圆心的距离百分比变化==固定圆半径的百分比变化
        float distance = GeometryUtil.getDistanceBetween2Points(dragCenter, stableCenter);//得到两圆心的距离
        float percent = distance / MAX_DRAG_DISTANCE;//计算百分比
        float tempRadius = GeometryUtil.evaluateValue(percent, stableRadius, MIN_STABLE_RADIUS);//根据百分比来动态计算出固定圆的半径
        if (tempRadius < MIN_STABLE_RADIUS)//处理最小半径
            tempRadius = MIN_STABLE_RADIUS;

        if (!isOutOfRange) {//只有没有超出范围才绘制固定圆和中间图形

            //绘制中间图形,直接基于两圆来绘制,其步骤为:
            //1、基于两圆来计算真实的附着点与曲线的控制点
            float dx = dragCenter.x - stableCenter.x;
            float dy = dragCenter.y - stableCenter.y;
            double linek = 0;
            if (dx != 0) {//被除数不能为0,防止异常
                linek = dy / dx;
            }
            //获得实际的附着点
            dragPoints = GeometryUtil.getIntersectionPoints(dragCenter, dragRadius, linek);
            stablePoints = GeometryUtil.getIntersectionPoints(stableCenter, tempRadius, linek);
            controlPoint = GeometryUtil.getMiddlePoint(dragCenter, stableCenter);

            //2、开始绘制中间图形
            // 一、移动到固定圆的附着点1;
            path.moveTo(stablePoints[0].x, stablePoints[0].y);
            // 二、向拖拽圆附着点1绘制贝塞尔曲线;
            path.quadTo(controlPoint.x, controlPoint.y, dragPoints[0].x, dragPoints[0].y);
            // 三、向拖拽圆的附着点2绘制直线;
            path.lineTo(dragPoints[1].x, dragPoints[1].y);
            // 四、向固定圆的附着点2绘制贝塞尔曲线;
            path.quadTo(controlPoint.x, controlPoint.y, stablePoints[1].x, stablePoints[1].y);
            // 五、闭合;
            path.close();
            canvas.drawPath(path, paint);
            path.reset();//解决滑动时路径重叠的问题

            ////绘制固定圆
            canvas.drawCircle(stableCenter.x, stableCenter.y, tempRadius, paint);
        }
        //绘制拖拽圆
        canvas.drawCircle(dragCenter.x, dragCenter.y, dragRadius, paint);

        canvas.restore();
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                //event.getX();//点击的点距离当前自定义控件的左边缘的距离
                float rawX = event.getRawX();//点击的点距离当前手机屏幕的左边缘的距离
                float rawY = event.getRawY()/* - statusBarHeight*/;
                dragCenter.set(rawX, rawY);
                invalidate();
                break;
            case MotionEvent.ACTION_MOVE:
                rawX = event.getRawX();
                rawY = event.getRawY();
                dragCenter.set(rawX, rawY);
                //当拖拽超出必定范围后,固定圆和中间图形都消失了,其"消失了"用程序来讲就是在onDraw()中不绘制某一段了
                //判断拖拽的距离,判断距离是否超出最大距离
                float distance = GeometryUtil.getDistanceBetween2Points(stableCenter, dragCenter);
                if (distance > MAX_DRAG_DISTANCE) {
                    //当超出最大距离则再也不对固定圆和中间图形进行绘制
                    isOutOfRange = true;
                }
                invalidate();
                break;
            case MotionEvent.ACTION_UP:
                break;
        }
        //return super.onTouchEvent(event);
        return true;//表示当前控件想要处理事件
    }

    @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        super.onSizeChanged(w, h, oldw, oldh);
        statusBarHeight = getStatusBarHeight(this);
    }

    /**
     * 获取状态栏高度
     *
     * @param view
     * @return
     */
    private int getStatusBarHeight(View view) {
        Rect rect = new Rect();
        //获取视图对应的可视范围,会把视图的左上右下的数据传入到一个矩形中
        view.getWindowVisibleDisplayFrame(rect);
        return rect.top;
    }
}

其中在onDraw()中为了方便条件判断的编写代码,将圆的绘制顺序调到最后面去绘了,将绘制拖拽圆的代码单独放到条件语句以外,编译运行:

如今因为木有处理UP事件,因此说如今状态回不去了,为了方便调试,在DOWN时将状态还原,以下:

效果以下:

处理move和up均超出最大范围,当松手时则拖拽圆消失:

这个跟上一个步骤相似,只不过这时须要对UP事件进行处理了,先去处理判断条件,而后在onDraw()中去加入这个条件的处理,以下:

/**
 * Goo:是粘性的意思,QQ汽泡效果
 * move和up均超出最大范围的处理:当松手时则拖拽圆消失
 */
public class GooView extends View {

    /* 拖拽圆移动的最大距离 */
    private static final float MAX_DRAG_DISTANCE = 200f;
    /* 固定圆缩放的最小半径 */
    private static final float MIN_STABLE_RADIUS = 5f;

    private Paint paint;
    /* 固定圆的圆心 */
    private PointF stableCenter = new PointF(200f, 200f);
    /* 固定圆的半径 */
    private float stableRadius = 20f;
    /* 固定圆的两个附着点 */
    private PointF[] stablePoints;
    /* 拖拽圆的圆心 */
    private PointF dragCenter = new PointF(100f, 100f);
    /* 拖拽圆的半径 */
    private float dragRadius = 30f;
    /* 拖拽圆的两个附着点 */
    private PointF[] dragPoints;
    /* 绘制中间不规则的路径 */
    private Path path;
    /* 贝塞尔曲线的控制点 */
    private PointF controlPoint;
    /* 状态栏高度 */
    private int statusBarHeight;
    /* 是否拖拽已经超出最大范围了,超出则不绘制拖拽圆和中间图形实现拉断效果 */
    private boolean isOutOfRange = false;
    /* 是否所有消失,若是true则全部图形消失 */
    private boolean isDisappear = false;

    public GooView(Context context) {
        this(context, null);
    }

    public GooView(Context context, @Nullable AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public GooView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        init();
    }

    private void init() {
        paint = new Paint();
        paint.setAntiAlias(true);
        paint.setColor(Color.RED);

        path = new Path();
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        canvas.save();
        canvas.translate(0, -statusBarHeight);//优雅的解决拖拽位置不是在圆心的问题,不用在触摸事件中单独处理了

        //处理随着拖拽圆和固定圆的圆心距离愈来愈大,固定圆的半径愈来愈小:拖拽圆和固定圆圆心的距离百分比变化==固定圆半径的百分比变化
        float distance = GeometryUtil.getDistanceBetween2Points(dragCenter, stableCenter);//得到两圆心的距离
        float percent = distance / MAX_DRAG_DISTANCE;//计算百分比
        float tempRadius = GeometryUtil.evaluateValue(percent, stableRadius, MIN_STABLE_RADIUS);//根据百分比来动态计算出固定圆的半径
        if (tempRadius < MIN_STABLE_RADIUS)//处理最小半径
            tempRadius = MIN_STABLE_RADIUS;

        if (!isDisappear) {
            if (!isOutOfRange) {//只有没有超出范围才绘制固定圆和中间图形

                //绘制中间图形,直接基于两圆来绘制,其步骤为:
                //1、基于两圆来计算真实的附着点与曲线的控制点
                float dx = dragCenter.x - stableCenter.x;
                float dy = dragCenter.y - stableCenter.y;
                double linek = 0;
                if (dx != 0) {//被除数不能为0,防止异常
                    linek = dy / dx;
                }
                //获得实际的附着点
                dragPoints = GeometryUtil.getIntersectionPoints(dragCenter, dragRadius, linek);
                stablePoints = GeometryUtil.getIntersectionPoints(stableCenter, tempRadius, linek);
                controlPoint = GeometryUtil.getMiddlePoint(dragCenter, stableCenter);

                //2、开始绘制中间图形
                // 一、移动到固定圆的附着点1;
                path.moveTo(stablePoints[0].x, stablePoints[0].y);
                // 二、向拖拽圆附着点1绘制贝塞尔曲线;
                path.quadTo(controlPoint.x, controlPoint.y, dragPoints[0].x, dragPoints[0].y);
                // 三、向拖拽圆的附着点2绘制直线;
                path.lineTo(dragPoints[1].x, dragPoints[1].y);
                // 四、向固定圆的附着点2绘制贝塞尔曲线;
                path.quadTo(controlPoint.x, controlPoint.y, stablePoints[1].x, stablePoints[1].y);
                // 五、闭合;
                path.close();
                canvas.drawPath(path, paint);
                path.reset();//解决滑动时路径重叠的问题

                ////绘制固定圆
                canvas.drawCircle(stableCenter.x, stableCenter.y, tempRadius, paint);
            }
            //绘制拖拽圆
            canvas.drawCircle(dragCenter.x, dragCenter.y, dragRadius, paint);
        }
        canvas.restore();
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                isOutOfRange = false;//方便调试
                isDisappear = false;//方便调试
                //event.getX();//点击的点距离当前自定义控件的左边缘的距离
                float rawX = event.getRawX();//点击的点距离当前手机屏幕的左边缘的距离
                float rawY = event.getRawY()/* - statusBarHeight*/;
                dragCenter.set(rawX, rawY);
                invalidate();
                break;
            case MotionEvent.ACTION_MOVE:
                rawX = event.getRawX();
                rawY = event.getRawY();
                dragCenter.set(rawX, rawY);
                //当拖拽超出必定范围后,固定圆和中间图形都消失了,其"消失了"用程序来讲就是在onDraw()中不绘制某一段了
                //判断拖拽的距离,判断距离是否超出最大距离
                float distance = GeometryUtil.getDistanceBetween2Points(stableCenter, dragCenter);
                if (distance > MAX_DRAG_DISTANCE) {
                    //当超出最大距离则再也不对固定圆和中间图形进行绘制
                    isOutOfRange = true;
                }
                invalidate();
                break;
            case MotionEvent.ACTION_UP:
                distance = GeometryUtil.getDistanceBetween2Points(stableCenter, dragCenter);
                //判断在move的时候是否超出过最大范围
                if (isOutOfRange) {
                    //判断up的时候是否在最大范围外
                    if (distance > MAX_DRAG_DISTANCE)
                        isDisappear = true;
                } else {
                    //TODO
                }
                invalidate();
                break;
        }
        //return super.onTouchEvent(event);
        return true;//表示当前控件想要处理事件
    }

    @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        super.onSizeChanged(w, h, oldw, oldh);
        statusBarHeight = getStatusBarHeight(this);
    }

    /**
     * 获取状态栏高度
     *
     * @param view
     * @return
     */
    private int getStatusBarHeight(View view) {
        Rect rect = new Rect();
        //获取视图对应的可视范围,会把视图的左上右下的数据传入到一个矩形中
        view.getWindowVisibleDisplayFrame(rect);
        return rect.top;
    }
}

效果以下:

处理move超出而且up未超出、move和up均未超出最大范围:

最后还剩两个条件木有处理:

①、move超出,up未超出最大范围,这时应该将拖拽圆的圆心设置成固定圆的圆心,让其还原。

比较简单,直接上代码:

编译运行:

②、move和up均未超出最大范围,这时松手会有一个回弹效果。

这时应该是执行一段平移动画,从UP释放的位置到固定圆圆心之间进行,其实也就是从拖拽的距离到0之间的一个动画,因此能够用ValueAnimator来精准控制,以下:

其实能够想一下,对于平移其实就是拖拽圆和固定圆的一段动画,要是说能拿到一个距离执行的百分比,那经过这个百分比来算出两圆心的某个坐标,而后再去更新拖拽圆圆心,最后再不断刷新是否是就能达到平移的效果了呢?那这动画有获取到百分值的办法么?固然有,以下:

运行看下打印:

而关于两个值之间根据百分比获取指定值在以前已经实现了,如:

因此,根据两圆心点也能够利用它来获取,将其封装一下:

因此,调用一下它,以下:

编译运行:

可是!!还差最后一个回弹效果,这个对于动画来讲就比较简单了,由于有回弹效果的插值器,设置看效果:

若是对回弹的力度不够还能够传值加以调节,以下:

至此代码就所有实现了,最后把全部效果都跑一遍:

说实话,真心以为比较复杂,涉及到的计算也很多,因此需好好消化~以后会将它用在列表当中滴~