打造一個絲滑般自動輪播無限循環Android庫

本文同步發佈在掘金,如需轉載請註明出處。

BannerViewPager系列文章共三篇,此文爲第一篇,另外兩篇參看下面鏈接:

《BannerViewPager源碼剖析》

《剖析BannerViewPager中Indicator的設計思想》

最近公司項目在升級AndroidX,由於項目中用到的一些比較老的庫都已停止更新維護,因此需要將這些庫替換掉,其中就包括自動輪播的Banner庫。恰逢筆者在之前寫過一個輪播圖,因此就在此基礎上重構,打造出了一個全新的支持多種樣式的輪播庫—BannerViewPager。個人覺得BannerViewPager要優於其它開源的Banner庫,不僅僅是因爲它擁有簡潔高效的代碼,更是因爲它高度的可定製性。BannerViewPager不僅支持任意的頁面佈局,而且可以支持任意的Indicator樣式。甚至連Indicator的位置都可以做到任意擺放。是的,就是這麼隨心所欲。無圖言叼,還是先通過圖片和代碼一覽BannerViewPager的功能吧(多圖預警)。

一、BannerViewPager效果預覽及API介紹

由於GIF圖片質量問題,下面的預覽圖並不清晰,大家可以點擊下面鏈接或者掃描二維碼下載Apk體驗。Apk存放在github上,下載速度可能會比較慢。

點擊或掃描二維碼下載apk
在這裏插入圖片描述

1.setIndicatorStyle(開局就放王炸?)

BannerViewPager目前內置了CIRCLE和DASH兩種樣式的指示器,通過setIndicatorStyle(int)一行代碼就可以切換指示器的樣式。當然,如果內置樣式不滿足你的需求。BannerViewPager還提供了自定義指示器的功能。只要繼承BaseIndicatorView或者實現IIndicator接口,並重寫相應方法,就可以通過自定義View爲所欲爲的打造任意的Indicator了。如下圖【自定義】就是自己實現的指示器樣式。

CIRCLE DASH 自定義

下面通過代碼演示如何切換指示器:

mViewPager.setIndicatorStyle(IndicatorStyle.DASH)
          .setIndicatorHeight(BannerUtils.dp2px(3f))
          .setIndicatorWidth(BannerUtils.dp2px(3), BannerUtils.dp2px(10))
          .setHolderCreator(() -> new ImageResourceViewHolder(0))
          .create(mDrawableList)

通過5行代碼就輕鬆的實現了上圖【Dash】仿支付寶的Indicator樣式(大家可以留意一下支付寶的輪播Indicator,挺有意思)。

關於自定義IndicatorView將會放在後邊章節詳細講解。

2.setPageStyle

通過setPageStyle(int)一行代碼開啓一屏三頁模式,一屏三頁模式下目前有三種樣式,分別如下圖所示:

MULTI_PAGE MULTI_PAGE_SCALE MULTI_PAGE_OVERLAP
MULTI_PAGE MULTI_PAGE_SCALE MULTI_PAGE_OVERLAP

代碼演示:

    mViewPager.setPageStyle(PageStyle.MULTI_PAGE)
              .setPageMargin(BannerUtils.dp2px(10))
              .setRevealWidth(BannerUtils.dp2px(10))
              .setHolderCreator(() -> new ImageResourceViewHolder(BannerUtils.dp2px(5)))
              .create(mDrawableList);

同樣通過短短5行代碼就實現了上圖【MULTI_PAGE】的效果,簡單好用!

3.如何實現指示器位置任意擺放?

我們看到上面圖表中MULTI_PAGE_OVERLAP模式下指示器顯示到了Banner的下邊。這種效果該怎麼實現呢?其實BannerViewPager是支持把Indicator擺放在任意位置的。之所以能如此強大是因爲我們通過自定義指示器替換了內置的IndicatorView,也就是說此時的IndicatorView已經脫離了BannerViewPager,也就理所當然的可以放在任意位置了。接下來通過代碼來看下如何實現:

(1)Xml佈局文件如下

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <com.zhpan.bannerview.BannerViewPager
        android:id="@+id/banner_view"
        android:layout_width="match_parent"
        android:layout_height="180dp"
        android:layout_marginTop="20dp"
        app:bvp_page_style="multi_page" />

    <com.zhpan.bannerview.indicator.CircleIndicatorView
        android:id="@+id/indicator_view"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_below="@id/banner_view"
        android:layout_centerHorizontal="true"
        android:layout_marginTop="10dp" />
 </RelativeLayout>

(2)通過setIndicatorView(IIndicator)替換內部指示器

        CircleIndicatorView indicatorView = findViewById(R.id.indicator_view);
 	mViewPager.setIndicatorView(indicatorView)
                  .setIndicatorColor(Color.parseColor("#888888"),
                       Color.parseColor("#118EEA"))
                  .setHolderCreator(() -> new ImageResourceViewHolder(BannerUtils.dp2px(5)))
                  .create(mDrawableList);

CircleIndicatorView是什麼?其實他就是內置在BannerViewPager中的指示器,現在你只需要把它同BannerViewPager放在同一個佈局文件中就可以了。又是僅僅通過一行代碼就完成了對內部指示器的替換,不知道你看完之後是否會拍案叫絕,竟然如此簡單!

4.setIndicatorSlideMode

我們應該見過很多App輪播圖的指示器都會跟隨頁面一起滑動。BannerViewPager自然也不會少了這個功能。通過setIndicatorSlideMode(int)一行代碼就可以輕鬆切換到下圖(SMOOTH)的效果。

NORMAL SMOOTH
在這裏插入圖片描述 在這裏插入圖片描述

代碼實現仍然非常簡單,使用BannerViewPager你只需要記住一個核心–Only One Line!所以演示代碼不再貼出你應該不會揍我吧?

5.setPageTransformerStyle

關於Transform更好的方式應該是留給開發者自己去實現,因此BannerViewPager中目前僅內置了四種常用Transform樣式,如果不能滿足需求,可以通過BannerViewPager的setPageTransformer(ViewPager.PageTransformer transformer)設置自定義的Transform。四種內置Transform樣式如下:

STACK ACCORDION DEPTH ROTATE

當然,BannerViewPager的功能並不僅僅侷限於此,更多功能就不再演示,可以看下面所有開放的API接口。

6.BannerViewPager開放的API

BannerViewPager開放了衆多API,以供滿足不同的需求,具體如下表:

