打造一個絲滑般自動輪播無限循環Android庫--源碼解析

《打造一個絲滑般自動輪播無限循環Android庫》android

《打造一個絲滑般自動輪播無限循環Android庫--源碼解析》git

上篇文章《打造一個絲滑般自動輪播無限循環Android庫》介紹了BannerViewPager的基礎功能及使用方法。咱們瞭解了BannerViewPager不但能夠支持任意的頁面佈局,並且能夠支持任意的Indicator。那麼BannerViewPager的高度可定製性及無限輪播是如何實現的呢?本篇文章將深刻源碼來了解BannerViewPager的設計思路。github

1、如何支持任意的Item佈局

產品的需求變幻無窮,你永遠也猜不到下一步產品會給你提一個什麼樣的需求。所以對於一個比較人性化的Banner庫來講,它也應該支持開發者去自定義任意的Item頁面佈局。BannerViewPager就是本着這樣的思路來作的。那麼究竟其內部是如何實現的呢?咱們先從setHolderCreator(HolderCreator holderCreator)這個方法提及。在使用BannerViewPager的時候咱們能夠爲其設置一個HolderCreator,代碼以下:數組

bannerViewPager.setHolderCreator(new HolderCreator<CustomPageViewHolder>() {
                    @Override
                    public CustomPageViewHolder createViewHolder() {
                        return new CustomPageViewHolder();
                    }
                })
複製代碼

而在HolderCreator的createViewHolder方法中返回了一個CustomPageViewHolder,這個CustomPageViewHolder是咱們本身實現的。其內部經過createView方法來inflate出來一個咱們自定義的itemView,並在onBind方法中爲itemView綁定數據。其代碼以下:bash

public class CustomPageViewHolder implements ViewHolder<CustomBean> {
    private ImageView mImageView;
    private TextView mTextView;
 @Override
    public View createView(ViewGroup viewGroup, Context context, int position) {
        View view = LayoutInflater.from(context).inflate(R.layout.item_custom_view, viewGroup, false);
        mImageView = view.findViewById(R.id.banner_image);
        mTextView = view.findViewById(R.id.tv_describe);
        return view;
    }

    @Override
    public void onBind(Context context, CustomBean data, int position, int size) {
        mImageView.setImageResource(data.getImageRes());
        mTextView.setText(data.getImageDescription());
    }
    ...
}
複製代碼

setHolderCreator以後再BannerViewPager內部是如何處理的呢?咱們接下來繼續看:ide

/**
     * 必須爲BannerViewPager設置HolderCreator,HolderCreator中建立ViewHolder,
     * 在ViewHolder中管理BannerViewPager的ItemView.
     *
     * @param holderCreator HolderCreator
     */
    public BannerViewPager<T, VH> setHolderCreator(HolderCreator<VH> holderCreator) {
        this.holderCreator = holderCreator;
        return this;
    }

	 private void setupViewPager() {
	        if (holderCreator != null) {
	            BannerPagerAdapter<T, VH> bannerPagerAdapter =
	                    new BannerPagerAdapter<>(mList, holderCreator);
	        		...
	        } else {
	            throw new NullPointerException("You must set HolderCreator for BannerViewPager");
	        }
	    }
複製代碼

上述代碼中判斷若是holderCreator爲null時就拋出了一個NullPointerException,這也解釋了爲何必需要爲BannenrViewPager設置holderCreator。當holderCreator不爲null時,將holder傳遞到了BannerPagerAdapter中。咱們接下來到BannerPagerAdapter中一探究竟:oop

public class BannerPagerAdapter<T, VH extends ViewHolder> extends PagerAdapter {
	 @Override
    public @NonNull
    Object instantiateItem(@NonNull final ViewGroup container, final int position) {
    	//	爲了方便理解,此處與源碼並不一致,詳情請參看BannerPagerAdapter源碼
        View itemView =getView(position, container);
        container.addView(itemView);
        return itemView;
    }

		...
	@SuppressWarnings("unchecked")
    private View getView(final int position, ViewGroup container) {
        ViewHolder<T> holder = holderCreator.createViewHolder();
        if (holder == null) {
            throw new RuntimeException("can not return a null holder");
        }
        return createView(holder, position, container);
    }
    
