1. 程式人生 > >Android仿蘋果版QQ下拉重新整理實現(二) ——貝塞爾曲線開發"鼻涕"下拉粘連效果

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,這個就要涉及到了我們的觸控事件監聽了.

三、觸控事件監聽以及收回

其實到這裡為止,我們就已經可以畫出我們想要的效果了,但是如果想要做動態的效果,自然而然就要加入觸控事件,我們先來看一下博主的觸控事件處理程式碼:

    @Override
    public boolean dispatchTouchEvent(MotionEvent event) {
        switch (event.getAction()) {
        case MotionEvent.ACTION_DOWN:
            lastY = event.getRawY();
            break;
        case MotionEvent.ACTION_MOVE:
            float delayY=event.getRawY() - lastY;//滑動高度的偏移量
            if(delayY<0){
                return true;
            }
            offset=1-delayY/maxHeight;//滑動的偏移量offset 範圍 offset∈(1,0)
            //如果偏移量大於等於0.2的時候我們就讓它開始重繪,
            // 這樣可以給下面的圓留下一點可見半徑,要不然offset為0的時候下面的圓就成了點
            if(offset>=0.2){
                bottomCircleRadius = defaultRadius * offset;
                bottomCircleX = topCircleX;
                bottomCircleY = topCircleY + delayY;
                topCircleRadius = (float) (defaultRadius * (Math.pow(offset, 1 / 3.0)));
                postInvalidate();
            }
            break;
        case MotionEvent.ACTION_UP:
            animToReset(false);
            break;
        case MotionEvent.ACTION_CANCEL:
            animToReset(false);
            break;
        }
        return true;
    }

主要程式碼在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

轉載請註明出處~謝謝~

相關推薦

Android仿蘋果QQ重新整理實現() ——曲線開發"鼻涕"粘連效果

前言 下面上一下本章需要實現的效果圖: 大家看到這個效果肯定不會覺得陌生,QQ已經把粘滯效果做的滿大街都是,相信不少讀者或多或少對於貝塞爾曲線有所瞭解,不瞭解的朋友們也沒有關係,在這裡我會帶領讀者領略一下貝塞爾的魅力! 一、關於貝塞爾曲線 我們知道

Android仿蘋果QQ重新整理實現(一) ——打造簡單平滑的通用重新整理控制元件

前言: 因為公司人員變動原因,導致了博主四個月沒有動安卓,一直在做IOS開發,如今接近年前,終於可以花一定的時間放在安卓上了.好了,廢話不多說,今天我們要帶來的效果是蘋果版本的QQ下拉重新整理.首先看一下目標效果以及demo效果:      因為此效果實現的步驟較多,所

Android仿蘋果QQ刷新實現(一) ——打造簡單平滑的通用刷新控件

公司 ase pda false 當前 undle nat rst urn 前言: 因為公司人員變動原因,導致了博主四個月沒有動安卓,一直在做IOS開發,如今接近年前,終於可以花一定的時間放在安卓上了.好了,廢話不多說,今天我們要帶來的效果是蘋果版本的QQ下拉刷新.首先看

Android開源專案解析】QQ“一鍵下班”功能實現解析——學習Path及曲線的基本使用

早在很久很久以前,QQ就實現了“一鍵下班”功能。何為“一鍵下班”?當你QQ有資訊時,下部會有資訊數量提示紅點,點選拖動之後,就會出現“一鍵下班”效果。本文將結合github上關於此功能的一個簡單實現,介紹這個功能的基本實現思路。 專案地址

Android開發曲線進階篇(仿直播送禮物,餓了麼購物車動畫)

又是一年畢業季,今年終於輪到我了,最近一邊忙著公司的專案,一邊趕著畢設和論文,還私下和朋友搞了些小外包,然後還要還抽出時間寫部落格,真是忙的不要不要的。 好了,言歸正傳,前幾天寫了一篇關於貝塞爾曲線的基礎篇,如果你對貝塞爾曲線還不是很瞭解,建議你先去閱讀下:Android開發之貝塞爾曲線初體驗 ,今天這篇文

Android曲線應用-跳動的水滴

dir 貝塞爾曲線 href 完成 通過 android load 繪制 canvas 主要通過6個控制點實現。 val startPoint = PointF() val endPoint = PointF() val control1 = PointF() val c

