Ver Fonte

fix: LocalMusicFragment崩溃

wutiaorong há 11 meses atrás
pai
commit
25dfe2d689

+ 2 - 2
module/music/src/main/java/com/adealink/weparty/music/MusicServiceImpl.kt

@@ -8,7 +8,7 @@ import com.adealink.weparty.module.music.IMusicService
 import com.adealink.weparty.module.music.data.MusicEntrance
 import com.adealink.weparty.module.music.data.StopMusicReason
 import com.adealink.weparty.module.music.viewmodel.IMusicViewModel
-import com.adealink.weparty.music.local.LocalMusicFragment
+import com.adealink.weparty.music.local.LocalMusicDialog
 import com.adealink.weparty.music.manager.musicManager
 import com.adealink.weparty.music.viewmodel.MusicViewModel
 
@@ -38,7 +38,7 @@ class MusicServiceImpl : IMusicService {
     }
 
     override fun getLocalMusicFragment(): DialogFragment? {
-        return LocalMusicFragment.newInstance(true)
+        return LocalMusicDialog.newInstance()
     }
 
     override fun getService(): IMusicService {

+ 253 - 0
module/music/src/main/java/com/adealink/weparty/music/local/LocalMusicDialog.kt

@@ -0,0 +1,253 @@
+package com.adealink.weparty.music.local
+
+import android.animation.ObjectAnimator
+import android.animation.ValueAnimator
+import android.content.Intent
+import android.media.MediaScannerConnection
+import android.net.Uri
+import android.os.Bundle
+import android.view.View
+import android.view.animation.LinearInterpolator
+import androidx.activity.result.ActivityResult
+import androidx.activity.result.contract.ActivityResultContracts
+import androidx.annotation.OptIn
+import androidx.appcompat.app.AppCompatActivity.RESULT_OK
+import androidx.constraintlayout.widget.ConstraintLayout
+import androidx.core.view.updateLayoutParams
+import androidx.fragment.app.viewModels
+import androidx.media3.common.MimeTypes
+import androidx.media3.common.MimeTypes.BASE_TYPE_AUDIO
+import androidx.media3.common.util.UnstableApi
+import androidx.recyclerview.widget.LinearLayoutManager
+import androidx.recyclerview.widget.RecyclerView
+import com.adealink.frame.base.Rlt
+import com.adealink.frame.base.fastLazy
+import com.adealink.frame.ext.isViewBindingValid
+import com.adealink.frame.log.Log
+import com.adealink.frame.mvvm.view.viewBinding
+import com.adealink.frame.util.getPathFromUri
+import com.adealink.frame.util.runOnUiThread
+import com.adealink.weparty.commonui.ext.gone
+import com.adealink.weparty.commonui.ext.show
+import com.adealink.weparty.commonui.recycleview.adapter.ExtMultiTypeAdapter
+import com.adealink.weparty.commonui.toast.util.showFailedToast
+import com.adealink.weparty.commonui.widget.BottomDialogFragment
+import com.adealink.weparty.module.music.TAG_MUSIC
+import com.adealink.weparty.module.music.data.MusicItem
+import com.adealink.weparty.music.ISearchMusicFragment
+import com.adealink.weparty.music.R
+import com.adealink.weparty.music.databinding.DialogLocalMusicBinding
+import com.adealink.weparty.music.databinding.FragmentLocalMusicBinding
+import com.adealink.weparty.music.listener.OnMusicItemListener
+import com.adealink.weparty.music.local.adapter.LocalMusicItemViewBinder
+import com.adealink.weparty.music.viewmodel.MusicViewModel
+
+class LocalMusicDialog : BottomDialogFragment(R.layout.dialog_local_music),
+    ISearchMusicFragment,
+    OnMusicItemListener {
+
+    companion object {
+
+        fun newInstance(): LocalMusicDialog {
+            return LocalMusicDialog()
+        }
+    }
+
+    private val binding by viewBinding(DialogLocalMusicBinding::bind)
+    private val musicViewModel by viewModels<MusicViewModel>({ requireActivity() })
+    private val adapter by fastLazy { ExtMultiTypeAdapter() }
+    private var scanAnim: ValueAnimator? = null
+    override var searchMode: Boolean = false
+
+    override fun initViews() {
+        super.initViews()
+        binding.backBtn.setOnClickListener {
+            dismiss()
+        }
+        binding.scanBtn.setOnClickListener {
+            startScanAnim()
+
+            openFilePicker()
+        }
+        binding.musicList.itemAnimator = null
+        binding.musicList.layoutManager = LinearLayoutManager(context, RecyclerView.VERTICAL, false)
+        adapter.register(LocalMusicItemViewBinder(this))
+        binding.musicList.adapter = adapter
+    }
+
+    private val filePickerLauncher =
+        registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result: ActivityResult ->
+            if (result.resultCode == RESULT_OK) {
+                val data = result.data
+                if (data != null) {
+                    handleFilePickerResult(data)
+                }
+                return@registerForActivityResult
+            }
+
+            musicViewModel.getLocalMusic(true).observe(viewLifecycleOwner) {
+                stopScanAnim()
+            }
+        }
+
+    private fun openFilePicker() {
+        val intent = Intent(Intent.ACTION_OPEN_DOCUMENT)
+        intent.setType("audio/*")
+        intent.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, true)
+        intent.addCategory(Intent.CATEGORY_OPENABLE)
+        filePickerLauncher.launch(intent)
+    }
+
+    private fun handleFilePickerResult(data: Intent) {
+        val uris: MutableList<Uri> = ArrayList()
+        if (data.clipData != null) {
+            val count = data.clipData!!.itemCount
+            for (i in 0 until count) {
+                val fileUri = data.clipData!!.getItemAt(i).uri
+                uris.add(fileUri)
+            }
+        } else if (data.data != null) {
+            val fileUri = data.data ?: return
+            uris.add(fileUri)
+        }
+
+        // 扫描选定的文件
+        scanFiles(uris.toList())
+    }
+
+    @OptIn(UnstableApi::class)
+    private fun scanFiles(uris: List<Uri>) {
+        val paths = arrayOfNulls<String>(uris.size)
+        for (i in uris.indices) {
+            val uri = uris[i]
+            Log.v(TAG_MUSIC, "scanFile: $uri")
+            val path: String = getPathFromUri(requireContext(), uri).toString()
+            paths[i] = path
+        }
+        var count = 0
+        MediaScannerConnection.scanFile(
+            context,
+            paths,
+            arrayOf(MimeTypes.normalizeMimeType(BASE_TYPE_AUDIO))
+        ) { path, uri ->
+            Log.v(TAG_MUSIC, "onScanCompleted: $path $uri")
+            count++
+            if (count == paths.size) {
+                runOnUiThread {
+                    if (!isViewBindingValid()) {
+                        return@runOnUiThread
+                    }
+                    musicViewModel.getLocalMusic(true).observe(viewLifecycleOwner) {
+                        stopScanAnim()
+                    }
+                }
+            }
+        }
+
+    }
+
+    override fun observeViewModel() {
+        super.observeViewModel()
+        musicViewModel.localMusicLD.observe(viewLifecycleOwner) {
+            updateList(it)
+        }
+        musicViewModel.localMusicItemChangedLD.observe(viewLifecycleOwner) {
+            adapter.notifyItemChanged(it) { old, new -> old.audioId == new.audioId }
+        }
+    }
+
+    override fun loadData() {
+        super.loadData()
+        musicViewModel.getLocalMusic(false)
+    }
+
+    override fun onAddClick(item: MusicItem) {
+        super.onAddClick(item)
+        musicViewModel.addToMyMusic(item).observe(viewLifecycleOwner) {
+            showFailedToast(it)
+        }
+    }
+
+    override fun onRemoveClick(item: MusicItem) {
+        super.onRemoveClick(item)
+        musicViewModel.removeFromMyMusic(item)
+    }
+
+    private fun showEmpty() {
+        binding.musicList.visibility = View.GONE
+        binding.emptyView.visibility = View.VISIBLE
+        binding.emptyView.show(com.adealink.weparty.R.drawable.common_list_empty_ic, R.string.music_empty)
+    }
+
+    private fun showContent() {
+        binding.musicList.visibility = View.VISIBLE
+        binding.emptyView.visibility = View.GONE
+    }
+
+    private fun startScanAnim() {
+        if (!isViewBindingValid()) {
+            return
+        }
+        if (scanAnim == null) {
+            scanAnim = ObjectAnimator.ofFloat(binding.scanIcon, "rotation", 0f, 360f)
+            scanAnim?.duration = 200
+            scanAnim?.repeatMode = ValueAnimator.RESTART
+            scanAnim?.repeatCount = ValueAnimator.INFINITE
+            scanAnim?.interpolator = LinearInterpolator()
+        }
+        scanAnim?.start()
+    }
+
+    private fun stopScanAnim() {
+        scanAnim?.end()
+    }
+
+    private fun clearAdapter() {
+        updateList(arrayListOf())
+    }
+
+    private fun updateList(list: List<MusicItem>) {
+        if (list.isEmpty()) {
+            showEmpty()
+        } else {
+            showContent()
+        }
+        adapter.items = list
+        adapter.notifyDataSetChanged()
+    }
+
+    override fun search(keyword: String) {
+        if (!isViewCreated()) {
+            return
+        }
+
+        if (!searchMode) {
+            clearAdapter()
+        }
+        searchMode = true
+        musicViewModel.searchLocalMusic(keyword).observe(viewLifecycleOwner) {
+            if (!searchMode) {
+                return@observe
+            }
+
+            if (it is Rlt.Success) {
+                updateList(it.data)
+            }
+        }
+    }
+
+    override fun finishSearch() {
+        if (!searchMode) {
+            return
+        }
+
+        searchMode = false
+        if (!isViewCreated()) {
+            return
+        }
+
+        clearAdapter()
+        musicViewModel.getLocalMusic(false)
+    }
+
+}

