Switch from ViewPager to BottomNavigationView

Use BottomNavigationView as the main navigator of Auxio instead of a ViewPager + BottomNavigationView, primarily to fix some memory leaks and give Auxio a better UI design overall.
This commit is contained in:
OxygenCobalt 2020-11-16 21:09:55 -07:00
parent 212ffbf2c7
commit c3a61e6071
14 changed files with 206 additions and 106 deletions

View file

@ -73,10 +73,9 @@ dependencies {
implementation 'androidx.media:media:1.2.0'
// Database
def room_version = '2.2.5'
def room_version = '2.3.0-alpha03'
implementation "androidx.room:room-runtime:$room_version"
kapt "androidx.room:room-compiler:$room_version"
implementation 'androidx.legacy:legacy-support-v4:1.0.0'
implementation "androidx.room:room-ktx:$room_version"
// --- THIRD PARTY ---

View file

@ -1,22 +1,20 @@
package org.oxycblt.auxio
import android.content.res.ColorStateList
import android.os.Bundle
import android.util.Log
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.core.content.ContextCompat
import androidx.fragment.app.Fragment
import androidx.fragment.app.activityViewModels
import androidx.navigation.findNavController
import androidx.navigation.fragment.NavHostFragment
import androidx.navigation.fragment.findNavController
import androidx.viewpager2.adapter.FragmentStateAdapter
import com.google.android.material.tabs.TabLayout
import com.google.android.material.tabs.TabLayoutMediator
import androidx.navigation.ui.setupWithNavController
import org.oxycblt.auxio.databinding.FragmentMainBinding
import org.oxycblt.auxio.library.LibraryFragment
import org.oxycblt.auxio.music.MusicStore
import org.oxycblt.auxio.playback.PlaybackViewModel
import org.oxycblt.auxio.songs.SongsFragment
import org.oxycblt.auxio.ui.accent
import org.oxycblt.auxio.ui.getInactiveAlpha
import org.oxycblt.auxio.ui.getTransparentAccent
@ -52,40 +50,24 @@ class MainFragment : Fragment() {
accent.first,
getInactiveAlpha(accent.first)
)
val navIconTints = ColorStateList(
arrayOf(
intArrayOf(-android.R.attr.state_checked),
intArrayOf(android.R.attr.state_checked)
),
intArrayOf(colorInactive, colorActive)
)
// --- UI SETUP ---
// TODO: Add nested viewpager navigation [If practical]
binding.lifecycleOwner = this
binding.mainViewPager.adapter = PagerAdapter()
// Link the ViewPager & Tab View
TabLayoutMediator(binding.mainTabs, binding.mainViewPager) { tab, position ->
tab.icon = ContextCompat.getDrawable(requireContext(), tabIcons[position])
// Set the icon tint to deselected if its not the default tab
if (position > 0) {
tab.icon?.setTint(colorInactive)
}
}.attach()
// Set up the selected/deselected colors
binding.mainTabs.addOnTabSelectedListener(
object : TabLayout.OnTabSelectedListener {
override fun onTabSelected(tab: TabLayout.Tab) {
tab.icon?.setTint(colorActive)
}
override fun onTabUnselected(tab: TabLayout.Tab) {
tab.icon?.setTint(colorInactive)
}
override fun onTabReselected(tab: TabLayout.Tab?) {
}
}
)
binding.navBar.itemIconTintList = navIconTints
binding.navBar.itemTextColor = navIconTints
((childFragmentManager.findFragmentById(R.id.explore_nav_host) as NavHostFragment?))?.let {
// TODO: Add animation with BottomNavigationView navs
binding.navBar.setupWithNavController(it.findNavController())
}
// --- VIEWMODEL SETUP ---
@ -107,35 +89,4 @@ class MainFragment : Fragment() {
return binding.root
}
private fun fragmentAt(position: Int): Fragment {
return when (position) {
0 -> LibraryFragment()
1 -> SongsFragment()
else -> SongsFragment()
}
}
private inner class PagerAdapter :
FragmentStateAdapter(childFragmentManager, viewLifecycleOwner.lifecycle) {
override fun getItemCount(): Int = shownFragments.size
override fun createFragment(position: Int): Fragment {
Log.d(this::class.simpleName, "Switching to fragment $position.")
if (shownFragments.contains(position)) {
return fragmentAt(position)
}
// If a fragment that shouldn't be shown is somehow shown anyway, just return
// its intended fragment.
Log.e(
this::class.simpleName,
"Attempted to index a fragment that shouldn't be shown."
)
return fragmentAt(position)
}
}
}

View file

