Android開發之ViewPager切換動畫(只有你想不到,沒有做不到)

好久不見,這段時間忙著做自己的產品,又有一段時間沒更博了,今天帶來ViewPager的切換動畫,先來看下效果圖:

ViewPager畫廊效果
ViewPager卡片效果

這篇文章打算從3點切入:
1、關于clipChildren屬性的了解與使用(實現ViewPager單屏顯示多頁面效果)
2、關于ViewPager切換動畫的基礎和示例講解
3、關于PageTransFormer的實例講解

1、關于clipChildren屬性的了解與使用(實現ViewPager一屏顯示多頁面效果)

關于clipChildren這個屬性,因為并不是很常用,可能有些朋友不熟悉甚至都沒見過,其實它非常強大,可以幫你完成一些特殊的UI效果。
舉個栗子:

底部欄的實現

對于這種底部Tab欄,大家應該再熟悉不過吧?那么中間這個突出來的播放按鈕應該怎么實現呢?有的朋友會說用一層幀布局FrameLayout或者相對布局RelativeLayout作為外層,然后里面再嵌套一層線性布局LinearLayout,把其它部分的背景顏色設置成透明不就可以了。這樣做其實是可以的,只不過這邊我想帶給你一種更加省時省力的方式,就是clipChildren這個屬性的應用。
clipChildren這個屬性是用來修飾ViewGroup容器的,它可以限制處于當前這個容器里的子控件是否可以越界繪制,默認為true,也就是限制子控件不允許越界,然后再來看下上面的底部Tab欄,我們不需要嵌套布局,只需要在根節點添加android:clipChildren="false"這個屬性即可,來看一下示例代碼:

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:clipChildren="false">

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="60dp"
        android:layout_alignParentBottom="true"
        android:background="#03b9fc"
        android:orientation="horizontal">

        <ImageView
            android:layout_width="0dp"
            android:layout_height="match_parent"
            android:layout_weight="1"
            android:src="@mipmap/ic_launcher" />

        <ImageView
            android:layout_width="0dp"
            android:layout_height="match_parent"
            android:layout_weight="1"
            android:src="@mipmap/ic_launcher" />

        <ImageView
            android:layout_width="0dp"
            android:layout_height="80dp"
            android:layout_gravity="bottom"
            android:layout_weight="1"
            android:src="@mipmap/ic_launcher" />

        <ImageView
            android:layout_width="0dp"
            android:layout_height="match_parent"
            android:layout_weight="1"
            android:src="@mipmap/ic_launcher" />

        <ImageView
            android:layout_width="0dp"
            android:layout_height="match_parent"
            android:layout_weight="1"
            android:src="@mipmap/ic_launcher" />
    </LinearLayout>
</RelativeLayout>
底部Tab的實現

上面是效果圖,我們允許相對布局RelativeLayout的子控件越界繪制,所以中間的icon圖片可以突破LinearLayout布局,這里還需要注意的一點是,我設置中間的ImageView的對齊方式為android:layout_gravity="bottom",不然圖標是會往下沉屏幕之外去的。

有了這個基礎后,我們來實現下ViewPager單屏顯示多頁面的效果

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:clipChildren="false">

   <android.support.v4.view.ViewPager
        android:id="@+id/vp_main_pager"
        android:layout_width="280dp"
        android:layout_height="200dp"
        android:layout_centerInParent="true"
        android:clipChildren="false" />

</RelativeLayout>

這是一個在根節點和ViewPager節點里添加了android:clipChildren="false"屬性的布局,這里可能有人會問為什么2個節點都需要設置android:clipChildren呢?因為ViewPager默認只顯示一屏,我們添加上這個屬性后,ViewPager里面承載的ImageView就可以突破限制,在ViewPager之外繪制了,然后又因為ViewPager是在最外層布局RelativeLayout容器內的,所以也需要設置上這個屬性。
然后我們補一下代碼,就可以看到下面的效果了。

        mViewPager= (ViewPager) findViewById(R.id.vp_main_pager);
        mViewPagerAdapter=new ViewPagerAdapter(this,mImages);
        mViewPager.setOffscreenPageLimit(3);
        mViewPager.setAdapter(mViewPagerAdapter);

