android高级UI之贝塞尔曲线<下>--贝塞尔曲线运用:QQ消息气泡

在上一次https://www.cnblogs.com/webor2006/p/12901271.html对于贝塞尔曲线的绘制方法进行了一个基础学习,接下来则利用贝塞尔曲线来实际应用一把,其效果之前也展示过,如下:

由于之前https://www.cnblogs.com/webor2006/p/7726174.html还学习过一个更加完整关于它的效果,这里就相当于是一个复习+巩固,当然有一些内容在之前已经阐述很清楚的在这里就会略过了,比如直角函数的计算之类的,这里会稍微提一下。

QQ气泡实现:

效果简单分析:

在正式撸码实现之前,先对效果简单进行一个分析,其实这个气泡效果是有几个状态的,如下:

1、气泡静止状态:

这个就没啥可说的,就是一个小圆圈气泡:

 

2、气泡连接状态:

接下来咱们要拖拽气泡对吧,此状态对应的效果就是它:

这个状态也是唯一应用贝塞尔曲线的地方,当然也是整个效果最难的。

3、气泡分离状态:

当拖拽到一定极限,则会让气泡产生分离:

4、气泡消失状态:

当气泡状态分离之后,再松手,此时气泡就已经消失了:

这炸裂效果看着挺酷的对吧,其实是一个帧动画。

5、气泡还原状态:

这个就比较简单了,就是当气泡消失之后,点击还原按钮又可以看到气泡初始的状态:

实现:

新建一个View:

这里还是基于上一次的工程进行构建,先新建一个要绘制的View:

定义气泡状态常量:

如上面所分析的,它有几种状态对吧,先定义一下状态常量,待之后实现逻辑时使用:

绘制静止状态的气泡:

1、定义气泡的圆心:

这里将静止圆定义在屏幕的中心位置,所以定义如下:

那么问题来了,现在我是想获取屏幕的宽高信息,在自定义View中如何来获取呢?可能你第一时间想到的是它:“getWindowManager().getDefaultDisplay().getWidth();”,但是它是需要获取Activity的Context的,因为getWindowManager()是定义在了Activity当中:

所以,你也可以在自定义View中这样来获取:

但是这不是最佳之道,最好的是通过这个回调方法:

所以,咱们就可以定义气泡的圆心了,如下:

其中定义了一个状态变量,待之后是需要进行状态判断用的:

2、定义气泡的半径:

接下来则需要来定义圆的半径了,对于这个圆的半径,打算灵活一些,采用自定义属性由用户在调用这个View时手动可以进行动态调整,所以先整个自定义属性:

3、绘制:

既然要绘制,则肯定是需要准备好画笔喽,而对于画笔的颜色这里也让用户可以定义,由于比较简单,整个绘制过程就一气呵成了:

package com.cexo.beziermaker.test;

import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.PointF;
import android.support.annotation.Nullable;
import android.util.AttributeSet;
import android.view.View;

//二阶贝塞尔曲线应用----QQ气泡效果
public class DragBubbleView extends View {

    //constants
    /* 气泡默认状态--静止 */
    private static final int BUBBLE_STATE_DEFAULT = 0;
    /* 气泡相连 */
    private static final int BUBBLE_STATE_CONNECT = 1;
    /* 气泡分离 */
    private static final int BUBBLE_STATE_APART = 2;
    /* 气泡消失 */
    private static final int BUBBLE_STATE_DISMISS = 3;

    //variables
    /* 气泡状态标志 */
    private int bubbleState = BUBBLE_STATE_DEFAULT;

    /* 气泡半径,由用户可以进行设置更改 */
    private float bubbleRadius;
    /* 气泡的画笔 */
    private Paint bubblePaint;
    /* 气泡颜色 */
    private int bubbleColor;

    /* 不动气泡的圆心 */
    private PointF bubStillCenter;
    /* 不动气泡的半径 */
    private float bubStillRadius;

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

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

