2. GIF

Method of use

Rely on

compile 'com. SZD: messagebubble: 1.0.1'Copy the code

Note: Add the Android :clipChildren=”false” attribute to the parent layout so that bubbles can be dragged in the parent layout. If an interface consists of a RelativeLayout that contains a Recyclerview, add this property to the Recyclerview Item layout, Recyclerview, and RelativeLayout.

Optional attributes are:

attribute role
app:radius The radius of the circle
app:circleColor The color of the circle
app:textSize Size of unread messages
app:number Number of unread messages
app:textSize Size of unread messages

SetDisappearPic (): Accepts an array of ints. You can pass in the vanishing animation that you want to customize into an array. SetNumber (): Sets the number of unread messages to display. SetOnActionListener (): A listener for an operation, including:

  • onDrag(): When being dragged, the maximum drag distance is not exceeded.
  • onMove(): The maximum drag distance is exceeded when being dragged.
  • onDisappear: After the dragged circle disappears.
  • onRestore: Being dragged back to the origin.

Implementation approach

First, we need two circles, one is at the origin does not need to follow your finger circle, a circle is follow your finger, when users click on drawing with finger round and round on the number of unread messages in the fingers to move at the same time, constantly judge whether the distance between the two circles over the distance we set, if not more than the distance, is between the two circles, Bezier curves are drawn with the middle point of two circles as the control point. If the distance exceeds, the bezier curves are stopped and the two circles move in an independent state. When the user loosens the finger, the distance between the two circles will be judged as well. For example, in the furthest distance, the dragged circle will return to the origin by itself. If it exceeds the furthest distance, the deletion animation will be played in the position where the finger is released.

1. Initialization

After we have the idea, we should first determine the parameters that users may need to customize. At present, I have set the following five customized parameters, and the specific meanings can be seen in the table in the usage method.

<?xml version="1.0" encoding="utf-8"? >
<resources>
    <declare-styleable name="MessageBubble">
        <attr name="radius" format="dimension" />
        <attr name="textSize" format="dimension" />
        <attr name="circleColor" format="color" />
        <attr name="textColor" format="color" />
        <attr name="number" format="string"/>
    </declare-styleable>
</resources>Copy the code

After doing some preparatory work, for example we will need a Path to draw Bezier curves, different brushes to draw circles, numbers, disappear animations, define the x and Y coordinates of the initial circle, etc.

2. Size the View

If we use the wrap_content property here, we’ll give the view a 400px by 400px size,

    @Override
    protected void onMeasure(int widthMeasure, int heightMeasure) {
        int widthMode = MeasureSpec.getMode(widthMeasure);
        int widthSize = MeasureSpec.getSize(widthMeasure);
        int heightMode = MeasureSpec.getMode(heightMeasure);
        int heightSize = MeasureSpec.getSize(heightMeasure);

        if (widthMode == MeasureSpec.EXACTLY) {
            mWidth = widthSize;
        } else {
            mWidth = getPaddingLeft() + 400 + getPaddingRight();
        }
        if (heightMode == MeasureSpec.EXACTLY) {
            mHeight = heightSize;
        } else {
            mHeight = getPaddingTop() + 400 + getPaddingBottom();
        }
        setMeasuredDimension(mWidth, mHeight);
    }Copy the code

3. Rewrite the onTouchEvent ()

First of all, we divided the View into 4 states, namely normal state, drag state, move state, disappear state. When the View receives the finger press namely ACTION_DOWN events, first of all determine whether the location of the finger click in, within the scope of the original round here we through the event to get the finger click on x, y coordinates, calculate the finger position and the distance of the center of the circle, if the distance is less than the radius of circle, can prove that the finger click within the scope of the circle. This range should also be adjusted to accommodate smaller phones.

When the finger is inside the circle and starts dragging, the View starts consuming ACTION_MOVE events and is set to STATE_DRAGING. First we also need to get the x and y coordinates of the finger click position to draw the center of the dragged circle. In drag at the same time, we are going to determine whether the current drag distance beyond the distance of the biggest can drag and drop, if not more than, at the same time in the drawing being dragged round to plot the adhesion effect between the two round, if more than the maximum drag distance, the View is set as STATE_MOVE state, no longer paint adhesion effect, drawn by dragging the circle independent.

