使用ViewPager實現卡片疊加效果
背景
在開發項目時,需要對 App的某個資源模塊進行界面重構,其中在資源展示部分中新的交互以卡片疊加的效果替代了原來的資源組織樹門禁展示方式。在新的資源展示方式中,每一個新的卡片都是在最上面的,其順序以棧的形式存儲在內存。卡片支持疊加效果,左右滑動切換到下一頁或上一頁,且卡片中的資源是以列表的形式展示,支持上下滑動,上拉刷新,下拉加載更多。目前網上存在的卡片布局第三方庫,並不能滿足我們的項目需求,有的是無法達到疊加效果,有的會是卡片中不能有列表,否則會產生View滑動事件沖突,導致列表無法滑動,因此考慮使用已有的知識,自己實現這樣的功能。
實現
在Android系統中,沒有能直接能實現該效果的控件,可以實現左右滑動切換頁面的控件首先想到ViewPager,但ViewPager並不能直接實現頁面疊加效果,通過查閱資料,發現可以自定義ViewPager.PageTransformer接口去控制ViewPager中各個頁面的偏移顯示效果。
編碼嘗試:
1、創建基本界面結構:
首先我們先創建一個Activity,配置好頁面,就像以下效果。一個ViewPager,里面放fragment,由於卡片是圓角的,考慮到圓角可以使用CardView實現,所以在fragment里面再放一個CardView。還需要給ViewPager的setOffscreenPageLimit一個大一點的值,這樣可以使Viewpager預加載多個頁面。
正常情況下,ViewPager里面的內容是水平排列的,如下圖:

現在要做的第一步,就是將ViewPager里面所有的view都顯示在同一個位置,那么就需要自定義PageTransformer去實現了。
自定義PageTransformer:
PageTransformer介紹,當ViewPager中頁面滑動切換時,將會回調方法transformPage(View page, float position);該方法有兩個參數,第一個view當然就是當前正在滑動的頁面,第二個是一個float類型的值,不是我們平常見到的position位置,而是當前滑動狀態的表示,相對於當前position的position。它有三個臨界值-1 0 1,0代表當前屏幕顯示的view的position,1代表當前view的下一個view所在的position,-1代表當前view的前一個view所在的position。

當前view左滑、右滑時各個view positon的變化情況:

既然ViewPager里面的View默認是水平排列的,那么只要將每個view的x軸坐標更改為:view的寬度乘以下標的負數,這樣就排列在一起了,為了方便起見,還給view增加了一個透明度。代碼如下:
public void transformPage(View page, float position) {
//設置透明度
page.setAlpha(0.5f);
//設置每個View在中間,即設置相對原位置偏移量
page.setTranslationX((-page.getWidth() * position));
}
具體實現效果如下:

卡片都疊加在了一起,說明X方向水平偏移達到了預期效果,然后還需要實現卡片在Y方向垂直偏移,和卡片大小的縮放操作,就可以實現疊加效果了,定義了一個變量mOffset表示偏移量,賦值為40px。
代碼如下:
//設置水平方向偏移量
page.setTranslationX((-page.getWidth() * position));
//縮放比例
float scale = (page.getWidth() - mOffset * position) / (float) (page.getWidth());
//設置水平方向縮放
page.setScaleX(scale);
//設置豎直方向縮放
page.setScaleY(scale);
//設置豎直方向偏移量
page.setTranslationY(mOffset * position);

至此,卡片疊加效果已經達到了我們的預期效果,但此時左右卡片滑動時,卻發現不管怎么滑動都是沒有效果的。這是為什么呢?因為沒有處理划出去的那一頁,無論該接口傳過來參數值的是多少,我們都只是讓頁面疊加排列,因此需要增加一個下標判斷,即當position<= 0的情況下就是表示當前頁面在翻頁,接下來看代碼:
public void transformPage(View page, float position) {
if (position <= 0.0f) {
//被滑動的那頁,設置水平位置偏移量為0,即無偏移
page.setTranslationX(0f);
} else {//未被滑動的頁
page.setTranslationX((-page.getWidth() * position));
//縮放比例
float scale = (page.getWidth() - mOffset * position) / (float) (page.getWidth());
page.setScaleX(scale);
page.setScaleY(scale);
page.setTranslationY(mOffset * position);
}
}

