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