When the finger is released, the View consumes the ACTION_UP event. At this point, we first need to judge the distance between the center of the circle being dragged and the original circle when the finger is released. If it is beyond the maximum range, the state will be changed to STATE_DISAPPEAR and the bubble disappearing animation will be played at the same time. If it is within the maximum range, the state will be changed to STATE_RESTORE and the bubble recovery animation will be played at the same time.

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                getParent().requestDisallowInterceptTouchEvent(true);
                if(curState ! = STATE_DISAPPEAR) {// Calculate the distance between click position and bubble
                    d = (float) Math.hypot(centerCircleX - event.getX()
                    , centerCircleY - event.getY());
                    if (d < centerRadius + 10) {
                        curState = STATE_DRAGING;
                    } else{ curState = STATE_NORMAL; }}break;
            case MotionEvent.ACTION_MOVE:
                dragCircleX = (int) event.getX();
                dragCircleY = (int) event.getY();
                // The drag distance is calculated when the drag state is exceeded
                if (curState == STATE_DRAGING) {
                    d = (float) Math.hypot(centerCircleX - event.getX()
                    , centerCircleY - event.getY());
                    if (d <= maxDragLength - maxDragLength / 7) {
                        centerRadius = dragRadius - d / 4;
                        if(actionListener ! =null) { actionListener.onDrag(); }}else {
                        centerRadius = 0;
                        curState = STATE_MOVE;
                    }
                  // If the drag distance exceeds the maximum, the middle circle disappears
                } else if (curState == STATE_MOVE) {
                    if(actionListener ! =null) {
                        actionListener.onMove();
                    }
                }
                invalidate();
                break;
            case MotionEvent.ACTION_UP:
                getParent().requestDisallowInterceptTouchEvent(false);
                // When dragging, lift the finger to respond
                if (curState == STATE_DRAGING || curState == STATE_MOVE) {
                    d = (float) Math.hypot(centerCircleX - event.getX()
                    , centerCircleY - event.getY());
                    if (d > maxDragLength) {// If the drag distance is greater than the maximum drag distance, it disappears
                        curState = STATE_DISAPPEAR;
                        startDisappear = true;
                        disappearAnim();
                    } else {// If the drag distance is smaller than the drag distance, restore the bubble position
                        restoreAnim();
                    }
                    invalidate();
                }
                break;
        }
        return true;
    }Copy the code

4. Draw

As mentioned in onTouchEvent(), we have divided the View into four different states, so when drawing, we only need to draw according to each state.

    @Override
    protected void onDraw(Canvas canvas) {
        if (curState == STATE_NORMAL) {
            // Draw the initial circle
            canvas.drawCircle(centerCircleX, centerCircleY, centerRadius, mPaint);
            // Draw the number (do it after the Bezier curve is drawn, otherwise it will be blocked)
            canvas.drawText(mNumber, centerCircleX, centerCircleY + textMove, textPaint);
        }
        // If you start dragging, draw dragCircle
        if (curState == STATE_DRAGING) {
            // Draw the initial circle
            canvas.drawCircle(centerCircleX, centerCircleY, centerRadius, mPaint);
            // Draw the dragged circle
            canvas.drawCircle(dragCircleX, dragCircleY, dragRadius, mPaint);
            drawBezier(canvas);
            canvas.drawText(mNumber, dragCircleX, dragCircleY + textMove, textPaint);
        }

        if (curState == STATE_MOVE) {
            canvas.drawCircle(dragCircleX, dragCircleY, dragRadius, mPaint);
            canvas.drawText(mNumber, dragCircleX, dragCircleY + textMove, textPaint);
        }

        if (curState == STATE_DISAPPEAR && startDisappear) {
            if(disappearBitmap ! =null) {
                canvas.drawBitmap(disappearBitmap[bitmapIndex], null, bitmapRect, disappearPaint); }}}Copy the code

We need to calculate the starting point, control point and center point of the Bezier curve to draw the adhesion effect between two circles. Here I refer to this blog to write, because junior high school mathematics early forget clean, really can’t work out.

High imitation QQ unread message bubble drag bonding effect

If you want to write it yourself, you can also refer to this very useful diagram, which I don’t understand, so I’m going to skip it and just paste it in.





Bezier curve control point calculation. JPG

