自定義輪播控件BannerView

一、介紹

在項目中使用的自動輪播控件一直是網(wǎng)上別人做的焕梅,在出現(xiàn)問題的時候去看代碼細(xì)節(jié)掃雷就非常浪費時間迹鹅。于是痛定思痛自己造個輪子。
這個控件在app中使用非常頻繁贞言,并且原理也不復(fù)雜斜棚,就是在前后各加一頁。相信每一個android開發(fā)者都會做這個東西该窗。
功能介紹:
1.無限自動輪播弟蚀。
2.指示器(下方的小點點)
3.滾動動畫時間可調(diào)
4.拖拽的時候停止輪播


實際效果圖

全部代碼和示例代碼已經(jīng)上傳到GitHub上了:
https://github.com/CuteWen/BannerView
有興趣的可以下過來看看。

二酗失、實現(xiàn)

首先要自定義一個View去繼承ViewPager
然后我們自動輪播實現(xiàn)的關(guān)鍵其實都在PagerAdapter里面义钉,我們可以自己封裝一個PagerAdapter,但是自己封裝的Adapter就會讓使用者在寫邏輯的時候要了解你的adapter封裝到什么程度了规肴,放出哪些方法捶闸,個人不太喜歡那樣子,所以我這里使用了裝飾者模式來擴(kuò)展使用者寫好的Adapter拖刃,這樣使用的時候只要寫一個最普通的PagerAdapter 就可以附加上自動輪播的功能了删壮。

注:不太懂裝飾者模式的同學(xué)可以去這里看一下,里面講解的挺好的兑牡。
https://www.cnblogs.com/chenxing818/p/4705919.html

1.包裝類

思考一下我們需要包裝的功能央碟,其實也就是要將頁數(shù)+2,主要就是getCount這個方法了均函,另外在里面也要寫好兩個適配器之間的position轉(zhuǎn)化的方法亿虽,統(tǒng)一調(diào)用這些方法可以避免邏輯的混亂。
下面就是我們的包裝類了边酒。

/**
     * 適配器的包裝類---------------------------------------------------------
     */
    private class BannerAdapterWrapper extends PagerAdapter {
        private PagerAdapter pagerAdapter;

        public BannerAdapterWrapper(PagerAdapter pagerAdapter) {
            this.pagerAdapter = pagerAdapter;
        }

        @Override
        public int getCount() {
            return pagerAdapter.getCount() > 1 ? pagerAdapter.getCount() + 2 : pagerAdapter.getCount();
        }

        @Override
        public boolean isViewFromObject(View view, Object object) {
            return view.equals(object);
        }

        @Override
        public Object instantiateItem(ViewGroup container, int position) {
            return pagerAdapter.instantiateItem(container, bannerToAdapterPosition(position));
        }

        @Override
        public void destroyItem(ViewGroup container, int position, Object object) {
            pagerAdapter.destroyItem(container, position, object);
        }

        /**
         * 展示出的position和實際的position 轉(zhuǎn)換
         */
        public int bannerToAdapterPosition(int position) {
            int adapterCount = pagerAdapter.getCount();
            if (adapterCount <= 1) return 0;
            int adapterPosition = (position - 1) % adapterCount;
            if (adapterPosition < 0) adapterPosition += adapterCount;
            return adapterPosition;
        }

        public int toWrapperPosition(int position) {
            return position + 1;
        }
    }

主要做了:
1.getCount的上限加了2 也就是前后各多一頁的作用经柴。
2.寫了兩個適配器之間的position之間的轉(zhuǎn)換方法方便調(diào)用。

2.暗度陳倉(AdapterWrapper)之后的善后工作

看一下setAdapter方法:

 /**
     * 設(shè)置適配器的時候做初始化工作
     */
    @Override
    public void setAdapter(PagerAdapter adapter) {
        this.adapter = adapter;
        //注冊原適配器刷新時的監(jiān)聽
        this.adapter.registerDataSetObserver(new BannerPagerObserver());
        //初始化包裝適配器
        bannerAdapterWrapper = new BannerAdapterWrapper(adapter);
        //實際配置的adapter是包裝后的適配器
        super.setAdapter(bannerAdapterWrapper);
        //注冊適配器的監(jiān)聽 (這個在后文介紹)
        addOnPageChangeListener(new BannerPageChangeListener());
        //初始化handler處理定時事件 (這個在后文介紹)
        looperHandler = new LooperHandler(this);
    }

這里注冊了一個DataSetObserver墩朦,這個平時用到的還比較少坯认,它是用來監(jiān)聽Adapter.notifyDataSetChanged()的。
因為我們實際上綁定BannerView的是Wrapper之后的適配器adapter氓涣,而使用者手里調(diào)用的是原adapter的notifyDataSetChanged()牛哺,所以需要進(jìn)行一個傳遞過程!

