Android——基于ViewPager的輪播(附帶生命周期控制)

引言

在app中,輪播已是一種非常普遍的效果了,通常會出現(xiàn)在首頁的列表頭部進行banner(廣告位)輪播展現(xiàn)。以下為輪播效果圖:

輪播
  • 輪播效果簡單地可以拆分為:
    1. 循環(huán):第一頁左滑,能滑倒最后一頁;最后一頁右滑,能滑倒第一頁。
    2. 定時:設(shè)定一個時間單位,每隔一個時間單位觸發(fā)自動滑動。這里又一個優(yōu)化點,就是當頁面pause或者destroy的時候,輪播的定時器也要跟隨pause或者destroy。
    3. 觸點暫停:當手指點擊或者滑動輪播控件的時候,輪播效果要暫停掉,否則會影響用戶的操作體驗。

依賴

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)完成。附上源碼

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

推薦閱讀更多精彩內(nèi)容

  • Android 自定義View的各種姿勢1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 173,045評論 25 708
  • 【日更189】 看了一個標題為“批判性思維”的課程,看到中途才發(fā)現(xiàn)其實跟真正的“批判性思維”關(guān)系不大,但是為了不至...
    唐斬2086閱讀 131評論 0 1
  • 檢測手機在前臺鎖屏//檢測在前臺鎖屏—需要導(dǎo)入 #import<notify.h> #define Notific...
    不瘋魔難以成佛閱讀 874評論 0 2
  • 筆者曾經(jīng)在網(wǎng)上看到過這么一則新聞。說家里培養(yǎng)了一個女大學生,然后千叮嚀萬囑咐別找國外男友,結(jié)果女兒沒聽,在國外...
    燚月仁心閱讀 395評論 0 0
  • 啟言不凡筆力好, 武略文舀都用到。 聰穎過人有才氣, 慧眼獨具就是高。
    小車16閱讀 220評論 0 0