前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >Android--仿QQ气泡

Android--仿QQ气泡

作者头像
aruba
发布2020-07-03 11:15:50
9170
发布2020-07-03 11:15:50
举报
文章被收录于专栏:android技术android技术
主要利用三角函数和贝塞尔曲线实现粘连效果,角度和坐标对应关系如下
/**
 * qq气泡
 */
public class BubbleView extends View {
    //原始气泡半径
    private int radius;
    //气泡颜色
    private int bubbleColor;
    private Paint bubblePaint = new Paint();
    //数字
    private String textNumber;
    //字体颜色
    private int textColor;
    //字体大小
    private int textSize;
    private Paint textPaint = new Paint();
    //气泡初始坐标
    private PointF bubblePoint = new PointF();
    //气泡移动坐标
    private PointF bubbleMovePoint = new PointF();
    //移动气泡和初始点的距离
    private int dst;
    //移动气泡最大距离
    private int maxDst;
    //气泡状态
    private int status;
    //静止状态
    private final static int status_bubble_default = 0;
    //连接状态
    private final static int status_bubble_connect = 1;
    //断开状态
    private final static int status_bubble_disconnect = 2;
    //消失状态
    private final static int status_bubble_dismiss = 3;
    //数字和气泡的间距
    private float padding;
    //触摸偏移
    private int offset = 100;

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

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

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

    @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
    public BubbleView(Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) {
        super(context, attrs, defStyleAttr, defStyleRes);
        init(attrs);
    }

    private void init(AttributeSet attrs) {
        TypedArray typedArray = getContext().obtainStyledAttributes(attrs, R.styleable.BubbleView);

        textNumber = typedArray.getString(R.styleable.BubbleView_textNumber);
        if (textNumber == null) textNumber = "0";

        bubbleColor = typedArray.getColor(R.styleable.BubbleView_bubbleColor, Color.RED);
        textColor = typedArray.getColor(R.styleable.BubbleView_textColor, Color.WHITE);
        padding = typedArray.getDimensionPixelSize(R.styleable.BubbleView_padding, 15);

        int textSize = typedArray.getDimensionPixelSize(R.styleable.BubbleView_textSize, dpToPx(14));
        setTextSize(textSize);

        bubblePaint.setAntiAlias(true);
        bubblePaint.setColor(bubbleColor);

        textPaint.setAntiAlias(true);
        textPaint.setColor(textColor);
    }

    public void initView(int x, int y) {
        bubblePoint.x = bubbleMovePoint.x = x;
        bubblePoint.y = bubbleMovePoint.y = y;

        status = status_bubble_default;
        invalidate();
    }

    @Override
    protected void onDraw(Canvas canvas) {
        if (status != status_bubble_dismiss) {
            if (status == status_bubble_connect) {
                //画原始气泡,随距离变大,半径不断变小
                int initRadius = (int) (radius - dst / 8f);
                canvas.drawCircle(bubblePoint.x, bubblePoint.y, initRadius, bubblePaint);
                //画两条贝塞尔曲线
                int anchorX, anchorY, pathAX, pathAY, pathBX, pathBY, pathCX, pathCY, pathDX, pathDY;
                float sin = (bubbleMovePoint.y - bubblePoint.y) / (float) dst;
                float cos = (bubbleMovePoint.x - bubblePoint.x) / (float) dst;
                anchorX = (int) (bubblePoint.x + (bubbleMovePoint.x - bubblePoint.x) / 2);
                anchorY = (int) (bubblePoint.y + (bubbleMovePoint.y - bubblePoint.y) / 2);
                pathAX = (int) (bubblePoint.x - initRadius * sin);
                pathAY = (int) (bubblePoint.y + initRadius * cos);
                pathBX = (int) (bubbleMovePoint.x - (radius - 5) * sin);
                pathBY = (int) (bubbleMovePoint.y + (radius - 5) * cos);
                pathCX = (int) (bubbleMovePoint.x + (radius - 5) * sin);
                pathCY = (int) (bubbleMovePoint.y - (radius - 5) * cos);
                pathDX = (int) (bubblePoint.x + initRadius * sin);
                pathDY = (int) (bubblePoint.y - initRadius * cos);

                Path path = new Path();
                path.moveTo(pathAX, pathAY);
                path.quadTo(anchorX, anchorY, pathBX, pathBY);

                path.lineTo(pathCX, pathCY);
                path.quadTo(anchorX, anchorY, pathDX, pathDY);
                path.close();
                canvas.drawPath(path, bubblePaint);
            }

            //画文字
            Rect textRect = new Rect();
            textPaint.getTextBounds(textNumber, 0, textNumber.length(), textRect);
            Paint.FontMetrics m = new Paint.FontMetrics();
            textPaint.getFontMetrics(m);
            int baseLine = (int) (-(m.top + m.bottom) / 2);

            //画气泡
            RectF rectF = new RectF(textRect);
            if (textRect.height() > textRect.width()) {
                rectF.left -= (textRect.height() - textRect.width()) / 2;
                rectF.right += (textRect.height() - textRect.width()) / 2;
            }
            rectF.left -= padding;
            rectF.top -= padding;
            rectF.right += padding;
            rectF.bottom += padding;
            rectF.offset(bubbleMovePoint.x - textRect.width() / 2, bubbleMovePoint.y + textRect.height() / 2);

            radius = (int) (rectF.height() / 2);
            maxDst = radius * 8;
            canvas.drawRoundRect(rectF, radius, radius, bubblePaint);

            canvas.drawText(textNumber, bubbleMovePoint.x - textRect.width() / 2, bubbleMovePoint.y + baseLine, textPaint);
        }
    }