	private View createView(ViewHolder<T> holder, int position, ViewGroup container) {
	        View view = null;
	        if (list != null && list.size() > 0) {
	          view = holder.createView(container, container.getContext(), position);
	          holder.onBind(container.getContext(), list.get(position), position, list.size());
	        return view;
	    }

}
複製代碼

在BannerPagerAdapter的getView方法中經過holderCreator.createViewHolder()拿到了咱們自定義的ViewHolder,此時即爲上邊的CustomPageViewHolder 。接下來在createView方法中調用CustomPageViewHolder的createView方法拿到咱們自定義的itemView,並經過holder.onBind方法將集合中的數據傳遞給了CustomPageViewHolder。到這裏咱們就完成了自定義item佈局以及item數據的綁定。佈局

2、BannerViewPager的泛型設計

上一節中講解了如何經過HolderCreator來支持任意的頁面佈局,那麼此時咱們應該會面臨一個難點,既然能夠支持任意的頁面佈局那麼BannerViewPager中接收的數據也應該時任意類型的。面對此問題咱們能夠引入泛型來實現。首先看BannerViewPager的泛型:post

public class BannerViewPager<T, VH extends ViewHolder> extends FrameLayout implements
        ViewPager.OnPageChangeListener {
        
	// 輪播數據集合
    private List<T> mList;

	private HolderCreator<VH> holderCreator;
    
	private void setupViewPager() {
        if (holderCreator != null) {
            BannerPagerAdapter<T, VH> bannerPagerAdapter =
                    new BannerPagerAdapter<>(mList, holderCreator);
     		...
        } else {
            throw new NullPointerException("You must set HolderCreator for BannerViewPager");
        }
    }
}
複製代碼

BannerViewPager有兩個泛型參數,第一個參數T是對應的數據類型,它用來做爲BannerViewPager中List集合的泛型。另外一個泛型參數VH規定了必須是繼承ViewHolder的類,用來做爲HolderCreator的泛型。而ViewHolder和HolderCreator均是一個帶有泛型參數的接口,其代碼以下:性能

public interface ViewHolder<T> {
    View createView(ViewGroup viewGroup,Context context, int position);
    
    /**
     * @param context context
     * @param data 實體類對象
     * @param position 當前位置
     * @param size 頁面個數
     */
    void onBind(Context context,T data,int position,int size);
}

public interface HolderCreator<VH extends ViewHolder> {
    /**
     * 建立ViewHolder
     */
    VH createViewHolder();
}
複製代碼

另外,T和VH兩個泛型也同時做爲BannerPagerAdapter的泛型參數:

public class BannerPagerAdapter<T, VH extends ViewHolder> extends PagerAdapter {

	private List<T> list;
	 
