Android 广告轮播全实现:图片与视频混合展示的完整方案
广告轮播是移动应用中提升用户转化率的核心组件,尤其在电商、资讯类应用中应用广泛。传统轮播仅支持图片展示,而现代应用需要兼顾图片和视频内容以增强吸引力。本文将详细讲解如何实现一个支持图片与视频混合播放的高性能广告轮播,涵盖布局设计、媒体加载、自动轮播、生命周期管理等关键技术点,并提供可直接复用的代码方案。
一、核心组件与技术选型
实现混合媒体轮播需要解决三个核心问题:不同类型媒体的统一管理、视频播放的资源控制、轮播切换的流畅性。选择合适的技术栈是实现的基础。
1.1 核心组件选择
组件 | 选型方案 | 优势 |
轮播容器 | ViewPager2 | 支持垂直 / 水平滑动、 RecyclerView 复用机制、页面 Transformer |
图片加载 | Coil | Kotlin 友好、支持 GIF、自动内存管理 |
视频播放 | ExoPlayer | 支持多种格式、低延迟、资源控制精细 |
生命周期管理 | LifecycleObserver | 自动感知组件生命周期,释放资源 |
缓存策略 | 三级缓存(内存 + 磁盘 + 网络) | 减少重复请求,提升加载速度 |
1.2 媒体数据模型设计
统一图片和视频的数据模型,便于适配器统一处理:
// 媒体类型枚举
enum class MediaType {IMAGE, VIDEO
}// 广告数据模型
data class AdMedia(val id: String,val url: String, // 图片或视频URLval type: MediaType,val duration: Long = 5000 // 展示时长(毫秒),视频可使用自身时长
)// 示例数据
val testAds = listOf(AdMedia("1", "https://example.com/banner1.jpg", MediaType.IMAGE),AdMedia("2", "https://example.com/promo.mp4", MediaType.VIDEO),AdMedia("3", "https://example.com/banner2.jpg", MediaType.IMAGE)
)
二、基础布局与轮播容器实现
轮播的基础结构由三部分组成:ViewPager2 容器、媒体展示项、页码指示器。合理的布局设计是实现流畅体验的前提。
2.1 主布局设计
<!-- res/layout/layout_ad_carousel.xml -->
<androidx.constraintlayout.widget.ConstraintLayoutxmlns:android="http://schemas.android.com/apk/res/android"xmlns:app="http://schemas.android.com/apk/res-auto"android:layout_width="match_parent"android:layout_height="200dp"><!-- 轮播容器 --><androidx.viewpager2.widget.ViewPager2android:id="@+id/viewPager"android:layout_width="match_parent"android:layout_height="match_parent"app:layout_constraintTop_toTopOf="parent"app:layout_constraintBottom_toBottomOf="parent"app:layout_constraintLeft_toLeftOf="parent"app:layout_constraintRight_toRightOf="parent"/><!-- 页码指示器 --><LinearLayoutandroid:id="@+id/indicatorContainer"android:layout_width="wrap_content"android:layout_height="wrap_content"android:orientation="horizontal"android:spacing="8dp"app:layout_constraintBottom_toBottomOf="parent"app:layout_constraintEnd_toEndOf="parent"app:layout_constraintStart_toStartOf="parent"app:layout_constraintBottom_margin="16dp"/></androidx.constraintlayout.widget.ConstraintLayout>
2.2 媒体项布局(图片与视频共用)
<!-- res/layout/item_media_container.xml -->
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"android:layout_width="match_parent"android:layout_height="match_parent"><!-- 图片容器 --><androidx.appcompat.widget.AppCompatImageViewandroid:id="@+id/ivImage"android:layout_width="match_parent"android:layout_height="match_parent"android:scaleType="centerCrop"android:visibility="gone"/><!-- 视频容器 --><FrameLayoutandroid:id="@+id/videoContainer"android:layout_width="match_parent"android:layout_height="match_parent"android:visibility="gone"><!-- ExoPlayer的SurfaceView --><com.google.android.exoplayer2.ui.StyledPlayerViewandroid:id="@+id/playerView"android:layout_width="match_parent"android:layout_height="match_parent"android:keepScreenOn="true"/><!-- 视频加载中指示器 --><ProgressBarandroid:id="@+id/progressBar"android:layout_width="wrap_content"android:layout_height="wrap_content"android:layout_gravity="center"android:visibility="gone"/></FrameLayout></FrameLayout>
三、核心适配器实现
ViewPager2 的适配器需要处理两种视图类型(图片 / 视频),并实现高效的复用机制。关键在于分离媒体加载逻辑,确保资源正确释放。
3.1 适配器基础结构
class MediaCarouselAdapter(private val lifecycle: Lifecycle, // 用于绑定播放器生命周期private val ads: List<AdMedia>
) : RecyclerView.Adapter<MediaCarouselAdapter.MediaViewHolder>() {// 视图类型:图片=0,视频=1override fun getItemViewType(position: Int): Int {return when (ads[position].type) {MediaType.IMAGE -> 0MediaType.VIDEO -> 1}}override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MediaViewHolder {val view = LayoutInflater.from(parent.context).inflate(R.layout.item_media_container, parent, false)return MediaViewHolder(view, viewType, lifecycle)}override fun onBindViewHolder(holder: MediaViewHolder, position: Int) {holder.bind(ads[position])}override fun getItemCount() = ads.size// 视图持有者class MediaViewHolder(itemView: View,viewType: Int,lifecycle: Lifecycle) : RecyclerView.ViewHolder(itemView) {// 视图绑定与媒体加载逻辑(见3.2、3.3节)}// 回收资源override fun onViewRecycled(holder: MediaViewHolder) {super.onViewRecycled(holder)holder.release()}
}
3.2 图片加载实现
使用 Coil 加载图片,支持自动缓存和生命周期管理:
// 在MediaViewHolder中
private val imageView: AppCompatImageView = itemView.findViewById(R.id.ivImage)private fun bindImage(ad: AdMedia) {// 显示图片视图,隐藏视频视图imageView.visibility = View.VISIBLEvideoContainer.visibility = View.GONE// 使用Coil加载图片Coil.imageLoader(itemView.context).enqueue(ImageRequest.Builder(itemView.context).data(ad.url).target(imageView).crossfade(true).placeholder(R.drawable.ic_ad_placeholder).error(R.drawable.ic_ad_error).listener(onSuccess = { request, metadata ->// 图片加载成功回调},onError = { request, throwable ->Log.e("ImageLoad", "加载失败", throwable)}).build())
}
3.3 视频播放实现
基于 ExoPlayer 实现视频加载与播放控制,关键在于与生命周期绑定:
// 在MediaViewHolder中
private val videoContainer: FrameLayout = itemView.findViewById(R.id.videoContainer)
private val playerView: StyledPlayerView = itemView.findViewById(R.id.playerView)
private val progressBar: ProgressBar = itemView.findViewById(R.id.progressBar)
private var player: ExoPlayer? = null
private var mediaItem: MediaItem? = null
private var isPlaying = false// 初始化播放器
private fun initPlayer(lifecycle: Lifecycle) {if (player == null) {val context = itemView.context// 创建播放器实例player = ExoPlayer.Builder(context).build()playerView.player = player// 绑定生命周期,自动释放资源lifecycle.addObserver(object : LifecycleObserver {@OnLifecycleEvent(Lifecycle.Event.ON_PAUSE)fun onPause() {player?.pause()isPlaying = false}@OnLifecycleEvent(Lifecycle.Event.ON_DESTROY)fun onDestroy() {releasePlayer()}})}
}// 加载并播放视频
private fun bindVideo(ad: AdMedia) {// 显示视频视图,隐藏图片视图videoContainer.visibility = View.VISIBLEimageView.visibility = View.GONEprogressBar.visibility = View.VISIBLEinitPlayer(lifecycle)mediaItem = MediaItem.fromUri(ad.url)player?.apply {setMediaItem(mediaItem!!)prepare()addListener(object : Player.Listener {override fun onPlaybackStateChanged(state: Int) {if (state == Player.STATE_READY) {progressBar.visibility = View.GONEplay()isPlaying = true} else if (state == Player.STATE_ERROR) {progressBar.visibility = View.GONE}}override fun onPlayWhenReadyChanged(playWhenReady: Boolean, reason: Int) {isPlaying = playWhenReady}})}
}// 释放视频资源
private fun releasePlayer() {player?.stop()player?.release()player = nullplayerView.player = nullisPlaying = false
}
3.4 绑定媒体数据
在 ViewHolder 中根据类型分发处理:
// 在MediaViewHolder中
fun bind(ad: AdMedia) {when (ad.type) {MediaType.IMAGE -> bindImage(ad)MediaType.VIDEO -> bindVideo(ad)}
}// 回收资源
fun release() {when (ads[adapterPosition].type) {MediaType.IMAGE -> {// 取消图片加载请求Coil.imageLoader(itemView.context).cancelAll()}MediaType.VIDEO -> {releasePlayer()}}
}
四、自动轮播与交互控制
自动轮播需要实现定时切换、用户交互暂停、循环播放等功能,同时处理视频播放时的特殊逻辑。
4.1 自动轮播核心逻辑
class AutoCarouselManager(private val viewPager: ViewPager2,private val ads: List<AdMedia>,private val interval: Long = 5000 // 默认5秒切换一次
) {private val handler = Handler(Looper.getMainLooper())private val carouselRunnable = object : Runnable {override fun run() {val current = viewPager.currentItemval next = (current + 1) % ads.sizeviewPager.setCurrentItem(next, true)handler.postDelayed(this, getNextDelay(next))}}// 根据媒体类型获取下一次延迟时间(视频使用自身时长)private fun getNextDelay(position: Int): Long {return if (ads[position].type == MediaType.VIDEO) {ads[position].duration // 视频使用预设时长或实际时长} else {interval // 图片使用默认间隔}}// 开始自动轮播fun start() {stop() // 先停止再启动,避免重复handler.postDelayed(carouselRunnable, getNextDelay(viewPager.currentItem))}// 停止自动轮播fun stop() {handler.removeCallbacks(carouselRunnable)}// 跟随生命周期管理fun attachToLifecycle(lifecycle: Lifecycle) {lifecycle.addObserver(object : LifecycleObserver {@OnLifecycleEvent(Lifecycle.Event.ON_RESUME)fun onResume() {start()}@OnLifecycleEvent(Lifecycle.Event.ON_PAUSE)fun onPause() {stop()}@OnLifecycleEvent(Lifecycle.Event.ON_DESTROY)fun onDestroy() {stop()}})}
}
4.2 用户交互处理
当用户触摸轮播时暂停自动播放,结束触摸后恢复:
// 在轮播初始化时设置触摸监听
viewPager.registerOnPageChangeCallback(object : ViewPager2.OnPageChangeCallback() {override fun onPageSelected(position: Int) {// 更新指示器updateIndicator(position)// 重置自动轮播计时器autoCarouselManager.stop()autoCarouselManager.start()}
})// 触摸监听:按下时停止,抬起时恢复
viewPager.setOnTouchListener { _, event ->when (event.action) {MotionEvent.ACTION_DOWN -> {autoCarouselManager.stop()}MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL -> {autoCarouselManager.start()}}false // 不消费事件,确保滑动正常
}
4.3 页码指示器实现
动态创建指示器点,并在页面切换时更新状态:
private fun initIndicator(container: LinearLayout, count: Int) {// 清除现有指示器container.removeAllViews()// 创建指示器点repeat(count) {val indicator = View(container.context).apply {layoutParams = LinearLayout.LayoutParams(8.dp, 8.dp).apply {gravity = Gravity.CENTER}setBackgroundResource(R.drawable.selector_indicator)isSelected = it == 0 // 默认第一个选中}container.addView(indicator)}
}private fun updateIndicator(position: Int) {val count = indicatorContainer.childCountfor (i in 0 until count) {indicatorContainer.getChildAt(i).isSelected = i == position}
}// 指示器选择器(res/drawable/selector_indicator.xml)
<selector xmlns:android="http://schemas.android.com/apk/res/android"><item android:drawable="@drawable/indicator_selected" android:state_selected="true"/><item android:drawable="@drawable/indicator_normal"/>
</selector>
五、性能优化与资源管理
混合媒体轮播容易出现内存泄漏和性能问题,需要针对性优化。
5.1 内存优化策略
1.图片缓存控制:
// 配置Coil缓存策略
val imageLoader = ImageLoader.Builder(context).memoryCachePolicy(CachePolicy.ENABLED).diskCachePolicy(CachePolicy.ENABLED).diskCache(DiskCache.Builder().directory(context.cacheDir.resolve("image_cache")).maxSizeBytes(512L * 1024 * 1024) // 512MB缓存上限.build()).build()
2.视频资源回收:
// 在适配器的onViewRecycled中强制释放
override fun onViewRecycled(holder: MediaViewHolder) {super.onViewRecycled(holder)holder.release() // 调用前面实现的release方法
}// 限制ViewPager2的缓存页数
viewPager.offscreenPageLimit = 1 // 只缓存当前页和相邻页
3.避免大型 Bitmap:
// 加载图片时指定尺寸(与轮播控件匹配)
ImageRequest.Builder(context).size(1080, 600) // 与轮播容器尺寸一致.scale(Scale.FILL).build()
5.2 播放体验优化
1.视频预加载:
// 在ViewPager2中预加载下一个视频
viewPager.registerOnPageChangeCallback(object : ViewPager2.OnPageChangeCallback() {override fun onPageSelected(position: Int) {val nextPos = (position + 1) % ads.sizeif (ads[nextPos].type == MediaType.VIDEO) {// 预加载下一个视频(仅准备,不播放)preloadVideo(ads[nextPos].url)}}
})
2.视频静音播放:
// 默认静音播放,提升用户体验
playerView.controllerShowTimeoutMs = 0 // 隐藏控制器
player?.volume = 0f // 静音
3.滑动时暂停视频:
// 页面滚动时暂停所有视频
viewPager.registerOnPageChangeCallback(object : ViewPager2.OnPageChangeCallback() {override fun onPageScrollStateChanged(state: Int) {when (state) {ViewPager2.SCROLL_STATE_DRAGGING -> {// 开始滑动,暂停所有可见视频pauseVisibleVideos()}ViewPager2.SCROLL_STATE_IDLE -> {// 滑动结束,恢复当前视频播放resumeCurrentVideo()}}}
})
5.3 异常处理
1.网络状态适配:
// 监听网络变化,弱网环境下优先加载图片
private val networkCallback = object : ConnectivityManager.NetworkCallback() {override fun onNetworkCapabilitiesChanged(network: Network,capabilities: NetworkCapabilities) {val isMetered = connectivityManager.isActiveNetworkMeteredval isLowBandwidth = !capabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_HIGH_BANDWIDTH)if (isMetered || isLowBandwidth) {// 弱网环境,替换视频为封面图replaceVideosWithCovers()}}
}
2.错误重试机制:
// 视频加载失败时重试
private fun setupRetry机制(player: ExoPlayer) {var retryCount = 0player.addListener(object : Player.Listener {override fun onPlayerError(error: PlaybackException) {if (retryCount < 3) { // 最多重试3次retryCount++player.prepare()} else {// 重试失败,显示错误图片showVideoErrorPlaceholder()}}})
}
六、完整集成示例
将上述组件整合到 Activity 或 Fragment 中:
class AdCarouselActivity : AppCompatActivity() {private lateinit var binding: LayoutAdCarouselBindingprivate lateinit var adapter: MediaCarouselAdapterprivate lateinit var autoCarouselManager: AutoCarouselManageroverride fun onCreate(savedInstanceState: Bundle?) {super.onCreate(savedInstanceState)binding = LayoutAdCarouselBinding.inflate(layoutInflater)setContentView(binding.root)// 初始化数据val ads = getAdData()// 初始化适配器adapter = MediaCarouselAdapter(lifecycle, ads)binding.viewPager.adapter = adapter// 初始化指示器initIndicator(binding.indicatorContainer, ads.size)// 初始化自动轮播autoCarouselManager = AutoCarouselManager(binding.viewPager, ads)autoCarouselManager.attachToLifecycle(lifecycle)// 页面切换监听binding.viewPager.registerOnPageChangeCallback(object : ViewPager2.OnPageChangeCallback() {override fun onPageSelected(position: Int) {updateIndicator(position)}})// 启动轮播autoCarouselManager.start()}private fun getAdData(): List<AdMedia> {// 实际项目中从网络或本地获取return testAds}override fun onDestroy() {super.onDestroy()// 手动释放资源(双重保障)autoCarouselManager.stop()}
}
七、扩展功能与最佳实践
7.1 常用扩展功能
1.轮播动画:
// 设置页面切换动画
binding.viewPager.setPageTransformer(DepthPageTransformer())// 深度动画实现
class DepthPageTransformer : ViewPager2.PageTransformer {override fun transformPage(page: View, position: Float) {page.apply {val pageWidth = widthwhen {position < -1 -> alpha = 0fposition <= 0 -> {alpha = 1ftranslationX = 0f}position <= 1 -> {alpha = 1 - positiontranslationX = pageWidth * -position}else -> alpha = 0f}}}
}
2.点击事件处理:
// 在适配器中设置点击监听
class MediaCarouselAdapter(private val onAdClick: (AdMedia) -> Unit,// 其他参数...
) {// 在onBindViewHolder中holder.itemView.setOnClickListener {onAdClick(ads[position])}
}
7.2 最佳实践总结
1.生命周期管理:所有资源(播放器、图片请求、计时器)必须与生命周期绑定,避免内存泄漏
2.资源优先级:视频加载优先级低于图片,弱网环境下自动降级为图片展示
3.用户体验:
- 首次加载显示占位图
- 视频默认静音播放,提供手动开启声音按钮
- 滑动时平滑过渡,避免卡顿
4.测试覆盖:
- 测试不同网络环境(4G/5G/WiFi/ 弱网)
- 测试不同尺寸设备的适配性
- 测试视频播放时的内存占用
实现一个高质量的混合媒体轮播需要兼顾功能完整性和性能稳定性。通过本文的方案,开发者可以构建一个支持图片与视频混合展示、自动轮播、生命周期感知的广告组件,同时通过优化策略确保在各种设备上的流畅体验。关键在于合理管理媒体资源,平衡加载速度与内存占用,最终提升用户体验和广告转化效果。