ViewPager系列之 打造一个通用的ViewPager

背景

CommonViewPager.png

ViewPager是Android开发者比较常用的一个控件了,由于它允许数据页从左到右或者从右到左翻页,因此这种交互也备受设计师的青睐。在APP中的很多场景都用得到,比如第一次安装APP时的用户引导页、图片浏览时左右翻页、广告Banner页等等都会用到ViewPager。ViewPager 的使用和RecyclerView的使用方式很相似,熟悉RecyclerView的朋友都知道,我们要使用RecyclerView,就得给RecyclerView提供一个Adapter来提供布局和装载数据。但是有一个比较麻烦的事情是,我们每次使用RecyclerView都要给他提供一个Adapter,并且这些Adapter中的一些方法和代码都是相同的,这使得我们写了很多重复的代码,降低了我们的开发效率,因此github有各种个样的对RecyclerView 的再度封装,目的就是减少这些重复的代码,尽量代码复用,使开发更简单。那么ViewPager的使用和RecyclerView 是非常相似的,我们同样也是给ViewPager提供一个Adapter来提供布局和装载数据。写Adapter的时候同样会写很多重复代码,那么我们是否能像RecyclerView一样,也对Viewpager来做一个再次封装,达到复用和简单的效果呢?答案是肯定的,因此这篇文章就一起来封装一个通用的ViewPager。

现状

看过一些技术博客,对于普通的ViewPager使用封装的比较少,大多数的封装只是在用作Banner 的时候,也就是ViewPager 每页只显示一张图片。对外提供一个接口,传递一个imageUrl 数组就直接展示,不用再写其他的Adapter之类的。但是这样封装其实还是有一些局限性的。

  1. 每个项目用的图片加载框架是不一样的,Picasso、Glide、ImageLoader等等各不相同,那么我们还需要在显示图片的时候换成自己用的图片加载框架才行。

  2. 并不是所有的Banner 都只是显示一张图片,还有各种个样的文案展示等等,因此不能个性化定制,这是比较致命的。

看看上面的局限性,是什么造成了这些局限性呢?答案是我们没有主动权,主动权在Adapter手中,他控制了布局,控制了数据绑定,所以它说怎样展示就怎样展示,它说展示什么就展示什么。那么现在问题的关键来了,我们又不想写Adapter,又想按照我们的指示展示布局和数据,怎么办呢?那就要从Adapter中夺回主动权,我们想ViewPager展示成什么样子我们自己说了算。Adapter只需要把我们提供给他的东西按照我们的指示展示就行了。具体的布局和数据绑定都我们自己控制。因此,有了主动权,展示什么布局我们能控制,用什么框架加载图片我们同样能控制。用什么方式来告诉Adapter 做页面展示呢?就用万能的接口啦。

封装通用的ViewPager

通过上面现状的分析,我们知道了,要封装一个比较通用的ViewPager,首先就是要从Adapter那里夺回主动权,因为它控制了布局和数据绑定。有了主动权之后,我们提供布局给Adapter,然后我们自己控制数据绑定。其中有2个关键的点:1,提供布局 。 2,数据绑定。 看到这两个点是不是觉得很熟悉?当然很熟悉,这不就是RecyclerViewViewHolder干的事情嘛。既然是这样我们就借鉴一下 RecyclerViewViewHolder呗。

第一步:定义一个ViewHolder接口来提供布局和绑定数据:ViewPagerHolder代码如下:

/**
 * Created by zhouwei on 17/5/28.
 */

public interface ViewPagerHolder<T> {
    /**
     *  创建View
     * @param context
     * @return
     */
    View createView(Context context);

    /**
     * 绑定数据
     * @param context
     * @param position
     * @param data
     */
    void onBind(Context context,int position,T data);
}

ViewPagerHolder 接收一个泛型T,这是绑定数据要用的实体类。其中有2个方法,一个提供给Adapter布局,另一个则用于绑定数据。

** 第二步:** 创建一个ViewHolder生成器,用来生成各种ViewHolder:
ViewPagerHolderCreator 代码如下:

/**
 * Created by zhouwei on 17/5/28.
 */

public interface ViewPagerHolderCreator<VH extends ViewPagerHolder> {
    /**
     * 创建ViewHolder
     * @return
     */
    public VH createViewHolder();
}

该类接受一个 泛型,但是必须得是ViewPagerHolder 的子类,一个方法createViewHolder,返回ViewHolder实例。

** 第三步:** 重写 ViewPager 的Adapter:

/**
 * Created by zhouwei on 17/5/28.
 */

public class CommonViewPagerAdapter<T> extends PagerAdapter {
    private List<T> mDatas;
    private ViewPagerHolderCreator mCreator;//ViewHolder生成器

    public CommonViewPagerAdapter(List<T> datas, ViewPagerHolderCreator creator) {
        mDatas = datas;
        mCreator = creator;
    }

    @Override
    public int getCount() {
        return mDatas == null ? 0:mDatas.size();
    }

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

