本文还有配套的精品资源,点击获取 menu-r.4af5f7ec.gif

简介:ZBanner是一个专为Android平台设计的开源轮播控件,支持图片与视频混合展示,适用于广告、首页推荐等高交互场景。该控件提供丰富的样式自定义功能,包括动画效果、指示器样式和轮播速度调节,帮助开发者实现流畅的多媒体轮播体验。通过集成Glide、ExoPlayer等主流框架,ZBanner高效处理资源加载与播放,并具备良好的内存优化与Android版本兼容性。本项目包含完整源码、示例应用及文档,便于快速集成与二次开发,提升App的视觉吸引力和用户体验。
ZBanner.zip

1. ZBanner控件功能概述

ZBanner作为一款高度可定制的Android轮播控件,集成了图片与视频混合播放、自动轮播、动画切换、高效加载与内存优化等多项核心功能。其采用 ViewPager2 作为基础容器,结合 FragmentStateAdapter 实现高性能页面复用,支持多类型Item动态渲染,兼顾流畅滑动体验与资源消耗平衡。通过统一的 BannerItem 数据模型,可灵活配置图片链接、视频源地址及自定义UI组件,同时无缝集成Glide/Picasso进行图像加载,并对接ExoPlayer实现视频播放生命周期管理。控件提供丰富的API用于指示器样式定制、轮播间隔设置与动画效果切换,适用于首页广告、商品展示、新闻图集等高交互场景,具备良好的扩展性与系统兼容性,为复杂业务需求提供稳定支撑。

2. 图片/视频混合轮播实现原理

在现代移动应用界面中,轮播控件已成为首页展示、广告推广和内容聚合的核心组件之一。ZBanner作为一款高性能的Android轮播控件,其核心亮点在于支持 图片与视频混合播放 ,即在同一轮播容器中无缝切换静态图像与动态视频资源。这种能力不仅提升了用户体验的丰富度,也对底层架构设计提出了更高的要求:如何统一管理异构资源?如何协调不同媒体类型的生命周期?又如何保证滑动流畅性与资源加载效率?本章将深入剖析ZBanner中图片与视频混合轮播的技术实现路径,重点解析自定义ViewGroup结构设计、多类型资源统一管理机制、页面切换逻辑同步以及视频播放状态联动等关键技术环节。

2.1 自定义ViewGroup的结构设计

为了支撑图片与视频共存的复杂交互场景,ZBanner并未直接使用基础的 ViewPager RecyclerView ,而是基于 ViewPager2 构建了一个高度可扩展的自定义容器基类。该设计不仅继承了 ViewPager2 在滑动性能、RTL布局支持和Fragment集成方面的优势,还通过封装通用逻辑实现了更灵活的数据绑定与视图复用策略。

### 2.1.1 继承ViewPager2构建容器基类

ZBanner的核心容器继承自 androidx.viewpager2.widget.ViewPager2 ,并在此基础上封装了初始化配置、自动轮播调度、指示器联动等功能模块。通过封装,开发者无需关心底层实现细节即可快速接入轮播功能。

public class ZBanner extends ViewPager2 {
    private BannerAdapter mAdapter;
    private List<BannerItem> mDataList;
    private boolean mAutoPlay = true;
    private long mInterval = 3000;

    public ZBanner(@NonNull Context context, AttributeSet attrs) {
        super(context, attrs);
        init();
    }

    private void init() {
        // 关闭用户滑动(可选)
        setUserInputEnabled(true);
        // 设置页面间距
        setOffscreenPageLimit(1);
        // 设置页边距以实现部分可见效果
        ((MarginLayoutParams) getLayoutParams()).setMargins(20, 0, 20, 0);
        setPageTransformer(new DepthPageTransformer());
    }
}
代码逻辑逐行解读:
  • 第1行 :声明 ZBanner 类继承自 ViewPager2 ,获得横向滑动、预加载、动画过渡等原生能力。
  • 第4~6行 :定义关键成员变量,包括适配器、数据源列表、是否开启自动播放及轮播间隔时间。
  • 第8~11行 :构造函数调用父类初始化,并进入自定义初始化流程。
  • 第14行 setUserInputEnabled(true) 允许用户手势滑动;若设为 false 则仅支持程序控制翻页。
  • 第16行 setOffscreenPageLimit(1) 设置预加载一页,平衡内存占用与滑动流畅性。
  • 第18~19行 :通过外层布局参数设置页间留白,结合 PageTransformer 可实现“画廊”式视觉效果。
  • 第20行 :注册自定义页面变换器,用于实现深度平移动画。

此设计模式体现了 组合优于继承 的原则——在保留 ViewPager2 强大能力的同时,通过封装屏蔽复杂性,提升API易用性。

### 2.1.2 支持FragmentStateAdapter的数据绑定机制

由于 ViewPager2 推荐使用 RecyclerView.Adapter 风格的适配器模型,ZBanner采用 FragmentStateAdapter 作为底层适配器实现,适用于大量页面场景下的高效内存管理。

class BannerAdapter extends FragmentStateAdapter {
    private final List<BannerItem> mDataList;

    public BannerAdapter(@NonNull FragmentActivity fa, List<BannerItem> data) {
        super(fa);
        mDataList = data;
    }

    @NonNull
    @Override
    public Fragment createFragment(int position) {
        int realIndex = position % mDataList.size();
        BannerItem item = mDataList.get(realIndex);
        return MediaFragment.newInstance(item);
    }

    @Override
    public int getItemCount() {
        return Integer.MAX_VALUE; // 实现无限循环
    }
}
参数说明与逻辑分析:
参数 类型 含义
fa FragmentActivity 提供FragmentManager用于Fragment事务管理
data List 外部传入的轮播项数据集合
position int ViewPager2请求的虚拟位置(可能极大)
  • 第10行 createFragment(int position) 是核心方法,根据当前位置创建对应的Fragment实例。
  • 第11行 :通过取模运算将无限索引映射到真实数据范围内,实现 逻辑上的无限循环轮播
  • 第15行 :返回 Integer.MAX_VALUE 作为条目数,使ViewPager2认为有“无穷多页”,从而支持左右无限滑动。
mermaid流程图:Fragment创建与索引映射过程
graph TD
    A[ViewPager2请求 position] --> B{position % dataList.size()}
    B --> C[realIndex]
    C --> D[BannerItem item = dataList.get(realIndex)]
    D --> E[MediaFragment.newInstance(item)]
    E --> F[返回Fragment实例]

