手把手、腦把腦教你實現一個無限循環的輪播控件

人的理想志向每每和他的能力成正比。 —— 約翰遜android

摘要

圖片輪播已經成爲了不少App必備功能,且不說它具備炫酷的視覺效果,對於不少靠廣告收入的App來講,圖片輪播是必不可少的,由於它經過輪播減小了廣告位對界面的佔用。雖然圖片輪播很是的經常使用了,可是相信不少開發者對圖片輪播的實現仍是一知半曉,做爲一個有抱負、有追求的程序員,咱們仍是但願刨根問底,因此,必要時重複造下輪子仍是有必要的,況且圖片輪播並無咱們想象的那麼困難,尤爲在Android技術如此成熟的今天,結合官方控件來實現仍是很是容易的,固然,這篇文章是比較適合剛入門的Android開發者和初級Android開發者,我仍是不敢在大牛面前班門弄斧的,但願大牛們多多包涵。git

以前我發佈了一個開源的輪播控件AdPlayBanner,不少同窗都說本身也想實現一個,可是不知道從何下手,本文標題是手把手教學,因此本文會用簡單粗俗的語言教你們,如何按照->->實現這個思路來解決咱們所遇到的問題,但願作到真正授之以漁,而非授之以魚。程序員

Stpe1.腦子想

在作任何東西以前的第一步,就是咱們得在腦子裏有一個思考過程。github

就像咱們作這個圖片輪播,首先咱們就會想,在Google提供的官方Api中,有沒有相似的控件已經有實現類似的功能?而後咱們在腦子裏想啊想,終於,想到了兩個比較經常使用、比較流行的控件 ViewPagerRecyclerView尤爲ViewPager,它已經基本實現了圖片輪播的功能,只是缺乏了自動播放;而RecyclerView咱們都知道,它已經支持了水平的瀑布流,你們試想,當咱們將RecyclerView設爲水平佈局,而且每個item寬度爲屏幕寬度,一樣咱們也能夠實現圖片輪播的功能。假如,你真的沒有想到這兩個控件,你能夠經過自定義View來實現,固然,這個過程相對會比較複雜。緩存

OK。在經歷了上面一段腦子思考以後,我就選擇了採用ViewPager來實現,由於它是最接近圖片輪播的一個官方控件。那麼咱們還會想ViewPager距離咱們理想的圖片輪播到底有多少差距,首先,它還不支持自動播放,其次,它並不能從最後一張滑動回第一張。網絡

經歷完上面的腦力勞動以後,就該進行接下來一步,動手作!ide

Step2.動手作

從腦子想完以後,咱們選擇了ViewPager來實現圖片輪播,可是面臨了兩個須要解決的問題:oop

  1. ViewPager如何實現自動播放?佈局

  2. ViewPager如何實現從最後一張滑到第一張?post

