天天看點

Android中ViewPager支援一屏多個View、切換動畫以及無限滾動

1. 首先看一下最終的效果圖

Android中ViewPager支援一屏多個View、切換動畫以及無限滾動

2. 需求拆解

第一眼看見上面的效果,是不是有些朋友覺得這個效果很酷,有的高手會覺得這個效果很簡單。筆者昨天剛拿到需求的時候,最開始也是覺得這個很簡單,可是越分析越發現好像實作出來并不是那麼容易。單個的效果可能很簡單,但是這麼多的效果疊在一起,可能就比較複雜了。我簡單得将這個效果任務拆解一下:

  1. 一屏要展示3個View,支援左右滑動
  2. 螢幕中間兩側的View移向中間時,會有一個放大的效果;中間的View移向兩側時,會有一個縮小的效果
  3. 點選螢幕兩側的View,會自動滾動到螢幕的中間,同理效果要滿足第2點
  4. 當滑動到最左側或者最右側時,需要立馬銜接最末尾的View和最開頭的View,也就是無限滾動
  5. 考慮性能問題,必須有一個緩存機制。否則控件的count大于30的時候,會有非常明顯的卡頓

起初我想過用HorizontalScrollView來做,但是在性能問題和無限滾動的實作上有一點困難;然後我想過用RecyclerView來做橫向的清單,但是在無限滾動和居中放大的實作上有一點困難;然後我考慮直接重寫View來做,但是這樣的風險和成本實在太高,我不能不負責任地把這種代碼投放到實際項目中。最後想來想去,我覺得用ViewPager來實作是最穩妥的,雖然裡面仍然有不少坑,不過都被我解決了。

3. 實作一個普通的ViewPager

這裡實作一個最最最普通的ViewPager,一頁一個Item,支援橫向滑動:

activity_main.xml

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

    <android.support.v4.view.ViewPager
        android:id="@+id/viewPager"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        />
</LinearLayout>
           

MainActivity.java

public class MainActivity extends Activity {

    private ViewPager viewPager;

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        viewPager = (ViewPager) findViewById(R.id.viewPager);
        OnePageThreeItemAdapter adapter = new OnePageThreeItemAdapter(this);
        viewPager.setAdapter(adapter);
    }

}
           

OnePageThreeItemAdapter.java

public class OnePageThreeItemAdapter extends PagerAdapter {

    private Context context;
    private List<String> sourceData = new ArrayList<String>();

    public OnePageThreeItemAdapter(Context context) {
        this.context = context;
        initData();
    }

    /**
     * 初始化原始資料
     */
    public void initData(){
        sourceData.clear();
        for(int i =  ; i < ; i++){
            sourceData.add(i + "");
        }
    }

    @Override
    public int getCount() {
        return sourceData.size();
    }

    @Override
    public boolean isViewFromObject(View view, Object object) {
        return view == object;
    }

    @Override
    public Object instantiateItem(ViewGroup container, final int position) {
        View view = View.inflate(context, R.layout.item, null);
        view.setTag(String.valueOf(position));
        TextView txt = (TextView) view.findViewById(R.id.txt);
        txt.setText(sourceData.get(position));
        container.addView(view);
        return view;
    }

    @Override
    public void destroyItem(ViewGroup container, int position, Object object) {
        container.removeView((View) object);
    }
}
           

item.xml

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:gravity="center"
    android:orientation="vertical">

    <ImageView
        android:id="@+id/img"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:src="@mipmap/ic_launcher" />

    <TextView
        android:id="@+id/txt"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginTop="10dp"
        android:gravity="center"
        android:textColor="@android:color/black"
        android:textSize="15sp" />

</LinearLayout>
           

4. 實作一屏三個View

這個功能網上已經有很多了,相信大家都已經看見過了,是以我也就簡單得說一下

A. 首先ViewPager和包含ViewPager的ViewGroup設定clipChildren屬性

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

    <android.support.v4.view.ViewPager
        android:id="@+id/viewPager"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:clipChildren="false"
        />
</LinearLayout>
           

clipChildren的意思是是否裁剪,當設定為false時,一屏有多個View的時候不裁剪多餘的View;layerType的關閉硬體加速,具體原因我也不知道,網友是這樣給的,我這樣弄了能跑起來就沒管了,若有知情者可以告知我喲。

B. 重寫PagerAdapter的getPageWidth方法

@Override
public float getPageWidth(int position) {
    return f / ONE_PAGE_ITEM_COUNT; //ONE_PAGE_ITEM_COUNT = 3
}
           

