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

前言

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

下面上一下本章须要实现的效果图:算法

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

 

1、关于贝塞尔曲线

 

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

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

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

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

 

(图片来自于网络)post

公式:性能

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先绘制两个一样大小并重叠的圆形
  • 按照小圆的大小咱们设置圆形上刷新图标
  • 重写触摸事件,绘制咱们的贝塞尔曲线
  • 动画收回

 

2、自定义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,这样看起来效果更好一点!
 

3、绘制贝塞尔曲线

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


图中有六个重要的点,p一、p二、p三、p四、anchor一、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,这个就要涉及到了咱们的触摸事件监听了.
 

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

 
其实到这里为止,咱们就已经能够画出咱们想要的效果了,可是若是想要作动态的效果,天然而然就要加入触摸事件,咱们先来看一下博主的触摸事件处理代码:
 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动画回调,在这里咱们根据返回的每一帧率,动态设置回咱们的初始状态而且添加了动画结束的回调,到此,咱们的贝塞尔控件所有完成
 

4、使用和总结

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

感谢你们的支持,谢谢!

 

 

 

 

 

做者:yangpeixing

QQ:313930500

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

转载请注明出处~谢谢~