组件使用之Fragment与ViewPager

来源:互联网 发布:nginx的最新性能指标 编辑:程序博客网 时间:2024/06/06 08:44

组件使用之Fragment与ViewPager

  Fragment和ViewPager都是安卓原生的组件,尤其是Fragment更是Activity之外的又一个重要的界面组分,它们各自有各自适合使用的场景,也各有各的使用技巧,但它们两者结合能产生的效果优于两者分别使用的效果,所以放到一起谈谈。


麻雀虽小五脏俱全的Fragment

  Fragment是继Activity之后安卓的又一重要组件,它的作用就如其名称那样,片段或者分段,它能将一个复杂的应用分解成不同的段来实现,每个分段有自己的生命周期和布局展示,有效地补充了Activity的不足。
  可以先看看网上流传的Fragment生命周期图
  这里写图片描述
  和Activity很相似,但也有不少不同的地方。
  在大部分情况下,编程人员关注最多的是onAttach,onCreate和onCreateView三个方法,它们的重要性如同Activity的onCreate方法。
  其次,onResume和onPause也是值得注意的部分,它们在多个Fragment组合的时候会发挥不小的作用。
  最后的onDestory一般用于回收资源,在有相应需求的时候也要注意考虑。
  Fragment的使用一般就三种方法

  1. 直接放在Xml文件中声明使用
  2. 通过在FragmentActivity(新版API已经可以直接使用Activity)中使用FragmentManager来将需要的Fragment替换到界面上
  3. 使用ViewPager切换多个Fragment
    直接放在Xml文件中使用是最简单的,但是其灵活性略差了一些,有点像是include标签的加强版。

  要这样使用Fragment的话,只需要编写好Fragment要展示的Xml布局文件,随后编写一个继承了Fragment类的Java类,重写onCreateView方法把准备好的Xml布局创建出来并返回,接着在主界面的Xml布局中写一个fragment标签。

<fragment    android:id="@+id/id_fragment"    android:name="com.xx.xxx.xxxx"    android:layout_width="match_parent"    android:layout_height="wrap_content"/>

  如图所示,只要name属性正确地写出了对应Fragment类的完整路径,运行时自定义的Fragment就会展示在fragment标签所在的地方。
  第二种方法相对比较常用,它比第一种方法来的灵活,可以随意切换Fragment而不必在界面上写死是哪个Fragment在起效。这种方法需要使用到安卓系统提供的关于管理Fragment的工具,一个FragmentManager以及配套使用的FragmentTransaction。
  要使用FragmentManager来管理Fragment的展示,首先需要一个Xml布局,在其中写一个FrameLayout标签(也可以用其它的Layout,比如LinearLayout),并给它一个ID。

<FrameLayout    android:id="@+id/id_fragment"    android:layout_width="match_parent"    android:layout_height="wrap_content"/>

  随后在Activity中加载该Xml布局,使用FragmentManager和FragmentTransaction把对应的Fragment替换上去。

