手動實現輪播圖(二):迴圈滾動、定時切換與指示器

rosuH發表於2018-04-21

前言

文章釋出於我的個人部落格: 手動實現輪播圖(二):迴圈滾動、定時切換與指示器

在上一篇文章手動實現輪播圖(一):ViewPager 入門實踐中,我們認識了ViewPager這個佈局,也簡單上手了一下。

接下來這篇文章,我們會進一步朝著輪播圖的方向前進。

原來的文章末尾,我使用了 Glide 載入 Gif 圖片作為輪播圖的內容,所以現在也是基於那個程式碼繼續下去的。

如果對這部分比較陌生,建議回去看一下文章末尾倉庫地址裡的程式碼哦

本文章中我們將會實現:

  • 迴圈滾動

  • 切換指示器

  • 定時切換

接下來就讓我們開工吧。

# 1. 迴圈滾動

ViewPager雖然好用,但是並不原生支援迴圈滾動,也就是你:

  • 第一個往左滑,會跳到最後一個

  • 從最後一個往右滑,會調回第一個

我們之前實現的效果裡,第一個就無法再往左滑了,最後一個就無法再往右滑了。這樣輪播圖就不是“輪”播了。

所以我們需要自己來實現迴圈滾動這個效果。

該怎麼實現呢?目前也有比較成熟的三個做法:

多頁面假迴圈

  • 建立很多個頁面,即便我們真正需要展示的時候只有 5 個頁面

    • 把起始點放在佇列中間,如果到了要展示的第一個頁面,繼續往左的時候,我們把接下來就把頁面設定為最後一個的樣式

    • 這樣不管使用者往左還是往右滑,只要是正常情況下,使用者都是滑不到頭的,造成視覺上的迴圈

      • 正常 App 中,即便你使用一個這樣的頁面佇列來顯示,使用者也沒有耐心一直滑下去

假設我們現在建立了 1000 個頁面的ViewPager,然後我們實際需要展示的只有 5 個頁面,那麼實現的效果如下:

我們把第一個展示的頁面設定為 500,那使用者需要滑動 499 才會到頭。

手動實現輪播圖(二):迴圈滾動、定時切換與指示器

這樣效能會不會很差?

  • 不會

因為雖然說的是“建立1000”個頁面,但是實際上我們只是告訴ViewPagerAdapter我們會使用這麼多個,不代表他會建立這麼多個。

我們會在AdaptergetCount方法裡返回 1000,這個方法只是幫助Adapter獲取正確的position的,並不是真正建立出來。(通過閱讀PagerAdapter 的原始碼得出)

記得前面我們說過的,FragmentPagerAdapter會預設幫我們建立三個頁面,所以這裡也只會建立三個頁面,超過前中後的其他頁面都會被回收。

其餘兩種實現方法

我們主要使用第一種,理解起來簡單易懂,也沒有明顯的短板。

其餘兩種方法描述看下面這篇文章:Android實現真正的ViewPager【平滑過渡】...

介紹完實現思路,我們就可以開始實現了。

開啟MainActivity.java,修改程式碼如下:

public class MainActivity extends AppCompatActivity {
    private static final int MAX_NUMBER = 1000;
    private static final int START_POSITION = MAX_NUMBER/2;
    ...
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        ...
        mViewPager.setAdapter(new FragmentPagerAdapter(fm) {
            
            private int mIndex; // 儲存通過 position 計算出正確的陣列索引
            
            @Override
            public Fragment getItem(int position) {
                mIndex = Math.abs(position - START_POSITION) % mStringList.length;
                
                if (position < START_POSITION && mIndex != 0){
                    mIndex = mStringList.length - mIndex;
                }
                
                return PageFragment.newInstance(mIndex);
            }
​
            @Override
            public int getCount() {
                return MAX_NUMBER;
            }
​
​
        });
        
        mViewPager.setCurrentItem(START_POSITION);
​
        ...
    }
    
    
}複製程式碼

  • 定義兩個常量,分別是

    • MAX_NUMBER:頁面總數,一共 1000 個

    • START_POSITION:起始的頁面,從中間第 500 個開始

mIndex = Math.abs(position - START_POSITION) % mStringList.length;
                
