引言
在app中,輪播已是一種非常普遍的效果了,通常會出現(xiàn)在首頁的列表頭部進行banner(廣告位)輪播展現(xiàn)。以下為輪播效果圖:
輪播
- 輪播效果簡單地可以拆分為:
- 循環(huán):第一頁左滑,能滑倒最后一頁;最后一頁右滑,能滑倒第一頁。
- 定時:設(shè)定一個時間單位,每隔一個時間單位觸發(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ù),可以自行設(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,假設(shè)為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時,還是假設(shè)為30。獲取ViewPager當前的頁面位置,然后判斷程序邊界。
- 當前程序頁面為第1頁(對于肉眼來說為第1頁),則直接替換為程序頁面的第4頁
- 當前程序頁面為最后一頁(對于肉眼來說為第3頁),則直接替換為程序頁面的第3頁
- 綜上,我們的循環(huán)效果就完美實現(xiàn)了。
定時和觸點暫停
- 老規(guī)矩先上代碼:
/**
* 輪播效果的{@link ViewPager}
* <ol>
* <li>當attach到window上時,會自動觸發(fā)輪播定時任務(wù)</li>
* <li>當從window中detach時,會觸發(fā)關(guān)閉輪播定時任務(wù)</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將因無法恢復(fù)線程執(zhí)行行,而是錯誤地促使整個Timer被取消了,那么整個定時任務(wù)就無法調(diào)度來。而SingleThreadScheduledExecutor不會有這些問題,當線程池中的唯一線程因特殊原因掛掉,線程池會自動開啟一個新線程,保證定時任務(wù)繼續(xù)。
- Timer基于絕對時間,SingleThreadScheduledExecutor基于想對時間。
- 接著來看定時器任務(wù)的觸發(fā)和停止:
- 觸發(fā),onAttachedToWindow()
- 停止,onDetachedFromWindow()
- 然后來看定時器基于生命周期的恢復(fù)、暫停、銷毀,當前做了簡單處理(暫停==銷毀),代碼簡單就不做介紹。而生命周期是在外部調(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)來設(shè)置標記位mIsTouching,定時任務(wù)通過對標記位的判斷,來決定是否執(zhí)行。
至此,整個輪播的實現(xiàn)過程就已經(jīng)完成。附上源碼