该机制有效解决了传统 PagerAdapter 在大数据量下内存占用高的问题,同时借助Fragment的生命周期回调实现视频播放状态的精准控制。

### 2.1.3 多类型Item视图的复用策略

面对图片与视频两种差异显著的渲染需求,ZBanner采用了 多类型ViewHolder + 动态组件注入 的设计方案,在同一个 MediaFragment 中完成资源类型判断与视图渲染。

<!-- fragment_media.xml -->
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

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

    <com.google.android.exoplayer2.ui.PlayerView
        android:id="@+id/player_view"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:visibility="gone" />

</FrameLayout>
public class MediaFragment extends Fragment {
    private static final String ARG_ITEM = "banner_item";
    private BannerItem mItem;
    private PlayerView mPlayerView;
    private ImageView mImageView;
    private SimpleExoPlayer mPlayer;

    public static MediaFragment newInstance(BannerItem item) {
        Bundle args = new Bundle();
        args.putSerializable(ARG_ITEM, item);
        MediaFragment fragment = new MediaFragment();
        fragment.setArguments(args);
        return fragment;
    }

    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
        View root = inflater.inflate(R.layout.fragment_media, container, false);
        mImageView = root.findViewById(R.id.iv_image);
        mPlayerView = root.findViewById(R.id.player_view);

        if (mItem.getType() == BannerItemType.IMAGE) {
            loadImage();
        } else if (mItem.getType() == BannerItemType.VIDEO) {
            setupVideoPlayer();
        }

        return root;
    }
}
表格:视图组件可见性控制策略
资源类型 ImageView 状态 PlayerView 状态 加载动作
IMAGE VISIBLE GONE Glide加载图片
VIDEO GONE VISIBLE 初始化ExoPlayer并准备播放
  • 第17~21行 :通过 newInstance(BannerItem) 工厂方法传递数据,避免直接暴露构造风险。
  • 第29~35行 :在 onCreateView 中根据 BannerItem.getType() 判断资源类型,动态启用对应视图组件。
  • 第37行 :调用 loadImage() 使用Glide加载网络图片,配合占位符防止闪烁。
  • 第39行 setupVideoPlayer() 负责创建ExoPlayer实例并绑定 PlayerView

该复用策略充分利用了 FragmentStateAdapter 的页面回收机制,确保非活跃页面的资源及时释放,避免内存泄漏。

2.2 图片与视频资源的统一管理

要在同一轮播器中稳定运行异构媒体资源,必须建立一套统一的数据抽象与资源调度体系。ZBanner通过定义标准化的 BannerItem 实体类、动态加载渲染组件、精细化控制预加载时机等方式,实现了对图片与视频资源的统一管理。

### 2.2.1 数据模型设计:BannerItem实体类定义

所有轮播内容均被抽象为 BannerItem 对象,包含资源URL、类型标识、标题、附加元数据等字段。

public class BannerItem implements Serializable {
    public enum Type { IMAGE, VIDEO }

    private String url;
    private Type type;
    private String title;
    private Map<String, Object> extras;

    public BannerItem(String url, Type type) {
        this.url = url;
        this.type = type;
        this.extras = new HashMap<>();
    }

    // getter & setter ...
}
字段说明:
字段名 类型 用途
url String 图片或视频的网络地址(支持http/https/content等协议)
type Enum(Type) 明确区分资源类型,驱动后续渲染分支
title String 可用于显示文字描述(如新闻标题)
extras Map 扩展字段,可用于传递封面图、播放起始时间等

该设计具备良好的扩展性,未来可轻松支持GIF、SVG甚至WebP动画等新型资源类型。

### 2.2.2 资源类型识别与渲染组件动态加载

MediaFragment 接收到 BannerItem 后,需依据 type 字段决定加载路径:

private void loadResource() {
    if (mItem.getType() == BannerItem.Type.IMAGE) {
        Glide.with(this)
             .load(mItem.getUrl())
             .placeholder(R.drawable.placeholder)
             .error(R.drawable.error)
             .into(mImageView);
    } else if (mItem.getType() == BannerItem.Type.VIDEO) {
        initializeExoPlayer();
        MediaItem mediaItem = MediaItem.fromUri(mItem.getUrl());
        mPlayer.setMediaItem(mediaItem);
        mPlayer.prepare();
    }
}
执行逻辑分析:
  • 第3行 :使用Glide进行图片加载,自动处理缓存、缩放、线程调度。
  • 第8~11行 :为视频资源创建 MediaItem 并交由ExoPlayer准备解码。
  • 第12行 :调用 prepare() 触发异步资源探测(如获取分辨率、码率),但 不开始播放

⚠️ 注意:此时仅完成准备工作,实际播放由可见性检测触发,避免后台消耗流量。

### 2.2.3 视频预加载与懒加载时机控制

为兼顾用户体验与性能开销,ZBanner采用“ 近场预加载 + 远场懒加载 ”策略:

@Override
public void onViewAttachedToWindow(@NonNull RecyclerView.ViewHolder holder) {
    int position = holder.getBindingAdapterPosition();
    int currentPage = getCurrentItem();
    boolean isNearCurrent = Math.abs(position - currentPage) <= 1;

    if (isNearCurrent && getItem(position).getType() == BannerItem.Type.VIDEO) {
        preloadVideo(position);
    }
}
预加载决策表:
当前页 目标页 距离差 是否预加载 原因
5 4 1 ✅ 是 左侧相邻页即将进入视野
5 5 0 ✅ 是 当前页需立即准备
5 6 1 ✅ 是 右侧相邻页可能滑入
5 7 2 ❌ 否 距离过远,节省资源

该策略通过监听 RecyclerView 的视图绑定事件,在合适时机启动视频解码器初始化,显著降低首次播放延迟。

2.3 页面切换逻辑与状态同步

轮播控件的核心体验之一是“无缝循环”。ZBanner通过 setCurrentItem 模拟无限滚动,并借助 OnPageChangeCallback 实现索引映射与UI状态同步。

### 2.3.1 setCurrentItem循环轮播逻辑实现

private void startAutoScroll() {
    final Runnable scrollRunnable = new Runnable() {
        @Override
        public void run() {
            int currentItem = getCurrentItem();
            int nextItem = (currentItem + 1) % getDataSize();
            setCurrentItem(nextItem, true);
            postDelayed(this, mInterval);
        }
    };
    postDelayed(scrollRunnable, mInterval);
}
  • 第6行 :获取当前虚拟位置(可能是几万)
  • 第7行 :计算下一目标页的真实索引(取模后落在[0, n-1]区间)
  • 第8行 :启用动画跳转至目标页
  • 第9行 :递归延迟执行,形成自动轮播闭环

