18.在视图之间滑动

问题

需要在应用程序的UI中通过手势滑动来实现页面切换,例如视图之间或Fragment之间的切换。

解决方案

(API level 4)
实现ViewPager小部件以提供手势滑动时页面切换的功能。ViewPager是AdapterView模式修改后的实现,ListView和GridView小部件也使用了框架的这种模式。ViewPager需要一个继承自PagerAdapter的子类适配器实现,但从概念上讲,该适配器与BaseAdapter和ListAdapter中使用的模式非常类似。ViewPager本身并不能实现分页控件的回收,但它每时每刻都提供了回调方法来进行条目的创建和销毁。所以在特定的时间内,内存中运行的内容视图的数量是固定的。

要点:
ViewPager是当前只可以通过Android支持库使用的控件。无论在哪个级别的Android平台中,原生的SDK都不包含ViewPager。不过,所有目标版本为API Level 4或以上级别的应用程序都可以通过包括支持库来使用该小部件。关于在项目中包括支持库的更多信息,请参考https://developer.android.com/tools/support-library/index.html 。

实现机制

使用ViewPager最大的工作就是PagerAdapter的实现。让我们开始一个简单的示例,参见以下清单代码,它实现了一系列图片的分页显示。
自定义图像PagerAdapter

public class ImagePagerAdapter extends PagerAdapter {
    private Context mContext;
    
    private static final int[] IMAGES = {
        android.R.drawable.ic_menu_camera,
        android.R.drawable.ic_menu_add,
        android.R.drawable.ic_menu_delete,
        android.R.drawable.ic_menu_share,
        android.R.drawable.ic_menu_edit
    };
    
    private static final int[] COLORS = {
        Color.RED,
        Color.BLUE,
        Color.GREEN,
        Color.GRAY,
        Color.MAGENTA
    };
    
    public ImagePagerAdapter(Context context) {
        super();
        mContext = context;
    }
    
    /*
     * 提供页面的总数
     */
    @Override
    public int getCount() {
        return 5;
    }
    
    /*
     * 如果要在Viewpager内一次显示超过一页的内容,那么需要重写该方法
     */
    @Override
    public float getPageWidth(int position) {
        return 0.333f;
    }

    @Override
    public Object instantiateItem(ViewGroup container, int position) {
        // 创建一个新的ImageView并把它添加到提供的容器中
        ImageView iv = new ImageView(mContext);
        // 设置此位置的内容
        iv.setImageResource(IMAGES[position]);
        iv.setBackgroundColor(COLORS[position]);
        // 这里你必须自己添加视图,Android框架是不会为你添加的
        container.addView(iv);
        //将这个视图作为这个位置的键对象返回
        return iv;
    }
    
    @Override
    public void destroyItem(ViewGroup container, int position, Object object) {
        //此处从容器中删除视图
        container.removeView((View) object);
    }
    
    @Override
    public boolean isViewFromObject(View view, Object object) {
        // 检查从instantiateItem() 返回的对象与添加到容器相应位置的视图是否
        //是同个对象。 我们的示例在这两个地方使用的是同一个对象。
        return (view == object);
    }   
}

在这个示例中,我们实现了一个PagerAdapter,它提供了很多的ImageView实例供用户翻看。和AdapterView的适配器一样,PagerAdapter中第一个需要重写的就是getCount()方法,它会返回要显示条目的总数。
ViewPager是基于跟踪每个条目的键对象以及显示该对象的视图进行工作的,这样会将适配器条目和它们的视图(开发人员在使用AdapterView时经常会用到)分离开来。但是它们的实现方式略有不同。如果使用AdapterView,适配器的getView()方法会构建和返回条目上显示的视图。而使用ViewPager,当需要创建一个新视图,或者某个视图滚动超出了页数限制的范围后需要删除该视图时,就会分别调用instantiateItem()和destoryItem()回调方法,通过setOffscreenPageLimit()方法来设置每个ViewPager可持有条目的数量。

注意:
屏幕以外的页数默认限制值为3,这意味着ViewPager将会跟踪当前可见页面、当前页面左侧的页面以及当前页面右侧的页面。跟踪页面的编号总是围绕当前可见的页面居中进行的。

