Android custom View--QQ unread message bubble drag and drop effect

There will be a bubble to show how many messages are coming from friends in the QQ chat interface.

The effect is as follows:

Insert picture description here

analysis:

The components of the animation are as shown in the figure
Insert picture description here
below⚠️Key points
1. The animation consists of three parts: a fixed circle, a movable circle, and a bonding part (AB and CD are two Bezier curves)

Second, do not move the circle center e stationary (apparent) movable round the center f is the finger slide position (known)

Third, the maximum distance that can be dragged, we set 8 times the radius of the movable circle here

Fourth, the fixed circle radius as the movable sheave and the fixed circle distance change change (no drag when the movable radius as the maximum distance when the drag radius 0)
so that: r1 = r2 - g/8 r1 is exactly equal to 0 when g is the largest

Fifth, find the four points of ABCE and draw the Bezier curve link.
Here we use the trigonometric formula to calculate the four points of ABCE.
First calculate the sine (sin) and cosine (cos).
fx represents the x coordinate
of the center of the circle fy represents the center of the circle The y coordinate
ex of f represents the x coordinate
of the center e ey represents the y coordinate of the center e
sin = (fy-ey) /g
cos=(fx-ey) /g
Ax represents the x coordinate of point A
Ax = ex + r1* sin
Ay = ey-r1*cos
. . . .
Four points link AB CD to draw a Bezier curve

6. Finally? The explosion animation is a frame-by-frame animation, but if
you don’t understand here, look at the math

code show as below:

/*
* 模仿腾讯qq气泡拉拽的效果
* */
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  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, @Nullable AttributeSet attrs) {
        this(context, attrs,0);
    }

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

    private void init(Context context, AttributeSet attrs, int 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 = mBubbleRadius;//可动气泡半径
        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(context.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,mBubbleRadius,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){
            Log.d("mile","mCurDrawableIndex:"+mCurDrawableIndex);

            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);
        }
    }

    @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){//加上偏移量方便拖拽
                        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){//如果拖拽距离小于2* mBubbleRadius距离
                        startBubbleRestAnim();//回到原理位置动画
                    }else {
                        startBubbleBurstAnim();//气泡爆炸动画
                    }

                }
                break;
        }

        return true;
    }

    private void startBubbleBurstAnim() {
        mBubbleState = BUBBLE_STATE_DISMISS;
        ValueAnimator anim = ValueAnimator.ofInt(0,mBurstDrawablesArray.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(200);
        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();
    }
}

The code in the layout is as follows:

<com.yulore.customviewgroup.view.DragBubbleView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        app:bubble_color="#ff0000"
        app:bubble_radius="12dp"
        app:bubble_text="12"
        app:bubble_textColor="#ffffff"
        app:bubble_textSize="12dp"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toTopOf="parent"/>

Resource download: csdn resources
need to share points, if there are no shared points, you can go to github
to download the
github address: https://github.com/rain86/MileAndroid
in the CustomView directory as shown below
Insert picture description here

Guess you like

Origin blog.csdn.net/yanwenyuan0304/article/details/94649801