Android 贝赛尔曲线实现QQ红点拖动

参考:
https://blog.csdn.net/harvic880925/article/details/51615221

https://study.163.com/course/courseLearn.htm?courseId=1209399928#/learn/live?lessonId=1279356094&courseId=1209399928

Github:

实现步骤:

  • 自定义控件,监听onTouchEvent的down,up和move方法。
  • 画出起点圆以及终点圆。
  • 得到两个圆之间的连接桥坐标进行路径绘制。
  • 各种逻辑之间的判断以及处理。

Path类:
Path封装了由直线和曲线(二次、三次贝塞尔曲线)构成的几何路径。用Canvas中的drawPath来把这条路径画出来(同样支持Paint的不同绘制模式),也可以用于裁剪画布和根据路径绘制文字。我们有时会用Path来描述一个图像的轮廓,所以也会称为轮廓线(轮廓线仅是Path的一种使用方法,两者并不等价)

Rect类
用于装载控件在屏幕中的对角坐标。

第1步 自定义控件

1 布局文件中添加FrameLayout

用于放置自定义的WaterView。

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical">
    <FrameLayout
        android:id="@+id/frameLayout"
        android:layout_width="match_parent"
        android:layout_height="match_parent" />
</LinearLayout>

2 添加自定义View

在Activity中将自定义View添加到FrameLayout中。

	WaterView waterView;
    FrameLayout frameLayout;
    FrameLayout.LayoutParams layoutParams;

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_second);
        initView();
    }

    private void initView(){
        frameLayout = findViewById(R.id.frameLayout);
        layoutParams = new FrameLayout.LayoutParams(FrameLayout.LayoutParams.MATCH_PARENT,
                FrameLayout.LayoutParams.MATCH_PARENT);
        waterView = new WaterView(this);//1
        frameLayout.removeAllViews();//2
        frameLayout.addView(waterView);//3
    }

注释1:初始化WaterView
注释2:先移除frameLayout中的所有的View
注释3:添加WaterView到frameLayout中


3 新建自定义WaterView

新建WaterView.java,继承FrameLayout。

public class WaterView extends FrameLayout {

    //定义一个文本控件
    TextView textView;
    //文本框的初始坐标
    private PointF initPosition;
    //手指移动到的坐标
    private PointF movePosition;
    private boolean isClicked = false;

    public WaterView(Context context) {
        super(context);
        init();
    }

    /**
     * 初始化整个效果的控件
     */
    private void init() {
        initPosition = new PointF(500, 500);
        movePosition = new PointF();
        textView = new TextView(getContext());
        textView.setPadding(20, 20, 20, 20);
        textView.setTextColor(Color.WHITE);
        textView.setText("99+");
        textView.setBackgroundResource(R.drawable.tv_bg);
        LayoutParams layoutParams = new LayoutParams(
                ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
        textView.setLayoutParams(layoutParams);
        this.addView(textView);
    }

    /**
     * 绘制包括本身的
     */
//    @Override
//    protected void onDraw(Canvas canvas) {
//        super.onDraw(canvas);
//    }

    /**
     * 绘制当前控件里面的内容的控件
     */
    @Override
    protected void dispatchDraw(Canvas canvas) {

        //保存canvas的状态
        canvas.save();

        if (isClicked) {
            textView.setX(movePosition.x - textView.getWidth() / 2);
            textView.setY(movePosition.y - textView.getHeight() / 2);
        } else {
            //设置初始坐标为控件的中心点
            textView.setX(initPosition.x - textView.getWidth() / 2);
            textView.setY(initPosition.y - textView.getHeight() / 2);
        }

        // 恢复canvas的状态
        canvas.restore();
        super.dispatchDraw(canvas);

    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
            	//手指按下的时候,设置movePosition为initPosition的值
            	//(即:移动位置为初始化位置)
                movePosition.set(initPosition.x, initPosition.y);
                //判断当前位置是否在文本控件里面
                //这个对象是用来封装文本控件的范围的对象
                Rect rect = new Rect();
                int[] location = new int[2];

                //获取到textView控件在窗体中的X,Y坐标
                textView.getLocationOnScreen(location);

                //初始化Rect对象
                rect.left = location[0];
                rect.top = location[1];
                rect.right = location[0] + textView.getWidth();
                rect.bottom = location[1] + textView.getHeight();

                //判断当前点击的坐标是否是在范围之内,如果在范围内设置isClicked为true
                //getRawX和getRawY是相对于父控件
                if (rect.contains((int) event.getRawX(), (int) event.getRawY())) {
                    isClicked = true;
                }
                break;
            case MotionEvent.ACTION_UP:
                isClicked = false;
                //手指抬起后,回到起始位置
                movePosition.set(initPosition.x, initPosition.y);
                break;
            case MotionEvent.ACTION_MOVE:
                //getX和getY是相对于屏幕的坐标
                movePosition.set(event.getX(), event.getY());
                break;

        }

        //通过这个API可以调用到dispatchDraw的方法
        postInvalidate();
        return true;
    }
}


