悬浮窗功能

This commit is contained in:
litingting 2025-06-19 18:20:08 +08:00
parent 481452e688
commit d8180a1ff6
27 changed files with 719 additions and 64 deletions

View File

@ -59,6 +59,13 @@
<!-- <category android:name="android.intent.category.LAUNCHER" /> -->
<!-- </intent-filter> -->
</activity>
<activity
android:name=".activity.ScreenPermissionActivity"
android:excludeFromRecents="true"
android:taskAffinity=""
android:theme="@style/Theme.Transparent"
android:launchMode="singleTop"
android:exported="true" />
<service
android:name=".service.ScreenRecordService"

View File

@ -0,0 +1,73 @@
package com.audio.record.screen.test.activity
import android.content.Context
import android.content.Intent
import android.media.projection.MediaProjectionManager
import android.os.Bundle
import android.os.PersistableBundle
import androidx.activity.result.ActivityResultLauncher
import androidx.activity.result.contract.ActivityResultContracts
import androidx.appcompat.app.AppCompatActivity
import com.audio.record.screen.test.base.BaseActivity
import com.audio.record.screen.test.databinding.ActivityPlayBinding
import com.audio.record.screen.test.service.FloatingWindowBridge
import com.audio.record.screen.test.tool.Common
/**
* Service悬浮窗中请求权限的透明Activity
*/
class ScreenPermissionActivity : AppCompatActivity() {
private lateinit var screenCaptureLauncher: ActivityResultLauncher<Intent>
//录音权限
private lateinit var micLauncher: ActivityResultLauncher<String>
companion object{
const val key_with_audio="key_with_audio"
}
private var withAudio = false
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
Common.setStatusBarTextColor(this, true)
withAudio = intent.getBooleanExtra(key_with_audio, false)
screenCaptureLauncher = registerForActivityResult(
ActivityResultContracts.StartActivityForResult()
) { result ->
if (result.resultCode == RESULT_OK && result.data != null) {
val data: Intent? = result.data
FloatingWindowBridge.updateMediaProjection(result.resultCode, data!!)
}
finish()
}
micLauncher = registerForActivityResult(
ActivityResultContracts.RequestPermission()
) { isGranted ->
if (isGranted) {
Common.showLog("mic 权限授予")
requestScreenPermission()
} else {
Common.showLog("mic 权限拒绝")
}
}
if(withAudio){
micLauncher.launch(android.Manifest.permission.RECORD_AUDIO)
}else{
requestScreenPermission()
}
}
private fun requestScreenPermission() {
val mediaProjectionManager = getSystemService(Context.MEDIA_PROJECTION_SERVICE) as MediaProjectionManager
val intent = mediaProjectionManager.createScreenCaptureIntent()
screenCaptureLauncher.launch(intent)
}
}

View File

@ -4,14 +4,14 @@ import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentActivity
import androidx.viewpager2.adapter.FragmentStateAdapter
import com.audio.record.screen.test.fragment.MainFragment
import com.audio.record.screen.test.fragment.RecorderFragment
import com.audio.record.screen.test.fragment.RecordMainFragment
class ViewPager2Adapter(fragmentActivity: FragmentActivity) :
FragmentStateAdapter(fragmentActivity) {
private val fragments: List<Fragment> = arrayListOf(
MainFragment.newInstance(),
RecorderFragment.newInstance(),
RecordMainFragment.newInstance(),
MainFragment.newInstance()
)
override fun getItemCount(): Int = fragments.size

View File

@ -4,19 +4,16 @@ import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.navigation.NavController
import androidx.navigation.NavDestination
import androidx.navigation.fragment.NavHostFragment
import com.audio.record.screen.test.R
import com.audio.record.screen.test.base.BaseFragment
import com.audio.record.screen.test.databinding.FragmentRecordBinding
import com.audio.record.screen.test.tool.Common
class RecorderFragment : BaseFragment<FragmentRecordBinding>() {
class RecordMainFragment : BaseFragment<FragmentRecordBinding>() {
companion object {
@JvmStatic
fun newInstance() =
RecorderFragment().apply {
RecordMainFragment().apply {
}
}

View File

@ -10,7 +10,10 @@ import android.view.View
import android.view.ViewGroup
import androidx.activity.result.ActivityResultLauncher
import androidx.activity.result.contract.ActivityResultContracts
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle
import androidx.navigation.NavController
import androidx.navigation.fragment.findNavController
import com.audio.record.screen.test.R
@ -21,16 +24,18 @@ import com.audio.record.screen.test.dialog.DialogPermission
import com.audio.record.screen.test.service.FloatingCallback
import com.audio.record.screen.test.service.FloatingWindowBridge
import com.audio.record.screen.test.tool.Common
import com.audio.record.screen.test.tool.ConstValue
import com.audio.record.screen.test.tool.Permission
import com.audio.record.screen.test.tool.ScreenCaptureHelper
import com.audio.record.screen.test.viewmodel.MainViewModel
import com.audio.record.screen.test.viewmodel.PreviewViewModel
import kotlinx.coroutines.launch
class RecordNormalFragment : BaseFragment<FragmentRecordNormalBinding>(), FloatingCallback {
private lateinit var navController: NavController
private val REQUEST_SCREENSHOT = 125
private lateinit var viewModel: MainViewModel
private lateinit var mediaProjectionManager: MediaProjectionManager
@ -51,6 +56,9 @@ class RecordNormalFragment : BaseFragment<FragmentRecordNormalBinding>(), Floati
//是否带音频录制
private var isWithAudio = false
private var pendingType: Int = -1
private val key_type = "check_type"
override fun initBinding(
inflater: LayoutInflater,
container: ViewGroup?
@ -115,7 +123,7 @@ class RecordNormalFragment : BaseFragment<FragmentRecordNormalBinding>(), Floati
if (result.resultCode == Activity.RESULT_OK) {
val data: Intent? = result.data
FloatingWindowBridge.updateMediaProjection(result.resultCode, data!!)
FloatingWindowBridge.sendCommand(FloatingWindowBridge.COMMEND_show_screenshot)
} else {
Common.showLog("用户取消了录屏权限授权")
}
@ -125,10 +133,16 @@ class RecordNormalFragment : BaseFragment<FragmentRecordNormalBinding>(), Floati
recorderLauncher =
registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
if (result.resultCode == Activity.RESULT_OK && result.data != null) {
val data = result.data
val intent = result.data
val resultCode = result.resultCode
FloatingWindowBridge.updateMediaProjection(result.resultCode, data!!)
startCountDown()
FloatingWindowBridge.updateMediaProjection(result.resultCode, intent!!)
when(pendingType){
ConstValue.type_record->{ startCountDown()}
ConstValue.type_show_screenshot->{
FloatingWindowBridge.sendCommand(FloatingWindowBridge.COMMEND_show_screenshot)
}
}
} else {
Common.showLog("录屏授权失败 ")
}
@ -196,10 +210,7 @@ class RecordNormalFragment : BaseFragment<FragmentRecordNormalBinding>(), Floati
private fun setScreenshot(boolean: Boolean) {
if (boolean) {
if (FloatingWindowBridge.getMediaProjection() == null) {
val captureIntent = mediaProjectionManager.createScreenCaptureIntent()
screenCaptureLauncher.launch(captureIntent)
} else {
requestRecordPermission(ConstValue.type_show_screenshot){
FloatingWindowBridge.sendCommand(FloatingWindowBridge.COMMEND_show_screenshot)
}
@ -217,14 +228,28 @@ class RecordNormalFragment : BaseFragment<FragmentRecordNormalBinding>(), Floati
}
private fun startRecorder() {
if (FloatingWindowBridge.getMediaProjection() == null) {
val captureIntent = mediaProjectionManager.createScreenCaptureIntent()
recorderLauncher.launch(captureIntent)
} else {
requestRecordPermission( ConstValue.type_record){
startCountDown()
}
}
/**
* 请求录屏权限
*/
private fun requestRecordPermission(type:Int,check:(()->Unit)? = null){
// TODO: requestRecordPermission
if (FloatingWindowBridge.getMediaProjection() == null) {
val captureIntent = mediaProjectionManager.createScreenCaptureIntent()
pendingType = type
recorderLauncher.launch(captureIntent)
}else{
check?.invoke()
}
}
private fun showAudioDialog() {
mAudioDialog = mAudioDialog ?: DialogAudio {
when (it) {
@ -249,7 +274,22 @@ class RecordNormalFragment : BaseFragment<FragmentRecordNormalBinding>(), Floati
}
override fun onStartRecording() {
Common.showLog(" FragmentRecordNormal 回调 onStartRecording")
if (lifecycle.currentState.isAtLeast(Lifecycle.State.STARTED)) {
navController.navigate(R.id.action_to_recording)
Common.showLog(" FragmentRecordNormal 回调 onStartRecording 执行111111111")
} else {
Common.showLog(" FragmentRecordNormal 回调 onStartRecording 延迟执行")
// 延迟执行
lifecycleScope.launch {
lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) {
Common.showLog(" FragmentRecordNormal 回调 onStartRecording 延迟执行 11111111")
navController.navigate(R.id.action_to_recording)
}
}
}
}

View File

@ -7,4 +7,6 @@ interface FloatingCallback {
fun onUpdateRecordTime(time:String){}
fun onStopRecord(){}
}

View File

@ -80,6 +80,7 @@ object FloatingWindowBridge {
}
fun sendCommand(command: String, countdownValues: Array<String>? = null) {
Common.showLog("-------------command=${command}")
when (command) {
COMMEND_hide_camera -> service?.hideFrontCamera()
COMMEND_show_camera -> service?.showCameraView()

View File

@ -0,0 +1,7 @@
package com.audio.record.screen.test.service
enum class RecordState {
DEFAULT,
RECORDING,
PAUSE }

View File

@ -5,7 +5,6 @@ import android.app.Notification
import android.app.NotificationChannel
import android.app.NotificationManager
import android.app.Service
import android.content.ContentUris
import android.content.Context
import android.content.Intent
import android.graphics.PixelFormat
@ -17,12 +16,11 @@ import android.media.projection.MediaProjectionManager
import android.net.Uri
import android.os.Binder
import android.os.Build
import android.os.Environment
import android.os.CountDownTimer
import android.os.Handler
import android.os.IBinder
import android.os.Looper
import android.os.ParcelFileDescriptor
import android.provider.MediaStore
import android.view.Gravity
import android.view.LayoutInflater
import android.view.View
@ -30,6 +28,7 @@ import android.view.WindowManager
import android.view.animation.AccelerateDecelerateInterpolator
import android.widget.FrameLayout
import android.widget.ImageView
import android.widget.LinearLayout
import android.widget.RemoteViews
import android.widget.TextView
import androidx.activity.result.ActivityResultLauncher
@ -39,21 +38,26 @@ import androidx.camera.lifecycle.ProcessCameraProvider
import androidx.camera.view.PreviewView
import androidx.core.app.NotificationCompat
import androidx.core.content.ContextCompat
import androidx.core.content.FileProvider
import androidx.core.view.isVisible
import com.audio.record.screen.test.R
import com.audio.record.screen.test.activity.PlayActivity
import com.audio.record.screen.test.activity.ScreenPermissionActivity
import com.audio.record.screen.test.tool.Common
import com.audio.record.screen.test.tool.ConstValue
import com.audio.record.screen.test.tool.DraggableViewHelper
import com.audio.record.screen.test.tool.Extend.dpToPx
import com.audio.record.screen.test.tool.Extend.pxToDp
import com.audio.record.screen.test.tool.ScreenCaptureHelper
import com.audio.record.screen.test.tool.VideoFileHelper
import com.audio.record.screen.test.view.CountDownFloatingManager
import java.io.File
import java.lang.ref.WeakReference
class ScreenRecordService : Service() {
private var lifecycleOwner: CustomLifecycleOwner? = null
private var isBallViewAdded = false
private var isBallExpandViewAdded = false
private var isScreenshotViewAdded = false
private var isWebcamViewAdded = false
private var isRecordViewAdded = false
@ -64,6 +68,17 @@ class ScreenRecordService : Service() {
//悬浮球View
private var ballView: View? = null
private var imIconVideo: ImageView? = null
private var smallLayoutRecording: LinearLayout? = null
private var smallTvTime: TextView? = null
//悬浮球展开View
private var ballViewExpand: View? = null
private var imRecord: ImageView? = null
private var tvRecordTime: TextView? = null
private var imNoAudioRecord: ImageView? = null
private var imScreenshot:ImageView? = null
//截屏View
private var screenshotView: View? = null
@ -74,9 +89,10 @@ class ScreenRecordService : Service() {
private var index = 0
//录屏成功View
private var recordView: View? = null
private var recordCompleteView: View? = null
private val windowManager by lazy { getSystemService(WINDOW_SERVICE) as WindowManager }
private lateinit var layoutParamsBallExpand: WindowManager.LayoutParams
private lateinit var layoutParamsBall: WindowManager.LayoutParams
private lateinit var layoutParamsScreenshot: WindowManager.LayoutParams
private lateinit var layoutParamsCameraView: WindowManager.LayoutParams
@ -93,17 +109,22 @@ class ScreenRecordService : Service() {
private var callbackRef: FloatingCallback? = null
//截屏
var mIntent: Intent? = null
private var mCode: Int = 0
private lateinit var mediaRecorder: MediaRecorder
private lateinit var tmpVideoUri: Uri
private lateinit var videoName: String
private var tmpVideoPfd: ParcelFileDescriptor? = null
//当前录制是否带音频
private var isWithAudio = false
private val callbacks = mutableListOf<WeakReference<FloatingCallback>>()
private var countDownTimer: CountDownTimer? = null
//录制状态标志 true 录制中
private var mRecordingStatus = -1
//录制时间监听相关
private var recordingStartTime: Long = 0L // 本次录制开始时间
@ -116,7 +137,10 @@ class ScreenRecordService : Service() {
private var isPause = false
private lateinit var virtualDisplay: VirtualDisplay
private var mediaProjection:MediaProjection? = null
//悬浮球点击进行的动作 type_record_audio/type_record_without_audio/type_screenshot
private var ballType: Int? = null
private var mediaProjection: MediaProjection? = null
private val timeUpdateRunnable = object : Runnable {
override fun run() {
@ -125,6 +149,8 @@ class ScreenRecordService : Service() {
System.currentTimeMillis() - recordingStartTime - totalPausedTime
val elapsed = currentRecordingTimeInMillis / 1000
val onRecordingTimeChanged = Common.onRecordingTimeChanged(elapsed)
tvRecordTime?.text = onRecordingTimeChanged
smallTvTime?.text = onRecordingTimeChanged
callbacks.forEach {
it.get()?.onUpdateRecordTime(onRecordingTimeChanged)
}
@ -180,9 +206,26 @@ class ScreenRecordService : Service() {
mIntent = m
mCode = code
// mediaProjection = mediaProjectionManager.getMediaProjection(code, m)
ballType?.let {
when (it) {
ConstValue.type_record_without_audio -> {
isWithAudio = false
val arrayOf = arrayOf("3", "2", "1")
showCountDownView(arrayOf)
}
ConstValue.type_record_audio -> {
isWithAudio = true
val arrayOf = arrayOf("3", "2", "1")
showCountDownView(arrayOf)
}
ConstValue.type_screenshot -> {
startScreenshot()
}
}
ballType = null
}
fun resetIntent(){
mIntent = null
}
@ -224,6 +267,22 @@ class ScreenRecordService : Service() {
.build()
}
/**
* 开始截屏
*/
private fun startScreenshot(){
// TODO: 开始截屏
mIntent?.let { intent->
mediaProjectionManager.getMediaProjection(mCode, intent)?.let { mediaProjection->
hideScreenshot()
ScreenCaptureHelper.startScreenCapture(this, mediaProjection) {
//截屏完成
showScreenshot()
}
}
}
}
@SuppressLint("ClickableViewAccessibility")
fun showCameraView() {
if (frontCameraView == null) {
@ -255,6 +314,10 @@ class ScreenRecordService : Service() {
hideFrontCamera() // 自动清除
hideBall()
hideScreenshot()
hideBallExpand()
hideRecordCompleteView()
ballViewExpand = null
recordCompleteView = null
frontCameraView = null
ballView = null
screenshotView = null
@ -298,9 +361,15 @@ class ScreenRecordService : Service() {
layoutParamsBall.gravity = Gravity.TOP or Gravity.START
layoutParamsBall.x = screenWH.first - size - 20
layoutParamsBall.y = screenWH.second / 2 - size.div(2) // 居中
smallLayoutRecording = ballView!!.findViewById<LinearLayout>(R.id.layout_recording)
imIconVideo = ballView!!.findViewById<ImageView>(R.id.image)
smallTvTime = ballView!!.findViewById<TextView>(R.id.tv_small_time)
DraggableViewHelper.attachToWindow(ballView!!, layoutParamsBall, windowManager, true)
DraggableViewHelper.attachToWindow(ballView!!, layoutParamsBall, windowManager, true) {
hideBall()
showBallExpand()
}
Common.showLog("-------layoutParamsBall.x=${layoutParamsBall.x} layoutParamsBall.y=${layoutParamsBall.y}")
}
if (!isBallViewAdded) {
@ -310,6 +379,116 @@ class ScreenRecordService : Service() {
}
fun showBallExpand() {
if (ballViewExpand == null) {
ballViewExpand = LayoutInflater.from(this).inflate(R.layout.floating_ball_expand, null)
layoutParamsBallExpand = getLayoutParams()
layoutParamsBallExpand.gravity = Gravity.TOP or Gravity.START
imRecord = ballViewExpand!!.findViewById<ImageView>(R.id.ic_record)
tvRecordTime = ballViewExpand!!.findViewById<TextView>(R.id.tv_record_time)
imNoAudioRecord = ballViewExpand!!.findViewById<ImageView>(R.id.ic_without_audio)
imScreenshot = ballViewExpand!!.findViewById<ImageView>(R.id.ic_screenshot)
imRecord?.setOnClickListener {
it.isSelected.let { select ->
if (!select) {
//录制带音频的视频
ballType = ConstValue.type_record_audio
intentPermission(true)
} else {
//录制完成
stopRecording()
}
}
}
imNoAudioRecord?.setOnClickListener {
// TODO:
when (mRecordingStatus) {
ConstValue.status_recording -> {
//暂停录制
pauseRecording()
}
ConstValue.status_pause -> {
//继续录制
resumeRecording()
}
else -> {
// 开始录制没有声音的视频
ballType = ConstValue.type_record_without_audio
intentPermission(false)
}
}
}
imScreenshot?.setOnClickListener {
ballType = ConstValue.type_screenshot
intentPermission(false)
}
}
if (!isBallExpandViewAdded) {
val size = Common.dpToPx(36, this)
val aWidthPx = Common.dpToPx(55, this)
val aHeightPx = Common.dpToPx(250, this)
// 转换 B 的宽高为像素
val bWidthPx = size
val bHeightPx = size
// 计算 B 的中心点坐标
val bCenterX = layoutParamsBall.x + bWidthPx / 2
val bCenterY = layoutParamsBall.y + bHeightPx / 2
// 让 A 的左上角从 B 的中心点出发(居中显示)
var aX = bCenterX - aWidthPx / 2
var aY = bCenterY - aHeightPx / 2
val (screenWidth, screenHeight) = Common.getScreenWH(this)
// 修正边界:防止 A 悬浮窗超出屏幕边界
aX = aX.coerceIn(0, screenWidth - aWidthPx)
aY = aY.coerceIn(0, screenHeight - aHeightPx)
if (aX >= screenWidth/2) {
layoutParamsBallExpand.x = aX - 10.dpToPx(this)
Common.showLog("--111111111--aX=${aX} layoutParamsBallExpand.x=${ layoutParamsBallExpand.x}")
} else {
layoutParamsBallExpand.x = aX + 10.dpToPx(this)
Common.showLog("---22222222--aX=${aX} layoutParamsBallExpand.x=${ layoutParamsBallExpand.x}")
}
layoutParamsBallExpand.y = aY
windowManager.addView(ballViewExpand, layoutParamsBallExpand)
isBallExpandViewAdded = true
delayRemove {
Common.showLog("-------切换ball显示")
hideBallExpand()
showBall()
}
}
}
/**
* 3秒后自动隐藏
*/
private fun delayRemove(onFinish: (() -> Unit)?) {
countDownTimer?.cancel()
countDownTimer = object : CountDownTimer(3000L, 1000L) {
override fun onTick(millisUntilFinished: Long) {
}
override fun onFinish() {
onFinish?.invoke()
countDownTimer = null
}
}
countDownTimer?.start()
}
//显示截屏
fun showScreenshot() {
if (screenshotView == null) {
@ -327,17 +506,7 @@ class ScreenRecordService : Service() {
windowManager,
true
) {
Common.showLog("--------------callbackRef=${callbackRef}")
// callbackRef?.onFloatingButtonClicked(FloatingWindowBridge.CLICK_screenshot)
mIntent?.let {
mediaProjectionManager.getMediaProjection(mCode, it)?.let {
hideScreenshot()
ScreenCaptureHelper.startScreenCapture(this, it) {
// showScreenshot()
}
}
}
startScreenshot()
}
}
if (!isScreenshotViewAdded) {
@ -349,16 +518,21 @@ class ScreenRecordService : Service() {
//显示截屏
fun showRecordView() {
if (recordView == null) {
recordView = LayoutInflater.from(this).inflate(R.layout.floating_record_complete, null)
if (recordCompleteView == null) {
recordCompleteView =
LayoutInflater.from(this).inflate(R.layout.floating_record_complete, null)
layoutParamsRecordView = getLayoutParams()
layoutParamsRecordView.gravity = Gravity.CENTER
val imClose = recordView!!.findViewById<ImageView>(R.id.close).setOnClickListener {
hideRecordView()
val imClose =
recordCompleteView!!.findViewById<ImageView>(R.id.close).setOnClickListener {
hideRecordCompleteView()
}
recordView!!.findViewById<FrameLayout>(R.id.layout_video).setOnClickListener {
recordCompleteView!!.findViewById<FrameLayout>(R.id.layout_video).setOnClickListener {
// TODO: 跳到播放页面
hideRecordCompleteView()
val intent = Intent(this, PlayActivity::class.java).apply {
putExtra(PlayActivity.KEY_URI, tmpVideoUri)
putExtra(PlayActivity.KEY_name, videoName)
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
}
this.startActivity(intent)
@ -366,15 +540,15 @@ class ScreenRecordService : Service() {
}
}
if (!isRecordViewAdded) {
val videoThumbnail = Common.getVideoThumbnail(tmpVideoPfd!!)
Common.showLog("--------------videoThumbnail=${videoThumbnail}")
val thumb = recordView!!.findViewById<ImageView>(R.id.image)
val thumb = recordCompleteView!!.findViewById<ImageView>(R.id.image)
thumb.setImageBitmap(videoThumbnail)
windowManager.addView(recordView, layoutParamsRecordView)
windowManager.addView(recordCompleteView, layoutParamsRecordView)
isRecordViewAdded = true
delayRemove {
hideRecordCompleteView()
}
}
}
@ -392,9 +566,23 @@ class ScreenRecordService : Service() {
// frontCameraView = null
}
fun hideBallExpand() {
ballViewExpand?.let {
try {
if (isBallExpandViewAdded) {
windowManager.removeView(it)
isBallExpandViewAdded = false
}
} catch (_: Exception) {
}
}
// frontCameraView = null
}
//隐藏录制预览View
fun hideRecordView() {
recordView?.let {
fun hideRecordCompleteView() {
recordCompleteView?.let {
try {
if (isRecordViewAdded) {
windowManager.removeView(it)
@ -451,7 +639,7 @@ class ScreenRecordService : Service() {
hideCountDown()
mIntent?.let { intent ->
mediaProjectionManager.getMediaProjection(mCode, intent)
?.let { media->
?.let { media ->
mediaProjection = media
startRecording()
callbacks.forEach {
@ -516,11 +704,14 @@ class ScreenRecordService : Service() {
*/
fun startRecording() {
Common.showLog("-------录屏中.....")
// TODO: 录屏中.
val fullScreenSize = Common.getFullScreenSize(this)
val width = VideoFileHelper.alignTo16(fullScreenSize.first)
val height = VideoFileHelper.alignTo16(fullScreenSize.second)
initRecorder(width, height)
mediaRecorder.start()
mRecordingStatus = ConstValue.status_recording
updateBall()
recordingStartTime = System.currentTimeMillis()
totalPausedTime = 0L
mRecorderHandler.post(timeUpdateRunnable)
@ -556,8 +747,6 @@ class ScreenRecordService : Service() {
}, null
)
}
}
@ -600,8 +789,11 @@ class ScreenRecordService : Service() {
* 暂停录制视频
*/
fun pauseRecording() {
// TODO: 暂停录制视频
Common.showLog("-------暂停.....")
mediaRecorder.pause()
mRecordingStatus = ConstValue.status_pause
updateBall()
pauseStartTime = System.currentTimeMillis()
isPause = true
}
@ -610,15 +802,67 @@ class ScreenRecordService : Service() {
* 继续录制视频
*/
fun resumeRecording() {
// TODO: 继续录制视频
Common.showLog("------继续.....")
mediaRecorder.resume()
mRecordingStatus = ConstValue.status_recording
updateBall()
totalPausedTime += System.currentTimeMillis() - pauseStartTime
isPause = false
mRecorderHandler.post(timeUpdateRunnable)
}
/**
* 录制状态更新
*/
private fun updateBall() {
// TODO: 录制状态更新
when (mRecordingStatus) {
ConstValue.status_recording -> {
imNoAudioRecord?.run {
isSelected = true
isActivated = false
}
imRecord?.isSelected = true
tvRecordTime?.isVisible = true
imIconVideo?.isVisible = false
smallLayoutRecording?.isVisible = true
}
ConstValue.status_pause -> {
imNoAudioRecord?.run {
isSelected = false
isActivated = true
}
imIconVideo?.isVisible = false
smallLayoutRecording?.isVisible = true
}
ConstValue.status_complete -> {
imNoAudioRecord?.run {
isSelected = false
isActivated = false
}
imRecord?.isSelected = false
tvRecordTime?.isVisible = false
imIconVideo?.isVisible = true
smallLayoutRecording?.isVisible = false
hideBallExpand()
showBall()
}
}
}
private fun initRecorder(width: Int, height: Int) {
val (Uri, pfd) = VideoFileHelper.createVideoFile(this, Common.folderName)
videoName = System.currentTimeMillis().toString()
val (Uri, pfd) = VideoFileHelper.createVideoFile(this, Common.folderName, videoName)
tmpVideoUri = Uri
tmpVideoPfd = pfd
mediaRecorder = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
@ -626,14 +870,18 @@ class ScreenRecordService : Service() {
} else {
MediaRecorder()
}.apply {
if (isWithAudio)
if (isWithAudio) {
setAudioSource(MediaRecorder.AudioSource.MIC)
}
setVideoSource(MediaRecorder.VideoSource.SURFACE)
setOutputFormat(MediaRecorder.OutputFormat.MPEG_4)
setOutputFile(pfd?.fileDescriptor)
setVideoSize(width, height)
setVideoEncoder(MediaRecorder.VideoEncoder.H264)
if (isWithAudio) {
setAudioEncoder(MediaRecorder.AudioEncoder.AAC)
}
setVideoEncoder(MediaRecorder.VideoEncoder.H264)
setVideoEncodingBitRate(8 * 1000 * 1000)
setVideoFrameRate(30)
prepare()
@ -641,6 +889,9 @@ class ScreenRecordService : Service() {
}
fun stopRecording() {
// TODO: 录屏完成
mRecordingStatus = ConstValue.status_complete
updateBall()
Common.showLog("-------录屏完成.....")
mRecorderHandler.removeCallbacks(timeUpdateRunnable)
releaseAll()
@ -669,4 +920,12 @@ class ScreenRecordService : Service() {
}
private fun intentPermission(requestAudio: Boolean) {
val intent = Intent(this, ScreenPermissionActivity::class.java)
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
intent.putExtra(ScreenPermissionActivity.key_with_audio, requestAudio)
this.startActivity(intent)
}
}

View File

@ -0,0 +1,26 @@
package com.audio.record.screen.test.tool
object ConstValue {
//--------------RecordNormalFragment
//开始录制
const val type_record = 0
//显示截屏悬浮球
const val type_show_screenshot = 1
//--------------ScreenRecordService
//一次性使用
const val type_record_audio = 3
const val type_record_without_audio = 4
const val type_screenshot = 5
//当前状态
const val status_recording = 6 //录制当中
const val status_pause = 7 //录制暂停
const val status_complete = 8 //录制完成
}

View File

@ -6,6 +6,9 @@ import android.media.Image
import android.util.TypedValue
import android.view.View
import android.view.ViewGroup
import android.widget.ImageView
import androidx.annotation.DrawableRes
import androidx.core.content.ContextCompat
object Extend {
@ -44,11 +47,18 @@ object Extend {
layoutParams = params
}
fun ImageView.setDrawableCompat(@DrawableRes resId: Int) {
val drawable = ContextCompat.getDrawable(context, resId)
this.setImageDrawable(drawable)
}
fun Int.dpToPx(context: Context): Int =
TypedValue.applyDimension(
TypedValue.COMPLEX_UNIT_DIP, this.toFloat(),
context.resources.displayMetrics
).toInt()
fun Context.pxToDp(px: Int): Float {
return px / resources.displayMetrics.density
}
}

View File

@ -68,8 +68,8 @@ object ScreenCaptureHelper {
if (image != null) {
val bitmap = imageToBitmap(image)
saveBitmap(context, bitmap, folderName)
isOK.invoke()
showScreenshotPreviewAnimation(context, bitmap,width,height)
isOK.invoke()
image.close()
}
imageReader.close()

View File

@ -0,0 +1,12 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="26dp"
android:height="26dp"
android:viewportWidth="26"
android:viewportHeight="26">
<path
android:pathData="M1.733,13C1.733,6.778 6.778,1.733 13,1.733C19.222,1.733 24.267,6.778 24.267,13C24.267,19.222 19.222,24.267 13,24.267C6.778,24.267 1.733,19.222 1.733,13ZM0,13C0,20.18 5.82,26 13,26C20.18,26 26,20.18 26,13C26,5.82 20.18,0 13,0C5.82,0 0,5.82 0,13Z"
android:fillColor="#ffffff"/>
<path
android:pathData="M7,13C7,9.686 9.686,7 13,7C16.314,7 19,9.686 19,13C19,16.314 16.314,19 13,19C9.686,19 7,16.314 7,13Z"
android:fillColor="#DD2432"/>
</vector>

View File

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="15dp"
android:height="19dp"
android:viewportWidth="28"
android:viewportHeight="32">
<path
android:pathData="M26.901,14.257C28.261,15.021 28.261,16.979 26.901,17.743L3.731,30.778C2.397,31.528 0.75,30.564 0.75,29.035L0.75,2.965C0.75,1.436 2.397,0.472 3.731,1.222L26.901,14.257Z"
android:fillColor="#ffffff"/>
</vector>

View File

@ -0,0 +1,17 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="26dp"
android:height="26dp"
android:viewportWidth="26"
android:viewportHeight="26">
<group>
<clip-path
android:pathData="M0,0h26v26h-26z"/>
<group>
<clip-path
android:pathData="M26,0H0.005V26H26V0Z"/>
<path
android:pathData="M20.195,25.962H15.027C14.514,25.962 14.101,25.548 14.101,25.034V16.99C14.101,16.986 14.093,16.975 14.085,16.975H11.657C11.653,16.975 11.642,16.983 11.642,16.99V25.034C11.642,25.548 11.229,25.962 10.715,25.962H5.551C4.003,25.962 2.744,24.701 2.744,23.15V14.976H2.181C1.308,14.976 0.525,14.457 0.177,13.653C-0.166,12.853 -0.004,11.925 0.598,11.291L10.043,1.298C10.808,0.49 11.881,0.026 12.993,0.026C14.104,0.026 15.177,0.49 15.942,1.298L25.383,11.29C25.981,11.924 26.147,12.852 25.804,13.653C25.46,14.453 24.673,14.975 23.8,14.975H23.001V23.15C23.001,24.7 21.743,25.961 20.195,25.961V25.962ZM15.953,24.105H20.195C20.72,24.105 21.148,23.676 21.148,23.15V14.047C21.148,13.533 21.561,13.119 22.075,13.119H23.8C23.9,13.119 24.039,13.069 24.101,12.922C24.163,12.775 24.105,12.639 24.039,12.566L14.59,2.574C14.17,2.13 13.602,1.882 12.989,1.882C12.375,1.882 11.807,2.126 11.387,2.574L1.942,12.567C1.872,12.64 1.815,12.775 1.88,12.922C1.942,13.069 2.081,13.12 2.181,13.12H3.671C4.184,13.12 4.597,13.533 4.597,14.047V23.151C4.597,23.676 5.026,24.106 5.551,24.106H9.793V16.99C9.793,15.958 10.63,15.119 11.661,15.119H14.089C15.119,15.119 15.957,15.958 15.957,16.99V24.106H15.953L15.953,24.105Z"
android:fillColor="#ffffff"/>
</group>
</group>
</vector>

View File

@ -0,0 +1,12 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="15dp"
android:height="14dp"
android:viewportWidth="29"
android:viewportHeight="38">
<path
android:pathData="M1,0L10,0A1,1 0,0 1,11 1L11,37A1,1 0,0 1,10 38L1,38A1,1 0,0 1,0 37L0,1A1,1 0,0 1,1 0z"
android:fillColor="#ffffff"/>
<path
android:pathData="M19,0L28,0A1,1 0,0 1,29 1L29,37A1,1 0,0 1,28 38L19,38A1,1 0,0 1,18 37L18,1A1,1 0,0 1,19 0z"
android:fillColor="#ffffff"/>
</vector>

View File

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="15dp"
android:height="15dp"
android:viewportWidth="15"
android:viewportHeight="15">
<path
android:pathData="M3,0L12,0A3,3 0,0 1,15 3L15,12A3,3 0,0 1,12 15L3,15A3,3 0,0 1,0 12L0,3A3,3 0,0 1,3 0z"
android:fillColor="#DD2432"/>
</vector>

View File

@ -0,0 +1,15 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="26dp"
android:height="26dp"
android:viewportWidth="26"
android:viewportHeight="26">
<path
android:pathData="M4.987,0H6.883V19.948H26V21.844H4.987V0Z"
android:fillColor="#ffffff"/>
<path
android:pathData="M21.844,25.169H19.948V6.052H0V4.156H21.844V25.169Z"
android:fillColor="#ffffff"/>
<path
android:pathData="M24.73,2.573L9.298,18.911L7.919,17.61L23.352,1.27L24.73,2.573Z"
android:fillColor="#ffffff"/>
</vector>

View File

@ -0,0 +1,23 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="26dp"
android:height="25dp"
android:viewportWidth="26"
android:viewportHeight="25">
<path
android:pathData="M5.871,10.903C5.871,8.124 8.124,5.871 10.903,5.871C13.682,5.871 15.936,8.124 15.936,10.903C15.936,13.682 13.682,15.936 10.903,15.936C8.124,15.936 5.871,13.682 5.871,10.903Z"
android:fillColor="#ffffff"/>
<group>
<clip-path
android:pathData="M15.097,13.419h10.903v10.903h-10.903z"/>
<group>
<clip-path
android:pathData="M26,13.419H15.099V24.323H26V13.419Z"/>
<path
android:pathData="M21.531,13.787C21.74,14.011 21.857,14.316 21.857,14.634V23.125C21.857,23.787 21.358,24.323 20.743,24.323C20.447,24.323 20.164,24.196 19.955,23.972L17.929,21.792C17.815,21.67 17.66,21.601 17.499,21.601H16.717C15.823,21.601 15.097,20.821 15.097,19.859V17.9C15.097,16.938 15.823,16.159 16.717,16.159H17.499C17.66,16.159 17.815,16.09 17.929,15.967L19.955,13.788C20.39,13.32 21.095,13.319 21.531,13.787ZM25.869,17.079C26.027,17.249 26.027,17.525 25.869,17.696L24.751,18.897L25.869,20.098C26.027,20.268 26.027,20.545 25.869,20.715C25.71,20.885 25.454,20.885 25.295,20.715L24.177,19.514L23.06,20.715C22.901,20.885 22.644,20.885 22.486,20.715C22.327,20.545 22.327,20.268 22.486,20.098L23.604,18.897L22.486,17.696C22.327,17.525 22.327,17.249 22.486,17.079C22.644,16.909 22.901,16.909 23.06,17.079L24.177,18.28L25.295,17.079C25.453,16.909 25.71,16.909 25.869,17.079L25.869,17.079Z"
android:fillColor="#ffffff"/>
</group>
</group>
<path
android:pathData="M10.903,0C16.925,0 21.807,4.882 21.807,10.903C21.807,11.186 21.792,11.465 21.771,11.742H20.313C20.338,11.466 20.353,11.186 20.353,10.903C20.353,5.685 16.122,1.454 10.903,1.454C5.685,1.454 1.454,5.685 1.454,10.903C1.454,16.122 5.685,20.353 10.903,20.353C11.775,20.353 12.618,20.232 13.42,20.011V20.163C13.42,20.177 13.422,20.191 13.422,20.205V21.51C12.613,21.701 11.771,21.807 10.903,21.807C4.882,21.807 0,16.925 0,10.903C0,4.882 4.882,0 10.903,0Z"
android:fillColor="#ffffff"/>
</vector>

View File

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<corners android:radius="25dp"/>
<solid android:color="@color/color_CC000000"/>
</shape>

View File

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="oval">
<solid android:color="@color/color_CC000000"/>
</shape>

View File

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="oval">
<solid android:color="@color/test_red"/>
</shape>

View File

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="@drawable/ball_recording" android:state_selected="true" />
<item android:drawable="@drawable/ball_audio_record" android:state_selected="false" />
</selector>

View File

@ -0,0 +1,14 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<!-- 暂停状态(优先级高) -->
<item android:drawable="@drawable/ball_continue_recording"
android:state_activated="true" />
<!-- 录制状态 -->
<item android:drawable="@drawable/ball_pause"
android:state_selected="true" />
<!-- 默认状态 -->
<item android:drawable="@drawable/ball_without_audio_record" />
</selector>

View File

@ -9,4 +9,29 @@
android:layout_height="36dp"
android:src="@drawable/global_ball" />
<LinearLayout
android:id="@+id/layout_recording"
android:layout_width="36dp"
android:layout_height="36dp"
android:visibility="gone"
android:background="@drawable/recording_bg_oval"
android:gravity="center"
android:orientation="vertical">
<ImageView
android:layout_width="7dp"
android:layout_height="7dp"
android:src="@drawable/recording_oval_red" />
<TextView
android:id="@+id/tv_small_time"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="2dp"
android:text="00:00:00"
android:textColor="@color/white"
android:textSize="6sp" />
</LinearLayout>
</FrameLayout>

View File

@ -0,0 +1,60 @@
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="wrap_content"
android:layout_height="wrap_content">
<LinearLayout
android:layout_width="55dp"
android:layout_height="wrap_content"
android:background="@drawable/bg_ball"
android:gravity="center"
android:orientation="vertical"
android:paddingTop="25dp"
android:paddingBottom="25dp">
<ImageView
android:id="@+id/ic_record"
android:layout_width="match_parent"
android:layout_height="29dp"
android:paddingTop="3dp"
android:paddingBottom="3dp"
android:src="@drawable/status_ball_record" />
<TextView
android:id="@+id/tv_record_time"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="00:00:00"
android:textColor="@color/white"
android:textSize="12sp"
android:visibility="gone" />
<ImageView
android:id="@+id/ic_without_audio"
android:layout_width="match_parent"
android:layout_height="29dp"
android:layout_marginTop="12dp"
android:paddingTop="3dp"
android:paddingBottom="3dp"
android:src="@drawable/status_ball_record_three" />
<ImageView
android:id="@+id/ic_screenshot"
android:layout_width="match_parent"
android:layout_height="29dp"
android:layout_marginTop="12dp"
android:paddingTop="3dp"
android:paddingBottom="3dp"
android:src="@drawable/ball_screenshot" />
<ImageView
android:id="@+id/ic_home"
android:layout_width="match_parent"
android:layout_height="29dp"
android:layout_marginTop="12dp"
android:paddingTop="3dp"
android:paddingBottom="3dp"
android:src="@drawable/ball_home" />
</LinearLayout>
</FrameLayout>

View File

@ -6,4 +6,13 @@
</style>
<style name="Theme.RecordScreen" parent="Base.Theme.RecordScreen" />
<style name="Theme.Transparent" parent="Theme.AppCompat.NoActionBar">
<item name="android:windowBackground">@color/color_transparent</item>
<item name="android:windowIsTranslucent">true</item>
<item name="android:windowNoTitle">true</item>
<item name="android:backgroundDimEnabled">false</item>
</style>
</resources>