彻底搞懂自定义View,看该系列就够了
前言
作为自定义View的完结篇,该篇自然就是结合一个经典例子来讲解,实现QQ消息的气泡控件:
它是可以拖拽的,像这种效果:
当然,要彻底写好这个控件,还是得搞懂自定义View的相关知识点,对整个自定义View的知识体系如果觉得还是不熟,可以回顾我的这个系列:
从上到下按照顺序看就可以,能帮助大家快速掌握自定义View的窍门。接下来就直接开讲如何去自定义这个QQ消息气泡。
提示:以下是本篇文章正文内容
一、思路与过程
首先自定义这个控件,命名为QQBubbleView:
public class QQBubbleView extends View {
public QQBubbleView(Context context) {
super(context);
}
public QQBubbleView(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
}
public QQBubbleView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
}
初始化方法不可少,然后就是思考要重写什么方法,从它是容器类还是非容器类控件入手,如果是容器类的话,就要考虑多一个方面,用不用重写拦截事件方法、以及布局方法等。而这里我们的控件因为是非容器类,所以肯定要重写onDraw()方法,另外也要重写onTouchEvent()方法,肯定它会拖拽,所以肯定会涉及到事件,onTouchEvent()自然要重写:
...
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
return super.onTouchEvent(event);
}
...
平时玩过QQ的人都应该知道这个气泡是可以用手指一开始拖着它拉动的时候,先是会气泡相连一段距离,然后继续拉动就会气泡分离开来,如果我在气泡分离开来后,拖着它拖拽一段很小的距离(我们假设为距离a)然后松手,它会回弹到原来的位置上,相反,如果在气泡分离开来后,拖着它拖拽的这段距离已经超过a的话,那气泡会继续随着我们的手指继续移动,然后松手的话,气泡就会消散。因此有四种状态:
/**
* 默认为静止静态
*/
private final int BUBBLE_DEFAUL = 0;
/**
* 气泡相连状态
*/
private final int BUBBLE_CONNECT = 1;
/**
* 气泡分离状态
*/
private final int BUBBLE_APART = 2;
/**
* 气泡消散状态
*/
private final int BUBBLE_DISMISS = 3;
因为气泡里有数字,所以需要画笔,接下来就是初始化画笔:
public class QQBubbleView extends View {
...
public QQBubbleView(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
init(context, attrs);
}
public QQBubbleView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
private void init(Context context, @Nullable AttributeSet attrs) {
//中间文字画笔
mTextPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
mTextPaint.setColor(Color.WHITE);
mTextPaint.setTextSize(mTextSize);
mTextPaint.setColor(mTextColor);
}
...
}
画笔设置抗锯齿效果,这样画出来效果平滑,没有锯齿。然后为这个控件自定义一些属性:
<?xml version="1.0" encoding="utf-8"?>
<resources>
<declare-styleable name="QQBubbleView">
<attr name="bubble_text" format="string"/>
<attr name="bubble_textSize" format="dimension"/>
<attr name="bubble_textColor" format="color"/>
<attr name="bubble_radius" format="dimension"/>
<attr name="bubble_color" format="color"/>
</declare-styleable>
</resources>
气泡颜色、气泡内文字内容、字体大小、颜色以及气泡半径大小等这些都要定义。然后就是获取我们在xml文件上设置的自定义属性值:
...
private void init(Context context, @Nullable AttributeSet attrs) {
TypedArray array = context.obtainStyledAttributes(attrs, R.styleable.QQBubbleView);
mBubbleRadius = array.getDimension(R.styleable.QQBubbleView_bubble_radius, mBubbleRadius);
mBubbleColor = array.getColor(R.styleable.QQBubbleView_bubble_color, Color.RED);
mTextStr = array.getString(R.styleable.QQBubbleView_bubble_text);
mTextSize = array.getDimension(R.styleable.QQBubbleView_bubble_textSize, mTextSize);
mTextColor = array.getColor(R.styleable.QQBubbleView_bubble_textColor, Color.WHITE);
array.recycle();
//中间文字画笔
mTextPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
mTextPaint.setColor(Color.WHITE);
mTextPaint.setTextSize(mTextSize);
mTextPaint.setColor(mTextColor);
...
}
...
接下来就是onDraw()方法里的逻辑,记住一点,就是当重写onDraw方法时,一定要分清楚组件的状态,不同的状态应该怎么绘制,这是很关键的。而onTouchEvent()方法则(通过事件)去改变状态的。
回到我们这个组件中,分这几种情况来绘制:
1)静止状态:就是不触摸气泡,也就是一开始默认的静止状态,气泡不动
2)运动状态:气泡相连
3)爆炸状态:气泡消散
...
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
if (mBubbleState != BUBBLE_DISMISS) {
//不是消失状态,也就是静止状态
}
if (mBubbleState == BUBBLE_CONNECT) {
//拖拽时状态,气泡相连
}
if (mBubbleState == BUBBLE_APART) {
//气泡发生爆炸,消散
}
}
...
每种状态负责各自的绘制逻辑,而状态的改变则由onTouchEvent()方法来负责。首先来绘制静止状态的气泡,不过这样就要获取气泡的宽高值,要记住一点是,凡是要获取宽高,就肯定是要先测量后才能获取到,而在这里,因为我们的组件不是容器类,而用onSizeChange()方法获取会更好,该方法是能在父容器的尺寸发生变化时触发的,也就是当我们的气泡所在的父容器即使发生了尺寸变化,它也会随着父容器变化而去测量自己的宽高,因为这样会比用onMearsure()方法更好。
...
@Override
protected void onSizeChanged(int w, int h, int ow, int oh) {
super.onSizeChanged(w, h, ow, oh);
if(mBubbleMoveCenter == null){
mBubbleMoveCenter = new PointF(w / 2, h / 2);
}else {
mBubbleMoveCenter.set(w / 2, h / 2);
}
}
...
得到静止时这个气泡的圆心之后,开始画这个气泡圆:
...
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
if (mBubbleState != BUBBLE_DISMISS) {
//不是消失状态,也就是静止状态
canvas.drawCircle(mBubbleMoveCenter.x,mBubbleMoveCenter.y,mBubbleRadius,mBubblePaint);
}
if (mBubbleState == BUBBLE_CONNECT) {
//拖拽时状态,气泡相连
}
if (mBubbleState == BUBBLE_APART) {
//气泡发生爆炸,消散
}
}
...
气泡画完后,就可以画文字,画文字是需要一个绘制区域的:
...
private void init(Context context, @Nullable AttributeSet attrs) {
...
//中间文字画笔
mTextPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
mTextPaint.setColor(Color.WHITE);
mTextPaint.setTextSize(mTextSize);
mTextPaint.setColor(mTextColor);
...
mTextRect = new Rect();//文字绘制区域
}
...
然后为画笔来获取这个文字的Bound,最后来写文字:
...
if (mBubbleState != BUBBLE_DISMISS) {
//不是消失状态,也就是静止状态
canvas.drawCircle(mBubbleMoveCenter.x,mBubbleMoveCenter.y,mBubbleRadius,mBubblePaint);
mTextPaint.getTextBounds(mTextStr, 0, mTextStr.length(), mTextRect);
canvas.drawText(mTextStr,
mBubbleMoveCenter.x,
mBubbleMoveCenter.y,
mTextPaint);
}
...
运行代码后,会发现文字会处在右上位置,因为getTextBounds返回的文字边框是从右上角开始画起的:
所以我们自然要把这个四方框往x左边减,往y下加:
canvas.drawText(mTextStr,
mBubbleMoveCenter.x - mTextRect.width()/2,
mBubbleMoveCenter.y + mTextRect.height()/2,
mTextPaint);
这样文字就正常居中了:
第一种状态静止状态就这样搞定,接下来就是第二种运动状态气泡相连,首先还是要先绘制一个不动的圆(左边那个圆):
...
if (mBubbleState == BUBBLE_CONNECT) {
//拖拽时状态,气泡相连
canvas.drawCircle(mBubbleNoCenter.x,mBubbleNoCenter.y,mBubbleNoRadius,mBubblePaint);
}
...
再来看一下这个气泡相连的效果:
它其实是两条贝塞尔曲线和两个圆组成,再来细分一下:
所以就是画三个区域,一个是小圆,一个是大圆,另一个是中间贝塞尔曲线组成的不规则区域(填充红色进去)。这样把它标上的点就是我们要找的点,然后用三角函数可以求出这个区域的每条边的值,而如果直接找切点当然会更好,但从该例子上去会比较难,涉及到更多数学知识,没必要,因为只要都填充同一种颜色进去,切面那里的空缺部分也变成红色,这样也能达到完全平滑的视觉效果。
以A点为例,怎样求它的x坐标和y坐标,它的纵坐标y等于小圆半径乘以cos,而x坐标等于负的小圆半径乘以sin,这是三角函数的知识,希望大家没忘。那它的cos值怎么获得,又是几何的知识,角a是等于角b的,所以求出角b的cos就等于求出角a的cos,思路分析完之后,就来写代码。
所以关键在于大圆的x坐标和y坐标,它其实是我们手指拖动的圆,因此要从onTouchEvent()方法中获取这个手指事件的坐标:
...
@Override
public boolean onTouchEvent(MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN: {
//手指按下
}
case MotionEvent.ACTION_MOVE: {
//手指移动
//记录事件坐标
mBubbleMoveCenter.x = event.getX();
mBubbleMoveCenter.y = event.getY();
}
case MotionEvent.ACTION_UP: {
//手指抬起
}
}
}
...
小圆的坐标也要有,它在onSizeChange()方法里初始化,原因跟上面一样说过,不作复述了:
...
@Override
protected void onSizeChanged(int w, int h, int ow, int oh) {
super.onSizeChanged(w, h, ow, oh);
if(mBubbleMoveCenter == null){
mBubbleMoveCenter = new PointF(w / 2, h / 2);
}else {
mBubbleMoveCenter.set(w / 2, h / 2);
}
if(mBubStillCenter == null){
mBubbleNoCenter = new PointF(w / 2,h / 2);
}else{
mBubbleNoCenter.set(w / 2,h / 2);
}
}
...
有了P点的x和y值后,通过勾股定理(系统都有API直接调用便可)求出两圆心组成的三角形斜边值OP:
...
@Override
public boolean onTouchEvent(MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN: {
//手指按下
}
case MotionEvent.ACTION_MOVE: {
//手指移动
//记录事件坐标
mBubbleMoveCenter.x = event.getX();
mBubbleMoveCenter.y = event.getY();
//OP
mDistance = (float) Math.hypot(event.getX() - mBubbleNoCenter.x,
event.getY() - mBubbleNoCenter.y);
}
case MotionEvent.ACTION_UP: {
//手指抬起
}
}
}
...
因为你是拖动着大圆在移动,事件会变,圆心自然也是在变,因此在move方法里去获取。知道了斜边之后,那角b这个角的cos和sin又怎么求呢,其实根据几何知识可以知道角b是等于角d的:
所以转而求角d,角d的cos等于OE距离除以斜边,而sin等于PE距离除以斜边,而OE距离就是大圆圆心的x坐标值,而PE的距离则是大圆圆心的y坐标值,因此:
...
if (mBubbleState == BUBBLE_CONNECT) {
//拖拽时状态,气泡相连
canvas.drawCircle(mBubbleNoCenter.x,mBubbleNoCenter.y,mBubbleNoRadius,mBubblePaint);
//cos
float cosTana = (mBubbleMoveCenter.x - mBubbleNoCenter.x) / mDistance;
//sin
float sinTana = (mBubbleMoveCenter.y - mBubbleNoCenter.y) / mDistance;
}
...
求到了角b的cos和sin,那根据上文我们分析,角a的cos和sin也就是这个cos和sin,那A点的x坐标值和y坐标值也就能求出来了:
...
if (mBubbleState == BUBBLE_CONNECT) {
//拖拽时状态,气泡相连
canvas.drawCircle(mBubbleNoCenter.x,mBubbleNoCenter.y,mBubbleNoRadius,mBubblePaint);
//cos
float cosTana = (mBubbleMoveCenter.x - mBubbleNoCenter.x) / mDistance;
//sin
float sinTana = (mBubbleMoveCenter.y - mBubbleNoCenter.y) / mDistance;
//A
float mAStartX = mBubbleNoCenter.x - mBubbleNoRadius * sinTana;
float mAStartY = mBubbleNoCenter.y + mBubbleNoRadius * cosTana;
}
...
那接下来B点的x坐标值和y坐标值也很好求,因为它的角跟角a相等,所以cos和sin也就是角a的,大圆的x坐标值和y坐标值也知道,但这里要注意的是,从图中可以看出,AD的距离跟BC的距离不一样,因为AD的距离是固定不变的,而BC的距离是随着我们的拖拽越远,就越变长,所以是动态的,不是固定的,因此BC的距离可以在onTouchEvent()方法里处理:
...
@Override
public boolean onTouchEvent(MotionEvent event) {
switch (event.getAction()) {
...
case MotionEvent.ACTION_MOVE: {
//手指移动
//记录事件坐标
mBubbleMoveCenter.x = event.getX();
mBubbleMoveCenter.y = event.getY();
//OP
mDistance = (float) Math.hypot(event.getX() - mBubbleNoCenter.x,
event.getY() - mBubbleNoCenter.y);
mBubbleMoveRadius = mDistance * 0.5;//根据实际情况来设置
}
...
}
}
...
这样再去求B点:
...
if (mBubbleState == BUBBLE_CONNECT) {
//拖拽时状态,气泡相连
canvas.drawCircle(mBubbleNoCenter.x,mBubbleNoCenter.y,mBubbleNoRadius,mBubblePaint);
//cos
float cosTana = (mBubbleMoveCenter.x - mBubbleNoCenter.x) / mDistance;
//sin
float sinTana = (mBubbleMoveCenter.y - mBubbleNoCenter.y) / mDistance;
//A
float mAStartX = mBubbleNoCenter.x - mBubbleNoRadius * sinTana;
float mAStartY = mBubbleNoCenter.y + mBubbleNoRadius * cosTana;
//B
float mBEndX = mBubbleMoveCenter.x - mBubbleRadius * sinTana;
float mBEndY = mBubbleMoveCenter.y + mBubbleMoveRadius * cosTana;
}
...
剩下的C点则就是利用角b的cos和sin去跟大圆的半径去作三角函数运算,同样也是要注意大圆是动态变大,直径要算上变动时的距离:
//C
float mCStartX = mBubbleMoveCenter.x + mBubbleMoveRadius * sinTana;
float mCStartY = mBubbleMoveCenter.y - mBubbleMoveRadius * cosTana;
D点也是同样的原理,这里就不作复述了:
//D
float mDEndX = mBubbleNoCenter.x + mBubbleNoRadius * sinTana;
float mDEndY = mBubbleNoCenter.y - mBubbleNoRadius * cosTana;
G点则更加好求:
//G
int mGCenterX = (int) ((mBubbleNoCenter.x + mBubbleMoveCenter.x) / 2);
int mGCenterY = (int) ((mBubbleNoCenter.y + mBubbleMoveCenter.y) / 2);
得到这些点的x坐标和y坐标之后,就可以把它们连起来,画成中间那块不规则区域,先来初始化path对象:
...
//文字画笔
mTextPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
mTextPaint.setColor(Color.WHITE);
mTextPaint.setTextSize(mTextSize);
mTextPaint.setColor(mTextColor);
...
mBeiPath= new Path();
...
贝塞尔曲线
在此之前要讲一下贝塞尔曲线,因为点A跟点B要用贝塞尔曲线连着,点C和点D也是要用贝塞尔曲线相连。贝塞尔曲线是一条根据四个位置任意的点坐标绘制的光滑曲线。贝塞尔曲线是分阶的,如果是2阶的,那么就有三个点,一个是初始点,一个是终结点,另一个是控制点;如果是3阶的,则四个点,以此类推,所以2阶是这样子的:
3阶的话则是四个点,初始点、终结点和两个控制点:
控制点不在线上,而是用来牵引来改变线的形状。我们的气泡很明显上半部那条曲线只有一个控制点,下面那条曲线也只有一个控制点,所以都是二阶的:
所以G点就是它们的控制点。接下来开始画这个区域:
...
if (mBubbleState == BUBBLE_CONNECT) {
//拖拽时状态,气泡相连
...
//G
int mGCenterX = (int) ((mBubbleNoCenter.x + mBubbleMoveCenter.x) / 2);
int mGCenterY = (int) ((mBubbleNoCenter.y + mBubbleMoveCenter.y) / 2);
mBeiPath.reset();
//A点为开始点
mBeiPath.moveTo(mAStartX, mAStartY);
}
...
先调用moveTo()方法移动到A点,以它作为起始点,然后使用quadTo()方法来画贝塞尔曲线,先来看该方法的源码:
注释已经说的很清楚,x1和y1是控制点的x、y坐标值,而x2和y2则是终点的x、y坐标值,因此我们以G点为控制点,B点为终点进行调用:
...
mBeiPath.reset();
//A点为开始点
mBeiPath.moveTo(mAStartX, mAStartY);
//控制点为G点,终点为B点
mBeiPath.quadTo(mGCenterX, mGCenterY, mBEndX, mBEndY);
...
这样上半部分的贝塞尔曲线就画好了,接下来就是要沿着BC方向来画直线,从B点画到C点,调用lineTo()来画:
...
mBeiPath.reset();
//A点为开始点
mBeiPath.moveTo(mAStartX, mAStartY);
//控制点为G点,终点为B点
mBeiPath.quadTo(mGCenterX, mGCenterY, mBEndX, mBEndY);
//画到C点
mBeiPath.lineTo(mCStartX, mCStartY);
...
然后又是画CD的贝塞尔曲线,控制点是G,终点是D:
...
mBeiPath.reset();
//A点为开始点
mBeiPath.moveTo(mAStartX, mAStartY);
//控制点为G点,终点为B点
mBeiPath.quadTo(mGCenterX, mGCenterY, mBEndX, mBEndY);
//画到C点
mBeiPath.lineTo(mCStartX, mCStartY);
//控制点为G点,终点为D点
mBeiPath.quadTo(mGCenterX, mGCenterY,mDEndX, mDEndY);
...
然后把这条路径闭合,然后调用drwaPath()方法,就可以完成这个不规则图形的绘制:
...
mBeiPath.reset();
//A点为开始点
mBeiPath.moveTo(mAStartX, mAStartY);
//控制点为G点,终点为B点
mBeiPath.quadTo(mGCenterX, mGCenterY, mBEndX, mBEndY);
//画到C点
mBeiPath.lineTo(mCStartX, mCStartY);
//控制点为G点,终点为D点
mBeiPath.quadTo(mGCenterX, mGCenterY,mDEndX, mDEndY);
mBeiPath.close();
canvas.drawPath(mBeiPath, mBubblePaint);
...
接下来就是气泡消失状态的逻辑,当手指离开的时候,分两种情况,一种是此时滑过的距离超过某个值(max)则气泡消失,另一种情况则是当滑过的距离不超过这个max值则回弹到原来的位置。因此手指移动时要记录手指滑动距离,然后定义的这个max的值(可以根据实际情况来自己决定):
...
//文字画笔
mTextPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
mTextPaint.setColor(Color.WHITE);
mTextPaint.setTextSize(mTextSize);
mTextPaint.setColor(mTextColor);
...
mMaxDistance = 6 * mBubbleRadius;//根据实际情况来设定
移动的时候就进行判断:
case MotionEvent.ACTION_MOVE: {
mBubbleMoveCenter.x = event.getX();
mBubbleMoveCenter.y = event.getY();
mDistance = (float) Math.hypot(event.getX() - mBubbleNoCenter.x,
event.getY() - mBubbleNoCenter.y);
if (mBubbleState == BUBBLE_CONNECT) {
if (mDistance > mMaxDistance) {
mBubbleState = BUBBLE_APART;
} else {
mBubbleNoRadius = mBubbleRadius - mDistance / 6;//根据实际需求设定
}
}
break;
}
当移动距离超过这个最大值,状态就变成消失状态,不超过的话,则要回弹回去,也就是让小圆的半径逐渐减少,因为移动的过程中,除了大圆的半径变大之外,小圆也在变小:
变小多少,根据实际来自行决定。
然后回到手指抬起的逻辑:
case MotionEvent.ACTION_UP: {
if (mBubbleState == BUBBLE_CONNECT) {
//没有超出最大距离
//回弹动画
startBubbleReset();
} else if (mBubbleState == BUBBLE_APART) {
//超出最大距离
}
break;
}
没有超出最大值则弹回原来位置,这是一个属性动画过程:
private void startBubbleReset() {
ValueAnimator animator = ValueAnimator.ofObject(new PointFEvaluator(),
new PointF(mBubbleMoveCenter.x, mBubbleMoveCenter.y),
new PointF(mBubbleNoCenter.x, mBubbleNoCenter.y));
animator.setDuration(500);
animator.setInterpolator(new OvershootInterpolator(5f));
animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
mBubbleMoveCenter = (PointF) animation.getAnimatedValue();
invalidate();
}
});
animator.start();
}
PointFEvaluator和OvershootInterpolator都是系统提供,它们实现的效果是先反向压缩一小段距离,然后加速弹出。如果对动画还是不理解可以看回我的文章 自定义View系列:安卓动画机制
回弹的逻辑搞定之后,接下来就是气泡爆炸消失效果的逻辑:
...
/**
* 爆炸图片数组
*/
private int[] mBurstDrawablesArray = {R.drawable.pic_1, R.drawable.pic_2,
R.drawable.pic_3, R.drawable.pic_4, R.drawable.pic_5};
...
private void startBubbleBurstAnim () {
//变为消散状态
mBubbleState = BUBBLE_DISMISS;
...
animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
//设置当前绘制的爆炸图片索引
mCurDrawIndex = (int) animation.getAnimatedValue();
invalidate();
}
});
animator.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
//是否执行动画
mIsNoShowStart = false;
}
});
animator.start();
}
...
代码很好理解,更新动画的时候来更新爆炸图片的数组索引值,然后渲染绘制图片的工作在onDraw()方法中进行:
if (mBubbleState == BUBBLE_APART) {
mBurstRect.set((int) (mBubbleMoveCenter.x - mBubbleMoveRadius),
(int) (mBubbleMoveCenter.y - mBubbleMoveRadius),
(int) (mBubbleMoveCenter.x + mBubbleMoveRadius),
(int) (mBubbleMoveCenter.y + mBubbleMoveRadius));
canvas.drawBitmap(mBurstBitmapsArray[mCurDrawIndex], null, mBurstRect, mBubblePaint);
}
drawBitmap()要传一个(爆炸)区域参数Rect和Bitmap,所以还要给图片drawable转成bitmap:
...
//文字画笔
mTextPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
mTextPaint.setColor(Color.WHITE);
mTextPaint.setTextSize(mTextSize);
mTextPaint.setColor(mTextColor);
...
mBurstBitmapsArray = new Bitmap[mBurstDrawablesArray.length];
for (int i = 0; i < mBurstDrawablesArray.length; i++) {
//将气泡爆炸的drawable转为bitmap
Bitmap bitmap = BitmapFactory.decodeResource(getResources(), mBurstDrawablesArray[i]);
mBurstBitmapsArray[i] = bitmap;
}
...
所以整个UP事件逻辑就是这样:
case MotionEvent.ACTION_UP: {
if (mBubbleState == BUBBLE_STATE_CONNECT) {
//没有超出最大距离
//回弹动画
startBubbleReset();
} else if (mBubbleState == BUBBLE_STATE_APART) {
//超出最大距离
if(mDistance < 2 * mBubbleRadius){//根据实际需求设定
//回弹动画
startBubbleReset();
}else {
//炸裂的动画
startBubbleBurstAnim();
}
}
break;
}
最后,别忘了,给MOVE事件添加重绘方法:
case MotionEvent.ACTION_MOVE: {
mBubbleMoveCenter.x = event.getX();
mBubbleMoveCenter.y = event.getY();
mDistance = (float) Math.hypot(event.getX() - mBubbleNoCenter.x,
event.getY() - mBubbleNoCenter.y);
if (mBubbleState == BUBBLE_CONNECT) {
if (mDistance > mMaxDistance) {
mBubbleState = BUBBLE_APART;
} else {
mBubbleNoRadius = mBubbleRadius - mDistance / 6;//根据实际需求设定
}
}
invalidate();
break;
}
因为一直拖拽气泡,组件要随着事件变化一直重绘的。最终实现的效果就是这样:
所以自定义View要有这样一个思路,onDraw()方法里对于不同情况下使用各种api去绘制view在不同状态下的形态,而这个“情况”的改变则由onTouchEvent()去决定,然后尺寸则有onMeasure()方法或者onSizeChange()方法去决定。搞清楚每个重写方法该干嘛,这样自定义View时就不会再晕头转向。另外感兴趣想要以上完整代码可关我公号:Pingred,欢迎大家一起交流与学习。