Android - 頂部滑動導航

###一、綜述
在 Android 開發中,經常需要使用頂部或者底部的導航來切換當前顯示的 Fragment。
在很多應用中還添加了滑動切換的效果,大體效果如下:

Pager滑動導航.gif

這類程序分爲兩個部分。
下方使用 ViewPager 實現多頁滑動顯示。滑動時,ViewPager 顯示不同的 Fragment,我們可以爲 ViewPager 設置適配器來實現這樣的效果。
上方的四個 TextView 的顯示需要我們自己實現,主要是在 ViewPager 切換的時候進行文字顏色的設置以及下方橫線的滑動。
####程序源碼:PagerSlide

###二、Fragment
ViewPager 本身是一個可以滑動的對象,我們可以在其中添加滑動的廣告,或者是這裏說的 Fragment 的切換。
如果只是添加圖片之類的控件,我們只需要設置相應的佈局文件即可,但是添加 Fragment 卻不是這麼簡單的。下面我們從 Fragment 生命週期開始講起。

###1. Fragment 生命週期
Fragment 生命週期.png
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 會更優秀,有興趣的話就去學習一下吧。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章