《打造一個絲滑般自動輪播無限循環Android庫》android
《打造一個絲滑般自動輪播無限循環Android庫--源碼解析》git
上篇文章《打造一個絲滑般自動輪播無限循環Android庫》介紹了BannerViewPager的基礎功能及使用方法。咱們瞭解了BannerViewPager不但能夠支持任意的頁面佈局,並且能夠支持任意的Indicator。那麼BannerViewPager的高度可定製性及無限輪播是如何實現的呢?本篇文章將深刻源碼來了解BannerViewPager的設計思路。github
產品的需求變幻無窮,你永遠也猜不到下一步產品會給你提一個什麼樣的需求。所以對於一個比較人性化的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數據的綁定。佈局
上一節中講解了如何經過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;
}
}
複製代碼
能夠看到,咱們經過泛型約束,使得涉及到的相關類中的參數數據類型保持了同步。
關於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;
}
}
複製代碼
在最開始的設計中,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;
}
...
複製代碼
在BannerViewPager的開發過程當中碰到不少小問題,雖然並不難解決,可是仍是有必要記錄一下。
自動輪播的功能是經過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;
});
}
複製代碼
咱們知道,在ViewPager每次切換頁面的時候都會調用instantiateItem去實例化ItemView,也就意味着咱們在這個方法中經過ViewHolder的createView方法每次切換頁面都會被調用從新初始化綁定數據。這樣對程序來講是一種性能上的浪費。針對這種狀況,咱們能夠作些優化操做。咱們在BannerPagerAdapter中維護一個List
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;
}
}
複製代碼
到這裏《打造一個絲滑般自動輪播無限循環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