Android App Banner,用它就够了。无限轮播、简单易用、扩展性强的BannerView

按照惯例先上效果图:
Android App Banner,用它就够了。无限轮播、简单易用、扩展性强的BannerView_第1张图片



还有各种效果

体验Demo

点击下载或扫码下载DemoApk

Android App Banner,用它就够了。无限轮播、简单易用、扩展性强的BannerView_第2张图片

写在前面

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 {
    implementation 'com.github.kelinZhou:Banner:2.5.6'
}

XML中使用


可以看到,基本所有的配置在布局中都可以完成(当然,也提供了通过代码配置的方法)。

指示器的使用

如果你需要指示器,本依赖库默认提供了两种指示器。你不需要在代码中做任何事情,所有的配置都可以在XML中完成。

圆点型指示器在XML中的使用


数字型指示器在XML中的使用


BannerView的自定义属性

app:pagingIntervalTime翻页间隔时长,用来配置每次自动翻页之间所间隔的时间,单位为毫秒。例如你想每5秒自动翻页一次,那么该属性应该为5000。

app:decelerateMultiple翻页动画减速倍数,因为BannerView是继承自ViewPager,所以每次自动翻页所需要的时长都是较短的,你可以通过设置该属性来配置减速倍数,也就是你希望每次自动翻页所需要的时长是ViewPager原时长的多少倍。

app:bannerIndicator为BannerView指定指示器,只要是实现了Pageable接口的View都可以,该库中所提供的所以指示器都实现了Pageable接口,如果你不满足于我提供的指示器控件,那么你可以自己动手写只要实现Pageable接口就可以配合BannerView使用。例如样例中的该属性的值为:@+id/biv_indicator

app:titleView为BannerView指定用来显示标题的控件,通过该属性配置标题控件后你就不需要监听BannerView的切换,然后再为标题控件赋值。只要你配置了该属性我会自动为你赋值。前提是你要配置的控件必须是TextView或其子类。例如样例中的该属性的值为:@+id/tv_title

app:subTitleView为BannerView指定用来显示副标题的控件,通过该属性配置标题控件后你就不需要监听BannerView的切换,然后再为副标题控件赋值。只要你配置了该属性我会自动为你赋值。前提是你要配置的控件必须是TextView或其子类。例如该属性的值为:@+id/tv_sub_title

app:interpolator翻页动画差值器,可以通过该属性配置自动翻页动画的动画差值器。例如该属性的值为:@android:anim/bounce_interpolator

app:singlePageMode因为支持无限轮播,那么只有一张图片的时候是否还需要无限轮播?这个属性就是用来配置当Banner中的图片只有一张时的处理方式的。该属性是一个flag属性,一共有以下两个值:

  1. noIndicator表示如果只有一张图片则没有指示器。也就是说无论你是否设置了指示器,如果只有一张图片的话那么指示器都是不显示的。但是依然是支持无限轮播的。

  2. canNotPaging表示如果只有一张图片则不可以轮播。但是如你设置了指示器的话,指示器依然会显示。

    上面两个属性可以同时配置,中间用"|"符号链接,例如上面代码中的配置。这样的话如果只有一张图片则既不会轮播而且无论你是否设置了指示器则都不会显示。

    以上所说的只有一张图片是指通过BannerViewsetEntries方法设置数据源时数据源集合的size()等于1。所说的设置只是器是指在XML代码中为BannerView配置app:bannerIndicator属性或者通过代码BannerView.setIndicatorView(@NonNull BannerIndicator indicatorView)为BannerView设置指示器。

app:loopMode配置轮播模式,该属性为枚举属性,有以下三个值可以配置:

  1. infiniteLoop无限循环轮播。
  2. fromCoverToCover从第一页轮播到最后一页,然后停止轮播。
  3. fromCoverToCoverLoop从第一页轮播到最后一页,然后再会到第一页后再轮播到最后一页,一直重复。

app:touchPauseEnable用来配置触摸暂停轮播是否可用,BannerView默认在被触摸时是会暂停自动轮播的,如果你不希望在BannerView被触摸后被暂停自动轮播这可以为该属性赋值为:false

PointIndicatorView的自定义属性

app:totalCount一共有多少个点(也就是总页数),如果是配合BannerView使用的则以BannerView的页数为准。这个属性最大的用途就是在写布局文件时可以及时看到效果,方便调试UI。

android:gravity设置偏移。只支持以下值的单一配置及合理组合:

Gravity.TOP、Gravity.BOTTOM、View.NO_IDGravity.LEFT、View.NO_IDGravity.RIGHT、View.NO_IDGravity.START、View.NO_IDGravity.END、View.NO_IDGravity.CENTER、View.NO_IDGravity.CENTER_VERTICAL、View.NO_IDGravity.CENTER_HORIZONTAL。

可以同时配置多个值,多个值之间用"|"(或)符号连接。但是不支持View.NO_IDGravity#FILL、View.NO_IDGravity#FILL_VERTICAL、View.NO_IDGravity#RELATIVE_HORIZONTAL_GRAVITY_MASK、以及View.NO_IDGravity#FILL_HORIZONTAL等类似配置。

app:pointSpacing点与点之间的间距,默认为最小的点的直径。

app:pointRadius点的半径。默认为3dp。

app:selectedPointRadius选中时点的半径,默认与pointRadius属性的值一直,如果你为pointRadius属性赋值5dp,那么该值的默认值就是5dp。

app:pointColor点的颜色。默认为25%透明度的白色。

app:selectedPointColor选中时点的颜色,默认为白色。

NumberIndicatorView的自定义属性

app:totalCount一共有多少个点(也就是总页数),如果是配合BannerView使用的则以BannerView的页数为准。这个属性最大的用途就是在写布局文件时可以及时看到效果,方便调试UI。

android:gravity设置偏移。只支持以下值的单一配置及合理组合:

android:textSize字体大小。

android:textColor字体颜色。

app:separator分隔符号。例如:/

app:separatorTextColor分割符文本颜色。

app:currentPageTextColor当前页码文本颜色。

app:totalPageTextColor总页码文本颜色。

代码中使用

//找到控件。
BannerView bannerView = itemView.findViewById(R.id.vp_view_pager);
//设置数据源,默认会启动轮播。如果不想启动轮播-bannerView.setEntries(entries, false);
bannerView.setEntries(entries);

设置数据源非常简单,调用BannerView的public void setEntries(List items),setEntries方法有一个重载public void setEntries(@NonNull List items, boolean start)第二个参数是说你设置完数据源是否需要启动轮播。而一个参数的方法模式也是调用的两个参数的,是默认启动轮播的。如果你不希望轮播则调用两个参数的方法。可以看到数据源必须是BannerEntry的子类。

数据模型BannerEntry源码

public interface BannerEntry {

    /**
     * 创建视图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 SimpleBannerEntry {

    public MyBannerEntry(MyBannerPage bannerPage) {
        super(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 getValue().getImgUrl();
    }

    @Override
    public CharSequence getTitle() {
        return getValue().getTitle();
    }

    @Override
    public CharSequence getSubTitle() {
        //没有子标题所以这里返回null。
        return null;
    }

    @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 {
    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 getBannerPagers() {
        List 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 o = (List) items.get(position);
                ArrayList 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 {

        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



R.layout.item_banner_layout




    

    

        
        

        
        
    

R.layout.item_normal_layout



R.layout.layout_title_banner_item



创作不易希望给个Star,你的支持是我的动力!如果有意见或者建议可以在下方留言也可以在GitHub上留言或者发邮件给我。

欢迎光临我的GitHub项目地址点击传送

你可能感兴趣的:(自定义控件,Android,UI)