该篇主要是对MessageBubbleView仿QQ消息控件的修改。因为我发现这个QQ消息气泡开源控件是规则的圆,所以稍加修改,对onDraw()绘画图形做了变动,更加接近于QQ气泡了。毕竟前人栽树后人乘凉,该控件又是通过手指触摸调用事件分发处理又是贝塞尔曲线的应用,多少目前能力有限,只有借鉴了。需要的文件图片请从文中提供的MessageBubbleView仿QQ消息控件下载。
参考博客:仿 QQ 未读消息气泡,可拖拽删除,粘连效果
参考博客中的实现思路:
首先我们需要两个圆,一个是在原点不需要跟随手指的圆,一个是跟随手指的圆,当用户开始点击时,绘制跟随手指的圆和圆上的未读消息数量,同时在手指移动时,不停地判断两圆之间的距离是否超过我们所设定的最远距离,如果未超过这个距离,则在两圆之间,以两圆圆心的中间点为控制点绘制贝塞尔曲线,如果超过距离,则停止绘制贝塞尔曲线,两圆成独立状态移动。用户松开手指时,同样对两圆之间的距离进行判断,如在最远距离内,被拖动的圆自行回到原点,如超过最远距离,则在手指释放位置播放删除动画。
qq.gif废话不多说,先看一下效果图:
1.需要自定义控件属性,在attrs.xml中添加如下
<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>
2.在布局中添加控件使用
注意:使用时需要在所有父布局中加入android:clipChildren="false"属性,使气泡可以在父布局中拖动。
<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:clipChildren="false"
tools:context=".PointQQActivity">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="50dp"
android:layout_marginTop="200dp"
android:background="@color/colorPrimary"
android:clipChildren="false"
app:layout_constraintTop_toTopOf="@+id/ly_qq">
<com.fivefloor.bo.myview.widgets.QQMessageBubbleView
android:layout_width="50dp"
android:layout_height="50dp"
android:layout_gravity="center"
app:number="9"
app:radius="10dp"
app:textSize="12sp" />
<com.fivefloor.bo.myview.widgets.QQMessageBubbleView
android:layout_width="50dp"
android:layout_height="50dp"
android:layout_gravity="center"
app:number="99"
app:radius="10dp"
app:textSize="12sp" />
<com.fivefloor.bo.myview.widgets.QQMessageBubbleView
android:layout_width="50dp"
android:layout_height="50dp"
android:layout_gravity="center"
app:number="99+"
app:radius="10dp"
app:textSize="12sp" />
</LinearLayout>
</android.support.constraint.ConstraintLayout>
3.完整自定义控件代码如下
public class QQMessageBubbleView extends View {
/**
* 画圆的Paint画笔
*/
Paint mPaint;
/**
* 字体画笔
*/
Paint textPaint;
/**
* 消失图片的画笔
*/
Paint disappearPaint;
/**
* 贝塞尔曲线
*/
Path mPath;
/**
* 字体垂直偏移量
*/
float textMove;
/**
* 中心圆半径,初始位的圆
*/
float centerRadius;
/**
* 拖拽圆的半径
*/
float dragRadius;
/**
* 手指移动坐标x
*/
int dragCircleX;
/**
* 手指移动坐标y
*/
int dragCircleY;
/**
* 控件中心点坐标x
*/
int centerCircleX;
/**
* 控件中心点坐标y
*/
int centerCircleY;
/**
* 两个圆的距离
*/
float distance;
int mWidth;
int mHeight;
/**
* 显示的文本内容
*/
String mNumber;
/**
* 最大可拖拽距离
*/
int maxDragLength;
/**
* 用户设定的字体大小
*/
float textSize;
/**
* 用户设定的字体颜色
*/
int textColor;
/**
* 用户设定的圆圈颜色
*/
int circleColor;
/**
* 图片资源id
*/
int[] disappearPic;
/**
* 图片位图
*/
Bitmap[] disappearBitmap;
Rect bitmapRect;
/**
* 消失动画播放图片的index
*/
int bitmapIndex;
/**
* 判断是否正在播放消失动画,防止死循环重复绘制
*/
boolean startDisappear;
ActionListener actionListener;
/**
* 当前状态
*/
int curState;
/**
* 原位,初始位
*/
public static int STATE_NORMAL = 0;
/**
* 消失
*/
public static int STATE_DISAPPEAR = 1;
/**
* 拖拽
*/
public static int STATE_DRAGING = 2;
/**
* 移动(无粘连效果)
*/
public static int STATE_MOVE = 3;
public QQMessageBubbleView(Context context) {
super(context);
}
public QQMessageBubbleView(Context context, AttributeSet attrs) {
super(context, attrs);
TypedArray ta = context.obtainStyledAttributes(attrs, R.styleable.MessageBubble);
circleColor = ta.getColor(R.styleable.MessageBubble_circleColor, Color.RED);
textColor = ta.getColor(R.styleable.MessageBubble_textColor, Color.WHITE);
textSize = ta.getDimension(R.styleable.MessageBubble_textSize, 30);
centerRadius = ta.getDimension(R.styleable.MessageBubble_radius, 30);
mNumber = ta.getString(R.styleable.MessageBubble_number);
if (mNumber == null) {//防止xml中未给mNumber赋值造成预览时报错
mNumber = "";
}
ta.recycle();
}
public QQMessageBubbleView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
initView();
}
private void initView() {
//画圆的Paint
mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);//去除抗锯齿
mPaint.setColor(circleColor);
//画数字的Paint
textPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
textPaint.setColor(textColor);
textPaint.setTextAlign(Paint.Align.CENTER);//文字书写方向
textPaint.setTextSize(textSize);
//画消失图片的Paint
disappearPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
disappearPaint.setFilterBitmap(false);//对位图进行滤波处理
startDisappear = false;
Paint.FontMetrics textFontMetrics = textPaint.getFontMetrics();
textMove = -textFontMetrics.ascent - (-textFontMetrics.ascent + textFontMetrics.descent) / 2;
// drawText从baseline开始
// ,baseline的值为0,baseline的上面为负值,baseline的下面为正值,即这里ascent为负值,descent为正值,比如ascent为-20
// ,descent为5,那需要移动的距离就是20 - (20 + 5)/ 2
mPath = new Path();
if (centerRadius <= 2) {
//如果不是第一次创建,上次的拖动删除会因为中心圆半径随着拖放变为零
centerRadius = dragRadius;
} else {
dragRadius = centerRadius;
}
maxDragLength = (int) (4 * dragRadius);
//设定圆的初始位置为View正中心
centerCircleX = getWidth() / 2;
centerCircleY = getHeight() / 2;
//防止被拖动圆因上一次拖动而未回到原位
dragCircleX = centerCircleX;
dragCircleY = centerCircleY;
//消失气泡图片
if (disappearPic == null) {
disappearPic = new int[]{R.drawable.explosion_one, R.drawable.explosion_two, R.drawable.explosion_three
, R.drawable.explosion_four, R.drawable.explosion_five};
}
disappearBitmap = new Bitmap[disappearPic.length];
for (int i = 0; i < disappearPic.length; i++) {
disappearBitmap[i] = BitmapFactory.decodeResource(getResources(), disappearPic[i]);
}
curState = STATE_NORMAL;
}
@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);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
//父布局禁用拦截事件功能,禁用down事件
getParent().requestDisallowInterceptTouchEvent(true);
if (curState != STATE_DISAPPEAR) {
//计算点击位置与气泡的距离,内部应用勾股定理a²=b²+c²
distance = (float) Math.hypot(centerCircleX - event.getX(), centerCircleY - event.getY());
if (distance < centerRadius + 10) {
curState = STATE_DRAGING;
} else {
//距离过大不触发拖拽
curState = STATE_NORMAL;
}
}
break;
case MotionEvent.ACTION_MOVE:
dragCircleX = (int) event.getX();
dragCircleY = (int) event.getY();
if (curState == STATE_DRAGING) {
//拖拽状态下计算拖拽距离,超出後不再計算
distance = (float) Math.hypot(centerCircleX - event.getX(), centerCircleY - event.getY());
if (distance <= maxDragLength - maxDragLength / 7) {
centerRadius = dragRadius - distance / 4;
if (actionListener != null) {
actionListener.onDrag();
}
} else {
centerRadius = 0;
curState = STATE_MOVE;
}
} else if (curState == STATE_MOVE) {
//超出最大拖拽距离,则中间的圆消失
if (actionListener != null) {
actionListener.onMove();
}
}
invalidate();
break;
case MotionEvent.ACTION_UP:
getParent().requestDisallowInterceptTouchEvent(false);
//当正在拖动时,抬起手指才会做响应的处理
if (curState == STATE_DRAGING || curState == STATE_MOVE) {
distance = (float) Math.hypot(centerCircleX - event.getX(), centerCircleY - event.getY());
if (distance > maxDragLength) {
//如果拖拽距离大于最大可拖拽距离,则消失
curState = STATE_DISAPPEAR;
startDisappear = true;
disappearAnim();
} else {
//小于可拖拽距离,则复原气泡位置
restoreAnim();
}
invalidate();
}
break;
}
return true;
}
private int roundRectRadius;//圆角矩形半径
private int textContentLength;//总字体宽度
@Override
protected void onDraw(Canvas canvas) {
textContentLength = (int) textPaint.measureText(mNumber, 0, mNumber.length()) + 20;//计算内容的长度,想要圆角矩形越宽将20该为更大值
//原位
if (curState == STATE_NORMAL) {
roundRectRadius = (int) Math.max(centerRadius * 2, textContentLength); //圆角矩形宽度
if (roundRectRadius != textContentLength) {
//画原位圆
canvas.drawCircle(centerCircleX, centerCircleY, centerRadius, mPaint);
} else {
//画圆角矩形
canvas.drawRoundRect(new RectF(centerCircleX - roundRectRadius / 2, centerCircleY - centerRadius,
centerCircleX + roundRectRadius / 2, centerCircleY + centerRadius), centerRadius, centerRadius, mPaint);
}
//画数字(要在画完贝塞尔曲线之后绘制,不然会被挡住)
canvas.drawText(mNumber, centerCircleX, centerCircleY + textMove, textPaint);
}
//如果开始拖拽,则画dragCircle
if (curState == STATE_DRAGING) {
//画初始时停留的圆
canvas.drawCircle(centerCircleX, centerCircleY, centerRadius, mPaint);
//画被拖拽的圆
roundRectRadius = (int) Math.max(dragRadius * 2, textContentLength);
if (roundRectRadius != textContentLength) {
canvas.drawCircle(dragCircleX, dragCircleY, dragRadius, mPaint);
} else {
canvas.drawRoundRect(new RectF(dragCircleX - roundRectRadius / 2, dragCircleY - dragRadius,
dragCircleX + roundRectRadius / 2, dragCircleY + dragRadius), dragRadius, dragRadius, mPaint);
}
drawBezier(canvas);//贝塞尔黏连效果
canvas.drawText(mNumber, dragCircleX, dragCircleY + textMove, textPaint);
}
// 移动(无粘连效果)
if (curState == STATE_MOVE) {
roundRectRadius = (int) Math.max(dragRadius * 2, textContentLength);
if (roundRectRadius != textContentLength) {
canvas.drawCircle(dragCircleX, dragCircleY, dragRadius, mPaint);
} else {
canvas.drawRoundRect(new RectF(dragCircleX - roundRectRadius / 2, dragCircleY - dragRadius,
dragCircleX + roundRectRadius / 2, dragCircleY + dragRadius), dragRadius, dragRadius, mPaint);
}
canvas.drawText(mNumber, dragCircleX, dragCircleY + textMove, textPaint);
}
//消失,通过属性动画动态设置bitmap实现动画效果
if (curState == STATE_DISAPPEAR && startDisappear) {
if (disappearBitmap != null) {
canvas.drawBitmap(disappearBitmap[bitmapIndex], null, bitmapRect, disappearPaint);
}
}
}
/**
* 气泡消失动画,动画采用类似帧动画的原理
*/
private void disappearAnim() {
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();
}
/**
* 气泡复原动画
*/
private void restoreAnim() {
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.571429f;
return (float) (Math.pow(2, -4 * input) * Math.sin((input - f / 4) * (2 * Math.PI) / f) + 1);
}
});
//系统插值器,运动到终点后,冲过终点后再回弹
// valueAnimator.setInterpolator(new OvershootInterpolator(5));
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) {
//复原了
centerRadius = dragRadius;
curState = STATE_NORMAL;
if (actionListener != null) {
actionListener.onRestore();
}
}
});
valueAnimator.start();
}
/**
* 绘制贝塞尔曲线
*
* @param canvas canvas
*/
private void drawBezier(Canvas canvas) {
float controlX = (centerCircleX + dragCircleX) / 2;//贝塞尔曲线控制点X坐标
float controlY = (dragCircleY + centerCircleY) / 2;//贝塞尔曲线控制点Y坐标
//计算曲线的起点终点
distance = (float) Math.hypot(centerCircleX - dragCircleX, centerCircleY - dragCircleY);
float sin = (centerCircleY - dragCircleY) / distance;
float cos = (centerCircleX - dragCircleX) / distance;
//计算第一条贝塞尔曲线起点终点坐标
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);
}
/**
* 重置
*/
public void resetBezierView() {
initView();
invalidate();
}
/**
* 设置显示的消息数量(超过99需要自己定义为"99+")
*
* @param number 消息的数量
*/
public void setNumber(String number) {
mNumber = number;
invalidate();
}
/**
* 设置消失动画
*
* @param disappearPic
*/
public void setDisappearPic(int[] disappearPic) {
if (disappearPic != null) {
this.disappearPic = disappearPic;
}
}
/**
* 自定义监听,对外接口
*/
public interface ActionListener {
/**
* 被拖动时
*/
void onDrag();
/**
* 消失后
*/
void onDisappear();
/**
* 拖动距离不足,气泡回到原位后
*/
void onRestore();
/**
* 拖动时超出了最大粘连距离,气泡单独移动时
*/
void onMove();
}
public void setOnActionListener(ActionListener actionListener) {
this.actionListener = actionListener;
}
/**
* PointF动画估值器(复原时的振动动画)
*/
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 new PointF(x, y);
}
}
}
控件中贝塞尔曲线的辅助图
贝塞尔辅助图.png
其中主要是对绘制onDraw()内做了修改,将圆替换成圆角矩形。加了判断当当前内容宽度小于设置的直径时画圆显示,当内容宽度大于等于直径时显示圆角矩形。
@Override
protected void onDraw(Canvas canvas) {
textContentLength = (int) textPaint.measureText(mNumber, 0, mNumber.length())+5;//计算内容的长度,想要圆角矩形越宽将5该为更大值
//原位
if (curState == STATE_NORMAL) {
roundRectRadius = (int) Math.max(centerRadius * 2, textContentLength); //圆角矩形宽度
if (roundRectRadius != textContentLength) {
//画原位圆
canvas.drawCircle(centerCircleX, centerCircleY, centerRadius, mPaint);
} else {
//画圆角矩形
canvas.drawRoundRect(new RectF(centerCircleX - roundRectRadius / 2, centerCircleY - centerRadius,
centerCircleX + roundRectRadius / 2, centerCircleY + centerRadius), centerRadius, centerRadius, mPaint);
}
//画数字(要在画完贝塞尔曲线之后绘制,不然会被挡住)
canvas.drawText(mNumber, centerCircleX, centerCircleY + textMove, textPaint);
}
//如果开始拖拽,则画dragCircle
if (curState == STATE_DRAGING) {
理论同上 。。。。。。
}
// 移动(无粘连效果)
if (curState == STATE_MOVE) {
理论同上 。。。。。。
}
//消失
if (curState == STATE_DISAPPEAR && startDisappear) {
if (disappearBitmap != null) {
canvas.drawBitmap(disappearBitmap[bitmapIndex], null, bitmapRect, disappearPaint);
}
}
}
另外我添加了两个变量
private int roundRectRadius;//圆角矩形半径
private int textContentLength;//总字体宽度
网友评论