安卓给app提供了:

MediaPlayer:播放视频或音频功能,详见谷歌MediaPlayer文档

借助MediaPlayer,我们可以轻松的实现一个简单的播放器app。一般来说app显示内容放在Activity中。但是试想一般的播放器要求app进入后台后可以继续播放声音,app回到前台后可以继续播放视频。因此其实MediaPlayer更适合放在Service中来播放视频,而我们的Activity仅显示视频即可。接下来我们依次实现MediaService和MediaActivity。

首先实现我们的核心类MediaService:

class MediaService : Service() {val mMediaPlayer = MediaPlayer()  // 播放媒体文件的对象val mMediaBinder = MediaBinder()  // 传给Activity,使得Activity可以与我们这个Service通信var mIsForeground = false  // 为true时变成前台服务,为false时停止前台服务成为一般的后台服务var mMediaListener: MediaListener? = null  // mMediaPlayer的相关回调var mTimer: Timer? = null  // 用来定时调用mMediaListener.onProgress回调方法的定时器var mTimerTask = object : TimerTask() {  // 定时调用mMediaListener.onProgress回调方法的任务override fun run() {mMediaListener?.onProgress(mMediaPlayer.currentPosition, mMediaPlayer.duration)}}// 此类的方法都是从Activity调用的inner class MediaBinder : Binder() {// 设置显示视频的surfaceHolderfun setDisplay(surfaceHolder: SurfaceHolder?) {mMediaPlayer.setDisplay(surfaceHolder)}// 设置一个额外的媒体播放完毕时回调fun setMediaListener(mediaListener: MediaListener?) {mMediaListener = mediaListenermMediaPlayer.setOnVideoSizeChangedListener(mMediaListener)if (mMediaListener == null) {mTimer?.cancel()mTimer = null} else if (mTimer == null) {mTimer = Timer().apply { schedule(mTimerTask, 0, 100) }}}// 打开媒体文件fun open(uri: Uri) {mMediaPlayer.reset()mMediaPlayer.setDataSource(this@MediaService, uri)mMediaPlayer.prepareAsync()}// 继续或暂停播放媒体,返回true代表继续播放了,返回false代表暂停播放了fun playOrPause(): Boolean {if (mMediaPlayer.isPlaying) mMediaPlayer.pause()else mMediaPlayer.start()updateServiceState()return mMediaPlayer.isPlaying}// 停止播放媒体fun stop() {mMediaPlayer.stop()updateServiceState()}// 调整播放进度fun seekTo(millisecond: Int) {mMediaPlayer.seekTo(millisecond)}}override fun onBind(intent: Intent): IBinder {return mMediaBinder}override fun onCreate() {super.onCreate()mMediaPlayer.setOnPreparedListener {mMediaPlayer.start()updateServiceState()}mMediaPlayer.setOnCompletionListener {mMediaListener?.onCompletion(it)updateServiceState()}mMediaPlayer.setOnErrorListener { mp, what, extra ->Log.e(TAG, "MediaPlayer error! mediaPlayer=$mp, what=$what, extra=$extra")updateServiceState()true}}// 如果正在播放媒体文件,则变成前台服务,否则变成一般的后台服务private fun updateServiceState() {if (mMediaPlayer.isPlaying != mIsForeground) {mIsForeground = !mIsForegroundif (mIsForeground) {startForegroundNotification()} else {stopForeground(true)}}}// 创建notification,变成前台服务private fun startForegroundNotification() {val notificationManager = getSystemService(NOTIFICATION_SERVICE) as NotificationManagerval channel = NotificationChannel("Media", "Media", NotificationManager.IMPORTANCE_HIGH)notificationManager.createNotificationChannel(channel)val intent = Intent(this, MediaActivity::class.java)val pendingIntent = PendingIntent.getActivity(this, 0, intent, PendingIntent.FLAG_IMMUTABLE)val notification = Notification.Builder(this, "Media").setSmallIcon(R.drawable.ic_media).setContentTitle("Media").setContentText("Playing...").setContentIntent(pendingIntent).build()startForeground(123, notification)}override fun onDestroy() {super.onDestroy()mMediaPlayer.release()}companion object {private val TAG = MediaService::class.simpleName!!}
}

MediaService可以在播放视频时成为前台Service,这样保证我们的MediaService播放视频时不会被突然杀掉,并且也创建了相应的通知,用户只要点击通知即可进入MediaActivity观看视频。
在MediaService中还有MediaBinder,其提供了一些方法供MediaActivity那边调用。

接下来实现我们的布局文件activity_media.xml:

