贝塞尔曲线-仿QQ消息拖动效果

482 阅读8分钟

先看效果图


我们先来把这个效果拆分成4个步骤

//1,静止状态,绘制 一个气泡加消息数据

//2,当用手指拖动时  连接状态,绘制 一个气泡加消息数据,贝塞尔曲线,本身位置上气泡,大小可变化

//3,当拖动到一定的距离后 分离状态,此时只有一个气泡加消息数据

//4,当手指松开时 消失状态,爆炸效果

具体代码实现,首先定义一些气泡状态及画笔 ,初始化资源文件

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

/**
 * 气泡半径
 */
private float mBubbleRadius;
/**
 * 气泡颜色
 */
private int mBubbleColor;
/**
 * 气泡消息文字
 */
private String mTextStr;
/**
 * 气泡消息文字颜色
 */
private int mTextColor;
/**
 * 气泡消息文字大小
 */
private float mTextSize;
/**
 * 不动气泡的半径
 */
private float mBubFixedRadius;
/**
 * 可动气泡的半径
 */
private float mBubMovableRadius;
/**
 * 不动气泡的圆心
 */
private PointF mBubFixedCenter;
/**
 * 可动气泡的圆心
 */
private PointF mBubMovableCenter;
/**
 * 气泡的画笔
 */
private Paint mBubblePaint;
/**
 * 贝塞尔曲线path
 */
private Path mBezierPath;

private Paint mTextPaint;

//文本绘制区域
private Rect mTextRect;

private Paint mBurstPaint;

//爆炸绘制区域
private Rect mBurstRect;

/**
 * 气泡状态标志
 */
private int mBubbleState = BUBBLE_STATE_DEFAULT;
/**
 * 两气泡圆心距离
 */
private float mDist;
/**
 * 气泡相连状态最大圆心距离
 */
private float mMaxDist;
/**
 * 手指触摸偏移量
 */
private final float MOVE_OFFSET;

/**
 * 气泡爆炸的bitmap数组
 */
private Bitmap[] mBurstBitmapsArray;
/**
 * 是否在执行气泡爆炸动画
 */
private boolean mIsBurstAnimStart = false;

/**
 * 当前气泡爆炸图片index
 */
private int mCurDrawableIndex;

/**
 * 气泡爆炸的图片id数组
 */
private int[] mBurstDrawablesArray = {R.mipmap.burst_1, R.mipmap.burst_2, R.mipmap.burst_3, R.mipmap.burst_4, R.mipmap.burst_5};

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

public DragBubbleView(Context context, 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);
    mBubbleRadius = array.getDimension(R.styleable.DragBubbleView_bubble_radius, mBubbleRadius);
    mBubbleColor = array.getColor(R.styleable.DragBubbleView_bubble_color, Color.RED);
    mTextStr = array.getString(R.styleable.DragBubbleView_bubble_text);
    mTextSize = array.getDimension(R.styleable.DragBubbleView_bubble_textSize, mTextSize);
    mTextColor = array.getColor(R.styleable.DragBubbleView_bubble_textColor, Color.WHITE);
    array.recycle();

    //两个圆半径大小一致
    mBubFixedRadius = mBubbleRadius;
    mBubMovableRadius = mBubFixedRadius;
    mMaxDist = 8 * mBubbleRadius;
    MOVE_OFFSET = mMaxDist / 4;

    //抗锯齿
    mBubblePaint = new Paint(Paint.ANTI_ALIAS_FLAG);
    mBubblePaint.setColor(mBubbleColor);
    mBubblePaint.setStyle(Paint.Style.FILL);
    mBezierPath = new Path();

    //文本画笔
    mTextPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
    mTextPaint.setColor(mTextColor);
    mTextPaint.setTextSize(mTextSize);
    mTextRect = new Rect();

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


重写onSizeChanged方法,一开始让 可动气泡和不动气泡的圆心坐标都在屏幕中点

@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh)
{
    super.onSizeChanged(w, h, oldw, oldh);
    //不动气泡圆心
    if (mBubFixedCenter == null)
    {
        mBubFixedCenter = new PointF(w / 2, h / 2);
    } else
    {
        mBubFixedCenter.set(w / 2, h / 2);
    }

    //可动气泡圆心
    if (mBubMovableCenter == null)
    {
        mBubMovableCenter = new PointF(w / 2, h / 2);
    } else
    {
        mBubMovableCenter.set(w / 2, h / 2);
    }
}

接下来,我们首先绘制可动气泡,就是一个小球并且小球上的消息数量


