文章目录
- 前言
- 一、先来张效果图
- 二、使用步骤
- 1.配置清单文件
- 2.编写 Service
- 3. Activity
- 4.请求权限
- 5.浮窗的页面贴一下
- 三、画中画
- 总结
前言
本篇以简单的浮窗视频为例, 练习 Service, 浮窗, MediaPlayer视频播放等;
本篇涉及内容:
Service 的基本用法;
MediaPlayer 播放本地视频
通过 WindowManager 添加浮窗
Android Result API 自定义协议类, 校验浮窗权限
一、先来张效果图
二、使用步骤
1.配置清单文件
<!-- SD卡读写权限 -->
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<!-- 8.0 以上 前台服务权限 -->
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<!-- 悬浮窗权限 -->
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />
<application
...
<!-- 读取本地文件需要 -->
android:requestLegacyExternalStorage="true" >
<!-- 自定义的视频浮窗 Service -->
<service android:name=".test.textservice.VideoFloatingService"/>
2.编写 Service
这次的 Service
并没有 上一篇 简单的音乐播放器 中的 Service
复杂;
只需要初始化 MediaPlayer
, 播放视频. 添加悬浮窗. 以及内部简单的控制逻辑;
class VideoFloatingService: Service() {
private val TAG = "VideoFloatingService"
private lateinit var windowManager: WindowManager
private lateinit var layoutParams: WindowManager.LayoutParams
private lateinit var binding: ViewVideoFloatingBinding
private lateinit var mediaPlayer: MediaPlayer
override fun onCreate() {
super.onCreate()
init() // 初始化播放器
showFloatingWindow() // 添加浮窗
}
override fun onBind(intent: Intent?): IBinder? = null
private fun showFloatingWindow() {
windowManager = getSystemService(WINDOW_SERVICE) as WindowManager
// 初始化浮窗 layoutParams
layoutParams = WindowManager.LayoutParams().also {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
it.type = WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY
} else {
it.type = WindowManager.LayoutParams.TYPE_PHONE
}
it.format = PixelFormat.RGBA_8888
it.flags = WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL or
WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
// 浮窗位置和尺寸, 偏移量等
it.gravity = Gravity.CENTER_VERTICAL or Gravity.START
it.width = WindowManager.LayoutParams.WRAP_CONTENT
it.height = WindowManager.LayoutParams.WRAP_CONTENT
it.x = 0
it.y = 0
}
// 初始化浮窗布局, 为播放按钮添加事件; 为整个浮窗添加触摸监听(满足拖动, 点击等)
binding = ViewVideoFloatingBinding.inflate(LayoutInflater.from(this))
binding.ivPlay.setOnClickListener {
binding.ivPlay.setImageResource(if(mediaPlayer.isPlaying){
mediaPlayer.pause()
R.mipmap.video_icon_play
}else{
mediaPlayer.start()
R.mipmap.video_icon_suspend
})
}
// 添加拖拽事件
binding.root.setOnTouchListener(FloatingListener())
// 要等 SurfaceHolder Created
binding.svVideo.holder.addCallback(object : SurfaceHolder.Callback {
override fun surfaceCreated(holder: SurfaceHolder) {
mediaPlayer.setDisplay(binding.svVideo.holder)
}
override fun surfaceChanged(s: SurfaceHolder, f: Int, w: Int, h: Int ) {}
override fun surfaceDestroyed(holder: SurfaceHolder) {}
})
// 添加浮窗
windowManager.addView(binding.root, layoutParams)
}
private fun init() {
mediaPlayer = MediaPlayer().also {
it.isLooping = true // 循环播放
val file = File(Environment.getExternalStorageDirectory(),"big_buck_bunny.mp4")
it.setDataSource(file.path)
it.prepareAsync()
it.setOnPreparedListener {
mediaPlayer.start()
}
}
}
override fun onDestroy() {
mediaPlayer.stop()
mediaPlayer.release()
windowManager.removeView(binding.root)
super.onDestroy()
}
inner class FloatingListener: View.OnTouchListener{
private var x = 0 // 当前位置值
private var y = 0
private var cx = 0 // 点击初始值;
private var cy = 0
private var checkClick: Boolean = false
@SuppressLint("ClickableViewAccessibility")
override fun onTouch(v: View?, event: MotionEvent?): Boolean {
when(event?.action){
MotionEvent.ACTION_DOWN -> {
checkClick = true
cx = event.rawX.toInt()
cy = event.rawY.toInt()
x = cx
y = cy
}
MotionEvent.ACTION_MOVE -> {
val nowX = event.rawX.toInt()
val nowY = event.rawY.toInt()
val movedX = nowX - x
val movedY = nowY - y
x = nowX
y = nowY
layoutParams.let {
it.x = it.x + movedX
it.y = it.y + movedY
}
// 更新悬浮窗控件布局
windowManager.updateViewLayout(binding.root, layoutParams)
if (checkClick && (abs(x - cx) >= 5 || abs(y - cy) >= 5)) {
checkClick = false
}
}
MotionEvent.ACTION_CANCEL, MotionEvent.ACTION_UP -> {
if (checkClick && abs(cx - event.rawX.toInt()) < 5 && abs(cy - event.rawY.toInt()) < 5) {
// 判断为 浮窗点击事件; 控制播放按钮显隐;
if(binding.ivPlay.visibility == View.VISIBLE){
binding.ivPlay.visibility = View.GONE
}else{
binding.ivPlay.visibility = View.VISIBLE
binding.ivPlay.setImageResource(if(mediaPlayer.isPlaying){
R.mipmap.video_icon_suspend
} else {
R.mipmap.video_icon_play
})
}
}
checkClick = false
}
}
return true
}
}
}
代码看起来不少, 但逻辑却很简单; 总体来看只有 初始化播放器, 添加浮窗, 添加触摸监听 等;
3. Activity
Activity 就很简单了, 先校验或请求一下权限. 然后直接启动 Service 就完事了;
// 点击事件
fun onClick(v: View) {
when(v.id){
R.id.tv_one -> {
startFloatingWindow() // 检验权限后 开启Service
}
R.id.tv_two -> {
stopService(Intent(this, VideoFloatingService::class.java))
}
}
}
private fun startFloatingWindow() {
// Android Result Api 的方式, 请求权限
launcher2.launch(null)
}
private val launcher2 = registerForActivityResult(RequestFloating(this)) {
if(it){
// toast("") 这是 kotlin 自定义的扩展函数.
toast("有权限")
// 有了权限, 直接启动 Service 即可
startService(Intent(this, VideoFloatingService::class.java))
}else{
toast("授权失败")
}
}
4.请求权限
这是 俺自定义的 协议类; 有权限时 同步响应回调; 没有权限, 则跳转开启权限的页面;
class RequestFloating(val context: Context): ActivityResultContract<Unit, Boolean>(){
@RequiresApi(Build.VERSION_CODES.M)
override fun createIntent(context: Context, input: Unit?) = Intent().also {
it.action = Settings.ACTION_MANAGE_OVERLAY_PERMISSION
it.data = Uri.parse("package:${context.packageName}")
}
override fun getSynchronousResult(context: Context, input: Unit?): SynchronousResult<Boolean>? {
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && !Settings.canDrawOverlays(context)) {
null
} else {
SynchronousResult(true)
}
}
override fun parseResult(resultCode: Int, intent: Intent?): Boolean{
return Build.VERSION.SDK_INT < Build.VERSION_CODES.M || Settings.canDrawOverlays(context)
}
}
不了解 Android Result Api
的小伙伴可以看我这篇文章: Android Result API
当然也可以用下面这段代码:
//检查悬浮窗权限,小于6.0系统不需要权限
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && !Settings.canDrawOverlays(this)) {
Intent intent = new Intent(Settings.ACTION_MANAGE_OVERLAY_PERMISSION);
intent.setData(Uri.parse("package:" + getPackageName()));
startActivityForResult(intent, 100);
}else{
//已有权限
}
这种. 还得重写 onActivityResult; 但是 onActivityResult 中并没有结果, 需要自己再判断一遍
5.浮窗的页面贴一下
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content">
<SurfaceView
android:id="@+id/sv_video"
android:layout_width="180dp"
android:layout_height="108dp"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintStart_toStartOf="parent"/>
<ImageView
android:id="@+id/iv_play"
style="@style/img_wrap"
android:layout_width="30dp"
android:layout_height="30dp"
android:src="@mipmap/video_icon_play"
android:visibility="gone"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"/>
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>
Activity 的页面就不贴了; 就俩按钮.
至此, 简单的视频浮窗播放 Service 完成 😀
三、画中画
博主本想在下一篇文章中 用画中画实现视频浮窗, 但发现写不出什么东西.
这里干脆只把关键代码帖一帖吧. 就当笔记了.
推荐文章:
官方文档 - 画中画支持
1.清单文件配置
<!-- android:supportsPictureInPicture="true" 是关键代码 - 开启画中画支持 -->
<activity android:name=".test.textservice.VideoDetailActivity"
android:launchMode="singleTask"
android:resizeableActivity="true"
android:supportsPictureInPicture="true"
android:configChanges=
"screenSize|smallestScreenSize|screenLayout|orientation"/>
2.进入画中画模式
/**
*进入画中画模式
*/
private var mPictureInPictureParamsBuilder: PictureInPictureParams.Builder? = null
private fun enterPiPMode() {
//
// 低内存设备可能无法使用画中画模式。 进行检查以确保可以使用画中画。
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O &&
packageManager.hasSystemFeature(PackageManager.FEATURE_PICTURE_IN_PICTURE)) {
if (mPictureInPictureParamsBuilder == null) {
mPictureInPictureParamsBuilder = PictureInPictureParams.Builder()
}
// 这是设置窗口宽高比例; 纵横比不能太极端(必须在0.418410和2.390000之间)。
val aspectRatio = Rational(binding.svVideo.width, binding.svVideo.height)
mPictureInPictureParamsBuilder?.setAspectRatio(aspectRatio)
//进入pip模式
enterPictureInPictureMode(mPictureInPictureParamsBuilder!!.build())
}
}
3.在Activity中监听 PIP模式的切换
override fun onPictureInPictureModeChanged(
isInPictureInPictureMode: Boolean,
newConfig: Configuration?
) {
super.onPictureInPictureModeChanged(isInPictureInPictureMode, newConfig)
if (isInPictureInPictureMode) {
// Hide the full-screen UI (controls, etc.) while in picture-in-picture mode.
// 隐藏多余 View ...
} else {
// Restore the full-screen UI.
// 恢复隐藏的 View ...
}
}
需要注意, 启动画中画模式时, Activity 生命周期是处于 paused 状态. 所以控制食品播放和暂停 可以放在 onStart() onStop() 中
画中画模式中, 事件是传不到 Activity 中的, 所以想要自定义窗口操作的UI, 需要额外处理.
总结
新手还是建议 自己把代码敲一下. 涉及的东西还是不少的. 至于视频播放, 博主会在下一篇文章中, 用画中画.. 好吧不用了