前言

接着上一期Android仿苹果版QQ下拉刷新实现(一) ——打造简单平滑的通用下拉刷新控件 的博客开始,同样,在开始前我们先来看一下目标效果:

下面上一下本章需要实现的效果图:

大家看到这个效果肯定不会觉得陌生,QQ已经把粘滞效果做的满大街都是,相信不少读者或多或少对于贝塞尔曲线有所了解,不了解的朋友们也没有关系,在这里我会带领读者领略一下贝塞尔的魅力!

一、关于贝塞尔曲线

我们知道,任何一条线段是由起始点和终止点的连线组成,两点组成一条直线,这就是最简单的一阶公式(就是线段):

一阶贝塞尔曲线表达公式(图略):

B(t) = P0 + ( P1 - P0 ) t = ( 1 - t ) P0 + t P1 , t∈[0,1]

很显然,一阶的贝塞尔只是用于一条线段,其中t的变化率代表着线性插值大小.所以我们的效果用于一阶贝塞尔曲线公式肯定不行,下面我们来着重介绍一下二阶(次)贝塞尔曲线变化率和公式:

(图片来自于网络)

公式:

B(t) = ( 1 - t )² P0 + 2 t ( 1 - t ) P1 + t² P2 , t∈[0,1]

其实公式对于我们的开发者来说并没有太大的意义,因为主要的算法我们的API都已经包含,不过我们需要了解的是,我们的辅助点的查找.首先,我们需要了解曲线是如何画出来的?从图中我们可以看出我们的辅助点是p1点,由p0和p1组成的线段加上p1和p2组成的线段一共是有两条线段,我们需要一个变化率t,t从p0走到p1和从p1走到p2的时间是一样的,这样我们连接两点,就产生了第三条直线(图中绿色的线),这条直线其实就是我们的贝塞尔曲线的切线,只要有了这条直线,我们就可以确定我们的贝塞尔曲线轨迹(这一点至关重要).

当然,有一阶二阶,肯定也会有三阶、四阶等等.因为辅助点的增加,曲线也会发生各种变化,在这里,博主就不介绍了,想了解更深入的读者,可以在很多关于贝塞尔的博客中去了解.

介绍完了贝塞尔曲线,接下来我们就要开始着手打造QQ的粘滞效果了.在开始编写代码前我们先分析一下,我们要实现这个效果所需要的准备工作:

  • 自定义View先绘制两个同样大小并重叠的圆形
  • 按照小圆的大小我们设置圆形上刷新图标
  • 重写触摸事件,绘制我们的贝塞尔曲线
  • 动画收回

二、自定义View绘制圆形

 
在这里,博主选择了自定义view而不是ViewGroup,可能会有人觉得,我们的刷新图标放在ViewGroup中会不会更方便,可以是可以,但是View本身也有绘制图片的功能,所以直接继承View就好.在重写ondraw前,我们先定义好一些变量:
 /**
* 圆的画笔
*/
private Paint circlePaint;
/**
* 画笔的路径
*/
private Path circlePath; /**
* 可拖动的最远距离
*/
private int maxHeight; /**
* 刷新图标
*/
private Bitmap bt; private float topCircleRadius;//默认上面圆形半径
private float topCircleX;//默认上面圆形x
private float topCircleY;//默认上面圆形y private float bottomCircleRadius;//默认上面圆形半径
private float bottomCircleX;//默认下面圆形x
private float bottomCircleY;//默认下面圆形y private float defaultRadius;//默认上面圆形半径 float offset=1.0f; float lastY; OnAnimResetListener listener; ObjectAnimator anim;

  

变量比较多,但是非常好理解,该写的注释也已经标注了,下面我们来看构造函数以及初始化:
 public YPXBezierView(Context context) {
this(context, null);
} public YPXBezierView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
} public YPXBezierView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init();
} protected void init() {
maxHeight=dp(60);
topCircleX=ScreenUtils.getScreenWidth(getContext())/2;
topCircleY=dp(100);
topCircleRadius=dp(15); bottomCircleX=topCircleX;
bottomCircleY=topCircleY;
bottomCircleRadius=topCircleRadius; defaultRadius=topCircleRadius; circlePath = new Path(); circlePaint = new Paint();
circlePaint.setAntiAlias(true);
circlePaint.setStyle(Paint.Style.FILL_AND_STROKE);
circlePaint.setStrokeWidth(1);
circlePaint.setColor(Color.parseColor("#999999"));
}
代码很简单,我们首先定义好我们的一些参数值和初始化画笔,其中maxHeight代表可以拉伸的高度,可以由用户自己去设置,然后就是定位我们的圆形在屏幕上方且居中,最后把底部圆形和顶部圆形重叠.
初始化好我们的参数,接下来就要看我们的绘制代码了:
 @Override
