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