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

背景

CommonViewPager.png

ViewPager是Android開發(fā)者比較常用的一個控件了溪窒,由于它允許數據頁從左到右或者從右到左翻頁,因此這種交互也備受設計師的青睞。在APP中的很多場景都用得到宣羊,比如第一次安裝APP時的用戶引導頁、圖片瀏覽時左右翻頁汰蜘、廣告Banner頁等等都會用到ViewPager仇冯。ViewPager 的使用和RecyclerView的使用方式很相似,熟悉RecyclerView的朋友都知道族操,我們要使用RecyclerView苛坚,就得給RecyclerView提供一個Adapter來提供布局和裝載數據。但是有一個比較麻煩的事情是色难,我們每次使用RecyclerView都要給他提供一個Adapter,并且這些Adapter中的一些方法和代碼都是相同的泼舱,這使得我們寫了很多重復的代碼,降低了我們的開發(fā)效率枷莉,因此github有各種個樣的對RecyclerView 的再度封裝娇昙,目的就是減少這些重復的代碼,盡量代碼復用笤妙,使開發(fā)更簡單冒掌。那么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> {
    /**
     *  創(chuàng)建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布局证舟,另一個則用于綁定數據硕旗。

** 第二步:** 創(chuàng)建一個ViewHolder生成器,用來生成各種ViewHolder:
ViewPagerHolderCreator 代碼如下:

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

public interface ViewPagerHolderCreator<VH extends ViewPagerHolder> {
    /**
     * 創(chuàng)建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){
            //創(chuàng)建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,對于其他的展示集合數據的控件同樣實用。其實整個封裝還是蠻簡單的悠瞬,但是我覺得這種方法值得推廣们豌,以后像我們自己寫一個擴展性比較強的控件時,就可以用這種方式浅妆。如果把這些一個個控件做成獨立的通用的組件望迎,那么我們開發(fā)的效率要提高很多。

喜歡的同學可以看一下我的其他幾個通用系列的文章:
PopupWindow 通用系列:
通用PopupWindow凌外,幾行代碼搞定PopupWindow彈窗
通用PopupWindow辩尊,幾行代碼搞定PopupWindow彈窗(續(xù))

RecylerView 通用系列:
RecyclerView 之Adapter的簡化過程淺析
RecyclerView Adapter 優(yōu)雅封裝,一個Adapter搞定所有列表

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
  • 序言:七十年代末康辑,一起剝皮案震驚了整個濱河市摄欲,隨后出現的幾起案子,更是在濱河造成了極大的恐慌疮薇,老刑警劉巖胸墙,帶你破解...
    沈念sama閱讀 210,978評論 6 490
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現場離奇詭異惦辛,居然都是意外死亡劳秋,警方通過查閱死者的電腦和手機,發(fā)現死者居然都...
    沈念sama閱讀 89,954評論 2 384
  • 文/潘曉璐 我一進店門胖齐,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人嗽冒,你說我怎么就攤上這事呀伙。” “怎么了添坊?”我有些...
    開封第一講書人閱讀 156,623評論 0 345
  • 文/不壞的土叔 我叫張陵剿另,是天一觀的道長。 經常有香客問我贬蛙,道長雨女,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 56,324評論 1 282
  • 正文 為了忘掉前任阳准,我火速辦了婚禮氛堕,結果婚禮上,老公的妹妹穿的比我還像新娘野蝇。我一直安慰自己讼稚,他們只是感情好括儒,可當我...
    茶點故事閱讀 65,390評論 5 384
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著锐想,像睡著了一般帮寻。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上赠摇,一...
    開封第一講書人閱讀 49,741評論 1 289
  • 那天固逗,我揣著相機與錄音,去河邊找鬼藕帜。 笑死烫罩,一個胖子當著我的面吹牛,可吹牛的內容都是我干的耘戚。 我是一名探鬼主播嗡髓,決...
    沈念sama閱讀 38,892評論 3 405
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼收津!你這毒婦竟也來了饿这?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 37,655評論 0 266
  • 序言:老撾萬榮一對情侶失蹤撞秋,失蹤者是張志新(化名)和其女友劉穎长捧,沒想到半個月后,有當地人在樹林里發(fā)現了一具尸體吻贿,經...
    沈念sama閱讀 44,104評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡串结,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 36,451評論 2 325
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現自己被綠了舅列。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片肌割。...
    茶點故事閱讀 38,569評論 1 340
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖帐要,靈堂內的尸體忽然破棺而出把敞,到底是詐尸還是另有隱情,我是刑警寧澤榨惠,帶...
    沈念sama閱讀 34,254評論 4 328
  • 正文 年R本政府宣布奋早,位于F島的核電站,受9級特大地震影響赠橙,放射性物質發(fā)生泄漏耽装。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 39,834評論 3 312
  • 文/蒙蒙 一期揪、第九天 我趴在偏房一處隱蔽的房頂上張望掉奄。 院中可真熱鬧,春花似錦横侦、人聲如沸挥萌。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,725評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽引瀑。三九已至狂芋,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間憨栽,已是汗流浹背帜矾。 一陣腳步聲響...
    開封第一講書人閱讀 31,950評論 1 264
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留屑柔,地道東北人屡萤。 一個月前我還...
    沈念sama閱讀 46,260評論 2 360
  • 正文 我出身青樓,卻偏偏與公主長得像掸宛,于是被迫代替她去往敵國和親死陆。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 43,446評論 2 348

推薦閱讀更多精彩內容