1 认识一下ViewPager?
ViewPager最早出自4.0版本,那么低版本如何能使用ViewPager呢?为了兼容低版本安卓设备,谷歌官方给我们提供了一个的软件包android.support.v4.view。这个V4包囊了只有在安卓3.0以上可以使用的api,而viewpager就是其中之一。利用它,我们可以做很多事情,从最简单的引导页导航,到轮转广告,到页面菜单等等,无不出现ViewPager的身影。应用广泛,简单好用,更好的交互性,这也是ViewPager一出现便大受程序员欢迎的原因。如此好用的控件,你是不是已经蠢蠢欲动了呢?不废话,我们将以项目为向导,由浅入深的讲解ViewPager,开始ViewPager的学习之旅吧。
2 什么时候可以使用ViewPager?
任何新的技术,最难的不是学习如何使用它,而是明白什么时候使用它最合适。正所谓物尽其用,只有正确的技术用在了正确的地方,那么才能发挥该技术最大的功效,做出好的应用。下面结合一些典型场景来让不了解ViewPager的你了解在什么情况下使用ViewPager才是最好的。ViewPager最典型的应用场景主要包括引导页导航,轮转广告,和页面菜单。可以这么说,但凡遇到界面切换的需求,都可以考虑ViewPager。抛砖引玉,剩下的就看读者发挥想象力了。
3 ViewPager的基本入门(和ListView对比学习)
那如何使用它呢,与ListView类似,我们也需要一个适配器,他就是PagerAdapter。ViewPager采用MVC模式将前段显示与后端数据进行分离,也就是说器装载数据并不是直接添加数据,而是,需要使用PagerAdapter。PagerAdapter相当于,MVC模式中的C(Controller,控制器),ViewPager相当MVC模式中的V(View,视图),为ViewPager提供的数据List,数组或者数据库,就相当于MVC中的M(Mode,模型)。
学习ViewPager不仅仅是学习ViewPager单一个控件那么简单,我们需要围绕MVC模式,把ViewPager用到的数据(M),视图(V),控制器(C)都理一遍,明白如何把他们,组合在一起,达到ViewPager的切换效果。
我们通过一个简单的项目来认识一下ViewPager的使用方式。
首先新建项目,引入ViewPager控件
ViewPager,它是google SDk中自带的一个附加包的一个类,可以用来实现屏幕间的切换,在V4包中。
三步曲:
3.1 准备视图 View
在主布局文件main.xml中添加ViewPager如下:
activity_main.xml
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:layout_width="fill_parent" android:layout_height="fill_parent" tools:context="com.example.testviewpage_1.MainActivity" > <android.support.v4.view.ViewPager android:id="@+id/viewpager" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_gravity="center" /> </RelativeLayout>
其中,其中 <android.support.v4.view.ViewPager /> 是ViewPager对应的组件,要将其放到想要滑动的位置,可以全屏显示,也可以半屏,任意大小,由程序员按需求控制。
3.2 准备数据模型 ,Mode
① 新建三个layout,用于滑动切换的视图:
我们的三个视图都非常简单,里面没有任何的控件,大家当然可以往里添加各种控件,但这里是个DEMO,只详解原理即可,所以我这里仅仅用背景来区别不用layout布局。
layout1.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="match_parent" android:background="#ffffff" android:orientation="vertical" > </LinearLayout>
layout2.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="match_parent" android:background="#ffff00" android:orientation="vertical" > </LinearLayout>
layout3.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="match_parent" android:background="#ff00ff" android:orientation="vertical" > </LinearLayout>
② 声明变量:
private View view1, view2, view3;
private List<View> viewList;//view数组
private ViewPager viewPager; //对应的viewPager
我们来看看上面的变量声明:
首先viewPager对应 <android.support.v4.view.ViewPager/>控件。
view1, view2, view3对应我们的三个layout,即layout1.xml,layout2.xml,layout3.xml
viewList是一个View数组,盛装上面的三个VIEW
③ 数据的初始化:
viewPager = (ViewPager) findViewById(R.id.viewpager); LayoutInflater inflater=getLayoutInflater(); view1 = inflater.inflate(R.layout.layout1, null); view2 = inflater.inflate(R.layout.layout2,null); view3 = inflater.inflate(R.layout.layout3, null); viewList = new ArrayList<View>();// 将要分页显示的View装入数组中 viewList.add(view1); viewList.add(view2); viewList.add(view3);
获取到找到ViewPager ,赋值给变量,最后将实例化的view1,view2,view3添加到viewList中。
3.3 准备控制器(Controller)—— PagerAdapter
PagerAdapter 是ViewPager的适配器。
适配器我们在ListView里面早就使用过,listView通过重写GetView()函数来获取当前要加载的Item。而PageAdapter不太相同,毕竟PageAdapter是单个VIew的合集。PagerAdapter在instantiateItem()里面给布局容器添加了将要显示的视图。
PageAdapter 必须重写的四个函数:
boolean isViewFromObject(View arg0, Object arg1)
int getCount()
void destroyItem(ViewGroup container, int position,Object object)
Object instantiateItem(ViewGroup container, int position)
下面,我们就看看四个主要方法改如何重写,都分别做了什么吧
@Override public int getCount() { // TODO Auto-generated method stub return viewList.size(); } getCount(),返回滑动的View的个数。 @Override public void destroyItem(ViewGroup container, int position, Object object) { // TODO Auto-generated method stub container.removeView(viewList.get(position)); } destroyItem,从容器中删除指定position的View @Override public Object instantiateItem(ViewGroup container, int position) { // TODO Auto-generated method stub container.addView(viewList.get(position)); return viewList.get(position); } };
instantiateItem()方法中,我先讲指定position位置的View添加到容器中,末了,将本View返回。
@Override public boolean isViewFromObject(View arg0, Object arg1) { // TODO Auto-generated method stub return arg0 == arg1; }
这里为什么这么写暂不做讲解,知道这样写即可,后面我们会单独讲解清楚。
这么简单,我们就实现了三个view间的相互滑动。
第一个界面想第二个界面滑动 第二个界面想第三个界面滑动
以下是全部核心代码:
package com.example.testviewpage_1; import java.util.ArrayList; import java.util.List; import java.util.zip.Inflater; import android.app.Activity; import android.os.Bundle; import android.support.v4.view.PagerAdapter; import android.support.v4.view.ViewPager; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; public class MainActivity extends Activity { private View view1, view2, view3; private ViewPager viewPager; //对应的viewPager private List<View> viewList;//view数组 @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); viewPager = (ViewPager) findViewById(R.id.viewpager); LayoutInflater inflater=getLayoutInflater(); view1 = inflater.inflate(R.layout.layout1, null); view2 = inflater.inflate(R.layout.layout2,null); view3 = inflater.inflate(R.layout.layout3, null); viewList = new ArrayList<View>();// 将要分页显示的View装入数组中 viewList.add(view1); viewList.add(view2); viewList.add(view3); PagerAdapter pagerAdapter = new PagerAdapter() { @Override public boolean isViewFromObject(View arg0, Object arg1) { // TODO Auto-generated method stub return arg0 == arg1; } @Override public int getCount() { // TODO Auto-generated method stub return viewList.size(); } @Override public void destroyItem(ViewGroup container, int position, Object object) { // TODO Auto-generated method stub container.removeView(viewList.get(position)); } @Override public Object instantiateItem(ViewGroup container, int position) { // TODO Auto-generated method stub container.addView(viewList.get(position)); return viewList.get(position); } }; viewPager.setAdapter(pagerAdapter); } }
至此我们已经基本了解了ViewPager,学会了基本用法,接下来,我们就来详细学习ViewPager的核心PagerAdapter。
4 从PagerAdapter说开去——解读PagerAdapter的四大函数
4.1 且看官方文档怎么说?
最权威的讲解是官方文档,都是英文的,不好排版,我就不贴出来了,以下是我根据文档翻译出来的。有不明白的,可以自己看官方文档:
http://developer.android.com/reference/android/support/v4/view/PagerAdapter.html(加红)
安卓提供一个适配器用于填充ViewPager页面. 你很可能想要使用一个更加具体的实现, 例如:
FragmentPagerAdapter or FragmentStatePagerAdapter.
当你实现一个PagerAdapter时,至少需要覆盖以下几个方法:
instantiateItem(ViewGroup, int)
destroyItem(ViewGroup, int, Object)
isViewFromObject(View, Object)
PagerAdapter比AdapterView的使用更加普通.ViewPager使用回调函数来表示一个更新的步骤,而不是使用一个视图回收机制。在需要的时候pageradapter也可以实现视图的回收或者使用一种更为巧妙的方法来管理视图,比如采用可以管理自身视图的fragment。
① viewpager不直接处理每一个视图而是将各个视图与一个键联系起来。这个键用来跟踪且唯一代表一个页面,不仅如此,该键还独立于这个页面所在adapter的位置。当pageradapter将要改变的时候他会调用startUpdate函数, 接下来会调用一次或多次的instantiateItem或者destroyItem。最后在更新的后期会调用finishUpdate。当finishUpdate返回时 instantiateItem返回的对象应该添加到父ViewGroup destroyItem返回的对象应该被ViewGroup删除。methodisViewFromObject(View, Object)代表了当前的页面是否与给定的键相关联。
② 对于非常简单的pageradapter或许你可以选择用page本身作为键,在创建并且添加到viewgroup后instantiateItem方法里返回该page本身即可
destroyItem将会将该page从viewgroup里面移除。isViewFromObject方法里面直接可以返回view == object。
pageradapter支持数据集合的改变,数据集合的改变必须要在主线程里面执行,然后还要调用notifyDataSetChanged方法。和baseadapter非常相似。数据集合的改变包括页面的添加删除和修改位置。viewpager要维持当前页面是活动的,所以你必须提供getItemPosition方法。
上面的FragmentPagerAdapter 和FragmentStatePagerAdapter非常常用,我们放到后面来讲。
上面的话,重点只有两段:① ,②
针对上面两段,集中理解两点:
(1)第一段说明了,键(Key)的概念,首先这里要清楚的一点是,每个滑动页面都对应一个Key,而且这个Key值是用来唯一追踪这个页面的,也就是说每个滑动页面都与一个唯一的Key一一对应。大家先有这个概念就好,关于这个Key是怎么来的,下面再讲。
(2)当前page本身可以作为键,直接在destroyItem()返回,用来标示自己。下面,我们来讲讲Key
4.2 ViewPager的 key
① destroyItem(ViewGroup, int, Object)
该方法把给定位置的界面丛容器中移除,负责从容器中删除视图,确保在finishUpdate(viewGroup)返回时视图能够被移除。
来看看我们前面的项目是怎么重写这个方法的:
@Override public void destroyItem(ViewGroup container, int position, Object object) { // TODO Auto-generated method stub container.removeView(viewList.get(position)); }
将给定位置的视图从container中移除了…… 这个方法必须被实现,而且不能调用父类,否则抛出异常。(说该方法没有被覆盖)
返回当前有效视图的个数。
@Override public int getCount() { // TODO Auto-generated method stub return viewList.size(); }
返回了当前需要显示的视图的个数。
接下来的两个方法是重点。
③ instantiateItem(ViewGroup, int)
这个方法实现的功能是创建指定位置的视图,同时肩负着添加该创建的视图到指定容器container中,而这一步,要确保在finishUpdate(viewGroup)返回之后完成。
该方法返回一个代表该视图的键(key),没必要非是视图本身,也可以是这个页面的其他容器,我的理解是没必要视图本身,只要这个返回值能代表当前视图,并与视图一意对应即可,比如返回和该视图对应的position可以吗?(接下来我们做个例子试试)
总结:
给container添加一个视图。
返回代表该视图的Key
该方法和destroyItem(ViewGroup, int, Object)一样,在finishUpdate(ViewGroup)这句话执行完之后执行。
我们来看看我们是怎么做的:
@Override public Object instantiateItem(ViewGroup container, int position) { // TODO Auto-generated method stub container.addView(viewList.get(position)); return viewList.get(position); } };
没有错,这里我们给container添加了一个View viewList.get(position),,并将该视图作为key返回了。
回过头来,我们看看第四章的官方文档翻译:
② 对于非常简单的pageradapter或许你可以选择用page本身作为键,在创建并且添加到viewgroup后instantiateItem方法里返回该page本身即可
destroyItem将会将该page从viewgroup里面移除。isViewFromObject方法里面直接可以返回view == object。
就是这里,把当前的View作为key传出去,那么这个key在哪里被使用呢?就得来看看下面的方法了。
④ isViewFromObject(View, Object)
功能:该函数用来判断instantiateItem(ViewGroup, int)函数所返回来的Key与一个页面视图是否是代表的同一个视图(即它俩是否是对应的,对应的表示同一个View)
返回值:如果对应的是同一个View,返回True,否则返回False。
在上面的项目中,我们这样做的:
@Override public boolean isViewFromObject(View arg0, Object arg1) { // TODO Auto-generated method stub return arg0 == arg1; }
由于在instantiateItem()中,我们作为Key返回来的是当前的View,所以在这里判断时,我们直接将Key与View看是否相等来判断是否是同一个View。
发散思维:如果我们在instantiateItem()返回的是代表当前视图的position而非本身呢?这里该怎么做?接下来我们就解答你的疑问。
4.3 自定义key
上面我们想必对key有个初步认识,下面我们举个例子来说明一下key和View的关系,由于key要和View一一对应,这里我把和View一一对应的position作为key返回,然后在上面的项目的基础上修改。这里只展示需要修改的代码。
我们更改了两个地方:
(1)instantiateItem()
@Override public Object instantiateItem(ViewGroup container, int position) { // TODO Auto-generated method stub container.addView(viewList.get(position)); return position ; } };
(2)2、isViewFromObject ()
@Override public boolean isViewFromObject(View arg0, Object arg1) { // TODO Auto-generated method stub //根据传来的key(arg1),找到view,判断与传来的参数View arg0是不是同一个视图 return arg0 == viewList.get((int)Integer.parseInt(arg1.toString())); }
判断instantiateItem()返回的key与视图是否对应,这里我们返回的是position,我们需要根据position找到对应的View,与传过来的View对比,看看是否对应。注意:这里,我们要先将obect对应转换为int类型:(int)Integer.parseInt(arg1.toString());然后再根据position找到对应的View;
5 ViewPager的进阶,添加标题栏
5.1 PagerTitleStrip
View可以添加标题栏,用来指示当前滑动到哪一页。先来一张效果图:
PagerTabStrip是ViewPager的一个关于当前页面、上一个页面和下一个页面的一个非交互的指示器。它经常作为ViewPager控件的一个子控件被被添加在XML布局文件中。在你的布局文件中,将它作为子控件添加在ViewPager中。而且要将它的 android:layout_gravity 属性设置为TOP或BOTTOM来将它显示在ViewPager的顶部或底部。每个页面的标题是通过适配器的getPageTitle(int)函数提供给ViewPager的。
主要是两点:
① PagerTabStrip可以作为控件直接添加到xml布局文件中。
② 重写getPageTitle(int)来给PagerTabStrip提供标题。
你也许会发现上面只有上部分一部分的地方才有滑动切换,是因为我更改了布局文件。
(1) 先来看看布局文件:
<RelativeLayout 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" tools:context="com.example.testviewpage_2.MainActivity" > <android.support.v4.view.ViewPager android:id="@+id/viewpager" android:layout_width="wrap_content" android:layout_height="200dip" android:layout_gravity="center"> <android.support.v4.view.PagerTitleStrip android:id="@+id/pagertitle" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_gravity="top" /> </android.support.v4.view.ViewPager> </RelativeLayout>
这里将layout_height更改为200dip,只所以这么做,是为了告诉大家,只要在想要实现滑动切换的地方添加上<android.support.v4.view.ViewPager />就可以实现切换,无所谓位置和大小,跟普通控件一样!!!!!!
重点是我们将PagerTabStrip作为子控件直接镶嵌在ViewPager中,设置layout_gravity="top" 或者 buttom。
(2) 重写适配器的getPageTitle()函数
在元项目基础上我们做了如下更改:
1、定义变量:
private List<String> titleList; //标题列表数组
申请了一个标题数组,来存储三个页面所对应的标题、
2、初始化
titleList = new ArrayList<String>();// 每个页面的Title数据
titleList.add("王鹏");
titleList.add("姜语");
titleList.add("结婚");
添加了标题数据
3、重写CharSequence getPageTitle(int )函数
@Override public CharSequence getPageTitle(int position) { // TODO Auto-generated method stub return titleList.get(position); }
根据位置返回当前所对应的标题。
5.2 PagerTabStrip
PagerTabStrip使用方法和上面类似。
先来看看效果:
效果和PagerTitleStrip差不多,但是有微小差别:
PagerTabStrip在当前页面下,标题的下方有一个横线作为导航。
PagerTabStrip的Tab是可以点击的,点击标题可以跳转到对应的页面。
PagerTabStrip是ViewPager的一个关于当前页面、上一个页面和下一个页面的一个可交互的指示器。它经常作为ViewPager控件的一个子控件被被添加在XML布局文件中。在你的布局文件中,将它作为子控件添加在ViewPager中。而且要将它的 android:layout_gravity 属性设置为TOP或BOTTOM来将它显示在ViewPager的顶部或底部。每个页面的标题是通过适配器的getPageTitle(int)函数提供给ViewPager的。
注意:可交互的,这就是PagerTabStrip和PagerTitleStrip最大的不一样。PagerTabStrip是可交互的,PagerTitleStrip是不可交互的。
用法与PagerTitleStrip完全相同,即:
1、首先,文中提到:在你的布局文件中,将它作为子控件添加在ViewPager中。
2、第二,标题的获取,是重写适配器的getPageTitle(int)函数来获取的。
看看实例:
1、XML布局
<RelativeLayout 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" tools:context="com.example.testviewpage_2.MainActivity" > <android.support.v4.view.ViewPager android:id="@+id/viewpager" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_gravity="center"> <android.support.v4.view.PagerTabStrip android:id="@+id/pagertab" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_gravity="top"/> </android.support.v4.view.ViewPager> </RelativeLayout>
可以看到,同样,是将PagerTabStrip作为ViewPager的一个子控件直接插入其中,当然android:layout_gravity=""的值一样要设置为top或bottom。
2、重写适配器的getPageTitle()函数
代码里面不用改
@Override public CharSequence getPageTitle(int position) { // TODO Auto-generated method stub return titleList.get(position); }
根据位置返回当前所对应的标题。
6 Fragment 和 ViewPager的完美结合—— FragmentPagerAdapter
前面讲解了ViewPager的普通实现方法,但android官方最推荐的一种实现方法却是使用fragment,Fragment的碎片化功能大大的丰富了ViewPager的功能和表现形式。先前我们实现ViewPager使用的是ViewPagerAdapter。而对于fragment,使用的是FragmentPagerAdapter和FragmentStatePagerAdapter。下面我们来学习一下。
6.1 FragmentPagerAdapter
FragmentPagerAdapter是PagerAdapter的子类,专门用来呈现fragment页面的,这些Fragment会一直保存在FragmentManager,以方便用户随时取用。
FragmentPagerAdapter适用于有限个fragment的页面管理,因为你所访问过的fragment都会保存在内存中。由于,fragment保存着大量的各种状态,这样就造成了比较大的内存开销。故,当遇到大量的页面切换的时候,建议采用FragmentStatePagerAdapter,这个我们会在下面的章节讲到。
FragmentPagerAdapter使用过程:
6.1.1 适配器的实现:
public class FragAdapter extends FragmentPagerAdapter { private List<Fragment> mFragments; public FragAdapter(FragmentManager fm,List<Fragment> fragments) { super(fm); // TODO Auto-generated constructor stub mFragments=fragments; } @Override public Fragment getItem(int arg0) { // TODO Auto-generated method stub return mFragments.get(arg0); } @Override public int getCount() { // TODO Auto-generated method stub return mFragments.size(); } }
很简单吧,只需要继承FragmentPagerAdapter实现两个方法getItem(int arg)和 getCount(),就可以了。
这里,我们定义了一个fragment的List对象,在构造方法里面初始化了。如下
public FragAdapter(FragmentManager fm,List<Fragment> fragments) { super(fm); // TODO Auto-generated constructor stub mFragments=fragments; }
接下来我们实现了getCount(),和前面一样返回了页面的个数。这里我们返回了List对象的大小。List就是fragment的集合,有多少个fragment就展示多少个页面,这点很容易理解。如下:
@Override public int getCount() { // TODO Auto-generated method stub return mFragments.size(); }
最后,根据传过来的键Key参数,返回该当前要显示的fragment,如下:
@Override public Fragment getItem(int arg0) { // TODO Auto-generated method stub return mFragments.get(arg0); }
6.1.2 构造Fragment类。
下面我们要分别构造3个Fragment,这里,我们第一个fragment1有一个可以点击的按钮,第二个和第三个fragment2,fragment3分别用不同的背景代替。
第一个Fragment类:
XML:(layout1.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="match_parent" android:background="#ffffff" android:orientation="vertical" > <Button android:id="@+id/fragment1_btn" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="show toast" /> </LinearLayout>
Fragment1的java代码:
public class Fragment1 extends Fragment { @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { // TODO Auto-generated method stub View view= inflater.inflate(R.layout.layout1, container, false); //对View中控件的操作方法 Button btn = (Button)view.findViewById(R.id.fragment1_btn); btn.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { // TODO Auto-generated method stub Toast.makeText(getActivity(), "点击了第一个fragment的BTN", Toast.LENGTH_SHORT).show(); } }); return view; } }
这里我加入了一个按钮,在onCreateView()方法里面加载了layout1,返回了要显示的View,同时,给按钮添加了一个监听事件,这里为了向读者说明利用了fragment我们可以实现各种各样的交互,ViewPager能做到的不仅仅是动态的图片,而是动态的交互。
第二个Fragment类:
XML代码:(layout2.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="match_parent" android:background="#ffff00" android:orientation="vertical" > </LinearLayout>
java代码:
public class Fragment2 extends Fragment { @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { // TODO Auto-generated method stub View view=inflater.inflate(R.layout.layout2, container, false); return view; } }
第三个Fragment类:
XML代码:(layout3.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="match_parent" android:background="#ff00ff" android:orientation="vertical" > </LinearLayout>
Java代码
public class Fragment3 extends Fragment { @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { // TODO Auto-generated method stub View view=inflater.inflate(R.layout.layout3, container, false); return view; } }
6.1.3 主Activity我继承了FragmentActivity,只有FragmentActivity内部才能内嵌Fragment普通Activity是不行的。
public class MainActivity extends FragmentActivity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); //构造适配器 List<Fragment> fragments=new ArrayList<Fragment>(); fragments.add(new Fragment1()); fragments.add(new Fragment2()); fragments.add(new Fragment3()); FragAdapter adapter = new FragAdapter(getSupportFragmentManager(), fragments); //设定适配器 ViewPager vp = (ViewPager)findViewById(R.id.viewpager); vp.setAdapter(adapter); } }
很简单,我们构造了一个适配器,然后为ViewPager设置的适配器,和前面几乎一样。而适配器里面传入的,就是那三个我们准备好的fragment。
看看效果:
在第一个页面加一个按钮 第一页面向第二页面滑动
第二页面向第三个页面滑动
6.2 FragmentStatePagerAdapter
和FragmentPagerAdapter相比,它更适用于大量页面的展示,当整个fragment不再被访问,则会被销毁(由于预加载,默认最多保存3个fragment),只保存其状态,这相对于FragmentPagerAdapter占有了更少的内存,为什么大量页面用FragmentStatePagerAdapter?不言而喻了吧。
FragmentStatePagerAdapter的用法和FragmentPagerAdapter一样,这里就不再赘述。
注意:
在初次使用的FragmentPagerAdapter的时候,曾爆出类型转换的异常。这是为什么呢?跟踪代码才发现出错的地方发生在fragments.add(new Fragment1()); 错误提示无法将Fragment1(Fragment的子类)强制转换成Fragment,当时真是莫名其妙,明明Fragment1就是Fragment,为什么说不是呢?经过仔细排查才发现在Fragment1里面导入的是android.app.Fragment,而在Activity类导入的是为android.support.v4.app.Fragment。统一导入之后才消除异常,不细心造成的错误往往难以排查,让人纠结,这里我们在使用FragmentPagerAdapter必须注意导入正确的包android.support.v4.app.Fragment。
7 ViewPager的预加载机制。
ViewPager能够如此流畅的切换页面得益于其预加载的机制,那么什么是ViewPager的预加载呢?
归纳掌握两点:
① ViewPager会预先加载左右两边的图片,预加载的个数最多3个。前方超出当个数由4个的时候,最前方的会被销毁。预加载和销毁分别回调以下两个方法:
instantiateItem(ViewGroup, int)
destroyItem(ViewGroup, int, Object)
② 限制:当左边图片的position小于0的时候,不会预加载;
当右边的图片的position大于或者等于item总数的时候,也不会预加载。
我画了一张示意图:如下左边0位置的被销毁
8 学以致用,用ViewPager做个选项卡。
至此,我们已经基本学完了ViewPager的常用特性。学贵于致用,接下来我们通过一个涵盖面全的例子,来把我们所学的知识用一遍。
做一个选项卡效果,我们立刻想到ViewPagerIndicator,利用我们以上学过的知识,就可以轻易实现这个功能。
先来一张效果图,激发激发热血吧:
上图 左右滑动,或者点击文字,界面会切换,同时,页卡文字下方的滑块也会滑动,指示当前显示的页面。
8.1 准备布局
回忆一下,我们在使用ViewPagerIndicator的时候,会在ViewPager的上面添加ViewPagerIndicator,然后通过ViewPagerIndicator的setViewPager(ViewPager mPager)设置ViewPager,使得ViewPagerIndicator指示器与ViewPager相关联。
这里,我们用一个包含几个 TextView的LinearLayout,下边一个ImageView 替换,如下:
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="fill_parent" android:layout_height="fill_parent" android:orientation="vertical" > <LinearLayout android:id="@+id/linearLayout1" android:layout_width="fill_parent" android:layout_height="60dip" android:background="#FFFFFF" > <TextView android:id="@+id/text1" android:layout_width="fill_parent" android:layout_height="fill_parent" android:layout_weight="1.0" android:gravity="center" android:text="页卡1" android:textColor="#000000" android:textSize="22.0dip" /> <TextView android:id="@+id/text2" android:layout_width="fill_parent" android:layout_height="fill_parent" android:layout_weight="1.0" android:gravity="center" android:text="页卡2" android:textColor="#000000" android:textSize="22.0dip" /> <TextView android:id="@+id/text3" android:layout_width="fill_parent" android:layout_height="fill_parent" android:layout_weight="1.0" android:gravity="center" android:text="页卡3" android:textColor="#000000" android:textSize="22.0dip" /> </LinearLayout> <ImageView android:id="@+id/cursor" android:layout_width="fill_parent" android:layout_height="wrap_content" android:scaleType="matrix" android:src="@drawable/a" /> <android.support.v4.view.ViewPager android:id="@+id/vPager" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_gravity="center" android:layout_weight="1.0" android:background="#000000" android:flipInterval="30" android:persistentDrawingCache="animation" /> </LinearLayout>
下面是ViewPager。
接下来准备3个切换的布局:(3个布局都是一个RelativeLayout,只是,背景颜色不同而已)
fragment_main_1.xml,fragment_main_2.xml,fragment_main_3.xml
fragment_main_1.xml:
<RelativeLayout 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:background="#000000" > </RelativeLayout>
fragment_main_2.xml:
<RelativeLayout 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:background="#ffffff" > </RelativeLayout>
fragment_main_3.xml:
<RelativeLayout 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:background="#ff0000" > </RelativeLayout>
下面由浅入深,一步步把功能做完:
8.2 先完成ViewPager界面切换,和3 基本入门那一章节一致。
① 初始化 ViewPager布局
private void initViewPager() { vPager = (ViewPager) findViewById(R.id.vPager); List<View> listViews = new ArrayList<View>(); listViews.add(View.inflate(this, R.layout.fragment_main_1, null)); listViews.add(View.inflate(this, R.layout.fragment_main_2, null)); listViews.add(View.inflate(this, R.layout.fragment_main_3, null)); MyPagerAdapter adapter = new MyPagerAdapter(listViews); vPager.setAdapter(adapter); // 给ViewPager设置监听 MyOnPagerChangeListener listener = new MyOnPagerChangeListener(); vPager.setOnPageChangeListener(listener); }
这一部分相信大家很熟悉了,无非做了2步:
(1)给ViewPager设置适配器。(加载了3个我们已经准备好的布局)
(2)给ViewPager设置滑动页面监听。
在此就不再多讲,下面是适配器的实现:
class MyPagerAdapter extends PagerAdapter{ List<View> listViews; public MyPagerAdapter(List<View> listViews) { super(); this.listViews = listViews; } @Override public int getCount() { // TODO Auto-generated method stub return listViews.size(); } @Override public boolean isViewFromObject(View arg0, Object arg1) { // TODO Auto-generated method stub return arg0 == arg1; } @Override public Object instantiateItem(View container, int position) { // TODO Auto-generated method stub ((ViewPager)container).addView(listViews.get(position)); return listViews.get(position); } @Override public void destroyItem(View container, int position, Object object) { ((ViewPager)container).removeView(listViews.get(position)); } }
至此,我们已经可以切换界面了。可是,我们发现选项卡下面的指示滑动条并不能随着页面的切换而移动,从而标识当前页面。这就是我们下一步要做的。
8.3 这里,我们完成指示滑动条的移动。
思路:
通过对ViewPager页面切换的监听,用唯一动画相应的距离实现标识滑块的移动。
① 滑块相关数据初始化
这一段是很重要的,先贴出核心代码,随后详细讲解。
private void initImageView() { cursor = (ImageView) findViewById(R.id.cursor); bmpw = BitmapFactory.decodeResource(getResources(), R.drawable.a).getWidth(); //滑块的宽度 DisplayMetrics dm = new DisplayMetrics(); getWindowManager().getDefaultDisplay().getMetrics(dm); //给DisplayMetrics赋值 screenW = dm.widthPixels; offset = (screenW/3 - bmpw)/2; //滑块动画初始位置 //设置动画初始位置 Matrix matrix = new Matrix(); matrix.postTranslate(offset, 0); cursor.setImageMatrix(matrix); }
通过上面的代码主要做了这几个事儿:
(1)
cursor = (ImageView) findViewById(R.id.cursor);
//滑块的宽度
bmpw = BitmapFactory.decodeResource(getResources(), R.drawable.a).getWidth();
加载了滑块,获得了滑块的宽度。
(2)获得了屏幕的宽度
DisplayMetrics dm = new DisplayMetrics();
getWindowManager().getDefaultDisplay().getMetrics(dm); //给DisplayMetrics赋值
screenW = dm.widthPixels; //获得屏幕宽度
上面的代码通过一个类WindowManager获得屏幕的相关信息,保存在 DisplayMetrics 对象里面,然后获取其屏幕宽度。
(3)计算滑块的初始位置
offset = (screenW/3 - bmpw)/2; //滑块动画初始位置
滑块的初始位置的计算,请看如下示意图
(4)
//设置动画初始位置
Matrix matrix = new Matrix();
matrix.postTranslate(offset, 0);
cursor.setImageMatrix(matrix);
这段代码做的事情也很简单,给滑块设置了初始位置,即当选项页面为第0页的时候滑块的位置。这里是通过Matrix 对象来设置滑块的位置信息。
② 实现页面监听类的方法
class MyOnPagerChangeListener implements OnPageChangeListener{ int one = offset * 2 + bmpw; //选项卡0 -> 1 的偏移量 int two = one * 2; // //选项卡1 -> 2 的偏移量 @Override public void onPageScrollStateChanged(int arg0) { // TODO Auto-generated method stub } @Override public void onPageScrolled(int arg0, float arg1, int arg2) { // TODO Auto-generated method stub } @Override public void onPageSelected(int position) { Animation animation = null; Toast.makeText(MainActivity.this, "position:"+position, Toast.LENGTH_SHORT).show(); Log.i("hql-->", "one=="+one+" two=="+two); Log.i("hql-->", "offset=="+offset+" bmpw=="+bmpw+"screenW"+screenW); switch (position) { case 0: if(currentIndex == 1){ animation = new TranslateAnimation(one, 0, 0, 0); }else if(currentIndex == 2){ animation = new TranslateAnimation(two, 0, 0, 0); } break; case 1: if(currentIndex == 0){ animation = new TranslateAnimation(offset, one, 0, 0); }else if(currentIndex == 2){ animation = new TranslateAnimation(two, one, 0, 0); } break; case 2: if(currentIndex == 0){ animation = new TranslateAnimation(offset, two, 0, 0); }else if(currentIndex == 1){ animation = new TranslateAnimation(one, two, 0, 0); } break; } currentIndex = position; //记录当前的页面号 animation.setDuration(300); //设置动画时间 animation.setFillAfter(true); //设置停留在动画后 cursor.startAnimation(animation); } }
上面主要做了两件事:
① 计算由第0页到第1页,滑块移动的距离。
int one = offset * 2 + bmpw; //选项卡0 -> 1 的偏移量
int two = one * 2; // //选项卡1 -> 2 的偏移量
计算方法无非就是数学题,我画了张示意图。
② 实现onPageSelected(int Position)方法
@Override public void onPageSelected(int position) { Animation animation = null; Toast.makeText(MainActivity.this, "position:"+position, Toast.LENGTH_SHORT).show(); Log.i("hql-->", "one=="+one+" two=="+two); Log.i("hql-->", "offset=="+offset+" bmpw=="+bmpw+"screenW"+screenW); switch (position) { case 0: if(currentIndex == 1){ animation = new TranslateAnimation(one, 0, 0, 0); }else if(currentIndex == 2){ animation = new TranslateAnimation(two, 0, 0, 0); } break; case 1: if(currentIndex == 0){ animation = new TranslateAnimation(offset, one, 0, 0); }else if(currentIndex == 2){ animation = new TranslateAnimation(two, one, 0, 0); } break; case 2: if(currentIndex == 0){ animation = new TranslateAnimation(offset, two, 0, 0); }else if(currentIndex == 1){ animation = new TranslateAnimation(one, two, 0, 0); } break; } currentIndex = position; //记录当前的页面号 animation.setDuration(300); //设置动画时间 animation.setFillAfter(true); //设置停留在动画后 cursor.startAnimation(animation); }
通过该方法,我们可以很清晰的看到,这个方法里根据传入的代表当前页面的键,这里是Position,来在滑动的时候使滑块做相应的移动。
做完这一步,我们的滑块已经可以随着页面切换而移动起来了。
8.4 为了更好的交互,完成点击选项卡切换页面。
思路:给3个选项卡(这里是3个TextView)设置点击事件,在点击事件里面通过ViewPager.setCurrentItem(int num)设置当前页面的键(KEY).
private void initTextView() { TextView text1 = (TextView) findViewById(R.id.text1); TextView text2 = (TextView) findViewById(R.id.text2); TextView text3 = (TextView) findViewById(R.id.text3); text1.setOnClickListener(this); text2.setOnClickListener(this); text3.setOnClickListener(this); }
初始化选项卡文字,这些文字做成控件的时候可以设置,同时给他们设置监听。
处理点击事件:
@Override public void onClick(View v) { switch (v.getId()) { case R.id.text1: vPager.setCurrentItem(0); break; case R.id.text2: vPager.setCurrentItem(1); break; case R.id.text3: vPager.setCurrentItem(2); break; }
代码很简单,至此,点击选项卡也可以实现页面切换,实现了双向互动。
这里我再把变量申明和OnCreate()方法里面的调用代码贴出。
private int offset; //滑块动画初始位置 private int bmpw; //滑块的宽度 private int currentIndex = 0; //默认当前也卡号为0 private ImageView cursor; //滑块 private int screenW; //屏幕宽度 private ViewPager vPager; //ViewPager @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); initImageView(); //初始化滑块 initTextView(); //初始化文字 initViewPager(); //初始化ViewPager布局 }
好了,一个双向互动的选项卡就完成了,怎么样,是不是很简单?其实ViewPagerIndicator实现的思路和我们做的选项卡差不多,亲爱读者们,花点时间把这个Demo封装一下,提供一些方便的设置方法,比如设置任意长度的title,就是一个精简的选显卡控件了。
由此我们可以逆推,当然我们要掌握一个开源的控件时并不难,都是由使用到熟悉。通常都是在布局里面引用该控件,然后在java代码通过findViewById()方法找到该空间,之后通过该控件的一些方法,设置相应参数即可使用。当然,熟悉的基础上,下一步,就是深入了解,毕竟市面上的开源控件再怎么样都是人写的,那就很可能不是你想当然那样,所以通过一些方法去了解开源控件很重要。我了解一个控件的特性一般先阅读说明,然后通过调试,打log日志和假设验证的方式,来了解一个控件,这些方式都很普通,也很简单,却多用几次就得心应手了,但是却很实用,重要的是要有求真精神。
一路来,从认识到灵活运用,由浅入深,我们好像挺顺利,其实不然,一个好的应用都是从bug中产出的,好的程序也是错误中不断优化出来。我们在计算滑块的移动距离的时候,会发生计算结果为0的情况:如下
int one = offset * 2 + bmpw; //选项卡0 -> 1 的偏移量
int two = one * 2; // //选项卡1 -> 2 的偏移量
这是为什么呢?
经过代码跟踪,最后才发现我们先初始化ViewPager的监听类,在初始化ViewPager的过程中,我们计算了移动的偏移量,滑块动画初始位置offset 和screenW都是还没有初始化,都是0,此时我们再计算滑块长度和初始值,导致移动距离one和two都为0.所以当我们遇到问题,调试是一个很好的办法。
前面的代码太简单了,就不上传了,这里分享最后的自定义选项卡的Demo大家可以下载来看看: