###一、綜述
在 Android 開發中,經常需要使用頂部或者底部的導航來切換當前顯示的 Fragment。
在很多應用中還添加了滑動切換的效果,大體效果如下:
這類程序分爲兩個部分。
下方使用 ViewPager 實現多頁滑動顯示。滑動時,ViewPager 顯示不同的 Fragment,我們可以爲 ViewPager 設置適配器來實現這樣的效果。
上方的四個 TextView 的顯示需要我們自己實現,主要是在 ViewPager 切換的時候進行文字顏色的設置以及下方橫線的滑動。
####程序源碼:PagerSlide
###二、Fragment
ViewPager 本身是一個可以滑動的對象,我們可以在其中添加滑動的廣告,或者是這裏說的 Fragment 的切換。
如果只是添加圖片之類的控件,我們只需要設置相應的佈局文件即可,但是添加 Fragment 卻不是這麼簡單的。下面我們從 Fragment 生命週期開始講起。
###1. Fragment 生命週期
Fragment 的生命週期很複雜,我們只看重點,Fragment 在 onCreateView() 中加載視圖。經過 onActivityCreate() --> onStart() --> onResume() 後才真正顯示。
而在 Fragment 顯示前,還有一個 onActivityCreate() 函數,我們可以在這裏加載 Fragment 所需要的數據(這個例子沒有數據,但在真正的項目裏,這裏一般加載聯網數據)。
###2. BaseFragment
我們創建一個繼承自 Fragment(support.v4 包) 的抽象類 BaseFragment,在裏面實現一些公共的方法。我們所有的自定義 Fragment 都將繼承自 BaseFragment。
BaseFragment 的子類必須都重寫 initView() 方法(因爲每個 Fragment 都需要加載佈局),這個方法返回當前 Fragment 的 View 對象。
而在 onActivityCreated() 方法中我們通過 initData() 加載數據,如果子類需要加載數據並重寫了此方法,那麼根據上面講的生命週期,數據就會在 Fragment 顯示前加載完畢。
public abstract class BaseFragment extends Fragment {
// 上下文對象
protected Context mContext;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
mContext = getActivity();
}
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
return initView();
}
@Override
public void onActivityCreated(Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
initData();
}
// 繼承此類的子類必須重寫此方法加載佈局
public abstract View initView();
// 加載數據的方法
public void initData() { }
}
###3. 子 Fragment
有了 BaseFragment,我們就可以自定義需要顯示的 Fragment 了。Fragment 的佈局文件隨你樂意,這裏我只加了一張圖片。
我們在 initView() 中加載並返回了 View 視圖對象,在 initData() 中加載數據。這兩個方法裏都有 Log 日誌打印,這個待會有用。
public class Fragment1 extends BaseFragment {
@Override
public View initView() {
Log.e("TAG", "Fragment1 --> initView");
View view = View.inflate(mContext, R.layout.fragment1, null);
return view;
}
@Override
public void initData() {
super.initData();
// ......加載數據
Log.e("TAG", "Fragment1 --> initData");
}
}
之後再定義三個相似的 Fragment 即可。
###三、佈局文件
定義四個橫向的 Textview 用於頂部導航。
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
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:orientation="vertical"
tools:context="com.learn.lister.pagerslide.activity.MainActivity">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<LinearLayout
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:padding="10dp"
android:gravity="center">
<TextView
android:id="@+id/page_0"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="首頁"
android:textSize="16sp"
android:textColor="@android:color/black"/>
</LinearLayout>
<LinearLayout
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:padding="10dp"
android:gravity="center">
<TextView
android:id="@+id/page_1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="朋友"
android:textSize="16sp"
android:textColor="@android:color/black"/>
</LinearLayout>
<LinearLayout
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:padding="10dp"
android:gravity="center">
<TextView
android:id="@+id/page_2"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="動態"
android:textSize="16sp"
android:textColor="@android:color/black"/>
</LinearLayout>
<LinearLayout
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:padding="10dp"
android:gravity="center">
<TextView
android:id="@+id/page_3"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="附近"
android:textSize="16sp"
android:textColor="@android:color/black"/>
</LinearLayout>
</LinearLayout>
<View
android:layout_width="match_parent"
android:layout_height="2dp"
android:background="@android:color/darker_gray"/>
<ImageView
android:id="@+id/main_tab_line"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="@drawable/slider"/>
<android.support.v4.view.ViewPager
android:id="@+id/main_pager"
android:layout_width="match_parent"
android:layout_height="match_parent">
</android.support.v4.view.ViewPager>
</LinearLayout>
###四、主要代碼
###1. 適配器
爲了支持在 ViewPager 滑動時向其中添加不同的 Fragment,我們需要爲 ViewPager 設置一個適配器。我們可以自定義一個繼承於 FragmentPagerAdapter 的適配器。
####官方文檔對 FragmentPagerAdapter 的解釋大致如下:
FragmentPagerAdapter 派生自 PagerAdapter,它是用來呈現Fragment頁面的,這些Fragment頁面會一直保存在fragment manager中,以便用戶可以隨時取用。
這個適配器適用於有限個靜態fragment頁面的管理。儘管不可見的視圖有時會被銷燬,但用戶所有訪問過的fragment都會被保存在內存中。
而繼承自 FragmentPagerAdapter 的適配器也只需要重寫 getCount() 和 getItem(int position) 兩個方法。
/**
* Fragment 滑動適配器
* BaseFragment 爲自定義的 Fragment 基類。
*/
public class PagerSlideAdapter extends FragmentPagerAdapter {
private List<BaseFragment> mFragmentList;
public PagerSlideAdapter(FragmentManager fm, List<BaseFragment> fragmentList) {
super(fm);
this.mFragmentList = fragmentList;
}
@Override
public Fragment getItem(int position) {
return mFragmentList.get(position);
}
@Override
public int getCount() {
return mFragmentList.size();
}
}
從代碼中我們可以看出,在構造函數中需要傳入一個 Fragment 的合集並初始化,這些就是 ViewPager 中滑動的對象。
###2. MainActivity
ViewPager 的滑動是設置適配器的效果,而滑動頁面時文字的變化以及橫條的移動就需要我們自己動手了。
public class MainActivity extends AppCompatActivity implements View.OnClickListener {
@BindView(R.id.page_0) TextView text0;
@BindView(R.id.page_1) TextView text1;
@BindView(R.id.page_2) TextView text2;
@BindView(R.id.page_3) TextView text3;
@BindView(R.id.main_tab_line) ImageView tab_line;
@BindView(R.id.main_pager) ViewPager mViewPager;
private int screenWidth;
private List<BaseFragment> mFragmentList = new ArrayList<>();
private PagerSlideAdapter adapter;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
ButterKnife.bind(this);
initData(); // 初始化數據
initWidth(); // 初始化滑動橫條的寬度
setListener(); // 設置監聽器
}
private void initData() {
// 將我們自定義 Fragment 的對象添加到 List<BaseFragment> 中。
mFragmentList.add(new Fragment1());
mFragmentList.add(new Fragment2());
mFragmentList.add(new Fragment3());
mFragmentList.add(new Fragment4());
// 新建適配器
adapter = new PagerSlideAdapter(getSupportFragmentManager(), mFragmentList);
// 爲 ViewPager 設置適配器
mViewPager.setAdapter(adapter);
// 打開應用時 ViewPager 顯示第一個 Fragment
mViewPager.setCurrentItem(0);
text0.setTextColor(Color.BLUE);
}
private void setListener() {
mViewPager.addOnPageChangeListener(new ViewPager.OnPageChangeListener() {
/**
* This method will be invoked when the current page is scrolled, either as part
* of a programmatically initiated smooth scroll or a user initiated touch scroll.
*
* @param position Position index of the first page currently being displayed.
* Page position+1 will be visible if positionOffset is nonzero.
* @param positionOffset Value from [0, 1) indicating the offset from the page at position.
* @param positionOffsetPixels Value in pixels indicating the offset from position.
* 這個參數的使用是爲了在滑動頁面時有文字下方橫條的滑動效果
*/
@Override
public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {
LinearLayout.LayoutParams lp = (LinearLayout.LayoutParams) tab_line.getLayoutParams();
lp.leftMargin = screenWidth/4*position + positionOffsetPixels/4;
tab_line.setLayoutParams(lp);
}
@Override
public void onPageSelected(int position) {
// 在每次切換頁面時重置 TextView 的顏色
resetTextView();
switch (position) {
case 0:
text0.setTextColor(Color.BLUE);
break;
case 1:
text1.setTextColor(Color.BLUE);
break;
case 2:
text2.setTextColor(Color.BLUE);
break;
case 3:
text3.setTextColor(Color.BLUE);
break;
}
}
@Override
public void onPageScrollStateChanged(int state) {
}
});
text0.setOnClickListener(this);
text1.setOnClickListener(this);
text2.setOnClickListener(this);
text3.setOnClickListener(this);
}
private void resetTextView() {
text0.setTextColor(Color.BLACK);
text1.setTextColor(Color.BLACK);
text2.setTextColor(Color.BLACK);
text3.setTextColor(Color.BLACK);
}
// 初始化滑動橫條的寬度
private void initWidth() {
DisplayMetrics dpMetrics = new DisplayMetrics();
getWindow().getWindowManager().getDefaultDisplay().getMetrics(dpMetrics);
screenWidth = dpMetrics.widthPixels;
LinearLayout.LayoutParams lp = (LinearLayout.LayoutParams) tab_line.getLayoutParams();
lp.width = screenWidth / 4;
tab_line.setLayoutParams(lp);
}
// 設置文字的點擊事件,點擊某個 TextView 就跳到相應頁面
@Override
public void onClick(View v) {
switch (v.getId()) {
case R.id.page_0:
mViewPager.setCurrentItem(0);
break;
case R.id.page_1:
mViewPager.setCurrentItem(1);
break;
case R.id.page_2:
mViewPager.setCurrentItem(2);
break;
case R.id.page_3:
mViewPager.setCurrentItem(3);
break;
}
}
}
###五、Fragment 的緩存
到這裏我們的程序已經可以運行了,但還記得我們之前在自定義 Fragment 類中的 Log 日誌嗎?運行程序,讓我們看一下這個日誌。
程序剛運行時日誌:
E/TAG: Fragment1 --> initView
E/TAG: Fragment1 --> initData
E/TAG: Fragment2 --> initView
E/TAG: Fragment2 --> initData
程序剛打開時不是隻顯示一個 Fragment 嗎?爲什麼會加載兩個 Fragment 的資源?這時滑動到第二個 Fragment,你會發現日誌是這樣的:
E/TAG: Fragment3 --> initView
E/TAG: Fragment3 --> initData
看起來適配器總是會預先加載一個頁面,但是當你滑動到最後一個頁面,再往前滑動時,日誌是這樣的:
E/TAG: Fragment2 --> initView
E/TAG: Fragment2 --> initData
Fragment2 之前不是加載過了嗎?怎麼又來?
其實是這樣,適配器爲你保存在內存中的 Fragment 時當前所顯示的 Fragmen以及當前 Fragment 的前一個和後一個。在內存中最多隻會緩存三個 Fragment。(剛打開時只緩存了兩個)
###六、總結
這裏講到了滑動 ViewPager 顯示不同 Fragment,但是這裏的 Fragment 都是靜態的,如果要處理大量的頁面切換,FragmentStatePagerAdapter 會更優秀,有興趣的話就去學習一下吧。