<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"xmlns:tools="http://schemas.android.com/tools"android:id="@+id/surface_container"android:layout_width="match_parent"android:layout_height="match_parent"tools:context="com.sc.media.MediaActivity"><SurfaceViewandroid:id="@+id/surface_view"android:layout_width="match_parent"android:layout_height="match_parent"android:layout_gravity="center" /><RelativeLayoutandroid:id="@+id/media_control"android:layout_width="match_parent"android:layout_height="84dp"android:layout_gravity="bottom"><RelativeLayoutandroid:layout_width="match_parent"android:layout_height="60dp"android:layout_alignParentBottom="true"android:paddingVertical="10dp"android:background="#80000000"><ImageButtonandroid:id="@+id/open_media_file_button"android:layout_width="wrap_content"android:layout_height="wrap_content"android:layout_toStartOf="@id/play_pause_media_button"android:src="@drawable/ic_file"android:scaleType="fitCenter"android:background="@android:color/transparent"android:contentDescription="@string/open_media_file" /><ImageButtonandroid:id="@+id/play_pause_media_button"android:layout_width="wrap_content"android:layout_height="wrap_content"android:layout_centerInParent="true"android:src="@drawable/ic_play_white"android:scaleType="fitCenter"android:background="@android:color/transparent"android:contentDescription="@string/play_or_pause_media" /><ImageButtonandroid:id="@+id/stop_media_button"android:layout_width="wrap_content"android:layout_height="wrap_content"android:layout_toEndOf="@id/play_pause_media_button"android:src="@drawable/ic_stop"android:scaleType="fitCenter"android:background="@android:color/transparent"android:contentDescription="@string/stop_play_media" /></RelativeLayout><com.google.android.material.slider.Sliderandroid:id="@+id/slider"android:layout_width="match_parent"android:layout_height="wrap_content"android:layout_alignParentTop="true"android:background="@android:color/transparent"android:stepSize="1.0"android:valueFrom="0.0"android:valueTo="1.0"android:value="0.0" /></RelativeLayout></FrameLayout>

我们主要是使用了一个SurfaceView来显示视频,并且使用ImageButton实现我们的打开媒体文件、暂停/播放和停止按钮,还使用Slider实现播放进度条的显示与控制。

最后来实现我们的MediaActivity:

