Look at the renderings first



Let’s break this effect down into four steps

//1, static state, draw a bubble and message data //2, when dragging the finger to connect the state, draw a bubble and message data, Bessel curve, its position of the bubble, size can be changed //3, when dragging a certain distance to separate the state, at this time only one bubble and message data //4, Disappear when fingers are released. Explosion effectCopy the code

Specific code implementation, first define some bubble state and brush, initialization resource file

/** * bubble default state -- static */ 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; /** * bubble radius */ privatefloatmBubbleRadius; /** * private int mBubbleColor; /** * private String mTextStr; /** * private int mTextColor; /** * Bubble message text size */ privatefloatmTextSize; /** * static bubble radius */ privatefloatmBubFixedRadius; /** * The radius of the moving bubble */ privatefloatmBubMovableRadius; /** * private PointF mBubFixedCenter; /** * private PointF mBubMovableCenter; /** * private Paint mBubblePaint; /** * private path mBezierPath; private Paint mTextPaint; Private Rect mTextRect; private Paint mBurstPaint; Private Rect mBurstRect; /** * private int mBubbleState = BUBBLE_STATE_DEFAULT; /** * two bubble center distance */ privatefloatmDist; /** * maximum circle center distance */ privatefloatmMaxDist; /** * Touch offset */ private finalfloatMOVE_OFFSET; /** * private bitMap [] mBurstBitmapsArray; */ private Boolean mIsBurstAnimStart =false; /** * private int mCurDrawableIndex; Private int[] mBurstDrawablesArray = {r.map.burst_1, R.map.burst_2, R.map.burst_3, private int[] mBurstDrawablesArray = {R.map.burst_1, R.map.burst_2, R.map.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; // Anti-aliasing 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++) {/ / will bubble explosion drawable to bitmap bitmap bitmap. = BitmapFactory decodeResource (getResources (), mBurstDrawablesArray [I]); mBurstBitmapsArray[i] = bitmap; }}Copy the code


rewriteThe onSizeChanged method starts with the center coordinates of both movable and fixed bubbles at the midpoint of the screen

@Override protected void onSizeChanged(int w, int h, int oldw, int oldh) { super.onSizeChanged(w, h, oldw, oldh); // Do not move bubble centerif (mBubFixedCenter == null)
    {
        mBubFixedCenter = new PointF(w / 2, h / 2);
    } else{ mBubFixedCenter.set(w / 2, h / 2); } // Can move bubble centerif (mBubMovableCenter == null)
    {
        mBubMovableCenter = new PointF(w / 2, h / 2);
    } else{ mBubMovableCenter.set(w / 2, h / 2); }}Copy the code

Next, we first draw the moving bubble, which is a ball and the number of messages on the ball


@Override
protected void onDraw(Canvas canvas)
{
    super.onDraw(canvas);
if(mBubbleState ! DrawCircle (mbubMovablecenter. x, mBubMovAblecenter. y, mBubMovableRadius, 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); }}Copy the code




Next, when our finger is pressed, we need to draw the immovable bubble movable bubble and two Bessel curves, and the center of the immovable bubble is the center of the screen, with the change of the distance between the movable bubble and change, Bessel curve is also changed with the change of distance




// Check whether the state is connectedifDrawCircle (mBubFixedCenter. X, mBubFixedCenter. Y, mBubFixedCenter. mBubFixedRadius, mBubblePaint); }Copy the code


The key is to draw bezier curve between two bubbles, as shown in figure, the figure of the origin (0, 0) for the mobile coordinate, namely the left upper corner of the screen, the original two circle is in the center of the screen, what exactly is that small circle in the center of the screen, great circle around the small circle This abstract them to close to phone the origin coordinate So easy to understand the following coordinates are given




Float x1, float y1, float x2, float y2 public void quadTo(float x1, float y1, float x2, float y2)

The float x1, float y1, represents the (x, y) coordinates of G, and float x2, float y2, represents the end point of the curve, and B is the end point if A is the starting point

D is the starting point, and C is the ending point. You only need to figure out the coordinates of A,B,G,C, and G to draw


Knowing the centers of two circles, or radii, find the distance between the centers, as shown in the figure above, in triangle OEP

Op is the distance between the two centers, and p is the coordinates of p based on onTouchEvent Event.getx (), event.gety () triggered by finger press. The Pythagorean theorem can be used to find the length of op. MBubFixedCenter. X mBubFixedCenter

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


So op is equal to mDist and we know that the length of op POE is sin and cosine

POE is equal to DOH and AOM, and we also know that the radii of these two circles,

So the coordinates of points A, B, C, and D are available


MBubFixedCenter mBubMovableCenter can move the center of the circle

Int iAnchorX = (int) ((mbubFixedCenter.x + mbubMovablecenter.x) / 2); int iAnchorY = (int) ((mBubFixedCenter.y + mBubMovableCenter.y) / 2);Copy the code

Find the sine and cosine of Angle POE

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

Find the coordinates of the four points

//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;Copy the code

Plot bezier curves

mBezierPath.reset(); mBezierPath.moveTo(iBubFixedStartX, iBubFixedStartY); mBezierPath.quadTo(iAnchorX, iAnchorY, iBubMovableEndX, iBubMovableEndY); // move to B mbezierPath. lineTo(iBubMovableStartX, iBubMovableStartY); mBezierPath.quadTo(iAnchorX, iAnchorY, iBubFixedEndX, iBubFixedEndY); mBezierPath.close(); canvas.drawPath(mBezierPath, mBubblePaint);Copy the code


In addition, you also need to use attribute animations, such as rubber band animations and // explosions

Rubber band animation effect

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(newAnimatorListenerAdapter() { @Override public void onAnimationEnd(Animator animation) { super.onAnimationEnd(animation); mBubbleState = BUBBLE_STATE_DEFAULT; }}); anim.start(); }Copy the code

Explosion effect


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(); }Copy the code


Basically, there are some details of the condition judgment is not an analysis, affixed with the source code

Public class DragBubbleView extends View {/** * bubble default state -- static */ 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; /** * bubble radius */ privatefloatmBubbleRadius; /** * private int mBubbleColor; /** * private String mTextStr; /** * private int mTextColor; /** * Bubble message text size */ privatefloatmTextSize; /** * static bubble radius */ privatefloatmBubFixedRadius; /** * The radius of the moving bubble */ privatefloatmBubMovableRadius; /** * private PointF mBubFixedCenter; /** * private PointF mBubMovableCenter; /** * private Paint mBubblePaint; /** * private path mBezierPath; private Paint mTextPaint; Private Rect mTextRect; private Paint mBurstPaint; Private Rect mBurstRect; /** * private int mBubbleState = BUBBLE_STATE_DEFAULT; /** * two bubble center distance */ privatefloatmDist; /** * maximum circle center distance */ privatefloatmMaxDist; /** * Touch offset */ private finalfloatMOVE_OFFSET; /** * private bitMap [] mBurstBitmapsArray; */ private Boolean mIsBurstAnimStart =false; /** * private int mCurDrawableIndex; Private int[] mBurstDrawablesArray = {r.map.burst_1, R.map.burst_2, R.map.burst_3, private int[] mBurstDrawablesArray = {R.map.burst_1, R.map.burst_2, R.map.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; // Anti-aliasing 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++) {/ / will bubble explosion drawable to 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); // Do not move bubble centerif (mBubFixedCenter == null)
        {
            mBubFixedCenter = new PointF(w / 2, h / 2);
        } else{ mBubFixedCenter.set(w / 2, h / 2); } // Can move bubble centerif (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);

        ifDrawCircle (mBubFixedCenter. X, mBubFixedCenter. Y, mBubFixedCenter. 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;
            floatiBubMovableEndY = mBubMovableCenter.y + mBubMovableRadius * cosTheta; mBezierPath.reset(); mBezierPath.moveTo(iBubFixedStartX, iBubFixedStartY); mBezierPath.quadTo(iAnchorX, iAnchorY, iBubMovableEndX, iBubMovableEndY); // move to B mbezierPath. lineTo(iBubMovableStartX, iBubMovableStartY); mBezierPath.quadTo(iAnchorX, iAnchorY, iBubFixedEndX, iBubFixedEndY); mBezierPath.close(); canvas.drawPath(mBezierPath, mBubblePaint); }if(mBubbleState ! DrawCircle (mbubMovablecenter. x, mBubMovAblecenter. y, mBubMovableRadius, 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, static state, one bubble plus message data //2, connected state, one bubble plus message data, Bezier curve, bubble on its own position, size variable //3, separated state, one bubble plus message data //4, disappearing state, } @override public Boolean onTouchEvent(MotionEvent) {switch (event.getAction()) {Override public Boolean onTouchEvent(MotionEvent) {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) {// Add MOVE_OFFSET to make it easier to drag and drop 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)
                    {
                        ifMDist < mMaxDist - MOVE_OFFSET) {mBubFixedRadius = mbubbleradius-mdist / 8; mDist < mMaxDist - MOVE_OFFSET) {mBubFixedRadius = mbubbleradius-mdist / 8; }else{ mBubbleState = BUBBLE_STATE_APART; }} invalidate(); }break;
            case MotionEvent.ACTION_UP:
                if(mBubbleState == BUBBLE_STATE_CONNECT) {// Rubber band animation startBubbleRestAnim(); }else if (mBubbleState == BUBBLE_STATE_APART)
                {
                    if (mDist < 2 * mBubbleRadius)
                    {
                        startBubbleRestAnim();
                    } else{// Burst effect 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 voidstartBubbleRestAnim()
    {
        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(newAnimatorListenerAdapter() { @Override public void onAnimationEnd(Animator animation) { super.onAnimationEnd(animation); mBubbleState = BUBBLE_STATE_DEFAULT; }}); anim.start(); }}Copy the code
<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>Copy the code


<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"
 />Copy the code