效果雖然是達到了,但是為什么會留一個角呢?因為里面的view移動的是一個屏幕的寬度,當我們平移的時候剛好移動到了屏幕的外面,當然沒有問題。但是旋轉卻是以中心為原點進行旋轉的,所以自然,就會漏出一個角了。
解決方法是view進行旋轉的同時,將view的X軸進行減少,減少多少呢?從上圖看,大概⅓就差不多能夠移動到屏幕外面了。
代碼:
public void transformPage(View page, float position) {
if (position <= 0.0f) {//被滑動的那頁 position 是-下標~ 0
page.setTranslationX(0f);
//旋轉角度 45° * -0.1 = -4.5°
page.setRotation((45 * position));
//X軸偏移 li: 300/3 * -0.1 = -10
page.setTranslationX((page.getWidth() / 3 * position));
} else {
//縮放比例
float scale = (page.getWidth() - mScaleOffset * position) / (float) (page.getWidth());
page.setScaleX(scale);
page.setScaleY(scale);
page.setTranslationX((-page.getWidth() * position));
page.setTranslationY((mScaleOffset * 0.8f) * position);
}
}
應用
如下圖是資源頁面卡片層疊效果結合業務邏輯的具體實現:

在cardpager包中,CardPageTransformer實現了ViewPager.PageTransformer接口,用於控制ViewPager中頁面的偏移效果。DoorResourceView則是用於顯示整個資源頁面的根View,DoorResourceView里面包含一個ViewPager,該ViewPager中包含多個ResourceFragment,一個ResourceFragment就代表一個卡片的實現,每個卡片ResourceFragment中又包含一個列表控件(RecyclerView)用於顯示門禁點、區域或中心資源。
在卡片資源頁面中,在某一卡片頁面下拉刷新時,按照產品業務邏輯是需要將該卡片之后的卡片都從viewpager中移除,並且在點擊每個區域或中心都需要新開啟一個卡片,也需要移除該頁面之后的卡片,如下是實現代碼:
/**
* 移除之后的的fragment
*
* @param index 位置
*/
private void removeFragment(int index) {
if (mFragments.size() > index + 1 && index > -1) {
for (int i = mFragments.size() - 1; i > index; i--) {
mFragments.remove(i);
}
mAdapter.notifyDataSetChanged();
}
}
在實際操作中,發現當開啟到第三個卡片之后,卡片層疊明顯出現較大的偏差,有些卡片疊加效果並不顯示,與預期不符。並且在刷新過程中,動態增刪卡片后,都有可能會導致疊加效果突然消失,如下圖所示,本應該有多層卡片疊加效果,但在刷新后只剩下一層卡片:

經過多次打印日志、斷點調試后發現,在每次對卡片進行增加或刪除時,需要通過調用transformPage(..)方法對所有的卡片重新排序,完美的解決了該問題,實際代碼如下:
//避免刷新時卡片消失
//1.當前最前面的一頁縮放正確,層級顯示正確
int currIndex = mPager.getCurrentItem();
View view = mFragments.get(currIndex).getView();
mCardPageTransformer.transformPage(view, -9.999259E-4f);
//2.后面的頁縮放正確,層級顯示正確
for (int i = 0; i <= currIndex; i++) {
mCardPageTransformer.transformPage(mFragments.get(i).getView(), -currIndex + i);
}
已下是具體實現效果:

無論如何刷新資源,動態增刪卡片頁面,疊加效果也不會突然消失了。
總結
在開發新功能時,要多動腦思考,從原理上掌握實現方法,這樣當遇到問題時,就可以迅速定位,從根本上解決問題,保證了代碼的質量和功能的穩定性。
