一. 继承

1
2
3
4
java.lang.Object
↳android.view.View
↳android.view.ViewGroup
↳androidx.viewpager.widget.ViewPager

ViewPager 可以让用户左右切换当前的 View。它之前是android.support.v4里面的类,现在归为到了androidx.viewpager.widget里面。

ViewPager 需要一个PagerAdapter适配器来提供数据。

ViewPager 经常和 Fragment 一起使用,并且提供了专门的 FragmentAdapter 和 FragmentStateAdapter 适配器。

二. PagerAdapter

1. 方法讲解

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
viewPager.setAdapter(new PagerAdapter() {
//获取要滑动的View的数量
//这个函数在ViewPager对象创建后自动执行 且只会执行一次
@Override
public int getCount() {
return 0;
}

//这个View的id是不是Object
@Override
public boolean isViewFromObject(@NonNull View view, @NonNull Object object) {
return false;
}

//初始化视图,并且返回一个Object作为key
@NonNull
@Override
public Object instantiateItem(@NonNull ViewGroup container, int position) {
return super.instantiateItem(container, position);
}

//销毁视图
@Override
public void destroyItem(@NonNull ViewGroup container, int position, @NonNull Object object) {
super.destroyItem(container, position, object);
}
});

① 对 boolean isViewFromObject(View view, Object object) 方法的理解

  • 首先明确,ViewPager里面对每个页面的管理是key-value形式的,也就是说每个page都有个对应的id(id是object类型),需要对page操作的时候都是通过id来完成的。

  • Object instantiateItem(ViewGroup container, int position) 方法就是用来往PageView里添加自己需要的page。同时注意它还有个返回值object,这就是那个id。

  • 最后,boolean isViewFromObject(View view, Object object) 就是告诉框架,这个view的id是不是这个object。所以,该方法一般返回 view == object

② 对 Object instantiateItem(@NonNull ViewGroup container, int position) 方法的理解

  • ViewPager会预加载子页面,在预加载时会自动调用这个方法。虽然一共有几个子页面已经知道了,但这些页面并未创建出来。
  • 我们要在这个方法中创建子页面 并且将要展示的内容添加到子页面中

  • 当ViewPage第一次加载时会执行这个函数并且执行两次,第一次执行加载第0页面, 第二次执行加载第1页。

  • 当从第0页滑动到第1页后 会触发这个函数 预加载第2页,当从第1页滑到第2页后 会触发这个函数 预加载第3页,以类此推。同理,反向滑动也是一样的。

③ 对 void destroyItem(@NonNull ViewGroup container, int position, @NonNull Object object) 方法的理解

  • 这个方法用来销毁某个子页面释放资源。

  • 当刚进入 ViewPager 时 这时已经加载了 0号页与1号页,此时用户处于0号页 用户可能还要滑到1号页 此时哪个页面都不会销毁。

  • 当用户从0号页滑到1号页 这时已加载的页面时 0、1、2 页,此时用户即可能滑到 0号页 也可能滑到 2号页 所以也不会触发这个函数。

  • 当用户从 1号页 滑到 2号页 此时已加载的有 0、1、2、3 页,用户处于 2号页 不可能会直接滑到0号页 所以,此时会触发这个函数来销毁 0号页。

④ ViewPager 默认加载两个页面,能不能自己设置加载的页面数量?

设置预加载的页面数量,默认是1。但是就算设置 0,源码里面也会把你重新设为1。这里面说的数量是屏幕之外加载的数量。

1
public void setOffscreenPageLimit(int limit);

⑤ 设置当前显示哪一个页面

1
2
3
public void setCurrentItem(int item);

public void setCurrentItem(int item, boolean smoothScroll)

item:控制显示哪一个页面。

smoothScroll:true 换页速度慢一点,平滑一点。false相反。

2. 简单的使用

① 布局文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
<!--activity_main.xml-->

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">

<androidx.viewpager.widget.ViewPager
android:id="@+id/view_pager"
android:layout_width="match_parent"
android:layout_height="wrap_content"
/>

</LinearLayout>

<!--page_layout.xml-->

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">