class MediaActivity : AppCompatActivity() {lateinit var mSurfaceContainer: FrameLayout  // SurfaceView的全屏父Viewlateinit var mSurfaceView: SurfaceView  // 用来播放视频的SurfaceViewlateinit var mMediaControlView: View  // 媒体播放工具栏lateinit var mPlayPauseButton: ImageButton  // 控制媒体播放暂停的按钮lateinit var mSlider: Slider  // 媒体播放进度条lateinit var mMediaBinder: MediaService.MediaBinder  // 用来与MediaService通信的Binder对象var mVideoRatio = 0f  // 视频的宽高比var mSliderTrackingTouch = false  // mSlider是否正在被拖val mMediaListener = object : MediaListener {// 视频尺寸改变时调用override fun onVideoSizeChanged(mp: MediaPlayer?, width: Int, height: Int) {mVideoRatio = if (height == 0) 0f else width.toFloat() / height.toFloat()updateSurfaceSize()}// 媒体播放完毕时调用override fun onCompletion(mp: MediaPlayer?) {mPlayPauseButton.setImageResource(R.drawable.ic_play_white)}// 播放进度改变时调用override fun onProgress(position: Int, duration: Int) {if (mSliderTrackingTouch) returnmSlider.valueTo = duration.toFloat().coerceAtLeast(1f)mSlider.value = position.toFloat().coerceAtLeast(mSlider.valueFrom).coerceAtMost(mSlider.valueTo)}}// 连接MediaService的对象val mServiceConnection = object : ServiceConnection {override fun onServiceConnected(name: ComponentName, service: IBinder) {mMediaBinder = service as MediaService.MediaBindermMediaBinder.setMediaListener(mMediaListener)mSurfaceView.holder.addCallback(object : SurfaceHolder.Callback {override fun surfaceCreated(holder: SurfaceHolder) {mMediaBinder.setDisplay(holder)}override fun surfaceChanged(holder: SurfaceHolder, format: Int, width: Int, height: Int) {Log.d(TAG, "Surface changed! width=$width, height=$height")}override fun surfaceDestroyed(holder: SurfaceHolder) {mMediaBinder.setDisplay(null)}})}override fun onServiceDisconnected(name: ComponentName?) { }}......override fun onCreate(savedInstanceState: Bundle?) {super.onCreate(savedInstanceState)setContentView(R.layout.activity_media)// 隐藏SystemUI,包括导航栏、状态栏等WindowCompat.setDecorFitsSystemWindows(window, false)WindowInsetsControllerCompat(window, window.decorView).apply {hide(WindowInsetsCompat.Type.systemBars())systemBarsBehavior = WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE}mSurfaceContainer = findViewById(R.id.surface_container)mSurfaceView = findViewById(R.id.surface_view)mMediaControlView = findViewById(R.id.media_control)mPlayPauseButton = findViewById(R.id.play_pause_media_button)mSlider = findViewById(R.id.slider)// 保证转屏时可以正确更新SurfaceView的尺寸mSurfaceView.viewTreeObserver.addOnGlobalLayoutListener { updateSurfaceSize() }// 连接绑定MediaServiceval bindIntent = Intent(this, MediaService::class.java)bindService(bindIntent, mServiceConnection, BIND_AUTO_CREATE)// 打开媒体文件按钮findViewById<ImageButton>(R.id.open_media_file_button).setOnClickListener {Intent(Intent.ACTION_GET_CONTENT).apply {type = "*/*"addCategory(Intent.CATEGORY_OPENABLE)startActivityForResult(this, 0)}}// 播放/暂停按钮mPlayPauseButton.setOnClickListener {if (mMediaBinder.playOrPause()) mPlayPauseButton.setImageResource(R.drawable.ic_pause_white)else mPlayPauseButton.setImageResource(R.drawable.ic_play_white)}// 停止按钮findViewById<ImageButton>(R.id.stop_media_button).setOnClickListener {mMediaBinder.stop()mPlayPauseButton.setImageResource(R.drawable.ic_play_white)}// 按住进度条时,让进度条显示格式为(分:秒.毫秒)的时间mSlider.setLabelFormatter { value: Float ->val millis = value.toInt()val seconds = millis / 1000val minutes = seconds / 60"$minutes:${seconds % 60}.${millis % 1000}"}// 更新mSliderTrackingTouch,使得我们通过这个变量可以得知当前进度条是否正在被拖mSlider.addOnSliderTouchListener(object : Slider.OnSliderTouchListener {override fun onStartTrackingTouch(slider: Slider) { mSliderTrackingTouch = true }override fun onStopTrackingTouch(slider: Slider) { mSliderTrackingTouch = false }})// 通过进度条调整播放进度mSlider.addOnChangeListener { slider, value, fromUser ->if (mSliderTrackingTouch and fromUser) {mMediaBinder.seekTo(value.toInt())}}}// 得到打开媒体文件结果override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {super.onActivityResult(requestCode, resultCode, data)val uri: Uri = data?.data ?: returnmMediaBinder.open(uri)  // 要MediaService开始播放媒体文件mPlayPauseButton.setImageResource(R.drawable.ic_pause_white)}// 根据视频宽高比mVideoRatio和mSurfaceContainer的尺寸更新mSurfaceView的尺寸private fun updateSurfaceSize() {if (mVideoRatio <= 0) returnval playerRatio = mSurfaceContainer.width.toFloat() / mSurfaceContainer.height.toFloat()val params = mSurfaceView.layoutParamsif (playerRatio > mVideoRatio) {params.width = (mSurfaceContainer.height.toFloat() * mVideoRatio).toInt()params.height = mSurfaceContainer.height} else if (playerRatio < mVideoRatio) {params.width = mSurfaceContainer.widthparams.height = (mSurfaceContainer.width.toFloat() / mVideoRatio).toInt()} else {params.width = mSurfaceContainer.widthparams.height = mSurfaceContainer.height}mSurfaceView.layoutParams = params}override fun onDestroy() {super.onDestroy()mMediaBinder.setMediaListener(null)unbindService(mServiceConnection)}companion object {private val TAG = MediaActivity::class.simpleName!!fun actionStart(context: Context) {val intent = Intent(context, MediaActivity::class.java)context.startActivity(intent)}}
}

在MediaActivity中,我们在onCreate中首先使用WindowInsetsControllerCompat类隐藏导航栏和状态栏,防止这些系统窗口影响我们看视频的体验。然后调用bindService方法与我们的MediaService建立连接。最后设置打开媒体文件、播放/暂停等按钮设置按键监听器,实现这些按键的功能。

源代码地址:https://github.com/SSSxCCC/SCApp

总结

最好在前台服务中播放视频,这样不管我们的Activity是什么状态都可以继续播放视频。

