目录
- 一、简介
- 二、基础使用
- 2.1 ViewPager布局
- 2.2 准备数据
- 2.3 ViewPager的适配器
- 2.4 添加适配器
- 2.5 监听器OnPageChangeListener
- 三、另外两个适配器
- 3.1 FragmentStatePagerAdapter:
- 3.2 FragmentPagerAdapter:
- 四、进阶知识
- 4.1 PagerTransformer定制页面切换效果
- 4.2 ViewPager的缓存机制
- 4.3 ViewPager的懒加载策略
- 4.3.1 核心思路
- 4.3.2 解决措施
- 4.4 ViewPager的卡顿及性能优化
- 4.4.1 优化一:设置缓存页面数
- 4.4.2 优化二:避免重复创建View
- 五、TabLayout+ViewPager+Fragment
- 六、TabLayout属性
一、简介
ViewPager是Android中常用的一个控件,用于实现页面之间的切换效果,类似于滑动切换不同页面的功能。ViewPager通常用于实现轮播图、引导页、图片浏览等功能。
以下是ViewPager的一些特点和用法:
特点:
- 允许用户通过手势(如滑动)或程序控制来切换页面。
- 支持左右滑动切换页面,也可以自定义切换效果。
- 可以容纳多个子视图(即多个页面),每个子视图对应一个页面。
类直接继承了ViewGroup类,所以它是一个容器类,可以在其中添加其他的view类。
类需要一个PagerAdapter适配器类给它提供数据。
经常和Fragment一起使用,并且提供了专门的FragmentPagerAdapter和FragmentStatePagerAdapter类供Fragment中的ViewPager使用。
二、基础使用
2.1 ViewPager布局
ViewPager布局的xml编写
<
android:id="@+id/guide_vp"
android:layout_width="match_parent"
android:layout_height="match_parent" />
2.2 准备数据
使用四张ImageView作为素材放到集合中,待会添加到适配器中
private int[] imgs = {R.drawable.y0, R.drawable.y1, R.drawable.y2, R.drawable.y3};
private void initImgs() {
ViewPager.LayoutParams params = new ViewPager.LayoutParams();
imageViews = new ArrayList<ImageView>();
for (int i = 0; i < imgs.length; i++) {
ImageView imageView = new ImageView(this);
imageView.setLayoutParams(params);
imageView.setImageResource(imgs[i]);
imageView.setScaleType(ImageView.ScaleType.CENTER_CROP);
imageViews.add(imageView);
}
}
2.3 ViewPager的适配器
自定义一个ViewPager的适配器,继承自PagerAdapter ,实现它的两个必写的抽象方法getCount
()和isViewFromObject
(@NonNull View view, @NonNull Object object)
介绍一下里面最重要的四个方法:
- int
getCount()
用途:获取PagerAdapter管理的子视图的数量。
返回值:返回子视图的数量,通常是数据集合的大小。
示例:
@Override
public int getCount() {
return ();
}- boolean
isViewFromObject(View view, Object object)
:
用途:判断instantiateItem()返回的对象是否与当前视图相关联。
参数:
view:当前视图。
object:instantiateItem()返回的对象。
返回值:如果view与object相关联,则返回true;否则返回false。
示例:
@Override
public boolean isViewFromObject(View view, Object object) {
return view == object;
}- Object
instantiateItem(ViewGroup container, int position)
:
用途:实例化指定位置的子视图并添加到容器中。
参数:
container:ViewPager容器,子视图将被添加到此容器中。
position:子视图在数据集合中的位置。
返回值:返回实例化的子视图对象。
示例:
@Override
public Object instantiateItem(ViewGroup container, int position) {
View view = (context).inflate(.item_layout, container, false);
(view);
return view;
}- void
destroyItem(ViewGroup container, int position, Object object)
:
用途:销毁指定位置的子视图。
参数:
container:ViewPager容器。
position:要销毁的子视图在数据集合中的位置。
object:要销毁的子视图对象,通常是通过instantiateItem()返回的对象。
示例:
@Override
public void destroyItem(ViewGroup container, int position, Object object) {
((View) object);
}
用List包装ImageView作适配器的参数
public class GuideAdapter extends PagerAdapter {
private final List<ImageView> imageViews;
public GuideAdapter(List<ImageView> imageViews) {
this.imageViews = imageViews;
}
/**
* 获取当前要显示对象的数量
*/
@Override
public int getCount() {
return imageViews.size();
}
/**
* 判断是否用对象生成界面
*/
@Override
public boolean isViewFromObject(@NonNull View view, @NonNull Object o) {
return view == o;
}
/**
* 从ViewGroup中移除当前对象
*/
@Override
public void destroyItem(@NonNull ViewGroup container, int position, @NonNull Object object) {
container.removeView(imageViews.get(position));
}
/**
* 当前要显示的对象
*/
@NonNull
@Override
public Object instantiateItem(@NonNull ViewGroup container, int position) {
container.addView(imageViews.get(position));
return imageViews.get(position);
}
}
2.4 添加适配器
private ViewPager vp;
private int[] imgs = {R.drawable.y0, R.drawable.y1, R.drawable.y2, R.drawable.y3};
private GuideAdapter adapter;
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_guide);
vp = findViewById(R.id.guide_vp);
//初始化图片
initImgs();
adapter = new GuideAdapter(imageViews);
vp.setAdapter(adapter);
......
2.5 监听器OnPageChangeListener
ViewPager监听器OnPageChangeListener对象,实现了页面切换时的监听和处理。OnPageChangeListener接口中包含了三个方法,分别是onPageScrolled
、onPageSelected
和onPageScrollStateChanged
。
方法 | 用途 | 参数解释 |
---|---|---|
onPageScrolled(int position, float positionOffset, int positionOffsetPixels) | 用途:当页面在滑动过程中被调用。 | position:当前页面的索引,即正在被滑动的页面。 positionOffset:当前页面偏移的百分比,取值范围为[0, 1)。 positionOffsetPixels:当前页面偏移的像素值。 |
onPageSelected(int position): | 用途:当新的页面被选中时被调用。 | position:新选中页面的索引。 |
onPageScrollStateChanged(int state): | 用途:当页面滑动状态改变时被调用。 | state:当前页面滑动状态,有三个取值:SCROLL_STATE_IDLE(空闲)、SCROLL_STATE_DRAGGING(拖动中)、SCROLL_STATE_SETTLING(固定中)。 |
当新的页面被选中的时候,选中不同的圆标,最后一个图片被选中时,Button可见。
vp.addOnPageChangeListener(new ViewPager.OnPageChangeListener() {
@Override
public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {
}
@Override
public void onPageSelected(int position) {
for (int i = 0; i < dotViews.length; i++) {
if (position == i) {
dotViews[i].setImageResource(R.drawable.guide_selector);
} else {
dotViews[i].setImageResource(R.drawable.guide_white);
}
if (position == dotViews.length - 1) {
btn.setVisibility(View.VISIBLE);
} else {
btn.setVisibility(View.GONE);
}
}
}
@Override
public void onPageScrollStateChanged(int state) {
}
});
看一下基础效果:
三、另外两个适配器
基础的适配器讲完了,另外的两个适配器,FragmentPagerAdapter
和FragmentStatePagerAdapter
,更专注于每一页是Fragment的情况,而这两个子类适配器使用情况也是有区别的。FragmentPagerAdapter适用于页面比较少的情况,FragmentStatePagerAdapter适用于页面比较多的情况。为什么?简单分析下两个适配器的源码就可以知道了。
3.1 FragmentStatePagerAdapter:
@Override
public Object instantiateItem(ViewGroup container, int position) {
// If we already have this item instantiated, there is nothing
// to do. This can happen when we are restoring the entire pager
// from its saved state, where the fragment manager has already
// taken care of restoring the fragments we previously had instantiated.
if (mFragments.size() > position) {
Fragment f = mFragments.get(position);//fragment被释放后这里得到的null值
if (f != null) {
return f;
}
}
if (mCurTransaction == null) {
mCurTransaction = mFragmentManager.beginTransaction();
}
Fragment fragment = getItem(position);//fragment被释放后或者是初次进入页面拿到新的Fragment实例
if (DEBUG) Log.v(TAG, "Adding item #" + position + ": f=" + fragment);
if (mSavedState.size() > position) {
Fragment.SavedState fss = mSavedState.get(position);
if (fss != null) {
fragment.setInitialSavedState(fss);
}
}
while (mFragments.size() <= position) {
mFragments.add(null);
}
fragment.setMenuVisibility(false);
fragment.setUserVisibleHint(false);
mFragments.set(position, fragment);
mCurTransaction.add(container.getId(), fragment);//新的Fragment实例 是add上去的
return fragment;
}
@Override
public void destroyItem(ViewGroup container, int position, Object object) {
Fragment fragment = (Fragment) object;
if (mCurTransaction == null) {
mCurTransaction = mFragmentManager.beginTransaction();
}
if (DEBUG) Log.v(TAG, "Removing item #" + position + ": f=" + object
+ " v=" + ((Fragment)object).getView());
while (mSavedState.size() <= position) {
mSavedState.add(null);
}
mSavedState.set(position, fragment.isAdded()
? mFragmentManager.saveFragmentInstanceState(fragment) : null);
mFragments.set(position, null);//真正释放了fragment实例
mCurTransaction.remove(fragment);
}
3.2 FragmentPagerAdapter:
@Override
public Object instantiateItem(ViewGroup container, int position) {
if (mCurTransaction == null) {
mCurTransaction = mFragmentManager.beginTransaction();
}
final long itemId = getItemId(position);
// Do we already have this fragment?
String name = makeFragmentName(container.getId(), itemId);
Fragment fragment = mFragmentManager.findFragmentByTag(name);
if (fragment != null) {
if (DEBUG) Log.v(TAG, "Attaching item #" + itemId + ": f=" + fragment);
mCurTransaction.attach(fragment);//因为fragment实例没有被真正释放,所以可以直接attach效率高
} else {
fragment = getItem(position);//初始化页面的时候拿到fragment的实例
if (DEBUG) Log.v(TAG, "Adding item #" + itemId + ": f=" + fragment);
mCurTransaction.add(container.getId(), fragment,
makeFragmentName(container.getId(), itemId));//add上去
}
if (fragment != mCurrentPrimaryItem) {
fragment.setMenuVisibility(false);
fragment.setUserVisibleHint(false);
}
return fragment;
}
@Override
public void destroyItem(ViewGroup container, int position, Object object) {
if (mCurTransaction == null) {
mCurTransaction = mFragmentManager.beginTransaction();
}
if (DEBUG) Log.v(TAG, "Detaching item #" + getItemId(position) + ": f=" + object
+ " v=" + ((Fragment)object).getView());
mCurTransaction.detach((Fragment)object);//并没有真正释放fragment对象只是detach
}
从源码中我们可以看出FragmentStatePagerAdapter中fragment实例在destroyItem的时候被真正释放,所以FragmentStatePagerAdapter省内存。FragmentPagerAdapter中的fragment实例在destroyItem的时候并没有真正释放fragment对象只是detach,所以FragmentPagerAdapter消耗更多的内存,带来的好处就是效率更高一些。所以得出这样的结论:FragmentPagerAdapter适用于页面比较少的情况,FragmentStatePagerAdapter适用于页面比较多的情况,因此不同的场合选择合适的适配器才是正确的做法。
四、进阶知识
4.1 PagerTransformer定制页面切换效果
ViewPager 默认的切换效果有些平淡,我们可以定制 ViewPager 的页面切换效果,只需用到 ViewPager 的一个方法setPageTransformer(boolean reverseDrawingOrder, @Nullable PageTransformer transformer),实现一个接口 PageTransformer
。
参考链接:
利用ViewPage的PagerTransformer定制页面切换效果
ViewPager一屏显示多个页面
4.2 ViewPager的缓存机制
viewPager.setOffscreenPageLimit(int limit);
这个方法默认值为1,Google在开发ViewPager时,考虑到如果滑动的时候才创建Fragment实例时会带来一定程度的卡顿,因此为ViewPager设置了缓存机制,上述函数则是设置缓存Fragment的数量。
默认情况下,limit的值代表着还要缓存当前Fragment左右各limit个Fragment,一共会创建2*limit+1个Fragment。超出这个limit范围的Fragment就会被销毁。
4.3 ViewPager的懒加载策略
Android的View绘制流程是最消耗时间的操作,尤其是在ViewPager缓存Fragment的情况下,如果在View绘建的同时还进行多个Fragment的数据加载,那用户体验简直是爆炸(不仅浪费流量,而且还造成不必要的卡顿)。。。因此,需要对Fragment们进行懒加载策略。什么是懒加载?就是被动加载,当Fragment页面可见时,才从网络加载数据并显示出来。那什么时候Fragment可见呢?Fragment之中有这样一个函数:
@Override
public void setUserVisibleHint(boolean isVisibleToUser) {
super.setUserVisibleHint(isVisibleToUser);
loaddata();
}
当Fragment的可见状态发生变化时就会调用这个函数,boolean参数isVisibleToUser代表当前的Fragment是否可见。
这里有一个巨坑,这个setUserVisibleHint函数是游离在Fragment生命周期之外的,它的执行有可能早于onCreate和onCreateView,那么要实现数据的加载,就必须要在onCreateView创建完视图过后才能使用,不然就会返回空指针崩溃,懒加载的重点也是在这儿,那么实行懒加载必须满足哪些条件呢?
4.3.1 核心思路
视图加载完毕,即onCreateView()执行完成
2.当前Fragment可见,即setUserVisibleHint()的参数为true
3.初次加载,防止多次滑动重复加载
4.3.2 解决措施
可以通过几个标志位的判断来实现上述懒加载思路。
boolean mIsPrepare = false; //视图还没准备好
boolean mIsVisible= false; //不可见
boolean mIsFirstLoad = true; //第一次加载
在onCreateView中确保了View已经准备好时,将mIsPrepare置为true,
在setUserVisibleHint中确保了当前可见时,mIsVisible置为true,
第一次加载完毕后则将mIsFirstLoad 置为false,避免重复加载。
@Override
public void onViewCreated(View view, @Nullable Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
mIsPrepare = true;
lazyLoad();
}
@Override
public void setUserVisibleHint(boolean isVisibleToUser) {
super.setUserVisibleHint(isVisibleToUser);
//isVisibleToUser这个boolean值表示:该Fragment的UI 用户是否可见
if (isVisibleToUser) {
mIsVisible = true;
lazyLoad();
} else {
mIsVisible = false;
}
}
懒加载的lazyLoad()代码:
private void lazyLoad() {
//这里进行三个条件的判断,如果有一个不满足,都将不进行加载
if (!mIsPrepare || !mIsVisible||!mIsFirstLoad) {
return;
}
loadData();
//数据加载完毕,恢复标记,防止重复加载
mIsFirstLoad = false;
}
private void loadData() {
//这里进行网络请求和数据装载
}
如果Fragment销毁的话,还应该将三个标志位进行默认值初始化:
@Override
public void onDestroyView() {
super.onDestroyView();
mIsFirstLoad=true;
mIsPrepare=false;
mIsVisible = false;
}
在onDestroyView中进行而不是在onDestroy中进行,是因为之前提到Adapter的差异,onDestroy并不一定会调用。
4.4 ViewPager的卡顿及性能优化
Fragment的加载最为耗时的步骤主要有两个,一个是Fragment创建(尤其是创建View的过程),另一个就是读取数据填充到View上的过程。懒加载能够解决后者所造成的卡顿,但是针对前者来说,并没有效果。
4.4.1 优化一:设置缓存页面数
(int limit) 能够有效地一次性缓存多个Fragment,这样就能够解决在之后每次切换时不会创建实例对象,看起来也会流畅。但是这样的做法,最大的缺点就是容易造成第一次启动时非常缓慢!如果第一次启动时间满足要求的话,就使用这种简单地办法吧。
4.4.2 优化二:避免重复创建View
优化Viewpager和Fragment的方法就是尽可能地避免Fragment频繁创建,当然,最为耗时的都是View的创建。所以更加优秀的优化方案,就是在Fragment中缓存自身有关的View,防止onCreateView函数的频繁执行,直接上源码:
public class MyFragment extends Fragment {
View rootView;
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
if (rootView == null) {
rootView = inflater.inflate(R.layout.fragment_my, container, false);
}
return rootView;
@Override
public void onDestroyView() {
super.onDestroyView();
Log.d(TAG, "onDestroyView: " + mParam1);
mIsFirstLoad=true;
mIsPrepare=false;
mIsVisible = false;
if (rootView != null) {
((ViewGroup) rootView.getParent()).removeView(rootView);
}
}
onCreateView中将会对rootView进行null判断,如果为null,说明还没有缓存当前的View,因此会进行过缓存,反之则直接利用。当然,最为重要的是需要在onDestroyView() 方法中及时地移除rootView,因为每一个View只能拥有一个Parent,如果不移除,将会重复加载而导致程序崩溃。
五、TabLayout+ViewPager+Fragment
很多app的主页面都是通过这种方式来实现的,当然现在可能使用jetpack的导航去实现了,不过知识嘛,仍然是嫌少不嫌多的。
整体思路:
创建存储多个Fragment实例的列表
创建PagerAdapter实例并关联到Viewpager中
将ViewPager关联到Tablayout中
根据需求改写Tablayout属性*
看一下mainFragment的布局
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginLeft="10dp"
android:layout_marginRight="10dp"
android:orientation="vertical">
<
android:id="@+id/tab_layout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:tabGravity="fill"
app:tabIndicatorColor="#F8C221"
app:tabMode="fixed"
app:tabSelectedTextColor="#FFC107"
app:tabTextColor="#121212" />
<
android:id="@+id/view_pager"
android:layout_width="match_parent"
android:layout_height="match_parent"/>
</LinearLayout>
public void initFragments() {
String[] title = {"漫画", "小说", "电影"};
List<Fragment> fragmentlist = new ArrayList<>();
fragmentlist.add(new ComicFragment());
fragmentlist.add(new FictionFragment());
fragmentlist.add(new MovieFragent());
newTabAdapter = new ViewPageAdapter(getChildFragmentManager(), fragmentlist, title);
viewPager.setAdapter(newTabAdapter);
viewPager.setOffscreenPageLimit(3);
tab_layout.setupWithViewPager(viewPager);
}
注意ViewPageAdapter中使用了第一个参数使用了getChildFragmentManager,而不是常用的getFragmentManager() 或 getSupportFragmentManager()。为什么呢?
因为我的这块布局上面仍然是一块Fragment。
当在 Activity 中使用 ViewPager 时,一般会使用 getSupportFragmentManager() 方法来获取 Activity 的 FragmentManager。但在 Fragment 中,为了正确管理子 Fragment,应该使用 getChildFragmentManager() 来获取子 Fragment 管理器。
如果在 Fragment 中使用 getFragmentManager() 或 getSupportFragmentManager() 来获取父 Fragment 或 Activity 的 FragmentManager,可能会导致子 Fragment 的生命周期和父 Fragment 的生命周期出现不一致的情况。
因此,必须使用 getChildFragmentManager() 的情况是在 Fragment 中管理子 Fragment 的情况下。如果你没有子 Fragment,或者子 Fragment的生命周期不受父 Fragment 影响,那么你可以考虑使用 getFragmentManager() 或getSupportFragmentManager()。
看一下 ViewPageAdapter
public class ViewPageAdapter extends FragmentPagerAdapter {
private List<Fragment> fragmentList;
private String[] titles;
public ViewPageAdapter(@NonNull FragmentManager fm, List<Fragment> fragmentList, String[] titles) {
super(fm);
this.fragmentList = fragmentList;
this.titles = titles;
}
@NonNull
@Override
public Fragment getItem(int position) {
return fragmentList.get(position);
}
/**
* fragment中的个数
*/
@Override
public int getCount() {
return fragmentList.size();
}
/**
* 返回当前的标题
*/
@Nullable
@Override
public CharSequence getPageTitle(int position) {
return titles[position];
}
}
实现效果:
六、TabLayout属性
属性 | 描述 |
---|---|
app:tabTextColor=“” | 设置tab未被选中时候文本的颜色。 |
app:tabSelectedTextColor=“” | 设置tab选中时候文本的颜色。 |
app:tabIndicatorColor=“” | 设置指示器的颜色,即文本下方的横条。 |
app:tabIndicatorHeight=“10dp” | 设置指示器的高度,即横条的高度。 |
app:tabBackground=“@color/color_00ff00” | 设置TabLayout组件的背景色,注意该值需要是attr,也就是资源引用,无法输入类似”#123456”的具体颜色值。 |
app:tabTextAppearance=“@style/YourStyle” | 设置文件中样式引用,一般是关于文本的属性设置,并且文本大小的设置只有这一种方式。默认的字体大小使用的是design_tab_text_size,自己在中覆写上就可以替换系统的设置。 |
app:tabMode=“” | 有三个值【auto、fixed、scrollable】默认是fixed,表示item默认填充满且平均分布,当item过多的时候,越文本多的item字体会越小进而导致不规范,而scrollable则是表示该TabLayout表示可滑动,很多新闻类应用上方的滑动切换标签都是这样的。 |
app:tabGravity=“” | 有三个值【fill、center、start】默认是fill,并且需要tabMode为fixed时才有效,center其实是按照wrap_content的形式居中显示。和tabMaxWidth、tabMinWidth属性不能共用。 |
tabMinWidth=“” | 设置Tab的最小宽度,和app:tabGravity属性互斥。 |
tabMaxWidth=“” | 设置Tab的最大宽度,和app:tabGravity属性互斥。 |
tabContentStart | 官方说设置TabLayout开始位置的偏移量,但是小空没用过,不知道具体啥样。 |
tabPadding,tabPaddingStart,tabPaddingEnd,tabPaddingTop,tabPaddingBottom | 设置tab项的内边距。 |
app:tabIconTint=“” | 设置文本上面的图标颜色,结合tabIconTintMode使用,这个颜色设置后其实是控制的图标的一个方形区域内所有的颜色,即使这个图标是透明PNG里面内容没有宽高,最终的展示效果受tabIconTintMode影响。你可以找一个红心图片,该属性设置为绿色,tabIconTintMode设置为add就能看出来了。 |
app:tabIconTintMode=“” | 用来设置TabItem的图标颜色和tabIconTint设置的颜色是什么样的混合模式。共有6个值:src_atop、src_over、add、screen、src_in、multiply(更多详情可查询资料xfermode,涉及的内容很多不是一两句话说的清的)。 |
app:tabIndicatorFullWidth | 设置指示器是否沾满整个Item的宽度,当true的时候每个item会根据自身的宽度设置指示器的宽度,默认为true。如果是false表示指示器的宽度一般会随着item中文本的宽度而设定。 |
app:tabIndicatorGravity=“bottom” | 设置指示器所在的位置,有四个值【top、bottom、center、stretch】。Top表示在最上方,也就是图片上方,center表示指示器在图片和文字的中间,stretchj表示占满整个item,默认是bottom,也就是在底部。 |
app:tabIndicatorAnimationDuration=“3000” | 表示设置指示器的动画时间。 |
app:tabIndicatorAnimationMode=“” | 表示设置动画模式,有两个值【linear、elastic】,elastic表示是弹性的,也就是在切换过程中会逐渐拉长然后收缩为正常;而linear表示指示器一直是默认长度。 |
tabInlineLabel=“” | 表示设置图标和文件的方向(默认为false),false表示图片在上文本在下,true表示图片在左文本在右。 |
app:tabRippleColor=“@color/color_00ff00” | 表示设置点击item的时候水波纹的颜色。 |
app:tabUnboundedRipple=“true” | 表示水波纹是否有边界,默认是false代表有边界,点击的时候会看到看方形背景,设置为true则无边框,背景为圆。 |
参考链接:
TabLayout+ViewPager+Fragment实现切页展示「建议收藏」
利用ViewPage的PagerTransformer定制页面切换效果
ViewPager一屏显示多个页面
Android TabLayout 使用进阶(含源码)