雖然網(wǎng)上這種自定義ViewPager實現(xiàn)的banner挺多纳寂,但還是自己動手寫下會好很多。權(quán)當(dāng)練手吧圃伶。
一堤如、效果預(yù)覽
二、需求分析
1.重寫ViewPager控件實現(xiàn)bannerView
2.需要定時輪播
3.需要無限輪播
4.內(nèi)存優(yōu)化
5.ViewPager自身切換速率太快窒朋,需要重新設(shè)置
三搀罢、自定義View套路代碼
直接繼承自ViewPager
public class BannerViewPager extends ViewPager {
//內(nèi)存優(yōu)化,復(fù)用的View.
private SparseArray<View> mConvertViews;
public BannerViewPager(Context context) {
this(context, null);
}
public BannerViewPager(Context context, AttributeSet attrs) {
super(context, attrs);
//內(nèi)存優(yōu)化侥猩,緩存頁面數(shù)據(jù)删性,下面會講到
mConvertViews = new SparseArray<>();
}
}
四蛙婴、采用Adapter設(shè)計模式
原始的ViewPager就是采用了adapter設(shè)計模式奢入,方便用戶使用展辞,所以這里我們也仿照源碼思想,采用adapter設(shè)計模式划提。
新建一個BannerAdapter類:
public abstract class BannerAdapter {
//Observable枫弟,觀察者模式,下面講到
private BannerViewPager.AdapterDataObservable mObservable = new BannerViewPager.AdapterDataObservable();
//返回頁面的View
public abstract View getView(int position);
//返回BannerView的頁面數(shù)量鹏往,如果是無限輪播淡诗,記住要返回真是頁面數(shù)量。
public abstract int getCount();
//ViewPager頁面切換特效伊履,方便自定義袜漩,返回null采用默認(rèn)特效
public Transformer getTransformer() {
return null;
}
//************************** 觀察者設(shè)計模式 **************************
public void registerAdapterDataObserver(BannerViewPager.AdapterDataObserver observer) {
mObservable.registerObserver(observer);
}
public void unregisterAdapterDataObserver(BannerViewPager.AdapterDataObserver observer) {
mObservable.unregisterObserver(observer);
}
public final void notifyDataSetChanged() {
mObservable.notifyChanged();
}
}
BannerViewPager中定義setAdapter方法:
public void setAdapter(BannerAdapter adapter) {
if (mBannerAdapter != null) {
mBannerAdapter.unregisterAdapterDataObserver(mObserver);
}
this.mBannerAdapter = adapter;
if (mBannerAdapter == null) {
throw new IllegalArgumentException("BannerAdapter不能為null");
}
mBannerAdapter.registerAdapterDataObserver(mObserver);
//設(shè)置切換動畫
if (mBannerAdapter.getTransformer() != null) {
mTransformer = mBannerAdapter.getTransformer();
mTransformer.bind(this);
}
mBannerPagerAdapter = new BannerPagerAdapter();
setAdapter(mBannerPagerAdapter);
setCurrentItem(mBannerAdapter.getCount());
}
在這個方法中,我們主要做了這幾件事:
1.觀察者設(shè)計模式的處理湾碎。
2.bannerViewPager翻頁動畫。
3.給ViewPager設(shè)置真實的adapter:BannerPagerAdapter奠货,下面就看下這個Adapter內(nèi)部類介褥。
在BannerViewPager中定義BannerPagerAdapter類:
private class BannerPagerAdapter extends PagerAdapter {
@Override
public int getCount() {
return mBannerAdapter.getCount() == 1 ? 1 : Integer.MAX_VALUE;
}
@Override
public boolean isViewFromObject(View view, Object object) {
return view == object;
}
@Override
public Object instantiateItem(ViewGroup container, int position) {
//先轉(zhuǎn)化為實際position
final int realPosition = position % mBannerAdapter.getCount();
View bannerItemView = mBannerAdapter.getView(realPosition);
container.addView(bannerItemView);
//點擊事件
bannerItemView.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
if (mItemClickListener != null) {
mItemClickListener.onBannerItemClick(realPosition);
}
}
});
return bannerItemView;
}
@Override
public void destroyItem(ViewGroup container, int position, Object object) {
container.removeView((View) object);
}
}
其中有頁面點擊事件處理,很簡單的回調(diào)
public BannerItemClickListener mItemClickListener;
public interface BannerItemClickListener {
void onBannerItemClick(int position);
}
/**
* 條目點擊事件
*/
public void setOnItemClickListener(BannerItemClickListener listener) {
this.mItemClickListener = listener;
}
這里說下無限輪播的思路,奧妙都在BannerPagerAdapter 這個類中柔滔,
getCount方法返回Integer.MAX_VALUE溢陪,代表ViewPager有2^31-1個頁面
instantiateItem方法中根據(jù)postion返回每個頁面的View,postion最大為Integer.MAX_VALUE-1睛廊,而實際頁面數(shù)只有有限的幾個形真,所以需要做下轉(zhuǎn)換final int realPosition = position % mBannerAdapter.getCount();
這樣處理后,假設(shè)有實際頁面ABC超全,最終的效果就是[position=0,頁面A] [position=1,頁面B] [position=2,頁面C] [position=3,頁面A] [position=4,頁面B] [position=5,頁面C] [position=6,頁面A] [position=7,頁面B] ...
五咆霜、自動輪播
自動輪播我們采用handler,handler.postDelayed(Runnable r, long delayMillis)嘶朱,同時傳入的runable的run方法里蛾坯,再次postDelayed調(diào)用自身runnable方法,形成一個遞歸疏遏。
//初始化handler
@SuppressLint("HandlerLeak")
private void initHandler() {
mHandler = new Handler();
}
//定義一個runnable脉课,并在run方法內(nèi)再次mHandler.postDelayed自身
private final Runnable task = new Runnable() {
@Override
public void run() {
setCurrentItem(getCurrentItem() + 1);
mHandler.postDelayed(task, mCutDownTime);
}
};
接著定義開始和停止?jié)L動的兩個方法,其實就是控制handler的執(zhí)行和停止任務(wù):
public void startScroll() {
if (mBannerAdapter == null) {
return;
}
boolean scrollable = mBannerAdapter.getCount() != 1;
if (scrollable && mHandler != null) {
mHandler.postDelayed(task, mCutDownTime);
}
}
public void stopScroll() {
if (mBannerAdapter == null) {
return;
}
if (mHandler != null) {
mHandler.removeCallbacks(task);
}
}
mCutDownTime就是輪播間隔時間财异,當(dāng)然也得要提供給用戶api控制這個間隔
private int mCutDownTime = 5000;
/**
* 設(shè)置每個條目切換時間
*/
public void setCutDownTime(int millis) {
this.mCutDownTime = millis;
}
到此倘零,自動輪播處理完畢。
六戳寸、自定義Scroller改變ViewPager默認(rèn)切換速率
通過查看源碼我們發(fā)現(xiàn)呈驶,ViewPager的滾動內(nèi)部采用scroller控制,那么我們自己定義一個scroller將他自帶的替換掉即可庆揩。
public class BannerScroller extends Scroller {
private int mDuration = 1000;
public BannerScroller(Context context) {
super(context);
}
public void setScrollerDuration(int duration) {
this.mDuration = duration;
}
@Override
public void startScroll(int startX, int startY, int dx, int dy) {
super.startScroll(startX, startY, dx, dy,mDuration);
}
@Override
public void startScroll(int startX, int startY, int dx, int dy, int duration) {
super.startScroll(startX, startY, dx, dy, mDuration);
}
}
mDuration即控制滑動時間的變量俐东。
在BannerViewPager的構(gòu)造方法中,替換默認(rèn)mScroller為我們自定義的Scroller订晌,不過遺憾的是ViewPager并沒有提供相關(guān)api虏辫,且mScroller為私有的,我們這里只能采取一些手段了:反射锈拨。
public BannerViewPager(Context context, AttributeSet attrs) {
super(context, attrs);
mConvertViews = new SparseArray<>();
//反射修改ViewPager默認(rèn)的滾動速率
try {
final Field field = ViewPager.class.getDeclaredField("mScroller");
mBannerScroller = new BannerScroller(context);
field.setAccessible(true);
field.set(this, mBannerScroller);
} catch (Exception e) {
e.printStackTrace();
}
initHandler();
}
到這里砌庄,自定義ViewPager也算勉強可以用了,不過還有不少值得優(yōu)化的地方奕枢。
七娄昆、內(nèi)存優(yōu)化
回到BannerPagerAdapter這個類中,instantiateItem方法缝彬,每次獲取頁面萌焰,直接調(diào)用mBannerAdapter.getView(realPosition);
,我們的輪播圖條目數(shù)可以有Integer.MAX_VALUE-1個的谷浅,這個方法就要調(diào)用無數(shù)次扒俯,頁面就要初始化無數(shù)次奶卓,而實際的頁面只有有限的幾個,能不能優(yōu)化呢撼玄,將初始化后的頁面緩存起來即可夺姑!
//用SparseArray緩存,鍵為postion掌猛,真實的postion盏浙;值為頁面數(shù)據(jù)。
private SparseArray<View> mConvertViews;
public View getConvertView(int position) {
final View convertView = mConvertViews.get(position, null);
if (convertView == null || convertView.getParent() != null) {
//健壯性判斷荔茬,如果緩存的convertView有它的parent废膘,那么返回null。
return null;
}
return convertView;
}
//內(nèi)存優(yōu)化后的BannerPagerAdapter
private class BannerPagerAdapter extends PagerAdapter {
@Override
public int getCount() {
return mBannerAdapter.getCount() == 1 ? 1 : Integer.MAX_VALUE;
}
@Override
public boolean isViewFromObject(View view, Object object) {
return view == object;
}
@Override
public Object instantiateItem(ViewGroup container, int position) {
//先轉(zhuǎn)化為實際position
final int realPosition = position % mBannerAdapter.getCount();
View bannerItemView = getConvertView(realPosition);
//先從緩存中拿兔院,如果拿不到殖卑,那么就重新初始化
if (bannerItemView == null) {
bannerItemView = mBannerAdapter.getView(realPosition);
}
container.addView(bannerItemView);
bannerItemView.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
if (mItemClickListener != null) {
mItemClickListener.onBannerItemClick(realPosition);
}
}
});
return bannerItemView;
}
@Override
public void destroyItem(ViewGroup container, int position, Object object) {
container.removeView((View) object);
//頁面從viewpager移除的同時,緩存到mConvertViews中
mConvertViews.put(position % mBannerAdapter.getCount(), (View) object);
}
}
八坊萝、bug修復(fù)
在使用中孵稽,存在這樣一個現(xiàn)象,比如嵌套在RecyclerView中使用十偶,如果該bannerViewPager被滑出屏幕再滑進(jìn)屏幕菩鲜,ViewPager的第一次切換沒有動畫,很生硬惦积。
查看源碼:
@Override
protected void onAttachedToWindow() {
super.onAttachedToWindow();
mFirstLayout = true;
}
這個onAttachedToWindow方法中接校,將mFirstLayout 置為true,在切換時狮崩,會先判斷該字段蛛勉,只有mFirstLayout =false時才啟用滑動動畫,所以重寫該方法即可睦柴,同樣mFirstLayout 為私有诽凌,采取反射
@Override
protected void onAttachedToWindow() {
super.onAttachedToWindow();
try {
//緩存反射字段
if (mFirstLayoutField == null) {
//反射耗時,緩存一下
mFirstLayoutField = ViewPager.class.getDeclaredField("mFirstLayout");
}
mFirstLayoutField.setAccessible(true);
mFirstLayoutField.set(this,false);
} catch (Exception e) {
e.printStackTrace();
}
startScroll();
}
//離開屏幕坦敌,暫停輪播侣诵。
@Override
protected void onDetachedFromWindow() {
super.onDetachedFromWindow();
stopScroll();
}
九、處理點擊暫停
手指在觸摸或滑動bannerView的時候不應(yīng)自動輪播狱窘,需要停止杜顺。
開始我想重寫onTouchEvent或onInterceptTouchEvent,都沒能達(dá)到效果蘸炸,于是我就翻看了其他BannerView的處理躬络,發(fā)現(xiàn)他們都是在dispatchTouchEvent中處理的。
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
int action = ev.getAction();
if (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_CANCEL
|| action == MotionEvent.ACTION_OUTSIDE) {
startScroll();
} else if (action == MotionEvent.ACTION_DOWN) {
stopScroll();
}
return super.dispatchTouchEvent(ev);
}
后來我分析了一下事件分發(fā):
對于onTouchEvent搭儒,
ACTION_DOWN 不經(jīng)過此方法洗鸵,向下傳遞到它的子view時越锈,子view的onTouchEvent返回true(bannerViewPager的子view設(shè)置了點擊事件),因此事件不再回傳給該BannerViewPager。
ACTION_MOVE膘滨,ACTION_UP經(jīng)過此方法,子view不處理這兩種事件稀拐,交給該BannerViewPager處理拖拽滑動事件火邓。
對于onInterceptTouchEvent,
ACTION_DOWN 經(jīng)過此方法
ACTION_MOVE ACTION_UP不經(jīng)過此方法德撬,因為在move和up的時候铲咨,onTouchEvent處理了拖拽滑動事件,此時mFirstTouchTarget被置為null, dispatchTouchEvent處理分發(fā)事件時蜓洪,就不走onInterceptTouchEvent方法了纤勒。
十、加入切換動畫
我使用了仿魅族商店的切換動畫隆檀,具體原理看這里
http://www.reibang.com/p/e67aa68d2766
算法什么的我就無恥的直接拿過來用了
定義接口:
public interface Transformer {
void bind(BannerViewPager viewPager);
int getChildDrawingOrder(int childCount, int n);
}
仿魅族切換動畫
public class MZTransformImpl implements Transformer{
//中間放大系數(shù)
private float mScaleMax = 1.0f;
//兩邊縮小系數(shù)
private float mScaleMin = 0.9f;
//重疊部分
private float mCoverWidth = 80f;
private ArrayList<Integer> childCenterXAbs = new ArrayList<>();
private SparseArray<Integer> childIndex = new SparseArray<>();
private BannerViewPager mBannerViewPager;
@Override
public void bind(BannerViewPager viewPager) {
this.mBannerViewPager = viewPager;
mBannerViewPager.setPageTransformer(true, new SPageTransformer());//默認(rèn)調(diào)用了 setChildrenDrawingOrderEnabledCompat(true);使得getChildDrawingOrder起作用
mBannerViewPager.setClipToPadding(false);
mBannerViewPager.setOverScrollMode(ViewPager.OVER_SCROLL_NEVER);
}
public int getChildDrawingOrder(int childCount, int n) {
if (n == 0 || childIndex.size() != childCount) {
childCenterXAbs.clear();
childIndex.clear();
int viewCenterX = getViewCenterX(mBannerViewPager);
for (int i = 0; i < childCount; ++i) {
int indexAbs = Math.abs(viewCenterX - getViewCenterX(mBannerViewPager.getChildAt(i)));
//兩個距離相同摇天,后來的那個做自增,從而保持abs不同
if (childIndex.get(indexAbs) != null) {
++indexAbs;
}
childCenterXAbs.add(indexAbs);
childIndex.append(indexAbs, i);
}
Collections.sort(childCenterXAbs);//1,0,2 0,1,2
}
//那個item距離中心點遠(yuǎn)一些恐仑,就先draw它泉坐。(最近的就是中間放大的item,最后draw)
return childIndex.get(childCenterXAbs.get(childCount - 1 - n));
}
private int getViewCenterX(View view) {
int[] array = new int[2];
view.getLocationOnScreen(array);
return array[0] + view.getWidth() / 2;
}
class SPageTransformer implements ViewPager.PageTransformer {
private float reduceX = 0.0f;
private float itemWidth = 0;
private float offsetPosition = 0f;
@Override
public void transformPage(View view, float position) {
if (offsetPosition == 0f) {
float paddingLeft = mBannerViewPager.getPaddingLeft();
float paddingRight = mBannerViewPager.getPaddingRight();
float width = mBannerViewPager.getMeasuredWidth();
offsetPosition = paddingLeft / (width - paddingLeft - paddingRight);
}
float currentPos = position - offsetPosition;
if (itemWidth == 0) {
itemWidth = view.getWidth();
//由于左右邊的縮小而減小的x的大小的一半
reduceX = (2.0f - mScaleMax - mScaleMin) * itemWidth / 2.0f;
}
if (currentPos <= -1.0f) {
view.setTranslationX(reduceX + mCoverWidth);
view.setScaleX(mScaleMin);
view.setScaleY(mScaleMin);
} else if (currentPos <= 1.0) {
float scale = (mScaleMax - mScaleMin) * Math.abs(1.0f - Math.abs(currentPos));
float translationX = currentPos * -reduceX;
if (currentPos <= -0.5) {//兩個view中間的臨界,這時兩個view在同一層裳仆,左側(cè)View需要往X軸正方向移動覆蓋的值()
view.setTranslationX(translationX + mCoverWidth * Math.abs(Math.abs(currentPos) - 0.5f) / 0.5f);
} else if (currentPos <= 0.0f) {
view.setTranslationX(translationX);
} else if (currentPos >= 0.5) {//兩個view中間的臨界腕让,這時兩個view在同一層
view.setTranslationX(translationX - mCoverWidth * Math.abs(Math.abs(currentPos) - 0.5f) / 0.5f);
} else {
view.setTranslationX(translationX);
}
view.setScaleX(scale + mScaleMin);
view.setScaleY(scale + mScaleMin);
} else {
view.setScaleX(mScaleMin);
view.setScaleY(mScaleMin);
view.setTranslationX(-reduceX - mCoverWidth);
}
}
}
}
回到BannerViewPager類中,setAdapter方法內(nèi)設(shè)置切換動畫
public void setAdapter(BannerAdapter adapter) {
```
//設(shè)置切換動畫
if (mBannerAdapter.getTransformer() != null) {
mTransformer = mBannerAdapter.getTransformer();
mTransformer.bind(this);
}
```
}
重寫getChildDrawingOrder控制繪制順序
/**
* 控制子View的繪制順序
*/
@Override
protected int getChildDrawingOrder(int childCount, int i) {
if (mTransformer != null) {
return mTransformer.getChildDrawingOrder(childCount, i);
}
return super.getChildDrawingOrder(childCount, i);
}
注意歧斟,如果要采用仿魅族切換樣式纯丸,在BannerViewpager需要設(shè)置paddingLeft和paddingRight,在xml中定義即可静袖。
十一觉鼻、最后講下觀察者設(shè)計模式
源碼中Adaper里很多到用到了觀察者模式,Adapter中數(shù)據(jù)變化勾徽,調(diào)用notifyDataSetChanged滑凉,那么對應(yīng)的View也會變化,這都是有一定模板套路的喘帚,也可參考上一篇http://www.reibang.com/p/d75edebb6c8f的講解畅姊。
這里就總結(jié)下套路:
首先要弄明白,誰是觀察者(observer)吹由,誰是被觀察者(observable)
記住一點:被觀察者發(fā)生變化若未,觀察者就會響應(yīng)變化。
舉個例子倾鲫,手機是被觀察者吧粗合,人就是觀察者萍嬉,手機來微信了,就是被觀察者發(fā)生變化隙疚,我們會很自然的去打開微信查看壤追,這就是觀察者響應(yīng)變化。
類比一下供屉,adapter就是被觀察者行冰,BannerViewpager就是觀察者,因為adapter數(shù)據(jù)變化伶丐,BannerViewpager得要響應(yīng)改變ui悼做。
在代碼世界里,這種響應(yīng)是如何做到的呢哗魂,為什么observer會響應(yīng)observable的變化肛走,說白了就是observable中持有了observer的引用,observable發(fā)生變化后录别,再主動調(diào)用observer相關(guān)變化的方法即可朽色。
開始上代碼,BannerViewPager中:
private final BannerDataObserver mObserver = new BannerDataObserver();
static class AdapterDataObservable extends Observable<AdapterDataObserver> {
public void notifyChanged() {
// since onChanged() is implemented by the app, it could do anything, including
// removing itself from {@link mObservers} - and that could cause problems if
// an iterator is used on the ArrayList {@link mObservers}.
// to avoid such problems, just march thru the list in the reverse order.
for (int i = mObservers.size() - 1; i >= 0; i--) {
mObservers.get(i).onChanged();
}
}
}
public static abstract class AdapterDataObserver {
public void onChanged() {
}
}
private class BannerDataObserver extends AdapterDataObserver{
@Override
public void onChanged() {
mBannerPagerAdapter.notifyDataSetChanged();
}
}
BannerAdapter中:
private BannerViewPager.AdapterDataObservable mObservable = new BannerViewPager.AdapterDataObservable();
//************************** 觀察者設(shè)計模式 **************************
public void registerAdapterDataObserver(BannerViewPager.AdapterDataObserver observer) {
mObservable.registerObserver(observer);
}
public void unregisterAdapterDataObserver(BannerViewPager.AdapterDataObserver observer) {
mObservable.unregisterObserver(observer);
}
public final void notifyDataSetChanged() {
mObservable.notifyChanged();
}
在setAdapter(BannerAdapter adapter)中庶灿,將觀察者注冊給被觀察者就完事了纵搁,實質(zhì)就是依賴注入。
public void setAdapter(BannerAdapter adapter) {
if (mBannerAdapter != null) {
mBannerAdapter.unregisterAdapterDataObserver(mObserver);
}
this.mBannerAdapter = adapter;
if (mBannerAdapter == null) {
throw new IllegalArgumentException("BannerAdapter不能為null");
}
mBannerAdapter.registerAdapterDataObserver(mObserver);
...
}
這樣往踢,調(diào)用BannerAdapter的notifyDataSetChanged方法就是調(diào)用 mObservable.notifyChanged();->AdapterDataObserver.onChanged()->mBannerPagerAdapter.notifyDataSetChanged();最終ui發(fā)生改變腾誉。