+ 3 - 31
module/music/src/main/java/com/adealink/weparty/music/local/LocalMusicFragment.kt

@@ -5,15 +5,12 @@ import android.animation.ValueAnimator
 import android.content.Intent
 import android.media.MediaScannerConnection
 import android.net.Uri
-import android.os.Bundle
 import android.view.View
 import android.view.animation.LinearInterpolator
 import androidx.activity.result.ActivityResult
 import androidx.activity.result.contract.ActivityResultContracts
 import androidx.annotation.OptIn
 import androidx.appcompat.app.AppCompatActivity.RESULT_OK
-import androidx.constraintlayout.widget.ConstraintLayout
-import androidx.core.view.updateLayoutParams
 import androidx.fragment.app.viewModels
 import androidx.media3.common.MimeTypes
 import androidx.media3.common.MimeTypes.BASE_TYPE_AUDIO
@@ -28,11 +25,8 @@ import com.adealink.frame.mvvm.view.viewBinding
 import com.adealink.frame.util.getPathFromUri
 import com.adealink.frame.util.runOnUiThread
 import com.adealink.weparty.commonui.BaseFragment
-import com.adealink.weparty.commonui.ext.gone
-import com.adealink.weparty.commonui.ext.show
 import com.adealink.weparty.commonui.recycleview.adapter.ExtMultiTypeAdapter
 import com.adealink.weparty.commonui.toast.util.showFailedToast