    public DragBubbleView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        TypedArray array = context.obtainStyledAttributes(attrs, R.styleable.DragBubbleView, defStyleAttr, 0);
        bubbleRadius = array.getDimension(R.styleable.DragBubbleView_bubble_radius, bubbleRadius);
        bubbleColor = array.getColor(R.styleable.DragBubbleView_bubble_color, Color.RED);
        array.recycle();

        bubStillRadius = bubbleRadius;

        bubblePaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        bubblePaint.setColor(bubbleColor);
        bubblePaint.setStyle(Paint.Style.FILL);
    }

    @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        super.onSizeChanged(w, h, oldw, oldh);
        initView(w, h);
    }

    /**
     * 初始化气泡位置
     */
    private void initView(int w, int h) {

        //设置两气泡圆心初始坐标
        if (bubStillCenter == null) {
            bubStillCenter = new PointF(w / 2, h / 2);
        } else {
            bubStillCenter.set(w / 2, h / 2);
        }

        bubbleState = BUBBLE_STATE_DEFAULT;
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        // 1、画静止气泡
        canvas.drawCircle(bubStillCenter.x, bubStillCenter.y,
                bubStillRadius, bubblePaint);
    }
}

4、运行:

在运行之前,由于有一些自定义的属性,需要在布局中调用一下才行,所以修改一下布局:

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="#666666"
    tools:context=".MainActivity">

    <com.cexo.beziermaker.test.DragBubbleView
        android:id="@+id/drag_buddle_view"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_centerInParent="true"
        app:bubble_color="#ff0000"
        app:bubble_radius="12dp" />

    <Button
        android:id="@+id/reset_btn"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_alignParentBottom="true"
        android:layout_margin="20dp"
        android:text="还原"
        android:textColor="#666666" />

</RelativeLayout>

此时运行就是一个小圆点:

小疑问?

在预期的效果中,貌似静止状态下在气泡上还有一个文本的呀:

其实这个文本应该是跟着移动时的气泡的,为啥?在我们拖拽时就可以看出来了:

绘制移动状态相连气泡效果:

1、原理剖析:

接下来的重点就是来绘制经过气泡拖拽之后的两圆的一个相连效果了,这里先来看一下图,可能你会蒙圈:

其中要绘制相连状态,其实就是在两点之间绘制两条贝塞尔曲线对吧?

由于在之前https://www.cnblogs.com/webor2006/p/7726174.html对于整个的计算过程详细进行过分析了,所以这里整体过一下,就不过多的再从头来解释了,核心就是利用数学的三角函数来进行计算。

a、锚点Anchor计算:

对于一个贝塞尔曲线,核心点是需要计算出控制点对吧【关于这块如基础不牢的可以参考https://www.cnblogs.com/webor2006/p/12901271.html,这也就是为啥对于学习打好基础的重要性,基础不牢那么在使用时就有点蒙圈】?也就是图中的它:

 

这个计算就比较简单了,因为两圆心咱们都已经知道了,也就是:

 

其实锚点就是以两坐标的中间为坐标点,那不很轻松的可以计算出来,如下:

AnchorX = (BubStillCenter.x + BubMoveableCenter.x)/2;
AnchorY = (BubStillCenter.y + BubMoveableCenter.y)/2;

b、A点计算:【重点,理解它了其它点就都知道了】

接下来这个点的计算就算是一个难点了,只要你能把它计算出来,其它的B、C、D点的坐标就雷同了,所以这里重点对A点的计算稍微详细一点,其它点的计算就一带而过了, 其中也就是要计算出直角三角形的这两条边:

注意,其实这里应该是算出A点在屏幕中的x,y坐标,由于O1中间坐标点已经知道,那么对于A点最终在屏幕中的坐标应该是:

BubStillStartX = BubStillCenter.x - x;
BubStillStartY = BubStillCenter.y + y;

而要想计算出x、y,其中已知了这条直角三角形的斜边了:

是不是只要算出这个角度的cos和sin,那么x,y就简单了?

