QQ气泡效果剖析
对于QQ汽泡效果我想不用多说了,都非常的熟悉,而且当时这效果出来简直亮瞎眼了,挺炫的,这里再来感受下:
而这次只实现单个汽泡的效果,并不涉及到加入Listview上的处理,一步步来,先上一下最终这次要实现的效果:
分析:
对于这么复杂的效果首先得要将它的功能进行拆解,首先先静止观察其效果:
两端可以分为两个圆:
然后中间连接成曲线:
这时再将空心的地方进行红颜色填充,如下:
最终是不是就达到了类似的汽泡可以拖拽的效果了:
这是对于整个动作的拆解,所以可以看出对于这个效果的实现,可以拆解成三个图形的绘制:两圆、中间连接图形:
而在正式编码实现之前,还得给上面图形进行一个身份明确:
拖拽圆:表示该圆是随手指可以去拖动的圆。
固定圆:表示该圆是固定不能随手指拖动的。
中间图形:表示需要将它与两圆进行相交合并的图形。
而当中间图形与圆进行汇合时产生的焦点也将其概念化:
所以对于中间图形就可以这样定义了:
为什么要定义好这些东东呢?在代码实现的时候会比较清晰,另外变量命名按照上面的概念去写可读性也比较好,所以说这个分析很有必要!
明确了这些概念之后下面则可以开始正式的代码编写啦,不过会细分成很多步骤一点点去实现:
绘制两个静态圆【不考虑动态效果】:
具体框架的搭建这里就省略了,直接贴上View的自定义实现,由于这一步比较简单,直接上代码:
/** * Goo:是粘性的意思,QQ汽泡效果 * 绘制两个圆,此时圆是固定的,先不考虑随手指变化的问题 */ public class GooView extends View { private Paint paint; /* 固定圆的圆心 */ private PointF stableCenter = new PointF(200f, 200f); /* 固定圆的半径 */ private float stableRadius = 20f; /* 拖拽圆的圆心 */ private PointF dragCenter = new PointF(100f, 100f); /* 拖拽圆的半径 */ private float dragRadius = 30f; public GooView(Context context) { this(context, null); } public GooView(Context context, @Nullable AttributeSet attrs) { this(context, attrs, 0); } public GooView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); init(); } private void init() { paint = new Paint(); paint.setAntiAlias(true); paint.setColor(Color.RED); } @Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); //绘制两个固定圆:固定圆、拖拽圆 canvas.drawCircle(stableCenter.x, stableCenter.y, stableRadius, paint); canvas.drawCircle(dragCenter.x, dragCenter.y, dragRadius, paint); } }
编译运行:
绘制中间图形:
对于这个不规则的图形需要用到Path的绘制了,而有两条曲线则需要用到贝赛尔曲线,关于这方面的知识在之前已经学习过了【http://www.cnblogs.com/webor2006/p/7341697.html】,所以直接看下代码:
/** * Goo:是粘性的意思,QQ汽泡效果 * 中间图形的绘制,等它绘制好了下一步就可以将它与圆进行整合 */ public class GooView extends View { private Paint paint; /* 固定圆的圆心 */ private PointF stableCenter = new PointF(200f, 200f); /* 固定圆的半径 */ private float stableRadius = 20f; /* 固定圆的两个附着点 */ private PointF[] stablePoints = new PointF[]{ new PointF(200f, 300f), new PointF(200f, 350f) }; /* 拖拽圆的圆心 */ private PointF dragCenter = new PointF(100f, 100f); /* 拖拽圆的半径 */ private float dragRadius = 30f; /* 拖拽圆的两个附着点 */ private PointF[] dragPoints = new PointF[]{ new PointF(100f, 300f), new PointF(100f, 350f) }; /* 绘制中间不规则的路径 */ private Path path; /* 贝塞尔曲线的控制点 */ private PointF controlPoint = new PointF(150f, 325f); public GooView(Context context) { this(context, null); } public GooView(Context context, @Nullable AttributeSet attrs) { this(context, attrs, 0); } public GooView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); init(); } private void init() { paint = new Paint(); paint.setAntiAlias(true); paint.setColor(Color.RED); path = new Path(); } @Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); //绘制两个固定圆:固定圆、拖拽圆 canvas.drawCircle(stableCenter.x, stableCenter.y, stableRadius, paint); canvas.drawCircle(dragCenter.x, dragCenter.y, dragRadius, paint); //绘制中间图形,其步骤为: // 1、移动到固定圆的附着点1; path.moveTo(stablePoints[0].x, stablePoints[0].y); // 2、向拖拽圆附着点1绘制贝塞尔曲线; path.quadTo(controlPoint.x, controlPoint.y, dragPoints[0].x, dragPoints[0].y); // 3、向拖拽圆的附着点2绘制直线; path.lineTo(dragPoints[1].x, dragPoints[1].y); // 4、向固定圆的附着点2绘制贝塞尔曲线; path.quadTo(controlPoint.x, controlPoint.y, stablePoints[1].x, stablePoints[1].y); // 5、闭合; path.close(); canvas.drawPath(path, paint); } }
编译运行:
将中间图形与两圆进行整合,实现初步的拖拽雏形【重要!】:
接下来就到了一个关键步骤,上一步的附着点是用的静态定死的数据,而这一步要做的就是依据现有的两个圆来将附着点及曲线的控制点改为活的,所以这里涉及到了一些计算的工作,下面一步步来实现:
首先去掉上步骤中定死的附着点与控制点的数据:
问题的核心就回到了如何计算附着点了,只有附着点确定了,那控制点就很容易就可以求出来了,那倒底如何计算呢?下面用图来说明下:
用直接连着两个圆的圆心,接着再基于这条直线做垂直线,如下:
那究境得如何算出这两个附着点呢?首先根据两圆心相对水平线是有一个夹角的:
而目前已经知道的是圆心与圆的半径,要求出这两上点继续可以做出延长线,如下:
接下来就比较重要啦,其中图中的角度其实都是一样大的:
这里不论证了,从图中就可以观察出来,这时就有一个关键点啦!想办法求得这个角度的sin或cos,具体如下:
①、利用两中心点的连接可以求得斜率:已知两点坐标:(x1,y1),(x2,y2),其斜率为:k=(y2-y1)/(x2-x1);
②、有了斜率就可以求得这个角度的弧度值:radian = Math.atan(k);
③、有了弧度值,那sin和cos就可以利用Math类求得具体值啦,而参数刚好是传的弧度值。
【说明】:对于斜率及弧度的求解在之前博客中也已经有用到过。
知道了角度的sin和cos,那两个附着点的计算就迎刃而解啦:
整个计算公式如下:
k=(y2-y1)/(x2-x1)
radian = Math.atan(k);
xOffset = (float) (Math.sin(radian) * radius);
yOffset = (float) (Math.cos(radian) * radius);
第一个拖拽圆的附着点结果为:dragPoints[0] = (dragCenter.x - xOffset, dragCenter.y + yOffset);
而同理,由于另一个三角函数计算的xOffset和yOffset是一模一样的:
所以另一个附着点的结果也就出来了,结果为:dragPoints[0] = (dragCenter.x + xOffset, dragCenter.y + yOffset);
所以这里可以将这个附着点的计算封装成一个工具方法,如下:
只要理解上刚才的推导过程,对于这个工具方法就完全能够理解啦,好了这块比较绕,但是也是实现的核心,回到正题,有了这个工具方法,那接下来就可以算得两个圆真实的附着点了,如下:
最后还差一个曲线的控制点,这个比较容易了,就不过多解释了,取的就是两点的中点,这里也将其封装成工具方法:
修改代码如下:
编译运行:
【提示】:关于附着点的计算还有另外一种更加直观的计算方法,不过稍麻烦一些,但是比较好理解,具体可以参考博文:http://www.jb51.net/article/108265.htm
实现拖动效果:
首先得重写onTouchEvent():
然后去获取当前触摸的坐标点:
对于上面获取的方法需要注意:这是点击的点是距离当前自定义控件的左边缘的距离,而最终我们会将其用到列表当中的,那时当前控件一丁点大,所以这种获取坐标的方式不可行,得用另外一个api,获取距离手机屏幕的左边缘的距离,如下:
看下效果:
呃~~怎么这个样子,确实是点到哪拖拽圆就到了哪,但是之前绘制的貌似木有消失,这其实是之前也遇到过绘制path的问题【http://www.cnblogs.com/webor2006/p/7401912.html】,因为path会记录上一次绘制的颜色,解决之道:在每次绘制完path之后则需要reset()一下,如下:
嗯~~完美!!!真的完美了么,其实还是有一个小BUG的,看:
实际上这个差的高度就是状态栏的高度:
因为:
所以需要获得状态栏的高度,然后在rawY上将其减掉,那如何获取状态栏的高度呢?
/** * Goo:是粘性的意思,QQ汽泡效果 * 处理移动效果初步:图形会随着手指的移动而移动 */ public class GooView extends View { private Paint paint; /* 固定圆的圆心 */ private PointF stableCenter = new PointF(200f, 200f); /* 固定圆的半径 */ private float stableRadius = 20f; /* 固定圆的两个附着点 */ private PointF[] stablePoints; /* 拖拽圆的圆心 */ private PointF dragCenter = new PointF(100f, 100f); /* 拖拽圆的半径 */ private float dragRadius = 30f; /* 拖拽圆的两个附着点 */ private PointF[] dragPoints; /* 绘制中间不规则的路径 */ private Path path; /* 贝塞尔曲线的控制点 */ private PointF controlPoint; /* 状态栏高度 */ private int statusBarHeight; public GooView(Context context) { this(context, null); } public GooView(Context context, @Nullable AttributeSet attrs) { this(context, attrs, 0); } public GooView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); init(); } private void init() { paint = new Paint(); paint.setAntiAlias(true); paint.setColor(Color.RED); path = new Path(); } @Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); //绘制两个固定圆:固定圆、拖拽圆 canvas.drawCircle(stableCenter.x, stableCenter.y, stableRadius, paint); canvas.drawCircle(dragCenter.x, dragCenter.y, dragRadius, paint); //绘制中间图形,直接基于两圆来绘制,其步骤为: //一、基于两圆来计算真实的附着点与曲线的控制点 float dx = dragCenter.x - stableCenter.x; float dy = dragCenter.y - stableCenter.y; double linek = 0; if (dx != 0) {//被除数不能为0,防止异常 linek = dy / dx; } //得到实际的附着点 dragPoints = GeometryUtil.getIntersectionPoints(dragCenter, dragRadius, linek); stablePoints = GeometryUtil.getIntersectionPoints(stableCenter, stableRadius, linek); controlPoint = GeometryUtil.getMiddlePoint(dragCenter, stableCenter); //二、开始绘制中间图形 // 1、移动到固定圆的附着点1; path.moveTo(stablePoints[0].x, stablePoints[0].y); // 2、向拖拽圆附着点1绘制贝塞尔曲线; path.quadTo(controlPoint.x, controlPoint.y, dragPoints[0].x, dragPoints[0].y); // 3、向拖拽圆的附着点2绘制直线; path.lineTo(dragPoints[1].x, dragPoints[1].y); // 4、向固定圆的附着点2绘制贝塞尔曲线; path.quadTo(controlPoint.x, controlPoint.y, stablePoints[1].x, stablePoints[1].y); // 5、闭合; path.close(); canvas.drawPath(path, paint); path.reset(); } @Override public boolean onTouchEvent(MotionEvent event) { switch (event.getAction()) { case MotionEvent.ACTION_DOWN: //event.getX();//点击的点距离当前自定义控件的左边缘的距离 float rawX = event.getRawX();//点击的点距离当前手机屏幕的左边缘的距离 float rawY = event.getRawY() - statusBarHeight; dragCenter.set(rawX, rawY); invalidate(); break; case MotionEvent.ACTION_MOVE: break; case MotionEvent.ACTION_UP: break; } //return super.onTouchEvent(event); return true;//表示当前控件想要处理事件 } @Override protected void onSizeChanged(int w, int h, int oldw, int oldh) { super.onSizeChanged(w, h, oldw, oldh); statusBarHeight = getStatusBarHeight(this); } /** * 获取状态栏高度 * * @param view * @return */ private int getStatusBarHeight(View view) { Rect rect = new Rect(); //获取视图对应的可视范围,会把视图的左上右下的数据传入到一个矩形中 view.getWindowVisibleDisplayFrame(rect); return rect.top; } }
这时再看效果:
完美解决,但是还有一种更加优雅的解决之道,那就是去移动画布,而不是去让Y坐标去减状态栏的高度,如下:
/** * Goo:是粘性的意思,QQ汽泡效果 * 处理移动效果初步:图形会随着手指的移动而移动 */ public class GooView extends View { private Paint paint; /* 固定圆的圆心 */ private PointF stableCenter = new PointF(200f, 200f); /* 固定圆的半径 */ private float stableRadius = 20f; /* 固定圆的两个附着点 */ private PointF[] stablePoints; /* 拖拽圆的圆心 */ private PointF dragCenter = new PointF(100f, 100f); /* 拖拽圆的半径 */ private float dragRadius = 30f; /* 拖拽圆的两个附着点 */ private PointF[] dragPoints; /* 绘制中间不规则的路径 */ private Path path; /* 贝塞尔曲线的控制点 */ private PointF controlPoint; /* 状态栏高度 */ private int statusBarHeight; public GooView(Context context) { this(context, null); } public GooView(Context context, @Nullable AttributeSet attrs) { this(context, attrs, 0); } public GooView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); init(); } private void init() { paint = new Paint(); paint.setAntiAlias(true); paint.setColor(Color.RED); path = new Path(); } @Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); canvas.save();//因为移动画布了,所以需要save()和restore()一下 canvas.translate(0, -statusBarHeight);//优雅的解决拖拽位置不是在圆心的问题,不用在触摸事件中单独处理了 //绘制两个固定圆:固定圆、拖拽圆 canvas.drawCircle(stableCenter.x, stableCenter.y, stableRadius, paint); canvas.drawCircle(dragCenter.x, dragCenter.y, dragRadius, paint); //绘制中间图形,直接基于两圆来绘制,其步骤为: //一、基于两圆来计算真实的附着点与曲线的控制点 float dx = dragCenter.x - stableCenter.x; float dy = dragCenter.y - stableCenter.y; double linek = 0; if (dx != 0) {//被除数不能为0,防止异常 linek = dy / dx; } //得到实际的附着点 dragPoints = GeometryUtil.getIntersectionPoints(dragCenter, dragRadius, linek); stablePoints = GeometryUtil.getIntersectionPoints(stableCenter, stableRadius, linek); controlPoint = GeometryUtil.getMiddlePoint(dragCenter, stableCenter); //二、开始绘制中间图形 // 1、移动到固定圆的附着点1; path.moveTo(stablePoints[0].x, stablePoints[0].y); // 2、向拖拽圆附着点1绘制贝塞尔曲线; path.quadTo(controlPoint.x, controlPoint.y, dragPoints[0].x, dragPoints[0].y); // 3、向拖拽圆的附着点2绘制直线; path.lineTo(dragPoints[1].x, dragPoints[1].y); // 4、向固定圆的附着点2绘制贝塞尔曲线; path.quadTo(controlPoint.x, controlPoint.y, stablePoints[1].x, stablePoints[1].y); // 5、闭合; path.close(); canvas.drawPath(path, paint); path.reset();//解决滑动时路径重叠的问题 canvas.restore(); } @Override public boolean onTouchEvent(MotionEvent event) { switch (event.getAction()) { case MotionEvent.ACTION_DOWN: //event.getX();//点击的点距离当前自定义控件的左边缘的距离 float rawX = event.getRawX();//点击的点距离当前手机屏幕的左边缘的距离 float rawY = event.getRawY()/* - statusBarHeight*/; dragCenter.set(rawX, rawY); invalidate(); break; case MotionEvent.ACTION_MOVE: break; case MotionEvent.ACTION_UP: break; } //return super.onTouchEvent(event); return true;//表示当前控件想要处理事件 } @Override protected void onSizeChanged(int w, int h, int oldw, int oldh) { super.onSizeChanged(w, h, oldw, oldh); statusBarHeight = getStatusBarHeight(this); } /** * 获取状态栏高度 * * @param view * @return */ private int getStatusBarHeight(View view) { Rect rect = new Rect(); //获取视图对应的可视范围,会把视图的左上右下的数据传入到一个矩形中 view.getWindowVisibleDisplayFrame(rect); return rect.top; } }
接着再来处理MOVE事件,其代码跟DOWN类似:
这时再看效果:
处理随着拖拽距离固定圆半径的变化:
接着处理当拖拽距离在变化时,固定圆的大小也随之变化的效果,也就是说固定圆的半径会进行变化了而不是像目前写死的半径一样,也就是说:
那问题的核心回到了如何去动态去得到变化的这个固定圆的半径呢?首先先拿预期的效果来分析一下:
很显示有一个最大拖拽距离,当超过这个距离之后则就会有个被拉断的效果,另外固定圆随着拖拽距离越来越大,最终它的缩放也有一个最小半径,所以先定义出这两上常是来:
而接着要算出两圆的距离占了移动最大距离的百分比,算出它之后,则可以用它来算出固定圆的变化半径了,所以百分比的计算首先得计算两圆心的距离,两点的距离计算公式如下:
所以可以将这个距离的求解也封装到工具类中,如下:
所以其百分比计算的代码如下:
接着根据百分比来算出临时拖拽圆的半径,其实就是从当前固定圆的半径到最小圆半径之间来根据百分比来计算,所以也可以将这个百分比计算半径的封装成工具方法,如下:
所以算临时半径如下:
然后将原来写死的固定圆的半径改成它:
编译运行:
处理超出最大拖拽范围时,有个拉断的效果:
这个实现就比较简单了,其思路是:当判断超出最大拖拽值时,则不绘制固定圆和中间图形,只剩下拖拽圆,所以说具体代码如下:
/** * Goo:是粘性的意思,QQ汽泡效果 * 处理超出最大拖拽范围时,有个拉断的效果 */ public class GooView extends View { /* 拖拽圆移动的最大距离 */ private static final float MAX_DRAG_DISTANCE = 200f; /* 固定圆缩放的最小半径 */ private static final float MIN_STABLE_RADIUS = 5f; private Paint paint; /* 固定圆的圆心 */ private PointF stableCenter = new PointF(200f, 200f); /* 固定圆的半径 */ private float stableRadius = 20f; /* 固定圆的两个附着点 */ private PointF[] stablePoints; /* 拖拽圆的圆心 */ private PointF dragCenter = new PointF(100f, 100f); /* 拖拽圆的半径 */ private float dragRadius = 30f; /* 拖拽圆的两个附着点 */ private PointF[] dragPoints; /* 绘制中间不规则的路径 */ private Path path; /* 贝塞尔曲线的控制点 */ private PointF controlPoint; /* 状态栏高度 */ private int statusBarHeight; /* 是否拖拽已经超出最大范围了,超出则不绘制拖拽圆和中间图形实现拉断效果 */ private boolean isOutOfRange = false; public GooView(Context context) { this(context, null); } public GooView(Context context, @Nullable AttributeSet attrs) { this(context, attrs, 0); } public GooView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); init(); } private void init() { paint = new Paint(); paint.setAntiAlias(true); paint.setColor(Color.RED); path = new Path(); } @Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); canvas.save(); canvas.translate(0, -statusBarHeight);//优雅的解决拖拽位置不是在圆心的问题,不用在触摸事件中单独处理了 //处理随着拖拽圆和固定圆的圆心距离越来越大,固定圆的半径越来越小:拖拽圆和固定圆圆心的距离百分比变化==固定圆半径的百分比变化 float distance = GeometryUtil.getDistanceBetween2Points(dragCenter, stableCenter);//获得两圆心的距离 float percent = distance / MAX_DRAG_DISTANCE;//计算百分比 float tempRadius = GeometryUtil.evaluateValue(percent, stableRadius, MIN_STABLE_RADIUS);//根据百分比来动态计算出固定圆的半径 if (tempRadius < MIN_STABLE_RADIUS)//处理最小半径 tempRadius = MIN_STABLE_RADIUS; if (!isOutOfRange) {//只有没有超出范围才绘制固定圆和中间图形 //绘制中间图形,直接基于两圆来绘制,其步骤为: //一、基于两圆来计算真实的附着点与曲线的控制点 float dx = dragCenter.x - stableCenter.x; float dy = dragCenter.y - stableCenter.y; double linek = 0; if (dx != 0) {//被除数不能为0,防止异常 linek = dy / dx; } //得到实际的附着点 dragPoints = GeometryUtil.getIntersectionPoints(dragCenter, dragRadius, linek); stablePoints = GeometryUtil.getIntersectionPoints(stableCenter, tempRadius, linek); controlPoint = GeometryUtil.getMiddlePoint(dragCenter, stableCenter); //二、开始绘制中间图形 // 1、移动到固定圆的附着点1; path.moveTo(stablePoints[0].x, stablePoints[0].y); // 2、向拖拽圆附着点1绘制贝塞尔曲线; path.quadTo(controlPoint.x, controlPoint.y, dragPoints[0].x, dragPoints[0].y); // 3、向拖拽圆的附着点2绘制直线; path.lineTo(dragPoints[1].x, dragPoints[1].y); // 4、向固定圆的附着点2绘制贝塞尔曲线; path.quadTo(controlPoint.x, controlPoint.y, stablePoints[1].x, stablePoints[1].y); // 5、闭合; path.close(); canvas.drawPath(path, paint); path.reset();//解决滑动时路径重叠的问题 ////绘制固定圆 canvas.drawCircle(stableCenter.x, stableCenter.y, tempRadius, paint); } //绘制拖拽圆 canvas.drawCircle(dragCenter.x, dragCenter.y, dragRadius, paint); canvas.restore(); } @Override public boolean onTouchEvent(MotionEvent event) { switch (event.getAction()) { case MotionEvent.ACTION_DOWN: //event.getX();//点击的点距离当前自定义控件的左边缘的距离 float rawX = event.getRawX();//点击的点距离当前手机屏幕的左边缘的距离 float rawY = event.getRawY()/* - statusBarHeight*/; dragCenter.set(rawX, rawY); invalidate(); break; case MotionEvent.ACTION_MOVE: rawX = event.getRawX(); rawY = event.getRawY(); dragCenter.set(rawX, rawY); //当拖拽超出一定范围后,固定圆和中间图形都消失了,其"消失了"用程序来说就是在onDraw()中不绘制某一段了 //判断拖拽的距离,判断距离是否超出最大距离 float distance = GeometryUtil.getDistanceBetween2Points(stableCenter, dragCenter); if (distance > MAX_DRAG_DISTANCE) { //当超出最大距离则不再对固定圆和中间图形进行绘制 isOutOfRange = true; } invalidate(); break; case MotionEvent.ACTION_UP: break; } //return super.onTouchEvent(event); return true;//表示当前控件想要处理事件 } @Override protected void onSizeChanged(int w, int h, int oldw, int oldh) { super.onSizeChanged(w, h, oldw, oldh); statusBarHeight = getStatusBarHeight(this); } /** * 获取状态栏高度 * * @param view * @return */ private int getStatusBarHeight(View view) { Rect rect = new Rect(); //获取视图对应的可视范围,会把视图的左上右下的数据传入到一个矩形中 view.getWindowVisibleDisplayFrame(rect); return rect.top; } }
其中在onDraw()中为了方便条件判断的编写代码,将圆的绘制顺序调到最后面去绘了,将绘制拖拽圆的代码单独放到条件语句之外,编译运行:
现在由于木有处理UP事件,所以说现在状态回不去了,为了方便调试,在DOWN时将状态还原,如下:
效果如下:
处理move和up均超出最大范围,当松手时则拖拽圆消失:
这个跟上一个步骤类似,只不过这时需要对UP事件进行处理了,先去处理判断条件,然后在onDraw()中去加入这个条件的处理,如下:
/** * Goo:是粘性的意思,QQ汽泡效果 * move和up均超出最大范围的处理:当松手时则拖拽圆消失 */ public class GooView extends View { /* 拖拽圆移动的最大距离 */ private static final float MAX_DRAG_DISTANCE = 200f; /* 固定圆缩放的最小半径 */ private static final float MIN_STABLE_RADIUS = 5f; private Paint paint; /* 固定圆的圆心 */ private PointF stableCenter = new PointF(200f, 200f); /* 固定圆的半径 */ private float stableRadius = 20f; /* 固定圆的两个附着点 */ private PointF[] stablePoints; /* 拖拽圆的圆心 */ private PointF dragCenter = new PointF(100f, 100f); /* 拖拽圆的半径 */ private float dragRadius = 30f; /* 拖拽圆的两个附着点 */ private PointF[] dragPoints; /* 绘制中间不规则的路径 */ private Path path; /* 贝塞尔曲线的控制点 */ private PointF controlPoint; /* 状态栏高度 */ private int statusBarHeight; /* 是否拖拽已经超出最大范围了,超出则不绘制拖拽圆和中间图形实现拉断效果 */ private boolean isOutOfRange = false; /* 是否全部消失,如果true则所有图形消失 */ private boolean isDisappear = false; public GooView(Context context) { this(context, null); } public GooView(Context context, @Nullable AttributeSet attrs) { this(context, attrs, 0); } public GooView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); init(); } private void init() { paint = new Paint(); paint.setAntiAlias(true); paint.setColor(Color.RED); path = new Path(); } @Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); canvas.save(); canvas.translate(0, -statusBarHeight);//优雅的解决拖拽位置不是在圆心的问题,不用在触摸事件中单独处理了 //处理随着拖拽圆和固定圆的圆心距离越来越大,固定圆的半径越来越小:拖拽圆和固定圆圆心的距离百分比变化==固定圆半径的百分比变化 float distance = GeometryUtil.getDistanceBetween2Points(dragCenter, stableCenter);//获得两圆心的距离 float percent = distance / MAX_DRAG_DISTANCE;//计算百分比 float tempRadius = GeometryUtil.evaluateValue(percent, stableRadius, MIN_STABLE_RADIUS);//根据百分比来动态计算出固定圆的半径 if (tempRadius < MIN_STABLE_RADIUS)//处理最小半径 tempRadius = MIN_STABLE_RADIUS; if (!isDisappear) { if (!isOutOfRange) {//只有没有超出范围才绘制固定圆和中间图形 //绘制中间图形,直接基于两圆来绘制,其步骤为: //一、基于两圆来计算真实的附着点与曲线的控制点 float dx = dragCenter.x - stableCenter.x; float dy = dragCenter.y - stableCenter.y; double linek = 0; if (dx != 0) {//被除数不能为0,防止异常 linek = dy / dx; } //得到实际的附着点 dragPoints = GeometryUtil.getIntersectionPoints(dragCenter, dragRadius, linek); stablePoints = GeometryUtil.getIntersectionPoints(stableCenter, tempRadius, linek); controlPoint = GeometryUtil.getMiddlePoint(dragCenter, stableCenter); //二、开始绘制中间图形 // 1、移动到固定圆的附着点1; path.moveTo(stablePoints[0].x, stablePoints[0].y); // 2、向拖拽圆附着点1绘制贝塞尔曲线; path.quadTo(controlPoint.x, controlPoint.y, dragPoints[0].x, dragPoints[0].y); // 3、向拖拽圆的附着点2绘制直线; path.lineTo(dragPoints[1].x, dragPoints[1].y); // 4、向固定圆的附着点2绘制贝塞尔曲线; path.quadTo(controlPoint.x, controlPoint.y, stablePoints[1].x, stablePoints[1].y); // 5、闭合; path.close(); canvas.drawPath(path, paint); path.reset();//解决滑动时路径重叠的问题 ////绘制固定圆 canvas.drawCircle(stableCenter.x, stableCenter.y, tempRadius, paint); } //绘制拖拽圆 canvas.drawCircle(dragCenter.x, dragCenter.y, dragRadius, paint); } canvas.restore(); } @Override public boolean onTouchEvent(MotionEvent event) { switch (event.getAction()) { case MotionEvent.ACTION_DOWN: isOutOfRange = false;//方便调试 isDisappear = false;//方便调试 //event.getX();//点击的点距离当前自定义控件的左边缘的距离 float rawX = event.getRawX();//点击的点距离当前手机屏幕的左边缘的距离 float rawY = event.getRawY()/* - statusBarHeight*/; dragCenter.set(rawX, rawY); invalidate(); break; case MotionEvent.ACTION_MOVE: rawX = event.getRawX(); rawY = event.getRawY(); dragCenter.set(rawX, rawY); //当拖拽超出一定范围后,固定圆和中间图形都消失了,其"消失了"用程序来说就是在onDraw()中不绘制某一段了 //判断拖拽的距离,判断距离是否超出最大距离 float distance = GeometryUtil.getDistanceBetween2Points(stableCenter, dragCenter); if (distance > MAX_DRAG_DISTANCE) { //当超出最大距离则不再对固定圆和中间图形进行绘制 isOutOfRange = true; } invalidate(); break; case MotionEvent.ACTION_UP: distance = GeometryUtil.getDistanceBetween2Points(stableCenter, dragCenter); //判断在move的时候是否超出过最大范围 if (isOutOfRange) { //判断up的时候是否在最大范围外 if (distance > MAX_DRAG_DISTANCE) isDisappear = true; } else { //TODO } invalidate(); break; } //return super.onTouchEvent(event); return true;//表示当前控件想要处理事件 } @Override protected void onSizeChanged(int w, int h, int oldw, int oldh) { super.onSizeChanged(w, h, oldw, oldh); statusBarHeight = getStatusBarHeight(this); } /** * 获取状态栏高度 * * @param view * @return */ private int getStatusBarHeight(View view) { Rect rect = new Rect(); //获取视图对应的可视范围,会把视图的左上右下的数据传入到一个矩形中 view.getWindowVisibleDisplayFrame(rect); return rect.top; } }
效果如下:
处理move超出并且up未超出、move和up均未超出最大范围:
最后还剩两个条件木有处理:
①、move超出,up未超出最大范围,这时应该将拖拽圆的圆心设置成固定圆的圆心,让其还原。
比较简单,直接上代码:
编译运行:
②、move和up均未超出最大范围,这时松手会有一个回弹效果。
这时应该是执行一段平移动画,从UP释放的位置到固定圆圆心之间进行,其实也就是从拖拽的距离到0之间的一个动画,所以可以用ValueAnimator来精准控制,如下:
其实可以想一下,对于平移其实就是拖拽圆和固定圆的一段动画,要是说能拿到一个距离执行的百分比,那通过这个百分比来算出两圆心的某个坐标,然后再去更新拖拽圆圆心,最后再不断刷新是不是就能达到平移的效果了呢?那这动画有获取到百分值的办法么?当然有,如下:
运行看下打印:
而关于两个值之间根据百分比获取指定值在之前已经实现了,如:
所以,根据两圆心点也可以利用它来获取,将其封装一下:
所以,调用一下它,如下:
编译运行:
但是!!还差最后一个回弹效果,这个对于动画来说就比较简单了,因为有回弹效果的插值器,设置看效果:
如果对回弹的力度不够还可以传值加以调节,如下:
至此代码就全部实现了,最后把所有效果都跑一遍:
说实话,真心觉得比较复杂,涉及到的计算也不少,所以需好好消化~之后会将它用在列表当中滴~