	public BannerPagerAdapter(List<T> list, HolderCreator<VH> holderCreator) {
	        this.list = list;
	        this.holderCreator = holderCreator;
	    }

}
複製代碼

能夠看到,咱們經過泛型約束,使得涉及到的相關類中的參數數據類型保持了同步。

3、如何實現無限循環輪播

關於ViewPager的無限循環無外乎兩種方案。第一種方案是在PagerAdapter的getCount中返回一個Integer.MAX_VALUE,即一個最大的Integer整數。而後將setCurrentItem的值設置爲 Integer.MAX_VALUE / 2,在滑動過程當中不斷取餘以此來達到一個無限循環輪播的假象。另一種方案是額外增長兩個ViewPager的item count,而後在第0個Item填充最後一條數據,在最後一個Item填充第0條數據。當右滑到第一個Item的時候將currentItem置爲pageSize-1,當滑動到最後一個Item的時候將currentItem置爲1,以此來達到一個無限循環的目的,此方案的示意圖以下:

無限循環示意圖
固然,這兩種實現方案各有優劣,各位讀者小夥伴能夠自行甄別。BannerViewPager的無限輪播使用的就是第二種方案。接下來,咱們就經過代碼具體來看BannerViewPager的內部實現。

上節中咱們已經提到了BannerPagerAdapter,接下來咱們將目光定位到BannerPagerAdapter的getCount方法。代碼以下:

@Override
    public int getCount() {
        return (isCanLoop && list.size() > 1) ? list.size() + 2 : list.size();
    }
複製代碼

在這個方法中先判斷ViewPager是否能夠循環。若是能夠循環而且集合的size大於1,那麼就在list.size()的基礎上加2。不然直接返回list.size()便可。 接下來,須要將ViewPager的Item和list中的數據對應起來。固然,若是在不能夠循環的狀況下,咱們將其一一對應便可。代碼以下:

private View createView(ViewHolder<T> holder, int position, ViewGroup container) {
        View view = null;
        if (list != null && list.size() > 0) {
            if (isCanLoop && list.size() > 1) {
               ...
            } else {
                view = holder.createView(container, container.getContext(), position);
                holder.onBind(container.getContext(), list.get(position), position, list.size());
            }
        }
        return view;
    }
複製代碼

可是若是在能夠循環而且list.size大於1的狀況下仍然讓數據一一對應,那麼就可能出現數組越界的狀況了。不要忘了,此時咱們在getCount中人爲的多添加了2個item。所以,此種狀況下,咱們須要將list的數據與item作一個變換對應,以避免出現數組越界或者頁面錯亂的狀況,這次完善後的createView方法以下:

private View createView(ViewHolder<T> holder, int position, ViewGroup container) {
        View view = null;
        if (list != null && list.size() > 0) {
            if (isCanLoop && list.size() > 1) {
                int size = list.size();
                if (position == 0) {
                    view = holder.createView(container, container.getContext(), list.size() - 1);
                    holder.onBind(container.getContext(), list.get(list.size() - 1), list.size() - 1, size);
                } else if (position == list.size() + 1) {
                    view = holder.createView(container, container.getContext(), 0);
                    holder.onBind(container.getContext(), list.get(0), 0, size);
                } else {
                    view = holder.createView(container, container.getContext(), position - 1);
                    holder.onBind(container.getContext(), list.get(position - 1), position - 1, size);
                }
            } else {
                view = holder.createView(container, container.getContext(), position);
                holder.onBind(container.getContext(), list.get(position), position, list.size());
            }
            setViewListener(view, position);
        }
        return view;
    }
複製代碼

到這裏無限循環的核心已經實現了。可是不要忘了,若是爲ViewPager添加了addOnPageChangeListener,那麼在onPageSelected(int position)方法及onPageScrolled(int position, float positionOffset, int positionOffsetPixels)方法中拿到的position都不是真正的list集合所對應的position。所以,在此種狀況下也須要對position作一個變換,來獲取到真實的position。在BannerViewPager中有以下代碼來獲取真實的position:

private int getRealPosition(int position) {
        if (isCanLoop) {
            if (position == 0) {
                return mList.size() - 1;
            } else if (position == mList.size() + 1) {
                return 0;
            } else {
                return --position;
            }
        } else {
            return position;
        }
    }
複製代碼

4、變幻無窮的Indicator

在最開始的設計中,BannerViewPager同其它大多Bannenr庫同樣,內部維護了一個Indicator的List集合用來存放Indicator的icon,而後根據頁面size動態的添加Indicator。顯然這樣的Indicator很是不靈活,若是UI以爲以前顏色很差看,須要換個顏色。你說,OK!不要緊,你給我切圖就行了。可是若是UI說我須要一個Indicator跟隨ViewPager滑動的效果,那麼此時你必定一臉茫然不知所措!因而和UI開啓了漫長的拉鋸戰...扯遠了,咱們繼續迴歸正題,考慮到這個問題後呢,再後續的版本中針對Indicator進行了屢次重構,實現了能夠支持多種Style的Indicator。而且在仍然不能知足需求的狀況下開發者能夠隨時定製本身的Indicator樣式。接下來,就來探究BannerViewPager是如何實現變幻無窮的Indicator的。 首先,定義了一個IIndicator的接口,該接口繼承了ViewPager.OnPageChangeListener。其代碼以下:

public interface IIndicator extends ViewPager.OnPageChangeListener {
    void setPageSize(int pageSize);

