前言
好記性不如爛筆頭,學(xué)習(xí)的知識(shí)總要記錄下來,通過本文來加深對(duì) ViewPager 方方面面的理解:
- ViewPager 的基礎(chǔ)介紹
- PagerAdapter + FragmentPagerAdapter&FragmentStatePagerAdapter
- 與 Fragment + TabLayout 的聯(lián)動(dòng)使用
- Banner 輪播圖
- 自定義切換動(dòng)畫
- 首次登錄引導(dǎo)界面
閑話少說,下面進(jìn)入正題。
基礎(chǔ)介紹
ViewPager 是Android support v4 包中的類,官方文檔對(duì)其描述如下:
Layout manager that allows the user to flip left and right through pages of data.
意思是說,其本身是一個(gè)布局管理器,允許我們左右滑動(dòng)來切換不同的數(shù)據(jù)頁面。
它直接繼承自 ViewGroup 類,說明它是一個(gè)容器類,可以在其中添加其他View,實(shí)際上我們也就是這么用的。
在使用時(shí),直接在布局中加入 ViewPager 即可,相信大家都會(huì),至于其中的屬性,就只有一個(gè) android:clipChildren 需要注意一下,我們后面會(huì)說,其他都和一般的 ViewGroup 沒什么區(qū)別(其實(shí)這個(gè)clipChildren屬性也是源自 ViewGroup 的~)。
這里提一下幾個(gè)動(dòng)態(tài)設(shè)置方法,能不能實(shí)現(xiàn) 漂亮花哨的效果,基本就靠這幾個(gè)方法:
-
setAdapter(PagerAdapter adapter)設(shè)置適配器 -
setOffscreenPageLimit(int limit)設(shè)置緩存的頁面?zhèn)€數(shù),默認(rèn)是 1 -
setCurrentItem(int item)跳轉(zhuǎn)到特定的頁面 -
addOnPageChangeListener(..)設(shè)置頁面滑動(dòng)時(shí)的監(jiān)聽器 -
setPageTransformer(..PageTransformer)設(shè)置頁面切換時(shí)的動(dòng)畫效果 -
setPageMargin(int marginPixels)設(shè)置不同頁面之間的間隔 -
setPageMarginDrawable(..)設(shè)置不同頁面間隔之間的裝飾圖也就是 divide ,要想顯示設(shè)置的圖片,需要同時(shí)設(shè)置setPageMargin()
同時(shí)它需要實(shí)現(xiàn)一個(gè) PagerAdapter 適配器,和 ListView,RecyclerView 類似,適配器用來提供數(shù)據(jù),填充頁面。
ViewPager 適配器 - PagerAdapter
PagerAdapter 是一個(gè)抽象類,因此我們只能使用它的實(shí)現(xiàn)類,官方為我們提供了兩個(gè)直接子類 FragmentPagerAdapter 和 FragmentStatePagerAdapter ,基本都是ViewPager + Fragment 搭配時(shí)使用的。
但是,我們使用ViewPager顯然不是只為了和 Fragment 打交道的,比如實(shí)現(xiàn)后面會(huì)講到的輪播圖,因此我們?nèi)砸葱鑼?shí)現(xiàn)合適的適配器,現(xiàn)在先看看如何去實(shí)現(xiàn)一個(gè)PagerAdapter子類,主要就是以下4個(gè)方法(必須實(shí)現(xiàn)):
-
int getCount():獲取頁面數(shù)。 -
boolean isViewFromObject(View view, Object object):判斷頁面視圖是否和instantiateItem()方法返回的對(duì)象相關(guān)聯(lián),總之通常直接返回return view == object; -
Object instantiateItem(View container, int position):作用是對(duì)要顯示或緩存的界面,進(jìn)行布局的初始化。 -
void destroyItem(ViewGroup container, int position, Object object): 銷毀頁面。
我們來看一下源碼中對(duì) ViewPager執(zhí)行流程的解釋,來加深理解。
ViewPager associates each page with a key Object instead of working with Views directly. This key is used to track and uniquely identify a given page independent of its position in the adapter.
ViewPager 并不是直接處理視圖,而是將每個(gè)頁面與一個(gè)key Object(沒錯(cuò)就是instantiateItem()返回的東西)關(guān)聯(lián)起來,這個(gè) key Object 跟蹤并且唯一標(biāo)識(shí)一個(gè)給定的頁面。
A very simple PagerAdapter may choose to use the page Views themselves as key objects, returning them from {@link #instantiateItem(ViewGroup, int)} after creation and adding them to the parent ViewGroup. A matching {@link #destroyItem(ViewGroup, int, Object)} implementation would remove the View from the parent ViewGroup and {@link #isViewFromObject(View, Object)} could be implemented as
return view == object;.
最通常的PagerAdapter實(shí)現(xiàn)(也就是只實(shí)現(xiàn)上面的4個(gè)方法),是將頁面視圖本身作為key Object,在創(chuàng)建后通過instantiateItem()方法返回,并將它們添加到父容器ViewGroup 中,當(dāng)我們不需要某視圖或者緩存達(dá)到上限時(shí),destroyItem()方法被調(diào)用,會(huì)將該視圖從父ViewGroup中移除。最后Google建議我們直接在isViewFromObject()方法中直接返回return view == object;
更多關(guān)于ViewPager的處理邏輯,建議直接看源碼中的注釋,涉及到其他的各種方法,此處就不再多說了。
ViewPager + TabLayout + Fragment
理論
Google 官方文檔中 Creating swipe views with tabs 這一節(jié)中,介紹的是 ViewPgaer + Fragment + Action bar tabs/ PagerTitleStrip 實(shí)現(xiàn)導(dǎo)航頁,三者聯(lián)動(dòng)使用。但是隨著 Material Design 中 TabLayout 的推出,直接秒殺上述tabs或PagerTitleStrip(其實(shí)從效果上來看差不太多,但是 TabLayout 可以一行代碼外加一個(gè)方法搞定和ViewPager 的聯(lián)動(dòng),比前者方便太多),所以本文就直接介紹和 TabLayout 的配合使用。
前面我們也提到了官方為我們提供了兩個(gè)PagerAdapter的直接子類:FragmentPagerAdapter和FragmentStatePagerAdapter,不知道在座的讀者你們是什么感覺,我是覺得很奇怪,為什么針對(duì) Fragment 要搞兩個(gè)子類出來?
這種時(shí)候,看看源碼就清楚了,主要區(qū)別主要在destroyItem()方法:
//FragmentPagerAdapter.java
@Override
public void destroyItem(ViewGroup container, int position, Object object) {
if (mCurTransaction == null) {
mCurTransaction = mFragmentManager.beginTransaction();
}
if (DEBUG) Log.v(TAG, "Detaching item #" + getItemId(position) + ": f=" + object
+ " v=" + ((Fragment)object).getView());
mCurTransaction.detach((Fragment)object);
}
//FragmentStatePagerAdapter.java
@Override
public void destroyItem(ViewGroup container, int position, Object object) {
Fragment fragment = (Fragment) object;
if (mCurTransaction == null) {
mCurTransaction = mFragmentManager.beginTransaction();
}
if (DEBUG) Log.v(TAG, "Removing item #" + position + ": f=" + object
+ " v=" + ((Fragment)object).getView());
while (mSavedState.size() <= position) {
mSavedState.add(null);
}
mSavedState.set(position, fragment.isAdded()
? mFragmentManager.saveFragmentInstanceState(fragment) : null);
mFragments.set(position, null);
mCurTransaction.remove(fragment);
}
源碼解釋的很清楚,F(xiàn)ragmentPagerAdapter 只是將 銷毀視圖,而不是銷毀Fragment 實(shí)例,而FragmentStatePagerAdapter 則是徹底將 Fragment 從當(dāng)前的 FragmentManager中溢出,但是會(huì)保存 Fragment 的狀態(tài)信息(也就是名字中State的意義),等到需要重建(切換回該頁面)時(shí),通過狀態(tài)信息進(jìn)行恢復(fù)創(chuàng)建。
官方(源碼)建議我們使用這二者的場景如下:
FragmentPagerAdapter:適合用于展示靜態(tài)的fragment,主頁面等,類似幾個(gè)tabs。此時(shí),不會(huì)占有太大的內(nèi)存,同時(shí)避免因反復(fù)銷毀創(chuàng)建浪費(fèi)時(shí)間。
FragmentStatePagerAdapter:類似ListView,需要展示大量頁面時(shí),由于大量頁面對(duì)用戶不可見,當(dāng)Fragment被銷毀時(shí),我們只會(huì)保存其狀態(tài)信息,這樣會(huì)節(jié)省大量的內(nèi)存。
emmm...好像說的有點(diǎn)遠(yuǎn)了,下面介紹如何使用。
二者從使用上來看是毫無區(qū)別的,實(shí)現(xiàn)兩個(gè)方法:
-
public Fragment getItem(int position)返回對(duì)應(yīng) Fragment 實(shí)例,一般我們?cè)谑褂脮r(shí),會(huì)通過構(gòu)造傳入一個(gè)要顯示的 Fragment 的集合,我們只要在這里把對(duì)應(yīng)的 Fragment 返回就行了 -
public int getCount()返回的是頁面的個(gè)數(shù),我們只要返回傳入 Fragment 集合的長度就行了。
嗯,下面就到如何實(shí)現(xiàn) TabLayout 和 ViewPager 的聯(lián)動(dòng)了,等我下面介紹完,我相信你會(huì)驚訝于它怎么會(huì)如此簡單的,只要兩個(gè)步驟:
- 初始化后調(diào)用
TabLayout.setupWithViewPager(ViewPager)方法,將二者綁定到一起。 - 重寫 PagerAdapter 的
public CharSequence getPageTitle(int position)方法。 TabLayout 會(huì)通過setupWithViewPager()方法底部會(huì)調(diào)用 PagerAdapter 中的getPageTitle()方法來獲取 title 并更新自己的 tab 的。
在網(wǎng)上看到一篇文章說到
setupWithViewPager()方法存在三個(gè)坑,看了下好像的確有些道理,大家可以自行了解一下。http://www.itdecent.cn/p/896b149aaa43
理論知識(shí)暫時(shí)告一段落,下面進(jìn)入實(shí)踐時(shí)間。
實(shí)例
先放上最終的效果圖:(頂部綠色導(dǎo)航基于 TabLayout 實(shí)現(xiàn),而下方的藍(lán)(青?)色的是 PagerTitleStrip 的默認(rèn)效果,只是為了凸顯二者的區(qū)別)
嗯。。還是挺簡單的,直接上代碼吧:
TabActivity.java
public class TabActivity extends AppCompatActivity {
private ViewPager mViewPager;
private TabLayout mTabLayout;
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_tabfragment);
mViewPager = findViewById(R.id.view_pager_tab);
mViewPager.setAdapter(new FragmentPagerAdapter(getSupportFragmentManager()) {
private String[] titles = new String[]{"Deemo", "Cytus", "蘭空", "萬向物語", "絕地求生", "魔女之泉"};
@Override
public Fragment getItem(int position) {
return PageFragment.newinstance(position);
}
@Override
public int getCount() {
return titles.length;
}
@Nullable
@Override
public CharSequence getPageTitle(int position) {
return titles[position];
}
});
mTabLayout = findViewById(R.id.tablayout);
mTabLayout.setupWithViewPager(mViewPager);
//設(shè)置標(biāo)簽擺放方式
//默認(rèn)為MODE_FIXED,固定模式
//mTabLayout.setTabMode(TabLayout.MODE_FIXED);
//滑動(dòng)模式
mTabLayout.setTabMode(TabLayout.MODE_SCROLLABLE);
}
}
PageFragment.java
public class PageFragment extends Fragment {
public static final String ARGS = "PageFragment";
private int curPage;
public static PageFragment newinstance(int curPage) {
Bundle args = new Bundle();
args.putInt(ARGS, curPage);
PageFragment fragment = new PageFragment();
fragment.setArguments(args);
return fragment;
}
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
curPage = getArguments().getInt(ARGS);
}
@Nullable
@Override
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.fragment_page, container, false);
TextView textView = view.findViewById(R.id.text_view);
textView.setText("Page :" + curPage);
return view;
}
}
activity_tabfragment.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent">
<android.support.design.widget.TabLayout
android:id="@+id/tablayout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="#00ffaa"/>
<android.support.v4.view.ViewPager
android:id="@+id/view_pager_tab"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1">
<android.support.v4.view.PagerTitleStrip
android:id="@+id/pager_title_strip"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="top"
android:background="#33b5e5"
android:textColor="#fff"
android:paddingTop="4dp"
android:paddingBottom="4dp" />
</android.support.v4.view.ViewPager>
</LinearLayout>
fragment_page.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">
<TextView
android:id="@+id/text_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_gravity="center"
android:gravity="center" />
</LinearLayout>
這個(gè)組合還是挺常用的,尤其是MD風(fēng)格的APP中尤為常見,建議還是要能夠熟練使用(雖然我才入坑不久,但是菜就不能提建議了么~)
ViewPager 輪播
首先盜個(gè)圖~