textView的背景tv_bg.xml代码如下:

<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
    <corners android:radius="10dp"/>
    <solid android:color="#ff0000"/>
    <stroke android:color="#0f000000" android:width="1dp"/>
</shape>

运行后显示效果如下:
在这里插入图片描述

第2步 画出起点圆以及终点圆

public class WaterView extends FrameLayout {

    //定义一个文本控件
    TextView textView;

    //文本框的初始坐标
    private PointF initPosition;
    //手指移动到的坐标
    private PointF movePosition;
    private boolean isClicked = false;
    //绘制的圆的半径
    private float mRadius = 40;
    //绘制的画笔
    private Paint mPaint;

    public WaterView(Context context) {
        super(context);
        init();
    }

    /**
     * 初始化整个效果的控件
     */
    private void init() {

        initPosition = new PointF(500, 500);
        movePosition = new PointF();

        //初始化画笔的样式为填充
        mPaint = new Paint();
        mPaint.setColor(Color.RED);
        mPaint.setStyle(Paint.Style.FILL);


        textView = new TextView(getContext());
        textView.setPadding(20, 20, 20, 20);
        textView.setTextColor(Color.WHITE);
        textView.setText("99+");
        textView.setBackgroundResource(R.drawable.tv_bg);
        LayoutParams layoutParams = new LayoutParams(
                ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
        textView.setLayoutParams(layoutParams);

        this.addView(textView);
    }

    /**
     * 绘制包括本身的
     */
//    @Override
//    protected void onDraw(Canvas canvas) {
//        super.onDraw(canvas);
//    }

    /**
     * 绘制当前控件里面的内容的控件
     */
    @Override
    protected void dispatchDraw(Canvas canvas) {

        //保存canvas的状态
        canvas.save();

        if (isClicked) {
            textView.setX(movePosition.x - textView.getWidth() / 2);
            textView.setY(movePosition.y - textView.getHeight() / 2);

            //画两个圆
            //画第一个圆,是初始化坐标的圆
            canvas.drawCircle(initPosition.x,initPosition.y,mRadius,mPaint);
            //画第二个圆,是终点的圆
            canvas.drawCircle(movePosition.x,movePosition.y,mRadius,mPaint);

        } else {
            //设置初始坐标为控件的中心点
            textView.setX(initPosition.x - textView.getWidth() / 2);
            textView.setY(initPosition.y - textView.getHeight() / 2);
        }

        // 恢复canvas的状态
        canvas.restore();
        super.dispatchDraw(canvas);

    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:

                movePosition.set(initPosition.x, initPosition.y);

                //判断当前位置是否在文本控件里面
                //这个对象是用来封装文本控件的范围的对象
                Rect rect = new Rect();
                int[] location = new int[2];

                //获取到textView控件在窗体中的X,Y坐标
                textView.getLocationOnScreen(location);

                //初始化Rect对象
                rect.left = location[0];
                rect.top = location[1];
                rect.right = location[0] + textView.getWidth();
                rect.bottom = location[1] + textView.getHeight();

                //判断当前点击的坐标是否是在范围之内
                //getRawX和getRawY是相对于父控件
                if (rect.contains((int) event.getRawX(), (int) event.getRawY())) {
                    isClicked = true;
                }

                break;
            case MotionEvent.ACTION_UP:
                isClicked = false;
                movePosition.set(initPosition.x, initPosition.y);
                break;
            case MotionEvent.ACTION_MOVE:
                //getX和getY是相对于屏幕的坐标
                movePosition.set(event.getX(), event.getY());
                break;

        }

        //通过这个API可以调用到dispatchDraw的方法
        postInvalidate();
        return true;
    }
}