### 2.3.2 页面变化监听器OnPageChangeCallback注册与回调

registerOnPageChangeCallback(new OnPageChangeCallback() {
    @Override
    public void onPageSelected(int position) {
        super.onPageSelected(position);
        int realIndex = position % mDataList.size();
        updateIndicator(realIndex); // 更新指示器
        dispatchItemClick(realIndex); // 分发点击事件
    }
});

此回调确保无论用户手动滑动还是自动播放,都能准确更新UI状态。

### 2.3.3 当前索引与真实数据索引之间的映射关系处理

由于 ViewPager2 使用虚拟无限索引,而实际数据有限,必须建立映射函数:

\text{realIndex} = \text{virtualPosition} \mod N

其中 $N$ 为真实数据长度。该映射贯穿整个生命周期,确保数据访问不会越界。

2.4 视频播放生命周期联动

视频资源具有明显的状态依赖性,ZBanner通过Fragment生命周期与可见性检测实现播放控制自动化。

### 2.4.1 可见性判断与播放状态自动控制

@Override
public void setUserVisibleHint(boolean isVisibleToUser) {
    super.setUserVisibleHint(isVisibleToUser);
    if (isVisibleToUser && mItem.getType() == VIDEO) {
        startPlayback();
    } else {
        pausePlayback();
    }
}

利用 setUserVisibleHint (兼容性考虑)或 lifecycle.addObserver 精确感知可见状态。

### 2.4.2 滑出可视区域时暂停播放并释放资源

private void pausePlayback() {
    if (mPlayer != null && mPlayer.isPlaying()) {
        mPlayer.pause();
    }
}

private void releasePlayer() {
    if (mPlayer != null) {
        mPlayer.release();
        mPlayer = null;
    }
}

防止后台播放造成流量浪费与发热。

### 2.4.3 用户交互触发下的恢复播放机制

监听触摸事件,允许用户点击继续播放:

mPlayerView.setOnClickListener(v -> {
    if (mPlayer != null) {
        if (mPlayer.isPlaying()) {
            mPlayer.pause();
        } else {
            mPlayer.play();
        }
    }
});

增强用户控制感,提升交互体验。


以上各节共同构成了ZBanner中图片与视频混合轮播的技术基石,实现了高稳定性、低延迟、低内存占用的综合表现。

3. 基于Handler/Runnable的自动轮播机制

在现代Android应用开发中,轮播控件已成为首页展示、广告推广和内容推荐等高交互场景中的标配组件。ZBanner作为一款高度可定制化的轮播控件,其核心特性之一便是支持 自动轮播功能 ,即在用户无操作的情况下,页面能够按照预设的时间间隔自动切换至下一项内容。该机制不仅提升了用户体验的流畅性,也增强了信息曝光率。本章将深入剖析ZBanner中基于 Handler Runnable 实现的自动轮播调度系统,从定时任务构建、生命周期感知调度、手动交互影响处理到异常容错策略,层层递进地揭示其背后的设计逻辑与工程实践。

3.1 自动轮播的定时任务实现

自动轮播的核心在于“周期性触发页面切换”,而Android平台中最经典且轻量级的延迟执行方案便是 Handler 结合 Runnable 的消息机制。ZBanner采用此方式构建了一个稳定可靠的轮播调度器,能够在主线程安全地发送延迟消息,并通过循环调用实现持续轮播。

3.1.1 使用Handler配合Runnable发送延迟消息

在ZBanner内部,定义了一个专用的 Handler 实例用于管理轮播任务:

private final Handler mAutoScrollHandler = new Handler(Looper.getMainLooper());
private final Runnable mAutoScrollRunnable = new Runnable() {
    @Override
    public void run() {
        int currentItem = mViewPager2.getCurrentItem();
        int nextItem = (currentItem + 1) % mAdapter.getItemCount(); // 循环取模
        mViewPager2.setCurrentItem(nextItem, true);
        // 再次调度自身,形成闭环
        mAutoScrollHandler.postDelayed(this, mIntervalMillis);
    }
};

上述代码中, mAutoScrollRunnable 是一个实现了 Runnable 接口的任务体,它会在执行时获取当前ViewPager2的位置,计算下一个目标位置(通过取模运算实现循环),然后调用 setCurrentItem() 方法进行页面跳转。最关键的是最后一行——再次调用 postDelayed() 将自己重新提交到消息队列,从而形成一个无限循环的定时任务。

逻辑逐行分析:
  • int currentItem = mViewPager2.getCurrentItem(); :获取当前显示页面索引。
  • int nextItem = (currentItem + 1) % mAdapter.getItemCount(); :利用取模确保索引不越界,实现首尾相连的循环效果。
  • mViewPager2.setCurrentItem(nextItem, true); :平滑滚动到下一页面,第二个参数 true 表示启用动画过渡。
  • mAutoScrollHandler.postDelayed(this, mIntervalMillis); :延迟指定毫秒后再次执行该任务,构成自调度闭环。

这种设计避免了使用 Timer ScheduledExecutorService 带来的线程切换开销,完全运行于主线程,适合UI更新类任务。

3.1.2 设置轮播间隔时间与可配置化接口暴露

为了提升灵活性,ZBanner对外提供了可配置的轮播间隔设置接口:

public ZBanner setAutoScrollInterval(long interval) {
    if (interval < MIN_INTERVAL) {
        throw new IllegalArgumentException("Interval cannot be less than " + MIN_INTERVAL + "ms");
    }
    this.mIntervalMillis = interval;
    return this;
}

开发者可通过链式调用设置轮播频率,例如:

zBanner.setAutoScrollInterval(3000) // 每3秒切换一次
       .startAutoScroll();

此外,框架还内置了默认值(通常为3000ms)和最小值限制(如1000ms),防止因过短间隔导致频繁刷新引起性能问题或视觉不适。

参数名 类型 默认值 说明
interval long 3000ms 轮播间隔时间,单位毫秒
MIN_INTERVAL 常量 1000ms 最小合法间隔,防止过度刷新

该设计体现了良好的封装性与健壮性,既满足个性化需求,又规避潜在风险。

3.1.3 循环任务的启动、停止与重置控制

自动轮播并非始终开启,需根据业务状态动态控制启停。ZBanner提供了一套完整的生命周期控制API:

public void startAutoScroll() {
    stopAutoScroll(); // 防止重复添加
    mAutoScrollHandler.postDelayed(mAutoScrollRunnable, mIntervalMillis);
}

public void stopAutoScroll() {
    mAutoScrollHandler.removeCallbacks(mAutoScrollRunnable);
}

其中, startAutoScroll() 先清除已有任务再重新投递,避免多重注册; stopAutoScroll() 则通过 removeCallbacks() 移除待执行任务,彻底终止轮播。

更进一步,当数据源发生变化(如刷新列表)时,还需支持重置机制:

public void resetAutoScroll() {
    stopAutoScroll();
    startAutoScroll();
}

这保证了数据变更后轮播能从头开始计时,符合预期行为。

sequenceDiagram
    participant User
    participant ZBanner
    participant Handler
    User->>ZBanner: startAutoScroll()
    ZBanner->>Handler: postDelayed(runnable, 3000)
    Handler-->>ZBanner: 触发run()
    ZBanner->>ViewPager2: setCurrentItem(next)
    ZBanner->>Handler: postDelayed(runnable, 3000)
    loop 每3秒执行一次
        Handler-->>ZBanner: 触发run()
        ZBanner->>ViewPager2: 切换页面
        ZBanner->>Handler: 重新投递
    end
    User->>ZBanner: stopAutoScroll()
    ZBanner->>Handler: removeCallbacks()
    Note right of ZBanner: 任务终止

该流程图清晰展示了自动轮播的完整生命周期:启动 → 延迟执行 → 页面切换 → 自我调度 → 循环往复 → 显式停止。

3.2 生命周期感知的轮播调度

尽管 Handler+Runnable 机制简单高效,但若忽视组件生命周期,极易引发内存泄漏或无效渲染。例如,当Activity已销毁,Handler仍持有Runnable引用并尝试更新UI,会导致崩溃或资源浪费。因此,ZBanner引入了对 LifecycleOwner 的支持,实现智能启停调度。

3.2.1 结合Activity/Fragment生命周期暂停与恢复

ZBanner允许传入 LifecycleOwner (如Activity或Fragment),并在其生命周期变化时自动调整轮播状态:

public ZBanner registerLifecycle(LifecycleOwner lifecycleOwner) {
    lifecycleOwner.getLifecycle().addObserver(new DefaultLifecycleObserver() {
        @Override
        public void onStart(@NonNull LifecycleOwner owner) {
            if (mIsAutoScrollEnabled) {
                startAutoScroll();
            }
        }

        @Override
        public void onStop(@NonNull LifecycleOwner owner) {
            stopAutoScroll();
        }
    });
    return this;
}

当宿主进入 onStart() 状态时恢复轮播,进入 onStop() 时暂停。这样即使用户切到后台,也不会继续消耗CPU与电量。

3.2.2 使用LifecycleOwner实现自动注册与解绑

通过观察者模式监听生命周期事件,ZBanner无需手动调用 start/stop ,即可做到“随生命周期自动启停”。这一机制极大降低了使用成本,提升了稳定性。

更重要的是, LifecycleObserver 由系统管理,不会造成内存泄漏。相比传统在 onDestroy() 中手动解绑的方式,更加安全可靠。

// 使用示例
zBanner.registerLifecycle(getActivity())
       .setAutoScrollInterval(4000)
       .startAutoScroll();

一旦绑定,后续所有调度均由系统驱动,开发者无需关心细节。

3.2.3 避免内存泄漏的关键引用管理

早期版本中,若 Handler 声明为匿名内部类,会隐式持有外部类(如Activity)的强引用,导致即使Activity销毁也无法被回收。

ZBanner采用静态内部类+弱引用的方式解决该问题:

private static class AutoScrollHandler extends Handler {
    private final WeakReference<ZBanner> bannerRef;

    AutoScrollHandler(ZBanner banner) {
        super(Looper.getMainLooper());
        this.bannerRef = new WeakReference<>(banner);
    }

    @Override
    public void handleMessage(@NonNull Message msg) {
        ZBanner banner = bannerRef.get();
        if (banner != null && !banner.isDetached()) {
            banner.handleAutoScrollMessage();
        }
    }
}
  • WeakReference 确保不会阻止ZBanner及其宿主被垃圾回收。
  • isDetached() 额外判断是否已脱离窗口,双重防护。

同时, Runnable 也应避免非静态内部类形式,防止闭包捕获外部对象。

方案 是否安全 说明
匿名内部类Handler 持有外部类强引用,易泄漏
静态内部类+WeakReference 推荐做法,安全释放
移除Callbacks+解绑Lifecycle 辅助手段,增强健壮性

结合以上措施,ZBanner实现了真正意义上的无泄漏自动轮播机制。

3.3 用户手动滑动后的轮播行为调整

自动轮播虽便利,但必须尊重用户的主动交互。当用户手动滑动页面后,若立即继续倒计时,可能打断操作节奏,产生不良体验。为此,ZBanner在用户干预后重置轮播计时器,确保“以用户为中心”。

3.3.1 手动操作后重置倒计时机制

关键在于监听 ViewPager2.OnPageChangeCallback ,并在页面变化时判断是否由用户手势触发:

private final ViewPager2.OnPageChangeCallback mPageChangeCallback =
    new ViewPager2.OnPageChangeCallback() {
        @Override
        public void onPageSelected(int position) {
            super.onPageSelected(position);
            if (mIsUserScroll) {
                resetAutoScroll(); // 重置计时
                mIsUserScroll = false;
            }
        }

        @Override
        public void onPageScrollStateChanged(int state) {
            if (state == ViewPager2.SCROLL_STATE_DRAGGING) {
                mIsUserScroll = true;
            }
        }
    };
  • SCROLL_STATE_DRAGGING 时标记为用户拖拽;
  • onPageSelected 中检测该标志,若是则调用 resetAutoScroll()

此举让用户“掌控感”更强,滑动后不会立刻被自动轮播打断。

3.3.2 onTouchEvent拦截与事件分发影响分析

为精确识别用户行为,ZBanner有时需覆写 onTouchEvent() 进行事件拦截:

@Override
public boolean onTouchEvent(MotionEvent ev) {
    if (ev.getAction() == MotionEvent.ACTION_DOWN) {
        stopAutoScroll(); // 触摸瞬间停止轮播
    } else if (ev.getAction() == MotionEvent.ACTION_UP) {
        postDelayed(this::startAutoScroll, 2000); // 抬起后延时重启
    }
    return super.onTouchEvent(ev);
}