曲線動畫demo(仿美人相機效果

效果如圖: 仿美人相機,手勢滑動隱藏頂部view。為了方便講解,將螢幕分為幾個區域,如圖: 在拖動過程中: 1、拖動距離小於minMoveDistance,貝賽爾曲線發生形變 2、拖動大於minMoveDistance,整個view開始下移 在鬆開手時: 1、

android流式佈局、待辦事項應用、曲線、MVP+Rxjava+Retrofit、藝術圖片應用等原始碼

Android精選原始碼 android模仿淘寶首頁效果原始碼 一款藝術圖片應用,採用T-MVVM打造 Android MVP + RxJava + Retrofit專案 android流式佈局實現熱門標籤效果 android仿淘寶客戶端商品詳

Android曲線實現水波紋的效果

前兩天朋友找我實現一個水波紋的效果,因為這塊一直沒做過,所以花了一上午時間研究一下,參考了網上的一些方法,得知Android還有Path.quadTo()這麼一個方法。 話不多說,程式碼如下: public class MyView extends View implem

2014-11-6Android學習------Android 模擬翻頁效果實現--------曲線

寫一篇文章很辛苦啊!!! 轉載請註明,聯絡請郵件[email protected] 我學習Android都是結合原始碼去學習,這樣比較直觀,非常清楚的看清效果,覺得很好,今天的學習原始碼是網上找的原始碼 百度搜就知道很多下載的地方  網上原始碼的名字叫:A

Android自定義View——曲線實現水波紋進度球

效果圖 原理分析 首先需要了解的水波紋實現效果,可以在部落格的自定義View專題找到,其實現原理如下 利用貝塞爾曲線繪製螢幕外和螢幕內的sin曲線 利用path將sin曲線的左下角和右下角連線起來成為一塊區域 通過不斷的平移sin曲線,然後平移完

Android 控制元件沿曲線運動(中)

看了Android貝塞爾曲線屬性動畫(上)是不是在罵我SB,換個貝塞爾曲線的起始點,控制點,終點,控制元件還是按原來路徑運動,So.... 下面我實現了控制元件在隨機的一個貝塞爾曲線上的運動 package com.example.propertyanimsecdemo;

Android開發之Path的高階用法用曲線繪製波浪線

前言:貝塞爾曲線分為一級曲線,二級曲線,三級曲線和多級曲線,利用貝塞爾曲線可以做出很多有意思的動畫和圖形,今天我們就來實現一個比較簡單的波浪線。 -----------------分割線--------------- 初步認識貝塞爾曲線: mPath.moveTo:設定起點

Android自定義View進階 - 曲線

                Path之貝塞爾曲線 作者微博: @GcsSloop 【本系列相關文章】 在

Android自定義View阻尼動畫&曲線實現

效果圖:直接上程式碼啦:package com.example.administrator.myapplication.customview; import android.animation.Animator; import android.animation.Anim

Android-曲線

從去年開始瞭解貝塞爾曲線之後,發現開發中,不管是Android/Ios平臺,還是web前端等,都有貝塞爾曲線的應用,通過繪製貝塞爾曲線,可以幫助開發者實現很多效果,例如一段時間內很流行的粘合型的下拉重新整理、又如天氣曲線圖,同時,以貝塞爾曲線為基礎的貝塞爾工具是所有繪圖軟

Android 繪圖基礎:Path(繪製三角形、曲線、正餘弦)

學習重點: 理解path的使用 理解貝塞爾曲線的繪製原理 可動正餘弦的繪製 Path的簡單介紹   在 Android 繪圖基礎:Canvas畫布——自定義View(繪製錶盤、矩形、圓形、弧、漸變) 中我們可以看到Canvas的強大功能,其實Canva

Android曲線 階的簡單處理

二階效果圖 控制點只有一個 private float mStartPointX; private float mStartPointY; private float mEndPointX; private fl

Android曲線-水波篇

在做自定義view時,很多時候會用到貝塞爾曲線這個東西去實現一些效果,像以前寫的那個仿直播點贊動畫的實現就是用到了貝塞爾曲線,這次說的水波也會用到貝塞爾曲線這個東西。 Android貝塞爾曲線api 首先看下貝塞爾曲線公式: 一階 二階 其

Android 自定義View高階特效,神奇的曲線

初始化引數 private static final String TAG = "BIZIER"; private static final int LINEWIDTH = 5; private static final int POINTWIDTH = 10; private Context mContex