前言
好記性不如爛筆頭,學習的知識總要記錄下來,通過本文來加深對 ViewPager 方方面面的理解:
- ViewPager 的基礎介紹
- PagerAdapter + FragmentPagerAdapter&FragmentStatePagerAdapter
- 與 Fragment + TabLayout 的聯動使用
- Banner 輪播圖
- 自定義切換動畫
- 首次登錄引導界面
閑話少說,下面進入正題。
基礎介紹
ViewPager 是Android support v4 包中的類,官方文檔對其描述如下:
Layout manager that allows the user to flip left and right through pages of data.
意思是說,其本身是一個布局管理器,允許我們左右滑動來切換不同的數據頁面。
它直接繼承自 ViewGroup 類,說明它是一個容器類,可以在其中添加其他View,實際上我們也就是這么用的。
在使用時,直接在布局中加入 ViewPager 即可,相信大家都會,至于其中的屬性,就只有一個 android:clipChildren
需要注意一下,我們后面會說,其他都和一般的 ViewGroup 沒什么區別(其實這個clipChildren屬性也是源自 ViewGroup 的~)。
這里提一下幾個動態設置方法,能不能實現 漂亮花哨的效果,基本就靠這幾個方法:
-
setAdapter(PagerAdapter adapter)
設置適配器 -
setOffscreenPageLimit(int limit)
設置緩存的頁面個數,默認是 1 -
setCurrentItem(int item)
跳轉到特定的頁面 -
addOnPageChangeListener(..)
設置頁面滑動時的監聽器 -
setPageTransformer(..PageTransformer)
設置頁面切換時的動畫效果 -
setPageMargin(int marginPixels)
設置不同頁面之間的間隔 -
setPageMarginDrawable(..)
設置不同頁面間隔之間的裝飾圖也就是 divide ,要想顯示設置的圖片,需要同時設置setPageMargin()
同時它需要實現一個 PagerAdapter 適配器,和 ListView,RecyclerView 類似,適配器用來提供數據,填充頁面。
ViewPager 適配器 - PagerAdapter
PagerAdapter 是一個抽象類,因此我們只能使用它的實現類,官方為我們提供了兩個直接子類 FragmentPagerAdapter 和 FragmentStatePagerAdapter ,基本都是ViewPager + Fragment 搭配時使用的。
但是,我們使用ViewPager顯然不是只為了和 Fragment 打交道的,比如實現后面會講到的輪播圖,因此我們仍要按需實現合適的適配器,現在先看看如何去實現一個PagerAdapter子類,主要就是以下4個方法(必須實現):
-
int getCount()
:獲取頁面數。 -
boolean isViewFromObject(View view, Object object)
:判斷頁面視圖是否和instantiateItem()
方法返回的對象相關聯,總之通常直接返回return view == object;
-
Object instantiateItem(View container, int position)
:作用是對要顯示或緩存的界面,進行布局的初始化。 -
void destroyItem(ViewGroup container, int position, Object object)
: 銷毀頁面。
我們來看一下源碼中對 ViewPager執行流程的解釋,來加深理解。
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 并不是直接處理視圖,而是將每個頁面與一個key Object(沒錯就是instantiateItem()
返回的東西)關聯起來,這個 key Object 跟蹤并且唯一標識一個給定的頁面。
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實現(也就是只實現上面的4個方法),是將頁面視圖本身作為key Object,在創建后通過instantiateItem()
方法返回,并將它們添加到父容器ViewGroup 中,當我們不需要某視圖或者緩存達到上限時,destroyItem()
方法被調用,會將該視圖從父ViewGroup中移除。最后Google建議我們直接在isViewFromObject()
方法中直接返回return view == object;
更多關于ViewPager的處理邏輯,建議直接看源碼中的注釋,涉及到其他的各種方法,此處就不再多說了。
ViewPager + TabLayout + Fragment
理論
Google 官方文檔中 Creating swipe views with tabs 這一節中,介紹的是 ViewPgaer + Fragment + Action bar tabs/ PagerTitleStrip 實現導航頁,三者聯動使用。但是隨著 Material Design 中 TabLayout 的推出,直接秒殺上述tabs或PagerTitleStrip(其實從效果上來看差不太多,但是 TabLayout 可以一行代碼外加一個方法搞定和ViewPager 的聯動,比前者方便太多),所以本文就直接介紹和 TabLayout 的配合使用。
前面我們也提到了官方為我們提供了兩個PagerAdapter的直接子類:FragmentPagerAdapter和FragmentStatePagerAdapter,不知道在座的讀者你們是什么感覺,我是覺得很奇怪,為什么針對 Fragment 要搞兩個子類出來?
這種時候,看看源碼就清楚了,主要區別主要在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);
}
源碼解釋的很清楚,FragmentPagerAdapter 只是將 銷毀視圖,而不是銷毀Fragment 實例,而FragmentStatePagerAdapter 則是徹底將 Fragment 從當前的 FragmentManager中溢出,但是會保存 Fragment 的狀態信息(也就是名字中State的意義),等到需要重建(切換回該頁面)時,通過狀態信息進行恢復創建。
官方(源碼)建議我們使用這二者的場景如下:
FragmentPagerAdapter:適合用于展示靜態的fragment,主頁面等,類似幾個tabs。此時,不會占有太大的內存,同時避免因反復銷毀創建浪費時間。
FragmentStatePagerAdapter:類似ListView,需要展示大量頁面時,由于大量頁面對用戶不可見,當Fragment被銷毀時,我們只會保存其狀態信息,這樣會節省大量的內存。
emmm...好像說的有點遠了,下面介紹如何使用。
二者從使用上來看是毫無區別的,實現兩個方法:
-
public Fragment getItem(int position)
返回對應 Fragment 實例,一般我們在使用時,會通過構造傳入一個要顯示的 Fragment 的集合,我們只要在這里把對應的 Fragment 返回就行了 -
public int getCount()
返回的是頁面的個數,我們只要返回傳入 Fragment 集合的長度就行了。
嗯,下面就到如何實現 TabLayout 和 ViewPager 的聯動了,等我下面介紹完,我相信你會驚訝于它怎么會如此簡單的,只要兩個步驟:
- 初始化后調用
TabLayout.setupWithViewPager(ViewPager)
方法,將二者綁定到一起。 - 重寫 PagerAdapter 的
public CharSequence getPageTitle(int position)
方法。 TabLayout 會通過setupWithViewPager()
方法底部會調用 PagerAdapter 中的getPageTitle()
方法來獲取 title 并更新自己的 tab 的。
在網上看到一篇文章說到
setupWithViewPager()
方法存在三個坑,看了下好像的確有些道理,大家可以自行了解一下。http://www.lxweimin.com/p/896b149aaa43
理論知識暫時告一段落,下面進入實踐時間。
實例
先放上最終的效果圖:(頂部綠色導航基于 TabLayout 實現,而下方的藍(青?)色的是 PagerTitleStrip 的默認效果,只是為了凸顯二者的區別)
嗯。。還是挺簡單的,直接上代碼吧:
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);
//設置標簽擺放方式
//默認為MODE_FIXED,固定模式
//mTabLayout.setTabMode(TabLayout.MODE_FIXED);
//滑動模式
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>
這個組合還是挺常用的,尤其是MD風格的APP中尤為常見,建議還是要能夠熟練使用(雖然我才入坑不久,但是菜就不能提建議了么~)
ViewPager 輪播
首先盜個圖~
從構成元素來講,就這么幾個:標題&指示器、切換動畫、自動輪播、首位循環無限輪播。(頁面本身用一個 ImageView 填充,應該不需要在額外強調什么吧~)
標題&指示器
比較常見的寫法是在ViewPager所在布局中,聲明指示器和標題布局:
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>
可能有童鞋要問:為什么不直接把 標題和指示器 放到 Banner 的 Item 里面呢,這樣我們只要復寫 instantiateItem()
不就可以直接完成初始化了?嗯,關于這點,只是為了切換效果好一點,僅此而已,沒有什么額外的用意。
然后需要注意我們上面 小圓點 指示器使用了一個 LinearLayout,這是因為某些情況下,我們預先可能不知道會有多少個頁面,所以我們干脆直接用一個 LinearLayout,在代碼中動態加載指示器的 view 添加進來。
現在我們有了標題和指示器,下面就要考慮如何讓這二者與頁面聯動了。
這就用到了 addOnPageChangeListener()
這個方法,該方法會設置一個OnPageChangeListener監聽器,用來監聽頁面的變化。其中有三個回調方法:
-
onPageScrolled()
:當前頁面發生滑動時調用 -
onPageSelected()
:頁面滑動結束,選定頁面時調用。需要注意的是,該方法調用時,動畫未必完成 -
onPageScrollStateChanged()
:當滑動狀態改變時調用,即處理何時開始滑動,或何時滑動停止。
于是乎,我們只需要回調onPageSelected()
方法即可,在此方法中設置標題和指示器跟隨變化即可。
實例如下:
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) {
}
});
關于頁面本身的加載,就只是用一個 ArrayList<ImageView> 來存 Banner 的圖片資源,當然為了順暢運行,我是使用了 Glide 加載圖片(直接調用imageView.setImageResource(R.drawable.XXXX);
時模擬器卡的動不了,主要還是圖片資源太大了。= =),以下是實現 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));
}
});
切換動畫
切換動畫,主要是用到setPageTransformer(boolean .. ,PageTransformer ...)
方法來設置動畫,該方法會接收一個 PageTransfromer 參數,這就是動畫的核心關鍵所在了。
PageTransformer 實際上是一個接口,內部只有一個方法 void transformPage(@NonNull View page, float position);
,該方法接收兩個參數,一個 View 顯然就是我們的頁面了,當然這個 頁面 涵蓋了當前顯示的頁面、即將滑出的頁面、即將滑入的頁面以及隱藏的頁面,而這么多頁面,如何區分呢?這就第二個參數 position 的作用了。首先,千萬不要和 ViewPager 下標的 position 混淆了(float 類型你告訴我是下標?),源碼中對 position 的解釋如下:
View 的 position 和 ViewPager 當前的中心位置有關,當前選中的頁面 position 是 0,前一個頁面是 -1,后一個頁面是 1。
但是有同學指出:
前后 item position 為 -1 和 1 的前提是你沒有給 ViewPager 設置 pageMargin。如果你設置了 pageMargin,前后 item 的 position 需要分別加上(或減去,前減后加)一個偏移量(偏移量的計算方式為 pageMargin / pageWidth)。
嗯,然后當我們頁面滑動的時候,position 是動態變化的,transformPage()
會根據 position 的值來對頁面進行屬性變換,position 的變化規律如下:(不考慮pageMargin,方便講解)
- position 分為三段:(-∞,-1)[-1,1](1,∞)
- 對于左右兩個,多數時是不可見的,因此只需要分析以下[-1,1]區間
- 以第一頁->第二頁(左滑)為例:
- 頁1的position:0->-1
- 頁1的position:1->0
- 根據上述,我們就可以通過
setAlpha()
等方法設置屬性,以此達到自定義切換動畫的效果。(實際和屬性動畫有那么一點點類似)
實例嘛,見這節結束的實例就好了,此處不多搞了。
切換動畫,可塑性實在是太高了,基本只有你想不到,沒有它做不到的,于是后面我們會再擴充幾種切換動畫來加深理解。
自動輪播
自動輪播,聽起來高大上,原理簡單的離譜:每隔一定時間給它一個事件,告訴它“嘿,你該切換頁面了”。嗯,說到這,不就是調用Handler.sendEmptyMessageDelayed(int what, long delayMillis)
的小事了么~
Handler mHandler = new Handler() {
@Override
public void handleMessage(Message msg) {
//mViewPager.setCurrentItem(mViewPager.getCurrentItem() + 1);//當實現首尾循環無限輪播時的第一種方案時會這么設置,后面再說。
mViewPager.setCurrentItem((mViewPager.getCurrentItem() + 1) % mList.size());
this.sendEmptyMessageDelayed(MSG_WHAT, 2000);
}
};
有了上述代碼,我們只需要在初始化 ViewPager 之后調用依次Handler.sendEmptyMessageDelayed(int what, long delayMillis)
就ok了。
當然實踐中,我們可能需要對自動輪播進一步處理,譬如判斷滑動手勢暫停輪播,我們總不會希望“我錯過了一個感興趣的廣告,然后把頁面滑動回去,結果很快頁面又!自動滑動回來了”,這種體驗估計就很差。我在此處就不加以實現了,大家可以自行嘗試一下,畢竟我只是講解向~~(其實只是手勢判斷還沒接觸 ~~)。
首尾循環無限輪播
關于首尾無限輪播,指的是在第一個頁面時向左滑動能夠連貫的滑動到最后一頁,而在最后一頁向右滑動時,能順暢的滑動到第一頁。
起初我是沒有注意到有什么坑的,但是當我按照上面的代碼運行之后,發現首尾十分的不連貫,會連續滑過中間的所有頁面,顯然并不能滿足我們的需求。
對于首尾循環的輪播,我也是參考網上的思路,就簡單介紹一下:
- 設置 ViewPager 展示的個數為Inreger.MAX_VALUE,初始化時,將當前頁面設置為n*mList.size(),除非閑得蛋疼,不然沒什么人有毅力滑個Integer.MAX_VALUE次吧,所以說通常是沒什么問題的。
- 在首尾分別加入最后一頁和當前一頁,比如 原來是 a,b,c 現在變為 c,a,b,c,a,當從末尾的c滑動到a時,將頁面切換為第一個a。同理在第一個a左滑動到c時,將頁面切換到第二個c。缺點可能就時可能會有短暫的延時?
貼出來參考的文章
實例
效果圖呈上:
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);//無限輪播時
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);//設置緩存頁面數量
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
見前幾節。
ViewPager 切換動畫擴充
ZoomOutPageTransformer
RotateDownPageTransformer
注意,為了再ViewPager中可以同時顯示多個頁面,我們需要再布局中 設置 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>
嗯,其他的好像沒什么可說得了(畢竟我的這兩個切換效果一個是摘自Google官方,一個摘自 鴻洋 大佬。。),就推薦一個兩個開源庫吧
- GitHub上比較火的廣告輪播控件,雖然是幾年前的東西,但還是很值得參考的:AndroidImageSlider
- 一個看起來還不錯的切換效果合輯 PageTransformerHelp
另外,給出 鴻洋 大佬關于自定義切換效果的文章,大佬的文章還是很值得學習的。
View Pager + Fragment + SharedPreferences 首次登錄引導界面
還是先將效果圖放出來吧(圖片和上面相同的資源,畢竟只是講解思路嘛~ 丑點就丑點吧~)
(為了圖省事,直接從CSDN把圖扒過來,然后又圖省事,在線壓縮gif,結果就來了兩重水印。。蛋碎了一地。)
實際上和上面也沒有什么本質上的區別,所以在此就只介紹一下思路吧。
只是利用 SharedPreferences 來記錄當前是否為第一次登錄,指示器和上述實現一致,同時加入兩個按鈕,右上角 skip(始終存在),指示器上方 got it(當滑動到最后一頁時出現),二者點擊時都會啟動主頁面。
除此之外,該模式可以有很多變型:
- 右上角 skip 倒計時,倒計時完成后自動啟動主頁面,也可點擊進入主頁面
- 左右滑動的頁面可以設置為 自動輪播,播放到最后一頁時 自動進入主頁面
- 不給 skip ,強制觀看完所有引導頁之后,才能通過彈出的got it 進入主頁面
- …………
代碼如下:(其實你會發現,代碼和上面的代碼 差別很小~)
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>
總結
本文針對 ViewPager 盡可能的介紹各種使用方法,涵蓋如下:
- 基礎介紹
- PagerAdapter + FragmentPagerAdapter&FragmentStatePagerAdapter
- 與 Fragment + TabLayout 的聯動使用
- Banner 輪播圖
- 自定義切換動畫
- 首次登錄引導界面
放上源碼地址,可以下載下來配合學習。
源碼地址:https://github.com/whdalive/Demo-ViewPager
洋洋灑灑寫了這么多,最后愿本文對大家有所幫助。互勉。