这种方式可在触摸开始时立即中断自动滚动,提升响应速度。但需注意不要过度拦截,以免影响ViewPager2自身的滑动逻辑。

更优做法是依赖 OnPageChangeCallback 的状态机判断,而非直接干预触摸事件,保持职责分离。

3.3.3 平滑过渡至下一页的逻辑封装

在重置轮播后,不应立即跳转,而应给予一定缓冲时间(如2秒),让用户完成浏览后再继续自动播放。

private void resetAutoScroll() {
    stopAutoScroll();
    mAutoScrollHandler.postDelayed(mAutoScrollRunnable, mIntervalMillis);
}

此即“重置”而非“立即启动”,体现人性化设计思想。

stateDiagram-v2
    [*] --> Idle
    Idle --> Scrolling: 用户手指滑动
    Scrolling --> Stopped: ACTION_UP,停止轮播
    Stopped --> Waiting: 延迟2秒等待
    Waiting --> AutoScrolling: 启动自动轮播
    AutoScrolling --> Scrolling: 用户再次滑动
    AutoScrolling --> AutoScrolling: 定时切换页面

该状态图展现了用户交互与自动轮播之间的状态流转关系,强调了“中断→等待→恢复”的合理路径。

3.4 异常情况下的容错处理

任何自动化机制都必须考虑边界条件与异常场景。ZBanner在自动轮播模块中加入了多项容错机制,确保在极端情况下依然稳定运行。

3.4.1 空数据或单页场景下的自动轮播禁用

当数据源为空或仅有一项内容时,轮播失去意义,甚至可能导致无限空转或崩溃。

ZBanner在启动前进行校验:

public void startAutoScroll() {
    int itemCount = mAdapter.getItemCount();
    if (itemCount <= 1) {
        stopAutoScroll();
        return;
    }
    // 正常启动逻辑...
}

同时,在 notifyDataSetChanged() 后也应重新评估是否启用:

private void checkAndAdjustAutoScroll() {
    if (mAdapter.getItemCount() <= 1) {
        stopAutoScroll();
    } else if (mIsAutoScrollEnabled) {
        resetAutoScroll();
    }
}
数据状态 是否启用轮播 处理方式
itemCount == 0 停止并忽略
itemCount == 1 停止并忽略
itemCount >= 2 正常调度

此检查贯穿于初始化、刷新、删除等全流程,确保逻辑一致性。

3.4.2 主线程阻塞导致消息延迟的应对方案

由于 Handler 依赖主线程消息队列,若主线程被长时间占用(如密集计算、IO操作),可能导致 postDelayed 实际延迟远超设定值,破坏轮播节奏。

为此,ZBanner引入时间戳比对机制,判断是否错过多个周期:

private long mLastScrollTime;

private final Runnable mAutoScrollRunnable = () -> {
    long currentTime = System.currentTimeMillis();
    if (currentTime - mLastScrollTime > mIntervalMillis * 2) {
        // 超过两个周期未执行,可能是卡顿,补偿一次
        int nextItem = (mViewPager2.getCurrentItem() + 1) % mAdapter.getCount();
        mViewPager2.setCurrentItem(nextItem, true);
    }
    mLastScrollTime = currentTime;
    mAutoScrollHandler.postDelayed(this, mIntervalMillis);
};

虽然无法完全消除卡顿,但可通过补偿机制减少视觉断层。

3.4.3 多实例共存时的消息队列隔离设计

在同一个界面存在多个ZBanner实例时,若共用同一 Handler ,可能因消息交叉导致混乱。ZBanner为每个实例维护独立的 Handler Runnable

public class ZBanner {
    private final Handler mAutoScrollHandler = new Handler(Looper.getMainLooper());
    private final Runnable mAutoScrollRunnable = new AutoScrollTask();
    ...
}

每个实例拥有独立调度器,互不干扰。同时, WeakReference 机制防止跨实例引用泄漏。

此外,还可通过日志标识区分不同实例:

private static final String TAG = "ZBanner[" + System.identityHashCode(this) + "]";

便于调试与监控。

Log.d(TAG, "Starting auto-scroll with interval: " + mIntervalMillis);

综上所述,ZBanner通过对多种异常场景的周密防御,构建了一个鲁棒性强、适应面广的自动轮播系统,为复杂应用场景提供了坚实保障。

4. 使用ObjectAnimator实现页面切换动画

在现代移动应用开发中,流畅且富有视觉吸引力的界面交互已成为提升用户体验的关键要素之一。ZBanner作为一款高性能、可扩展性强的轮播控件,在提供稳定功能的同时,也高度重视动画表现力。其中,页面切换动画是用户感知最直接的部分之一。本章将深入探讨如何通过 ObjectAnimator 结合 ViewPager2.PageTransformer 实现丰富多样的页面切换效果,并从原理层面解析属性动画的工作机制,同时分析其性能影响与优化策略。

4.1 页面切换动画的接入方式

为了实现自定义的页面滑动动画,Android 提供了 ViewPager2.PageTransformer 接口,允许开发者对每一页在滚动过程中的变换行为进行精细控制。ZBanner 正是基于这一机制,结合 ObjectAnimator 属性动画系统,实现了高度灵活的动画配置能力。

4.1.1 ViewPager2的PageTransformer接口实现

ViewPager2 支持设置一个或多个 PageTransformer ,该接口的核心方法为:

public void transformPage(@NonNull View page, float position)

其中, page 是当前参与变换的页面视图, position 表示该页面相对于屏幕中心的位置偏移量(如 -1 表示完全左移出屏幕,0 表示居中,1 表示完全右移出)。这个回调会在每次页面滑动时频繁调用,因此必须保证内部逻辑轻量高效。

在 ZBanner 中,我们通过如下方式注册自定义转换器:

bannerViewPager.setPageTransformer(new DepthPageTransformer());

这种方式使得动画逻辑与页面容器解耦,便于复用和动态替换。

参数 类型 含义说明
page View 当前正在变换的 Fragment 或 ItemView
position float 相对位置系数,范围通常为 [-2, 2],取决于预加载页数

该设计模式支持运行时动态更换动画类型,极大增强了控件的灵活性。

graph TD
    A[ViewPager2滑动事件触发] --> B{是否设置了PageTransformer?}
    B -- 是 --> C[调用transformPage(page, position)]
    C --> D[根据position计算动画属性值]
    D --> E[更新View的translation/scale/alpha等属性]
    E --> F[渲染线程绘制新状态]
    B -- 否 --> G[执行默认滑动动画]