protected void onDraw(Canvas canvas) {
drawPath();
float left=topCircleX-topCircleRadius;
float top=topCircleY-topCircleRadius; canvas.drawPath(circlePath, circlePaint);
canvas.drawCircle(bottomCircleX, bottomCircleY, bottomCircleRadius, circlePaint);
canvas.drawCircle(topCircleX, topCircleY, topCircleRadius, circlePaint); int btWidth=(int) topCircleRadius* 2-dp(6);
if ((btWidth) > 0) {
bt = BitmapFactory.decodeResource(getResources(), R.mipmap.refresh);
bt = Bitmap.createScaledBitmap(bt,btWidth, btWidth, true);
canvas.drawBitmap(bt, left+dp(3), top+dp(2) , null);
bt.recycle();
}
super.onDraw(canvas); }
drawPath是我们绘制贝塞尔的代码,暂且先忽视掉,我们直接从第三行开始,我们要先确定好顶部圆形的左边距离以及顶部距离.为什么要这两个参数呢,因为我们需要根据上圆的位置来定位我们的刷新图标,而自定义View中关于绘制图片的方法最适合本文的莫过于
public void drawBitmap(Bitmap bitmap, float left, float top, Paint paint)
这个方法了,画圆形的代码不用多说,直接drawCircle就好,关于刷新图标,我们需要说一下,因为我们的刷新图标是需要跟随大圆的大小变化而变化的,所以它自身的大小一定是可变的,我查阅了关于修改bitmap大小的方法,发现只有在创建的时候使用createScaledBitmap方法,该方法支持bitmap的缩放,但是美中不足的是,它的效果是叠加的,如果把bitmap只创建一次并且不去释放,那么每次刷新的时候会发现我们的刷新图标越来越模糊,目前博主没有什么好的解决方案,只能在绘制的时候重新生成bitmap,如果有了解更优化的方案的话,欢迎大神联系交流~我们的边距是3dp,所以我们的位置需要减去6dp,这样看起来效果更好一点!
 

三、绘制贝塞尔曲线

 
关于绘制贝塞尔曲线,安卓系统中有一个专门的方法叫做quadTo,这个是Path的方法,即绘制贝塞尔路径.使用该方法的前提是我们需要找到我们的辅助点,那么我们的重点来了,辅助点怎么找?我们先来看一下博主自己做的一张图解:

图中有六个重要的点,p1、p2、p3、p4、anchor1、anchor2,因为我们的粘滞小球尽量需要平滑一点,所以博主选择了最简单的四个交叉点(p1~p4),这四个点不涉及到三角函数的处理,所以坐标很容易的就可以得到:

topCircleX=大圆的X坐标                       bottomCircleX=小圆的X坐标
topCircleY==大圆的Y坐标                     bottomCircleY==小圆的Y坐标
topCircleRadius=大圆的半径                 bottomCircleRadius=小圆的半径
四个点的坐标可以表达为:
p1 (topCircleX-topCircleRadius , topCircleY)
p2 (topCircleX+topCircleRadius , topCircleY)
p3 (bottomCircleX-bottomCircleRadius , bottomCircleY)
p4 (bottomCircleX+bottomCircleRadius , bottomCircleY)
那么我们知道了这四个点有什么用呢?
首先,我们知道左边贝塞尔曲线的初始点(p1)和结束点(p3)以及右边的贝塞尔曲线的初始点(p2)和结束点(p4),我们至少已经确定了两个点,接下来我们去寻找辅助点,回到上图,从图中可以看出,我们的贝塞尔曲线由我们的辅助点anchor1控制,辅助点又是被起点p1和终点p3控制着,因此,当两个圆距离越大,曲线越趋于平缓,当两个圆距离越小,曲线的波动度越大,这样,我们想要的粘连的效果就实现了。所以连接p1和p4,取线段p1p4的中点,我们就可以得左边的辅助点(右边同理),那么我们的两个辅助点坐标:
anchor1 ((p1x+p4x)/2 , (p1y+p4y)/2)
anchor1 ((p2x+p3x)/2 , (p2y+p3y)/2)
知道了原理我们再来看代码就清晰了很多:
 private void drawPath() {

        float  p1X = topCircleX - topCircleRadius ;
float p1Y = topCircleY ;
float p2X = topCircleX + topCircleRadius;
float p2Y = topCircleY ;
float p3X = bottomCircleX - bottomCircleRadius ;
float p3Y = bottomCircleY ;
float p4X = bottomCircleX + bottomCircleRadius ;
float p4Y = bottomCircleY ; float anchorX = (p1X+ p4X) / 2-topCircleRadius*offset;
float anchorY = (p1Y + p4Y) / 2; float anchorX2 = (p2X +p3X) / 2+topCircleRadius*offset;
float anchorY2 = (p2Y + p3Y) / 2; /* 画粘连体 */
circlePath.reset();
circlePath.moveTo(p1X, p1Y);
circlePath.quadTo(anchorX, anchorY, p3X, p3Y);
circlePath.lineTo(p4X, p4Y);
circlePath.quadTo(anchorX2, anchorY2, p2X, p2Y);
circlePath.lineTo(p1X, p1Y); }
可能细心的朋友发现,我们的两个辅助点的x坐标动态的加减了 topCircleRadius*offset ,其实这是博主的一个小小的优化,因为按照效果图上的六个点,已经可以画出贝塞尔的粘滞效果,但是我们会发现,描边并不是很圆润,因为我们的曲线是穿过两个圆,所以看起来就和QQ未读消息数的那个气泡效果一样,很显然,和我们的预期刷新效果有一点点不同.在这里我之所以加上这个距离,是想让贝塞尔的起点相对往外切于圆的边上,这样描边出来的效果才更像"鼻涕",为什么要*offset,这个就要涉及到了我们的触摸事件监听了.
 

三、触摸事件监听以及收回

 
其实到这里为止,我们就已经可以画出我们想要的效果了,但是如果想要做动态的效果,自然而然就要加入触摸事件,我们先来看一下博主的触摸事件处理代码:
 private void drawPath() {

        float  p1X = topCircleX - topCircleRadius ;
float p1Y = topCircleY ;
float p2X = topCircleX + topCircleRadius;
float p2Y = topCircleY ;
float p3X = bottomCircleX - bottomCircleRadius ;
float p3Y = bottomCircleY ;
float p4X = bottomCircleX + bottomCircleRadius ;
float p4Y = bottomCircleY ; float anchorX = (p1X+ p4X) / 2-topCircleRadius*offset;
float anchorY = (p1Y + p4Y) / 2; float anchorX2 = (p2X +p3X) / 2+topCircleRadius*offset;
float anchorY2 = (p2Y + p3Y) / 2; /* 画粘连体 */
circlePath.reset();
circlePath.moveTo(p1X, p1Y);
circlePath.quadTo(anchorX, anchorY, p3X, p3Y);
circlePath.lineTo(p4X, p4Y);
circlePath.quadTo(anchorX2, anchorY2, p2X, p2Y);
circlePath.lineTo(p1X, p1Y); }
主要代码在Move中处理,我们先得到手指滑动的高度,然后判断当前滑动的方向,过滤掉向上的滑动,因为我们的粘滞效果自上而下,所以不需要处理向上的操作(在这里说明一下,如果用户的需求是可以任意方向,就好比QQ的未读消息气泡,那么我们的触摸事件就需要针对手势进行判断,然后在绘制贝塞尔曲线时也要进行方向判断).有了滑动的距离,有了最大滑动距离,那么我们就可以得到滑动的偏移量:
offset = 1-手指滑动的距离/最大滑动高度  offset∈( 0 ,1 );
有了offset,我们就可以动态的去设置大圆和小圆的大小及位置,
小圆的半径 = 初始半径(初始化时大圆的半径)*offset
小圆的位置向下偏移手指滑动的距离(delayY)
同时,大圆的半径缩小.这个缩小不是随随便便的缩小的,而是有一个曲线变化,这个曲线变化我们需要改变我们的offset变化率,即:
offset=(1/3) offset
这样我们的大圆的半径就会跟随手指一动逐渐缩小,到此,我们的Move事件完整结束.
介绍完Move事件,我们来看UP,毕竟当我们手指离开控件的时候,我们需要收回,收回很简单,我们只需要把控件置于初始化时状态就好,可是收回的效果很快,几乎是一瞬间,这样的交互并不符合我们一开始的效果,所以,博主决定加入属性动画进行收回:
 public void animToReset(boolean lock){
if(!lock) {
Log.e("onAnimationEnd", "动画开始");
anim= ObjectAnimator.ofFloat(offset, "ypx", 0.0F, 1.0F).setDuration(200);
//使用反弹算法插值器,貌似没有什么太大的效果 - -!
anim.setInterpolator(new BounceInterpolator());
anim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
float cVal = (Float) animation.getAnimatedValue();
offset = cVal;
bottomCircleX=bottomCircleX+(topCircleX-bottomCircleX)*offset;
bottomCircleY=bottomCircleY+(topCircleY-bottomCircleY)*offset;
bottomCircleRadius=bottomCircleRadius+(topCircleRadius-bottomCircleRadius)*offset;
topCircleRadius=topCircleRadius+(defaultRadius-topCircleRadius)*offset;
postInvalidate();
}
});
anim.addListener(new Animator.AnimatorListener() {
@Override
public void onAnimationStart(Animator animator) { } @Override
public void onAnimationEnd(Animator animator) {
Log.e("onAnimationEnd", "动画结束");
if (listener != null) {
listener.onReset();
}
} @Override
public void onAnimationCancel(Animator animator) { } @Override
public void onAnimationRepeat(Animator animator) { }
});
anim.start();
}
}
忽视掉lock参数,这个参数是为了后面QQ刷新准备的,在此不多介绍,我们直接看onAnimationUpdate动画回调,在这里我们根据返回的每一帧率,动态设置回我们的初始状态并且添加了动画结束的回调,到此,我们的贝塞尔控件全部完成
 