從構(gòu)成元素來講,就這么幾個(gè):標(biāo)題&指示器、切換動(dòng)畫、自動(dòng)輪播、首位循環(huán)無限輪播。(頁面本身用一個(gè) ImageView 填充,應(yīng)該不需要在額外強(qiáng)調(diào)什么吧~)
標(biāo)題&指示器
比較常見的寫法是在ViewPager所在布局中,聲明指示器和標(biāo)題布局:
acctivity_banner.xml
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="160dp"
android:layout_centerInParent="true"
android:background="#1be2be">
<android.support.v4.view.ViewPager
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:id="@+id/view_pager"
android:layout_gravity="center"/>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="bottom"
android:orientation="vertical">
<LinearLayout
android:id="@+id/indicator"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center_horizontal"
android:orientation="horizontal" />
<TextView
android:id="@+id/banner_title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="#7d868585"
android:text="I'm whdalive, an handsome man"/>
</LinearLayout>
</FrameLayout>
可能有童鞋要問:為什么不直接把 標(biāo)題和指示器 放到 Banner 的 Item 里面呢,這樣我們只要復(fù)寫 instantiateItem() 不就可以直接完成初始化了?嗯,關(guān)于這點(diǎn),只是為了切換效果好一點(diǎn),僅此而已,沒有什么額外的用意。
然后需要注意我們上面 小圓點(diǎn) 指示器使用了一個(gè) LinearLayout,這是因?yàn)槟承┣闆r下,我們預(yù)先可能不知道會(huì)有多少個(gè)頁面,所以我們干脆直接用一個(gè) LinearLayout,在代碼中動(dòng)態(tài)加載指示器的 view 添加進(jìn)來。
現(xiàn)在我們有了標(biāo)題和指示器,下面就要考慮如何讓這二者與頁面聯(lián)動(dòng)了。
這就用到了 addOnPageChangeListener()這個(gè)方法,該方法會(huì)設(shè)置一個(gè)OnPageChangeListener監(jiān)聽器,用來監(jiān)聽頁面的變化。其中有三個(gè)回調(diào)方法:
-
onPageScrolled():當(dāng)前頁面發(fā)生滑動(dòng)時(shí)調(diào)用 -
onPageSelected():頁面滑動(dòng)結(jié)束,選定頁面時(shí)調(diào)用。需要注意的是,該方法調(diào)用時(shí),動(dòng)畫未必完成 -
onPageScrollStateChanged():當(dāng)滑動(dòng)狀態(tài)改變時(shí)調(diào)用,即處理何時(shí)開始滑動(dòng),或何時(shí)滑動(dòng)停止。
于是乎,我們只需要回調(diào)onPageSelected()方法即可,在此方法中設(shè)置標(biāo)題和指示器跟隨變化即可。
實(shí)例如下:
mViewPager.addOnPageChangeListener(new ViewPager.OnPageChangeListener() {
@Override
public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {
}
@Override
public void onPageSelected(int position) {
//處理指示器(小圓點(diǎn))的顯示邏輯
for (int i = 0; i < dotsList.size(); i++) {
if (position % dotsList.size() == i) {
dotsList.get(i).setImageResource(R.drawable.indicator_focus);
} else {
dotsList.get(i).setImageResource(R.drawable.indicator_normal);
}
}
//設(shè)置標(biāo)題
bannerTitle.setText(titles[position]);
}
@Override
public void onPageScrollStateChanged(int state) {
}
});
關(guān)于頁面本身的加載,就只是用一個(gè) ArrayList<ImageView> 來存 Banner 的圖片資源,當(dāng)然為了順暢運(yùn)行,我是使用了 Glide 加載圖片(直接調(diào)用imageView.setImageResource(R.drawable.XXXX);時(shí)模擬器卡的動(dòng)不了,主要還是圖片資源太大了。= =),以下是實(shí)現(xiàn) PagerAdapter 子類填充頁面的部分代碼。
mViewPager.setAdapter(new PagerAdapter() {
@Override
public int getCount() {
return imgs.length;
}
@Override
public boolean isViewFromObject(@NonNull View view, @NonNull Object object) {
return view == object;
}
@NonNull
@Override
public Object instantiateItem(@NonNull ViewGroup container, int position) {
container.addView(mList.get(position));
return mList.get(position);
}
@Override
public void destroyItem(@NonNull ViewGroup container, int position, @NonNull Object object) {
container.removeView(mList.get(position));
}
});
切換動(dòng)畫
切換動(dòng)畫,主要是用到setPageTransformer(boolean .. ,PageTransformer ...)方法來設(shè)置動(dòng)畫,該方法會(huì)接收一個(gè) PageTransfromer 參數(shù),這就是動(dòng)畫的核心關(guān)鍵所在了。
PageTransformer 實(shí)際上是一個(gè)接口,內(nèi)部只有一個(gè)方法 void transformPage(@NonNull View page, float position);,該方法接收兩個(gè)參數(shù),一個(gè) View 顯然就是我們的頁面了,當(dāng)然這個(gè) 頁面 涵蓋了當(dāng)前顯示的頁面、即將滑出的頁面、即將滑入的頁面以及隱藏的頁面,而這么多頁面,如何區(qū)分呢?這就第二個(gè)參數(shù) position 的作用了。首先,千萬不要和 ViewPager 下標(biāo)的 position 混淆了(float 類型你告訴我是下標(biāo)?),源碼中對(duì) position 的解釋如下:
View 的 position 和 ViewPager 當(dāng)前的中心位置有關(guān),當(dāng)前選中的頁面 position 是 0,前一個(gè)頁面是 -1,后一個(gè)頁面是 1。
但是有同學(xué)指出:
前后 item position 為 -1 和 1 的前提是你沒有給 ViewPager 設(shè)置 pageMargin。如果你設(shè)置了 pageMargin,前后 item 的 position 需要分別加上(或減去,前減后加)一個(gè)偏移量(偏移量的計(jì)算方式為 pageMargin / pageWidth)。
嗯,然后當(dāng)我們頁面滑動(dòng)的時(shí)候,position 是動(dòng)態(tài)變化的,transformPage()會(huì)根據(jù) position 的值來對(duì)頁面進(jìn)行屬性變換,position 的變化規(guī)律如下:(不考慮pageMargin,方便講解)
- position 分為三段:(-∞,-1)[-1,1](1,∞)
- 對(duì)于左右兩個(gè),多數(shù)時(shí)是不可見的,因此只需要分析以下[-1,1]區(qū)間
- 以第一頁->第二頁(左滑)為例:
- 頁1的position:0->-1
- 頁1的position:1->0
- 根據(jù)上述,我們就可以通過
setAlpha()等方法設(shè)置屬性,以此達(dá)到自定義切換動(dòng)畫的效果。(實(shí)際和屬性動(dòng)畫有那么一點(diǎn)點(diǎn)類似)
實(shí)例嘛,見這節(jié)結(jié)束的實(shí)例就好了,此處不多搞了。
切換動(dòng)畫,可塑性實(shí)在是太高了,基本只有你想不到,沒有它做不到的,于是后面我們會(huì)再擴(kuò)充幾種切換動(dòng)畫來加深理解。
自動(dòng)輪播
自動(dòng)輪播,聽起來高大上,原理簡單的離譜:每隔一定時(shí)間給它一個(gè)事件,告訴它“嘿,你該切換頁面了”。嗯,說到這,不就是調(diào)用Handler.sendEmptyMessageDelayed(int what, long delayMillis)的小事了么~
Handler mHandler = new Handler() {
@Override
public void handleMessage(Message msg) {
//mViewPager.setCurrentItem(mViewPager.getCurrentItem() + 1);//當(dāng)實(shí)現(xiàn)首尾循環(huán)無限輪播時(shí)的第一種方案時(shí)會(huì)這么設(shè)置,后面再說。
mViewPager.setCurrentItem((mViewPager.getCurrentItem() + 1) % mList.size());
this.sendEmptyMessageDelayed(MSG_WHAT, 2000);
}
};
有了上述代碼,我們只需要在初始化 ViewPager 之后調(diào)用依次Handler.sendEmptyMessageDelayed(int what, long delayMillis)就ok了。
當(dāng)然實(shí)踐中,我們可能需要對(duì)自動(dòng)輪播進(jìn)一步處理,譬如判斷滑動(dòng)手勢暫停輪播,我們總不會(huì)希望“我錯(cuò)過了一個(gè)感興趣的廣告,然后把頁面滑動(dòng)回去,結(jié)果很快頁面又!自動(dòng)滑動(dòng)回來了”,這種體驗(yàn)估計(jì)就很差。我在此處就不加以實(shí)現(xiàn)了,大家可以自行嘗試一下,畢竟我只是講解向~~(其實(shí)只是手勢判斷還沒接觸 ~~)。
首尾循環(huán)無限輪播
關(guān)于首尾無限輪播,指的是在第一個(gè)頁面時(shí)向左滑動(dòng)能夠連貫的滑動(dòng)到最后一頁,而在最后一頁向右滑動(dòng)時(shí),能順暢的滑動(dòng)到第一頁。
起初我是沒有注意到有什么坑的,但是當(dāng)我按照上面的代碼運(yùn)行之后,發(fā)現(xiàn)首尾十分的不連貫,會(huì)連續(xù)滑過中間的所有頁面,顯然并不能滿足我們的需求。
對(duì)于首尾循環(huán)的輪播,我也是參考網(wǎng)上的思路,就簡單介紹一下:
- 設(shè)置 ViewPager 展示的個(gè)數(shù)為Inreger.MAX_VALUE,初始化時(shí),將當(dāng)前頁面設(shè)置為n*mList.size(),除非閑得蛋疼,不然沒什么人有毅力滑個(gè)Integer.MAX_VALUE次吧,所以說通常是沒什么問題的。
- 在首尾分別加入最后一頁和當(dāng)前一頁,比如 原來是 a,b,c 現(xiàn)在變?yōu)?c,a,b,c,a,當(dāng)從末尾的c滑動(dòng)到a時(shí),將頁面切換為第一個(gè)a。同理在第一個(gè)a左滑動(dòng)到c時(shí),將頁面切換到第二個(gè)c。缺點(diǎn)可能就時(shí)可能會(huì)有短暫的延時(shí)?
貼出來參考的文章
實(shí)例
效果圖呈上:
BannerActivity.java
public class BannerActivity extends AppCompatActivity {
private static final int MSG_WHAT = 0;
private int[] imgs;
private ViewPager mViewPager;
private List<ImageView> mList = new ArrayList<>();
private String[] titles;
Handler mHandler = new Handler() {
@Override
public void handleMessage(Message msg) {
//mViewPager.setCurrentItem(mViewPager.getCurrentItem() + 1);//無限輪播時(shí)
mViewPager.setCurrentItem((mViewPager.getCurrentItem() + 1) % mList.size());
this.sendEmptyMessageDelayed(MSG_WHAT, 2000);
}
};
private LinearLayout mLinearLayout;
private ArrayList<ImageView> dotsList;
private TextView bannerTitle;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_banner);
imgs = new int[]{R.drawable.a, R.drawable.b, R.drawable.c, R.drawable.e, R.drawable.f};
titles = new String[]{"To think as great minds, to do as idiots","One Step Closer To The Hell","Knowing Everything of Something","Nothing For Nothing","No Royal Road To Anything"};
bannerTitle = findViewById(R.id.banner_title);
mLinearLayout = findViewById(R.id.indicator);
init();
initDots();
mViewPager = findViewById(R.id.view_pager);
mViewPager.setOffscreenPageLimit(3);//設(shè)置緩存頁面數(shù)量
mViewPager.setPageTransformer(true, new BannerPageTransformer());
mViewPager.setAdapter(new PagerAdapter() {
@Override
public int getCount() {
return imgs.length;
}
@Override
public boolean isViewFromObject(@NonNull View view, @NonNull Object object) {
return view == object;
}
@NonNull
@Override
public Object instantiateItem(@NonNull ViewGroup container, int position) {
container.addView(mList.get(position));
return mList.get(position);
}
@Override
public void destroyItem(@NonNull ViewGroup container, int position, @NonNull Object object) {
container.removeView(mList.get(position));
}
});
mViewPager.addOnPageChangeListener(new ViewPager.OnPageChangeListener() {
@Override
public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {
}
@Override
public void onPageSelected(int position) {
for (int i = 0; i < dotsList.size(); i++) {
if (position % dotsList.size() == i) {
dotsList.get(i).setImageResource(R.drawable.indicator_focus);
} else {
dotsList.get(i).setImageResource(R.drawable.indicator_normal);
}
}
bannerTitle.setText(titles[position]);
}
@Override
public void onPageScrollStateChanged(int state) {
}
});
mHandler.sendEmptyMessageDelayed(MSG_WHAT, 2000);
}
private void init() {
for (int img : imgs) {
ImageView imageView = new ImageView(getApplicationContext());
imageView.setScaleType(ImageView.ScaleType.CENTER_CROP);
//imageView.setImageResource(imgid);
Glide.with(getApplicationContext()).load(img).into(imageView);
mList.add(imageView);
}
}
private void initDots() {
dotsList = new ArrayList<>();
for (int i = 0; i < imgs.length; i++) {
ImageView imageView = new ImageView(getApplicationContext());
if (i == 0) {
imageView.setImageResource(R.drawable.indicator_focus);
} else {
imageView.setImageResource(R.drawable.indicator_normal);
}
LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(16, 16);
params.setMargins(5, 0, 5, 0);
mLinearLayout.addView(imageView, params);
dotsList.add(imageView);
}
}
}
BannerPageTransformer.java
public class BannerPageTransformer implements ViewPager.PageTransformer {
@Override
public void transformPage(@NonNull View page, float position) {
int width = page.getWidth();
if (position < -1) {
page.setScrollX((int) (width * 0.75 * -1));
} else if (position <= 1) {
page.setScrollX((int) (width * 0.75 * position));
} else {
page.setScrollX((int) (width * 0.75));
}
}
}
activity_banner.xml
見前幾節(jié)。
ViewPager 切換動(dòng)畫擴(kuò)充
ZoomOutPageTransformer
RotateDownPageTransformer
注意,為了再ViewPager中可以同時(shí)顯示多個(gè)頁面,我們需要再布局中 設(shè)置 ViewPager 及其父容器的 clipChildren 屬性為 false。
activity_trans.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="160dp"
android:clipChildren="false"
android:layout_centerInParent="true"
android:background="#1be2be">
<android.support.v4.view.ViewPager
android:layout_width="match_parent"
android:layout_height="120dp"
android:id="@+id/view_pager_trans"
android:layout_marginLeft="60dp"
android:layout_marginRight="60dp"
android:layout_gravity="center"
android:clipChildren="false"/>
</LinearLayout>
嗯,其他的好像沒什么可說得了(畢竟我的這兩個(gè)切換效果一個(gè)是摘自Google官方,一個(gè)摘自 鴻洋 大佬。。),就推薦一個(gè)兩個(gè)開源庫吧
- GitHub上比較火的廣告輪播控件,雖然是幾年前的東西,但還是很值得參考的:AndroidImageSlider
- 一個(gè)看起來還不錯(cuò)的切換效果合輯 PageTransformerHelp
另外,給出 鴻洋 大佬關(guān)于自定義切換效果的文章,大佬的文章還是很值得學(xué)習(xí)的。
View Pager + Fragment + SharedPreferences 首次登錄引導(dǎo)界面
還是先將效果圖放出來吧(圖片和上面相同的資源,畢竟只是講解思路嘛~ 丑點(diǎn)就丑點(diǎn)吧~)
(為了圖省事,直接從CSDN把圖扒過來,然后又圖省事,在線壓縮gif,結(jié)果就來了兩重水印。。蛋碎了一地。)

