【Android开源项目解析】QQ“一键下班”功能实现解析——学习Path及贝塞尔曲线的基本使用
2015-09-13 20:10
876 查看
早在很久很久以前,QQ就实现了“一键下班”功能。何为“一键下班”?当你QQ有信息时,下部会有信息数量提示红点,点击拖动之后,就会出现“一键下班”效果。本文将结合github上关于此功能的一个简单实现,介绍这个功能的基本实现思路。
注:下面内容请参照项目源码观看。
其实如果从代码来看,实现的过程并不复杂,重点需要掌握的就是
path的用法
贝塞尔曲线的使用。
这个项目的核心就是BezierView,继承自FrameLayout,拖动的时候,相当于覆盖在屏幕上一样。在init()方法中主要进行了以下操作
初始化了Path和Paint对象,然后动态生成了两个ImageView
exploredImageView 主要用来实现爆炸效果,默认不可见
tipImageView 手指进行拖动时的红色图标
exploredImageView设置的图片资源是一个AnimationDrawable,下面是res中的声明,控制每张图片的播放顺序和持续时间,这也很好理解
我们在学习这种自定义控件的时候,可以按照View的绘制过程,对代码进行重点的查看,比如说,我们可以从
下面这个顺序来对这个项目进行学习。
onMeasure()
onLayout()
onDraw()
onTouchEvent()
因为这个项目没有重写onMeasure(),所以我们直接从onLayout看看做了什么
代码还是非常还理解的,无非就是初始化了ImageView的位置,在这里出现了两个变量,startX和startY,这两个变量控制的是红点的初始化坐标,在整个过程中不会发生改变。
那么onDraw()呢?
onDraw()里面的操作也并不复杂,如果正在执行动画或者是没有在触摸模式,就画一个透明的颜色,否则,就开始画真正的界面了。calculate()这个方法是一个重点,从命名来看应该是计算了一些坐标值,然后开始画了两个圆,这两个圆的坐标,一个是(startX,startY),另一个是(x,y),颜色和半径都是相同的,这个是为了简化计算,所以将两个圆的半径设置成相同的啦。
我们先继续看一下在onTouchEvent()里面进行了什么操作
首先在按下的时候,取得了tipImageView的屏幕坐标位置,然后根据触摸点的位置,来判断是否是触摸状态,从而改变isTouch的取值,而如果不是按下时间,则推出改变isTouch,从而触摸状态,还原tipImageVIew的位置。但是无论如何,都会执行invalidate(),来调用onDraw(),在那里面就执行了实际的画圆的操作,这个咱们一会在看。再往下呢,就是根据动画状态是否正在播放,来更新x、y坐标,还有anchorX和anchorY的值。
x和y的值其实就是触摸点的位置,主要用来控制手指所按下的圆的位置,那么anchorX和anchorY呢?这两个值其实就是控制锚点的坐标,用于贝塞尔曲线的绘制。
说到了这里,我相信你应该明白实现的基本思路了,但是最重要的,就是拉扯效果,到底是如何实现的呢?那么咱们就来看一下最重要的calculate()到底做了些什么!
在看这段代码之前,咱们先简单学习一下贝塞尔曲线及如何绘制。
贝塞尔曲线于1962年由法国数学家Pierre Bézier第一次研究使用并给出了详细的计算公式,So该曲线也是由其名字命名。
Path中给出的quadTo方法属于二阶贝赛尔曲线。
来看下高清无码GIF动图,从爱哥那边偷的,别告诉他^_^
从上面的动图中,我们可以发现,二阶贝塞尔曲线,我们只需要确定三个点,就可以画出一条平滑的曲线,P0和P2是起点和终点,而P1就是我们的锚点,也就是前面提到的anchorX和anchorY。
那么问题来了,如果我们要实现这种拖拽拉伸的效果,需要知道几个点呢?
先来张设计图
可以看到,在设计图中,有P1-P4四个坐标点,是两条圆外切线与圆的交点坐标,因为需要P1-P2和P3-P4两条贝塞尔曲线的歪曲程度相同,所以锚点只需要在P0到原点坐标的连线上取一个点即可,所以,咱们就需要5个坐标点。
容我喝口水^_^
来来来,咱们继续!
那么,既然知道了需要哪五个坐标点,anchorX和anchorY在onTouchEvent()里面已经算出来了,那么,剩下的4个坐标点怎么求呢?其实这就是calculate()内部所做的主要工作。
由于将两个圆的半径设置为相同,可以精简计算,所以下面的代码也是假设两个圆的半径相同进行操作的,凯子哥再给你手绘一张高清无码大图
startX和startY是指定值,这里我们以它为坐标原点,另外一个圆的坐标为(x,y),即手指触摸的位置坐标,两圆半径相同,则外切线平行,过(x,y)点做垂直线垂直于两条切线。
现在,已知(startX,startY),(x,y),半径radius,还有个直角,因此,我们只需要知道一个角度,然后就可以求出offsetX和offsetY,也就求出P1-P4的四点坐标了~~~
那么这个角度好求么?
简单,再来张高清无码大图~
因为
∠α=∠3
∠3+∠2=90
∠1+∠2=90
所以
∠α=∠1
这是初中的三段式么…忘记了
那么∠1怎么求呢?简单啊,(x,y)都知道了,
tan∠1= (y-startY)/(x-startX);
因此可得
∠1 = arctan((y-startY)/(x-startX))
知道角度,知道radius,还求不出offsetX和offsetY么~
所以
那么。,,现在再来看下面的代码,你还说你看不懂吗?
算出4个点的坐标,并且知道锚点位置,用path连起来就Ok啦
肚子饿了,这一篇就到这里了,下去吃饭饭
QQ手机版 5.0“一键下班”设计小结
项目地址https://github.com/THEONE10211024/WaterDropListView
-
尊重原创,转载请注明:From 凯子哥(http://blog.csdn.net/zhaokaiqiang1992) 侵权必究!
关注我的微博,可以获得更多精彩内容
项目地址
https://github.com/chenupt/BezierDemo最终实现效果
实现原理解析
我个人感觉,这个效果实现的很漂亮啊!那么咱们就来看看实现原理是什么~注:下面内容请参照项目源码观看。
其实如果从代码来看,实现的过程并不复杂,重点需要掌握的就是
path的用法
贝塞尔曲线的使用。
这个项目的核心就是BezierView,继承自FrameLayout,拖动的时候,相当于覆盖在屏幕上一样。在init()方法中主要进行了以下操作
private void init(){ path = new Path(); paint = new Paint(); paint.setAntiAlias(true); paint.setStyle(Paint.Style.FILL_AND_STROKE); paint.setStrokeWidth(2); paint.setColor(Color.RED); LayoutParams params = new LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT); exploredImageView = new ImageView(getContext()); exploredImageView.setLayoutParams(params); exploredImageView.setImageResource(R.drawable.tip_anim); exploredImageView.setVisibility(View.INVISIBLE); tipImageView = new ImageView(getContext()); tipImageView.setLayoutParams(params); tipImageView.setImageResource(R.drawable.skin_tips_newmessage_ninetynine); addView(tipImageView); addView(exploredImageView); }
初始化了Path和Paint对象,然后动态生成了两个ImageView
exploredImageView 主要用来实现爆炸效果,默认不可见
tipImageView 手指进行拖动时的红色图标
exploredImageView设置的图片资源是一个AnimationDrawable,下面是res中的声明,控制每张图片的播放顺序和持续时间,这也很好理解
<?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>
我们在学习这种自定义控件的时候,可以按照View的绘制过程,对代码进行重点的查看,比如说,我们可以从
下面这个顺序来对这个项目进行学习。
onMeasure()
onLayout()
onDraw()
onTouchEvent()
因为这个项目没有重写onMeasure(),所以我们直接从onLayout看看做了什么
@Override protected void onLayout(boolean changed, int left, int top, int right, int bottom) { exploredImageView.setX(startX - exploredImageView.getWidth()/2); exploredImageView.setY(startY - exploredImageView.getHeight()/2); tipImageView.setX(startX - tipImageView.getWidth()/2); tipImageView.setY(startY - tipImageView.getHeight()/2); super.onLayout(changed, left, top, right, bottom); }
代码还是非常还理解的,无非就是初始化了ImageView的位置,在这里出现了两个变量,startX和startY,这两个变量控制的是红点的初始化坐标,在整个过程中不会发生改变。
那么onDraw()呢?
@Override protected void onDraw(Canvas canvas){ if(isAnimStart || !isTouch){ canvas.drawColor(Color.TRANSPARENT, PorterDuff.Mode.OVERLAY); }else{ calculate(); canvas.drawColor(Color.TRANSPARENT, PorterDuff.Mode.OVERLAY); canvas.drawPath(path, paint); canvas.drawCircle(startX, startY, radius, paint); canvas.drawCircle(x, y, radius, paint); } super.onDraw(canvas); }
onDraw()里面的操作也并不复杂,如果正在执行动画或者是没有在触摸模式,就画一个透明的颜色,否则,就开始画真正的界面了。calculate()这个方法是一个重点,从命名来看应该是计算了一些坐标值,然后开始画了两个圆,这两个圆的坐标,一个是(startX,startY),另一个是(x,y),颜色和半径都是相同的,这个是为了简化计算,所以将两个圆的半径设置成相同的啦。
我们先继续看一下在onTouchEvent()里面进行了什么操作
@Override public boolean onTouchEvent(MotionEvent event) { if(event.getAction() == MotionEvent.ACTION_DOWN){ // 判断触摸点是否在tipImageView中 Rect rect = new Rect(); int[] location = new int[2]; tipImageView.getDrawingRect(rect); tipImageView.getLocationOnScreen(location); rect.left = location[0]; rect.top = location[1]; rect.right = rect.right + location[0]; rect.bottom = rect.bottom + location[1]; if (rect.contains((int)event.getRawX(), (int)event.getRawY())){ isTouch = true; } }else if(event.getAction() == MotionEvent.ACTION_UP || event.getAction() == MotionEvent.ACTION_CANCEL){ isTouch = false; tipImageView.setX(startX - tipImageView.getWidth()/2); tipImageView.setY(startY - tipImageView.getHeight()/2); } invalidate(); if(isAnimStart){ return super.onTouchEvent(event); } anchorX = (event.getX() + startX)/2; anchorY = (event.getY() + startY)/2; x = event.getX(); y = event.getY(); return true; }
首先在按下的时候,取得了tipImageView的屏幕坐标位置,然后根据触摸点的位置,来判断是否是触摸状态,从而改变isTouch的取值,而如果不是按下时间,则推出改变isTouch,从而触摸状态,还原tipImageVIew的位置。但是无论如何,都会执行invalidate(),来调用onDraw(),在那里面就执行了实际的画圆的操作,这个咱们一会在看。再往下呢,就是根据动画状态是否正在播放,来更新x、y坐标,还有anchorX和anchorY的值。
x和y的值其实就是触摸点的位置,主要用来控制手指所按下的圆的位置,那么anchorX和anchorY呢?这两个值其实就是控制锚点的坐标,用于贝塞尔曲线的绘制。
说到了这里,我相信你应该明白实现的基本思路了,但是最重要的,就是拉扯效果,到底是如何实现的呢?那么咱们就来看一下最重要的calculate()到底做了些什么!
在看这段代码之前,咱们先简单学习一下贝塞尔曲线及如何绘制。
贝塞尔曲线于1962年由法国数学家Pierre Bézier第一次研究使用并给出了详细的计算公式,So该曲线也是由其名字命名。
Path中给出的quadTo方法属于二阶贝赛尔曲线。
来看下高清无码GIF动图,从爱哥那边偷的,别告诉他^_^
从上面的动图中,我们可以发现,二阶贝塞尔曲线,我们只需要确定三个点,就可以画出一条平滑的曲线,P0和P2是起点和终点,而P1就是我们的锚点,也就是前面提到的anchorX和anchorY。
那么问题来了,如果我们要实现这种拖拽拉伸的效果,需要知道几个点呢?
先来张设计图
可以看到,在设计图中,有P1-P4四个坐标点,是两条圆外切线与圆的交点坐标,因为需要P1-P2和P3-P4两条贝塞尔曲线的歪曲程度相同,所以锚点只需要在P0到原点坐标的连线上取一个点即可,所以,咱们就需要5个坐标点。
容我喝口水^_^
来来来,咱们继续!
那么,既然知道了需要哪五个坐标点,anchorX和anchorY在onTouchEvent()里面已经算出来了,那么,剩下的4个坐标点怎么求呢?其实这就是calculate()内部所做的主要工作。
由于将两个圆的半径设置为相同,可以精简计算,所以下面的代码也是假设两个圆的半径相同进行操作的,凯子哥再给你手绘一张高清无码大图
startX和startY是指定值,这里我们以它为坐标原点,另外一个圆的坐标为(x,y),即手指触摸的位置坐标,两圆半径相同,则外切线平行,过(x,y)点做垂直线垂直于两条切线。
现在,已知(startX,startY),(x,y),半径radius,还有个直角,因此,我们只需要知道一个角度,然后就可以求出offsetX和offsetY,也就求出P1-P4的四点坐标了~~~
那么这个角度好求么?
简单,再来张高清无码大图~
因为
∠α=∠3
∠3+∠2=90
∠1+∠2=90
所以
∠α=∠1
这是初中的三段式么…忘记了
那么∠1怎么求呢?简单啊,(x,y)都知道了,
tan∠1= (y-startY)/(x-startX);
因此可得
∠1 = arctan((y-startY)/(x-startX))
知道角度,知道radius,还求不出offsetX和offsetY么~
所以
float offsetX = (float) (radius*Math.sin(Math.atan((y - startY) / (x - startX)))); float offsetY = (float) (radius*Math.cos(Math.atan((y - startY) / (x - startX))));
那么。,,现在再来看下面的代码,你还说你看不懂吗?
private void calculate(){
float distance = (float) Math.sqrt(Math.pow(y-startY, 2) + Math.pow(x-startX, 2));
radius = -distance/15+DEFAULT_RADIUS;
if(radius < 9){
isAnimStart = true;
exploredImageView.setVisibility(View.VISIBLE);
exploredImageView.setImageResource(R.drawable.tip_anim);
((AnimationDrawable) exploredImageView.getDrawable()).stop();
((AnimationDrawable) exploredImageView.getDrawable()).start();
tipImageView.setVisibility(View.GONE);
}
// 根据角度算出四边形的四个点
float offsetX = (float) (radius*Math.sin(Math.atan((y - startY) / (x - startX)))); float offsetY = (float) (radius*Math.cos(Math.atan((y - startY) / (x - startX))));
float x1 = startX - offsetX;
float y1 = startY + offsetY;
float x2 = x - offsetX;
float y2 = y + offsetY;
float x3 = x + offsetX;
float y3 = y - offsetY;
float x4 = startX + offsetX;
float y4 = startY - offsetY;
path.reset();
path.moveTo(x1, y1);
path.quadTo(anchorX, anchorY, x2, y2);
path.lineTo(x3, y3);
path.quadTo(anchorX, anchorY, x4, y4);
path.lineTo(x1, y1);
// 更改图标的位置
tipImageView.setX(x - tipImageView.getWidth()/2);
tipImageView.setY(y - tipImageView.getHeight()/2);
}
算出4个点的坐标,并且知道锚点位置,用path连起来就Ok啦
肚子饿了,这一篇就到这里了,下去吃饭饭
相关项目及文章
手机QQ5.0红点拖拽消除的实现QQ手机版 5.0“一键下班”设计小结
相关项目
项目地址https://github.com/dodola/MetaballLoading项目地址https://github.com/THEONE10211024/WaterDropListView
-
尊重原创,转载请注明:From 凯子哥(http://blog.csdn.net/zhaokaiqiang1992) 侵权必究!关注我的微博,可以获得更多精彩内容
相关文章推荐
- Android之使用Http协议实现文件上传功能
- 查看github pages文档的方式
- 使用BAE的基于Web.py的简单博客程序
- Ubuntu12.04下QQ完美走起啊!走起啊!有木有啊!
- 菜鸟说给菜鸟听之Beginning Linux Programming——Chapter1(1)
- QQ商业化,如何实现从0到1的破局?
- 十年生死两茫茫,Linux QQ 突然复活!
- VB实现的《QQ美女找茬游戏》作弊器实例
- 路由器端QQ封堵方案
- QQ输入法自动删除其它输入法的解决方法
- 让普通QQ号也能克隆QQ好友
- sqlserver FOR XML PATH 语句的应用
- VBS取QQ或TM自动登录代码并防止关闭的脚本
- Ruby微信开发的几个开源项目介绍
- 两分钟学会如何在github托管代码
- set_include_path在win和linux下的区别
- php获取QQ头像并显示的方法
- 利用AJAX开源项目 在网页里播放视频实现方法