單屏ViewPager顯示多頁面效果

我們再來設置下頁面與頁面之間的外邊距mViewPager.setPageMargin(40);,就可以看到這樣的效果:
單屏ViewPager顯示多頁面效果

2、關于ViewPager切換動畫的基礎(PageTransFormer)和示例講解

首先我們先來了解下關于ViewPager里的setPageTransformer方法,這里是源碼:

  /**
     * Sets a {@link PageTransformer} that will be called for each attached page whenever
     * the scroll position is changed. This allows the application to apply custom property
     * transformations to each page, overriding the default sliding behavior.
     *
     * <p><em>Note:</em> Prior to Android 3.0 ({@link Build.VERSION_CODES#HONEYCOMB API 11}),
     * the property animation APIs did not exist. As a result, setting a PageTransformer prior
     * to API 11 will have no effect.</p>
     *
     * @param reverseDrawingOrder true if the supplied PageTransformer requires page views
     *                            to be drawn from last to first instead of first to last.
     * @param transformer PageTransformer that will modify each page's animation properties
     * @param pageLayerType View layer type that should be used for ViewPager pages. It should be
     *                      either {@link ViewCompat#LAYER_TYPE_HARDWARE},
     *                      {@link ViewCompat#LAYER_TYPE_SOFTWARE}, or
     *                      {@link ViewCompat#LAYER_TYPE_NONE}.
     */
    public void setPageTransformer(boolean reverseDrawingOrder, PageTransformer transformer,
            int pageLayerType) {
        if (Build.VERSION.SDK_INT >= 11) {
            final boolean hasTransformer = transformer != null;
            final boolean needsPopulate = hasTransformer != (mPageTransformer != null);
            mPageTransformer = transformer;
            setChildrenDrawingOrderEnabledCompat(hasTransformer);
            if (hasTransformer) {
                mDrawingOrder = reverseDrawingOrder ? DRAW_ORDER_REVERSE : DRAW_ORDER_FORWARD;
                mPageTransformerLayerType = pageLayerType;
            } else {
                mDrawingOrder = DRAW_ORDER_DEFAULT;
            }
            if (needsPopulate) populate();
        }
    }

這里我們提取一下重點:
關于方法的參數:
參數1表示加載到ViewPager的Pager頁面是按正序還是逆序添加,這里我們一般設置成true就行,這個一般只有在幀布局的時候才有視覺效果區別。

參數2是一個PageTransformer接口,接口里帶有一個回調方法,這個是重點,下面會單獨提到。

關于兼容Andorid API11(3.0版本):
這個東西對于現在來說意義不是很大了,現在開發APP基本上都是兼容到4.1以上了,如果非要兼容3.0以下,拷貝下ViewPager的源碼,把這個判斷判斷注釋掉即可。

