按照慣例先上效果圖:
寫在前面
GitHub上也是有比較詳細的使用介紹的,如果你想直接看GitHub上的也可以直接點擊後面的傳送門去往GitHub。我是傳送門
本文的內容可能有點長,如果你想要直接但Demo的源碼的,可以直接跳到最後,最後有完整的代碼(包括Java代碼和XML代碼)。
前言
今天給大家推薦一款簡單易用、擴展性強且超級穩定的輪播圖庫。
·爲什麼說簡單易用?答:因爲實現起來比較簡單,兩行代碼就可以輕鬆實現。
//找到控件。
BannerView bannerView = findViewById(R.id.vp_banner_view);
//設置數據源並啓動輪播。
bannerView.setEntries(entries, true);
·爲什麼說擴展性強?答:佈局樣式完全由自己決定,想怎麼佈局就怎麼佈局,我的原則是你的佈局你做主。如果你需要指示器你可以使用我提供的圓點型指示器也可以使用數字型指示器。什麼?都不喜歡?沒關係,你還可以實現Pageable接口或繼承BannerIndicator抽象類實現自己什麼腦洞打開的指示器都沒關係。什麼?不會寫自定義控件?沒關係可以使用任何第三方的或者任何類型的炫酷的NB的自定義控件作爲指示器,只是這時你需要對BannerView設置監聽,通過回調方法void onPageSelected(BannerEntry entry, int index)
來爲你的自定義指示器設置指針。你想要自定義翻頁動畫?沒關係因爲這個庫是基於ViewPager實現的,所以你可以向使用ViewPager那樣對BannerView(ViewPager的子類)調用void setPageTransformer(boolean reverseDrawingOrder, PageTransformer transformer)
方法設置翻頁動畫。不瞭解PageTransformer的可以百度、google或則直接拷貝google官方文檔中的樣板,網上以大堆。注意,雖然BannerView是ViewPager的子類,但是依然支持改變翻頁動畫時長,依然支持自定義動畫差值器(可通過代碼和XML兩種方式實現)。
·爲什麼說超級穩定?答:大家都知道我們的輪播圖一般都是配合RecyclerView或ListView作爲它的一個Iitem使用的。但是VeiwPager在配合RecyclerView使用時有很多問題(ListView沒有驗證過不過根據Bug的原因推測也是有問題的)。比如,當ViewPager自動滑動到一半的時候,將其隱藏再顯示後,會出現無法自動滑完,動畫會在隱藏時的位置卡住,直到下一次自動輪播纔會恢復(只不過很多app的翻頁動畫時間過短,所以很難出現這種問題)。再比如,當ViewPager完全隱藏後再次顯示則在下一次輪播時沒有動畫。還有其他的問題就不一一贅述了(以上問題在市場上的很多app都存在。)。這些問題這個庫都解決了。(不知不覺寫了這麼多,是不是有點王婆賣瓜?)
下面進入正題
Gradle配置
首先要在你的Gradle中進行配置纔可以使用。
第一步:添加 JitPack 倉庫到你項目根目錄的 gradle 文件中。
allprojects {
repositories {
...
maven { url 'https://jitpack.io' }
}
}
第二步:添加這個依賴。
dependencies {
compile 'com.github.kelinZhou:Banner:2.1.0'
}
XML中使用
<com.kelin.banner.view.BannerView
android:id="@+id/vp_view_pager"
android:layout_width="match_parent"
android:layout_height="match_parent"
<!--爲BannerView指定指示器,只要是實現了Pageable接口的View都可以。-->
app:bannerIndicator="@+id/biv_indicator"
<!--爲BannerView指定用來顯示標題的控件。-->
app:titleView="@+id/tv_title"
<!--爲BannerView指定用來顯示副標題的控件。-->
<!--app:subTitleView=""-->
<!--爲BannerView設置翻頁間隔爲3秒,默認爲5000(5秒)。-->
app:pagingIntervalTime="3000"
<!--當輪播圖中的頁面只有一頁時的輪播模式,下面是同時設置爲:及不輪播也不顯示指示器,默認是及能輪播也有指示器(前提是你設置了指示器也啓動了輪播)。-->
app:singlePageMode="canNotPaging|noIndicator"
<!--爲BannerView設置翻頁時長減速倍數(是ViewPager默認時長的幾倍)。默認與ViewPager一致(1倍)。-->
app:decelerateMultiple="4"
<!--爲BannerView指定動畫差值器-->
<!--app:interpolator=""-->
android:background="#FFF"/>
可以看到,基本所有的配置在佈局中都可以完成(當然,也提供了通過代碼配置的方法)。
指示器的使用
如果你需要指示器,本依賴庫默認提供了兩種指示器。你不需要在代碼中做任何事情,所有的配置都可以在XML中完成。
圓點型指示器在XML中的使用
<com.kelin.banner.view.PointIndicatorView
android:id="@+id/biv_indicator"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
<!--設置總頁數,這個參數設置了也是沒有意義的,最總會以BannerView的總頁數爲準。配置自定義屬性只是爲了能再佈局文件中看到效果-->
app:totalCount="4"
<!--圓點的半徑,默認3dp-->
app:pointRadius="3dp"
<!--選中時(也就是當前頁)圓點、的半徑默認與沒有選中時一致-->
app:selectedPointRadius="4dp"
<!--圓點與圓點之間的間距,默認爲選中時圓點的直徑。-->
app:pointSpacing="4dp"
<!--圓點的顏色,默認爲25%的透明白色。-->
app:pointColor="#5fff"
<!--選中時(也就是當前頁)圓點的顏色,默認爲白色。-->
app:selectedPointColor="@android:color/white"/>
數字型指示器在XML中的使用
<com.kelin.banner.view.NumberIndicatorView
android:id="@+id/biv_indicator"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
<!--字體大小-->
android:textSize="16sp"
<!--字體顏色,如果你爲所有的字體都單獨設置了顏色,那麼這個設置就會失效,如果有任何一個(例如當前頁碼)沒有單獨設置字體顏色,那麼她就會使用這個顏色。-->
android:textColor="@android:color/white"
<!--指定分隔符文本-->
app:separator="/"
<!--分隔符的字體顏色-->
app:separatorTextColor="@android:color/holo_green_light"
<!--當前頁碼的字體顏色-->
app:currentPageTextColor="@android:color/white"
<!--總頁碼的字體顏色-->
app:totalPageTextColor="@android:color/white"
app:totalCount="6"/>
代碼中使用
//找到控件。
BannerView bannerView = itemView.findViewById(R.id.vp_view_pager);
//設置數據源,默認會啓動輪播。如果不想啓動輪播-bannerView.setEntries(entries, false);
bannerView.setEntries(entries);
設置數據源非常簡單,調用BannerView的public void setEntries(List<? extends BannerEntry> items)
,setEntries方法有一個重載public void setEntries(@NonNull List<? extends BannerEntry> items, boolean start)
第二個參數是說你設置完數據源是否需要啓動輪播。而一個參數的方法模式也是調用的兩個參數的,是默認啓動輪播的。如果你不希望輪播則調用兩個參數的方法。可以看到數據源必須是BannerEntry的子類。
數據模型BannerEntry源碼
public interface BannerEntry<VALUE> {
/**
* 創建視圖View。
*/
View onCreateView(ViewGroup parent);
/**
* 獲取標題。
*/
CharSequence getTitle();
/**
* 獲取子標題。
*/
CharSequence getSubTitle();
/**
* 獲取當前頁面的數據。改方法爲輔助方法,是爲了方便使用者調用而提供的,Api本身並沒有任何調用。如果你不需要該方法可以空實現。
*/
VALUE getValue();
/**
* 比較兩個模型是否相同。這個方法類似於Object.equals(Object)方法。
*/
boolean same(BannerEntry newEntry);
}
大致就這幾個方法,獲取標題、獲取子標題、創建頁面中的View視圖。爲什麼沒有獲取圖片的方法?因爲視圖完全是自己創建的,所以我不需要關心你的圖片是什麼,因爲你有可能是使用本地圖片,也有可能使用網絡圖片。如果是網絡圖片的話,那麼你的圖片加載器有可能是任何方式。所以視圖完全由自己創建。
重要介紹onCreateView方法
這個方法是需要你創建視圖的時候調用的,你需要將你創建好的視圖返回,這有點像Fragment的onCreateView方法,不過你不用擔心,雖然輪播圖是無限輪播的,但是onCreateView並不是每次新的頁面顯示出來就會執行,而是你的輪播圖有幾頁就只會執行幾次,也就是說相對於當前對象而言,只會執行一次。當已經出現過的頁面再次進入屏幕時不會重新執行onCreateView,而是直接複用上一次已經創建好的View。
重要介紹same方法
這個方法是在2.0版本纔有的,這個是幹嘛用的呢?雖然註釋已經寫的很詳細了,我還是要在囉嗦一下。因爲我們設置數據源基本都是在onBindViewHolder的時候設置的,而onBindViewHolder不是隻調用一次,隨着你的ViewHolder在屏幕中的顯示與消失會不停的調用,如果每次設置數據源都刷新視圖的話,將會有點浪費性能,所以我在設置數據源後對數據源進行比較,如果本次設置的數據源與上一次的一致就不進行刷新視圖的操作。但是有些東西並不是我所知道的,比如圖片,我不知道你是本地圖片還是網絡圖片,所以能提供了這樣一個方法。也是爲了提高性能而提供的。
BannerEntry的兩種實現方式
第一種是像下面這種,我稱之爲包裝實現方式,是講我們自己的數據模型包裝到BannerEntry的子類中:
private class MyBannerEntry implements BannerEntry<MyBannerPage> {
private MyBannerPage mBannerPage;
public MyBannerEntry(MyBannerPage bannerPage) {
mBannerPage = bannerPage;
}
@Override
public View onCreateView(ViewGroup parent) {
View entryView = LayoutInflater.from(parent.getContext()).inflate(R.layout.layout_title_banner_item, parent, false);
ImageView imageView = entryView.findViewById(R.id.iv_image);
//這個庫沒有集成圖片框架是因爲大家的項目中所使用的圖片框架可能不是都是一樣的。使用什麼圖片框架應該由大家自己決定,而不是依賴庫來決定。
Glide.with(parent.getContext())
.load(getImgUrl())
.into(imageView);
return entryView;
}
private String getImgUrl() {
return mBannerPage.getImgUrl();
}
@Override
public CharSequence getTitle() {
return mBannerPage.getTitle();
}
@Override
public CharSequence getSubTitle() {
//沒有子標題所以這裏返回null。
return null;
}
@Override
public MyBannerPage getValue() {
//這個方法api本身沒有任何調用,也可以空實現。是爲方便開發者而提供的。有點類似於View.getTag()方法。
return mBannerPage;
}
@Override
public boolean same(BannerEntry newEntry) {
return newEntry != null //兌現不爲null
&& newEntry instanceof MyBannerEntry //類型相同
&& TextUtils.equals(newEntry.getTitle(), getTitle()) //標題相同
&& TextUtils.equals(((MyBannerEntry) newEntry).getImgUrl(), getImgUrl()); //圖片地址相同
}
}
這種方式適合網絡模型中的字段比較多,而且大多都是有用的。比如我們點擊輪播圖後要將模型攜帶到新的Activity。
第二種是下面這種懶漢實現方式,就是讓我們自己的模型直接實現BannerEntry接口:
public class MyBannerEntry implements BannerEntry<String> {
private final String webUrl;
private String title;
private String subTitle;
private String imgUrl;
MyBannerEntry(String title, String subTitle, String imgUrl, String webUrl) {
this.title = title;
this.subTitle = subTitle;
this.imgUrl = imgUrl;
this.webUrl = webUrl;
}
@Override
public View onCreateView(ViewGroup parent) {
View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.layout_title_banner_item, parent, false);
ImageView imageView = (ImageView) view.findViewById(R.id.iv_image);
Glide.with(parent.getContext())
.load(imgUrl)
.into(imageView);
return view;
}
/**
* 獲取標題
*
* @return 返回當前條目的標題。
*/
@Override
public CharSequence getTitle() {
return title;
}
/**
* 獲取子標題。
*
* @return 返回當前條目的子標題。
*/
@Nullable
@Override
public CharSequence getSubTitle() {
return subTitle;
}
/**
* 獲取當前頁面的數據。
*
* @return 返回當前頁面的數據。
*/
@Override
public String getValue() {
return webUrl;
}
@Override
public boolean same(BannerEntry newEntry) {
return newEntry instanceof MyBannerEntry
&& TextUtils.equals(title, newEntry.getTitle())
&& TextUtils.equals(subTitle, newEntry.getSubTitle())
&& TextUtils.equals(imgUrl, ((MyBannerEntry) newEntry).imgUrl)
&& TextUtils.equals(webUrl, ((MyBannerEntry) newEntry).webUrl);
}
}
這種方式爲什麼我說是懶漢式呢?因爲我是直接用網絡數據模型實現BannerEntry接口,這麼做就不用在做模型轉換,從網絡框架中得到的數據就直接可以使用。比較適合喜歡偷懶且數據模型中沒有太多字段,或大多字段都沒有什麼用處。
設置監聽
頁面點擊監聽
bannerView.setOnPageClickListener(new BannerView.OnPageClickListener() {
@Override
protected void onPageClick(BannerEntry entry, int index) {
//某個頁面被單擊後執行,entry就是這個頁面的數據模型。index是頁面索引,從0開始。
}
});
頁面長按監聽
bannerView.setOnPageLongClickListener(new BannerView.OnPageLongClickListener() {
@Override
public void onPageLongClick(BannerEntry entry, int index) {
//某個頁面被長按後執行,entry就是這個頁面的數據模型。index是頁面索引,從0開始。
}
});
頁面改變監聽
bannerView.setOnPageChangedListener(new BannerView.OnPageChangeListener() {
@Override
public void onPageSelected(BannerEntry entry, int index) {
//某個頁面被選中後執行,entry就是這個頁面的數據模型。index是頁面索引,從0開始。
}
@Override
public void onPageScrolled(int index, float positionOffset, int positionOffsetPixels) {
//頁面滑動中執行,這個與ViewPage的回調一致。
}
@Override
public void onPageScrollStateChanged(int state) {
//頁面滑動的狀態被改變時執行,也是與ViewPager的回調一致。
}
});
到這裏貌似都說完了,可能我說的有點囉嗦了有人更喜歡通過看代碼瞭解,下面我吧完整的代碼發出來吧,便於閱讀我都寫成了內部類。
Demo中所有的代碼
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
RecyclerView recyclerView = findViewById(R.id.rv_list);
recyclerView.setLayoutManager(new LinearLayoutManager(this));
MyRecyclerViewAdapter adapter = new MyRecyclerViewAdapter(getData());
recyclerView.setAdapter(adapter);
}
@SuppressWarnings("unchecked")
public List getData() {
List list = new ArrayList();
list.add(getBannerPagers());
for (int i = 0; i < 100; i++) {
list.add("我是條目" + i);
}
return list;
}
public List<MyBannerPage> getBannerPagers() {
List<MyBannerPage> list = new ArrayList<>();
//下面的BannerPage就好比是你從網絡上獲取到的數據模型,大家能明白這個意思就行。
MyBannerPage bannerPage1 = new MyBannerPage("大話西遊:“炸毛韜”引誘老妖", "http://m.qiyipic.com/common/lego/20171026/dd116655c96d4a249253167727ed37c8.jpg");
MyBannerPage bannerPage2 = new MyBannerPage("天使之路:藏風大片遇高反危機", "http://m.qiyipic.com/common/lego/20171029/c9c3800f35f84f1398b89740f80d8aa6.jpg");
MyBannerPage bannerPage3 = new MyBannerPage("星空海2:陸漓設局害慘吳居藍", "http://m.qiyipic.com/common/lego/20171023/bd84e15d8dd44d7c9674218de30ac75c.jpg");
MyBannerPage bannerPage4 = new MyBannerPage("中國職業脫口秀大賽:狂笑首播", "http://m.qiyipic.com/common/lego/20171028/f1b872de43e649ddbf624b1451ebf95e.jpg");
MyBannerPage bannerPage5 = new MyBannerPage("奇秀好音樂,你身邊的音樂真人秀", "http://pic2.qiyipic.com/common/20171027/cdc6210c26e24f08940d36a5eb918c34.jpg");
//將我們所有的BannerPage的實現類都放入的List集合中。
list.add(bannerPage1);
list.add(bannerPage2);
list.add(bannerPage3);
list.add(bannerPage4);
list.add(bannerPage5);
return list;
}
private class MyRecyclerViewAdapter extends RecyclerView.Adapter {
private List items;
MyRecyclerViewAdapter(List items) {
this.items = items;
}
@Override
public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
if (viewType == 0) {
return new BannerViewHolder(parent);
} else {
return new ItemViewHolder(parent);
}
}
@Override
public int getItemViewType(int position) {
return position == 0 ? 0 : 1;
}
@Override
@SuppressWarnings("unchecked")
public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {
if (getItemViewType(position) == 0) {
BannerViewHolder viewHolder = (BannerViewHolder) holder;
List<MyBannerPage> o = (List<MyBannerPage>) items.get(position);
ArrayList<MyBannerEntry> entries = new ArrayList<>();
for (MyBannerPage page : o) {
entries.add(new MyBannerEntry(page));
}
viewHolder.mBannerView.setEntries(entries);
} else {
ItemViewHolder viewHolder = (ItemViewHolder) holder;
viewHolder.mTextView.setText((String) items.get(position));
}
}
@Override
public int getItemCount() {
return items == null ? 0 : items.size();
}
}
private class BannerViewHolder extends RecyclerView.ViewHolder {
private final BannerView mBannerView;
BannerViewHolder(ViewGroup parent) {
super(LayoutInflater.from(parent.getContext()).inflate(R.layout.item_banner_layout, parent, false));
mBannerView = itemView.findViewById(R.id.vp_view_pager);
mBannerView.setOnPageClickListener(new BannerView.OnPageClickListener() {
@Override
protected void onPageClick(BannerEntry entry, int index) {
//因爲index是索引而索引是從0開始的所以表示頁數是:index+1
Toast.makeText(getApplicationContext(), String.format(Locale.CHINA, "您點擊了BannerView的第%d頁!", index + 1), Toast.LENGTH_SHORT).show();
}
});
}
}
private class ItemViewHolder extends RecyclerView.ViewHolder {
private final TextView mTextView;
ItemViewHolder(ViewGroup parent) {
super(LayoutInflater.from(parent.getContext()).inflate(R.layout.item_normal_layout, parent, false));
mTextView = (TextView) itemView;
}
}
private class MyBannerEntry implements BannerEntry<MyBannerPage> {
private MyBannerPage mBannerPage;
public MyBannerEntry(MyBannerPage bannerPage) {
mBannerPage = bannerPage;
}
@Override
public View onCreateView(ViewGroup parent) {
View entryView = LayoutInflater.from(parent.getContext()).inflate(R.layout.layout_title_banner_item, parent, false);
ImageView imageView = entryView.findViewById(R.id.iv_image);
//這個庫沒有集成圖片框架是因爲大家的項目中所使用的圖片框架可能不是都是一樣的。使用什麼圖片框架應該由大家自己決定,而不是依賴庫來決定。
Glide.with(parent.getContext())
.load(getImgUrl())
.into(imageView);
return entryView;
}
private String getImgUrl() {
return mBannerPage.getImgUrl();
}
@Override
public CharSequence getTitle() {
return mBannerPage.getTitle();
}
@Override
public CharSequence getSubTitle() {
//沒有子標題所以這裏返回null。
return null;
}
@Override
public MyBannerPage getValue() {
//這個方法api本身沒有任何調用,也可以空實現。是爲方便開發者而提供的。有點類似於View.getTag()方法。
return mBannerPage;
}
@Override
public boolean same(BannerEntry newEntry) {
return newEntry != null //兌現不爲null
&& newEntry instanceof MyBannerEntry //類型相同
&& TextUtils.equals(newEntry.getTitle(), getTitle()) //標題相同
&& TextUtils.equals(((MyBannerEntry) newEntry).getImgUrl(), getImgUrl()); //圖片地址相同
}
}
private class MyBannerPage {
private String title;
private String imgUrl;
public MyBannerPage(String title, String imgUrl) {
this.title = title;
this.imgUrl = imgUrl;
}
public String getTitle() {
return title;
}
public String getImgUrl() {
return imgUrl;
}
}
}
Demo中所有的佈局文件
R.layout.activity_main
<?xml version="1.0" encoding="utf-8"?>
<android.support.v7.widget.RecyclerView
xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/rv_list"
android:layout_width="match_parent"
android:layout_height="match_parent"/>
R.layout.item_banner_layout
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="175dp"
android:orientation="vertical">
<com.kelin.banner.view.BannerView
android:id="@+id/vp_view_pager"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:bannerIndicator="@+id/biv_indicator"
app:titleView="@+id/tv_title"
app:pagingIntervalTime="3000"
app:singlePageMode="canNotPaging|noIndicator"
app:decelerateMultiple="4"
android:background="#FFF"/>
<LinearLayout android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_alignParentBottom="true"
android:background="#8000"
android:gravity="center_vertical"
android:orientation="horizontal"
android:padding="6dp">
<!--用來顯示標題的控件-->
<TextView
android:id="@+id/tv_title"
android:layout_width="0dp"
android:layout_weight="1"
android:layout_height="wrap_content"
android:textColor="@android:color/white"
android:textSize="15sp"
android:textStyle="bold"
tools:text="我是標題!"/>
<!--Banner的圓點型指示器-->
<com.kelin.banner.view.PointIndicatorView
android:id="@+id/biv_indicator"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:totalCount="4"
app:pointRadius="3dp"
app:selectedPointRadius="4dp"
app:pointSpacing="4dp"
app:pointColor="#5fff"
app:selectedPointColor="@android:color/white"/>
</LinearLayout>
</RelativeLayout>
R.layout.item_normal_layout
<?xml version="1.0" encoding="utf-8"?>
<TextView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingBottom="16dp"
android:paddingLeft="8dp"
android:paddingRight="8dp"
android:paddingTop="18dp"
android:textColor="@android:color/black"
android:textSize="16sp"
tools:text="條目一"/>
R.layout.layout_title_banner_item
<?xml version="1.0" encoding="utf-8"?>
<ImageView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/iv_image"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:scaleType="centerCrop"/>
GitHub地址:https://github.com/kelinZhou/Banner 如果你喜歡希望給個Star如果有意見或者建議可以在下方留言也可以在GitHub上留言或者發郵件給我。