if (position < START_POSITION && mIndex != 0){
    mIndex = mStringList.length - mIndex;
}複製程式碼
  • 計算當前位置position和起始位置(START_POSITION)的距離,然後把結果和真正要展示的頁面數量(此處暫時使用mStringList的長度代替)取餘

    • 距離有正負,所以取了絕對值。但是如果只是絕對值然後去取餘的話,左滑的時候,就不是 1->5 -> 4 這樣子,而是 1 ->2 ->3 這樣。這是取餘運算的結果,不熟悉的同學可以回憶一下取餘的結果

    • 所以我們加了判斷

      • 頁面position大於起始位置 ,那就直接用相對距離取餘

      • 如若小於起始位置,那麼用實際頁面數量減去取餘結果,就可以實現倒數的效果了

@Override
public int getCount() {
    return MAX_NUMBER;
}複製程式碼
  • 此處告訴Adapter一共有多少個頁面

記得設定起始頁面哦:

mViewPager.setCurrentItem(START_POSITION);複製程式碼

這樣我們的迴圈滾動就完成了~快試試看吧。

手動實現輪播圖(二):迴圈滾動、定時切換與指示器

2. 頁面指示器

許多輪播圖都有一個小指示器,用來標誌當前的頁面。我們現在就來做一個。

做了前面的迴圈滾動,這樣的頁面指示器原理應該不難理解。

思路是:

  • 建立控制元件樣式

    • 選中的樣式

    • 未選中的樣式

  • 新增控制元件到檢視裡面

  • 當頁面滑動的時候,修改指示器的樣式

建立控制元件樣式

res/drawable資料夾裡,建立兩個檔案:

正常樣式:

dot_normal.xml

<?xml version="1.0" encoding="utf-8" ?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
    android:shape="oval">
    <size android:width="5dp"
        android:height="5dp"/>
    <solid android:color="@android:color/holo_red_dark"/>
</shape>複製程式碼

被選中樣式:

dot_selected.xml

<?xml version="1.0" encoding="utf-8" ?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
    android:shape="oval">
    <size android:width="5dp"
        android:height="5dp"/>
    <solid android:color="@color/colorPrimary"/>
</shape>複製程式碼

接著在activity_main.xml里加入一個LinearLayout佈局,後面我們使用程式碼的方式把小點加入進去:

<?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.support.v4.view.ViewPager
        android:id="@+id/view_pager_inside"
        android:layout_width="400dp"
        android:layout_height="400dp"
        android:background="@android:color/darker_gray"
        android:layout_centerInParent="true">
    </android.support.v4.view.ViewPager>
    
    <LinearLayout
        android:id="@+id/ll_inside"
        android:layout_below="@+id/view_pager_inside"
        android:layout_width="match_parent"
        android:layout_height="30dp"
        android:orientation="horizontal"
        android:gravity="center"/>
​
</RelativeLayout>複製程式碼

新增控制元件到檢視中

此處的程式碼思路來自Android ViewPager 無限迴圈左右滑動(可自動) 實現

回到MainActivity.java中,

public class MainActivity extends AppCompatActivity {
    private List<TextView> mTextViews;
    private LinearLayout mLinearLayout;
​
    ...
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        mViewPager = findViewById(R.id.view_pager_inside);
        mLinearLayout = findViewById(R.id.ll_inside);
        
        initCircle();
        ....
    }
    
    ...
    private void initCircle() {
        mTextViews = new ArrayList<>();
        int d = 20;
        int m = 7;
​
        for (int i = 0; i < mStringList.length; i++){
            TextView textView = new TextView(this);
            if (i == 0){
                textView.setBackgroundResource(R.drawable.dot_selected);
            }else {
                textView.setBackgroundResource(R.drawable.dot_normal);
            }
​
            LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(d, d);
​
            params.setMargins(m, m, m, m);
            textView.setLayoutParams(params);
            mTextViews.add(textView);
            mLinearLayout.addView(textView);
        }
    }
    ...
}複製程式碼

  • 定義兩個變數

    • mTextViews:存放小點的列表

      • 我們的小點其實是由TetxView構成,然後背景顏色設定為圓形的

    • mLinearLayout:引用剛剛建立的LinearLayout佈局

  • 建立一個initCIrcle()方法

    • 使用程式碼的方式建立TextView檢視,為每個檢視設定寬高、外邊距和背景等屬性

      • 背景樣式就是剛剛建立的兩個.xml檔案

    • 使用 addView方法把小點新增到佈局當中

Oncreate()方法中呼叫之後,我們就會看到小點已經出現了。

現在我們需要根據頁面來修改樣式,以達到指示器的作用。

public class MainActivity extends AppCompatActivity {
    
