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

轉載請註明出處~謝謝~

相關文章
相關標籤/搜索