而此时相矛盾了,要想知道这个角度的sin和cos,是不是得要知道直角形的两条直边的值,也就是x,y,而这俩个值是未知的正要待求解的,此时有一个小小的技巧了,发现,其实这些夹角都是一样的:

关于这块的论证这里就不过多说明了,也是数学的一个基础,那么,我们可以选择这些夹角相等的任意一个直角三角形来算出它的sin和cos的呀,发现可以利用它来算:

为啥?因为对于这个直接三角形,三条边的值都可以根据两圆心点算出来,直角的两条边计算比较简单:

很明显就是算两点之间的直线距离嘛:

x = mBubMoveableCenter.x - mBubStillCenter.x 

y = mBubMoveableCenter.y - mBubStillCenter.y

好,剩下的就是直角三角形的斜边了:

这里就需要使用到一个求两坐标点距离【注意不是两点距离,而是两个坐标点的距离哟】的数学公式了,肯定忘了对吧,度娘一下就出来了:

而开根我们可以使用Java的这个数学函数:

所以,对于斜边也可以得到值了,这里比如叫这条边为Dist,直角三角形的三条边都已知,此时的sin、cos就出来了:

sinθ= (BubMoveableCenter.y - BubStillCenter.y) / Dist;
cosθ= (BubMoveableCenter.x - BubStillCenter.x) / Dist;

【提示】:在之前https://www.cnblogs.com/webor2006/p/7726174.html算这个角度时,采用了另一种方式,使用了斜率的计算公式,具体这里就不过多说明了,具体还是翻看之前实现的细节。

而:

那此时对于我们要计算的A点所在的直角三角形的两条直边是不是就可以算出来了,如下:

x=BubStillRadius * sinθ;

y=BubStillRadius * cosθ;

而最终是要求出A点在屏幕中的坐标点,根据上面咱们所计算的公式:

最终的坐标点是不是就出来了? 

BubStillStartX = BubStillCenter.x - BubStillRadius * sinθ;
BubStillStartY = BubStillCenter.y + BubStillRadius * cosθ; 

c、B点计算:

好,对于A点的计算已经清楚了,接下来其它点的计算都雷点,这里直接给出公式了:

BubMoveableEndX = BubMoveableCenter.x - BubMoveableRadius * sinθ;
BubMoveableEndY = BubMoveableCenter.y + BubMoveableRadius * cosθ;

d、C点计算:

BubMoveableStartX = BubMoveableCenter.x + BubMoveableRadius * sinθ;
BubMoveableStartY = BubMoveableCenter.y - BubMoveableRadius * cosθ;

d、D点计算:

BubStillEndX = BubStillCenter.x + BubStillRadius * sinθ;
BubStillEndY = BubStillCenter.y - BubStillRadius * cosθ;

2、绘制:

在明白了整个的绘制原理之后,接下来实现就比较简单了。

a、定义移动圆:

由于目前咱们还没有加入手势的处理,为了先看到运行效果,先将固定圆的坐标定死:

b、计算出各个点:

@Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        // 1、画静止气泡
        canvas.drawCircle(bubStillCenter.x, bubStillCenter.y,
                bubStillRadius, bubblePaint);

        // 2、画相连曲线
        // 计算控制点坐标,两个圆心的中点
        int iAnchorX = (int) ((bubStillCenter.x + bubMoveableCenter.x) / 2);
        int iAnchorY = (int) ((bubStillCenter.y + bubMoveableCenter.y) / 2);

        float cosTheta = (bubMoveableCenter.x - bubStillCenter.x) / dist;
        float sinTheta = (bubMoveableCenter.y - bubStillCenter.y) / dist;

        float iBubStillStartX = bubStillCenter.x - bubStillRadius * sinTheta;
        float iBubStillStartY = bubStillCenter.y + bubStillRadius * cosTheta;
        float iBubMoveableEndX = bubMoveableCenter.x - bubMoveableRadius * sinTheta;
        float iBubMoveableEndY = bubMoveableCenter.y + bubMoveableRadius * cosTheta;
        float iBubMoveableStartX = bubMoveableCenter.x + bubMoveableRadius * sinTheta;
        float iBubMoveableStartY = bubMoveableCenter.y - bubMoveableRadius * cosTheta;
        float iBubStillEndX = bubStillCenter.x + bubStillRadius * sinTheta;
        float iBubStillEndY = bubStillCenter.y - bubStillRadius * cosTheta;
    }