    /**
     * 设置文字字体大小
     *
     * @param textSize
     */
    public void setTextSize(int textSize) {
        this.textSize = textSize;
        textPaint.setTextSize(textSize);
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN: {
                if (status != status_bubble_dismiss) {
                    dst = (int) Math.hypot(event.getX() - bubblePoint.x,
                            event.getY() - bubblePoint.y);

                    if (dst < offset) {//距离在点击触摸范围内,改变成连接状态
                        status = status_bubble_connect;
                        bubbleMovePoint.x = (int) event.getX();
                        bubbleMovePoint.y = (int) event.getY();
                        invalidate();
                    }
                }
                break;
            }
            case MotionEvent.ACTION_MOVE: {
                if (status != status_bubble_dismiss && status != status_bubble_default) {
                    dst = (int) Math.hypot(event.getX() - bubblePoint.x,
                            event.getY() - bubblePoint.y);

                    if (dst > maxDst - offset) {//距离大于最大距离,变为断开状态
                        status = status_bubble_disconnect;
                    }

                    bubbleMovePoint.x = (int) event.getX();
                    bubbleMovePoint.y = (int) event.getY();
                    invalidate();
                }
                break;
            }
            case MotionEvent.ACTION_UP: {
                if (status != status_bubble_dismiss && status != status_bubble_default) {

                    if (status == status_bubble_disconnect && dst > radius * 2) {//断开状态,并距离大于2倍的半径,消失
                        status = status_bubble_dismiss;
                    } else {//还原为默认状态
                        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
                            startResetAnim();
                        } else {
                            bubbleMovePoint.x = bubblePoint.x;
                            bubbleMovePoint.y = bubblePoint.y;
                            status = status_bubble_default;
                        }
                    }

                    invalidate();
                }
                break;
            }
        }
        return true;
    }

    private ValueAnimator resetValueAnimator;

    @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
    private void startResetAnim() {
        resetValueAnimator = ValueAnimator.ofObject(new PointFEvaluator(), new PointF(bubbleMovePoint.x, bubbleMovePoint.y),
                new PointF(bubblePoint.x, bubblePoint.y));
        resetValueAnimator.setDuration(200);
        resetValueAnimator.setInterpolator(new OvershootInterpolator());
        resetValueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator animation) {
                bubbleMovePoint = (PointF) animation.getAnimatedValue();
                postInvalidate();
            }
        });
        resetValueAnimator.addListener(new AnimatorListenerAdapter() {
            @Override
            public void onAnimationEnd(Animator animation) {
                bubbleMovePoint.x = bubblePoint.x;
                bubbleMovePoint.y = bubblePoint.y;
                status = status_bubble_default;
            }
        });

        resetValueAnimator.start();
    }

    public int dpToPx(int size) {
        final float scale = getContext().getResources().getDisplayMetrics().density;
        return (int) (size * scale + 0.5f);
    }
}
项目地址:https://gitee.com/aruba/BubbleView.git
本文参与 腾讯云自媒体分享计划,分享自作者个人站点/博客。
如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 主要利用三角函数和贝塞尔曲线实现粘连效果,角度和坐标对应关系如下
  • 项目地址:https://gitee.com/aruba/BubbleView.git
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档