Android - 顶部滑动导航

一、概述

在 Android 开发中,经常需要使用顶部或者底部的导航来切换当前显示的 Fragment。
在很多应用中还添加了滑动切换的效果,大体效果如下:

Pager滑动导航.gif

这类程序分为两个部分。
下方使用 ViewPager 实现多页滑动显示。滑动时,ViewPager 显示不同的 Fragment,我们可以为 ViewPager 设置适配器来实现这样的效果。
上方的四个 TextView 的显示需要我们自己实现,主要是在 ViewPager 切换的时候进行文字颜色的设置以及下方横线的滑动。

程序源码:PagerSlide

二、Fragment

ViewPager 本身是一个可以滑动的对象,我们可以在其中添加滑动的广告,或者是这里说的 Fragment 的切换。
如果只是添加图片之类的控件,我们只需要设置相应的布局文件即可,但是添加 Fragment 却不是这么简单的。下面我们从 Fragment 生命周期开始讲起。

1. Fragment 生命周期

Android - 顶部滑动导航_第1张图片
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 用于顶部导航。




    

        
            
        

        
            
        

        
            
        

        
            
        

    

    

    

    
    


四、主要代码

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 mFragmentList;

    public PagerSlideAdapter(FragmentManager fm, List 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 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 中。
        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 会更优秀,有兴趣的话就去学习一下吧。

你可能感兴趣的:(Android - 顶部滑动导航)