免费高清特黄a大片,九一h片在线免费看,a免费国产一级特黄aa大,国产精品国产主播在线观看,成人精品一区久久久久,一级特黄aa大片,俄罗斯无遮挡一级毛片

分享

FlyRefresh

 歲月如風(fēng)99 2016-11-25

編輯推薦:稀土掘金,這是一個(gè)針對(duì)技術(shù)開(kāi)發(fā)者的一個(gè)應(yīng)用,你可以在掘金上獲取最新最優(yōu)質(zhì)的技術(shù)干貨,不僅僅是Android知識(shí)、前端、后端以至于產(chǎn)品和設(shè)計(jì)都有涉獵,想成為全棧工程師的朋友不要錯(cuò)過(guò)!

原文:http://www./flyrefresh/ ,很給力有木有。


幾天前在網(wǎng)上看到 @Zee Young 的一個(gè)下拉刷新的設(shè)計(jì) Replace。如下圖:

replace-zeeyoung.gif


第一眼看到這個(gè)設(shè)計(jì)就覺(jué)得眼前一亮,在Dribble上獲得了 1.7k 多的 like,微博上也有大量轉(zhuǎn)發(fā)??梢?jiàn)確實(shí)一個(gè)很成功的設(shè)計(jì)。我準(zhǔn)備在 Android 上來(lái)實(shí)現(xiàn)它。


經(jīng)過(guò)幾天的折騰,最終實(shí)現(xiàn)并開(kāi)源在 Github 上,項(xiàng)目地址: FlyRefresh,實(shí)際效果如下圖:


flyrefresh-screenshot.gif


總體上還原了設(shè)計(jì)的70%~80%,還有一些細(xì)節(jié)需要改進(jìn)。因?yàn)闆](méi)有拿到設(shè)計(jì)師的設(shè)計(jì)源文件,動(dòng)畫和顏色的細(xì)節(jié)并沒(méi)有能夠做的完全一致。下面分享一下實(shí)現(xiàn)的過(guò)程。

1 分析設(shè)計(jì)效果圖

要實(shí)現(xiàn)這個(gè)設(shè)計(jì),就要非常仔細(xì)的分析這個(gè)動(dòng)畫的每個(gè)細(xì)節(jié)。由于沒(méi)有設(shè)計(jì)源文件,我最開(kāi)始就一直盯著這個(gè) GIF 圖看,然后構(gòu)思一下大致的實(shí)現(xiàn)流程。在寫代碼的過(guò)程中,甚至把 GIF 圖分解成一幀一幀的圖片來(lái)分析,把 GIF 圖分解的方法如下:


  1. convert -coalesce animation.gif frame.png


從設(shè)計(jì)圖中,得到大致如下的結(jié)論:

  1. 總體上是一個(gè)下拉刷新的效果;

  2. 頁(yè)面上大概分為兩部分:頭部和內(nèi)容部分;

  3. 頭部塊疊放在內(nèi)容塊的下面;

  4. 內(nèi)容塊可以下拉,放手能夠回彈,并觸發(fā)飛機(jī)飛出的動(dòng)畫;

  5. 頭部塊隨著下拉過(guò)程中有動(dòng)畫(這個(gè)是重點(diǎn),后面會(huì)詳細(xì)介紹);

2 軟件設(shè)計(jì)

軟件上我打算把它實(shí)現(xiàn)成一個(gè)下拉刷新的控件。一說(shuō)到下拉刷新,有一大堆的開(kāi)源實(shí)現(xiàn),都或多或少的需要一些修改才能滿足我這里的需求,我打算自己實(shí)現(xiàn)一個(gè)量身定做的。 控件的布局關(guān)系大概如下圖所示:

header-size