    @Override
    public Object instantiateItem(ViewGroup container, int position) {
        //重点就在这儿了,不再是把布局写死,而是用接口提供的布局
        // 也不在这里绑定数据,数据绑定交给Api调用者。
        View view = getView(position,null,container);
        container.addView(view);
        return view;
    }

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

    /**
     * 获取viewPager 页面展示View
     * @param position
     * @param view
     * @param container
     * @return
     */
    private View getView(int position,View view ,ViewGroup container){

        ViewPagerHolder holder =null;
        if(view == null){
            //创建Holder
            holder = mCreator.createViewHolder();
            view = holder.createView(container.getContext());
            view.setTag(R.id.common_view_pager_item_tag,holder);
        }else{
            holder = (ViewPagerHolder) view.getTag(R.id.common_view_pager_item_tag);
        }
        if(holder!=null && mDatas!=null && mDatas.size()>0){
            // 数据绑定
            holder.onBind(container.getContext(),position,mDatas.get(position));
        }

        return view;
    }
}

这个类比较重要,因为以前我们的布局提供和数据绑定都是在Adapter中的,因此现在我们就将这两项工作交给我们的ViewHolder。CommonViewPagerAdapter 的构造方法需要展示的数据集合和ViewPagerHolderCreator 生成器。其他代码都有注释一看便明白。

第四部:包装ViewPager
Adapter和ViewHolder都有了,现在我们只需要一个ViewPager 就大功告成了。我们采用自定义View 组合的方式来写这个ViewPager.
1 . 提供ViewPager 布局:

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
                xmlns:app="http://schemas.android.com/apk/res-auto"
                android:orientation="vertical"
                android:layout_width="match_parent"
                android:layout_height="match_parent">
     <!-- ViewPager-->
     
     <android.support.v4.view.ViewPager
         android:id="@+id/common_view_pager"
         android:layout_width="match_parent"
         android:layout_height="match_parent"/>
     
     <!-- 指示器 indicatorView-->
     <com.zhouwei.indicatorview.CircleIndicatorView
         android:id="@+id/common_view_pager_indicator_view"
         android:layout_width="wrap_content"
         android:layout_height="wrap_content"
         android:layout_alignParentBottom="true"
         android:layout_marginBottom="10dp"
         app:indicatorSelectColor="@android:color/white"
         app:indicatorColor="@android:color/darker_gray"
         app:fill_mode="none"
         app:indicatorSpace="5dp"
         android:layout_centerHorizontal="true"
         />
</RelativeLayout>

布局中一个ViewPager 和一个指示器View, IndicatorView 用的是前面分享的CircleIndicatorView 。详情请看https://github.com/pinguo-zhouwei/CircleIndicatorView,博客地址:Android自定义View之 实现一个多功能的IndicatorView

2 . CommonViewPager ,代码如下:

/**
 * Created by zhouwei on 17/5/28.
 */

public class CommonViewPager<T> extends RelativeLayout {
    private ViewPager mViewPager;
    private CommonViewPagerAdapter mAdapter;
    private CircleIndicatorView mCircleIndicatorView;
    public CommonViewPager(@NonNull Context context) {
        super(context);
        init();
    }

    public CommonViewPager(@NonNull Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
        init();
    }

    public CommonViewPager(@NonNull Context context, @Nullable AttributeSet attrs, @AttrRes int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        init();
    }

    @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
    public CommonViewPager(@NonNull Context context, @Nullable AttributeSet attrs, @AttrRes int defStyleAttr, @StyleRes int defStyleRes) {
        super(context, attrs, defStyleAttr, defStyleRes);
        init();
    }

    private void init(){
        View view = LayoutInflater.from(getContext()).inflate(R.layout.common_view_pager_layout,this,true);
        mViewPager = (ViewPager) view.findViewById(R.id.common_view_pager);
        mCircleIndicatorView = (CircleIndicatorView) view.findViewById(R.id.common_view_pager_indicator_view);
    }

    /**
     * 设置数据
     * @param data
     * @param creator
     */
    public void setPages(List<T> data, ViewPagerHolderCreator creator){
        mAdapter = new CommonViewPagerAdapter(data,creator);
        mViewPager.setAdapter(mAdapter);
        mAdapter.notifyDataSetChanged();
        mCircleIndicatorView.setUpWithViewPager(mViewPager);
    }

    public void setCurrentItem(int currentItem){
        mViewPager.setCurrentItem(currentItem);
    }

    public int getCurrentItem(){
        return mViewPager.getCurrentItem();
    }

    public void setOffscreenPageLimit(int limit){
        mViewPager.setOffscreenPageLimit(limit);
    }

    /**
     * 设置切换动画,see {@link ViewPager#setPageTransformer(boolean, ViewPager.PageTransformer)}
     * @param reverseDrawingOrder
     * @param transformer
     */
    public void setPageTransformer(boolean reverseDrawingOrder, ViewPager.PageTransformer transformer){
        mViewPager.setPageTransformer(reverseDrawingOrder,transformer);
    }