這個方法傳回的是每一個Item所占目前螢幕的比例,我們需要一屏展示三個View,是以這裡傳回1/3。

5. 實作居中時放大,左右滑動時縮小的動畫效果

提起ViewPager的動畫效果,大家第一反應肯定是PageTransformer,沒錯,本文的确是用PageTransformer來實作的,不過因為是一屏展示多個View,是以這裡的實作上跟網上的大部分都有很大的差異。首先我們了解一下PageTransformer的transformPage方法的參數

A. 參數講解

transformPage方法有兩個參數,第一個View代表目前的子View,這個沒什麼好說的。主要是第二個參數position,是一個float類型的。這個position是什麼類型呢,我們分兩種情況示範一下:

如果一屏隻有一個View,也就是最普通最簡單的情況

Android中ViewPager支援一屏多個View、切換動畫以及無限滾動

一屏有三個View

Android中ViewPager支援一屏多個View、切換動畫以及無限滾動

可以看出,如果一屏隻有一個View,那麼目前View就是0;如果一屏有多個View,那麼目前頁的第一個View就是0。然後向左依次遞減每一個View的寬度;向右依次遞增每一個View的寬度。

B. 一屏三個View的實作

通過上面的圖,我們可以知道,我們需要處理的區間有(-INFINITE, 0)、(0, 1/3)、(1/3, 2/3)、(2/3, +INFINITE)

public class OnePageThreeItemTransformer implements ViewPager.PageTransformer {

    @Override
    public void transformPage(View page, float position) {
        //這裡的position是指每一個Item相對于Page所占的比例,目前頁的第一個Item是0,左邊的依次減去Item的寬度,右邊的依次加上Item的寬度
        float centerPosition = f / ONE_PAGE_ITEM_COUNT;
        if (position <= ) {
            page.setScaleX();
            page.setScaleY();
        } else if (position <= centerPosition) {
            page.setScaleX( + (itemMaxScale - ) * position / centerPosition);
            page.setScaleY( + (itemMaxScale - ) * position / centerPosition);
        } else if (position <= centerPosition * ) {
            page.setScaleX( + (itemMaxScale - ) * ( * centerPosition - position) / centerPosition);
            page.setScaleY( + (itemMaxScale - ) * ( * centerPosition - position) / centerPosition);
        } else {
            page.setScaleX();
            page.setScaleY();
        }
    }
}
           

其中itemMaxScale代表最大放大的比例,這裡我暫定的2倍,值可以随便修改。

然後設定ViewPager的PageTransformer就可以了

viewPager.setPageTransformer(true, new OnePageThreeItemTransformer());
           

6. 點選螢幕中的View,實作居中效果

網上很多種實作,原理大約是通過TouchEvent擷取記憶體中的View所在的index,然後再調用setCurrentItem方法平滑過去。在這裡,我憤怒的告訴大家:這種做法是完全錯誤的。順帶黑一句,中國的部落格寫手很多都不嚴謹,代碼都沒有經過分析與驗證,就直接釋出出來或者轉發。大家都知道ViewPager和ListView等都是有緩存原理的,記憶體中的View所在的index,跟setCurrentItem的參數position,是完全兩碼事的東西。

後來經過琢磨,我找到了一種實作思路:

在PagerAdapter的instantiateItem方法中,給每一個View設定點選監聽器,instantiateItem的position是指每一個View的position,這個position跟資料源的position是保持一緻的。在View的點選監聽器中調用setCurrentItem方法,注意到setCurrentItem的position參數的意義:如果一屏隻有一個View,那麼position就是目前頁的position;如果一屏有三個View,那麼position是指目前頁的第一個View的position。我舉個例子:

Android中ViewPager支援一屏多個View、切換動畫以及無限滾動

如圖所示,左邊一頁的position為3;中間一頁的position為6;右邊一頁的position為9。

也就是說,如果我想要指定position居中,那麼我隻需要調用setCurrentItem(position - 1)就可以了,這裡要考慮position為0的情況。

@Override
public Object instantiateItem(ViewGroup container, final int position) {
    View view = View.inflate(context, R.layout.item, null);
    view.setTag(String.valueOf(position));
    TextView txt = (TextView) view.findViewById(R.id.txt);
    txt.setText(sourceData.get(position));
    view.setOnClickListener(new View.OnClickListener() {
        @Override
        public void onClick(View v) {
            //這裡的position是指每一頁的第一個Item的position
            viewPager.setCurrentItem(position ==  ?  : position - , true);
        }
    });
    container.addView(view);
    return view;
}
           