方法名 方法描述 說明
BannerViewPager<T, VH> setCanLoop(boolean canLoop) 是否開啓循環 默認值true
BannerViewPager<T, VH> setAutoPlay(boolean autoPlay) 是否開啓自動輪播 默認值true
BannerViewPager<T, VH> setInterval(int interval) 自動輪播時間間隔 單位毫秒,默認值3000
BannerViewPager<T, VH> setScrollDuration(int scrollDuration) 設置頁面滾動時間 設置頁面滾動時間
BannerViewPager<T, VH> setRoundCorner(int radius) 設置圓角 默認無圓角 需要SDK_INT>=LOLLIPOP(API 21)
BannerViewPager<T, VH> setOnPageClickListener(OnPageClickListener onPageClickListener) 設置頁面點擊事件
BannerViewPager<T, VH> setHolderCreator(HolderCreator<VH> holderCreator) 設置HolderCreator 必須設置HolderCreator,否則會拋出NullPointerException
BannerViewPager<T, VH> setIndicatorVisibility(@Visibility int visibility) indicator vibility 默認值VISIBLE 2.4.2 新增
BannerViewPager<T, VH> setIndicatorStyle(int indicatorStyle) 設置指示器樣式 可選枚舉(CIRCLE, DASH) 默認CIRCLE
BannerViewPager<T, VH> setIndicatorGravity(int gravity) 指示器位置 可選值(CENTER、START、END)默認值CENTER
BannerViewPager<T, VH> setIndicatorColor(int normalColor,int checkedColor) 指示器圓點顏色 normalColor:未選中時顏色默認"#8C6C6D72", checkedColor:選中時顏色 默認"#8C18171C"
BannerViewPager<T, VH> setIndicatorSlideMode(int slideMode) 設置Indicator滑動模式 可選(NORMAL、SMOOTH),默認值SMOOTH
BannerViewPager<T, VH> setIndicatorRadius(int radius) 設置指示器圓點半徑 默認值4dp
BannerViewPager<T, VH> setIndicatorRadius(int normalRadius,int checkRadius) 設置指示器圓點半徑 normalRadius:未選中時半徑 checkedRadius:選中時的半徑,默認值4dp
BannerViewPager<T, VH> setIndicatorWidth(int indicatorWidth) 設置指示器寬度,如果是圓形指示器,則爲直徑 默認值8dp
BannerViewPager<T, VH> setIndicatorWidth(int normalWidth, int checkWidth) 設置指示器寬度,如果是圓形指示器,則爲直徑 默認值8dp
BannerViewPager<T, VH> setIndicatorHeight(int indicatorHeight) 設置指示器高度,僅在Indicator樣式爲DASH時有效 默認值normalIndicatorWidth/2
BannerViewPager<T, VH> setIndicatorGap(int indicatorMargin) 指示器圓點間距 默認值爲指示器寬度(或者是圓的直徑)
BannerViewPager<T, VH> setIndicatorView(IIndicator indicatorView) 設置自定義指示器
BannerViewPager<T, VH> setPageTransformerStyle(int style) 設置頁面Transformer內置樣式
BannerViewPager<T, VH> setCurrentItem(int item) Set the currently selected page. 2.3.5新增
void getCurrentItem() 獲取當前position 2.3.5新增
BannerViewPager<T, VH> setPageStyle(PageStyle pageStyle) 設置頁面樣式 2.4.0新增 可選(MULTI_PAGE、NORMAL)MULTI_PAGE:一屏多頁樣式
BannerViewPager<T, VH> setPageMargin(int pageMargin) 設置頁面間隔 2.4.0新增
BannerViewPager<T, VH> setIndicatorMargin(int left, int top, int right, int bottom) 設置Indicator邊距 2.4.1新增
BannerViewPager<T, VH> setOnPageChangeListener(OnPageChangeListener l) 頁面改變的監聽事件 2.4.3新增
void startLoop() 開啓自動輪播 初始化BannerViewPager時不必調用該方法,設置setAutoPlay後會調用startLoop()
void stopLoop() 停止自動輪播 如果開啓自動輪播,爲避免內存泄漏需要在onStop()或onDestroy中調用此方法
List<T> getList() 獲取Banner中的集合數據
void create(List list) 初始化並構造BannerViewPager 必須調用,否則前面設置的參數無效

7.BannerViewPager支持的attrs

你也可以通過xml來設置BannerViewPager,xml支持的attrs如下:

Attributes format description
bvp_interval integer 自動輪播時間間隔
bvp_scroll_duration integer 頁面切換時滑動時間
bvp_can_loop boolean 是否循環
bvp_auto_play boolean 是否自動播放
bvp_indicator_checked_color color indicator選中時顏色
bvp_indicator_normal_color color indicator未選中時顏色
bvp_indicator_radius dimension indicator圓點半徑或者Dash模式的1/2寬度
bvp_round_corner dimension Banner圓角大小
bvp_page_margin dimension 頁面item間距
bvp_reveal_width dimension 一屏多頁模式下兩邊item漏出的寬度
bvp_indicator_style enum indicator樣式(circle/dash)
bvp_indicator_slide_mode enum indicator滑動模式(normal/smooth)
bvp_indicator_gravity enum indicator位置(center/start/end)
bvp_page_style enum page樣式(normal/multi_page/multi_page_overlap/multi_page_scale)
bvp_transformer_style enum transform樣式(normal/depth/stack/accordion)
bvp_indicator_visibility enum indicator visibility(visible/gone/invisible)

二、BannerViewPager詳細使用說明

1.gradle中添加依賴

如果您已遷移到AndroidX請使用latestVersion(>=2.4.3.1)

implementation 'com.zhpan.library:bannerview:latestVersion'

如果未遷移到AndroidX請使用(非Androidx的包託管在JCenter上):

implementation 'com.zhpan.library:bannerview:2.4.3.1'