<ImageView
android:id="@+id/imageView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
/>

<TextView
android:id="@+id/textView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_alignBottom="@+id/imageView"
android:background="#33000000"
android:gravity="center"
android:paddingTop="5dp"
android:paddingBottom="5dp"
android:textStyle="bold"
android:textColor="#ffffff"
/>

</RelativeLayout>

② 主要的代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
public class MainActivity extends AppCompatActivity {

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);

//找到控件
final ViewPager viewPager = findViewById(R.id.view_pager);

//资源数组
int[] resources = {R.drawable.pic_1,R.drawable.pic_2,R.drawable.pic_3};
String[] texts = {"桥边吹笛","城市风景","漩涡鸣人"};

//视图数组
final List<View> views = new ArrayList<>();

//模拟数据
for (int i = 0; i < resources.length; i++) {
//加载布局
RelativeLayout inflate = (RelativeLayout) LayoutInflater.from(this).inflate(R.layout.page_layout, null);

//找到控件
ImageView imageView = inflate.findViewById(R.id.imageView);
TextView textView = inflate.findViewById(R.id.textView);

//设置数据
imageView.setBackgroundResource(resources[i]);
textView.setText(texts[i]);

//加入
views.add(inflate);
}

//设置适配器 使用匿名类的方式
viewPager.setAdapter(new PagerAdapter() {
@Override
public int getCount() {
return views.size();
}

@Override
public boolean isViewFromObject(@NonNull View view, @NonNull Object object) {
return view == object;
}

@NonNull
@Override
public Object instantiateItem(@NonNull ViewGroup container, int position) {
container.addView(views.get(position));
return views.get(position);
}

@Override
public void destroyItem(@NonNull ViewGroup container, int position, @NonNull Object object) {
container.removeView(views.get(position));
}
});
}
}

③ 运行效果

④ 补充:适配器中的方法还可以类似这样书写

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<!-- pager_layout.xml -->

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content">

<ImageView
android:id="@+id/image_view"
android:layout_width="match_parent"
android:layout_height="wrap_content"
/>

</LinearLayout>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
public class RotateActivity extends AppCompatActivity {

//图片资源
private int[] resources = {R.drawable.pic_1,R.drawable.pic_2,R.drawable.pic_3};

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_rotate);

//找到控件
ViewPager viewPager = findViewById(R.id.pager);

//设置适配器
viewPager.setAdapter(new PagerAdapter() {
@Override
public int getCount() {
return resources.length;
}

@Override
public boolean isViewFromObject(@NonNull View view, @NonNull Object object) {
return object == view;
}

@NonNull
@Override
public Object instantiateItem(@NonNull ViewGroup container, int position) {
//设置布局
View inflate = LayoutInflater.from(getApplicationContext()).inflate(R.layout.pager_layout, null);
ImageView imageView = inflate.findViewById(R.id.image_view);
imageView.setBackgroundResource(resources[position]);

//添加视图
container.addView(inflate);

return inflate;
}

@Override
public void destroyItem(@NonNull ViewGroup container, int position, @NonNull Object object) {
container.removeView((View)object);
}
});

//设置动画方式

}
}

3. 监听滑动的方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
viewPager.addOnPageChangeListener(new ViewPager.OnPageChangeListener() {
//当页面在滑动的时候会调用此方法,在滑动被停止之前,此方法回一直得到调用
//position: 当前页面,即你点击滑动的页面 (从A滑B,则是A页面的position)
//positionOffset: 当前页面偏移的百分比
//positionOffsetPixels: 当前页面偏移的像素位置
@Override
public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {

}

//页面跳转完后得到调用
//position: 是你当前选中的页面 (从A滑动到B,B就是position)
@Override
public void onPageSelected(int position) {

}

//状态改变的时候调用
//state 有三个值
//SCROLL_STATE_DRAGGING(1): 表示用户手指 "按在屏幕上并且开始拖动" 的状态(手指按下但是还没有拖动的时候还不是这个状态,只有按下并且手指开始拖动后才是。)
//SCROLL_STATE_SETTLING(2): "手指离开屏幕" 的状态
//SCROLL_STATE_IDLE(0): "滑动动画做完" 的状态
//一个完整的滑动动作,三种状态的顺序是:1 -> 2 -> 0
@Override
public void onPageScrollStateChanged(int state) {

}
});

