您的位置:首页>科学 >Android自定义View实现QQ气泡效果

Android自定义View实现QQ气泡效果

2023-09-29 06:58

首先看一下最终效果:

根据我们上面分解的公式,我们来看看每个效果需要如何实现:

红圈:canvas.drawCircle

消息号:canvas.drawText

拖粘效果:canvas.drawPath、(两条二阶)贝塞尔曲线(精髓)

反弹效果:属性动画

跟随移动:OnTouchEvent 处理 MotionEvent.ACTION_MOVE 事件

爆炸效果:属性动画

查看自定义属性

为了提高自定义View的灵活性,我们需要提供几个自定义属性供外部设置,包括以下属性:

气泡半径: bubble_radius

气泡颜色:bubble_color

气泡消息编号: bubble_text

气泡消息号码字体大小:bubble_textSize

气泡消息号码颜色:bubble_textColor

属性定义

在 res -> 值下添加以下内容​​attrs.xml文件:



只需在初始化方法中获取这些属性即可。完整的代码如下所示。

初始化两个圆的中心坐标

View的大小确定后我们需要初始化圆心坐标,所以需要在onSizeChanged中初始化。

 @Overrideprotected void onSizeChanged(int w, int h, int oldw, int oldh) {super.onSizeChanged(w, h, oldw, oldh); // 设置两个圆心的初始位置为View的中心,也可以通过自定义属性从外部自由设置该属性 //初始化不可移动气泡的圆心 if (mBubMoveableCenter == null){ mBubMoveableCenter = new PointF(w / 2f, h / 2f);} else {mBubMoveableCenter.set(w / 2f , h / 2f);}//初始化可移动气泡中心 if (mBubStillCenter == null){mBubStillCenter = new PointF(w / 2f, h / 2f);} else {mBubStillCenter.set(w / 2f, h / 2f);}}

定义气泡状态

气泡可分为四种状态:

依然

已连接(粘滞拖动、反弹状态)

分离(跟随触摸运动状态)

消失(爆炸状态)

对应以下四种状态

静止状态,一个气泡+消息数

连接状态,1个气泡+消息条数+贝塞尔曲线+原位置气泡(变化)

分离状态,一个气泡+消息数

消失状态、爆炸效果

这些效果主要在onDraw和onTouchEvent中实现。绘制过程涉及贝塞尔曲线。下面分析一下实现思路和一些细节:

我们需要做的是 抽签 AB , CD 这是一条二阶贝塞尔曲线。而ABCD这个不规则多边形就是气泡” ,根 根据贝塞尔曲线的定义,可以发现O点,P 点是已知点, G 点为 AB , CD这个2 贝塞尔曲线的控制点, A B C D 分别是 AB CD贝塞尔曲线的数据点。因此,求 A , B , CD G5 可以用一个点的坐标来绘制 这个 2 贝塞尔曲线! 关于坐标的解法,我们一一来说:? P 的点 您可以通过添加x,y并除以2来计算 ABCD 点,根据高中数学相关知识: O y 坐标 O x 坐标 sin POE = PE / OP cos POE = OE / OP A 坐标: x = O x 坐标 -罪恶 POE * 固定圆半径 y = O y 坐标 - cos POE * 固定圆半径 B 坐标: x = P x 坐标 -罪恶 POE * 动圆半径 y = P y 坐标 - cos POE * 动圆半径 C 坐标:x = POE * 移动圆半径 y = P y 坐标 + cos POE * 动圆半径 D 坐标: x = O x 坐标 +罪恶 POE * 固定圆半径 y = O y 坐标 + cos POE * 固定圆半径
 // 必须分隔状态文本 if (mBubbleState == BUBBLE_STATE_CONNECT) {canvas.drawCircle(mBubStillCenter.x, mBubStillCenter.y, mBubStillRadius, mBubblePaint);// cos +float cosThrta = (mBubMoveableCenter.x - mBubStillCenter.x ) / mDist;// sin +float sinTheta = (mBubMoveableCenter.y - mBubStillCenter.y) / mDist;// 浮动 iBubStillStartX = mBubStillCenter.x - mBubStillRadius * sinTheta;float iBubStillStartY = mBubStillCenter.y + mBubStillRadius * cosThrta;// Bfloat iBubMoveable结束X = mBubMoveableCenter.x - mBubbleRadius * sinTheta;float iBubMoveableEndY = mBubMoveableCenter.y + mBubMoveableRadius * cosThrta;//Cfloat iBubMoveableStartX = mBubMoveableCenter.x + mBubMoveableRadius * sinTheta;float iBubMoveableStartY = m BubMoveableCenter.y - mBub MoveableRadius * cosThrta;//Dfloat iBubStillEndX = mBubStillCenter.x + mBubStillRadius * sinTheta;float iBubStillEndY = mBubStillCenter.y - mBubStillRadius * cosThrta;//G计算控制点坐标,两个圆心的中点 int iAnchorX = (int) ((mBubStillCenter.x + mBubMoveableCenter.x) / 2);int iAnchorY = (int) ((mBubStillCenter.y + mBubMoveableCenter.y) / 2);mBezierPath.reset();//移动到B点//绘制上半部分arc mBezierPath.moveTo(iBubStillStartX, iBubStillStartY);mBezierPath.quadTo(iAnchorX, iAnchorY, iBubMoveableEndX, iBubMoveableEndY);//绘制下半圆弧 mBezierPath.lineTo(iBubMoveableStartX, iBubMoveableStartY);mBezierPath.quadTo(i AnchorX, iAnch或者Y,iBubStillEndX, iBubStillEndY); mBezierPath.close();canvas.drawPath(mBezierPath, mBubblePaint);}

设置触摸事件

手指按下时,开始拖动,气泡状态变为连接状态;
手指移动时,处理粘拖(连接状态)和跟随(分离状态);
手指松开时可以处理爆炸效果(消失状态)和反弹(返回静止状态);

 @Overridepublic boolean onTouchEvent(MotionEvent event) {switch (event.getAction()) {case MotionEvent.ACTION_DOWN: {// 非消失状态 if (mBubbleState != BUBBLE_STATE_DISMISS) {// 计算两个圆心之间的距离,当前触摸位置为可移动圆的中心 mDist = (float) Math.hypot(event.getX() - mBubStillCenter.x, event.getY() - mBubStillCenter.y); // 为了方便拖动,增大拖动拖动识别范围 if (mDist < mBubbleRadius) {//更改为连接状态 mBubbleState = BUBBLE_STATE_CONNECT;} else {//重置为默认状态 mBubbleState = BUBBLE_STATE_DEFAUL;}}break;}case MotionEvent.ACTION_MOVE: {// 非稳态 if (mBubbleState != BUBBLE_STATE_DEFAUL){// 计算两个圆心之间的距离。当前触摸位置为可动圆中心 mDist = (float) Math.hypot(event.getX() - mBubStillCenter.x, event.getY() - mBubStillCenter.y);//修改可动圆中心到触摸点 mBubMoveableCenter.x = event.getX();mBubMoveableCenter.y = event.getY();//连接状态 if (mBubbleState == BUBBLE_STATE_CONNECT ){if (mDist < mMaxDist){//当拖动距离为在指定范围内,调整固定圆半径 mBubStillRadius = mBubbleRadius - mDist / 8;} else {//超出指定范围,分离状态 mBubbleState = BUBBLE_STATE_APART;}} //重画 invalidate();}break;}case中号otionEvent.ACTION_UP: {// 连接状态下释放 if (mBubbleState == BUBBLE_STATE_CONNECT) {// 反弹效果 startBubbleRestAnim();} else if (mBubbleState == BUBBLE_STATE_APART){// 分离状态下释放 if (mDist < 2 * mBubbleRadius){// 距离近时,反弹不爆炸 startBubbleRestAnim();} else {// 爆炸效果 startBubbleBurstAnim();}}break;}}return true;}

自定义的代码完整的控件类如下:

package com.xifei.mydragbubbleview;导入android.animation.Animator;
导入 android.animation.AnimatorListenerAdapter;
导入 android.animation.PointFEvaluator;
导入 android.animation.ValueAnimator;
导入 android.content.Context;
导入 android.content.res.TypedArray;
导入 android.graphics.Bitmap;
导入 android.graphics.BitmapFactory;
导入 android.graphics.Canvas;
导入 android.graphics.Color;
导入 android.graphics.Paint;
导入 android.graphics.Path;
导入 android.graphics.PointF;
导入 android.graphics.Rect;
导入 www.webguidecorpuschristi.com;
导入 android.util.AttributeSet;
导入 android.view.MotionEvent;导入 android.view.View;
导入 android.view.animation.LinearInterpolator;
导入 android.view.animation.OvershootInterpolator;导入 androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;public class DragBubbleView extends View {private final int BUBBLE_STATE_DEFAUL = 0;//是否在执行气泡爆炸动画private boolean mIsBurstAnimStart = false;//气泡相连private final int BUBBLE_STATE_CONNECT = 1;//气泡分离private final int BUBBLE_STATE_APART = 2;//气泡消失private final int BUBBLE_STATE_DISMISS = 3;private int mBubbleState = BUBBLE_STATE_DEFAUL;//文字private Paint mTextPaint;//气泡画笔private Paint mBubblePaint;//气泡半径private float mBubbleRadius;//气泡消息文字private String mTextStr;//气泡消息文字颜色private int mTextColor;//气泡消息文字大小private float mTextSize;//气泡颜色private int mBubbleColor;//不动气泡的圆心private PointF mBubStillCenter;//可动气泡的圆心private PointF mBubMoveableCenter;//文本绘制区域private Rect mTextRect;//两气泡圆心的距离private float mDist;//可动气泡的半径private float mBubMoveableRadius;//贝塞尔曲线pathprivate Path mBezierPath;//气泡相连状态最大圆心的距离private float mMaxDist;//不动气泡的半径private float mBubStillRadius;//气泡爆炸的图片id数组private int[] mBurstDrawablesArray = {R.drawable.burst_1, R.drawable.burst_2, R.drawable.burst_3, R.drawable.burst_4, R.drawable.burst_5};//气泡爆炸的bitmap数组private Bitmap[] mBurstBitmapsArray;//爆炸绘制区域private Rect mBurstRect;//当前气泡爆炸图片indexprivate int mCurDrawableIndex;public DragBubbleView(Context context, @Nullable AttributeSet attrs) {super(context, attrs);init(context, attrs);}public DragBubbleView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {super(context, attrs, defStyleAttr);}private void init(Context context, AttributeSet attrs) {// 获取自定义属性数组TypedArray array = context.obtainStyledAttributes(attrs, R.styleable.DragBubbleView);mBubbleRadius = array.getDimension(R.styleable.DragBubbleView_bubble_radius, mBubbleRadius);mBubbleColor = array.getColor(R.styleable.DragBubbleView_bubble_color, www.webguidecorpuschristi.com);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();//文本画笔mTextPaint = new Paint(Paint.ANTI_ALIAS_FLAG);mTextPaint.setColor(Color.WHITE);//        textSizemTextPaint.setTextSize(mTextSize);mTextPaint.setColor(mTextColor);//抗锯齿  气泡画笔mBubblePaint = new Paint(Paint.ANTI_ALIAS_FLAG);mBubblePaint.setColor(mBubbleColor);mBubblePaint.setStyle(Paint.Style.FILL);mTextRect = new Rect();mBubMoveableRadius = mBubbleRadius;mBubStillRadius = mBubbleRadius;mBezierPath = new Path();mMaxDist = 8 * mBubbleRadius;mBurstRect = new Rect();mBurstBitmapsArray = new Bitmap[mBurstDrawablesArray.length];for (int i = 0; i < mBurstDrawablesArray.length; i++) {//将气泡爆炸的drawable转为bitmapBitmap bitmap = BitmapFactory.decodeResource(getResources(), mBurstDrawablesArray[i]);mBurstBitmapsArray[i] = bitmap;}}@Overrideprotected void onSizeChanged(int w, int h, int oldw, int oldh) {super.onSizeChanged(w, h, oldw, oldh);// 设置两个圆心初始位置在View中心位置,也可以通过自定义属性将此属性由外部自由设置//初始化不动气泡的圆心if (mBubMoveableCenter == null){mBubMoveableCenter = new PointF(w / 2f, h / 2f);} else {mBubMoveableCenter.set(w / 2f, h / 2f);}//初始化可动气泡的圆心if (mBubStillCenter == null){mBubStillCenter = new PointF(w / 2f, h / 2f);} else {mBubStillCenter.set(w / 2f, h / 2f);}}@Overrideprotected void onDraw(Canvas canvas) {//        一定要分状态 文字if (mBubbleState == BUBBLE_STATE_CONNECT) {canvas.drawCircle(mBubStillCenter.x, mBubStillCenter.y, mBubStillRadius, mBubblePaint);// cos    +float cosThrta = (mBubMoveableCenter.x - mBubStillCenter.x) / mDist;//  sin   +float sinTheta = (mBubMoveableCenter.y - mBubStillCenter.y) / mDist;// Afloat iBubStillStartX = mBubStillCenter.x - mBubStillRadius * sinTheta;float iBubStillStartY = mBubStillCenter.y + mBubStillRadius * cosThrta;// Bfloat iBubMoveableEndX = mBubMoveableCenter.x - mBubbleRadius * sinTheta;float iBubMoveableEndY = mBubMoveableCenter.y + mBubMoveableRadius * cosThrta;//Cfloat iBubMoveableStartX = mBubMoveableCenter.x + mBubMoveableRadius * sinTheta;float iBubMoveableStartY = mBubMoveableCenter.y - mBubMoveableRadius * cosThrta;//Dfloat iBubStillEndX = mBubStillCenter.x + mBubStillRadius * sinTheta;float iBubStillEndY = mBubStillCenter.y - mBubStillRadius * cosThrta;//  G计算控制点坐标,两个圆心的中点int iAnchorX = (int) ((mBubStillCenter.x + mBubMoveableCenter.x) / 2);int iAnchorY = (int) ((mBubStillCenter.y + mBubMoveableCenter.y) / 2);mBezierPath.reset();//  移动到B点// 画上半弧mBezierPath.moveTo(iBubStillStartX, iBubStillStartY);mBezierPath.quadTo(iAnchorX, iAnchorY, iBubMoveableEndX, iBubMoveableEndY);// 画下半弧mBezierPath.lineTo(iBubMoveableStartX, iBubMoveableStartY);mBezierPath.quadTo(iAnchorX, iAnchorY, iBubStillEndX, iBubStillEndY);mBezierPath.close();canvas.drawPath(mBezierPath, mBubblePaint);}if (mBubbleState != BUBBLE_STATE_DISMISS) {// 绘制一个大小不变的气泡(可动气泡)canvas.drawCircle(mBubMoveableCenter.x, mBubMoveableCenter.y, mBubbleRadius, mBubblePaint);// 测量消息数的文本,并将测量数据保存在mTextRect中mTextPaint.getTextBounds(mTextStr, 0, mTextStr.length(), mTextRect);// 绘制文本在可动气泡的中心(参数位置是绘制区域的左下角的坐标)canvas.drawText(mTextStr, mBubMoveableCenter.x - mTextRect.width() / 2f,mBubMoveableCenter.y + mTextRect.height() / 2f, mTextPaint);} else if (mCurDrawableIndex < mBurstBitmapsArray.length) {//爆炸状态//onDraw方法中mBurstRect.set((int) (mBubMoveableCenter.x - mBubMoveableRadius),(int) (mBubMoveableCenter.y - mBubMoveableRadius),(int) (mBubMoveableCenter.x + mBubMoveableRadius),(int) (mBubMoveableCenter.y + mBubMoveableRadius));canvas.drawBitmap(mBurstBitmapsArray[mCurDrawableIndex], null,mBurstRect, mBubblePaint);}//  AmTextPaint.getTextBounds(mTextStr,0, mTextStr.length(), mTextRect);canvas.drawText(mTextStr,mBubMoveableCenter.x - mTextRect.width() / 2,mBubMoveableCenter.y + mTextRect.height() / 2,mTextPaint);}private void startBubbleRestAnim() {ValueAnimator anim = ValueAnimator.ofObject(new PointFEvaluator(),new PointF(mBubMoveableCenter.x, mBubMoveableCenter.y),new PointF(mBubStillCenter.x, mBubStillCenter.y));anim.setDuration(400);//  反向执行  加速回来anim.setInterpolator(new OvershootInterpolator(5f));anim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {@Overridepublic void onAnimationUpdate(ValueAnimator animation) {mBubMoveableCenter = (PointF) animation.getAnimatedValue();invalidate();}});anim.start();}private void startBubbleBurstAnim() {//气泡改为消失状态mBubbleState = BUBBLE_STATE_DISMISS;mIsBurstAnimStart = true;//做一个int型属性动画,从0~mBurstDrawablesArray.length结束ValueAnimator anim = ValueAnimator.ofInt(0, mBurstDrawablesArray.length);anim.setInterpolator(new LinearInterpolator());anim.setDuration(500);anim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {@Overridepublic void onAnimationUpdate(ValueAnimator animation) {//设置当前绘制的爆炸图片indexmCurDrawableIndex = (int) animation.getAnimatedValue();invalidate();}});anim.addListener(new AnimatorListenerAdapter() {@Overridepublic void onAnimationEnd(Animator animation) {//修改动画执行标志mIsBurstAnimStart = false;}});anim.start();}@Overridepublic boolean onTouchEvent(MotionEvent event) {switch (event.getAction()) {case MotionEvent.ACTION_DOWN: {// 非消失状态if (mBubbleState != BUBBLE_STATE_DISMISS) {// 计算两个圆心的距离,当前触摸位置即为可动圆的圆心mDist = (float) Math.hypot(event.getX() - mBubStillCenter.x, event.getY() - mBubStillCenter.y);// 为了方便进行拖拽,增大拖拽识别范围if (mDist < mBubbleRadius) {// 更改为连接状态mBubbleState = BUBBLE_STATE_CONNECT;} else {// 重置为默认状态mBubbleState = BUBBLE_STATE_DEFAUL;}}break;}case MotionEvent.ACTION_MOVE: {// 非静止状态if (mBubbleState != BUBBLE_STATE_DEFAUL){// 计算两个圆心的距离,当前触摸位置即为可动圆的圆心mDist = (float) Math.hypot(event.getX() - mBubStillCenter.x, event.getY() - mBubStillCenter.y);//修改可动圆的圆心为触摸点mBubMoveableCenter.x = event.getX();mBubMoveableCenter.y = event.getY();// 连接状态if (mBubbleState == BUBBLE_STATE_CONNECT){if (mDist < mMaxDist){//当拖拽距离在指定范围内,调整不动圆半径mBubStillRadius = 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){// 距离较近时,回弹,不爆炸startBubbleRestAnim();} else {// 爆炸效果startBubbleBurstAnim();}}break;}}return true;}public void reset(){// 重置状态mBubbleState = BUBBLE_STATE_DEFAUL;// 重置可动气泡圆心位置mBubMoveableCenter = new PointF(mBubStillCenter.x, mBubStillCenter.y);// 重绘invalidate();}
}

另外附上源码,需要的小伙伴可以去下载,其实这个自定义控件练手很适合,一定要自己手写下,理解原理。

https://www.webguidecorpuschristi.com/download/xifei66/13124488