引言
在app中,輪播已是一種非常普遍的效果了乖坠,通常會出現(xiàn)在首頁的列表頭部進行banner(廣告位)輪播展現(xiàn)。以下為輪播效果圖:
- 輪播效果簡單地可以拆分為:
- 循環(huán):第一頁左滑,能滑倒最后一頁窘俺;最后一頁右滑,能滑倒第一頁复凳。
- 定時:設定一個時間單位瘤泪,每隔一個時間單位觸發(fā)自動滑動。這里又一個優(yōu)化點育八,就是當頁面pause或者destroy的時候对途,輪播的定時器也要跟隨pause或者destroy。
- 觸點暫停:當手指點擊或者滑動輪播控件的時候髓棋,輪播效果要暫停掉实檀,否則會影響用戶的操作體驗。
依賴
maven:
<dependency>
<groupId>com.xpleemoon.view</groupId>
<artifactId>carouselviewpager</artifactId>
<version>0.1.0</version>
<type>pom</type>
</dependency>
or gradle:
compile 'com.xpleemoon.view:carouselviewpager:0.1.0'
實現(xiàn)
我們采用ViewPager+PagerAdapter+SingleThreadScheduledExecutor的方式來實現(xiàn)輪播按声。
循環(huán)
為了讓ViewPager達到循環(huán)的目的膳犹,只需要對PagerAdapter做處理即可,先來看我們實現(xiàn)的抽象PagerAdapter:
/**
* {@link CarouselViewPager 輪播控件}所需的adapter
*
* @author xpleemoon
*/
public abstract class CarouselPagerAdapter<V extends CarouselViewPager> extends PagerAdapter {
/**
* 系數(shù)签则,可以自行設置须床,但又以下原則需要遵循:
* <ul>
* <li>必須大于1</li>
* <li>盡量小</li>
* </ul>
*/
private static final int COEFFICIENT = 10;
private V mViewPager;
public CarouselPagerAdapter(V viewPager) {
this.mViewPager = viewPager;
}
/**
* 視覺上所見的數(shù)據(jù)數(shù)量
*
* @return
*/
@IntRange(from = 0)
public abstract int getCountOfVisual();
/**
* 實際數(shù)據(jù)量
* <ul>
* <li>{@link #getCount()}>{@link #getCountOfVisual()}</li>
* </ul>
*
* @return
*/
@Override
public final int getCount() {
long realDataCount = getCountOfVisual();
if (realDataCount > 1) {
realDataCount = getCountOfVisual() * COEFFICIENT;
realDataCount = realDataCount > Integer.MAX_VALUE ? Integer.MAX_VALUE : realDataCount;
}
return (int) realDataCount;
}
@Override
public final boolean isViewFromObject(View view, Object object) {
return view == object;
}
@Override
public final Object instantiateItem(ViewGroup container, int position) {
position = position % getCountOfVisual();
return this.instantiateRealItem(container, position);
}
public abstract Object instantiateRealItem(ViewGroup container, int position);
@Override
public final void destroyItem(ViewGroup container, int position, Object object) {
container.removeView((View) object);
}
@Override
public final void finishUpdate(ViewGroup container) {
// 數(shù)量為1,不做position替換
if (getCount() <= 1) {
return;
}
int position = mViewPager.getCurrentItem();
// ViewPager的更新即將完成渐裂,替換position豺旬,以達到無限循環(huán)的效果
if (position == 0) {
position = getCountOfVisual();
mViewPager.setCurrentItem(position, false);
} else if (position == getCount() - 1) {
position = getCountOfVisual() - 1;
mViewPager.setCurrentItem(position, false);
}
}
}
- 首先來看getCount()和getCountOfVisual():
- getCountOfVisual()是抽象方法,用來表示ViewPager綁定的實際數(shù)據(jù)列表的長度柒凉。
- getCount()是final方法族阅,意味著子類不能重寫,同時該方法還做了特殊處理扛拨。
- 當getCountOfVisual()返回1(即肉眼可見有1屏)的時候耘分,那么getCount()就返回1,也就是對于程序來說實際有1屏
- 當getCountOfVisual()返回大于1绑警,假設為3(即肉眼可見3屏)的時候求泰,那么getCount()就返回30,也就是對于程序來說實際有30屏计盒。那么可以想象一下渴频,當我們不到達頁面邊界(第1屏或第30屏),是不是就完成了循環(huán)的效果呢北启。BUT卜朗,當?shù)竭_邊界拔第,第1屏沒法左滑,第30屏沒法右滑场钉,這和真正的輪播需求還是有差距的蚊俺。
- OK,帶著上面的問題再來看finishUpdate()逛万,它也是一個final方法泳猬,它的效果實際上就是一個偷偷替換問題。(此處參考自循環(huán)廣告位組件的實現(xiàn))
- 當我們的getCount()(程序頁面數(shù)量)小于等于1時宇植,不做替換
- 當我們的getCount()(程序頁面數(shù)量)大于1時得封,還是假設為30。獲取ViewPager當前的頁面位置指郁,然后判斷程序邊界忙上。
- 當前程序頁面為第1頁(對于肉眼來說為第1頁),則直接替換為程序頁面的第4頁
- 當前程序頁面為最后一頁(對于肉眼來說為第3頁)闲坎,則直接替換為程序頁面的第3頁
- 綜上疫粥,我們的循環(huán)效果就完美實現(xiàn)了。
定時和觸點暫停
- 老規(guī)矩先上代碼:
/**
* 輪播效果的{@link ViewPager}
* <ol>
* <li>當attach到window上時箫柳,會自動觸發(fā)輪播定時任務</li>
* <li>當從window中detach時手形,會觸發(fā)關閉輪播定時任務</li>
* </ol>
*
* @author xpleemoon
*/
public class CarouselViewPager extends ViewPager {
@IntDef({RESUME, PAUSE, DESTROY})
@Retention(RetentionPolicy.SOURCE)
public @interface LifeCycle {
}
public static final int RESUME = 0;
public static final int PAUSE = 1;
public static final int DESTROY = 2;
/**
* 生命周期狀態(tài),保證{@link #mCarouselTimer}在各生命周期選擇執(zhí)行策略
*/
private int mLifeCycle = RESUME;
/**
* 是否正在觸摸狀態(tài)悯恍,用以防止觸摸滑動和自動輪播沖突
*/
private boolean mIsTouching = false;
/**
* 輪播定時器
*/
private ScheduledExecutorService mCarouselTimer;
public CarouselViewPager(Context context) {
super(context);
}
public CarouselViewPager(Context context, AttributeSet attrs) {
super(context, attrs);
}
public void setLifeCycle(@LifeCycle int lifeCycle) {
this.mLifeCycle = lifeCycle;
}
@Override
public boolean onTouchEvent(MotionEvent ev) {
switch (ev.getAction()) {
case MotionEvent.ACTION_DOWN:
case MotionEvent.ACTION_MOVE:
mIsTouching = true;
break;
case MotionEvent.ACTION_CANCEL:
case MotionEvent.ACTION_UP:
mIsTouching = false;
break;
}
return super.onTouchEvent(ev);
}
@Override
protected void onAttachedToWindow() {
super.onAttachedToWindow();
shutdownTimer();
mCarouselTimer = Executors.newSingleThreadScheduledExecutor();
mCarouselTimer.scheduleAtFixedRate(new Runnable() {
@Override
public void run() {
switch (mLifeCycle) {
case RESUME:
if (!mIsTouching
&& getAdapter() != null
&& getAdapter().getCount() > 1) {
post(new Runnable() {
@Override
public void run() {
setCurrentItem(getCurrentItem() + 1);
}
});
}
break;
case PAUSE:
break;
case DESTROY:
shutdownTimer();
break;
}
}
}, 1000 * 2, 1000 * 2, TimeUnit.MILLISECONDS);
}
@Override
protected void onDetachedFromWindow() {
super.onDetachedFromWindow();
shutdownTimer();
}
private void shutdownTimer() {
if (mCarouselTimer != null && mCarouselTimer.isShutdown() == false) {
mCarouselTimer.shutdown();
}
mCarouselTimer = null;
}
}
定時
- 先來看定時器的實現(xiàn)(Executors.newSingleThreadScheduledExecutor())库糠,是一個單線程的線程池,那為什么不用Timer來進行實現(xiàn)呢涮毫。參考自《Java并發(fā)編程實戰(zhàn)》:
- Timer只會創(chuàng)建一個線程并且不會捕獲異常瞬欧,因此當TimerTask拋出未檢查的異常時將會終止Timer。這種情形下罢防,Timer將因無法恢復線程執(zhí)行行艘虎,而是錯誤地促使整個Timer被取消了,那么整個定時任務就無法調(diào)度來咒吐。而SingleThreadScheduledExecutor不會有這些問題野建,當線程池中的唯一線程因特殊原因掛掉,線程池會自動開啟一個新線程恬叹,保證定時任務繼續(xù)候生。
- Timer基于絕對時間,SingleThreadScheduledExecutor基于想對時間绽昼。
- 接著來看定時器任務的觸發(fā)和停止:
- 觸發(fā)唯鸭,onAttachedToWindow()
- 停止,onDetachedFromWindow()
- 然后來看定時器基于生命周期的恢復硅确、暫停目溉、銷毀明肮,當前做了簡單處理(暫停==銷毀),代碼簡單就不做介紹缭付。而生命周期是在外部調(diào)用setLifeCycle()實現(xiàn)柿估,具體代碼可見MainActivity的生命周期方法:
public class MainActivity extends AppCompatActivity implements View.OnClickListener {
private CarouselViewPager mCarouselView;
private CarouselPagerAdapter mAdapter;
private IndicatorDotView mIndicatorDotView;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
mCarouselView = (CarouselViewPager) findViewById(R.id.carousel);
mAdapter = new SimpleCarouselAdapter(mCarouselView,
new int[]{R.layout.page1, R.layout.page2, R.layout.page3});
mCarouselView.setAdapter(mAdapter);
mIndicatorDotView = (IndicatorDotView) findViewById(R.id.indicator);
mIndicatorDotView.setCount(mAdapter.getCountOfVisual(), 0); // init indicator
mCarouselView.addOnPageChangeListener(new ViewPager.SimpleOnPageChangeListener() {
@Override
public void onPageSelected(int position) {
super.onPageSelected(position);
// position % mAdapter.getCountOfVisual()——因為CarouselViewPager實現(xiàn)的原因,這里的position已經(jīng)不是我們視覺上所看到的position了
mIndicatorDotView.setSelectPosition(position % mAdapter.getCountOfVisual());
}
});
findViewById(R.id.resume).setOnClickListener(this);
findViewById(R.id.pause).setOnClickListener(this);
}
@Override
protected void onResume() {
super.onResume();
mCarouselView.setLifeCycle(CarouselViewPager.RESUME);
}
@Override
protected void onPause() {
super.onPause();
mCarouselView.setLifeCycle(CarouselViewPager.PAUSE);
}
@Override
protected void onDestroy() {
super.onDestroy();
mCarouselView.setLifeCycle(CarouselViewPager.DESTROY);
}
@Override
public void onClick(View v) {
switch (v.getId()) {
case R.id.resume:
mCarouselView.setLifeCycle(CarouselViewPager.RESUME);
Toast.makeText(getApplicationContext(), "resume carousel", Toast.LENGTH_SHORT).show();
break;
case R.id.pause:
mCarouselView.setLifeCycle(CarouselViewPager.PAUSE);
Toast.makeText(getApplicationContext(), "pause carousel", Toast.LENGTH_SHORT).show();
break;
}
}
private static class SimpleCarouselAdapter extends CarouselPagerAdapter<CarouselViewPager> {
private int[] viewResIds;
public SimpleCarouselAdapter(CarouselViewPager viewPager, int[] viewResIds) {
super(viewPager);
this.viewResIds = viewResIds;
}
@Override
public int getCountOfVisual() {
return viewResIds != null ? viewResIds.length : 0;
}
@Override
public Object instantiateRealItem(ViewGroup container, int position) {
int resId = viewResIds[position];
View bannerView = LayoutInflater.from(container.getContext()).inflate(resId, null);
container.addView(bannerView, ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.MATCH_PARENT);
return bannerView;
}
}
}
- 最后來看觸點停止的實現(xiàn):onTouchEvent()根據(jù)手勢狀態(tài)來設置標記位mIsTouching蛉腌,定時任務通過對標記位的判斷官份,來決定是否執(zhí)行。
至此烙丛,整個輪播的實現(xiàn)過程就已經(jīng)完成。附上源碼