1 简介
在做项目的过程中,有很多都会涉及「消息」这一块的内容,未读消息,会有一个圆形气泡提示未读消息数量,已读就不再显示。这个效果在QQ中做的很漂亮,今天我们在此效果上继续实现「任意控件都可以拖拽消失」。「文末有福利」
话不多说直接进入主题,图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 简单的消息拖拽实现
我们先来分析 图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) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dip, getResources().getDisplayMetrics()); } }
代码写到这里运行一下,效果如下:
初步效果已经出来了,接下来我们去实现拖动时的贝塞尔曲线效果。
关于贝塞尔曲线的概念知识,这里不是重点,就不再说明,如果不了解请先自行百度,下面上拖拽时的手绘图:
这张图就是一些简单的数据计算,不难理解,就是先假设所有条件都已知,计算出我们关心的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 效果,先整理下思路:
1. 如何做到任意View都可以拖动
2. 当拖动不超过一定距离时,该View会回弹到原来的位置,还可以继续下一次的拖动
3. 当拖动超过一定距离时,会有一个爆炸消失的效果
4. 如何通知用户,你的控件消失了(监听回调)
下面我们一一来解决上面的问题
1. 我们给 MessageBubbleView 开发一个方法,用于绑定待拖拽View并处理触摸事件
2. 重写触摸监听
用户按下时,把原来的控件隐藏,我们通过 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); }
到这里基本也就实现的差不多了,细节代码就不再贴了,可以动手写一写。
点我 我是源码 来获取源码,验证码为「fw6z」。
这是我在简书写的第一篇文章,如果有错误请指出,同时如果文章对您有帮助,请点个「心」给予支持。
我是标签: 怎么理解贝塞尔曲线? 贝塞尔曲线
感谢 大牛Darren, 从他的博客里学到了很多很实用知识,推荐大家学习。
作者:firstxia168