Android JetPack Compose實(shí)現(xiàn)網(wǎng)絡(luò)請求分頁加載熙暴,ExoPlayer視頻播放闺属,無縫全屏播放| Compose 與 View的互相調(diào)用

最近幾天一直在研究谷歌的JetPack Compose,給我最大的感受就是便捷,往往使用RecyclerView和Adapter需要實(shí)現(xiàn)的功能周霉,包括自定義View,或者簡單到一個(gè)View的自定義掂器,代碼比起Compose要多了很多。

自己嘗試實(shí)現(xiàn)了一款視頻列表播放Demo俱箱,代碼還有很多需要優(yōu)化的地方国瓮,目前只是實(shí)現(xiàn)了簡單的效果。

效果圖

一狞谱、分析

  1. 網(wǎng)絡(luò)請求與API:

網(wǎng)絡(luò)請求依然用retrofit, 視頻列表API隨便找一個(gè)即可乃摹,分頁跟上次一樣選用Paging3,個(gè)人感覺非常搭配Jetpack Compose

  1. 視頻播放器的選擇:

播放器可以選用大名鼎鼎的 ijkplayer跟衅,我就用ExoPlayer,自己貼了個(gè)controller_view上去孵睬。

  1. 橫豎屏切換:

同一個(gè)PlayerView,全屏的時(shí)候 ,先從列表item中remove(),然后addView()給R.id.content ,豎屏反過來操作伶跷。

二掰读、分頁與網(wǎng)絡(luò)請求:

  1. 實(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
        }
    }
    
  1. 定義列表接口:

    interface VideoListService {
    
        @GET("api/v4/discovery/hot")
        suspend fun getVideoList(
            @Query("start") itemStart: Int = 1,
            @Query("num") pageSize: Int = 6
        ): VideoStore
    }
    
  2. 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)
            }
        }
    }
    
  3. 數(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)
}
  1. 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{

      貼一張視頻封面占位
}
  1. 卡片上面的文字和封面部分:
@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()
                )
            }
        }
    }
}
  1. 播放器部分胞谈,需要在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
                }
            }
        }
    }
}
  1. 那么傳統(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()
                               )
                           }
                       }
                   })
               }
            )
    }
    
  1. 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播放器:

  1. 播放器創(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
            }
    }
}
  1. 實(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
    }
}
  1. 緩存設(shè)置與緩存策略:

    114031.jpg
/**
 * 緩存基本設(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視頻播放

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市举畸,隨后出現(xiàn)的幾起案子埋嵌,更是在濱河造成了極大的恐慌,老刑警劉巖俱恶,帶你破解...
    沈念sama閱讀 206,602評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件雹嗦,死亡現(xiàn)場離奇詭異,居然都是意外死亡合是,警方通過查閱死者的電腦和手機(jī)了罪,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,442評論 2 382
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來聪全,“玉大人泊藕,你說我怎么就攤上這事∧牙瘢” “怎么了娃圆?”我有些...
    開封第一講書人閱讀 152,878評論 0 344
  • 文/不壞的土叔 我叫張陵玫锋,是天一觀的道長。 經(jīng)常有香客問我讼呢,道長撩鹿,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 55,306評論 1 279
  • 正文 為了忘掉前任悦屏,我火速辦了婚禮节沦,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘础爬。我一直安慰自己甫贯,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,330評論 5 373
  • 文/花漫 我一把揭開白布看蚜。 她就那樣靜靜地躺著叫搁,像睡著了一般。 火紅的嫁衣襯著肌膚如雪供炎。 梳的紋絲不亂的頭發(fā)上常熙,一...
    開封第一講書人閱讀 49,071評論 1 285
  • 那天,我揣著相機(jī)與錄音碱茁,去河邊找鬼裸卫。 笑死,一個(gè)胖子當(dāng)著我的面吹牛纽竣,可吹牛的內(nèi)容都是我干的墓贿。 我是一名探鬼主播,決...
    沈念sama閱讀 38,382評論 3 400
  • 文/蒼蘭香墨 我猛地睜開眼蜓氨,長吁一口氣:“原來是場噩夢啊……” “哼聋袋!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起穴吹,我...
    開封第一講書人閱讀 37,006評論 0 259
  • 序言:老撾萬榮一對情侶失蹤幽勒,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后港令,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體啥容,經(jīng)...
    沈念sama閱讀 43,512評論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 35,965評論 2 325
  • 正文 我和宋清朗相戀三年顷霹,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了咪惠。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 38,094評論 1 333
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡淋淀,死狀恐怖遥昧,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤炭臭,帶...
    沈念sama閱讀 33,732評論 4 323
  • 正文 年R本政府宣布永脓,位于F島的核電站,受9級特大地震影響鞋仍,放射性物質(zhì)發(fā)生泄漏常摧。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,283評論 3 307
  • 文/蒙蒙 一凿试、第九天 我趴在偏房一處隱蔽的房頂上張望排宰。 院中可真熱鬧似芝,春花似錦那婉、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,286評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至寞奸,卻和暖如春呛谜,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背枪萄。 一陣腳步聲響...
    開封第一講書人閱讀 31,512評論 1 262
  • 我被黑心中介騙來泰國打工隐岛, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人瓷翻。 一個(gè)月前我還...
    沈念sama閱讀 45,536評論 2 354
  • 正文 我出身青樓聚凹,卻偏偏與公主長得像,于是被迫代替她去往敵國和親齐帚。 傳聞我的和親對象是個(gè)殘疾皇子妒牙,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,828評論 2 345

推薦閱讀更多精彩內(nèi)容