我们之前的例子是在加载视图的时候视图的图片和文本已经更新了,我们可以在 onPageSelected 中刷新数据。

三. FragmentPagerAdapter

1. 介绍

我们自己写一个类继承 FragmentPagerAdapter,体会一下其使用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public class MyAdapterFragment extends FragmentPagerAdapter {

/**
* 构造方法
* @param fm
* @param behavior
*/
public MyAdapterFragment(@NonNull FragmentManager fm, int behavior) {
super(fm, behavior);
}

//返回对应位置的 fragment
@NonNull
@Override
public Fragment getItem(int position) {
return null;
}

//获取 fragment 的数量
@Override
public int getCount() {
return 0;
}
}

问题1:数据源在哪里?

我们可以修改构造方法,多传一个参数用来获取数据源。

问题2:FragmentManager是什么东西,怎么获取到?

FragmentActivity 或者 AppCompatActivity 中可以通过 getSupportFragmentManager() 方法 获取到 FragmentManager。

问题3:构造方法的第二个参数 behavior 是什么意思?

behavior 有两个值:

  • BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT
  • BEHAVIOR_SET_USER_VISIBLE_HINT。 已经被弃用了,主要是为了兼容老的代码。

在懒加载中会继续说这个问题,现在只需要记住,传入第一个值就可。

2. 单独使用

① 适配器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
public class MyAdapterFragment extends FragmentPagerAdapter {

//保存传递的fragment集合
private List<Fragment> fragments;

/**
* 构造方法
* @param fm
* @param behavior
*/
public MyAdapterFragment(@NonNull FragmentManager fm, int behavior, List<Fragment> fragments) {
super(fm, behavior);

//获取fragments数据源
this.fragments = fragments;
}

//返回对应位置的 fragment
@NonNull
@Override
public Fragment getItem(int position) {
return fragments.get(position);
}

//获取 fragment 的数量
@Override
public int getCount() {
return fragments.size();
}
}

② MainActivity

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
public class MainActivity extends AppCompatActivity {

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);

//找到控件
ViewPager pager = findViewById(R.id.pager);

//数据源
List<Fragment> fragments = new ArrayList<>();
fragments.add(new MyFragment1());
fragments.add(new MyFragment2());

//设置适配器
pager.setAdapter(
new MyAdapterFragment(
getSupportFragmentManager(),
FragmentPagerAdapter.BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT,
fragments
)
);
}
}

③ Fragment

布局文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">

<TextView
android:id="@+id/textView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerInParent="true"
android:textStyle="bold"
android:textSize="24sp"
/>

</RelativeLayout>

fragment

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
public class MyFragment1 extends Fragment {

@Nullable
@Override
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
//加载布局
View inflate = inflater.inflate(R.layout.fragment_layout, null);
//找到控件
TextView textView = inflate.findViewById(R.id.textView);
//设置文本
textView.setText("界面:小猫");
return inflate;
}
}

public class MyFragment2 extends Fragment {

@Nullable
@Override
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
//加载布局
View inflate = inflater.inflate(R.layout.fragment_layout, null);
//找到控件
TextView textView = inflate.findViewById(R.id.textView);
//设置文本
textView.setText("界面:小狗");
return inflate;
}
}

④ 运行效果

3. 配和 Tablayout

我们只需要简单的修改一下代码

① 适配器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
public class MyAdapterFragment extends FragmentPagerAdapter {

//保存传递的fragment集合
private List<Fragment> fragments;

//保存传递的标题数组
private String[] titles;

/**
* 构造方法
* @param fm
* @param behavior
*/
public MyAdapterFragment(@NonNull FragmentManager fm, int behavior, List<Fragment> fragments,String[] titles) {
super(fm, behavior);

//获取fragments数据源
this.fragments = fragments;

//获取标题的数据源
this.titles = titles;
}

//返回对应位置的 fragment
@NonNull
@Override
public Fragment getItem(int position) {
return fragments.get(position);
}

//获取 fragment 的数量
@Override
public int getCount() {
return fragments.size();
}

//设置标题
@Nullable
@Override
public CharSequence getPageTitle(int position) {
return titles[position];
}
}