@Override
protected void onDraw(Canvas canvas)
{
    super.onDraw(canvas);
if (mBubbleState != BUBBLE_STATE_DISMISS)
{
    //绘制一个气泡加消息数据
    canvas.drawCircle(mBubMovableCenter.x, mBubMovableCenter.y, mBubMovableRadius, mBubblePaint);
    mTextPaint.getTextBounds(mTextStr, 0, mTextStr.length(), mTextRect);
    canvas.drawText(mTextStr, mBubMovableCenter.x - mTextRect.width() / 2, mBubMovableCenter.y + mTextRect.height() / 2, mTextPaint);
}}



 接下来,当我们手指按下时,就需要绘制 不动气泡 可动气泡 以及两条贝塞尔曲线 ,并且不动气泡的圆心就是屏幕的中心,随着与可动气泡间距离的变化而变化,贝塞尔曲线也是随着距离的变化而变化



//判断 此时的状态 是否是连接状态
if (mBubbleState == BUBBLE_STATE_CONNECT)
{
//绘制不动气泡
canvas.drawCircle(mBubFixedCenter.x, mBubFixedCenter.y, mBubFixedRadius, mBubblePaint);
}


最关键的是绘制两个气泡间的贝塞尔曲线,如图,图中的(0,0)为手机原点坐标,即屏幕左上角,本来这两个圆是在屏幕中心的,准确的说是小圆在屏幕中心,大圆围着小圆转 这里把它们抽象到紧贴手机原点坐标  这样便于理解下面的坐标求法



图中蓝色的两条线就是两个气泡之间的贝塞尔曲线,这是两条二阶曲线,android中的 public void quadTo(float x1, float y1, float x2, float y2)

方法可用于绘制贝塞尔曲线,其中 float x1, float y1,表示的时数据控制点,就是G点的(x,y)坐标,float x2, float y2,表示曲线的结束点,如果以A点为开始点的话,B就是那个结束点

D为开始点,c就是结束点,只需要求出 A,B,G,C,G 这五个点的坐标就可以绘制


 知道两个圆的圆心即半径,要求出两个圆心的距离,如上图,在三角形OEP中

op就是两个圆心间的距离,而p点此时根据 手指按下 触发的 onTouchEvent event.getX(),event.getY(),就是p点的坐标, 利用勾股定理可求出op的长度 mBubFixedCenter.x  mBubFixedCenter.y为不动圆的圆心坐标 即小圆

mDist = (float) Math.hypot(event.getX() - mBubFixedCenter.x, event.getY() - mBubFixedCenter.y);


即 op=mDist 知道了op的长度 角POE的 三角函数 sin cos 值可求

根据数学知识 角POE= 角DOH 和角AOM,两个圆的半径我们也知道,

所以点 A,B,C,D的坐标可求


根据数学知识 G点坐标  mBubFixedCenter 不动圆圆心 mBubMovableCenter可动圆心

//控制点坐标
int iAnchorX = (int) ((mBubFixedCenter.x + mBubMovableCenter.x) / 2);
int iAnchorY = (int) ((mBubFixedCenter.y + mBubMovableCenter.y) / 2);

求出角POE的sin cos

float sinTheta = (mBubMovableCenter.y - mBubFixedCenter.y) / mDist;
float cosTheta = (mBubMovableCenter.x - mBubFixedCenter.x) / mDist;

求出四个点的坐标

//B
float iBubMovableStartX = mBubMovableCenter.x + sinTheta * mBubMovableRadius;
float iBubMovableStartY = mBubMovableCenter.y - cosTheta * mBubMovableRadius;

//A
float iBubFixedEndX = mBubFixedCenter.x + mBubFixedRadius * sinTheta;
float iBubFixedEndY = mBubFixedCenter.y - mBubFixedRadius * cosTheta;

//D
float iBubFixedStartX = mBubFixedCenter.x - mBubFixedRadius * sinTheta;
float iBubFixedStartY = mBubFixedCenter.y + mBubFixedRadius * cosTheta;
//C
float iBubMovableEndX = mBubMovableCenter.x - mBubMovableRadius * sinTheta;
float iBubMovableEndY = mBubMovableCenter.y + mBubMovableRadius * cosTheta;

绘制贝塞尔曲线