    void setNormalColor(int normalColor);

    void setCheckedColor(int checkedColor);

    void setSlideMode(IndicatorSlideMode slideStyle);

    void setIndicatorGap(int gap);

    void setIndicatorWidth(int normalIndicatorWidth, int checkedIndicatorWidth);

    void notifyDataChanged();
}

複製代碼

在BannerViewPager內部持有了IIndicator的實例:

public class BannerPagerAdapter<T, VH extends ViewHolder> extends PagerAdapter {
	
		// 輪播指示器
	    private IIndicator mIndicatorView;

		/**
	     * 設置自定義View指示器,自定義View須要須要繼承BaseIndicator或者實現IIndicator接口自行繪製指示器。
	     * 注意,一旦設置了自定義IndicatorView,經過BannerViewPager設置的部分IndicatorView參數將失效。
	     *
	     * @param customIndicator 自定義指示器
	     */
	    public BannerViewPager<T, VH> setIndicatorView(IIndicator customIndicator) {
	        if (customIndicator instanceof View) {
	            isCustomIndicator = true;
	            mIndicatorView = customIndicator;
	        }
	        return this;
	    }
}
複製代碼

所以,只要是繼承了View並實現了IIndicator接口均可以經過setIndicatorView方法來設置自定義的IndicatorView。雖然此時咱們將自定義的Indicator設置到了BannerViewPager的內部,那麼如何作到Indicator與關聯ViewPager關聯呢?繼續看BannerViewPager的代碼:

@Override
    public void onPageSelected(int position) {
        currentPosition = position;
        if (showIndicator && mIndicatorView != null) {
            mIndicatorView.onPageSelected(getRealPosition(position));
        }
    }

    @Override
    public void onPageScrollStateChanged(int state) {
        if (showIndicator && mIndicatorView != null) {
            mIndicatorView.onPageScrollStateChanged(state);
        }
       ...
    }

    @Override
    public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {
        if (showIndicator && mIndicatorView != null) {
            mIndicatorView.onPageScrolled(getRealPosition(position), positionOffset, positionOffsetPixels);
        }
    }
複製代碼

能夠看到在ViewPager的回調方法中,咱們調用了Indicator中與之對應的方法,所以只要在自定義的IndicatorView中重寫這些方法便可任意實現想要的效果了。 另外,由於大部分的Indicator滑動樣式都基本相似。所以,代碼中提供了一個Indicator的基類--BaseIndicatorView,這個類中處理了ViewPager的滑動以及對會用到的數據進行了一些簡單的計算,所以在自定義Indicator的時候咱們能夠直接繼承BaseIndicatorView便可。BaseIndicatorView的代碼以下:

public class BaseIndicatorView extends View implements IIndicator {
    /**
     * 頁面size
     */
    protected int pageSize;
    /**
     * 未選中時Indicator顏色
     */
    protected int normalColor;
    /**
     * 選中時Indicator顏色
     */
    protected int checkedColor;
    /**
     * Indicator間距
     */
    protected float indicatorGap;
    /**
     * 從一個點滑動到另外一個點的進度
     */
    protected float slideProgress;
    /**
     * 指示器當前位置
     */
    protected int currentPosition;
    /**
     * 指示器上一個位置
     */
    private int prePosition;
    /**
     * 是不是向右滑動,true向右,false向左
     */
    protected boolean slideToRight;
    /**
     * Indicator滑動模式,目前僅支持兩種
     *
     * @see IndicatorSlideMode#NORMAL
     * @see IndicatorSlideMode#SMOOTH
     */
    protected IndicatorSlideMode slideMode;

    protected float normalIndicatorWidth;
    protected float checkedIndicatorWidth;

    public BaseIndicatorView(Context context) {
        super(context);
    }

    public BaseIndicatorView(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
    }

    public BaseIndicatorView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        normalIndicatorWidth = DpUtils.dp2px(8);
        checkedIndicatorWidth = normalIndicatorWidth;
        indicatorGap = normalIndicatorWidth;
        normalColor = Color.parseColor("#8C18171C");
        checkedColor = Color.parseColor("#8C6C6D72");
        slideMode = IndicatorSlideMode.NORMAL;
    }