四、使用和总结

 
关于使用,肯定是直接在布局中定义即可,不过要注意的是我们的控件并没有添加测量代码,因为滑动的高度有可能是可变的,有可能是不变的,与其让用户去设置,还不如不设置,让其充满它的父控件即可,所以在布局中,宽高设置成match_parent,当然,如果有些极端的情况下,比如父控件的高度要随着我们的小球变化而变化,那么我们就需要在代码中添加onmearsure方法了,让它在wrap_content的时候按照最大距离来测量,在这里,因为博主的效果用不到就没有添加代码,如果有这方面的需求的话,可以联系博主~
总的来说,本章的效果实现并不是很难,主要在于辅助点的查找,我们可以取一些特殊点,避免复杂的三角函数公式计算,这样不仅我们的性能可以提高,而且也省了很多的代码量,再难的效果都是有一定的原理的,只要花时间弄清楚原理,肯定都能完成.到这里,我们离最后的QQ下拉刷新效果只差一步之摇了,最后一章我会结合以上两篇文章的知识和代码,并且延伸出当前主流的另一种特效,下拉放大效果,有兴趣的还希望读者多多支持哦~
 
 

感谢大家的支持,谢谢!

作者:yangpeixing

QQ:313930500

下载地址:http://download.csdn.net/detail/qq_16674697/9741375

转载请注明出处~谢谢~

 
 
 
 