/**
     * 數(shù)據(jù)刷新 傳遞刷新信號-----------------------------------------------------
     */
    private class BannerPagerObserver extends DataSetObserver {

        @Override
        public void onChanged() {
            super.onChanged();
            dataSetChanged();
        }

        @Override
        public void onInvalidated() {
            super.onInvalidated();
            dataSetChanged();
        }
    }

    /**
     * 刷新數(shù)據(jù)方法
     */
    private void dataSetChanged() {
        if (bannerAdapterWrapper != null && pagerAdapter.getCount() > 0) {
            bannerAdapterWrapper.notifyDataSetChanged();
            bannerIndicatorView.setCount(pagerAdapter.getCount());
            setCurrentItem(0);
        }
    }

同理劳吠,我們在調(diào)用setCurrentItem()方法的時候position也是不一樣的引润。


    @Override
    public void setCurrentItem(int item, boolean smoothScroll) {
        super.setCurrentItem(bannerAdapterWrapper.toWrapperPosition(item), smoothScroll);
    }

    @Override
    public void setCurrentItem(int item) {
        super.setCurrentItem(bannerAdapterWrapper.toWrapperPosition(item));
    }

    @Override
    public int getCurrentItem() {
        return bannerAdapterWrapper.bannerToAdapterPosition(super.getCurrentItem());
    }

3.翻頁監(jiān)聽

/**
    * 監(jiān)聽翻頁----------------------------------------------------------------
    */
  private class BannerPageChangeListener implements OnPageChangeListener {

       @Override
       public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {

       }

       @Override
       public void onPageSelected(int position) {
           // 在這里同步指示器
           if (bannerIndicatorView != null) {
               bannerIndicatorView.setSelect(bannerAdapterWrapper.bannerToAdapterPosition(position));
           }
       }

       @Override
       public void onPageScrollStateChanged(int state) {
           int position = BannerView.super.getCurrentItem();
           // 無限輪播的跳轉(zhuǎn)
           if (state == ViewPager.SCROLL_STATE_IDLE &&
                   (position == 0 || position == bannerAdapterWrapper.getCount() - 1)) {
               setCurrentItem(bannerAdapterWrapper.bannerToAdapterPosition(position), false);
           }
           // 手指拖動翻頁的時候暫停自動輪播
           if (state == ViewPager.SCROLL_STATE_IDLE) {
               if (timer == null) {
                   timer = new Timer();
                   timer.schedule(new TimerTask() {
                       @Override
                       public void run() {
                           looperHandler.sendEmptyMessage(0);
                       }
                   }, intervalTime + scrollTime, intervalTime + scrollTime);
               }
           } else if (state == ViewPager.SCROLL_STATE_DRAGGING) {
               if (timer != null) {
                   timer.cancel();
                   timer = null;
               }
           }
       }
   }

里面的同步指示器和暫停自動輪播代碼暫且不表。
主要就是無限輪播的跳轉(zhuǎn)那一段代碼 完成“無限”的實現(xiàn)痒玩。

4.自動輪播

這里我們使用了Timer+Handler的組合來完成定時滑動的操作:

    /**
     * 設(shè)置間隔時間 并開始Timer任務(wù)
     */
    public void setIntervalTime(int intervalTime) {
        this.intervalTime = intervalTime;
        timer = new Timer();
        timer.schedule(new TimerTask() {
            @Override
            public void run() {
                looperHandler.sendEmptyMessage(0);
            }
        }, intervalTime + scrollTime, intervalTime + scrollTime);
    }

    /**
     * 處理定時任務(wù)-------------------------------------------------------------------
     */
    private static class LooperHandler extends Handler {
        private WeakReference<BannerView> weakReference;

        public LooperHandler(BannerView bannerView) {
            this.weakReference = new WeakReference<>(bannerView);
        }

        @Override
        public void handleMessage(Message msg) {
            super.handleMessage(msg);
            weakReference.get().setCurrentItem(weakReference.get().getCurrentItem() + 1);
        }
    }

另外還有設(shè)置滾動的時間,這里需要使用一下反射去修改mScroller這個對象淳附。

    /**
     * 設(shè)置滾動時間  利用反射
     */
    public void setScrollTime(int scrollTime) {
        try {
            Field field = ViewPager.class.getDeclaredField("mScroller");
            field.setAccessible(true);
            FixedSpeedScroller scroller = new FixedSpeedScroller(getContext(),
                    new AccelerateInterpolator());
            field.set(this, scroller);
            scroller.setScrollDuration(scrollTime);
        } catch (NoSuchFieldException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        }
    }