    ...
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        ...
        mViewPager.addOnPageChangeListener(new ViewPager.OnPageChangeListener() {
            @Override
            public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {
​
            }
​
            @Override
            public void onPageSelected(int position) {
                changePoints(position % mStringList.length);
            }
​
            @Override
            public void onPageScrollStateChanged(int state) {
            }
        });  
    }
    
    private void changePoints(int pos){
        if (mTextViews != null){
            for (int i = 0; i < mTextViews.size(); i++){
                if (pos == i){
                    mTextViews.get(i).setBackgroundResource(R.drawable.dot_selected);
                }else {
                    mTextViews.get(i).setBackgroundResource(R.drawable.dot_normal);
                }
            }
        }
    }
}複製程式碼
  • mViewPager新增一個狀態監聽器ViewPager.OnPageChangeListener

    • 重寫onPageSelected()方法:該方法會在頁面被選中的時候呼叫

    • 在該方法內,我們呼叫changePoint()方法來改變指示器的樣式

我們在呼叫changePoint()的時候,傳入的是position % mStringList.length。這裡是有問題的。

如果直接使用positionmString.length進行取模,在這個例子裡是沒問題,因為我們起始位置(500)恰好是mString.length的倍數。所以此時會從 0 開始。但如果我們以後修改了起始位置亦或者修改了展示圖片的數量的話,這裡就會出錯了。

所以我們還是使用和之前一樣的方式來獲得索引值。修改一下onPageSelected()方法:

private int mIndex;
​
@Override
public void onPageSelected(int position) {
    mIndex = Math.abs(position - START_POSITION) % mStringList.length;
    if (position < START_POSITION && mIndex != 0){
        mIndex = mStringList.length - mIndex;
     }
    changePoints(mIndex);
​
}複製程式碼

這裡為了方便,就直接使用這段程式碼了。有時間的同學可以自己優化一下,提高複用率。

按照道理,現在應該就可以了。

手動實現輪播圖(二):迴圈滾動、定時切換與指示器

3. 定時播放

輪播圖的其中一個特點,就是定時播放。

我們已經實現了這麼多效果了,定時播放應該也是小菜一碟。

我們可以使用Handle呼叫setCurrentItem()即可。

以下程式碼思路來自Android ViewPager 無限迴圈左右滑動(可自動) 實現

修改我們的MainActivity.java

private Handler mHandler = new Handler();
​
protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        ...
        mHandler = new Handler();
        mHandler.postDelayed(new TimerRunnable(),5000);
    }
​
    class TimerRunnable implements Runnable{
​
        @Override
        public void run() {
            int curItem = mViewPager.getCurrentItem();
            mViewPager.setCurrentItem(curItem+1);
            if (mHandler!=null){
                mHandler.postDelayed(this,5000);
            }
        }
    }
​
    @Override
    protected void onDestroy() {
        super.onDestroy();
        mHandler = null; //此處在Activity退出時及時 回收
}複製程式碼

4. 修改過渡動畫

呼叫ViewPager.setPageTransformer()方法即可自行設定動畫。

讓我們先新建一個動畫類PhotoTransformer.java

package me.rosuh.android.viewpagernew;
​
import android.support.annotation.NonNull;
import android.support.v4.view.ViewPager;
import android.view.View;
​
​
public class PhotoTransformer implements ViewPager.PageTransformer {
    @Override
    public void transformPage(@NonNull View page, float position) {
        int pageWidth = page.getWidth();
​
        if (position < -1){
            page.setAlpha(1);
        }else if (position <= 1){
            page.setPivotX(position < 0f ? page.getWidth() : 0f);
            page.setPivotY(page.getHeight() * 0.5f);
            page.setRotationY(90f * position);
​
        }else {
            page.setAlpha(1);
        }
    }
}
​複製程式碼

然後為mViewPager設定動畫:

...
FragmentManager fm = getSupportFragmentManager();
​
mViewPager.setPageTransformer(true, new PhotoTransformer());
​
mViewPager.setAdapter(...)複製程式碼

設定這個動畫,最好把CardView的陰影屬性設定為 0。然後稍微修改一下佈局。(在此不列出,可以到程式碼倉庫自己看一下)。下面是效果:

手動實現輪播圖(二):迴圈滾動、定時切換與指示器

結語

本專案地址ViewPagerDemo

目前為止,我們的輪播圖就已經做好了。

這兩篇文章的目標讀者是剛入門的同學,所以有許多地方還有改進的空間。

但是不礙於我們掌握。

文章作者畢竟經驗不多,水平有限,所以缺漏在所難免,希望路過讀到本文的前輩們不吝賜教,謝謝~

感謝一下參考文章和資料:

手動實現輪播圖(二):迴圈滾動、定時切換與指示器


相關文章