其中需要定义一下两中间点的距离:

 

c、绘制曲线:

 

看一下这里绘制贝塞尔曲线用的是Android现成的API,并没有用到咱们之前学习时所使用的德卡斯特里奥算法对吧,因为已经满足咱们的要求了,这里是绘制的二阶贝塞尔,阶数比较低,要高阶的话,德卡斯特里奥算法就有意义的,前提你得了解有德卡斯特里奥算法这么个东东才行。

3、运行:

好,接下来运行看一下效果:

可以看到了曲线的效果了对吧,但是还差一个移动圆的绘制对吧,接下来就是完善它们。

5、绘制拖拽圆和文本:

接下来再来完善拖拽状态的绘制,首先差一个拖拽圆的绘制,比较简单:

此时的样子:

 

然后对于拖拽圆上应该有一个文本的,这里也绘制一下,有个问题是怎么能绘制在这个拖拽圆的中间呢?这里就直接贴出来了,看一遍就会了,调个api既可,这里将文本的颜色、内容、文字大小也可以由用户进行设置,所以当然又得自定义属性啦,如下:

然后在调用处需要定义一下新加的属性了:

此时运行:

6、拖拽圆坐标改为手指触摸:

目前咱们的拖拽圆是写死的,正常应该是需要随手指的移动而移动对吧,这里则加入一个手指触摸事件,实现一个最基本的拖拽圆跟着手指移动而动态改变的,也比较简单,如下:

运行看一下:

加入状态处理逻辑:

目前最难的效果已经实现了,接下来则需要根据手势的情况来加入不同的状态处理了。

绘制时加入状态判断逻辑:

目前气泡在绘制时完全没有考虑到状态情况对吧,所以这里先对代码进行一个调整,这块逻辑也不难理解,直接给出了:

@Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);

        //画拖拽的气泡 和 文字
        if (bubbleState != BUBBLE_STATE_DISMISS) {
            //绘制拖拽圆
            canvas.drawCircle(bubMoveableCenter.x, bubMoveableCenter.y,
                    bubMoveableRadius, bubblePaint);
            //绘制拖拽圆文本
            textPaint.getTextBounds(textStr, 0, textStr.length(), textRect);
            canvas.drawText(textStr, bubMoveableCenter.x - textRect.width() / 2,
                    bubMoveableCenter.y + textRect.height() / 2, textPaint);
        }

        // 2、画相连的气泡状态
        if (bubbleState == BUBBLE_STATE_CONNECT) {
            // 1、画静止气泡
            canvas.drawCircle(bubStillCenter.x, bubStillCenter.y,
                    bubStillRadius, bubblePaint);

            // 2、画相连曲线
            // 计算控制点坐标,两个圆心的中点
            int iAnchorX = (int) ((bubStillCenter.x + bubMoveableCenter.x) / 2);
            int iAnchorY = (int) ((bubStillCenter.y + bubMoveableCenter.y) / 2);

            float cosTheta = (bubMoveableCenter.x - bubStillCenter.x) / dist;
            float sinTheta = (bubMoveableCenter.y - bubStillCenter.y) / dist;

            float iBubStillStartX = bubStillCenter.x - bubStillRadius * sinTheta;
            float iBubStillStartY = bubStillCenter.y + bubStillRadius * cosTheta;
            float iBubMoveableEndX = bubMoveableCenter.x - bubMoveableRadius * sinTheta;
            float iBubMoveableEndY = bubMoveableCenter.y + bubMoveableRadius * cosTheta;
            float iBubMoveableStartX = bubMoveableCenter.x + bubMoveableRadius * sinTheta;
            float iBubMoveableStartY = bubMoveableCenter.y - bubMoveableRadius * cosTheta;
            float iBubStillEndX = bubStillCenter.x + bubStillRadius * sinTheta;
            float iBubStillEndY = bubStillCenter.y - bubStillRadius * cosTheta;

            //绘制贝塞尔曲线
            bezierPath.reset();
            // a、画上半弧
            bezierPath.moveTo(iBubStillStartX, iBubStillStartY);
            bezierPath.quadTo(iAnchorX, iAnchorY, iBubMoveableEndX, iBubMoveableEndY);
            // b、画上半弧
            bezierPath.lineTo(iBubMoveableStartX, iBubMoveableStartY);
            bezierPath.quadTo(iAnchorX, iAnchorY, iBubStillEndX, iBubStillEndY);
            bezierPath.close();//注意:这里一定要封闭
            canvas.drawPath(bezierPath, bubblePaint);
        }
    }