Prior to Android 3.0 ({@link Build.VERSION_CODES#HONEYCOMB API 11}),the property animation APIs did not exist. As a result, setting a PageTransformer prior to API 11 will have no effect.

上面這句話是講解為什么官方不支持3.0以下的原因,因為在Android3.0之前是沒有屬性動畫的,而官方的示例代碼使用的卻是屬性動畫,我們如果要向下兼容,使用其它的方式實現動畫即可。

    public interface PageTransformer {
        /**
         * Apply a property transformation to the given page.
         *
         * @param page Apply the transformation to this page
         * @param position Position of page relative to the current front-and-center
         *                 position of the pager. 0 is front and center. 1 is one full
         *                 page position to the right, and -1 is one page position to the left.
         */
        void transformPage(View page, float position);
    }

重點來了,這里的transformPage是PageTransformer接口里未實現的方法,有兩個回調參數,它們分別表示:
參數page:當前頁的View
參數position:這里需要特別注意,它不代表當前第幾頁,而是代表當前頁面值和一個滑動距離的數值,在當前手機屏幕能看到的頁面永遠為0,往左遞減,往右遞增,也就是如下圖:


數字代表position

記住:(重要的話說三遍)
當前手機屏幕顯示的頁面的position值永遠為0
當前手機屏幕顯示的頁面的position值永遠為0
當前手機屏幕顯示的頁面的position值永遠為0

接下來我們來分析一種情況:當我們的手指往左滑的時候,此時發生的事情:
頁面:位置0會走到原-1的位置,位置1會走到原0的位置,以此類推。
position值:position的值逐漸從0開始下降到-1,從1開始下降到0,這里我們只考慮[-1,1]區間,之外的區間我們是看不到的,因為我們每次只能滑一屏。

具體大家打印下Log日志就會很清晰了:

   mViewPager.setPageTransformer(true, new ViewPager.PageTransformer() {
            @Override
            public void transformPage(View page, float position) {
                 Log.i("TAG","【page】:"+page+",【position】:"+position);
            }
        });

日志顯示,反方向滑動頁面正好是相反的,這里就不貼出來了。

【頁面從1到0】
【position】:0.95357144
【position】:0.94166666
【position】:0.572619
【position】:0.086904764
【position】:0.0035714286
【position】:0.0
========================
【頁面從0到-1】
【position】:-0.09404762
【position】:-0.10595238
【position】:-0.475
【position】:-0.79761904
【position】:-0.9607143
【position】:-1.00

我們所有的動畫操作就可以根據這里的page和position來完成了,當然官方也給了我們小例子示范,一起來看下:


官方示例 DepthPageTransformer
public class DepthPageTransformer implements ViewPager.PageTransformer {
    private static final float MIN_SCALE = 0.75f;

    public void transformPage(View view, float position) {
        int pageWidth = view.getWidth();

        if (position < -1) { // [-Infinity,-1)
            // This page is way off-screen to the left.
            view.setAlpha(0);

        } else if (position <= 0) { // [-1,0]
            // Use the default slide transition when moving to the left page
            view.setAlpha(1);
            view.setTranslationX(0);
            view.setScaleX(1);
            view.setScaleY(1);

        } else if (position <= 1) { // (0,1]
            // Fade the page out.
            view.setAlpha(1 - position);

            // Counteract the default slide transition
            view.setTranslationX(pageWidth * -position);

            // Scale the page down (between MIN_SCALE and 1)
            float scaleFactor = MIN_SCALE
                    + (1 - MIN_SCALE) * (1 - Math.abs(position));
            view.setScaleX(scaleFactor);
            view.setScaleY(scaleFactor);

        } else { // (1,+Infinity]
            // This page is way off-screen to the right.
            view.setAlpha(0);
        }
    }
}

這里簡單講一下(當前屏幕顯示的position值為0),假設頁面從1到2,也就是手指向左滑,頁面往左走:
1、position<-1的時候剛好是當前屏幕的左數第二頁,是看不到的,position>1剛好是當前屏幕的右數第二頁,也是看不到的,所以這邊設置了透明度為0。
2、position <= 0,position從0到-1(動圖頁面1到2),此時的頁面1的效果是不透明,不縮放,向左走。
3、position <= 1,position從1到0(動圖頁面2到1),此時的效果是頁面隨著position值比例的減小,頁面從透明到不透明,從小到大,頁面不滑動(抵消)。

3、關于PageTransFormer的實例講解

說了這么多,來實戰一下吧,先從上面我們實現的單屏顯示多頁面的ViewPager開始,加一些效果,比如我們想讓邊上的兩頁面顯示半透明并伴隨著左右滑動有放大縮小的效果,直接來看下PageTransformer的代碼:

package com.lcw.dynamicviewpager.transformer;

import android.support.v4.view.ViewPager;
import android.view.View;

/**
 * 畫廊效果Transformer(半透明+縮放)
 * Create by: chenwei.li
 * Date: 2017/12/8
 * time: 14:55
 * Email: lichenwei.me@foxmail.com
 */

public class GalleryTransformer implements ViewPager.PageTransformer {

    private static final float MAX_ALPHA=0.5f;
    private static final float MAX_SCALE=0.9f;

    @Override
    public void transformPage(View page, float position) {
        if(position<-1||position>1){
            //不可見區域
            page.setAlpha(MAX_ALPHA);
            page.setScaleX(MAX_SCALE);
            page.setScaleY(MAX_SCALE);
        }else {
            //可見區域,透明度效果
            if(position<=0){
                //pos區域[-1,0)
                page.setAlpha(MAX_ALPHA+MAX_ALPHA*(1+position));
            }else{
                //pos區域[0,1]
                page.setAlpha(MAX_ALPHA+MAX_ALPHA*(1-position));
            }
            //可見區域,縮放效果
            float scale=Math.max(MAX_SCALE,1-Math.abs(position));
            page.setScaleX(scale);
            page.setScaleY(scale);
        }
    }
}

我們讓可見區域[-1,1]根據position值的變化做了一些動作,假設頁面往左滑:
1、在區間[-1,0)里是從0到-1,也就是透明度隨著position的減小而減小,最終為0.5f(半透明)。
2、在區間(0,1)里是從1到0,也就是透明度隨著position的減小而增大,最終為1f(不透明)。
3、它們的縮放規則是隨著position減小,先小后大,因為這邊有個絕對值abs方法和最大數max方法作用著,當position的絕對值大于0.9f的時候,頁面才開始放大。


畫廊效果圖

是不是很簡單啊?哈哈,再來寫個效果吧,橫向的ViewPager我們看多了,這里我們來實現一個縱向排列的ViewPager,并實現它的滑動效果。

首先我們來思考下如何實現縱向排列,我們知道ViewPager默認是水平排開的,每個頁面之間的距離是頁面的寬度,那么是不是讓第二個頁面的x軸往左移動一個頁面的距離,第三個頁面的x軸往左移動2個頁面的距離。。。以此類推

          //移動X軸左邊,使得卡片在同一坐標
          page.setTranslationX(-position * page.getWidth());

然后我們需要把每個頁面往下拉一點,也就是把y軸坐標往下移動一些,這里為了使頁面好看一些,我做了一個按比例縮放效果,mOffset是一個偏移值

           //縮放卡片并調整位置
            float scale = (page.getWidth() - mOffset * position) / page.getWidth();
            page.setScaleX(scale);
            page.setScaleY(scale);
            //移動Y軸坐標
            page.setTranslationY(position * mOffset);

此時運行代碼我們就可以看到這樣的效果了:


縱向排列的ViewPager

不過此時會發現,頁面滑動不了了,這是為什么呢,還記得我們之前設置的x軸偏移嗎?

          //移動X軸左邊,使得卡片在同一坐標
          page.setTranslationX(-position * page.getWidth());

正常情況下,手指往左滑,當頁面從0到-1的時候是向左走的,但是我們這邊設置了x軸的移動,隨著position的減小,頁面是往右走的(正數為x軸右邊),剛好互相抵消了,所以這里我們需要對滑動進行特殊處理(順帶加了一個45°的旋轉動畫):

     if (position <= 0) {
            //頁面滑動的時候
            page.setTranslationX(0f);
            page.setRotation(45 * position);
            page.setTranslationX((page.getWidth() / 2 * position));
        } else {
            //頁面不滑動的時候
            //移動X軸坐標,使得卡片在同一坐標
            page.setTranslationX(-position * page.getWidth());
            //縮放卡片并調整位置
            float scale = (page.getWidth() - mOffset * position) / page.getWidth();
            page.setScaleX(scale);
            page.setScaleY(scale);
            //移動Y軸坐標
            page.setTranslationY(position * mOffset);
        }
卡片效果

到這里,以上的效果就都實現了,有了這些基礎,剩下的就靠你的想象力了,貧窮會限制你的想象力,但代碼不會,快去實現自己的狂拽酷炫吊炸天的動畫效果吧~

最后補充:

其實以上說的這些效果還有一種更加優雅的實現方法,就是利用RecyclerView實現,哈哈哈,先賣個關子,有時間再來寫吧~

源碼下載:

這里附上源碼地址(歡迎Star,歡迎Fork):源碼下載

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,818評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,185評論 3 414
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,656評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,647評論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,446評論 6 405
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 54,951評論 1 321
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,041評論 3 440
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,189評論 0 287
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,718評論 1 333
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,602評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,800評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,316評論 5 358
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,045評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,419評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,671評論 1 281
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,420評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,755評論 2 371

推薦閱讀更多精彩內容