背景
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之類的。但是這樣封裝其實還是有一些侷限性的。
每個專案用的圖片載入框架是不一樣的,Picasso、Glide、ImageLoader等等各不相同,那麼我們還需要在顯示圖片的時候換成自己用的圖片載入框架才行。
並不是所有的Banner 都只是顯示一張圖片,還有各種個樣的文案展示等等,因此不能個性化定製,這是比較致命的。
看看上面的侷限性,是什麼造成了這些侷限性呢?答案是我們沒有主動權,主動權在Adapter手中,他控制了佈局,控制了資料繫結,所以它說怎樣展示就怎樣展示,它說展示什麼就展示什麼。那麼現在問題的關鍵來了,我們又不想寫Adapter,又想按照我們的指示展示佈局和資料,怎麼辦呢?那就要從Adapter中奪回主動權,我們想ViewPager展示成什麼樣子我們自己說了算。Adapter只需要把我們提供給他的東西按照我們的指示展示就行了。具體的佈局和資料繫結都我們自己控制。因此,有了主動權,展示什麼佈局我們能控制,用什麼框架載入圖片我們同樣能控制。用什麼方式來告訴Adapter 做頁面展示呢?就用萬能的介面啦。
封裝通用的ViewPager
通過上面現狀的分析,我們知道了,要封裝一個比較通用的ViewPager,首先就是要從Adapter那裡奪回主動權,因為它控制了佈局和資料繫結。有了主動權之後,我們提供佈局給Adapter,然後我們自己控制資料繫結。其中有2個關鍵的點:1,提供佈局 。 2,資料繫結。 看到這兩個點是不是覺得很熟悉?當然很熟悉,這不就是RecyclerView
的ViewHolder
乾的事情嘛。既然是這樣我們就借鑑一下 RecyclerView
的ViewHolder
唄。
第一步:定義一個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 。詳情請看github.com/pinguo-zhou…,部落格地址: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,對於其他的展示集合資料的控制元件同樣實用。其實整個封裝還是蠻簡單的,但是我覺得這種方法值得推廣,以後像我們自己寫一個擴充套件性比較強的控制元件時,就可以用這種方式。如果把這些一個個控制元件做成獨立的通用的元件,那麼我們開發的效率要提高很多。
喜歡的同學可以看一下我的其他幾個通用系列的文章:
PopupWindow 通用系列:
通用PopupWindow,幾行程式碼搞定PopupWindow彈窗
通用PopupWindow,幾行程式碼搞定PopupWindow彈窗(續)
RecylerView 通用系列:
RecyclerView 之Adapter的簡化過程淺析
RecyclerView Adapter 優雅封裝,一個Adapter搞定所有列表