气泡相连拖拽让不动气泡变小:

在最终效果中,可以发现,当气泡随着手指的移动,其不动圆的半径会不变进行变化,可以体会一下,如鼠标所指示的那个:

具体处理则需要在触摸事件那块了,如下:

记得将之前定死的移动圆的坐标值给改一下,如下:

 

此时运行再看一下效果:

可以看到,效果基本就已经快实现了,只是说目前还木有处理手指松掉之后的处理。其中有一个细节不知你有木有理解透,就是关于它:

不理解,咱们将它注释掉跑一下直观感受一下就知道这个变量的作用吧:

运行感受一下区别:

是不是就明白了,也就是为了让不动圆缩没了才断出,比较好的效果肯定是拉一定比例不需要等不动圆缩没了再断开,仔细体会一下~~ 

分离松手气泡回弹效果: 

接下来则只剩松手的状态处理了,首先来处理松手时,其拉得不够长气泡还不足以拉断的情况,也就是:

其实现也不难,用到了属性动画,关于属性动画在之后会专门再去探讨的,这里就直接给出代码了:

@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
    private void startBubbleRestAnim() {
        //从移动圆中心点到不动圆中心点进行动画
        ValueAnimator anim = ValueAnimator.ofObject(new PointFEvaluator(),
                new PointF(bubMoveableCenter.x, bubMoveableCenter.y),
                new PointF(bubStillCenter.x, bubStillCenter.y));

        anim.setDuration(200);
        anim.setInterpolator(new OvershootInterpolator(5f));
        anim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator animation) {
                //监听值的变化,来更改拖动圆的圆心
                bubMoveableCenter = (PointF) animation.getAnimatedValue();
                invalidate();
            }
        });
        anim.addListener(new AnimatorListenerAdapter() {
            @Override
            public void onAnimationEnd(Animator animation) {
                //动画执行完,则让气泡回归最开始的状态
                bubbleState = BUBBLE_STATE_DEFAULT;
            }
        });
        anim.start();
    }

代码有相关的注释说明,就不过多解释了,不难,下面再运行一下:

分离松手气泡消失爆炸效果: 

最后就是拉断气泡消失的效果了,其实这个效果是一个图片的帧动画,先把资源图贴出来:

然后代码也挺简单的,没啥好解释的:

package com.cexo.beziermaker.test;

import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.animation.PointFEvaluator;
import android.animation.ValueAnimator;
import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Path;
import android.graphics.PointF;
import android.graphics.Rect;
import android.os.Build;
import android.support.annotation.Nullable;
import android.support.annotation.RequiresApi;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.View;
import android.view.animation.LinearInterpolator;
import android.view.animation.OvershootInterpolator;

//二阶贝塞尔曲线应用----QQ气泡效果
public class DragBubbleView extends View {

    //constants
    /* 气泡默认状态--静止 */
    private static final int BUBBLE_STATE_DEFAULT = 0;
    /* 气泡相连 */
    private static final int BUBBLE_STATE_CONNECT = 1;
    /* 气泡分离 */
    private static final int BUBBLE_STATE_APART = 2;
    /* 气泡消失 */
    private static final int BUBBLE_STATE_DISMISS = 3;