/**
     * 修改ViewPager的滑動動畫時間-----------------------------------------------------------
     */
    private class FixedSpeedScroller extends Scroller {
        private int duration = 300;

        public FixedSpeedScroller(Context context, Interpolator interpolator) {
            super(context, interpolator);
        }

        @Override
        public void startScroll(int startX, int startY, int dx, int dy, int duration) {
            super.startScroll(startX, startY, dx, dy, this.duration);
        }

        @Override
        public void startScroll(int startX, int startY, int dx, int dy) {
            super.startScroll(startX, startY, dx, dy, this.duration);
        }

        public void setScrollDuration(int duration) {
            this.duration = duration;
        }
    }

5. 指示器

先上代碼

public class BannerIndicatorView extends View {
    private int count;
    private int select;

    private Paint pointPaint;
    private Paint selectPaint;
    private String selectColor = "#FFFFFF";
    private String normalColor = "#80FFFFFF";

    private int radius = 10;
    private int interval = 10;

    public BannerIndicatorView(Context context) {
        this(context, null);
    }

    public BannerIndicatorView(Context context, @Nullable AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public BannerIndicatorView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        init();
    }

    private void init() {
        pointPaint = new Paint();
        pointPaint.setFlags(Paint.ANTI_ALIAS_FLAG);
        pointPaint.setColor(Color.parseColor(normalColor));
        pointPaint.setStyle(Paint.Style.FILL_AND_STROKE);

        selectPaint = new Paint();
        selectPaint.setFlags(Paint.ANTI_ALIAS_FLAG);
        selectPaint.setColor(Color.parseColor(selectColor));
        selectPaint.setStyle(Paint.Style.FILL_AND_STROKE);
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        // 畫出各個點的位置
        for (int i = 0; i < count; i++) {
            if (i == select) {
                canvas.drawCircle(radius + i * (radius * 2 + interval), getHeight() / 2, radius, selectPaint);
            } else {
                canvas.drawCircle(radius + i * (radius * 2 + interval), getHeight() / 2, radius, pointPaint);
            }
        }
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        int width = count * radius * 2 + (count - 1) * interval;
        int height = radius * 2;
        widthMeasureSpec = MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY);
        heightMeasureSpec = MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY);
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
    }
    /**
     * 設(shè)置第幾個點選中议慰,然后刷新
     */
    public void setSelect(int select) {
        this.select = select;
        invalidate();
    }

    /**
     * 設(shè)置個數(shù)
     */
    public void setCount(int c) {
        count = c;
    }

    public void setSelectColor(String selectColor) {
        this.selectColor = selectColor;
    }

    public void setNormalColor(String normalColor) {
        this.normalColor = normalColor;
    }
}

這部分還是比較簡單的,就是繪制了幾個白色小圓點奴曙,然后提供setSelect的方法來變化選中點别凹。

然后在BannerView里面寫上setIndicator()的方法

    /**
     * 設(shè)置指示器,需要在setAdapter之后
     */
    public void setIndicator(BannerIndicatorView bannerIndicatorView) {
        this.bannerIndicatorView = bannerIndicatorView;
        if (pagerAdapter != null) {
            bannerIndicatorView.setCount(pagerAdapter.getCount());
        }
    }

三:示例與全部代碼

在XML中的示例寫法:

    <com.wzl.custom.BannerView
        android:id="@+id/bv_activity_banner"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"/>

    <com.wzl.custom.BannerIndicatorView
        android:id="@+id/biv_activity_banner"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_alignBottom="@id/bv_activity_banner"
        android:layout_marginBottom="7dp"
        android:layout_centerHorizontal="true"
        />

注意: android:layout_centerHorizonta = "true" 是為了讓點居中洽糟。

class BannerActivity : AppCompatActivity() {
    var bannerView: BannerView? = null
    var indicatorView: BannerIndicatorView? = null
    var adapter: BannerAdapter? = null

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_banner)
        bannerView = findViewById(R.id.bv_activity_banner) as BannerView
        indicatorView = findViewById(R.id.biv_activity_banner) as BannerIndicatorView
        adapter = BannerAdapter(this)
        // 設(shè)置adapter
        bannerView?.adapter = adapter
        // 綁定指示器
        bannerView?.setIndicator(indicatorView)
        // 滾動動畫的時間
        bannerView?.setScrollTime(500)
        // 設(shè)置輪播間隔
        bannerView?.setIntervalTime(3000)
        val data:ArrayList<String> = ArrayList()
        data.add("1111")
        data.add("2222")
        data.add("1111")
        data.add("2222")
        adapter?.addData(data)
    }
}

