From d8180a1ff6ce00cacdb79962f08f09fe8e7092ff Mon Sep 17 00:00:00 2001 From: litingting Date: Thu, 19 Jun 2025 18:20:08 +0800 Subject: [PATCH] =?UTF-8?q?=E6=82=AC=E6=B5=AE=E7=AA=97=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/src/main/AndroidManifest.xml | 7 + .../test/activity/ScreenPermissionActivity.kt | 73 ++++ .../screen/test/adapter/ViewPager2Adapter.kt | 4 +- ...orderFragment.kt => RecordMainFragment.kt} | 7 +- .../fragment/child/RecordNormalFragment.kt | 68 +++- .../screen/test/service/FloatingCallback.kt | 2 + .../test/service/FloatingWindowBridge.kt | 1 + .../record/screen/test/service/RecordState.kt | 7 + .../test/service/ScreenRecordService.kt | 343 +++++++++++++++--- .../record/screen/test/tool/ConstValue.kt | 26 ++ .../audio/record/screen/test/tool/Extend.kt | 10 + .../screen/test/tool/ScreenCaptureHelper.kt | 2 +- .../main/res/drawable/ball_audio_record.xml | 12 + .../res/drawable/ball_continue_recording.xml | 9 + app/src/main/res/drawable/ball_home.xml | 17 + app/src/main/res/drawable/ball_pause.xml | 12 + app/src/main/res/drawable/ball_recording.xml | 9 + app/src/main/res/drawable/ball_screenshot.xml | 15 + .../drawable/ball_without_audio_record.xml | 23 ++ app/src/main/res/drawable/bg_ball.xml | 9 + .../main/res/drawable/recording_bg_oval.xml | 7 + .../main/res/drawable/recording_oval_red.xml | 7 + .../main/res/drawable/status_ball_record.xml | 5 + .../res/drawable/status_ball_record_three.xml | 14 + app/src/main/res/layout/floating_ball.xml | 25 ++ .../main/res/layout/floating_ball_expand.xml | 60 +++ app/src/main/res/values/themes.xml | 9 + 27 files changed, 719 insertions(+), 64 deletions(-) create mode 100644 app/src/main/java/com/audio/record/screen/test/activity/ScreenPermissionActivity.kt rename app/src/main/java/com/audio/record/screen/test/fragment/{RecorderFragment.kt => RecordMainFragment.kt} (83%) create mode 100644 app/src/main/java/com/audio/record/screen/test/service/RecordState.kt create mode 100644 app/src/main/java/com/audio/record/screen/test/tool/ConstValue.kt create mode 100644 app/src/main/res/drawable/ball_audio_record.xml create mode 100644 app/src/main/res/drawable/ball_continue_recording.xml create mode 100644 app/src/main/res/drawable/ball_home.xml create mode 100644 app/src/main/res/drawable/ball_pause.xml create mode 100644 app/src/main/res/drawable/ball_recording.xml create mode 100644 app/src/main/res/drawable/ball_screenshot.xml create mode 100644 app/src/main/res/drawable/ball_without_audio_record.xml create mode 100644 app/src/main/res/drawable/bg_ball.xml create mode 100644 app/src/main/res/drawable/recording_bg_oval.xml create mode 100644 app/src/main/res/drawable/recording_oval_red.xml create mode 100644 app/src/main/res/drawable/status_ball_record.xml create mode 100644 app/src/main/res/drawable/status_ball_record_three.xml create mode 100644 app/src/main/res/layout/floating_ball_expand.xml diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 702c638..4a5bf1f 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -59,6 +59,13 @@ + + + //录音权限 + private lateinit var micLauncher: ActivityResultLauncher + 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) + } + + + +} \ No newline at end of file diff --git a/app/src/main/java/com/audio/record/screen/test/adapter/ViewPager2Adapter.kt b/app/src/main/java/com/audio/record/screen/test/adapter/ViewPager2Adapter.kt index 3984667..e48fa28 100644 --- a/app/src/main/java/com/audio/record/screen/test/adapter/ViewPager2Adapter.kt +++ b/app/src/main/java/com/audio/record/screen/test/adapter/ViewPager2Adapter.kt @@ -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 = arrayListOf( MainFragment.newInstance(), - RecorderFragment.newInstance(), + RecordMainFragment.newInstance(), MainFragment.newInstance() ) override fun getItemCount(): Int = fragments.size diff --git a/app/src/main/java/com/audio/record/screen/test/fragment/RecorderFragment.kt b/app/src/main/java/com/audio/record/screen/test/fragment/RecordMainFragment.kt similarity index 83% rename from app/src/main/java/com/audio/record/screen/test/fragment/RecorderFragment.kt rename to app/src/main/java/com/audio/record/screen/test/fragment/RecordMainFragment.kt index 365308d..7c7bb0b 100644 --- a/app/src/main/java/com/audio/record/screen/test/fragment/RecorderFragment.kt +++ b/app/src/main/java/com/audio/record/screen/test/fragment/RecordMainFragment.kt @@ -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() { +class RecordMainFragment : BaseFragment() { companion object { @JvmStatic fun newInstance() = - RecorderFragment().apply { + RecordMainFragment().apply { } } diff --git a/app/src/main/java/com/audio/record/screen/test/fragment/child/RecordNormalFragment.kt b/app/src/main/java/com/audio/record/screen/test/fragment/child/RecordNormalFragment.kt index 0222f9a..55dff58 100644 --- a/app/src/main/java/com/audio/record/screen/test/fragment/child/RecordNormalFragment.kt +++ b/app/src/main/java/com/audio/record/screen/test/fragment/child/RecordNormalFragment.kt @@ -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(), 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(), 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(), 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(), 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(), 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(), 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(), Floati } override fun onStartRecording() { - navController.navigate(R.id.action_to_recording) + 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) + } + } + } + + } diff --git a/app/src/main/java/com/audio/record/screen/test/service/FloatingCallback.kt b/app/src/main/java/com/audio/record/screen/test/service/FloatingCallback.kt index c3fe21b..550877e 100644 --- a/app/src/main/java/com/audio/record/screen/test/service/FloatingCallback.kt +++ b/app/src/main/java/com/audio/record/screen/test/service/FloatingCallback.kt @@ -7,4 +7,6 @@ interface FloatingCallback { fun onUpdateRecordTime(time:String){} fun onStopRecord(){} + + } diff --git a/app/src/main/java/com/audio/record/screen/test/service/FloatingWindowBridge.kt b/app/src/main/java/com/audio/record/screen/test/service/FloatingWindowBridge.kt index 4271e07..6e84a13 100644 --- a/app/src/main/java/com/audio/record/screen/test/service/FloatingWindowBridge.kt +++ b/app/src/main/java/com/audio/record/screen/test/service/FloatingWindowBridge.kt @@ -80,6 +80,7 @@ object FloatingWindowBridge { } fun sendCommand(command: String, countdownValues: Array? = null) { + Common.showLog("-------------command=${command}") when (command) { COMMEND_hide_camera -> service?.hideFrontCamera() COMMEND_show_camera -> service?.showCameraView() diff --git a/app/src/main/java/com/audio/record/screen/test/service/RecordState.kt b/app/src/main/java/com/audio/record/screen/test/service/RecordState.kt new file mode 100644 index 0000000..b61f47b --- /dev/null +++ b/app/src/main/java/com/audio/record/screen/test/service/RecordState.kt @@ -0,0 +1,7 @@ +package com.audio.record.screen.test.service + + +enum class RecordState { + DEFAULT, + RECORDING, + PAUSE } \ No newline at end of file diff --git a/app/src/main/java/com/audio/record/screen/test/service/ScreenRecordService.kt b/app/src/main/java/com/audio/record/screen/test/service/ScreenRecordService.kt index bb1f53a..1df6a64 100644 --- a/app/src/main/java/com/audio/record/screen/test/service/ScreenRecordService.kt +++ b/app/src/main/java/com/audio/record/screen/test/service/ScreenRecordService.kt @@ -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>() + 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) - } - fun resetIntent(){ - mIntent = null + 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 + } } @@ -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(R.id.layout_recording) + imIconVideo = ballView!!.findViewById(R.id.image) + smallTvTime = ballView!!.findViewById(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(R.id.ic_record) + tvRecordTime = ballViewExpand!!.findViewById(R.id.tv_record_time) + imNoAudioRecord = ballViewExpand!!.findViewById(R.id.ic_without_audio) + imScreenshot = ballViewExpand!!.findViewById(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(R.id.close).setOnClickListener { - hideRecordView() - } - recordView!!.findViewById(R.id.layout_video).setOnClickListener { + val imClose = + recordCompleteView!!.findViewById(R.id.close).setOnClickListener { + hideRecordCompleteView() + } + recordCompleteView!!.findViewById(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(R.id.image) - + val thumb = recordCompleteView!!.findViewById(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) + if (isWithAudio) { + setAudioEncoder(MediaRecorder.AudioEncoder.AAC) + } setVideoEncoder(MediaRecorder.VideoEncoder.H264) - setAudioEncoder(MediaRecorder.AudioEncoder.AAC) + 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) + + } + } \ No newline at end of file diff --git a/app/src/main/java/com/audio/record/screen/test/tool/ConstValue.kt b/app/src/main/java/com/audio/record/screen/test/tool/ConstValue.kt new file mode 100644 index 0000000..be7ea50 --- /dev/null +++ b/app/src/main/java/com/audio/record/screen/test/tool/ConstValue.kt @@ -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 //录制完成 +} \ No newline at end of file diff --git a/app/src/main/java/com/audio/record/screen/test/tool/Extend.kt b/app/src/main/java/com/audio/record/screen/test/tool/Extend.kt index 7ef813c..48f05b6 100644 --- a/app/src/main/java/com/audio/record/screen/test/tool/Extend.kt +++ b/app/src/main/java/com/audio/record/screen/test/tool/Extend.kt @@ -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 + } } \ No newline at end of file diff --git a/app/src/main/java/com/audio/record/screen/test/tool/ScreenCaptureHelper.kt b/app/src/main/java/com/audio/record/screen/test/tool/ScreenCaptureHelper.kt index 0381c1a..1ccf91d 100644 --- a/app/src/main/java/com/audio/record/screen/test/tool/ScreenCaptureHelper.kt +++ b/app/src/main/java/com/audio/record/screen/test/tool/ScreenCaptureHelper.kt @@ -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() diff --git a/app/src/main/res/drawable/ball_audio_record.xml b/app/src/main/res/drawable/ball_audio_record.xml new file mode 100644 index 0000000..0a0a8c2 --- /dev/null +++ b/app/src/main/res/drawable/ball_audio_record.xml @@ -0,0 +1,12 @@ + + + + diff --git a/app/src/main/res/drawable/ball_continue_recording.xml b/app/src/main/res/drawable/ball_continue_recording.xml new file mode 100644 index 0000000..47420af --- /dev/null +++ b/app/src/main/res/drawable/ball_continue_recording.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ball_home.xml b/app/src/main/res/drawable/ball_home.xml new file mode 100644 index 0000000..c56d94b --- /dev/null +++ b/app/src/main/res/drawable/ball_home.xml @@ -0,0 +1,17 @@ + + + + + + + + + diff --git a/app/src/main/res/drawable/ball_pause.xml b/app/src/main/res/drawable/ball_pause.xml new file mode 100644 index 0000000..42991e7 --- /dev/null +++ b/app/src/main/res/drawable/ball_pause.xml @@ -0,0 +1,12 @@ + + + + diff --git a/app/src/main/res/drawable/ball_recording.xml b/app/src/main/res/drawable/ball_recording.xml new file mode 100644 index 0000000..544a172 --- /dev/null +++ b/app/src/main/res/drawable/ball_recording.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ball_screenshot.xml b/app/src/main/res/drawable/ball_screenshot.xml new file mode 100644 index 0000000..87abd1a --- /dev/null +++ b/app/src/main/res/drawable/ball_screenshot.xml @@ -0,0 +1,15 @@ + + + + + diff --git a/app/src/main/res/drawable/ball_without_audio_record.xml b/app/src/main/res/drawable/ball_without_audio_record.xml new file mode 100644 index 0000000..ee8da10 --- /dev/null +++ b/app/src/main/res/drawable/ball_without_audio_record.xml @@ -0,0 +1,23 @@ + + + + + + + + + + + diff --git a/app/src/main/res/drawable/bg_ball.xml b/app/src/main/res/drawable/bg_ball.xml new file mode 100644 index 0000000..ebb411b --- /dev/null +++ b/app/src/main/res/drawable/bg_ball.xml @@ -0,0 +1,9 @@ + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/recording_bg_oval.xml b/app/src/main/res/drawable/recording_bg_oval.xml new file mode 100644 index 0000000..2fd72a3 --- /dev/null +++ b/app/src/main/res/drawable/recording_bg_oval.xml @@ -0,0 +1,7 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/recording_oval_red.xml b/app/src/main/res/drawable/recording_oval_red.xml new file mode 100644 index 0000000..fea773e --- /dev/null +++ b/app/src/main/res/drawable/recording_oval_red.xml @@ -0,0 +1,7 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/status_ball_record.xml b/app/src/main/res/drawable/status_ball_record.xml new file mode 100644 index 0000000..d359667 --- /dev/null +++ b/app/src/main/res/drawable/status_ball_record.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/status_ball_record_three.xml b/app/src/main/res/drawable/status_ball_record_three.xml new file mode 100644 index 0000000..4c92fa6 --- /dev/null +++ b/app/src/main/res/drawable/status_ball_record_three.xml @@ -0,0 +1,14 @@ + + + + + + + + + + + + diff --git a/app/src/main/res/layout/floating_ball.xml b/app/src/main/res/layout/floating_ball.xml index 991260b..202dff4 100644 --- a/app/src/main/res/layout/floating_ball.xml +++ b/app/src/main/res/layout/floating_ball.xml @@ -9,4 +9,29 @@ android:layout_height="36dp" android:src="@drawable/global_ball" /> + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/floating_ball_expand.xml b/app/src/main/res/layout/floating_ball_expand.xml new file mode 100644 index 0000000..f08d6b7 --- /dev/null +++ b/app/src/main/res/layout/floating_ball_expand.xml @@ -0,0 +1,60 @@ + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/values/themes.xml b/app/src/main/res/values/themes.xml index 16dcb64..099924a 100644 --- a/app/src/main/res/values/themes.xml +++ b/app/src/main/res/values/themes.xml @@ -6,4 +6,13 @@ + \ No newline at end of file