安卓应用 - 视频播放器简单示例
首屏 | 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 设计方式,实现一个简单的适应性布局并不算太难。
参考资料
- ExoPlayer
- 适应性布局