在我们的示例中,我们使用instantiateItem()来创建一个新的ImageView并设置该ImageView的相关属性。和AdapterView不同的是,PagerAdapter不同的是,PageAdapter除了通过返回唯一的键对象来表示某个条目外,还必须把要显示的View关联到给定的ViewGroup上。这两个操作不一定需要相同,但可以像本例中这样简单处理。需要重写PagerAdapter的isViewFromObject()回调方法,这样应用程序就可以将键对象和视图关联起来。在我们的示例中,将ImageView添加到给定的父视图上并将该ImageView作为instantiateItem()的键对象返回值。如此一来,isViewFromObject()中的代码就变得简单了,如果两个参数的实例是相同的,就返回true。
作为实例化过程的补充,PagerAdapter同样需要在destoryItem()方法中将指定的视图从父容器移除。如果页面上显示的是重量级视图,同时你想实现可以在适配器中循环利用的基本视图,这个视图被删除后可以保存它,这样它就可以在instantiateItem()中附加在另一个键对象上。以下代码清单展示了一个Activity示例,在Viewpager中使用我们自定义的适配器。
使用了Viewpager和ImagePagerAdapter的Activity

public class PagerActivity extends ActionBarActivity {
    
@Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        ViewPager pager = new ViewPager(this);
        pager.setAdapter(new ImagePagerAdapter(this));
        
        setContentView(pager);
    }
}

运行这个应用程序后,用户可以水平滑动手指来分页浏览自定义适配器提供的所有图片,而且每张图片都是全屏显示。本例中有一个定义的方法我们没有提到:getPageWidth()。这个方法允许在每个位置设置图片页面大小相对于ViewPager页面大小的百分比。默认值设置为1,前面的示例也没有改变该默认值。但如果要一次显示几个页面,可以通过调整这个方法的返回值来实现。
如果按照下面的代码片段修改getPageWidth(),那么我们一次可以显示三个页面:

    /**
     * 如果要在ViewPager内一次显示超过一页的内容,那么需要重写该方法。
     */
    @Override
    public float getPageWidth(int position){
        //每个页面的宽应该是视图的1/3
        return 0.333f; 
    }

如下图所示展示了应用程序的修改结果。
每次显示三个页面的ViewPager

1.添加和删除页面

以下代码清单演示了一个用于ViewPager的稍微复杂的适配器。它使用框架中的FragmentPagerAdapter作为父类,FragmentPagerAdapter的每个页面条目都是Fragment而不是简单的视图。
显示了一个列表的FragmentPagerAdapter

public class ListPagerAdapter extends FragmentPagerAdapter {

    private static final int ITEMS_PER_PAGE = 3;
    
    private List<String> mItems;
    
    public ListPagerAdapter(FragmentManager manager, List<String> items) {
        super(manager);
        mItems = items;
    }
    
    /*
     * This method will only get called the first time a Fragment is needed for this position.
     */
    @Override
    public Fragment getItem(int position) {
        int start = position * ITEMS_PER_PAGE;
        return ArrayListFragment.newInstance(getPageList(position), start);
    }

    @Override
    public int getCount() {
        //Get whole number
        int pages = mItems.size() / ITEMS_PER_PAGE;
        // Add one more page for any remaining values if list size is not divisible by page size
        int excess = mItems.size() % ITEMS_PER_PAGE;
        if (excess > 0) {
            pages++;
        }

        return pages;
    }

    /*
     * This will get called after getItem() for new Fragments, but also when Fragments
     * beyond the off-screen page limit are added back; we need to make sure to update the
     * list for these elements.
     */
    @Override
    public Object instantiateItem(ViewGroup container, int position) {
        ArrayListFragment fragment = (ArrayListFragment) super.instantiateItem(container, position);
        fragment.updateListItems(getPageList(position));
        return fragment;
    }
    
    /*
     * Called by the framework when notifyDataSetChanged() is called, we must decide how
     * each Fragment has changed for the new data set.  We also return POSITION_NONE if
     * a Fragment at a particular position is no longer needed so the adapter can
     * remove it.
     */
    @Override
    public int getItemPosition(Object object) {
        ArrayListFragment fragment = (ArrayListFragment)object;
        int position = fragment.getBaseIndex() / ITEMS_PER_PAGE;
        if(position >= getCount()) {
            //This page no longer needed
            return POSITION_NONE;
        } else {
            //Refresh fragment data display
            fragment.updateListItems(getPageList(position));

            return position;
        }
    }
    