2. 在xml文件中添加如下代碼:

    <com.zhpan.bannerview.BannerViewPager
            android:id="@+id/banner_view"
            android:layout_width="match_parent"
            android:layout_margin="10dp"
            android:layout_height="160dp" />

3.Banner的Item頁面佈局

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="wrap_content">

    <ImageView
        android:id="@+id/banner_image"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:scaleType="centerCrop" />

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_alignParentBottom="true"
        android:background="#66000000"
        android:gravity="center_vertical">

        <TextView
            android:id="@+id/tv_describe"
            android:layout_width="wrap_content"
            android:layout_height="match_parent"
            android:layout_gravity="center_vertical"
            android:layout_marginStart="15dp"
            android:gravity="center_vertical"
            android:paddingTop="5dp"
            android:paddingBottom="5dp"
            android:textColor="#FFFFFF"
            android:textSize="16sp" />
    </LinearLayout>

</RelativeLayout>

4.自定義ViewHolder

public class NetViewHolder implements ViewHolder<BannerData> {
    private ImageView mImageView;
    private TextView mTextView;

    @Override
    public View createView(ViewGroup viewGroup, Context context, int position) {
        View view = LayoutInflater.from(context).inflate(R.layout.item_net, viewGroup, false);
        mImageView = view.findViewById(R.id.banner_image);
        mTextView = view.findViewById(R.id.tv_describe);
        return view;
    }

    @Override
    public void onBind(Context context, BannerData data, int position, int size) {
        ImageLoaderOptions options = new ImageLoaderOptions.Builder().into(mImageView).load(data.getImagePath()).placeHolder(R.drawable.placeholder).build();
        ImageLoaderManager.getInstance().loadImage(options);
        mTextView.setText(data.getTitle());
    }
}

5.BannerViewPager參數配置

    private BannerViewPager<BannerData, NetViewHolder> mBannerViewPager;
	private void initViewPager() {
             mBannerViewPager = findViewById(R.id.banner_view);
             mBannerViewPager.showIndicator(true)
                .setInterval(3000)
                .setCanLoop(false)
                .setAutoPlay(true)
                .setRoundCorner(DpUtils.dp2px(7))
                .setIndicatorColor(Color.parseColor("#935656"), Color.parseColor("#FF4C39"))
                .setIndicatorGravity(BannerViewPager.END)
                .setScrollDuration(1000).setHolderCreator(NetViewHolder::new)
                .setOnPageClickListener(position -> {
                    BannerData bannerData = mBannerViewPager.getList().get(position);
                    Toast.makeText(NetworkBannerActivity.this,
                            "點擊了圖片" + position + " " + bannerData.getDesc(), Toast.LENGTH_SHORT).show();

                }).create(mList);
        }

6.開啓與停止輪播

2.5.0之後版本無需自行在Activity或Fragment中管理stopLoop和startLoop方法,但這兩個方法依舊保留對外開發

如果開啓了自動輪播功能,請務必在onDestroy中停止輪播,以免出現內存泄漏。

	@Override
    protected void onDestroy() {
        super.onDestroy();
        if (mBannerViewPager != null)
    		mViewpager.stopLoop();
    }

爲了節省性能也可以在onStop中停止輪播,在onResume中開啓輪播:

    @Override
    protected void onStop() {
        super.onStop();
        if (mBannerViewPager != null)
            mBannerViewPager.stopLoop();
    }

    @Override
    protected void onResume() {
        super.onResume();
        if (mBannerViewPager != null)
            mBannerViewPager.startLoop();
    }

三、高級功能—自定義IndicatorView

因爲指示器的樣式千變萬化,BannerViewPager中不可能內置所有的樣式,因此我將定義權限交給了開發者自己來實現,這樣就可以滿足所有開發者的需求了。但是自定義IndicatorView需要有一定的自定義View基礎,儘管我已經在BaseIndicatorView中處理了許多邏輯,但是還是要開發者根據自身需求進行Indicator的繪製。好了,下面就讓我們來看看如何實現自定義IndicatorView吧。