布局分為上下兩塊,上部實(shí)線框?yàn)轭^部,虛線框?yàn)閮?nèi)容區(qū)域。內(nèi)容區(qū)域覆蓋在頭部上面。通常情況下,內(nèi)容區(qū)域覆蓋頭部,留出頭部 Normal height 的高度。內(nèi)容區(qū)域可以上滑,最多覆蓋到Shrink height高度;下滑最多可以把頭部區(qū)域留出Expended height,下滑超過(guò)Normal height的時(shí)候,放手會(huì)自動(dòng)彈回。內(nèi)容區(qū)域可以滑動(dòng)的距離為Expended_height - Shrink_height。

這是一個(gè)比較通用的布局模式,只要重載這個(gè)布局,基本上可以涵蓋了所有下刷新的模式。例如Shrink_height=0的話,頭部可以全部收起來(lái)的;如果Shrink_height==Normal height的話,就是一個(gè)有固定頭部的下拉控件;如果Expended_height > Normal height > Shrink_height,就是頭部可以擴(kuò)展收縮的下拉控件。

頭部動(dòng)畫部分,這里可能不同的設(shè)計(jì),變化最大的部分。但是有一個(gè)共同點(diǎn),就是頭部顯示會(huì)根據(jù)內(nèi)容塊的滑動(dòng)情況來(lái)變化。在軟件上,設(shè)計(jì)出接口,不同的動(dòng)畫,實(shí)現(xiàn)此接口就可以。本文的 FlyRefresh 的動(dòng)畫只是這個(gè)接口的一個(gè)具體實(shí)現(xiàn)。如果要實(shí)現(xiàn)其他的刷新動(dòng)畫,并不需要做多大的改動(dòng)。

3 具體實(shí)現(xiàn)


根據(jù)上面的設(shè)計(jì),畫出類圖如下:

flyrefresh-uml

3.1 PullHeaderLayout

這是一個(gè)基類,實(shí)現(xiàn)了布局和滑動(dòng)功能。從類圖中可以看到,這個(gè)布局中主要包含兩部分View:mHeaderView,mContent,另外還有 mFlyView,這頭部和內(nèi)容連接處的按鈕。布局也比較簡(jiǎn)單,具體實(shí)現(xiàn)可以參考代碼 layoutChildren()。

滑動(dòng)是這里這個(gè)類的實(shí)現(xiàn)重點(diǎn),這里需要特別小心處理 Touch 事件。Touch 事件需要滿足的是,如果 ContentView 可以整體滑動(dòng),我們的 Layout 就需要截獲 Touch 事件。否這需要把 Touch 事件傳遞給子 View,這樣才不會(huì)影響內(nèi)部子 View 的功能。

在處理Touch事件的時(shí)候,需要時(shí)刻判斷 View 所處的狀態(tài),這里借助兩個(gè)輔助類 HeaderControllerScrollChecker。HeaderController 主要是保存和判斷當(dāng)前 Header 的高度和狀態(tài)。ScrollChecker 用來(lái)檢測(cè) ContentView 是否可以滑動(dòng)。為了讓滑動(dòng)流暢,還需要小心處理 Fling 狀態(tài),這里借助了 ScrollerVelocityTracker兩個(gè)工具類。

另外值得一提的是,當(dāng)滑動(dòng) Header 的高度大于 Normal height 的時(shí)候,ContentView 需要自動(dòng)恢復(fù)回去。仔細(xì)觀察原設(shè)計(jì)的動(dòng)畫,這個(gè)回彈過(guò)程是有類似橡皮筋一樣的彈性的。這里利用了屬性動(dòng)畫類,使用自定義的插值器實(shí)現(xiàn),具體參考源代碼的 'ElasticOutInterpolator' 類(參考自:AnimationEasingFunctions)。

因?yàn)檫@里這個(gè)類的功能和常見(jiàn)的下拉刷新的類似,這樣就有很多優(yōu)秀的開(kāi)源庫(kù)可以參考,我的實(shí)現(xiàn)中很大程度上借鑒了優(yōu)秀的開(kāi)源庫(kù):Ultra Pull To Refresh,讓我避免了很多坑。

3.2 FlyRefreshLayout