上述流程图展示了 PageTransformer 的完整执行路径,强调了其在每一次布局重绘中的实时性要求。

4.1.2 自定义Transformer中使用属性动画机制

虽然 transformPage 方法本身并不直接支持传统意义上的“动画播放”,但它可以通过修改 View 的属性来模拟连续变化的效果。而 ObjectAnimator 虽然常用于启动一段持续时间明确的动画,但在 PageTransformer 中更推荐 直接操作属性值 而非启动完整动画实例——因为后者会脱离手势控制节奏,造成冲突。

然而,在某些场景下,例如进入某页时希望触发一次独立的入场动画(非跟随滑动手势),则可以结合 ObjectAnimator 实现:

public class FadeInPageTransformer implements ViewPager2.PageTransformer {
    private static final String ALPHA = "alpha";

    @Override
    public void transformPage(@NonNull View page, float position) {
        if (position > 0 && position <= 1) {
            // 右侧滑入的页面淡入
            page.setAlpha(1 - position);
            // 额外启动一次缩放动画(仅首次可见)
            if (Math.abs(position) < 0.01f) {
                ObjectAnimator scaleAnimator = ObjectAnimator.ofFloat(page, "scaleX", 0.9f, 1.0f);
                scaleAnimator.setDuration(300);
                scaleAnimator.setInterpolator(new DecelerateInterpolator());
                scaleAnimator.start();
            }
        } else if (position <= 0 && position > -1) {
            // 当前页面逐渐变暗
            page.setAlpha(1 + position);
        } else {
            page.setAlpha(0);
        }
    }
}
代码逻辑逐行解读:
  • 第5行 :定义透明度属性名,用于后续构建 ObjectAnimator
  • 第8~18行 :当页面从右侧滑入时 ( position ∈ (0,1] ),设置其透明度随位置递减,形成淡入效果。
  • 第12~17行 :当页面接近居中时( position ≈ 0 ),判断为“即将显示”,此时额外启动一个 scaleX 的放大动画,增强视觉反馈。
  • 第20~23行 :左侧页面退出时逐渐变暗。
  • 第25行 :超出可视范围的页面设为不可见。

⚠️ 注意:此处使用的 ObjectAnimator 并非作用于整个滑动过程,而是作为“附加动画”在特定时机触发,避免与手势驱动的属性更改发生冲突。

参数说明:
  • "scaleX" :表示沿 X 轴方向的缩放比例,0.9f 到 1.0f 模拟轻微弹跳感;
  • DecelerateInterpolator() :减速插值器,使动画开始快、结束慢,符合自然运动规律;
  • duration=300ms :适中时长,确保不干扰用户快速滑动操作。

此设计体现了 ZBanner 对“主从动画”的分层处理思想:主动画由 PageTransformer 控制,保持与手势同步;次级装饰性动画由 ObjectAnimator 触发,提升沉浸感。

4.1.3 动画参数外露以支持运行时配置

为了让开发者能够自由定制动画行为,ZBanner 将关键参数抽象为公开 API:

public class BannerAnimationConfig {
    private float minScale = 0.85f;
    private float maxRotation = 15f;
    private long animationDuration = 300;
    private Interpolator interpolator = new AccelerateDecelerateInterpolator();

    // Getter & Setter 省略
}

并在构造 PageTransformer 时传入:

public class CustomDepthTransformer implements ViewPager2.PageTransformer {
    private BannerAnimationConfig config;

    public CustomDepthTransformer(BannerAnimationConfig config) {
        this.config = config;
    }

    @Override
    public void transformPage(@NonNull View page, float position) {
        if (position < -1) {
            page.setScaleX(config.getMinScale());
        } else if (position <= 1) {
            float scaleFactor = Math.max(config.getMinScale(), 1 - Math.abs(position));
            page.setScaleX(scaleFactor);
            page.setRotationY(position * config.getMaxRotation());
        } else {
            page.setScaleX(config.getMinScale());
        }
    }
}

通过这种封装方式,所有动画参数均可在 XML 或 Java/Kotlin 代码中动态调整,满足不同业务风格需求。

4.2 常见轮播动画效果实现

4.2.1 淡入淡出(Alpha动画)效果构建

淡入淡出是最常见也是最温和的页面切换方式,适用于内容差异较小、强调平滑过渡的场景,如图文资讯轮播。

其实现核心在于根据 position 控制两个相邻页面的透明度叠加:

public class AlphaPageTransformer implements ViewPager2.PageTransformer {
    private static final float MIN_ALPHA = 0.5f;

    @Override
    public void transformPage(@NonNull View page, float position) {
        page.setTranslationX(-position * page.getWidth());
        if (position <= -1.0f || position >= 1.0f) {
            page.setAlpha(MIN_ALPHA);
        } else if (position == 0.0f) {
            page.setAlpha(1.0f);
        } else {
            page.setAlpha(1.0f - Math.abs(position));
        }
    }
}
逻辑分析:
  • 使用 setTranslationX 防止两页重叠导致的 z-order 错乱;
  • 当页面完全偏离中心( |position| ≥ 1 )时,保留最低可见度(0.5)防止突然消失;
  • 中间区域采用线性衰减函数控制透明度,实现自然渐变。
position 区间 alpha 值 视觉表现
[-∞, -1] 0.5 完全隐藏左侧旧页
(-1, 0) 1 + pos 左页渐隐
0 1.0 当前页完全可见
(0, 1) 1 - pos 右页渐显
[1, ∞) 0.5 新页准备就绪

该动画简单高效,CPU/GPU 开销极低,适合低端设备广泛使用。

4.2.2 缩放旋转(Scale+Rotation组合)动画设计

此类动画更具动感,适合电商商品展示、广告推广等需要吸引注意力的场景。

public class ScaleRotateTransformer implements ViewPager2.PageTransformer {
    private static final float SCALE_FACTOR = 0.9f;
    private static final float ROTATION_FACTOR = 30f;

    @Override
    public void transformPage(@NonNull View page, float position) {
        if (position < -1) {
            page.setScaleX(SCALE_FACTOR);
            page.setScaleY(SCALE_FACTOR);
            page.setRotationY(-ROTATION_FACTOR);
        } else if (position <= 1) {
            float scale = SCALE_FACTOR + (1 - SCALE_FACTOR) * (1 - Math.abs(position));
            page.setScaleX(scale);
            page.setScaleY(scale);
            page.setRotationY(position * ROTATION_FACTOR);
        } else {
            page.setScaleX(SCALE_FACTOR);
            page.setScaleY(SCALE_FACTOR);
            page.setRotationY(ROTATION_FACTOR);
        }
    }
}
代码解释:
  • 第8~11行 :左侧远端页面缩小并左旋;
  • 第13~18行 :中间区域按距离中心的程度动态调整缩放与旋转角度;
  • 第20~23行 :右侧远端同理。

