init
This commit is contained in:
commit
481452e688
15
.gitignore
vendored
Normal file
15
.gitignore
vendored
Normal file
@ -0,0 +1,15 @@
|
||||
*.iml
|
||||
.gradle
|
||||
/local.properties
|
||||
/.idea/caches
|
||||
/.idea/libraries
|
||||
/.idea/modules.xml
|
||||
/.idea/workspace.xml
|
||||
/.idea/navEditor.xml
|
||||
/.idea/assetWizardSettings.xml
|
||||
.DS_Store
|
||||
/build
|
||||
/captures
|
||||
.externalNativeBuild
|
||||
.cxx
|
||||
local.properties
|
||||
1
app/.gitignore
vendored
Normal file
1
app/.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
||||
/build
|
||||
86
app/build.gradle.kts
Normal file
86
app/build.gradle.kts
Normal file
@ -0,0 +1,86 @@
|
||||
plugins {
|
||||
id("com.android.application")
|
||||
id("org.jetbrains.kotlin.android")
|
||||
}
|
||||
|
||||
android {
|
||||
namespace = "com.audio.record.screen.test"
|
||||
compileSdk = 35
|
||||
|
||||
defaultConfig {
|
||||
applicationId = "com.audio.record.screen.test"
|
||||
minSdk = 24
|
||||
targetSdk = 35
|
||||
versionCode = 1
|
||||
versionName = "1.0"
|
||||
|
||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
release {
|
||||
isMinifyEnabled = false
|
||||
proguardFiles(
|
||||
getDefaultProguardFile("proguard-android-optimize.txt"),
|
||||
"proguard-rules.pro"
|
||||
)
|
||||
}
|
||||
}
|
||||
compileOptions {
|
||||
sourceCompatibility = JavaVersion.VERSION_1_8
|
||||
targetCompatibility = JavaVersion.VERSION_1_8
|
||||
}
|
||||
kotlinOptions {
|
||||
jvmTarget = "1.8"
|
||||
}
|
||||
buildFeatures{
|
||||
viewBinding = true
|
||||
}
|
||||
externalNativeBuild {
|
||||
cmake {
|
||||
version = "3.22.1"
|
||||
}
|
||||
}
|
||||
// repositories {
|
||||
// flatDir {
|
||||
// dirs("libs")
|
||||
// }
|
||||
// }
|
||||
}
|
||||
|
||||
dependencies {
|
||||
|
||||
implementation("androidx.core:core-ktx:1.13.0")
|
||||
implementation("androidx.appcompat:appcompat:1.7.0")
|
||||
implementation("com.google.android.material:material:1.12.0")
|
||||
implementation("androidx.constraintlayout:constraintlayout:2.2.1")
|
||||
|
||||
|
||||
|
||||
implementation ("androidx.camera:camera-core:1.4.2")
|
||||
implementation ("androidx.camera:camera-camera2:1.4.2")
|
||||
implementation ("androidx.camera:camera-lifecycle:1.4.2")
|
||||
implementation ("androidx.camera:camera-view:1.4.2")
|
||||
|
||||
// implementation ("com.arthenica:ffmpeg-kit-full:4.5.1")
|
||||
// implementation ("com.arthenica:ffmpeg-kit-full:6.0")
|
||||
|
||||
// implementation ("com.arthenica:ffmpeg-kit-full-gpl-6.0")
|
||||
|
||||
implementation ("androidx.media3:media3-exoplayer:1.6.1")
|
||||
implementation ("androidx.media3:media3-ui:1.6.1")
|
||||
|
||||
implementation("com.github.bumptech.glide:glide:4.16.0")
|
||||
|
||||
implementation ("com.github.Jay-Goo:RangeSeekBar:v3.0.0")
|
||||
|
||||
implementation("androidx.navigation:navigation-ui-ktx:2.8.9")
|
||||
implementation("androidx.navigation:navigation-fragment-ktx:2.8.9")
|
||||
|
||||
implementation("com.github.bumptech.glide:glide:4.16.0")
|
||||
|
||||
implementation(files("libs/jetified-ffmpeg-kit-full-6.0.aar"))
|
||||
implementation(files("libs/smart-exception-common-0.2.1.jar"))
|
||||
implementation(files("libs/smart-exception-java-0.2.1.jar"))
|
||||
|
||||
}
|
||||
BIN
app/libs/jetified-ffmpeg-kit-full-6.0.aar
Normal file
BIN
app/libs/jetified-ffmpeg-kit-full-6.0.aar
Normal file
Binary file not shown.
BIN
app/libs/smart-exception-common-0.2.1.jar
Normal file
BIN
app/libs/smart-exception-common-0.2.1.jar
Normal file
Binary file not shown.
BIN
app/libs/smart-exception-java-0.2.1.jar
Normal file
BIN
app/libs/smart-exception-java-0.2.1.jar
Normal file
Binary file not shown.
21
app/proguard-rules.pro
vendored
Normal file
21
app/proguard-rules.pro
vendored
Normal file
@ -0,0 +1,21 @@
|
||||
# Add project specific ProGuard rules here.
|
||||
# You can control the set of applied configuration files using the
|
||||
# proguardFiles setting in build.gradle.
|
||||
#
|
||||
# For more details, see
|
||||
# http://developer.android.com/guide/developing/tools/proguard.html
|
||||
|
||||
# If your project uses WebView with JS, uncomment the following
|
||||
# and specify the fully qualified class name to the JavaScript interface
|
||||
# class:
|
||||
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
|
||||
# public *;
|
||||
#}
|
||||
|
||||
# Uncomment this to preserve the line number information for
|
||||
# debugging stack traces.
|
||||
#-keepattributes SourceFile,LineNumberTable
|
||||
|
||||
# If you keep the line number information, uncomment this to
|
||||
# hide the original source file name.
|
||||
#-renamesourcefileattribute SourceFile
|
||||
@ -0,0 +1,24 @@
|
||||
package com.audio.record.screen.test
|
||||
|
||||
import androidx.test.platform.app.InstrumentationRegistry
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
|
||||
import org.junit.Assert.*
|
||||
|
||||
/**
|
||||
* Instrumented test, which will execute on an Android device.
|
||||
*
|
||||
* See [testing documentation](http://d.android.com/tools/testing).
|
||||
*/
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
class ExampleInstrumentedTest {
|
||||
@Test
|
||||
fun useAppContext() {
|
||||
// Context of the app under test.
|
||||
val appContext = InstrumentationRegistry.getInstrumentation().targetContext
|
||||
assertEquals("com.audio.record.screen.test", appContext.packageName)
|
||||
}
|
||||
}
|
||||
79
app/src/main/AndroidManifest.xml
Normal file
79
app/src/main/AndroidManifest.xml
Normal file
@ -0,0 +1,79 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools">
|
||||
|
||||
<uses-feature
|
||||
android:name="android.hardware.camera"
|
||||
android:required="false" /> <!-- 通知 -->
|
||||
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" /> <!-- 前台服务 -->
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_MEDIA_PROJECTION" /> <!-- 录音 -->
|
||||
<uses-permission android:name="android.permission.RECORD_AUDIO" /> <!-- 相机 -->
|
||||
<uses-permission android:name="android.permission.CAMERA" /> <!-- 悬浮窗 -->
|
||||
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />
|
||||
<uses-permission
|
||||
android:name="android.permission.WRITE_EXTERNAL_STORAGE"
|
||||
android:maxSdkVersion="32"
|
||||
tools:ignore="ScopedStorage" />
|
||||
<uses-permission
|
||||
android:name="android.permission.READ_EXTERNAL_STORAGE"
|
||||
android:maxSdkVersion="32" />
|
||||
|
||||
<application
|
||||
android:name=".App"
|
||||
android:allowBackup="true"
|
||||
android:dataExtractionRules="@xml/data_extraction_rules"
|
||||
android:fullBackupContent="@xml/backup_rules"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:label="@string/app_name"
|
||||
android:roundIcon="@mipmap/ic_launcher_round"
|
||||
android:supportsRtl="true"
|
||||
android:theme="@style/Theme.RecordScreen"
|
||||
tools:targetApi="31">
|
||||
<activity
|
||||
android:name=".activity.PlayActivity"
|
||||
android:exported="false" />
|
||||
<activity
|
||||
android:name=".activity.ImageViewActivity"
|
||||
android:exported="false" />
|
||||
<activity
|
||||
android:name=".activity.PreviewActivity"
|
||||
android:exported="false" />
|
||||
<activity
|
||||
android:name=".activity.MainActivity1"
|
||||
android:exported="true">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
<activity
|
||||
android:name=".activity.MainActivity"
|
||||
android:exported="true">
|
||||
|
||||
<!-- <intent-filter> -->
|
||||
<!-- <action android:name="android.intent.action.MAIN" /> -->
|
||||
|
||||
|
||||
<!-- <category android:name="android.intent.category.LAUNCHER" /> -->
|
||||
<!-- </intent-filter> -->
|
||||
</activity>
|
||||
|
||||
<service
|
||||
android:name=".service.ScreenRecordService"
|
||||
android:exported="false"
|
||||
android:foregroundServiceType="mediaProjection" />
|
||||
|
||||
<provider
|
||||
android:name="androidx.core.content.FileProvider"
|
||||
android:authorities="com.audio.record.screen.test.fileprovider"
|
||||
android:exported="false"
|
||||
android:grantUriPermissions="true">
|
||||
<meta-data
|
||||
android:name="android.support.FILE_PROVIDER_PATHS"
|
||||
android:resource="@xml/file_paths" />
|
||||
</provider>
|
||||
</application>
|
||||
|
||||
</manifest>
|
||||
17
app/src/main/java/com/audio/record/screen/test/App.kt
Normal file
17
app/src/main/java/com/audio/record/screen/test/App.kt
Normal file
@ -0,0 +1,17 @@
|
||||
package com.audio.record.screen.test
|
||||
|
||||
import android.app.Application
|
||||
|
||||
class App : Application() {
|
||||
|
||||
companion object{
|
||||
val TAG = "-------Screen--------"
|
||||
lateinit var instanceApp:App
|
||||
|
||||
}
|
||||
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
instanceApp = this
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,60 @@
|
||||
package com.audio.record.screen.test.activity
|
||||
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import androidx.core.view.isVisible
|
||||
import com.audio.record.screen.test.base.BaseActivity
|
||||
import com.audio.record.screen.test.databinding.ActivityImageViewBinding
|
||||
import com.audio.record.screen.test.tool.Common
|
||||
import com.bumptech.glide.Glide
|
||||
|
||||
/**
|
||||
* 图片预览页面
|
||||
*/
|
||||
class ImageViewActivity : BaseActivity<ActivityImageViewBinding>() {
|
||||
|
||||
companion object {
|
||||
val KEY_URI = "uri_key"
|
||||
val KEY_name = "name"
|
||||
}
|
||||
|
||||
|
||||
private var mUri: Uri? = null
|
||||
private var displayName: String? = null
|
||||
override fun initBinding(): ActivityImageViewBinding =
|
||||
ActivityImageViewBinding.inflate(layoutInflater)
|
||||
|
||||
override fun getFullColor(): Boolean? = null
|
||||
override fun onInitPadding(): Boolean = false
|
||||
override fun onSetViewBefore() {
|
||||
}
|
||||
|
||||
override fun onCreateInit() {
|
||||
Common.enableImmersiveMode(this)
|
||||
mUri = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||
intent.getParcelableExtra(KEY_URI, Uri::class.java)
|
||||
} else {
|
||||
intent.getParcelableExtra(KEY_URI)
|
||||
}
|
||||
displayName = intent.getStringExtra(KEY_name)
|
||||
initView()
|
||||
|
||||
}
|
||||
|
||||
|
||||
private fun initView() {
|
||||
binding.run {
|
||||
back.setOnClickListener {
|
||||
finish()
|
||||
}
|
||||
textName.text = displayName
|
||||
Glide.with(this@ImageViewActivity)
|
||||
.load(mUri)
|
||||
.into(imgView)
|
||||
imgView.setOnClickListener {
|
||||
layoutName.isVisible = !layoutName.isVisible
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,286 @@
|
||||
package com.audio.record.screen.test.activity
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.pm.PackageManager
|
||||
import android.hardware.display.DisplayManager
|
||||
import android.media.MediaRecorder
|
||||
import android.media.projection.MediaProjection
|
||||
import android.media.projection.MediaProjectionManager
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.provider.Settings
|
||||
import androidx.activity.result.ActivityResultLauncher
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.core.content.ContextCompat
|
||||
import com.audio.record.screen.test.base.BaseActivity
|
||||
import com.audio.record.screen.test.databinding.ActivityMainBinding
|
||||
import com.audio.record.screen.test.service.FloatingWindowBridge
|
||||
import com.audio.record.screen.test.tool.Common
|
||||
import com.audio.record.screen.test.tool.ScreenCaptureHelper
|
||||
import com.audio.record.screen.test.tool.VideoFileHelper
|
||||
|
||||
class MainActivity : BaseActivity<ActivityMainBinding>() {
|
||||
val NOTIFICATION_PERMISSION_REQUEST_CODE = 123
|
||||
val SCREEN_CAPTURE_REQUEST_CODE = 124
|
||||
val REQUEST_SCREENSHOT = 125
|
||||
|
||||
lateinit var requestPermissionLauncher: ActivityResultLauncher<String>
|
||||
lateinit var micPermissionLauncher: ActivityResultLauncher<String>
|
||||
lateinit var cameraPermissionLauncher: ActivityResultLauncher<String>
|
||||
lateinit var requestStoragePermission: ActivityResultLauncher<String>
|
||||
|
||||
lateinit var mediaProjectionManager: MediaProjectionManager
|
||||
// private var floatingService: ScreenRecordService? = null
|
||||
|
||||
private lateinit var tmpVideoUri:Uri
|
||||
|
||||
// private val connection = object : ServiceConnection {
|
||||
// override fun onServiceConnected(name: ComponentName?, service: IBinder?) {
|
||||
// val binder = service as ScreenRecordService.FloatingBinder
|
||||
// floatingService = binder.getService()
|
||||
// binder.setCallback(object : FloatingCallback {
|
||||
// override fun onFloatingButtonClicked(action: String) {
|
||||
// // 处理来自悬浮窗的点击
|
||||
// when (action) {
|
||||
// "stop_clicked" -> {
|
||||
//
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// })
|
||||
// }
|
||||
//
|
||||
// override fun onServiceDisconnected(name: ComponentName?) {
|
||||
// floatingService = null
|
||||
// }
|
||||
// }
|
||||
|
||||
|
||||
override fun initBinding(): ActivityMainBinding = ActivityMainBinding.inflate(layoutInflater)
|
||||
|
||||
override fun getFullColor(): Boolean? = true
|
||||
|
||||
override fun onCreateInit() {
|
||||
initPermissionLauncher()
|
||||
// checkSyswindow(this@MainActivity)
|
||||
checkStoragePermissionAndDoSomething()
|
||||
|
||||
|
||||
binding.btn1.setOnClickListener {
|
||||
requestNotification()
|
||||
}
|
||||
binding.btn2.setOnClickListener {
|
||||
checkStoragePermissionAndDoSomething()
|
||||
}
|
||||
binding.btn3.setOnClickListener {
|
||||
micPermissionLauncher.launch(android.Manifest.permission.RECORD_AUDIO)
|
||||
}
|
||||
binding.btn4.setOnClickListener {
|
||||
checkCamera()
|
||||
}
|
||||
binding.btnShowcamera.setOnClickListener {
|
||||
it.isSelected = !it.isSelected
|
||||
if(it.isSelected){
|
||||
FloatingWindowBridge.sendCommand("show")
|
||||
}else{
|
||||
FloatingWindowBridge.sendCommand("hide")
|
||||
}
|
||||
|
||||
}
|
||||
binding.btn7.setOnClickListener {
|
||||
startActivity(Intent(this,PreviewActivity::class.java))
|
||||
|
||||
}
|
||||
|
||||
mediaProjectionManager =
|
||||
getSystemService(Context.MEDIA_PROJECTION_SERVICE) as MediaProjectionManager
|
||||
binding.btn5.setOnClickListener {
|
||||
startActivityForResult(mediaProjectionManager.createScreenCaptureIntent(), SCREEN_CAPTURE_REQUEST_CODE)
|
||||
}
|
||||
binding.btn6.setOnClickListener {
|
||||
stopRecording()
|
||||
}
|
||||
|
||||
binding.btnScreenshot.setOnClickListener {
|
||||
|
||||
startActivityForResult( mediaProjectionManager.createScreenCaptureIntent(), REQUEST_SCREENSHOT)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onInitPadding(): Boolean = true
|
||||
|
||||
|
||||
private fun initPermissionLauncher() {
|
||||
requestPermissionLauncher =
|
||||
registerForActivityResult(ActivityResultContracts.RequestPermission()) { isGranted ->
|
||||
if (isGranted) {
|
||||
startForegroundService()
|
||||
Common.showLog("权限授予")
|
||||
} else {
|
||||
Common.showLog("权限拒绝")
|
||||
}
|
||||
}
|
||||
|
||||
micPermissionLauncher = registerForActivityResult(
|
||||
ActivityResultContracts.RequestPermission()
|
||||
) { isGranted ->
|
||||
if (isGranted) {
|
||||
Common.showLog("mic 权限授予")
|
||||
} else {
|
||||
Common.showLog("mic 权限拒绝")
|
||||
}
|
||||
}
|
||||
cameraPermissionLauncher = registerForActivityResult(
|
||||
ActivityResultContracts.RequestPermission()
|
||||
) { isGranted ->
|
||||
if (isGranted) {
|
||||
Common.showLog("CAMERA 权限授予")
|
||||
} else {
|
||||
Common.showLog("CAMERA 权限拒绝")
|
||||
}
|
||||
}
|
||||
requestStoragePermission = registerForActivityResult(
|
||||
ActivityResultContracts.RequestPermission()
|
||||
) { isGranted ->
|
||||
if (isGranted) {
|
||||
// 权限已授予
|
||||
Common.showLog("已获取存储权限")
|
||||
// 执行写入文件操作
|
||||
} else {
|
||||
Common.showLog( "存储权限被拒绝" )
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
fun checkStoragePermissionAndDoSomething() {
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q &&
|
||||
ContextCompat.checkSelfPermission(this, android.Manifest.permission.WRITE_EXTERNAL_STORAGE)
|
||||
!= PackageManager.PERMISSION_GRANTED
|
||||
) {
|
||||
requestStoragePermission.launch(android.Manifest.permission.WRITE_EXTERNAL_STORAGE)
|
||||
} else {
|
||||
// Android 10+ 不需要写权限,或者权限已获取
|
||||
// 执行写入文件操作
|
||||
Common.showLog("已获取存储权限")
|
||||
}
|
||||
}
|
||||
|
||||
private fun checkSyswindow(context: Context) {
|
||||
if (!Settings.canDrawOverlays(context)) {
|
||||
val intent = Intent(
|
||||
Settings.ACTION_MANAGE_OVERLAY_PERMISSION,
|
||||
Uri.parse("package:${context.packageName}")
|
||||
)
|
||||
startActivity(intent)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
private fun requestNotification() {
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||
if (ContextCompat.checkSelfPermission(
|
||||
this,
|
||||
android.Manifest.permission.POST_NOTIFICATIONS
|
||||
)
|
||||
!= PackageManager.PERMISSION_GRANTED
|
||||
) {
|
||||
requestPermissionLauncher.launch(android.Manifest.permission.POST_NOTIFICATIONS)
|
||||
} else {
|
||||
// 权限已授予,可以发送通知
|
||||
Common.showLog("权限已授予,可以发送通知")
|
||||
startForegroundService()
|
||||
}
|
||||
} else {
|
||||
// Android 12 及以下,不需要请求权限
|
||||
Common.showLog("不需要请求权限")
|
||||
startForegroundService()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private fun checkCamera() {
|
||||
if (ContextCompat.checkSelfPermission(
|
||||
this,
|
||||
android.Manifest.permission.CAMERA
|
||||
)
|
||||
!= PackageManager.PERMISSION_GRANTED
|
||||
) {
|
||||
cameraPermissionLauncher.launch(android.Manifest.permission.CAMERA)
|
||||
} else {
|
||||
Common.showLog("权限已授予 CAMERA")
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
||||
super.onActivityResult(requestCode, resultCode, data)
|
||||
val mediaProjection = mediaProjectionManager.getMediaProjection(resultCode, data!!)
|
||||
if (requestCode == SCREEN_CAPTURE_REQUEST_CODE && resultCode == RESULT_OK) {
|
||||
|
||||
startRecording(mediaProjection)
|
||||
}else if (requestCode == REQUEST_SCREENSHOT && resultCode == RESULT_OK && data != null) {
|
||||
// ScreenCaptureHelper.startScreenCapture(this,mediaProjection)
|
||||
}
|
||||
}
|
||||
|
||||
private fun startForegroundService() {
|
||||
// FloatingWindowBridge.startAndBindService(this)
|
||||
}
|
||||
|
||||
fun startRecording(mediaProjection: MediaProjection) {
|
||||
val screen = VideoFileHelper.getScreenInfo(this@MainActivity)
|
||||
val width = VideoFileHelper.alignTo16(screen.width)
|
||||
val height = VideoFileHelper.alignTo16(screen.height)
|
||||
initRecorder(width, height)
|
||||
mediaRecorder.start()
|
||||
|
||||
val virtualDisplay = mediaProjection.createVirtualDisplay(
|
||||
"ScreenRecord",
|
||||
width, height, resources.displayMetrics.densityDpi,
|
||||
DisplayManager.VIRTUAL_DISPLAY_FLAG_AUTO_MIRROR,
|
||||
mediaRecorder.surface, null, null
|
||||
)
|
||||
}
|
||||
|
||||
fun stopRecording() {
|
||||
Common.showLog("-------录屏完成.....")
|
||||
mediaRecorder.stop()
|
||||
mediaRecorder.reset()
|
||||
}
|
||||
|
||||
|
||||
private lateinit var mediaRecorder: MediaRecorder
|
||||
|
||||
fun initRecorder(width: Int, height: Int) {
|
||||
|
||||
val (videoUri, pfd) = VideoFileHelper.createVideoFile(this, Common.folderName)
|
||||
tmpVideoUri = videoUri
|
||||
|
||||
mediaRecorder = MediaRecorder().apply {
|
||||
setAudioSource(MediaRecorder.AudioSource.MIC)
|
||||
setVideoSource(MediaRecorder.VideoSource.SURFACE)
|
||||
setOutputFormat(MediaRecorder.OutputFormat.MPEG_4)
|
||||
setOutputFile(pfd?.fileDescriptor)
|
||||
setVideoSize(width, height)
|
||||
setVideoEncoder(MediaRecorder.VideoEncoder.H264)
|
||||
setAudioEncoder(MediaRecorder.AudioEncoder.AAC)
|
||||
setVideoEncodingBitRate(8 * 1000 * 1000)
|
||||
setVideoFrameRate(30)
|
||||
prepare()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
super.onDestroy()
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
@ -0,0 +1,229 @@
|
||||
package com.audio.record.screen.test.activity
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.pm.PackageManager
|
||||
import android.media.projection.MediaProjectionManager
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.provider.Settings
|
||||
import android.view.LayoutInflater
|
||||
import android.widget.ImageView
|
||||
import androidx.activity.result.ActivityResultLauncher
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.view.marginBottom
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import com.audio.record.screen.test.R
|
||||
import com.audio.record.screen.test.adapter.ViewPager2Adapter
|
||||
import com.audio.record.screen.test.base.BaseActivity
|
||||
import com.audio.record.screen.test.databinding.ActivityMain1Binding
|
||||
import com.audio.record.screen.test.dialog.DialogPermission
|
||||
import com.audio.record.screen.test.fragment.MainFragment
|
||||
import com.audio.record.screen.test.service.ConnectionListener
|
||||
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.Extend.setMarginBottom
|
||||
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 com.google.android.material.tabs.TabLayout
|
||||
import com.google.android.material.tabs.TabLayout.OnTabSelectedListener
|
||||
import com.google.android.material.tabs.TabLayoutMediator
|
||||
|
||||
|
||||
class MainActivity1 : BaseActivity<ActivityMain1Binding>(), ConnectionListener,FloatingCallback {
|
||||
private lateinit var viewModel: MainViewModel
|
||||
private var isNotification = false
|
||||
private var isOverlay = false
|
||||
private lateinit var requestStoragePermission: ActivityResultLauncher<String>
|
||||
private lateinit var requestNotificationLauncher: ActivityResultLauncher<String>
|
||||
private var mPermissionDialog: DialogPermission? = null
|
||||
|
||||
//截屏
|
||||
private lateinit var screenCaptureLauncher: ActivityResultLauncher<Intent>
|
||||
private lateinit var mediaProjectionManager: MediaProjectionManager
|
||||
|
||||
override fun onSetViewBefore() {
|
||||
super.onSetViewBefore()
|
||||
// val navigationBarHeight = Common.getNavigationBarHeight(this)
|
||||
Common.setNavigation(binding.tabLayout)
|
||||
}
|
||||
override fun initBinding(): ActivityMain1Binding = ActivityMain1Binding.inflate(layoutInflater)
|
||||
override fun onInitPadding(): Boolean = false
|
||||
override fun getFullColor(): Boolean = true
|
||||
override fun onCreateInit() {
|
||||
mediaProjectionManager =
|
||||
getSystemService(Context.MEDIA_PROJECTION_SERVICE) as MediaProjectionManager
|
||||
viewModel = ViewModelProvider(this)[MainViewModel::class.java]
|
||||
bingVp()
|
||||
initLauncher()
|
||||
firstCheck()
|
||||
checkStoragePermissionAndDoSomething()
|
||||
}
|
||||
|
||||
private fun initLauncher() {
|
||||
requestStoragePermission = registerForActivityResult(
|
||||
ActivityResultContracts.RequestPermission()
|
||||
) { isGranted ->
|
||||
if (isGranted) {
|
||||
// 权限已授予
|
||||
Common.showLog("已获取存储权限")
|
||||
showPermissionDialog()
|
||||
// 执行写入文件操作
|
||||
} else {
|
||||
Common.showLog("存储权限被拒绝")
|
||||
}
|
||||
}
|
||||
|
||||
requestNotificationLauncher =
|
||||
registerForActivityResult(ActivityResultContracts.RequestPermission()) { isGranted ->
|
||||
if (isGranted) {
|
||||
startForegroundService()
|
||||
Common.showLog("权限授予")
|
||||
} else {
|
||||
Common.showLog("权限拒绝")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// screenCaptureLauncher = registerForActivityResult(
|
||||
// ActivityResultContracts.StartActivityForResult()
|
||||
// ) { result ->
|
||||
// if (result.resultCode == Activity.RESULT_OK) {
|
||||
// val data: Intent? = result.data
|
||||
// val mediaProjection =
|
||||
// mediaProjectionManager.getMediaProjection(result.resultCode, data!!)
|
||||
// ScreenCaptureHelper.startScreenCapture(this, mediaProjection)
|
||||
// } else {
|
||||
// Common.showLog("用户取消了录屏权限授权")
|
||||
// }
|
||||
// }
|
||||
}
|
||||
|
||||
fun checkStoragePermissionAndDoSomething() {
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q &&
|
||||
ContextCompat.checkSelfPermission(
|
||||
this,
|
||||
android.Manifest.permission.WRITE_EXTERNAL_STORAGE
|
||||
)
|
||||
!= PackageManager.PERMISSION_GRANTED
|
||||
) {
|
||||
requestStoragePermission.launch(android.Manifest.permission.WRITE_EXTERNAL_STORAGE)
|
||||
} else {
|
||||
// Android 10+ 不需要写权限,或者权限已获取
|
||||
// 执行写入文件操作
|
||||
Common.showLog("已获取存储权限")
|
||||
showPermissionDialog()
|
||||
}
|
||||
}
|
||||
|
||||
private fun bingVp() {
|
||||
val tabIcons =
|
||||
intArrayOf(R.drawable.tab1_selector, R.drawable.tab2_selector, R.drawable.tab3_selector)
|
||||
binding.run {
|
||||
viewPager2.isUserInputEnabled = false
|
||||
viewPager2.adapter = ViewPager2Adapter(this@MainActivity1)
|
||||
TabLayoutMediator(tabLayout, viewPager2) { tab, position ->
|
||||
val customView =
|
||||
LayoutInflater.from(this@MainActivity1).inflate(R.layout.tab_item, null)
|
||||
val icon = customView.findViewById<ImageView>(R.id.tab_icon)
|
||||
|
||||
if (position in tabIcons.indices) {
|
||||
icon.setImageResource(tabIcons[position])
|
||||
}
|
||||
icon.isSelected = position == 0
|
||||
tab.customView = customView
|
||||
}.attach()
|
||||
tabLayout.addOnTabSelectedListener(object : OnTabSelectedListener {
|
||||
override fun onTabSelected(tab: TabLayout.Tab?) {
|
||||
tab?.customView?.run {
|
||||
findViewById<ImageView>(R.id.tab_icon).isSelected = true
|
||||
}
|
||||
}
|
||||
|
||||
override fun onTabUnselected(tab: TabLayout.Tab?) {
|
||||
tab?.customView?.run {
|
||||
findViewById<ImageView>(R.id.tab_icon).isSelected = false
|
||||
}
|
||||
}
|
||||
|
||||
override fun onTabReselected(tab: TabLayout.Tab?) {
|
||||
|
||||
}
|
||||
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private fun showPermissionDialog() {
|
||||
if (isNotification && isOverlay) {
|
||||
return
|
||||
}
|
||||
mPermissionDialog = mPermissionDialog ?: DialogPermission {
|
||||
when (it) {
|
||||
DialogPermission.type_ball -> {
|
||||
intentSysWindow(this@MainActivity1)
|
||||
}
|
||||
|
||||
DialogPermission.type_notification -> {
|
||||
requestNotificationLauncher.launch(android.Manifest.permission.POST_NOTIFICATIONS)
|
||||
}
|
||||
}
|
||||
}
|
||||
mPermissionDialog?.show(supportFragmentManager, "")
|
||||
}
|
||||
|
||||
|
||||
//悬浮窗
|
||||
private fun intentSysWindow(context: Context) {
|
||||
val intent = Intent(
|
||||
Settings.ACTION_MANAGE_OVERLAY_PERMISSION,
|
||||
Uri.parse("package:${context.packageName}")
|
||||
)
|
||||
startActivity(intent)
|
||||
}
|
||||
|
||||
//检查通知和悬浮窗是否已经enable
|
||||
private fun firstCheck() {
|
||||
Permission.checkNotification(this@MainActivity1) {
|
||||
isNotification = it
|
||||
if (it) {
|
||||
startForegroundService()
|
||||
}
|
||||
}
|
||||
Permission.checkOvalApp(this@MainActivity1) {
|
||||
isOverlay = it
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private fun startForegroundService() {
|
||||
FloatingWindowBridge.startAndBindService(this@MainActivity1)
|
||||
FloatingWindowBridge.addListener(this)
|
||||
}
|
||||
|
||||
override fun onStartRecording() {
|
||||
this@MainActivity1.moveTaskToBack(true)
|
||||
|
||||
}
|
||||
|
||||
override fun onServiceConnected() {
|
||||
Common.showLog(" MainActivity1 onServiceConnected registerCallback")
|
||||
viewModel.updateServiceConnectStatus(true)
|
||||
FloatingWindowBridge.registerCallback(this)
|
||||
}
|
||||
|
||||
|
||||
override fun onDestroy() {
|
||||
super.onDestroy()
|
||||
FloatingWindowBridge.unregisterCallback(this)
|
||||
FloatingWindowBridge.removeListener(this)
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,124 @@
|
||||
package com.audio.record.screen.test.activity
|
||||
|
||||
import android.graphics.Bitmap
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.view.ViewGroup
|
||||
import android.widget.ImageButton
|
||||
import android.widget.ImageView
|
||||
import android.widget.RelativeLayout
|
||||
import android.widget.TextView
|
||||
import androidx.annotation.OptIn
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.media3.common.MediaItem
|
||||
import androidx.media3.common.Player
|
||||
import androidx.media3.common.util.UnstableApi
|
||||
import androidx.media3.exoplayer.ExoPlayer
|
||||
import androidx.media3.ui.DefaultTimeBar
|
||||
import com.audio.record.screen.test.R
|
||||
import com.audio.record.screen.test.base.BaseActivity
|
||||
import com.audio.record.screen.test.databinding.ActivityPlayBinding
|
||||
import com.audio.record.screen.test.tool.Common
|
||||
|
||||
|
||||
/**
|
||||
* 视频播放页面
|
||||
*/
|
||||
class PlayActivity : BaseActivity<ActivityPlayBinding>() {
|
||||
|
||||
companion object {
|
||||
val KEY_URI = "uri_key"
|
||||
val KEY_name = "name"
|
||||
}
|
||||
|
||||
private var exoPlayer: ExoPlayer? = null
|
||||
|
||||
|
||||
private var mUri: Uri? = null
|
||||
|
||||
private var displayName: String? = null
|
||||
private lateinit var timeBar:DefaultTimeBar
|
||||
|
||||
private lateinit var btnPlay:ImageButton
|
||||
override fun initBinding(): ActivityPlayBinding = ActivityPlayBinding.inflate(layoutInflater)
|
||||
override fun getFullColor(): Boolean? = null
|
||||
override fun onInitPadding(): Boolean = false
|
||||
override fun onSetViewBefore() {
|
||||
}
|
||||
|
||||
override fun onCreateInit() {
|
||||
Common.enableImmersiveMode(this)
|
||||
mUri = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||
intent.getParcelableExtra(KEY_URI, Uri::class.java)
|
||||
} else {
|
||||
intent.getParcelableExtra(KEY_URI)
|
||||
}
|
||||
|
||||
displayName = intent.getStringExtra(KEY_name)
|
||||
initControllerView()
|
||||
initPlay()
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
private fun initControllerView() {
|
||||
val controller = binding.playerView.findViewById<ViewGroup>(R.id.exo_controller)
|
||||
controller.findViewById<ImageView>(R.id.back).setOnClickListener {
|
||||
finish()
|
||||
}
|
||||
controller.findViewById<TextView>(R.id.text_name).text = displayName
|
||||
btnPlay = controller.findViewById<ImageButton>(R.id.exo_play_pause)
|
||||
val layoutProgress = controller.findViewById<RelativeLayout>(R.id.layout_progress)
|
||||
timeBar = controller.findViewById<DefaultTimeBar>(R.id.exo_progress)
|
||||
}
|
||||
|
||||
@OptIn(UnstableApi::class)
|
||||
private fun initPlay() {
|
||||
btnPlay.isVisible = false
|
||||
exoPlayer = ExoPlayer.Builder(this).build()
|
||||
binding.playerView.run {
|
||||
player = exoPlayer
|
||||
controllerShowTimeoutMs = 3000 // 设置控制器显示时间 3 秒后自动隐藏
|
||||
controllerAutoShow = true // 播放时点击可以自动显示控制器
|
||||
}
|
||||
exoPlayer?.run {
|
||||
val mediaItem = MediaItem.fromUri(mUri!!)
|
||||
setMediaItem(mediaItem)
|
||||
prepare()
|
||||
playWhenReady = true
|
||||
addListener(object : Player.Listener {
|
||||
override fun onIsPlayingChanged(isPlaying: Boolean) {
|
||||
if(isPlaying){
|
||||
timeBar.isVisible = true
|
||||
}
|
||||
}
|
||||
|
||||
override fun onPlaybackStateChanged(playbackState: Int) {
|
||||
if (playbackState == Player.STATE_ENDED) {
|
||||
// 播放完成
|
||||
btnPlay.isVisible = true
|
||||
}
|
||||
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
override fun onDestroy() {
|
||||
super.onDestroy()
|
||||
binding.playerView.player = null
|
||||
releasePlayer()
|
||||
}
|
||||
|
||||
private fun releasePlayer() {
|
||||
exoPlayer?.run {
|
||||
stop() // 可选:停止播放
|
||||
clearMediaItems()
|
||||
release()
|
||||
}
|
||||
exoPlayer = null
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,183 @@
|
||||
package com.audio.record.screen.test.activity
|
||||
|
||||
|
||||
import android.graphics.RectF
|
||||
import android.net.Uri
|
||||
import android.view.View
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import androidx.media3.common.MediaItem
|
||||
import androidx.media3.common.PlaybackParameters
|
||||
import androidx.media3.exoplayer.ExoPlayer
|
||||
import androidx.navigation.fragment.NavHostFragment
|
||||
import com.audio.record.screen.test.App
|
||||
import com.audio.record.screen.test.R
|
||||
import com.audio.record.screen.test.base.BaseActivity
|
||||
import com.audio.record.screen.test.databinding.ActivityPreviewBinding
|
||||
import com.audio.record.screen.test.tool.Common
|
||||
import com.audio.record.screen.test.tool.FFmpegKitTool
|
||||
import com.audio.record.screen.test.viewmodel.PreviewViewModel
|
||||
import java.io.File
|
||||
import kotlin.properties.Delegates
|
||||
|
||||
|
||||
class PreviewActivity : BaseActivity<ActivityPreviewBinding>(), View.OnClickListener {
|
||||
|
||||
private lateinit var exoPlayer: ExoPlayer
|
||||
|
||||
//将当前需要处理的原视频复制到内部存储,方便操作
|
||||
private lateinit var copyFile: File
|
||||
|
||||
private var copyResult by Delegates.notNull<Boolean>()
|
||||
private lateinit var viewModel: PreviewViewModel
|
||||
override fun initBinding(): ActivityPreviewBinding =
|
||||
ActivityPreviewBinding.inflate(layoutInflater)
|
||||
|
||||
|
||||
override fun getFullColor(): Boolean = true
|
||||
override fun onInitPadding(): Boolean = true
|
||||
override fun onCreateInit() {
|
||||
initPlay()
|
||||
viewModel = ViewModelProvider(this)[PreviewViewModel::class.java]
|
||||
binding.imPlay.setOnClickListener {
|
||||
if (!binding.imPlay.isSelected) {
|
||||
exoPlayer.play()
|
||||
} else {
|
||||
exoPlayer.pause()
|
||||
}
|
||||
binding.imPlay.isSelected = !binding.imPlay.isSelected
|
||||
}
|
||||
|
||||
val thumbDir = File(App.instanceApp.cacheDir, "thumb").apply {
|
||||
if (!exists()) {
|
||||
mkdir()
|
||||
} else {
|
||||
Common.deleteAllFilesInDirectory(this.absolutePath)
|
||||
}
|
||||
}
|
||||
// val thumbAdapter = ThumbAdapter(this@PreviewActivity)
|
||||
// binding.thumbRecycler.apply {
|
||||
// layoutManager =
|
||||
// LinearLayoutManager(this@PreviewActivity, RecyclerView.HORIZONTAL, false)
|
||||
// adapter = thumbAdapter
|
||||
// }
|
||||
val open = App.instanceApp.assets.open("record_1748398994963.mp4")
|
||||
copyFile = File(App.instanceApp.cacheDir, "temp_video.mp4")
|
||||
//原视频复制到内部存储
|
||||
FFmpegKitTool.copy(copyFile, open, thumbDir.absolutePath) {
|
||||
Common.showLog("--------copy success")
|
||||
copyResult = it
|
||||
}
|
||||
|
||||
|
||||
initListener()
|
||||
viewModel.cropRatioText.observe(this) {
|
||||
if (it.equals("原始")) {
|
||||
val videoRatio = Common.getVideoRatio(copyFile.absolutePath)
|
||||
binding.cropView.setAspectRatio(videoRatio)
|
||||
} else {
|
||||
val split = it.split(":")
|
||||
binding.cropView.setAspectRatio(split[0].toFloat() / split[1].toFloat())
|
||||
}
|
||||
}
|
||||
viewModel.saveCrop.observe(this){
|
||||
|
||||
val cropFile = File(App.instanceApp.cacheDir, "crop_video_${System.currentTimeMillis()}.mp4")
|
||||
val videoWH = Common.getVideoWH(copyFile.absolutePath)
|
||||
|
||||
val rawCropRect = binding.cropView.getCropRectInVideoCoords(videoWH.first ,
|
||||
videoWH.second
|
||||
)
|
||||
|
||||
val x = rawCropRect?.left?.toInt() ?: 0
|
||||
val y = rawCropRect?.top?.toInt() ?: 0
|
||||
val w = rawCropRect?.width()?.toInt() ?: 0
|
||||
val h = rawCropRect?.height()?.toInt() ?: 0
|
||||
|
||||
Common.showLog("-------videoWH w=${videoWH.first} h=${videoWH.second} x=${x} y = $y w= $w h= $h")
|
||||
FFmpegKitTool.cropVideo(copyFile.absolutePath,cropFile.absolutePath,x,y,w,h)
|
||||
|
||||
|
||||
}
|
||||
viewModel.changeSpeed.observe(this){
|
||||
exoPlayer.playbackParameters = PlaybackParameters(it, it)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private fun initListener() {
|
||||
val navHostFragment = supportFragmentManager
|
||||
.findFragmentById(R.id.nav_host_fragment) as NavHostFragment
|
||||
val navController = navHostFragment.navController
|
||||
navController.addOnDestinationChangedListener { controller, destination, arguments ->
|
||||
Common.showLog("NavControllerListener 当前的目的地: id=${destination.id} name=${destination.displayName} label=${destination.label}")
|
||||
binding.cropView.isVisible = false
|
||||
when (destination.id) {
|
||||
R.id.fragmentCut -> {
|
||||
viewModel.updateCopyResult(Pair(copyResult, copyFile.absolutePath))
|
||||
Common.showLog("----fragmentCut")
|
||||
}
|
||||
|
||||
R.id.fragmentCropping -> {
|
||||
Common.showLog("----fragmentCropping")
|
||||
initCropView()
|
||||
|
||||
}
|
||||
|
||||
R.id.fragmentVolume -> {
|
||||
Common.showLog("----fragmentVolume")
|
||||
viewModel.updateCopyResult(Pair(copyResult, copyFile.absolutePath))
|
||||
}
|
||||
|
||||
R.id.fragmentSpeed -> {
|
||||
Common.showLog("----fragmentSpeed")
|
||||
viewModel.updateCopyResult(Pair(copyResult, copyFile.absolutePath))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private fun initCropView() {
|
||||
binding.cropView.isVisible = true
|
||||
// 延迟执行以确保布局完成
|
||||
binding.playerView.post {
|
||||
val contentFrame: View =
|
||||
binding.playerView.findViewById(androidx.media3.ui.R.id.exo_content_frame)
|
||||
if (contentFrame != null) {
|
||||
// 获取在 cropView 坐标系中的显示区域
|
||||
val videoRect = RectF()
|
||||
val contentLoc = IntArray(2)
|
||||
val cropLoc = IntArray(2)
|
||||
contentFrame.getLocationOnScreen(contentLoc)
|
||||
binding.cropView.getLocationOnScreen(cropLoc)
|
||||
val offsetX = (contentLoc[0] - cropLoc[0]).toFloat()
|
||||
val offsetY = (contentLoc[1] - cropLoc[1]).toFloat()
|
||||
videoRect.left = offsetX
|
||||
videoRect.top = offsetY
|
||||
videoRect.right = offsetX + contentFrame.width
|
||||
videoRect.bottom = offsetY + contentFrame.height
|
||||
// 设置裁剪区域限制边界
|
||||
binding.cropView.setVideoDisplayBounds(videoRect)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private fun initPlay() {
|
||||
val uri = Uri.parse("asset:///record_1748398994963.mp4")
|
||||
exoPlayer = ExoPlayer.Builder(this).build()
|
||||
|
||||
binding.playerView.player = exoPlayer
|
||||
val mediaItem = MediaItem.fromUri(uri)
|
||||
exoPlayer.setMediaItem(mediaItem)
|
||||
exoPlayer.prepare()
|
||||
// exoPlayer.playWhenReady = true
|
||||
}
|
||||
|
||||
override fun onClick(p0: View?) {
|
||||
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@ -0,0 +1,33 @@
|
||||
package com.audio.record.screen.test.adapter
|
||||
|
||||
import android.content.Context
|
||||
import android.view.LayoutInflater
|
||||
import android.view.ViewGroup
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.audio.record.screen.test.App
|
||||
import com.audio.record.screen.test.base.BaseAdapter
|
||||
import com.audio.record.screen.test.databinding.AdapterCropBinding
|
||||
import com.audio.record.screen.test.databinding.AdapterThumbBinding
|
||||
import com.bumptech.glide.Glide
|
||||
|
||||
class CropAdapter(context: Context, private var click: (cropRatio:String) -> Unit) :
|
||||
BaseAdapter<String, AdapterCropBinding>(context) {
|
||||
override fun getViewBinding(parent: ViewGroup?): AdapterCropBinding {
|
||||
return AdapterCropBinding.inflate(LayoutInflater.from(parent!!.context), parent, false)
|
||||
}
|
||||
|
||||
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
|
||||
val itemHolder: VHolder<AdapterCropBinding> = holder as VHolder<AdapterCropBinding>
|
||||
|
||||
val item = data[position]
|
||||
with(itemHolder.vb.btn) {
|
||||
text = item
|
||||
setOnClickListener {
|
||||
click.invoke(item)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,42 @@
|
||||
package com.audio.record.screen.test.adapter
|
||||
|
||||
import android.content.Context
|
||||
import android.view.LayoutInflater
|
||||
import android.view.ViewGroup
|
||||
import androidx.recyclerview.widget.GridLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.audio.record.screen.test.base.BaseAdapter
|
||||
import com.audio.record.screen.test.data.ImageGroup
|
||||
import com.audio.record.screen.test.data.VideoGroup
|
||||
import com.audio.record.screen.test.databinding.AdapterVideoDateBinding
|
||||
import com.audio.record.screen.test.tool.Extend.dpToPx
|
||||
import com.audio.record.screen.test.tool.NoScrollLinearLayoutManager
|
||||
import com.audio.record.screen.test.view.GridSpacingItemDecoration
|
||||
|
||||
class ImageGroupAdapter(context: Context, private var click: (cropRatio: String) -> Unit) :
|
||||
BaseAdapter<ImageGroup, AdapterVideoDateBinding>(context) {
|
||||
override fun getViewBinding(parent: ViewGroup?): AdapterVideoDateBinding {
|
||||
return AdapterVideoDateBinding.inflate(LayoutInflater.from(parent!!.context), parent, false)
|
||||
}
|
||||
|
||||
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
|
||||
val itemHolder: VHolder<AdapterVideoDateBinding> =
|
||||
holder as VHolder<AdapterVideoDateBinding>
|
||||
|
||||
val item = data[position]
|
||||
itemHolder.vb.run {
|
||||
|
||||
tvDate.text = item.date
|
||||
|
||||
videoInfoRecycler.run {
|
||||
adapter = ImageInfoAdapter(mContext, position == 0) {}.apply {
|
||||
updateData(item.images)
|
||||
}
|
||||
layoutManager = GridLayoutManager(mContext, 3)
|
||||
isNestedScrollingEnabled = false
|
||||
addItemDecoration(GridSpacingItemDecoration(3, 5.dpToPx(mContext), true))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,45 @@
|
||||
package com.audio.record.screen.test.adapter
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.view.LayoutInflater
|
||||
import android.view.ViewGroup
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.audio.record.screen.test.activity.ImageViewActivity
|
||||
import com.audio.record.screen.test.activity.PlayActivity
|
||||
import com.audio.record.screen.test.base.BaseAdapter
|
||||
import com.audio.record.screen.test.data.ImageInfo
|
||||
import com.audio.record.screen.test.data.VideoInfo
|
||||
import com.audio.record.screen.test.databinding.AdapterImageInfoBinding
|
||||
import com.audio.record.screen.test.databinding.AdapterVideoInfoBinding
|
||||
import com.audio.record.screen.test.tool.Common
|
||||
|
||||
class ImageInfoAdapter(
|
||||
context: Context,
|
||||
var showNew: Boolean,
|
||||
private var click: (cropRatio: String) -> Unit
|
||||
) :
|
||||
BaseAdapter<ImageInfo, AdapterImageInfoBinding>(context) {
|
||||
override fun getViewBinding(parent: ViewGroup?): AdapterImageInfoBinding {
|
||||
return AdapterImageInfoBinding.inflate(LayoutInflater.from(parent!!.context), parent, false)
|
||||
}
|
||||
|
||||
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
|
||||
val itemHolder: VHolder<AdapterImageInfoBinding> = holder as VHolder<AdapterImageInfoBinding>
|
||||
Common.showLog("----$position ")
|
||||
val item = data[position]
|
||||
|
||||
itemHolder.vb.run {
|
||||
tvNew.isVisible = (showNew && position == 0)
|
||||
image.setImageBitmap(item.thumbnail)
|
||||
|
||||
root.setOnClickListener {
|
||||
mContext.startActivity(Intent(mContext, ImageViewActivity::class.java).apply {
|
||||
putExtra(ImageViewActivity.KEY_URI, item.uri)
|
||||
putExtra(ImageViewActivity.KEY_name,item.displayName)
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,26 @@
|
||||
package com.audio.record.screen.test.adapter
|
||||
|
||||
import android.content.Context
|
||||
import android.view.LayoutInflater
|
||||
import android.view.ViewGroup
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.audio.record.screen.test.App
|
||||
import com.audio.record.screen.test.base.BaseAdapter
|
||||
import com.audio.record.screen.test.databinding.AdapterThumbBinding
|
||||
import com.bumptech.glide.Glide
|
||||
|
||||
class ThumbAdapter(context: Context) : BaseAdapter<String, AdapterThumbBinding>(context) {
|
||||
override fun getViewBinding(parent: ViewGroup?): AdapterThumbBinding {
|
||||
return AdapterThumbBinding.inflate(LayoutInflater.from(parent!!.context), parent, false)
|
||||
}
|
||||
|
||||
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
|
||||
val itemHolder: VHolder<AdapterThumbBinding> = holder as VHolder<AdapterThumbBinding>
|
||||
|
||||
|
||||
Glide.with(mContext!!).load(data[position]).into(itemHolder.vb.imThumb)
|
||||
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,30 @@
|
||||
package com.audio.record.screen.test.adapter
|
||||
|
||||
import android.content.Context
|
||||
import android.view.LayoutInflater
|
||||
import android.view.ViewGroup
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.audio.record.screen.test.App
|
||||
import com.audio.record.screen.test.base.BaseAdapter
|
||||
import com.audio.record.screen.test.databinding.AdapterThumbBinding
|
||||
import com.audio.record.screen.test.databinding.AdapterToolBinding
|
||||
import com.bumptech.glide.Glide
|
||||
|
||||
class ToolAdapter(context: Context, private var click:(type:Int)->Unit) : BaseAdapter<String, AdapterToolBinding>(context) {
|
||||
override fun getViewBinding(parent: ViewGroup?): AdapterToolBinding {
|
||||
return AdapterToolBinding.inflate(LayoutInflater.from(parent!!.context), parent, false)
|
||||
}
|
||||
|
||||
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
|
||||
val itemHolder = holder as VHolder<AdapterToolBinding>
|
||||
val item = data[position]
|
||||
with(itemHolder.vb.btn) {
|
||||
text = item
|
||||
setOnClickListener {
|
||||
click.invoke(itemHolder.bindingAdapterPosition)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,39 @@
|
||||
package com.audio.record.screen.test.adapter
|
||||
|
||||
import android.content.Context
|
||||
import android.view.LayoutInflater
|
||||
import android.view.ViewGroup
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.audio.record.screen.test.base.BaseAdapter
|
||||
import com.audio.record.screen.test.data.VideoGroup
|
||||
import com.audio.record.screen.test.databinding.AdapterVideoDateBinding
|
||||
import com.audio.record.screen.test.tool.NoScrollLinearLayoutManager
|
||||
|
||||
class VideoGroupAdapter(context: Context, private var click: (cropRatio:String) -> Unit) :
|
||||
BaseAdapter<VideoGroup, AdapterVideoDateBinding>(context) {
|
||||
override fun getViewBinding(parent: ViewGroup?): AdapterVideoDateBinding {
|
||||
return AdapterVideoDateBinding.inflate(LayoutInflater.from(parent!!.context), parent, false)
|
||||
}
|
||||
|
||||
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
|
||||
val itemHolder: VHolder<AdapterVideoDateBinding> = holder as VHolder<AdapterVideoDateBinding>
|
||||
|
||||
val item = data[position]
|
||||
itemHolder.vb.run {
|
||||
|
||||
tvDate.text = item.date
|
||||
|
||||
videoInfoRecycler.run {
|
||||
adapter = VideoInfoAdapter(mContext){}.apply {
|
||||
updateData(item.videos)
|
||||
}
|
||||
layoutManager = NoScrollLinearLayoutManager(mContext)
|
||||
isNestedScrollingEnabled = false
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,41 @@
|
||||
package com.audio.record.screen.test.adapter
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.view.LayoutInflater
|
||||
import android.view.ViewGroup
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.audio.record.screen.test.activity.PlayActivity
|
||||
import com.audio.record.screen.test.base.BaseAdapter
|
||||
import com.audio.record.screen.test.data.VideoInfo
|
||||
import com.audio.record.screen.test.databinding.AdapterVideoInfoBinding
|
||||
import com.audio.record.screen.test.tool.Common
|
||||
|
||||
class VideoInfoAdapter(context: Context, private var click: (cropRatio: String) -> Unit) :
|
||||
BaseAdapter<VideoInfo, AdapterVideoInfoBinding>(context) {
|
||||
override fun getViewBinding(parent: ViewGroup?): AdapterVideoInfoBinding {
|
||||
return AdapterVideoInfoBinding.inflate(LayoutInflater.from(parent!!.context), parent, false)
|
||||
}
|
||||
|
||||
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
|
||||
val itemHolder: VHolder<AdapterVideoInfoBinding> =
|
||||
holder as VHolder<AdapterVideoInfoBinding>
|
||||
Common.showLog("----$position ")
|
||||
val item = data[position]
|
||||
|
||||
itemHolder.vb.run {
|
||||
image.setImageBitmap(item.thumbnail)
|
||||
tvName.text = item.displayName
|
||||
tvTime.text = Common.formatDuration(item.duration)
|
||||
tvSize.text = Common.formatFileSize(item.size)
|
||||
root.setOnClickListener {
|
||||
mContext.startActivity(Intent(mContext,PlayActivity::class.java).apply {
|
||||
putExtra(PlayActivity.KEY_URI, item.uri)
|
||||
putExtra(PlayActivity.KEY_name,item.displayName)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,20 @@
|
||||
package com.audio.record.screen.test.adapter
|
||||
|
||||
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
|
||||
|
||||
|
||||
class ViewPager2Adapter(fragmentActivity: FragmentActivity) :
|
||||
FragmentStateAdapter(fragmentActivity) {
|
||||
private val fragments: List<Fragment> = arrayListOf(
|
||||
MainFragment.newInstance(),
|
||||
RecorderFragment.newInstance(),
|
||||
MainFragment.newInstance()
|
||||
)
|
||||
override fun getItemCount(): Int = fragments.size
|
||||
|
||||
override fun createFragment(position: Int): Fragment = fragments[position]
|
||||
}
|
||||
@ -0,0 +1,20 @@
|
||||
package com.audio.record.screen.test.adapter
|
||||
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.viewpager2.adapter.FragmentStateAdapter
|
||||
import com.audio.record.screen.test.fragment.child.ImageFragment
|
||||
import com.audio.record.screen.test.fragment.child.VideoFragment
|
||||
|
||||
|
||||
class VpFragmentAdapter(mFragment: Fragment) :
|
||||
FragmentStateAdapter(mFragment) {
|
||||
|
||||
val fragments: List<Fragment> = arrayListOf(
|
||||
VideoFragment.newInstance(),
|
||||
ImageFragment.newInstance()
|
||||
)
|
||||
|
||||
override fun getItemCount(): Int = fragments.size
|
||||
|
||||
override fun createFragment(position: Int): Fragment = fragments[position]
|
||||
}
|
||||
@ -0,0 +1,37 @@
|
||||
package com.audio.record.screen.test.base
|
||||
|
||||
import android.os.Bundle
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.core.view.WindowCompat
|
||||
import androidx.viewbinding.ViewBinding
|
||||
import com.audio.record.screen.test.tool.Common
|
||||
import com.audio.record.screen.test.tool.Common.setStatusBarTextColor
|
||||
|
||||
|
||||
abstract class BaseActivity<T : ViewBinding> : AppCompatActivity() {
|
||||
|
||||
protected lateinit var binding: T
|
||||
|
||||
abstract fun initBinding(): T
|
||||
|
||||
|
||||
//true 深色 false 浅色 null 不全屏
|
||||
abstract fun getFullColor(): Boolean?
|
||||
open fun onSetViewBefore(){}
|
||||
abstract fun onCreateInit()
|
||||
abstract fun onInitPadding():Boolean
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
binding = initBinding()
|
||||
getFullColor()?.let {
|
||||
if(onInitPadding()){
|
||||
binding.root.setPadding(0,Common.dpToPx(40,this),0,0)
|
||||
}
|
||||
setStatusBarTextColor(this,it)
|
||||
}
|
||||
onSetViewBefore()
|
||||
setContentView(binding.root)
|
||||
onCreateInit()
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,64 @@
|
||||
package com.audio.record.screen.test.base
|
||||
|
||||
import android.content.Context
|
||||
import android.view.ViewGroup
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import androidx.viewbinding.ViewBinding
|
||||
|
||||
abstract class BaseAdapter<K, T : ViewBinding?> : RecyclerView.Adapter<RecyclerView.ViewHolder> {
|
||||
protected var data: MutableList<K> = ArrayList()
|
||||
protected lateinit var mContext: Context
|
||||
var isLoadingAdded = false
|
||||
protected set
|
||||
|
||||
constructor()
|
||||
constructor(mContext: Context) {
|
||||
this.mContext = mContext
|
||||
}
|
||||
|
||||
fun addData(data: List<K>?) {
|
||||
this.data.addAll(data!!)
|
||||
notifyDataSetChanged()
|
||||
}
|
||||
|
||||
fun updateData(data: List<K>?) {
|
||||
this.data.clear()
|
||||
this.data.addAll(data!!)
|
||||
notifyDataSetChanged()
|
||||
}
|
||||
|
||||
fun addLoadingFooter() {
|
||||
isLoadingAdded = true
|
||||
notifyItemInserted(data.size)
|
||||
}
|
||||
|
||||
// Hide loading footer
|
||||
fun removeLoadingFooter() {
|
||||
val position = itemCount
|
||||
if (position >= 0) notifyItemRemoved(position)
|
||||
isLoadingAdded = false
|
||||
}
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
|
||||
val viewBinding = getViewBinding(parent)
|
||||
return VHolder(viewBinding)
|
||||
}
|
||||
|
||||
protected abstract fun getViewBinding(parent: ViewGroup?): T
|
||||
override fun getItemViewType(position: Int): Int {
|
||||
return if (position == data.size && isLoadingAdded) TYPE_FOOTER else TYPE_ITEM
|
||||
}
|
||||
|
||||
override fun getItemCount(): Int {
|
||||
return data.size + if (isLoadingAdded) 1 else 0
|
||||
}
|
||||
|
||||
class VHolder<V : ViewBinding?>(val vb: V) : RecyclerView.ViewHolder(
|
||||
vb!!.root
|
||||
)
|
||||
|
||||
companion object {
|
||||
protected const val TYPE_ITEM = 0
|
||||
protected const val TYPE_FOOTER = 1
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,24 @@
|
||||
package com.audio.record.screen.test.base
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.viewbinding.ViewBinding
|
||||
|
||||
abstract class BaseFragment<T : ViewBinding> : Fragment() {
|
||||
|
||||
protected lateinit var binding: T
|
||||
|
||||
abstract fun initBinding(inflater: LayoutInflater, container: ViewGroup?): T
|
||||
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater, container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
): View? {
|
||||
binding = initBinding(inflater,container)
|
||||
return binding.root
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,6 @@
|
||||
package com.audio.record.screen.test.data
|
||||
|
||||
data class ImageGroup (
|
||||
val date: String, // 格式:yyyy-MM-dd
|
||||
val images: List<ImageInfo>
|
||||
)
|
||||
@ -0,0 +1,14 @@
|
||||
package com.audio.record.screen.test.data
|
||||
|
||||
import android.graphics.Bitmap
|
||||
import android.net.Uri
|
||||
|
||||
|
||||
data class ImageInfo(
|
||||
val uri: Uri,
|
||||
val displayName: String,
|
||||
val size: Long,
|
||||
val dateAdded: Long,
|
||||
val dateModified: Long,
|
||||
val thumbnail: Bitmap?
|
||||
)
|
||||
@ -0,0 +1,8 @@
|
||||
package com.audio.record.screen.test.data
|
||||
|
||||
|
||||
data class VideoGroup(
|
||||
val date: String,
|
||||
val videos: List<VideoInfo>
|
||||
)
|
||||
|
||||
@ -0,0 +1,15 @@
|
||||
package com.audio.record.screen.test.data
|
||||
|
||||
import android.graphics.Bitmap
|
||||
import android.net.Uri
|
||||
|
||||
|
||||
data class VideoInfo(
|
||||
val uri: Uri,
|
||||
val displayName: String,
|
||||
val size: Long, // 字节
|
||||
val duration: Long, // 毫秒
|
||||
val dateAdded: Long, // 秒
|
||||
val dateModified: Long, // 秒
|
||||
val thumbnail: Bitmap? = null // 可选封面图
|
||||
)
|
||||
@ -0,0 +1,67 @@
|
||||
package com.audio.record.screen.test.dialog
|
||||
|
||||
import android.graphics.Color
|
||||
import android.graphics.drawable.ColorDrawable
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.fragment.app.DialogFragment
|
||||
import com.audio.record.screen.test.databinding.DialogAudioBinding
|
||||
import com.audio.record.screen.test.databinding.DialogPermissionBinding
|
||||
import com.audio.record.screen.test.tool.Permission
|
||||
import com.google.android.material.bottomsheet.BottomSheetDialogFragment
|
||||
|
||||
class DialogAudio(private var mClickType: (type: Int) -> Unit) : DialogFragment() {
|
||||
private lateinit var vb: DialogAudioBinding
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater,
|
||||
container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
): View {
|
||||
vb = DialogAudioBinding.inflate(layoutInflater)
|
||||
init()
|
||||
return vb.root
|
||||
}
|
||||
|
||||
override fun onStart() {
|
||||
super.onStart()
|
||||
val dialog = dialog
|
||||
if (dialog != null) {
|
||||
dialog.setCanceledOnTouchOutside(false)
|
||||
val window = dialog.window
|
||||
if (window != null) {
|
||||
window.setBackgroundDrawable(ColorDrawable(Color.TRANSPARENT))
|
||||
window.setLayout(
|
||||
ViewGroup.LayoutParams.MATCH_PARENT,
|
||||
ViewGroup.LayoutParams.MATCH_PARENT
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun init() {
|
||||
vb.run {
|
||||
close.setOnClickListener {
|
||||
dismiss()
|
||||
}
|
||||
imWithAudio.setOnClickListener {
|
||||
mClickType.invoke(type_with_audio)
|
||||
dismiss()
|
||||
}
|
||||
imWithoutAudio.setOnClickListener {
|
||||
mClickType.invoke(type_without_audio)
|
||||
dismiss()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val type_with_audio = 0
|
||||
const val type_without_audio = 1
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,84 @@
|
||||
package com.audio.record.screen.test.dialog
|
||||
|
||||
import android.graphics.Color
|
||||
import android.graphics.drawable.ColorDrawable
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.core.view.isVisible
|
||||
import com.audio.record.screen.test.databinding.DialogPermissionBinding
|
||||
import com.audio.record.screen.test.tool.Permission
|
||||
import com.google.android.material.bottomsheet.BottomSheetDialogFragment
|
||||
|
||||
class DialogPermission(private var mClickType: (type: Int) -> Unit) : BottomSheetDialogFragment() {
|
||||
private lateinit var vb: DialogPermissionBinding
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater,
|
||||
container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
): View? {
|
||||
vb = DialogPermissionBinding.inflate(layoutInflater)
|
||||
init()
|
||||
return vb!!.root
|
||||
}
|
||||
|
||||
override fun onStart() {
|
||||
super.onStart()
|
||||
val dialog = dialog
|
||||
if (dialog != null) {
|
||||
dialog.setCanceledOnTouchOutside(true)
|
||||
val window = dialog.window
|
||||
if (window != null) {
|
||||
window.setBackgroundDrawable(ColorDrawable(Color.TRANSPARENT))
|
||||
window.setLayout(
|
||||
ViewGroup.LayoutParams.MATCH_PARENT,
|
||||
ViewGroup.LayoutParams.WRAP_CONTENT
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun init() {
|
||||
vb.run {
|
||||
|
||||
enableTvBall.setOnClickListener {
|
||||
mClickType.invoke(type_ball)
|
||||
}
|
||||
enableTvNotification.setOnClickListener {
|
||||
mClickType.invoke(type_notification)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
refreshUI()
|
||||
}
|
||||
|
||||
private fun refreshUI() {
|
||||
var ovalay = false
|
||||
var notification = false
|
||||
Permission.checkOvalApp(requireContext()) {
|
||||
vb.layoutBall.isVisible = !it
|
||||
ovalay = it
|
||||
if(ovalay&¬ification){
|
||||
dismiss()
|
||||
}
|
||||
}
|
||||
Permission.checkNotification(requireContext()) {
|
||||
vb.layoutNotification.isVisible = !it
|
||||
notification = it
|
||||
if(ovalay&¬ification){
|
||||
dismiss()
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val type_ball = 0
|
||||
const val type_notification = 1
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,76 @@
|
||||
package com.audio.record.screen.test.fragment
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import androidx.navigation.NavController
|
||||
import androidx.navigation.fragment.findNavController
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.audio.record.screen.test.R
|
||||
import com.audio.record.screen.test.adapter.CropAdapter
|
||||
import com.audio.record.screen.test.base.BaseFragment
|
||||
import com.audio.record.screen.test.databinding.FragmentCropBinding
|
||||
import com.audio.record.screen.test.viewmodel.PreviewViewModel
|
||||
|
||||
class CropFragment : BaseFragment<FragmentCropBinding>(), View.OnClickListener {
|
||||
|
||||
|
||||
private lateinit var navController: NavController
|
||||
private lateinit var viewModel: PreviewViewModel
|
||||
|
||||
companion object {
|
||||
|
||||
@JvmStatic
|
||||
fun newInstance() =
|
||||
CropFragment().apply {
|
||||
arguments = Bundle().apply {
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
arguments?.let {
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
override fun initBinding(inflater: LayoutInflater, container: ViewGroup?): FragmentCropBinding =
|
||||
FragmentCropBinding.inflate(inflater, container, false)
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
navController = findNavController()
|
||||
|
||||
viewModel = ViewModelProvider(requireActivity())[PreviewViewModel::class.java]
|
||||
binding.recycler.run {
|
||||
val stringList = resources.getStringArray(R.array.crop_text).toList()
|
||||
adapter = CropAdapter(requireContext()) {
|
||||
viewModel.updateCropText(it)
|
||||
|
||||
}.apply { updateData(stringList) }
|
||||
layoutManager = LinearLayoutManager(requireContext(), RecyclerView.HORIZONTAL, false)
|
||||
}
|
||||
initClick()
|
||||
}
|
||||
|
||||
private fun initClick() {
|
||||
binding.imClose.setOnClickListener(this)
|
||||
binding.imSave.setOnClickListener(this)
|
||||
}
|
||||
|
||||
override fun onClick(v: View?) {
|
||||
v?.let {
|
||||
if (it == binding.imSave) {
|
||||
viewModel.updateClickCropSave(true)
|
||||
} else if (it == binding.imClose) {
|
||||
navController.navigateUp()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,148 @@
|
||||
package com.audio.record.screen.test.fragment
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import androidx.navigation.NavController
|
||||
import androidx.navigation.fragment.findNavController
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.audio.record.screen.test.App
|
||||
import com.audio.record.screen.test.adapter.ThumbAdapter
|
||||
import com.audio.record.screen.test.base.BaseFragment
|
||||
import com.audio.record.screen.test.databinding.FragmentCutBinding
|
||||
import com.audio.record.screen.test.tool.Common
|
||||
import com.audio.record.screen.test.tool.FFmpegKitTool
|
||||
import com.audio.record.screen.test.viewmodel.PreviewViewModel
|
||||
import com.jaygoo.widget.OnRangeChangedListener
|
||||
import com.jaygoo.widget.RangeSeekBar
|
||||
import java.io.File
|
||||
import kotlin.properties.Delegates
|
||||
|
||||
class CutFragment : BaseFragment<FragmentCutBinding>() ,View.OnClickListener{
|
||||
//毫秒单位
|
||||
private var leftV by Delegates.notNull<Float>()
|
||||
private lateinit var viewModel: PreviewViewModel
|
||||
private var rightV by Delegates.notNull<Float>()
|
||||
private var param1: String? = null
|
||||
private var param2: String? = null
|
||||
private lateinit var copyFilePath: String
|
||||
private lateinit var navController:NavController
|
||||
companion object {
|
||||
|
||||
@JvmStatic
|
||||
fun newInstance() =
|
||||
CutFragment().apply {
|
||||
arguments = Bundle().apply {
|
||||
// putString(ARG_PARAM1, param1)
|
||||
// putString(ARG_PARAM2, param2)
|
||||
}
|
||||
}
|
||||
}
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
arguments?.let {
|
||||
// param1 = it.getString(ARG_PARAM1)
|
||||
// param2 = it.getString(ARG_PARAM2)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
override fun initBinding(inflater: LayoutInflater, container: ViewGroup?): FragmentCutBinding =
|
||||
FragmentCutBinding.inflate(inflater, container, false)
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
navController = findNavController()
|
||||
viewModel = ViewModelProvider(requireActivity())[PreviewViewModel::class.java]
|
||||
initThumb()
|
||||
initClick()
|
||||
}
|
||||
|
||||
|
||||
private fun initThumb(){
|
||||
val thumbDir = File(App.instanceApp.cacheDir, "thumb")
|
||||
val thumbAdapter = ThumbAdapter(requireContext())
|
||||
binding.thumbRecycler.apply {
|
||||
layoutManager =
|
||||
LinearLayoutManager(requireContext(), RecyclerView.HORIZONTAL, false)
|
||||
adapter = thumbAdapter
|
||||
}
|
||||
// val open = App.instanceApp.assets.open("temp_video.mp4")
|
||||
// tempFile = File(App.instanceApp.cacheDir, "temp_video.mp4")
|
||||
viewModel.copySuccess.observe(requireActivity()){
|
||||
if (it.first) {
|
||||
Common.showLog("-------11111111111111")
|
||||
copyFilePath = it.second
|
||||
val allImagePaths = Common.getNaturallySortedThumbFiles(thumbDir.absolutePath)
|
||||
thumbAdapter.updateData(allImagePaths)
|
||||
initSeekBar()
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
private fun initClick() {
|
||||
binding.imClose.setOnClickListener(this)
|
||||
binding.imSave.setOnClickListener(this)
|
||||
}
|
||||
private fun initSeekBar() {
|
||||
val durationMs = Common.getVideoDurationMs(copyFilePath)
|
||||
|
||||
// val millisToSeconds = Common.millisToSeconds(durationMs)
|
||||
Common.showLog("----durationMs=${durationMs} ")
|
||||
binding.rangeSlider.run {
|
||||
durationMs.toFloat().let {
|
||||
setRange(0f, it)
|
||||
leftSeekBar?.setIndicatorText(Common.formatSeconds(0f))
|
||||
rightSeekBar?.setIndicatorText(Common.formatSeconds(it))
|
||||
setProgress(0f, it)
|
||||
|
||||
}
|
||||
|
||||
|
||||
setOnRangeChangedListener(object : OnRangeChangedListener {
|
||||
override fun onRangeChanged(
|
||||
view: RangeSeekBar?,
|
||||
leftValue: Float,
|
||||
rightValue: Float,
|
||||
isFromUser: Boolean
|
||||
) {
|
||||
view?.leftSeekBar?.setIndicatorText(Common.formatSeconds(leftValue))
|
||||
view?.rightSeekBar?.setIndicatorText(Common.formatSeconds(rightValue))
|
||||
leftV = leftValue
|
||||
rightV = rightValue
|
||||
|
||||
}
|
||||
|
||||
override fun onStartTrackingTouch(view: RangeSeekBar?, isLeft: Boolean) {
|
||||
|
||||
}
|
||||
|
||||
override fun onStopTrackingTouch(view: RangeSeekBar?, isLeft: Boolean) {
|
||||
|
||||
}
|
||||
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
override fun onClick(p0: View?) {
|
||||
p0?.let {
|
||||
if (it == binding.imSave) {
|
||||
trimVideoFile()
|
||||
}else if(it == binding.imClose){
|
||||
navController.navigateUp()
|
||||
}
|
||||
}
|
||||
}
|
||||
fun trimVideoFile() {
|
||||
val resultFile = File(requireContext().cacheDir, "test_${System.currentTimeMillis()}.mp4")
|
||||
val left = leftV.toString()
|
||||
val right = rightV.toString()
|
||||
Common.showLog("------left=${left} right=${right}")
|
||||
// FFmpegKitTool.trimVideo(tempFile.absolutePath, resultFile.absolutePath, left, right)
|
||||
FFmpegKitTool.cropVideoWithFFmpeg(copyFilePath, resultFile.absolutePath,leftV,rightV)
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,102 @@
|
||||
package com.audio.record.screen.test.fragment
|
||||
|
||||
import android.app.Activity
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.ImageView
|
||||
import android.widget.TextView
|
||||
import androidx.fragment.app.Fragment
|
||||
import com.audio.record.screen.test.R
|
||||
import com.audio.record.screen.test.adapter.ViewPager2Adapter
|
||||
import com.audio.record.screen.test.adapter.VpFragmentAdapter
|
||||
import com.audio.record.screen.test.base.BaseFragment
|
||||
import com.audio.record.screen.test.databinding.FragmentMainBinding
|
||||
import com.audio.record.screen.test.tool.Common
|
||||
import com.google.android.material.tabs.TabLayout
|
||||
import com.google.android.material.tabs.TabLayoutMediator
|
||||
|
||||
class MainFragment : BaseFragment<FragmentMainBinding>() {
|
||||
|
||||
private var param1: String? = null
|
||||
private var param2: String? = null
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
arguments?.let {
|
||||
// param1 = it.getString(ARG_PARAM1)
|
||||
// param2 = it.getString(ARG_PARAM2)
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
@JvmStatic
|
||||
fun newInstance() =
|
||||
MainFragment().apply {
|
||||
arguments = Bundle().apply {
|
||||
// putString(ARG_PARAM1, param1)
|
||||
// putString(ARG_PARAM2, param2)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun initBinding(inflater: LayoutInflater, container: ViewGroup?): FragmentMainBinding =
|
||||
FragmentMainBinding.inflate(inflater, container, false)
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
bingVp()
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
private fun bingVp() {
|
||||
val tabString =
|
||||
arrayOf(R.string.Video, R.string.Image)
|
||||
binding.run {
|
||||
viewPager2.isUserInputEnabled = false
|
||||
viewPager2.adapter = VpFragmentAdapter(this@MainFragment)
|
||||
TabLayoutMediator(tabLayout, viewPager2) { tab, position ->
|
||||
val customView = LayoutInflater.from(this@MainFragment.context)
|
||||
.inflate(R.layout.tab_child_item, null)
|
||||
val tabTv = customView.findViewById<TextView>(R.id.tab_tv)
|
||||
tabTv.text = getString(tabString[position])
|
||||
if(position == 0){
|
||||
tabTv.isSelected = true
|
||||
tabTv.background = Common.getIcon(R.drawable.tab_child_selected)
|
||||
}
|
||||
|
||||
tab.customView = customView
|
||||
}.attach()
|
||||
tabLayout.addOnTabSelectedListener(object : TabLayout.OnTabSelectedListener {
|
||||
override fun onTabSelected(tab: TabLayout.Tab?) {
|
||||
tab?.customView?.run {
|
||||
findViewById<TextView>(R.id.tab_tv).run {
|
||||
isSelected = true
|
||||
background = Common.getIcon(R.drawable.tab_child_selected)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onTabUnselected(tab: TabLayout.Tab?) {
|
||||
tab?.customView?.run {
|
||||
findViewById<TextView>(R.id.tab_tv).run {
|
||||
isSelected = false
|
||||
background = null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onTabReselected(tab: TabLayout.Tab?) {
|
||||
|
||||
}
|
||||
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,45 @@
|
||||
package com.audio.record.screen.test.fragment
|
||||
|
||||
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>() {
|
||||
companion object {
|
||||
@JvmStatic
|
||||
fun newInstance() =
|
||||
RecorderFragment().apply {
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
override fun initBinding(inflater: LayoutInflater, container: ViewGroup?): FragmentRecordBinding =
|
||||
FragmentRecordBinding.inflate(inflater, container, false)
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
}
|
||||
|
||||
|
||||
private fun initNavigation(){
|
||||
val navHostFragment = childFragmentManager
|
||||
.findFragmentById(R.id.nav_host_view) as NavHostFragment
|
||||
val navController = navHostFragment.navController
|
||||
navController.addOnDestinationChangedListener { controller, destination, arguments ->
|
||||
when (destination.id){
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@ -0,0 +1,105 @@
|
||||
package com.audio.record.screen.test.fragment
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.SeekBar
|
||||
import android.widget.SeekBar.OnSeekBarChangeListener
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import androidx.navigation.NavController
|
||||
import androidx.navigation.fragment.findNavController
|
||||
import com.audio.record.screen.test.App
|
||||
import com.audio.record.screen.test.base.BaseFragment
|
||||
import com.audio.record.screen.test.databinding.FragmentSpeedBinding
|
||||
import com.audio.record.screen.test.tool.Common
|
||||
import com.audio.record.screen.test.tool.FFmpegKitTool
|
||||
import com.audio.record.screen.test.viewmodel.PreviewViewModel
|
||||
import java.io.File
|
||||
|
||||
class SpeedFragment : BaseFragment<FragmentSpeedBinding>(), View.OnClickListener {
|
||||
|
||||
|
||||
private lateinit var viewModel: PreviewViewModel
|
||||
private lateinit var navController: NavController
|
||||
private lateinit var copyFilePath: String
|
||||
private var speed = 1f
|
||||
|
||||
companion object {
|
||||
|
||||
@JvmStatic
|
||||
fun newInstance() =
|
||||
SpeedFragment().apply {
|
||||
arguments = Bundle().apply {
|
||||
// putString(ARG_PARAM1, param1)
|
||||
// putString(ARG_PARAM2, param2)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
arguments?.let {
|
||||
// param1 = it.getString(ARG_PARAM1)
|
||||
// param2 = it.getString(ARG_PARAM2)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
override fun initBinding(
|
||||
inflater: LayoutInflater,
|
||||
container: ViewGroup?
|
||||
): FragmentSpeedBinding =
|
||||
FragmentSpeedBinding.inflate(inflater, container, false)
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
viewModel = ViewModelProvider(requireActivity())[PreviewViewModel::class.java]
|
||||
navController = findNavController()
|
||||
|
||||
initClick()
|
||||
viewModel.copySuccess.observe(requireActivity()){
|
||||
if (it.first) {
|
||||
Common.showLog("-------11111111111111")
|
||||
copyFilePath = it.second
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private fun initClick() {
|
||||
binding.imClose.setOnClickListener(this)
|
||||
binding.imSave.setOnClickListener(this)
|
||||
binding.seekbar.setOnSeekBarChangeListener(object : OnSeekBarChangeListener {
|
||||
override fun onProgressChanged(seekBar: SeekBar?, progress: Int, fromUser: Boolean) {
|
||||
speed = 0.5f + (progress / 150f) * 1.5f
|
||||
binding.progressText.text = "${speed}X"
|
||||
viewModel.updateSpeed(speed)
|
||||
Common.showLog("--------progress=${progress} speed=${speed}")
|
||||
}
|
||||
|
||||
override fun onStartTrackingTouch(seekBar: SeekBar?) {
|
||||
|
||||
}
|
||||
|
||||
override fun onStopTrackingTouch(seekBar: SeekBar?) {
|
||||
|
||||
}
|
||||
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
override fun onClick(p0: View?) {
|
||||
p0?.let {
|
||||
if (it == binding.imSave) {
|
||||
val volumeFile = File(App.instanceApp.cacheDir, "speed_video_${System.currentTimeMillis()}.mp4")
|
||||
FFmpegKitTool.buildSpeedCommand(copyFilePath,volumeFile.absolutePath,speed)
|
||||
} else if (it == binding.imClose) {
|
||||
navController.navigateUp()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,73 @@
|
||||
package com.audio.record.screen.test.fragment
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.navigation.fragment.NavHostFragment.Companion.findNavController
|
||||
import androidx.navigation.fragment.findNavController
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.audio.record.screen.test.R
|
||||
import com.audio.record.screen.test.adapter.ToolAdapter
|
||||
import com.audio.record.screen.test.base.BaseFragment
|
||||
import com.audio.record.screen.test.databinding.FragmentMainBinding
|
||||
import com.audio.record.screen.test.databinding.FragmentStartBinding
|
||||
|
||||
class StartFragment : BaseFragment<FragmentStartBinding>() {
|
||||
|
||||
private var param1: String? = null
|
||||
private var param2: String? = null
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
arguments?.let {
|
||||
// param1 = it.getString(ARG_PARAM1)
|
||||
// param2 = it.getString(ARG_PARAM2)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
override fun initBinding(inflater: LayoutInflater, container: ViewGroup?): FragmentStartBinding =
|
||||
FragmentStartBinding.inflate(inflater, container, false)
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
val navController = findNavController()
|
||||
|
||||
binding.toolRecycler.run {
|
||||
val stringList = resources.getStringArray(R.array.tool_text).toList()
|
||||
adapter = ToolAdapter(requireContext()){
|
||||
when(it){
|
||||
0->{
|
||||
navController.navigate(R.id.action_to_fragmentCut)
|
||||
}
|
||||
1-> {
|
||||
navController.navigate(R.id.action_to_fragmentCropping)
|
||||
}
|
||||
2->{
|
||||
navController.navigate(R.id.action_to_fragmentVolume)
|
||||
}
|
||||
3->{
|
||||
navController.navigate(R.id.action_to_fragmentSpeed)
|
||||
}
|
||||
}
|
||||
|
||||
}.apply { updateData(stringList) }
|
||||
layoutManager = LinearLayoutManager(requireContext(), RecyclerView.HORIZONTAL, false)
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
@JvmStatic
|
||||
fun newInstance() =
|
||||
StartFragment().apply {
|
||||
arguments = Bundle().apply {
|
||||
// putString(ARG_PARAM1, param1)
|
||||
// putString(ARG_PARAM2, param2)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,102 @@
|
||||
package com.audio.record.screen.test.fragment
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.SeekBar
|
||||
import android.widget.SeekBar.OnSeekBarChangeListener
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import androidx.navigation.NavController
|
||||
import androidx.navigation.fragment.findNavController
|
||||
import com.audio.record.screen.test.App
|
||||
import com.audio.record.screen.test.base.BaseFragment
|
||||
import com.audio.record.screen.test.databinding.FragmentVolumeBinding
|
||||
import com.audio.record.screen.test.tool.Common
|
||||
import com.audio.record.screen.test.tool.FFmpegKitTool
|
||||
import com.audio.record.screen.test.viewmodel.PreviewViewModel
|
||||
import java.io.File
|
||||
|
||||
class VolumeFragment : BaseFragment<FragmentVolumeBinding>(), View.OnClickListener {
|
||||
|
||||
|
||||
private lateinit var viewModel: PreviewViewModel
|
||||
private lateinit var navController: NavController
|
||||
private lateinit var copyFilePath: String
|
||||
companion object {
|
||||
|
||||
@JvmStatic
|
||||
fun newInstance() =
|
||||
VolumeFragment().apply {
|
||||
arguments = Bundle().apply {
|
||||
// putString(ARG_PARAM1, param1)
|
||||
// putString(ARG_PARAM2, param2)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
arguments?.let {
|
||||
// param1 = it.getString(ARG_PARAM1)
|
||||
// param2 = it.getString(ARG_PARAM2)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
override fun initBinding(
|
||||
inflater: LayoutInflater,
|
||||
container: ViewGroup?
|
||||
): FragmentVolumeBinding =
|
||||
FragmentVolumeBinding.inflate(inflater, container, false)
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
viewModel = ViewModelProvider(requireActivity())[PreviewViewModel::class.java]
|
||||
navController = findNavController()
|
||||
|
||||
initClick()
|
||||
viewModel.copySuccess.observe(requireActivity()){
|
||||
if (it.first) {
|
||||
Common.showLog("-------11111111111111")
|
||||
copyFilePath = it.second
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private fun initClick() {
|
||||
binding.imClose.setOnClickListener(this)
|
||||
binding.imSave.setOnClickListener(this)
|
||||
binding.seekbar.setOnSeekBarChangeListener(object : OnSeekBarChangeListener {
|
||||
override fun onProgressChanged(seekBar: SeekBar?, progress: Int, fromUser: Boolean) {
|
||||
binding.progressText.text = "${progress}%"
|
||||
}
|
||||
|
||||
override fun onStartTrackingTouch(seekBar: SeekBar?) {
|
||||
|
||||
}
|
||||
|
||||
override fun onStopTrackingTouch(seekBar: SeekBar?) {
|
||||
|
||||
}
|
||||
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
override fun onClick(p0: View?) {
|
||||
p0?.let {
|
||||
if (it == binding.imSave) {
|
||||
val volumeFile = File(App.instanceApp.cacheDir, "volume_video_${System.currentTimeMillis()}.mp4")
|
||||
val volume = (binding.seekbar.progress.coerceIn(0, 200)) / 100.0f
|
||||
FFmpegKitTool.setVideVolume(copyFilePath,volumeFile.absolutePath,volume)
|
||||
|
||||
} else if (it == binding.imClose) {
|
||||
navController.navigateUp()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,61 @@
|
||||
package com.audio.record.screen.test.fragment.child
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import com.audio.record.screen.test.adapter.ImageGroupAdapter
|
||||
import com.audio.record.screen.test.base.BaseFragment
|
||||
import com.audio.record.screen.test.databinding.FragmentImageBinding
|
||||
import com.audio.record.screen.test.tool.Common
|
||||
import com.audio.record.screen.test.tool.VideoFileHelper
|
||||
|
||||
class ImageFragment : BaseFragment<FragmentImageBinding>() {
|
||||
|
||||
companion object {
|
||||
|
||||
@JvmStatic
|
||||
fun newInstance() =
|
||||
ImageFragment()
|
||||
}
|
||||
|
||||
override fun initBinding(
|
||||
inflater: LayoutInflater,
|
||||
container: ViewGroup?
|
||||
): FragmentImageBinding =
|
||||
FragmentImageBinding.inflate(inflater, container, false)
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
|
||||
val queryVideoInfoListInFolder =
|
||||
VideoFileHelper.queryGroupedImagesByDay(requireContext(),Common.imagesFolderDir)
|
||||
|
||||
if (queryVideoInfoListInFolder.isEmpty()||queryVideoInfoListInFolder[0].images.isEmpty()) {
|
||||
binding.run {
|
||||
layoutEmpty.isVisible = true
|
||||
}
|
||||
Common.showLog("IMag isEmpty()")
|
||||
return
|
||||
} else {
|
||||
binding.run {
|
||||
layoutEmpty.isVisible = false
|
||||
}
|
||||
}
|
||||
val imageGroupAdapter = ImageGroupAdapter(requireContext()) {
|
||||
}
|
||||
|
||||
binding.videoRecycler.run {
|
||||
adapter = imageGroupAdapter.apply {
|
||||
updateData(queryVideoInfoListInFolder)
|
||||
}
|
||||
layoutManager = LinearLayoutManager(requireContext())
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@ -0,0 +1,262 @@
|
||||
package com.audio.record.screen.test.fragment.child
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.media.projection.MediaProjectionManager
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.activity.result.ActivityResultLauncher
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import androidx.navigation.NavController
|
||||
import androidx.navigation.fragment.findNavController
|
||||
import com.audio.record.screen.test.R
|
||||
import com.audio.record.screen.test.base.BaseFragment
|
||||
import com.audio.record.screen.test.databinding.FragmentRecordNormalBinding
|
||||
import com.audio.record.screen.test.dialog.DialogAudio
|
||||
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.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
|
||||
|
||||
|
||||
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
|
||||
|
||||
//摄像头
|
||||
private lateinit var cameraLauncher: ActivityResultLauncher<String>
|
||||
|
||||
//截屏
|
||||
private lateinit var screenCaptureLauncher: ActivityResultLauncher<Intent>
|
||||
|
||||
//录制
|
||||
private lateinit var recorderLauncher: ActivityResultLauncher<Intent>
|
||||
|
||||
//录音权限
|
||||
private lateinit var micPermissionLauncher: ActivityResultLauncher<String>
|
||||
|
||||
private var mAudioDialog: DialogAudio? = null
|
||||
|
||||
//是否带音频录制
|
||||
private var isWithAudio = false
|
||||
|
||||
override fun initBinding(
|
||||
inflater: LayoutInflater,
|
||||
container: ViewGroup?
|
||||
): FragmentRecordNormalBinding = FragmentRecordNormalBinding.inflate(inflater, container, false)
|
||||
|
||||
|
||||
companion object {
|
||||
@JvmStatic
|
||||
fun newInstance() = RecordNormalFragment()
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
navController = findNavController()
|
||||
mediaProjectionManager =
|
||||
requireActivity().getSystemService(Context.MEDIA_PROJECTION_SERVICE) as MediaProjectionManager
|
||||
viewModel = ViewModelProvider(requireActivity())[MainViewModel::class.java]
|
||||
initClick()
|
||||
initLauncher()
|
||||
|
||||
viewModel.serviceConnectStatus.observe(requireActivity()) {
|
||||
Common.showLog(" FragmentRecordNormal registerCallback")
|
||||
FloatingWindowBridge.registerCallback(this)
|
||||
}
|
||||
viewModel.ballStatus.observe(requireActivity()) {
|
||||
Common.showLog(" FragmentRecordNormal 更新ballStatus")
|
||||
binding.btnFloatingBall.isSelected = it
|
||||
setBall(it)
|
||||
|
||||
}
|
||||
viewModel.screenshotStatus.observe(requireActivity()) {
|
||||
Common.showLog(" FragmentRecordNormal 更新screenshotStatus")
|
||||
binding.btnScreenshot.isSelected = it
|
||||
setScreenshot(it)
|
||||
}
|
||||
viewModel.webcamStatus.observe(requireActivity()) {
|
||||
Common.showLog(" FragmentRecordNormal 更新webcamStatus")
|
||||
binding.btnWebcam.isSelected = it
|
||||
setWebcam(it)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
private fun initLauncher() {
|
||||
cameraLauncher = registerForActivityResult(
|
||||
ActivityResultContracts.RequestPermission()
|
||||
) { isGranted ->
|
||||
if (isGranted) {
|
||||
FloatingWindowBridge.sendCommand(FloatingWindowBridge.COMMEND_show_camera)
|
||||
Common.showLog("CAMERA 权限授予")
|
||||
} else {
|
||||
// TODO:
|
||||
Common.showLog("CAMERA 权限拒绝")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
screenCaptureLauncher = registerForActivityResult(
|
||||
ActivityResultContracts.StartActivityForResult()
|
||||
) { result ->
|
||||
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("用户取消了录屏权限授权")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
recorderLauncher =
|
||||
registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
|
||||
if (result.resultCode == Activity.RESULT_OK && result.data != null) {
|
||||
val data = result.data
|
||||
val resultCode = result.resultCode
|
||||
FloatingWindowBridge.updateMediaProjection(result.resultCode, data!!)
|
||||
startCountDown()
|
||||
} else {
|
||||
Common.showLog("录屏授权失败 ")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
micPermissionLauncher = registerForActivityResult(
|
||||
ActivityResultContracts.RequestPermission()
|
||||
) { isGranted ->
|
||||
if (isGranted) {
|
||||
Common.showLog("mic 权限授予")
|
||||
startRecorder()
|
||||
} else {
|
||||
Common.showLog("mic 权限拒绝")
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private fun initClick() {
|
||||
binding.run {
|
||||
btnWebcam.setOnClickListener {
|
||||
it.isSelected = !it.isSelected
|
||||
it.isSelected.let { currentStatus ->
|
||||
viewModel.updateWebcamStatus(currentStatus)
|
||||
}
|
||||
|
||||
}
|
||||
btnScreenshot.setOnClickListener {
|
||||
it.isSelected = !it.isSelected
|
||||
|
||||
it.isSelected.let { currentStatus ->
|
||||
viewModel.updateScreenshotStatus(currentStatus)
|
||||
}
|
||||
|
||||
}
|
||||
btnFloatingBall.setOnClickListener {
|
||||
it.isSelected = !it.isSelected
|
||||
|
||||
it.isSelected.let { currentStatus ->
|
||||
viewModel.updateBallStatus(currentStatus)
|
||||
}
|
||||
}
|
||||
btnRecorder.setOnClickListener {
|
||||
showAudioDialog()
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private fun setWebcam(boolean: Boolean) {
|
||||
if (boolean) {
|
||||
Permission.checkCamera(requireContext()) { openCamera ->
|
||||
if (openCamera) {
|
||||
FloatingWindowBridge.sendCommand(FloatingWindowBridge.COMMEND_show_camera)
|
||||
} else {
|
||||
cameraLauncher.launch(android.Manifest.permission.CAMERA)
|
||||
}
|
||||
}
|
||||
|
||||
} else {
|
||||
FloatingWindowBridge.sendCommand(FloatingWindowBridge.COMMEND_hide_camera)
|
||||
}
|
||||
}
|
||||
|
||||
private fun setScreenshot(boolean: Boolean) {
|
||||
if (boolean) {
|
||||
if (FloatingWindowBridge.getMediaProjection() == null) {
|
||||
val captureIntent = mediaProjectionManager.createScreenCaptureIntent()
|
||||
screenCaptureLauncher.launch(captureIntent)
|
||||
} else {
|
||||
FloatingWindowBridge.sendCommand(FloatingWindowBridge.COMMEND_show_screenshot)
|
||||
}
|
||||
|
||||
} else {
|
||||
FloatingWindowBridge.sendCommand(FloatingWindowBridge.COMMEND_hide_screenshot)
|
||||
}
|
||||
}
|
||||
|
||||
private fun setBall(boolean: Boolean) {
|
||||
if (boolean) {
|
||||
FloatingWindowBridge.sendCommand(FloatingWindowBridge.COMMEND_show_ball)
|
||||
} else {
|
||||
FloatingWindowBridge.sendCommand(FloatingWindowBridge.COMMEND_hide_ball)
|
||||
}
|
||||
}
|
||||
|
||||
private fun startRecorder() {
|
||||
if (FloatingWindowBridge.getMediaProjection() == null) {
|
||||
val captureIntent = mediaProjectionManager.createScreenCaptureIntent()
|
||||
recorderLauncher.launch(captureIntent)
|
||||
} else {
|
||||
startCountDown()
|
||||
}
|
||||
}
|
||||
|
||||
private fun showAudioDialog() {
|
||||
mAudioDialog = mAudioDialog ?: DialogAudio {
|
||||
when (it) {
|
||||
DialogAudio.type_with_audio -> {
|
||||
isWithAudio = true
|
||||
micPermissionLauncher.launch(android.Manifest.permission.RECORD_AUDIO)
|
||||
}
|
||||
|
||||
DialogAudio.type_without_audio -> {
|
||||
isWithAudio = false
|
||||
startRecorder()
|
||||
}
|
||||
}
|
||||
}
|
||||
mAudioDialog?.show(childFragmentManager, "")
|
||||
}
|
||||
|
||||
private fun startCountDown() {
|
||||
FloatingWindowBridge.updateAudio(isWithAudio)
|
||||
val arrayOf = arrayOf("3", "2", "1")
|
||||
FloatingWindowBridge.sendCommand(FloatingWindowBridge.COMMEND_show_countdown, arrayOf)
|
||||
}
|
||||
|
||||
override fun onStartRecording() {
|
||||
navController.navigate(R.id.action_to_recording)
|
||||
}
|
||||
|
||||
|
||||
|
||||
override fun onDestroy() {
|
||||
super.onDestroy()
|
||||
FloatingWindowBridge.unregisterCallback(this)
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,99 @@
|
||||
package com.audio.record.screen.test.fragment.child
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import androidx.navigation.NavController
|
||||
import androidx.navigation.fragment.findNavController
|
||||
import com.audio.record.screen.test.base.BaseFragment
|
||||
import com.audio.record.screen.test.databinding.FragmentRecordingBinding
|
||||
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.viewmodel.MainViewModel
|
||||
|
||||
|
||||
class RecordingFragment : BaseFragment<FragmentRecordingBinding>(), FloatingCallback {
|
||||
|
||||
private lateinit var viewModel: MainViewModel
|
||||
private lateinit var mFindNavController: NavController
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
arguments?.let {
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
override fun initBinding(
|
||||
inflater: LayoutInflater,
|
||||
container: ViewGroup?
|
||||
): FragmentRecordingBinding = FragmentRecordingBinding.inflate(inflater, container, false)
|
||||
|
||||
|
||||
companion object {
|
||||
@JvmStatic
|
||||
fun newInstance() =
|
||||
RecordingFragment().apply {
|
||||
arguments = Bundle().apply {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
mFindNavController = findNavController()
|
||||
viewModel = ViewModelProvider(requireActivity())[MainViewModel::class.java]
|
||||
initClick()
|
||||
viewModel.serviceConnectStatus.observe(requireActivity()) {
|
||||
Common.showLog(" RecordingFragment registerCallback")
|
||||
FloatingWindowBridge.registerCallback(this)
|
||||
}
|
||||
viewModel.ballStatus.observe(requireActivity()) {
|
||||
binding.imBall.isSelected = it
|
||||
|
||||
}
|
||||
viewModel.screenshotStatus.observe(requireActivity()) {
|
||||
binding.imScreenshot.isSelected = it
|
||||
}
|
||||
viewModel.webcamStatus.observe(requireActivity()) {
|
||||
binding.imWebcam.isSelected = it
|
||||
}
|
||||
}
|
||||
|
||||
private fun initClick() {
|
||||
binding.run {
|
||||
imWebcam.setOnClickListener {
|
||||
it.isSelected = !it.isSelected
|
||||
viewModel.updateWebcamStatus(it.isSelected)
|
||||
}
|
||||
imScreenshot.setOnClickListener {
|
||||
it.isSelected = !it.isSelected
|
||||
viewModel.updateScreenshotStatus(it.isSelected)
|
||||
}
|
||||
imBall.setOnClickListener {
|
||||
it.isSelected = !it.isSelected
|
||||
viewModel.updateBallStatus(it.isSelected)
|
||||
}
|
||||
|
||||
imPauseResume.setOnClickListener {
|
||||
it.isSelected = !it.isSelected
|
||||
if (it.isSelected) {
|
||||
FloatingWindowBridge.sendCommand(FloatingWindowBridge.COMMEND_pause_record)
|
||||
} else {
|
||||
FloatingWindowBridge.sendCommand(FloatingWindowBridge.COMMEND_resume_record)
|
||||
}
|
||||
}
|
||||
imStop.setOnClickListener {
|
||||
FloatingWindowBridge.sendCommand(FloatingWindowBridge.COMMEND_stop_record)
|
||||
mFindNavController.navigateUp()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onUpdateRecordTime(time: String) {
|
||||
super.onUpdateRecordTime(time)
|
||||
binding.tvTimer.text = time
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,119 @@
|
||||
package com.audio.record.screen.test.fragment.child
|
||||
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import com.audio.record.screen.test.activity.PlayActivity
|
||||
import com.audio.record.screen.test.adapter.VideoGroupAdapter
|
||||
import com.audio.record.screen.test.base.BaseFragment
|
||||
import com.audio.record.screen.test.data.VideoInfo
|
||||
import com.audio.record.screen.test.data.VideoGroup
|
||||
import com.audio.record.screen.test.databinding.FragmentVideoBinding
|
||||
import com.audio.record.screen.test.tool.Common
|
||||
import com.audio.record.screen.test.tool.VideoFileHelper
|
||||
|
||||
class VideoFragment : BaseFragment<FragmentVideoBinding>() {
|
||||
companion object {
|
||||
|
||||
@JvmStatic
|
||||
fun newInstance() =
|
||||
VideoFragment()
|
||||
}
|
||||
|
||||
override fun initBinding(
|
||||
inflater: LayoutInflater,
|
||||
container: ViewGroup?
|
||||
): FragmentVideoBinding =
|
||||
FragmentVideoBinding.inflate(inflater, container, false)
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
|
||||
|
||||
val queryVideoInfoListInFolder =
|
||||
VideoFileHelper.queryVideoInfoListInFolder(requireContext(),Common.videosFolderDir)
|
||||
|
||||
if (queryVideoInfoListInFolder.isEmpty()) {
|
||||
binding.run {
|
||||
layoutEmpty.isVisible = true
|
||||
layoutRecent.isVisible = false
|
||||
}
|
||||
Common.showLog("queryVideoInfoListInFolder.isEmpty()")
|
||||
return
|
||||
} else {
|
||||
binding.run {
|
||||
layoutEmpty.isVisible = false
|
||||
layoutRecent.isVisible = true
|
||||
}
|
||||
}
|
||||
val grouped = VideoFileHelper.groupVideosByDay(queryVideoInfoListInFolder)
|
||||
val sortedGrouped = grouped.toSortedMap(compareByDescending { it })
|
||||
|
||||
val listOf = mutableListOf<VideoGroup>()
|
||||
sortedGrouped.forEach { (date, videos) ->
|
||||
listOf.add(VideoGroup(date, videos))
|
||||
}
|
||||
initRecent(listOf[0].videos[0])
|
||||
|
||||
val removeRecent = removeRecent(listOf)
|
||||
if (removeRecent.isEmpty()||removeRecent[0].videos.isEmpty()) {
|
||||
Common.showLog("removeRecent.isEmpty()")
|
||||
return
|
||||
}
|
||||
Common.showLog("--------------------videoRecycler-")
|
||||
|
||||
val videoGroupAdapter = VideoGroupAdapter(requireContext()) {
|
||||
}
|
||||
|
||||
binding.videoRecycler.run {
|
||||
adapter = videoGroupAdapter.apply {
|
||||
updateData(removeRecent)
|
||||
}
|
||||
layoutManager = LinearLayoutManager(requireContext())
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
private fun removeRecent(list: MutableList<VideoGroup>): MutableList<VideoGroup> {
|
||||
val firstItem = list[0]
|
||||
if (firstItem.videos.isNotEmpty()) {
|
||||
val updatedFirstItem = firstItem.copy(
|
||||
videos = firstItem.videos.drop(1) // 去除 videos 的第一个元素
|
||||
)
|
||||
list[0] = updatedFirstItem
|
||||
}
|
||||
return list
|
||||
|
||||
|
||||
}
|
||||
|
||||
private fun initRecent(videoInfo: VideoInfo) {
|
||||
binding.run {
|
||||
val duration = Common.formatDuration(videoInfo.duration)
|
||||
recentImg.setImageBitmap(videoInfo.thumbnail)
|
||||
recentName.text = videoInfo.displayName
|
||||
recentTime.text = duration
|
||||
|
||||
|
||||
layoutRecent.setOnClickListener {
|
||||
requireActivity().startActivity(
|
||||
Intent(
|
||||
requireContext(),
|
||||
PlayActivity::class.java
|
||||
).apply {
|
||||
putExtra(PlayActivity.KEY_URI, videoInfo.uri)
|
||||
putExtra(PlayActivity.KEY_name,videoInfo.displayName)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,6 @@
|
||||
package com.audio.record.screen.test.service
|
||||
|
||||
interface ConnectionListener {
|
||||
fun onServiceConnected()
|
||||
|
||||
}
|
||||
@ -0,0 +1,22 @@
|
||||
package com.audio.record.screen.test.service;
|
||||
|
||||
import androidx.lifecycle.Lifecycle;
|
||||
import androidx.lifecycle.LifecycleOwner;
|
||||
import androidx.lifecycle.LifecycleRegistry;
|
||||
|
||||
public class CustomLifecycleOwner implements LifecycleOwner {
|
||||
private final LifecycleRegistry lifecycleRegistry = new LifecycleRegistry(this);
|
||||
|
||||
@Override
|
||||
public Lifecycle getLifecycle() {
|
||||
return lifecycleRegistry;
|
||||
}
|
||||
|
||||
public void onStart() {
|
||||
lifecycleRegistry.markState(Lifecycle.State.STARTED);
|
||||
}
|
||||
|
||||
public void onStop() {
|
||||
lifecycleRegistry.markState(Lifecycle.State.CREATED);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,10 @@
|
||||
package com.audio.record.screen.test.service
|
||||
|
||||
interface FloatingCallback {
|
||||
//开始录制
|
||||
fun onStartRecording(){}
|
||||
//更新录制时间
|
||||
fun onUpdateRecordTime(time:String){}
|
||||
|
||||
fun onStopRecord(){}
|
||||
}
|
||||
@ -0,0 +1,121 @@
|
||||
package com.audio.record.screen.test.service
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.ComponentName
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.ServiceConnection
|
||||
import android.media.projection.MediaProjection
|
||||
import android.os.IBinder
|
||||
import androidx.core.content.ContextCompat
|
||||
import com.audio.record.screen.test.tool.Common
|
||||
import java.lang.ref.WeakReference
|
||||
|
||||
object FloatingWindowBridge {
|
||||
|
||||
|
||||
//activity 向service发送
|
||||
val COMMEND_show_camera = "show_camera"
|
||||
val COMMEND_hide_camera = "hide_camera"
|
||||
|
||||
val COMMEND_show_screenshot = "show_screenshot"
|
||||
val COMMEND_hide_screenshot = "hide_screenshot"
|
||||
|
||||
val COMMEND_show_ball = "show_ball"
|
||||
val COMMEND_hide_ball = "hide_ball"
|
||||
|
||||
val COMMEND_show_countdown = "show_countdown"
|
||||
val COMMEND_pause_record = "pause_record"
|
||||
val COMMEND_resume_record = "resume_record"
|
||||
val COMMEND_stop_record = "stop_record"
|
||||
|
||||
|
||||
//service 向activity反馈
|
||||
val CALL_start_recording = "start_recording"
|
||||
|
||||
@SuppressLint("StaticFieldLeak")
|
||||
private var service: ScreenRecordService? = null
|
||||
private var isBound = false
|
||||
|
||||
private val listeners = mutableSetOf<WeakReference<ConnectionListener>>()
|
||||
|
||||
private var mBinder: ScreenRecordService.FloatingBinder? = null
|
||||
private val connection = object : ServiceConnection {
|
||||
override fun onServiceConnected(name: ComponentName?, binder: IBinder?) {
|
||||
Common.showLog("=======onServiceConnected")
|
||||
mBinder = binder as? ScreenRecordService.FloatingBinder
|
||||
service = mBinder?.getService()
|
||||
listeners.forEach { it.get()?.onServiceConnected() }
|
||||
}
|
||||
|
||||
override fun onServiceDisconnected(name: ComponentName?) {
|
||||
Common.showLog("=======onServiceDisconnected")
|
||||
service = null
|
||||
isBound = false
|
||||
}
|
||||
}
|
||||
|
||||
fun addListener(listener: ConnectionListener) {
|
||||
listeners.add(WeakReference(listener))
|
||||
}
|
||||
|
||||
fun removeListener(listener: ConnectionListener) {
|
||||
listeners.removeAll { it.get() == null || it.get() == listener }
|
||||
}
|
||||
|
||||
fun startAndBindService(context: Context) {
|
||||
val intent = Intent(context, ScreenRecordService::class.java)
|
||||
//不受进程存在与否限制
|
||||
ContextCompat.startForegroundService(context, intent)
|
||||
if (!isBound) {
|
||||
//通信
|
||||
context.bindService(intent, connection, Context.BIND_AUTO_CREATE)
|
||||
isBound = true
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
fun updateAudio(audio: Boolean) {
|
||||
service?.setIsAudio(audio)
|
||||
}
|
||||
|
||||
fun sendCommand(command: String, countdownValues: Array<String>? = null) {
|
||||
when (command) {
|
||||
COMMEND_hide_camera -> service?.hideFrontCamera()
|
||||
COMMEND_show_camera -> service?.showCameraView()
|
||||
COMMEND_hide_ball -> service?.hideBall()
|
||||
COMMEND_show_ball -> service?.showBall()
|
||||
COMMEND_hide_screenshot -> service?.hideScreenshot()
|
||||
COMMEND_show_screenshot -> service?.showScreenshot()
|
||||
COMMEND_show_countdown -> countdownValues?.let { service?.showCountDownView(it) }
|
||||
COMMEND_pause_record -> service?.pauseRecording()
|
||||
COMMEND_resume_record -> service?.resumeRecording()
|
||||
COMMEND_stop_record -> service?.stopRecording()
|
||||
else -> {
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun updateMediaProjection(code: Int, mIntent: Intent) {
|
||||
service?.createMediaProjection(code, mIntent)
|
||||
|
||||
}
|
||||
|
||||
fun getMediaProjection() = service?.mIntent
|
||||
|
||||
fun registerCallback(callback: FloatingCallback) {
|
||||
mBinder?.registerCallback(callback)
|
||||
}
|
||||
|
||||
fun unregisterCallback(callback: FloatingCallback) {
|
||||
mBinder?.unregisterCallback(callback)
|
||||
}
|
||||
|
||||
fun unbind(context: Context) {
|
||||
if (isBound) {
|
||||
context.unbindService(connection)
|
||||
isBound = false
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,672 @@
|
||||
package com.audio.record.screen.test.service
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
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
|
||||
import android.hardware.display.DisplayManager
|
||||
import android.hardware.display.VirtualDisplay
|
||||
import android.media.MediaRecorder
|
||||
import android.media.projection.MediaProjection
|
||||
import android.media.projection.MediaProjectionManager
|
||||
import android.net.Uri
|
||||
import android.os.Binder
|
||||
import android.os.Build
|
||||
import android.os.Environment
|
||||
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
|
||||
import android.view.WindowManager
|
||||
import android.view.animation.AccelerateDecelerateInterpolator
|
||||
import android.widget.FrameLayout
|
||||
import android.widget.ImageView
|
||||
import android.widget.RemoteViews
|
||||
import android.widget.TextView
|
||||
import androidx.activity.result.ActivityResultLauncher
|
||||
import androidx.camera.core.CameraSelector
|
||||
import androidx.camera.core.Preview
|
||||
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 com.audio.record.screen.test.R
|
||||
import com.audio.record.screen.test.activity.PlayActivity
|
||||
import com.audio.record.screen.test.tool.Common
|
||||
import com.audio.record.screen.test.tool.DraggableViewHelper
|
||||
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 isScreenshotViewAdded = false
|
||||
private var isWebcamViewAdded = false
|
||||
private var isRecordViewAdded = false
|
||||
|
||||
//前置摄像头View
|
||||
private var frontCameraView: View? = null
|
||||
private lateinit var cameraView: PreviewView
|
||||
|
||||
//悬浮球View
|
||||
private var ballView: View? = null
|
||||
|
||||
//截屏View
|
||||
private var screenshotView: View? = null
|
||||
|
||||
//倒计时
|
||||
private var countdownView: View? = null
|
||||
private lateinit var countDownHandler: Handler
|
||||
private var index = 0
|
||||
|
||||
//录屏成功View
|
||||
private var recordView: View? = null
|
||||
|
||||
private val windowManager by lazy { getSystemService(WINDOW_SERVICE) as WindowManager }
|
||||
private lateinit var layoutParamsBall: WindowManager.LayoutParams
|
||||
private lateinit var layoutParamsScreenshot: WindowManager.LayoutParams
|
||||
private lateinit var layoutParamsCameraView: WindowManager.LayoutParams
|
||||
private lateinit var layoutParamsCountDown: WindowManager.LayoutParams
|
||||
private lateinit var layoutParamsRecordView: WindowManager.LayoutParams
|
||||
private lateinit var screenWH: Pair<Int, Int>
|
||||
|
||||
|
||||
//截屏
|
||||
private lateinit var screenCaptureLauncher: ActivityResultLauncher<Intent>
|
||||
private lateinit var mediaProjectionManager: MediaProjectionManager
|
||||
|
||||
|
||||
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 var tmpVideoPfd: ParcelFileDescriptor? = null
|
||||
private var isWithAudio = false
|
||||
|
||||
private val callbacks = mutableListOf<WeakReference<FloatingCallback>>()
|
||||
|
||||
|
||||
//录制时间监听相关
|
||||
private var recordingStartTime: Long = 0L // 本次录制开始时间
|
||||
private val mRecorderHandler = Handler(Looper.getMainLooper())
|
||||
private val updateInterval = 1000L
|
||||
private var pauseStartTime: Long = 0L // 本次暂停的开始时间
|
||||
private var totalPausedTime: Long = 0L // 所有暂停段的总时间
|
||||
|
||||
//当前是否处于录制暂停
|
||||
private var isPause = false
|
||||
private lateinit var virtualDisplay: VirtualDisplay
|
||||
|
||||
private var mediaProjection:MediaProjection? = null
|
||||
|
||||
private val timeUpdateRunnable = object : Runnable {
|
||||
override fun run() {
|
||||
Common.showLog("-------timeUpdateRunnable 更新时间.....")
|
||||
val currentRecordingTimeInMillis =
|
||||
System.currentTimeMillis() - recordingStartTime - totalPausedTime
|
||||
val elapsed = currentRecordingTimeInMillis / 1000
|
||||
val onRecordingTimeChanged = Common.onRecordingTimeChanged(elapsed)
|
||||
callbacks.forEach {
|
||||
it.get()?.onUpdateRecordTime(onRecordingTimeChanged)
|
||||
}
|
||||
if (!isPause)
|
||||
mRecorderHandler.postDelayed(this, updateInterval)
|
||||
}
|
||||
}
|
||||
|
||||
inner class FloatingBinder : Binder() {
|
||||
fun getService() = this@ScreenRecordService
|
||||
fun setCallback(cb: FloatingCallback) {
|
||||
callbackRef = cb
|
||||
|
||||
}
|
||||
|
||||
fun registerCallback(callback: FloatingCallback) {
|
||||
// 避免重复添加
|
||||
if (callbacks.any { it.get() == callback }) return
|
||||
callbacks.add(WeakReference(callback))
|
||||
Common.showLog("=======registerCallback callback=${callback}")
|
||||
}
|
||||
|
||||
fun unregisterCallback(callback: FloatingCallback) {
|
||||
callbacks.removeAll {
|
||||
it.get() == null || it.get() == callback
|
||||
}
|
||||
Common.showLog("=======unregisterCallback callback=${callback}")
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置当前录制是否带音频
|
||||
*/
|
||||
fun setIsAudio(audio: Boolean) {
|
||||
isWithAudio = audio
|
||||
Common.showLog("Service---isWithAudio=${isWithAudio}")
|
||||
}
|
||||
|
||||
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
Common.showLog("Service---onCreate")
|
||||
lifecycleOwner = CustomLifecycleOwner()
|
||||
screenWH = Common.getScreenWH(this)
|
||||
|
||||
mediaProjectionManager =
|
||||
getSystemService(Context.MEDIA_PROJECTION_SERVICE) as MediaProjectionManager
|
||||
|
||||
|
||||
}
|
||||
|
||||
fun createMediaProjection(code: Int, m: Intent) {
|
||||
mIntent = m
|
||||
mCode = code
|
||||
// mediaProjection = mediaProjectionManager.getMediaProjection(code, m)
|
||||
}
|
||||
fun resetIntent(){
|
||||
mIntent = null
|
||||
}
|
||||
|
||||
|
||||
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
||||
Common.showLog("Service---onStartCommand")
|
||||
lifecycleOwner?.onStart()
|
||||
// 初始化 MediaProjection、MediaRecorder 逻辑等
|
||||
return START_NOT_STICKY
|
||||
}
|
||||
|
||||
override fun onBind(intent: Intent?): IBinder {
|
||||
lifecycleOwner?.onStart()
|
||||
Common.showLog("Service---onBind")
|
||||
startForeground(1, createNotification())
|
||||
return FloatingBinder()
|
||||
}
|
||||
|
||||
|
||||
//构建通知栏
|
||||
private fun createNotification(): Notification {
|
||||
|
||||
val remoteViews = RemoteViews(packageName, R.layout.custom_notification)
|
||||
val channelId = "screen_record_channel"
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
val channel = NotificationChannel(
|
||||
channelId,
|
||||
"录屏",
|
||||
NotificationManager.IMPORTANCE_LOW
|
||||
)
|
||||
getSystemService(NotificationManager::class.java).createNotificationChannel(channel)
|
||||
}
|
||||
|
||||
return NotificationCompat.Builder(this, channelId)
|
||||
.setContentTitle("屏幕录制中")
|
||||
.setContentText("正在录制屏幕...")
|
||||
.setCustomContentView(remoteViews)
|
||||
.setCustomBigContentView(remoteViews)
|
||||
.setSmallIcon(R.drawable.test)
|
||||
.build()
|
||||
}
|
||||
|
||||
@SuppressLint("ClickableViewAccessibility")
|
||||
fun showCameraView() {
|
||||
if (frontCameraView == null) {
|
||||
frontCameraView =
|
||||
LayoutInflater.from(this).inflate(R.layout.floating_front_camera, null)
|
||||
layoutParamsCameraView = getLayoutParams()
|
||||
layoutParamsCameraView.gravity = Gravity.TOP or Gravity.START
|
||||
cameraView = frontCameraView!!.findViewById<PreviewView>(R.id.previewView)
|
||||
startFrontCamera(cameraView)
|
||||
DraggableViewHelper.attachToWindow(
|
||||
frontCameraView!!,
|
||||
layoutParamsCameraView,
|
||||
windowManager,
|
||||
true
|
||||
)
|
||||
|
||||
}
|
||||
if (!isWebcamViewAdded) {
|
||||
windowManager.addView(frontCameraView, layoutParamsCameraView)
|
||||
isWebcamViewAdded = true
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
override fun onDestroy() {
|
||||
super.onDestroy()
|
||||
hideFrontCamera() // 自动清除
|
||||
hideBall()
|
||||
hideScreenshot()
|
||||
frontCameraView = null
|
||||
ballView = null
|
||||
screenshotView = null
|
||||
callbackRef = null
|
||||
lifecycleOwner?.onStop()
|
||||
Common.showLog("Service---onDestroy")
|
||||
}
|
||||
|
||||
fun startFrontCamera(view: PreviewView) {
|
||||
val cameraProviderFuture = ProcessCameraProvider.getInstance(this)
|
||||
cameraProviderFuture.addListener({
|
||||
val cameraProvider = cameraProviderFuture.get()
|
||||
|
||||
// 设置预览
|
||||
val preview = Preview.Builder().build().also {
|
||||
it.surfaceProvider = view.surfaceProvider
|
||||
}
|
||||
|
||||
// 前置摄像头选择器
|
||||
val cameraSelector = CameraSelector.Builder()
|
||||
.requireLensFacing(CameraSelector.LENS_FACING_FRONT)
|
||||
.build()
|
||||
|
||||
try {
|
||||
cameraProvider.unbindAll()
|
||||
cameraProvider.bindToLifecycle(lifecycleOwner!!, cameraSelector, preview)
|
||||
} catch (e: Exception) {
|
||||
Common.showLog("绑定前置摄像头失败==$e")
|
||||
}
|
||||
|
||||
}, ContextCompat.getMainExecutor(this))
|
||||
}
|
||||
|
||||
|
||||
//显示悬浮球
|
||||
fun showBall() {
|
||||
if (ballView == null) {
|
||||
ballView = LayoutInflater.from(this).inflate(R.layout.floating_ball, null)
|
||||
layoutParamsBall = getLayoutParams()
|
||||
val size = Common.dpToPx(36, this)
|
||||
layoutParamsBall.gravity = Gravity.TOP or Gravity.START
|
||||
layoutParamsBall.x = screenWH.first - size - 20
|
||||
layoutParamsBall.y = screenWH.second / 2 - size.div(2) // 居中
|
||||
|
||||
|
||||
DraggableViewHelper.attachToWindow(ballView!!, layoutParamsBall, windowManager, true)
|
||||
Common.showLog("-------layoutParamsBall.x=${layoutParamsBall.x} layoutParamsBall.y=${layoutParamsBall.y}")
|
||||
}
|
||||
if (!isBallViewAdded) {
|
||||
windowManager.addView(ballView, layoutParamsBall)
|
||||
isBallViewAdded = true
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
//显示截屏
|
||||
fun showScreenshot() {
|
||||
if (screenshotView == null) {
|
||||
screenshotView = LayoutInflater.from(this).inflate(R.layout.floating_screenshot, null)
|
||||
layoutParamsScreenshot = getLayoutParams()
|
||||
|
||||
val size = Common.dpToPx(36, this)
|
||||
layoutParamsScreenshot.gravity = Gravity.TOP or Gravity.START
|
||||
layoutParamsScreenshot.x = 20 // 右侧偏移
|
||||
layoutParamsScreenshot.y = screenWH.second / 2 - size.div(2) // 居中
|
||||
|
||||
DraggableViewHelper.attachToWindow(
|
||||
screenshotView!!,
|
||||
layoutParamsScreenshot,
|
||||
windowManager,
|
||||
true
|
||||
) {
|
||||
|
||||
Common.showLog("--------------callbackRef=${callbackRef}")
|
||||
// callbackRef?.onFloatingButtonClicked(FloatingWindowBridge.CLICK_screenshot)
|
||||
mIntent?.let {
|
||||
mediaProjectionManager.getMediaProjection(mCode, it)?.let {
|
||||
hideScreenshot()
|
||||
ScreenCaptureHelper.startScreenCapture(this, it) {
|
||||
// showScreenshot()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!isScreenshotViewAdded) {
|
||||
windowManager.addView(screenshotView, layoutParamsScreenshot)
|
||||
isScreenshotViewAdded = true
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
//显示截屏
|
||||
fun showRecordView() {
|
||||
if (recordView == null) {
|
||||
recordView = 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()
|
||||
}
|
||||
recordView!!.findViewById<FrameLayout>(R.id.layout_video).setOnClickListener {
|
||||
// TODO: 跳到播放页面
|
||||
val intent = Intent(this, PlayActivity::class.java).apply {
|
||||
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||
}
|
||||
this.startActivity(intent)
|
||||
|
||||
}
|
||||
}
|
||||
if (!isRecordViewAdded) {
|
||||
|
||||
val videoThumbnail = Common.getVideoThumbnail(tmpVideoPfd!!)
|
||||
Common.showLog("--------------videoThumbnail=${videoThumbnail}")
|
||||
val thumb = recordView!!.findViewById<ImageView>(R.id.image)
|
||||
|
||||
thumb.setImageBitmap(videoThumbnail)
|
||||
|
||||
windowManager.addView(recordView, layoutParamsRecordView)
|
||||
isRecordViewAdded = true
|
||||
}
|
||||
}
|
||||
|
||||
fun hideFrontCamera() {
|
||||
frontCameraView?.let {
|
||||
try {
|
||||
if (isWebcamViewAdded) {
|
||||
windowManager.removeView(it)
|
||||
isWebcamViewAdded = false
|
||||
}
|
||||
|
||||
} catch (_: Exception) {
|
||||
}
|
||||
}
|
||||
// frontCameraView = null
|
||||
}
|
||||
|
||||
//隐藏录制预览View
|
||||
fun hideRecordView() {
|
||||
recordView?.let {
|
||||
try {
|
||||
if (isRecordViewAdded) {
|
||||
windowManager.removeView(it)
|
||||
isRecordViewAdded = false
|
||||
}
|
||||
|
||||
} catch (_: Exception) {
|
||||
}
|
||||
}
|
||||
// frontCameraView = null
|
||||
}
|
||||
|
||||
//隐藏悬浮球
|
||||
fun hideBall() {
|
||||
ballView?.let {
|
||||
try {
|
||||
windowManager.removeView(it)
|
||||
isBallViewAdded = false
|
||||
} catch (_: Exception) {
|
||||
}
|
||||
}
|
||||
// ballView = null
|
||||
}
|
||||
|
||||
//隐藏截屏
|
||||
fun hideScreenshot() {
|
||||
screenshotView?.let {
|
||||
try {
|
||||
windowManager.removeView(it)
|
||||
isScreenshotViewAdded = false
|
||||
} catch (_: Exception) {
|
||||
}
|
||||
}
|
||||
// screenshotView = null
|
||||
}
|
||||
|
||||
|
||||
//显示倒计时
|
||||
fun showCountDownView(countdownValues: Array<String>) {
|
||||
if (countdownView == null) {
|
||||
countdownView =
|
||||
LayoutInflater.from(this).inflate(R.layout.floating_view_countdown, null)
|
||||
layoutParamsCountDown = getLayoutParams()
|
||||
layoutParamsCountDown.gravity = Gravity.CENTER
|
||||
|
||||
}
|
||||
if (countdownView?.windowToken == null) {
|
||||
windowManager.addView(countdownView, layoutParamsCountDown)
|
||||
val tvCountdown = countdownView!!.findViewById<TextView>(R.id.tv_countdown)
|
||||
index = 0
|
||||
countDownHandler = Handler(Looper.getMainLooper())
|
||||
animateNext(countdownValues, tvCountdown) {
|
||||
//倒计时结束后,开启录制
|
||||
hideCountDown()
|
||||
mIntent?.let { intent ->
|
||||
mediaProjectionManager.getMediaProjection(mCode, intent)
|
||||
?.let { media->
|
||||
mediaProjection = media
|
||||
startRecording()
|
||||
callbacks.forEach {
|
||||
it.get()?.onStartRecording()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
private fun animateNext(
|
||||
countdownValues: Array<String>,
|
||||
tvCountdown: TextView,
|
||||
onFinish: (() -> Unit)? = null
|
||||
) {
|
||||
if (index >= countdownValues.size) {
|
||||
CountDownFloatingManager.remove(windowManager)
|
||||
onFinish?.invoke()
|
||||
return
|
||||
}
|
||||
|
||||
tvCountdown.text = countdownValues[index]
|
||||
tvCountdown.scaleX = 0f
|
||||
tvCountdown.scaleY = 0f
|
||||
tvCountdown.alpha = 0f
|
||||
|
||||
tvCountdown.animate()
|
||||
.scaleX(1f).scaleY(1f)
|
||||
.alpha(1f)
|
||||
.setDuration(400)
|
||||
.setInterpolator(AccelerateDecelerateInterpolator())
|
||||
.withEndAction {
|
||||
countDownHandler?.postDelayed({
|
||||
index++
|
||||
animateNext(countdownValues, tvCountdown, onFinish)
|
||||
}, 600)
|
||||
}
|
||||
.start()
|
||||
}
|
||||
|
||||
|
||||
//移除倒计时View
|
||||
fun hideCountDown() {
|
||||
countDownHandler.removeCallbacksAndMessages(null)
|
||||
countdownView?.let {
|
||||
try {
|
||||
windowManager.removeView(it)
|
||||
} catch (e: Exception) {
|
||||
// view already removed
|
||||
}
|
||||
}
|
||||
countdownView = null
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 开启录制视频
|
||||
*/
|
||||
fun startRecording() {
|
||||
Common.showLog("-------录屏中.....")
|
||||
val fullScreenSize = Common.getFullScreenSize(this)
|
||||
val width = VideoFileHelper.alignTo16(fullScreenSize.first)
|
||||
val height = VideoFileHelper.alignTo16(fullScreenSize.second)
|
||||
initRecorder(width, height)
|
||||
mediaRecorder.start()
|
||||
recordingStartTime = System.currentTimeMillis()
|
||||
totalPausedTime = 0L
|
||||
mRecorderHandler.post(timeUpdateRunnable)
|
||||
|
||||
mediaProjection?.let {
|
||||
it.registerCallback(object : MediaProjection.Callback() {
|
||||
override fun onStop() {
|
||||
Common.showLog("MediaProjection 被系统或用户停止")
|
||||
// 这里应该释放 MediaRecorder 和 VirtualDisplay 等
|
||||
stopRecording()
|
||||
|
||||
}
|
||||
}, Handler(Looper.getMainLooper()))
|
||||
virtualDisplay = it.createVirtualDisplay(
|
||||
"ScreenRecord",
|
||||
width, height, resources.displayMetrics.densityDpi,
|
||||
DisplayManager.VIRTUAL_DISPLAY_FLAG_AUTO_MIRROR,
|
||||
mediaRecorder.surface, object : VirtualDisplay.Callback() {
|
||||
override fun onPaused() {
|
||||
super.onPaused()
|
||||
Common.showLog("--VirtualDisplay.Callback..onPaused...")
|
||||
}
|
||||
|
||||
override fun onResumed() {
|
||||
super.onResumed()
|
||||
Common.showLog("--VirtualDisplay.Callback..onResumed...")
|
||||
}
|
||||
|
||||
override fun onStopped() {
|
||||
super.onStopped()
|
||||
Common.showLog("--VirtualDisplay.Callback..onStopped...")
|
||||
}
|
||||
}, null
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
private fun releaseAll() {
|
||||
if (::mediaRecorder.isInitialized) {
|
||||
try {
|
||||
mediaRecorder.stop()
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
|
||||
try {
|
||||
mediaRecorder.reset()
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
|
||||
try {
|
||||
mediaRecorder.release()
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
virtualDisplay?.release()
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
|
||||
try {
|
||||
mediaProjection?.stop()
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 暂停录制视频
|
||||
*/
|
||||
fun pauseRecording() {
|
||||
Common.showLog("-------暂停.....")
|
||||
mediaRecorder.pause()
|
||||
pauseStartTime = System.currentTimeMillis()
|
||||
isPause = true
|
||||
}
|
||||
|
||||
/**
|
||||
* 继续录制视频
|
||||
*/
|
||||
fun resumeRecording() {
|
||||
Common.showLog("------继续.....")
|
||||
mediaRecorder.resume()
|
||||
totalPausedTime += System.currentTimeMillis() - pauseStartTime
|
||||
isPause = false
|
||||
mRecorderHandler.post(timeUpdateRunnable)
|
||||
}
|
||||
|
||||
private fun initRecorder(width: Int, height: Int) {
|
||||
val (Uri, pfd) = VideoFileHelper.createVideoFile(this, Common.folderName)
|
||||
tmpVideoUri = Uri
|
||||
tmpVideoPfd = pfd
|
||||
mediaRecorder = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
||||
MediaRecorder(applicationContext)
|
||||
} else {
|
||||
MediaRecorder()
|
||||
}.apply {
|
||||
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)
|
||||
setAudioEncoder(MediaRecorder.AudioEncoder.AAC)
|
||||
setVideoEncodingBitRate(8 * 1000 * 1000)
|
||||
setVideoFrameRate(30)
|
||||
prepare()
|
||||
}
|
||||
}
|
||||
|
||||
fun stopRecording() {
|
||||
Common.showLog("-------录屏完成.....")
|
||||
mRecorderHandler.removeCallbacks(timeUpdateRunnable)
|
||||
releaseAll()
|
||||
VideoFileHelper.markVideoFileCompleted(this, tmpVideoUri)
|
||||
showRecordView()
|
||||
callbacks.forEach {
|
||||
it.get()?.onStopRecord()
|
||||
}
|
||||
}
|
||||
|
||||
private fun getLayoutParams(): WindowManager.LayoutParams {
|
||||
return WindowManager.LayoutParams(
|
||||
WindowManager.LayoutParams.WRAP_CONTENT,
|
||||
WindowManager.LayoutParams.WRAP_CONTENT,
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O)
|
||||
WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY
|
||||
else
|
||||
WindowManager.LayoutParams.TYPE_PHONE,
|
||||
WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL or
|
||||
WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE or
|
||||
WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS,
|
||||
|
||||
PixelFormat.TRANSLUCENT
|
||||
)
|
||||
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@ -0,0 +1,16 @@
|
||||
package com.audio.record.screen.test.service
|
||||
|
||||
|
||||
import android.content.Context
|
||||
import android.util.AttributeSet
|
||||
import android.widget.FrameLayout
|
||||
|
||||
class TouchThroughLayout @JvmOverloads constructor(
|
||||
context: Context,
|
||||
attrs: AttributeSet? = null,
|
||||
defStyleAttr: Int = 0
|
||||
) : FrameLayout(context, attrs, defStyleAttr) {
|
||||
override fun onInterceptTouchEvent(ev: android.view.MotionEvent?) = false
|
||||
override fun onTouchEvent(event: android.view.MotionEvent?) = false
|
||||
}
|
||||
|
||||
431
app/src/main/java/com/audio/record/screen/test/tool/Common.kt
Normal file
431
app/src/main/java/com/audio/record/screen/test/tool/Common.kt
Normal file
@ -0,0 +1,431 @@
|
||||
package com.audio.record.screen.test.tool
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.pm.ActivityInfo
|
||||
import android.graphics.Bitmap
|
||||
import android.graphics.Color
|
||||
import android.media.MediaMetadataRetriever
|
||||
import android.os.Build
|
||||
import android.os.Environment
|
||||
import android.os.ParcelFileDescriptor
|
||||
import android.util.DisplayMetrics
|
||||
import android.util.Log
|
||||
import android.util.TypedValue
|
||||
import android.view.KeyCharacterMap
|
||||
import android.view.KeyEvent
|
||||
import android.view.View
|
||||
import android.view.ViewConfiguration
|
||||
import android.view.WindowManager
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.view.ViewCompat
|
||||
import androidx.core.view.WindowCompat
|
||||
import androidx.core.view.WindowInsetsCompat
|
||||
import androidx.core.view.WindowInsetsControllerCompat
|
||||
import com.audio.record.screen.test.App
|
||||
import com.audio.record.screen.test.tool.Extend.setMarginBottom
|
||||
import java.io.File
|
||||
|
||||
object Common {
|
||||
val folderName = "ScreenRecording_Test"
|
||||
|
||||
val videosFolderDir = Environment.DIRECTORY_MOVIES + "/${folderName}"
|
||||
val imagesFolderDir = Environment.DIRECTORY_PICTURES + "/${folderName}"
|
||||
|
||||
fun getIcon(drawId: Int) = ContextCompat.getDrawable(App.instanceApp, drawId)
|
||||
|
||||
fun showLog(msg: String) {
|
||||
Log.d(App.TAG, msg)
|
||||
}
|
||||
|
||||
fun setStatusBarTextColor(activity: Activity, dark: Boolean) {
|
||||
// val window = activity.window
|
||||
// val decor = window.decorView
|
||||
//
|
||||
// // 设置状态栏图标颜色(深色图标表示浅色背景)
|
||||
// var flags = decor.systemUiVisibility
|
||||
// flags = if (dark) {
|
||||
// flags or View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR
|
||||
// } else {
|
||||
// flags and View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR.inv()
|
||||
// }
|
||||
// // 保持布局全屏
|
||||
// flags = flags or View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
|
||||
// decor.systemUiVisibility = flags
|
||||
//
|
||||
// // 去除 TRANSLUCENT_STATUS,使用透明背景更现代
|
||||
// window.clearFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS)
|
||||
// window.statusBarColor = Color.TRANSPARENT
|
||||
|
||||
|
||||
|
||||
val window = activity.window
|
||||
val decor = window.decorView
|
||||
|
||||
// 设置状态栏图标颜色:深色图标表示浅色背景
|
||||
var flags = decor.systemUiVisibility
|
||||
flags = if (dark) {
|
||||
flags or View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR
|
||||
} else {
|
||||
flags and View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR.inv()
|
||||
}
|
||||
|
||||
// 保持布局延伸到状态栏区域
|
||||
flags = flags or View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN or View.SYSTEM_UI_FLAG_LAYOUT_STABLE
|
||||
decor.systemUiVisibility = flags
|
||||
|
||||
// 去除旧的半透明标志,改为真正透明
|
||||
window.clearFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS)
|
||||
window.addFlags(WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS)
|
||||
window.statusBarColor = Color.TRANSPARENT
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
fun dpToPx(dp: Int, context: Context): Int =
|
||||
TypedValue.applyDimension(
|
||||
TypedValue.COMPLEX_UNIT_DIP,
|
||||
dp.toFloat(),
|
||||
context.resources.displayMetrics,
|
||||
).toInt()
|
||||
|
||||
|
||||
fun getScreenWH(context: Context): Pair<Int, Int> {
|
||||
val screenWidth = context.resources.displayMetrics.widthPixels
|
||||
val screenHeight = context.resources.displayMetrics.heightPixels
|
||||
|
||||
return Pair(screenWidth, screenHeight)
|
||||
}
|
||||
|
||||
/**
|
||||
* 在service中获取设备屏幕的实际像素尺寸(包含状态栏、导航栏)
|
||||
*/
|
||||
fun getFullScreenSize(context: Context): Pair<Int, Int> {
|
||||
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||
// Android 11 (API 30)+
|
||||
val windowManager = context.getSystemService(Context.WINDOW_SERVICE) as WindowManager
|
||||
val bounds = windowManager.maximumWindowMetrics.bounds
|
||||
Pair(bounds.width(), bounds.height())
|
||||
} else {
|
||||
// Android 10 及以下
|
||||
val windowManager = context.getSystemService(Context.WINDOW_SERVICE) as WindowManager
|
||||
val metrics = DisplayMetrics()
|
||||
@Suppress("DEPRECATION")
|
||||
windowManager.defaultDisplay.getRealMetrics(metrics)
|
||||
Pair(metrics.widthPixels, metrics.heightPixels)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
fun getFull(context: Context): Pair<Int, Int> {
|
||||
val windowManager = context.getSystemService(Context.WINDOW_SERVICE) as WindowManager
|
||||
val realMetrics = DisplayMetrics()
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||
context.display?.getRealMetrics(realMetrics)
|
||||
} else {
|
||||
@Suppress("DEPRECATION")
|
||||
windowManager.defaultDisplay.getRealMetrics(realMetrics)
|
||||
}
|
||||
|
||||
val realWidth = realMetrics.widthPixels
|
||||
val realHeight = realMetrics.heightPixels
|
||||
showLog("getScreenWH realWidth=${realWidth} realHeight=${realHeight}")
|
||||
return Pair(realWidth, realHeight)
|
||||
}
|
||||
|
||||
fun getNavigationBarHeight(context: Context): Int {
|
||||
val resources = context.resources
|
||||
val resourceId = resources.getIdentifier("navigation_bar_height", "dimen", "android")
|
||||
return if (resourceId > 0 && hasNavigationBar(context)) {
|
||||
showLog(" fun getNavigationBarHeight = ${resources.getDimensionPixelSize(resourceId)}")
|
||||
resources.getDimensionPixelSize(resourceId)
|
||||
} else {
|
||||
showLog(" fun getNavigationBarHeight = 0")
|
||||
0
|
||||
}
|
||||
}
|
||||
|
||||
fun hasNavigationBar(context: Context): Boolean {
|
||||
val id = context.resources.getIdentifier("config_showNavigationBar", "bool", "android")
|
||||
return if (id > 0) {
|
||||
context.resources.getBoolean(id)
|
||||
} else {
|
||||
// fallback 方法:判断是否有物理返回键(导航栏可能隐藏)
|
||||
val hasMenuKey = ViewConfiguration.get(context).hasPermanentMenuKey()
|
||||
val hasBackKey = KeyCharacterMap.deviceHasKey(KeyEvent.KEYCODE_BACK)
|
||||
!(hasMenuKey || hasBackKey)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// 获取状态栏高度(兼容所有API)
|
||||
fun getStatusBarHeight(context: Context): Int {
|
||||
return when {
|
||||
Build.VERSION.SDK_INT >= Build.VERSION_CODES.M -> {
|
||||
val windowInsets = (context as? Activity)?.window?.decorView?.rootWindowInsets
|
||||
windowInsets?.stableInsetTop ?: getSystemStatusBarHeight(context)
|
||||
}
|
||||
|
||||
else -> getSystemStatusBarHeight(context)
|
||||
}
|
||||
}
|
||||
|
||||
private fun getSystemStatusBarHeight(context: Context): Int {
|
||||
val resources = context.resources
|
||||
val resourceId = resources.getIdentifier("status_bar_height", "dimen", "android")
|
||||
return if (resourceId > 0) {
|
||||
resources.getDimensionPixelSize(resourceId)
|
||||
} else {
|
||||
// 默认回退值(24dp是常见值)
|
||||
(24 * resources.displayMetrics.density).toInt()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
fun getAllImagePaths(directoryPath: String): List<String> {
|
||||
val imagePaths = mutableListOf<String>()
|
||||
val directory = File(directoryPath)
|
||||
|
||||
if (directory.exists() && directory.isDirectory) {
|
||||
directory.walk().forEach { file ->
|
||||
if (file.isFile && isImageFile(file)) {
|
||||
imagePaths.add(file.absolutePath)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return imagePaths
|
||||
}
|
||||
|
||||
fun isImageFile(file: File): Boolean {
|
||||
val imageExtensions = listOf("jpg", "jpeg", "png", "bmp", "gif", "webp")
|
||||
val extension = file.extension.lowercase()
|
||||
return extension in imageExtensions
|
||||
}
|
||||
|
||||
fun deleteAllFilesInDirectory(path: String) {
|
||||
val dir = File(path)
|
||||
if (dir.exists() && dir.isDirectory) {
|
||||
dir.listFiles()?.forEach { file ->
|
||||
if (file.isFile) {
|
||||
file.delete()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 将缩略图按照 thumb_1、thumb_2排序返回
|
||||
*/
|
||||
fun getNaturallySortedThumbFiles(path: String): List<String> {
|
||||
val dir = File(path)
|
||||
if (!dir.exists() || !dir.isDirectory) return emptyList()
|
||||
val files = dir.listFiles()
|
||||
?.filter { it.isFile && it.name.startsWith("thumb_") }
|
||||
?.sortedBy { extractNumberFromName(it.name) } ?: emptyList()
|
||||
return files.map {
|
||||
showLog("-----${it.absolutePath}")
|
||||
it.absolutePath
|
||||
}
|
||||
}
|
||||
|
||||
// 提取文件名中的数字部分(如 thumb_5.jpg → 5)
|
||||
private fun extractNumberFromName(name: String): Int {
|
||||
val regex = Regex("thumb_(\\d+)")
|
||||
val match = regex.find(name)
|
||||
return match?.groupValues?.get(1)?.toIntOrNull() ?: Int.MAX_VALUE
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算视频时长,精确到毫秒
|
||||
*/
|
||||
fun getVideoDurationMs(path: String): Long {
|
||||
val retriever = MediaMetadataRetriever()
|
||||
retriever.setDataSource(path)
|
||||
val duration =
|
||||
retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_DURATION)?.toLong() ?: 0L
|
||||
retriever.release()
|
||||
return duration
|
||||
}
|
||||
|
||||
fun millisToSeconds(millis: Long): String {
|
||||
val seconds = millis / 1000.0
|
||||
return String.format("%.1f", seconds)
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 毫秒数格式化时间格式 00:00:00.0
|
||||
*/
|
||||
fun formatSeconds(ms: Float): String {
|
||||
val totalSeconds = ms / 1000f
|
||||
val hours = (totalSeconds / 3600).toInt()
|
||||
val minutes = ((totalSeconds % 3600) / 60).toInt()
|
||||
val seconds = (totalSeconds % 60).toInt()
|
||||
val tenth = ((totalSeconds - totalSeconds.toInt()) * 10).toInt() // 小数点后一位
|
||||
|
||||
return String.format("%02d:%02d:%02d.%01d", hours, minutes, seconds, tenth)
|
||||
}
|
||||
|
||||
/**
|
||||
* 毫秒数格式化时间格式 00:00:00
|
||||
*/
|
||||
fun formatDuration(durationMs: Long): String {
|
||||
val totalSeconds = durationMs / 1000
|
||||
val hours = totalSeconds / 3600
|
||||
val minutes = (totalSeconds % 3600) / 60
|
||||
val seconds = totalSeconds % 60
|
||||
|
||||
return String.format("%02d:%02d:%02d", hours, minutes, seconds)
|
||||
}
|
||||
/**
|
||||
* bytes格式化
|
||||
*/
|
||||
fun formatFileSize(bytes: Long): String {
|
||||
val kb = 1024
|
||||
val mb = kb * 1024
|
||||
val gb = mb * 1024
|
||||
|
||||
return when {
|
||||
bytes >= gb -> "%.2f GB".format(bytes / gb.toFloat())
|
||||
bytes >= mb -> "%.2f MB".format(bytes / mb.toFloat())
|
||||
bytes >= kb -> "%.2f KB".format(bytes / kb.toFloat())
|
||||
else -> "$bytes B"
|
||||
}
|
||||
}
|
||||
|
||||
fun getVideoRatio(videoPath: String): Float {
|
||||
val retriever = MediaMetadataRetriever()
|
||||
retriever.setDataSource(videoPath)
|
||||
val width = retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_VIDEO_WIDTH)!!
|
||||
.toInt()
|
||||
val height = retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_VIDEO_HEIGHT)!!
|
||||
.toInt()
|
||||
val aspectRatio = width.toFloat() / height
|
||||
retriever.release()
|
||||
return aspectRatio
|
||||
}
|
||||
|
||||
fun getVideoWH(videoPath: String): Pair<Int, Int> {
|
||||
val retriever = MediaMetadataRetriever()
|
||||
retriever.setDataSource(videoPath)
|
||||
val width =
|
||||
retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_VIDEO_WIDTH)!!.toInt()
|
||||
val height =
|
||||
retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_VIDEO_HEIGHT)!!.toInt()
|
||||
retriever.release()
|
||||
return Pair(width, height)
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 返回桌面
|
||||
*/
|
||||
fun backHome(mContext: Context){
|
||||
val intent = Intent(Intent.ACTION_MAIN)
|
||||
intent.addCategory(Intent.CATEGORY_HOME)
|
||||
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK
|
||||
mContext.startActivity(intent)
|
||||
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 时间转换格式
|
||||
*/
|
||||
fun onRecordingTimeChanged(seconds: Long):String {
|
||||
val minutes = seconds / 60
|
||||
val sec = seconds % 60
|
||||
val formatted = String.format("%02d:%02d", minutes, sec)
|
||||
showLog("Recorder Recording duration: $formatted")
|
||||
return formatted
|
||||
// 示例:更新 UI TextView
|
||||
// binding.timerText.text = formatted
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取视频封面
|
||||
*/
|
||||
fun getVideoThumbnail(pfd: ParcelFileDescriptor): Bitmap? {
|
||||
val retriever = MediaMetadataRetriever()
|
||||
try {
|
||||
retriever.setDataSource(pfd.fileDescriptor)
|
||||
// 获取第1帧,时间戳 0 微秒
|
||||
return retriever.getFrameAtTime(0, MediaMetadataRetriever.OPTION_CLOSEST_SYNC)
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
return null
|
||||
} finally {
|
||||
retriever.release()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
fun hideSystemUI(activity: Activity) {
|
||||
activity.window.decorView.systemUiVisibility =
|
||||
View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY or
|
||||
View.SYSTEM_UI_FLAG_FULLSCREEN or
|
||||
View.SYSTEM_UI_FLAG_HIDE_NAVIGATION or
|
||||
View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN or
|
||||
View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION or
|
||||
View.SYSTEM_UI_FLAG_LAYOUT_STABLE
|
||||
}
|
||||
|
||||
fun enableImmersiveMode(activity: Activity) {
|
||||
// 允许内容扩展进系统栏区域
|
||||
WindowCompat.setDecorFitsSystemWindows(activity.window, false)
|
||||
|
||||
val controller = WindowInsetsControllerCompat(activity.window, activity.window.decorView)
|
||||
|
||||
// 隐藏状态栏和导航栏
|
||||
controller.hide(WindowInsetsCompat.Type.systemBars())
|
||||
|
||||
// 沉浸式行为:滑动出现系统栏后自动隐藏
|
||||
controller.systemBarsBehavior =
|
||||
WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE
|
||||
}
|
||||
|
||||
fun enterFullScreen(activity: Activity) {
|
||||
activity.window.setFlags(
|
||||
WindowManager.LayoutParams.FLAG_FULLSCREEN,
|
||||
WindowManager.LayoutParams.FLAG_FULLSCREEN
|
||||
)
|
||||
activity.window.decorView.systemUiVisibility = (
|
||||
View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY
|
||||
or View.SYSTEM_UI_FLAG_HIDE_NAVIGATION
|
||||
or View.SYSTEM_UI_FLAG_FULLSCREEN
|
||||
)
|
||||
|
||||
// 如果需要横屏播放:
|
||||
// activity.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE
|
||||
}
|
||||
|
||||
fun exitFullScreen(activity: Activity) {
|
||||
activity.window.clearFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN)
|
||||
activity.window.decorView.systemUiVisibility = View.SYSTEM_UI_FLAG_VISIBLE
|
||||
activity.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_PORTRAIT
|
||||
}
|
||||
|
||||
|
||||
fun setNavigation(rootView:View){
|
||||
ViewCompat.setOnApplyWindowInsetsListener(rootView) { view, insets ->
|
||||
val navBarInsets = insets.getInsets(WindowInsetsCompat.Type.navigationBars())
|
||||
val bottomPadding = navBarInsets.bottom
|
||||
view.setMarginBottom(bottomPadding)
|
||||
// view.setPadding(
|
||||
// view.paddingLeft,
|
||||
// view.paddingTop,
|
||||
// view.paddingRight,
|
||||
// bottomPadding // 系统判断好了该不该加 padding
|
||||
// )
|
||||
insets
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,147 @@
|
||||
package com.audio.record.screen.test.tool
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.view.MotionEvent
|
||||
import android.view.View
|
||||
import android.view.WindowManager
|
||||
|
||||
object DraggableViewHelper {
|
||||
|
||||
|
||||
/**
|
||||
* view大小自适应,在全屏拖拽,更新在windows上的位置
|
||||
* @param snapToEdge 是否吸边
|
||||
*/
|
||||
@SuppressLint("ClickableViewAccessibility")
|
||||
fun attachToWindow(
|
||||
view: View,
|
||||
layoutParams: WindowManager.LayoutParams,
|
||||
windowManager: WindowManager,
|
||||
snapToEdge: Boolean = true,
|
||||
onClick: (() -> Unit)? = null
|
||||
) {
|
||||
var dX = 0
|
||||
var dY = 0
|
||||
var downTime = 0L
|
||||
var isDragging = false
|
||||
|
||||
val clickThreshold = 200 // 毫秒,判断点击
|
||||
val moveThreshold = 10 // 像素,判断是否移动
|
||||
|
||||
val screenWidth = Common.getScreenWH(view.context).first
|
||||
val screenHeight = Common.getScreenWH(view.context).second
|
||||
|
||||
|
||||
view.setOnTouchListener { _, event ->
|
||||
when (event.action) {
|
||||
MotionEvent.ACTION_DOWN -> {
|
||||
dX = (event.rawX - layoutParams.x).toInt()
|
||||
dY = (event.rawY - layoutParams.y).toInt()
|
||||
|
||||
downTime = System.currentTimeMillis()
|
||||
isDragging = false
|
||||
true
|
||||
}
|
||||
|
||||
MotionEvent.ACTION_MOVE -> {
|
||||
|
||||
val newX = (event.rawX - dX).toInt()
|
||||
val newY = (event.rawY - dY).toInt()
|
||||
|
||||
// 判断是否是有效拖动
|
||||
if (kotlin.math.abs(newX - layoutParams.x) > moveThreshold ||
|
||||
kotlin.math.abs(newY - layoutParams.y) > moveThreshold
|
||||
) {
|
||||
isDragging = true
|
||||
}
|
||||
|
||||
// 计算新的位置
|
||||
layoutParams.x = (event.rawX - dX).toInt().coerceIn(0, screenWidth - view.width)
|
||||
layoutParams.y =
|
||||
(event.rawY - dY).toInt().coerceIn(0, screenHeight - view.height)
|
||||
|
||||
// 更新 window
|
||||
windowManager.updateViewLayout(view, layoutParams)
|
||||
true
|
||||
}
|
||||
|
||||
MotionEvent.ACTION_UP -> {
|
||||
|
||||
if (!isDragging && System.currentTimeMillis() - downTime < clickThreshold) {
|
||||
if (onClick != null) {
|
||||
onClick.invoke()
|
||||
}
|
||||
} else if (snapToEdge) {
|
||||
val middle = screenWidth / 2
|
||||
layoutParams.x = if (layoutParams.x + view.width / 2 <= middle) {
|
||||
0
|
||||
} else {
|
||||
screenWidth - view.width
|
||||
}
|
||||
windowManager.updateViewLayout(view, layoutParams)
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
else -> false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressLint("ClickableViewAccessibility")
|
||||
fun attach(view: View, snapToEdge: Boolean = true) {
|
||||
var dX = 0f
|
||||
var dY = 0f
|
||||
|
||||
val displayMetrics = view.context.resources.displayMetrics
|
||||
val screenWidth = displayMetrics.widthPixels
|
||||
val screenHeight = displayMetrics.heightPixels
|
||||
Common.showLog("--------screenWidth=$screenWidth screenHeight=${screenHeight}")
|
||||
|
||||
view.setOnTouchListener { v, event ->
|
||||
when (event.action) {
|
||||
MotionEvent.ACTION_DOWN -> {
|
||||
dX = event.rawX - v.x
|
||||
dY = event.rawY - v.y
|
||||
true
|
||||
}
|
||||
|
||||
MotionEvent.ACTION_MOVE -> {
|
||||
var newX = event.rawX - dX
|
||||
var newY = event.rawY - dY
|
||||
|
||||
// 限制不出屏幕
|
||||
newX = newX.coerceIn(0f, (screenWidth - v.width).toFloat())
|
||||
newY = newY.coerceIn(0f, (screenHeight - v.height).toFloat())
|
||||
|
||||
v.x = newX
|
||||
v.y = newY
|
||||
true
|
||||
}
|
||||
|
||||
MotionEvent.ACTION_UP -> {
|
||||
if (snapToEdge) {
|
||||
// 吸边:判断中线位置
|
||||
val middle = screenWidth / 2
|
||||
val targetX = if (v.x + v.width / 2 <= middle) {
|
||||
0f // 左吸边
|
||||
} else {
|
||||
(screenWidth - v.width).toFloat() // 右吸边
|
||||
}
|
||||
|
||||
// 动画吸附
|
||||
v.animate()
|
||||
.x(targetX)
|
||||
.setDuration(200)
|
||||
.start()
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
else -> false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@ -0,0 +1,54 @@
|
||||
package com.audio.record.screen.test.tool
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.Bitmap
|
||||
import android.media.Image
|
||||
import android.util.TypedValue
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
|
||||
object Extend {
|
||||
|
||||
fun Image.toBitmap(): Bitmap {
|
||||
val plane = planes[0]
|
||||
val buffer = plane.buffer
|
||||
val pixelStride = plane.pixelStride
|
||||
val rowStride = plane.rowStride
|
||||
val width = this.width
|
||||
val height = this.height
|
||||
|
||||
val bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888)
|
||||
val bitmapBuffer = IntArray(width * height)
|
||||
var offset = 0
|
||||
|
||||
for (i in 0 until height) {
|
||||
buffer.position(i * rowStride)
|
||||
for (j in 0 until width) {
|
||||
// 每次取4字节一个像素 (RGBA格式)
|
||||
val pixel = buffer.int
|
||||
bitmapBuffer[offset++] = pixel
|
||||
}
|
||||
}
|
||||
|
||||
bitmap.setPixels(bitmapBuffer, 0, width, 0, 0, width, height)
|
||||
return bitmap
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @param margin PX
|
||||
*/
|
||||
fun View.setMarginBottom(margin: Int) {
|
||||
val params = layoutParams as? ViewGroup.MarginLayoutParams ?: return
|
||||
params.bottomMargin = margin
|
||||
layoutParams = params
|
||||
}
|
||||
|
||||
|
||||
fun Int.dpToPx(context: Context): Int =
|
||||
TypedValue.applyDimension(
|
||||
TypedValue.COMPLEX_UNIT_DIP, this.toFloat(),
|
||||
context.resources.displayMetrics
|
||||
).toInt()
|
||||
|
||||
}
|
||||
@ -0,0 +1,224 @@
|
||||
package com.audio.record.screen.test.tool
|
||||
|
||||
import android.media.MediaMetadataRetriever
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import android.util.Log
|
||||
import com.arthenica.ffmpegkit.FFmpegKit
|
||||
import com.arthenica.ffmpegkit.ReturnCode
|
||||
import com.audio.record.screen.test.App
|
||||
import java.io.File
|
||||
import java.io.FileOutputStream
|
||||
import java.io.InputStream
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
object FFmpegKitTool {
|
||||
|
||||
val mainHandler = Handler(Looper.getMainLooper())
|
||||
|
||||
fun copy(
|
||||
tempFile: File,
|
||||
inputStream: InputStream,
|
||||
thumbDir: String,
|
||||
result: (ok: Boolean) -> Unit
|
||||
) {
|
||||
|
||||
FileOutputStream(tempFile).use { outputStream ->
|
||||
inputStream.copyTo(outputStream)
|
||||
outputStream.flush()
|
||||
extractThumbnailsFFmpeg(tempFile.absolutePath, thumbDir, result)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
fun createThumb(inputPath: String, outputDir: String, result: (ok: Boolean) -> Unit) {
|
||||
// val inputPath = "/storage/emulated/0/Movies/sample.mp4"
|
||||
// val outputDir = "/storage/emulated/0/Movies/thumbs" // 请确保已创建
|
||||
val cmd = "-i $inputPath -vf fps=10 $outputDir/thumb_%04d.jpg"
|
||||
|
||||
FFmpegKit.executeAsync(cmd) { session ->
|
||||
val returnCode = session.returnCode
|
||||
if (ReturnCode.isSuccess(returnCode)) {
|
||||
Common.showLog("FFmpegKit 缩略图生成成功 ${Thread.currentThread().name}")
|
||||
result.invoke(true)
|
||||
} else {
|
||||
result.invoke(false)
|
||||
Common.showLog("FFmpegKit 缩略图生成失败:${session.failStackTrace}")
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 截取视频缩略图,均分6张图
|
||||
*/
|
||||
fun extractThumbnailsFFmpeg(
|
||||
videoPath: String,
|
||||
outputDir: String,
|
||||
result: (ok: Boolean) -> Unit
|
||||
) {
|
||||
var count = 0
|
||||
val durationMs = Common.getVideoDurationMs(videoPath)
|
||||
val durationSec = durationMs / 1000.0
|
||||
val step = durationSec / 7 // 均分6帧,跳过首尾
|
||||
Common.showLog("durationSec $durationSec step $step")
|
||||
for (i in 1..6) {
|
||||
val timestamp = i * step
|
||||
val outputPath = "$outputDir/thumb_$i.jpg"
|
||||
Common.showLog("i = $i timestamp $timestamp ")
|
||||
val cmd = "-ss $timestamp -i \"$videoPath\" -frames:v 1 -q:v 2 \"$outputPath\""
|
||||
FFmpegKit.executeAsync(cmd) { session ->
|
||||
val returnCode = session.returnCode
|
||||
count++
|
||||
if (count == 6) {
|
||||
mainHandler.post {
|
||||
if (returnCode.isValueSuccess) {
|
||||
result.invoke(true)
|
||||
Common.showLog("FFmpeg Frame $i saved to $outputPath")
|
||||
} else {
|
||||
result.invoke(false)
|
||||
Common.showLog("FFmpeg Failed to extract frame $i")
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 剪切视频时长
|
||||
* @param startTime
|
||||
*/
|
||||
fun cropVideoWithFFmpeg(
|
||||
inputPath: String,
|
||||
outputPath: String,
|
||||
startMs: Float,
|
||||
endMs: Float
|
||||
) {
|
||||
val durationMs = endMs - startMs
|
||||
if (durationMs <= 0) {
|
||||
println("结束时间必须大于开始时间")
|
||||
return
|
||||
}
|
||||
val formatFloatTime = formatFloatTime(durationMs)
|
||||
val startTime = formatFloatTime(startMs)
|
||||
val endTime = formatFloatTime(endMs)
|
||||
val command =
|
||||
"-ss $startTime -i \"$inputPath\" -to $formatFloatTime -c:v copy -c:a copy \"$outputPath\""
|
||||
Common.showLog("--command=${command}")
|
||||
|
||||
FFmpegKit.executeAsync(command) { session ->
|
||||
val returnCode = session.returnCode
|
||||
if (returnCode.isValueSuccess) {
|
||||
Common.showLog("裁剪成功: $outputPath")
|
||||
} else {
|
||||
Common.showLog("裁剪失败: ${session.failStackTrace}")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 毫秒转为 ffmpeg 时间格式:hh:mm:ss.S
|
||||
fun formatFloatTime(ms: Float): String {
|
||||
val totalSeconds = ms / 1000f
|
||||
val hours = (totalSeconds / 3600).toInt()
|
||||
val minutes = ((totalSeconds % 3600) / 60).toInt()
|
||||
val seconds = (totalSeconds % 60).toInt()
|
||||
val tenths = ((totalSeconds % 1) * 10).toInt() // 保留一位小数
|
||||
|
||||
return String.format("%02d:%02d:%02d.%01d", hours, minutes, seconds, tenths)
|
||||
}
|
||||
//
|
||||
// fun trimVideo(inputPath: String, outputPath: String, startTime: String, endTime: String) {
|
||||
// val command = "-ss $startTime -i $inputPath -to $endTime -c copy $outputPath"
|
||||
//
|
||||
// Common.showLog("--command=${command}")
|
||||
//
|
||||
// FFmpegKit.executeAsync(command) { session ->
|
||||
// when {
|
||||
// ReturnCode.isSuccess(session.returnCode) -> {
|
||||
// Log.d("FFmpegKit", "剪切成功!输出路径: $outputPath")
|
||||
// }
|
||||
//
|
||||
// session.returnCode.value == ReturnCode.CANCEL -> {
|
||||
// Log.w("FFmpegKit", "用户取消操作")
|
||||
// }
|
||||
//
|
||||
// else -> {
|
||||
// Log.e("FFmpegKit", "剪切失败. 错误日志: ${session.output}")
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
|
||||
/**
|
||||
* 裁切视频显示内容尺寸
|
||||
*/
|
||||
fun cropVideo(
|
||||
inputPath: String,
|
||||
outputPath: String,
|
||||
cropX: Int,
|
||||
cropY: Int,
|
||||
cropWidth: Int,
|
||||
cropHeight: Int
|
||||
) {
|
||||
// val cmd = "-i $inputPath -vf crop=$cropWidth:$cropHeight:$cropX:$cropY -c:a copy $outputPath"
|
||||
val cmd =
|
||||
"-i $inputPath -vf crop=$cropWidth:$cropHeight:$cropX:$cropY -c:v mpeg4 -qscale:v 2 -c:a copy $outputPath"
|
||||
|
||||
Common.showLog("FFmpeg Crop 比例裁剪 $cmd")
|
||||
FFmpegKit.executeAsync(cmd) {
|
||||
if (ReturnCode.isSuccess(it.returnCode)) {
|
||||
Common.showLog("FFmpeg Crop 比例裁剪成功 $outputPath")
|
||||
} else {
|
||||
Common.showLog("FFmpeg Crop 比例裁剪失败: $outputPath ${it.failStackTrace}")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 调整视频音量
|
||||
*/
|
||||
fun setVideVolume(
|
||||
inputPath: String,
|
||||
outputPath: String, volumeLevel: Float
|
||||
) {
|
||||
// val volumeLevel = 1.5 // 音量倍数(例如 1.5 表示音量加大 50%)
|
||||
val cmd = "-y -i $inputPath -filter:a volume=$volumeLevel -c:v copy $outputPath"
|
||||
Common.showLog("FFmpeg 音量 $cmd")
|
||||
FFmpegKit.executeAsync(cmd) { session ->
|
||||
val returnCode = session.returnCode
|
||||
if (ReturnCode.isSuccess(returnCode)) {
|
||||
Common.showLog("FFmpeg 音量调节成功")
|
||||
} else {
|
||||
Common.showLog("FFmpeg 音量调节失败: ${session.failStackTrace}")
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* 调整视频播放速度
|
||||
*/
|
||||
fun buildSpeedCommand(inputPath: String, outputPath: String, speed: Float) {
|
||||
val videoFilter = "setpts=${1 / speed}*PTS"
|
||||
val audioFilter = "atempo=$speed"
|
||||
val cmd = "-y -i $inputPath -filter_complex \"[0:v]$videoFilter[v];[0:a]$audioFilter[a]\" -map \"[v]\" -map \"[a]\" -preset ultrafast $outputPath"
|
||||
Common.showLog("FFmpeg 播放速度调整 $cmd")
|
||||
FFmpegKit.executeAsync(cmd) { session ->
|
||||
if (ReturnCode.isSuccess(session.returnCode)) {
|
||||
Common.showLog("FFmpeg 播放速度调整成功")
|
||||
} else {
|
||||
Common.showLog("FFmpeg 播放速度调整失败: ${session.failStackTrace}")
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,8 @@
|
||||
package com.audio.record.screen.test.tool
|
||||
|
||||
import android.content.Context
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
|
||||
class NoScrollLinearLayoutManager(context: Context) : LinearLayoutManager(context) {
|
||||
override fun canScrollVertically(): Boolean = false
|
||||
}
|
||||
@ -0,0 +1,69 @@
|
||||
package com.audio.record.screen.test.tool
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.pm.PackageManager
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.provider.Settings
|
||||
import androidx.core.content.ContextCompat
|
||||
|
||||
object Permission {
|
||||
|
||||
|
||||
/**
|
||||
* 检查通知权限
|
||||
*/
|
||||
fun checkNotification(mContext:Context,result:(b:Boolean)->Unit){
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||
if (ContextCompat.checkSelfPermission(
|
||||
mContext,
|
||||
android.Manifest.permission.POST_NOTIFICATIONS
|
||||
)
|
||||
!= PackageManager.PERMISSION_GRANTED
|
||||
) {
|
||||
result.invoke(false)
|
||||
} else {
|
||||
// 权限已授予,可以发送通知
|
||||
Common.showLog("权限已授予,可以发送通知")
|
||||
result.invoke(true)
|
||||
}
|
||||
} else {
|
||||
// Android 12 及以下,不需要请求权限
|
||||
Common.showLog("不需要请求权限")
|
||||
result.invoke(true)
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 检查悬浮窗权限
|
||||
*/
|
||||
fun checkOvalApp(mContext:Context,result:(b:Boolean)->Unit){
|
||||
if (!Settings.canDrawOverlays(mContext)) {
|
||||
result.invoke(false)
|
||||
}else{
|
||||
result.invoke(true)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 检查摄像头权限
|
||||
*/
|
||||
fun checkCamera(mContext:Context,result:(b:Boolean)->Unit) {
|
||||
if (ContextCompat.checkSelfPermission(
|
||||
mContext,
|
||||
android.Manifest.permission.CAMERA
|
||||
)
|
||||
!= PackageManager.PERMISSION_GRANTED
|
||||
) {
|
||||
result.invoke(false)
|
||||
} else {
|
||||
result.invoke(true)
|
||||
Common.showLog("权限已授予 CAMERA")
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,229 @@
|
||||
package com.audio.record.screen.test.tool
|
||||
|
||||
import android.content.ContentValues
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.res.Resources
|
||||
import android.graphics.Bitmap
|
||||
import android.graphics.PixelFormat
|
||||
import android.hardware.display.DisplayManager
|
||||
import android.hardware.display.VirtualDisplay
|
||||
import android.media.Image
|
||||
import android.media.ImageReader
|
||||
import android.media.projection.MediaProjection
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.os.Environment
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import android.provider.MediaStore
|
||||
import android.view.Gravity
|
||||
import android.view.LayoutInflater
|
||||
import android.view.WindowManager
|
||||
import android.view.animation.DecelerateInterpolator
|
||||
import android.widget.ImageView
|
||||
import com.audio.record.screen.test.R
|
||||
import java.io.File
|
||||
import java.io.FileOutputStream
|
||||
|
||||
/**
|
||||
* 截屏
|
||||
*/
|
||||
object ScreenCaptureHelper {
|
||||
|
||||
private var virtualDisplay:VirtualDisplay? = null
|
||||
|
||||
fun startScreenCapture(
|
||||
context: Context,
|
||||
mediaProjection: MediaProjection,
|
||||
folderName: String = Common.imagesFolderDir,
|
||||
isOK:()->Unit
|
||||
) {
|
||||
val metrics = Resources.getSystem().displayMetrics
|
||||
val full = Common.getFullScreenSize(context)
|
||||
val width = full.first
|
||||
val height = full.second
|
||||
val density = metrics.densityDpi
|
||||
|
||||
Common.showLog("startScreenCapture width=${width}, height=${height} density=${density} Thread=${Thread.currentThread().name}")
|
||||
val imageReader = ImageReader.newInstance(width, height, PixelFormat.RGBA_8888, 2)
|
||||
mediaProjection.registerCallback(object : MediaProjection.Callback() {
|
||||
override fun onStop() {
|
||||
Common.showLog("startScreenCapture MediaProjection 被系统或用户停止")
|
||||
// 这里应该释放 MediaRecorder 和 VirtualDisplay 等
|
||||
virtualDisplay?.release()
|
||||
mediaProjection.stop()
|
||||
|
||||
}
|
||||
}, Handler(Looper.getMainLooper()))
|
||||
virtualDisplay = mediaProjection.createVirtualDisplay(
|
||||
"ScreenCapture",
|
||||
width, height, density,
|
||||
DisplayManager.VIRTUAL_DISPLAY_FLAG_AUTO_MIRROR,
|
||||
imageReader.surface, null, null
|
||||
)
|
||||
|
||||
Handler(Looper.getMainLooper()).postDelayed({
|
||||
val image = imageReader.acquireLatestImage()
|
||||
if (image != null) {
|
||||
val bitmap = imageToBitmap(image)
|
||||
saveBitmap(context, bitmap, folderName)
|
||||
isOK.invoke()
|
||||
showScreenshotPreviewAnimation(context, bitmap,width,height)
|
||||
image.close()
|
||||
}
|
||||
imageReader.close()
|
||||
virtualDisplay?.release()
|
||||
mediaProjection.stop()
|
||||
}, 100)
|
||||
}
|
||||
private fun imageToBitmap(image: Image): Bitmap {
|
||||
val width = image.width
|
||||
val height = image.height
|
||||
val plane = image.planes[0]
|
||||
val buffer = plane.buffer
|
||||
val pixelStride = plane.pixelStride
|
||||
val rowStride = plane.rowStride
|
||||
val rowPadding = rowStride - pixelStride * width
|
||||
|
||||
val bitmap = Bitmap.createBitmap(
|
||||
width + rowPadding / pixelStride,
|
||||
height,
|
||||
Bitmap.Config.ARGB_8888
|
||||
)
|
||||
bitmap.copyPixelsFromBuffer(buffer)
|
||||
return Bitmap.createBitmap(bitmap, 0, 0, width, height)
|
||||
}
|
||||
private fun saveBitmap(context: Context, bitmap: Bitmap, folderName: String) {
|
||||
val fileName = "screenshot_${System.currentTimeMillis()}.png"
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||
// Android 10+
|
||||
val contentValues = ContentValues().apply {
|
||||
put(MediaStore.Images.Media.DISPLAY_NAME, fileName)
|
||||
put(MediaStore.Images.Media.MIME_TYPE, "image/png")
|
||||
put(
|
||||
MediaStore.Images.Media.RELATIVE_PATH,
|
||||
folderName
|
||||
)
|
||||
put(MediaStore.Images.Media.IS_PENDING, 1)
|
||||
}
|
||||
|
||||
val contentResolver = context.contentResolver
|
||||
val uri = contentResolver.insert(
|
||||
MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
|
||||
contentValues
|
||||
)
|
||||
|
||||
uri?.let {
|
||||
contentResolver.openOutputStream(it)?.use { stream ->
|
||||
bitmap.compress(Bitmap.CompressFormat.PNG, 100, stream)
|
||||
}
|
||||
contentValues.clear()
|
||||
contentValues.put(MediaStore.Images.Media.IS_PENDING, 0)
|
||||
contentResolver.update(uri, contentValues, null, null)
|
||||
}
|
||||
} else {
|
||||
// Android 7-9
|
||||
val picturesDir =
|
||||
Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES)
|
||||
val folder = File(folderName)
|
||||
if (!folder.exists()) folder.mkdirs()
|
||||
|
||||
val file = File(folder, fileName)
|
||||
FileOutputStream(file).use { fos ->
|
||||
bitmap.compress(Bitmap.CompressFormat.PNG, 100, fos)
|
||||
}
|
||||
|
||||
// 通知媒体库刷新
|
||||
context.sendBroadcast(Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE, Uri.fromFile(file)))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 截屏的动画效果
|
||||
*/
|
||||
fun showScreenshotPreviewAnimation(context: Context, screenshotBitmap: Bitmap,screenWidth:Int,screenHeight:Int) {
|
||||
val windowManager = context.getSystemService(Context.WINDOW_SERVICE) as WindowManager
|
||||
|
||||
val inflate = LayoutInflater.from(context).inflate(R.layout.floating_screenshot_anim, null)
|
||||
val imageView = inflate.findViewById<ImageView>(R.id.image)
|
||||
imageView.setImageBitmap(screenshotBitmap)
|
||||
|
||||
val layoutParams = WindowManager.LayoutParams(
|
||||
WindowManager.LayoutParams.WRAP_CONTENT,
|
||||
WindowManager.LayoutParams.WRAP_CONTENT,
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O)
|
||||
WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY
|
||||
else
|
||||
WindowManager.LayoutParams.TYPE_PHONE,
|
||||
WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL or
|
||||
WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE or
|
||||
WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS,
|
||||
PixelFormat.TRANSLUCENT
|
||||
).apply {
|
||||
gravity = Gravity.TOP or Gravity.START
|
||||
}
|
||||
imageView.post {
|
||||
Common.showLog("实际宽高 width=${imageView.width}, height=${imageView.height}")
|
||||
|
||||
}
|
||||
Common.showLog("layoutParams layoutParams x=${layoutParams.x}, y=${layoutParams.y}")
|
||||
windowManager.addView(inflate, layoutParams)
|
||||
|
||||
|
||||
val scale = 0.3f
|
||||
val targetWidth = (screenWidth * scale).toInt()
|
||||
val targetHeight = (screenHeight * scale).toInt()
|
||||
|
||||
val targetX = screenWidth - targetWidth - 20
|
||||
val navigationBarHeight = Common.getNavigationBarHeight(context)
|
||||
val statusBarHeight = Common.getStatusBarHeight(context)
|
||||
Common.showLog("navigationBarHeight=${navigationBarHeight}, statusBarHeight=${statusBarHeight} ")
|
||||
|
||||
val targetY = screenHeight - targetHeight - navigationBarHeight - statusBarHeight - 20
|
||||
|
||||
|
||||
Common.showLog("宽高 targetWidth=${targetWidth}, targetHeight=${targetHeight} targetX = ${targetX} targetY = ${targetY}")
|
||||
|
||||
inflate.animate()
|
||||
.scaleX(scale)
|
||||
.scaleY(scale)
|
||||
.setDuration(600)
|
||||
.setInterpolator(DecelerateInterpolator())
|
||||
.withEndAction {
|
||||
// 清除动画偏移
|
||||
inflate.scaleX = 1f
|
||||
inflate.scaleY = 1f
|
||||
inflate.translationX = 0f
|
||||
inflate.translationY = 0f
|
||||
|
||||
// 更新 layoutParams 为最终目标位置与尺寸
|
||||
layoutParams.width = targetWidth
|
||||
layoutParams.height = targetHeight
|
||||
layoutParams.x = targetX
|
||||
layoutParams.y = targetY
|
||||
Common.showLog("动画结束 targetWidth=${targetWidth}, targetHeight=${targetHeight} targetX = ${targetX} targetY = ${targetY}")
|
||||
windowManager.updateViewLayout(inflate, layoutParams)
|
||||
|
||||
|
||||
// 5秒后自动移除
|
||||
inflate.postDelayed({
|
||||
try {
|
||||
windowManager.removeView(inflate)
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
}, 3000)
|
||||
}
|
||||
.start()
|
||||
|
||||
inflate.setOnClickListener {
|
||||
Common.showLog("-----点击小截图")
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@ -0,0 +1,368 @@
|
||||
package com.audio.record.screen.test.tool
|
||||
|
||||
import android.content.ContentUris
|
||||
import android.content.ContentValues
|
||||
import android.content.Context
|
||||
import android.graphics.BitmapFactory
|
||||
import android.media.MediaMetadataRetriever
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.os.Environment
|
||||
import android.os.ParcelFileDescriptor
|
||||
import android.provider.MediaStore
|
||||
import android.util.DisplayMetrics
|
||||
import android.util.Size
|
||||
import android.view.WindowManager
|
||||
import androidx.core.content.FileProvider
|
||||
import com.audio.record.screen.test.data.ImageGroup
|
||||
import com.audio.record.screen.test.data.ImageInfo
|
||||
import com.audio.record.screen.test.data.VideoInfo
|
||||
import java.io.File
|
||||
import java.io.IOException
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.Date
|
||||
import java.util.Locale
|
||||
|
||||
object VideoFileHelper {
|
||||
|
||||
data class ScreenInfo(val width: Int, val height: Int, val dpi: Int)
|
||||
|
||||
fun getScreenInfo(context: Context): ScreenInfo {
|
||||
val metrics = DisplayMetrics()
|
||||
val windowManager = context.getSystemService(Context.WINDOW_SERVICE) as WindowManager
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||
context.display?.getRealMetrics(metrics)
|
||||
} else {
|
||||
@Suppress("DEPRECATION")
|
||||
windowManager.defaultDisplay.getRealMetrics(metrics)
|
||||
}
|
||||
return ScreenInfo(metrics.widthPixels, metrics.heightPixels, metrics.densityDpi)
|
||||
}
|
||||
|
||||
fun alignTo16(value: Int): Int = (value + 15) / 16 * 16
|
||||
|
||||
|
||||
/**
|
||||
* 创建一个用于 MediaRecorder 或其他输出的视频文件路径
|
||||
* @param context 上下文
|
||||
* @param folderName 你想要创建的文件夹名称(位于 Movies/ 下)
|
||||
* @param displayName 文件名(不含扩展名)
|
||||
* @return 文件的 Uri 和 FileDescriptor(可选)
|
||||
*/
|
||||
fun createVideoFile(
|
||||
context: Context,
|
||||
folderName: String,
|
||||
displayName: String = "${System.currentTimeMillis()}"
|
||||
): Pair<Uri, ParcelFileDescriptor?> {
|
||||
val resolver = context.contentResolver
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||
val contentValues = ContentValues().apply {
|
||||
put(MediaStore.Video.Media.DISPLAY_NAME, "$displayName.mp4")
|
||||
put(MediaStore.Video.Media.MIME_TYPE, "video/mp4")
|
||||
put(
|
||||
MediaStore.Video.Media.RELATIVE_PATH,
|
||||
Environment.DIRECTORY_MOVIES + "/$folderName"
|
||||
)
|
||||
put(MediaStore.Video.Media.IS_PENDING, 1)
|
||||
}
|
||||
|
||||
val videoUri = resolver.insert(
|
||||
MediaStore.Video.Media.EXTERNAL_CONTENT_URI,
|
||||
contentValues
|
||||
) ?: throw IOException("无法创建视频文件 Uri")
|
||||
|
||||
val pfd = try {
|
||||
resolver.openFileDescriptor(videoUri, "w")
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
null
|
||||
}
|
||||
|
||||
return Pair(videoUri, pfd)
|
||||
} else {
|
||||
// Android 9及以下,使用旧方式(注意申请权限)
|
||||
val moviesDir =
|
||||
Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_MOVIES)
|
||||
val folder = File(moviesDir, folderName).apply { if (!exists()) mkdirs() }
|
||||
val file = File(folder, "$displayName.mp4").apply { if (!exists()) createNewFile() }
|
||||
|
||||
val uri = FileProvider.getUriForFile(
|
||||
context,
|
||||
"${context.packageName}.fileprovider", // 记得配置 provider
|
||||
file
|
||||
)
|
||||
|
||||
val pfd = ParcelFileDescriptor.open(file, ParcelFileDescriptor.MODE_WRITE_ONLY)
|
||||
return Pair(uri, pfd)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
fun markVideoFileCompleted(context: Context, videoUri: Uri) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||
val resolver = context.contentResolver
|
||||
val contentValues = ContentValues().apply {
|
||||
put(MediaStore.Video.Media.IS_PENDING, 0)
|
||||
}
|
||||
resolver.update(videoUri, contentValues, null, null)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 遍历指定目录folderName下的所有视频
|
||||
*/
|
||||
|
||||
fun queryVideoInfoListInFolder(context: Context, folderName: String): List<VideoInfo> {
|
||||
Common.showLog("------folderName=${folderName}")
|
||||
val videoInfoList = mutableListOf<VideoInfo>()
|
||||
val resolver = context.contentResolver
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||
// Android 10+ 用 MediaStore 查询
|
||||
val projection = arrayOf(
|
||||
MediaStore.Video.Media._ID,
|
||||
MediaStore.Video.Media.DISPLAY_NAME,
|
||||
MediaStore.Video.Media.SIZE,
|
||||
MediaStore.Video.Media.DURATION,
|
||||
MediaStore.Video.Media.DATE_ADDED,
|
||||
MediaStore.Video.Media.DATE_MODIFIED
|
||||
)
|
||||
|
||||
val selection = "${MediaStore.Video.Media.RELATIVE_PATH} = ?"
|
||||
val selectionArgs = arrayOf("$folderName/")
|
||||
|
||||
val sortOrder = "${MediaStore.Video.Media.DATE_ADDED} DESC"
|
||||
|
||||
resolver.query(
|
||||
MediaStore.Video.Media.EXTERNAL_CONTENT_URI,
|
||||
projection,
|
||||
selection,
|
||||
selectionArgs,
|
||||
sortOrder
|
||||
)?.use { cursor ->
|
||||
val idColumn = cursor.getColumnIndexOrThrow(MediaStore.Video.Media._ID)
|
||||
val nameColumn = cursor.getColumnIndexOrThrow(MediaStore.Video.Media.DISPLAY_NAME)
|
||||
val sizeColumn = cursor.getColumnIndexOrThrow(MediaStore.Video.Media.SIZE)
|
||||
val durationColumn = cursor.getColumnIndexOrThrow(MediaStore.Video.Media.DURATION)
|
||||
val dateAddedColumn = cursor.getColumnIndexOrThrow(MediaStore.Video.Media.DATE_ADDED)
|
||||
val dateModifiedColumn = cursor.getColumnIndexOrThrow(MediaStore.Video.Media.DATE_MODIFIED)
|
||||
|
||||
while (cursor.moveToNext()) {
|
||||
val id = cursor.getLong(idColumn)
|
||||
val uri = ContentUris.withAppendedId(MediaStore.Video.Media.EXTERNAL_CONTENT_URI, id)
|
||||
val displayName = cursor.getString(nameColumn)
|
||||
val size = cursor.getLong(sizeColumn)
|
||||
val duration = cursor.getLong(durationColumn)
|
||||
val dateAdded = cursor.getLong(dateAddedColumn)
|
||||
val dateModified = cursor.getLong(dateModifiedColumn)
|
||||
|
||||
// 获取封面图 (Android Q+ 推荐用 loadThumbnail)
|
||||
val thumbnail = try {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||
resolver.loadThumbnail(uri, Size(1280,720), null)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
null
|
||||
}
|
||||
|
||||
val videoInfo = VideoInfo(
|
||||
uri = uri,
|
||||
displayName = displayName,
|
||||
size = size,
|
||||
duration = duration,
|
||||
dateAdded = dateAdded,
|
||||
dateModified = dateModified,
|
||||
thumbnail = thumbnail
|
||||
)
|
||||
|
||||
videoInfoList.add(videoInfo)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Android 9 及以下,File API 查询
|
||||
val folderPath = Environment.getExternalStoragePublicDirectory(
|
||||
folderName
|
||||
).absolutePath
|
||||
|
||||
val folder = File(folderPath)
|
||||
if (folder.exists() && folder.isDirectory) {
|
||||
val videoFiles = folder.listFiles { file ->
|
||||
file.isFile && file.extension.equals("mp4", ignoreCase = true)
|
||||
}?.toList() ?: emptyList()
|
||||
|
||||
videoFiles.forEach { file ->
|
||||
val uri = FileProvider.getUriForFile(
|
||||
context,
|
||||
"${context.packageName}.fileprovider",
|
||||
file
|
||||
)
|
||||
|
||||
// 获取视频基本信息(只能通过 MediaMetadataRetriever 获取部分信息)
|
||||
val retriever = MediaMetadataRetriever()
|
||||
retriever.setDataSource(context, uri)
|
||||
|
||||
val duration = retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_DURATION)?.toLongOrNull() ?: 0L
|
||||
val thumbnail = try {
|
||||
retriever.getFrameAtTime(0, MediaMetadataRetriever.OPTION_CLOSEST_SYNC)
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
null
|
||||
}
|
||||
|
||||
retriever.release()
|
||||
|
||||
val videoInfo = VideoInfo(
|
||||
uri = uri,
|
||||
displayName = file.name,
|
||||
size = file.length(),
|
||||
duration = duration,
|
||||
dateAdded = file.lastModified() / 1000,
|
||||
dateModified = file.lastModified() / 1000,
|
||||
thumbnail = thumbnail
|
||||
)
|
||||
|
||||
videoInfoList.add(videoInfo)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return videoInfoList
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* 遍历指定目录folderName下的所有图片
|
||||
*/
|
||||
private fun queryImagesInFolder(context: Context, folderName: String): List<ImageInfo> {
|
||||
Common.showLog("-----图片-folderName=${folderName}")
|
||||
val imageInfoList = mutableListOf<ImageInfo>()
|
||||
val resolver = context.contentResolver
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||
val projection = arrayOf(
|
||||
MediaStore.Images.Media._ID,
|
||||
MediaStore.Images.Media.DISPLAY_NAME,
|
||||
MediaStore.Images.Media.SIZE,
|
||||
MediaStore.Images.Media.DATE_ADDED,
|
||||
MediaStore.Images.Media.DATE_MODIFIED
|
||||
)
|
||||
|
||||
val selection = "${MediaStore.Images.Media.RELATIVE_PATH} = ?"
|
||||
val selectionArgs = arrayOf("$folderName/")
|
||||
|
||||
val sortOrder = "${MediaStore.Images.Media.DATE_ADDED} DESC"
|
||||
|
||||
resolver.query(
|
||||
MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
|
||||
projection,
|
||||
selection,
|
||||
selectionArgs,
|
||||
sortOrder
|
||||
)?.use { cursor ->
|
||||
val idColumn = cursor.getColumnIndexOrThrow(MediaStore.Images.Media._ID)
|
||||
val nameColumn = cursor.getColumnIndexOrThrow(MediaStore.Images.Media.DISPLAY_NAME)
|
||||
val sizeColumn = cursor.getColumnIndexOrThrow(MediaStore.Images.Media.SIZE)
|
||||
val dateAddedColumn = cursor.getColumnIndexOrThrow(MediaStore.Images.Media.DATE_ADDED)
|
||||
val dateModifiedColumn = cursor.getColumnIndexOrThrow(MediaStore.Images.Media.DATE_MODIFIED)
|
||||
|
||||
while (cursor.moveToNext()) {
|
||||
val id = cursor.getLong(idColumn)
|
||||
val uri = ContentUris.withAppendedId(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, id)
|
||||
val displayName = cursor.getString(nameColumn)
|
||||
val size = cursor.getLong(sizeColumn)
|
||||
val dateAdded = cursor.getLong(dateAddedColumn)
|
||||
val dateModified = cursor.getLong(dateModifiedColumn)
|
||||
|
||||
val thumbnail = try {
|
||||
resolver.loadThumbnail(uri, Size(320, 320), null)
|
||||
} catch (e: Exception) {
|
||||
null
|
||||
}
|
||||
Common.showLog("-----图片-displayName=${displayName}")
|
||||
val info = ImageInfo(
|
||||
uri = uri,
|
||||
displayName = displayName,
|
||||
size = size,
|
||||
dateAdded = dateAdded,
|
||||
dateModified = dateModified,
|
||||
thumbnail = thumbnail
|
||||
)
|
||||
imageInfoList.add(info)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Android 7-9 (API 24~28),用 File API
|
||||
val folder = File(folderName)
|
||||
|
||||
if (folder.exists() && folder.isDirectory) {
|
||||
val files = folder.listFiles { file ->
|
||||
file.isFile && (file.extension.equals("jpg", true)
|
||||
|| file.extension.equals("png", true)
|
||||
|| file.extension.equals("jpeg", true))
|
||||
}?.toList() ?: emptyList()
|
||||
|
||||
files.forEach { file ->
|
||||
val uri = FileProvider.getUriForFile(
|
||||
context,
|
||||
"${context.packageName}.fileprovider",
|
||||
file
|
||||
)
|
||||
|
||||
val bitmap = try {
|
||||
BitmapFactory.decodeFile(file.absolutePath)
|
||||
} catch (e: Exception) {
|
||||
null
|
||||
}
|
||||
|
||||
val info = ImageInfo(
|
||||
uri = uri,
|
||||
displayName = file.name,
|
||||
size = file.length(),
|
||||
dateAdded = file.lastModified() / 1000,
|
||||
dateModified = file.lastModified() / 1000,
|
||||
thumbnail = bitmap
|
||||
)
|
||||
imageInfoList.add(info)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return imageInfoList
|
||||
}
|
||||
|
||||
|
||||
fun queryGroupedImagesByDay(context: Context, folderName: String): List<ImageGroup> {
|
||||
val allImages = queryImagesInFolder(context, folderName)
|
||||
Common.showLog("-----图片-allImages=${allImages.size}")
|
||||
// 分组处理:按 dateAdded 转换为 yyyy-MM-dd
|
||||
val dateFormat = SimpleDateFormat("yyyy-MM-dd", Locale.getDefault())
|
||||
|
||||
return allImages
|
||||
.groupBy { image ->
|
||||
val date = Date(image.dateAdded * 1000) // 秒 → 毫秒
|
||||
dateFormat.format(date)
|
||||
}
|
||||
.map { (date, images) ->
|
||||
ImageGroup(date, images)
|
||||
}
|
||||
.sortedByDescending { it.date } // 按日期倒序排
|
||||
}
|
||||
|
||||
fun groupVideosByDay(videoList: List<VideoInfo>): Map<String, List<VideoInfo>> {
|
||||
val sdf = SimpleDateFormat("yyyy-MM-dd", Locale.getDefault())
|
||||
|
||||
return videoList.groupBy { video ->
|
||||
val timestampMillis = video.dateAdded * 1000 // 秒转毫秒
|
||||
sdf.format(Date(timestampMillis)) // 格式化成日期字符串
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,276 @@
|
||||
package com.audio.record.screen.test.view;
|
||||
|
||||
import android.content.Context;
|
||||
import android.graphics.Canvas;
|
||||
import android.graphics.Color;
|
||||
import android.graphics.Paint;
|
||||
import android.graphics.RectF;
|
||||
import android.os.Build;
|
||||
import android.util.AttributeSet;
|
||||
import android.view.MotionEvent;
|
||||
import android.view.View;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
public class AspectRatioCropView extends View {
|
||||
|
||||
private RectF cropRect = new RectF();
|
||||
private RectF lastRect = new RectF();
|
||||
private RectF videoBounds = null;
|
||||
|
||||
private Paint borderPaint;
|
||||
private Paint shadePaint;
|
||||
private float aspectRatio = 4f / 5f;
|
||||
private int minSize = 100;
|
||||
|
||||
private boolean isDragging = false;
|
||||
private float lastTouchX, lastTouchY;
|
||||
private int dragHandle = 0;
|
||||
|
||||
public AspectRatioCropView(Context context) {
|
||||
super(context);
|
||||
init();
|
||||
}
|
||||
|
||||
public AspectRatioCropView(Context context, AttributeSet attrs) {
|
||||
super(context, attrs);
|
||||
init();
|
||||
}
|
||||
|
||||
private void init() {
|
||||
borderPaint = new Paint();
|
||||
borderPaint.setColor(Color.WHITE);
|
||||
borderPaint.setStyle(Paint.Style.STROKE);
|
||||
borderPaint.setStrokeWidth(4f);
|
||||
|
||||
shadePaint = new Paint();
|
||||
shadePaint.setColor(0x33000000);
|
||||
}
|
||||
|
||||
public void setVideoDisplayBounds(RectF bounds) {
|
||||
this.videoBounds = bounds;
|
||||
updateCropRect(bounds.centerX(), bounds.centerY(),
|
||||
Math.min(bounds.width(), bounds.height()) * 0.5f);
|
||||
}
|
||||
|
||||
private void updateCropRect(float centerX, float centerY, float size) {
|
||||
float width, height;
|
||||
if (aspectRatio >= 1) {
|
||||
width = size;
|
||||
height = width / aspectRatio;
|
||||
} else {
|
||||
height = size;
|
||||
width = height * aspectRatio;
|
||||
}
|
||||
|
||||
cropRect.left = centerX - width / 2;
|
||||
cropRect.top = centerY - height / 2;
|
||||
cropRect.right = centerX + width / 2;
|
||||
cropRect.bottom = centerY + height / 2;
|
||||
|
||||
enforceBounds();
|
||||
invalidate();
|
||||
}
|
||||
|
||||
// private void enforceBounds() {
|
||||
// if (videoBounds == null) return;
|
||||
//
|
||||
// float dx = 0, dy = 0;
|
||||
//
|
||||
// if (cropRect.left < videoBounds.left) dx = videoBounds.left - cropRect.left;
|
||||
// if (cropRect.top < videoBounds.top) dy = videoBounds.top - cropRect.top;
|
||||
// if (cropRect.right > videoBounds.right) dx = videoBounds.right - cropRect.right;
|
||||
// if (cropRect.bottom > videoBounds.bottom) dy = videoBounds.bottom - cropRect.bottom;
|
||||
//
|
||||
// cropRect.offset(dx, dy);
|
||||
// }
|
||||
|
||||
private void enforceBounds() {
|
||||
if (videoBounds == null) return;
|
||||
|
||||
float dx = 0, dy = 0;
|
||||
|
||||
// 缩小裁剪框使其适应边界
|
||||
if (cropRect.width() > videoBounds.width()) {
|
||||
float newWidth = videoBounds.width();
|
||||
float newHeight = newWidth / aspectRatio;
|
||||
cropRect.left = videoBounds.left;
|
||||
cropRect.right = videoBounds.right;
|
||||
cropRect.top = (videoBounds.top + videoBounds.bottom - newHeight) / 2;
|
||||
cropRect.bottom = cropRect.top + newHeight;
|
||||
} else if (cropRect.height() > videoBounds.height()) {
|
||||
float newHeight = videoBounds.height();
|
||||
float newWidth = newHeight * aspectRatio;
|
||||
cropRect.top = videoBounds.top;
|
||||
cropRect.bottom = videoBounds.bottom;
|
||||
cropRect.left = (videoBounds.left + videoBounds.right - newWidth) / 2;
|
||||
cropRect.right = cropRect.left + newWidth;
|
||||
}
|
||||
|
||||
// 位置微调(避免轻微偏移)
|
||||
if (cropRect.left < videoBounds.left) dx = videoBounds.left - cropRect.left;
|
||||
if (cropRect.top < videoBounds.top) dy = videoBounds.top - cropRect.top;
|
||||
if (cropRect.right > videoBounds.right) dx = videoBounds.right - cropRect.right;
|
||||
if (cropRect.bottom > videoBounds.bottom) dy = videoBounds.bottom - cropRect.bottom;
|
||||
|
||||
cropRect.offset(dx, dy);
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
protected void onDraw(Canvas canvas) {
|
||||
super.onDraw(canvas);
|
||||
|
||||
int saveCount = canvas.save();
|
||||
|
||||
// 阴影层
|
||||
canvas.drawRect(0, 0, getWidth(), getHeight(), shadePaint);
|
||||
|
||||
// 剪裁中间区域
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
canvas.clipOutRect(cropRect);
|
||||
} else {
|
||||
canvas.clipRect(cropRect, android.graphics.Region.Op.DIFFERENCE);
|
||||
}
|
||||
|
||||
canvas.drawColor(0x99000000);
|
||||
canvas.restoreToCount(saveCount);
|
||||
|
||||
// 边框
|
||||
canvas.drawRect(cropRect, borderPaint);
|
||||
|
||||
// 四角标记
|
||||
float cornerSize = 20f;
|
||||
canvas.drawLine(cropRect.left, cropRect.top, cropRect.left + cornerSize, cropRect.top, borderPaint);
|
||||
canvas.drawLine(cropRect.left, cropRect.top, cropRect.left, cropRect.top + cornerSize, borderPaint);
|
||||
|
||||
canvas.drawLine(cropRect.right, cropRect.top, cropRect.right - cornerSize, cropRect.top, borderPaint);
|
||||
canvas.drawLine(cropRect.right, cropRect.top, cropRect.right, cropRect.top + cornerSize, borderPaint);
|
||||
|
||||
canvas.drawLine(cropRect.right, cropRect.bottom, cropRect.right - cornerSize, cropRect.bottom, borderPaint);
|
||||
canvas.drawLine(cropRect.right, cropRect.bottom, cropRect.right, cropRect.bottom - cornerSize, borderPaint);
|
||||
|
||||
canvas.drawLine(cropRect.left, cropRect.bottom, cropRect.left + cornerSize, cropRect.bottom, borderPaint);
|
||||
canvas.drawLine(cropRect.left, cropRect.bottom, cropRect.left, cropRect.bottom - cornerSize, borderPaint);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onTouchEvent(MotionEvent event) {
|
||||
if (videoBounds == null) return false;
|
||||
|
||||
float x = event.getX();
|
||||
float y = event.getY();
|
||||
|
||||
switch (event.getAction()) {
|
||||
case MotionEvent.ACTION_DOWN:
|
||||
lastRect.set(cropRect);
|
||||
|
||||
if (isInCorner(x, y, cropRect.left, cropRect.top)) dragHandle = 2;
|
||||
else if (isInCorner(x, y, cropRect.right, cropRect.top)) dragHandle = 3;
|
||||
else if (isInCorner(x, y, cropRect.right, cropRect.bottom)) dragHandle = 4;
|
||||
else if (isInCorner(x, y, cropRect.left, cropRect.bottom)) dragHandle = 5;
|
||||
else if (cropRect.contains(x, y)) dragHandle = 1;
|
||||
else dragHandle = 0;
|
||||
|
||||
lastTouchX = x;
|
||||
lastTouchY = y;
|
||||
isDragging = dragHandle != 0;
|
||||
return isDragging;
|
||||
|
||||
case MotionEvent.ACTION_MOVE:
|
||||
if (!isDragging) return false;
|
||||
lastRect.set(cropRect);
|
||||
|
||||
float dx = x - lastTouchX;
|
||||
float dy = y - lastTouchY;
|
||||
|
||||
if (dragHandle == 1) {
|
||||
cropRect.offset(dx, dy);
|
||||
enforceBounds();
|
||||
} else {
|
||||
float newWidth, newHeight;
|
||||
|
||||
switch (dragHandle) {
|
||||
case 2:
|
||||
newWidth = cropRect.width() - dx;
|
||||
newHeight = newWidth / aspectRatio;
|
||||
cropRect.left = cropRect.right - newWidth;
|
||||
cropRect.top = cropRect.bottom - newHeight;
|
||||
break;
|
||||
case 3:
|
||||
newWidth = cropRect.width() + dx;
|
||||
newHeight = newWidth / aspectRatio;
|
||||
cropRect.right = cropRect.left + newWidth;
|
||||
cropRect.top = cropRect.bottom - newHeight;
|
||||
break;
|
||||
case 4:
|
||||
newWidth = cropRect.width() + dx;
|
||||
newHeight = newWidth / aspectRatio;
|
||||
cropRect.right = cropRect.left + newWidth;
|
||||
cropRect.bottom = cropRect.top + newHeight;
|
||||
break;
|
||||
case 5:
|
||||
newWidth = cropRect.width() - dx;
|
||||
newHeight = newWidth / aspectRatio;
|
||||
cropRect.left = cropRect.right - newWidth;
|
||||
cropRect.bottom = cropRect.top + newHeight;
|
||||
break;
|
||||
}
|
||||
|
||||
if (cropRect.width() < minSize || cropRect.height() < minSize) {
|
||||
cropRect.set(lastRect);
|
||||
}
|
||||
|
||||
enforceBounds();
|
||||
}
|
||||
|
||||
lastTouchX = x;
|
||||
lastTouchY = y;
|
||||
invalidate();
|
||||
return true;
|
||||
|
||||
case MotionEvent.ACTION_UP:
|
||||
isDragging = false;
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private boolean isInCorner(float x, float y, float cornerX, float cornerY) {
|
||||
float touchArea = 50f;
|
||||
return x >= cornerX - touchArea && x <= cornerX + touchArea &&
|
||||
y >= cornerY - touchArea && y <= cornerY + touchArea;
|
||||
}
|
||||
|
||||
public void setAspectRatio(float ratio) {
|
||||
this.aspectRatio = ratio;
|
||||
if (videoBounds != null) {
|
||||
float centerX = cropRect.centerX();
|
||||
float centerY = cropRect.centerY();
|
||||
float size = Math.max(cropRect.width(), cropRect.height());
|
||||
updateCropRect(centerX, centerY, size);
|
||||
}
|
||||
}
|
||||
|
||||
public RectF getCropRect() {
|
||||
return new RectF(cropRect);
|
||||
}
|
||||
|
||||
|
||||
@Nullable
|
||||
public RectF getCropRectInVideoCoords(int videoWidth, int videoHeight) {
|
||||
if (videoBounds == null) return null;
|
||||
|
||||
float scaleX = videoWidth / videoBounds.width();
|
||||
float scaleY = videoHeight / videoBounds.height();
|
||||
|
||||
float left = (cropRect.left - videoBounds.left) * scaleX;
|
||||
float top = (cropRect.top - videoBounds.top) * scaleY;
|
||||
float width = cropRect.width() * scaleX;
|
||||
float height = cropRect.height() * scaleY;
|
||||
|
||||
return new RectF(left, top, left + width, top + height);
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,87 @@
|
||||
package com.audio.record.screen.test.view
|
||||
import android.content.Context
|
||||
import android.graphics.PixelFormat
|
||||
import android.os.Build
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import android.view.*
|
||||
import android.view.animation.AccelerateDecelerateInterpolator
|
||||
import android.widget.TextView
|
||||
import com.audio.record.screen.test.R
|
||||
|
||||
object CountDownFloatingManager {
|
||||
|
||||
private var countdownView: View? = null
|
||||
private var handler: Handler? = null
|
||||
|
||||
fun show(context: Context, onFinish: (() -> Unit)? = null) {
|
||||
if (countdownView != null) return // Already showing
|
||||
|
||||
val windowManager = context.getSystemService(Context.WINDOW_SERVICE) as WindowManager
|
||||
val inflater = LayoutInflater.from(context)
|
||||
countdownView = inflater.inflate(R.layout.floating_view_countdown, null)
|
||||
val tvCountdown = countdownView!!.findViewById<TextView>(R.id.tv_countdown)
|
||||
|
||||
val layoutParams = WindowManager.LayoutParams(
|
||||
WindowManager.LayoutParams.WRAP_CONTENT,
|
||||
WindowManager.LayoutParams.WRAP_CONTENT,
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O)
|
||||
WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY
|
||||
else
|
||||
WindowManager.LayoutParams.TYPE_PHONE,
|
||||
WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE or WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE,
|
||||
PixelFormat.TRANSLUCENT
|
||||
)
|
||||
layoutParams.gravity = Gravity.CENTER
|
||||
|
||||
windowManager.addView(countdownView, layoutParams)
|
||||
|
||||
val countdownValues = arrayOf("3", "2", "1")
|
||||
var index = 0
|
||||
|
||||
handler = Handler(Looper.getMainLooper())
|
||||
|
||||
fun animateNext() {
|
||||
if (index >= countdownValues.size) {
|
||||
remove(windowManager)
|
||||
onFinish?.invoke()
|
||||
return
|
||||
}
|
||||
|
||||
tvCountdown.text = countdownValues[index]
|
||||
tvCountdown.scaleX = 0f
|
||||
tvCountdown.scaleY = 0f
|
||||
tvCountdown.alpha = 0f
|
||||
|
||||
tvCountdown.animate()
|
||||
.scaleX(1f).scaleY(1f)
|
||||
.alpha(1f)
|
||||
.setDuration(400)
|
||||
.setInterpolator(AccelerateDecelerateInterpolator())
|
||||
.withEndAction {
|
||||
handler?.postDelayed({
|
||||
index++
|
||||
animateNext()
|
||||
}, 600)
|
||||
}
|
||||
.start()
|
||||
}
|
||||
|
||||
animateNext()
|
||||
}
|
||||
|
||||
fun remove(windowManager: WindowManager) {
|
||||
handler?.removeCallbacksAndMessages(null)
|
||||
handler = null
|
||||
countdownView?.let {
|
||||
try {
|
||||
windowManager.removeView(it)
|
||||
} catch (e: Exception) {
|
||||
// view already removed
|
||||
}
|
||||
}
|
||||
countdownView = null
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@ -0,0 +1,37 @@
|
||||
package com.audio.record.screen.test.view
|
||||
|
||||
import android.graphics.Rect
|
||||
import android.view.View
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
|
||||
|
||||
class GridSpacingItemDecoration(
|
||||
private val spanCount: Int, // 列数
|
||||
private val spacing: Int, // 间距(px)
|
||||
private val includeEdge: Boolean // 是否包含边缘间距
|
||||
) : RecyclerView.ItemDecoration() {
|
||||
|
||||
override fun getItemOffsets(
|
||||
outRect: Rect, view: View,
|
||||
parent: RecyclerView, state: RecyclerView.State
|
||||
) {
|
||||
val position = parent.getChildAdapterPosition(view) // item position
|
||||
val column = position % spanCount // item column
|
||||
|
||||
if (includeEdge) {
|
||||
outRect.left = spacing - column * spacing / spanCount
|
||||
outRect.right = (column + 1) * spacing / spanCount
|
||||
|
||||
if (position < spanCount) { // top edge
|
||||
outRect.top = spacing
|
||||
}
|
||||
outRect.bottom = spacing // item bottom
|
||||
} else {
|
||||
outRect.left = column * spacing / spanCount
|
||||
outRect.right = spacing - (column + 1) * spacing / spanCount
|
||||
if (position >= spanCount) {
|
||||
outRect.top = spacing // item top
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,91 @@
|
||||
package com.audio.record.screen.test.view
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.Canvas
|
||||
import android.graphics.Color
|
||||
import android.graphics.Paint
|
||||
import android.util.AttributeSet
|
||||
import android.view.MotionEvent
|
||||
import android.view.View
|
||||
import java.lang.Math.abs
|
||||
|
||||
class RangeSliderView @JvmOverloads constructor(
|
||||
context: Context, attrs: AttributeSet? = null
|
||||
) : View(context, attrs) {
|
||||
|
||||
private var leftThumbX = 100f
|
||||
private var rightThumbX = 600f
|
||||
private val thumbRadius = 30f
|
||||
|
||||
private val trackHeight = 20f
|
||||
|
||||
private val trackPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
|
||||
color = Color.RED
|
||||
}
|
||||
|
||||
private val thumbPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
|
||||
color = Color.RED
|
||||
}
|
||||
|
||||
private val linePaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
|
||||
color = Color.WHITE
|
||||
strokeWidth = 6f
|
||||
}
|
||||
|
||||
private var activeThumb: Thumb? = null
|
||||
|
||||
enum class Thumb { LEFT, RIGHT }
|
||||
|
||||
override fun onDraw(canvas: Canvas) {
|
||||
super.onDraw(canvas)
|
||||
|
||||
// Draw selected range (red track)
|
||||
val centerY = height / 2f
|
||||
canvas.drawRect(leftThumbX, centerY - trackHeight / 2, rightThumbX, centerY + trackHeight / 2, trackPaint)
|
||||
|
||||
// Draw thumbs
|
||||
canvas.drawRect(leftThumbX - thumbRadius, centerY - 2 * thumbRadius,
|
||||
leftThumbX + thumbRadius, centerY + 2 * thumbRadius, thumbPaint)
|
||||
canvas.drawRect(rightThumbX - thumbRadius, centerY - 2 * thumbRadius,
|
||||
rightThumbX + thumbRadius, centerY + 2 * thumbRadius, thumbPaint)
|
||||
|
||||
// Draw white lines on thumbs
|
||||
val lineHeight = 40f
|
||||
canvas.drawLine(leftThumbX, centerY - lineHeight, leftThumbX, centerY + lineHeight, linePaint)
|
||||
canvas.drawLine(rightThumbX, centerY - lineHeight, rightThumbX, centerY + lineHeight, linePaint)
|
||||
}
|
||||
|
||||
override fun onTouchEvent(event: MotionEvent): Boolean {
|
||||
val x = event.x
|
||||
when (event.action) {
|
||||
MotionEvent.ACTION_DOWN -> {
|
||||
activeThumb = when {
|
||||
isInsideThumb(x, leftThumbX) -> Thumb.LEFT
|
||||
isInsideThumb(x, rightThumbX) -> Thumb.RIGHT
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
MotionEvent.ACTION_MOVE -> {
|
||||
activeThumb?.let {
|
||||
when (it) {
|
||||
Thumb.LEFT -> {
|
||||
leftThumbX = x.coerceIn(0f, rightThumbX - 100f)
|
||||
}
|
||||
Thumb.RIGHT -> {
|
||||
rightThumbX = x.coerceIn(leftThumbX + 100f, width.toFloat())
|
||||
}
|
||||
}
|
||||
invalidate()
|
||||
}
|
||||
}
|
||||
MotionEvent.ACTION_UP -> {
|
||||
activeThumb = null
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
private fun isInsideThumb(touchX: Float, thumbX: Float): Boolean {
|
||||
return abs(touchX - thumbX) <= thumbRadius * 2
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,44 @@
|
||||
package com.audio.record.screen.test.viewmodel
|
||||
|
||||
import androidx.lifecycle.LiveData
|
||||
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
|
||||
import androidx.lifecycle.ViewModel
|
||||
|
||||
class MainViewModel : ViewModel() {
|
||||
|
||||
//服务连接
|
||||
private val _serviceConnectStatus = MutableLiveData<Boolean>()
|
||||
val serviceConnectStatus: LiveData<Boolean> get() = _serviceConnectStatus
|
||||
|
||||
|
||||
//前置摄像头状态
|
||||
private val _webcamStatus = MutableLiveData<Boolean>()
|
||||
val webcamStatus: LiveData<Boolean> get() = _webcamStatus
|
||||
|
||||
|
||||
//截屏状态
|
||||
private val _screenshotStatus = MutableLiveData<Boolean>()
|
||||
val screenshotStatus: LiveData<Boolean> get() = _screenshotStatus
|
||||
|
||||
|
||||
//悬浮球状态
|
||||
private val _ballStatus = MutableLiveData<Boolean>()
|
||||
val ballStatus: LiveData<Boolean> get() = _ballStatus
|
||||
|
||||
|
||||
fun updateServiceConnectStatus(message: Boolean) {
|
||||
_serviceConnectStatus.value = message
|
||||
}
|
||||
|
||||
fun updateBallStatus(message: Boolean) {
|
||||
_ballStatus.value = message
|
||||
}
|
||||
fun updateWebcamStatus(message: Boolean) {
|
||||
_webcamStatus.value = message
|
||||
}
|
||||
fun updateScreenshotStatus(message: Boolean) {
|
||||
_screenshotStatus.value = message
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,46 @@
|
||||
package com.audio.record.screen.test.viewmodel
|
||||
|
||||
import androidx.lifecycle.LiveData
|
||||
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
|
||||
import androidx.lifecycle.ViewModel
|
||||
|
||||
class PreviewViewModel : ViewModel() {
|
||||
|
||||
//裁剪比例选择变化
|
||||
private val _cropRatioText = MutableLiveData<String>()
|
||||
val cropRatioText: LiveData<String> get() = _cropRatioText
|
||||
|
||||
|
||||
//复制当前文件到内部存储的结果
|
||||
private val _copySuccess = MutableLiveData<Pair<Boolean,String>>()
|
||||
val copySuccess: LiveData<Pair<Boolean,String>> get() = _copySuccess
|
||||
|
||||
|
||||
//保存裁剪文件
|
||||
private val _saveCrop = MutableLiveData<Boolean>()
|
||||
val saveCrop: LiveData<Boolean> get() = _saveCrop
|
||||
|
||||
|
||||
//更新当前播放速度
|
||||
private val _changeSpeed = MutableLiveData<Float>()
|
||||
val changeSpeed: LiveData<Float> get() = _changeSpeed
|
||||
|
||||
|
||||
fun updateCropText(message: String) {
|
||||
_cropRatioText.value = message
|
||||
}
|
||||
|
||||
fun updateCopyResult(message: Pair<Boolean,String>) {
|
||||
_copySuccess.value = message
|
||||
}
|
||||
|
||||
fun updateClickCropSave(message: Boolean) {
|
||||
_saveCrop.value = message
|
||||
}
|
||||
|
||||
fun updateSpeed(message: Float) {
|
||||
_changeSpeed.value = message
|
||||
}
|
||||
}
|
||||
5
app/src/main/res/color/selector_tab_child_text_color.xml
Normal file
5
app/src/main/res/color/selector_tab_child_text_color.xml
Normal file
@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<selector xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item android:color="@color/color_978080" android:state_selected="false"/>
|
||||
<item android:color="@color/white" android:state_selected="true"/>
|
||||
</selector>
|
||||
BIN
app/src/main/res/drawable/bg_empty.png
Normal file
BIN
app/src/main/res/drawable/bg_empty.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 21 KiB |
BIN
app/src/main/res/drawable/bg_recording.png
Normal file
BIN
app/src/main/res/drawable/bg_recording.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 44 KiB |
7
app/src/main/res/drawable/btn_off.xml
Normal file
7
app/src/main/res/drawable/btn_off.xml
Normal file
@ -0,0 +1,7 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:shape="rectangle">
|
||||
<corners android:radius="18dp"/>
|
||||
<solid android:color="@color/btn_off"/>
|
||||
|
||||
</shape>
|
||||
7
app/src/main/res/drawable/btn_on.xml
Normal file
7
app/src/main/res/drawable/btn_on.xml
Normal file
@ -0,0 +1,7 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:shape="rectangle">
|
||||
<solid android:color="@color/btn_on"/>
|
||||
<corners android:radius="18dp"/>
|
||||
|
||||
</shape>
|
||||
5
app/src/main/res/drawable/btn_state.xml
Normal file
5
app/src/main/res/drawable/btn_state.xml
Normal file
@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<selector xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item android:state_selected="false" android:drawable="@drawable/btn_off"/>
|
||||
<item android:state_selected="true" android:drawable="@drawable/btn_on"/>
|
||||
</selector>
|
||||
9
app/src/main/res/drawable/close.xml
Normal file
9
app/src/main/res/drawable/close.xml
Normal file
@ -0,0 +1,9 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="256dp"
|
||||
android:height="256dp"
|
||||
android:viewportWidth="1024"
|
||||
android:viewportHeight="1024">
|
||||
<path
|
||||
android:pathData="M512,570.9l196.9,196.8 58.9,-58.9L570.8,512l196.9,-196.9 -58.8,-58.9L512,453.2 315.1,256.3l-58.9,58.9L453.2,512l-196.9,196.9 58.9,58.9z"
|
||||
android:fillColor="#28354C"/>
|
||||
</vector>
|
||||
14
app/src/main/res/drawable/dialog_audio_close.xml
Normal file
14
app/src/main/res/drawable/dialog_audio_close.xml
Normal file
@ -0,0 +1,14 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="77dp"
|
||||
android:height="77dp"
|
||||
android:viewportWidth="77"
|
||||
android:viewportHeight="77">
|
||||
<path
|
||||
android:pathData="M38.5,38.5m-37.5,0a37.5,37.5 0,1 1,75 0a37.5,37.5 0,1 1,-75 0"
|
||||
android:strokeWidth="2"
|
||||
android:fillColor="#00000000"
|
||||
android:strokeColor="#D5D5D5"/>
|
||||
<path
|
||||
android:pathData="M50.489,28.96L40.794,38.654L51,48.86L49.469,50.391L39.264,40.186L29.059,50.391L27.527,48.86L37.732,38.654L28.038,28.96L29.568,27.429L39.264,37.124L48.959,27.429L50.489,28.96Z"
|
||||
android:fillColor="#D9D9D9"/>
|
||||
</vector>
|
||||
@ -0,0 +1,7 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:shape="rectangle">
|
||||
<corners android:topLeftRadius="22dp" android:topRightRadius="22dp"/>
|
||||
<solid android:color="@color/color_F6F5F5"/>
|
||||
|
||||
</shape>
|
||||
@ -0,0 +1,7 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:shape="rectangle">
|
||||
<corners android:radius="14dp"/>
|
||||
<solid android:color="@color/tab_selected_color"/>
|
||||
|
||||
</shape>
|
||||
7
app/src/main/res/drawable/dialog_permission_item_bg.xml
Normal file
7
app/src/main/res/drawable/dialog_permission_item_bg.xml
Normal file
@ -0,0 +1,7 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:shape="rectangle">
|
||||
<corners android:radius="13dp"/>
|
||||
<solid android:color="@color/white"/>
|
||||
|
||||
</shape>
|
||||
18
app/src/main/res/drawable/dialog_with_audio.xml
Normal file
18
app/src/main/res/drawable/dialog_with_audio.xml
Normal file
@ -0,0 +1,18 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="77dp"
|
||||
android:height="77dp"
|
||||
android:viewportWidth="77"
|
||||
android:viewportHeight="77">
|
||||
<path
|
||||
android:pathData="M38.5,38.5m-38.5,0a38.5,38.5 0,1 1,77 0a38.5,38.5 0,1 1,-77 0"
|
||||
android:fillColor="#FD5965"/>
|
||||
<path
|
||||
android:pathData="M41.923,20.262C42.733,21.071 43.188,22.169 43.188,23.315V53.919C43.188,56.302 41.256,58.234 38.873,58.234C37.727,58.234 36.629,57.779 35.82,56.969L27.972,49.114C27.53,48.672 26.931,48.424 26.306,48.424H23.278C19.811,48.424 17,45.614 17,42.147V35.087C17,31.62 19.811,28.81 23.278,28.81H26.306C26.931,28.81 27.53,28.562 27.972,28.12L35.82,20.265C37.504,18.579 40.236,18.578 41.923,20.262Z"
|
||||
android:fillColor="#ffffff"/>
|
||||
<path
|
||||
android:pathData="M57.36,39.252C57.36,32.002 52.735,26.665 47.617,26.624L47.557,26.623L47.521,26.623C46.782,26.604 46.188,25.999 46.188,25.254C46.188,24.498 46.8,23.885 47.557,23.885L47.641,23.885C54.797,23.941 60.098,31.101 60.098,39.252C60.098,47.434 54.755,54.618 47.557,54.618C46.8,54.618 46.188,54.005 46.188,53.249C46.188,52.493 46.8,51.88 47.557,51.88C52.699,51.88 57.36,46.53 57.36,39.252Z"
|
||||
android:fillColor="#ffffff"/>
|
||||
<path
|
||||
android:pathData="M49.506,39.251C49.506,37.621 48.636,36.193 47.327,35.407L47.265,35.37L47.231,35.35C46.525,34.917 46.289,33.997 46.706,33.276C47.123,32.555 48.038,32.301 48.765,32.698L48.8,32.717L48.853,32.748C51.074,34.058 52.571,36.479 52.571,39.251C52.571,42.045 51.051,44.483 48.8,45.785C48.067,46.208 47.129,45.958 46.706,45.225C46.282,44.493 46.532,43.555 47.265,43.131C48.607,42.355 49.506,40.907 49.506,39.251Z"
|
||||
android:fillColor="#ffffff"/>
|
||||
</vector>
|
||||
20
app/src/main/res/drawable/dialog_without_audio.xml
Normal file
20
app/src/main/res/drawable/dialog_without_audio.xml
Normal file
@ -0,0 +1,20 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="77dp"
|
||||
android:height="77dp"
|
||||
android:viewportWidth="77"
|
||||
android:viewportHeight="77">
|
||||
<path
|
||||
android:pathData="M38.5,38.5m-38.5,0a38.5,38.5 0,1 1,77 0a38.5,38.5 0,1 1,-77 0"
|
||||
android:fillColor="#FD5965"/>
|
||||
<group>
|
||||
<clip-path
|
||||
android:pathData="M17,19h42v39h-42z"/>
|
||||
<group>
|
||||
<clip-path
|
||||
android:pathData="M59,19H17.007V58H59V19Z"/>
|
||||
<path
|
||||
android:pathData="M41.783,20.314C42.589,21.117 43.041,22.206 43.041,23.343V53.717C43.041,56.082 41.12,58 38.75,58C37.611,58 36.519,57.548 35.714,56.744L27.91,48.949C27.471,48.51 26.875,48.264 26.255,48.264H23.243C19.795,48.264 17.001,45.474 17.001,42.035V35.027C17.001,31.586 19.795,28.798 23.243,28.798H26.255C26.876,28.798 27.471,28.551 27.91,28.112L35.714,20.317C37.389,18.644 40.106,18.642 41.783,20.314ZM58.495,32.089C59.105,32.698 59.105,33.686 58.495,34.295L54.19,38.592L58.495,42.889C59.105,43.498 59.105,44.486 58.495,45.095C57.884,45.705 56.895,45.705 56.284,45.095L51.979,40.799L47.674,45.095C47.063,45.705 46.074,45.705 45.463,45.095C44.853,44.486 44.853,43.498 45.463,42.889L49.769,38.593L45.463,34.296C44.852,33.687 44.852,32.699 45.463,32.09C46.073,31.48 47.063,31.48 47.673,32.09L51.978,36.386L56.284,32.09C56.895,31.48 57.884,31.48 58.495,32.09L58.495,32.089Z"
|
||||
android:fillColor="#ffffff"/>
|
||||
</group>
|
||||
</group>
|
||||
</vector>
|
||||
17
app/src/main/res/drawable/global_ball.xml
Normal file
17
app/src/main/res/drawable/global_ball.xml
Normal file
@ -0,0 +1,17 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="36dp"
|
||||
android:height="36dp"
|
||||
android:viewportWidth="36"
|
||||
android:viewportHeight="36">
|
||||
<path
|
||||
android:pathData="M17.873,17.873m-16.873,0a16.873,16.873 0,1 1,33.745 0a16.873,16.873 0,1 1,-33.745 0"
|
||||
android:strokeWidth="1.5"
|
||||
android:fillColor="#FD5965"
|
||||
android:strokeColor="#000000"/>
|
||||
<path
|
||||
android:pathData="M8.499,14.263C8.499,13.15 9.401,12.248 10.514,12.248H20.501C21.614,12.248 22.516,13.15 22.516,14.263V21.655C22.516,22.768 21.614,23.67 20.501,23.67H10.514C9.401,23.67 8.499,22.768 8.499,21.655V14.263Z"
|
||||
android:fillColor="#ffffff"/>
|
||||
<path
|
||||
android:pathData="M20.307,18.821C19.659,18.429 19.659,17.489 20.307,17.097L25.53,13.938C26.201,13.532 27.059,14.015 27.059,14.8L27.059,21.118C27.059,21.903 26.201,22.386 25.53,21.98L20.307,18.821Z"
|
||||
android:fillColor="#ffffff"/>
|
||||
</vector>
|
||||
20
app/src/main/res/drawable/global_screenshot.xml
Normal file
20
app/src/main/res/drawable/global_screenshot.xml
Normal file
@ -0,0 +1,20 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="36dp"
|
||||
android:height="36dp"
|
||||
android:viewportWidth="36"
|
||||
android:viewportHeight="36">
|
||||
<path
|
||||
android:pathData="M17.873,17.873m-16.873,0a16.873,16.873 0,1 1,33.745 0a16.873,16.873 0,1 1,-33.745 0"
|
||||
android:strokeWidth="1.5"
|
||||
android:fillColor="#FD5965"
|
||||
android:strokeColor="#000000"/>
|
||||
<path
|
||||
android:pathData="M10.923,8H12.374V22.681H27V24.077H10.923V8Z"
|
||||
android:fillColor="#ffffff"/>
|
||||
<path
|
||||
android:pathData="M24.077,27H22.681V12.374H8V10.923H24.077V27Z"
|
||||
android:fillColor="#ffffff"/>
|
||||
<path
|
||||
android:pathData="M25.538,10.325L14.805,21.154L13.846,20.291L24.58,9.461L25.538,10.325Z"
|
||||
android:fillColor="#ffffff"/>
|
||||
</vector>
|
||||
170
app/src/main/res/drawable/ic_launcher_background.xml
Normal file
170
app/src/main/res/drawable/ic_launcher_background.xml
Normal file
@ -0,0 +1,170 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="108dp"
|
||||
android:height="108dp"
|
||||
android:viewportWidth="108"
|
||||
android:viewportHeight="108">
|
||||
<path
|
||||
android:fillColor="#3DDC84"
|
||||
android:pathData="M0,0h108v108h-108z" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M9,0L9,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M19,0L19,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M29,0L29,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M39,0L39,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M49,0L49,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M59,0L59,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M69,0L69,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M79,0L79,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M89,0L89,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M99,0L99,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,9L108,9"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,19L108,19"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,29L108,29"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,39L108,39"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,49L108,49"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,59L108,59"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,69L108,69"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,79L108,79"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,89L108,89"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,99L108,99"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M19,29L89,29"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M19,39L89,39"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M19,49L89,49"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M19,59L89,59"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M19,69L89,69"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M19,79L89,79"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M29,19L29,89"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M39,19L39,89"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M49,19L49,89"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M59,19L59,89"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M69,19L69,89"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M79,19L79,89"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
</vector>
|
||||
30
app/src/main/res/drawable/ic_launcher_foreground.xml
Normal file
30
app/src/main/res/drawable/ic_launcher_foreground.xml
Normal file
@ -0,0 +1,30 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:aapt="http://schemas.android.com/aapt"
|
||||
android:width="108dp"
|
||||
android:height="108dp"
|
||||
android:viewportWidth="108"
|
||||
android:viewportHeight="108">
|
||||
<path android:pathData="M31,63.928c0,0 6.4,-11 12.1,-13.1c7.2,-2.6 26,-1.4 26,-1.4l38.1,38.1L107,108.928l-32,-1L31,63.928z">
|
||||
<aapt:attr name="android:fillColor">
|
||||
<gradient
|
||||
android:endX="85.84757"
|
||||
android:endY="92.4963"
|
||||
android:startX="42.9492"
|
||||
android:startY="49.59793"
|
||||
android:type="linear">
|
||||
<item
|
||||
android:color="#44000000"
|
||||
android:offset="0.0" />
|
||||
<item
|
||||
android:color="#00000000"
|
||||
android:offset="1.0" />
|
||||
</gradient>
|
||||
</aapt:attr>
|
||||
</path>
|
||||
<path
|
||||
android:fillColor="#FFFFFF"
|
||||
android:fillType="nonZero"
|
||||
android:pathData="M65.3,45.828l3.8,-6.6c0.2,-0.4 0.1,-0.9 -0.3,-1.1c-0.4,-0.2 -0.9,-0.1 -1.1,0.3l-3.9,6.7c-6.3,-2.8 -13.4,-2.8 -19.7,0l-3.9,-6.7c-0.2,-0.4 -0.7,-0.5 -1.1,-0.3C38.8,38.328 38.7,38.828 38.9,39.228l3.8,6.6C36.2,49.428 31.7,56.028 31,63.928h46C76.3,56.028 71.8,49.428 65.3,45.828zM43.4,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2c-0.3,-0.7 -0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C45.3,56.528 44.5,57.328 43.4,57.328L43.4,57.328zM64.6,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2s-0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C66.5,56.528 65.6,57.328 64.6,57.328L64.6,57.328z"
|
||||
android:strokeWidth="1"
|
||||
android:strokeColor="#00000000" />
|
||||
</vector>
|
||||
17
app/src/main/res/drawable/icon_floatingball.xml
Normal file
17
app/src/main/res/drawable/icon_floatingball.xml
Normal file
@ -0,0 +1,17 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="56dp"
|
||||
android:height="56dp"
|
||||
android:viewportWidth="56"
|
||||
android:viewportHeight="56">
|
||||
<path
|
||||
android:pathData="M28,28m-28,0a28,28 0,1 1,56 0a28,28 0,1 1,-56 0"
|
||||
android:fillColor="#FD5965"/>
|
||||
<path
|
||||
android:pathData="M28,28m14,0a14,14 0,1 0,-28 0a14,14 0,1 0,28 0"
|
||||
android:strokeWidth="2"
|
||||
android:fillColor="#00000000"
|
||||
android:strokeColor="#ffffff"/>
|
||||
<path
|
||||
android:pathData="M27.788,28.212m7.212,0a7.212,7.212 0,1 0,-14.424 0a7.212,7.212 0,1 0,14.424 0"
|
||||
android:fillColor="#ffffff"/>
|
||||
</vector>
|
||||
17
app/src/main/res/drawable/icon_floatingball_normal.xml
Normal file
17
app/src/main/res/drawable/icon_floatingball_normal.xml
Normal file
@ -0,0 +1,17 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="56dp"
|
||||
android:height="56dp"
|
||||
android:viewportWidth="56"
|
||||
android:viewportHeight="56">
|
||||
<path
|
||||
android:pathData="M28,28m-28,0a28,28 0,1 1,56 0a28,28 0,1 1,-56 0"
|
||||
android:fillColor="#FFF0F1"/>
|
||||
<path
|
||||
android:pathData="M28,28m14,0a14,14 0,1 0,-28 0a14,14 0,1 0,28 0"
|
||||
android:strokeWidth="2"
|
||||
android:fillColor="#00000000"
|
||||
android:strokeColor="#E63946"/>
|
||||
<path
|
||||
android:pathData="M28,28m7.212,0a7.212,7.212 0,1 0,-14.424 0a7.212,7.212 0,1 0,14.424 0"
|
||||
android:fillColor="#E63946"/>
|
||||
</vector>
|
||||
20
app/src/main/res/drawable/icon_main_recorder.xml
Normal file
20
app/src/main/res/drawable/icon_main_recorder.xml
Normal file
@ -0,0 +1,20 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="77dp"
|
||||
android:height="77dp"
|
||||
android:viewportWidth="77"
|
||||
android:viewportHeight="77">
|
||||
<path
|
||||
android:pathData="M38.5,38.5m-33.5,0a33.5,33.5 0,1 1,67 0a33.5,33.5 0,1 1,-67 0"
|
||||
android:fillColor="#FD5965"/>
|
||||
<path
|
||||
android:strokeWidth="1"
|
||||
android:pathData="M38.5,38.5m-38,0a38,38 0,1 1,76 0a38,38 0,1 1,-76 0"
|
||||
android:fillColor="#00000000"
|
||||
android:strokeColor="#FD5965"/>
|
||||
<path
|
||||
android:pathData="M24.461,27.677L44.292,27.677A4,4 0,0 1,48.292 31.677L48.292,46.354A4,4 0,0 1,44.292 50.354L24.461,50.354A4,4 0,0 1,20.461 46.354L20.461,31.677A4,4 0,0 1,24.461 27.677z"
|
||||
android:fillColor="#ffffff"/>
|
||||
<path
|
||||
android:pathData="M43.906,40.727C42.62,39.949 42.62,38.082 43.906,37.304L54.276,31.032C55.609,30.225 57.312,31.185 57.312,32.743L57.312,45.288C57.312,46.845 55.609,47.805 54.276,46.999L43.906,40.727Z"
|
||||
android:fillColor="#ffffff"/>
|
||||
</vector>
|
||||
12
app/src/main/res/drawable/icon_recording_pause.xml
Normal file
12
app/src/main/res/drawable/icon_recording_pause.xml
Normal file
@ -0,0 +1,12 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="29dp"
|
||||
android:height="38dp"
|
||||
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>
|
||||
9
app/src/main/res/drawable/icon_recording_record.xml
Normal file
9
app/src/main/res/drawable/icon_recording_record.xml
Normal file
@ -0,0 +1,9 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="28dp"
|
||||
android:height="32dp"
|
||||
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>
|
||||
12
app/src/main/res/drawable/icon_recording_stop.xml
Normal file
12
app/src/main/res/drawable/icon_recording_stop.xml
Normal file
@ -0,0 +1,12 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="77dp"
|
||||
android:height="77dp"
|
||||
android:viewportWidth="77"
|
||||
android:viewportHeight="77">
|
||||
<path
|
||||
android:pathData="M38.5,38.5m-38.5,0a38.5,38.5 0,1 1,77 0a38.5,38.5 0,1 1,-77 0"
|
||||
android:fillColor="#ffffff"/>
|
||||
<path
|
||||
android:pathData="M29,26L47,26A3,3 0,0 1,50 29L50,47A3,3 0,0 1,47 50L29,50A3,3 0,0 1,26 47L26,29A3,3 0,0 1,29 26z"
|
||||
android:fillColor="#ED606A"/>
|
||||
</vector>
|
||||
27
app/src/main/res/drawable/icon_screenshot.xml
Normal file
27
app/src/main/res/drawable/icon_screenshot.xml
Normal file
@ -0,0 +1,27 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="56dp"
|
||||
android:height="56dp"
|
||||
android:viewportWidth="56"
|
||||
android:viewportHeight="56">
|
||||
<path
|
||||
android:pathData="M28,28m-28,0a28,28 0,1 1,56 0a28,28 0,1 1,-56 0"
|
||||
android:fillColor="#FD5965"/>
|
||||
<path
|
||||
android:pathData="M20.26,16V37.039H40.422"
|
||||
android:strokeWidth="2"
|
||||
android:fillColor="#00000000"
|
||||
android:strokeColor="#ffffff"
|
||||
android:strokeLineCap="square"/>
|
||||
<path
|
||||
android:pathData="M36.039,40.545V20.383H15"
|
||||
android:strokeWidth="2"
|
||||
android:fillColor="#00000000"
|
||||
android:strokeColor="#ffffff"
|
||||
android:strokeLineCap="square"/>
|
||||
<path
|
||||
android:pathData="M23.766,33.532L38.669,17.753"
|
||||
android:strokeWidth="2"
|
||||
android:fillColor="#00000000"
|
||||
android:strokeColor="#ffffff"
|
||||
android:strokeLineCap="square"/>
|
||||
</vector>
|
||||
27
app/src/main/res/drawable/icon_screenshot_normal.xml
Normal file
27
app/src/main/res/drawable/icon_screenshot_normal.xml
Normal file
@ -0,0 +1,27 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="56dp"
|
||||
android:height="56dp"
|
||||
android:viewportWidth="56"
|
||||
android:viewportHeight="56">
|
||||
<path
|
||||
android:pathData="M28,28m-28,0a28,28 0,1 1,56 0a28,28 0,1 1,-56 0"
|
||||
android:fillColor="#FFF0F1"/>
|
||||
<path
|
||||
android:pathData="M20.26,16V37.039H40.422"
|
||||
android:strokeWidth="2"
|
||||
android:fillColor="#00000000"
|
||||
android:strokeColor="#E63946"
|
||||
android:strokeLineCap="square"/>
|
||||
<path
|
||||
android:pathData="M36.039,40.545V20.383H15"
|
||||
android:strokeWidth="2"
|
||||
android:fillColor="#00000000"
|
||||
android:strokeColor="#E63946"
|
||||
android:strokeLineCap="square"/>
|
||||
<path
|
||||
android:pathData="M23.766,33.532L38.669,17.753"
|
||||
android:strokeWidth="2"
|
||||
android:fillColor="#00000000"
|
||||
android:strokeColor="#F4878F"
|
||||
android:strokeLineCap="square"/>
|
||||
</vector>
|
||||
18
app/src/main/res/drawable/icon_small_pause.xml
Normal file
18
app/src/main/res/drawable/icon_small_pause.xml
Normal file
@ -0,0 +1,18 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="46dp"
|
||||
android:height="46dp"
|
||||
android:viewportWidth="46"
|
||||
android:viewportHeight="46">
|
||||
<path
|
||||
android:pathData="M23,23m-22,0a22,22 0,1 1,44 0a22,22 0,1 1,-44 0"
|
||||
android:strokeWidth="2"
|
||||
android:fillColor="#000000"
|
||||
android:fillAlpha="0.48"
|
||||
android:strokeColor="#ffffff"/>
|
||||
<path
|
||||
android:pathData="M16,14h4v18h-4z"
|
||||
android:fillColor="#ffffff"/>
|
||||
<path
|
||||
android:pathData="M26,14h4v18h-4z"
|
||||
android:fillColor="#ffffff"/>
|
||||
</vector>
|
||||
15
app/src/main/res/drawable/icon_thumb_video_play.xml
Normal file
15
app/src/main/res/drawable/icon_thumb_video_play.xml
Normal file
@ -0,0 +1,15 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="46dp"
|
||||
android:height="46dp"
|
||||
android:viewportWidth="46"
|
||||
android:viewportHeight="46">
|
||||
<path
|
||||
android:pathData="M23,23m-22,0a22,22 0,1 1,44 0a22,22 0,1 1,-44 0"
|
||||
android:strokeWidth="2"
|
||||
android:fillColor="#000000"
|
||||
android:fillAlpha="0.48"
|
||||
android:strokeColor="#ffffff"/>
|
||||
<path
|
||||
android:pathData="M33,22.5L17.25,31.593L17.25,13.407L33,22.5Z"
|
||||
android:fillColor="#ffffff"/>
|
||||
</vector>
|
||||
25
app/src/main/res/drawable/icon_webcam.xml
Normal file
25
app/src/main/res/drawable/icon_webcam.xml
Normal file
@ -0,0 +1,25 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="56dp"
|
||||
android:height="56dp"
|
||||
android:viewportWidth="56"
|
||||
android:viewportHeight="56">
|
||||
<path
|
||||
android:pathData="M28,28m-28,0a28,28 0,1 1,56 0a28,28 0,1 1,-56 0"
|
||||
android:fillColor="#FD5965"/>
|
||||
<path
|
||||
android:pathData="M28.227,26.227m-9.227,0a9.227,9.227 0,1 1,18.455 0a9.227,9.227 0,1 1,-18.455 0"
|
||||
android:strokeWidth="2"
|
||||
android:fillColor="#00000000"
|
||||
android:strokeColor="#ffffff"/>
|
||||
<path
|
||||
android:pathData="M28.227,26.227m-4.318,0a4.318,4.318 0,1 1,8.636 0a4.318,4.318 0,1 1,-8.636 0"
|
||||
android:strokeWidth="2"
|
||||
android:fillColor="#00000000"
|
||||
android:strokeColor="#ffffff"/>
|
||||
<path
|
||||
android:pathData="M22.091,40.545C24.039,38.693 29.221,36.1 34.363,40.545"
|
||||
android:strokeWidth="2"
|
||||
android:fillColor="#00000000"
|
||||
android:strokeColor="#ffffff"
|
||||
android:strokeLineCap="round"/>
|
||||
</vector>
|
||||
25
app/src/main/res/drawable/icon_webcam_normal.xml
Normal file
25
app/src/main/res/drawable/icon_webcam_normal.xml
Normal file
@ -0,0 +1,25 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="56dp"
|
||||
android:height="56dp"
|
||||
android:viewportWidth="56"
|
||||
android:viewportHeight="56">
|
||||
<path
|
||||
android:pathData="M28,28m-28,0a28,28 0,1 1,56 0a28,28 0,1 1,-56 0"
|
||||
android:fillColor="#FFF0F1"/>
|
||||
<path
|
||||
android:pathData="M28.227,26.227m-9.227,0a9.227,9.227 0,1 1,18.455 0a9.227,9.227 0,1 1,-18.455 0"
|
||||
android:strokeWidth="2"
|
||||
android:fillColor="#00000000"
|
||||
android:strokeColor="#E63946"/>
|
||||
<path
|
||||
android:pathData="M28.227,26.227m-4.318,0a4.318,4.318 0,1 1,8.636 0a4.318,4.318 0,1 1,-8.636 0"
|
||||
android:strokeWidth="2"
|
||||
android:fillColor="#00000000"
|
||||
android:strokeColor="#E63946"/>
|
||||
<path
|
||||
android:pathData="M22.091,40.545C24.039,38.693 29.221,36.1 34.364,40.545"
|
||||
android:strokeWidth="2"
|
||||
android:fillColor="#00000000"
|
||||
android:strokeColor="#E63946"
|
||||
android:strokeLineCap="round"/>
|
||||
</vector>
|
||||
18
app/src/main/res/drawable/icon_white_back.xml
Normal file
18
app/src/main/res/drawable/icon_white_back.xml
Normal file
@ -0,0 +1,18 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="19dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="19">
|
||||
<path
|
||||
android:pathData="M10,1L2,9.5L10,18"
|
||||
android:strokeWidth="2"
|
||||
android:fillColor="#00000000"
|
||||
android:strokeColor="#ffffff"
|
||||
android:strokeLineCap="round"/>
|
||||
<path
|
||||
android:pathData="M3,9H23"
|
||||
android:strokeWidth="2"
|
||||
android:fillColor="#00000000"
|
||||
android:strokeColor="#ffffff"
|
||||
android:strokeLineCap="round"/>
|
||||
</vector>
|
||||
7
app/src/main/res/drawable/label_recent.xml
Normal file
7
app/src/main/res/drawable/label_recent.xml
Normal file
@ -0,0 +1,7 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:shape="rectangle">
|
||||
<corners android:radius="12dp"/>
|
||||
<solid android:color="@color/tab_selected_color"/>
|
||||
|
||||
</shape>
|
||||
9
app/src/main/res/drawable/name_background.xml
Normal file
9
app/src/main/res/drawable/name_background.xml
Normal 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">
|
||||
<gradient
|
||||
android:angle="-90"
|
||||
android:endColor="@color/color_61"
|
||||
android:startColor="@color/color_16" />
|
||||
|
||||
</shape>
|
||||
21
app/src/main/res/drawable/pb_splash.xml
Normal file
21
app/src/main/res/drawable/pb_splash.xml
Normal file
@ -0,0 +1,21 @@
|
||||
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item android:id="@android:id/background">
|
||||
<shape>
|
||||
<corners android:radius="5dp" />
|
||||
<solid android:color="@color/color_737373" /> <!-- 背景颜色 -->
|
||||
</shape>
|
||||
</item>
|
||||
|
||||
<item android:id="@android:id/progress">
|
||||
<clip>
|
||||
<shape>
|
||||
<corners android:radius="20dp" />
|
||||
<gradient
|
||||
android:startColor="@color/test_yellow"
|
||||
android:centerColor="@color/test_yellow"
|
||||
android:endColor="@color/test_yellow"
|
||||
android:angle="0" />
|
||||
</shape>
|
||||
</clip>
|
||||
</item>
|
||||
</layer-list>
|
||||
10
app/src/main/res/drawable/play_message_background.xml
Normal file
10
app/src/main/res/drawable/play_message_background.xml
Normal file
@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:shape="rectangle">
|
||||
<gradient
|
||||
android:angle="90"
|
||||
android:centerColor="@color/color_61"
|
||||
android:endColor="@color/color_CC000000"
|
||||
android:startColor="@color/color_transparent" />
|
||||
|
||||
</shape>
|
||||
10
app/src/main/res/drawable/play_message_background_bottom.xml
Normal file
10
app/src/main/res/drawable/play_message_background_bottom.xml
Normal file
@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:shape="rectangle">
|
||||
<gradient
|
||||
android:angle="-90"
|
||||
android:centerColor="@color/color_61"
|
||||
android:endColor="@color/color_CC000000"
|
||||
android:startColor="@color/color_transparent" />
|
||||
|
||||
</shape>
|
||||
30
app/src/main/res/drawable/progress.xml
Normal file
30
app/src/main/res/drawable/progress.xml
Normal file
@ -0,0 +1,30 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<!-- 背景 -->
|
||||
<item>
|
||||
<shape android:shape="rectangle">
|
||||
<solid android:color="@android:color/transparent" />
|
||||
</shape>
|
||||
</item>
|
||||
<!-- 左侧透明边框 -->
|
||||
<item android:left="-2dp" android:right="2dp"> <!-- 注意这里的偏移量 -->
|
||||
<shape android:shape="rectangle">
|
||||
<solid android:color="@android:color/transparent" />
|
||||
<stroke android:width="2dp" android:color="#FF0000" android:dashGap="0dp" android:dashWidth="0dp"/>
|
||||
</shape>
|
||||
</item>
|
||||
<!-- 右侧透明边框 -->
|
||||
<item android:left="2dp" android:right="-2dp"> <!-- 注意这里的偏移量 -->
|
||||
<shape android:shape="rectangle">
|
||||
<solid android:color="@android:color/transparent" />
|
||||
<stroke android:width="2dp" android:color="#FF0000" android:dashGap="0dp" android:dashWidth="0dp"/>
|
||||
</shape>
|
||||
</item>
|
||||
<!-- 顶部和底部实色边框 -->
|
||||
<item>
|
||||
<shape android:shape="rectangle">
|
||||
<solid android:color="@android:color/transparent" /> <!-- 背景透明 -->
|
||||
<stroke android:width="2dp" android:color="#FF0000"/> <!-- 边框颜色 -->
|
||||
</shape>
|
||||
</item>
|
||||
</layer-list>
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user