    @Override
    public void onPageSelected(int position) {
        if (slideMode == IndicatorSlideMode.NORMAL) {
            currentPosition = position;
            slideProgress = 0;
            invalidate();
        } else if (slideMode == IndicatorSlideMode.SMOOTH) {
            if (position == 0 && slideToRight) {
//                    Log.e(tag, "slideToRight position-----》" + position);
                currentPosition = 0;
                slideProgress = 0;
                invalidate();

            } else if (position == pageSize - 1 && !slideToRight) {
                currentPosition = pageSize - 1;
                slideProgress = 0;
                invalidate();
            }
        }
    }

    private static final String tag = "BaseIndicatorView";

    @Override
    public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {
        if (slideMode == IndicatorSlideMode.SMOOTH) {
            if ((prePosition == 0 && position == pageSize - 1)) {
                slideToRight = false;
            } else if (prePosition == pageSize - 1 && position == 0) {
//                Log.e(tag, "prePosition-----》" + prePosition);
//                Log.e(tag, "position-----》" + position);
                slideToRight = true;
            } else {
                slideToRight = (position + positionOffset - prePosition) > 0;
            }
            //  TODO 解決滑動過快時positionOffset不會等0的狀況
            if (positionOffset == 0) {
                prePosition = position;
            }
            if (!(position == pageSize - 1 && slideToRight || (position == pageSize - 1 && !slideToRight))) {
                slideProgress = (currentPosition == pageSize - 1) && slideToRight ? 0 : positionOffset;
                currentPosition = position;
                invalidate();
            }
        }
    }

    @Override
    public void setPageSize(int pageSize) {
        this.pageSize = pageSize;
        requestLayout();
    }
    ...
}

複製代碼

固然,自定義Indicator是在BannerViewPager提供的樣式不能知足需求時的作法。更多的狀況可能BannerViewPager內置樣式就已經知足咱們需求了,並不須要咱們本身去自定義Indicator。所以,在BannerViewPager內部封裝了諸多設置Indicator的屬性的方法,列舉部分代碼:

/**
     * 設置Indicator樣式
     *
     * @param indicatorStyle indicator樣式,目前有圓和斷線兩種樣式
     *                       {@link IndicatorStyle#CIRCLE}
     *                       {@link IndicatorStyle#DASH}
     */
    public BannerViewPager<T, VH> setIndicatorStyle(IndicatorStyle indicatorStyle) {
        mIndicatorStyle = indicatorStyle;
        return this;
    }

  /**
     * 設置IndicatorView滑動模式,默認值{@link IndicatorSlideMode#SMOOTH}
     *
     * @param slideMode Indicator滑動模式
     * @see com.zhpan.bannerview.enums.IndicatorSlideMode#NORMAL
     * @see com.zhpan.bannerview.enums.IndicatorSlideMode#SMOOTH
     */
    public BannerViewPager<T, VH> setIndicatorSlideMode(IndicatorSlideMode slideMode) {
        mIndicatorSlideMode = slideMode;
        return this;
    }

	/**
     * @param checkedColor 選中時指示器顏色
     * @param normalColor  未選中時指示器顏色
     */
    public BannerViewPager<T, VH> setIndicatorColor(@ColorInt int normalColor,
                                                    @ColorInt int checkedColor) {
        indicatorCheckedColor = checkedColor;
        indicatorNormalColor = normalColor;
        return this;
    }

 /**
     * 設置Indicator半徑
     *
     * @param normalRadius 未選中時半徑
     * @param checkRadius  選中時半徑
     */
    public BannerViewPager<T, VH> setIndicatorRadius(int normalRadius, int checkRadius) {
        this.normalIndicatorWidth = normalRadius * 2;
        this.checkedIndicatorWidth = checkRadius * 2;
        return this;
    }
    
 /**
     * 設置指示器位置
     *
     * @param gravity 指示器位置
     *                {@link BannerViewPager#CENTER}
     *                {@link BannerViewPager#START}
     *                {@link BannerViewPager#END}
     */
    public BannerViewPager<T, VH> setIndicatorGravity(@IndicatorGravity int gravity) {
        this.gravity = gravity;
        return this;
    }
    ...