Android仿苹果版QQ下拉刷新实现(二) ——贝塞尔曲线开发"鼻涕"下拉粘连效果的更多相关文章

  1. Android仿苹果版QQ下拉刷新实现(一) ——打造简单平滑的通用下拉刷新控件

    前言: 忙完了结婚乐APP的开发,终于可以花一定的时间放在博客上了.好了,废话不多说,今天我们要带来的效果是苹果版本的QQ下拉刷新.首先看一下目标效果以及demo效果:      因为此效果实现的步骤 ...

  2. [Swift通天遁地]二、表格表单-(4)使用系统自带的下拉刷新控件,制作表格的下拉刷新效果

    ★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★➤微信公众号:山青咏芝(shanqingyongzhi)➤博客园地址:山青咏芝(https://www.cnblogs. ...

  3. Android平台下利用zxing实现二维码开发

    Android平台下利用zxing实现二维码开发 现在走在大街小巷都能看到二维码,而且最近由于项目需要,所以研究了下二维码开发的东西,开源的二维码扫描库主要有zxing和zbar,zbar在iPos平 ...

  4. (转载)Android平台下利用zxing实现二维码开发

    Android平台下利用zxing实现二维码开发 现在走在大街小巷都能看到二维码,而且最近由于项目需要,所以研究了下二维码开发的东西,开源的二维码扫描库主要有zxing和zbar,zbar在iPos平 ...

  5. [Android]仿新版QQ的tab下面拖拽标记为已读的效果

    以下内容为原创,欢迎转载,转载请注明 来自天天博客:http://www.cnblogs.com/tiantianbyconan/p/4182929.html 可拖拽的红点,(仿新版QQ,tab下面拖 ...

  6. ListView实现下拉刷新(二)隐藏头布局

    一.问题分析 在上一篇中,我们将头布局加到了ListView上.但是没有隐藏他.你可能会想,隐藏还不简单,直接给它设置为GONE属性不就可以了吗,在需要的时候再设定为可见.没错,这正是ListView ...

  7. Android仿微信QQ等实现锁屏消息提醒

    demo代码如下: import android.content.Intent; import android.os.Bundle; import android.support.v7.app.App ...

  8. mui实现分页上拉加载更多 下拉刷新数据的简单实现 移动端下拉上拉

    空下来把mui上拉加载更多,下拉刷新数据做了一个简单的实现,希望可以帮助到需要的朋友 demo项目的结构 <!DOCTYPE html> <html> <head> ...

  9. 【转】Android平台下利用zxing实现二维码开发

    http://www.cnblogs.com/dolphin0520/p/3355728.html 现在走在大街小巷都能看到二维码,而且最近由于项目需要,所以研究了下二维码开发的东西,开源的二维码扫描 ...

随机推荐

  1. 免费SSL证书(https网站)申请

    如何拥有一个自己的免费的SSL证书,并且能够长期拥有.这篇文章让你找到可用的免费证书o(* ̄︶ ̄*)o 各厂商提供的免费SSL基本是Symantec(赛门铁克),申请一年,不支持通配符,有数量限制. ...

  2. Java NIO 详解(一)

    一.基本概念描述 1.1 I/O简介 I/O即输入输出,是计算机与外界世界的一个借口.IO操作的实际主题是操作系统.在java编程中,一般使用流的方式来处理IO,所有的IO都被视作是单个字节的移动,通 ...

  3. pgm10

    这部分讨论 MAP 估计.从某个角度上来说,我们可以将这个问题转换成为前面讨论过的: 这样一来我们只需要将原先的 sum-product 换成 max-sum 即可.话虽这么说,我们还是看看 Koll ...

  4. 牛客网-湘潭大学校赛重现H题 (线段树 染色问题)

    链接:https://www.nowcoder.com/acm/contest/105/H来源:牛客网 n个桶按顺序排列,我们用1~n给桶标号.有两种操作: 1 l r c 区间[l,r]中的每个桶中 ...

  5. Git合并的代码 不提交服务器的方法

    使用Git下载代码的时候,常遇到合并的情况,然后再上传的时候,系统就会自动把合并代码的过程也上传,有时候会感觉非常的烦Merge remote-tracking branch 'choose_remo ...

  6. 【题解】 [ZJOI2008] 泡泡堂(贪心/二分图/动态规划)

    懒得复制,戳我戳我 Solution: 就是有一个贪心策略:(以下假设使\(A\)队分数更高) \(First:\)比较两个分值的最小值,如果\(A\)最小分比\(B\)最小分大就直接比较两个最小的, ...

  7. BZOJ 2879 [Noi2012]美食节 | 费用流 动态开点

    这道题就是"修车"的数据加强版--但是数据范围扩大了好多,应对方法是"动态开点". 首先先把"所有厨师做的倒数第一道菜"和所有菜连边,然后跑 ...

  8. CF1025D Recovering BST

    题意:给定序列,问能否将其构成一颗BST,使得所有gcd(x, fa[x]) > 1 解:看起来是区间DP但是普通的f[l][r]表示不了根,f[l][r][root]又是n4的会超时,怎么办? ...

  9. 本地如何连接虚拟机上的MySql

    今天在本地链接虚拟机上的MySql,然而链接失败了!甚是尴尬! 首先想一想是什么原因导致链接失败: 基础环境:在Linux上安装mysql 1.检查虚拟机IP在本地是否可以ping 通过 虚拟机IP: ...

  10. 借读:分布式锁和双写Redis

      本帖最后由 howtodown 于 2016-10-3 16:01 编辑问题导读1.为什么会产生分布式锁?2.使用分布式锁的方法有哪些?3.本文创造的分布式锁的双写Redis框架都包含哪些内容? ...