运行效果如下:
第二个圆被textView挡住了,也就是在textView的后面。
在这里插入图片描述

第3步 连接桥坐标进行路径绘制

在这里插入图片描述
左下角的圆形是初始圆形(圆心坐标是x1,y1),右上角的圆形是拖动后的圆形(圆心坐标是x2,y2);

3.1 计算角度值

先计算出
θ = a t a n y 2 − y 1 x 2 − x 1 \theta = atan\frac{y_2 - y_1}{x_2-x_1} θ=atanx2x1y2y1

代码如下:

   		//获取到终点与起点的X坐标的差值 A2
        float widthX = movePosition.x - initPosition.x;
        //获取到终点与起点的Y坐标的差值 A3
        float widthY = movePosition.y - initPosition.y;

        //得到三角形的锐角的角度值 正切值
        double atan = Math.atan(widthY / widthX);

3.2 计算各个点坐标

在这里插入图片描述

我们单独把这个三角形拿出来,这里可以很明显的可以看出A点的坐标是:
A X = x 1 + o f f s e t x AX = x_1 + offsetx AX=x1+offsetx
A Y = y 1 − o f f s e t y AY = y_1 - offsety AY=y1offsety
B X = x 2 + o f f s e t x BX = x_2 + offsetx BX=x2+offsetx
B Y = y 2 − o f f s e t y BY = y_2 - offsety BY=y2offsety
C X = x 2 − o f f s e t x CX = x_2 - offsetx CX=x2offsetx
C Y = y 2 + o f f s e t y CY = y_2 + offsety CY=y2+offsety
D X = x 1 − o f f s e t x DX = x_1 - offsetx DX=x1offsetx
D Y = y 1 + o f f s e t y DY = y_1 + offsety DY=y1+offsety

代码如下:

 		//获取到offsetX的长度
        float offsetX = (float) (mRadius * Math.sin(atan));
        //获取到offsetY的长度
        float offsetY = (float) (mRadius * Math.cos(atan));

        Log.d("WaterView" ,"offsetX = " + offsetX );
        Log.d("WaterView" ,"offsetY = " + offsetY );

        //获取到A坐标
        float AX = initPosition.x + offsetX;
        float AY = initPosition.y - offsetY;
        //获取到B坐标
        float BX = movePosition.x + offsetX;
        float BY = movePosition.y - offsetY;
        //获取到C坐标
        float CX = movePosition.x - offsetX;
        float CY = movePosition.y + offsetY;
        //获取到D坐标
        float DX = initPosition.x - offsetX;
        float DY = initPosition.y + offsetY;

3.3 计算中心点坐标

还需要计算出起点坐标跟终点坐标的中心点
c o n X = x 1 + x 2 2 conX =\frac{x_1 + x_2}{2} conX=2x1+x2
c o n Y = y 1 + y 2 2 conY =\frac{y_1 + y_2}{2} conY=2y1+y2

代码如下:

 		//获取到起点坐标跟终点坐标的中心点
        float conX = (initPosition.x + movePosition.x)/2;
        float conY = (initPosition.y + movePosition.y)/2;

3.4 使用Path存储连接桥

使用Path存储连接桥的对象

 	    //初始化path对象
        mPath.reset();
        //将起点移动到A坐标
        mPath.moveTo(AX,AY);
        //从A坐标连接到B坐标
        mPath.quadTo(conX,conY,BX,BY);
        //从B点连接到C点
        mPath.lineTo(CX,CY);
        //从C点连接到D点
        mPath.quadTo(conX,conY,DX,DY);
        //从D点连接到A点
        mPath.lineTo(AX,AY);

4 其他优化

4.1 控制范围

控制在一定范围内可拖动,超出范围隐藏

先计算是否超出范围,代码如下:

   		//获取两个点之间的直线距离
        float s = (float) Math.sqrt(Math.pow(widthX,2) + Math.pow(widthY,2));

        if(s >= 400){
            isOut = true;
        }else {
            isOut = false;
        }