    /**
     * see {@link ViewPager#setPageTransformer(boolean, ViewPager.PageTransformer)}
     * @param reverseDrawingOrder
     * @param transformer
     * @param pageLayerType
     */
    public void setPageTransformer(boolean reverseDrawingOrder, ViewPager.PageTransformer transformer,
                                   int pageLayerType) {
        mViewPager.setPageTransformer(reverseDrawingOrder,transformer,pageLayerType);
    }

    /**
     * see {@link ViewPager#addOnPageChangeListener(ViewPager.OnPageChangeListener)}
     * @param listener
     */
    public void addOnPageChangeListener(ViewPager.OnPageChangeListener listener){
        mViewPager.addOnPageChangeListener(listener);
    }

    /**
     * 设置是否显示Indicator
     * @param visible
     */
    private void setIndicatorVisible(boolean visible){
        if(visible){
            mCircleIndicatorView.setVisibility(VISIBLE);
        }else{
            mCircleIndicatorView.setVisibility(GONE);
        }

    }

    public ViewPager getViewPager() {
        return mViewPager;
    }
}

CommonViewPager 是对ViewPager的包装,提供了一些ViewPager的常用方法。 其中有一个非常重要的方法public void setPages(List<T> data, ViewPagerHolderCreator creator),提供数据和ViewHolder。其他的基本上都是ViewPager的方法。也可以通过getViewPager 获取到ViewPager 再调用ViewPager的方法。

到此封装也就全部完成了。

CommonViewPager 简便使用

啰嗦了这么久的封装,那么用起来方便不呢?看一下就知道。
1 , activity 布局文件:

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout
    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="com.zhouwei.commonviewpager.MainActivity">

    <com.zhouwei.viewpagerlib.CommonViewPager
        android:id="@+id/activity_common_view_pager"
        android:layout_width="match_parent"
        android:layout_height="200dp"
        />

</RelativeLayout>

ViewPager Item 的布局文件:

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
              android:orientation="vertical"
              android:layout_width="match_parent"
              android:layout_height="match_parent">
   <ImageView
       android:id="@+id/viewPager_item_image"
       android:layout_width="match_parent"
       android:layout_height="match_parent"
       android:scaleType="centerCrop"
       />
   <TextView
       android:id="@+id/item_desc"
       android:layout_width="match_parent"
       android:layout_height="wrap_content"
       android:textSize="15sp"
       android:gravity="center"
       android:layout_centerInParent="true"
       android:textColor="@android:color/white"
       />
</RelativeLayout>

Activity 代码:

  private void initView() {
        mCommonViewPager = (CommonViewPager) findViewById(R.id.activity_common_view_pager);
        // 设置数据
        mCommonViewPager.setPages(mockData(), new ViewPagerHolderCreator<ViewImageHolder>() {
            @Override
            public ViewImageHolder createViewHolder() {
                // 返回ViewPagerHolder
                return new ViewImageHolder();
            }
        });
    }

    /**
     * 提供ViewPager展示的ViewHolder
     * <P>用于提供布局和绑定数据</P>
     */
    public static class ViewImageHolder implements ViewPagerHolder<DataEntry>{
        private ImageView mImageView;
        private TextView mTextView;
        @Override
        public View createView(Context context) {
            // 返回ViewPager 页面展示的布局
            View view = LayoutInflater.from(context).inflate(R.layout.view_pager_item,null);
            mImageView = (ImageView) view.findViewById(R.id.viewPager_item_image);
            mTextView = (TextView) view.findViewById(R.id.item_desc);
            return view;
        }

        @Override
        public void onBind(Context context, int position, DataEntry data) {
           // 数据绑定
           // 自己绑定数据,灵活度很大 
           mImageView.setImageResource(data.imageResId);
           mTextView.setText(data.desc);
        }
    }

代码逻辑很清晰,也很简单,只需要提供一个ViewHolder,ViewHolder 自己实现,然后调用setPages 方法绑定数据就好了。最后上一张效果图:

ViewPager效果.gif

总结

本篇文章的这种封装思想不仅仅对于ViewPager,对于其他的展示集合数据的控件同样实用。其实整个封装还是蛮简单的,但是我觉得这种方法值得推广,以后像我们自己写一个扩展性比较强的控件时,就可以用这种方式。如果把这些一个个控件做成独立的通用的组件,那么我们开发的效率要提高很多。

喜欢的同学可以看一下我的其他几个通用系列的文章:
PopupWindow 通用系列:
通用PopupWindow,几行代码搞定PopupWindow弹窗
通用PopupWindow,几行代码搞定PopupWindow弹窗(续)

RecylerView 通用系列:
RecyclerView 之Adapter的简化过程浅析
RecyclerView Adapter 优雅封装,一个Adapter搞定所有列表

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 151,511评论 1 330
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 64,495评论 1 273
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 101,595评论 0 225
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 42,558评论 0 190
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 50,715评论 3 270
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 39,672评论 1 192
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,112评论 2 291
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 29,837评论 0 181
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 33,417评论 0 228
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 29,928评论 2 232
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,316评论 1 242
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 27,773评论 2 234
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,253评论 3 220
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 25,827评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,440评论 0 180
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 34,523评论 2 249
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 34,583评论 2 249

推荐阅读更多精彩内容