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

引言

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

輪播
  • 輪播效果簡單地可以拆分為:
    1. 循環(huán):第一頁左滑,能滑倒最后一頁窘俺;最后一頁右滑,能滑倒第一頁复凳。
    2. 定時:設定一個時間單位瘤泪,每隔一個時間單位觸發(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ù)签则,可以自行設置须床,但又以下原則需要遵循:
     * <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)完成。附上源碼

最后編輯于
?著作權歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末羔味,一起剝皮案震驚了整個濱河市河咽,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌赋元,老刑警劉巖忘蟹,帶你破解...
    沈念sama閱讀 216,544評論 6 501
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異搁凸,居然都是意外死亡媚值,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,430評論 3 392
  • 文/潘曉璐 我一進店門护糖,熙熙樓的掌柜王于貴愁眉苦臉地迎上來褥芒,“玉大人,你說我怎么就攤上這事嫡良∶谭觯” “怎么了?”我有些...
    開封第一講書人閱讀 162,764評論 0 353
  • 文/不壞的土叔 我叫張陵寝受,是天一觀的道長坷牛。 經(jīng)常有香客問我,道長很澄,這世上最難降的妖魔是什么京闰? 我笑而不...
    開封第一講書人閱讀 58,193評論 1 292
  • 正文 為了忘掉前任,我火速辦了婚禮甩苛,結果婚禮上蹂楣,老公的妹妹穿的比我還像新娘。我一直安慰自己浪藻,他們只是感情好捐迫,可當我...
    茶點故事閱讀 67,216評論 6 388
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著爱葵,像睡著了一般施戴。 火紅的嫁衣襯著肌膚如雪反浓。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,182評論 1 299
  • 那天赞哗,我揣著相機與錄音雷则,去河邊找鬼。 笑死肪笋,一個胖子當著我的面吹牛月劈,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播藤乙,決...
    沈念sama閱讀 40,063評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼猜揪,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了坛梁?” 一聲冷哼從身側響起而姐,我...
    開封第一講書人閱讀 38,917評論 0 274
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎划咐,沒想到半個月后拴念,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,329評論 1 310
  • 正文 獨居荒郊野嶺守林人離奇死亡褐缠,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,543評論 2 332
  • 正文 我和宋清朗相戀三年政鼠,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 39,722評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖砰琢,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情俐载,我是刑警寧澤,帶...
    沈念sama閱讀 35,425評論 5 343
  • 正文 年R本政府宣布登失,位于F島的核電站遏佣,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏揽浙。R本人自食惡果不足惜状婶,卻給世界環(huán)境...
    茶點故事閱讀 41,019評論 3 326
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望馅巷。 院中可真熱鬧膛虫,春花似錦、人聲如沸钓猬。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,671評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至账月,卻和暖如春综膀,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背局齿。 一陣腳步聲響...
    開封第一講書人閱讀 32,825評論 1 269
  • 我被黑心中介騙來泰國打工剧劝, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人抓歼。 一個月前我還...
    沈念sama閱讀 47,729評論 2 368
  • 正文 我出身青樓讥此,卻偏偏與公主長得像,于是被迫代替她去往敵國和親谣妻。 傳聞我的和親對象是個殘疾皇子萄喳,可洞房花燭夜當晚...
    茶點故事閱讀 44,614評論 2 353

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

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