    /*
     * Helper method to obtain the piece of the overall list that should be
     * applied to a given Fragment
     */
    private List<String> getPageList(int position) {
        int start = position * ITEMS_PER_PAGE;
        int end = Math.min(start + ITEMS_PER_PAGE, mItems.size());
        List<String> itemPage = mItems.subList(start, end);
        
        return itemPage;
    }
    
    /*
     * Internal custom Fragment that displays a list section inside
     * of a ListView, and provides external methods for updating the list
     */
    public static class ArrayListFragment extends Fragment {
        private ArrayList<String> mItems;
        private ArrayAdapter<String> mAdapter;
        private int mBaseIndex;
        
        //Fragments are created by convention using a Factory pattern
        static ArrayListFragment newInstance(List<String> page, int baseIndex) {
            ArrayListFragment fragment = new ArrayListFragment();
            fragment.updateListItems(page);
            fragment.setBaseIndex(baseIndex);
            return fragment;
        }
        
        public ArrayListFragment() {
            super();
            mItems = new ArrayList<String>();
        }
        
        @Override
        public void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            //Make a new adapter for the list items
            mAdapter = new ArrayAdapter<String>(getActivity(),
                    android.R.layout.simple_list_item_1, mItems);
        }
        
        @Override
        public View onCreateView(LayoutInflater inflater, ViewGroup container,
                Bundle savedInstanceState) {
            //Construct and return a Listview with our adapter attached
            ListView list = new ListView(getActivity());
            list.setAdapter(mAdapter);
            return list;
        }
        
        //Save the index in the global list where this page starts
        public void setBaseIndex(int index) {
            mBaseIndex = index;
        }
        
        //Retrieve the index in the global list where this page starts
        public int getBaseIndex() {
            return mBaseIndex;
        }
        
        public void updateListItems(List<String> items) {
            mItems.clear();
            for (String piece : items) {
                mItems.add(piece);
            }
            
            if (mAdapter != null) {
                mAdapter.notifyDataSetChanged();
            }
        }
    }
}

这个示例使用一个很长的数据列表并将其分解成小段显示在每个页面上。这个适配器显示的Fragment是一个自定义内部实现,它会接收条目的一个列表并将这些条目显示在ListView中。
FragmentPagerAdapter帮助我们实现了PagerAdapter底层的很多功能。不必再实现instantiateItem()、destroyItem()和isViewFromObject()方法,只需要重写onItem()来为每个页面位置提供相应的Fragment。本例为每个页面应该显示的列表条目的数量定义了一个常量。在getItem()内创建Fragment时,会传入列表中的一部分数据,而这些数据是根据索引偏移和之前定义的常量来计算的。分页的数量由getCount()方法返回,这个值是通过列表条目总量除以每页显示的条目常量计算得到的。

提示:
FragmentPagerAdapter将所有Fragment实例保持为活动状态,无论它们在屏幕外页数限制内是否被激活。如果ViewPager需要容纳更多数量的Fragment,或者一些ViewPager更加重量级,则可以改为使用FragmentStatePagerAdapter。FragmentStatePagerAdapter会销毁超出屏幕外页数限制的Fragment,同时保留其已保存的状态,这一点类似于旋转操作。

