ViewPager封裝輪播效果+指示器 實現一行程式碼展示輪播圖

無量變何以質變
概述
平時應用開發中首頁經常會有一個Banner輪播的展示,不可避免的需要封裝一個自定義View,在使用的時候能夠方便的只用一句程式碼設定圖片地址集合,就可以啟動輪播效果,本文將通過ViewPager一步步對輪播圖進行實現,最終效果如下:

輪播.gif
需要定製的特性
1.是否顯示指示器
2.指示器圓點大小、間距
3.輪播自動切換的間隔時長
4.是否自動輪播
5.左右無邊界輪播
注:目前暫時只支援url的圖片形式,其他形式例如本地圖片可自行在Adapter中instantiateItem方法進行調整,且本文的圖片展示是依賴Glide來展示。
實現思路
我們都知道ViewPager是可以左右滑動的,並且可以設定Adapter, 那如果能夠設定為無限大,且每隔一段時間就呼叫滑動,即可達到輪播效果,主要需要解決以下幾個問題:如何實現無限迴圈,自動輪播以及相容手勢滑動。
1)實現無限迴圈
首先實現左右無限迴圈的效果,思路就是將ViewPager的getCount()返回為Integer.MAX_VALUE,然後在ViewPager每次切換Item的時候,會呼叫PagerAdapter的instantiateItem方法,這個方法返回當前準備切換到的下標,由於我們設定的Item數量是Integer.MAX_VALUE,因此這裡返回的下標有可能是0-2147483647。
假設我們要顯示的banner圖總共有3張,那在第3張即將切換到第1張的時候,需要重新將position置為0,因此可以採用對下標取餘的方式讓position可以一直處於0-3的無限迴圈之中。
關鍵程式碼如下:
private class InnerPagerAdapter extends PagerAdapter { public InnerPagerAdapter() {} @Override public int getCount() { return Integer.MAX_VALUE; } @Override public boolean isViewFromObject(View arg0, Object arg1) { return arg0 == arg1; } @Override public Object instantiateItem(ViewGroup container, int position) { position %= mBannerUrlList.size(); if (position < 0) { position = mBannerUrlList.size() + position; } ImageView bannerIv = new ImageView(getContext()); bannerIv.setScaleType(ImageView.ScaleType.CENTER_CROP); bannerIv.setLayoutParams(new LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT)); Glide.with(getContext()).load(mBannerUrlList.get(position)).into(bannerIv); container.addView(bannerIv); //bannerIv.setLayoutParams(new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT)); return bannerIv; } @Override public void destroyItem(ViewGroup container, int position, Object object) { } }
2)實現自動輪播
實現了左右無限輪播,接下來就要讓它能夠自動動起來,這裡採用handler的方式,每次將下標+1之後setCurrentItem為新的下標,關鍵程式碼如下:
/** * 是否自動滑動 */ private boolean mIsAutoScroll = true; /** * 預設頁面之間自動切換的時間間隔 */ private long mDelayTime = 2000; //播放標誌 private boolean isPlay = false; //觸發輪播的訊息標誌位 private final int PLAY = 0x123; /** * 輪播計時器 */ private Handler mHandler = new Handler() { public void handleMessage(android.os.Message msg) { if (msg.what == PLAY && mIsAutoScroll) { mBannerViewPager.setCurrentItem(mCurrentIndex); if (isPlay) { play(); } } } }; public void startPlay(long delayMillis) { isPlay = true; mDelayTime = delayMillis; play(); } private void play() { mCurrentIndex++; mHandler.sendEmptyMessageDelayed(PLAY, mDelayTime); }
3)相容手勢滑動
實現了自動輪播之後,我們還要相容使用者主動滑動的場景,即使用者主動滑動時,應該暫停自動輪詢,先以使用者主動滑動的動作為主,當用戶放開手指之後,再重新繼續自動輪詢,既然要區分是否是手勢滑動,可以在ViewPager的滑動監聽介面 onPageScrollStateChange 中去判斷,如下:
/** * ViewPager滑動監聽 */ ViewPager.OnPageChangeListener mPageListener = new ViewPager.OnPageChangeListener() { @Override public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) { } @Override public void onPageSelected(int position) { } @Override public void onPageScrollStateChanged(int state) { onPageScrollStateChange(state); } }; /** * 手指滑動時暫停自動輪播,手指鬆開時重新啟動自動輪播 * * @param state */ private void onPageScrollStateChange(int state) { if (!mIsAutoScroll) { return; } switch (state) { case ViewPager.SCROLL_STATE_IDLE: if (!mGestureScroll) { return; } mGestureScroll = false; mHandler.removeMessages(PLAY); mHandler.sendEmptyMessageDelayed(PLAY, 100); break; case ViewPager.SCROLL_STATE_DRAGGING: // 手指滑動時,清除播放下一張,防止滑動過程中自動播放下一張 mGestureScroll = true; mHandler.removeMessages(PLAY); break; default: break; } }
ViewPager的SCRPLL_STATE_IDLE狀態是表示使用者手指鬆開時,這個時候就繼續sendMessage啟動輪詢,SCROLL_STATE_DRAGGING狀態表示使用者手指正在滑動過程中,removeMessage將當前的輪詢暫停
4)新增輪播指示器
以上完成了ViewPager的部分,實現了輪播圖的自動輪詢效果,但一般輪播圖都會有個小小的指示器來讓使用者感知當前處於哪個banner,一共有多少個banner,所以我們還需套自定義一個指示器View
/** * 指示器View */ public class BannerIndicator extends View{ private int mCellCount; private int currentPosition; private Paint mPaint; /** * 指示器小圓點半徑 */ private int mCellRadius = dp2px(3); /** * 指示器小圓點間距 */ private int mCellMargin = dp2px(4); /** * 指示器小圓點啟用狀態的顏色 */ private int mIndicatorColor = Color.parseColor("#000000"); public BannerIndicator(Context context) { super(context); init(); } public void init(){ mPaint = new Paint(); mPaint.setAntiAlias(true); } public void setCellCount(int cellCount) { mCellCount = cellCount; invalidate(); } public void setCurrentPosition(int currentPosition) { this.currentPosition = currentPosition; invalidate(); } @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { super.onMeasure(widthMeasureSpec, heightMeasureSpec); // 重新測量當前介面的寬度 int width = getPaddingLeft() + getPaddingRight() + mCellRadius * 2 * mCellCount + mCellMargin * (mCellCount - 1); int height = getPaddingTop() + getPaddingBottom() + mCellRadius * 2; width = resolveSize(width, widthMeasureSpec); height = resolveSize(height, heightMeasureSpec); setMeasuredDimension(width, height); } @Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); for (int i = 0; i < mCellCount; i++) { if (i == currentPosition) { mPaint.setColor(mIndicatorColor); } else { mPaint.setColor(Color.WHITE); } int left = getPaddingLeft() + i * mCellRadius * 2 + mCellMargin * i; canvas.drawCircle(left + mCellRadius, getHeight() / 2, mCellRadius, mPaint); } } }
其實就是根據banner的數量來進行繪製每個小圓圈的位置,並且用一個currentPosition來標誌當前哪個小圓圈是處於啟用的狀態,顯示不同的顏色。在ViewPager每次回撥onPageSelect的時候,將當前的currentPosition也同步更新,並且呼叫invalidate觸發onDraw重新繪製。
5)開放滑動監聽介面
最後,雖然內部實現了自動輪播,但是我們還是要將滑動切換的介面放開來
/** * 滾動監聽回撥介面 */ ScrollPageListener mScrollPageListener; public void setScrollPageListener(ScrollPageListener mScrollPageListener) { this.mScrollPageListener = mScrollPageListener; } public interface ScrollPageListener { void onPageSelected(int position); } ViewPager.OnPageChangeListener mPageListener = new ViewPager.OnPageChangeListener() { @Override public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {} @Override public void onPageSelected(int position) { if (mScrollPageListener != null) { mScrollPageListener.onPageSelected(smallPos); } } @Override public void onPageScrollStateChanged(int state) { onPageScrollStateChange(state); } };
6)提供設定Banner資料介面
提供一個供外界設定banner資料的方法:
/** * 設定Banner圖片地址資料 * @param bannerData */ public void setBannerData(List<String> bannerData) { mBannerUrlList.clear(); mBannerUrlList.addAll(bannerData); mAdapter.notifyDataSetChanged(); startPlay(mDelayTime); mIndicator.setCellCount(bannerData.size()); }
應用
xml佈局中引用(如果是wrap_content,預設是200dp的高度,可在自定義View的onMeasure中自行調整):
<com.example.zjy.zjywidget.banner.BannerView android:id="@+id/banner_view" android:layout_width="match_parent" android:layout_height="wrap_content"> </com.example.zjy.zjywidget.banner.BannerView>
從此之後實現Banner輪播效果就方便多了,只需要一句程式碼即可:
mBannerView.setBannerData(getBannerData());
getBannerData就是你的圖片url集合,將其設定進去,開啟你的自動輪播吧~~~
後續
最近有點沉迷於自定義View,其實很多看似很基礎的東西還是很重要的,底層基礎決定上層建築,本篇的輪播效果儘管並不複雜,但是巧妙利用View的屬性有時候能夠帶來不錯的效果。這裡還存在一個無限大所導致的記憶體隱患問題,不知道廣大簡友有沒有更好的方案進行優化?
原始碼傳送門: GitHub-ZJYWidget-YCircleProgressBar
CSDN部落格: IT_ZJYANG
簡 書: Android小Y
裡面還有很多實用的自定義View原始碼及demo,會長期維護,歡迎Star~ 如有不足之處或建議還望指正,相互學習,相互進步,如果覺得不錯動動小手給個Star, 謝謝~