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 会更优秀,有兴趣的话就去学习一下吧。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 198,932评论 5 466
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 83,554评论 2 375
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 145,894评论 0 328
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 53,442评论 1 268
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 62,347评论 5 359
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 47,899评论 1 275
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,325评论 3 390
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,980评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,196评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,163评论 2 317
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,085评论 1 328
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,826评论 3 316
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,389评论 3 302
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,501评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,753评论 1 255
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,171评论 2 344
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 41,616评论 2 339

推荐阅读更多精彩内容

  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 171,226评论 25 707
  • 内容抽屉菜单ListViewWebViewSwitchButton按钮点赞按钮进度条TabLayout图标下拉刷新...
    皇小弟阅读 46,678评论 22 664
  • 郭芳艳 焦点网络初级五期 坚持原创分享第131天 2017.9.30 昨天还热闹异常的国培楼,今天一...
    冰山蓝鹰阅读 187评论 0 0
  • 老公和儿子都爱吃!!! (芝士应该用马拉里苏芝士,家里只有安佳的,不拉丝了,但丝毫不影响味道!!中西结合的一道晚餐...
    与民同乐阅读 588评论 0 0
  • 概念:就是说两个人之间的合作,一定要双方都获得价值,如果一方赚钱,是建立在另一方的损失之上,那我就不干。"双赢思维...
    小芭蕾_7ed2阅读 168评论 0 0