该动画利用三维旋转制造“翻牌”错觉,需开启硬件加速:

<androidx.viewpager2.widget.ViewPager2
    android:layerType="hardware"
    ... />

否则可能出现闪烁或掉帧现象。

4.2.3 深度平移(Depth Page Transformer)模拟3D感

深度动画模仿卡片堆叠效果,营造空间层次感:

public class DepthPageTransformer implements ViewPager2.PageTransformer {
    private static final float MIN_SCALE = 0.75f;

    @Override
    public void transformPage(View view, float position) {
        int pageWidth = view.getWidth();
        view.setElevation(Math.max(0, 1 - Math.abs(position)));

        if (position < 0) { // 页面向左滑出
            float scaleFactor = MIN_SCALE + (1 - MIN_SCALE) * (1 - Math.abs(position));
            view.setScaleX(scaleFactor);
            view.setScaleY(scaleFactor);
            view.setTranslationX(pageWidth * -position * 0.25f);
        } else { // 页面向右进入
            view.setAlpha(MIN_SCALE + (1 - MIN_SCALE) * (1 - position));
            view.setTranslationX(-pageWidth * position * 0.25f);
            float scaleFactor = MIN_SCALE + (1 - MIN_SCALE) * (1 - position);
            view.setScaleX(scaleFactor);
            view.setScaleY(scaleFactor);
        }
    }
}
特性说明:
  • setElevation() 提升层级,避免被遮挡;
  • translationX 添加深度位移,增强透视感;
  • 左右两侧采用不对称动画策略,突出主次关系。

4.3 ObjectAnimator核心机制解析

4.3.1 属性动画作用于View属性的基本原理

ObjectAnimator 是 Android 属性动画体系的核心类之一,它通过反射机制周期性地调用目标对象的 setter 方法,改变其某个属性值,从而实现动画效果。

其工作流程如下:

ObjectAnimator animator = ObjectAnimator.ofFloat(view, "rotation", 0f, 360f);
animator.setDuration(1000);
animator.start();

上述代码会让 view 在 1 秒内完成一圈旋转。系统内部维护一个时间计时器,按照帧率(约 60FPS)计算当前插值进度 t ∈ [0,1] ,然后映射到属性值区间 [0, 360] ,并通过 setRotation(float) 更新视图。

优势包括:
- 不依赖 View 的绘制逻辑;
- 可作用于任意具有 getter/setter 的 JavaBean;
- 支持链式动画(AnimatorSet)。

4.3.2 动画插值器(Interpolator)的选择与自定义

插值器决定了动画的节奏曲线。ZBanner 内置多种选择:

插值器 效果描述 适用场景
LinearInterpolator 匀速运动 机械感切换
AccelerateInterpolator 加速到底 快速收尾
DecelerateInterpolator 先快后慢 自然停止
AnticipateOvershootInterpolator 先回拉再冲过头 强调动作

也可自定义贝塞尔曲线插值器:

public class BezierInterpolator implements TimeInterpolator {
    @Override
    public float getInterpolation(float input) {
        return PathInterpolatorCompat.create(0.25f, 0.1f, 0.25f, 1.0f).getInterpolation(input);
    }
}

应用于动画:

animator.setInterpolator(new BezierInterpolator());

4.3.3 多属性并发动画的性能考量与优化

同时改变多个属性可能导致过度绘制或 GC 频繁:

AnimatorSet set = new AnimatorSet();
set.playTogether(
    ObjectAnimator.ofFloat(v, "alpha", 0, 1),
    ObjectAnimator.ofFloat(v, "scaleX", 0.8f, 1.2f),
    ObjectAnimator.ofFloat(v, "scaleY", 0.8f, 1.2f)
);
set.setDuration(400);
set.start();

建议:
- 减少动画数量,优先使用 PageTransformer 手势联动;
- 避免在列表项中大规模使用复杂动画;
- 使用 Choreographer 监控掉帧情况。

4.4 动画过程中的卡顿与过度绘制问题

4.4.1 使用硬件加速提升渲染效率

确保在 Activity Theme 或清单文件中启用硬件加速:

<application android:hardwareAccelerated="true" ... >

对于特定页面还可局部开启:

view.setLayerType(View.LAYER_TYPE_HARDWARE, null);

4.4.2 避免频繁GC引发的帧率波动

属性动画频繁创建临时对象易引发 GC:

  • 复用 ObjectAnimator 实例;
  • 使用 PropertyValuesHolder 批量设置;
  • 避免在 transformPage 中新建对象。

4.4.3 动画复杂度与用户体验的平衡策略

并非越炫越好。应根据设备性能分级加载动画:

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O &&
    context.getPackageManager().hasSystemFeature(PackageManager.FEATURE_VULKAN_VERSION_1_0)) {
    // 高端机启用高级动画
    applyComplexAnimation();
} else {
    // 低端机降级为淡入淡出
    applySimpleAlphaAnimation();
}

最终目标是在视觉美感与系统稳定性之间取得最佳平衡。

5. 高效资源加载与系统兼容性保障实践

5.1 集成Glide/Picasso实现图片加载与缓存

在ZBanner控件中,图片作为轮播内容的重要组成部分,其加载效率直接影响用户体验。为实现高性能、低内存占用的图片展示,ZBanner通过抽象层统一集成了主流图片加载框架如Glide和Picasso,并支持运行时切换。

统一加载接口设计

为避免对单一框架产生强依赖,ZBanner定义了 ImageLoader 接口:

public interface ImageLoader {
    void loadImage(ImageView imageView, String url, ImageLoadConfig config);
}

// 使用示例
public class GlideLoader implements ImageLoader {
    @Override
    public void loadImage(ImageView imageView, String url, ImageLoadConfig config) {
        RequestBuilder<Drawable> request = Glide.with(imageView)
                .load(url)
                .placeholder(config.getPlaceholderRes())
                .error(config.getErrorRes())
                .transform(new MultiTransformation<>(new CenterCrop(), new RoundedCorners(config.getCornerRadius())));

        if (config.isSkipMemoryCache()) {
            request.skipMemoryCache(true);
        }

        request.into(imageView);
    }
}

该设计允许开发者根据项目需求注入自定义实现,提升可扩展性。