這里 FlyRefreshLayout 直接繼承與上面的 PullHeaderLayout。因?yàn)榇蟛糠止ぷ鞫荚诨愔型瓿?,這個(gè)類實(shí)現(xiàn)很簡(jiǎn)單。這個(gè)類主要是為了簡(jiǎn)化使用,默認(rèn)添加了動(dòng)畫頭部 MountanScenceView 和添加了刷新的接口 OnPullRefreshListener。

紙飛機(jī)的動(dòng)畫就在這里實(shí)現(xiàn)。紙飛機(jī)動(dòng)畫包括三個(gè)部分:

  1. 隨著下拉,逆時(shí)針轉(zhuǎn)動(dòng);

  2. 放手的時(shí)候,觸發(fā)刷新,發(fā)射出去;

  3. 刷新完成,飛機(jī)飛回來(lái),回到原來(lái)的位置。

動(dòng)畫 1:實(shí)現(xiàn)非常簡(jiǎn)單,因?yàn)?PullHeaderLayoutonMoveHeader() 的回調(diào),只要重載這個(gè)函數(shù),設(shè)置旋轉(zhuǎn) view.setRotation(degree)即可;

動(dòng)畫 2:仔細(xì)觀察設(shè)計(jì),這是一個(gè)組合動(dòng)畫:整體向右上角移動(dòng),同時(shí)繞 X 軸做 3D 轉(zhuǎn)動(dòng),飛機(jī)頭部慢慢趨向水平,并且慢慢縮小。這里需要實(shí)現(xiàn),因?yàn)樾枰险鎸?shí)的物理效果,否這可能看起來(lái)會(huì)非常生硬。注意這里,我們可以使用 PathInterpolatorCompat 來(lái)幫助我們生成任意貝塞爾曲線插值器。

動(dòng)畫 3:這一步和動(dòng)畫2類似。

在紙飛機(jī)執(zhí)行動(dòng)畫的同時(shí),頭部的山脈和樹(shù)也會(huì)隨著動(dòng),這里動(dòng)效比較復(fù)雜,而且比較獨(dú)立,我這里就寫到一個(gè)專門的類 MountanScenceView 中,見(jiàn) 3.3 節(jié)。

3.3 MountanScenceView

最后來(lái)實(shí)現(xiàn)最抓人眼球的 MountanScenceView。和之前的思路一樣,我們先來(lái)分解一下原設(shè)計(jì)的動(dòng)畫:山脈按照遠(yuǎn)近分為三層景深,近處的山的顏色比較深,而且隨著下拉的時(shí)候也會(huì)向下移動(dòng),并且呈現(xiàn)視差,并且伴隨這樹(shù)的扭動(dòng),這是整個(gè)動(dòng)畫的點(diǎn)睛之筆。

從畫面的風(fēng)格來(lái)看,這是矢量圖,隨著畫面大小后者長(zhǎng)寬變化,山脈應(yīng)該能夠自動(dòng)適應(yīng),并充滿視圖。需要注意的是,不管畫面怎么變化,需要保持長(zhǎng)寬比不變。這樣的話,用如果用圖片就不能很好的滿足要求了,所以決定是 Path 來(lái)手動(dòng)繪制整個(gè)場(chǎng)景。因?yàn)閳?chǎng)景要適應(yīng) View 的大小,所以在 onMeasure() 的時(shí)候,計(jì)算出縮放比例:

  1. @Override
  2. protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {  
  3.     super.onMeasure(widthMeasureSpec, heightMeasureSpec);
  4.     final float width = getMeasuredWidth();
  5.     final float height = getMeasuredHeight();
  6.     mScaleX = width / WIDTH;
  7.     mScaleY = height / HEIGHT;
  8.  
  9.     updateMountainPath(mMoveFactor);
  10.     updateTreePath(mMoveFactor, true);
  11. }

