Skip to content

liolok/ExoPlayer-Sample

master
Switch branches/tags

Name already in use

A tag already exists with the provided branch name. Many Git commands accept both tag and branch names, so creating this branch may cause unexpected behavior. Are you sure you want to create this branch?
Code

Latest commit

 

Git stats

Files

Permalink
Failed to load latest commit information.
Type
Name
Latest commit message
Commit time
 
 
app
 
 
 
 
 
 
 
 
 
 
 
 
 
 

安卓应用 - 视频播放器简单示例

首屏 | Splash Activity

展示 C++ Logo 及应用名称,延时两秒自动淡出至主页面。

首屏

核心代码

Handler().postDelayed({
    startActivity(Intent(this, MainActivity::class.java))
    finish(); overridePendingTransition(anim.fade_in, anim.fade_out)  // use fade animation
}, DELAY_MILLIS)  // jump to MainActivity after delay

完整代码详见 SplashActivity.kt

主页面 | Main Activity

适配两种布局,单窗格(默认)和双窗格(Dual Pane),使用布局文件的限定符 sw600dp(最小宽度为 600dp 的屏幕)及 Fragment 实现。

单窗格

主页面

双窗格

主页面 - 双窗格

双窗格横屏

主页面 - 双窗格横屏

核心代码

判断当前布局类型:

// Determine current layout: https://developer.android.com/training/multiscreen/adaptui#TaskDetermineCurLayout
val fragmentContainer: View? = findViewById(R.id.fragmentContainer)
isDualPane = fragmentContainer?.visibility == View.VISIBLE

双窗格布局下,使用列表首个条目添加默认 Fragment:

if (isDualPane) {
    // However, if we're being restored from a previous state,
    // then we don't need to do anything and should return or else
    // we could end up with overlapping fragments.
    if (savedInstanceState != null) return

    val defaultFragment = PlayerFragment()  // create a new Fragment to be placed in the activity layout
    val bundle = Bundle(); bundle.putParcelable(Item.EXTRA_KEY, items[0])  // pack first item up with a bundle
    defaultFragment.arguments = bundle  // pass first item to default fragment

    // Add the fragment to the 'fragmentContainer' FrameLayout
    supportFragmentManager.beginTransaction().add(R.id.fragmentContainer, defaultFragment).commit()
}

针对两种不同布局,对列表条目的点击进行不同的操作。双窗格布局下替换 Fragment,单窗格布局下启动 DetailActivity(详情页面):

override fun onItemClick(item: Item) {
    if (isDualPane) {
        val newFragment = PlayerFragment()
        val bundle = Bundle(); bundle.putParcelable(Item.EXTRA_KEY, item)  // pack the item up with a bundle
        newFragment.arguments = bundle  // pass first item to first fragment
        supportFragmentManager.beginTransaction().replace(R.id.fragmentContainer, newFragment).commit()
    }  // layout is at sw600dp variation, replace fragment on list item clicked.
    else {
        startActivity(Intent(this, DetailActivity::class.java).putExtra(Item.EXTRA_KEY, item))
    }  // layout is at normal variation, start detail activity on list item clicked.
}  // implement RecyclerAdapter.OnItemClickListener

以上片段完整代码详见 MainActivity.kt

两种布局公共部分为视频列表,列表使用 RecyclerView 实现,条目使用 CardView 实现,两者的绑定代码如下:

// Provide a reference to the views for each item
class ItemViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
    fun bind(item: Item, listener: OnItemClickListener) = with(itemView) {
        title.text = item.title
        duration.text = item.duration
        setOnClickListener { listener.onItemClick(item) }
    }  // bind an item object to its view
}

完整代码详见 RecyclerAdapter.kt

详情 | Detail Activity

详情

视频播放核心代码移至 PlayerFragment 以在不同布局下复用,在 DetailActivity 中仅需将 MainActivity 的传入数据再次传递给 PlayerFragment 并添加即可:

核心代码

val playerFragment = PlayerFragment()  // create a player fragment

val item = intent.getParcelableExtra<Item>(Item.EXTRA_KEY)  // get item object from MainActivity
supportActionBar?.title = item.title  // set actionbar title with item's
val bundle = Bundle(); bundle.putParcelable(Item.EXTRA_KEY, item)  // pack item object up with a bundle
playerFragment.arguments = bundle  // pass item object to player fragment through bundle

supportFragmentManager.beginTransaction().add(R.id.detailContainer, playerFragment).commit()  // show fragment

完整代码详见 DetailActivity.kt

视频播放

使用 ExoPlayer 库简单实现,支持缓冲就绪自动播放:

核心代码

class PlayerFragment : Fragment() {
    private lateinit var player: SimpleExoPlayer

    override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
        if (container == null) return null  // create and return view only in dual pane mode

        val playerView = PlayerView(activity)

        // https://exoplayer.dev/hello-world.html
        player = ExoPlayerFactory.newSimpleInstance(activity)  // initialize player instance
        playerView.player = player  // bind the player to the view
        val item = arguments?.getParcelable<Item>(Item.EXTRA_KEY)  // get item object from list activity
        player.apply {
            playWhenReady = true  // whether auto-play video when ready
            prepare(
                ProgressiveMediaSource.Factory(DefaultHttpDataSourceFactory(getString(R.string.app_name)))
                    .createMediaSource(Uri.parse(item?.url))
            )  // prepare the player with `ProgressiveMediaSource`, which is for regular media files like "*.mp4".
        }

        return playerView
    }

    override fun onDestroy() {
        super.onDestroy()
        player.release()  // release the player when fragment is destroyed, or it will still play in background.
    }
}

完整代码详见 PlayerFragment.kt

数据字典

@Parcelize  // for intent extra, see more in RecyclerAdapter.ItemViewHolder.onClick() and DetailActivity.onCreate()
@Serializable  // for reading list from json resource file, see more in MainActivity.onCreate()
data class Item(val title: String, val duration: String, val url: String) : Parcelable {
    companion object {
        const val EXTRA_KEY = "${BuildConfig.APPLICATION_ID}.Item"
    }  // intent extra key, use app's package name as prefix to be unique
}

初始数据存储在 res/raw/items.json,在 MainActivity 中使用 Kotlinx Serialization 解析并传递给 RecyclerAdapter

心得体会

  • 视频播放:ExoPlayer 作为一个半官方库,可以轻松胜任简单的播放需求,同时扩展性也很强,可实现更多功能,如记住播放位置等;
  • 适应性布局:Android 官方提供了一个较为人性化的 UI 设计方式,实现一个简单的适应性布局并不算太难。

参考资料

About

安卓应用 - 视频播放器简单示例

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Languages