安卓实现播放器app相关推荐

  1. 安卓音乐播放器app开发(一)---功能分析及启动页的制作

    音乐播放器app-功能分析及启动页的制作 现如今的音乐播放器的app种类繁多,让有选择困难症的同胞们难以抉择.现在,让Ryan带你打造一款属于自己的音乐播放器app. 功能介绍 实现本地音乐及在线音乐 ...

  2. 毕业设计 安卓音乐播放器APP

    文章目录 0 项目说明 1 模块设计架构 2 界面效果 3 项目源码 4 最后 0 项目说明 基于安卓APP的音乐播放器设计 提示:适合用于课程设计或毕业设计,工作量达标,源码开放 1 模块设计架构 ...

  3. 【翻译】安卓新播放器EXOplayer介绍

    [翻译]安卓新播放器EXOplayer介绍 http://developer.android.com/guide/topics/media/exoplayer.html 前言: Playing vid ...

  4. sony android mp3播放器,高音质与流媒体兼具,索尼 NW-ZX500 安卓音乐播放器评测

    原标题:高音质与流媒体兼具,索尼 NW-ZX500 安卓音乐播放器评测 用传统的音乐播放器来听歌,似乎已经逐渐变成了一个相对小众的需求. 在我眼里,曾被称呼为「随身听」的音乐播放器,已经逐渐被归类为如 ...

  5. 在线音乐播放器app

    在线音乐播放器app 前言 该app是安卓课程的大作业,旷了一学期的课,代码有点乱. 使用的API:网易云音乐 API 代码地址:https://github.com/xjhqre/music-pla ...

  6. 设计灵感|音乐播放器App界面如何设计?

    音乐播放器 App 界面要怎么设计?来看看集设网精选的 12 款移动端音乐播放器,学习一下如何设计出一个易用性和交互性良好.设计感受舒适.展示层级清晰的界面. 音乐播放器App界面如何设计? - 集设 ...

  7. 音乐播放器App界面优秀案例,通过案例看大咖如何设计?

    音乐播放器 App 界面要怎么设计?集设网 www.ijishe.ccom精选的 12 款移动端音乐播放器,学习一下如何设计出一个易用性和交互性良好.设计感受舒适.展示层级清晰的界面. 看这里

  8. Android 12.0 系统多个播放器app时,设置默认播放器

    目录 1.概述 2.系统多个播放器app时,设置默认播放器的核心类

  9. Android项目实践(四)——音乐播放器APP

    关于Android制作音乐播放器APP的几点建议 1.权限获得 1.在AndroidManifest.xml文件中,做如下声明: <uses-permission android:name=&q ...

  10. 【Android】音乐播放器APP的设计与实现

    [Android]音乐播放器APP的设计与实现 一.界面设计 二.核心代码 一.界面设计 (1)注册登录 (2)主界面 (3)音乐播放器 可以实现开始,暂停,下一首,上一首功能:滑动进度条可以改变音乐 ...

最新文章

  1. Go 分布式学习利器(11)-- Go语言通过单链表 实现队列
  2. ACMNO.11 一个数如果恰好等于它的因子之和,这个数就称为“完数“。 例如,6的因子为1、2、3,而6=1+2+3,因此6是“完数“。 编程序找出N之内的所有完数,并按下面格式输出其因子
  3. LeetCode01_二分法专题
  4. [MOSS开发]:WSS v3授权
  5. java8 stream遍历_Java8新特性:Stream流详解
  6. java 安装报错2503_Windows安装Node.js报错:2503、2502的解决方法
  7. es scroll 时间_游标查询 Scroll | Elasticsearch: 权威指南 | Elastic
  8. rubymine 保存成unix格式_如何免费在线试用 200+ Linux 和 Unix 发行版?
  9. (11)FPGA复位设计原则
  10. java 同步异步_Java中的同步于异步
  11. 数据可视化图表ECharts
  12. ICESat 数据介绍
  13. rbw数字信号处理_基于FPGA的数字中频信号处理的设计与实现
  14. C语言运算符优先级列表(超详细)
  15. matlab求系统根轨迹代码_第九讲? 根轨迹法
  16. 7-10 抢楼层 (20分) ---注意歧义啊!
  17. php背景四周向中间渐变色,使用CSS巧妙地制作背景色渐变动画实例
  18. 2013-1-20·
  19. 在本机搭建FTP服务器
  20. RenderTexture实现小地图和炫酷的传送门!(干货收藏)

热门文章

  1. 计算机识别不到硬盘,电脑检测不到硬盘怎么办,怎么修复硬盘问题
  2. [置顶] 关于Android图形系统的一些事实真相
  3. QVTKWidget控件显示三维图片
  4. linux俄罗斯方块游戏
  5. 运维监控之——云原生运维监控报警架构(prometheus+grafana+netdata+Thanos+Alertmanager+Consul)
  6. 【图解新个税】2019年1月1日起,个税专项附加扣除要这么扣
  7. 数据结构-树-愿天下有情人都是失散多年的兄妹
  8. 服务器常见高可用方案
  9. 数值分析原理课程实验——牛顿(Newton)迭代法
  10. java转Js原生,Java到JavaScript的转换