天天看點

怎樣在Android上實作一個iOS多任務清單效果

| 導語 蘋果在iOS 7的時候就引入了卡片清單進行多任務切換,往上滑動就可以移除掉某個app,到了最新的iOS 13,其多任務清單也是在這種卡片清單樣式的基礎上進行了優化;Android陣營的華為,小米等廠商也是陸續地引入這種多任務清單樣式。 那怎樣在Android上實作一個iOS多任務清單效果呢?

一. 實作效果

先看看iOS的多任務清單長啥樣。

怎樣在Android上實作一個iOS多任務清單效果

再來看看華為的多任務清單。

怎樣在Android上實作一個iOS多任務清單效果

最後來看看本篇實作的效果。

怎樣在Android上實作一個iOS多任務清單效果

二. 實作方案

實作這樣一個iOS多任務清單,需要具備以下幾個基本能力:

       1)橫向清單

       2)卡片堆疊效果

       3)滑動移除動畫

       4)支援大量資料綁定,每個卡片都有獨立的容器管理

縱觀Android标準的控件庫,能想到的就隻有ViewPager比較合适,其首先滿足第1點,ViewPager又是直接使用Adapter來管理資料,然後通過Fragment來管理每個item,滿足第4點(這一點很重要,Adapter+Fragment這種成熟的設計,會讓調用代碼很簡潔),剩下的2,3點都是UI層面的效果,應該改改ViewPager的源碼就可以實作了吧,微笑。

三. 基于ViewPager的代碼實作

       如果對實作細節感興趣,請繼續往下看,以下内容超過2000字!!!

基于ViewPager現有能力,要仿照iOS多任務清單效果,還需要修改以下幾點:

       1)ViewPager預設的item排列是橫向順序排列,需要變成卡片疊加排列

       2)ViewPager不管你滑動地多快,他隻會切換到前一個或後一個item,需要變成可以根據滑動速度滾動不同的距離(可以了解成fling效果)

       3)需要支援上下滑動item以移除,移除後,其後面的item要有補齊上來的動畫效果

1. 卡片疊加效果

我們要實作的卡片疊加效果大概分兩步,第1步是讓item的寬高縮放到一個卡片的大小,第2步是卡片之間有重疊,而且重疊部分會随着滑動過程在變化(如果是華為那種多任務清單,這一步就省略了)。

      下面看下實作細節。

1.1. 卡片寬高

我們知道,正常情況我們在Fragment傳回的View是鋪滿ViewPager寬高的,上下的空隙我們可以設定padding來實作,左右的是不是也可以設定padding來實作呢?

       實踐一下,如下效果:

怎樣在Android上實作一個iOS多任務清單效果

左右的效果果然不符合我們預期,item的寬度是變小了,但左右的padding一直空白着,經過一番嘗試,最終通過一個屬性解決了這個問題:

viewPager.setClipToPadding(false);

       這個是ViewGroup的基礎接口,預設是true,設為false後,就可以允許内容區顯示在padding區域内,不止是ViewPager,平時的listview,scrollview這類滾動控件,都是可以通過這個接口來避免上述問題,内部實作原理這裡不展開。

       另外再談一個問題,設定ViewPager的padding,影響到的應該是整個ViewPager的内容區域(即所有item view加起來的區域)大小,為什麼作為ViewPager的一個item view也受影響?ViewPager的源碼有一個接口如下:

怎樣在Android上實作一個iOS多任務清單效果

       getClientWidth這個接口在ViewPager裡被頻繁使用到,包括在onMeasure裡對child View進行measure計算的時候,可以看出,ViewPager在一開始設計的時候,就是假設一個item 的寬度(即ClientWidth)是measureWidth-paddingLeft-paddingRight的,微笑。

1.2. 卡片重疊

       ViewPager提供的接口已經可以支援這種效果,有兩種方法。

       第一種比較簡單,直接調viewPager.setPageMargin,給一個負值,卡片就會重疊在一起,但重疊的區域大小不會随着滾動而變化,顯然不是我們想要的;

       第二種是使用PageTransformer,滾動過程中,ViewPager會回調transformPage(View page, float position),在這裡面做想要的變化就行,PageTransformer具體的使用方法這裡不細講,網上有很多例子,放到我們這個場景下,變化邏輯是,item從右邊往左邊移動過程中,item view逐漸放大,x方向的偏移也會逐漸增大,具體代碼在demo的DefaultPageTransformer裡面。需要注意的是,setPageTransformer方法的第一個參數(reverseDrawingOrder)需要是true,因為我們的效果是後面的item被疊在前面item的後面,而ViewPager預設的實作是後面的item蓋在前面item上面。

這裡講一下ViewPager是怎麼調用PageTransformer的,隻有一處地方回調,如下:

怎樣在Android上實作一個iOS多任務清單效果

可以看到,ViewPager在onPageScrolled方法裡都會對每一個child調用mPageTransformer.transformPage來進行View變化,這裡重點看transformPos這個值,也即transformPage回調方法裡的position參數,transformPos = (child.getLeft() - scrollX) / getClientWidth(),getClientWidth也即item view的寬度,這麼算什麼意思?

       有一點抽象,以目前顯示在ViewPager的最左邊item A為例,A的left緊貼着ViewPager的left,這時候child.getLeft() – scrollX = 0,即transformPos=0,再假設緊挨着A後面的item是B,B的left應該是A.left+A.width,是以B的transformPos=(B.getLeft() - scrollX) / getClientWidth()=(A.left+A.width-A.left) / A.width = 1,是以從B的位置滾動到A的位置,position也從1變化到0,其他位置的position以此類推,當item已經在View Pager顯示範圍左邊時(超出螢幕外),這個值就是負的。