繪制山脈比較簡(jiǎn)單,Path 也不復(fù)雜,比如其中一個(gè)山的Path的生成如下:

  1. private void updateMountainPath(float factor) {
  2.  
  3.   mTransMatrix.reset();
  4.   mTransMatrix.setScale(mScaleX, mScaleY);
  5.  
  6.   int offset1 = (int) (10 * factor);
  7.   mMount1.reset();
  8.   mMount1.moveTo(0, 95 + offset1);
  9.   mMount1.lineTo(55, 74 + offset1);
  10.   mMount1.lineTo(146, 104 + offset1);
  11.   mMount1.lineTo(227, 72 + offset1);
  12.   mMount1.lineTo(WIDTH, 80 + offset1);
  13.   mMount1.lineTo(WIDTH, HEIGHT);
  14.   mMount1.lineTo(0, HEIGHT);
  15.   mMount1.close();
  16.   mMount1.transform(mTransMatrix);
  17.   ...
  18. }


其實(shí)由代碼可知,其實(shí)就是畫一個(gè)封閉的多邊形。其中 offset1 是根據(jù)滑動(dòng)的程度計(jì)算出的移動(dòng)距離。

下面重點(diǎn)是看樹(shù)的繪制。這里的樹(shù)可以分解成兩部分:樹(shù)干和樹(shù)枝。樹(shù)干可以看成是一個(gè)矩形,然后上面加一個(gè)三角形;樹(shù)枝是下部一個(gè)半圓,往上逐漸收縮成到一點(diǎn)。其實(shí)這里還是比較簡(jiǎn)單,但問(wèn)題是需要隨著滑動(dòng),樹(shù)要逐漸彎曲。

這里我做了很多嘗試,例如每條邊都用貝塞爾曲線,效果不都是很理想。最后還是采用比較“簡(jiǎn)單粗暴”的方法:

整個(gè)樹(shù)對(duì)稱中心,用一條“不可見(jiàn)”的貝塞爾曲線支撐,樹(shù)干和樹(shù)枝圍繞這條中心線密集的用直線堆積構(gòu)建。樹(shù)的彎曲效果,只需要移動(dòng)貝塞爾曲線的控制點(diǎn)。

具體實(shí)現(xiàn)是這樣的,首先我們還是利用 PathInterpolatorCompat 來(lái)創(chuàng)建一個(gè)貝塞爾曲線插值器:


  1. Interpolator interpolator = PathInterpolatorCompat.create(0.8f, -0.5f * factor);


其中, (0.8, -0.5*factor)是控制點(diǎn),factor 是彎曲程度,這里的參數(shù)根據(jù)需要可以調(diào)整。然后對(duì)這個(gè)曲線進(jìn)行采樣,獲得歸一化曲線坐標(biāo),我這里采樣25個(gè)點(diǎn)。我感覺(jué)這樣實(shí)現(xiàn)并不完美,這里就是我前面說(shuō)的“簡(jiǎn)單粗暴”的原因。采樣的方法如下:


  1. final int N = 25;  
  2. final float dp = 1f / N;  
  3. final float dy = -dp * height;  
  4. float y = y0;  
  5. float p = 0;  
  6. float[] xx = new float[N + 1];  
  7. float[] yy = new float[N + 1];  
  8. for (int i = 0; i <= N; i++) {  
  9.     // 把歸一化的采樣坐標(biāo)轉(zhuǎn)換為實(shí)際坐標(biāo)
  10.     xx[i] = interpolator.getInterpolation(p) * maxMove + x0;
  11.     yy[i] = y;
  12.     y += dy;
  13.     p += dp;
  14. }


