Heavily refactor navigation

Make how navigation to the detail fragments much simpler/easier to maintain compared to previously.
This commit is contained in:
OxygenCobalt 2021-01-13 16:11:59 -07:00
parent fafaa0bf1f
commit 60af5f8656
No known key found for this signature in database
GPG key ID: 37DBE3621FE9AD47
15 changed files with 143 additions and 120 deletions

View file

@ -14,8 +14,6 @@ import androidx.navigation.fragment.NavHostFragment
import androidx.navigation.fragment.findNavController
import org.oxycblt.auxio.databinding.FragmentMainBinding
import org.oxycblt.auxio.detail.DetailViewModel
import org.oxycblt.auxio.music.Album
import org.oxycblt.auxio.music.Artist
import org.oxycblt.auxio.music.MusicStore
import org.oxycblt.auxio.music.Song
import org.oxycblt.auxio.playback.PlaybackViewModel
@ -95,12 +93,10 @@ class MainFragment : Fragment() {
if (it != null && navController != null) {
val curDest = navController.currentDestination?.id
val isOk = when (it) {
is Song -> (detailModel.currentAlbum.value?.id == it.album.id) magic (curDest != R.id.album_detail_fragment)
is Album -> (detailModel.currentAlbum.value?.id == it.id) magic (curDest != R.id.album_detail_fragment)
is Artist -> (detailModel.currentArtist.value?.id == it.id) magic (curDest != R.id.artist_detail_fragment)
var isOk = false
else -> false
if (curDest == R.id.songs_fragment || curDest == R.id.settings_fragment) {
isOk = true
}
if (isOk) {
@ -116,17 +112,6 @@ class MainFragment : Fragment() {
return binding.root
}
/**
* Magic boolean logic that gets navigation working.
* true true -> true |
* true false -> false |
* false true -> true |
* false false -> false |
*/
private infix fun Boolean.magic(other: Boolean): Boolean {
return if (!this && !other) false else !(this && !other)
}
/**
* Custom navigator code that has proper animations, unlike BottomNavigationView.setupWithNavController().
*/

View file

@ -10,6 +10,7 @@ import org.oxycblt.auxio.R
import org.oxycblt.auxio.detail.adapters.AlbumDetailAdapter
import org.oxycblt.auxio.logD
import org.oxycblt.auxio.music.Album
import org.oxycblt.auxio.music.Artist
import org.oxycblt.auxio.music.BaseModel
import org.oxycblt.auxio.music.MusicStore
import org.oxycblt.auxio.music.Song
@ -82,28 +83,41 @@ class AlbumDetailFragment : DetailFragment() {
detailAdapter.submitList(data)
}
detailModel.doneWithNavToParent()
detailModel.navToParent.observe(viewLifecycleOwner) {
if (it) {
findNavController().navigate(
AlbumDetailFragmentDirections.actionShowParentArtist(
detailModel.currentAlbum.value!!.artist.id
)
)
detailModel.doneWithNavToParent()
}
}
detailModel.navToItem.observe(viewLifecycleOwner) {
if (it != null) {
if (it is Song) {
scrollToPlayingItem()
}
logD(it.name)
when (it) {
is Song -> {
if (detailModel.currentAlbum.value!!.id == it.album.id) {
scrollToItem(it.id)
if (it is Album && it.id == detailModel.currentAlbum.value!!.id) {
detailModel.doneWithNavToItem()
detailModel.doneWithNavToItem()
} else {
findNavController().navigate(
AlbumDetailFragmentDirections.actionShowAlbum(it.album.id)
)
}
}
is Album -> {
if (detailModel.currentAlbum.value!!.id == it.id) {
binding.detailRecycler.scrollToPosition(0)
detailModel.doneWithNavToItem()
} else {
findNavController().navigate(
AlbumDetailFragmentDirections.actionShowAlbum(it.id)
)
}
}
is Artist -> {
logD("Hello?")
findNavController().navigate(
AlbumDetailFragmentDirections.actionShowArtist(it.id)
)
}
else -> {}
}
}
}
@ -144,14 +158,11 @@ class AlbumDetailFragment : DetailFragment() {
}
}
/**
* Scroll to the currently playing item.
*/
private fun scrollToPlayingItem() {
private fun scrollToItem(id: Long) {
// Calculate where the item for the currently played song is
val pos = detailModel.albumSortMode.value!!.getSortedSongList(
detailModel.currentAlbum.value!!.songs
).indexOf(playbackModel.song.value)
).indexOfFirst { it.id == id }
if (pos != -1) {
binding.detailRecycler.post {
@ -167,8 +178,6 @@ class AlbumDetailFragment : DetailFragment() {
binding.detailAppbar.isLifted = true
}
}
detailModel.doneWithNavToItem()
}
}
}

View file

@ -12,6 +12,7 @@ import org.oxycblt.auxio.music.Album
import org.oxycblt.auxio.music.Artist
import org.oxycblt.auxio.music.BaseModel
import org.oxycblt.auxio.music.MusicStore
import org.oxycblt.auxio.music.Song
import org.oxycblt.auxio.playback.state.PlaybackMode
import org.oxycblt.auxio.ui.ActionMenu
import org.oxycblt.auxio.ui.requireCompatActivity
@ -77,8 +78,22 @@ class ArtistDetailFragment : DetailFragment() {
}
detailModel.navToItem.observe(viewLifecycleOwner) {
if (it != null && it is Artist) {
detailModel.doneWithNavToItem()
if (it != null) {
if (it is Artist) {
if (it.id == detailModel.currentArtist.value!!.id) {
detailModel.doneWithNavToItem()
} else {
findNavController().navigate(
ArtistDetailFragmentDirections.actionShowArtist(it.id)
)
}
} else {
val albumId = if (it is Song) it.album.id else it.id
findNavController().navigate(
ArtistDetailFragmentDirections.actionShowAlbum(albumId)
)
}
}
}

View file

@ -13,6 +13,7 @@ import androidx.recyclerview.widget.RecyclerView
import org.oxycblt.auxio.databinding.FragmentDetailBinding
import org.oxycblt.auxio.music.BaseModel
import org.oxycblt.auxio.playback.PlaybackViewModel
import org.oxycblt.auxio.ui.fixAnimationInfoMemoryLeak
import org.oxycblt.auxio.ui.isLandscape
import org.oxycblt.auxio.ui.memberBinding
@ -44,6 +45,12 @@ abstract class DetailFragment : Fragment() {
callback.isEnabled = false
}
override fun onDestroyView() {
super.onDestroyView()
fixAnimationInfoMemoryLeak()
}
/**
* Shortcut method for doing setup of the detail toolbar.
*/

View file

@ -36,16 +36,10 @@ class DetailViewModel : ViewModel() {
private val mCurrentAlbum = MutableLiveData<Album?>()
val currentAlbum: LiveData<Album?> get() = mCurrentAlbum
// Navigation flags
// Primary navigation flag.
private val mNavToItem = MutableLiveData<BaseModel?>()
val navToItem: LiveData<BaseModel?> get() = mNavToItem
private val mNavToParent = MutableLiveData<Boolean>()
val navToParent: LiveData<Boolean> get() = mNavToParent
private val mNavToChild = MutableLiveData<BaseModel?>()
val navToChild: LiveData<BaseModel?> get() = mNavToChild
/**
* Update the current navigation status
* @param value Whether the current [DetailFragment] is navigating or not.
@ -113,23 +107,4 @@ class DetailViewModel : ViewModel() {
fun doneWithNavToItem() {
mNavToItem.value = null
}
/** Mark that parent navigation should occur */
fun navToParent() {
mNavToParent.value = true
}
/** Mark that the UI is done with the parent navigation */
fun doneWithNavToParent() {
mNavToParent.value = false
}
/** Navigate to some child item (Primarily used by GenreDetailFragment) */
fun navToChild(child: BaseModel) {
mNavToChild.value = child
}
fun doneWithNavToChild() {
mNavToChild.value = null
}
}

View file

@ -12,6 +12,7 @@ import org.oxycblt.auxio.music.Album
import org.oxycblt.auxio.music.Artist
import org.oxycblt.auxio.music.BaseModel
import org.oxycblt.auxio.music.MusicStore
import org.oxycblt.auxio.music.Song
import org.oxycblt.auxio.playback.state.PlaybackMode
import org.oxycblt.auxio.ui.ActionMenu
import org.oxycblt.auxio.ui.requireCompatActivity
@ -69,19 +70,23 @@ class GenreDetailFragment : DetailFragment() {
detailAdapter.submitList(data)
}
detailModel.navToChild.observe(viewLifecycleOwner) {
detailModel.navToItem.observe(viewLifecycleOwner) {
if (it != null) {
if (it is Artist) {
findNavController().navigate(
GenreDetailFragmentDirections.actionGoArtist(it.id)
when (it) {
is Artist -> findNavController().navigate(
GenreDetailFragmentDirections.actionShowArtist(it.id)
)
} else if (it is Album) {
findNavController().navigate(
GenreDetailFragmentDirections.actionGoAlbum(it.id)
)
}
detailModel.doneWithNavToChild()
is Album -> findNavController().navigate(
GenreDetailFragmentDirections.actionShowAlbum(it.id)
)
is Song -> findNavController().navigate(
GenreDetailFragmentDirections.actionShowAlbum(it.album.id)
)
else -> {}
}
}
}

View file

@ -14,8 +14,8 @@ import androidx.navigation.fragment.findNavController
import androidx.recyclerview.widget.GridLayoutManager
import org.oxycblt.auxio.R
import org.oxycblt.auxio.databinding.FragmentSearchBinding
import org.oxycblt.auxio.detail.DetailViewModel
import org.oxycblt.auxio.logD
import org.oxycblt.auxio.logE
import org.oxycblt.auxio.music.Album
import org.oxycblt.auxio.music.Artist
import org.oxycblt.auxio.music.BaseModel
@ -26,6 +26,7 @@ import org.oxycblt.auxio.playback.PlaybackViewModel
import org.oxycblt.auxio.settings.SettingsManager
import org.oxycblt.auxio.ui.ActionMenu
import org.oxycblt.auxio.ui.accent
import org.oxycblt.auxio.ui.fixAnimationInfoMemoryLeak
import org.oxycblt.auxio.ui.getLandscapeSpans
import org.oxycblt.auxio.ui.isLandscape
import org.oxycblt.auxio.ui.requireCompatActivity
@ -40,6 +41,7 @@ class SearchFragment : Fragment() {
// SearchViewModel only scoped to this Fragment
private val searchModel: SearchViewModel by viewModels()
private val playbackModel: PlaybackViewModel by activityViewModels()
private val detailModel: DetailViewModel by activityViewModels()
override fun onCreateView(
inflater: LayoutInflater,
@ -113,25 +115,27 @@ class SearchFragment : Fragment() {
}
}
detailModel.navToItem.observe(viewLifecycleOwner) {
if (it != null) {
findNavController().navigate(
when (it) {
is Song -> SearchFragmentDirections.actionShowAlbum(it.album.id)
is Album -> SearchFragmentDirections.actionShowAlbum(it.id)
is Artist -> SearchFragmentDirections.actionShowArtist(it.id)
else -> return@observe
}
)
}
}
return binding.root
}
override fun onDestroyView() {
super.onDestroyView()
try {
// Use reflection to fix a memory leak in the fragment source code that occurs
// from leaving an EditText focused when exiting the view.
// I cant believe I have to do this.
Fragment::class.java.getDeclaredMethod("setFocusedView", View::class.java).apply {
isAccessible = true
invoke(this@SearchFragment, null)
}
} catch (e: Exception) {
logE("Hacky reflection leak fix failed.")
e.printStackTrace()
}
fixAnimationInfoMemoryLeak()
}
override fun onResume() {

View file

@ -137,13 +137,13 @@ class ActionMenu(
R.id.action_go_album -> {
if (data is Song) {
determineWhereToNavWithSong(data.album)
detailModel.navToItem(data.album)
}
}
R.id.action_go_artist -> {
if (data is Song) {
determineWhereToNavWithSong(data.album.artist)
detailModel.navToItem(data.album.artist)
} else if (data is Album) {
detailModel.navToItem(data.artist)
}
@ -151,14 +151,6 @@ class ActionMenu(
}
}
private fun determineWhereToNavWithSong(parent: BaseModel) {
when (flag) {
FLAG_NONE -> detailModel.navToItem(parent)
FLAG_IN_ALBUM -> detailModel.navToParent()
FLAG_IN_GENRE -> detailModel.navToChild(parent)
}
}
companion object {
/** No Flags **/
const val FLAG_NONE = -1

View file

@ -9,6 +9,7 @@ import android.graphics.Point
import android.os.Build
import android.text.Spanned
import android.util.DisplayMetrics
import android.view.View
import android.view.WindowManager
import android.widget.ImageButton
import android.widget.TextView
@ -20,6 +21,7 @@ import androidx.core.text.HtmlCompat
import androidx.fragment.app.Fragment
import com.google.android.material.button.MaterialButton
import org.oxycblt.auxio.R
import org.oxycblt.auxio.logE
// --- VIEW CONFIGURATION ---
@ -139,6 +141,7 @@ fun Activity.isIrregularLandscape(): Boolean {
* Check if the system bars are on the bottom.
* @return If the system bars are on the bottom, false if no.
*/
@Suppress("DEPRECATION")
private fun isSystemBarOnBottom(activity: Activity): Boolean {
val realPoint = Point()
val metrics = DisplayMetrics()
@ -170,3 +173,21 @@ private fun isSystemBarOnBottom(activity: Activity): Boolean {
return (!canMove || width < height)
}
// --- HACKY NIGHTMARES ---
/**
* Use R E F L E C T I O N to fix a memory leak where mAnimationInfo will keep a reference to
* its focused view.
* I can't believe I have to do this.
*/
fun Fragment.fixAnimationInfoMemoryLeak() {
try {
Fragment::class.java.getDeclaredMethod("setFocusedView", View::class.java).let {
it.isAccessible = true
it.invoke(this, null)
}
} catch (e: Exception) {
logE("mAnimationInfo leak fix failed.")
}
}

View file

@ -24,7 +24,6 @@
android:id="@+id/playback_layout"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:animateLayoutChanges="true"
android:background="@color/background"
android:fitsSystemWindows="true">

View file

@ -59,7 +59,7 @@
android:background="@drawable/ui_ripple"
android:clickable="true"
android:focusable="true"
android:onClick="@{() -> detailModel.navToParent()}"
android:onClick="@{() -> detailModel.navToItem(album.artist)}"
android:text="@{album.artist.name}"
android:textAppearance="?android:attr/textAppearanceListItem"
android:textColor="?android:attr/textColorSecondary"

View file

@ -24,7 +24,6 @@
android:id="@+id/playback_layout"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:animateLayoutChanges="true"
android:background="@color/background"
android:fitsSystemWindows="true">

View file

@ -24,7 +24,6 @@
android:id="@+id/playback_layout"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:animateLayoutChanges="true"
android:background="@color/background"
android:fitsSystemWindows="true">

View file

@ -55,7 +55,7 @@
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="@dimen/margin_medium"
android:onClick="@{() -> detailModel.navToParent()}"
android:onClick="@{() -> detailModel.navToItem(album.artist)}"
android:text="@{album.artist.name}"
android:textAppearance="?android:attr/textAppearanceListItem"
android:textColor="?android:attr/textColorSecondary"

View file

@ -39,12 +39,18 @@
<argument
android:name="artistId"
app:argType="long" />
<action
android:id="@+id/action_show_artist"
app:destination="@id/artist_detail_fragment"
app:enterAnim="@anim/nav_default_enter_anim"
app:exitAnim="@anim/nav_default_exit_anim"
app:popEnterAnim="@anim/nav_default_pop_enter_anim"
app:popExitAnim="@anim/nav_default_pop_exit_anim" />
<action
android:id="@+id/action_show_album"
app:destination="@id/album_detail_fragment"
app:enterAnim="@anim/nav_default_enter_anim"
app:exitAnim="@anim/nav_default_exit_anim"
app:launchSingleTop="true"
app:popEnterAnim="@anim/nav_default_pop_enter_anim"
app:popExitAnim="@anim/nav_default_pop_exit_anim" />
</fragment>
@ -57,12 +63,19 @@
android:name="albumId"
app:argType="long" />
<action
android:id="@+id/action_show_parent_artist"
android:id="@+id/action_show_artist"
app:destination="@id/artist_detail_fragment"
app:enterAnim="@anim/nav_default_enter_anim"
app:exitAnim="@anim/nav_default_exit_anim"
app:popEnterAnim="@anim/nav_default_pop_enter_anim"
app:popExitAnim="@anim/nav_default_pop_exit_anim" />
<action
android:id="@+id/action_show_album"
app:destination="@id/album_detail_fragment"
app:enterAnim="@anim/nav_default_enter_anim"
app:exitAnim="@anim/nav_default_exit_anim"
app:popEnterAnim="@anim/nav_default_pop_enter_anim"
app:popExitAnim="@anim/nav_default_pop_exit_anim" />
</fragment>
<fragment
android:id="@+id/genre_detail_fragment"
@ -73,14 +86,14 @@
android:name="genreId"
app:argType="long" />
<action
android:id="@+id/action_go_artist"
android:id="@+id/action_show_artist"
app:destination="@id/artist_detail_fragment"
app:enterAnim="@anim/nav_default_enter_anim"
app:exitAnim="@anim/nav_default_exit_anim"
app:popEnterAnim="@anim/nav_default_pop_enter_anim"
app:popExitAnim="@anim/nav_default_pop_exit_anim" />
<action
android:id="@+id/action_go_album"
android:id="@+id/action_show_album"
app:destination="@id/album_detail_fragment"
app:enterAnim="@anim/nav_default_enter_anim"
app:exitAnim="@anim/nav_default_exit_anim"
@ -92,11 +105,6 @@
android:name="org.oxycblt.auxio.songs.SongsFragment"
android:label="fragment_songs"
tools:layout="@layout/fragment_songs" />
<fragment
android:id="@+id/settings_fragment"
android:name="org.oxycblt.auxio.settings.SettingsFragment"
android:label="SettingsFragment"
tools:layout="@layout/fragment_settings" />
<fragment
android:id="@+id/search_fragment"
android:name="org.oxycblt.auxio.search.SearchFragment"
@ -124,4 +132,9 @@
app:popEnterAnim="@anim/nav_default_pop_enter_anim"
app:popExitAnim="@anim/nav_default_pop_exit_anim" />
</fragment>
<fragment
android:id="@+id/settings_fragment"
android:name="org.oxycblt.auxio.settings.SettingsFragment"
android:label="SettingsFragment"
tools:layout="@layout/fragment_settings" />
</navigation>