这个适配器还覆写了前面简单示例中未曾见到过的另一个方法:getItemPosition()。当应用程序从外部调用notifyDataSetChanged()时,这个方法会被调用。它主要的作用是在页面发生变化时判断页面中的条目应该被移动还是删除。如果条目的位置发生改变,该实现就应该返回新位置的值。如果条目不应该被移动,该实现就会返回一个常量值PagerAdapter.POSITION_UNCHANGED。如果页面应该被删除,应用程序应该返回PagerAdapter.POSITION_NONE。
这个示例会比较检查当前页面的位置(我们需要从初始索引数据开始重新创建)和当前页面数量的大小。如果当前页面位置大于当前页面数量,就需要从列表中删除足够的条目,如此一来就不再需要该页面了,然后返回POSITION_NONE。而在其他情况下,我们会更新当前Fragment中显示的列表条目,并返回重新计算得到的位置值。
每个ViewPager当前跟踪的页面都会调用getItemPosition(),调用的次数即为getOffscreenPageLimit()返回的页面数量。然而,虽然ViewPager不会跟踪滚动出限定值之外的Fragment,但FragmentManager会继续追踪。因此,当之前的Fragment回滚时,getItem()不会被再次调用,因为Fragment已经存在了。但是正因为如此,如果一个数据集在这期间发生改变,Fragment列表数据不会跟着更新。这就是要重写instantiateItem()的原因。虽然这个适配器不需要重写instantiateItem(),但是当列表发生变化时,确实需要更新超出屏幕外页数限制的Fragment。因为Fragment回滚到页数限制内以后,每次都会调用instantiateItem(),所以这是重置显示列表的好时机。

让我们看一个使用该适配器的示例应用程序。参见以下两段代码清单:
res/layout/main.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:orientation="vertical" >
    <Button
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="Add Item"
        android:onClick="onAddClick" />
    <Button
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="Remove Item"
        android:onClick="onRemoveClick" />
    
    <android.support.v4.view.ViewPager
        android:id="@+id/view_pager"
        android:layout_width="match_parent"
        android:layout_height="match_parent" />
</LinearLayout>

使用了ListPagerAdapter的Activity

public class FragmentPagerActivity extends ActionBarActivity {

    private ArrayList<String> mListItems;
    private ListPagerAdapter mAdapter;
    
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);
        //Create the initial data set
        mListItems = new ArrayList<String>();
        mListItems.add("Mom");
        mListItems.add("Dad");
        mListItems.add("Sister");
        mListItems.add("Brother");
        mListItems.add("Cousin");
        mListItems.add("Niece");
        mListItems.add("Nephew");
        //Attach the data to the pager
        ViewPager pager = (ViewPager) findViewById(R.id.view_pager);
        mAdapter = new ListPagerAdapter(getSupportFragmentManager(), mListItems);
        
        pager.setAdapter(mAdapter);
    }
    
    public void onAddClick(View v) {
        //Add a new unique item to the end of the list
        mListItems.add("Crazy Uncle " + System.currentTimeMillis());
        mAdapter.notifyDataSetChanged();
    }
    
    public void onRemoveClick(View v) {
        //Remove an item from the head of the list
        if (!mListItems.isEmpty()) {
            mListItems.remove(0);
        }
        mAdapter.notifyDataSetChanged();
    }
}

就像ViewPager的效果一样,这个示例中有两个按钮用来添加和删除数据集中的条目,注意ViewPager必须在XML文件中定义并使用完全限定的包名,因为它仅是支持库中的类,在android.widget或android.view包中并没有这个类。该Activity构建了一个默认的条目列表并把它传入我们自定义的适配器中,然后再把该适配器关联到ViewPager上。
每次单击Add按钮都在列表末尾添加一个新的条目并通过调用notofyDataSetChanged()来触发ListPagerAdapter进行更新。每次单击Remove按钮都会在列表顶部删除一个条目,然后再次通知适配器。每次变化期间,适配器都会调整当前可用页数并更新Viewpager。如果当前可见页的所有条目都被删除,那么该页也会被删除并显示上一页。

2.其他有用的方法

ViewPager中有几个其他的方法,它们会对你的应用程序很有帮助:

  • setPagerMargin()和setPageMarginDrawable()允许在页面之间设置一些额外的间隔,并且使用一个Drawable(可选)来填充间隔的内容。
  • setCurrentItem()允许你以编程的方式设置要显示的页面,并提供了一个选项来禁用页面切换时的滚动动画。
  • OnPageChangeListener用于将滚动和变更动作通知给应用程序。
    onPageSelected()会在显示一个新页面时被调用。
    当发生滚动操作时会连续调用onPageScrolled()。
    onPageScrollStateChanged()在ViewPager处于以下状态时会被调用:闲置时、用户主动滚动ViewPager时、自动滚动对齐到最近的页面时。

猜你喜欢

转载自blog.csdn.net/qq_41121204/article/details/83650602
18.