有了問題,咱們就會想着怎麼去解決。

  • 第一個問題比較簡單, 咱們都知道ViewPager的Api裏有一個方法叫作setCurrentItem(int position),顧名思義,就是設置當前的Item爲數據源的第position個數據,那麼咱們就能夠經過一個runnable的run()方法裏面調用這個方法,而後在每次頁面切換完成時,延時執行這個runnable便可。

  • 第二個問題會比較複雜,咱們都知道ViewPager是沒法從最後一頁設置到第一頁,可是,咱們能不能將ViewPager的Adapter裏面設置它的size()爲一個很是大的值呢?這樣咱們就能夠實現無限循環了。那咱們怎麼保證數據的正確性呢?假如數據源只有幾個數據,而Adapter裏面的size()很是大,咱們就能夠經過取餘的方式來保證滑動頁面一直對應着數據源的幾個數據。還有就是,假如Adapter的size()很是大,咱們在Adapter的instantiateItem(ViewGroup container, int position)中就會須要返回不少new出來的View,這樣子會形成沒必要要的內存浪費,因此,咱們能夠經過一個ArrayList來做爲緩存,當咱們Adapter的destroyItem(ViewGroup container, int position, Object object)方法中,將廢棄的object存到緩存中,重複利用,避免了內存浪費。

    這兩個問題就這樣輕鬆地被解決了,也許會有人問,爲何這part叫動手作呢,不是想一想就行了嗎?要知道,這是我已經想好的思路,假如你面對的是一個沒有接觸過的問題,假如你不動動手在紙上構思,你的空想並不能給你帶來什麼。

    那麼接下來,咱們就該實現了!

    Step3.敲代碼

    當你梳理清楚了前面兩步的問題,那麼當你敲代碼實現的時候就很是簡單了。

    (1) 實現MyCircleBanner繼承ViewPager

    首先,實現一個類MyCircleBanner繼承於ViewPager,而後重寫構造方法。

    public class MyCircleBanner extends ViewPager {
        public MyCircleBanner(Context context) {
            super(context);
        }
    }
    複製代碼

    (2) 實現一個ViewPager的Adapter

    首先,在MyCircleBanner實現一個內部類BannerAdapter繼承PagerAdapter,它要求咱們必須重寫getCount()isViewFromObject(View view, Object object),並設置全局變量mViewCachesmInfos,其中mViewCaches用以緩存頁面沒被使用時被ViewPager置空的對象,mInfos做爲數據源集合。

    class BannerAdapter extends PagerAdapter{
    	private final ArrayList<Object> mViewCaches = new ArrayList<>();    //緩存ViewPager廢棄的對象
        private List<String> mInfos;	//數據源
    
        public BannerAdapter(List<String> mInfos) {
            this.mInfos = mInfos;
        }
    
        @Override
        public int getCount() {
            return 0;
        }
    
        @Override
        public boolean isViewFromObject(View view, Object object) {
            return false;
        }
    }
    複製代碼

    在第二步中,咱們討論到了一個問題,那就是,當ViewPager滑到最後一頁時沒法滑到第一頁,因此咱們能夠再getCount()方法裏面下手,返回一個很大的值,咱們取爲Integer.MAX_VALUE,即2^31 - 1,很是大的一個數,足以模擬近乎無限循環,因此getCount()能夠這麼實現:

    @Override
    public int getCount() {
        if (null != mInfos) {
            // 當只有一張圖片的時候,不滑動,返回1便可
            if (mInfos.size() == 1) {
                return 1;
            } else {
                // 不然循環播輪播,返回Int型的最大值
                return Integer.MAX_VALUE;
            }
        }
        else return 0;  // mInfos爲空時返回0
    }
    複製代碼

    isViewFromObject(View view, Object object)則直接這樣寫:

    @Override
    public boolean isViewFromObject(View view, Object object) {
        return view == object;
    }
    複製代碼

    在BannerAdapter中,除了實現這兩個方法還不夠的,還須要實現instantiateItemdestroyItem這兩個方法。

    那麼這兩個方法是什麼意思呢?首先public Object instantiateItem(ViewGroup container, int position)這個方法是說,當ViewPager顯示初始化到該頁面時,須要執行的方法,咱們能夠看到參數有一個container,即整個ViewPager的外佈局,position就是初始化到該頁面的位置,而且須要咱們返回一個Object類型,能夠理解爲返回咱們顯示當前ViewPager頁面的View,作一個輪播圖,咱們能夠直接返回一個ImageView。而另外一個方法void destroyItem(ViewGroup container, int position, Object object),就是當該頁面已經超出了用戶的可視範圍時,須要執行的方法。

    在實現這個方法以前,咱們來說解一下ViewPager這兩個方法的執行機制:

    因此ViewPager每一次都是隻有當前顯示頁和相鄰兩頁被初始化,試想,假如咱們將size()設置到很大,咱們一直向右滑,不斷執行instantiateItem方法,而後咱們在instantiateItem方法裏不斷地new一個ImageView出來,要知道destroyItem默認是空實現,那麼就會有愈來愈多沒有用到的ImageView佔用了內存,因此這時作緩存很是有必要,那麼這兩個方法的實現能夠以下:

    @Override
    public Object instantiateItem(ViewGroup container, int position) {
        if (mInfos != null && mInfos.size() > 0) {
            ImageView imageView;
            // 當緩存集合數量爲0時
            if (mViewCaches.isEmpty()) {
                imageView = new ImageView(context);   // 新建一個ImageView
                // 設置ImageView的基本寬高,和ScaleType
                imageView.setLayoutParams(new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT));
                imageView.setScaleType(ImageView.ScaleType.FIT_XY);
            } else {
                // 當緩存集合有數據時,複用,而後緩存再也不持有它的引用
                imageView = (ImageView) mViewCaches.remove(0);
            }
            // 使用Picasso加載網絡圖片
            Picasso.with(context).load(mInfos.get(position % mInfos.size())).into(imageView);
    
            // 把ViewPager這個佈局加載ImageView進來
            container.addView(imageView);
            return imageView;
        } else {
            return null;
        }
    }
    
    @Override
    public void destroyItem(ViewGroup container, int position, Object object) {
        // 當頁面不可見時,該View就會被ViewPager傳到這個方法的object中,咱們拿到該object轉爲ImageView
        ImageView imageView = (ImageView) object;
        // 在ViewPager佈局中移除這個view
        container.removeView(imageView);
        // 加到緩存裏
        mViewCaches.add(imageView);
    }
    複製代碼

    因此,咱們的BannerAdapter也就大功告成,整個BannerAdapter的代碼以下:

    class MyAdapter extends PagerAdapter {
        private final ArrayList<Object> mViewCaches = new ArrayList<>();     //緩存ViewPager廢棄的對象
        private List<String> mInfos;    //數據源
        private Context context;
    
        public MyAdapter(List<String> mInfos, Context context) {
            this.mInfos = mInfos;
            this.context = context;
        }
    
        @Override
        public int getCount() {
            if (null != mInfos) {
                // 當只有一張圖片的時候,不可滑動
                if (mInfos.size() == 1) {
                    return 1;
                } else {
                    // 不然循環播放滑動
                    return Integer.MAX_VALUE;
                }
            } else {
                return 0;  // mInfos爲空時返回0
            }
        }
    
    
        @Override
        public Object instantiateItem(ViewGroup container, int position) {
            if (mInfos != null && mInfos.size() > 0) {
                ImageView imageView;
                // 當緩存集合數量爲0時
                if (mViewCaches.isEmpty()) {
                    imageView = new ImageView(context);   // 新建一個ImageView
                    // 設置ImageView的基本寬高,和ScaleType
                    imageView.setLayoutParams(new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT));
                    imageView.setScaleType(ImageView.ScaleType.FIT_XY);
                } else {
                    // 當緩存集合有數據時,複用,而後緩存再也不持有它的引用
                    imageView = (ImageView) mViewCaches.remove(0);
                }
                // 使用Picasso加載網絡圖片
                Picasso.with(context).load(mInfos.get(position % mInfos.size())).into(imageView);
    
                // 把ViewPager這個佈局加載ImageView進來
                container.addView(imageView);
                return imageView;
            } else {
                return null;
            }
        }
    
        @Override
        public void destroyItem(ViewGroup container, int position, Object object) {
            // 當頁面不可見時,該View就會被ViewPager傳到這個方法的object中,咱們拿到該object轉爲ImageView
            ImageView imageView = (ImageView) object;
            // 在ViewPager佈局中移除這個view
            container.removeView(imageView);
            // 加到緩存裏
            mViewCaches.add(imageView);
        }
    
        @Override
        public boolean isViewFromObject(View view, Object object) {
            return view == object;
        }
    }
    複製代碼

    (3) 實現自動輪播功能

    首先須要實現一個Runnable任務,主要就是調用setCurrentItem()方法來設置ViewPager滑動到下一頁,固然要判斷一些極端case,例如滑動到最右邊時,處理爲返回到第一個。

    getInitPosition()則是獲取到從0-Integer.MAX_VALUE的中間左右位置,該位置並要和數據源的第一個元素取餘爲0,這樣就保證了ViewPager默認是從0-Integer.MAX_VALUE的中間位置開始滑動,使得它左右均可以實現近乎無限循環滑動。

    startAdvertPlay()則是把任務延時加到任務隊列,這裏設置延時3s,stopAdvertPlay()則是在ViewPager被Destroy時,清空任務隊列。

    /**
     * 自動播聽任務
     */
    private Runnable mImageTimmerTask = new Runnable() {
        @Override
        public void run() {
            if (mSelectedIndex == Integer.MAX_VALUE) {
                // 當滑到最右邊時,返回返回第一個元素
                // 固然,幾乎不可能滑到
                int rightPos = mSelectedIndex % mInfos.size();
                setCurrentItem(getInitPosition() + rightPos + 1, true);
            } else {
                // 常規執行這裏
                setCurrentItem(mSelectedIndex + 1, true);
            }
        }
    };
    
    /**
     * 獲取banner的初始位置,即0-Integer.MAX_VALUE之間的大概中間位置
     * 保證初始位置和數據源的第1個元素的取餘爲0
     *
     * @return
     */
    private int getInitPosition() {
        if (mInfos.isEmpty()) {
            return 0;
        }
        int halfValue = Integer.MAX_VALUE / 2;
        int position = halfValue % mInfos.size();
    	// 保證初始位置和數據源的第1個元素的取餘爲0
        return halfValue - position;
    }
    
    /**
     * 開始廣告滾動任務
     */
    private void startAdvertPlay() {
        stopAdvertPlay();
        mUIHandler.postDelayed(mImageTimmerTask, 1000);
    }
    
    /**
     * 中止廣告滾動任務
     */
    private void stopAdvertPlay() {
        mUIHandler.removeCallbacks(mImageTimmerTask);
    }
    複製代碼

    (4) 設置ViewPager的監聽器

    實現OnPageChangeListener能夠完成自動輪播功能,當ViewPager每次切換界面完成時都會執行三個方法,之因此在onPageScrollStateChanged()方法裏面調用startAdvertPlay()是由於當手指按下ViewPager時,咱們不會執行這個任務,只有當手指離開ViewPager時,纔會執行。

    /**
     * 輪播圖片狀態監聽器
     */
    private OnPageChangeListener mOnPageChangeListener = new OnPageChangeListener() {
    
        @Override
        public void onPageSelected(int position) {
            // 獲取當前的位置
            mSelectedIndex = position;
        }
    
        @Override
        public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {
        }
    
        @Override
        public void onPageScrollStateChanged(int state) {
    		// 當手指離開屏幕時,纔會執行
            if (state == ViewPager.SCROLL_STATE_IDLE) {
                startAdvertPlay();
            }
        }
    };
    複製代碼

    (5) 提供MyCircleBanner調用接口

    在完成了上面接口和方法實現以後,那麼就須要在MyCircleBanner內提供接口,傳入數據便可實現輪播控件自動播放。

    public void play(List<String> mInfos) {
        if (null != mInfos && mInfos.size() > 0) {
            this.mInfos = mInfos;
            mUIHandler = new Handler(Looper.getMainLooper());
            // new一個Adapter
            MyAdapter adapter = new MyAdapter(mInfos, getContext());
            // 設置adapter
            setAdapter(adapter);
            // 設置監聽器
            addOnPageChangeListener(mOnPageChangeListener);
            // 設置默認位置爲中間位置
            setCurrentItem(getInitPosition());
            if (mInfos.size() >= 1) {
                // 開始自動播放
                startAdvertPlay();
            }
        }
    }
    複製代碼

    因此整個MyCircleBanner的代碼是這樣的:

    public class MyCircleBanner extends ViewPager {
        private int mSelectedIndex = 0;     // 當前下標
        private Handler mUIHandler;
        private List<String> mInfos = new ArrayList<>();
    
    
        public MyCircleBanner(Context context) {
            this(context, null);
        }
    
        public MyCircleBanner(Context context, AttributeSet attrs) {
            super(context, attrs);
        }
    
        public void play(List<String> mInfos) {
            if (null != mInfos && mInfos.size() > 0) {
                this.mInfos = mInfos;
                mUIHandler = new Handler(Looper.getMainLooper());
                // new一個Adapter
                MyAdapter adapter = new MyAdapter(mInfos, getContext());
                // 設置adapter
                setAdapter(adapter);
                // 設置監聽器
                addOnPageChangeListener(mOnPageChangeListener);
                // 設置默認位置爲中間位置
                setCurrentItem(getInitPosition());
                if (mInfos.size() >= 1) {
                    // 開始自動播放
                    startAdvertPlay();
                }
            }
        }
    
        /**
         * 輪播圖片狀態監聽器
         */
        private OnPageChangeListener mOnPageChangeListener = new OnPageChangeListener() {
    
            @Override
            public void onPageSelected(int position) {
                Log.d("TAG", position + "");
                // 獲取當前的位置
                mSelectedIndex = position;
            }
    
            @Override
            public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {
            }
    
            @Override
            public void onPageScrollStateChanged(int state) {
                if (state == ViewPager.SCROLL_STATE_IDLE) {
                    startAdvertPlay();
                }
            }
        };
    
        /**
         * 自動播聽任務
         */
        private Runnable mImageTimmerTask = new Runnable() {
            @Override
            public void run() {
                if (mSelectedIndex == Integer.MAX_VALUE) {
                    // 當滑到最右邊時,返回返回第一個元素
                    // 固然,幾乎不可能滑到
                    int rightPos = mSelectedIndex % mInfos.size();
                    setCurrentItem(getInitPosition() + rightPos + 1, true);
                } else {
                    // 常規執行這裏
                    setCurrentItem(mSelectedIndex + 1, true);
                }
            }
        };
    
    
        /**
         * 獲取banner的初始位置,即0-Integer.MAX_VALUE之間的大概中間位置
         * 保證初始位置和數據源的第1個元素的取餘爲0
         *
         * @return
         */
    
        private int getInitPosition() {
            if (mInfos.isEmpty()) {
                return 0;
            }
            int halfValue = Integer.MAX_VALUE / 2;
            int position = halfValue % mInfos.size();
            // 保證初始位置和數據源的第1個元素的取餘爲0
            return halfValue - position;
        }
    
        /**
         * 開始廣告滾動任務
         */
        private void startAdvertPlay() {
            stopAdvertPlay();
            mUIHandler.postDelayed(mImageTimmerTask, 1000);
        }
    
        /**
         * 中止廣告滾動任務
         */
        private void stopAdvertPlay() {
            mUIHandler.removeCallbacks(mImageTimmerTask);
        }
    }
    複製代碼

    (6) 用法

    到此爲止,自定義的輪播控件已經完成,咱們只要在Xml裏面添加該控件,像這樣:

    <com.ryane.teach_circlebanner.MyCircleBanner
        android:id="@+id/mBanner"
        android:layout_width="match_parent"
        android:layout_height="200dp" />
    複製代碼

    而後,在Activity的oncreate()方法中:

    mBanner = (MyCircleBanner) findViewById(R.id.mBanner);
    
    // 設置數據源
    List<String> mInfos = new ArrayList<>();
    mInfos.add("http://onq81n53u.bkt.clouddn.com/photo1.jpg");
    mInfos.add("http://onq81n53u.bkt.clouddn.com/photo2.jpg");
    
    // 使用mBanner的接口,直接自動播放 
    mBanner.play(mInfos);
    複製代碼

    那麼,一個輪播控件就完成了。

    後記

    到此爲止,相信你們已經能夠本身實現一個圖片輪播了,我把本身的實現過程完整地告訴你們,也是但願你們可以在遇到問題時,可以踐行->->實現這個過程,如何可以靜下心來,認真地走這個過程,那麼我想不少困難都迎刃而解。

    固然,這個Demo只是一個比較簡略的實現,在這裏強烈安利一波個人一個開源控件:

    AdPlayBanner:功能豐富、一鍵式使用的圖片輪播插件

相關文章
相關標籤/搜索