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

你可以通过在 RecyclerView 外部嵌套一个带有滑动导航栏的布局来实现这个效果。具体步骤如下: 1. 在布局文件中创建一个带有滑动导航栏的布局,例如使用 TabLayout 和 ViewPager2 实现: ```xml <com.google.android.material.tabs.TabLayout android:id="@+id/tab_layout" android:layout_width="match_parent" android:layout_height="wrap_content" app:tabGravity="center" app:tabMode="scrollable" /> <androidx.viewpager2.widget.ViewPager2 android:id="@+id/view_pager" android:layout_width="match_parent" android:layout_height="match_parent" /> ``` 2. 在代码中初始化 TabLayout 和 ViewPager2,并设置 Adapter: ```kotlin val tabLayout: TabLayout = findViewById(R.id.tab_layout) val viewPager: ViewPager2 = findViewById(R.id.view_pager) val adapter = MyAdapter(supportFragmentManager, lifecycle) viewPager.adapter = adapter TabLayoutMediator(tabLayout, viewPager) { tab, position -> tab.text = "Tab ${position + 1}" }.attach() ``` 3. 在 Adapter 中创建 RecyclerView,并在 onCreateViewHolder 中返回 ViewHolder: ```kotlin override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder { if (viewType == TYPE_RECYCLER_VIEW) { val view = LayoutInflater.from(parent.context).inflate(R.layout.recyclerview_layout, parent, false) return RecyclerViewHolder(view) } else { val view = LayoutInflater.from(parent.context).inflate(R.layout.view_pager_layout, parent, false) return ViewPagerHolder(view) } } ``` 4. 在 RecyclerView 的 Adapter 中添加一个 header item,用来显示滑动导航栏,例如: ```kotlin override fun getItemViewType(position: Int): Int { return if (position == 0) TYPE_HEADER else TYPE_RECYCLER_VIEW } override fun getItemCount(): Int { return list.size + 1 // 添加一个 header item } override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) { if (holder is RecyclerViewHolder) { val item = list[position - 1] // 减去 header item holder.bind(item) } } ``` 5. 在 RecyclerView 的 LayoutManager 中,设置偏移量来使 RecyclerView 的内容不会被滑动导航栏遮挡: ```kotlin val layoutManager = LinearLayoutManager(this) recyclerView.layoutManager = layoutManager recyclerView.addItemDecoration(object : RecyclerView.ItemDecoration() { override fun getItemOffsets(outRect: Rect, view: View, parent: RecyclerView, state: RecyclerView.State) { super.getItemOffsets(outRect, view, parent, state) if (parent.getChildAdapterPosition(view) == 0) { outRect.top = tabLayout.height } } }) ``` 这样,你就可以在 RecyclerView 中添加一个顶部滑动导航栏了。
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值