自定义View完结篇--从实现QQ消息气泡去理解自定义View

彻底搞懂自定义View,看该系列就够了


前言

作为自定义View的完结篇,该篇自然就是结合一个经典例子来讲解,实现QQ消息的气泡控件:
在这里插入图片描述
它是可以拖拽的,像这种效果:
在这里插入图片描述

当然,要彻底写好这个控件,还是得搞懂自定义View的相关知识点,对整个自定义View的知识体系如果觉得还是不熟,可以回顾我的这个系列:

UI绘制流程分析(后篇)–完结篇

彻底带你搞懂安卓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,欢迎大家一起交流与学习。

评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值