文章目录
参考:
https://blog.csdn.net/harvic880925/article/details/51615221
实现步骤:
- 自定义控件,监听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}
θ=atanx2−x1y2−y1
代码如下:
//获取到终点与起点的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=y1−offsety
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=y2−offsety
C
X
=
x
2
−
o
f
f
s
e
t
x
CX = x_2 - offsetx
CX=x2−offsetx
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=x1−offsetx
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