    //variables
    /* 气泡状态标志 */
    private int bubbleState = BUBBLE_STATE_DEFAULT;

    /* 气泡半径,由用户可以进行设置更改 */
    private float bubbleRadius;
    /* 气泡的画笔 */
    private Paint bubblePaint;
    /* 气泡颜色 */
    private int bubbleColor;

    /* 不动气泡的圆心 */
    private PointF bubStillCenter;
    /* 不动气泡的半径 */
    private float bubStillRadius;

    /* 可动气泡的圆心 */
    private PointF bubMoveableCenter;
    /* 可动气泡的半径 */
    private float bubMoveableRadius;

    /**
     * 贝塞尔曲线path
     */
    private Path bezierPath;
    /* 气泡文本画笔 */
    private Paint textPaint;
    /* 文本绘制区域,让其显示在拖拽圆的区域内 */
    private Rect textRect;

    /* 气泡消息文字 */
    private String textStr;
    /* 气泡消息文字颜色 */
    private int textColor;
    /* 气泡消息文字大小 */
    private float textSize;

    /* 两气泡圆心坐标点距离 */
    private float dist;
    /* 气泡相连状态最大圆心距离 */
    private float maxDist;
    /* 手指触摸偏移量 */
    private final float moveOffset;

    /* 气泡爆炸的bitmap数组 */
    private Bitmap[] burstBitmapsArray;
    /* 是否在执行气泡爆炸动画 */
    private boolean isBurstAnimStart = false;
    /* 爆炸绘制区域 */
    private Rect burstRect;
    private Paint burstPaint;
    /* 气泡爆炸的图片id数组 */
    private int[] burstDrawablesArray = {R.drawable.burst_1, R.drawable.burst_2
            , R.drawable.burst_3, R.drawable.burst_4, R.drawable.burst_5};
    /* 当前气泡爆炸图片index */
    private int curDrawableIndex;

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

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

    public DragBubbleView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        TypedArray array = context.obtainStyledAttributes(attrs, R.styleable.DragBubbleView, defStyleAttr, 0);
        bubbleRadius = array.getDimension(R.styleable.DragBubbleView_bubble_radius, bubbleRadius);
        bubbleColor = array.getColor(R.styleable.DragBubbleView_bubble_color, Color.RED);
        textStr = array.getString(R.styleable.DragBubbleView_bubble_text);
        textSize = array.getDimension(R.styleable.DragBubbleView_bubble_textSize, textSize);
        textColor = array.getColor(R.styleable.DragBubbleView_bubble_textColor, Color.WHITE);
        array.recycle();

        maxDist = 8 * bubbleRadius;
        moveOffset = maxDist / 4;

        bubStillRadius = bubbleRadius;
        bubMoveableRadius = bubStillRadius;

        bubblePaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        bubblePaint.setColor(bubbleColor);
        bubblePaint.setStyle(Paint.Style.FILL);
        bezierPath = new Path();

        //文本画笔
        textPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        textPaint.setColor(textColor);
        textPaint.setTextSize(textSize);
        textRect = new Rect();