② MainActivity

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
public class MainActivity extends AppCompatActivity {

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);

//找到控件
ViewPager pager = findViewById(R.id.pager);
TabLayout tab = findViewById(R.id.tab);

//fragment数据源
List<Fragment> fragments = new ArrayList<>();
fragments.add(new MyFragment1());
fragments.add(new MyFragment2());

//标题数据源
String[] titles = getResources().getStringArray(R.array.title);

//设置适配器
pager.setAdapter(
new MyAdapterFragment(
getSupportFragmentManager(),
FragmentPagerAdapter.BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT,
fragments,
titles
)
);

//绑定tab和fragment
tab.setupWithViewPager(pager);
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity"
android:orientation="vertical">

<com.google.android.material.tabs.TabLayout
android:id="@+id/tab"
android:layout_width="match_parent"
android:layout_height="wrap_content"

app:tabSelectedTextColor="#ff8000"
app:tabIndicatorColor="#ff8000"
android:background="#fbf4f2"
app:tabRippleColor="@null"

app:tabTextAppearance="@style/MyTextStyle"
/>

<androidx.viewpager.widget.ViewPager
android:id="@+id/pager"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
/>

</LinearLayout>

③ fragment (无改动)

布局文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">

<TextView
android:id="@+id/textView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerInParent="true"
android:textStyle="bold"
android:textSize="24sp"
/>

</RelativeLayout>

fragment

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
public class MyFragment1 extends Fragment {

@Nullable
@Override
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
//加载布局
View inflate = inflater.inflate(R.layout.fragment_layout, null);
//找到控件
TextView textView = inflate.findViewById(R.id.textView);
//设置文本
textView.setText("界面:小猫");
return inflate;
}
}

public class MyFragment2 extends Fragment {

@Nullable
@Override
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
//加载布局
View inflate = inflater.inflate(R.layout.fragment_layout, null);
//找到控件
TextView textView = inflate.findViewById(R.id.textView);
//设置文本
textView.setText("界面:小狗");
return inflate;
}
}

④ 运行效果

⑤ 总结

  • 我们只是在刚才的基础上,向适配器传递了标题的数据,通过代理方法 getPageTitles 设置对应位置的标题。

  • 我们是使用 setupWithViewPager 绑定 TabLayout 和 ViewPager的数据。

  • 慎用 setupWithViewPager ,有可能会出现问题。

  • 注意:这种设置TabLayout的Item的方式实际上也是通过 addTab 的方式,可以参考:如何手动修改TabPageIndicator的title的解决办法

四. FragmentStatePagerAdapter

1.FragmentStatePagerAdapter的实现和FragmentPagerAdapter的实现一样。

2.FragmentPagerAdapter适用于页面比较少的情况,FragmentStatePagerAdapter适用于页面比较多的情况。

3.FragmentStatePagerAdapter中fragment实例在destroyItem的时候被真正释放,所以FragmentStatePagerAdapter省内存。FragmentPagerAdapter中的fragment实例在destroyItem的时候并没有真正释放fragment对象只是detach,所以FragmentPagerAdapter消耗更多的内存,带来的好处就是效率更高一些。

4.关于 FragmentPagerAdapter 和 FragmentStatePagerAdapter 的区别参考:

FragmentPagerAdapter和FragmentStatePagerAdapter区别

五. ViewPager翻页动画

参考:ViewPager的翻页动画

六. ViewPager无限循环

参考:ViewPager的无限循环

七. ViewPager的预加载和懒加载

参考:ViewPager的预加载和懒加载

参考文章

【Android进阶】关于PagerAdapter的使用方法的总结

ViewPager适配器PagerAdapter重写方法解释

安卓PagerAdapter中的isViewFromObject()方法有什么用?

Viewpager OnPageChangeListener 滑动事件讲解

getSupportFragmentManager()方法不可用解决办法