BubbleView.gif
主要利用三角函数和贝塞尔曲线实现粘连效果,角度和坐标对应关系如下
QQ气泡.png
/**
* 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);
}
}
网友评论