1 简介
在做项目的过程中,有很多都会涉及「消息」这一块的内容,未读消息,会有一个圆形气泡提示未读消息数量,已读就不再显示。这个效果在QQ中做的很漂亮,今天我们在此效果上继续实现「任意控件都可以拖拽消失」。「文末有福利」
图1. 任意控件View拖拽爆炸效果.gif话不多说直接进入主题,图1就是我们这篇文章要实现的效果,任意View控件都可以实现拖拽爆炸效果。封装后的好处是:拿过来可以直接用,完全实现了效果与特定View分离,一行代码搞定。上代码:
// 通过我们的贝塞尔 View, 绑定任意想要拖拽的控件 view
MessageBubbleView.bindMessageView(findViewById(R.id.text_view), new OnMessageBubbleTouchListener.OnViewDragDisappearListener() {
@Override
public void onDisappear(View originalView) {
// 该 originalView 就是拖拽消失掉的 View
Toast.makeText(MainActivity.this, "TextView 控件消失了", Toast.LENGTH_SHORT).show();
}
});
不要着急,在实现 图1 的效果之前,我们先要实现下 图2 简单的消息拖拽效果。
图2. 在任意位置实现消息拖拽效果.gif2 简单的消息拖拽实现
我们先来分析 图2 :在任意位置按下并拖动,会出现两个一大一小实心圆,中间被一类似粘稠物连接,我们索性就称作一个固定圆和一个拖拽圆;
在拖拽圆拖拽的过程中,拖拽圆的大小是不变的,但是位置跟随手指移动;固定圆的圆心是不变的,但是半径是可变的,刚开始拖拽时,固定圆的圆心是最大的,两圆的距离越远,固定圆的半径越小,反之逐渐变大。
有了思路,我们就写代码,按下时先绘制两个圆,并实现拖拽变化
/*
实现思路:
1.手指按下的时候,绘制出两个圆(固定圆和拖拽圆)
固定圆的圆心位置固定,但是半径可发生变化
拖拽圆的圆心可变,半径固定
2.手指拖动的时候,不断更新拖拽圆的位置(不断的绘制),
同时改变固定圆的圆心大小(两个圆越近,固定圆半径越大;两圆越远,固定圆的半径越小;
两圆距离超过一定值时,固定圆消失不见
*/
public class MessageBubbleView extends View {
// 两个实心圆--根据点的坐标来绘制圆
private PointF mDragPoint, mFixationPoint;
private Paint mPaint;
private int mDragRadius = 9; // 拖拽圆半径
// 固定圆最大半径(初始半径)/半径的最小值
private int mFixationRadiusMax = 7;
private int mFixationRadiusMin = 3;
private int mFixationRadius;
public MessageBubbleView(Context context) {
this(context, null);
}
public MessageBubbleView(Context context, @Nullable AttributeSet attrs) {
this(context, attrs, 0);
}
public MessageBubbleView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
mPaint = new Paint();
mPaint.setColor(Color.RED);
mPaint.setAntiAlias(true);
mPaint.setDither(true);
mDragRadius = dip2px(mDragRadius);
mFixationRadiusMax = dip2px(mFixationRadiusMax);
mFixationRadiusMin = dip2px(mFixationRadiusMin);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
// 手指按下的时候,要在当前的位置初始化绘制两个圆
float downX = event.getX();
float downY = event.getY();
initPoint(downX, downY);
break;
case MotionEvent.ACTION_MOVE:
// 在移动的时候,不断的更新位置
float moveX = event.getX();
float moveY = event.getY();
updateDragPointLocation(moveX, moveY);
break;
}
invalidate();
return true;
}
private void updateDragPointLocation(float moveX, float moveY) {
mDragPoint.x = moveX;
mDragPoint.y = moveY;
}
@Override
protected void onDraw(Canvas canvas) {
if (mFixationPoint == null || mDragPoint == null) {
return;
}
// 画两个圆: 固定圆有一个初始化大小,而且随着两圆距离的增大而变小,小到一定程度就不见了(不画了)
// 拖拽圆 半径不变,位置跟随手指移动
canvas.drawCircle(mDragPoint.x, mDragPoint.y, mDragRadius, mPaint);
double distance = getPointsDistance(mDragPoint, mFixationPoint);
// 随着拖拽的距离变化,逐渐改变固定圆的半径
mFixationRadius = (int) (mFixationRadiusMax - distance / 16); // 这个除的值来控制固定圆消失时的距离
if (mFixationRadius > mFixationRadiusMin) {
canvas.drawCircle(mFixationPoint.x, mFixationPoint.y, mFixationRadius, mPaint);
}
}
/**
* 获取两个点之间的距离(勾股定理)
*/
private double getPointsDistance(PointF point1, PointF point2) {
return Math.sqrt((point1.x - point2.x) * (point1.x - point2.x) + (point1.y - point2.y) * (point1.y - point2.y));
}
/**
* 初始化点
*/
private void initPoint(float downX, float downY) {
mFixationPoint = new PointF(downX, downY);
mDragPoint = new PointF(downX, downY);
}
private void updateDragPointLocation(float moveX, float moveY) {
mDragPoint.x = moveX;
mDragPoint.y = moveY;
}
private int dip2px(int dip) {
return (int) dip, getResources().getDisplayMetrics());
}
}
代码写到这里运行一下,效果如下:
result1.gif
初步效果已经出来了,接下来我们去实现拖动时的贝塞尔曲线效果。
关于贝塞尔曲线的概念知识,这里不是重点,就不再说明,如果不了解请先自行百度,下面上拖拽时的手绘图:
贝塞尔曲线分析.png这张图就是一些简单的数据计算,不难理解,就是先假设所有条件都已知,计算出我们关心的P0,P1,P2 和 P3 点的坐标,然后再想办法求出∠a的值即可;图中红线和蓝线围起来的区域就是我们要实现的粘性区域。下面用代码来实现:
/**
* 获取贝塞尔曲线路径
*/
private Path getBesaierPath() {
double distance = getPointsDistance(mDragPoint, mFixationPoint);
// 随着拖拽的距离变化,不断改变固定圆的半径
mFixationRadius = (int) (mFixationRadiusMax - distance / 16);
if (mFixationRadius < mFixationRadiusMin) {
// 超过一定距离 贝塞尔曲线和固定圆都不要绘制了
return null;
}
Path besaierPath = new Path();
// 求角a
double angleA = Math.atan((mDragPoint.y - mFixationPoint.y) / (mDragPoint.x - mFixationPoint.x));
float P0x = (float) (mFixationPoint.x + mFixationRadius * Math.sin(angleA));
float P0y = (float) (mFixationPoint.y - mFixationRadius * Math.cos(angleA));
float P3x = (float) (mFixationPoint.x - mFixationRadius * Math.sin(angleA));
float P3y = (float) (mFixationPoint.y + mFixationRadius * Math.cos(angleA));
float P1x = (float) (mDragPoint.x + mDragRadius * Math.sin(angleA));
float P1y = (float) (mDragPoint.y - mDragRadius * Math.cos(angleA));
float P2x = (float) (mDragPoint.x - mDragRadius * Math.sin(angleA));
float P2y = (float) (mDragPoint.y + mDragRadius * Math.cos(angleA));
// 拼接 贝塞尔曲线路径
// 移动到我们的起始点,否则默认从(0,0)开始
besaierPath.moveTo(P0x, P0y);
// 求控制点坐标,我们取两圆圆心为控制点(如果取黄金比例0.618是比较好的)
PointF controlPoint = getControlPoint();
// 画第一条 前两个参数为控制点坐标 后两个参数为终点坐标
besaierPath.quadTo(controlPoint.x, controlPoint.y, P1x, P1y);
besaierPath.lineTo(P2x, P2y);
besaierPath.quadTo(controlPoint.x, controlPoint.y, P3x, P3y);
besaierPath.close();
return besaierPath;
}
下面就很简单了,在onDraw() 中,在绘制固定圆的同时绘制曲线。代码如下:
// 绘制贝塞尔曲线 如果两圆拖拽到一定距离,固定圆消失的同时不再绘制贝塞尔曲线
Path besaierPath = getBesaierPath();
if (besaierPath != null) {
// 固定圆半径可变 当拖拽在一定距离时才去绘制,超过一定距离就不在绘制
canvas.drawCircle(mFixationPoint.x, mFixationPoint.y, mFixationRadius, mPaint);
canvas.drawPath(besaierPath, mPaint);
}
到这里我们已经实现了「简单的消息拖拽」在任意的位置都可以实现消息拖拽效果了。
3 任意 View 控件拖拽爆炸消失
「重点来了」就 图1 效果,先整理下思路:
- 如何做到任意View都可以拖动
- 当拖动不超过一定距离时,该View会回弹到原来的位置,还可以继续下一次的拖动
- 当拖动超过一定距离时,会有一个爆炸消失的效果
- 如何通知用户,你的控件消失了(监听回调)
下面我们一一来解决上面的问题
- 我们给 MessageBubbleView 开发一个方法,用于绑定待拖拽View并处理触摸事件
- 重写触摸监听
用户按下时,把原来的控件隐藏,我们通过 WindowManager 添加一个 View,该 View 是被隐藏掉的View的「快照」;
我们拖动的是这个「快照」,当拖拽超过一定距离时,从 WindowManager 上移除该快照,并实现一个爆炸动画;
如果用户拖动没有超过该距离值,松手时该快照做回弹动画,动画执行完毕,让真实的View再次显示出来,就可以再次执行拖拽了;
好了,有什么样的想法,就有什么样的行动,我们用代码写出来:
自定义 View 的触摸监听 OnMessageBubbleTouchListener.java 中
@Override
public boolean onTouch(View v, MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
// 搞一个原始View的快照,并添加WindowManger中
mWindowManager.addView(mMessageBubbleView, mParams);
// 初始化贝塞尔View的中心点 也是原始View的中心点
int[] location = new int[2];
mOriginalView.getLocationOnScreen(location); //默认获取的是View左上角在屏幕上的坐标(y坐标包含状态栏的高度)
mMessageBubbleView.initPoint(location[0] + mOriginalView.getWidth() / 2, location[1] + mOriginalView.getHeight() / 2 - getStatusBarHeight(mContext));
// 这里需要减去状态栏的高度,然后在window上的位置才对
// 为什么不设置左上角呢? 拖拽时贝塞尔曲线会连到左上角 不美观
Bitmap copyBitmap = getCopyBitmapFromView(mOriginalView);
// 给拖拽的消息View设置一张原始View的快照
mMessageBubbleView.setDragBitmap(copyBitmap);
// 已经绘制过后再把原来的隐藏掉
//mOriginalView.setVisibility(View.INVISIBLE);
break;
case MotionEvent.ACTION_MOVE:
// 解决一点击View出现闪动的bug
if (mOriginalView.getVisibility() == View.VISIBLE) {
mOriginalView.setVisibility(View.INVISIBLE);
}
mMessageBubbleView.updateDragPointLocation(event.getRawX(), event.getRawY() - BubbleUtils.getStatusBarHeight(mContext)); // 同样要减去状态栏高度
break;
case MotionEvent.ACTION_UP:
mMessageBubbleView.handleActionUP();
break;
}
return true;
}
在触摸监听中同时再定义一个View消失的监听回调,该控件一旦爆炸消失,就会调用该方法。
/**
* 真正的处理View的消失的监听
*/
public interface OnViewDragDisappearListener {
/**
* 原始View消失的监听
*
* @param originalView 原始的View
*/
void onDisappear(View originalView);
}
……省略代码……
/**
* 拖拽的View消失时的监听方法
*
* @param pointF
*/
@Override
public void onViewDragDisappear(PointF pointF) {
// 移除消息气泡贝塞尔View,同时添加一个爆炸的View动画
mWindowManager.removeView(mMessageBubbleView);
mWindowManager.addView(mBombLayout, mParams);
mBombView.setBackgroundResource(R.drawable.anim_bubble_bomb);
AnimationDrawable bombDrawable = (AnimationDrawable) mBombView.getBackground();
// 矫正爆炸时,位置偏下的问题
mBombView.setX(pointF.x - bombDrawable.getIntrinsicWidth() / 2);
mBombView.setY(pointF.y - bombDrawable.getIntrinsicHeight() / 2);
bombDrawable.start();
mBombView.postDelayed(new Runnable() {
@Override
public void run() {
// 动画执行完毕,把爆炸布局及时从WindowManager移除
mWindowManager.removeView(mBombLayout);
if (mDisappearListener != null) {
mDisappearListener.onDisappear(mOriginalView);
}
}
}, getAnimationTotalTime(bombDrawable));
}
/**
* 松手后,拖拽View消失,原来的View重新显示的监听方法
*/
@Override
public void onViewDragRestore() {
mWindowManager.removeView(mMessageBubbleView);
mOriginalView.setVisibility(View.VISIBLE);
}
到这里基本也就实现的差不多了,细节代码就不再贴了,可以动手写一写。