然后,沿著這些采樣點(diǎn),逐點(diǎn)用 path.lineTo() 構(gòu)建樹(shù)枝和樹(shù)干。構(gòu)建樹(shù)干的代碼如下:


  1. final float trunkSize = width * 0.05f;  
  2. mTrunk.reset();  
  3. mTrunk.moveTo(x0 - trunkSize, y0);  
  4. int max = (int) (N * 0.7f); // 樹(shù)干的高度為整個(gè)樹(shù)的0.7  
  5. int max1 = (int) (max * 0.5f); // 三角形收縮開(kāi)始的點(diǎn)  
  6. float diff = max - max1;  
  7. // 添加樹(shù)干左邊的邊緣
  8. for (int i = 0; i < max; i++) {  
  9.     if (i < max1) { // 等距
  10.         mTrunk.lineTo(xx[i] - trunkSize, yy[i]);
  11.     } else { // 線性收縮
  12.         mTrunk.lineTo(xx[i] - trunkSize * (max - i) / diff, yy[i]);
  13.     }
  14. }
  15.  
  16. // 添加樹(shù)干右邊的邊緣,這里和上面對(duì)稱
  17. for (int i = max - 1; i >= 0; i--) {  
  18.     if (i < max1) {
  19.         mTrunk.lineTo(xx[i] + trunkSize, yy[i]);
  20.     } else {
  21.         mTrunk.lineTo(xx[i] + trunkSize * (max - i) / diff, yy[i]);
  22.     }
  23. }
  24. mTrunk.close();


因?yàn)闃?shù)的形態(tài)基本一致,只是大小和顏色不一樣,所以只要生成一個(gè)即可。生成樹(shù)枝 Path 的代碼和上面類似:


  1. mBranch.reset();  
  2. int min = (int) (N * 0.4f);  
  3. diff = N - min;
  4.  
  5. mBranch.moveTo(xx[min] - branchSize, yy[min]);  
  6. // 添加樹(shù)枝底部的半圓弧
  7. mBranch.addArc(new RectF(xx[min] - branchSize, yy[min] - branchSize, xx[min] + branchSize, yy[min] + branchSize), 0f, 180f);  
  8. // 添加樹(shù)枝左邊的邊緣
  9. for (int i = min; i <= N; i++) {  
  10.     float f = (i - min) / diff;
  11.     // 注意這里不是線性收縮,這樣看起來(lái)樹(shù)會(huì)更加圓潤(rùn)
  12.     mBranch.lineTo(xx[i] - branchSize + f * f * branchSize, yy[i]);
  13. }
  14. // 添加樹(shù)枝右邊的邊緣,和上面對(duì)稱
  15. for (int i = N; i >= min; i--) {  
  16.     float f = (i - min) / diff;
  17.     mBranch.lineTo(xx[i] + branchSize - f * f * branchSize, yy[i]);
  18. }


到這里,最關(guān)鍵的部分就已經(jīng)完成了。接下來(lái)就是把這些 Path 畫出來(lái)。這里畫的時(shí)候就是一些 canvas 的變換了,這里就不貼代碼了。可以直接參考源代碼。

3.4 列表動(dòng)畫的實(shí)現(xiàn)

列表本身不是 FlyRefresh 庫(kù)的重點(diǎn)。為了盡量還原原設(shè)計(jì),這里也實(shí)現(xiàn)一下。這里的列表可以用 ListView 或者 RecyclerView。因?yàn)?RecyclerView 對(duì)動(dòng)畫控制更靈活,這里就選用它。

如果仔細(xì)觀察,下拉回彈的時(shí)候,列表的第一項(xiàng)會(huì)因?yàn)閼T性晃動(dòng)一下。實(shí)現(xiàn)方法如下:


  1. private void bounceAnimateView(View view) {  
  2.     ...
  3.     Animator swing = ObjectAnimator.ofFloat(view, "rotationX", 0, 30, -20, 0);
  4.     swing.setDuration(400);
  5.     swing.setInterpolator(new AccelerateInterpolator());
  6.     swing.start();}


