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