Android自定义View仿QQ消息拖拽气泡实现
很多小的效果看上去很酷炫,其实操作起来很简单,就是要注意细节以及分析状态,然后去绘制,这里会实现qq消息拖拽气泡的实现。
- 画图分析
- 源代码
- 效果展示
画图分析
在画图之前,看一下原效果图,分析实现步骤
由图可知,它这里拖拽画的是图片,为了更接近原理以及自定义可扩展,我们将拖拽的99+也当成一个圆。如下图:
小圆是在原始位置,大圆是在当前手指的位置画出来的,我们这里的坐标系是根据平移画布得到的,代码里会标注;
两个圆的圆心分别是o、 p,圆半径分别为rbig、rsmall,两圆心的链接线为op,链接bc经过圆心垂直于op,链接ad垂直于op,设op的中心点为g,设p点的坐标为(x,y),那么贝塞尔曲线agb和dgc就可以很好的画出(如果贝塞尔曲线不了解的可以看我的其它博客)。那么主要就是求a、b、c、d四个点的坐标,由于p点坐标就是手势的坐标可以很好的求出;
解决a、b、c、d四个点的坐标
由图可知
1.∠epo=∠bpf(这不需要我推理把。。。。)那么∠poe=∠pbf;同理oa与y轴方向的夹角也可以求出(我这里没有标出点,您可以在草稿纸上标出,来求一下)
那么
2.sin∠poe=pe/op; cos∠poe=oe/op;
3.pe=0-y(这里都用起始坐标减去末尾坐标,以确定改点在哪个坐标系)oe=0-x
由以上结论可以求出ABCD四点的坐标
A :(0-rsmall*sin,0+rsmall*cos)
B :(x-rbig*sin,y+rbig*cos)
C :(x+rbig*sin,y-rbig*cos)
D :(0+rsmall*sin,0-rsmall*cos)
分析状态
四种状态:正常状态,拖拽状态,超出状态,恢复状态
NORMAL,DRAG,BEYOUND,COMEBACK
这里我们就提下拖拽状态的path:还是要让他成为一个封闭的路径,所以要满足方向闭合(以前博客有分析,我这里画一下,也就是Path.Direction)
如上图箭头所示,所以我在代码中设置了不同位置的坐标点来决定绘制的方向,已达到绘制出来的path是封闭图形
源代码
代码很简单,具体封装接口和回掉都没写,看你的操作了,代码有注释,很好理解,有什么不好的地方可以指出来,我会改正。 注意,如果想在控件之外展示,需要在其父控件写 android:clipChildren=”false”
public class BubbleView extends View {
private int mWidth,mHeight,paintextSize=20;
//相关坐标
private float[] startSmall=new float[2],endBig=new float[2],startBig=new float[2],
endSmall=new float[2],controlXY=new float[2],activeXY=new float[2],lastXY=new float[2];
//相关路径
private Path pathSmallC,pathBigC,pathStick,pathLine,pathText;
//画笔
private Paint paintNormal,paintText;
//矩阵
private Matrix matrix;
//区域的裁定
private Region regionClip,regionBubble;
//一些参数
private float radioSmall=10f,radioBig=25f,sin=2,cos=2,len=0,maxLen=200f,varySet=1;
private TYPE type=TYPE.NORMAL;
private ValueAnimator animatorBack,animatorGone;
private ValueAnimator.AnimatorUpdateListener uplistner;
//测量
private PathMeasure measure;
enum TYPE{
//四种状态:正常状态,拖拽状态,超出状态,恢复状态
NORMAL,DRAG,BEYOUND,COMEBACK
}
public BubbleView(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
initPaint();
initValuAnimator();
}
/**
* 初始化画笔
*/
private void initPaint() {
paintNormal=new Paint();
paintNormal.setStyle(Paint.Style.FILL);
paintNormal.setAntiAlias(true);
paintNormal.setColor(Color.RED);
paintText=new Paint();
paintText.setTextSize(paintextSize);
paintText.setAntiAlias(true);
paintText.setColor(Color.WHITE);
paintText.setStyle(Paint.Style.FILL);
paintText.setTextAlign(Paint.Align.CENTER);
matrix=new Matrix();
measure=new PathMeasure();
}
/**
* 初始化路径
*/
private void initPath() {
if(type==TYPE.NORMAL){
activeXY[0]=0;
activeXY[1]=0;
}
pathSmallC=new Path();
pathSmallC.addCircle(0,0,radioSmall, Path.Direction.CW);
pathLine=new Path();
pathLine.lineTo(activeXY[0],activeXY[1]);
if(type==TYPE.COMEBACK){
measure.setPath(pathLine,false);
measure.getPosTan(measure.getLength()*varySet,activeXY,null);
}
pathBigC=new Path();
pathText=new Path();
if(type==TYPE.BEYOUND){
pathBigC.addCircle(activeXY[0],activeXY[1],radioBig*varySet, Path.Direction.CW);
pathText.moveTo(activeXY[0]-radioBig,activeXY[1]+4*varySet);
pathText.lineTo(activeXY[0]+radioBig,activeXY[1]+4*varySet);
paintText.setTextSize(paintextSize*varySet);
}else {
pathBigC.addCircle(activeXY[0],activeXY[1],radioBig, Path.Direction.CW);
pathText.moveTo(activeXY[0]-radioBig,activeXY[1]+4);
pathText.lineTo(activeXY[0]+radioBig,activeXY[1]+4);
paintText.setTextSize(paintextSize);
}
regionBubble=new Region();
regionBubble.setPath(pathBigC,regionClip);
pathStick=new Path();
controlXY[0]=activeXY[0]/2;
controlXY[1]=activeXY[1]/2;
sin=(0-activeXY[1])/len;
cos=(0-activeXY[0])/len;
startSmall[0]=0-radioSmall*sin;
startSmall[1]=0+radioSmall*cos;
endBig[0]=activeXY[0]-radioBig*sin;
endBig[1]=activeXY[1]+radioBig*cos;
startBig[0]=activeXY[0]+radioBig*sin;
startBig[1]=activeXY[1]-radioBig*cos;
endSmall[0]=0+radioSmall*sin;
endSmall[1]=0-radioSmall*cos;
pathStick.moveTo(startSmall[0],startSmall[1]);
pathStick.quadTo(controlXY[0],controlXY[1],endBig[0],endBig[1]);
pathStick.lineTo(startBig[0],startBig[1]);
pathStick.quadTo(controlXY[0],controlXY[1],endSmall[0],endSmall[1]);
pathStick.close();
pathStick.setFillType(Path.FillType.WINDING);
}
/**
* 初始化动画
*/
private void initValuAnimator() {
uplistner = new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator valueAnimator) {
varySet= (float) valueAnimator.getAnimatedValue();
invalidate();
}
};
Animator.AnimatorListener listener=new Animator.AnimatorListener() {
@Override
public void onAnimationStart(Animator animator) {
}
@Override
public void onAnimationEnd(Animator animator) {
type=TYPE.NORMAL;
varySet=1;
}
@Override
public void onAnimationCancel(Animator animator) {
}
@Override
public void onAnimationRepeat(Animator animator) {
}
};
animatorBack=ValueAnimator.ofFloat(1.0000f,0.0000f);
animatorBack.setInterpolator(new LinearInterpolator());
animatorBack.setRepeatCount(0);
animatorBack.setDuration(1000);
animatorBack.addUpdateListener(uplistner);
animatorBack.addListener(listener);
animatorGone=ValueAnimator.ofFloat(1.0000f,0.0000f);
animatorGone.setInterpolator(new LinearInterpolator());
animatorGone.setRepeatCount(0);
animatorGone.setDuration(1000);
animatorGone.addUpdateListener(uplistner);
animatorGone.addListener(listener);
}
@Override
public boolean dispatchTouchEvent(MotionEvent event) {
getParent().requestDisallowInterceptTouchEvent(true);
return super.dispatchTouchEvent(event);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
switch (event.getAction()){
case MotionEvent.ACTION_DOWN:
if(type==TYPE.NORMAL) {
activeXY[0] = event.getRawX();
activeXY[1] = event.getRawY();
matrix.mapPoints(activeXY);
if (regionBubble.contains((int) activeXY[0], (int) activeXY[1])) {
type = TYPE.DRAG;
lastXY[0]=activeXY[0];
lastXY[1]=activeXY[1];
}
}
break;
case MotionEvent.ACTION_MOVE:
activeXY[0]= event.getRawX();
activeXY[1]=event.getRawY();
matrix.mapPoints(activeXY);
if(type==TYPE.DRAG){
len= (float) Math.hypot(activeXY[0],activeXY[1]);
if(len<=maxLen){
lastXY[0]=activeXY[0];
lastXY[1]=activeXY[1];
}else if(len>maxLen){
type=TYPE.BEYOUND;
}
}
invalidate();
break;
case MotionEvent.ACTION_UP:
if(type==TYPE.DRAG){
activeXY[0]=lastXY[0];
activeXY[1]=lastXY[1];
//开始动画
type=TYPE.COMEBACK;
animatorBack.start();
}else if(type==TYPE.BEYOUND){
//开始动画
animatorGone.start();
}
break;
default:
break;
}
return true;
}
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
mWidth=w;
mHeight=h;
regionClip=new Region(-mWidth,-mHeight,mWidth,mHeight);
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
//平移画布
canvas.translate(mWidth/2,mHeight/2);
matrix.reset();
if(matrix.isIdentity()){
canvas.getMatrix().invert(matrix);
}
initPath();
switch (type){
case NORMAL:
drawBigC(canvas);
break;
case DRAG:
drawStick(canvas);
drawSmallC(canvas);
drawBigC(canvas);
break;
case BEYOUND:
drawBigC(canvas);
break;
case COMEBACK:
drawStick(canvas);
drawSmallC(canvas);
drawBigC(canvas);
break;
default:break;
}
drawText(canvas);
}
/**
* 画小圆
* @param canvas
*/
private void drawSmallC(Canvas canvas) {
canvas.drawPath(pathSmallC,paintNormal);
}
/**
* 画拖拽
* @param canvas
*/
private void drawStick(Canvas canvas) {
canvas.drawPath(pathStick,paintNormal);
}
/**
* 画大圆
* @param canvas
*/
private void drawBigC(Canvas canvas) {
canvas.drawPath(pathBigC,paintNormal);
}
/**
* 画文字
* @param canvas
*/
private void drawText(Canvas canvas){canvas.drawTextOnPath("1",pathText,0,0,paintText);}
}
效果展示