Android自定义View仿QQ消息拖拽气泡实现

很多小的效果看上去很酷炫,其实操作起来很简单,就是要注意细节以及分析状态,然后去绘制,这里会实现qq消息拖拽气泡的实现。

  • 画图分析
  • 源代码
  • 效果展示

画图分析

在画图之前,看一下原效果图,分析实现步骤

android 仿消息圆点 android仿qq消息界面_android仿气泡

由图可知,它这里拖拽画的是图片,为了更接近原理以及自定义可扩展,我们将拖拽的99+也当成一个圆。如下图:

android 仿消息圆点 android仿qq消息界面_android仿气泡_02

小圆是在原始位置,大圆是在当前手指的位置画出来的,我们这里的坐标系是根据平移画布得到的,代码里会标注;
两个圆的圆心分别是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)

android 仿消息圆点 android仿qq消息界面_android仿气泡_03

如上图箭头所示,所以我在代码中设置了不同位置的坐标点来决定绘制的方向,已达到绘制出来的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);}

}


效果展示

android 仿消息圆点 android仿qq消息界面_Big_04