简介:这是一份面向Android开发者的Banner轮播组件源码包,版本1.4.10,适合需要快速实现广告位、推荐位等滚动展示效果,或希望研究轮播库封装思路的中初级开发者。压缩包共108个文件,约1.39MB,以45个Java源文件和32个XML布局/资源为主,另含JPG/PNG图片素材、Gradle构建脚本及工程配置文件,整体结构便于按模块查阅。内容围绕自动轮播、无限循环、过渡动画、手势切换、自定义适配器与样式、点击事件等常见需求展开,开发者可据此了解数据绑定、视图切换与图片加载优化的实现方式,并在此基础上做二次开发或集成到现有项目。已有396人学习下载,适合作为Android Banner功能落地的参考工程。
1. banner-release-1.4.10.zip 解包:android banner 轮播图那两百行胶水代码终于能删了
首页顶部三张活动图,产品要无限循环、自动轮播、手指按住就停、指示器跟着变、点进去还要埋点——这套需求在 android banner 场景里几乎每隔两周就复现一次。多数人第一次写会拿 ViewPager 加一个 Handler 定时器,两百多行胶水代码跑起来,上线两周后开始出现「退出页面还在切图」的卡顿反馈。banner-release-1.4.10.zip 就是把这个轮子固化成一份可直接依赖的构件包:控件本体、指示器绘制、自动轮播计时、无限循环的取模逻辑、生命周期开关全在里面,属性能在 XML 里配,也能在代码里改。它适合正在做电商首页、内容流顶部推荐位、活动弹窗轮播的 Android 开发,也适合接手老项目、需要把一个年久失修的轮播组件整体换掉的人。下面按「包里有什么 → 怎么接 → 参数怎么调 → 哪里会翻车」的顺序拆。
2. 拆包与接入:从压缩包到首页能滑的第一屏
拿到一个 release 压缩包,最忌讳的动作是直接扔进 libs 就开始写布局。先花三分钟看清它是什么形态,能省掉后面半小时的「找不到符号」。这一步的判断依据只有两个:包内是 aar 还是源码 module,以及控件的全限定名到底是什么。
2.1 先看清 release 包里到底有什么
release 包通常包含 aar、sources.jar、pom、javadoc 这几件东西,但不同打包脚本产出的目录层级差别很大,有的是banner-1.4.10/banner-1.4.10.aar,有的直接平铺在根目录。先列结构,再逐层往下钻,别靠猜。
# 1. 看压缩包顶层结构,判断是 aar 形态还是源码 module 形态 unzip -l banner-release-1.4.10.zip # 2. 解到临时目录,方便反复看 mkdir -p /tmp/banner_pkg && unzip -o banner-release-1.4.10.zip -d /tmp/banner_pkg # 3. 如果是 aar,看 aar 内部的 classes.jar / res / AndroidManifest unzip -l /tmp/banner_pkg/**/banner-*.aar # 4. 关键一步:从 classes.jar 的顶层目录反推控件包名 unzip -l /tmp/banner_pkg/**/classes.jar | head -n 40第 4 步是整个接入流程里性价比最高的一条命令。classes.jar 里的顶层目录名就是 Java 包路径,布局 XML 里要写的com.xxx.banner.Banner就是「顶层目录 + 类名」。我见过太多人卡在「控件类名到底叫什么」,其实一条unzip -l就出来了。如果解出来的不是 aar 而是一堆build.gradle和src/main/java,说明这是源码形态,走 module 引入:settings.gradle里include ':banner',app/build.gradle里implementation project(':banner'),其余步骤完全一致。
提示:包里如果同时有
-sources.jar,优先解它看attrs.xml和公开方法签名,比反编译 classes.jar 舒服得多,参数名和默认值一目了然。
2.2 把 aar 接进工程:三种引入方式与它们的差别
aar 的引入方式有三种:flatDir、files 直接依赖、本地 maven 仓库。选哪个取决于你是单人维护还是要给团队其他模块共用。单人项目用 flatDir 最快;多模块共用建议搭本地 maven 仓,版本号能写死在依赖里,升级不用挨个改路径。
// 方式一:flatDir,最省事,适合单个 aar 的单模块项目 // AGP 7+ 建议写在 settings.gradle 的 dependencyResolutionManagement 里 dependencyResolutionManagement { repositories { google() mavenCentral() flatDir { dirs "$rootDir/app/libs" } // aar 存放目录,按你实际路径改 } } // app/build.gradle dependencies { // name 是不带 .aar 后缀的文件名,ext 必须写 aar implementation(name: 'banner-1.4.10', ext: 'aar') // aar 不会自动带出它编译期依赖的三方库,缺什么按报错补什么 // 常见的是 viewpager、recyclerview 这类 UI 基础库, // 版本要和你工程里其他模块已用的对齐,避免同一库两个版本 implementation "androidx.viewpager:viewpager:${viewpagerVersion}" implementation "androidx.recyclerview:recyclerview:${recyclerViewVersion}" }参数含义说清楚:name是文件名去掉后缀,写错一个字符就是Failed to resolve;ext不写会被当成 jar 处理,编出来的东西缺资源文件,运行时指示器直接不显示。第二种implementation files('libs/banner-1.4.10.aar')也能编过,但 aar 内部 AndroidManifest 的合并行为在不同 AGP 版本上表现不一致,遇到过「资源找不到但明明打进去了」的情况,后来我统一改成 flatDir,省心。
注意:本地 aar 不做依赖传递,这是它和远程依赖最大的差别。编不过的第一反应应该是看报错里缺的是哪个包,而不是怀疑 aar 损坏。
2.3 布局与第一段绑定代码:先用本地图跑通链路
接入验证的顺序很重要:先用本地 drawable 跑通「能显示、能滑、能自动切、能点」,再去接网络图和业务数据。反过来做,一旦白屏你分不清是控件没接好还是图片加载失败,排查成本翻倍。
<!-- activity_main.xml:高度建议写死 dp 或按比例算,不要用 wrap_content --> <com.xxx.banner.Banner android:id="@+id/banner" android:layout_width="match_parent" android:layout_height="180dp" app:is_auto_loop="true" app:is_infinite_loop="true" app:delay_time="3000" /> <!-- com.xxx.banner.Banner 里的 xxx 换成 2.1 步查到的真实包名 -->Banner banner = findViewById(R.id.banner); // 先塞本地资源,验证控件本身没问题 List<Integer> resIds = Arrays.asList(R.drawable.banner_1, R.drawable.banner_2, R.drawable.banner_3); // ImageLoader 必须在 setImages 之前设置,否则首帧拿不到图 banner.setImageLoader(new ImageLoader() { @Override public void displayImage(Context context, Object path, ImageView imageView) { // path 就是 setImages 里塞进去的元素,这里强转回 Integer // 换成网络图时,把这一行替换成项目里现成的图片加载库调用即可 imageView.setImageResource((Integer) path); } }); banner.setImages(resIds); banner.setOnBannerListener(position -> { // position 是当前页在列表里的下标,埋点时直接用 Log.d("banner", "click " + position); }); banner.start(); // 顺序错了就不会自动轮播三个容易踩的点在代码注释里已经标了两个:setImageLoader要在setImages之前,start()要在最后。第三个是displayImage里的path类型——setImages传List<Integer>它就是 Integer,传 URL 字符串它就是 String,强转类型必须和传入的一致,否则就是那条经典的ClassCastException,而且在滑动到第二页才崩,日志看着莫名其妙。
3. 参数调优:间隔、指示器、自定义 Item 三块怎么配
控件跑起来只是及格线,真正决定体验的是三个时间参数和指示器样式。这一章把参数表摊开,说清楚每个值改大改小分别影响什么,以及什么时候该放弃内置 Item 布局自己写 Adapter。
3.1 三个时间参数:间隔、滚动时长、动画时长
轮播的「手感」几乎全部由这三个数决定,它们之间还互相制约。间隔太短,用户还没看清就切走了;滚动时长太短,切换像闪屏;动画时长和滚动时长打架,会出现「滑到一半又弹回去」的观感。
| 参数 | 典型值 | 改大的后果 | 改小的后果 |
|---|---|---|---|
| 轮播间隔 delay_time | 3000~5000ms | 用户注意力分散,首屏信息密度下降 | 图片来不及看清,误触率上升 |
| 单次滚动时长 scroll_time | 600~1000ms | 切换拖沓,快速滑动时手感发黏 | 像闪屏,长图尤其明显 |
| 切换动画时长 | 与 scroll_time 同量级 | 动画没播完就被下一次切换打断 | 动画几乎看不到,等于没有 |
// 单位都是毫秒,三个值别拍脑袋,按内容密度定 banner.setDelayTime(4000); // 活动图信息多,给到 4 秒 banner.setScrollTime(800); // 略慢于默认值,长图切换更顺 banner.isAutoLoop(true); // 只有单张图时建议关掉,避免原地抽搐我的经验是:三张图以内用 4000ms,五张以上用 3000ms,因为用户翻完一轮的总时长要控制在一个可接受的范围内。另外isAutoLoop在数据只有一条时一定要关,否则「无限循环 + 单条数据」会让控件反复切同一张,视觉上就是画面在抖。
3.2 指示器与样式:圆形、数字、自定义 Drawable 的取舍
指示器是最容易被产品改来改去的部分。内置样式一般覆盖三类:纯圆点、圆点加标题、数字指示器。圆点适合 3~6 张,超过 8 张圆点会密到看不清,这时候换数字样式更合适。
// 样式:圆点 + 标题,标题压在图片底部 banner.setBannerStyle(BannerConfig.CIRCLE_INDICATOR_TITLE_INSIDE); // 指示器位置:居中、靠左、靠右,具体常量名以 sources.jar 里的定义为准 banner.setIndicatorGravity(BannerConfig.CENTER); // 标题数据要和图片数量严格对齐,少一个就会错位 banner.setBannerTitles(Arrays.asList("活动一", "活动二", "活动三")); // 切换动画,横向滑动之外还有缩放、淡入这类 banner.setPageTransformer(Transformer.DefaultTransformer);指示器的宽高、间距、圆角、选中与未选中颜色,多数版本放在 XML 属性里而不是运行时方法。改样式时优先动布局文件,改完clean一次再跑——资源类的改动增量编译偶尔不生效,这个玄学问题我在三个项目上遇到过。
提示:标题数量和图片数量不一致时,不同版本表现不一样,有的错位有的直接崩。数据源里把 title 和 imageUrl 放进同一个实体类,从结构上杜绝数量不匹配。
3.3 自定义 Item 布局:Adapter 的三个方法与点击回调
内置布局只够放一张图和一行标题。真实业务里 Item 上往往还压着角标、标签、渐变遮罩、播放按钮,这时候必须自己写 Adapter。
banner.setAdapter(new BannerAdapter() { @Override public int getItemCount() { return mList.size(); // 数据条数,不要返回固定值 } @Override public int getLayoutId(int position) { return R.layout.item_banner; // 支持按 position 返回不同布局 } @Override public void getView(View view, int position, Object data) { // view 是已 inflate 的 Item 根布局,这里自己 findViewById 绑数据 TextView tag = view.findViewById(R.id.tv_tag); ItemBean bean = (ItemBean) data; tag.setText(bean.getTag()); } });三个方法里getItemCount返回的是原始数据条数,不是无限循环后的总数——无限循环是控件内部用取模实现的,这一点搞混了会写出一堆边界判断。getLayoutId按 position 返回不同布局这个能力很少人用,但做「首屏大图 + 后续小图」的混排时非常好使。
点击回调setOnBannerListener拿到的 position 是原始下标,可以直接当mList.get(position)用。如果你的列表在轮播过程中会被刷新(比如下拉刷新换了活动图),记得在数据更新后重新setImages并start(),否则点击下标和实际数据会错位——这个 bug 在线上表现为「点第一张进了第二个活动」,非常难复现。
4. 生命周期与复用:banner 挂到 Fragment、RecyclerView 里怎么不泄漏
轮播控件本质是一个带定时器的 ViewPager,定时器不关就是内存泄漏,ViewPager 不释放就是重复滚动。这一章解决三个真实场景:Activity/Fragment 里怎么配对开关、RecyclerView 复用怎么不打架、和嵌套滚动容器怎么共存。
4.1 start 与 stop 的配对:写在哪几个回调里
最稳的写法是onResume里start()、onStop里stop(),onDestroy里再兜一次。有人只在onCreate里 start,结果页面退到后台定时器还在跑,回来的时候画面已经不知道切到第几张了。
@Override protected void onResume() { super.onResume(); if (banner != null) banner.start(); // 回到前台恢复自动轮播 } @Override protected void onStop() { super.onStop(); if (banner != null) banner.stop(); // 退到后台立刻停,省电也防泄漏 } @Override protected void onDestroy() { super.onDestroy(); if (banner != null) { banner.stop(); banner.setAdapter(null); // 断开对 Adapter 和图片的持有 } }Fragment 里情况复杂一点。setUserVisibleHint已经废弃,现在主流做法是在onResume/onPause里做开关,配合 ViewPager2 时用setMaxLifecycle控制子 Fragment 的生命周期,轮播自然就跟着停。如果项目还在用老的 ViewPager + FragmentPagerAdapter,子页面切走后onResume不会触发,得靠父页面统一分发可见性——这种老架构我一般直接在父页面做一次判断,别在子 Fragment 里各自为政。
注意:
stop()只是停定时器,不释放图片内存。图片多的页面在onDestroy里把 adapter 置空,否则 ImageView 持有的 Bitmap 会跟着 Activity 一起滞留。
4.2 RecyclerView 里的 banner:复用错位与重复计时
把 banner 放进 RecyclerView 的 Item 里是需求重灾区,典型症状是「滑出去再滑回来,轮播位置乱了」或者「同一屏出现两个在滚的 banner」。
@Override public void onBindViewHolder(@NonNull Holder holder, int position) { Banner banner = holder.banner; banner.setImages(dataList); // 每次绑定都重设数据,避免复用残留 banner.start(); } @Override public void onViewRecycled(@NonNull Holder holder) { super.onViewRecycled(holder); holder.banner.stop(); // 被回收时停掉,防止后台继续计时 }核心逻辑是「绑定时重设 + 回收时停掉」。只在onCreateViewHolder里设置一次是错的,因为 ViewHolder 会被复用给另一条数据,图片换了但内部页码还停在上一条的第三页。另外onViewRecycled不一定每次都回调(比如页面整体销毁时),所以onDestroy里那次兜底不能省。
还有一种更隐蔽的情况:RecyclerView 开启setItemViewCacheSize后,滑出屏幕的 Item 不会立刻走 recycled 流程,banner 还在后台跑。表现是滑动时偶发卡顿,抓 trace 能看到定时任务。解决方式是在onBindViewHolder里判断当前 Item 是否可见,不可见就 stop。
4.3 手势冲突:嵌套滚动与下拉刷新
banner 横向滑、外层容器纵向滑,本来不冲突,但放进NestedScrollView或者和下拉刷新控件套在一起时,会出现「横向滑到一半被纵向抢走」的情况,体验非常糟。
// banner 内部 ViewPager 的触摸事件:横滑时请求父容器不要拦截 banner.setOnTouchListener((v, event) -> { switch (event.getAction()) { case MotionEvent.ACTION_DOWN: case MotionEvent.ACTION_MOVE: v.getParent().requestDisallowInterceptTouchEvent(true); break; case MotionEvent.ACTION_UP: case MotionEvent.ACTION_CANCEL: v.getParent().requestDisallowInterceptTouchEvent(false); break; } return false; // 返回 false,别把事件吞掉,否则点不动 });这段的关键在return false:只做「请求父容器别拦截」的动作,事件继续往下传,点击才能正常触发。有人写成return true,结果 banner 能滑了但点不动,排查半天。另外如果外层是下拉刷新,ACTION_DOWN阶段就直接请求不拦截,会让下拉手势在任何位置都失效,这种情况要把判断收窄到横向位移大于纵向位移时再请求。
5. 避坑与排查:五条线上真实翻车记录
下面五条是这类轮播控件最常见的故障,按「现象 → 原因 → 解决」写,遇到时可以直接对着查。
5.1 图片与内存相关
现象一:第一张图白屏,滑一下才出来。原因:setImageLoader在setImages之后才设置,或者图片加载库的占位图没配,首帧渲染时图片还在异步加载。解决:把setImageLoader提到最前面,同时在图片加载时配置占位色或占位图,避免白块。
现象二:连续滑动十几张后 OOM。原因:原图直出,没有按 ImageView 尺寸做降采样,一张 4000px 宽的活动图解码后接近 30MB。解决:在图片加载环节传目标宽高,让加载库按控件尺寸采样;同时在onDestroy里清理 adapter 引用,断开对 Bitmap 的持有链。
5.2 计时与生命周期相关
现象三:从详情页返回首页,轮播速度明显变快了。原因:每次onResume都调了一次start(),但旧定时器没停,现在有两个在跑,切换频率翻倍。解决:start()前先stop(),或者只在首次创建时 start、其余交给 stop/start 配对,别在onResume里重复调用。
现象四:页面退出后仍有定时任务在跑,Profiler 里能看到泄漏的 Activity。原因:stop()写在了onPause而 Activity 被销毁时走不到,或者压根没写。解决:onStop停定时器、onDestroy停定时器并置空 adapter,两层兜底。
5.3 布局与交互相关
现象五:banner 高度为 0,完全不显示。原因:layout_height写成wrap_content,控件内部没有测量到子 View 的期望高度。解决:高度写死 dp 或按屏幕宽度比例动态算(比如 16:9 用width * 9 / 16),在onCreate里post一个 Runnable 拿宽度再设置,避免拿到 0。
另外还有一条值得单独记:数据只有一条时开了无限循环,画面会持续抖动。解决是把isAutoLoop和isInfiniteLoop都按list.size() > 1动态设置,别写死 true。
6. 进阶技巧:把轮播状态接进埋点和图片预加载
轮播图的曝光数据是运营最关心的指标之一,但很多人只在点击时埋点,曝光一分钱都统计不到。我的做法是监听页码变化,停留超过一个阈值才记一次有效曝光。
banner.addOnPageChangeListener(new ViewPager.OnPageChangeListener() { private int lastPos = -1; @Override public void onPageSelected(int position) { if (position != lastPos) { lastPos = position; // 页码取模拿到真实数据下标,避免无限循环下越埋越大 int realIndex = position % mList.size(); banner.postDelayed(() -> reportExposure(realIndex), 800); } } // 另外两个回调按需实现 });这里的postDelayed是个小技巧:用户快速划过时不应该算曝光,延迟 800ms 后再上报,能过滤掉大部分无效滑动。position % mList.size()这一步不能省,无限循环下 position 会一直增大,直接用会把埋点数据打爆。预加载同理,在onPageSelected里把下一张的 URL 提前丢给图片加载库,切过去就是瞬显。
验证环节我固定跑三步:先在布局里临时加一圈红色背景确认控件占位正常,再用低网速模拟看首帧有没有白块,最后退到后台再回来,看切换节奏有没有变快。第三步专门用来抓重复定时器,抓到过一次就再也不敢省这个流程了——那次是首页轮播在onResume里重复 start,线上表现为「滑动越来越快」,用户反馈说像卡了。从那以后我每接一个 banner,都强制把 start/stop 配对、无限循环取模、曝光去抖这三件事走一遍再提测。希望帮到你。
本文还有配套的精品资源,点击获取