一款支持无限轮播、简单易用、扩展性强且超级稳定的轮播图库-Banner(BannerView)

来源:互联网 发布:parrot无人机软件 编辑:程序博客网 时间:2024/04/29 16:31

按照惯例先上效果图:
标题等信息跟随页面滚动
标题固定,圆点指示器
标题固定,数字指示器

写在前面

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上留言或者发邮件给我。