美文网首页Android开发干货集中营安卓进阶android进阶
ViewPager系列之 打造一个通用的ViewPager

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

作者: 依然范特稀西 | 来源:发表于2017-05-31 06:42 被阅读4495次

    背景

    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搞定所有列表

    相关文章

      网友评论

      • 5c5113a4a475:楼主封装的ViewPagerHolder,ViewPagerHolderCreator,CommonViewPagerAdapter很强,
        把页面的数据显示和页面数据的绑定从adapter抽取出来封装到ViewPagerHolder,又把原本要在adapter中生产的vh过程抽取到ViewPagerHolderCreator中,感觉这个解耦特别不错呀。原本在adapter中要做数据展示,数据绑定都分开了:+1: :+1:
      • artshell:ViewPagerHolder和CommonViewPager写得很好,后边就没必要用ViewGroup来包ViewPager
      • 梦华芳秋:请问大神,这个可以把指示器改到右侧居中吗?
        依然范特稀西:@梦华芳秋 可以的哈,请看第二篇文章http://www.jianshu.com/p/653680cfe877
      • 吾心彳亍:大神有遇到过自动轮播时,一次过两个页面的情况么?
      • 后来Memory:您好,我是小白 我不是很明白这样的做的有点何在!如果我有多个地方都要用到这样的viewpager那我不是需要些多个viewholder这样相对于使用adapter的优势在哪里了?
        依然范特稀西:@后来Memory ViewHolder相对Adapter来说代码更简洁简单,不用去处理View的创建、绑定、销毁等,而 ViewHolder则是专注与你的逻辑。省去了那些无用的代码。
      • dlihasa:首先感谢作者的分享,很nice。我有一个疑问,为什么不用view.setTag(holder),要额外添加一个id来setTag呢?另外其中自己写的getView缓存机制是由哪里实现的呢?
        依然范特稀西:@DHASA2017 对头,所以我在下一篇文章中封装banner的时候,已经将这个复用去掉了。这里这样用只是讲一个思路,以后自己写这种自定义控件的时候可以用这种方法复用。
        dlihasa:@依然范特稀西 哦哦,感谢解答。getview中的view是不是不存在复用呢?
        依然范特稀西:@DHASA2017 其实这两个setTag的方法,效果是一样的,只是用id标记这种方法可以在一个View上保存多个数据而已。这里你用哪一种都是一样。
      • 3cf4933d1229:可以无限轮播,,和自动轮播吗
        依然范特稀西:@开着拖拉机迎接春天 可以,请看我另一篇文章:http://www.jianshu.com/p/653680cfe877
      • 951634eff4fa:为什么把源码导进来运行不了
        951634eff4fa:@依然范特稀西 762696885
        951634eff4fa:@依然范特稀西 一直依赖不进来,,你加我qq下,,这评论好像不能弄图片
        依然范特稀西:@AAAA_5167 报什么错呢
      • 406ff1d31312:写的很好,启发很大,刚开始看的时候有些迷惑,看完研究了一下果然厉害
        依然范特稀西:@406ff1d31312 可以看一下用这种方法封装的一个banner库http://www.jianshu.com/p/653680cfe877
        依然范特稀西:@406ff1d31312 对你有帮助就好😄
      • 83e1ff2c892c:你好,请问 view.setTag (R.id.common_view_pager_item_tag,holder);中的id在哪设置?
        依然范特稀西:@zhangleiVip 有,https://github.com/pinguo-zhouwei/MZBannerView
        zhangleiVip:你好, 请问这个封装的viewpager 有demo吗 ? 可以借鉴一下吗 ? 1069778275@qq.com
        依然范特稀西:@adongbala values 下面的ids.xml 文件中

      本文标题:ViewPager系列之 打造一个通用的ViewPager

      本文链接:https://www.haomeiwen.com/subject/gyxhfxtx.html