關於自定義IndicatorView其實我們在第一節中講解Indicator擺放位置時已經提到了,就是通過setIndicator(IIndicator)來替換內部的指示器。當然,這個方法接收的參數不僅僅是內置的兩個IndicatorView,它還可以是我們自己實現的Indicator。前提只需要繼承BaseIndicatorView或者繼承View並實現IIndicator,然後根據需求繪製即可。

(1)認識BaseIndicatorView

BaseIndicatorView是BannerViewPager庫中的一個類,它繼承自View並實現了IIndicator接口。在這個類中存儲了BannerViewPager的許多參數信息,比如頁面個數(pageSize)、頁面滑動進度(slideProgress)以及當前頁面位置(currentPosition)等,這些都是在繪製IndicatorView時會用到的信息。有了這些參數之後我們就可以比較輕鬆的去繪製指示器了。如果你覺得我這些數據計算的不夠精確或者計算存在錯誤,那麼你大可以自己實現IIndicator接口自行計算。本文我會通過繼承BaseIndicatorView的方式來實現一個自定義指示器的例子。
你可以點擊鏈接查看BaseIndicatorView的完整代碼。

(2)開啓自定義IndicatorView之路

好了,接下來我們就來完成一個如下圖所示的自定義IndicatorView吧!

新建一個FigureIndicatorView類並繼承BaseIndicatorView

public class FigureIndicatorView extends BaseIndicatorView {

    private int radius = DpUtils.dp2px(20);

    private int backgroundColor = Color.parseColor("#88FF5252");

    private int textColor = Color.WHITE;

    private int textSize=DpUtils.dp2px(13);

    // ...省略無關代碼

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        setMeasuredDimension(2 * radius, 2 * radius);
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        mPaint.setColor(backgroundColor);
        canvas.drawCircle(getWidth() / 2, getHeight() / 2, radius, mPaint);
        mPaint.setColor(textColor);
        mPaint.setTextSize(textSize);
        String text = currentPosition + 1 + "/" + pageSize;
        int textWidth = (int) mPaint.measureText(text);
        Paint.FontMetricsInt fontMetricsInt = mPaint.getFontMetricsInt();
        int baseline = (getMeasuredHeight() - fontMetricsInt.bottom + fontMetricsInt.top) / 2 - fontMetricsInt.top;
        canvas.drawText(text, (getWidth() - textWidth) / 2, baseline, mPaint);
    }

    public void setRadius(int radius) {
        this.radius = radius;
    }

    @Override
    public void setBackgroundColor(@ColorInt int backgroundColor) {
        this.backgroundColor = backgroundColor;
    }

    public void setTextSize(int textSize) {
        this.textSize = textSize;
    }
    // ...省略無關代碼
}

有自定義View基礎的同學應該能很輕鬆的看懂上邊的代碼。首先通過onMeasure()方法測量了View的大小,接下來就是在onDraw方法中繪製圓和文字了。很容易就實現了一個自定義的IndicatorView。當然,這個例子本身就比較簡單。如果你需要繪製比較複雜且帶有動畫的Indicator,可以參考源碼中的CircleIndicatorView和DashIndicatorView,或許它能給你一些靈感。

(3)設置自定義指示器

接下來就將我們自己繪製的指示器設置到BannerViewPager中吧!

    FigureIndicatorView indicatorView = new FigureIndicatorView(mContext);
    indicatorView.setRadius(BannerUtils.dp2px(18));
    indicatorView.setTextSize(BannerUtils.dp2px(13));
    indicatorView.setBackgroundColor(Color.parseColor("#aa118EEA"));
    
    mViewPager.setIndicatorGravity(IndicatorGravity.END)
              .setIndicatorView(indicatorView)
              .setHolderCreator(() -> new ImageResourceViewHolder(0))
              .create(mDrawableList);    
        

依然如此瀟灑自然!好了,關於BannerViewPager的介紹今天就講解到這裏了。接下來的一篇文章將會對BannerViewPager的源碼進行剖析,瞭解下它是如何通過簡單的Api實現來實現複雜的功能的。

都看到這裏了,確定不到GitHub點個星再走?源碼已放到文章末尾。如果有好的Idea也歡迎Pull Request。

《BannerViewPager源碼解析》

源碼下載

文章上次更新2019.11.16

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章