mBezierPath.reset();
mBezierPath.moveTo(iBubFixedStartX, iBubFixedStartY);
mBezierPath.quadTo(iAnchorX, iAnchorY, iBubMovableEndX, iBubMovableEndY);
//移动到B点
mBezierPath.lineTo(iBubMovableStartX, iBubMovableStartY);
mBezierPath.quadTo(iAnchorX, iAnchorY, iBubFixedEndX, iBubFixedEndY);
mBezierPath.close();
canvas.drawPath(mBezierPath, mBubblePaint);


此外还需要用到属性动画,效果图中有 橡皮筋动画效果 和 //爆炸效果

橡皮筋动画效果

private void startBubbleRestAnim()
{
    ValueAnimator anim = ValueAnimator.ofObject(new PointFEvaluator(),
            new PointF(mBubMovableCenter.x, mBubMovableCenter.y),
            new PointF(mBubFixedCenter.x, mBubFixedCenter.y));
    anim.setDuration(100);
    anim.setInterpolator(new OvershootInterpolator(5f));
    anim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener()
    {
        @Override
        public void onAnimationUpdate(ValueAnimator animation)
        {
            mBubMovableCenter = (PointF) animation.getAnimatedValue();
            invalidate();
        }
    });
    anim.addListener(new AnimatorListenerAdapter()
    {
        @Override
        public void onAnimationEnd(Animator animation)
        {
            super.onAnimationEnd(animation);
            mBubbleState = BUBBLE_STATE_DEFAULT;
        }
    });
    anim.start();
}

爆炸效果


private void startBubbleBurstAnim()
{
    mBubbleState = BUBBLE_STATE_DISMISS;
    ValueAnimator anim = ValueAnimator.ofInt(0, mBurstBitmapsArray.length);
    anim.setDuration(500);
    anim.setInterpolator(new LinearInterpolator());
    anim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener()
    {
        @Override
        public void onAnimationUpdate(ValueAnimator animation)
        {
            mCurDrawableIndex = (int) animation.getAnimatedValue();
            invalidate();
        }
    });
    anim.start();
}


基本上就是如此,还有些细节的条件判断就不一一分析了,贴上源代码

public class DragBubbleView extends View
{

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

    /**
     * 气泡半径
     */
    private float mBubbleRadius;
    /**
     * 气泡颜色
     */
    private int mBubbleColor;
    /**
     * 气泡消息文字
     */
    private String mTextStr;
    /**
     * 气泡消息文字颜色
     */
    private int mTextColor;
    /**
     * 气泡消息文字大小
     */
    private float mTextSize;
    /**
     * 不动气泡的半径
     */
    private float mBubFixedRadius;
    /**
     * 可动气泡的半径
     */
    private float mBubMovableRadius;
    /**
     * 不动气泡的圆心
     */
    private PointF mBubFixedCenter;
    /**
     * 可动气泡的圆心
     */
    private PointF mBubMovableCenter;
    /**
     * 气泡的画笔
     */
    private Paint mBubblePaint;
    /**
     * 贝塞尔曲线path
     */
    private Path mBezierPath;

    private Paint mTextPaint;

    //文本绘制区域
    private Rect mTextRect;

    private Paint mBurstPaint;

    //爆炸绘制区域
    private Rect mBurstRect;

    /**
     * 气泡状态标志
     */
    private int mBubbleState = BUBBLE_STATE_DEFAULT;
    /**
     * 两气泡圆心距离
     */
    private float mDist;
    /**
     * 气泡相连状态最大圆心距离
     */
    private float mMaxDist;
    /**
     * 手指触摸偏移量
     */
    private final float MOVE_OFFSET;

    /**
     * 气泡爆炸的bitmap数组
     */
    private Bitmap[] mBurstBitmapsArray;
    /**
     * 是否在执行气泡爆炸动画
     */
    private boolean mIsBurstAnimStart = false;

    /**
     * 当前气泡爆炸图片index
     */
    private int mCurDrawableIndex;

    /**
     * 气泡爆炸的图片id数组
     */
    private int[] mBurstDrawablesArray = {R.mipmap.burst_1, R.mipmap.burst_2, R.mipmap.burst_3, R.mipmap.burst_4, R.mipmap.burst_5};

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

    public DragBubbleView(Context context, 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);
        mBubbleRadius = array.getDimension(R.styleable.DragBubbleView_bubble_radius, mBubbleRadius);
        mBubbleColor = array.getColor(R.styleable.DragBubbleView_bubble_color, Color.RED);
        mTextStr = array.getString(R.styleable.DragBubbleView_bubble_text);
        mTextSize = array.getDimension(R.styleable.DragBubbleView_bubble_textSize, mTextSize);
        mTextColor = array.getColor(R.styleable.DragBubbleView_bubble_textColor, Color.WHITE);
        array.recycle();