-import com.adealink.weparty.commonui.widget.BottomDialogFragment
 import com.adealink.weparty.module.music.TAG_MUSIC
 import com.adealink.weparty.module.music.data.MusicItem
 import com.adealink.weparty.music.ISearchMusicFragment
@@ -47,19 +41,12 @@ import com.adealink.weparty.R as APP_R
 /**
  * Created by sunxiaodong on 2021/7/15.
  */
-class LocalMusicFragment : BottomDialogFragment(R.layout.fragment_local_music), ISearchMusicFragment,
+class LocalMusicFragment : BaseFragment(R.layout.fragment_local_music), ISearchMusicFragment,
     OnMusicItemListener {
 
     companion object {
-
-        private const val EXTRA_WITH_BACK_NAV = "extra_with_back_nav"
-
-        fun newInstance(withBackNav: Boolean = false): LocalMusicFragment {
-            return LocalMusicFragment().apply {
-                arguments = Bundle().apply {
-                    putBoolean(EXTRA_WITH_BACK_NAV, withBackNav)
-                }
-            }
+        fun newInstance(): LocalMusicFragment {
+            return LocalMusicFragment()
         }
     }
 
@@ -71,21 +58,6 @@ class LocalMusicFragment : BottomDialogFragment(R.layout.fragment_local_music),
 
     override fun initViews() {
         super.initViews()
-        if (arguments?.getBoolean(EXTRA_WITH_BACK_NAV, false) == true) {
-            binding.title.show()
-            binding.backBtn.show()
-            binding.backBtn.setOnClickListener {
-                dismiss()
-            }
-            binding.root.setBackgroundResource(APP_R.drawable.common_white_top_radius_12_bg)
-        } else {
-            binding.title.gone()
-            binding.backBtn.gone()
-            binding.musicListLayout.updateLayoutParams<ConstraintLayout.LayoutParams>() {
-                height = ConstraintLayout.LayoutParams.MATCH_PARENT
-            }
-        }
-
         binding.scanBtn.setOnClickListener {
             startScanAnim()
 

+ 92 - 0
module/music/src/main/res/layout/dialog_local_music.xml

@@ -0,0 +1,92 @@
+<?xml version="1.0" encoding="utf-8"?>
+<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
+    android:layout_width="match_parent"
+    android:layout_height="wrap_content"
+    android:background="@drawable/common_white_top_radius_12_bg">
+
+    <androidx.appcompat.widget.AppCompatImageView
+        android:id="@+id/back_btn"
+        android:layout_width="20dp"
+        android:layout_height="20dp"
+        android:layout_marginStart="20dp"
+        android:layout_marginTop="14dp"
+        android:rotationY="@integer/locale_mirror_flip"
+        android:src="@drawable/commonui_back_black_48_ic"
+        app:layout_constraintStart_toStartOf="parent"
+        app:layout_constraintTop_toTopOf="parent" />
+
+    <androidx.appcompat.widget.AppCompatTextView
+        android:id="@+id/title"
+        android:layout_width="0dp"
+        android:layout_height="wrap_content"
+        android:layout_marginTop="15dp"
+        android:gravity="center"
+        android:text="@string/music_local"
+        android:textColor="@color/color_222222"
+        android:textSize="15sp"
+        android:textStyle="bold"
+        app:layout_constraintEnd_toEndOf="parent"
+        app:layout_constraintStart_toStartOf="parent"
+        app:layout_constraintTop_toTopOf="parent" />
+
+    <androidx.constraintlayout.widget.ConstraintLayout
+        android:id="@+id/scan_btn"
+        android:layout_width="0dp"
+        android:layout_height="wrap_content"
+        android:layout_marginHorizontal="20dp"
+        android:layout_marginTop="15dp"
+        android:background="@drawable/music_local_scan_to_add_bg"
+        android:paddingVertical="10dp"
+        app:layout_constraintEnd_toEndOf="parent"
+        app:layout_constraintStart_toStartOf="parent"
+        app:layout_constraintTop_toBottomOf="@+id/title">
+
+        <androidx.appcompat.widget.AppCompatImageView
+            android:id="@+id/scan_icon"
+            android:layout_width="20dp"
+            android:layout_height="20dp"
+            android:src="@drawable/common_scan_black_20_ic"
+            app:layout_constraintBottom_toBottomOf="parent"
+            app:layout_constraintEnd_toStartOf="@+id/scan_text"
+            app:layout_constraintHorizontal_chainStyle="packed"
+            app:layout_constraintStart_toStartOf="parent"
+            app:layout_constraintTop_toTopOf="parent" />
+
+        <androidx.appcompat.widget.AppCompatTextView
+            android:id="@+id/scan_text"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:layout_marginStart="4dp"
+            android:includeFontPadding="false"
+            android:text="@string/music_scan_to_add"
+            android:textColor="@color/color_222222"
+            android:textSize="14sp"
+            app:layout_constraintBottom_toBottomOf="parent"
+            app:layout_constraintEnd_toEndOf="parent"
+            app:layout_constraintStart_toEndOf="@+id/scan_icon"
+            app:layout_constraintTop_toTopOf="parent" />
+
+    </androidx.constraintlayout.widget.ConstraintLayout>
+
+    <FrameLayout
+        android:id="@+id/music_list_layout"
+        android:layout_width="0dp"
+        android:layout_height="372dp"
+        app:layout_constraintEnd_toEndOf="parent"
+        app:layout_constraintStart_toStartOf="parent"
+        app:layout_constraintTop_toBottomOf="@+id/scan_btn">
+
+        <androidx.recyclerview.widget.RecyclerView
+            android:id="@+id/music_list"
+            android:layout_width="match_parent"
+            android:layout_height="match_parent" />
+
+        <com.adealink.weparty.commonui.widget.CommonEmptyErrorView
+            android:id="@+id/empty_view"
+            android:layout_width="match_parent"
+            android:layout_height="match_parent"
+            android:visibility="gone" />
+
+    </FrameLayout>
+</androidx.constraintlayout.widget.ConstraintLayout>

+ 16 - 42
module/music/src/main/res/layout/fragment_local_music.xml

@@ -2,32 +2,7 @@
 <androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:app="http://schemas.android.com/apk/res-auto"
     android:layout_width="match_parent"
-    android:layout_height="wrap_content">
-
-    <androidx.appcompat.widget.AppCompatImageView
-        android:id="@+id/back_btn"
-        android:layout_width="20dp"
-        android:layout_height="20dp"
-        android:layout_marginStart="20dp"
-        android:layout_marginTop="14dp"
-        android:rotationY="@integer/locale_mirror_flip"
-        android:src="@drawable/commonui_back_black_48_ic"
-        app:layout_constraintStart_toStartOf="parent"
-        app:layout_constraintTop_toTopOf="parent" />
-
-    <androidx.appcompat.widget.AppCompatTextView
-        android:id="@+id/title"
-        android:layout_width="0dp"
-        android:layout_height="wrap_content"
-        android:layout_marginTop="15dp"
-        android:gravity="center"
-        android:text="@string/music_local"
-        android:textColor="@color/color_222222"
-        android:textSize="15sp"
-        android:textStyle="bold"
-        app:layout_constraintEnd_toEndOf="parent"
-        app:layout_constraintStart_toStartOf="parent"
-        app:layout_constraintTop_toTopOf="parent" />
+    android:layout_height="match_parent">
 
     <androidx.constraintlayout.widget.ConstraintLayout
         android:id="@+id/scan_btn"
@@ -39,7 +14,7 @@
         android:paddingVertical="10dp"
         app:layout_constraintEnd_toEndOf="parent"
         app:layout_constraintStart_toStartOf="parent"
-        app:layout_constraintTop_toBottomOf="@+id/title">
+        app:layout_constraintTop_toTopOf="parent">
 
         <androidx.appcompat.widget.AppCompatImageView
             android:id="@+id/scan_icon"
@@ -68,24 +43,23 @@
 
     </androidx.constraintlayout.widget.ConstraintLayout>
 
-    <FrameLayout
-        android:id="@+id/music_list_layout"
+    <androidx.recyclerview.widget.RecyclerView
+        android:id="@+id/music_list"
         android:layout_width="0dp"
-        android:layout_height="372dp"
+        android:layout_height="0dp"
+        app:layout_constraintBottom_toBottomOf="parent"
         app:layout_constraintEnd_toEndOf="parent"
         app:layout_constraintStart_toStartOf="parent"
-        app:layout_constraintTop_toBottomOf="@+id/scan_btn">
+        app:layout_constraintTop_toBottomOf="@id/scan_btn" />
 
-        <androidx.recyclerview.widget.RecyclerView
-            android:id="@+id/music_list"
-            android:layout_width="match_parent"
-            android:layout_height="match_parent" />
-
-        <com.adealink.weparty.commonui.widget.CommonEmptyErrorView
-            android:id="@+id/empty_view"
-            android:layout_width="match_parent"
-            android:layout_height="match_parent"
-            android:visibility="gone" />
+    <com.adealink.weparty.commonui.widget.CommonEmptyErrorView
+        android:id="@+id/empty_view"
+        android:layout_width="0dp"
+        android:layout_height="0dp"
+        android:visibility="gone"
+        app:layout_constraintBottom_toBottomOf="@id/music_list"
+        app:layout_constraintEnd_toEndOf="@id/music_list"
+        app:layout_constraintStart_toStartOf="@id/music_list"
+        app:layout_constraintTop_toTopOf="@id/music_list" />
 
-    </FrameLayout>
 </androidx.constraintlayout.widget.ConstraintLayout>