        //爆炸画笔
        burstPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        burstPaint.setFilterBitmap(true);
        burstRect = new Rect();
        burstBitmapsArray = new Bitmap[burstDrawablesArray.length];
        for (int i = 0; i < burstDrawablesArray.length; i++) {
            //将气泡爆炸的drawable转为bitmap
            Bitmap bitmap = BitmapFactory.decodeResource(getResources(), burstDrawablesArray[i]);
            burstBitmapsArray[i] = bitmap;
        }
    }

    @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        super.onSizeChanged(w, h, oldw, oldh);
        initView(w, h);
    }

    /**
     * 初始化气泡位置
     */
    private void initView(int w, int h) {

        //设置两气泡圆心初始坐标
        if (bubStillCenter == null) {
            bubStillCenter = new PointF(w / 2, h / 2);
        } else {
            bubStillCenter.set(w / 2, h / 2);
        }

        if (bubMoveableCenter == null) {
            bubMoveableCenter = new PointF(w / 2, h / 2);
        } else {
            bubMoveableCenter.set(w / 2, h / 2);
        }

        dist = (float) Math.hypot(bubMoveableCenter.x - bubStillCenter.x,
                bubMoveableCenter.y - bubStillCenter.y);

        bubbleState = BUBBLE_STATE_DEFAULT;
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);

        //画拖拽的气泡 和 文字
        if (bubbleState != BUBBLE_STATE_DISMISS) {
            //绘制拖拽圆
            canvas.drawCircle(bubMoveableCenter.x, bubMoveableCenter.y,
                    bubMoveableRadius, bubblePaint);
            //绘制拖拽圆文本
            textPaint.getTextBounds(textStr, 0, textStr.length(), textRect);
            canvas.drawText(textStr, bubMoveableCenter.x - textRect.width() / 2,
                    bubMoveableCenter.y + textRect.height() / 2, textPaint);
        }

        // 2、画相连的气泡状态
        if (bubbleState == BUBBLE_STATE_CONNECT) {
            // 1、画静止气泡
            canvas.drawCircle(bubStillCenter.x, bubStillCenter.y,
                    bubStillRadius, bubblePaint);

            // 2、画相连曲线
            // 计算控制点坐标,两个圆心的中点
            int iAnchorX = (int) ((bubStillCenter.x + bubMoveableCenter.x) / 2);
            int iAnchorY = (int) ((bubStillCenter.y + bubMoveableCenter.y) / 2);

            float cosTheta = (bubMoveableCenter.x - bubStillCenter.x) / dist;
            float sinTheta = (bubMoveableCenter.y - bubStillCenter.y) / dist;

            float iBubStillStartX = bubStillCenter.x - bubStillRadius * sinTheta;
            float iBubStillStartY = bubStillCenter.y + bubStillRadius * cosTheta;
            float iBubMoveableEndX = bubMoveableCenter.x - bubMoveableRadius * sinTheta;
            float iBubMoveableEndY = bubMoveableCenter.y + bubMoveableRadius * cosTheta;
            float iBubMoveableStartX = bubMoveableCenter.x + bubMoveableRadius * sinTheta;
            float iBubMoveableStartY = bubMoveableCenter.y - bubMoveableRadius * cosTheta;
            float iBubStillEndX = bubStillCenter.x + bubStillRadius * sinTheta;
            float iBubStillEndY = bubStillCenter.y - bubStillRadius * cosTheta;

            //绘制贝塞尔曲线
            bezierPath.reset();
            // a、画上半弧
            bezierPath.moveTo(iBubStillStartX, iBubStillStartY);
            bezierPath.quadTo(iAnchorX, iAnchorY, iBubMoveableEndX, iBubMoveableEndY);
            // b、画上半弧
            bezierPath.lineTo(iBubMoveableStartX, iBubMoveableStartY);
            bezierPath.quadTo(iAnchorX, iAnchorY, iBubStillEndX, iBubStillEndY);
            bezierPath.close();//注意:这里一定要封闭
            canvas.drawPath(bezierPath, bubblePaint);
        }

        // 3、画消失状态---爆炸动画
        if (isBurstAnimStart) {
            burstRect.set((int) (bubMoveableCenter.x - bubMoveableRadius),
                    (int) (bubMoveableCenter.y - bubMoveableRadius),
                    (int) (bubMoveableCenter.x + bubMoveableRadius),
                    (int) (bubMoveableCenter.y + bubMoveableRadius));

            canvas.drawBitmap(burstBitmapsArray[curDrawableIndex], null,
                    burstRect, bubblePaint);
        }
    }

    @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
    @Override
    public boolean onTouchEvent(MotionEvent event) {
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                if (bubbleState != BUBBLE_STATE_DISMISS) {
                    dist = (float) Math.hypot(event.getX() - bubStillCenter.x, event.getY() - bubStillCenter.y);
                    if (dist < bubbleRadius + moveOffset) {// 加上MOVE_OFFSET是为了方便拖拽
                        bubbleState = BUBBLE_STATE_CONNECT;
                    } else {
                        bubbleState = BUBBLE_STATE_DEFAULT;
                    }
                }
                break;
            case MotionEvent.ACTION_MOVE:
                if (bubbleState != BUBBLE_STATE_DEFAULT) {
                    bubMoveableCenter.x = event.getX();
                    bubMoveableCenter.y = event.getY();
                    dist = (float) Math.hypot(event.getX() - bubStillCenter.x, event.getY() - bubStillCenter.y);
                    if (bubbleState == BUBBLE_STATE_CONNECT) {
                        if (dist < maxDist - moveOffset) {
                            // 让不动气泡的圆半径随着手指的移动而动态变化,减去moveOffset是为了让不动气泡半径到一个较小值时就直接消失
                            bubStillRadius = bubbleRadius - dist / 8;
                        } else {
                            //分离状态
                            bubbleState = BUBBLE_STATE_APART;
                        }
                    }
                    invalidate();
                }
                break;
            case MotionEvent.ACTION_UP:
                if (bubbleState == BUBBLE_STATE_CONNECT) {
                    //没达到拉断的条件,则回弹到原点
                    startBubbleRestAnim();
                } else if (bubbleState == BUBBLE_STATE_APART) {
                    //达到拉断的条件,则让气泡消失
                    if (dist < 2 * bubbleRadius) {
                        startBubbleRestAnim();
                    } else {
                        startBubbleBurstAnim();
                    }
                }
                break;
        }
        return true;
    }

    private void startBubbleBurstAnim() {
        //气泡改为消失状态
        bubbleState = BUBBLE_STATE_DISMISS;
        isBurstAnimStart = true;
        //做一个int型属性动画,从0~mBurstDrawablesArray.length结束
        ValueAnimator anim = ValueAnimator.ofInt(0, burstDrawablesArray.length);
        anim.setInterpolator(new LinearInterpolator());
        anim.setDuration(500);
        anim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator animation) {
                //设置当前绘制的爆炸图片index
                curDrawableIndex = (int) animation.getAnimatedValue();
                invalidate();
            }
        });
        anim.addListener(new AnimatorListenerAdapter() {
            @Override
            public void onAnimationEnd(Animator animation) {
                //修改动画执行标志
                isBurstAnimStart = false;
            }
        });
        anim.start();

    }

    @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
    private void startBubbleRestAnim() {
        //从移动圆中心点到不动圆中心点进行动画
        ValueAnimator anim = ValueAnimator.ofObject(new PointFEvaluator(),
                new PointF(bubMoveableCenter.x, bubMoveableCenter.y),
                new PointF(bubStillCenter.x, bubStillCenter.y));

        anim.setDuration(200);
        anim.setInterpolator(new OvershootInterpolator(5f));
        anim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator animation) {
                //监听值的变化,来更改拖动圆的圆心
                bubMoveableCenter = (PointF) animation.getAnimatedValue();
                invalidate();
            }
        });
        anim.addListener(new AnimatorListenerAdapter() {
            @Override
            public void onAnimationEnd(Animator animation) {
                //动画执行完,则让气泡回归最开始的状态
                bubbleState = BUBBLE_STATE_DEFAULT;
            }
        });
        anim.start();
    }
}

运动效果就如预期的:

 

气泡还原:

最后就是点击还原按钮让气泡还原的功能,比较简单:

这块效果就不演示了。

总结:

至此,关于贝塞尔曲线的内容就学习到这了,这是一个比较简单的二阶曲线的应用,有了上一次的基础之后实现起来也挺轻松的,而对于QQ气泡效果这是纯练习用,里面涉及到点还是不少的,主要是对于绘制和事件处理的逻辑需要理清楚。

posted on 2021-10-11 15:39  cexo  阅读(315)  评论(0编辑  收藏  举报

导航