/** * Draw the Bezier curve *@param canvas canvas
     */
    private void drawBezier(Canvas canvas) {
        float controlX = (centerCircleX + dragCircleX) / 2;// Bezier curve control point X coordinates
        float controlY = (dragCircleY + centerCircleY) / 2;// The bezier curve controls the Y coordinate
        // Calculate the start and end of the curve
        d = (float) Math.hypot(centerCircleX - dragCircleX, centerCircleY - dragCircleY);
        float sin = (centerCircleY - dragCircleY) / d;
        float cos = (centerCircleX - dragCircleX) / d;
        float dragCircleStartX = dragCircleX - dragRadius * sin;
        float dragCircleStartY = dragCircleY + dragRadius * cos;
        float centerCircleEndX = centerCircleX - centerRadius * sin;
        float centerCircleEndY = centerCircleY + centerRadius * cos;
        float centerCircleStartX = centerCircleX + centerRadius * sin;
        float centerCircleStartY = centerCircleY - centerRadius * cos;
        float dragCircleEndX = dragCircleX + dragRadius * sin;
        float dragCircleEndY = dragCircleY - dragRadius * cos;

        mPath.reset();
        mPath.moveTo(centerCircleStartX, centerCircleStartY);
        mPath.quadTo(controlX, controlY, dragCircleEndX, dragCircleEndY);
        mPath.lineTo(dragCircleStartX, dragCircleStartY);
        mPath.quadTo(controlX, controlY, centerCircleEndX, centerCircleEndY);
        mPath.close();

        canvas.drawPath(mPath, mPaint);
    }Copy the code

Bubble Disappearing Animation: Disappearing animation pictures can be modified by calling setDisappearPic() method and also have default animation. Since the animation adopts the principle of similar frame animation, it is necessary to pass in an array of int type that saves the animation picture. The default animation is 500ms. If necessary, you can add a method to set the animation duration.

We use property animation, starting from 0 to the end of the number of disappearing animation pictures, take the current progress of the animation as subscript and inform the View to redraw. When redrawing, the View reads the current subscript value and takes the picture from the array for drawing.

    /** * Bubble disappear animation */
    private void disappearAnim(a) {
        bitmapRect = new Rect(dragCircleX - (int) dragRadius , dragCircleY - (int) dragRadius     ,dragCircleX + (int) dragRadius, dragCircleY + (int) dragRadius);
        ValueAnimator disappearAnimator = ValueAnimator.ofInt(0, disappearBitmap.length);
        disappearAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator animation) {
                bitmapIndex = (int) animation.getAnimatedValue(); invalidate(); }}); disappearAnimator.addListener(new AnimatorListenerAdapter() {
            @Override
            public void onAnimationEnd(Animator animation) {
                startDisappear = false;
                if(actionListener ! =null) { actionListener.onDisappear(); }}}); disappearAnimator.setInterpolator(new LinearInterpolator());
        disappearAnimator.setDuration(500);
        disappearAnimator.start();
    }Copy the code

Bubble Recovery animation:

/** * Bubble recovery animation */
    private void restoreAnim(a) {
        ValueAnimator valueAnimator = ValueAnimator.ofObject(new MyPointFEvaluator(), new PointF(dragCircleX, dragCircleY), new PointF(centerCircleX, centerCircleY));
        valueAnimator.setDuration(200);
        valueAnimator.setInterpolator(new TimeInterpolator() {
            @Override
            public float getInterpolation(float input) {
                float f = 0.571429 f;
                return (float) (Math.pow(2, -4 * input) * Math.sin((input - f / 4) * (2 * Math.PI) / f) + 1); }}); valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator animation) {
                PointF pointF = (PointF) animation.getAnimatedValue();
                dragCircleX = (int) pointF.x;
                dragCircleY = (int) pointF.y; invalidate(); }}); valueAnimator.addListener(new AnimatorListenerAdapter() {
            @Override
            public void onAnimationEnd(Animator animation) {
                / / recovery
                centerRadius = dragRadius;
                curState = STATE_NORMAL;
                if(actionListener ! =null) { actionListener.onRestore(); }}}); valueAnimator.start(); }/** * PointF animation estimator (vibration animation during restoration) */
    private class MyPointFEvaluator implements TypeEvaluator<PointF> {

        @Override
        public PointF evaluate(float fraction, PointF startPointF, PointF endPointF) {
            float x = startPointF.x + fraction * (endPointF.x - startPointF.x);
            float y = startPointF.y + fraction * (endPointF.y - startPointF.y);
            return newPointF(x, y); }}Copy the code

In this blog, the bubble recovery animation, disappearing animation picture materials and Bessel curve drawing part of the reference or reference “High imitation QQ unread message bubble drag adhesion effect” if any infringement will be immediately deleted and stop using.


Github address: github.com/icetea0822/…