這裡設定Tag是為了下一節實作無限循環滾動做了必要準備。

7. 實作無限循環滾動

A. 修複添加資料源

網上已經有很多實作ViewPager無限滾動的原理了,但是因為我們這兒是一屏有三個View,是以這裡的邏輯需要變動一點。

在資料源的前後各加3個資料源,前面加入原資料源的末三位;後面加入原資料源的前三位。如圖:

Android中ViewPager支援一屏多個View、切換動畫以及無限滾動

3-7是指我們原來的5個資料源;0-2是添加的頭資料源,具體的索引指向5-7;8-10是添加的尾資料源,具體的索引指向3-5。代碼如下

/**
 * 初始化原始資料
 */
public void initData(){
    sourceData.clear();
    for(int i =  ; i < ; i++){
        sourceData.add(i + "");
    }
}

/**
 * 為了能無限滾動,将末三項複制加入清單開頭,将前三項複制加入到清單末尾
 */
private void repairDataForLoop(){
    repairData.clear();
    List<String> beforeTemp = new ArrayList<String>();
    List<String> afterTemp = new ArrayList<String>();
    for(int i = ; i < ONE_PAGE_ITEM_COUNT; i++){
        beforeTemp.add(, sourceData.get(sourceData.size() -  - i));
        afterTemp.add(sourceData.get(i));
    }
    repairData.addAll(beforeTemp);
    repairData.addAll(sourceData);
    repairData.addAll(afterTemp);
}
           

我們将原生的資料源放入sourceData,便于資料檢索。而将修複後的資料源放入repairData。

B. 無限滾動的要領

然後當界面滾動到position=0時,立馬調用setCurrentItem滾動到position=5的位置;當界面滾動到position=8時,立馬調用setCurrentItem滾動到position=3的位置。注意這裡的position是指目前頁的第一個View的position,上面已經講過了。這裡需要有幾點需要注意一下:

  1. setCurrentItem的第二個參數要傳false,這樣才能無動畫滾動,才能對使用者無感。
  2. 在OnPageChangeListener中,需要在onPageScrollStateChanged中處理而不是在onPageSelected。因為onPageScrollStateChanged的SCROLL_STATE_IDLE狀态比onPageSelected晚調用。否則會出現界面突然跳動沒有平滑動畫的問題。
  3. 調用setCurrentItem(position, false)時,設定的PageTransformer動畫效果會失效。也就是說中間的View不會有放大的效果。是以必須手動設定放大的效果。這裡就需要前面instantiateItem中設定Tag的幫助了。
/**
 * 真正處理無限滾動的邏輯,在滑動到邊界時,立馬跳轉到對應的位置
 */
private void handleLoop(){
    viewPager.addOnPageChangeListener(new ViewPager.OnPageChangeListener() {
        // 是否應該強制跳轉到首頁
        boolean shouldToBefore = false;
        // 是否應該強制跳轉到末頁
        boolean shouldToAfter = false;

        @Override
        public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {

        }

        @Override
        public void onPageSelected(int position) {
            //這裡的position是指每一頁的第一個Item的position
            if (position == ){
                shouldToAfter = true;
            }else if(position == getCount() - ONE_PAGE_ITEM_COUNT){
                shouldToBefore = true;
            }
        }

        @Override
        public void onPageScrollStateChanged(int state) {
            //onPageScrollStateChanged的SCROLL_STATE_IDLE比onPageSelected晚調用,如果在onPageSelected中處理方法,則不會有滑動動畫效果
            if(state == ViewPager.SCROLL_STATE_IDLE){
                if(shouldToAfter){
                    //這裡的position是指每一頁的第一個Item的position
                    viewPager.setCurrentItem(getCount() - ONE_PAGE_ITEM_COUNT * , false);
                    //居中的position比目前position多1
                    handleScale(getCount() - ONE_PAGE_ITEM_COUNT *  + );
                    shouldToAfter = false;
                }
                if(shouldToBefore){
                    //這裡的position是指每一頁的第一個Item的position
                    viewPager.setCurrentItem(ONE_PAGE_ITEM_COUNT, false);
                    //居中的position比目前position多1
                    handleScale(ONE_PAGE_ITEM_COUNT + );
                    shouldToBefore = false;
                }
            }
        }
    });
}

/**
 * 調用了setCurrentItem(position, false)後,設定的PageTransformer不會生效,
 * 也就是說中間需要放大的項不會放大,是以手動将這一項放大
 */