没有超出范围才去绘制,在dispatchDraw方法中添加如下代码:

           drawPath();

            if(!isOut){
                //画两个圆
                //画第一个圆,是初始化坐标的圆
                canvas.drawCircle(initPosition.x,initPosition.y,mRadius,mPaint);
                //画第二个圆,是终点的圆
                canvas.drawCircle(movePosition.x,movePosition.y,mRadius,mPaint);
                //画连接桥
                canvas.drawPath(mPath,mPaint);
            }

然后在手指抬起时候判断是否超出范围,如果超出范围显示爆炸效果

现在在init()方法中初始化ImageView

  		imageView = new ImageView(getContext());
        imageView.setLayoutParams(layoutParams);
        imageView.setImageResource(R.drawable.tip_anim);
        this.addView(imageView);

手指抬起,如果超出范围,textView隐藏,并显示爆炸效果,代码如下:

 case MotionEvent.ACTION_UP:
                isClicked = false;
                if(isOut){
                    textView.setVisibility(View.GONE);
                    imageView.setX(movePosition.x - imageView.getWidth()/2);
                    imageView.setY(movePosition.y - imageView.getHeight()/2);
                    imageView.setVisibility(View.VISIBLE);
                    ((AnimationDrawable) imageView.getDrawable()).start();
                }
                break;

在这里插入图片描述


4.2 动态改变起始圆的大小

在drawPath方法中添加如下代码:

        mRadius = 40 - s/30;

在这里插入图片描述

贴上完整代码:

WaterView.java

public class WaterView extends FrameLayout {

    //定义一个文本控件
    TextView textView;
    //文本框的初始坐标
    private PointF initPosition;
    //手指移动到的坐标
    private PointF movePosition;
    private boolean isClicked = false;
    //绘制的圆的半径
    private float mRadius = 40;
    //绘制的画笔
    private Paint mPaint;
    //存储连接桥的对象
    private Path mPath;

    //判断文本框是否离开某个范围
    private boolean isOut = false;

    //爆炸效果的图片控件
    private ImageView imageView;

    public WaterView(Context context) {
        super(context);
        init();
    }

