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:
parent
212ffbf2c7
commit
c3a61e6071
14 changed files with 206 additions and 106 deletions
|
@ -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 ---
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
44
app/src/main/java/org/oxycblt/auxio/detail/DetailFragment.kt
Normal file
44
app/src/main/java/org/oxycblt/auxio/detail/DetailFragment.kt
Normal 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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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()
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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>
|
|
@ -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"
|
||||
|
|
11
app/src/main/res/menu/menu_nav.xml
Normal file
11
app/src/main/res/menu/menu_nav.xml
Normal 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>
|
92
app/src/main/res/navigation/nav_explore.xml
Normal file
92
app/src/main/res/navigation/nav_explore.xml
Normal 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>
|
Loading…
Reference in a new issue