複製代碼

5、遇到的其餘問題及解決方案

在BannerViewPager的開發過程當中碰到不少小問題,雖然並不難解決,可是仍是有必要記錄一下。

1.手指滑動頁面過程當中應中止自動輪播

自動輪播的功能是經過Handler來實現的。經過postDelayed開啓輪播,經過removeCallbacks中止輪播。代碼以下:

/**
     * 開啓輪播
     */
    public void startLoop() {
        if (!isLooping && isAutoPlay && mList.size() > 1) {
            mHandler.postDelayed(mRunnable, interval);
            isLooping = true;
        }
    }

    /**
     * 中止輪播
     */
    public void stopLoop() {
        if (isLooping) {
            mHandler.removeCallbacks(mRunnable);
            isLooping = false;
        }
    }
複製代碼

若是在手指滑動的過程當中沒有中止輪播,體驗上來講很是很差。所以,須要處理這種狀況。解決方案是重寫ViewPager的setOnTouchListener方法,監聽手指滑動的時候中止輪播,擡起手指的時候開啓輪播。代碼以下:

private void setTouchListener() {
        mViewPager.setOnTouchListener((v, event) -> {
            int action = event.getAction();
            switch (action) {
                case MotionEvent.ACTION_DOWN:
                case MotionEvent.ACTION_MOVE:
                    isLooping = true;
                    stopLoop();
                    break;
                case MotionEvent.ACTION_UP:
                case MotionEvent.ACTION_CANCEL:
                    isLooping = false;
                    startLoop();
                default:
                    break;
            }
            return false;
        });
    }
複製代碼

2.性能上的一些優化

咱們知道,在ViewPager每次切換頁面的時候都會調用instantiateItem去實例化ItemView,也就意味着咱們在這個方法中經過ViewHolder的createView方法每次切換頁面都會被調用從新初始化綁定數據。這樣對程序來講是一種性能上的浪費。針對這種狀況,咱們能夠作些優化操做。咱們在BannerPagerAdapter中維護一個List mViewList集合,用來存放建立出來的itemView.在itemView初始化成功後,爲其設置tag並保存到集合中,當在此切換頁面時咱們從集合中取出itemView並對比tag,若是一致則直接使用便可。這樣就避免了重複的建立對象,形成一些性能開銷。具體代碼以下:

public class BannerPagerAdapter<T, VH extends ViewHolder> extends PagerAdapter {

	private List<View> mViewList = new ArrayList<>();

	private View findViewByPosition(ViewGroup container, int position) {
	        for (View view : mViewList) {
	            if (((int) view.getTag()) == position && view.getParent() == null) {
	                return view;
	            }
	        }
	        View view = getView(position, container);
	        view.setTag(position);
	        mViewList.add(view);
	        return view;
	    }
}
複製代碼

6、總結及致謝

到這裏《打造一個絲滑般自動輪播無限循環Android庫》的兩篇文章就所有結束了,上一篇文章主要着重講解了BannerViewPager的功能及用法,而本篇文章則詳細的講解了BannerViewPager的實現原理。就目前而言,BannerViewPager中不少地方還有很大值得優化的空間或者有些功能存在一些小問題。例如,目前IndicatorView部分的代碼目前仍是比較亂,有待優化,以及IndicatorView的滑動模式設置爲SMOOTH時候滑動效果還有些bug。這些都是後面版本主須要優化解決的。固然,若是你有好的解決方案歡迎在文章下方留言,也能夠直接到github提交pull request。若是你有什麼好的建議或者遇到什麼問題也歡迎在文章下方留言。

在這裏要特別感謝saiwu-bigkoo大神的Android-ConvenientBanner庫以及youth5201314大神的banner庫。BannerViewPager中的不少思想來自這兩個庫。BannerViewPager中內置的四個ViewPager Transform來自ViewPagerTransforms庫,在此表示感謝。同時還要感謝玩Android提供的接口支持。

最後仍是要貼上源碼地址,歡迎star、fork

BannerViewPager

相關文章
相關標籤/搜索