    /**
     * 初始化整个效果的控件
     */
    private void init() {

        initPosition = new PointF(500, 500);
        movePosition = new PointF();

        //初始化画笔的样式为填充
        mPaint = new Paint();
        mPaint.setColor(Color.RED);
        mPaint.setStyle(Paint.Style.FILL);

        mPath = new Path();

        textView = new TextView(getContext());
        textView.setPadding(20, 20, 20, 20);
        textView.setTextColor(Color.WHITE);
        textView.setText("99+");
        textView.setBackgroundResource(R.drawable.tv_bg);
        LayoutParams layoutParams = new LayoutParams(
                ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
        textView.setLayoutParams(layoutParams);

        this.addView(textView);

        imageView = new ImageView(getContext());
        imageView.setLayoutParams(layoutParams);
        imageView.setImageResource(R.drawable.tip_anim);
        this.addView(imageView);

    }

    /**
     * 绘制当前控件里面的内容的控件
     */
    @Override
    protected void dispatchDraw(Canvas canvas) {

        //保存canvas的状态
        canvas.save();

        if (isClicked) {
            textView.setX(movePosition.x - textView.getWidth() / 2);
            textView.setY(movePosition.y - textView.getHeight() / 2);

            drawPath();

            if(!isOut){
                //画两个圆
                //画第一个圆,是初始化坐标的圆
                canvas.drawCircle(initPosition.x,initPosition.y,mRadius,mPaint);
                //画第二个圆,是终点的圆
                canvas.drawCircle(movePosition.x,movePosition.y,mRadius,mPaint);
                //画连接桥
                canvas.drawPath(mPath,mPaint);
            }

        } else {
            //设置初始坐标为控件的中心点
            textView.setX(initPosition.x - textView.getWidth() / 2);
            textView.setY(initPosition.y - textView.getHeight() / 2);
        }

        // 恢复canvas的状态
        canvas.restore();
        super.dispatchDraw(canvas);

    }

    public void drawPath(){
        //获取到终点与起点的X坐标的差值 A2
        float widthX = movePosition.x - initPosition.x;
        //获取到终点与起点的Y坐标的差值 A3
        float widthY = movePosition.y - initPosition.y;

        //获取两个点之间的直线距离
        float s = (float) Math.sqrt(Math.pow(widthX,2) + Math.pow(widthY,2));

        mRadius = 40 - s/30;

        if(s >= 400){
            isOut = true;
        }else {
            isOut = false;
        }

        //得到三角形的锐角的角度值 正切值
        double atan = Math.atan(widthY / widthX);

        //获取到offsetX的长度
        float offsetX = (float) (mRadius * Math.sin(atan));
        //获取到offsetY的长度
        float offsetY = (float) (mRadius * Math.cos(atan));


        //获取到A坐标
        float AX = initPosition.x + offsetX;
        float AY = initPosition.y - offsetY;
        //获取到B坐标
        float BX = movePosition.x + offsetX;
        float BY = movePosition.y - offsetY;
        //获取到C坐标
        float CX = movePosition.x - offsetX;
        float CY = movePosition.y + offsetY;
        //获取到D坐标
        float DX = initPosition.x - offsetX;
        float DY = initPosition.y + offsetY;

        //获取到起点坐标跟终点坐标的中心点
        float conX = (initPosition.x + movePosition.x)/2;
        float conY = (initPosition.y + movePosition.y)/2;

        //初始化path对象
        mPath.reset();
        //将起点移动到A坐标
        mPath.moveTo(AX,AY);
        //从A坐标连接到B坐标
        mPath.quadTo(conX,conY,BX,BY);
        //从B点连接到C点
        mPath.lineTo(CX,CY);
        //从C点连接到D点
        mPath.quadTo(conX,conY,DX,DY);
        //从D点连接到A点
        mPath.lineTo(AX,AY);

    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:

                movePosition.set(initPosition.x, initPosition.y);

                //判断当前位置是否在文本控件里面
                //这个对象是用来封装文本控件的范围的对象
                Rect rect = new Rect();
                int[] location = new int[2];

                //获取到textView控件在窗体中的X,Y坐标
                textView.getLocationOnScreen(location);

                //初始化Rect对象
                rect.left = location[0];
                rect.top = location[1];
                rect.right = location[0] + textView.getWidth();
                rect.bottom = location[1] + textView.getHeight();

                //判断当前点击的坐标是否是在范围之内
                //getRawX和getRawY是相对于父控件
                if (rect.contains((int) event.getRawX(), (int) event.getRawY())) {
                    isClicked = true;
                }

                break;
            case MotionEvent.ACTION_UP:
                isClicked = false;
//                movePosition.set(initPosition.x, initPosition.y);

                if(isOut){
                    textView.setVisibility(View.GONE);
                    imageView.setX(movePosition.x - imageView.getWidth()/2);
                    imageView.setY(movePosition.y - imageView.getHeight()/2);
                    imageView.setVisibility(View.VISIBLE);
                    ((AnimationDrawable) imageView.getDrawable()).start();

                }
                break;
            case MotionEvent.ACTION_MOVE:
                //getX和getY是相对于屏幕的坐标
                movePosition.set(event.getX(), event.getY());
                break;

        }

        //通过这个API可以调用到dispatchDraw的方法
        postInvalidate();
        return true;
    }
}

tip_naim.xml

<?xml version="1.0" encoding="utf-8"?>
<animation-list
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:oneshot="true">
    <item android:drawable="@drawable/idp" android:duration="300"/>
    <item android:drawable="@drawable/idq" android:duration="300"/>
    <item android:drawable="@drawable/idr" android:duration="300"/>
    <item android:drawable="@drawable/ids" android:duration="300"/>
    <item android:drawable="@drawable/idt" android:duration="300"/>
    <item android:drawable="@android:color/transparent" android:duration="300"/>
</animation-list>

tv_bg.xml

<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
    <corners android:radius="10dp"/>
    <solid android:color="#ff0000"/>
    <stroke android:color="#0f000000" android:width="1dp"/>
</shape>

idp.png
idq.png
idr.png
ids.png
idt.png
在这里插入图片描述

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
Github:
https://github.com/345166018/AndroidUI/tree/master/HxQQRedPoint

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值