廣告輪播是移動應用中提升用戶轉化率的核心組件,尤其在電商、資訊類應用中應用廣泛。傳統輪播僅支持圖片展示,而現代應用需要兼顧圖片和視頻內容以增強吸引力。本文將詳細講解如何實現一個支持圖片與視頻混合播放的高性能廣告輪播,涵蓋布局設計、媒體加載、自動輪播、生命周期管理等關鍵技術點,并提供可直接復用的代碼方案。
一、核心組件與技術選型
實現混合媒體輪播需要解決三個核心問題:不同類型媒體的統一管理、視頻播放的資源控制、輪播切換的流暢性。選擇合適的技術棧是實現的基礎。
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/ 弱網)
- 測試不同尺寸設備的適配性
- 測試視頻播放時的內存占用
實現一個高質量的混合媒體輪播需要兼顧功能完整性和性能穩定性。通過本文的方案,開發者可以構建一個支持圖片與視頻混合展示、自動輪播、生命周期感知的廣告組件,同時通過優化策略確保在各種設備上的流暢體驗。關鍵在于合理管理媒體資源,平衡加載速度與內存占用,最終提升用戶體驗和廣告轉化效果。