        //两个圆半径大小一致
        mBubFixedRadius = mBubbleRadius;
        mBubMovableRadius = mBubFixedRadius;
        mMaxDist = 8 * mBubbleRadius;
        MOVE_OFFSET = mMaxDist / 4;

        //抗锯齿
        mBubblePaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        mBubblePaint.setColor(mBubbleColor);
        mBubblePaint.setStyle(Paint.Style.FILL);
        mBezierPath = new Path();

        //文本画笔
        mTextPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        mTextPaint.setColor(mTextColor);
        mTextPaint.setTextSize(mTextSize);
        mTextRect = new Rect();

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

    @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh)
    {
        super.onSizeChanged(w, h, oldw, oldh);
        //不动气泡圆心
        if (mBubFixedCenter == null)
        {
            mBubFixedCenter = new PointF(w / 2, h / 2);
        } else
        {
            mBubFixedCenter.set(w / 2, h / 2);
        }

        //可动气泡圆心
        if (mBubMovableCenter == null)
        {
            mBubMovableCenter = new PointF(w / 2, h / 2);
        } else
        {
            mBubMovableCenter.set(w / 2, h / 2);
        }
    }


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

        if (mBubbleState == BUBBLE_STATE_CONNECT)
        {
            //绘制不动气泡
            canvas.drawCircle(mBubFixedCenter.x, mBubFixedCenter.y, mBubFixedRadius, mBubblePaint);
            //绘制贝塞尔曲线
            //控制点坐标
            int iAnchorX = (int) ((mBubFixedCenter.x + mBubMovableCenter.x) / 2);
            int iAnchorY = (int) ((mBubFixedCenter.y + mBubMovableCenter.y) / 2);

            float sinTheta = (mBubMovableCenter.y - mBubFixedCenter.y) / mDist;
            float cosTheta = (mBubMovableCenter.x - mBubFixedCenter.x) / mDist;

            //B
            float iBubMovableStartX = mBubMovableCenter.x + sinTheta * mBubMovableRadius;
            float iBubMovableStartY = mBubMovableCenter.y - cosTheta * mBubMovableRadius;

            //A
            float iBubFixedEndX = mBubFixedCenter.x + mBubFixedRadius * sinTheta;
            float iBubFixedEndY = mBubFixedCenter.y - mBubFixedRadius * cosTheta;

            //D
            float iBubFixedStartX = mBubFixedCenter.x - mBubFixedRadius * sinTheta;
            float iBubFixedStartY = mBubFixedCenter.y + mBubFixedRadius * cosTheta;
            //C
            float iBubMovableEndX = mBubMovableCenter.x - mBubMovableRadius * sinTheta;
            float iBubMovableEndY = mBubMovableCenter.y + mBubMovableRadius * cosTheta;

            mBezierPath.reset();
            mBezierPath.moveTo(iBubFixedStartX, iBubFixedStartY);
            mBezierPath.quadTo(iAnchorX, iAnchorY, iBubMovableEndX, iBubMovableEndY);
            //移动到B点
            mBezierPath.lineTo(iBubMovableStartX, iBubMovableStartY);
            mBezierPath.quadTo(iAnchorX, iAnchorY, iBubFixedEndX, iBubFixedEndY);
            mBezierPath.close();
            canvas.drawPath(mBezierPath, mBubblePaint);
        }

        if (mBubbleState != BUBBLE_STATE_DISMISS)
        {
            //绘制一个气泡加消息数据
            canvas.drawCircle(mBubMovableCenter.x, mBubMovableCenter.y, mBubMovableRadius, mBubblePaint);
            mTextPaint.getTextBounds(mTextStr, 0, mTextStr.length(), mTextRect);
            canvas.drawText(mTextStr, mBubMovableCenter.x - mTextRect.width() / 2, mBubMovableCenter.y + mTextRect.height() / 2, mTextPaint);
        }

        if (mBubbleState == BUBBLE_STATE_DISMISS && mCurDrawableIndex < mBurstBitmapsArray.length)
        {
            mBurstRect.set(
                    (int) (mBubMovableCenter.x - mBubMovableRadius),
                    (int) (mBubMovableCenter.y - mBubMovableRadius),
                    (int) (mBubMovableCenter.x + mBubMovableRadius),
                    (int) (mBubMovableCenter.y + mBubMovableRadius)
            );
            canvas.drawBitmap(mBurstBitmapsArray[mCurDrawableIndex], null, mBurstRect, mBubblePaint);
        }

        //1,静止状态,一个气泡加消息数据

        //2, 连接状态,一个气泡加消息数据,贝塞尔曲线,本身位置上气泡,大小可变化

        //3,分离状态,一个气泡加消息数据

        //4,消失状态,爆炸效果

    }

    @Override
    public boolean onTouchEvent(MotionEvent event)
    {
        switch (event.getAction())
        {
            case MotionEvent.ACTION_DOWN:
                if (mBubbleState != BUBBLE_STATE_DISMISS)
                {
                    mDist = (float) Math.hypot(event.getX() - mBubFixedCenter.x, event.getY() - mBubFixedCenter.y);
                    if (mDist < mBubbleRadius + MOVE_OFFSET)
                    { //加上MOVE_OFFSET是为了方便拖拽
                        mBubbleState = BUBBLE_STATE_CONNECT;
                    } else
                    {
                        mBubbleState = BUBBLE_STATE_DEFAULT;
                    }
                }
                break;
            case MotionEvent.ACTION_MOVE:
                if (mBubbleState != BUBBLE_STATE_DEFAULT)
                {
                    mDist = (float) Math.hypot(event.getX() - mBubFixedCenter.x, event.getY() - mBubFixedCenter.y);
                    mBubMovableCenter.x = event.getX();
                    mBubMovableCenter.y = event.getY();
                    if (mBubbleState == BUBBLE_STATE_CONNECT)
                    {
                        if (mDist < mMaxDist - MOVE_OFFSET)
                        {//当拖拽的距离在指定范围内,那么调整不动气泡的半径
                            mBubFixedRadius = mBubbleRadius - mDist / 8;
                        } else
                        {
                            mBubbleState = BUBBLE_STATE_APART;//当拖拽的距离超过指定范围,那么改成分离状态
                        }
                    }
                    invalidate();
                }
                break;
            case MotionEvent.ACTION_UP:
                if (mBubbleState == BUBBLE_STATE_CONNECT)
                {
                    //橡皮筋动画效果
                    startBubbleRestAnim();
                } else if (mBubbleState == BUBBLE_STATE_APART)
                {
                    if (mDist < 2 * mBubbleRadius)
                    {
                        startBubbleRestAnim();
                    } else
                    {
                        //爆炸效果
                        startBubbleBurstAnim();
                    }
                }
                break;
        }
        return true;
    }

    private void startBubbleBurstAnim()
    {
        mBubbleState = BUBBLE_STATE_DISMISS;
        ValueAnimator anim = ValueAnimator.ofInt(0, mBurstBitmapsArray.length);
        anim.setDuration(500);
        anim.setInterpolator(new LinearInterpolator());
        anim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener()
        {
            @Override
            public void onAnimationUpdate(ValueAnimator animation)
            {
                mCurDrawableIndex = (int) animation.getAnimatedValue();
                invalidate();
            }
        });
        anim.start();
    }

    private void startBubbleRestAnim()
    {
        ValueAnimator anim = ValueAnimator.ofObject(new PointFEvaluator(),
                new PointF(mBubMovableCenter.x, mBubMovableCenter.y),
                new PointF(mBubFixedCenter.x, mBubFixedCenter.y));
        anim.setDuration(100);
        anim.setInterpolator(new OvershootInterpolator(5f));
        anim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener()
        {
            @Override
            public void onAnimationUpdate(ValueAnimator animation)
            {
                mBubMovableCenter = (PointF) animation.getAnimatedValue();
                invalidate();
            }
        });
        anim.addListener(new AnimatorListenerAdapter()
        {
            @Override
            public void onAnimationEnd(Animator animation)
            {
                super.onAnimationEnd(animation);
                mBubbleState = BUBBLE_STATE_DEFAULT;
            }
        });
        anim.start();
    }
}
<declare-styleable name="DragBubbleView">
    <attr name="bubble_radius" format="dimension"/>
    <attr name="bubble_color" format="color"/>
    <attr name="bubble_text" format="string"/>
    <attr name="bubble_textSize" format="dimension"/>
    <attr name="bubble_textColor" format="color"/>
</declare-styleable>


<com.rx.myapplication.DragBubbleView
    android:id="@+id/bubbleView"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:text="Hello World!"
    app:bubble_color="#ff0000"
    app:bubble_radius="12dp"
    app:bubble_text="12"
    app:bubble_textColor="#ffffff"
    app:bubble_textSize="12dp"
 />