Android自定义View实现QQ气泡效果
首先我们来看一下最终的效果:
根据我们上边拆分出来的公式,我们分别看看每一个效果需要如何去实现:
红色圆:canvas.drawCircle
消息数字:canvas.drawText
拖拽粘性效果:canvas.drawPath、 (两条二阶)贝塞尔曲线 (精髓所在)
回弹效果:属性动画
跟随移动:OnTouchEvent处理MotionEvent.ACTION_MOVE事件
爆炸效果:属性动画
View自定义属性
为了提高自定义View的灵活性,我们需要提供几种自定义属性给外部来设置,有如下属性:
气泡半径:bubble_radius
气泡颜色:bubble_color
气泡消息数字:bubble_text
气泡消息数字字体大小:bubble_textSize
气泡消息数字颜色:bubble_textColor
属性定义
在res -> values下添加如下attrs.xml文件:
<?xml version="1.0" encoding="utf-8"?>
<resources><declare-styleable name="DragBubbleView"><attr name="bubble_radius" format="dimension"/><attr name="bubble_color" format="color"/><attr name="bubble_text" format="string"/><attr name="bubble_textSize" format="dimension"/><attr name="bubble_textColor" format="color"/></declare-styleable>
</resources>
在初始化方法中获取这些属性即可,下面完整代码会列出了。
初始化两个圆的圆心坐标
我们需要在View的size确定后初始化圆心坐标,所以需要在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);}}
定义气泡状态
气泡总共可以分为四个状态:
静止
相连(粘性拖拽、回弹状态)
分离(跟随触摸运动状态)
消失(爆炸状态)
分别对应下面四种状态
静止状态,一个气泡 + 消息数
连接状态,一个气泡 + 消息数 + 贝塞尔曲线 + 原本位置上的气泡(变化的)
分离状态,一个气泡 + 消息数
消失状态,爆炸效果
这些效果主要是在 onDraw 和 onTouchEvent 实现的,在绘制的过程涉及到了贝塞尔曲线,下面分析下实现的思路和一些细节:
// 一定要分状态 文字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);}
设置触摸事件
当手指按下时,开始拖拽,气泡状态变为连接状态;
当手指移动时,处理粘性拖拽(连接状态)和跟随(分离状态);
当手指松开是,处理爆炸效果(消失状态)和回弹(回到静止状态);
@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;}
自定义控件完整类的代码如下:
package com.xifei.mydragbubbleview;import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.animation.PointFEvaluator;
import android.animation.ValueAnimator;
import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Path;
import android.graphics.PointF;
import android.graphics.Rect;
import android.os.Build;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.View;
import android.view.animation.LinearInterpolator;
import android.view.animation.OvershootInterpolator;import 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, 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();//文本画笔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://download.csdn.net/download/xifei66/13124488
Android自定义View实现QQ气泡效果相关推荐
- android的动态tab,Android自定义view仿QQ的Tab按钮动画效果(示例代码)
话不多说 先上效果图 实现其实很简单,先用两张图 一张是背景的图,一张是笑脸的图片,笑脸的图片是白色,可能看不出来.实现思路:主要是再触摸view的时候同时移动这两个图片,但是移动的距离不一样,造成的 ...
- android 立体 流量球,Android自定义View——实现水波纹效果类似剩余流量球
Android自定义View--实现水波纹效果类似剩余流量球 三个点 pre ber block span 初始化 move 理解最近突然手痒就想搞个贝塞尔曲线做个水波纹效 ...
- oracle number型步数,Android自定义View仿QQ计步器
自定义计步器 Android自定义View是Android开发中比较重要的一项,也是很多开发者比较怕的一个东西.其实只要认真去学习,自定义View其实没有那么可怕:相反的,我们还能从自定义View中找 ...
- android自定义计步器形状,Android自定义View仿QQ运动步数效果
本文实例为大家分享了Android QQ运动步数的具体代码,供大家参考,具体内容如下 今天我们实现下面这样的效果: 首先自定义属性: 自定义View代码如下: /** * Created by Mic ...
- android 高仿ios开关,Android自定义view仿IOS开关效果
本文主要讲解如何在 Android 下实现高仿 iOS 的开关按钮,并非是在 Android 自带的 ToggleButton 上修改,而是使用 API 提供的 onDraw.onMeasure.Ca ...
- Android 自定义View实现QQ运动积分抽奖转盘
因为偶尔关注QQ运动, 看到QQ运动的积分抽奖界面比较有意思,所以就尝试用自定义View实现了下,原本想通过开发者选项查看下界面的一些信息,后来发现积分抽奖界面是在WebView中展示的,应该是在H5 ...
- android圆形波纹按钮,android自定义View——圆形波纹扫描效果
蓝牙项目,考虑到后面可能会用到这个扫描的效果,所以参照大神写好的控件,增加了自己需要使用的接口.也顺便巩固一下自定义view中各种零碎的知识点. 需要的效果图 先放一个效果图,点击中心图片开始动画,再 ...
- android 环绕布局,Android自定义View实现圆形环绕效果
之前项目中需要实现一个四周环绕中心圆形头像的效果,感觉还是自定义比较方便,于是就自己封装了一个控件去实现.先贴张图显示最终效果. 首先自定义一个View继承自LinearLayout,通过动态添加ch ...
- Android好评功能,Android自定义View实现五星好评效果
本文实例为大家分享了Android实现五星好评效果的具体代码,供大家参考,具体内容如下 这个效果想必大家都非常熟悉,那么Android如何自定义实现这种效果呢? 首先自定义属性: 下面看看具体实现: ...
最新文章
- python dump函数_python中实现php的var_dump函数功能
- 三维形状和外观重建一次全搞定: DeepSurfels在线融合实现逼真重建
- 【Python】Flask 框架安装虚拟环境报错—处理中......
- Java中判断两个Date时间段是否有交集的方法
- bash shell函数中返回任意值的四种方法
- SQLSERVER索引汇总
- c mysql日期时间格式_[单选] 妊娠期血液成分发生改变,下述哪项是正确的()...
- 【读书笔记】 多线程程序常见bug
- Ubuntu18.04-albert编译安装记录
- 华为HG255D救砖小总结1----概述及相关硬件准备
- Pycharm下了汉化包之后切换回英文界面
- 大华linux密码,大华wifi摄像头的初始化和读取视频流
- 材料成型计算机控制,材料成型及控制工程学什么 毕业后能干什么
- 存储器基本概念及分类介绍
- Linux上配置nginx访问图片报404解决方案
- 追风筝的人 第十二章
- nginx、php-fpm以及mysql运行在各个用户下的配置
- BZOJ3730 震波+BZOJ4372 烁烁的游戏(动态点分治)
- 联发科MT5597 4K数字电视芯片处理器介绍
- 尚硅谷JavaWeb教程