然后就是刷新完成,插入新的項(xiàng)的時(shí)候的動(dòng)畫。這可以通過(guò)給 RecyclerView 設(shè)置自定義的 ItemAnimator 來(lái)實(shí)現(xiàn)。為了方便,我這里直接用了開(kāi)源庫(kù) RecyclerView Animators,重載了BaseItemAnimator,插入新項(xiàng)的動(dòng)畫如下:


  1. @Override
  2. protected void preAnimateAddImpl(RecyclerView.ViewHolder holder) {  
  3.     // 設(shè)置初始狀態(tài)
  4.     View icon = holder.itemView.findViewById(R.id.icon);
  5.     icon.setRotationX(30);
  6.     View right = holder.itemView.findViewById(R.id.right);
  7.     // 注意這里是沿著最左邊旋轉(zhuǎn)
  8.     right.setPivotX(0);
  9.     right.setPivotY(0);
  10.     right.setRotationY(90);
  11. }
  12.  
  13. @Override
  14. protected void animateAddImpl(final RecyclerView.ViewHolder holder) {  
  15.     View target = holder.itemView;
  16.     View icon = target.findViewById(R.id.icon);
  17.     Animator swing = ObjectAnimator.ofFloat(icon, "rotationX", 45, 0);
  18.     swing.setInterpolator(new OvershootInterpolator(5));
  19.  
  20.     View right = holder.itemView.findViewById(R.id.right);
  21.     Animator rotateIn = ObjectAnimator.ofFloat(right, "rotationY", 90, 0);
  22.     rotateIn.setInterpolator(new DecelerateInterpolator());
  23.  
  24.     AnimatorSet animator = new AnimatorSet();
  25.     animator.setDuration(getAddDuration());
  26.     animator.playTogether(swing, rotateIn);
  27.  
  28.     animator.start();
  29. }


完成的其實(shí)就是 icon 的晃動(dòng)和內(nèi)容的 3D 旋轉(zhuǎn)。

4 寫在最后

首先,非??隙ǖ氖?Zee Young 的這個(gè)設(shè)計(jì)是很成功。因?yàn)樗倪@個(gè)漂亮的設(shè)計(jì),我的這個(gè)庫(kù)在 Github 這幾天也收獲了 800 多個(gè) Star,而且還一度在 Trending 的總榜排第一。我非常清楚,代碼實(shí)現(xiàn)質(zhì)量并不是多完美,大家都是被這個(gè)設(shè)計(jì)所吸引。

但是,在實(shí)現(xiàn)的過(guò)程中,我也注意到這個(gè)設(shè)計(jì)的些許不足:

  1. 作為一個(gè)下拉刷新設(shè)計(jì),一般包含至少三個(gè)狀態(tài):空閑狀態(tài),下拉,刷新中,刷新完成(可以細(xì)分為:刷新成功和刷新失?。?。這個(gè)設(shè)計(jì)中,缺少了刷新中的狀態(tài),或者說(shuō)不是很明確。我在實(shí)現(xiàn)中,使用紙飛機(jī)飛出,表示在刷新中,飛機(jī)飛回來(lái),表示刷新完成。這樣并不是很好,因?yàn)轱w機(jī)飛出去,并不是一個(gè)很明顯的刷新中的動(dòng)畫。對(duì)比普通的下拉刷新,是有一個(gè)轉(zhuǎn)動(dòng)的 ProgressBar 表示正在處理;

  2. 這個(gè)設(shè)計(jì)中,紙飛機(jī)按鈕的作用是什么?按照 Material Design 的規(guī)范,這是一個(gè) Float Action Button,主要用來(lái)做正向的操作。這里主要是用來(lái)刷新動(dòng)畫,如果點(diǎn)擊這個(gè)按鈕,紙飛機(jī)飛出去,動(dòng)畫并不能很好的連貫起來(lái),感覺(jué)也是有點(diǎn)怪怪的。

最后,源代碼在這里:FlyRefresh。


    本站是提供個(gè)人知識(shí)管理的網(wǎng)絡(luò)存儲(chǔ)空間,所有內(nèi)容均由用戶發(fā)布,不代表本站觀點(diǎn)。請(qǐng)注意甄別內(nèi)容中的聯(lián)系方式、誘導(dǎo)購(gòu)買等信息,謹(jǐn)防詐騙。如發(fā)現(xiàn)有害或侵權(quán)內(nèi)容,請(qǐng)點(diǎn)擊一鍵舉報(bào)。
    轉(zhuǎn)藏 分享 獻(xiàn)花(0

    0條評(píng)論

    發(fā)表

    請(qǐng)遵守用戶 評(píng)論公約

    類似文章 更多