private void handleScale(int position){
    View view = findCenterView(position);
    if(view == null){
        return;
    }
    view.setScaleX(itemMaxScale);
    view.setScaleY(itemMaxScale);
}

/**
 * 通過在instantiateItem方法給每個View設定的Tag來标記,找出記憶體中的View
 */
private View findCenterView(int position){
    for(int i = ; i < viewPager.getChildCount(); i++){
        View view = viewPager.getChildAt(i);
        if(String.valueOf(position).equals(view.getTag())){
            return view;
        }
    }
    return null;
}
           

8. 完整的代碼:

item.xml

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:gravity="center"
    android:orientation="vertical">

    <ImageView
        android:id="@+id/img"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:src="@mipmap/ic_launcher" />

    <TextView
        android:id="@+id/txt"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginTop="10dp"
        android:gravity="center"
        android:textColor="@android:color/black"
        android:textSize="15sp" />

</LinearLayout>
           

activity_main.xml

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

    <android.support.v4.view.ViewPager
        android:id="@+id/viewPager"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:clipChildren="false"
        />
</LinearLayout>
           

OnePageThreeItemAdapter.java

/**
 * Created by chenchen on 2017/8/15.
 *
 * 代碼中有幾個position需要注意一些
 * 1. setCurrentItem的position參數,是指目前頁的第一個Item的所在position
 * 2. OnPageChangeListener的position參數,同樣也是指目前頁的第一個Item的所在position
 * 3. instantiateItem的position參數,是指每一個Item的所在position
 * 4. PageTransformer的position參數,是指每一個Item相對于Page所占的比例,目前頁的第一個Item是0,左邊的依次減去Item的寬度,右邊的依次加上Item的寬度
 *    比如一頁隻有一個Item。那麼目前頁的position為0。左邊的position分别為-1。右邊的position分别為1;
 *    比如一頁有三個Item。那麼目前頁的Item的position分别為0、1/3、2/3。左邊的position分别為-1、-2/3、-1/3。右邊的position分别為1、4/3、5/3。
 */
public class OnePageThreeItemAdapter extends PagerAdapter {
    //一屏最多3個item,不能随便改變這個值,改這個數量必須要改動很多邏輯
    private static final int ONE_PAGE_ITEM_COUNT = ;

    private Context context;
    private ViewPager viewPager;
    // 居中的Item的最大放大值
    private float itemMaxScale;
    //在原始的資料前後各加了3項資料
    private List<String> repairData = new ArrayList<String>();
    //原始的資料,用于資料檢索等
    private List<String> sourceData = new ArrayList<String>();

    public OnePageThreeItemAdapter(ViewPager viewPager, float itemMaxScale) {
        this.viewPager = viewPager;
        this.context = viewPager.getContext();
        this.itemMaxScale = itemMaxScale;
        initData();
        repairDataForLoop();
        handleLoop();
    }

    /**
     * 調用之前保證initData方法已被調用
     */
    @Override
    public void notifyDataSetChanged() {
        repairDataForLoop();
        super.notifyDataSetChanged();
    }

    /**
     * 初始化原始資料
     */
    public void initData(){
        sourceData.clear();
        for(int i =  ; i < ; i++){
            sourceData.add(i + "");
        }
    }

    /**
     * 為了能無限滾動,将末三項複制加入清單開頭,将前三項複制加入到清單末尾
     */
    private void repairDataForLoop(){
        repairData.clear();
        List<String> beforeTemp = new ArrayList<String>();
        List<String> afterTemp = new ArrayList<String>();
        for(int i = ; i < ONE_PAGE_ITEM_COUNT; i++){
            beforeTemp.add(, sourceData.get(sourceData.size() -  - i));
            afterTemp.add(sourceData.get(i));
        }
        repairData.addAll(beforeTemp);
        repairData.addAll(sourceData);
        repairData.addAll(afterTemp);
    }