實(shí)際上和上面也沒有什么本質(zhì)上的區(qū)別,所以在此就只介紹一下思路吧。
只是利用 SharedPreferences 來記錄當(dāng)前是否為第一次登錄,指示器和上述實(shí)現(xiàn)一致,同時(shí)加入兩個(gè)按鈕,右上角 skip(始終存在),指示器上方 got it(當(dāng)滑動(dòng)到最后一頁時(shí)出現(xiàn)),二者點(diǎn)擊時(shí)都會(huì)啟動(dòng)主頁面。
除此之外,該模式可以有很多變型:
- 右上角 skip 倒計(jì)時(shí),倒計(jì)時(shí)完成后自動(dòng)啟動(dòng)主頁面,也可點(diǎn)擊進(jìn)入主頁面
- 左右滑動(dòng)的頁面可以設(shè)置為 自動(dòng)輪播,播放到最后一頁時(shí) 自動(dòng)進(jìn)入主頁面
- 不給 skip ,強(qiáng)制觀看完所有引導(dǎo)頁之后,才能通過彈出的got it 進(jìn)入主頁面
- …………
代碼如下:(其實(shí)你會(huì)發(fā)現(xiàn),代碼和上面的代碼 差別很小~)
WelcomeActivity.java
public class WelcomeActivity extends AppCompatActivity {
private ViewPager mViewPager;
private AppCompatButton btn_got;
private AppCompatButton btn_skip;
private LinearLayout mLinearLayout;
private ArrayList<ImageView> dotsList;
private int[] imgs;
private List<ImageView> mList = new ArrayList<>();
private SharedPreferences mPreferences;
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
mPreferences = PreferenceManager.getDefaultSharedPreferences(this);
imgs = new int[]{R.drawable.a, R.drawable.b, R.drawable.c, R.drawable.e, R.drawable.f};
if (mPreferences.getBoolean("FirstLaunch", true)) {
setContentView(R.layout.activity_welcome);
mLinearLayout = findViewById(R.id.indicator_welcome);
initView();
initDots();
mViewPager.setAdapter(new PagerAdapter() {
@Override
public int getCount() {
return imgs.length;
}
@Override
public boolean isViewFromObject(@NonNull View view, @NonNull Object object) {
return view == object;
}
@NonNull
@Override
public Object instantiateItem(@NonNull ViewGroup container, int position) {
container.addView(mList.get(position));
return mList.get(position);
}
@Override
public void destroyItem(@NonNull ViewGroup container, int position, @NonNull Object object) {
container.removeView(mList.get(position));
}
});
mViewPager.addOnPageChangeListener(new ViewPager.OnPageChangeListener() {
@Override
public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {
}
@Override
public void onPageSelected(int position) {
for (int i = 0; i < dotsList.size(); i++) {
if (position % dotsList.size() == i) {
dotsList.get(i).setImageResource(R.drawable.indicator_focus);
} else {
dotsList.get(i).setImageResource(R.drawable.indicator_normal);
}
}
btn_got.setVisibility(position == mList.size()-1?View.VISIBLE:View.GONE);
}
@Override
public void onPageScrollStateChanged(int state) {
}
});
btn_got.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
recordFirstLaunch();
notFirstLaunch();
}
});
btn_skip.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
recordFirstLaunch();
notFirstLaunch();
}
});
} else {
notFirstLaunch();
finish();
}
}
private void recordFirstLaunch() {
SharedPreferences.Editor editor = mPreferences.edit();
editor.putBoolean("FirstLaunch", false);
editor.apply();
notFirstLaunch();
}
private void notFirstLaunch() {
startActivity(new Intent(this, MainActivity.class));
}
private void initView() {
mViewPager = findViewById(R.id.view_pager);
btn_got = findViewById(R.id.btn_got);
btn_skip = findViewById(R.id.skip);
for (int img : imgs) {
ImageView imageView = new ImageView(getApplicationContext());
imageView.setScaleType(ImageView.ScaleType.CENTER_CROP);
//imageView.setImageResource(imgid);
Glide.with(getApplicationContext()).load(img).into(imageView);
mList.add(imageView);
}
}
private void initDots() {
dotsList = new ArrayList<>();
for (int i = 0; i < imgs.length; i++) {
ImageView imageView = new ImageView(getApplicationContext());
if (i == 0) {
imageView.setImageResource(R.drawable.indicator_focus);
} else {
imageView.setImageResource(R.drawable.indicator_normal);
}
LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(16, 16);
params.setMargins(5, 0, 5, 0);
mLinearLayout.addView(imageView, params);
dotsList.add(imageView);
}
}
}
activity_welcome.xml
<?xml version="1.0" encoding="utf-8"?>
<android.support.design.widget.CoordinatorLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/main_content"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fitsSystemWindows="true">
<android.support.v7.widget.AppCompatButton
android:id="@+id/skip"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="skip"
android:textAllCaps="false"
android:layout_gravity="top|end"/>
<android.support.v4.view.ViewPager
android:id="@+id/view_pager"
android:layout_width="match_parent"
android:layout_height="match_parent"/>
<android.support.v7.widget.AppCompatButton
android:id="@+id/btn_got"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Got it"
android:layout_gravity="bottom|center"
android:layout_marginBottom="16dp"
android:visibility="gone"/>
<LinearLayout
android:id="@+id/indicator_welcome"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="bottom"
android:gravity="center_horizontal"
android:orientation="horizontal">
</LinearLayout>
</android.support.design.widget.CoordinatorLayout>
總結(jié)
本文針對(duì) ViewPager 盡可能的介紹各種使用方法,涵蓋如下:
- 基礎(chǔ)介紹
- PagerAdapter + FragmentPagerAdapter&FragmentStatePagerAdapter
- 與 Fragment + TabLayout 的聯(lián)動(dòng)使用
- Banner 輪播圖
- 自定義切換動(dòng)畫
- 首次登錄引導(dǎo)界面
放上源碼地址,可以下載下來配合學(xué)習(xí)。
源碼地址:https://github.com/whdalive/Demo-ViewPager
洋洋灑灑寫了這么多,最后愿本文對(duì)大家有所幫助。互勉。