2. 快速滾動

第1點的實作,到目前還不需要修改ViewPager的源碼,但到了第2點這裡,就需要在ViewPager的源碼基礎上來修改我們想要的邏輯了。

先看現有ViewPager在onTouchEvent裡對于Up事件是怎麼處理的。

怎樣在Android上實作一個iOS多任務清單效果

重點看标紅的,第一步先調determineTargetPage算出最終要滾動到的page位置,第二步調setCurrentItemInternal滾動到最終位置;determineTargetPage的邏輯比較簡單,可以自己看看源碼裡的實作,主要就是根據目前的滑動方向,确定要滾動到上一個item還是下一個item,而我們現在想要快速滑動松手後,可以滾動到更遠位置,是不是直接修改determineTargetPage的邏輯就行了?

這裡直接看下實作後的代碼:

怎樣在Android上實作一個iOS多任務清單效果

标紅的部分是這次新加的,大概的邏輯是,根據目前的速度,在一個最大可滑行距離MAX_FLING_ITEM範圍内,算出一個最終目标page距離,這隻是一個比較簡單的實作方法,可以根據需求的需要做相應的算法調整。

3. 移除動畫

要做到iOS多任務清單的移除效果,需要分兩步,第一步是對要移除的item做上下滑動動畫;第二步是item滑出去後,其後面的item要做偏移動畫補齊到目前空白的位置。

3.1. item上下滑動動畫

這一步實作原理比較簡單,就是在ViewPager的onTouchEvent裡對move事件做上下滑動檢測,滿足條件時對目前的item view做上下移動即可,當up事件到達時,再根據速度和偏移條件,判斷是否真要滑動移除,要的話再觸發相應的動畫。

原理雖簡單,但實作起來代碼還是不少,這裡不詳細展開,可以看看MultCardViewPager新加的tryRemoveAItem方法。

怎樣在Android上實作一個iOS多任務清單效果

3.2. 對移除item後面的item做補齊動畫

在第一步的item移除動畫結束後,需要開始對後面的item做補齊動畫,邏輯在removeItemViewAndAnimate方法裡,如下:

怎樣在Android上實作一個iOS多任務清單效果

       這個方法主要做的事情是找出移除item的所有後續item,如果存在後續item,則調animateRestView觸發補齊動畫,這個方法這裡不詳細講,需要關注的是,擷取後續item需要通過mDrawingOrderedChildren來擷取,而不能通過getChildAt來擷取,因為ViewGroup的child數組存放View的順序并不完全對應螢幕顯示item的從左到右順序(為什麼?因為ViewPager可以先往後滑,再往前滑,這時候前面的item可能是剛建立出來的,addView的時候肯定就存在child數組的最後面,但事實上這個view是顯示在螢幕的最前面),而mDrawingOrderedChildren可以了解為ViewPager自己儲存的一個和目前顯示順序相同的數組,直接拿來用就行了。

       下面看看animateRestView怎麼做補齊動畫。

怎樣在Android上實作一個iOS多任務清單效果

第一步先初始化Animator的相關參數,之是以通過updateListener來做動畫,是因為後面顯示的item可能有多個,在onAnimationUpdate裡對所有要做偏移動畫的View調整位置即可;第二步是需要不斷地調整View的位置,注意的是,這裡再分兩步,第一步是調整View的left,為什麼是setLeft,而不是setTranslationX,因為這裡假設移除item已經移除掉了,後面的view就應該調整目前坐标位置了,嚴格來說,并不是在做偏移動畫,而是在調整位置,第二步需要調mPageTransformer.transformPage來确定最終的變化,因為我們這種場景下,item的位置受PageTransformer影響。

動畫做完了,是不是就結束了?

       不是的,還涉及到一個資料問題,我們上面移除item都是在View(ViewPager)層做的,可以說,隻是展示效果上實作了移除一個item,但真實的資料是在Adapter裡,需要在動畫結束後回調給Adapter,讓Adapter移除掉相應的資料,最後調notifyDataSetChanged同步資料。這裡新增了OnItemRemoveListener作為回調Listener,具體可以參考源碼。

四. 總結

最後總結一下,本篇介紹了如何基于ViewPager,實作了一個類似iOS多任務清單效果,主要目的在于驗證方案的可行性,即如何在已有控件的基礎上快速複用來實作我們要的效果,雖然效果實作出來了,但對比iOS的效果,仍然有不少地方需要優化,比如提高動畫的細膩程度和流暢度(這方面Android和iOS相比真有差距);另外,細心的同學可能會發現,iOS的多任務清單是從右邊開始,而我們的實作效果(或者說ViewPager)是從左邊開始的,要實作成從右邊開始,理論上可以實作,即把ViewPager所有和X坐标相關的操作都給他反過來就是了,目測需要改動的地方不少,先不折騰了,實作了的同學可以分享下哈。

更新:

       偶然看到androidx包下多了個ViewPager2,吃驚,看一下代碼,注釋如下:

怎樣在Android上實作一個iOS多任務清單效果

       可以看到,ViewPager2已經支援了從右到左的布局了,也支援豎向布局,其源碼實作是封裝了RecyclerView,但接口幾乎和ViewPager一緻,也解決了RecyclerView不能直接使用Fragment的問題,膩害呀!

       目前ViewPager2還是處于beta版,估計還有一些bug,期待後續正式上線