?

?
?
概述
平時(shí)應(yīng)用開發(fā)中首頁經(jīng)常會(huì)有一個(gè)Banner輪播的展示,不可避免的需要封裝一個(gè)自定義View,在使用的時(shí)候能夠方便的只用一句代碼設(shè)置圖片地址集合,就可以啟動(dòng)輪播效果,本文將通過ViewPager一步步對輪播圖進(jìn)行實(shí)現(xiàn),最終效果如下:

?
需要定制的特性
1.是否顯示指示器
2.指示器圓點(diǎn)大小、間距
3.輪播自動(dòng)切換的間隔時(shí)長
4.是否自動(dòng)輪播
5.左右無邊界輪播
注:目前暫時(shí)只支持url的圖片形式,其他形式例如本地圖片可自行在Adapter中instantiateItem方法進(jìn)行調(diào)整,且本文的圖片展示是依賴Glide來展示。
?
實(shí)現(xiàn)思路
我們都知道ViewPager是可以左右滑動(dòng)的,并且可以設(shè)置Adapter, 那如果能夠設(shè)置為無限大,且每隔一段時(shí)間就調(diào)用滑動(dòng),即可達(dá)到輪播效果,主要需要解決以下幾個(gè)問題:如何實(shí)現(xiàn)無限循環(huán),自動(dòng)輪播以及兼容手勢滑動(dòng)。
1)實(shí)現(xiàn)無限循環(huán)
首先實(shí)現(xiàn)左右無限循環(huán)的效果,思路就是將ViewPager的getCount()返回為Integer.MAX_VALUE,然后在ViewPager每次切換Item的時(shí)候,會(huì)調(diào)用PagerAdapter的instantiateItem方法,這個(gè)方法返回當(dāng)前準(zhǔn)備切換到的下標(biāo),由于我們設(shè)置的Item數(shù)量是Integer.MAX_VALUE,因此這里返回的下標(biāo)有可能是0-2147483647。
假設(shè)我們要顯示的banner圖總共有3張,那在第3張即將切換到第1張的時(shí)候,需要重新將position置為0,因此可以采用對下標(biāo)取余的方式讓position可以一直處于0-3的無限循環(huán)之中。
關(guān)鍵代碼如下:
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)實(shí)現(xiàn)自動(dòng)輪播
實(shí)現(xiàn)了左右無限輪播,接下來就要讓它能夠自動(dòng)動(dòng)起來,這里采用handler的方式,每次將下標(biāo)+1之后setCurrentItem為新的下標(biāo),關(guān)鍵代碼如下:
/**
* 是否自動(dòng)滑動(dòng)
*/
private boolean mIsAutoScroll = true;
/**
* 默認(rèn)頁面之間自動(dòng)切換的時(shí)間間隔
*/
private long mDelayTime = 2000;
//播放標(biāo)志
private boolean isPlay = false;
//觸發(fā)輪播的消息標(biāo)志位
private final int PLAY = 0x123;
/**
* 輪播計(jì)時(shí)器
*/
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)兼容手勢滑動(dòng)
實(shí)現(xiàn)了自動(dòng)輪播之后,我們還要兼容用戶主動(dòng)滑動(dòng)的場景,即用戶主動(dòng)滑動(dòng)時(shí),應(yīng)該暫停自動(dòng)輪詢,先以用戶主動(dòng)滑動(dòng)的動(dòng)作為主,當(dāng)用戶放開手指之后,再重新繼續(xù)自動(dòng)輪詢,既然要區(qū)分是否是手勢滑動(dòng),可以在ViewPager的滑動(dòng)監(jiān)聽接口onPageScrollStateChange中去判斷,如下:
/**
* ViewPager滑動(dòng)監(jiān)聽
*/
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);
}
};
/**
* 手指滑動(dòng)時(shí)暫停自動(dòng)輪播,手指松開時(shí)重新啟動(dòng)自動(dòng)輪播
*
* @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:
// 手指滑動(dòng)時(shí),清除播放下一張,防止滑動(dòng)過程中自動(dòng)播放下一張
mGestureScroll = true;
mHandler.removeMessages(PLAY);
break;
default:
break;
}
}
ViewPager的SCRPLL_STATE_IDLE狀態(tài)是表示用戶手指松開時(shí),這個(gè)時(shí)候就繼續(xù)sendMessage啟動(dòng)輪詢,SCROLL_STATE_DRAGGING狀態(tài)表示用戶手指正在滑動(dòng)過程中,removeMessage將當(dāng)前的輪詢暫停
?
?
4)添加輪播指示器
以上完成了ViewPager的部分,實(shí)現(xiàn)了輪播圖的自動(dòng)輪詢效果,但一般輪播圖都會(huì)有個(gè)小小的指示器來讓用戶感知當(dāng)前處于哪個(gè)banner,一共有多少個(gè)banner,所以我們還需套自定義一個(gè)指示器View
/**
* 指示器View
*/
public class BannerIndicator extends View{
private int mCellCount;
private int currentPosition;
private Paint mPaint;
/**
* 指示器小圓點(diǎn)半徑
*/
private int mCellRadius = dp2px(3);
/**
* 指示器小圓點(diǎn)間距
*/
private int mCellMargin = dp2px(4);
/**
* 指示器小圓點(diǎn)激活狀態(tài)的顏色
*/
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);
// 重新測量當(dāng)前界面的寬度
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);
}
}
}
其實(shí)就是根據(jù)banner的數(shù)量來進(jìn)行繪制每個(gè)小圓圈的位置,并且用一個(gè)currentPosition來標(biāo)志當(dāng)前哪個(gè)小圓圈是處于激活的狀態(tài),顯示不同的顏色。在ViewPager每次回調(diào)onPageSelect的時(shí)候,將當(dāng)前的currentPosition也同步更新,并且調(diào)用invalidate觸發(fā)onDraw重新繪制。
?
?
5)開放滑動(dòng)監(jiān)聽接口
最后,雖然內(nèi)部實(shí)現(xiàn)了自動(dòng)輪播,但是我們還是要將滑動(dòng)切換的接口放開來
/**
* 滾動(dòng)監(jiān)聽回調(diào)接口
*/
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)提供設(shè)置Banner數(shù)據(jù)接口
提供一個(gè)供外界設(shè)置banner數(shù)據(jù)的方法:
/**
* 設(shè)置Banner圖片地址數(shù)據(jù)
* @param bannerData
*/
public void setBannerData(List<String> bannerData) {
mBannerUrlList.clear();
mBannerUrlList.addAll(bannerData);
mAdapter.notifyDataSetChanged();
startPlay(mDelayTime);
mIndicator.setCellCount(bannerData.size());
}
?
?
應(yīng)用
xml布局中引用(如果是wrap_content,默認(rèn)是200dp的高度,可在自定義View的onMeasure中自行調(diào)整):
<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>
從此之后實(shí)現(xiàn)Banner輪播效果就方便多了,只需要一句代碼即可:
mBannerView.setBannerData(getBannerData());
getBannerData就是你的圖片url集合,將其設(shè)置進(jìn)去,開啟你的自動(dòng)輪播吧~~~
?
?
后續(xù)
ViewPager不止可以做輪播,還支持自定義各種切換動(dòng)畫,詳見我另一篇文章 Android ViewPager多屏顯示、切換動(dòng)畫,讓你的輪播炫起來
最近有點(diǎn)沉迷于自定義View,其實(shí)很多看似很基礎(chǔ)的東西還是很重要的,底層基礎(chǔ)決定上層建筑,本篇的輪播效果盡管并不復(fù)雜,但是巧妙利用View的屬性有時(shí)候能夠帶來不錯(cuò)的效果。這里還存在一個(gè)無限大所導(dǎo)致的內(nèi)存隱患問題,不知道廣大簡友有沒有更好的方案進(jìn)行優(yōu)化?
源碼傳送門:GitHub-ZJYWidget-YCircleProgressBar
CSDN博客:IT_ZJYANG
簡??????????書:Android小Y
里面還有很多實(shí)用的自定義View源碼及demo,會(huì)長期維護(hù),歡迎Star~ 如有不足之處或建議還望指正,相互學(xué)習(xí),相互進(jìn)步,如果覺得不錯(cuò)動(dòng)動(dòng)小手點(diǎn)個(gè)喜歡, 謝謝~