first commit
23
.gitignore
vendored
Normal file
@ -0,0 +1,23 @@
|
||||
*.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
|
||||
.idea/.gitignore
|
||||
.idea/compiler.xml
|
||||
.idea/deploymentTargetDropDown.xml
|
||||
.idea/gradle.xml
|
||||
.idea/kotlinc.xml
|
||||
.idea/migrations.xml
|
||||
.idea/misc.xml
|
||||
.idea/vcs.xml
|
||||
1
app/.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
||||
/build
|
||||
68
app/build.gradle.kts
Normal file
@ -0,0 +1,68 @@
|
||||
plugins {
|
||||
id("com.android.application")
|
||||
id("org.jetbrains.kotlin.android")
|
||||
id("kotlin-kapt")
|
||||
}
|
||||
|
||||
android {
|
||||
namespace = "com.player.musicoo"
|
||||
compileSdk = 34
|
||||
|
||||
defaultConfig {
|
||||
applicationId = "com.player.musicoo"
|
||||
minSdk = 24
|
||||
targetSdk = 34
|
||||
versionCode = 1
|
||||
versionName = "1.0.1"
|
||||
|
||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||
|
||||
setProperty("archivesBaseName", "Musicoo_${defaultConfig.versionName}(${defaultConfig.versionCode})")
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
release {
|
||||
isMinifyEnabled = true
|
||||
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
|
||||
buildConfig = true
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
|
||||
implementation("androidx.core:core-ktx:1.12.0")
|
||||
implementation("androidx.appcompat:appcompat:1.6.1")
|
||||
implementation("com.google.android.material:material:1.11.0")
|
||||
implementation("androidx.constraintlayout:constraintlayout:2.1.4")
|
||||
implementation("androidx.media3:media3-session:1.3.1")
|
||||
testImplementation("junit:junit:4.13.2")
|
||||
androidTestImplementation("androidx.test.ext:junit:1.1.5")
|
||||
androidTestImplementation("androidx.test.espresso:espresso-core:3.5.1")
|
||||
implementation("com.github.bumptech.glide:glide:4.16.0")
|
||||
implementation("androidx.room:room-ktx:2.6.1")
|
||||
implementation("androidx.room:room-runtime:2.6.1")
|
||||
//noinspection KaptUsageInsteadOfKsp
|
||||
kapt("androidx.room:room-compiler:2.6.1")
|
||||
implementation("com.geyifeng.immersionbar:immersionbar:3.2.2")
|
||||
implementation("com.geyifeng.immersionbar:immersionbar-ktx:3.2.2")
|
||||
implementation("com.github.lihangleo2:ShadowLayout:3.4.0")
|
||||
|
||||
implementation("androidx.media3:media3-exoplayer:1.3.1")
|
||||
implementation("androidx.media3:media3-ui:1.3.1")
|
||||
implementation("androidx.media3:media3-common:1.3.1")
|
||||
}
|
||||
21
app/proguard-rules.pro
vendored
Normal file
@ -0,0 +1,21 @@
|
||||
# Add project specific ProGuard rules here.
|
||||
# You can control the set of applied configuration files using the
|
||||
# proguardFiles setting in build.gradle.
|
||||
#
|
||||
# For more details, see
|
||||
# http://developer.android.com/guide/developing/tools/proguard.html
|
||||
|
||||
# If your project uses WebView with JS, uncomment the following
|
||||
# and specify the fully qualified class name to the JavaScript interface
|
||||
# class:
|
||||
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
|
||||
# public *;
|
||||
#}
|
||||
|
||||
# Uncomment this to preserve the line number information for
|
||||
# debugging stack traces.
|
||||
#-keepattributes SourceFile,LineNumberTable
|
||||
|
||||
# If you keep the line number information, uncomment this to
|
||||
# hide the original source file name.
|
||||
#-renamesourcefileattribute SourceFile
|
||||
@ -0,0 +1,24 @@
|
||||
package com.player.musicoo
|
||||
|
||||
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.player.musicoo", appContext.packageName)
|
||||
}
|
||||
}
|
||||
62
app/src/main/AndroidManifest.xml
Normal file
@ -0,0 +1,62 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
tools:ignore="LockedOrientationActivity">
|
||||
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_MEDIA_PLAYBACK" />
|
||||
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
||||
<uses-permission
|
||||
android:name="android.permission.READ_EXTERNAL_STORAGE"
|
||||
android:maxSdkVersion="32" />
|
||||
|
||||
<application
|
||||
android:name=".App"
|
||||
android:allowBackup="true"
|
||||
android:dataExtractionRules="@xml/data_extraction_rules"
|
||||
android:fullBackupContent="@xml/backup_rules"
|
||||
android:icon="@mipmap/musicoo_logo_img"
|
||||
android:label="@string/app_name"
|
||||
android:roundIcon="@mipmap/musicoo_logo_img"
|
||||
android:supportsRtl="true"
|
||||
android:theme="@style/Theme.Musicoo"
|
||||
tools:targetApi="31">
|
||||
<activity
|
||||
android:name=".activity.LaunchActivity"
|
||||
android:exported="true"
|
||||
android:screenOrientation="portrait">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
<activity
|
||||
android:name=".activity.MainActivity"
|
||||
android:screenOrientation="portrait" />
|
||||
<activity
|
||||
android:name=".activity.PlayDetailsActivity"
|
||||
android:screenOrientation="portrait" />
|
||||
<activity
|
||||
android:name=".activity.SettingsActivity"
|
||||
android:screenOrientation="portrait" />
|
||||
<activity
|
||||
android:name=".activity.AboutActivity"
|
||||
android:screenOrientation="portrait" />
|
||||
|
||||
<service android:name=".service.AudioPlayerService" />
|
||||
|
||||
<service
|
||||
android:name=".service.PlaybackService"
|
||||
android:exported="true"
|
||||
android:foregroundServiceType="mediaPlayback">
|
||||
<intent-filter>
|
||||
<action android:name="androidx.media3.session.MediaSessionService" />
|
||||
<action android:name="android.media.browse.MediaBrowserService" />
|
||||
</intent-filter>
|
||||
</service>
|
||||
|
||||
</application>
|
||||
|
||||
</manifest>
|
||||
BIN
app/src/main/assets/Real human voice pic/Breathe.png
Normal file
|
After Width: | Height: | Size: 810 KiB |
BIN
app/src/main/assets/Real human voice pic/Shh Shh.png
Normal file
|
After Width: | Height: | Size: 728 KiB |
BIN
app/src/main/assets/Real human voice pic/Shhh….png
Normal file
|
After Width: | Height: | Size: 453 KiB |
BIN
app/src/main/assets/Real human voice/Breathe.mp3
Normal file
BIN
app/src/main/assets/Real human voice/Shh Shh.mp3
Normal file
BIN
app/src/main/assets/Real human voice/Shhh….mp3
Normal file
BIN
app/src/main/assets/Sounds of appliances pic/Fireplace.png
Normal file
|
After Width: | Height: | Size: 552 KiB |
BIN
app/src/main/assets/Sounds of appliances pic/Mountain stream.png
Normal file
|
After Width: | Height: | Size: 718 KiB |
BIN
app/src/main/assets/Sounds of appliances pic/TV.png
Normal file
|
After Width: | Height: | Size: 342 KiB |
BIN
app/src/main/assets/Sounds of appliances pic/Water droplet.png
Normal file
|
After Width: | Height: | Size: 322 KiB |
BIN
app/src/main/assets/Sounds of appliances/Fireplace.mp3
Normal file
BIN
app/src/main/assets/Sounds of appliances/Mountain stream.mp3
Normal file
BIN
app/src/main/assets/Sounds of appliances/TV.mp3
Normal file
BIN
app/src/main/assets/Sounds of appliances/Water droplet.mp3
Normal file
BIN
app/src/main/assets/Sounds of nature pic/Beach.png
Normal file
|
After Width: | Height: | Size: 829 KiB |
BIN
app/src/main/assets/Sounds of nature pic/Call of Seagulls.png
Normal file
|
After Width: | Height: | Size: 515 KiB |
BIN
app/src/main/assets/Sounds of nature pic/Chirping of Birds.png
Normal file
|
After Width: | Height: | Size: 750 KiB |
BIN
app/src/main/assets/Sounds of nature pic/Cicada Chirping.png
Normal file
|
After Width: | Height: | Size: 450 KiB |
BIN
app/src/main/assets/Sounds of nature pic/Howling Wind.png
Normal file
|
After Width: | Height: | Size: 860 KiB |
BIN
app/src/main/assets/Sounds of nature pic/Nocturnal Insects.png
Normal file
|
After Width: | Height: | Size: 563 KiB |
BIN
app/src/main/assets/Sounds of nature pic/Seawater Surging.png
Normal file
|
After Width: | Height: | Size: 525 KiB |
BIN
app/src/main/assets/Sounds of nature pic/Summer Insects.png
Normal file
|
After Width: | Height: | Size: 379 KiB |
BIN
app/src/main/assets/Sounds of nature pic/waterfall.png
Normal file
|
After Width: | Height: | Size: 594 KiB |
BIN
app/src/main/assets/Sounds of nature/Beach.mp3
Normal file
BIN
app/src/main/assets/Sounds of nature/Call of Seagulls.mp3
Normal file
BIN
app/src/main/assets/Sounds of nature/Chirping of Birds.mp3
Normal file
BIN
app/src/main/assets/Sounds of nature/Cicada Chirping.mp3
Normal file
BIN
app/src/main/assets/Sounds of nature/Howling Wind.mp3
Normal file
BIN
app/src/main/assets/Sounds of nature/Nocturnal Insects.mp3
Normal file
BIN
app/src/main/assets/Sounds of nature/Seawater Surging.mp3
Normal file
BIN
app/src/main/assets/Sounds of nature/Summer Insects.mp3
Normal file
BIN
app/src/main/assets/Sounds of nature/waterfall.mp3
Normal file
99
app/src/main/assets/resources.json
Normal file
@ -0,0 +1,99 @@
|
||||
{
|
||||
"categories": [
|
||||
{
|
||||
"name": "Real human voice",
|
||||
"audios": [
|
||||
{
|
||||
"name": "Breathe",
|
||||
"file": "Real human voice/Breathe.mp3",
|
||||
"image": "Real human voice pic/Breathe.png"
|
||||
},
|
||||
{
|
||||
"name": "Shh Shh",
|
||||
"file": "Real human voice/Shh Shh.mp3",
|
||||
"image": "Real human voice pic/Shh Shh.png"
|
||||
},
|
||||
{
|
||||
"name": "Shhh...",
|
||||
"file": "Real human voice/Shhh….mp3",
|
||||
"image": "Real human voice pic/Shhh….png"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Sounds of appliances",
|
||||
"audios": [
|
||||
{
|
||||
"name": "Fireplace",
|
||||
"file": "Sounds of appliances/Fireplace.mp3",
|
||||
"image": "Sounds of appliances pic/Fireplace.png"
|
||||
},
|
||||
{
|
||||
"name": "Mountain stream",
|
||||
"file": "Sounds of appliances/Mountain stream.mp3",
|
||||
"image": "Sounds of appliances pic/Mountain stream.png"
|
||||
},
|
||||
{
|
||||
"name": "TV",
|
||||
"file": "Sounds of appliances/TV.mp3",
|
||||
"image": "Sounds of appliances pic/TV.png"
|
||||
},
|
||||
{
|
||||
"name": "Water droplet",
|
||||
"file": "Sounds of appliances/Water droplet.mp3",
|
||||
"image": "Sounds of appliances pic/Water droplet.png"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Sounds of nature",
|
||||
"audios": [
|
||||
{
|
||||
"name": "Beach",
|
||||
"file": "Sounds of nature/Beach.mp3",
|
||||
"image": "Sounds of nature pic/Beach.png"
|
||||
},
|
||||
{
|
||||
"name": "Call of Seagulls",
|
||||
"file": "Sounds of nature/Call of Seagulls.mp3",
|
||||
"image": "Sounds of nature pic/Call of Seagulls.png"
|
||||
},
|
||||
{
|
||||
"name": "Chirping of Birds",
|
||||
"file": "Sounds of nature/Chirping of Birds.mp3",
|
||||
"image": "Sounds of nature pic/Chirping of Birds.png"
|
||||
},
|
||||
{
|
||||
"name": "Cicada Chirping",
|
||||
"file": "Sounds of nature/Cicada Chirping.mp3",
|
||||
"image": "Sounds of nature pic/Cicada Chirping.png"
|
||||
},
|
||||
{
|
||||
"name": "Howling Wind",
|
||||
"file": "Sounds of nature/Howling Wind.mp3",
|
||||
"image": "Sounds of nature pic/Howling Wind.png"
|
||||
},
|
||||
{
|
||||
"name": "Nocturnal Insects",
|
||||
"file": "Sounds of nature/Nocturnal Insects.mp3",
|
||||
"image": "Sounds of nature pic/Nocturnal Insects.png"
|
||||
},
|
||||
{
|
||||
"name": "Seawater Surging",
|
||||
"file": "Sounds of nature/Seawater Surging.mp3",
|
||||
"image": "Sounds of nature pic/Seawater Surging.png"
|
||||
},
|
||||
{
|
||||
"name": "Summer Insects",
|
||||
"file": "Sounds of nature/Summer Insects.mp3",
|
||||
"image": "Sounds of nature pic/Summer Insects.png"
|
||||
},
|
||||
{
|
||||
"name": "waterfall",
|
||||
"file": "Sounds of nature/waterfall.mp3",
|
||||
"image": "Sounds of nature pic/waterfall.png"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
111
app/src/main/java/com/player/musicoo/App.kt
Normal file
@ -0,0 +1,111 @@
|
||||
package com.player.musicoo
|
||||
|
||||
import android.app.Application
|
||||
import android.content.Context
|
||||
import android.util.Log
|
||||
import com.player.musicoo.bean.Audio
|
||||
import com.player.musicoo.bean.CurrentPlayingAudio
|
||||
import com.player.musicoo.bean.ResourcesList
|
||||
import com.player.musicoo.database.AppDatabase
|
||||
import com.player.musicoo.database.CurrentAudioDatabase
|
||||
import com.player.musicoo.database.CurrentAudioManager
|
||||
import com.player.musicoo.database.DatabaseManager
|
||||
import com.player.musicoo.media.MediaControllerManager
|
||||
import com.player.musicoo.util.parseResources
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import java.io.BufferedReader
|
||||
import java.io.InputStreamReader
|
||||
|
||||
class App : Application() {
|
||||
companion object {
|
||||
lateinit var app: App
|
||||
private set
|
||||
lateinit var currentAudioManager: CurrentAudioManager
|
||||
private set
|
||||
lateinit var databaseManager: DatabaseManager
|
||||
private set
|
||||
var currentPlayingAudio: CurrentPlayingAudio? = null
|
||||
private set
|
||||
lateinit var importList: List<Audio>
|
||||
private set
|
||||
lateinit var resourcesList: ResourcesList
|
||||
private set
|
||||
|
||||
lateinit var realHumanVoiceList: List<Audio>
|
||||
private set
|
||||
|
||||
lateinit var soundsOfAppliancesList: List<Audio>
|
||||
private set
|
||||
|
||||
lateinit var soundsOfNatureList: List<Audio>
|
||||
private set
|
||||
|
||||
private var isInitialized = false
|
||||
|
||||
fun initialize(context: Context) {
|
||||
if (!isInitialized) {
|
||||
val jsonString = readAssetsFile(context)
|
||||
resourcesList = parseResources(context, jsonString)
|
||||
splitResourcesList()
|
||||
isInitialized = true
|
||||
}
|
||||
}
|
||||
|
||||
fun initCurrentPlayingAudio() {
|
||||
CoroutineScope(Dispatchers.IO).launch {
|
||||
currentPlayingAudio = currentAudioManager.getCurrentPlayingAudio()
|
||||
}
|
||||
}
|
||||
|
||||
fun initImportAudio(callback: (List<Audio>) -> Unit = {}) {
|
||||
CoroutineScope(Dispatchers.IO).launch {
|
||||
importList = databaseManager.getAllAudioFiles()
|
||||
withContext(Dispatchers.Main) {
|
||||
Log.d("ocean", "initImportAudio importList->${importList.size}")
|
||||
callback(importList)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun readAssetsFile(context: Context): String {
|
||||
val assetManager = context.assets
|
||||
val inputStream = assetManager.open("resources.json")
|
||||
val bufferedReader = BufferedReader(InputStreamReader(inputStream))
|
||||
val stringBuilder = StringBuilder()
|
||||
var line: String?
|
||||
while (bufferedReader.readLine().also { line = it } != null) {
|
||||
stringBuilder.append(line)
|
||||
}
|
||||
return stringBuilder.toString()
|
||||
}
|
||||
|
||||
private fun splitResourcesList() {
|
||||
realHumanVoiceList = mutableListOf()
|
||||
soundsOfAppliancesList = mutableListOf()
|
||||
soundsOfNatureList = mutableListOf()
|
||||
|
||||
for (category in resourcesList.categories) {
|
||||
when (category.name) {
|
||||
"Real human voice" -> realHumanVoiceList = category.audios
|
||||
"Sounds of appliances" -> soundsOfAppliancesList = category.audios
|
||||
"Sounds of nature" -> soundsOfNatureList = category.audios
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
app = this
|
||||
initialize(this)
|
||||
MediaControllerManager.init(this)
|
||||
currentAudioManager = CurrentAudioManager.getInstance(this)
|
||||
databaseManager = DatabaseManager.getInstance(this)
|
||||
initCurrentPlayingAudio()
|
||||
initImportAudio()
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,26 @@
|
||||
package com.player.musicoo.activity
|
||||
|
||||
import android.os.Bundle
|
||||
import com.gyf.immersionbar.ktx.immersionBar
|
||||
import com.player.musicoo.databinding.ActivityAboutBinding
|
||||
import com.player.musicoo.util.getAppVersion
|
||||
|
||||
class AboutActivity : BaseActivity() {
|
||||
|
||||
private lateinit var binding: ActivityAboutBinding
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
binding = ActivityAboutBinding.inflate(layoutInflater)
|
||||
setContentView(binding.root)
|
||||
immersionBar {
|
||||
statusBarDarkFont(false)
|
||||
statusBarView(binding.view)
|
||||
}
|
||||
binding.versionTv.text = "Version: " + getAppVersion(this)
|
||||
binding.backBtn.setOnClickListener {
|
||||
finish()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@ -0,0 +1,18 @@
|
||||
package com.player.musicoo.activity
|
||||
|
||||
import android.os.Bundle
|
||||
import android.util.Log
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import com.player.musicoo.media.MediaControllerManager
|
||||
|
||||
open class BaseActivity : AppCompatActivity() {
|
||||
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
}
|
||||
|
||||
override fun onStart() {
|
||||
super.onStart()
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,47 @@
|
||||
package com.player.musicoo.activity
|
||||
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import android.os.CountDownTimer
|
||||
import com.gyf.immersionbar.ktx.immersionBar
|
||||
import com.player.musicoo.databinding.ActivityLaunchBinding
|
||||
|
||||
class LaunchActivity : BaseActivity() {
|
||||
private lateinit var binding: ActivityLaunchBinding
|
||||
private val totalTime = 3000 // 5秒
|
||||
private val interval = 50 // 更新间隔,毫秒
|
||||
private val steps = totalTime / interval
|
||||
private val progressPerStep = 100 / steps
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
binding = ActivityLaunchBinding.inflate(layoutInflater)
|
||||
setContentView(binding.root)
|
||||
initTimer()
|
||||
|
||||
immersionBar {
|
||||
fullScreen(true)
|
||||
statusBarDarkFont(false)
|
||||
}
|
||||
}
|
||||
|
||||
private fun initTimer() {
|
||||
val progressBar = binding.customProgressBar
|
||||
val timer = object : CountDownTimer(totalTime.toLong(), interval.toLong()) {
|
||||
override fun onTick(millisUntilFinished: Long) {
|
||||
progressBar.setProgress(progressBar.getProgress() + progressPerStep)
|
||||
}
|
||||
|
||||
override fun onFinish() {
|
||||
progressBar.setProgress(100)
|
||||
toMainActivity()
|
||||
}
|
||||
}
|
||||
|
||||
timer.start()
|
||||
}
|
||||
|
||||
private fun toMainActivity() {
|
||||
startActivity(Intent(this, MainActivity::class.java))
|
||||
finish()
|
||||
}
|
||||
}
|
||||
256
app/src/main/java/com/player/musicoo/activity/MainActivity.kt
Normal file
@ -0,0 +1,256 @@
|
||||
package com.player.musicoo.activity
|
||||
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import android.os.Message
|
||||
import android.util.Log
|
||||
import android.view.View
|
||||
import android.widget.Toast
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.fragment.app.FragmentTransaction
|
||||
import androidx.media3.common.Player
|
||||
import com.bumptech.glide.Glide
|
||||
import com.gyf.immersionbar.ktx.immersionBar
|
||||
import com.player.musicoo.App
|
||||
import com.player.musicoo.R
|
||||
import com.player.musicoo.bean.Audio
|
||||
import com.player.musicoo.databinding.ActivityMainBinding
|
||||
import com.player.musicoo.fragment.HomeFragment
|
||||
import com.player.musicoo.fragment.ImportFragment
|
||||
import com.player.musicoo.media.MediaControllerManager
|
||||
import com.player.musicoo.util.convertMillisToMinutesAndSecondsString
|
||||
import com.player.musicoo.util.getAudioDurationFromAssets
|
||||
|
||||
|
||||
class MainActivity : BaseActivity() {
|
||||
private lateinit var binding: ActivityMainBinding
|
||||
private val mFragments: MutableList<Fragment> = ArrayList()
|
||||
private var currentIndex: Int = 0
|
||||
private var mCurrentFragment: Fragment? = null
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
binding = ActivityMainBinding.inflate(layoutInflater)
|
||||
setContentView(binding.root)
|
||||
// initImmersionBar()
|
||||
initView()
|
||||
}
|
||||
|
||||
private fun initImmersionBar() {
|
||||
immersionBar {
|
||||
statusBarDarkFont(false)
|
||||
statusBarView(binding.view)
|
||||
}
|
||||
}
|
||||
|
||||
private fun initView() {
|
||||
initClick()
|
||||
initFragment()
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
val currentPlayer = MediaControllerManager.getController()
|
||||
|
||||
if (App.currentPlayingAudio == null) {
|
||||
binding.playingStatusLayout.visibility = View.GONE
|
||||
} else {
|
||||
binding.playingStatusLayout.visibility = View.VISIBLE
|
||||
val currentAudio = App.currentPlayingAudio
|
||||
|
||||
Log.d("ocean","main currentAudio->$currentAudio")
|
||||
val maxProgress = try {
|
||||
getAudioDurationFromAssets(this, currentAudio?.file!!)
|
||||
} catch (e: Exception) {
|
||||
currentAudio?.duration
|
||||
}
|
||||
if (maxProgress != null) {
|
||||
binding.progressBar.setMaxProgress(maxProgress)
|
||||
}
|
||||
if (currentAudio?.image?.isNotEmpty() == true) {
|
||||
Glide.with(this)
|
||||
.load("file:///android_asset/${currentAudio?.image}")
|
||||
.into(binding.audioImg)
|
||||
} else {
|
||||
binding.audioImg.setImageResource(R.mipmap.musicoo_logo_img)
|
||||
}
|
||||
|
||||
binding.name.text = currentAudio?.name
|
||||
binding.desc.text = currentAudio?.name
|
||||
}
|
||||
|
||||
if (currentPlayer != null && currentPlayer.playbackState == Player.STATE_READY) {
|
||||
val isPlaying = currentPlayer.isPlaying
|
||||
updatePlayState(isPlaying)
|
||||
if (isPlaying) {
|
||||
updateProgressState()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun initClick() {
|
||||
binding.homeBtn.setOnClickListener {
|
||||
changeFragment(0)
|
||||
updateBtnState(0)
|
||||
}
|
||||
binding.importBtn.setOnClickListener {
|
||||
changeFragment(1)
|
||||
updateBtnState(1)
|
||||
}
|
||||
binding.playingStatusLayout.setOnClickListener {
|
||||
val currentAudio = App.currentPlayingAudio
|
||||
val duration = try {
|
||||
getAudioDurationFromAssets(
|
||||
this, currentAudio?.file!!
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
currentAudio?.duration!!
|
||||
}
|
||||
|
||||
val audio = Audio(
|
||||
currentAudio?.name!!,
|
||||
currentAudio.file,
|
||||
currentAudio.image,
|
||||
duration,
|
||||
false
|
||||
)
|
||||
val intent = Intent(this, PlayDetailsActivity::class.java);
|
||||
intent.putExtra(PlayDetailsActivity.KEY_DETAILS_AUDIO, audio)
|
||||
startActivity(intent)
|
||||
}
|
||||
binding.alarmClockBtn.setOnClickListener {
|
||||
|
||||
}
|
||||
|
||||
binding.playBlackBtn.setOnClickListener {
|
||||
val currentPlayer = MediaControllerManager.getController()
|
||||
if (currentPlayer != null) {
|
||||
Log.d("ocean", "currentPlayer.playbackState->${currentPlayer.playbackState}")
|
||||
if (currentPlayer.playbackState == Player.STATE_READY) {
|
||||
if (currentPlayer.isPlaying) {
|
||||
currentPlayer.pause()
|
||||
updatePlayState(false)
|
||||
} else {
|
||||
currentPlayer.play()
|
||||
updatePlayState(true)
|
||||
}
|
||||
updateProgressState()
|
||||
} else {
|
||||
MediaControllerManager.setupMedia(this@MainActivity, App.currentPlayingAudio!!,
|
||||
object : Player.Listener {
|
||||
override fun onPlayWhenReadyChanged(
|
||||
playWhenReady: Boolean,
|
||||
reason: Int
|
||||
) {
|
||||
Log.d("ocean", "main2 onPlayWhenReadyChanged")
|
||||
updatePlayState(playWhenReady)
|
||||
updateProgressState()
|
||||
}
|
||||
|
||||
override fun onPlaybackStateChanged(playbackState: Int) {
|
||||
Log.d("ocean", "main2 onPlaybackStateChanged")
|
||||
updateProgressState()
|
||||
}
|
||||
})
|
||||
}
|
||||
} else {
|
||||
Log.d("ocean", "main currentPlayer == null")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun updatePlayState(b: Boolean) {
|
||||
if (b) {
|
||||
binding.playStatusImg.setImageResource(R.drawable.playing_black_icon)
|
||||
} else {
|
||||
binding.playStatusImg.setImageResource(R.drawable.play_black_icon)
|
||||
}
|
||||
}
|
||||
|
||||
private fun initFragment() {
|
||||
mFragments.clear()
|
||||
mFragments.add(HomeFragment())
|
||||
mFragments.add(ImportFragment())
|
||||
changeFragment(0)
|
||||
updateBtnState(0)
|
||||
}
|
||||
|
||||
private fun changeFragment(index: Int) {
|
||||
currentIndex = index
|
||||
val ft: FragmentTransaction = supportFragmentManager.beginTransaction()
|
||||
if (null != mCurrentFragment) {
|
||||
ft.hide(mCurrentFragment!!)
|
||||
}
|
||||
var fragment = supportFragmentManager.findFragmentByTag(
|
||||
mFragments[currentIndex].javaClass.name
|
||||
)
|
||||
if (null == fragment) {
|
||||
fragment = mFragments[index]
|
||||
}
|
||||
mCurrentFragment = fragment
|
||||
|
||||
if (!fragment.isAdded) {
|
||||
ft.add(R.id.frame_layout, fragment, fragment.javaClass.name)
|
||||
} else {
|
||||
ft.show(fragment)
|
||||
}
|
||||
ft.commit()
|
||||
}
|
||||
|
||||
private fun updateBtnState(index: Int) {
|
||||
binding.apply {
|
||||
homeImg.setImageResource(
|
||||
when (index) {
|
||||
0 -> R.drawable.home_select_icon
|
||||
else -> R.drawable.home_unselect_icon
|
||||
}
|
||||
)
|
||||
importImg.setImageResource(
|
||||
when (index) {
|
||||
1 -> R.drawable.import_select_icon
|
||||
else -> R.drawable.import_unselect_icon
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateProgressState() {
|
||||
val currentPlayer = MediaControllerManager.getController()
|
||||
if (currentPlayer != null && currentPlayer.playbackState == Player.STATE_READY && currentPlayer.isPlaying) {
|
||||
progressHandler.removeCallbacksAndMessages(null)
|
||||
updatePlayState(currentPlayer.isPlaying)
|
||||
progressHandler.sendEmptyMessage(1)
|
||||
} else {
|
||||
progressHandler.removeCallbacksAndMessages(null)
|
||||
}
|
||||
}
|
||||
|
||||
private val progressHandler = object : Handler(Looper.myLooper()!!) {
|
||||
override fun handleMessage(msg: Message) {
|
||||
val currentPlayer = MediaControllerManager.getController()
|
||||
if (currentPlayer != null && currentPlayer.playbackState == Player.STATE_READY && currentPlayer.isPlaying) {
|
||||
val currentPosition = currentPlayer.currentPosition
|
||||
binding.progressBar.setProgress(currentPosition)
|
||||
sendEmptyMessageDelayed(1, 1000)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private var backPressedTime: Long = 0
|
||||
private val backToast: Toast by lazy {
|
||||
Toast.makeText(baseContext, "Press again to exit", Toast.LENGTH_SHORT)
|
||||
}
|
||||
|
||||
override fun onBackPressed() {
|
||||
if (backPressedTime + 2000 > System.currentTimeMillis()) {
|
||||
super.onBackPressed()
|
||||
backToast.cancel()
|
||||
return
|
||||
} else {
|
||||
backToast.show()
|
||||
}
|
||||
backPressedTime = System.currentTimeMillis()
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,267 @@
|
||||
package com.player.musicoo.activity
|
||||
|
||||
import android.animation.ValueAnimator
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Context
|
||||
import android.graphics.Bitmap
|
||||
import android.graphics.BitmapFactory
|
||||
import android.os.Bundle
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import android.os.Message
|
||||
import android.renderscript.Allocation
|
||||
import android.renderscript.Element
|
||||
import android.renderscript.RenderScript
|
||||
import android.renderscript.ScriptIntrinsicBlur
|
||||
import android.util.Log
|
||||
import android.widget.Toast
|
||||
import androidx.annotation.OptIn
|
||||
import androidx.media3.common.Player
|
||||
import androidx.media3.common.util.UnstableApi
|
||||
import com.bumptech.glide.Glide
|
||||
import com.google.android.material.slider.Slider.OnChangeListener
|
||||
import com.gyf.immersionbar.ktx.immersionBar
|
||||
import com.player.musicoo.R
|
||||
import com.player.musicoo.bean.Audio
|
||||
import com.player.musicoo.databinding.ActivityPlayDetailsBinding
|
||||
import com.player.musicoo.media.MediaControllerManager
|
||||
import com.player.musicoo.util.containsContent
|
||||
import com.player.musicoo.util.convertMillisToMinutesAndSecondsString
|
||||
import com.player.musicoo.util.getAudioDurationFromAssets
|
||||
import java.io.IOException
|
||||
import java.io.InputStream
|
||||
|
||||
class PlayDetailsActivity : BaseActivity() {
|
||||
|
||||
companion object {
|
||||
const val KEY_DETAILS_AUDIO = "key_details_audio"
|
||||
}
|
||||
|
||||
private lateinit var binding: ActivityPlayDetailsBinding
|
||||
private var audio: Audio? = null
|
||||
private var rotationAnimator: ValueAnimator? = null
|
||||
|
||||
@SuppressLint("ForegroundServiceType")
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
binding = ActivityPlayDetailsBinding.inflate(layoutInflater)
|
||||
setContentView(binding.root)
|
||||
initImmersionBar()
|
||||
audio = intent.getSerializableExtra(KEY_DETAILS_AUDIO) as Audio?
|
||||
if (audio == null) {
|
||||
onBackPressed()
|
||||
Toast.makeText(this, getString(R.string.data_error), Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
Log.d("ocean", "PlayDetailsActivity audio->$audio")
|
||||
initView()
|
||||
|
||||
}
|
||||
|
||||
private fun initImmersionBar() {
|
||||
immersionBar {
|
||||
statusBarDarkFont(false)
|
||||
statusBarView(binding.view)
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(UnstableApi::class)
|
||||
private fun initView() {
|
||||
|
||||
if (audio?.image?.isNotEmpty() == true) {
|
||||
Glide.with(this)
|
||||
.load("file:///android_asset/${audio?.image}")
|
||||
.into(binding.image)
|
||||
val bitmap = loadBitmapFromAsset(this, audio?.image!!)
|
||||
val blurredBitmap = applyGaussianBlur(bitmap, 25f, this)
|
||||
binding.imageView.setImageBitmap(blurredBitmap)
|
||||
|
||||
} else {
|
||||
binding.image.setImageResource(R.mipmap.musicoo_logo_img)
|
||||
val bitmap = loadBitmapFromAsset(R.mipmap.musicoo_logo_img)
|
||||
val blurredBitmap = applyGaussianBlur(bitmap, 25f, this)
|
||||
binding.imageView.setImageBitmap(blurredBitmap)
|
||||
}
|
||||
binding.seekBar.value = 0f
|
||||
binding.title.text = ""
|
||||
binding.nameTv.text = audio?.name
|
||||
binding.descTv.text = audio?.name
|
||||
if (containsContent(audio?.file!!)) {
|
||||
binding.totalDurationTv.text = convertMillisToMinutesAndSecondsString(audio?.duration!!)
|
||||
binding.seekBar.valueTo = audio?.duration!!.toFloat()
|
||||
} else {
|
||||
val s = getAudioDurationFromAssets(this, audio?.file!!)
|
||||
binding.totalDurationTv.text = convertMillisToMinutesAndSecondsString(s)
|
||||
|
||||
binding.seekBar.valueTo = s.toFloat()
|
||||
}
|
||||
|
||||
binding.backBtn.setOnClickListener {
|
||||
onBackPressed()
|
||||
}
|
||||
val currentPlayer = MediaControllerManager.getController()
|
||||
currentPlayer?.addListener(object : Player.Listener {
|
||||
override fun onPlayWhenReadyChanged(
|
||||
playWhenReady: Boolean,
|
||||
reason: Int
|
||||
) {
|
||||
Log.d("ocean", "details onPlayWhenReadyChanged")
|
||||
updatePlayState(playWhenReady, "playWhenReady")
|
||||
updateProgressState()
|
||||
}
|
||||
|
||||
override fun onPlaybackStateChanged(playbackState: Int) {
|
||||
Log.d("ocean", "details onPlaybackStateChanged")
|
||||
updateProgressState()
|
||||
}
|
||||
})
|
||||
binding.playImg.setOnClickListener {
|
||||
if (currentPlayer != null) {
|
||||
if (currentPlayer.isPlaying) {
|
||||
currentPlayer.pause()
|
||||
updatePlayState(false, "click")
|
||||
} else {
|
||||
currentPlayer.play()
|
||||
updatePlayState(true, "click")
|
||||
}
|
||||
updateProgressState()
|
||||
}
|
||||
}
|
||||
MediaControllerManager.setupMedia(this,
|
||||
audio!!,
|
||||
object : Player.Listener {
|
||||
override fun onPlayWhenReadyChanged(
|
||||
playWhenReady: Boolean,
|
||||
reason: Int
|
||||
) {
|
||||
updatePlayState(playWhenReady, "playWhenReady")
|
||||
updateProgressState()
|
||||
}
|
||||
|
||||
override fun onPlaybackStateChanged(playbackState: Int) {
|
||||
updateProgressState()
|
||||
}
|
||||
})
|
||||
|
||||
binding.seekBar.addOnChangeListener(OnChangeListener { slider, value, fromUser ->
|
||||
if (fromUser) {
|
||||
if (currentPlayer != null) {
|
||||
currentPlayer.seekTo(value.toLong())
|
||||
val ss = currentPlayer.isPlaying
|
||||
if (!ss) {
|
||||
currentPlayer.play()
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
val currentPlayer = MediaControllerManager.getController()
|
||||
if (currentPlayer != null && currentPlayer.playbackState == Player.STATE_READY) {
|
||||
val isPlaying = currentPlayer.isPlaying
|
||||
updatePlayState(isPlaying, "onResume")
|
||||
if (isPlaying) {
|
||||
updateProgressState()
|
||||
} else {
|
||||
val currentPosition = currentPlayer.currentPosition
|
||||
val currentString = convertMillisToMinutesAndSecondsString(currentPosition)
|
||||
binding.progressDurationTv.text = currentString
|
||||
binding.seekBar.value = currentPosition.toFloat()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private fun updatePlayState(b: Boolean, string: String) {
|
||||
if (b) {
|
||||
binding.playImg.setImageResource(R.drawable.playing_green_icon)
|
||||
} else {
|
||||
binding.playImg.setImageResource(R.drawable.play_green_icon)
|
||||
}
|
||||
}
|
||||
|
||||
private fun loadBitmapFromAsset(context: Context, filePath: String): Bitmap {
|
||||
return try {
|
||||
val inputStream = context.assets.open(filePath)
|
||||
BitmapFactory.decodeStream(inputStream)
|
||||
} catch (e: IOException) {
|
||||
e.printStackTrace()
|
||||
throw RuntimeException("Could not load bitmap from asset")
|
||||
}
|
||||
}
|
||||
|
||||
private fun loadBitmapFromAsset(id: Int): Bitmap {
|
||||
return try {
|
||||
val inputStream: InputStream = resources.openRawResource(id)
|
||||
BitmapFactory.decodeStream(inputStream)
|
||||
} catch (e: IOException) {
|
||||
e.printStackTrace()
|
||||
throw RuntimeException("Could not load bitmap from asset")
|
||||
}
|
||||
}
|
||||
|
||||
private fun applyGaussianBlur(inputBitmap: Bitmap, radius: Float, context: Context): Bitmap {
|
||||
val rsContext = RenderScript.create(context)
|
||||
val outputBitmap =
|
||||
Bitmap.createBitmap(inputBitmap.width, inputBitmap.height, inputBitmap.config)
|
||||
val blurScript = ScriptIntrinsicBlur.create(rsContext, Element.U8_4(rsContext))
|
||||
val tmpIn = Allocation.createFromBitmap(rsContext, inputBitmap)
|
||||
val tmpOut = Allocation.createFromBitmap(rsContext, outputBitmap)
|
||||
blurScript.setRadius(radius)
|
||||
blurScript.setInput(tmpIn)
|
||||
blurScript.forEach(tmpOut)
|
||||
tmpOut.copyTo(outputBitmap)
|
||||
rsContext.finish()
|
||||
return outputBitmap
|
||||
}
|
||||
|
||||
override fun onPause() {
|
||||
super.onPause()
|
||||
finishWithAnimation()
|
||||
}
|
||||
|
||||
override fun onBackPressed() {
|
||||
super.onBackPressed()
|
||||
}
|
||||
|
||||
private fun finishWithAnimation() {
|
||||
overridePendingTransition(R.anim.no_animation, R.anim.slide_down)
|
||||
Handler(Looper.getMainLooper()).postDelayed({
|
||||
finish()
|
||||
}, 500)
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
super.onDestroy()
|
||||
progressHandler.removeCallbacksAndMessages(null)
|
||||
rotationAnimator?.cancel()
|
||||
}
|
||||
|
||||
private fun updateProgressState() {
|
||||
val currentPlayer = MediaControllerManager.getController()
|
||||
if (currentPlayer != null && currentPlayer.playbackState == Player.STATE_READY && currentPlayer.isPlaying) {
|
||||
updatePlayState(currentPlayer.isPlaying, "playWhenReady")
|
||||
progressHandler.removeCallbacksAndMessages(null)
|
||||
progressHandler.sendEmptyMessage(1)
|
||||
} else {
|
||||
progressHandler.removeCallbacksAndMessages(null)
|
||||
}
|
||||
}
|
||||
|
||||
private val progressHandler = object : Handler(Looper.myLooper()!!) {
|
||||
override fun handleMessage(msg: Message) {
|
||||
val currentPlayer = MediaControllerManager.getController()
|
||||
if (currentPlayer != null && currentPlayer.playbackState == Player.STATE_READY && currentPlayer.isPlaying) {
|
||||
val currentPosition = currentPlayer.currentPosition
|
||||
val currentString = convertMillisToMinutesAndSecondsString(currentPosition)
|
||||
binding.progressDurationTv.text = currentString
|
||||
|
||||
binding.seekBar.value = currentPosition.toFloat()
|
||||
|
||||
sendEmptyMessageDelayed(1, 1000)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,55 @@
|
||||
package com.player.musicoo.activity
|
||||
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import com.gyf.immersionbar.ktx.immersionBar
|
||||
import com.player.musicoo.R
|
||||
import com.player.musicoo.databinding.ActivitySettingsBinding
|
||||
import com.player.musicoo.util.PRIVACY_POLICY_URL
|
||||
import com.player.musicoo.util.TERMS_OF_SERVICE_URL
|
||||
import com.player.musicoo.util.openPrivacyPolicy
|
||||
import com.player.musicoo.util.openTermsOfService
|
||||
import com.player.musicoo.util.sendFeedback
|
||||
import com.player.musicoo.util.shareApp
|
||||
|
||||
class SettingsActivity : AppCompatActivity() {
|
||||
private lateinit var binding: ActivitySettingsBinding
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
binding = ActivitySettingsBinding.inflate(layoutInflater)
|
||||
setContentView(binding.root)
|
||||
initImmersionBar()
|
||||
initView()
|
||||
}
|
||||
|
||||
private fun initImmersionBar() {
|
||||
immersionBar {
|
||||
statusBarDarkFont(false)
|
||||
statusBarView(binding.view)
|
||||
}
|
||||
}
|
||||
|
||||
private fun initView() {
|
||||
binding.backBtn.setOnClickListener {
|
||||
finish()
|
||||
}
|
||||
binding.aboutBtn.setOnClickListener {
|
||||
startActivity(Intent(this, AboutActivity::class.java))
|
||||
}
|
||||
binding.feedbackBtn.setOnClickListener {
|
||||
sendFeedback(this, "motaleb3024@gmail.com", getString(R.string.app_name))
|
||||
}
|
||||
binding.shareBtn.setOnClickListener {
|
||||
shareApp(this)
|
||||
}
|
||||
binding.ppBtn.setOnClickListener {
|
||||
openPrivacyPolicy(this, PRIVACY_POLICY_URL)
|
||||
}
|
||||
binding.tosBtn.setOnClickListener {
|
||||
openTermsOfService(this, TERMS_OF_SERVICE_URL)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,81 @@
|
||||
package com.player.musicoo.adapter
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.player.musicoo.App
|
||||
import com.player.musicoo.R
|
||||
import com.player.musicoo.activity.PlayDetailsActivity
|
||||
import com.player.musicoo.bean.Audio
|
||||
import com.player.musicoo.databinding.ParentsVoiceLayoutBinding
|
||||
import com.player.musicoo.util.containsContent
|
||||
import com.player.musicoo.util.convertMillisToMinutesAndSecondsString
|
||||
import com.player.musicoo.util.getAudioDurationFromAssets
|
||||
|
||||
class ParentsVoiceAdapter(
|
||||
private val context: Context,
|
||||
private val pdfList: List<Audio>,
|
||||
) :
|
||||
RecyclerView.Adapter<ParentsVoiceAdapter.ViewHolder>() {
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
|
||||
val binding = ParentsVoiceLayoutBinding.inflate(LayoutInflater.from(context), parent, false)
|
||||
return ViewHolder(binding)
|
||||
}
|
||||
|
||||
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
|
||||
val audio = pdfList[position]
|
||||
holder.bind(audio)
|
||||
holder.itemView.setOnClickListener {
|
||||
val intent = Intent(context, PlayDetailsActivity::class.java);
|
||||
intent.putExtra(PlayDetailsActivity.KEY_DETAILS_AUDIO, audio)
|
||||
context.startActivity(intent)
|
||||
}
|
||||
}
|
||||
|
||||
override fun getItemCount(): Int = pdfList.size
|
||||
|
||||
inner class ViewHolder(private val binding: ParentsVoiceLayoutBinding) :
|
||||
RecyclerView.ViewHolder(binding.root) {
|
||||
|
||||
fun bind(audio: Audio) {
|
||||
binding.apply {
|
||||
image.setImageResource(R.mipmap.musicoo_logo_img)
|
||||
name.text = audio.name
|
||||
name.requestFocus()
|
||||
if (containsContent(audio.file)) {
|
||||
desc.text = convertMillisToMinutesAndSecondsString(audio.duration)
|
||||
} else {
|
||||
val s = getAudioDurationFromAssets(context, audio.file)
|
||||
desc.text = convertMillisToMinutesAndSecondsString(s)
|
||||
}
|
||||
|
||||
if (App.currentPlayingAudio != null) {
|
||||
if (App.currentPlayingAudio?.file == audio.file) {
|
||||
playingLayout.visibility = View.VISIBLE
|
||||
name.setTextColor(context.getColor(R.color.green))
|
||||
desc.setTextColor(context.getColor(R.color.green))
|
||||
} else {
|
||||
playingLayout.visibility = View.GONE
|
||||
name.setTextColor(context.getColor(R.color.white))
|
||||
desc.setTextColor(context.getColor(R.color.white))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private var itemClickListener: OnItemClickListener? = null
|
||||
|
||||
fun setOnItemClickListener(listener: OnItemClickListener) {
|
||||
itemClickListener = listener
|
||||
}
|
||||
|
||||
interface OnItemClickListener {
|
||||
fun onItemClick(position: Int)
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,77 @@
|
||||
package com.player.musicoo.adapter
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.view.LayoutInflater
|
||||
import android.view.ViewGroup
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.bumptech.glide.Glide
|
||||
import com.player.musicoo.App
|
||||
import com.player.musicoo.R
|
||||
import com.player.musicoo.activity.PlayDetailsActivity
|
||||
import com.player.musicoo.bean.Audio
|
||||
import com.player.musicoo.databinding.RealHumanVoiceLayoutBinding
|
||||
import com.player.musicoo.util.convertMillisToMinutesAndSecondsString
|
||||
import com.player.musicoo.util.getAudioDurationFromAssets
|
||||
|
||||
class RealHumanVoiceAdapter(
|
||||
private val context: Context,
|
||||
private val pdfList: List<Audio>,
|
||||
) :
|
||||
RecyclerView.Adapter<RealHumanVoiceAdapter.PDFViewHolder>() {
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): PDFViewHolder {
|
||||
val binding =
|
||||
RealHumanVoiceLayoutBinding.inflate(LayoutInflater.from(context), parent, false)
|
||||
return PDFViewHolder(binding)
|
||||
}
|
||||
|
||||
override fun onBindViewHolder(holder: PDFViewHolder, position: Int) {
|
||||
val audio = pdfList[position]
|
||||
holder.bind(audio)
|
||||
holder.itemView.setOnClickListener {
|
||||
val intent = Intent(context, PlayDetailsActivity::class.java);
|
||||
intent.putExtra(PlayDetailsActivity.KEY_DETAILS_AUDIO, audio)
|
||||
context.startActivity(intent)
|
||||
}
|
||||
}
|
||||
|
||||
override fun getItemCount(): Int = pdfList.size
|
||||
|
||||
inner class PDFViewHolder(private val binding: RealHumanVoiceLayoutBinding) :
|
||||
RecyclerView.ViewHolder(binding.root) {
|
||||
|
||||
fun bind(audio: Audio) {
|
||||
binding.apply {
|
||||
Glide.with(context)
|
||||
.load("file:///android_asset/${audio.image}")
|
||||
.into(realImg)
|
||||
name.text = audio.name
|
||||
val s = getAudioDurationFromAssets(context, audio.file)
|
||||
desc.text = convertMillisToMinutesAndSecondsString(s)
|
||||
|
||||
if (App.currentPlayingAudio != null) {
|
||||
if (App.currentPlayingAudio?.file == audio.file) {
|
||||
stateImg.setImageResource(R.drawable.playing_white_icon)
|
||||
name.setTextColor(context.getColor(R.color.green))
|
||||
desc.setTextColor(context.getColor(R.color.green))
|
||||
} else {
|
||||
stateImg.setImageResource(R.drawable.play_white_icon)
|
||||
name.setTextColor(context.getColor(R.color.white))
|
||||
desc.setTextColor(context.getColor(R.color.white))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private var itemClickListener: OnItemClickListener? = null
|
||||
|
||||
fun setOnItemClickListener(listener: OnItemClickListener) {
|
||||
itemClickListener = listener
|
||||
}
|
||||
|
||||
interface OnItemClickListener {
|
||||
fun onItemClick(position: Int)
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,79 @@
|
||||
package com.player.musicoo.adapter
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.bumptech.glide.Glide
|
||||
import com.player.musicoo.App
|
||||
import com.player.musicoo.R
|
||||
import com.player.musicoo.activity.PlayDetailsActivity
|
||||
import com.player.musicoo.bean.Audio
|
||||
import com.player.musicoo.databinding.SoundsOfAppliancesLayoutBinding
|
||||
import com.player.musicoo.util.convertMillisToMinutesAndSecondsString
|
||||
import com.player.musicoo.util.getAudioDurationFromAssets
|
||||
|
||||
class SoundsOfAppliancesAdapter(
|
||||
private val context: Context,
|
||||
private val pdfList: List<Audio>,
|
||||
) :
|
||||
RecyclerView.Adapter<SoundsOfAppliancesAdapter.PDFViewHolder>() {
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): PDFViewHolder {
|
||||
val binding =
|
||||
SoundsOfAppliancesLayoutBinding.inflate(LayoutInflater.from(context), parent, false)
|
||||
return PDFViewHolder(binding)
|
||||
}
|
||||
|
||||
override fun onBindViewHolder(holder: PDFViewHolder, position: Int) {
|
||||
val audio = pdfList[position]
|
||||
holder.bind(audio)
|
||||
holder.itemView.setOnClickListener {
|
||||
val intent = Intent(context, PlayDetailsActivity::class.java);
|
||||
intent.putExtra(PlayDetailsActivity.KEY_DETAILS_AUDIO, audio)
|
||||
context.startActivity(intent)
|
||||
// mediaPlayer.setDataSource(this, Uri.parse("file:///android_asset/${audio.file}"))
|
||||
}
|
||||
}
|
||||
|
||||
override fun getItemCount(): Int = pdfList.size
|
||||
|
||||
inner class PDFViewHolder(private val binding: SoundsOfAppliancesLayoutBinding) :
|
||||
RecyclerView.ViewHolder(binding.root) {
|
||||
|
||||
fun bind(audio: Audio) {
|
||||
binding.apply {
|
||||
Glide.with(context)
|
||||
.load("file:///android_asset/${audio.image}")
|
||||
.into(image)
|
||||
name.text = audio.name
|
||||
val s = getAudioDurationFromAssets(context, audio.file)
|
||||
desc.text = convertMillisToMinutesAndSecondsString(s)
|
||||
|
||||
if (App.currentPlayingAudio != null) {
|
||||
if (App.currentPlayingAudio?.file == audio.file) {
|
||||
playingLayout.visibility = View.VISIBLE
|
||||
name.setTextColor(context.getColor(R.color.green))
|
||||
desc.setTextColor(context.getColor(R.color.green))
|
||||
} else {
|
||||
playingLayout.visibility = View.GONE
|
||||
name.setTextColor(context.getColor(R.color.white))
|
||||
desc.setTextColor(context.getColor(R.color.white))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private var itemClickListener: OnItemClickListener? = null
|
||||
|
||||
fun setOnItemClickListener(listener: OnItemClickListener) {
|
||||
itemClickListener = listener
|
||||
}
|
||||
|
||||
interface OnItemClickListener {
|
||||
fun onItemClick(position: Int)
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,80 @@
|
||||
package com.player.musicoo.adapter
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.bumptech.glide.Glide
|
||||
import com.player.musicoo.App
|
||||
import com.player.musicoo.R
|
||||
import com.player.musicoo.activity.PlayDetailsActivity
|
||||
import com.player.musicoo.bean.Audio
|
||||
import com.player.musicoo.databinding.SoundsOfAppliancesLayoutBinding
|
||||
import com.player.musicoo.databinding.SoundsOfNatureLayoutBinding
|
||||
import com.player.musicoo.util.convertMillisToMinutesAndSecondsString
|
||||
import com.player.musicoo.util.getAudioDurationFromAssets
|
||||
|
||||
class SoundsOfNatureAdapter(
|
||||
private val context: Context,
|
||||
private val pdfList: List<Audio>,
|
||||
) :
|
||||
RecyclerView.Adapter<SoundsOfNatureAdapter.PDFViewHolder>() {
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): PDFViewHolder {
|
||||
val binding =
|
||||
SoundsOfNatureLayoutBinding.inflate(LayoutInflater.from(context), parent, false)
|
||||
return PDFViewHolder(binding)
|
||||
}
|
||||
|
||||
override fun onBindViewHolder(holder: PDFViewHolder, position: Int) {
|
||||
val audio = pdfList[position]
|
||||
holder.bind(audio)
|
||||
holder.itemView.setOnClickListener {
|
||||
val intent = Intent(context, PlayDetailsActivity::class.java);
|
||||
intent.putExtra(PlayDetailsActivity.KEY_DETAILS_AUDIO, audio)
|
||||
context.startActivity(intent)
|
||||
// mediaPlayer.setDataSource(this, Uri.parse("file:///android_asset/${audio.file}"))
|
||||
}
|
||||
}
|
||||
|
||||
override fun getItemCount(): Int = pdfList.size
|
||||
|
||||
inner class PDFViewHolder(private val binding: SoundsOfNatureLayoutBinding) :
|
||||
RecyclerView.ViewHolder(binding.root) {
|
||||
|
||||
fun bind(audio: Audio) {
|
||||
binding.apply {
|
||||
Glide.with(context)
|
||||
.load("file:///android_asset/${audio.image}")
|
||||
.into(image)
|
||||
name.text = audio.name
|
||||
val s = getAudioDurationFromAssets(context, audio.file)
|
||||
desc.text = convertMillisToMinutesAndSecondsString(s)
|
||||
|
||||
if (App.currentPlayingAudio != null) {
|
||||
if (App.currentPlayingAudio?.file == audio.file) {
|
||||
playingLayout.visibility = View.VISIBLE
|
||||
name.setTextColor(context.getColor(R.color.green))
|
||||
desc.setTextColor(context.getColor(R.color.green))
|
||||
} else {
|
||||
playingLayout.visibility = View.GONE
|
||||
name.setTextColor(context.getColor(R.color.white))
|
||||
desc.setTextColor(context.getColor(R.color.white))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private var itemClickListener: OnItemClickListener? = null
|
||||
|
||||
fun setOnItemClickListener(listener: OnItemClickListener) {
|
||||
itemClickListener = listener
|
||||
}
|
||||
|
||||
interface OnItemClickListener {
|
||||
fun onItemClick(position: Int)
|
||||
}
|
||||
}
|
||||
20
app/src/main/java/com/player/musicoo/bean/Audio.kt
Normal file
@ -0,0 +1,20 @@
|
||||
package com.player.musicoo.bean
|
||||
|
||||
import androidx.annotation.Keep
|
||||
import androidx.room.ColumnInfo
|
||||
import androidx.room.Entity
|
||||
import androidx.room.PrimaryKey
|
||||
import java.io.Serializable
|
||||
|
||||
@Keep
|
||||
@Entity
|
||||
data class Audio(
|
||||
@ColumnInfo(name = "name") var name: String,
|
||||
@ColumnInfo(name = "file") var file: String,
|
||||
@ColumnInfo(name = "image") var image: String,
|
||||
@ColumnInfo(name = "duration") var duration: Long,
|
||||
@ColumnInfo(name = "selected") var selected: Boolean
|
||||
) : Serializable {
|
||||
@PrimaryKey(autoGenerate = true)
|
||||
var id: Long = 0
|
||||
}
|
||||
8
app/src/main/java/com/player/musicoo/bean/Category.kt
Normal file
@ -0,0 +1,8 @@
|
||||
package com.player.musicoo.bean
|
||||
|
||||
import java.io.Serializable
|
||||
|
||||
data class Category(
|
||||
val name: String,
|
||||
val audios: List<Audio>
|
||||
) : Serializable
|
||||
@ -0,0 +1,15 @@
|
||||
package com.player.musicoo.bean
|
||||
|
||||
import androidx.room.ColumnInfo
|
||||
import androidx.room.Entity
|
||||
import androidx.room.PrimaryKey
|
||||
|
||||
@Entity(tableName = "current_playing_audio")
|
||||
data class CurrentPlayingAudio(
|
||||
@PrimaryKey(autoGenerate = true) val id: Long = 0,
|
||||
@ColumnInfo(name = "name") val name: String,
|
||||
@ColumnInfo(name = "file") val file: String,
|
||||
@ColumnInfo(name = "image") val image: String,
|
||||
@ColumnInfo(name = "duration") var duration: Long,
|
||||
@ColumnInfo(name = "selected") val selected: Boolean
|
||||
)
|
||||
@ -0,0 +1,7 @@
|
||||
package com.player.musicoo.bean
|
||||
|
||||
import java.io.Serializable
|
||||
|
||||
data class ResourcesList(
|
||||
val categories: List<Category>
|
||||
) : Serializable
|
||||
11
app/src/main/java/com/player/musicoo/database/AppDatabase.kt
Normal file
@ -0,0 +1,11 @@
|
||||
|
||||
package com.player.musicoo.database
|
||||
|
||||
import androidx.room.Database
|
||||
import androidx.room.RoomDatabase
|
||||
import com.player.musicoo.bean.Audio
|
||||
|
||||
@Database(entities = [Audio::class], version = 1, exportSchema = false)
|
||||
abstract class AppDatabase : RoomDatabase() {
|
||||
abstract fun localAudioDao(): LocalAudioDao
|
||||
}
|
||||
@ -0,0 +1,11 @@
|
||||
|
||||
package com.player.musicoo.database
|
||||
|
||||
import androidx.room.Database
|
||||
import androidx.room.RoomDatabase
|
||||
import com.player.musicoo.bean.CurrentPlayingAudio
|
||||
|
||||
@Database(entities = [CurrentPlayingAudio::class], version = 1, exportSchema = false)
|
||||
abstract class CurrentAudioDatabase : RoomDatabase() {
|
||||
abstract fun currentPlayingAudioDao(): CurrentPlayingAudioDao
|
||||
}
|
||||
@ -0,0 +1,39 @@
|
||||
package com.player.musicoo.database
|
||||
|
||||
import android.content.Context
|
||||
import androidx.room.Room
|
||||
import com.player.musicoo.bean.CurrentPlayingAudio
|
||||
|
||||
class CurrentAudioManager private constructor(context: Context) {
|
||||
private val database: CurrentAudioDatabase = Room.databaseBuilder(
|
||||
context.applicationContext,
|
||||
CurrentAudioDatabase::class.java, "current_audio_app_database"
|
||||
).build()
|
||||
|
||||
private val currentPlayingAudioDao: CurrentPlayingAudioDao = database.currentPlayingAudioDao()
|
||||
|
||||
suspend fun getCurrentPlayingAudio(): CurrentPlayingAudio? {
|
||||
return currentPlayingAudioDao.getCurrentPlayingAudio()
|
||||
}
|
||||
|
||||
suspend fun setCurrentPlayingAudio(audio: CurrentPlayingAudio) {
|
||||
val currentAudio = getCurrentPlayingAudio()
|
||||
if (currentAudio == null) {
|
||||
currentPlayingAudioDao.insertCurrentPlayingAudio(audio)
|
||||
} else {
|
||||
// 如果已有数据,先删除现有数据,然后再插入新数据
|
||||
currentPlayingAudioDao.deleteCurrentPlayingAudio(currentAudio)
|
||||
currentPlayingAudioDao.insertCurrentPlayingAudio(audio)
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
@Volatile private var instance: CurrentAudioManager? = null
|
||||
|
||||
fun getInstance(context: Context): CurrentAudioManager {
|
||||
return instance ?: synchronized(this) {
|
||||
instance ?: CurrentAudioManager(context).also { instance = it }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,24 @@
|
||||
package com.player.musicoo.database
|
||||
|
||||
import androidx.room.Dao
|
||||
import androidx.room.Delete
|
||||
import androidx.room.Insert
|
||||
import androidx.room.OnConflictStrategy
|
||||
import androidx.room.Query
|
||||
import androidx.room.Update
|
||||
import com.player.musicoo.bean.CurrentPlayingAudio
|
||||
|
||||
@Dao
|
||||
interface CurrentPlayingAudioDao {
|
||||
@Query("SELECT * FROM current_playing_audio LIMIT 1")
|
||||
suspend fun getCurrentPlayingAudio(): CurrentPlayingAudio?
|
||||
|
||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||
suspend fun insertCurrentPlayingAudio(audio: CurrentPlayingAudio)
|
||||
|
||||
@Update
|
||||
suspend fun updateCurrentPlayingAudio(audio: CurrentPlayingAudio)
|
||||
|
||||
@Delete
|
||||
suspend fun deleteCurrentPlayingAudio(audio: CurrentPlayingAudio)
|
||||
}
|
||||
@ -0,0 +1,84 @@
|
||||
package com.player.musicoo.database
|
||||
|
||||
import android.content.Context
|
||||
import androidx.room.Room
|
||||
import com.player.musicoo.bean.Audio
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
class DatabaseManager private constructor(context: Context) {
|
||||
|
||||
private val database = Room.databaseBuilder(
|
||||
context.applicationContext,
|
||||
AppDatabase::class.java, "local_audio_viewer_database"
|
||||
).build()
|
||||
|
||||
private val audioFileDao = database.localAudioDao()
|
||||
|
||||
suspend fun insertAudioFile(audio: Audio) {
|
||||
withContext(Dispatchers.IO) {
|
||||
val existingAudioFile = getAudioFileByPath(audio.name)
|
||||
if (existingAudioFile == null) {
|
||||
audioFileDao.insertAudioFile(audio)
|
||||
} else {
|
||||
audioFileDao.updateAudioFile(audio)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun insertAudioFiles(audios: List<Audio>) {
|
||||
withContext(Dispatchers.IO) {
|
||||
for (audio in audios) {
|
||||
val existingAudioFile = getAudioFileByPath(audio.file)
|
||||
if (existingAudioFile == null) {
|
||||
audioFileDao.insertAudioFile(audio)
|
||||
} else {
|
||||
audioFileDao.updateAudioFile(audio)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun getAllAudioFiles(): List<Audio> {
|
||||
return withContext(Dispatchers.IO) {
|
||||
audioFileDao.getAllAudioFile()
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun deleteAudioFile(audioFile: Audio) {
|
||||
withContext(Dispatchers.IO) {
|
||||
audioFileDao.deleteAudioFile(audioFile)
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun deleteAllAudioFiles() {
|
||||
withContext(Dispatchers.IO) {
|
||||
audioFileDao.deleteAllAudioFile()
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun updateAudioFiles(audioFile: Audio) {
|
||||
withContext(Dispatchers.IO) {
|
||||
audioFileDao.updateAudioFile(audioFile)
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun getAudioFileByPath(path: String): Audio? {
|
||||
return audioFileDao.getAudioFileByPath(path)
|
||||
}
|
||||
|
||||
suspend fun getAudioBySelect(): List<Audio> {
|
||||
return audioFileDao.getAudioBySelected()
|
||||
}
|
||||
|
||||
companion object {
|
||||
@Volatile
|
||||
private var instance: DatabaseManager? = null
|
||||
|
||||
fun getInstance(context: Context): DatabaseManager {
|
||||
return instance ?: synchronized(this) {
|
||||
instance ?: DatabaseManager(context).also { instance = it }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,34 @@
|
||||
|
||||
|
||||
package com.player.musicoo.database
|
||||
|
||||
import androidx.room.*
|
||||
import com.player.musicoo.bean.Audio
|
||||
|
||||
@Dao
|
||||
interface LocalAudioDao {
|
||||
|
||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||
suspend fun insertAudioFile(barcode: Audio)
|
||||
|
||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||
suspend fun insertAudioFiles(audios: List<Audio>)
|
||||
|
||||
@Query("SELECT * FROM Audio")
|
||||
suspend fun getAllAudioFile(): List<Audio>
|
||||
|
||||
@Delete
|
||||
suspend fun deleteAudioFile(barcode: Audio)
|
||||
|
||||
@Query("DELETE FROM Audio")
|
||||
suspend fun deleteAllAudioFile()
|
||||
|
||||
@Update
|
||||
suspend fun updateAudioFile(audioFile: Audio)
|
||||
|
||||
@Query("SELECT * FROM Audio WHERE name = :path LIMIT 1")
|
||||
suspend fun getAudioFileByPath(path: String): Audio?
|
||||
|
||||
@Query("SELECT * FROM Audio WHERE selected = 1")
|
||||
suspend fun getAudioBySelected(): List<Audio>
|
||||
}
|
||||
123
app/src/main/java/com/player/musicoo/fragment/HomeFragment.kt
Normal file
@ -0,0 +1,123 @@
|
||||
package com.player.musicoo.fragment
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.recyclerview.widget.GridLayoutManager
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import com.gyf.immersionbar.ktx.immersionBar
|
||||
import com.player.musicoo.App
|
||||
import com.player.musicoo.R
|
||||
import com.player.musicoo.adapter.RealHumanVoiceAdapter
|
||||
import com.player.musicoo.adapter.SoundsOfAppliancesAdapter
|
||||
import com.player.musicoo.adapter.SoundsOfNatureAdapter
|
||||
import com.player.musicoo.databinding.FragmentHomeBinding
|
||||
import com.player.musicoo.util.GridSpacingItemDecoration
|
||||
import com.player.musicoo.util.HorizontalSpaceItemDecoration
|
||||
|
||||
class HomeFragment : Fragment() {
|
||||
private lateinit var binding: FragmentHomeBinding
|
||||
private var realHumanVoiceAdapter: RealHumanVoiceAdapter? = null
|
||||
private var soundsOfAppliancesAdapter: SoundsOfAppliancesAdapter? = null
|
||||
private var soundsOfNatureAdapter: SoundsOfNatureAdapter? = null
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater,
|
||||
container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
): View {
|
||||
binding = FragmentHomeBinding.inflate(layoutInflater)
|
||||
return binding.root
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
initView()
|
||||
}
|
||||
|
||||
private fun initImmersionBar() {
|
||||
immersionBar {
|
||||
statusBarDarkFont(false)
|
||||
statusBarView(binding.view)
|
||||
}
|
||||
}
|
||||
|
||||
private fun initView() {
|
||||
if (App.resourcesList.categories.isNotEmpty()) {
|
||||
binding.soundsName.text = App.resourcesList.categories[1].name
|
||||
binding.natureName.text = App.resourcesList.categories[2].name
|
||||
}
|
||||
if (App.realHumanVoiceList.isNotEmpty()) {
|
||||
realHumanVoiceAdapter = RealHumanVoiceAdapter(requireActivity(), App.realHumanVoiceList)
|
||||
binding.realRv.layoutManager =
|
||||
LinearLayoutManager(requireActivity(), LinearLayoutManager.HORIZONTAL, false)
|
||||
binding.realRv.addItemDecoration(HorizontalSpaceItemDecoration(requireActivity()))
|
||||
binding.realRv.adapter = realHumanVoiceAdapter
|
||||
}
|
||||
if (App.soundsOfAppliancesList.isNotEmpty()) {
|
||||
soundsOfAppliancesAdapter =
|
||||
SoundsOfAppliancesAdapter(requireActivity(), App.soundsOfAppliancesList)
|
||||
binding.soundsRv.layoutManager =
|
||||
LinearLayoutManager(requireActivity(), LinearLayoutManager.HORIZONTAL, false)
|
||||
binding.soundsRv.addItemDecoration(HorizontalSpaceItemDecoration(requireActivity()))
|
||||
binding.soundsRv.adapter = soundsOfAppliancesAdapter
|
||||
}
|
||||
if (App.soundsOfNatureList.isNotEmpty()) {
|
||||
soundsOfNatureAdapter =
|
||||
SoundsOfNatureAdapter(requireActivity(), App.soundsOfNatureList)
|
||||
binding.natureRv.layoutManager =
|
||||
GridLayoutManager(requireActivity(), 3, GridLayoutManager.HORIZONTAL, false)
|
||||
binding.natureRv.addItemDecoration(GridSpacingItemDecoration(requireActivity(), 10, 3))
|
||||
binding.natureRv.adapter = soundsOfNatureAdapter
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressLint("NotifyDataSetChanged")
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
if (App.currentPlayingAudio != null) {
|
||||
if (App.realHumanVoiceList.isNotEmpty()) {
|
||||
for ((index, audio) in App.realHumanVoiceList.withIndex()) {
|
||||
if (audio.file == App.currentPlayingAudio?.file) {
|
||||
notifyDataSetChanged()
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
if (App.soundsOfAppliancesList.isNotEmpty()) {
|
||||
for ((index, audio) in App.soundsOfAppliancesList.withIndex()) {
|
||||
if (audio.file == App.currentPlayingAudio?.file) {
|
||||
notifyDataSetChanged()
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
if (App.soundsOfNatureList.isNotEmpty()) {
|
||||
for ((index, audio) in App.soundsOfNatureList.withIndex()) {
|
||||
if (audio.file == App.currentPlayingAudio?.file) {
|
||||
notifyDataSetChanged()
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
initImmersionBar()
|
||||
}
|
||||
|
||||
override fun onHiddenChanged(hidden: Boolean) {
|
||||
super.onHiddenChanged(hidden)
|
||||
if(!hidden){
|
||||
initImmersionBar()
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressLint("NotifyDataSetChanged")
|
||||
private fun notifyDataSetChanged(){
|
||||
soundsOfAppliancesAdapter?.notifyDataSetChanged()
|
||||
realHumanVoiceAdapter?.notifyDataSetChanged()
|
||||
soundsOfNatureAdapter?.notifyDataSetChanged()
|
||||
}
|
||||
|
||||
}
|
||||
236
app/src/main/java/com/player/musicoo/fragment/ImportFragment.kt
Normal file
@ -0,0 +1,236 @@
|
||||
package com.player.musicoo.fragment
|
||||
|
||||
import android.Manifest
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.AlertDialog
|
||||
import android.content.ContentResolver
|
||||
import android.content.ContentUris
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.pm.PackageManager
|
||||
import android.media.MediaPlayer
|
||||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import android.provider.MediaStore
|
||||
import android.util.Log
|
||||
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.core.content.ContextCompat
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import com.gyf.immersionbar.ktx.immersionBar
|
||||
import com.player.musicoo.App
|
||||
import com.player.musicoo.R
|
||||
import com.player.musicoo.activity.SettingsActivity
|
||||
import com.player.musicoo.adapter.ParentsVoiceAdapter
|
||||
import com.player.musicoo.bean.Audio
|
||||
import com.player.musicoo.databinding.FragmentImportBinding
|
||||
import com.player.musicoo.util.uriToFile
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
class ImportFragment : Fragment() {
|
||||
private lateinit var binding: FragmentImportBinding
|
||||
private var parentsVoiceAdapter: ParentsVoiceAdapter? = null
|
||||
private var importAdapterList: MutableList<Audio> = mutableListOf()
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater,
|
||||
container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
): View {
|
||||
binding = FragmentImportBinding.inflate(layoutInflater)
|
||||
return binding.root
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
initView()
|
||||
}
|
||||
|
||||
private fun initView() {
|
||||
binding.settingBtn.setOnClickListener {
|
||||
startActivity(Intent(requireActivity(), SettingsActivity::class.java))
|
||||
}
|
||||
binding.addBtn.setOnClickListener {
|
||||
binding.addBtn.visibility = View.GONE
|
||||
if (ContextCompat.checkSelfPermission(
|
||||
requireActivity(),
|
||||
Manifest.permission.READ_EXTERNAL_STORAGE
|
||||
) != PackageManager.PERMISSION_GRANTED
|
||||
) {
|
||||
requestPermissionLauncher.launch(Manifest.permission.READ_EXTERNAL_STORAGE)
|
||||
} else {
|
||||
openAudioPicker()
|
||||
}
|
||||
}
|
||||
importAdapterList.clear()
|
||||
importAdapterList.addAll(App.importList)
|
||||
parentsVoiceAdapter = ParentsVoiceAdapter(requireActivity(), importAdapterList)
|
||||
binding.importRv.layoutManager =
|
||||
LinearLayoutManager(requireActivity(), LinearLayoutManager.VERTICAL, false)
|
||||
binding.importRv.adapter = parentsVoiceAdapter
|
||||
}
|
||||
|
||||
private var requestPermissionLauncher =
|
||||
registerForActivityResult(ActivityResultContracts.RequestPermission()) { isGranted: Boolean ->
|
||||
if (isGranted) {
|
||||
openAudioPicker()
|
||||
} else {
|
||||
showExplanationDialog()
|
||||
}
|
||||
}
|
||||
|
||||
private fun openAudioPicker() {
|
||||
binding.loadingLayout.visibility = View.VISIBLE
|
||||
getMusicFiles(requireActivity())
|
||||
}
|
||||
|
||||
private fun showExplanationDialog() {
|
||||
AlertDialog.Builder(requireActivity())
|
||||
.setTitle(getString(R.string.permission_request))
|
||||
.setMessage(getString(R.string.permission_request_desc))
|
||||
.setPositiveButton(getString(R.string.ok)) { dialog, which ->
|
||||
requestPermissionLauncher.launch(Manifest.permission.READ_EXTERNAL_STORAGE)
|
||||
dialog.dismiss()
|
||||
}
|
||||
.setNegativeButton(getString(R.string.cancel)) { dialog, which ->
|
||||
dialog.dismiss()
|
||||
}
|
||||
.show()
|
||||
}
|
||||
|
||||
@SuppressLint("NotifyDataSetChanged")
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
initImmersionBar()
|
||||
|
||||
if (importAdapterList.isNotEmpty()) {
|
||||
binding.noContentLayout.visibility = View.GONE
|
||||
} else {
|
||||
binding.noContentLayout.visibility = View.VISIBLE
|
||||
}
|
||||
|
||||
if (App.currentPlayingAudio != null) {
|
||||
if (App.importList.isNotEmpty()) {
|
||||
importAdapterList.clear()
|
||||
importAdapterList.addAll(App.importList)
|
||||
for ((index, audio) in importAdapterList.withIndex()) {
|
||||
if (audio.file == App.currentPlayingAudio?.file) {
|
||||
parentsVoiceAdapter?.notifyDataSetChanged()
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onHiddenChanged(hidden: Boolean) {
|
||||
super.onHiddenChanged(hidden)
|
||||
if (hidden) {
|
||||
initImmersionBar()
|
||||
}
|
||||
}
|
||||
|
||||
private fun initImmersionBar() {
|
||||
immersionBar {
|
||||
statusBarDarkFont(false)
|
||||
statusBarView(binding.view)
|
||||
}
|
||||
}
|
||||
|
||||
private fun getAudioDuration(uri: Uri): Long {
|
||||
var duration = 0L
|
||||
try {
|
||||
val mediaPlayer = MediaPlayer.create(requireContext(), uri)
|
||||
duration = mediaPlayer.duration.toLong()
|
||||
mediaPlayer.release()
|
||||
return duration
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
return duration
|
||||
}
|
||||
|
||||
@SuppressLint("NotifyDataSetChanged")
|
||||
fun getMusicFiles(context: Context): List<String> {
|
||||
val musicFiles = mutableListOf<String>()
|
||||
val contentResolver: ContentResolver = context.contentResolver
|
||||
|
||||
// 定义查询参数
|
||||
val projection = arrayOf(
|
||||
MediaStore.Audio.Media._ID,
|
||||
MediaStore.Audio.Media.DISPLAY_NAME,
|
||||
MediaStore.Audio.Media.DATA
|
||||
)
|
||||
|
||||
val selection = "${MediaStore.Audio.Media.IS_MUSIC} != 0"
|
||||
val sortOrder = "${MediaStore.Audio.Media.DISPLAY_NAME} ASC"
|
||||
|
||||
// 查询音频文件
|
||||
val cursor = contentResolver.query(
|
||||
MediaStore.Audio.Media.EXTERNAL_CONTENT_URI,
|
||||
projection,
|
||||
selection,
|
||||
null,
|
||||
sortOrder
|
||||
)
|
||||
|
||||
cursor?.use { cursor ->
|
||||
val idColumn = cursor.getColumnIndexOrThrow(MediaStore.Audio.Media._ID)
|
||||
val nameColumn = cursor.getColumnIndexOrThrow(MediaStore.Audio.Media.DISPLAY_NAME)
|
||||
val dataColumn = cursor.getColumnIndexOrThrow(MediaStore.Audio.Media.DATA)
|
||||
|
||||
while (cursor.moveToNext()) {
|
||||
val id = cursor.getLong(idColumn)
|
||||
val name = cursor.getString(nameColumn)
|
||||
val data = cursor.getString(dataColumn)
|
||||
val contentUri = ContentUris.withAppendedId(
|
||||
MediaStore.Audio.Media.EXTERNAL_CONTENT_URI,
|
||||
id
|
||||
)
|
||||
val duration2 = getAudioDuration(contentUri)
|
||||
val audio = Audio(name, contentUri.toString(), "", duration2, false)
|
||||
|
||||
musicFiles.add(contentUri.toString())
|
||||
// 如果你想要获取文件的具体路径,可以使用 data 变量
|
||||
// musicFiles.add(data)
|
||||
Log.d(
|
||||
"ocean",
|
||||
"name->${name} " +
|
||||
"uri.toString()->${contentUri.toString()} " +
|
||||
"duration2->$duration2 "
|
||||
+ "data->$data"
|
||||
)
|
||||
|
||||
CoroutineScope(Dispatchers.IO).launch {
|
||||
if (audio.duration > 0) {
|
||||
App.databaseManager.insertAudioFile(audio)
|
||||
}
|
||||
withContext(Dispatchers.Main) {
|
||||
App.initImportAudio {
|
||||
importAdapterList.clear()
|
||||
importAdapterList.addAll(App.importList)
|
||||
parentsVoiceAdapter?.notifyDataSetChanged()
|
||||
if (importAdapterList.isNotEmpty()) {
|
||||
binding.noContentLayout.visibility = View.GONE
|
||||
} else {
|
||||
binding.noContentLayout.visibility = View.VISIBLE
|
||||
}
|
||||
binding.loadingLayout.visibility = View.GONE
|
||||
|
||||
binding.addBtn.visibility = View.VISIBLE
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return musicFiles
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,50 @@
|
||||
package com.player.musicoo.media
|
||||
|
||||
import android.content.Context
|
||||
import android.os.Bundle
|
||||
import android.widget.RemoteViews
|
||||
import androidx.core.app.NotificationCompat
|
||||
import androidx.media3.common.Player
|
||||
import androidx.media3.common.util.UnstableApi
|
||||
import androidx.media3.session.CommandButton
|
||||
import androidx.media3.session.MediaNotification
|
||||
import androidx.media3.session.MediaSession
|
||||
import com.google.common.collect.ImmutableList
|
||||
import com.player.musicoo.R
|
||||
|
||||
@UnstableApi
|
||||
class MyMediaNotificationProvider(val context: Context) : MediaNotification.Provider {
|
||||
companion object {
|
||||
private const val CHANNEL_ID = "musicoo_notification_channel"
|
||||
private const val NOTIFICATION_ID = 1231
|
||||
}
|
||||
|
||||
override fun createNotification(
|
||||
mediaSession: MediaSession,
|
||||
customLayout: ImmutableList<CommandButton>,
|
||||
actionFactory: MediaNotification.ActionFactory,
|
||||
onNotificationChangedCallback: MediaNotification.Provider.Callback
|
||||
): MediaNotification {
|
||||
|
||||
val customLayoutRes = R.layout.my_notification_layout
|
||||
val customLayoutView = RemoteViews(context.packageName, customLayoutRes)
|
||||
|
||||
val notification = NotificationCompat.Builder(context, CHANNEL_ID)
|
||||
.setContentTitle("Custom Notification Title")
|
||||
.setContentText("Custom Notification Text")
|
||||
.setSmallIcon(R.mipmap.musicoo_logo_img)
|
||||
.setCustomContentView(customLayoutView)
|
||||
.build()
|
||||
|
||||
return MediaNotification(NOTIFICATION_ID, notification)
|
||||
}
|
||||
|
||||
override fun handleCustomCommand(
|
||||
session: MediaSession,
|
||||
action: String,
|
||||
extras: Bundle
|
||||
): Boolean {
|
||||
// 处理自定义命令
|
||||
return false
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,182 @@
|
||||
package com.player.musicoo.media
|
||||
|
||||
import android.content.ComponentName
|
||||
import android.content.Context
|
||||
import android.net.Uri
|
||||
import android.util.Log
|
||||
import androidx.media3.common.MediaItem
|
||||
import androidx.media3.common.MediaMetadata
|
||||
import androidx.media3.common.Player
|
||||
import androidx.media3.session.MediaController
|
||||
import androidx.media3.session.SessionToken
|
||||
import com.google.common.util.concurrent.ListenableFuture
|
||||
import com.google.common.util.concurrent.MoreExecutors
|
||||
import com.player.musicoo.App
|
||||
import com.player.musicoo.R
|
||||
import com.player.musicoo.bean.Audio
|
||||
import com.player.musicoo.bean.CurrentPlayingAudio
|
||||
import com.player.musicoo.service.PlaybackService
|
||||
import com.player.musicoo.util.containsContent
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
object MediaControllerManager {
|
||||
private var mediaController: MediaController? = null
|
||||
private var controllerFuture: ListenableFuture<MediaController>? = null
|
||||
private var currentAudioFile = ""
|
||||
|
||||
fun init(context: Context) {
|
||||
val sessionToken =
|
||||
SessionToken(context, ComponentName(context, PlaybackService::class.java))
|
||||
controllerFuture = MediaController.Builder(context, sessionToken).buildAsync()
|
||||
controllerFuture?.addListener({
|
||||
mediaController = controllerFuture?.get()
|
||||
Log.d("ocean", "MediaControllerManager init")
|
||||
|
||||
Log.d("ocean", "MediaController connected: ${mediaController?.isConnected}")
|
||||
}, MoreExecutors.directExecutor())
|
||||
}
|
||||
|
||||
fun getController(): MediaController? {
|
||||
return if (mediaController != null && mediaController!!.isConnected) {
|
||||
mediaController
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
fun setupMedia(context: Context, audio: Audio, listener: Player.Listener) {
|
||||
if (currentAudioFile != audio.file) {
|
||||
currentAudioFile = audio.file
|
||||
|
||||
val uri: Uri? = if (containsContent(audio.file)) {
|
||||
Uri.parse(audio.file)
|
||||
} else {
|
||||
Uri.parse("file:///android_asset/$currentAudioFile")
|
||||
}
|
||||
|
||||
Log.d("ocean","uri->$uri")
|
||||
|
||||
val resourceId = R.mipmap.musicoo_logo_img
|
||||
val imgUri: Uri? = if (audio.image.isNotEmpty()) {
|
||||
Uri.parse("file:///android_asset/${audio.image}")
|
||||
} else {
|
||||
Uri.parse("android.resource://${context.packageName}/$resourceId")
|
||||
}
|
||||
|
||||
val mediaItem =
|
||||
MediaItem.Builder()
|
||||
.setUri(uri)
|
||||
.setMediaMetadata(
|
||||
MediaMetadata.Builder()
|
||||
.setArtist(audio.name)
|
||||
.setTitle(audio.name)
|
||||
.setArtworkUri(imgUri)
|
||||
.build()
|
||||
)
|
||||
.build()
|
||||
if (isConnected()) {
|
||||
mediaController?.let {
|
||||
it.addListener(listener)
|
||||
it.setMediaItem(mediaItem)
|
||||
it.repeatMode = Player.REPEAT_MODE_ONE
|
||||
it.prepare()
|
||||
it.play()
|
||||
val currentPlayingAudio =
|
||||
CurrentPlayingAudio(
|
||||
audio.id,
|
||||
audio.name,
|
||||
audio.file,
|
||||
audio.image,
|
||||
audio.duration,
|
||||
false
|
||||
)
|
||||
CoroutineScope(Dispatchers.IO).launch {
|
||||
App.currentAudioManager.setCurrentPlayingAudio(currentPlayingAudio)
|
||||
withContext(Dispatchers.Main) {
|
||||
App.initCurrentPlayingAudio()//更新到入口变量中
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun setupMedia(context: Context, audio: CurrentPlayingAudio, listener: Player.Listener) {
|
||||
if (currentAudioFile != audio.file) {
|
||||
currentAudioFile = audio.file
|
||||
|
||||
val uri: Uri? = if (containsContent(audio.file)) {
|
||||
Uri.parse(audio.file)
|
||||
} else {
|
||||
Uri.parse("file:///android_asset/$currentAudioFile")
|
||||
}
|
||||
|
||||
Log.d("ocean","uri->$uri")
|
||||
|
||||
val resourceId = R.mipmap.musicoo_logo_img
|
||||
val imgUri: Uri? = if (audio.image.isNotEmpty()) {
|
||||
Uri.parse("file:///android_asset/${audio.image}")
|
||||
} else {
|
||||
Uri.parse("android.resource://${context.packageName}/$resourceId")
|
||||
}
|
||||
|
||||
// val uri = Uri.parse("file:///android_asset/$currentAudioFile")
|
||||
//// val mediaItem = MediaItem.fromUri(uri)
|
||||
// val imgUri = Uri.parse("file:///android_asset/${audio.image}")
|
||||
val mediaItem =
|
||||
MediaItem.Builder()
|
||||
.setUri(uri)
|
||||
.setMediaMetadata(
|
||||
MediaMetadata.Builder()
|
||||
.setArtist(audio.name)
|
||||
.setTitle(audio.name)
|
||||
.setArtworkUri(imgUri)
|
||||
.build()
|
||||
)
|
||||
.build()
|
||||
if (isConnected()) {
|
||||
mediaController?.let {
|
||||
it.addListener(listener)
|
||||
it.setMediaItem(mediaItem)
|
||||
it.repeatMode = Player.REPEAT_MODE_ONE
|
||||
it.prepare()
|
||||
it.play()
|
||||
val currentPlayingAudio =
|
||||
CurrentPlayingAudio(
|
||||
audio.id,
|
||||
audio.name,
|
||||
audio.file,
|
||||
audio.image,
|
||||
audio.duration,
|
||||
false
|
||||
)
|
||||
CoroutineScope(Dispatchers.IO).launch {
|
||||
App.currentAudioManager.setCurrentPlayingAudio(currentPlayingAudio)
|
||||
withContext(Dispatchers.Main) {
|
||||
App.initCurrentPlayingAudio()//更新到入口变量中
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun isConnected(): Boolean {
|
||||
mediaController?.let {
|
||||
return it.isConnected
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
|
||||
fun isPlaying(): Boolean {
|
||||
mediaController?.let {
|
||||
return it.isPlaying
|
||||
}
|
||||
return false
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,94 @@
|
||||
package com.player.musicoo.service
|
||||
|
||||
import android.app.NotificationChannel
|
||||
import android.app.NotificationManager
|
||||
import android.app.PendingIntent
|
||||
import android.app.Service
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.media.AudioAttributes
|
||||
import android.media.MediaPlayer
|
||||
import android.os.Binder
|
||||
import android.os.Build
|
||||
import android.os.IBinder
|
||||
import androidx.core.app.NotificationCompat
|
||||
import com.player.musicoo.R
|
||||
import com.player.musicoo.activity.MainActivity
|
||||
|
||||
class AudioPlayerService : Service() {
|
||||
|
||||
private var mediaPlayer: MediaPlayer? = null
|
||||
private val binder = AudioPlayerBinder()
|
||||
|
||||
inner class AudioPlayerBinder : Binder() {
|
||||
fun getService(): AudioPlayerService = this@AudioPlayerService
|
||||
}
|
||||
|
||||
override fun onBind(intent: Intent?): IBinder? {
|
||||
return binder
|
||||
}
|
||||
|
||||
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
||||
return START_NOT_STICKY
|
||||
}
|
||||
|
||||
fun playAudio(audioUri: String) {
|
||||
mediaPlayer?.let {
|
||||
it.stop()
|
||||
it.reset() // 重置 MediaPlayer,确保处于空闲状态
|
||||
it.release()
|
||||
}
|
||||
mediaPlayer = MediaPlayer().apply {
|
||||
setAudioAttributes(
|
||||
AudioAttributes.Builder()
|
||||
.setContentType(AudioAttributes.CONTENT_TYPE_MUSIC)
|
||||
.build()
|
||||
)
|
||||
|
||||
val assetFileDescriptor = assets.openFd(audioUri)
|
||||
setDataSource(
|
||||
assetFileDescriptor.fileDescriptor,
|
||||
assetFileDescriptor.startOffset,
|
||||
assetFileDescriptor.length
|
||||
)
|
||||
prepareAsync()
|
||||
setOnPreparedListener { start() }
|
||||
isLooping = true // 开启重复播放
|
||||
}
|
||||
startForegroundService()
|
||||
}
|
||||
|
||||
fun pauseAudio() {
|
||||
mediaPlayer?.pause()
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
mediaPlayer?.release()
|
||||
super.onDestroy()
|
||||
}
|
||||
|
||||
private fun startForegroundService() {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
val channelId = "audio_player_channel"
|
||||
val channelName = "Audio Player"
|
||||
val notificationManager =
|
||||
getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
|
||||
|
||||
val channel = NotificationChannel(channelId, channelName, NotificationManager.IMPORTANCE_LOW)
|
||||
notificationManager.createNotificationChannel(channel)
|
||||
|
||||
val notificationIntent = Intent(this, MainActivity::class.java)
|
||||
val pendingIntent = PendingIntent.getActivity(this, 0, notificationIntent,
|
||||
PendingIntent.FLAG_IMMUTABLE)
|
||||
|
||||
val notification = NotificationCompat.Builder(this, channelId)
|
||||
.setContentTitle("正在播放音频")
|
||||
.setContentText("点击以返回应用")
|
||||
.setSmallIcon(R.mipmap.musicoo_logo_img)
|
||||
.setContentIntent(pendingIntent)
|
||||
.build()
|
||||
|
||||
startForeground(1, notification)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,62 @@
|
||||
package com.player.musicoo.service
|
||||
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import androidx.media3.common.Player
|
||||
import androidx.media3.common.Player.COMMAND_SEEK_TO_NEXT
|
||||
import androidx.media3.common.Player.COMMAND_SEEK_TO_NEXT_MEDIA_ITEM
|
||||
import androidx.media3.common.Player.COMMAND_SEEK_TO_PREVIOUS
|
||||
import androidx.media3.common.Player.COMMAND_SEEK_TO_PREVIOUS_MEDIA_ITEM
|
||||
import androidx.media3.common.util.UnstableApi
|
||||
import androidx.media3.exoplayer.ExoPlayer
|
||||
import androidx.media3.session.CommandButton
|
||||
import androidx.media3.session.MediaSession
|
||||
import androidx.media3.session.MediaSessionService
|
||||
import androidx.media3.session.SessionCommand
|
||||
import com.player.musicoo.R
|
||||
|
||||
|
||||
@UnstableApi
|
||||
class PlaybackService : MediaSessionService() {
|
||||
|
||||
private var mediaSession: MediaSession? = null
|
||||
|
||||
// Create your player and media session in the onCreate lifecycle event
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
|
||||
val player = ExoPlayer.Builder(this).build()
|
||||
mediaSession = MediaSession.Builder(this, player)
|
||||
.build()
|
||||
|
||||
|
||||
// setMediaNotificationProvider(MyMediaNotificationProvider(this))
|
||||
|
||||
}
|
||||
|
||||
// The user dismissed the app from the recent tasks
|
||||
override fun onTaskRemoved(rootIntent: Intent?) {
|
||||
val player = mediaSession?.player!!
|
||||
if (!player.playWhenReady
|
||||
|| player.mediaItemCount == 0
|
||||
|| player.playbackState == Player.STATE_ENDED
|
||||
) {
|
||||
// Stop the service if not playing, continue playing in the background
|
||||
// otherwise.
|
||||
stopSelf()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onGetSession(controllerInfo: MediaSession.ControllerInfo): MediaSession? =
|
||||
mediaSession
|
||||
|
||||
// Remember to release the player and media session in onDestroy
|
||||
override fun onDestroy() {
|
||||
mediaSession?.run {
|
||||
player.release()
|
||||
release()
|
||||
mediaSession = null
|
||||
}
|
||||
super.onDestroy()
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,66 @@
|
||||
package com.player.musicoo.sp
|
||||
|
||||
import android.content.Context
|
||||
import android.content.SharedPreferences
|
||||
import androidx.core.content.edit
|
||||
import kotlin.properties.ReadWriteProperty
|
||||
import kotlin.reflect.KProperty
|
||||
|
||||
class SharedPreferencesHelper(context: Context) {
|
||||
companion object {
|
||||
const val CURRENT_PLAYING_AUDIO = "current_playing_audio"
|
||||
}
|
||||
|
||||
private val preferences: SharedPreferences = context.getSharedPreferences(context.packageName, Context.MODE_PRIVATE)
|
||||
|
||||
var currentPlayingAudio: Boolean by preferences.boolean(
|
||||
key = CURRENT_PLAYING_AUDIO,
|
||||
defaultValue = false
|
||||
)
|
||||
|
||||
private inline fun <reified T : Any> SharedPreferences.boolean(
|
||||
key: String,
|
||||
defaultValue: T
|
||||
): ReadWriteProperty<Any, T> {
|
||||
return object : ReadWriteProperty<Any, T> {
|
||||
override fun getValue(thisRef: Any, property: KProperty<*>): T {
|
||||
return when (T::class) {
|
||||
Boolean::class -> getBoolean(key, defaultValue as Boolean) as T
|
||||
else -> throw IllegalArgumentException("Unsupported type")
|
||||
}
|
||||
}
|
||||
|
||||
override fun setValue(thisRef: Any, property: KProperty<*>, value: T) {
|
||||
edit {
|
||||
when (T::class) {
|
||||
Boolean::class -> putBoolean(key, value as Boolean)
|
||||
else -> throw IllegalArgumentException("Unsupported type")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private inline fun <reified T : Any> SharedPreferences.string(
|
||||
key: String,
|
||||
defaultValue: T
|
||||
): ReadWriteProperty<Any, T> {
|
||||
return object : ReadWriteProperty<Any, T> {
|
||||
override fun getValue(thisRef: Any, property: KProperty<*>): T {
|
||||
return when (T::class) {
|
||||
String::class -> getString(key, defaultValue as String) as T
|
||||
else -> throw IllegalArgumentException("Unsupported type")
|
||||
}
|
||||
}
|
||||
|
||||
override fun setValue(thisRef: Any, property: KProperty<*>, value: T) {
|
||||
edit {
|
||||
when (T::class) {
|
||||
String::class -> putString(key, value as String)
|
||||
else -> throw IllegalArgumentException("Unsupported type")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
71
app/src/main/java/com/player/musicoo/util/AudioPlayer.kt
Normal file
@ -0,0 +1,71 @@
|
||||
package com.player.musicoo.util
|
||||
|
||||
import android.content.Context
|
||||
import android.media.AudioAttributes
|
||||
import android.media.MediaPlayer
|
||||
|
||||
class AudioPlayer(private val context: Context) {
|
||||
|
||||
private var mediaPlayer: MediaPlayer? = null
|
||||
|
||||
init {
|
||||
mediaPlayer = MediaPlayer().apply {
|
||||
setAudioAttributes(
|
||||
AudioAttributes.Builder()
|
||||
.setContentType(AudioAttributes.CONTENT_TYPE_MUSIC)
|
||||
.setUsage(AudioAttributes.USAGE_MEDIA)
|
||||
.build()
|
||||
)
|
||||
setOnCompletionListener {
|
||||
// 在播放完成时重置 MediaPlayer
|
||||
mediaPlayer?.seekTo(0)
|
||||
mediaPlayer?.start()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun playAudio(fileName: String) {
|
||||
try {
|
||||
val assetFileDescriptor = context.assets.openFd(fileName)
|
||||
mediaPlayer?.apply {
|
||||
reset()
|
||||
setDataSource(
|
||||
assetFileDescriptor.fileDescriptor,
|
||||
assetFileDescriptor.startOffset,
|
||||
assetFileDescriptor.length
|
||||
)
|
||||
prepare()
|
||||
start()
|
||||
isLooping = true // 开启重复播放
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
}
|
||||
|
||||
fun pauseAudio() {
|
||||
mediaPlayer?.pause()
|
||||
}
|
||||
|
||||
fun resumeAudio() {
|
||||
mediaPlayer?.start()
|
||||
}
|
||||
|
||||
fun stopAudio() {
|
||||
mediaPlayer?.stop()
|
||||
mediaPlayer?.release()
|
||||
mediaPlayer = null
|
||||
}
|
||||
|
||||
fun currentPosition(): Int {
|
||||
return mediaPlayer?.currentPosition!!
|
||||
}
|
||||
|
||||
fun seekTo(position: Int) {
|
||||
mediaPlayer?.seekTo(position)
|
||||
}
|
||||
|
||||
fun isPlaying(): Boolean {
|
||||
return mediaPlayer?.isPlaying!!
|
||||
}
|
||||
}
|
||||
21
app/src/main/java/com/player/musicoo/util/ConvertUtil.kt
Normal file
@ -0,0 +1,21 @@
|
||||
package com.player.musicoo.util
|
||||
|
||||
import android.content.Context
|
||||
import android.content.pm.PackageManager
|
||||
|
||||
fun convertMillisToMinutesAndSecondsString(millis: Long): String {
|
||||
val totalSeconds = millis / 1000
|
||||
val minutes = (totalSeconds / 60).toInt()
|
||||
val seconds = (totalSeconds % 60).toInt()
|
||||
return String.format("%02d:%02d", minutes, seconds)
|
||||
}
|
||||
|
||||
fun getAppVersion(context: Context): String {
|
||||
return try {
|
||||
val pInfo = context.packageManager.getPackageInfo(context.packageName, 0)
|
||||
pInfo.versionName
|
||||
} catch (e: PackageManager.NameNotFoundException) {
|
||||
e.printStackTrace()
|
||||
"N/A"
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,29 @@
|
||||
package com.player.musicoo.util
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.Rect
|
||||
import android.view.View
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
|
||||
class GridSpacingItemDecoration(
|
||||
private val context: Context,
|
||||
private val spacing: Int,
|
||||
private val spanCount: Int
|
||||
) : RecyclerView.ItemDecoration() {
|
||||
|
||||
override fun getItemOffsets(outRect: Rect, view: View, parent: RecyclerView, state: RecyclerView.State) {
|
||||
val position = parent.getChildAdapterPosition(view)
|
||||
val column = position % spanCount
|
||||
|
||||
if (spanCount > 1) {
|
||||
outRect.left = spacing - column * spacing / spanCount
|
||||
outRect.right = (column + 1) * spacing / spanCount
|
||||
|
||||
if (position < spanCount) {
|
||||
outRect.top = spacing
|
||||
}
|
||||
outRect.bottom = spacing
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -0,0 +1,31 @@
|
||||
package com.player.musicoo.util
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.Rect
|
||||
import android.view.View
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
|
||||
class HorizontalSpaceItemDecoration(private val context: Context) : RecyclerView.ItemDecoration() {
|
||||
private val defaultSpacing: Int
|
||||
private val lastItemSpacing: Int
|
||||
|
||||
init {
|
||||
val density = context.resources.displayMetrics.density
|
||||
defaultSpacing = (8 * density).toInt()
|
||||
lastItemSpacing = (16 * density).toInt()
|
||||
}
|
||||
|
||||
override fun getItemOffsets(outRect: Rect, view: View, parent: RecyclerView, state: RecyclerView.State) {
|
||||
super.getItemOffsets(outRect, view, parent, state)
|
||||
|
||||
val position = parent.getChildAdapterPosition(view)
|
||||
val itemCount = parent.adapter?.itemCount ?: 0
|
||||
|
||||
// 设置间隔,除了最后一个item外,其余item的右边都有间隔
|
||||
if (position < itemCount - 1) {
|
||||
outRect.right = defaultSpacing
|
||||
} else {
|
||||
outRect.right = lastItemSpacing
|
||||
}
|
||||
}
|
||||
}
|
||||
44
app/src/main/java/com/player/musicoo/util/OpenUrlUtil.kt
Normal file
@ -0,0 +1,44 @@
|
||||
package com.player.musicoo.util
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.widget.Toast
|
||||
import com.player.musicoo.R
|
||||
|
||||
const val PRIVACY_POLICY_URL = "https://musicoo.app/privacy"
|
||||
const val TERMS_OF_SERVICE_URL = "https://musicoo.app/terms"
|
||||
|
||||
fun openPrivacyPolicy(context: Context, privacyPolicyUrl: String) {
|
||||
val intent = Intent(Intent.ACTION_VIEW, Uri.parse(privacyPolicyUrl))
|
||||
context.startActivity(intent)
|
||||
}
|
||||
|
||||
fun openTermsOfService(context: Context, termsOfServiceUrl: String) {
|
||||
val intent = Intent(Intent.ACTION_VIEW, Uri.parse(termsOfServiceUrl))
|
||||
context.startActivity(intent)
|
||||
}
|
||||
|
||||
fun shareApp(context: Context) {
|
||||
val appPackageName = context.packageName
|
||||
val appName = context.applicationInfo.loadLabel(context.packageManager).toString()
|
||||
val appPlayStoreLink = "https://play.google.com/store/apps/details?id=$appPackageName"
|
||||
|
||||
val shareIntent = Intent(Intent.ACTION_SEND)
|
||||
shareIntent.type = "text/plain"
|
||||
shareIntent.putExtra(Intent.EXTRA_SUBJECT, "Check out this app: $appName")
|
||||
shareIntent.putExtra(Intent.EXTRA_TEXT, "Download $appName from Google Play: $appPlayStoreLink")
|
||||
context.startActivity(Intent.createChooser(shareIntent, "Share $appName via"))
|
||||
}
|
||||
|
||||
fun sendFeedback(context: Context, email: String, subject: String) {
|
||||
val emailIntent = Intent(Intent.ACTION_SEND)
|
||||
emailIntent.type = "message/rfc822"
|
||||
emailIntent.putExtra(Intent.EXTRA_EMAIL, arrayOf(email))
|
||||
emailIntent.putExtra(Intent.EXTRA_SUBJECT, subject)
|
||||
try {
|
||||
context.startActivity(Intent.createChooser(emailIntent, "Send Feedback"))
|
||||
} catch (ex: android.content.ActivityNotFoundException) {
|
||||
Toast.makeText(context,"There is no app that supports sending emails",Toast.LENGTH_LONG).show()
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,91 @@
|
||||
package com.player.musicoo.util
|
||||
|
||||
import android.content.ContentResolver
|
||||
import android.content.Context
|
||||
import android.database.Cursor
|
||||
import android.media.MediaMetadataRetriever
|
||||
import android.media.MediaPlayer
|
||||
import android.net.Uri
|
||||
import android.provider.MediaStore
|
||||
import com.player.musicoo.bean.Audio
|
||||
import com.player.musicoo.bean.Category
|
||||
import com.player.musicoo.bean.ResourcesList
|
||||
import org.json.JSONObject
|
||||
import java.io.File
|
||||
import java.io.InputStream
|
||||
|
||||
fun parseResources(context: Context, jsonString: String): ResourcesList {
|
||||
val jsonObject = JSONObject(jsonString)
|
||||
val categoriesArray = jsonObject.getJSONArray("categories")
|
||||
val categories = mutableListOf<Category>()
|
||||
|
||||
for (i in 0 until categoriesArray.length()) {
|
||||
val categoryObject = categoriesArray.getJSONObject(i)
|
||||
val categoryName = categoryObject.getString("name")
|
||||
val audiosArray = categoryObject.getJSONArray("audios")
|
||||
val audios = mutableListOf<Audio>()
|
||||
|
||||
for (j in 0 until audiosArray.length()) {
|
||||
val audioObject = audiosArray.getJSONObject(j)
|
||||
val audioName = audioObject.getString("name")
|
||||
val audioFile = audioObject.getString("file")
|
||||
val audioImage = audioObject.getString("image")
|
||||
val audio = Audio(
|
||||
audioName,
|
||||
audioFile,
|
||||
audioImage,
|
||||
getAudioDurationFromAssets(context, audioFile),
|
||||
false
|
||||
)
|
||||
audios.add(audio)
|
||||
}
|
||||
|
||||
val category = Category(categoryName, audios)
|
||||
categories.add(category)
|
||||
}
|
||||
|
||||
return ResourcesList(categories)
|
||||
}
|
||||
|
||||
fun getAudioDurationFromAssets(context: Context, fileName: String): Long {
|
||||
val assetFileDescriptor = context.assets.openFd(fileName)
|
||||
val retriever = MediaMetadataRetriever()
|
||||
retriever.setDataSource(
|
||||
assetFileDescriptor.fileDescriptor,
|
||||
assetFileDescriptor.startOffset,
|
||||
assetFileDescriptor.length
|
||||
)
|
||||
|
||||
val durationString = retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_DURATION)
|
||||
val duration = durationString?.toLong() ?: 0
|
||||
|
||||
retriever.release()
|
||||
assetFileDescriptor.close()
|
||||
|
||||
return duration
|
||||
}
|
||||
|
||||
fun containsContent(str: String): Boolean {
|
||||
return str.contains("content://")
|
||||
}
|
||||
|
||||
fun getInputStreamFromUri(context: Context, uri: Uri): InputStream? {
|
||||
return try {
|
||||
context.contentResolver.openInputStream(uri)
|
||||
} catch (e: Exception) {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
fun uriToFile(context: Context, uri: Uri): File? {
|
||||
val contentResolver = context.contentResolver
|
||||
val cursor: Cursor? = contentResolver.query(uri, null, null, null, null)
|
||||
cursor?.use {
|
||||
if (it.moveToFirst()) {
|
||||
val filePathColumn = it.getColumnIndex(MediaStore.Images.Media.DATA)
|
||||
val filePath = it.getString(filePathColumn)
|
||||
return File(filePath)
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
90
app/src/main/java/com/player/musicoo/view/BlurLayout.kt
Normal file
@ -0,0 +1,90 @@
|
||||
package com.player.musicoo.view
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.*
|
||||
import android.renderscript.*
|
||||
import android.util.AttributeSet
|
||||
import android.widget.FrameLayout
|
||||
|
||||
class BlurLayout : FrameLayout {
|
||||
private var blurRadius = 25f // 默认模糊半径
|
||||
private val cornerRadius = dpToPx(18) // 圆角半径
|
||||
constructor(context: Context) : super(context)
|
||||
|
||||
constructor(context: Context, attrs: AttributeSet) : super(context, attrs)
|
||||
|
||||
constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(
|
||||
context,
|
||||
attrs,
|
||||
defStyleAttr
|
||||
)
|
||||
|
||||
override fun dispatchDraw(canvas: Canvas) {
|
||||
// 创建一个 Bitmap 用于绘制原始内容
|
||||
val originalBitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888)
|
||||
val originalCanvas = Canvas(originalBitmap)
|
||||
// 调用父类的 dispatchDraw() 方法,把所有的子 View 绘制到这个 Bitmap 上
|
||||
super.dispatchDraw(originalCanvas)
|
||||
|
||||
// 创建一个 Bitmap 用于绘制模糊效果
|
||||
val blurredBitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888)
|
||||
|
||||
// 使用 RenderScript 处理模糊效果
|
||||
val rsContext = RenderScript.create(context)
|
||||
val input = Allocation.createFromBitmap(rsContext, originalBitmap)
|
||||
val output = Allocation.createTyped(
|
||||
rsContext,
|
||||
Type.createXY(
|
||||
rsContext,
|
||||
Element.RGBA_8888(rsContext),
|
||||
originalBitmap.width,
|
||||
originalBitmap.height
|
||||
)
|
||||
)
|
||||
val script = ScriptIntrinsicBlur.create(rsContext, Element.U8_4(rsContext))
|
||||
script.setInput(input)
|
||||
script.setRadius(blurRadius)
|
||||
script.forEach(output)
|
||||
output.copyTo(blurredBitmap)
|
||||
// 绘制原始内容
|
||||
canvas.drawBitmap(originalBitmap, 0f, 0f, null)
|
||||
|
||||
// 创建一个画笔,用于绘制模糊区域
|
||||
val paint = Paint()
|
||||
paint.xfermode = PorterDuffXfermode(PorterDuff.Mode.SRC_OVER)
|
||||
|
||||
// 定义模糊区域,即底部的 50dp
|
||||
val blurHeight = dpToPx(50).toInt()
|
||||
val blurRect = Rect(0, height - blurHeight, width, height)
|
||||
|
||||
// 绘制模糊效果
|
||||
canvas.drawBitmap(blurredBitmap, blurRect, blurRect, paint)
|
||||
|
||||
// 绘制一个具有指定颜色的矩形作为背景
|
||||
paint.color = Color.parseColor("#99000000") // 半透明黑色
|
||||
paint.xfermode = PorterDuffXfermode(PorterDuff.Mode.SRC_OVER) // 使用 SRC_OVER 模式覆盖绘制
|
||||
paint.isAntiAlias = true
|
||||
val rectF = RectF(blurRect)
|
||||
canvas.drawRect(blurRect,paint)
|
||||
// canvas.drawRoundRect(rectF,cornerRadius, cornerRadius, paint)
|
||||
|
||||
// 定义整个布局的矩形区域
|
||||
val layoutRect = Rect(0, 0, width, height)
|
||||
val rectFAll = RectF(layoutRect)
|
||||
|
||||
paint.color = Color.parseColor("#00000000") // 半透明黑色
|
||||
paint.xfermode = PorterDuffXfermode(PorterDuff.Mode.SRC_OVER) // 使用 SRC_OVER 模式覆盖绘制
|
||||
paint.isAntiAlias = true
|
||||
canvas.drawRoundRect(rectFAll,cornerRadius, cornerRadius, paint)
|
||||
|
||||
input.destroy()
|
||||
output.destroy()
|
||||
script.destroy()
|
||||
rsContext.destroy()
|
||||
}
|
||||
|
||||
private fun dpToPx(dp: Int): Float {
|
||||
val density = resources.displayMetrics.density
|
||||
return (dp * density)
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,73 @@
|
||||
package com.player.musicoo.view
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.Canvas
|
||||
import android.graphics.Paint
|
||||
import android.util.AttributeSet
|
||||
import android.view.View
|
||||
import kotlin.math.min
|
||||
|
||||
class CircularProgressBar(context: Context, attrs: AttributeSet?) : View(context, attrs) {
|
||||
|
||||
private var progressPaint: Paint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
|
||||
style = Paint.Style.STROKE
|
||||
isAntiAlias = true
|
||||
strokeWidth = dpToPx(3) // 设置圆环的宽度为20像素
|
||||
color = 0xFFFFFFFF.toInt() // 设置圆环的颜色为白色
|
||||
}
|
||||
|
||||
private var backgroundPaint: Paint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
|
||||
style = Paint.Style.STROKE
|
||||
isAntiAlias = true
|
||||
strokeWidth = dpToPx(3)
|
||||
color = 0x00000000
|
||||
}
|
||||
|
||||
private var progress: Long = 0 // 进度值,默认为0
|
||||
private var maxProgress: Long = 100 // 最大进度值,默认为100
|
||||
|
||||
override fun onDraw(canvas: Canvas) {
|
||||
super.onDraw(canvas)
|
||||
|
||||
val centerX = width / 2f
|
||||
val centerY = height / 2f
|
||||
val radius = (min(width, height) - progressPaint.strokeWidth) / 2f
|
||||
|
||||
// 绘制圆环背景
|
||||
canvas.drawCircle(centerX, centerY, radius, backgroundPaint)
|
||||
|
||||
// 绘制圆环进度
|
||||
val startAngle = -90f
|
||||
val sweepAngle = -360f * progress / maxProgress
|
||||
canvas.drawArc(
|
||||
centerX - radius,
|
||||
centerY - radius,
|
||||
centerX + radius,
|
||||
centerY + radius,
|
||||
startAngle,
|
||||
sweepAngle,
|
||||
false,
|
||||
progressPaint
|
||||
)
|
||||
}
|
||||
|
||||
fun setProgress(progress: Long) {
|
||||
// 确保进度值在有效范围内
|
||||
this.progress = progress.coerceIn(0, maxProgress)
|
||||
invalidate() // 重新绘制视图
|
||||
}
|
||||
|
||||
fun setMaxProgress(maxProgress: Long) {
|
||||
// 设置最大进度值
|
||||
this.maxProgress = maxProgress
|
||||
// 更新当前进度,确保当前进度在有效范围内
|
||||
this.progress = this.progress.coerceIn(0, maxProgress)
|
||||
invalidate() // 重新绘制视图
|
||||
}
|
||||
|
||||
private fun dpToPx(dp: Int): Float {
|
||||
val density = resources.displayMetrics.density
|
||||
return (dp * density)
|
||||
}
|
||||
}
|
||||
|
||||
@ -0,0 +1,62 @@
|
||||
package com.player.musicoo.view
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.Canvas
|
||||
import android.graphics.Color
|
||||
import android.graphics.LinearGradient
|
||||
import android.graphics.Paint
|
||||
import android.graphics.RectF
|
||||
import android.graphics.Shader
|
||||
import android.util.AttributeSet
|
||||
import android.view.View
|
||||
|
||||
class CustomProgressBar(context: Context, attrs: AttributeSet?) : View(context, attrs) {
|
||||
private var progress = 0 // 当前进度
|
||||
private val maxProgress = 100 // 最大进度
|
||||
private val progressBarHeight = 20f // 进度条高度
|
||||
private val cornerRadius = 10f // 圆角半径
|
||||
private val backgroundColor = Color.parseColor("#26FFFFFF")
|
||||
private val startColor = Color.parseColor("#FF1CC8EE") // 起始颜色
|
||||
private val middleColor = Color.parseColor("#FF69FE73") // 中间颜色
|
||||
private val endColor = Color.parseColor("#FFCBD64B") // 结束颜色
|
||||
private val paint = Paint()
|
||||
private val paintTow = Paint()
|
||||
|
||||
init {
|
||||
paint.style = Paint.Style.FILL
|
||||
paint.isAntiAlias = true
|
||||
paintTow.style = Paint.Style.FILL
|
||||
paintTow.isAntiAlias = true
|
||||
}
|
||||
|
||||
override fun onDraw(canvas: Canvas) {
|
||||
super.onDraw(canvas)
|
||||
// 绘制底色矩形
|
||||
paint.shader = null // 重置着色器
|
||||
paint.color = backgroundColor
|
||||
val backgroundRect = RectF(0f, (height / 2 - progressBarHeight / 2), width.toFloat(), (height / 2 + progressBarHeight / 2))
|
||||
canvas.drawRoundRect(backgroundRect, cornerRadius, cornerRadius, paint)
|
||||
|
||||
|
||||
// 计算进度条的宽度
|
||||
val progressBarWidth = (width * progress.toFloat() / maxProgress).toInt()
|
||||
|
||||
// 创建颜色渐变对象
|
||||
val gradient = LinearGradient(0f, 0f, width.toFloat(), 0f, intArrayOf(startColor, middleColor, endColor), null, Shader.TileMode.CLAMP)
|
||||
paintTow.shader = gradient
|
||||
|
||||
// 绘制带圆角的进度条矩形
|
||||
val rect = RectF(0f, (height / 2 - progressBarHeight / 2), progressBarWidth.toFloat(), (height / 2 + progressBarHeight / 2))
|
||||
canvas.drawRoundRect(rect, cornerRadius, cornerRadius, paintTow)
|
||||
}
|
||||
|
||||
fun getProgress():Int{
|
||||
return progress
|
||||
}
|
||||
|
||||
// 设置进度
|
||||
fun setProgress(progress: Int) {
|
||||
this.progress = progress
|
||||
invalidate() // 请求重绘
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,34 @@
|
||||
package com.player.musicoo.view;
|
||||
|
||||
import android.content.Context;
|
||||
import android.text.TextUtils;
|
||||
import android.util.AttributeSet;
|
||||
|
||||
public class MarqueeTextView extends androidx.appcompat.widget.AppCompatTextView {
|
||||
|
||||
public MarqueeTextView(Context context) {
|
||||
super(context);
|
||||
initView(context);
|
||||
}
|
||||
|
||||
public MarqueeTextView(Context context, AttributeSet attrs) {
|
||||
super(context, attrs);
|
||||
initView(context);
|
||||
}
|
||||
|
||||
public MarqueeTextView(Context context, AttributeSet attrs, int defStyleAttr) {
|
||||
super(context, attrs, defStyleAttr);
|
||||
initView(context);
|
||||
}
|
||||
|
||||
private void initView(Context context) {
|
||||
this.setEllipsize(TextUtils.TruncateAt.MARQUEE);
|
||||
this.setSingleLine(true);
|
||||
this.setMarqueeRepeatLimit(-1);
|
||||
}
|
||||
|
||||
//最关键的部分
|
||||
public boolean isFocused() {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
47
app/src/main/java/com/player/musicoo/view/RadiusLayout.kt
Normal file
@ -0,0 +1,47 @@
|
||||
package com.player.musicoo.view
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.*
|
||||
import android.renderscript.*
|
||||
import android.util.AttributeSet
|
||||
import android.widget.FrameLayout
|
||||
import android.widget.RelativeLayout
|
||||
|
||||
class RadiusLayout : RelativeLayout {
|
||||
private val cornerRadius = dpToPx(18) // 圆角半径
|
||||
constructor(context: Context) : super(context)
|
||||
|
||||
constructor(context: Context, attrs: AttributeSet) : super(context, attrs)
|
||||
|
||||
constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(
|
||||
context,
|
||||
attrs,
|
||||
defStyleAttr
|
||||
)
|
||||
|
||||
override fun dispatchDraw(canvas: Canvas) {
|
||||
// 创建一个 Bitmap 用于绘制原始内容
|
||||
val originalBitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888)
|
||||
val originalCanvas = Canvas(originalBitmap)
|
||||
// 调用父类的 dispatchDraw() 方法,把所有的子 View 绘制到这个 Bitmap 上
|
||||
super.dispatchDraw(originalCanvas)
|
||||
// 绘制原始内容
|
||||
canvas.drawBitmap(originalBitmap, 0f, 0f, null)
|
||||
// 创建一个画笔
|
||||
val paint = Paint()
|
||||
paint.xfermode = PorterDuffXfermode(PorterDuff.Mode.SRC_OVER)
|
||||
|
||||
// 定义整个布局的矩形区域
|
||||
val layoutRect = Rect(0, 0, width, height)
|
||||
val rectF = RectF(layoutRect)
|
||||
// 绘制具有圆角的矩形作为背景
|
||||
paint.color = Color.parseColor("#CCffffff") // 半透明黑色
|
||||
paint.isAntiAlias = true
|
||||
canvas.drawRoundRect(rectF, cornerRadius, cornerRadius, paint)
|
||||
}
|
||||
|
||||
private fun dpToPx(dp: Int): Float {
|
||||
val density = resources.displayMetrics.density
|
||||
return (dp * density)
|
||||
}
|
||||
}
|
||||
7
app/src/main/res/anim/no_animation.xml
Normal file
@ -0,0 +1,7 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<set xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<translate
|
||||
android:fromYDelta="-100%"
|
||||
android:toYDelta="0%"
|
||||
android:duration="500"/>
|
||||
</set>
|
||||
7
app/src/main/res/anim/slide_down.xml
Normal file
@ -0,0 +1,7 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<set xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<translate
|
||||
android:fromYDelta="0%"
|
||||
android:toYDelta="100%"
|
||||
android:duration="500"/>
|
||||
</set>
|
||||
7
app/src/main/res/anim/slide_up.xml
Normal file
@ -0,0 +1,7 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<set xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<translate
|
||||
android:fromYDelta="100%"
|
||||
android:toYDelta="0%"
|
||||
android:duration="500"/>
|
||||
</set>
|
||||
16
app/src/main/res/drawable/about_icon.xml
Normal file
@ -0,0 +1,16 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="20dp"
|
||||
android:height="20dp"
|
||||
android:viewportWidth="20"
|
||||
android:viewportHeight="20">
|
||||
<group>
|
||||
<clip-path
|
||||
android:pathData="M0,0h20v20h-20z"/>
|
||||
<path
|
||||
android:pathData="M9.886,13.773L14.034,7.136L12.761,6.341L9.615,11.376L8.86,7.602C8.826,7.433 8.734,7.28 8.6,7.17C8.466,7.06 8.298,7 8.125,7H6.25V8.5H7.51L8.515,13.523C8.545,13.67 8.618,13.805 8.725,13.91C8.832,14.015 8.968,14.086 9.116,14.113C9.264,14.14 9.416,14.122 9.553,14.061C9.691,14 9.807,13.9 9.886,13.773Z"
|
||||
android:fillColor="#80F988"/>
|
||||
<path
|
||||
android:pathData="M10,19.375C11.231,19.375 12.45,19.132 13.588,18.661C14.725,18.19 15.759,17.5 16.629,16.629C17.5,15.759 18.19,14.725 18.661,13.588C19.132,12.45 19.375,11.231 19.375,10C19.375,8.769 19.132,7.55 18.661,6.412C18.19,5.275 17.5,4.241 16.629,3.371C15.759,2.5 14.725,1.81 13.588,1.339C12.45,0.867 11.231,0.625 10,0.625C7.514,0.625 5.129,1.613 3.371,3.371C1.613,5.129 0.625,7.514 0.625,10C0.625,12.486 1.613,14.871 3.371,16.629C5.129,18.387 7.514,19.375 10,19.375ZM10,18.125C7.845,18.125 5.778,17.269 4.255,15.745C2.731,14.222 1.875,12.155 1.875,10C1.875,7.845 2.731,5.778 4.255,4.255C5.778,2.731 7.845,1.875 10,1.875C12.155,1.875 14.222,2.731 15.745,4.255C17.269,5.778 18.125,7.845 18.125,10C18.125,12.155 17.269,14.222 15.745,15.745C14.222,17.269 12.155,18.125 10,18.125Z"
|
||||
android:fillColor="#80F988"/>
|
||||
</group>
|
||||
</vector>
|
||||
13
app/src/main/res/drawable/add_icon.xml
Normal file
@ -0,0 +1,13 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<group>
|
||||
<clip-path
|
||||
android:pathData="M0,0h24v24h-24z"/>
|
||||
<path
|
||||
android:pathData="M12.04,2.182C12.401,2.182 12.748,2.325 13.004,2.581C13.26,2.837 13.403,3.184 13.403,3.545L13.403,10.734H21C21.361,10.734 21.708,10.878 21.964,11.133C22.22,11.389 22.364,11.736 22.364,12.098C22.364,12.459 22.22,12.806 21.964,13.062C21.708,13.318 21.361,13.461 21,13.461H13.403V21C13.403,21.362 13.259,21.708 13.003,21.964C12.748,22.22 12.401,22.364 12.039,22.364C11.677,22.364 11.331,22.22 11.075,21.964C10.819,21.708 10.675,21.362 10.675,21V13.461H3.545C3.184,13.461 2.837,13.318 2.581,13.062C2.325,12.806 2.182,12.459 2.182,12.098C2.182,11.736 2.325,11.389 2.581,11.133C2.837,10.878 3.184,10.734 3.545,10.734H10.675V3.545C10.675,3.184 10.819,2.837 11.075,2.581C11.331,2.325 11.677,2.182 12.039,2.182H12.04Z"
|
||||
android:fillColor="#282C33"/>
|
||||
</group>
|
||||
</vector>
|
||||
33
app/src/main/res/drawable/alarm_clock_icon.xml
Normal file
@ -0,0 +1,33 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="34dp"
|
||||
android:height="34dp"
|
||||
android:viewportWidth="34"
|
||||
android:viewportHeight="34">
|
||||
<path
|
||||
android:pathData="M17,31.403C24.172,31.403 29.986,25.589 29.986,18.417C29.986,11.245 24.172,5.43 17,5.43C9.828,5.43 4.014,11.245 4.014,18.417C4.014,25.589 9.828,31.403 17,31.403Z"
|
||||
android:strokeLineJoin="round"
|
||||
android:strokeWidth="1.66667"
|
||||
android:fillColor="#333333"
|
||||
android:strokeColor="#333333"/>
|
||||
<path
|
||||
android:pathData="M16.83,10.875L16.829,18.673L22.334,24.179"
|
||||
android:strokeLineJoin="round"
|
||||
android:strokeWidth="2"
|
||||
android:fillColor="#00000000"
|
||||
android:strokeColor="#ffffff"
|
||||
android:strokeLineCap="round"/>
|
||||
<path
|
||||
android:pathData="M2.833,6.375L7.792,2.833"
|
||||
android:strokeLineJoin="round"
|
||||
android:strokeWidth="2"
|
||||
android:fillColor="#00000000"
|
||||
android:strokeColor="#333333"
|
||||
android:strokeLineCap="round"/>
|
||||
<path
|
||||
android:pathData="M31.167,6.375L26.208,2.833"
|
||||
android:strokeLineJoin="round"
|
||||
android:strokeWidth="2"
|
||||
android:fillColor="#00000000"
|
||||
android:strokeColor="#333333"
|
||||
android:strokeLineCap="round"/>
|
||||
</vector>
|
||||
13
app/src/main/res/drawable/arrow_bottom_icon.xml
Normal file
@ -0,0 +1,13 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="20dp"
|
||||
android:height="20dp"
|
||||
android:viewportWidth="20"
|
||||
android:viewportHeight="20">
|
||||
<path
|
||||
android:pathData="M15.833,7.083L10,12.917L4.167,7.083"
|
||||
android:strokeLineJoin="round"
|
||||
android:strokeWidth="1.5"
|
||||
android:fillColor="#00000000"
|
||||
android:strokeColor="#ffffff"
|
||||
android:strokeLineCap="round"/>
|
||||
</vector>
|
||||
13
app/src/main/res/drawable/back_icon.xml
Normal file
@ -0,0 +1,13 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="20dp"
|
||||
android:height="20dp"
|
||||
android:viewportWidth="20"
|
||||
android:viewportHeight="20">
|
||||
<path
|
||||
android:pathData="M12.917,15.833L7.083,10L12.917,4.167"
|
||||
android:strokeLineJoin="round"
|
||||
android:strokeWidth="1.5"
|
||||
android:fillColor="#00000000"
|
||||
android:strokeColor="#ffffff"
|
||||
android:strokeLineCap="round"/>
|
||||
</vector>
|
||||
9
app/src/main/res/drawable/drw_add_bg.xml
Normal file
@ -0,0 +1,9 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:shape="oval">
|
||||
|
||||
<size
|
||||
android:width="58dp"
|
||||
android:height="58dp" />
|
||||
<solid android:color="#FF80F988" />
|
||||
</shape>
|
||||
14
app/src/main/res/drawable/drw_back_bg.xml
Normal file
@ -0,0 +1,14 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:shape="oval">
|
||||
|
||||
|
||||
<stroke
|
||||
android:width="1dp"
|
||||
android:color="#4DFFFFFF" />
|
||||
|
||||
<size
|
||||
android:width="42dp"
|
||||
android:height="42dp" />
|
||||
<solid android:color="#26FFFFFF" />
|
||||
</shape>
|
||||
8
app/src/main/res/drawable/drw_details_bg.xml
Normal file
@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:shape="rectangle">
|
||||
|
||||
<gradient android:startColor="#26000000"
|
||||
android:endColor="#000000"
|
||||
android:angle="270"/>
|
||||
</shape>
|
||||
8
app/src/main/res/drawable/drw_launch_bg.xml
Normal file
@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:shape="rectangle">
|
||||
|
||||
<gradient android:startColor="#26000000"
|
||||
android:endColor="#8080F988"
|
||||
android:angle="270"/>
|
||||
</shape>
|
||||
11
app/src/main/res/drawable/drw_main_bottom_bg.xml
Normal file
@ -0,0 +1,11 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:shape="rectangle">
|
||||
|
||||
<corners
|
||||
android:topLeftRadius="24dp"
|
||||
android:topRightRadius="24dp" />
|
||||
<gradient android:startColor="#3A3D3B"
|
||||
android:endColor="#445145"
|
||||
android:angle="270"/>
|
||||
</shape>
|
||||
8
app/src/main/res/drawable/drw_play_layout_bg.xml
Normal file
@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:shape="rectangle">
|
||||
|
||||
<solid android:color="#FF80F988" />
|
||||
<corners android:radius="36dp" />
|
||||
|
||||
</shape>
|
||||
@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:shape="rectangle">
|
||||
<corners
|
||||
android:bottomLeftRadius="18dp"
|
||||
android:bottomRightRadius="18dp" />
|
||||
<solid android:color="#CC000000" />
|
||||
</shape>
|
||||
9
app/src/main/res/drawable/drw_round_48_bg.xml
Normal file
@ -0,0 +1,9 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:shape="oval">
|
||||
|
||||
<solid android:color="#26FFFFFF" />
|
||||
<size
|
||||
android:width="48dp"
|
||||
android:height="48dp" />
|
||||
</shape>
|
||||
8
app/src/main/res/drawable/drw_round_stroke_bg.xml
Normal file
@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:shape="oval">
|
||||
|
||||
<stroke
|
||||
android:width="1dp"
|
||||
android:color="#26FFFFFF" />
|
||||
</shape>
|
||||
9
app/src/main/res/drawable/feedback_icon.xml
Normal file
@ -0,0 +1,9 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="20dp"
|
||||
android:height="20dp"
|
||||
android:viewportWidth="20"
|
||||
android:viewportHeight="20">
|
||||
<path
|
||||
android:pathData="M16.695,3.891H3.305C2.629,3.891 2.08,4.439 2.08,5.115V15.354C2.08,16.029 2.629,16.578 3.305,16.578H16.695C17.371,16.578 17.92,16.029 17.92,15.354V5.115C17.92,4.441 17.371,3.891 16.695,3.891ZM16.518,5.293V6.383C15.158,8.682 12.67,10.106 10,10.106C7.529,10.106 5.207,8.895 3.791,6.865C3.703,6.74 3.619,6.611 3.539,6.48C3.52,6.449 3.502,6.414 3.482,6.383V5.293H16.518ZM3.482,15.176V8.693C5.166,10.473 7.518,11.508 10,11.508C12.488,11.508 14.842,10.461 16.518,8.691V15.176H3.482Z"
|
||||
android:fillColor="#80F988"/>
|
||||
</vector>
|
||||
15
app/src/main/res/drawable/home_select_icon.xml
Normal file
@ -0,0 +1,15 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="36dp"
|
||||
android:height="36dp"
|
||||
android:viewportWidth="36"
|
||||
android:viewportHeight="36">
|
||||
<path
|
||||
android:strokeWidth="1"
|
||||
android:pathData="M18,18m-17.5,0a17.5,17.5 0,1 1,35 0a17.5,17.5 0,1 1,-35 0"
|
||||
android:fillColor="#00000000"
|
||||
android:strokeColor="#80F988"/>
|
||||
<path
|
||||
android:pathData="M11.342,9.793C8.231,12.053 6.676,13.183 6.065,14.852C6.016,14.986 5.972,15.122 5.933,15.259C5.446,16.969 6.04,18.797 7.228,22.455C8.416,26.112 9.011,27.94 10.41,29.037C10.522,29.125 10.637,29.209 10.756,29.288C12.231,30.28 14.154,30.28 18,30.28C21.845,30.28 23.768,30.28 25.244,29.288C25.362,29.209 25.478,29.125 25.59,29.037C26.989,27.94 27.583,26.112 28.771,22.455C29.96,18.797 30.554,16.969 30.067,15.259C30.028,15.122 29.984,14.986 29.935,14.852C29.324,13.183 27.768,12.053 24.657,9.793C21.546,7.533 19.99,6.402 18.214,6.337C18.071,6.332 17.928,6.332 17.786,6.337C16.009,6.402 14.454,7.533 11.342,9.793ZM15.666,23.913C15.183,23.913 14.792,24.304 14.792,24.788C14.792,25.271 15.183,25.663 15.666,25.663H20.333C20.816,25.663 21.208,25.271 21.208,24.788C21.208,24.304 20.816,23.913 20.333,23.913H15.666Z"
|
||||
android:fillColor="#80F988"
|
||||
android:fillType="evenOdd"/>
|
||||
</vector>
|
||||
15
app/src/main/res/drawable/home_unselect_icon.xml
Normal file
@ -0,0 +1,15 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="36dp"
|
||||
android:height="36dp"
|
||||
android:viewportWidth="36"
|
||||
android:viewportHeight="36">
|
||||
<path
|
||||
android:strokeWidth="1"
|
||||
android:pathData="M18,18m-17.5,0a17.5,17.5 0,1 1,35 0a17.5,17.5 0,1 1,-35 0"
|
||||
android:fillColor="#00000000"
|
||||
android:strokeColor="#9C9D9D"/>
|
||||
<path
|
||||
android:pathData="M11.342,9.793C8.231,12.053 6.676,13.183 6.065,14.852C6.016,14.986 5.972,15.122 5.933,15.259C5.446,16.969 6.04,18.797 7.228,22.455C8.416,26.112 9.011,27.94 10.41,29.037C10.522,29.125 10.637,29.209 10.756,29.288C12.231,30.28 14.154,30.28 18,30.28C21.845,30.28 23.768,30.28 25.244,29.288C25.362,29.209 25.478,29.125 25.59,29.037C26.989,27.94 27.583,26.112 28.771,22.455C29.96,18.797 30.554,16.969 30.067,15.259C30.028,15.122 29.984,14.986 29.935,14.852C29.324,13.183 27.768,12.053 24.657,9.793C21.546,7.533 19.99,6.402 18.214,6.337C18.071,6.332 17.928,6.332 17.786,6.337C16.009,6.402 14.454,7.533 11.342,9.793ZM15.666,23.913C15.183,23.913 14.792,24.304 14.792,24.788C14.792,25.271 15.183,25.663 15.666,25.663H20.333C20.816,25.663 21.208,25.271 21.208,24.788C21.208,24.304 20.816,23.913 20.333,23.913H15.666Z"
|
||||
android:fillColor="#9C9D9D"
|
||||
android:fillType="evenOdd"/>
|
||||
</vector>
|
||||
170
app/src/main/res/drawable/ic_launcher_background.xml
Normal file
@ -0,0 +1,170 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="108dp"
|
||||
android:height="108dp"
|
||||
android:viewportWidth="108"
|
||||
android:viewportHeight="108">
|
||||
<path
|
||||
android:fillColor="#3DDC84"
|
||||
android:pathData="M0,0h108v108h-108z" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M9,0L9,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M19,0L19,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M29,0L29,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M39,0L39,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M49,0L49,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M59,0L59,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M69,0L69,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M79,0L79,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M89,0L89,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M99,0L99,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,9L108,9"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,19L108,19"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,29L108,29"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,39L108,39"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,49L108,49"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,59L108,59"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,69L108,69"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,79L108,79"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,89L108,89"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,99L108,99"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M19,29L89,29"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M19,39L89,39"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M19,49L89,49"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M19,59L89,59"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M19,69L89,69"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M19,79L89,79"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M29,19L29,89"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M39,19L39,89"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M49,19L49,89"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M59,19L59,89"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M69,19L69,89"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M79,19L79,89"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
</vector>
|
||||
30
app/src/main/res/drawable/ic_launcher_foreground.xml
Normal file
@ -0,0 +1,30 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:aapt="http://schemas.android.com/aapt"
|
||||
android:width="108dp"
|
||||
android:height="108dp"
|
||||
android:viewportWidth="108"
|
||||
android:viewportHeight="108">
|
||||
<path android:pathData="M31,63.928c0,0 6.4,-11 12.1,-13.1c7.2,-2.6 26,-1.4 26,-1.4l38.1,38.1L107,108.928l-32,-1L31,63.928z">
|
||||
<aapt:attr name="android:fillColor">
|
||||
<gradient
|
||||
android:endX="85.84757"
|
||||
android:endY="92.4963"
|
||||
android:startX="42.9492"
|
||||
android:startY="49.59793"
|
||||
android:type="linear">
|
||||
<item
|
||||
android:color="#44000000"
|
||||
android:offset="0.0" />
|
||||
<item
|
||||
android:color="#00000000"
|
||||
android:offset="1.0" />
|
||||
</gradient>
|
||||
</aapt:attr>
|
||||
</path>
|
||||
<path
|
||||
android:fillColor="#FFFFFF"
|
||||
android:fillType="nonZero"
|
||||
android:pathData="M65.3,45.828l3.8,-6.6c0.2,-0.4 0.1,-0.9 -0.3,-1.1c-0.4,-0.2 -0.9,-0.1 -1.1,0.3l-3.9,6.7c-6.3,-2.8 -13.4,-2.8 -19.7,0l-3.9,-6.7c-0.2,-0.4 -0.7,-0.5 -1.1,-0.3C38.8,38.328 38.7,38.828 38.9,39.228l3.8,6.6C36.2,49.428 31.7,56.028 31,63.928h46C76.3,56.028 71.8,49.428 65.3,45.828zM43.4,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2c-0.3,-0.7 -0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C45.3,56.528 44.5,57.328 43.4,57.328L43.4,57.328zM64.6,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2s-0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C66.5,56.528 65.6,57.328 64.6,57.328L64.6,57.328z"
|
||||
android:strokeWidth="1"
|
||||
android:strokeColor="#00000000" />
|
||||
</vector>
|
||||