void onCreate() {    FragmentManager manager = getFragmentManager();    FragmentTransaction transaction = manager.beginTransaction();    Fragment fragment = new Fragment(); // 此处使用自定义的Fragment    transaction.replace(R.id.id_fragment, fragment);    transaction.commit();}

  这样一来,对应的Fragment就会在原来FrameLayout所在的位置展示。
  第三种方法是对ViewPager的拓展,通常来说ViewPager会用来管理好几个不同的View布局,但如果不采取一些手段,那么所有的View就必须要由ViewPager所处的Activity来管理,页面需求复杂的时候非常不方便而且耦合性较高,尤其是当所有页面都涉及到网络数据访问的时候。
  使用Fragment+ViewPager的方式来实现多页面切换能较好地解决复杂需求和耦合性的问题。这种方案由FragmentPagerAdapter或者FragmentStatePagerAdapter实现,其中前者会把所有需要维护的Fragment都放在内存中方便取用,而后者只会保存状态,在切换页面时重新创建对应的Fragment。
  这种方式的介绍将会在后文关于ViewPager的部分进行。


多页面管理与切换组件ViewPager

  ViewPager针对的是一种非常常见的设计需求,在桌面软件中有一种很方便的组件设计,那就是TabContent,通过顶部的标签切换不同的显示页面,来引导用户选择自己需要的功能。
  这样的需求在移动终端上也很快成为了重要的问题,因此安卓拿出了解决方案ViewPager。它是安卓应用中常见的一种组件,用于实现翻页切换不同功能页面的需求,比如左右滑动展示不同类型的新闻资讯之类的场景。
  ViewPager的使用大体上分为两种方式

  1. View+PagerAdapter方式
  2. Fragment+FragmentPagerAdapter方式

  第一种方式比较简单,只需要按照需求创建需要的View并且在Adapter中返回即可,具体来说有这么几种写法

  • 在Adapter中即时创建和销毁View
int pageCount = 5;public class MyPagerAdapter extends PagerAdapter {    private Context resContext;    public MyPagerAdapter(Context context) {        resContext = context;    }    @Override    public int getCount() {        return pageCount;    }    @Override    public Object instantiateItem(ViewGroup container, int position) {        View page = LayoutInflater.from(resContext).inflate(R.layout                .layout_pager_item, null);        container.addView(page);        return page;    }    @Override    public void destroyItem(ViewGroup container, int position, Object object) {        container.removeView((View) object);    }    @Override    public boolean isViewFromObject(View view, Object object) {        return view == object;    }}

  这是最简单粗暴的一种,Adapter里直接创建新的View并添加到ViewPager中显示,切换后按需销毁这些View。这种方式十分原始而且无论从功能上还是性能上都有所欠缺,因此实践中尽量不要使用这样的方法。

  • 使用列表来管理所有的View
List<View> pageList = new ArrayList<>();public class MyPagerAdapter extends PagerAdapter {    private Context resContext;    public MyPagerAdapter(Context context) {        resContext = context;    }    @Override    public int getCount() {        return pageList == null?0:pageList.size();    }    @Override    public Object instantiateItem(ViewGroup container, int position) {        View page = pageList.get(position);        container.addView(page);        return page;    }    @Override    public void destroyItem(ViewGroup container, int position, Object object) {        container.removeView(pageList.get(position));    }    @Override    public boolean isViewFromObject(View view, Object object) {        return view == object;    }}

  这种方法相对要好一些,使用一个列表来管理所有的View,这样就不需要频繁地创建和销毁需要展示的View了;这种方法还可以继续改进,比如使用自定义的View替代原始的View对象,或者使用自定义的抽象框架来管理复杂的页面展示逻辑等等。
  但无论如何改进,View+PagerAdapter的模式总是有一个不可忽视的缺陷,它只能管理View,在子View的内部逻辑很复杂的情况下,管理它们的Activity很快就会不堪重负,尤其是当需要展示的页面很多的时候。
  因此,使用Fragment是一条看得见的解决之道,ViewPager也不出意外地提供了这样的解决方案,那就是
  FragmentPagerAdapter
  FragmentStatePagerAdapter
  以上两种Adapter都可以接受Fragment作为ViewPager的页面元素,使用这种方式能较好地区分子View和主Activity 的逻辑关系,将子View的展示和逻辑代码都浓缩到Fragment的定义中,Activity只需要简单地管理一个Fragment列表即可。
  比如可以写出如下的代码

List<Fragment> fragList = new ArrayList<>();public class MyFragmentPagerAdapter extends FragmentPagerAdapter {    public MyFragmentPagerAdapter(FragmentManager fm) {        super(fm);    }    @Override    public Fragment getItem(int position) {        return fragList.get(position);    }    @Override    public int getCount() {        return fragList == null?0:fragList.size();    }}

  这是一种简单的应用方式,它适用于每个Fragment和Activity的关系较弱,而且不存在数据传递的情况,因为如果此时需要传递数据,可能要将部分管理数据的代码放入Activity中,增加耦合性。
  另一种Adapter,即FragmentStatePager Adapter和FragmentPagerAdapter最大的区别就在于前者只会保存Fragment的状态,而后者将整个Fragment保存在了内存中,大部分情况下两者区别不大,但如果有大量页面显示需求或者Fragment本身十分复杂则尽量采用FragmentStatePagerAdapter方式。
  两种Adapter均有要求使用FragmentManager作为构造函数的参数,即表示它们都使用了FragmentManager来管理Fragment,实际上ViewPager只有在第一次显示时会调用getItem方法获取Fragment,获取到后就会将它们(本体或者状态)放入FragmentManager中缓存,在后续的翻页过程中只会在缓存里寻找页面,新的页面则依然调用getItem放入。
  如果子页面需要在显示时拿到一些数据,比如传递一个Intent,则应当重写instantiateItem方法,获取到缓存的Fragment后调用setArgument方法为其设定参数。
  ViewPager的导航栏是很有用的组件,而且其实现方式也多种多样,最基本的导航栏可以使用PagerTabStrip或者PagerTitleStrip实现,它们的使用方法很简单,与ViewPager的绑定也是自动进行的。
  以PagerTabStrip为例,首先需要在Xml文件中声明该组件的位置,注意必须在ViewPager组件定义的内部声明。

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"              android:orientation="vertical"              android:layout_width="match_parent"              android:layout_height="match_parent">    <android.support.v4.view.ViewPager        android:id="@+id/vpMain"        android:layout_width="match_parent"        android:layout_height="match_parent">        <android.support.v4.view.PagerTabStrip            android:layout_width="match_parent"            android:layout_height="wrap_content"/>    </android.support.v4.view.ViewPager></LinearLayout>

  然后在定义Adapter时多重载一个方法

List<Fragment> fragList = new ArrayList<>();List<String> titleList = new ArrayList<>();public class MyFragmentPagerAdapter extends FragmentPagerAdapter {    public MyFragmentPagerAdapter(FragmentManager fm) {        super(fm);    }    @Override    public Fragment getItem(int position) {        return fragList.get(position);    }    @Override    public int getCount() {        return fragList == null?0:fragList.size();    }    @Override    public CharSequence getPageTitle(int position) {        return titleList.get(position);    }}

  之后PagerTabStrip就会自动通过getPageTitle方法获取每一页的标题并显示在ViewPager的顶部,也会随着ViewPager的滑动而变化。
  至于PagerTitleStrip的使用方法也类似,这两种Strip的最大区别在于所能显示的标题样式以及相关的设置选项等。
  除此之外还有一种方式,使用TabLayout布局的setupWithViewPager方法将TabLayout与ViewPager绑定,该方法不受Adapter限制,缺点在于必须手动为TabLayout添加显示的标题,而且TabLayout本身的样式也受限。
  TabLayout控件在安卓的Design附加包中,这也是较为不方便的地方。
  在使用安卓本身提供的控件之外,还可以自定义一个导航栏,然后自行通过代码实现导航栏与ViewPager的绑定,只要在点击回调中使用pager.setCurrentItem方法即可。


ViewPager的懒加载问题

  安卓的ViewPager组件为了保证滑动效率有一个预加载的功能,即它会在显示当前页面的同时加载并初始化左右两个页面,如果是头尾的页面则只加载附近的一个;这个功能可以通过ViewPager提供的setOffscreenPageLimit方法进行设置,自定义缓存的页面数量。
  预加载的功能在通常情况下不会影响ViewPager的使用,在某些情况下还有利于流畅地展示动画,但当ViewPager使用了Fragment并且Fragment中存在数据加载功能的时候,预加载功能就可能会带来麻烦,因为Fragment的生命周期是自行掌控的,一旦在涉及到生命周期的方法中存在功能代码,那么预加载就可能造成预料之外的代码执行问题。
  比如说多余的数据加载,每当切换到新的页面,其附近的至少一个页面也会加载,如果数据加载的代码放在了onCreate或者onResume函数中,则预加载时也会加载这些看不见的页面数据。
  或者考虑这样一个场景,在ViewPager中每个页面要有一个后台任务,这个任务需要在页面展示的同时开始执行,如果任务的启动放在了onCreate或者onResume函数中,则需求无法达成,因为预加载会提前启动后台任务。
  虽然说ViewPager提供了setOffscreenPageLimit方法用于设置预加载的页面数量,但使用该方法将预加载页面设置为0行不通,因为从源码可知ViewPager强制规定了默认预加载页面偏移是1,换言之如果设置为0则不满足自定义值“大于默认值”的条件,ViewPager依然会预加载左右各一个页面。
  因此需要另外一种解决方案,既不会受到预加载影响又能实现之前描述的类似需求,这样的解决方案被称为“懒加载”,即只有当页面可见时才进行加载
  Fragment有一个方法setUserVisibleHint可以描述一个Fragment是否可见,这是懒加载方案的核心方法。它的特点是会调用两次,第一次在Fragment创建时,第二次在Fragment展示时
  放到ViewPager中则是,选中的Fragment以及预加载的Fragment都会调用自己的setUserVisibleHint方法,但参数均为false表示不可见;随后选中的Fragment变得可见因此再调用一次该方法,此时参数为true表示可见
  如果仅仅是数据的加载希望在可见时进行,或者后台任务仅在可见时执行这样的简单需求,则直接使用setUserVisibleHint方法可以满足要求;但当需求里有在可见时对Fragment包含的控件进行操作这种情况的话则不能单纯使用setUserVisibleHint来解决问题,因为setUserVisibleHint的调用时机不确定,它可能在Fragment的View被创建前或者被销毁后才调用,此时操作控件会导致空指针异常。
  因此可以尝试按照需求编写一个能准确判断当前页面是否展示以及页面组件是否未创建或者已销毁的方法。

public class MyFragment extends Fragment {    private View fragView;    boolean viewCreated = false;    boolean fragmentVisible = false;    @Override    public void setUserVisibleHint(boolean isVisibleToUser) {        super.setUserVisibleHint(isVisibleToUser);        if(fragView != null) {            viewCreated = true;            if(isVisibleToUser) {                onFragmentVisibilityChange(true);                fragmentVisible = true;            } else if(fragmentVisible) {                onFragmentVisibilityChange(false);                fragmentVisible = false;            }        }    }    @Override    public void onViewCreated(View view, @Nullable Bundle savedInstanceState) {        super.onViewCreated(view, savedInstanceState);        if(!viewCreated && getUserVisibleHint()) {            onFragmentVisibilityChange(true);            fragmentVisible = true;        }    }    protected void onFragmentVisibilityChange(boolean visibility) {        // TODO 切换Fragment时的回调    }}

  如上便是一种可行的方案,只需要在onFragmentVisibilityChange方法中根据visibility的值来确定当前Fragment是否可见便可。
  关于Fragemnt和ViewPager的更多信息可以参考以下的文章

  1. Android Fragment 你应该知道的一切
  2. Android ViewPager使用详解
原创粉丝点击