Skip to content

Instantly share code, notes, and snippets.

@Skyyo
Last active August 16, 2020 11:21
Show Gist options
  • Select an option

  • Save Skyyo/bffe6903312865e6415990776ad0fd4d to your computer and use it in GitHub Desktop.

Select an option

Save Skyyo/bffe6903312865e6415990776ad0fd4d to your computer and use it in GitHub Desktop.
Custom shaped Bottom Navigation element. Middle button doesn't have any graph attached, and the tap is handled inside NavExtensions.kt line 73. #navigation #view
<com.app.CustomButtonGroup
android:id="@+id/bnv"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="bottom|center_horizontal"
android:clipChildren="false"
android:clipToPadding="false"
android:gravity="center_horizontal"
android:visibility="invisible"
tools:visibility="visible">
<com.google.android.material.button.MaterialButton
android:id="@+id/graph1"
style="@style/Widget.MaterialComponents.Button.UnelevatedButton"
android:layout_width="70dp"
android:layout_height="52dp"
android:backgroundTint="@color/bnvColor"
android:checkable="true"
android:elevation="1dp"
android:translationX="12dp"
app:icon="@drawable/ic_home"
app:iconGravity="textStart"
app:iconPadding="0dp"
app:iconTint="@color/bnv_selector_icon_checked_state"
app:rippleColor="@color/colorDayNightGreenLight"
app:shapeAppearanceOverlay="@style/ShapeLeftCornered" />
<com.google.android.material.button.MaterialButton
android:id="@+id/graph2"
style="@style/Widget.MaterialComponents.Button.UnelevatedButton"
android:layout_width="70dp"
android:layout_height="52dp"
android:backgroundTint="@color/bnvColor"
android:checkable="true"
android:elevation="1dp"
android:translationX="12dp"
app:icon="@drawable/ic_pill"
app:iconGravity="textStart"
app:iconPadding="0dp"
app:iconTint="@color/bnv_selector_icon_checked_state"
app:rippleColor="@color/colorDayNightGreenLight"
app:shapeAppearanceOverlay="@style/ShapeSquare" />
<com.google.android.material.button.MaterialButton
android:id="@+id/justAButton"
android:layout_width="90dp"
android:layout_height="70dp"
android:backgroundTint="@color/colorDayNightGreenLight"
app:icon="@drawable/ic_logo"
app:iconGravity="textStart"
app:iconPadding="0dp"
app:iconTint="@android:color/white"
app:shapeAppearanceOverlay="@style/ShapeBubble" />
<com.google.android.material.button.MaterialButton
android:id="@+id/graph3"
style="@style/Widget.MaterialComponents.Button.UnelevatedButton"
android:layout_width="70dp"
android:layout_height="52dp"
android:backgroundTint="@color/bnvColor"
android:checkable="true"
android:elevation="1dp"
android:translationX="-12dp"
app:icon="@drawable/ic_notification"
app:iconGravity="textStart"
app:iconPadding="0dp"
app:iconTint="@color/bnv_selector_icon_checked_state"
app:rippleColor="@color/colorDayNightGreenLight"
app:shapeAppearanceOverlay="@style/ShapeSquare" />
<com.google.android.material.button.MaterialButton
android:id="@+id/graph4"
style="@style/Widget.MaterialComponents.Button.UnelevatedButton"
android:layout_width="70dp"
android:layout_height="52dp"
android:backgroundTint="@color/bnvColor"
android:checkable="true"
android:elevation="1dp"
android:translationX="-12dp"
app:icon="@drawable/ic_user"
app:iconGravity="textStart"
app:iconPadding="0dp"
app:iconTint="@color/bnv_selector_icon_checked_state"
app:rippleColor="@color/colorDayNightGreenLight"
app:shapeAppearanceOverlay="@style/ShapeRightCornered" />
</com.app.CustomButtonGroup>
import android.annotation.SuppressLint
import android.content.Context
import android.util.AttributeSet
import android.view.View
import android.view.ViewGroup
import android.widget.LinearLayout
import androidx.annotation.IdRes
import com.google.android.material.button.MaterialButton
class CustomButtonGroup @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null) :
LinearLayout(context, attrs) {
var activeButtonId: Int? = null
private var selectListener: ((MaterialButton) -> Unit)? = null
private var buttons: List<MaterialButton> = listOf()
@SuppressLint("ClickableViewAccessibility")
override fun addView(child: View?, params: ViewGroup.LayoutParams?) {
super.addView(child, params)
if (child != null && child is MaterialButton) {
buttons = buttons + child
child.setOnClickListener {
selectButton(child)
}
}
}
private fun selectButton(btn: MaterialButton): Boolean {
if (btn.id == R.id.scanGraph) {
selectListener?.invoke(btn)
return false
}
if (btn.isSelected) return false
buttons.forEach {
if (it.isSelected) {
it.isSelected = false
}
}
btn.isSelected = true
activeButtonId = btn.id
selectListener?.invoke(btn)
return true
}
fun selectButton(@IdRes id: Int) = selectButton(findViewById<MaterialButton>(id))
fun setOnSelectListener(listener: (MaterialButton) -> Unit) {
selectListener = listener
}
}
import android.content.Intent
import android.content.pm.ActivityInfo
import android.os.Bundle
import android.view.View
import androidx.appcompat.app.AppCompatActivity
import androidx.core.animation.doOnEnd
import androidx.core.view.doOnLayout
import androidx.core.view.doOnPreDraw
import androidx.core.view.isInvisible
import androidx.core.view.isVisible
import androidx.lifecycle.LiveData
import androidx.lifecycle.Observer
import androidx.navigation.NavController
class MainActivity : AppCompatActivity() {
private lateinit var binding: ActivityBinding
private var currentNavController: LiveData<NavController>? = null
private val destinationChangedListener =
NavController.OnDestinationChangedListener { _, destination, _ ->
when (destination.id) {
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
if (savedInstanceState == null) {
setupBottomNavigationBar(R.id.dashboardGraph)
}
}
override fun onRestoreInstanceState(savedInstanceState: Bundle) {
super.onRestoreInstanceState(savedInstanceState)
setupBottomNavigationBar(savedInstanceState.getInt("activeId"))
}
override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(
outState.putInt("activeId", binding.bnv.activeButtonId ?: R.id.graph1)
)
}
override fun onSupportNavigateUp() = currentNavController?.value?.navigateUp() ?: false
private fun setupBottomNavigationBar(preselectedGraphId: Int) {
val controller = binding.bnv.setupWithNavController(
navGraphIds = listOf(
R.navigation.graph1,
R.navigation.graph2,
R.navigation.graph3,
R.navigation.graph4
),
fragmentManager = supportFragmentManager,
containerId = R.id.fragmentHost,
intent = intent,
preselectedGraphId = preselectedGraphId
)
controller.observe(
this,
Observer { navController ->
with(navController) {
removeOnDestinationChangedListener(destinationChangedListener)
addOnDestinationChangedListener(destinationChangedListener)
}
}
)
currentNavController = controller
}
}
import android.content.Intent
import android.util.SparseArray
import androidx.core.util.forEach
import androidx.core.util.set
import androidx.fragment.app.FragmentManager
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.navigation.NavController
import androidx.navigation.fragment.NavHostFragment
/**
* Manages the various graphs needed for a [CustomButtonGroup].
*
* This sample is a workaround until the Navigation Component supports multiple back stacks.
*/
fun CustomButtonGroup.setupWithNavController(
navGraphIds: List<Int>,
fragmentManager: FragmentManager,
containerId: Int,
intent: Intent,
preselectedGraphId: Int
): LiveData<NavController> {
// Map of tags
val graphIdToTagMap = SparseArray<String>()
// Result. Mutable live data with the selected controlled
val selectedNavController = MutableLiveData<NavController>()
var firstFragmentGraphId = 0
// First create a NavHostFragment for each NavGraph ID
navGraphIds.forEachIndexed { index, navGraphId ->
val fragmentTag = getFragmentTag(index)
// Find or create the Navigation host fragment
val navHostFragment = obtainNavHostFragment(
fragmentManager,
fragmentTag,
navGraphId,
containerId
)
// Obtain its id
val graphId = navHostFragment.navController.graph.id
if (index == 0) {
firstFragmentGraphId = graphId
}
// Save to the map
graphIdToTagMap[graphId] = fragmentTag
if (activeButtonId == null) {
selectButton(preselectedGraphId)
}
// Attach or detach nav host fragment depending on whether it's the selected item.
if (activeButtonId == graphId) {
// Update livedata with the selected graph
selectedNavController.value = navHostFragment.navController
attachNavHostFragment(fragmentManager, navHostFragment, index == 0)
} else {
detachNavHostFragment(fragmentManager, navHostFragment)
}
}
// Now connect selecting an item with swapping Fragments
var selectedItemTag = graphIdToTagMap[activeButtonId!!]
val firstFragTag = graphIdToTagMap[firstFragmentGraphId]
var isOnFirstFragment = selectedItemTag == firstFragTag
// When a navigation item is selected
setOnSelectListener { btn ->
if (btn.id == R.id.graph1) {
selectedNavController.value?.navigate(R.id.goToSpecialDestinationFromEachGraph)
return@setOnSelectListener
}
if (!fragmentManager.isStateSaved) {
val newlySelectedTag = graphIdToTagMap[btn.id]
if (selectedItemTag != newlySelectedTag) {
// Pop everything above the first fragment (the "fixed start destination")
fragmentManager.popBackStack(
firstFragTag,
FragmentManager.POP_BACK_STACK_INCLUSIVE
)
val selectedFragment =
fragmentManager.findFragmentByTag(newlySelectedTag) as NavHostFragment
// Exclude the first fragment tag because it's always in the back stack.
if (firstFragTag != newlySelectedTag) {
// Commit a transaction that cleans the
// back stack and adds the first fragment
// to it, creating the fixed started destination.
fragmentManager.beginTransaction()
.setCustomAnimations(
R.anim.nav_default_enter_anim,
R.anim.nav_default_exit_anim,
R.anim.nav_default_pop_enter_anim,
R.anim.nav_default_pop_exit_anim
)
.attach(selectedFragment)
.setPrimaryNavigationFragment(selectedFragment)
.apply {
// Detach all other Fragments
graphIdToTagMap.forEach { _, fragmentTagIter ->
if (fragmentTagIter != newlySelectedTag) {
detach(fragmentManager.findFragmentByTag(firstFragTag)!!)
}
}
}
.addToBackStack(firstFragTag)
.setReorderingAllowed(true)
.commit()
}
selectedItemTag = newlySelectedTag
isOnFirstFragment = selectedItemTag == firstFragTag
selectedNavController.value = selectedFragment.navController
}
}
}
// Handle deep link
setupDeepLinks(navGraphIds, fragmentManager, containerId, intent)
// Finally, ensure that we update our BottomNavigationView when the back stack changes
fragmentManager.addOnBackStackChangedListener {
if (!isOnFirstFragment && !fragmentManager.isOnBackStack(firstFragTag)) {
this.selectButton(firstFragmentGraphId)
}
// Reset the graph if the currentDestination is not valid (happens when the back
// stack is popped after using the back button).
selectedNavController.value?.let { controller ->
if (controller.currentDestination == null) {
controller.navigate(controller.graph.id)
}
}
}
return selectedNavController
}
private fun CustomButtonGroup.setupDeepLinks(
navGraphIds: List<Int>,
fragmentManager: FragmentManager,
containerId: Int,
intent: Intent
) {
navGraphIds.forEachIndexed { index, navGraphId ->
val fragmentTag = getFragmentTag(index)
// Find or create the Navigation host fragment
val navHostFragment = obtainNavHostFragment(
fragmentManager,
fragmentTag,
navGraphId,
containerId
)
// Handle Intent
if (navHostFragment.navController.handleDeepLink(intent) &&
activeButtonId != navHostFragment.navController.graph.id
) {
this.selectButton(navHostFragment.navController.graph.id)
}
}
}
private fun detachNavHostFragment(
fragmentManager: FragmentManager,
navHostFragment: NavHostFragment
) {
fragmentManager.beginTransaction()
.detach(navHostFragment)
.commitNow()
}
private fun attachNavHostFragment(
fragmentManager: FragmentManager,
navHostFragment: NavHostFragment,
isPrimaryNavFragment: Boolean
) {
fragmentManager.beginTransaction()
.attach(navHostFragment)
.apply {
if (isPrimaryNavFragment) {
setPrimaryNavigationFragment(navHostFragment)
}
}
.commitNow()
}
private fun obtainNavHostFragment(
fragmentManager: FragmentManager,
fragmentTag: String,
navGraphId: Int,
containerId: Int
): NavHostFragment {
// If the Nav Host fragment exists, return it
val existingFragment = fragmentManager.findFragmentByTag(fragmentTag) as NavHostFragment?
existingFragment?.let { return it }
// Otherwise, create it and return it.
val navHostFragment = NavHostFragment.create(navGraphId)
fragmentManager.beginTransaction()
.add(containerId, navHostFragment, fragmentTag)
.commitNow()
return navHostFragment
}
private fun FragmentManager.isOnBackStack(backStackName: String): Boolean {
val backStackCount = backStackEntryCount
for (index in 0 until backStackCount) {
if (getBackStackEntryAt(index).name == backStackName) {
return true
}
}
return false
}
private fun getFragmentTag(index: Int) = "bottomNavigation#$index"
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment