commit 481452e6886ac76831c7f908d38adb746febce1d Author: litingting Date: Mon Jun 16 11:27:43 2025 +0800 init diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..aa724b7 --- /dev/null +++ b/.gitignore @@ -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 diff --git a/app/.gitignore b/app/.gitignore new file mode 100644 index 0000000..42afabf --- /dev/null +++ b/app/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/app/build.gradle.kts b/app/build.gradle.kts new file mode 100644 index 0000000..ab92d13 --- /dev/null +++ b/app/build.gradle.kts @@ -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")) + +} \ No newline at end of file diff --git a/app/libs/jetified-ffmpeg-kit-full-6.0.aar b/app/libs/jetified-ffmpeg-kit-full-6.0.aar new file mode 100644 index 0000000..1f0fada Binary files /dev/null and b/app/libs/jetified-ffmpeg-kit-full-6.0.aar differ diff --git a/app/libs/smart-exception-common-0.2.1.jar b/app/libs/smart-exception-common-0.2.1.jar new file mode 100644 index 0000000..624d64d Binary files /dev/null and b/app/libs/smart-exception-common-0.2.1.jar differ diff --git a/app/libs/smart-exception-java-0.2.1.jar b/app/libs/smart-exception-java-0.2.1.jar new file mode 100644 index 0000000..2a16b31 Binary files /dev/null and b/app/libs/smart-exception-java-0.2.1.jar differ diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro new file mode 100644 index 0000000..481bb43 --- /dev/null +++ b/app/proguard-rules.pro @@ -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 \ No newline at end of file diff --git a/app/src/androidTest/java/com/audio/record/screen/test/ExampleInstrumentedTest.kt b/app/src/androidTest/java/com/audio/record/screen/test/ExampleInstrumentedTest.kt new file mode 100644 index 0000000..9fcc025 --- /dev/null +++ b/app/src/androidTest/java/com/audio/record/screen/test/ExampleInstrumentedTest.kt @@ -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) + } +} \ No newline at end of file diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000..702c638 --- /dev/null +++ b/app/src/main/AndroidManifest.xml @@ -0,0 +1,79 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/java/com/audio/record/screen/test/App.kt b/app/src/main/java/com/audio/record/screen/test/App.kt new file mode 100644 index 0000000..b806ed2 --- /dev/null +++ b/app/src/main/java/com/audio/record/screen/test/App.kt @@ -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 + } +} \ No newline at end of file diff --git a/app/src/main/java/com/audio/record/screen/test/activity/ImageViewActivity.kt b/app/src/main/java/com/audio/record/screen/test/activity/ImageViewActivity.kt new file mode 100644 index 0000000..cc63ea6 --- /dev/null +++ b/app/src/main/java/com/audio/record/screen/test/activity/ImageViewActivity.kt @@ -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() { + + 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 + } + } + + } +} \ No newline at end of file diff --git a/app/src/main/java/com/audio/record/screen/test/activity/MainActivity.kt b/app/src/main/java/com/audio/record/screen/test/activity/MainActivity.kt new file mode 100644 index 0000000..384d69f --- /dev/null +++ b/app/src/main/java/com/audio/record/screen/test/activity/MainActivity.kt @@ -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() { + val NOTIFICATION_PERMISSION_REQUEST_CODE = 123 + val SCREEN_CAPTURE_REQUEST_CODE = 124 + val REQUEST_SCREENSHOT = 125 + + lateinit var requestPermissionLauncher: ActivityResultLauncher + lateinit var micPermissionLauncher: ActivityResultLauncher + lateinit var cameraPermissionLauncher: ActivityResultLauncher + lateinit var requestStoragePermission: ActivityResultLauncher + + 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() + + } + + + + + +} \ No newline at end of file diff --git a/app/src/main/java/com/audio/record/screen/test/activity/MainActivity1.kt b/app/src/main/java/com/audio/record/screen/test/activity/MainActivity1.kt new file mode 100644 index 0000000..9f4607d --- /dev/null +++ b/app/src/main/java/com/audio/record/screen/test/activity/MainActivity1.kt @@ -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(), ConnectionListener,FloatingCallback { + private lateinit var viewModel: MainViewModel + private var isNotification = false + private var isOverlay = false + private lateinit var requestStoragePermission: ActivityResultLauncher + private lateinit var requestNotificationLauncher: ActivityResultLauncher + private var mPermissionDialog: DialogPermission? = null + + //截屏 + private lateinit var screenCaptureLauncher: ActivityResultLauncher + 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(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(R.id.tab_icon).isSelected = true + } + } + + override fun onTabUnselected(tab: TabLayout.Tab?) { + tab?.customView?.run { + findViewById(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) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/audio/record/screen/test/activity/PlayActivity.kt b/app/src/main/java/com/audio/record/screen/test/activity/PlayActivity.kt new file mode 100644 index 0000000..9abe374 --- /dev/null +++ b/app/src/main/java/com/audio/record/screen/test/activity/PlayActivity.kt @@ -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() { + + 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(R.id.exo_controller) + controller.findViewById(R.id.back).setOnClickListener { + finish() + } + controller.findViewById(R.id.text_name).text = displayName + btnPlay = controller.findViewById(R.id.exo_play_pause) + val layoutProgress = controller.findViewById(R.id.layout_progress) + timeBar = controller.findViewById(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 + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/audio/record/screen/test/activity/PreviewActivity.kt b/app/src/main/java/com/audio/record/screen/test/activity/PreviewActivity.kt new file mode 100644 index 0000000..61c6ebf --- /dev/null +++ b/app/src/main/java/com/audio/record/screen/test/activity/PreviewActivity.kt @@ -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(), View.OnClickListener { + + private lateinit var exoPlayer: ExoPlayer + + //将当前需要处理的原视频复制到内部存储,方便操作 + private lateinit var copyFile: File + + private var copyResult by Delegates.notNull() + 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?) { + + } + + +} \ No newline at end of file diff --git a/app/src/main/java/com/audio/record/screen/test/adapter/CropAdapter.kt b/app/src/main/java/com/audio/record/screen/test/adapter/CropAdapter.kt new file mode 100644 index 0000000..11acf70 --- /dev/null +++ b/app/src/main/java/com/audio/record/screen/test/adapter/CropAdapter.kt @@ -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(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 = holder as VHolder + + val item = data[position] + with(itemHolder.vb.btn) { + text = item + setOnClickListener { + click.invoke(item) + } + } + + + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/audio/record/screen/test/adapter/ImageGroupAdapter.kt b/app/src/main/java/com/audio/record/screen/test/adapter/ImageGroupAdapter.kt new file mode 100644 index 0000000..7e85423 --- /dev/null +++ b/app/src/main/java/com/audio/record/screen/test/adapter/ImageGroupAdapter.kt @@ -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(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 = + holder as VHolder + + 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)) + } + } + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/audio/record/screen/test/adapter/ImageInfoAdapter.kt b/app/src/main/java/com/audio/record/screen/test/adapter/ImageInfoAdapter.kt new file mode 100644 index 0000000..ed260ca --- /dev/null +++ b/app/src/main/java/com/audio/record/screen/test/adapter/ImageInfoAdapter.kt @@ -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(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 = holder as VHolder + 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) + }) + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/audio/record/screen/test/adapter/ThumbAdapter.kt b/app/src/main/java/com/audio/record/screen/test/adapter/ThumbAdapter.kt new file mode 100644 index 0000000..eb5d385 --- /dev/null +++ b/app/src/main/java/com/audio/record/screen/test/adapter/ThumbAdapter.kt @@ -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(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 = holder as VHolder + + + Glide.with(mContext!!).load(data[position]).into(itemHolder.vb.imThumb) + + + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/audio/record/screen/test/adapter/ToolAdapter.kt b/app/src/main/java/com/audio/record/screen/test/adapter/ToolAdapter.kt new file mode 100644 index 0000000..f739e76 --- /dev/null +++ b/app/src/main/java/com/audio/record/screen/test/adapter/ToolAdapter.kt @@ -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(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 + val item = data[position] + with(itemHolder.vb.btn) { + text = item + setOnClickListener { + click.invoke(itemHolder.bindingAdapterPosition) + } + } + + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/audio/record/screen/test/adapter/VideoGroupAdapter.kt b/app/src/main/java/com/audio/record/screen/test/adapter/VideoGroupAdapter.kt new file mode 100644 index 0000000..2c9abb1 --- /dev/null +++ b/app/src/main/java/com/audio/record/screen/test/adapter/VideoGroupAdapter.kt @@ -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(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 = holder as VHolder + + 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 + } + } + + + + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/audio/record/screen/test/adapter/VideoInfoAdapter.kt b/app/src/main/java/com/audio/record/screen/test/adapter/VideoInfoAdapter.kt new file mode 100644 index 0000000..e25a6c9 --- /dev/null +++ b/app/src/main/java/com/audio/record/screen/test/adapter/VideoInfoAdapter.kt @@ -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(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 = + holder as VHolder + 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) + }) + } + } + + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/audio/record/screen/test/adapter/ViewPager2Adapter.kt b/app/src/main/java/com/audio/record/screen/test/adapter/ViewPager2Adapter.kt new file mode 100644 index 0000000..3984667 --- /dev/null +++ b/app/src/main/java/com/audio/record/screen/test/adapter/ViewPager2Adapter.kt @@ -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 = arrayListOf( + MainFragment.newInstance(), + RecorderFragment.newInstance(), + MainFragment.newInstance() + ) + override fun getItemCount(): Int = fragments.size + + override fun createFragment(position: Int): Fragment = fragments[position] +} \ No newline at end of file diff --git a/app/src/main/java/com/audio/record/screen/test/adapter/VpFragmentAdapter.kt b/app/src/main/java/com/audio/record/screen/test/adapter/VpFragmentAdapter.kt new file mode 100644 index 0000000..d6c51cb --- /dev/null +++ b/app/src/main/java/com/audio/record/screen/test/adapter/VpFragmentAdapter.kt @@ -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 = arrayListOf( + VideoFragment.newInstance(), + ImageFragment.newInstance() + ) + + override fun getItemCount(): Int = fragments.size + + override fun createFragment(position: Int): Fragment = fragments[position] +} \ No newline at end of file diff --git a/app/src/main/java/com/audio/record/screen/test/base/BaseActivity.kt b/app/src/main/java/com/audio/record/screen/test/base/BaseActivity.kt new file mode 100644 index 0000000..b129363 --- /dev/null +++ b/app/src/main/java/com/audio/record/screen/test/base/BaseActivity.kt @@ -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 : 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() + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/audio/record/screen/test/base/BaseAdapter.kt b/app/src/main/java/com/audio/record/screen/test/base/BaseAdapter.kt new file mode 100644 index 0000000..085163a --- /dev/null +++ b/app/src/main/java/com/audio/record/screen/test/base/BaseAdapter.kt @@ -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 : RecyclerView.Adapter { + protected var data: MutableList = ArrayList() + protected lateinit var mContext: Context + var isLoadingAdded = false + protected set + + constructor() + constructor(mContext: Context) { + this.mContext = mContext + } + + fun addData(data: List?) { + this.data.addAll(data!!) + notifyDataSetChanged() + } + + fun updateData(data: List?) { + 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(val vb: V) : RecyclerView.ViewHolder( + vb!!.root + ) + + companion object { + protected const val TYPE_ITEM = 0 + protected const val TYPE_FOOTER = 1 + } +} \ No newline at end of file diff --git a/app/src/main/java/com/audio/record/screen/test/base/BaseFragment.kt b/app/src/main/java/com/audio/record/screen/test/base/BaseFragment.kt new file mode 100644 index 0000000..fe963f7 --- /dev/null +++ b/app/src/main/java/com/audio/record/screen/test/base/BaseFragment.kt @@ -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 : 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 + } +} \ No newline at end of file diff --git a/app/src/main/java/com/audio/record/screen/test/data/ImageGroup.kt b/app/src/main/java/com/audio/record/screen/test/data/ImageGroup.kt new file mode 100644 index 0000000..e5188b1 --- /dev/null +++ b/app/src/main/java/com/audio/record/screen/test/data/ImageGroup.kt @@ -0,0 +1,6 @@ +package com.audio.record.screen.test.data + +data class ImageGroup ( + val date: String, // 格式:yyyy-MM-dd + val images: List +) \ No newline at end of file diff --git a/app/src/main/java/com/audio/record/screen/test/data/ImageInfo.kt b/app/src/main/java/com/audio/record/screen/test/data/ImageInfo.kt new file mode 100644 index 0000000..e235a77 --- /dev/null +++ b/app/src/main/java/com/audio/record/screen/test/data/ImageInfo.kt @@ -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? +) diff --git a/app/src/main/java/com/audio/record/screen/test/data/VideoGroup.kt b/app/src/main/java/com/audio/record/screen/test/data/VideoGroup.kt new file mode 100644 index 0000000..ac145ed --- /dev/null +++ b/app/src/main/java/com/audio/record/screen/test/data/VideoGroup.kt @@ -0,0 +1,8 @@ +package com.audio.record.screen.test.data + + +data class VideoGroup( + val date: String, + val videos: List +) + diff --git a/app/src/main/java/com/audio/record/screen/test/data/VideoInfo.kt b/app/src/main/java/com/audio/record/screen/test/data/VideoInfo.kt new file mode 100644 index 0000000..fe5933e --- /dev/null +++ b/app/src/main/java/com/audio/record/screen/test/data/VideoInfo.kt @@ -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 // 可选封面图 +) diff --git a/app/src/main/java/com/audio/record/screen/test/dialog/DialogAudio.kt b/app/src/main/java/com/audio/record/screen/test/dialog/DialogAudio.kt new file mode 100644 index 0000000..276cbf1 --- /dev/null +++ b/app/src/main/java/com/audio/record/screen/test/dialog/DialogAudio.kt @@ -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 + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/audio/record/screen/test/dialog/DialogPermission.kt b/app/src/main/java/com/audio/record/screen/test/dialog/DialogPermission.kt new file mode 100644 index 0000000..d5992d2 --- /dev/null +++ b/app/src/main/java/com/audio/record/screen/test/dialog/DialogPermission.kt @@ -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 + } +} \ No newline at end of file diff --git a/app/src/main/java/com/audio/record/screen/test/fragment/CropFragment.kt b/app/src/main/java/com/audio/record/screen/test/fragment/CropFragment.kt new file mode 100644 index 0000000..43bb318 --- /dev/null +++ b/app/src/main/java/com/audio/record/screen/test/fragment/CropFragment.kt @@ -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(), 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() + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/audio/record/screen/test/fragment/CutFragment.kt b/app/src/main/java/com/audio/record/screen/test/fragment/CutFragment.kt new file mode 100644 index 0000000..6287b85 --- /dev/null +++ b/app/src/main/java/com/audio/record/screen/test/fragment/CutFragment.kt @@ -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() ,View.OnClickListener{ + //毫秒单位 + private var leftV by Delegates.notNull() + private lateinit var viewModel: PreviewViewModel + private var rightV by Delegates.notNull() + 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) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/audio/record/screen/test/fragment/MainFragment.kt b/app/src/main/java/com/audio/record/screen/test/fragment/MainFragment.kt new file mode 100644 index 0000000..34d6f98 --- /dev/null +++ b/app/src/main/java/com/audio/record/screen/test/fragment/MainFragment.kt @@ -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() { + + 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(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(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(R.id.tab_tv).run { + isSelected = false + background = null + } + } + } + + override fun onTabReselected(tab: TabLayout.Tab?) { + + } + + }) + + } + + } +} \ No newline at end of file diff --git a/app/src/main/java/com/audio/record/screen/test/fragment/RecorderFragment.kt b/app/src/main/java/com/audio/record/screen/test/fragment/RecorderFragment.kt new file mode 100644 index 0000000..365308d --- /dev/null +++ b/app/src/main/java/com/audio/record/screen/test/fragment/RecorderFragment.kt @@ -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() { + 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){ + + } + + } + } + + +} \ No newline at end of file diff --git a/app/src/main/java/com/audio/record/screen/test/fragment/SpeedFragment.kt b/app/src/main/java/com/audio/record/screen/test/fragment/SpeedFragment.kt new file mode 100644 index 0000000..1584a9c --- /dev/null +++ b/app/src/main/java/com/audio/record/screen/test/fragment/SpeedFragment.kt @@ -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(), 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() + } + } + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/audio/record/screen/test/fragment/StartFragment.kt b/app/src/main/java/com/audio/record/screen/test/fragment/StartFragment.kt new file mode 100644 index 0000000..d67e915 --- /dev/null +++ b/app/src/main/java/com/audio/record/screen/test/fragment/StartFragment.kt @@ -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() { + + 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) + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/audio/record/screen/test/fragment/VolumeFragment.kt b/app/src/main/java/com/audio/record/screen/test/fragment/VolumeFragment.kt new file mode 100644 index 0000000..818a040 --- /dev/null +++ b/app/src/main/java/com/audio/record/screen/test/fragment/VolumeFragment.kt @@ -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(), 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() + } + } + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/audio/record/screen/test/fragment/child/ImageFragment.kt b/app/src/main/java/com/audio/record/screen/test/fragment/child/ImageFragment.kt new file mode 100644 index 0000000..421140f --- /dev/null +++ b/app/src/main/java/com/audio/record/screen/test/fragment/child/ImageFragment.kt @@ -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() { + + 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()) + } + + + } + + +} \ No newline at end of file diff --git a/app/src/main/java/com/audio/record/screen/test/fragment/child/RecordNormalFragment.kt b/app/src/main/java/com/audio/record/screen/test/fragment/child/RecordNormalFragment.kt new file mode 100644 index 0000000..0222f9a --- /dev/null +++ b/app/src/main/java/com/audio/record/screen/test/fragment/child/RecordNormalFragment.kt @@ -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(), 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 + + //截屏 + private lateinit var screenCaptureLauncher: ActivityResultLauncher + + //录制 + private lateinit var recorderLauncher: ActivityResultLauncher + + //录音权限 + private lateinit var micPermissionLauncher: ActivityResultLauncher + + 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) + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/audio/record/screen/test/fragment/child/RecordingFragment.kt b/app/src/main/java/com/audio/record/screen/test/fragment/child/RecordingFragment.kt new file mode 100644 index 0000000..e81971c --- /dev/null +++ b/app/src/main/java/com/audio/record/screen/test/fragment/child/RecordingFragment.kt @@ -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(), 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 + } +} \ No newline at end of file diff --git a/app/src/main/java/com/audio/record/screen/test/fragment/child/VideoFragment.kt b/app/src/main/java/com/audio/record/screen/test/fragment/child/VideoFragment.kt new file mode 100644 index 0000000..b22d7a3 --- /dev/null +++ b/app/src/main/java/com/audio/record/screen/test/fragment/child/VideoFragment.kt @@ -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() { + 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() + 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): MutableList { + 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) + }) + } + } + + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/audio/record/screen/test/service/ConnectionListener.kt b/app/src/main/java/com/audio/record/screen/test/service/ConnectionListener.kt new file mode 100644 index 0000000..dfb1355 --- /dev/null +++ b/app/src/main/java/com/audio/record/screen/test/service/ConnectionListener.kt @@ -0,0 +1,6 @@ +package com.audio.record.screen.test.service + +interface ConnectionListener { + fun onServiceConnected() + +} \ No newline at end of file diff --git a/app/src/main/java/com/audio/record/screen/test/service/CustomLifecycleOwner.java b/app/src/main/java/com/audio/record/screen/test/service/CustomLifecycleOwner.java new file mode 100644 index 0000000..23ec5ff --- /dev/null +++ b/app/src/main/java/com/audio/record/screen/test/service/CustomLifecycleOwner.java @@ -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); + } +} \ No newline at end of file diff --git a/app/src/main/java/com/audio/record/screen/test/service/FloatingCallback.kt b/app/src/main/java/com/audio/record/screen/test/service/FloatingCallback.kt new file mode 100644 index 0000000..c3fe21b --- /dev/null +++ b/app/src/main/java/com/audio/record/screen/test/service/FloatingCallback.kt @@ -0,0 +1,10 @@ +package com.audio.record.screen.test.service + +interface FloatingCallback { + //开始录制 + fun onStartRecording(){} + //更新录制时间 + fun onUpdateRecordTime(time:String){} + + fun onStopRecord(){} +} diff --git a/app/src/main/java/com/audio/record/screen/test/service/FloatingWindowBridge.kt b/app/src/main/java/com/audio/record/screen/test/service/FloatingWindowBridge.kt new file mode 100644 index 0000000..4271e07 --- /dev/null +++ b/app/src/main/java/com/audio/record/screen/test/service/FloatingWindowBridge.kt @@ -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>() + + 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? = 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 + } + } +} diff --git a/app/src/main/java/com/audio/record/screen/test/service/ScreenRecordService.kt b/app/src/main/java/com/audio/record/screen/test/service/ScreenRecordService.kt new file mode 100644 index 0000000..bb1f53a --- /dev/null +++ b/app/src/main/java/com/audio/record/screen/test/service/ScreenRecordService.kt @@ -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 + + + //截屏 + private lateinit var screenCaptureLauncher: ActivityResultLauncher + 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>() + + + //录制时间监听相关 + 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(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(R.id.close).setOnClickListener { + hideRecordView() + } + recordView!!.findViewById(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(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) { + 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(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, + 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 + ) + + } + + +} \ No newline at end of file diff --git a/app/src/main/java/com/audio/record/screen/test/service/TouchThroughLayout.kt b/app/src/main/java/com/audio/record/screen/test/service/TouchThroughLayout.kt new file mode 100644 index 0000000..048baca --- /dev/null +++ b/app/src/main/java/com/audio/record/screen/test/service/TouchThroughLayout.kt @@ -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 +} + diff --git a/app/src/main/java/com/audio/record/screen/test/tool/Common.kt b/app/src/main/java/com/audio/record/screen/test/tool/Common.kt new file mode 100644 index 0000000..3fc57ce --- /dev/null +++ b/app/src/main/java/com/audio/record/screen/test/tool/Common.kt @@ -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 { + val screenWidth = context.resources.displayMetrics.widthPixels + val screenHeight = context.resources.displayMetrics.heightPixels + + return Pair(screenWidth, screenHeight) + } + + /** + * 在service中获取设备屏幕的实际像素尺寸(包含状态栏、导航栏) + */ + fun getFullScreenSize(context: Context): Pair { + 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 { + 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 { + val imagePaths = mutableListOf() + 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 { + 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 { + 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 + } + + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/audio/record/screen/test/tool/DraggableViewHelper.kt b/app/src/main/java/com/audio/record/screen/test/tool/DraggableViewHelper.kt new file mode 100644 index 0000000..b47eead --- /dev/null +++ b/app/src/main/java/com/audio/record/screen/test/tool/DraggableViewHelper.kt @@ -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 + } + } + } + + +} diff --git a/app/src/main/java/com/audio/record/screen/test/tool/Extend.kt b/app/src/main/java/com/audio/record/screen/test/tool/Extend.kt new file mode 100644 index 0000000..7ef813c --- /dev/null +++ b/app/src/main/java/com/audio/record/screen/test/tool/Extend.kt @@ -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() + +} \ No newline at end of file diff --git a/app/src/main/java/com/audio/record/screen/test/tool/FFmpegKitTool.kt b/app/src/main/java/com/audio/record/screen/test/tool/FFmpegKitTool.kt new file mode 100644 index 0000000..d8bfb10 --- /dev/null +++ b/app/src/main/java/com/audio/record/screen/test/tool/FFmpegKitTool.kt @@ -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}") + } + } + + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/audio/record/screen/test/tool/NoScrollLinearLayoutManager.kt b/app/src/main/java/com/audio/record/screen/test/tool/NoScrollLinearLayoutManager.kt new file mode 100644 index 0000000..6cf7a94 --- /dev/null +++ b/app/src/main/java/com/audio/record/screen/test/tool/NoScrollLinearLayoutManager.kt @@ -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 +} diff --git a/app/src/main/java/com/audio/record/screen/test/tool/Permission.kt b/app/src/main/java/com/audio/record/screen/test/tool/Permission.kt new file mode 100644 index 0000000..45d2580 --- /dev/null +++ b/app/src/main/java/com/audio/record/screen/test/tool/Permission.kt @@ -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") + + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/audio/record/screen/test/tool/ScreenCaptureHelper.kt b/app/src/main/java/com/audio/record/screen/test/tool/ScreenCaptureHelper.kt new file mode 100644 index 0000000..0381c1a --- /dev/null +++ b/app/src/main/java/com/audio/record/screen/test/tool/ScreenCaptureHelper.kt @@ -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(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("-----点击小截图") + } + + } + + +} diff --git a/app/src/main/java/com/audio/record/screen/test/tool/VideoFileHelper.kt b/app/src/main/java/com/audio/record/screen/test/tool/VideoFileHelper.kt new file mode 100644 index 0000000..12671f5 --- /dev/null +++ b/app/src/main/java/com/audio/record/screen/test/tool/VideoFileHelper.kt @@ -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 { + 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 { + Common.showLog("------folderName=${folderName}") + val videoInfoList = mutableListOf() + 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 { + Common.showLog("-----图片-folderName=${folderName}") + val imageInfoList = mutableListOf() + 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 { + 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): Map> { + val sdf = SimpleDateFormat("yyyy-MM-dd", Locale.getDefault()) + + return videoList.groupBy { video -> + val timestampMillis = video.dateAdded * 1000 // 秒转毫秒 + sdf.format(Date(timestampMillis)) // 格式化成日期字符串 + } + } + +} diff --git a/app/src/main/java/com/audio/record/screen/test/view/AspectRatioCropView.java b/app/src/main/java/com/audio/record/screen/test/view/AspectRatioCropView.java new file mode 100644 index 0000000..3af99d6 --- /dev/null +++ b/app/src/main/java/com/audio/record/screen/test/view/AspectRatioCropView.java @@ -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); + } + +} diff --git a/app/src/main/java/com/audio/record/screen/test/view/CountDownFloatingManager.kt b/app/src/main/java/com/audio/record/screen/test/view/CountDownFloatingManager.kt new file mode 100644 index 0000000..881c534 --- /dev/null +++ b/app/src/main/java/com/audio/record/screen/test/view/CountDownFloatingManager.kt @@ -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(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 + } + + +} \ No newline at end of file diff --git a/app/src/main/java/com/audio/record/screen/test/view/GridSpacingItemDecoration.kt b/app/src/main/java/com/audio/record/screen/test/view/GridSpacingItemDecoration.kt new file mode 100644 index 0000000..5443be4 --- /dev/null +++ b/app/src/main/java/com/audio/record/screen/test/view/GridSpacingItemDecoration.kt @@ -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 + } + } + } +} diff --git a/app/src/main/java/com/audio/record/screen/test/view/RangeSliderView.kt b/app/src/main/java/com/audio/record/screen/test/view/RangeSliderView.kt new file mode 100644 index 0000000..cce86b1 --- /dev/null +++ b/app/src/main/java/com/audio/record/screen/test/view/RangeSliderView.kt @@ -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 + } +} diff --git a/app/src/main/java/com/audio/record/screen/test/viewmodel/MainViewModel.kt b/app/src/main/java/com/audio/record/screen/test/viewmodel/MainViewModel.kt new file mode 100644 index 0000000..60866a7 --- /dev/null +++ b/app/src/main/java/com/audio/record/screen/test/viewmodel/MainViewModel.kt @@ -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() + val serviceConnectStatus: LiveData get() = _serviceConnectStatus + + + //前置摄像头状态 + private val _webcamStatus = MutableLiveData() + val webcamStatus: LiveData get() = _webcamStatus + + + //截屏状态 + private val _screenshotStatus = MutableLiveData() + val screenshotStatus: LiveData get() = _screenshotStatus + + + //悬浮球状态 + private val _ballStatus = MutableLiveData() + val ballStatus: LiveData 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 + } +} \ No newline at end of file diff --git a/app/src/main/java/com/audio/record/screen/test/viewmodel/PreviewViewModel.kt b/app/src/main/java/com/audio/record/screen/test/viewmodel/PreviewViewModel.kt new file mode 100644 index 0000000..ff392a6 --- /dev/null +++ b/app/src/main/java/com/audio/record/screen/test/viewmodel/PreviewViewModel.kt @@ -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() + val cropRatioText: LiveData get() = _cropRatioText + + + //复制当前文件到内部存储的结果 + private val _copySuccess = MutableLiveData>() + val copySuccess: LiveData> get() = _copySuccess + + + //保存裁剪文件 + private val _saveCrop = MutableLiveData() + val saveCrop: LiveData get() = _saveCrop + + + //更新当前播放速度 + private val _changeSpeed = MutableLiveData() + val changeSpeed: LiveData get() = _changeSpeed + + + fun updateCropText(message: String) { + _cropRatioText.value = message + } + + fun updateCopyResult(message: Pair) { + _copySuccess.value = message + } + + fun updateClickCropSave(message: Boolean) { + _saveCrop.value = message + } + + fun updateSpeed(message: Float) { + _changeSpeed.value = message + } +} \ No newline at end of file diff --git a/app/src/main/res/color/selector_tab_child_text_color.xml b/app/src/main/res/color/selector_tab_child_text_color.xml new file mode 100644 index 0000000..b74ecb0 --- /dev/null +++ b/app/src/main/res/color/selector_tab_child_text_color.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/bg_empty.png b/app/src/main/res/drawable/bg_empty.png new file mode 100644 index 0000000..ca0ea4b Binary files /dev/null and b/app/src/main/res/drawable/bg_empty.png differ diff --git a/app/src/main/res/drawable/bg_recording.png b/app/src/main/res/drawable/bg_recording.png new file mode 100644 index 0000000..d115e72 Binary files /dev/null and b/app/src/main/res/drawable/bg_recording.png differ diff --git a/app/src/main/res/drawable/btn_off.xml b/app/src/main/res/drawable/btn_off.xml new file mode 100644 index 0000000..b03d970 --- /dev/null +++ b/app/src/main/res/drawable/btn_off.xml @@ -0,0 +1,7 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/btn_on.xml b/app/src/main/res/drawable/btn_on.xml new file mode 100644 index 0000000..60cd7cc --- /dev/null +++ b/app/src/main/res/drawable/btn_on.xml @@ -0,0 +1,7 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/btn_state.xml b/app/src/main/res/drawable/btn_state.xml new file mode 100644 index 0000000..9769895 --- /dev/null +++ b/app/src/main/res/drawable/btn_state.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/close.xml b/app/src/main/res/drawable/close.xml new file mode 100644 index 0000000..de548c9 --- /dev/null +++ b/app/src/main/res/drawable/close.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/dialog_audio_close.xml b/app/src/main/res/drawable/dialog_audio_close.xml new file mode 100644 index 0000000..b2223d4 --- /dev/null +++ b/app/src/main/res/drawable/dialog_audio_close.xml @@ -0,0 +1,14 @@ + + + + diff --git a/app/src/main/res/drawable/dialog_permission_background.xml b/app/src/main/res/drawable/dialog_permission_background.xml new file mode 100644 index 0000000..6c3bf00 --- /dev/null +++ b/app/src/main/res/drawable/dialog_permission_background.xml @@ -0,0 +1,7 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/dialog_permission_enable_bg.xml b/app/src/main/res/drawable/dialog_permission_enable_bg.xml new file mode 100644 index 0000000..b51559f --- /dev/null +++ b/app/src/main/res/drawable/dialog_permission_enable_bg.xml @@ -0,0 +1,7 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/dialog_permission_item_bg.xml b/app/src/main/res/drawable/dialog_permission_item_bg.xml new file mode 100644 index 0000000..55642c0 --- /dev/null +++ b/app/src/main/res/drawable/dialog_permission_item_bg.xml @@ -0,0 +1,7 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/dialog_with_audio.xml b/app/src/main/res/drawable/dialog_with_audio.xml new file mode 100644 index 0000000..78ca9b2 --- /dev/null +++ b/app/src/main/res/drawable/dialog_with_audio.xml @@ -0,0 +1,18 @@ + + + + + + diff --git a/app/src/main/res/drawable/dialog_without_audio.xml b/app/src/main/res/drawable/dialog_without_audio.xml new file mode 100644 index 0000000..16ea944 --- /dev/null +++ b/app/src/main/res/drawable/dialog_without_audio.xml @@ -0,0 +1,20 @@ + + + + + + + + + + diff --git a/app/src/main/res/drawable/global_ball.xml b/app/src/main/res/drawable/global_ball.xml new file mode 100644 index 0000000..0725211 --- /dev/null +++ b/app/src/main/res/drawable/global_ball.xml @@ -0,0 +1,17 @@ + + + + + diff --git a/app/src/main/res/drawable/global_screenshot.xml b/app/src/main/res/drawable/global_screenshot.xml new file mode 100644 index 0000000..190109a --- /dev/null +++ b/app/src/main/res/drawable/global_screenshot.xml @@ -0,0 +1,20 @@ + + + + + + diff --git a/app/src/main/res/drawable/ic_launcher_background.xml b/app/src/main/res/drawable/ic_launcher_background.xml new file mode 100644 index 0000000..07d5da9 --- /dev/null +++ b/app/src/main/res/drawable/ic_launcher_background.xml @@ -0,0 +1,170 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/ic_launcher_foreground.xml b/app/src/main/res/drawable/ic_launcher_foreground.xml new file mode 100644 index 0000000..2b068d1 --- /dev/null +++ b/app/src/main/res/drawable/ic_launcher_foreground.xml @@ -0,0 +1,30 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/icon_floatingball.xml b/app/src/main/res/drawable/icon_floatingball.xml new file mode 100644 index 0000000..878eeb3 --- /dev/null +++ b/app/src/main/res/drawable/icon_floatingball.xml @@ -0,0 +1,17 @@ + + + + + diff --git a/app/src/main/res/drawable/icon_floatingball_normal.xml b/app/src/main/res/drawable/icon_floatingball_normal.xml new file mode 100644 index 0000000..0e79da3 --- /dev/null +++ b/app/src/main/res/drawable/icon_floatingball_normal.xml @@ -0,0 +1,17 @@ + + + + + diff --git a/app/src/main/res/drawable/icon_main_recorder.xml b/app/src/main/res/drawable/icon_main_recorder.xml new file mode 100644 index 0000000..b35b9cb --- /dev/null +++ b/app/src/main/res/drawable/icon_main_recorder.xml @@ -0,0 +1,20 @@ + + + + + + diff --git a/app/src/main/res/drawable/icon_recording_pause.xml b/app/src/main/res/drawable/icon_recording_pause.xml new file mode 100644 index 0000000..91a5df6 --- /dev/null +++ b/app/src/main/res/drawable/icon_recording_pause.xml @@ -0,0 +1,12 @@ + + + + diff --git a/app/src/main/res/drawable/icon_recording_record.xml b/app/src/main/res/drawable/icon_recording_record.xml new file mode 100644 index 0000000..d837719 --- /dev/null +++ b/app/src/main/res/drawable/icon_recording_record.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/icon_recording_stop.xml b/app/src/main/res/drawable/icon_recording_stop.xml new file mode 100644 index 0000000..17b7604 --- /dev/null +++ b/app/src/main/res/drawable/icon_recording_stop.xml @@ -0,0 +1,12 @@ + + + + diff --git a/app/src/main/res/drawable/icon_screenshot.xml b/app/src/main/res/drawable/icon_screenshot.xml new file mode 100644 index 0000000..a85244c --- /dev/null +++ b/app/src/main/res/drawable/icon_screenshot.xml @@ -0,0 +1,27 @@ + + + + + + diff --git a/app/src/main/res/drawable/icon_screenshot_normal.xml b/app/src/main/res/drawable/icon_screenshot_normal.xml new file mode 100644 index 0000000..eca463d --- /dev/null +++ b/app/src/main/res/drawable/icon_screenshot_normal.xml @@ -0,0 +1,27 @@ + + + + + + diff --git a/app/src/main/res/drawable/icon_small_pause.xml b/app/src/main/res/drawable/icon_small_pause.xml new file mode 100644 index 0000000..af0e3d4 --- /dev/null +++ b/app/src/main/res/drawable/icon_small_pause.xml @@ -0,0 +1,18 @@ + + + + + diff --git a/app/src/main/res/drawable/icon_thumb_video_play.xml b/app/src/main/res/drawable/icon_thumb_video_play.xml new file mode 100644 index 0000000..068a735 --- /dev/null +++ b/app/src/main/res/drawable/icon_thumb_video_play.xml @@ -0,0 +1,15 @@ + + + + diff --git a/app/src/main/res/drawable/icon_webcam.xml b/app/src/main/res/drawable/icon_webcam.xml new file mode 100644 index 0000000..e6bcfca --- /dev/null +++ b/app/src/main/res/drawable/icon_webcam.xml @@ -0,0 +1,25 @@ + + + + + + diff --git a/app/src/main/res/drawable/icon_webcam_normal.xml b/app/src/main/res/drawable/icon_webcam_normal.xml new file mode 100644 index 0000000..8c42b2a --- /dev/null +++ b/app/src/main/res/drawable/icon_webcam_normal.xml @@ -0,0 +1,25 @@ + + + + + + diff --git a/app/src/main/res/drawable/icon_white_back.xml b/app/src/main/res/drawable/icon_white_back.xml new file mode 100644 index 0000000..ec6c394 --- /dev/null +++ b/app/src/main/res/drawable/icon_white_back.xml @@ -0,0 +1,18 @@ + + + + diff --git a/app/src/main/res/drawable/label_recent.xml b/app/src/main/res/drawable/label_recent.xml new file mode 100644 index 0000000..bdb8601 --- /dev/null +++ b/app/src/main/res/drawable/label_recent.xml @@ -0,0 +1,7 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/name_background.xml b/app/src/main/res/drawable/name_background.xml new file mode 100644 index 0000000..b5e89b7 --- /dev/null +++ b/app/src/main/res/drawable/name_background.xml @@ -0,0 +1,9 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/pb_splash.xml b/app/src/main/res/drawable/pb_splash.xml new file mode 100644 index 0000000..e10d068 --- /dev/null +++ b/app/src/main/res/drawable/pb_splash.xml @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/play_message_background.xml b/app/src/main/res/drawable/play_message_background.xml new file mode 100644 index 0000000..ffc2aa3 --- /dev/null +++ b/app/src/main/res/drawable/play_message_background.xml @@ -0,0 +1,10 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/play_message_background_bottom.xml b/app/src/main/res/drawable/play_message_background_bottom.xml new file mode 100644 index 0000000..9426cc6 --- /dev/null +++ b/app/src/main/res/drawable/play_message_background_bottom.xml @@ -0,0 +1,10 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/progress.xml b/app/src/main/res/drawable/progress.xml new file mode 100644 index 0000000..2c5576d --- /dev/null +++ b/app/src/main/res/drawable/progress.xml @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/progress_defalut.xml b/app/src/main/res/drawable/progress_defalut.xml new file mode 100644 index 0000000..d0683eb --- /dev/null +++ b/app/src/main/res/drawable/progress_defalut.xml @@ -0,0 +1,5 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/rang_seekbar_progress.xml b/app/src/main/res/drawable/rang_seekbar_progress.xml new file mode 100644 index 0000000..117665c --- /dev/null +++ b/app/src/main/res/drawable/rang_seekbar_progress.xml @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/app/src/main/res/drawable/rang_seekbar_thumb.xml b/app/src/main/res/drawable/rang_seekbar_thumb.xml new file mode 100644 index 0000000..3345648 --- /dev/null +++ b/app/src/main/res/drawable/rang_seekbar_thumb.xml @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/record_complete_background.xml b/app/src/main/res/drawable/record_complete_background.xml new file mode 100644 index 0000000..fc78e2d --- /dev/null +++ b/app/src/main/res/drawable/record_complete_background.xml @@ -0,0 +1,7 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/recording_ball.xml b/app/src/main/res/drawable/recording_ball.xml new file mode 100644 index 0000000..4e791e4 --- /dev/null +++ b/app/src/main/res/drawable/recording_ball.xml @@ -0,0 +1,21 @@ + + + + + diff --git a/app/src/main/res/drawable/recording_ball_selected.xml b/app/src/main/res/drawable/recording_ball_selected.xml new file mode 100644 index 0000000..e137d98 --- /dev/null +++ b/app/src/main/res/drawable/recording_ball_selected.xml @@ -0,0 +1,19 @@ + + + + + diff --git a/app/src/main/res/drawable/recording_screenshot.xml b/app/src/main/res/drawable/recording_screenshot.xml new file mode 100644 index 0000000..4668578 --- /dev/null +++ b/app/src/main/res/drawable/recording_screenshot.xml @@ -0,0 +1,29 @@ + + + + + + diff --git a/app/src/main/res/drawable/recording_screenshot_selected.xml b/app/src/main/res/drawable/recording_screenshot_selected.xml new file mode 100644 index 0000000..e234d06 --- /dev/null +++ b/app/src/main/res/drawable/recording_screenshot_selected.xml @@ -0,0 +1,27 @@ + + + + + + diff --git a/app/src/main/res/drawable/recording_webcam.xml b/app/src/main/res/drawable/recording_webcam.xml new file mode 100644 index 0000000..fd4a71c --- /dev/null +++ b/app/src/main/res/drawable/recording_webcam.xml @@ -0,0 +1,27 @@ + + + + + + diff --git a/app/src/main/res/drawable/recording_webcam_selected.xml b/app/src/main/res/drawable/recording_webcam_selected.xml new file mode 100644 index 0000000..6610085 --- /dev/null +++ b/app/src/main/res/drawable/recording_webcam_selected.xml @@ -0,0 +1,25 @@ + + + + + + diff --git a/app/src/main/res/drawable/save.xml b/app/src/main/res/drawable/save.xml new file mode 100644 index 0000000..a5cfb28 --- /dev/null +++ b/app/src/main/res/drawable/save.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/seekbar_background.xml b/app/src/main/res/drawable/seekbar_background.xml new file mode 100644 index 0000000..2c0b5c2 --- /dev/null +++ b/app/src/main/res/drawable/seekbar_background.xml @@ -0,0 +1,6 @@ + + + + + diff --git a/app/src/main/res/drawable/seekbar_progress.xml b/app/src/main/res/drawable/seekbar_progress.xml new file mode 100644 index 0000000..6874efc --- /dev/null +++ b/app/src/main/res/drawable/seekbar_progress.xml @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/selector_small_play.xml b/app/src/main/res/drawable/selector_small_play.xml new file mode 100644 index 0000000..6f8ea7d --- /dev/null +++ b/app/src/main/res/drawable/selector_small_play.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/small_pause.xml b/app/src/main/res/drawable/small_pause.xml new file mode 100644 index 0000000..c4673e8 --- /dev/null +++ b/app/src/main/res/drawable/small_pause.xml @@ -0,0 +1,22 @@ + + + + + + + diff --git a/app/src/main/res/drawable/small_play.xml b/app/src/main/res/drawable/small_play.xml new file mode 100644 index 0000000..8cfeec4 --- /dev/null +++ b/app/src/main/res/drawable/small_play.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/status_floatingball.xml b/app/src/main/res/drawable/status_floatingball.xml new file mode 100644 index 0000000..0b63c2e --- /dev/null +++ b/app/src/main/res/drawable/status_floatingball.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/status_recording_ball.xml b/app/src/main/res/drawable/status_recording_ball.xml new file mode 100644 index 0000000..565a039 --- /dev/null +++ b/app/src/main/res/drawable/status_recording_ball.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/status_recording_record_status.xml b/app/src/main/res/drawable/status_recording_record_status.xml new file mode 100644 index 0000000..c76a0e7 --- /dev/null +++ b/app/src/main/res/drawable/status_recording_record_status.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/status_recording_screenshot.xml b/app/src/main/res/drawable/status_recording_screenshot.xml new file mode 100644 index 0000000..9023975 --- /dev/null +++ b/app/src/main/res/drawable/status_recording_screenshot.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/status_recording_webcam.xml b/app/src/main/res/drawable/status_recording_webcam.xml new file mode 100644 index 0000000..4bd8852 --- /dev/null +++ b/app/src/main/res/drawable/status_recording_webcam.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/status_screenshot.xml b/app/src/main/res/drawable/status_screenshot.xml new file mode 100644 index 0000000..c534df5 --- /dev/null +++ b/app/src/main/res/drawable/status_screenshot.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/status_webcam.xml b/app/src/main/res/drawable/status_webcam.xml new file mode 100644 index 0000000..aa26b89 --- /dev/null +++ b/app/src/main/res/drawable/status_webcam.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/tab1_normal.xml b/app/src/main/res/drawable/tab1_normal.xml new file mode 100644 index 0000000..ecf503f --- /dev/null +++ b/app/src/main/res/drawable/tab1_normal.xml @@ -0,0 +1,13 @@ + + + + + + diff --git a/app/src/main/res/drawable/tab1_selected.xml b/app/src/main/res/drawable/tab1_selected.xml new file mode 100644 index 0000000..d932035 --- /dev/null +++ b/app/src/main/res/drawable/tab1_selected.xml @@ -0,0 +1,13 @@ + + + + + + diff --git a/app/src/main/res/drawable/tab1_selector.xml b/app/src/main/res/drawable/tab1_selector.xml new file mode 100644 index 0000000..6625d9e --- /dev/null +++ b/app/src/main/res/drawable/tab1_selector.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/tab2_normal.xml b/app/src/main/res/drawable/tab2_normal.xml new file mode 100644 index 0000000..9c82bb0 --- /dev/null +++ b/app/src/main/res/drawable/tab2_normal.xml @@ -0,0 +1,12 @@ + + + + diff --git a/app/src/main/res/drawable/tab2_selected.xml b/app/src/main/res/drawable/tab2_selected.xml new file mode 100644 index 0000000..f7cc42f --- /dev/null +++ b/app/src/main/res/drawable/tab2_selected.xml @@ -0,0 +1,12 @@ + + + + diff --git a/app/src/main/res/drawable/tab2_selector.xml b/app/src/main/res/drawable/tab2_selector.xml new file mode 100644 index 0000000..3c7ec5a --- /dev/null +++ b/app/src/main/res/drawable/tab2_selector.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/tab3_normal.xml b/app/src/main/res/drawable/tab3_normal.xml new file mode 100644 index 0000000..58a2f3d --- /dev/null +++ b/app/src/main/res/drawable/tab3_normal.xml @@ -0,0 +1,13 @@ + + + + + + diff --git a/app/src/main/res/drawable/tab3_selected.xml b/app/src/main/res/drawable/tab3_selected.xml new file mode 100644 index 0000000..6d7a3db --- /dev/null +++ b/app/src/main/res/drawable/tab3_selected.xml @@ -0,0 +1,12 @@ + + + + + + diff --git a/app/src/main/res/drawable/tab3_selector.xml b/app/src/main/res/drawable/tab3_selector.xml new file mode 100644 index 0000000..94e5fe2 --- /dev/null +++ b/app/src/main/res/drawable/tab3_selector.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/tab_child_background.xml b/app/src/main/res/drawable/tab_child_background.xml new file mode 100644 index 0000000..eb5da42 --- /dev/null +++ b/app/src/main/res/drawable/tab_child_background.xml @@ -0,0 +1,7 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/tab_child_selected.xml b/app/src/main/res/drawable/tab_child_selected.xml new file mode 100644 index 0000000..1bb8128 --- /dev/null +++ b/app/src/main/res/drawable/tab_child_selected.xml @@ -0,0 +1,7 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/test.png b/app/src/main/res/drawable/test.png new file mode 100644 index 0000000..f1dfb61 Binary files /dev/null and b/app/src/main/res/drawable/test.png differ diff --git a/app/src/main/res/drawable/thumb_handle.xml b/app/src/main/res/drawable/thumb_handle.xml new file mode 100644 index 0000000..bf771fc --- /dev/null +++ b/app/src/main/res/drawable/thumb_handle.xml @@ -0,0 +1,6 @@ + + + + + + diff --git a/app/src/main/res/drawable/thumb_yellow_stroke.xml b/app/src/main/res/drawable/thumb_yellow_stroke.xml new file mode 100644 index 0000000..72bf713 --- /dev/null +++ b/app/src/main/res/drawable/thumb_yellow_stroke.xml @@ -0,0 +1,8 @@ + + + + + + diff --git a/app/src/main/res/drawable/volume_progress_drawable.xml b/app/src/main/res/drawable/volume_progress_drawable.xml new file mode 100644 index 0000000..0de91c7 --- /dev/null +++ b/app/src/main/res/drawable/volume_progress_drawable.xml @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/volume_thumb.xml b/app/src/main/res/drawable/volume_thumb.xml new file mode 100644 index 0000000..9620285 --- /dev/null +++ b/app/src/main/res/drawable/volume_thumb.xml @@ -0,0 +1,8 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_image_view.xml b/app/src/main/res/layout/activity_image_view.xml new file mode 100644 index 0000000..020a84c --- /dev/null +++ b/app/src/main/res/layout/activity_image_view.xml @@ -0,0 +1,45 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml new file mode 100644 index 0000000..4b51c4f --- /dev/null +++ b/app/src/main/res/layout/activity_main.xml @@ -0,0 +1,104 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_main1.xml b/app/src/main/res/layout/activity_main1.xml new file mode 100644 index 0000000..fc50bfe --- /dev/null +++ b/app/src/main/res/layout/activity_main1.xml @@ -0,0 +1,33 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_play.xml b/app/src/main/res/layout/activity_play.xml new file mode 100644 index 0000000..68c0710 --- /dev/null +++ b/app/src/main/res/layout/activity_play.xml @@ -0,0 +1,20 @@ + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_preview.xml b/app/src/main/res/layout/activity_preview.xml new file mode 100644 index 0000000..2b34ba9 --- /dev/null +++ b/app/src/main/res/layout/activity_preview.xml @@ -0,0 +1,45 @@ + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/adapter_crop.xml b/app/src/main/res/layout/adapter_crop.xml new file mode 100644 index 0000000..f914547 --- /dev/null +++ b/app/src/main/res/layout/adapter_crop.xml @@ -0,0 +1,19 @@ + + + + +