    /**
     * 真正處理無限滾動的邏輯,在滑動到邊界時,立馬跳轉到對應的位置
     */
    private void handleLoop(){
        viewPager.addOnPageChangeListener(new ViewPager.OnPageChangeListener() {
            // 是否應該強制跳轉到首頁
            boolean shouldToBefore = false;
            // 是否應該強制跳轉到末頁
            boolean shouldToAfter = false;

            @Override
            public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {

            }

            @Override
            public void onPageSelected(int position) {
                //這裡的position是指每一頁的第一個Item的position
                if (position == ){
                    shouldToAfter = true;
                }else if(position == getCount() - ONE_PAGE_ITEM_COUNT){
                    shouldToBefore = true;
                }
            }

            @Override
            public void onPageScrollStateChanged(int state) {
                //onPageScrollStateChanged的SCROLL_STATE_IDLE比onPageSelected晚調用,如果在onPageSelected中處理方法,則不會有滑動動畫效果
                if(state == ViewPager.SCROLL_STATE_IDLE){
                    if(shouldToAfter){
                        //這裡的position是指每一頁的第一個Item的position
                        viewPager.setCurrentItem(getCount() - ONE_PAGE_ITEM_COUNT * , false);
                        handleScale(getCount() - ONE_PAGE_ITEM_COUNT *  + );
                        shouldToAfter = false;
                    }
                    if(shouldToBefore){
                        //這裡的position是指每一頁的第一個Item的position
                        viewPager.setCurrentItem(ONE_PAGE_ITEM_COUNT, false);
                        handleScale(ONE_PAGE_ITEM_COUNT + );
                        shouldToBefore = false;
                    }
                }
            }
        });
    }

    /**
     * 調用了setCurrentItem(position, false)後,設定的PageTransformer不會生效,
     * 也就是說中間需要放大的項不會放大,是以手動将這一項放大
     */
    private void handleScale(int position){
        View view = findCenterView(position);
        if(view == null){
            return;
        }
        view.setScaleX(itemMaxScale);
        view.setScaleY(itemMaxScale);
    }

    /**
     * 通過在instantiateItem方法給每個View設定的Tag來标記,找出記憶體中的View
     */
    private View findCenterView(int position){
        for(int i = ; i < viewPager.getChildCount(); i++){
            View view = viewPager.getChildAt(i);
            if(String.valueOf(position).equals(view.getTag())){
                return view;
            }
        }
        return null;
    }

    @Override
    public int getCount() {
        return repairData.size();
    }

    @Override
    public boolean isViewFromObject(View view, Object object) {
        return view == object;
    }

    @Override
    public Object instantiateItem(ViewGroup container, final int position) {
        View view = View.inflate(context, R.layout.item, null);
        view.setTag(String.valueOf(position));
        TextView txt = (TextView) view.findViewById(R.id.txt);
        txt.setText(repairData.get(position));
        view.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                //這裡的position是指每一頁的第一個Item的position
                viewPager.setCurrentItem(position ==  ?  : position - , true);
                Toast.makeText(context, "點選了" + repairData.get(position), Toast.LENGTH_SHORT).show();
            }
        });
        container.addView(view);
        return view;
    }

    @Override
    public void destroyItem(ViewGroup container, int position, Object object) {
        container.removeView((View) object);
    }

    @Override
    public float getPageWidth(int position) {
        return f / ONE_PAGE_ITEM_COUNT;
    }

    public class OnePageThreeItemTransformer implements ViewPager.PageTransformer {

        @Override
        public void transformPage(View page, float position) {
            //這裡的position是指每一個Item相對于Page所占的比例,目前頁的第一個Item是0,左邊的依次減去Item的寬度,右邊的依次加上Item的寬度
            float centerPosition = f / ONE_PAGE_ITEM_COUNT;
            if (position <= ) {
                page.setScaleX();
                page.setScaleY();
            } else if (position <= centerPosition) {
                page.setScaleX( + (itemMaxScale - ) * position / centerPosition);
                page.setScaleY( + (itemMaxScale - ) * position / centerPosition);
            } else if (position <= centerPosition * ) {
                page.setScaleX( + (itemMaxScale - ) * ( * centerPosition - position) / centerPosition);
                page.setScaleY( + (itemMaxScale - ) * ( * centerPosition - position) / centerPosition);
            } else {
                page.setScaleX();
                page.setScaleY();
            }
        }
    }

    /**
     * 配置ViewPager一些其他的屬性
     */
    public void configViewPager(){
        viewPager.setOffscreenPageLimit(ONE_PAGE_ITEM_COUNT + );
        viewPager.setPageTransformer(true, new OnePageThreeItemTransformer());
        viewPager.setCurrentItem(ONE_PAGE_ITEM_COUNT);
    }
}
           

MainActivity.java

public class MainActivity extends Activity {

    private ViewPager viewPager;

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        viewPager = (ViewPager) findViewById(R.id.viewPager);
        OnePageThreeItemAdapter adapter = new OnePageThreeItemAdapter(viewPager, );
        viewPager.setAdapter(adapter);
        adapter.configViewPager();
    }

}