最近幾天一直在研究谷歌的JetPack Compose,給我最大的感受就是便捷,往往使用RecyclerView和Adapter需要實(shí)現(xiàn)的功能周霉,包括自定義View,或者簡單到一個(gè)View的自定義掂器,代碼比起Compose要多了很多。
自己嘗試實(shí)現(xiàn)了一款視頻列表播放Demo俱箱,代碼還有很多需要優(yōu)化的地方国瓮,目前只是實(shí)現(xiàn)了簡單的效果。
一狞谱、分析
- 網(wǎng)絡(luò)請求與API:
網(wǎng)絡(luò)請求依然用retrofit, 視頻列表API隨便找一個(gè)即可乃摹,分頁跟上次一樣選用Paging3,個(gè)人感覺非常搭配Jetpack Compose
- 視頻播放器的選擇:
播放器可以選用大名鼎鼎的 ijkplayer跟衅,我就用ExoPlayer,自己貼了個(gè)controller_view上去孵睬。
- 橫豎屏切換:
同一個(gè)PlayerView,全屏的時(shí)候 ,先從列表item中remove(),然后addView()給R.id.content ,豎屏反過來操作伶跷。
二掰读、分頁與網(wǎng)絡(luò)請求:
-
實(shí)例化Retrofit:
object RetrofitClient { private val instance: Retrofit by lazy { val logInterceptor = HttpLoggingInterceptor() if (BuildConfig.DEBUG) { //顯示日志 logInterceptor.level = HttpLoggingInterceptor.Level.BODY } else { logInterceptor.level = HttpLoggingInterceptor.Level.NONE } val okhttpClient = OkHttpClient.Builder().addInterceptor(logInterceptor) .connectTimeout(5, TimeUnit.SECONDS)//設(shè)置超時(shí)時(shí)間 .retryOnConnectionFailure(true).build() Retrofit.Builder() .client(okhttpClient) .baseUrl(BASE_URL) .addConverterFactory(GsonConverterFactory.create()) .build() } fun <T> createApi(clazz: Class<T>): T { return instance.create(clazz) as T } }
-
定義列表接口:
interface VideoListService { @GET("api/v4/discovery/hot") suspend fun getVideoList( @Query("start") itemStart: Int = 1, @Query("num") pageSize: Int = 6 ): VideoStore }
-
Paging分頁邏輯在VideoListDataSource.kt完成:
class VideoListDataSource(private val repository: Repository) : PagingSource<Int, VideoItem>() { private val TAG = "--ExamSource" override fun getRefreshKey(state: PagingState<Int, VideoItem>): Int? { return null } override suspend fun load(params: LoadParams<Int>): LoadResult<Int, VideoItem> { return try { val currentPage = params.key ?: 1 val pageSize = params.loadSize // 每一頁請求幾條數(shù)據(jù) val everyPageSize = 4 // 第一次初始請求,多加載一點(diǎn) val initPageSize = 8 // 當(dāng)前請求的起始位置叭莫,指起始下標(biāo) val curStartItem = if (currentPage == 1) 1 else (currentPage - 2) * everyPageSize + 1 + initPageSize val responseList = repository.getVideoList(curStartItem, pageSize = pageSize) .videoList ?: emptyList<VideoItem>() // 上一頁頁碼 val preKey = if (currentPage == 1) null else currentPage.minus(1) // 下一頁頁碼 var nextKey: Int? = currentPage.plus(1) Log.d(TAG, "currentPage: $currentPage") Log.d(TAG, "preKey: $preKey") Log.d(TAG, "nextKey: $nextKey") if (responseList.isEmpty()) { nextKey = null } LoadResult.Page( data = responseList, prevKey = preKey, nextKey = nextKey ) } catch (e: Exception) { e.printStackTrace() LoadResult.Error(e) } } }
-
數(shù)據(jù)請求:Repository,
谷歌之前推薦的架構(gòu)庫 官方Android應(yīng)用架構(gòu)庫(Architecture Components)推薦將ViewModel中的網(wǎng)絡(luò)請求數(shù)據(jù)庫交互部分交給Repository來處理蹈集,而ViewModel專注于業(yè)務(wù)和UI交互,并等待Repository去拿網(wǎng)絡(luò)數(shù)據(jù)雇初,大部分應(yīng)用不需要每次都請求新的頁面數(shù)據(jù)雾狈,最好是緩存到本地。于是該架構(gòu)推薦Room數(shù)據(jù)庫作為本地緩存抵皱,這樣是比較完美的善榛,也就是請求完列表頁面數(shù)據(jù)給Room, 頁面繪制優(yōu)先拿Room的數(shù)據(jù)。但是我這里沒有考慮實(shí)現(xiàn)呻畸。
object Repository {
suspend fun getVideoList(itemStart: Int, pageSize: Int) =
RetrofitClient.createApi(VideoListService::class.java)
.getVideoList(itemStart, pageSize)
}
-
ViewModel拿到數(shù)據(jù):
這里拿到是PagingData<T> 的流移盆,被viewModel收集,需要傳入?yún)f(xié)程作用域伤为,Paging內(nèi)部會(huì)安排發(fā)送流:
/** * The actual job that collects the upstream. */ private val job = scope.launch(start = CoroutineStart.LAZY) { src.withIndex() .collect { mutableSharedSrc.emit(it) pageController.record(it) } }.also { it.invokeOnCompletion { // Emit a final `null` message to the mutable shared flow. // Even though, this tryEmit might technically fail, it shouldn't because we have // unlimited buffer in the shared flow. mutableSharedSrc.tryEmit(null) } }
val videoItemList = Pager(
config = PagingConfig(
pageSize = 4,
initialLoadSize = 8, // 第一次加載數(shù)量
prefetchDistance = 2,
)
) {
VideoListDataSource(Repository)
}.flow.cachedIn(viewModelScope)
三咒循、加載列表
上面viewModel 我們得到Flow<PagingData<T>數(shù)據(jù)流,Compose提供了一種便捷加載LazyColumn(其實(shí)類似RecyclerView 只是用不著RecyclerAdapter)的方式:
/**
*從[PagingData]的[流]收集數(shù)據(jù)绞愚,將他們表現(xiàn)為一個(gè)[LazyPagingItems]實(shí)例叙甸。
* [LazyPagingItems]實(shí)例可以被[items]和[itemsIndexed]方法使用
*[LazyListScope]應(yīng)該是個(gè)上下文作用域,使用它就是為了從[PagingData]的[Flow]流獲取的數(shù)據(jù)能夠被LazyColumn使用位衩。大概是這個(gè)意思裆蒸,總之就是方便開發(fā)者。
*
* @sample androidx.paging.compose.samples.PagingBackendSample
*/
@Composable
public fun <T : Any> Flow<PagingData<T>>.collectAsLazyPagingItems(): LazyPagingItems<T> {
val lazyPagingItems = remember(this) { LazyPagingItems(this) }
LaunchedEffect(lazyPagingItems) {
lazyPagingItems.collectPagingData()
}
LaunchedEffect(lazyPagingItems) {
lazyPagingItems.collectLoadState()
}
return lazyPagingItems
}
列表實(shí)現(xiàn):
沒有什么特別的地方糖驴,但是有一點(diǎn)需要注意:列表隨著滑動(dòng)僚祷,始終對頂部可見的Item做播放佛致,所以需要判斷列表中頂部可見的項(xiàng)。
LazyListState源碼中有這樣一個(gè)方法:
/**
* The index of the first item that is visible
*/
val firstVisibleItemIndex: Int get() = scrollPosition.observableIndex
”可見的第一項(xiàng)的索引“ 就是第一項(xiàng)眼睛看到的Item索引
/**
* 首頁列表加載 ---普通加載辙谜,沒有下拉刷新俺榆,可加載下一頁
* */
@Composable
fun NormalVideoListScreen(
viewModel: MainViewModel,
context: Context,
) {
val collectAsLazyPagingIDataList = viewModel.videoItemList.collectAsLazyPagingItems()
// 首次加載業(yè)務(wù)邏輯
when (collectAsLazyPagingIDataList.loadState.refresh) {
is LoadState.NotLoading -> {
ContentInfoList(
collectAsLazyPagingIDataList = collectAsLazyPagingIDataList,
context = context,
viewModel = viewModel
)
}
is LoadState.Error -> ErrorPage() { collectAsLazyPagingIDataList.refresh() }
is LoadState.Loading -> LoadingPageUI()
}
}
@ExperimentalCoilApi
@Composable
fun ContentInfoList(
context: Context,
collectAsLazyPagingIDataList: LazyPagingItems<VideoItem>,
viewModel: MainViewModel
) {
val lazyListState = rememberLazyListState()
val focusIndex by derivedStateOf { lazyListState.firstVisibleItemIndex }
LazyColumn(
state = lazyListState
) {
itemsIndexed(collectAsLazyPagingIDataList) { index, videoItem ->
// 傳入列表卡片Item
VideoCardItem(
videoItem = videoItem!!,
isFocused = index == focusIndex,
onClick = { Toast.makeText(context, "ccc", Toast.LENGTH_SHORT).show() },
index = index,
viewModel = viewModel
)
}
// 加載下一頁業(yè)務(wù)邏輯
when (collectAsLazyPagingIDataList.loadState.append) {
is LoadState.NotLoading -> {
itemsIndexed(collectAsLazyPagingIDataList) { index, videoItem ->
VideoCardItem(
videoItem = videoItem!!,
isFocused = index == focusIndex,
onClick = { Toast.makeText(context, "ccc", Toast.LENGTH_SHORT).show() },
index = index,
viewModel = viewModel
)
}
}
is LoadState.Error -> item {
NextPageLoadError {
collectAsLazyPagingIDataList.retry()
}
}
LoadState.Loading -> item {
LoadingPageUI()
}
}
}
}
/**
* 頁面加載失敗重試
* */
@Composable
fun ErrorPage(onclick: () -> Unit = {}) {
Column(
modifier = Modifier.fillMaxSize(),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally
) {
Image(
modifier = Modifier.size(219.dp, 119.dp),
painter = painterResource(id = R.drawable.ic_default_empty),
contentDescription = "網(wǎng)絡(luò)問題",
contentScale = ContentScale.Crop
)
Button(
modifier = Modifier.padding(8.dp),
onClick = onclick,
) {
Text(text = "網(wǎng)絡(luò)不佳,請點(diǎn)擊重試")
}
}
}
/**
* 加載中動(dòng)效
* */
@Composable
fun LoadingPageUI() {
Box(
modifier = Modifier
.fillMaxSize()
.padding(170.dp), contentAlignment = Alignment.Center
) {
val animator by rememberInfiniteTransition().animateFloat(
initialValue = 0f,
targetValue = 360f,
animationSpec = infiniteRepeatable(
tween(800, easing = LinearEasing),
repeatMode = RepeatMode.Restart
)
)
Canvas(modifier = Modifier.fillMaxSize()) {
translate(80f, 80f) {
drawArc(
color = RedPink,
startAngle = 0f,
sweepAngle = animator,
useCenter = false,
size = Size(80 * 2f, 80 * 2f),
style = Stroke(12f),
alpha = 0.6f,
)
}
}
}
}
/**
* 加載下一頁失敗
* */
@Composable
fun NextPageLoadError(onClick: () -> Unit) {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(8.dp),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.Center,
) {
Button(onClick = onClick) {
Text(text = "重試")
}
}
}
四装哆、列表的Item
Item中需要嵌入播放器罐脊,由于播放器和布局是java代碼寫的,所以涉及兩者相互調(diào)用蜕琴。
LaunchedEffect:利用它爹殊,我們可以在@Compose中使用協(xié)程,官方文檔是這么描述的:
<u>要在可組合函數(shù)中安全地調(diào)用掛起函數(shù)奸绷,請使用launchedeeffect可組合函數(shù)梗夸。當(dāng)launchedeeffect進(jìn)入Composition時(shí),它會(huì)啟動(dòng)一個(gè)協(xié)程号醉,并將代碼塊作為參數(shù)傳遞反症。如果LaunchedEffect離開組合,協(xié)程將被取消畔派。</u>
先看布局代碼:
分為上面的文案描述和播放器部分铅碍,這里我通過判斷:
if(當(dāng)前item的下標(biāo) == 第一個(gè)可見Item的下標(biāo)){
布局播放器并preper
}else{
貼一張視頻封面占位
}
- 卡片上面的文字和封面部分:
@ExperimentalCoilApi
@Composable
fun VideoCardItem(
videoItem: VideoItem,
isFocused: Boolean,
onClick: () -> Unit,
index: Int,
viewModel: MainViewModel?
) {
val videoInfo = videoItem.videoInfo
Card(
modifier = Modifier
.fillMaxWidth()
.padding(start = 5.dp, top = 5.dp, end = 5.dp, bottom = 5.dp),
shape = RoundedCornerShape(10.dp),
elevation = 8.dp,
backgroundColor = if (isFocused) gray300 else MaterialTheme.colors.surface
) {
Column(
modifier = Modifier.padding(16.dp)
) {
Text(
text = "$index: ${videoInfo?.description}",
style = MaterialTheme.typography.h6
)
Text(
modifier = Modifier.padding(top = 8.dp),
text = videoInfo?.title ?: "",
style = MaterialTheme.typography.body1,
color = gray600
)
var width = 1280
var height = 720
videoInfo?.playInfo?.let {
if (it.isNotEmpty()) {
width = it[0].width
height = it[0].height
}
}
// 如果該Item是頂部可見,給它一個(gè)播放器自動(dòng)播放线椰,否則給一張海報(bào)占位
if (isFocused) {
ExoPlayerView(isFocused, videoInfo, viewModel)
} else {
// 截?cái)嘁韵聢D片Url
val coverUrl = videoInfo?.cover?.feed?.substringBefore('?')
CoilImage(
url = coverUrl,
modifier = Modifier
.aspectRatio(width.toFloat() / height)
.fillMaxWidth()
)
}
}
}
}
- 播放器部分胞谈,需要在Compose調(diào)用Android SDK的UI邏輯,俗稱Compose調(diào)用Android:
@ExperimentalCoilApi
@Composable
fun ExoPlayerView(isFocused: Boolean, videoInfo: VideoInfo?, viewModel: MainViewModel?) {
val context = LocalContext.current
// 獲取播放器實(shí)例
val exoPlayer = remember { ExoPlayerHolder.get(context = context) }
var playerView: MyPlayerView? = null
var width = 1280
var height = 720
videoInfo?.playInfo?.let {
if (it.isNotEmpty()) {
width = it[0].width
height = it[0].height
}
}
if (isFocused) {
videoInfo?.let {
LaunchedEffect(key1 = videoInfo.playUrl, key2 = it) {
val playUri = Uri.parse(it.playUrl)
val dataSourceFactory = VideoDataSourceHolder.getCacheFactory(context)
val mediaSource = when (Util.inferContentType(playUri)) {
C.TYPE_DASH -> DashMediaSource.Factory(dataSourceFactory)
.createMediaSource(MediaItem.fromUri(playUri))
C.TYPE_HLS -> HlsMediaSource.Factory(dataSourceFactory)
.createMediaSource(MediaItem.fromUri(playUri))
else -> ProgressiveMediaSource.Factory(dataSourceFactory)
.createMediaSource(MediaItem.fromUri(playUri))
}
exoPlayer.setMediaSource(mediaSource)
exoPlayer.prepare()
}
}
// Compose中使用傳統(tǒng)Android View 憨愉,谷歌文檔有這樣的描述:
/**
* 你可以在Compose UI中包含一個(gè)Android View層次結(jié)構(gòu)烦绳。如果你想使用在Compose中還不能使用的UI元素,比如AdView或
* MapView配紫,這種方法特別有用径密。這種方法還允許重用您設(shè)計(jì)的自定義視圖。要包含視圖元素或?qū)哟谓Y(jié)構(gòu)躺孝,請使用AndroidView可組
* 合享扔。AndroidView被傳遞一個(gè)lambda,返回一個(gè)View植袍。AndroidView還提供了一個(gè)更新回調(diào)函數(shù)惧眠,當(dāng)視圖膨脹時(shí)調(diào)用它。每當(dāng)在
* 回調(diào)中讀取State時(shí)于个,AndroidView就會(huì)重新組合氛魁。
*/
AndroidView(
modifier = Modifier.aspectRatio(width.toFloat() / height),
factory = { context ->
// 創(chuàng)建你需要的ViewGroup 或者 View
val frameLayout = FrameLayout(context)
frameLayout.setBackgroundColor(context.getColor(android.R.color.holo_purple))
frameLayout
},
update = { frameLayout ->
// 假如你定義了狀態(tài),則狀態(tài)發(fā)生改變或者它的父節(jié)點(diǎn)狀態(tài)改變,這里都會(huì)重建
logD("update removeAllViews, playerViewMode: ${PlayerViewManager.playerViewMode}, isFocused:$isFocused")
if (PlayerViewManager.playerViewMode == PlayViewMode.HALF_SCREEN) {
frameLayout.removeAllViews()
if (isFocused) {
playerView = PlayerViewManager.get(frameLayout.context)
// 切換播放器
MyPlayerView.switchTargetView(
exoPlayer,
PlayerViewManager.currentPlayerView,
playerView
)
PlayerViewManager.currentPlayerView = playerView
playerView?.apply {
player?.playWhenReady = true
(parent as? ViewGroup)?.removeView(this)
}
frameLayout.addView(
playerView,
FrameLayout.LayoutParams.MATCH_PARENT,
FrameLayout.LayoutParams.MATCH_PARENT
)
viewModel?.saveFrameLayout(frameLayout)
logD("update, frameLayout:$frameLayout")
} else if (playerView != null) {
playerView?.apply {
(parent as? ViewGroup)?.removeView(this)
PlayerViewManager.release(this)
}
playerView = null
}
}
}
)
DisposableEffect(key1 = videoInfo?.playUrl) {
onDispose {
logD("--onDispose, isFocused: $isFocused")
if (isFocused) {
playerView?.apply {
(parent as? ViewGroup)?.removeView(this)
}
exoPlayer.stop()
playerView?.let {
PlayerViewManager.release(it)
}
playerView = null
}
}
}
}
}
-
那么傳統(tǒng)Android如何調(diào)用Compose呢呆盖?
代碼或者xml中,F(xiàn)ragment中都可以使用Compose,如果是在代碼中贷笛,假設(shè)前面的視頻封面把他寫在上面的方法中应又,就可以這么寫:
if (isFocused) { // .... }else{ // 這里是Compose中插入Android View AndroidView( modifier = Modifier.aspectRatio(width.toFloat() / height), factory = { context -> val coverLayout = FrameLayout(context) coverLayout.setBackgroundColor(context.getColor(android.R.color.darker_gray)) coverLayout }, update = { coverLayout -> val coverUrl = videoInfo?.cover?.feed?.substringBefore('?') // 這里在Android View中插入Compose,使用ComposeView coverLayout.addView(ComposeView(context).apply { // 這個(gè)id需要注冊在res/values/ids.xml文件中 id = R.id.compose_view_cover setContent { MaterialTheme { CoilImage( url = coverUrl, modifier = Modifier.fillMaxWidth() ) } } }) } ) }
-
Android View與Compose調(diào)用其實(shí)還有很多,這里不多介紹乏苦,用到了就去了解株扛。下面再說說播放器邏輯:
播放器布局就用了exo自帶的PlayerView,添加了一個(gè)自己的player_controller_layout.xml
PlayView.java 和 PlayControllerView可以抽出來汇荐,自己按需要修改洞就。<?xml version="1.0" encoding="utf-8"?> <com.google.android.exoplayer2.ui.MyPlayerView xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" android:id="@+id/playerView" android:layout_width="match_parent" android:layout_height="match_parent" android:layout_gravity="center" android:minHeight="200dp" app:keep_content_on_player_reset="false" app:resize_mode="fixed_width" app:show_buffering="when_playing" app:shutter_background_color="@android:color/black" app:surface_type="surface_view" app:use_controller="true" app:controller_layout_id="@layout/player_controller_layout" />
五、exoPlayer播放器:
- 播放器創(chuàng)建:
/**
* 播放器實(shí)例創(chuàng)建
* */
object ExoPlayerHolder {
private var exoplayer: SimpleExoPlayer? = null
fun get(context: Context): SimpleExoPlayer {
if (exoplayer == null) {
exoplayer = createExoPlayer(context)
}
exoplayer!!.addListener(object : Player.Listener {
override fun onPlayerError(error: PlaybackException) {
super.onPlayerError(error)
Toast.makeText(context, error.message, Toast.LENGTH_SHORT).show()
logD("onPlayerError:${error.errorCode} ,${error.message}")
}
override fun onVideoSizeChanged(videoSize: VideoSize) {
super.onVideoSizeChanged(videoSize)
logD("onVideoSizeChanged:${videoSize.width} x ${videoSize.height} | ratio: ${videoSize.pixelWidthHeightRatio}")
}
override fun onSurfaceSizeChanged(width: Int, height: Int) {
super.onSurfaceSizeChanged(width, height)
logD("onSurfaceSizeChanged:$width x $height")
}
})
return exoplayer!!
}
// 創(chuàng)建ExoPlayer實(shí)例
private fun createExoPlayer(context: Context): SimpleExoPlayer {
return SimpleExoPlayer.Builder(context)
.setLoadControl(
DefaultLoadControl.Builder().setBufferDurationsMs(
// 設(shè)置預(yù)加載上限下限
DefaultLoadControl.DEFAULT_MIN_BUFFER_MS,
DefaultLoadControl.DEFAULT_MAX_BUFFER_MS,
DefaultLoadControl.DEFAULT_BUFFER_FOR_PLAYBACK_MS / 10,
DefaultLoadControl.DEFAULT_BUFFER_FOR_PLAYBACK_AFTER_REBUFFER_MS / 10
).build()
)
.build()
.apply {
// 播放模式掀淘,設(shè)置為不重復(fù)播放
repeatMode = Player.REPEAT_MODE_ONE
}
}
}
-
實(shí)例化PlayerView:
PlayerViewManager.kt
這里用到了androidx.core.util.Pools工具旬蟋,他是個(gè)對象池,復(fù)用對象池中的對象革娄,可以避免頻繁創(chuàng)建和銷毀堆中的對倾贰, 進(jìn)而減少垃圾收集器的負(fù)擔(dān)。設(shè)置為2就夠用了拦惋,acquire()先獲取對象匆浙,如果沒有獲取到就創(chuàng)建,使用完后release()即歸還給對象池厕妖。
對象池介紹: http://www.reibang.com/p/eb04e4e1869d
/**
* 用來管理 PlayerView
* */
object PlayerViewManager : ExoEventListener {
var currentPlayerView: MyPlayerView? = null
var playerViewMode = PlayViewMode.HALF_SCREEN
var activity: MainActivity? = null
var viewModel: MainViewModel? = null
private val playerViewPool = Pools.SimplePool<MyPlayerView>(2)
fun get(context: Context): MyPlayerView {
return playerViewPool.acquire() ?: createPlayerView(context)
}
fun release(player: MyPlayerView) {
playerViewPool.release(player)
}
/**
* 創(chuàng)建PlayerView
* */
private fun createPlayerView(context: Context): MyPlayerView {
val playView = (LayoutInflater.from(context)
.inflate(R.layout.exoplayer_texture_view, null, false) as MyPlayerView)
playView.setShowMultiWindowTimeBar(true)
playView.setShowBuffering(MyPlayerView.SHOW_BUFFERING_ALWAYS)
playView.controllerAutoShow = true
playView.playerController.setExoEventListener(this)
initOther(playView)
return playView
}
}
-
緩存設(shè)置與緩存策略:
/**
* 緩存基本設(shè)置首尼,exo內(nèi)部會(huì)提供一個(gè)命名 exoplayer_internal.db 的數(shù)據(jù)庫作為緩存
* */
object CacheHolder {
private var cache: SimpleCache? = null
private val lock = Object()
fun get(context: Context): SimpleCache {
synchronized(lock) {
if (cache == null) {
val cacheSize = 20L * 1024 * 1024
val exoDatabaseProvider = ExoDatabaseProvider(context)
cache = SimpleCache(
// 緩存文件地址
context.cacheDir,
// 釋放上次的緩存數(shù)據(jù)
LeastRecentlyUsedCacheEvictor(cacheSize),
// 提供數(shù)據(jù)庫
exoDatabaseProvider
)
}
}
return cache!!
}
}
/**
* 設(shè)置緩存策略
* */
object VideoDataSourceHolder {
private var cacheDataSourceFactory: CacheDataSource.Factory? = null
private var defaultDataSourceFactory: DataSource.Factory? = null
fun getCacheFactory(context: Context): CacheDataSource.Factory {
if (cacheDataSourceFactory == null) {
val simpleCache = CacheHolder.get(context)
val defaultFactory = getDefaultFactory(context)
cacheDataSourceFactory = CacheDataSource.Factory()
.setCache(simpleCache)
// 設(shè)置Uri協(xié)議相關(guān)參數(shù),用來從緩存做讀取操作
.setUpstreamDataSourceFactory(defaultFactory)
// 設(shè)置CacheDataSource工廠類型言秸,用來讀取緩存
.setCacheReadDataSourceFactory(FileDataSource.Factory())
// 緩存寫入設(shè)置
.setCacheWriteDataSinkFactory(
CacheDataSink.Factory()
.setCache(simpleCache)
.setFragmentSize(CacheDataSink.DEFAULT_FRAGMENT_SIZE)
)
}
return cacheDataSourceFactory!!
}
private fun getDefaultFactory(context: Context): DataSource.Factory {
if (defaultDataSourceFactory == null) {
defaultDataSourceFactory = DefaultDataSourceFactory(
context,
Util.getUserAgent(context, context.packageName)
)
}
return defaultDataSourceFactory!!
}
}
六软能、代碼:
ExoPlayer視頻播放