這部分使用kotlin寫的炉菲,不過調(diào)用就這幾個方法,應(yīng)該沒什么看不懂的地方了坤溃。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末拍霜,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子薪介,更是在濱河造成了極大的恐慌祠饺,老刑警劉巖,帶你破解...
    沈念sama閱讀 219,188評論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件昭灵,死亡現(xiàn)場離奇詭異吠裆,居然都是意外死亡伐谈,警方通過查閱死者的電腦和手機(jī)烂完,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,464評論 3 395
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來诵棵,“玉大人抠蚣,你說我怎么就攤上這事÷陌模” “怎么了嘶窄?”我有些...
    開封第一講書人閱讀 165,562評論 0 356
  • 文/不壞的土叔 我叫張陵,是天一觀的道長距贷。 經(jīng)常有香客問我柄冲,道長,這世上最難降的妖魔是什么忠蝗? 我笑而不...
    開封第一講書人閱讀 58,893評論 1 295
  • 正文 為了忘掉前任现横,我火速辦了婚禮,結(jié)果婚禮上阁最,老公的妹妹穿的比我還像新娘戒祠。我一直安慰自己,他們只是感情好速种,可當(dāng)我...
    茶點故事閱讀 67,917評論 6 392
  • 文/花漫 我一把揭開白布姜盈。 她就那樣靜靜地躺著,像睡著了一般配阵。 火紅的嫁衣襯著肌膚如雪馏颂。 梳的紋絲不亂的頭發(fā)上示血,一...
    開封第一講書人閱讀 51,708評論 1 305
  • 那天,我揣著相機(jī)與錄音救拉,去河邊找鬼矾芙。 笑死,一個胖子當(dāng)著我的面吹牛近上,可吹牛的內(nèi)容都是我干的剔宪。 我是一名探鬼主播,決...
    沈念sama閱讀 40,430評論 3 420
  • 文/蒼蘭香墨 我猛地睜開眼壹无,長吁一口氣:“原來是場噩夢啊……” “哼葱绒!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起斗锭,我...
    開封第一講書人閱讀 39,342評論 0 276
  • 序言:老撾萬榮一對情侶失蹤地淀,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后岖是,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體帮毁,經(jīng)...
    沈念sama閱讀 45,801評論 1 317
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,976評論 3 337
  • 正文 我和宋清朗相戀三年豺撑,在試婚紗的時候發(fā)現(xiàn)自己被綠了烈疚。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 40,115評論 1 351
  • 序言:一個原本活蹦亂跳的男人離奇死亡聪轿,死狀恐怖爷肝,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情陆错,我是刑警寧澤灯抛,帶...
    沈念sama閱讀 35,804評論 5 346
  • 正文 年R本政府宣布,位于F島的核電站音瓷,受9級特大地震影響对嚼,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜绳慎,卻給世界環(huán)境...
    茶點故事閱讀 41,458評論 3 331
  • 文/蒙蒙 一纵竖、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧偷线,春花似錦磨确、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,008評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至亥曹,卻和暖如春邓了,著一層夾襖步出監(jiān)牢的瞬間恨诱,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,135評論 1 272
  • 我被黑心中介騙來泰國打工骗炉, 沒想到剛下飛機(jī)就差點兒被人妖公主榨干…… 1. 我叫王不留照宝,地道東北人。 一個月前我還...
    沈念sama閱讀 48,365評論 3 373
  • 正文 我出身青樓句葵,卻偏偏與公主長得像厕鹃,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子乍丈,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 45,055評論 2 355

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

  • Android 自定義View的各種姿勢1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 172,178評論 25 707
  • 1 上層空間參考下圖進(jìn)行調(diào)整 2 3歲模特替換下來 更換為存儲空間 1 一塊層板上的疊裝需是同款不同色 2 家居服...
    Holiday澈閱讀 375評論 0 0
  • 我小時候最怕學(xué)生字轻专,見生字就頭痛忆矛,背完就忘,錯字連篇请垛。你會否也覺得學(xué)漢字太枯燥催训,太難了!你是否也是死記硬背...
    我是阿蘇閱讀 613評論 0 0
  • 都說時間的長河很長 歲月很慢 但過著過著就一年又一年 一切短的仿若昨日就在身邊 還沒來得及溫暖的日子 就這樣劃過指...
    尋一束光閱讀 132評論 0 1
  • 【最美山西·文化】 內(nèi)儲說上 ?七術(shù)(四) 不參之患 (韓非子) 原文: 叔孫相魯宗收,貴而主斷漫拭。其所...
    張梅霞?xì)g樂誦閱讀 580評論 0 0