一、介紹
在項目中使用的自動輪播控件一直是網(wǎng)上別人做的焕梅,在出現(xiàn)問題的時候去看代碼細(xì)節(jié)掃雷就非常浪費時間迹鹅。于是痛定思痛自己造個輪子。
這個控件在app中使用非常頻繁贞言,并且原理也不復(fù)雜斜棚,就是在前后各加一頁。相信每一個android開發(fā)者都會做這個東西该窗。
功能介紹:
1.無限自動輪播弟蚀。
2.指示器(下方的小點點)
3.滾動動畫時間可調(diào)
4.拖拽的時候停止輪播
全部代碼和示例代碼已經(jīng)上傳到GitHub上了:
https://github.com/CuteWen/BannerView
有興趣的可以下過來看看。
二酗失、實現(xiàn)
首先要自定義一個View去繼承ViewPager
然后我們自動輪播實現(xiàn)的關(guān)鍵其實都在PagerAdapter里面义钉,我們可以自己封裝一個PagerAdapter,但是自己封裝的Adapter就會讓使用者在寫邏輯的時候要了解你的adapter封裝到什么程度了规肴,放出哪些方法捶闸,個人不太喜歡那樣子,所以我這里使用了裝飾者模式來擴(kuò)展使用者寫好的Adapter拖刃,這樣使用的時候只要寫一個最普通的PagerAdapter 就可以附加上自動輪播的功能了删壮。
注:不太懂裝飾者模式的同學(xué)可以去這里看一下,里面講解的挺好的兑牡。
https://www.cnblogs.com/chenxing818/p/4705919.html
1.包裝類
思考一下我們需要包裝的功能央碟,其實也就是要將頁數(shù)+2,主要就是getCount這個方法了均函,另外在里面也要寫好兩個適配器之間的position轉(zhuǎn)化的方法亿虽,統(tǒng)一調(diào)用這些方法可以避免邏輯的混亂。
下面就是我們的包裝類了边酒。
/**
* 適配器的包裝類---------------------------------------------------------
*/
private class BannerAdapterWrapper extends PagerAdapter {
private PagerAdapter pagerAdapter;
public BannerAdapterWrapper(PagerAdapter pagerAdapter) {
this.pagerAdapter = pagerAdapter;
}
@Override
public int getCount() {
return pagerAdapter.getCount() > 1 ? pagerAdapter.getCount() + 2 : pagerAdapter.getCount();
}
@Override
public boolean isViewFromObject(View view, Object object) {
return view.equals(object);
}
@Override
public Object instantiateItem(ViewGroup container, int position) {
return pagerAdapter.instantiateItem(container, bannerToAdapterPosition(position));
}
@Override
public void destroyItem(ViewGroup container, int position, Object object) {
pagerAdapter.destroyItem(container, position, object);
}
/**
* 展示出的position和實際的position 轉(zhuǎn)換
*/
public int bannerToAdapterPosition(int position) {
int adapterCount = pagerAdapter.getCount();
if (adapterCount <= 1) return 0;
int adapterPosition = (position - 1) % adapterCount;
if (adapterPosition < 0) adapterPosition += adapterCount;
return adapterPosition;
}
public int toWrapperPosition(int position) {
return position + 1;
}
}
主要做了:
1.getCount的上限加了2 也就是前后各多一頁的作用经柴。
2.寫了兩個適配器之間的position之間的轉(zhuǎn)換方法方便調(diào)用。
2.暗度陳倉(AdapterWrapper)之后的善后工作
看一下setAdapter方法:
/**
* 設(shè)置適配器的時候做初始化工作
*/
@Override
public void setAdapter(PagerAdapter adapter) {
this.adapter = adapter;
//注冊原適配器刷新時的監(jiān)聽
this.adapter.registerDataSetObserver(new BannerPagerObserver());
//初始化包裝適配器
bannerAdapterWrapper = new BannerAdapterWrapper(adapter);
//實際配置的adapter是包裝后的適配器
super.setAdapter(bannerAdapterWrapper);
//注冊適配器的監(jiān)聽 (這個在后文介紹)
addOnPageChangeListener(new BannerPageChangeListener());
//初始化handler處理定時事件 (這個在后文介紹)
looperHandler = new LooperHandler(this);
}
這里注冊了一個DataSetObserver墩朦,這個平時用到的還比較少坯认,它是用來監(jiān)聽Adapter.notifyDataSetChanged()的。
因為我們實際上綁定BannerView的是Wrapper之后的適配器adapter氓涣,而使用者手里調(diào)用的是原adapter的notifyDataSetChanged()牛哺,所以需要進(jìn)行一個傳遞過程!
/**
* 數(shù)據(jù)刷新 傳遞刷新信號-----------------------------------------------------
*/
private class BannerPagerObserver extends DataSetObserver {
@Override
public void onChanged() {
super.onChanged();
dataSetChanged();
}
@Override
public void onInvalidated() {
super.onInvalidated();
dataSetChanged();
}
}
/**
* 刷新數(shù)據(jù)方法
*/
private void dataSetChanged() {
if (bannerAdapterWrapper != null && pagerAdapter.getCount() > 0) {
bannerAdapterWrapper.notifyDataSetChanged();
bannerIndicatorView.setCount(pagerAdapter.getCount());
setCurrentItem(0);
}
}
同理劳吠,我們在調(diào)用setCurrentItem()方法的時候position也是不一樣的引润。
@Override
public void setCurrentItem(int item, boolean smoothScroll) {
super.setCurrentItem(bannerAdapterWrapper.toWrapperPosition(item), smoothScroll);
}
@Override
public void setCurrentItem(int item) {
super.setCurrentItem(bannerAdapterWrapper.toWrapperPosition(item));
}
@Override
public int getCurrentItem() {
return bannerAdapterWrapper.bannerToAdapterPosition(super.getCurrentItem());
}
3.翻頁監(jiān)聽
/**
* 監(jiān)聽翻頁----------------------------------------------------------------
*/
private class BannerPageChangeListener implements OnPageChangeListener {
@Override
public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {
}
@Override
public void onPageSelected(int position) {
// 在這里同步指示器
if (bannerIndicatorView != null) {
bannerIndicatorView.setSelect(bannerAdapterWrapper.bannerToAdapterPosition(position));
}
}
@Override
public void onPageScrollStateChanged(int state) {
int position = BannerView.super.getCurrentItem();
// 無限輪播的跳轉(zhuǎn)
if (state == ViewPager.SCROLL_STATE_IDLE &&
(position == 0 || position == bannerAdapterWrapper.getCount() - 1)) {
setCurrentItem(bannerAdapterWrapper.bannerToAdapterPosition(position), false);
}
// 手指拖動翻頁的時候暫停自動輪播
if (state == ViewPager.SCROLL_STATE_IDLE) {
if (timer == null) {
timer = new Timer();
timer.schedule(new TimerTask() {
@Override
public void run() {
looperHandler.sendEmptyMessage(0);
}
}, intervalTime + scrollTime, intervalTime + scrollTime);
}
} else if (state == ViewPager.SCROLL_STATE_DRAGGING) {
if (timer != null) {
timer.cancel();
timer = null;
}
}
}
}
里面的同步指示器和暫停自動輪播代碼暫且不表。
主要就是無限輪播的跳轉(zhuǎn)那一段代碼 完成“無限”的實現(xiàn)痒玩。
4.自動輪播
這里我們使用了Timer+Handler的組合來完成定時滑動的操作:
/**
* 設(shè)置間隔時間 并開始Timer任務(wù)
*/
public void setIntervalTime(int intervalTime) {
this.intervalTime = intervalTime;
timer = new Timer();
timer.schedule(new TimerTask() {
@Override
public void run() {
looperHandler.sendEmptyMessage(0);
}
}, intervalTime + scrollTime, intervalTime + scrollTime);
}
/**
* 處理定時任務(wù)-------------------------------------------------------------------
*/
private static class LooperHandler extends Handler {
private WeakReference<BannerView> weakReference;
public LooperHandler(BannerView bannerView) {
this.weakReference = new WeakReference<>(bannerView);
}
@Override
public void handleMessage(Message msg) {
super.handleMessage(msg);
weakReference.get().setCurrentItem(weakReference.get().getCurrentItem() + 1);
}
}
另外還有設(shè)置滾動的時間,這里需要使用一下反射去修改mScroller這個對象淳附。
/**
* 設(shè)置滾動時間 利用反射
*/
public void setScrollTime(int scrollTime) {
try {
Field field = ViewPager.class.getDeclaredField("mScroller");
field.setAccessible(true);
FixedSpeedScroller scroller = new FixedSpeedScroller(getContext(),
new AccelerateInterpolator());
field.set(this, scroller);
scroller.setScrollDuration(scrollTime);
} catch (NoSuchFieldException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
}
}
/**
* 修改ViewPager的滑動動畫時間-----------------------------------------------------------
*/
private class FixedSpeedScroller extends Scroller {
private int duration = 300;
public FixedSpeedScroller(Context context, Interpolator interpolator) {
super(context, interpolator);
}
@Override
public void startScroll(int startX, int startY, int dx, int dy, int duration) {
super.startScroll(startX, startY, dx, dy, this.duration);
}
@Override
public void startScroll(int startX, int startY, int dx, int dy) {
super.startScroll(startX, startY, dx, dy, this.duration);
}
public void setScrollDuration(int duration) {
this.duration = duration;
}
}
5. 指示器
先上代碼
public class BannerIndicatorView extends View {
private int count;
private int select;
private Paint pointPaint;
private Paint selectPaint;
private String selectColor = "#FFFFFF";
private String normalColor = "#80FFFFFF";
private int radius = 10;
private int interval = 10;
public BannerIndicatorView(Context context) {
this(context, null);
}
public BannerIndicatorView(Context context, @Nullable AttributeSet attrs) {
this(context, attrs, 0);
}
public BannerIndicatorView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init();
}
private void init() {
pointPaint = new Paint();
pointPaint.setFlags(Paint.ANTI_ALIAS_FLAG);
pointPaint.setColor(Color.parseColor(normalColor));
pointPaint.setStyle(Paint.Style.FILL_AND_STROKE);
selectPaint = new Paint();
selectPaint.setFlags(Paint.ANTI_ALIAS_FLAG);
selectPaint.setColor(Color.parseColor(selectColor));
selectPaint.setStyle(Paint.Style.FILL_AND_STROKE);
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
// 畫出各個點的位置
for (int i = 0; i < count; i++) {
if (i == select) {
canvas.drawCircle(radius + i * (radius * 2 + interval), getHeight() / 2, radius, selectPaint);
} else {
canvas.drawCircle(radius + i * (radius * 2 + interval), getHeight() / 2, radius, pointPaint);
}
}
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int width = count * radius * 2 + (count - 1) * interval;
int height = radius * 2;
widthMeasureSpec = MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY);
heightMeasureSpec = MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY);
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
}
/**
* 設(shè)置第幾個點選中议慰,然后刷新
*/
public void setSelect(int select) {
this.select = select;
invalidate();
}
/**
* 設(shè)置個數(shù)
*/
public void setCount(int c) {
count = c;
}
public void setSelectColor(String selectColor) {
this.selectColor = selectColor;
}
public void setNormalColor(String normalColor) {
this.normalColor = normalColor;
}
}
這部分還是比較簡單的,就是繪制了幾個白色小圓點奴曙,然后提供setSelect的方法來變化選中點别凹。
然后在BannerView里面寫上setIndicator()的方法
/**
* 設(shè)置指示器,需要在setAdapter之后
*/
public void setIndicator(BannerIndicatorView bannerIndicatorView) {
this.bannerIndicatorView = bannerIndicatorView;
if (pagerAdapter != null) {
bannerIndicatorView.setCount(pagerAdapter.getCount());
}
}
三:示例與全部代碼
在XML中的示例寫法:
<com.wzl.custom.BannerView
android:id="@+id/bv_activity_banner"
android:layout_width="match_parent"
android:layout_height="wrap_content"/>
<com.wzl.custom.BannerIndicatorView
android:id="@+id/biv_activity_banner"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_alignBottom="@id/bv_activity_banner"
android:layout_marginBottom="7dp"
android:layout_centerHorizontal="true"
/>
注意: android:layout_centerHorizonta = "true" 是為了讓點居中洽糟。
class BannerActivity : AppCompatActivity() {
var bannerView: BannerView? = null
var indicatorView: BannerIndicatorView? = null
var adapter: BannerAdapter? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_banner)
bannerView = findViewById(R.id.bv_activity_banner) as BannerView
indicatorView = findViewById(R.id.biv_activity_banner) as BannerIndicatorView
adapter = BannerAdapter(this)
// 設(shè)置adapter
bannerView?.adapter = adapter
// 綁定指示器
bannerView?.setIndicator(indicatorView)
// 滾動動畫的時間
bannerView?.setScrollTime(500)
// 設(shè)置輪播間隔
bannerView?.setIntervalTime(3000)
val data:ArrayList<String> = ArrayList()
data.add("1111")
data.add("2222")
data.add("1111")
data.add("2222")
adapter?.addData(data)
}
}
這部分使用kotlin寫的炉菲,不過調(diào)用就這幾個方法,應(yīng)該沒什么看不懂的地方了坤溃。