@ -6,7 +6,6 @@ import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.appcompat.widget.PopupMenu
import androidx.fragment.app.Fragment
import androidx.fragment.app.activityViewModels
import androidx.navigation.fragment.findNavController
import androidx.navigation.fragment.navArgs
@ -20,7 +19,7 @@ import org.oxycblt.auxio.ui.applyDivider
import org.oxycblt.auxio.ui.disable
import org.oxycblt.auxio.ui.setupAlbumSongActions
class AlbumDetailFragment : Fragment() {
class AlbumDetailFragment : DetailFragment() {
private val args: AlbumDetailFragmentArgs by navArgs()
private val detailModel: DetailViewModel by activityViewModels()

View file

@ -5,7 +5,6 @@ import android.util.Log
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.Fragment
import androidx.fragment.app.activityViewModels
import androidx.navigation.fragment.findNavController
import androidx.navigation.fragment.navArgs
@ -17,7 +16,7 @@ import org.oxycblt.auxio.playback.PlaybackViewModel
import org.oxycblt.auxio.ui.applyDivider
import org.oxycblt.auxio.ui.disable
class ArtistDetailFragment : Fragment() {
class ArtistDetailFragment : DetailFragment() {
private val args: ArtistDetailFragmentArgs by navArgs()
private val detailModel: DetailViewModel by activityViewModels()
private val playbackModel: PlaybackViewModel by activityViewModels()
@ -107,10 +106,4 @@ class ArtistDetailFragment : Fragment() {
return binding.root
}
override fun onResume() {
super.onResume()
detailModel.updateNavigationStatus(false)
}
}

View file

@ -0,0 +1,44 @@
package org.oxycblt.auxio.detail
import android.os.Bundle
import android.view.View
import androidx.activity.OnBackPressedCallback
import androidx.fragment.app.Fragment
import androidx.navigation.fragment.findNavController
/**
* A Base [Fragment] implementing a [OnBackPressedCallback] so that Auxio will navigate upwards
* instead of out of the app if a Detail Fragment is currently open.
* @author OxygenCobalt
*/
abstract class DetailFragment : Fragment() {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
requireActivity().onBackPressedDispatcher.addCallback(viewLifecycleOwner, callback)
}
override fun onResume() {
super.onResume()
callback.isEnabled = true
}
override fun onPause() {
super.onPause()
callback.isEnabled = false
}
private val callback = object : OnBackPressedCallback(false) {
override fun handleOnBackPressed() {
val navController = findNavController()
// Check if it's the root of nested fragments in this navhost
if (navController.currentDestination?.id == navController.graph.startDestination) {
isEnabled = false
requireActivity().onBackPressed()
isEnabled = true
} else {
navController.navigateUp()
}
}
}
}

View file

@ -5,7 +5,6 @@ import android.util.Log
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.Fragment
import androidx.fragment.app.activityViewModels
import androidx.navigation.fragment.findNavController
import androidx.navigation.fragment.navArgs
@ -17,7 +16,7 @@ import org.oxycblt.auxio.playback.PlaybackViewModel
import org.oxycblt.auxio.ui.applyDivider
import org.oxycblt.auxio.ui.disable
class GenreDetailFragment : Fragment() {
class GenreDetailFragment : DetailFragment() {
private val args: GenreDetailFragmentArgs by navArgs()
private val detailModel: DetailViewModel by activityViewModels()

View file

@ -203,9 +203,9 @@ class LibraryFragment : Fragment(), SearchView.OnQueryTextListener {
findNavController().navigate(
when (baseModel) {
is Genre -> MainFragmentDirections.actionShowGenre(baseModel.id)
is Artist -> MainFragmentDirections.actionShowArtist(baseModel.id)
is Album -> MainFragmentDirections.actionShowAlbum(baseModel.id, true)
is Genre -> LibraryFragmentDirections.actionShowGenre(baseModel.id)
is Artist -> LibraryFragmentDirections.actionShowArtist(baseModel.id)
is Album -> LibraryFragmentDirections.actionShowAlbum(baseModel.id, true)
// If given model wasn't valid, then reset the navigation status
// and abort the navigation.

View file

@ -53,7 +53,11 @@ class QueueDragCallback(private val playbackModel: PlaybackViewModel) : ItemTouc
viewHolder: RecyclerView.ViewHolder,
target: RecyclerView.ViewHolder
): Boolean {
return playbackModel.moveQueueItems(viewHolder.adapterPosition, target.adapterPosition, queueAdapter)
return playbackModel.moveQueueItems(
viewHolder.adapterPosition,
target.adapterPosition,
queueAdapter
)
}
override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) {

View file

@ -539,7 +539,10 @@ class PlaybackStateManager private constructor() {
// If the parent was somehow dropped during saving, attempt to restore it.
mSong?.let {
if (mParent == null && mMode != PlaybackMode.ALL_SONGS) {
Log.d(this::class.simpleName, "Parent was corrupted while in mode $mMode. Attempting to restore.")
Log.d(
this::class.simpleName,
"Parent was corrupted while in mode $mMode. Attempting to restore."
)
mParent = when (mMode) {
PlaybackMode.IN_ARTIST -> it.album.artist
PlaybackMode.IN_ALBUM -> it.album

View file

@ -15,6 +15,8 @@
type="org.oxycblt.auxio.playback.PlaybackViewModel" />
</data>
<!-- TODO: Fix elevation not showing -->
<androidx.constraintlayout.widget.ConstraintLayout
android:animateLayoutChanges="true"
android:background="@drawable/ui_ripple"

View file

@ -4,43 +4,46 @@
xmlns:tools="http://schemas.android.com/tools"
tools:context=".MainFragment">
<LinearLayout
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/main_layout"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:animateLayoutChanges="true"
android:orientation="vertical">
<androidx.viewpager2.widget.ViewPager2
android:id="@+id/main_view_pager"
<androidx.fragment.app.FragmentContainerView
android:id="@+id/explore_nav_host"
android:name="androidx.navigation.fragment.NavHostFragment"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1" />
android:elevation="@dimen/elevation_normal"
app:layout_constraintBottom_toTopOf="@+id/compact_playback"
app:layout_constraintTop_toTopOf="parent"
app:navGraph="@navigation/nav_explore"
tools:layout="@layout/fragment_library" />
<androidx.fragment.app.FragmentContainerView
android:id="@+id/compact_playback"
android:name="org.oxycblt.auxio.playback.CompactPlaybackFragment"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:visibility="gone"
android:name="org.oxycblt.auxio.playback.CompactPlaybackFragment"
android:elevation="@dimen/elevation_normal" />
app:layout_constraintBottom_toTopOf="@+id/nav_bar"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
tools:layout="@layout/fragment_compact_playback" />
<com.google.android.material.tabs.TabLayout
android:id="@+id/main_tabs"
<com.google.android.material.bottomnavigation.BottomNavigationView
android:id="@+id/nav_bar"
android:layout_width="match_parent"
android:layout_height="@dimen/height_tab_menu"
android:layout_gravity="bottom"
android:background="?android:attr/windowBackground"
android:layout_height="wrap_content"
android:background="@color/background"
android:elevation="@dimen/elevation_normal"
app:itemRippleColor="@color/selection_color"
app:layout_constraintBottom_toBottomOf="parent"
app:tabGravity="fill"
app:tabIconTint="?attr/colorPrimary"
app:tabIconTintMode="src_in"
app:tabIndicator="@drawable/ui_indicator"
app:tabIndicatorColor="?attr/colorPrimary"
app:tabMode="fixed"
app:tabRippleColor="@color/selection_color"
tools:background="@color/control_color" />
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:menu="@menu/menu_nav" />
</LinearLayout>
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>

View file

@ -46,7 +46,7 @@
android:layout_margin="@dimen/margin_mid_large"
android:contentDescription="@{@string/description_album_cover(song.name)}"
android:outlineProvider="bounds"
android:elevation="4dp"
android:elevation="2dp"
app:coverArt="@{song}"
app:layout_constraintBottom_toTopOf="@+id/playback_song"
app:layout_constraintDimensionRatio="1:1"

View file

@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android">
<item
android:id="@+id/library_fragment"
android:title="@string/title_library_fragment"
android:icon="@drawable/ic_library" />
<item
android:id="@+id/songs_fragment"
android:title="@string/label_songs"
android:icon="@drawable/ic_song" />
</menu>

View file

@ -0,0 +1,92 @@
<?xml version="1.0" encoding="utf-8"?>
<navigation xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/nav_explore"
app:startDestination="@id/library_fragment">
<fragment
android:id="@+id/library_fragment"
android:name="org.oxycblt.auxio.library.LibraryFragment"
android:label="fragment_library"
tools:layout="@layout/fragment_library">
<action
android:id="@+id/action_show_genre"
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"
app:destination="@id/genre_detail_fragment" />
<action
android:id="@+id/action_show_artist"
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"
app:destination="@id/artist_detail_fragment" />
<action
android:id="@+id/action_show_album"
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"
app:destination="@id/album_detail_fragment" />
</fragment>
<fragment
android:id="@+id/artist_detail_fragment"
android:name="org.oxycblt.auxio.detail.ArtistDetailFragment"
android:label="ArtistDetailFragment"
tools:layout="@layout/fragment_artist_detail">
<argument
android:name="artistId"
app:argType="long" />
<action
android:id="@+id/action_show_album"
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"
app:destination="@id/album_detail_fragment"
app:launchSingleTop="true" />
</fragment>
<fragment
android:id="@+id/album_detail_fragment"
android:name="org.oxycblt.auxio.detail.AlbumDetailFragment"
android:label="AlbumDetailFragment"
tools:layout="@layout/fragment_album_detail">
<argument
android:name="albumId"
app:argType="long" />
<argument
android:name="enableParentNav"
app:argType="boolean" />
<action
android:id="@+id/action_show_parent_artist"
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"
app:destination="@id/artist_detail_fragment" />
</fragment>
<fragment
android:id="@+id/genre_detail_fragment"
android:name="org.oxycblt.auxio.detail.GenreDetailFragment"
android:label="GenreDetailFragment"
tools:layout="@layout/fragment_genre_detail">
<action
android:id="@+id/action_show_artist"
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"
app:destination="@id/artist_detail_fragment" />
<argument
android:name="genreId"
app:argType="long" />
</fragment>
<fragment
android:id="@+id/songs_fragment"
android:name="org.oxycblt.auxio.songs.SongsFragment"
android:label="fragment_songs"
tools:layout="@layout/fragment_songs" />
</navigation>