双级缓存策略应用

Glide默认采用 内存+磁盘 两级缓存机制:
- 内存缓存 :使用LruResourceCache管理活跃Bitmap对象,快速复用。
- 磁盘缓存 :基于DiskLruCache存储原始或转换后图像,避免重复网络请求。

缓存类型 存储位置 访问速度 生命周期
内存缓存 RAM 极快 进程内有效,OOM时释放
磁盘缓存 外部/内部存储 跨会话持久化

通过 .diskCacheStrategy(DiskCacheStrategy.ALL) 可精细控制缓存行为。

扩展功能支持

ZBanner封装常用UI增强特性:
- 占位图(loading placeholder)
- 错误图(error fallback)
- 圆角/圆形裁剪
- 渐进式JPEG加载

配置类 ImageLoadConfig 提供链式调用API,便于运行时动态设置。

5.2 集成ExoPlayer实现视频播放控制

为支持视频混合轮播,ZBanner集成Google官方媒体引擎ExoPlayer,具备高度可定制性和优秀解码性能。

ExoPlayer实例初始化

private void initializePlayer(String videoUrl) {
    if (mPlayer == null) {
        mPlayer = new SimpleExoPlayer.Builder(context).build();
        playerView.setPlayer(mPlayer);

        MediaItem mediaItem = MediaItem.fromUri(videoUrl);
        mPlayer.setMediaItem(mediaItem);
        mPlayer.prepare();
        mPlayer.setRepeatMode(Player.REPEAT_MODE_ONE); // 循环播放
    }
}

SimpleExoPlayer 结合 PlayerView 实现基础播放界面,支持全屏切换、进度条等交互组件。

预加载与带宽适配

通过 DefaultLoadControl 调节缓冲策略:

LoadControl loadControl = new DefaultLoadControl.Builder()
    .setBufferDurationsMs(
        30000,    // minBufferMs
        60000,    // maxBufferMs
        10000,    // minPlaybackStartBufferMs
        15000     // minRewindBufferMs
    )
    .createDefaultLoadControl();

同时利用 BandwidthMeter 监测网络状况,自动选择合适码率的HLS流或DASH变体。

播放状态同步至UI

借助 Player.Listener 监听关键事件:

mPlayer.addListener(new Player.Listener() {
    @Override
    public void onIsPlayingChanged(boolean isPlaying) {
        if (isPlaying) {
            updateIndicator("▶ 正在播放");
        } else {
            updateIndicator("⏸ 暂停");
        }
    }
});

指示器实时反馈当前播放状态,提升用户感知。

5.3 内存优化与资源回收策略

Bitmap回收与软引用管理

尽管Glide自动管理Bitmap生命周期,但在高频滑动场景下仍建议手动干预:

@Override
public void onViewRecycled(@NonNull BannerViewHolder holder) {
    Glide.with(context).clear(holder.imageView);
    if (holder.bitmap != null && !holder.bitmap.isRecycled()) {
        holder.bitmap.recycle();
    }
}

对于大图预览场景,可结合 SoftReference<Bitmap> 实现弱引用缓存,在内存紧张时由GC自动回收。

视频播放器异步释放

确保页面不可见时及时释放资源:

private void releasePlayer() {
    if (mPlayer != null) {
        mPlayer.stop();
        mPlayer.release();
        mPlayer = null;
        // 清理缓冲区
        CacheUtil.clearAllCachedFiles(context);
    }
}

配合 HandlerThread 执行耗时释放操作,防止主线程阻塞。

OOM预防机制

监控当前内存使用情况:

ActivityManager am = (ActivityManager) context.getSystemService(ACTIVITY_SERVICE);
int memoryClass = am.getMemoryClass(); // 单位MB
float freePercent = (float) am.getMemoryInfo(new MemoryInfo()).availMem / 
                    (1024 * 1024 * memoryClass);

if (freePercent < 0.1f) {
    // 触发主动清理策略
    trimMemory(TRIM_MEMORY_MODERATE);
}

并注册 ComponentCallbacks2 响应系统级内存警告。

5.4 AndroidX兼容性与多版本系统适配

最小SDK版本与API兼容处理

ZBanner设定minSdkVersion=21(Android 5.0),充分利用现代API同时保持广泛覆盖。

关键兼容代码片段:

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
    window.setStatusBarColor(ContextCompat.getColor(context, R.color.status_bar));
} else {
    // 降级处理:设置半透明状态栏
    getWindow().addFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS);
}

使用 BuildCompat 库检测新特性可用性,避免硬编码API判断。

多分辨率与屏幕密度适配

采用 ConstraintLayout 构建响应式布局,结合 dimens.xml 多目录配置:

res/
  ├── values/dimens.xml
  ├── values-sw600dp/dimens.xml   // 平板
  └── values-sw720dp-land/dimens.xml // 横屏大屏

图片资源按 mdpi , hdpi , xhdpi , xxhdpi , xxxhdpi 分级提供,确保高清显示。

主题样式与夜间模式支持

通过 AppCompatDelegate.setDefaultNightMode() 启用夜间主题联动:

<style name="ZBannerIndicatorStyle" parent="Widget.MaterialComponents.TabLayout">
    <item name="android:background">@color/banner_indicator_bg</item>
    <item name="tabIndicatorColor">@color/banner_indicator_selected</item>
</style>

颜色资源存放于 res/values-night/colors.xml ,实现深色模式自动切换。

本文还有配套的精品资源,点击获取 menu-r.4af5f7ec.gif

简介:ZBanner是一个专为Android平台设计的开源轮播控件,支持图片与视频混合展示,适用于广告、首页推荐等高交互场景。该控件提供丰富的样式自定义功能,包括动画效果、指示器样式和轮播速度调节,帮助开发者实现流畅的多媒体轮播体验。通过集成Glide、ExoPlayer等主流框架,ZBanner高效处理资源加载与播放,并具备良好的内存优化与Android版本兼容性。本项目包含完整源码、示例应用及文档,便于快速集成与二次开发,提升App的视觉吸引力和用户体验。


本文还有配套的精品资源,点击获取
menu-r.4af5f7ec.gif

Logo

火山引擎开发者社区是火山引擎打造的AI技术生态平台,聚焦Agent与大模型开发,提供豆包系列模型(图像/视频/视觉)、智能分析与会话工具,并配套评测集、动手实验室及行业案例库。社区通过技术沙龙、挑战赛等活动促进开发者成长,新用户可领50万Tokens权益,助力构建智能应用。

更多推荐