I've looked around and haven't been able to find exactly what I'm looking for. A little help would be appreciated. I'm attempting to implementing a SearchWidget as shown here. I'm getting a bizarre setup however. The search Icon is not even showing up, on the far right there is three vertical dots as part of the toolbar, and when I click on those a Search box appears. But clicking on that doesn't register anything through setOnClickListener or setOnQueryTextFocusChangeListener. Any help would be much appreciated. Like so:
toolbar when opening the app
popup search menu - doesn't do anything when I click on it
Here's what I've got
My SearchActivity:
class SearchCategoryActivity : MvvmActivity<SearchCategoryViewModel>() {
companion object {
private const val CATEGORY = "category"
fun newIntent(context: Context): Intent {
return Intent(context, SearchCategoryActivity::class.java)
}
}
#Inject
lateinit var viewModelFactory: ViewModelProvider.Factory
val listofCategories : List<Category>? = null
private lateinit var adapter: CategoryGroupAdapter
var browsingData : List<Category>? = null
override fun onCreate(savedInstanceState: Bundle?) {
AndroidInjection.inject(this)
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_search_category)
setSupportActionBar(toolbar)
val actionBar = supportActionBar
actionBar?.setDisplayHomeAsUpEnabled(true)
adapter = CategoryGroupAdapter(this)
adapter.setOnClickListener { category, _ -> onCategoryClick(category) }
recyclerView.layoutManager = LinearLayoutManager(this)
recyclerView.itemAnimator = DefaultItemAnimator()
recyclerView.adapter = adapter
toolbar.setNavigationOnClickListener { finish() }
//clearImageView.setOnClickListener { searchEdiText.text = null }
addFab.setOnClickListener { onAddFabClick() }
viewModel.loadCategories.subscribe(this, object : FlowableSubscriber<List<Category>> {
override fun onNext(data: List<Category>) {
browsingData = data
onLoadCategories(data)
}
override fun onComplete() {
Timber.error { "onComplete" }
}
override fun onError(error: Throwable) {
onLoadCategoriesFailed(error)
}
})
LceAnimator.showLoading(loading, content, error)
viewModel.loadCategories()
System.out.println("Here")
}
override fun onCreateOptionsMenu(menu: Menu): Boolean {
// Inflate the options menu from XML
val inflater = menuInflater
inflater.inflate(R.menu.menu_search, menu)
// Get the SearchView and set the searchable configuration
val searchManager = getSystemService(Context.SEARCH_SERVICE) as SearchManager
val searchView = menu.findItem(R.id.action_search).actionView as SearchView?
// Assumes current activity is the searchable activity
searchView?.setSearchableInfo(searchManager.getSearchableInfo(componentName))
searchView?.setIconifiedByDefault(false) // Do not iconify the widget; expand it by default
searchView?.setOnClickListener(object : View.OnClickListener {
override fun onClick(v: View?) {
TODO("not implemented") //To change body of created functions use File | Settings | File Templates.
System.out.println("clicked")
}
})
searchView?.setOnQueryTextFocusChangeListener(object : View.OnFocusChangeListener {
override fun onFocusChange(v: View?, hasFocus: Boolean) {
TODO("not implemented") //To change body of created functions use File | Settings | File Templates.
}
})
My SearchActivity.xml:
<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="#+id/container"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#color/background">
<!-- Dummy item to prevent AutoCompleteTextView from receiving focus -->
<!-- :nextFocusUp and :nextFocusLeft have been set to the id of this component
to prevent the dummy from receiving focus again -->
<com.google.android.material.appbar.AppBarLayout
android:id="#+id/appbar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:theme="#style/ThemeOverlay.AppCompat.Dark">
<androidx.appcompat.widget.Toolbar
android:id="#+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize">
</androidx.appcompat.widget.Toolbar>
</com.google.android.material.appbar.AppBarLayout>
<FrameLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_behavior="#string/appbar_scrolling_view_behavior">
<include layout="#layout/layout_loading" />
<include layout="#layout/layout_search" />
<include layout="#layout/layout_error" />
</FrameLayout>
</androidx.coordinatorlayout.widget.CoordinatorLayout>
My searchable.xml:
<?xml version="1.0" encoding="utf-8"?>
<searchable xmlns:android="http://schemas.android.com/apk/res/android"
android:label="#string/app_name"
android:hint="#string/search_hint" />
My search_menu.xml:
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<item
android:id="#+id/action_search"
android:actionViewClass="android.widget.SearchView"
android:layout_width="match_parent"
android:icon="#android:drawable/ic_search_category_default"
android:showAsAction="always"
android:title="#string/search"
app:queryBackground="#color/background"/>
</menu>
Try adding app instead of android and using v7:
app:actionViewClass="android.support.v7.widget.SearchView"
Also, to make it collapsable:
app:showAsAction="always|collapseActionView"
And no need for android:layout_width="match_parent".
You can try this so it fill would fill the toolbar
searchMenuItem.actionView.also {
it.post {
it.layoutParams = it.layoutParams.apply {
width = ViewGroup.LayoutParams.MATCH_PARENT
}
}
}
Related
So i just got started with android development a couple weeks ago, while trying to make navigation between two fragments with a bottom nav bar, for some reasons the click listener is just not working, no errors, no warnings, compiles no problem but it might be some logic problem? here's the code:
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val fragmentHome = HomeFragment()
val fragmentProfile = ProfileFragment()
replaceCurrentFragment(fragmentHome)
NavigationBarView.OnItemSelectedListener { item ->
when(item.itemId) {
R.id.page_home -> {
Log.i("NavBar","Home pressed")
replaceCurrentFragment(fragmentHome)
true
}
R.id.page_profile -> {
Log.i("NavBar","Profile pressed")
replaceCurrentFragment(fragmentProfile)
true
}
else -> {
Log.i("NavBar","Error?")
false
}
}
}
}
private fun replaceCurrentFragment(fragment: Fragment) =
supportFragmentManager.beginTransaction().apply {
replace(R.id.flFragment, fragment)
commit()
}
}
Layout:
<?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"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<FrameLayout
android:id="#+id/flFragment"
android:layout_width="0dp"
android:layout_height="0dp"
app:layout_constraintBottom_toTopOf="#+id/bottom_navigation"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<com.google.android.material.bottomnavigation.BottomNavigationView
app:labelVisibilityMode="selected"
android:id="#+id/bottom_navigation"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:menu="#menu/bottom_navigation_menu" />
</androidx.constraintlayout.widget.ConstraintLayout>
Menu:
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android">
<item
android:id="#+id/page_home"
android:icon="#drawable/ic_home"
android:title="Home"/>
<item
android:id="#+id/page_profile"
android:icon="#drawable/ic_profile"
android:title="Profile"/>
</menu>
the main page does get initialized on the home fragment, but when i click the nav bar buttons, nothing happens so the problem is in that listener part, copy pasted from the docs yet it doesn't work.
i also have a bug where the layout editor doesn't show the nav bar contents, so my ide might be glitched? would appreciate any help.
Replace "NavigationBarView" with your actual bottomNavigationView id.
This would work,
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val fragmentHome = HomeFragment()
val fragmentProfile = ProfileFragment()
replaceCurrentFragment(fragmentHome)
val myBottomNavigationView = findViewById<BottomNavigationView>(R.id.bottom_navigation)
myBottomNavigationView.OnItemSelectedListener { item ->
when(item.itemId) {
R.id.page_home -> {
Log.i("NavBar","Home pressed")
replaceCurrentFragment(fragmentHome)
true
}
R.id.page_profile -> {
Log.i("NavBar","Profile pressed")
replaceCurrentFragment(fragmentProfile)
true
}
else -> {
Log.i("NavBar","Error?")
false
}
}
}
}
private fun replaceCurrentFragment(fragment: Fragment) =
supportFragmentManager.beginTransaction().apply {
replace(R.id.flFragment, fragment)
commit()
}
}
I'm trying to open Fragment2 from Fragment1(TransformFragment)
throught click item in RecyclerView. I tried to use Navigation (NavHost) to solve this problem.
Fragment1 code as below:
class TransformFragment : Fragment() {
private lateinit var transformViewModel: TransformViewModel
private var _binding: FragmentTransformBinding? = null
// This property is only valid between onCreateView and
// onDestroyView.
private val binding
get() = _binding!!
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
transformViewModel = ViewModelProvider(this)[TransformViewModel::class.java]
_binding = FragmentTransformBinding.inflate(inflater, container, false)
val root: View = binding.root
val recyclerView = binding.recyclerviewTransform
val adapter = TransformAdapter()
recyclerView.adapter = adapter
transformViewModel.texts.observe(viewLifecycleOwner) { adapter.submitList(it) }
return root
}
override fun onDestroyView() {
super.onDestroyView()
_binding = null
}
class TransformAdapter() :
ListAdapter<String, TransformViewHolder>(
object : DiffUtil.ItemCallback<String>() {
override fun areItemsTheSame(oldItem: String, newItem: String): Boolean =
oldItem == newItem
override fun areContentsTheSame(oldItem: String, newItem: String): Boolean =
oldItem == newItem
}
) {
private val drawables =
listOf(
R.drawable.avatar_1,
R.drawable.avatar_2,
R.drawable.avatar_3,
R.drawable.avatar_4,
R.drawable.avatar_5,
R.drawable.avatar_6,
R.drawable.avatar_7,
R.drawable.avatar_8,
R.drawable.avatar_9,
R.drawable.avatar_10,
R.drawable.avatar_11,
R.drawable.avatar_12,
R.drawable.avatar_13,
R.drawable.avatar_14,
R.drawable.avatar_15,
R.drawable.avatar_16,
R.drawable.avatar_17,
)
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): TransformViewHolder {
val binding = ItemTransformBinding.inflate(LayoutInflater.from(parent.context))
return TransformViewHolder(binding)
}
override fun onBindViewHolder(holder: TransformViewHolder, position: Int) {
holder.textView.text = getItem(position)
holder.imageView.setImageDrawable(
ResourcesCompat.getDrawable(holder.imageView.resources, drawables[position], null)
)
holder.itemView.setOnClickListener {
// my navigation(NavHost) code
}
}
}
class TransformViewHolder(binding: ItemTransformBinding) :
RecyclerView.ViewHolder(binding.root) {
val imageView: ImageView = binding.imageViewItemTransform
val textView: TextView = binding.textViewItemTransform
}
}
I tried to use this:
val navController = findNavController(R.id.nav_host_fragment_content_main)
navController.navigate(R.id.nav_detail)
nav_detail to navigate Fragment2, and My Fragment Container. This's my XML. I know that to display Dynamiclly Fragments, you must use FrameLayout, but it produces errors and the fragment container is OK.
<LinearLayout 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:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
app:layout_behavior="#string/appbar_scrolling_view_behavior"
tools:showIn="#layout/app_bar_main">
<fragment
android:id="#+id/nav_host_fragment_content_main"
android:name="androidx.navigation.fragment.NavHostFragment"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_marginLeft="#dimen/fragment_horizontal_margin"
android:layout_marginRight="#dimen/fragment_horizontal_margin"
android:layout_weight="1"
app:defaultNavHost="true"
app:navGraph="#navigation/mobile_navigation" />
<!--
<FrameLayout
android:id="#+id/nav_host_fragment_content_main"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_marginLeft="#dimen/fragment_horizontal_margin"
android:layout_marginRight="#dimen/fragment_horizontal_margin"
android:layout_weight="1"
app:defaultNavHost="true"
app:navGraph="#navigation/mobile_navigation" />
-->
</LinearLayout>
I've found the correct answer in Android developers documentation.
holder.itemView.setOnClickListener {
it.findNavController().navigate(R.id.nav_detail)
}
To use findNavController() to navigate between fragments, you've to make sure that you've setup with activity properly first.
So, in your activity layout first add container, parent need nod to be FrameLayout itself, I'm gonna make use of ConstraintLayout below.
activity_main.xml
<?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"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<!-- We declare attribute id to hook with MainActivity.java class-->
<!-- Attribute name will change behavior of simple fragment to NavHostFragment-->
<!-- Keep width and height to match_parent so that all our fragments take whole space-->
<!-- We change this NavHost to behave default NavHost-->
<!-- Attach our app navigation graph to this NavHostFragment-->
<fragment
android:id="#+id/nav_host_fragment"
android:name="androidx.navigation.fragment.NavHostFragment"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:defaultNavHost="true"
app:navGraph="#navigation/nav_graph" />
</androidx.constraintlayout.widget.ConstraintLayout>
Now let's hook container as an acting host for activity.
MainActivity.kt
class MainActivity : AppCompatActivity() {
lateinit var navController: NavController
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
// NavigationController to set default NavHost as nav_host_fragment.
navController = Navigation.findNavController(this, R.id.nav_host_fragment)
NavigationUI.setupActionBarWithNavController(this, navController)
}
override fun onSupportNavigateUp(): Boolean {
return navController.navigateUp()
}
}
onSupportNavigateUp function helps you to navigate back to previous fragment on action trigered or called specifically on any view with navigateUp().
Now you want to navigate from Fragment1 -> Fragment2.
Make sure you've added two fragments to navigation graph.
Connect first fragment to second with action.
Keep track of destination Id's to call correct one.
<?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_graph"
app:startDestination="#id/firstFragment">
<fragment
android:id="#+id/firstFragment"
android:name=".FirstFragment"
android:label="FirstFragment"
tools:layout="#layout/fragment_first">
<action
android:id="#+id/action_first_fragment_to_second_fragment"
app:destination="#id/secondFragment" />
</fragment>
<fragment
android:id="#+id/secondFragment"
android:name=".SecondFragment"
android:label="Authors"
tools:layout="#layout/fragment_second" />
</navigation>
Now finally once action is triggered call action with id like below on controller.
yourListItemView.setOnClickListener {
findNavController(R.id.action_first_fragment_to_second_fragment)
}
That's it.
Please check the documentation, it has a good information.
Iv been stuck in trying to add the Navigation Component to my app. Specifically to my toolbar menu items.
I have been following the navigation migrate documentation on Android's site but I am getting a but confused on how it works or even to set it up.
This is a single-activity multi-fragment architecture.
The app would start in the MainFragment and then when tapping the menu items in the toolbar, such as Main Frag to Search Movie Frag. The user would be able to navigate using the menu items from anywhere in the app (e.g. if they click the home icon, they should be sent to the home screen form anywhere in the app. Still thinking if thats how it should be done).
The main thing is I don't know how to properly attach the Navigation Component to the menu items.
App
Nav Graph
nav_graph.xml
<?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_graph"
app:startDestination="#id/mainFragment">
<fragment
android:id="#+id/mainFragment"
android:name="com.example.movieapp.ui.main.MainFragment"
android:label="MainFragment">
<action
android:id="#+id/action_mainFragment_to_searchMovieFragment"
app:destination="#id/searchMovieFragment" />
</fragment>
<fragment
android:id="#+id/searchMovieFragment"
android:name="com.example.movieapp.ui.search.SearchMovieFragment"
android:label="SearchMovieFragment" />
</navigation>
main_activity.xml
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="#+id/container"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".activity.MainActivity">
<include
layout="#layout/appbar"
android:id="#+id/mainToolBar"/>
<androidx.fragment.app.FragmentContainerView
android:id="#+id/nav_host_fragment"
android:name="androidx.navigation.fragment.NavHostFragment"
android:layout_width="0dp"
android:layout_height="0dp"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toBottomOf="#id/mainToolBar"
app:layout_constraintBottom_toBottomOf="parent"
app:defaultNavHost="true"
app:navGraph="#navigation/nav_graph" />
</androidx.constraintlayout.widget.ConstraintLayout>
MainActivity.kt
class MainActivity : AppCompatActivity() {
private lateinit var toolbar: Toolbar
private val navController by lazy {
(supportFragmentManager.findFragmentById(R.id.nav_host_fragment) as NavHostFragment).navController
}
private lateinit var appBarConfiguration: AppBarConfiguration
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.main_activity)
toolbar = findViewById(R.id.mainToolBar)
setSupportActionBar(toolbar)
appBarConfiguration = AppBarConfiguration(navController.graph, null)
setupActionBarWithNavController(navController, appBarConfiguration)
}
override fun onCreateOptionsMenu(menu: Menu?): Boolean {
val inflater: MenuInflater = menuInflater
inflater.inflate(R.menu.main_menu_bar, menu)
return true
}
override fun onSupportNavigateUp(): Boolean {
return navController.navigateUp(appBarConfiguration) || super.onSupportNavigateUp()
}
}
MainFragment.kt
class MainFragment : Fragment() {
companion object {
fun newInstance() = MainFragment()
}
private lateinit var viewModel: MainViewModel
private lateinit var binding: MainFragmentBinding
private lateinit var navController: NavController
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
binding = DataBindingUtil.inflate(inflater, R.layout.main_fragment, container, false)
setHasOptionsMenu(true)
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
navController = Navigation.findNavController(view)
// navController.navigate(R.id.action_mainFragment_to_searchMovieFragment)
}
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
viewModel = ViewModelProvider(this).get(MainViewModel::class.java)
binding.viewModel = viewModel
// TODO: Use the ViewModel
}
}
main_fragment.xml
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<data>
<variable
name="viewModel"
type="com.example.movieapp.ui.main.MainViewModel" />
</data>
<LinearLayout
android:orientation="vertical"
android:id="#+id/main_fragment"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".ui.main.MainFragment">
<TextView
android:id="#+id/message"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="MainFragment1" />
</LinearLayout>
</layout>
The id for a menu item in your file main_menu_bar.xml needs to match the id for the destination(fragment) specified in your navigation graph (nav_graph.xml).
Your main_menu_bar.xml should look something like this:
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
tools:showIn="navigation_view">
<group android:checkableBehavior="single">
<item
android:id="#id/searchMovieFragment"
android:icon="#drawable/search"
android:title="#string/menu_search" />
<item
android:id="#id/mainFragment"
android:icon="#drawable/ic_menu_camera"
android:title="#string/menu_home" />
</group>
</menu>
Also, you need to override the onOptionsItemSelected(MenuItem) method and associate your menu items with destinations. Like so:
override fun onOptionsItemSelected(item: MenuItem): Boolean {
val navController = findNavController(R.id.nav_host_fragment)
return item.onNavDestinationSelected(navController) || super.onOptionsItemSelected(item)
}
Reference
You can add a Toolbar on top of the most basic FragmentContainerView, access this toolbar from other fragments, and change the toolbar I created separately according to the fragment in it while opening the relevant fragment as follows.
toolbar = requireActivity().findViewById(R.id.toolbar_base)
toolbar?.menu?.clear()
toolbar?.title = ""
val toolbarFlowFragment = context?.let { getInflateLayout(it, R.layout.toolbar_profile) }
toolbar?.addView(toolbarFlowFragment)
I'm using a BottomNavigationView in my app. Right now my navigation view looks like this:
but I want it to be with underlined selected item, like this:
Are there any ways to do this with some standard attributes?
You can do that using a SpannableString with UnderlineSpan to the item title when this item is selected by the user by setting OnNavigationItemSelectedListener listener to the BottomNavigationView
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
...
BottomNavigationView bottomNavigationView = (BottomNavigationView)
findViewById(R.id.bottom_navigation);
underlineMenuItem(bottomNavigationView.getMenu().getItem(0)); // underline the default selected item when the activity is launched
bottomNavigationView.setOnNavigationItemSelectedListener(
new BottomNavigationView.OnNavigationItemSelectedListener() {
#Override
public boolean onNavigationItemSelected(#NonNull MenuItem item) {
removeItemsUnderline(bottomNavigationView); // remove underline from all items
underlineMenuItem(item); // underline selected item
switch (item.getItemId()) {
// handle item clicks
}
return false;
}
});
}
private void removeItemsUnderline(BottomNavigationView bottomNavigationView) {
for (int i = 0; i < bottomNavigationView.getMenu().size(); i++) {
MenuItem item = bottomNavigationView.getMenu().getItem(i);
item.setTitle(item.getTitle().toString());
}
}
private void underlineMenuItem(MenuItem item) {
SpannableString content = new SpannableString(item.getTitle());
content.setSpan(new UnderlineSpan(), 0, content.length(), 0);
item.setTitle(content);
}
This works exactly if you're using text based items, but in your case you're just using icons in your menu, and to resolve this issue; you have to utilize the android:title of menu items in menu.xml with white spaces as follows
bottom_nav_menu.xml
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<item
android:id="#+id/action_favorites"
android:enabled="true"
android:icon="#drawable/ic_favorite_white_24dp"
android:title="#string/text_spaces"
app:showAsAction="ifRoom" />
<item
android:id="#+id/action_schedules"
android:enabled="true"
android:icon="#drawable/ic_access_time_white_24dp"
android:title="#string/text_spaces"
app:showAsAction="ifRoom" />
<item
android:id="#+id/action_music"
android:enabled="true"
android:icon="#drawable/ic_audiotrack_white_24dp"
android:title="#string/text_spaces"
app:showAsAction="ifRoom" />
</menu>
And use in your text as many times as you need spaces which will reflect on the length of the the line under each item
strings.xml
<resources>
...
<string name="text_spaces"> </string>
This is a preview
hope this solves your issue, and happy for any queries.
I know I'm late to the party, but for the next generations - this is a solution with more control + animation:) using constraint layout.
the example is for 4 items, adjust the numbers.
first, create a view with the desired characteristics in the (constraint) layout that contains the BottomNavigationView. set app:layout_constraintWidth_percent to 1/number of items
<com.google.android.material.bottomnavigation.BottomNavigationView
android:id="#+id/mainTabBottomNavigation"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="bottom"
android:background="#android:color/white"
android:nestedScrollingEnabled="true"
app:elevation="16dp"
app:itemIconTint="#drawable/nav_account_item"
app:labelVisibilityMode="unlabeled"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:menu="#menu/main_bottom_navigation"
/>
<View
android:id="#+id/underline"
android:layout_width="0dp"
android:layout_height="3dp"
android:background="#color/underlineColor"
android:elevation="16dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintWidth_percent="0.25" />
</androidx.constraintlayout.widget.ConstraintLayout>
Then, use this function inside OnNavigationItemSelectedListener:
private fun underlineSelectedItem(view: View, itemId: Int) {
val constraintLayout: ConstraintLayout = view as ConstraintLayout
TransitionManager.beginDelayedTransition(constraintLayout)
val constraintSet = ConstraintSet()
constraintSet.clone(constraintLayout)
constraintSet.setHorizontalBias(
R.id.underline,
getItemPosition(itemId) * 0.33f
)
constraintSet.applyTo(constraintLayout)
}
complete code (inside a fragment):
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
val navController = Navigation.findNavController(
requireActivity(),
R.id.mainNavigationFragment
)
mainTabBottomNavigation.setupWithNavController(navController)
underlineSelectedItem(view, R.id.bottomNavFragmentHome) //select first item
mainTabBottomNavigation.setOnNavigationItemSelectedListener { item ->
underlineSelectedItem(view, item.itemId)
true
}
}
private fun underlineSelectedItem(view: View, itemId: Int) {
val constraintLayout: ConstraintLayout = view as ConstraintLayout
TransitionManager.beginDelayedTransition(constraintLayout)
val constraintSet = ConstraintSet()
constraintSet.clone(constraintLayout)
constraintSet.setHorizontalBias(
R.id.underline,
getItemPosition(itemId) * 0.33f
)
constraintSet.applyTo(constraintLayout)
}
private fun getItemPosition(itemId: Int): Int {
return when (itemId) {
R.id.bottomNavFragmentHome -> 0
R.id.bottomNavFragmentMyAccount -> 1
R.id.bottomNavFragmentCoupon -> 2
R.id.bottomNavFragmentSettings -> 3
else -> 0
}
}
Notice that this implementation overrides the navigation functionality.
In order to maintain this functionality, you'll need to use NavigationUI.onNavDestinationSelected(item, navController)
at the end of the transition animation.
complete code:
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
val navController = Navigation.findNavController(
requireActivity(),
R.id.mainNavigationFragment
)
mainTabBottomNavigation.setupWithNavController(navController)
underlineSelectedItem(view, R.id.bottomNavFragmentHome, null, null, null)
mainTabBottomNavigation.setOnNavigationItemSelectedListener { item ->
underlineSelectedItem(view, item.itemId, item, navController) { item1, navController1 ->
safeLet(item1, navController1) { a, b->
NavigationUI.onNavDestinationSelected(a, b)
}
}
true
}
}
private fun underlineSelectedItem(
view: View,
itemId: Int,
item: MenuItem?,
navController: NavController?,
onAnimationEnd: ((item: MenuItem?, navController: NavController?) -> Unit)?
) {
val constraintLayout: ConstraintLayout = view as ConstraintLayout
val transition: Transition = ChangeBounds()
transition.addListener(object : Transition.TransitionListener {
override fun onTransitionStart(transition: Transition?) {
}
override fun onTransitionEnd(transition: Transition?) {
onAnimationEnd?.invoke(item, navController)
}
override fun onTransitionCancel(transition: Transition?) {
}
override fun onTransitionPause(transition: Transition?) {
}
override fun onTransitionResume(transition: Transition?) {
}
})
TransitionManager.beginDelayedTransition(constraintLayout, transition)
val constraintSet = ConstraintSet()
constraintSet.clone(constraintLayout)
constraintSet.setHorizontalBias(
R.id.underline,
getItemPosition(itemId) * 0.33f
)
constraintSet.applyTo(constraintLayout)
}
private fun getItemPosition(itemId: Int): Int {
return when (itemId) {
R.id.bottomNavFragmentHome -> 0
R.id.bottomNavFragmentMyAccount -> 1
R.id.bottomNavFragmentCoupon -> 2
R.id.bottomNavFragmentSettings -> 3
else -> 0
}
}
(safeLet is a Kotlin helper function for checking two variables nullabilty:
fun <T1 : Any, T2 : Any, R : Any> safeLet(p1: T1?, p2: T2?, block: (T1, T2) -> R?): R? {
return if (p1 != null && p2 != null) block(p1, p2) else null
}
)
final result:
This could be a simpler & better solution than my other answer; it also could have a variety of capabilities like the thickness of the width/height, corners, shapes, padding..etc stuff of drawable capabilities.
You can create a selector (only with a checked state) that has a gravity set to the bottom:
item_background.xml:
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:state_checked="true">
<layer-list>
<item android:gravity="bottom|center_horizontal">
<shape android:shape="rectangle">
<size android:width="100dp" android:height="5dp" />
<solid android:color="#03DAC5" />
<corners android:bottomLeftRadius="3dp" android:bottomRightRadius="3dp" />
</shape>
</item>
</layer-list>
</item>
</selector>
Set this to app:itemBackground:
<com.google.android.material.bottomnavigation.BottomNavigationView
....
app:itemBackground="#drawable/item_background"
I used to have a single activity that included the navigation host and the BottomNavigationView that allowed switching views between the 3 fragments. I recently changed my design so that the initial activity now represents a single new fragment, and there exists a button that allows the user to navigate to another fragment which should hold the aforementioned 3 tabbed bottom navigation.
I've managed to implement the initial activity and the first fragment. But the issue that I am having is that when I navigate to the second fragment with the 3 tabbed bottom navigation bar, I don't know how to implement the onClickListener on the tabs. It used to be fairly straightforward when I had the bottom navigation inside the AppCompatActivity. I'm guessing that fragments are not supposed to be used for this case. I could easily use another activity class but I wanted to build a single activity application and wondered if there was another solution to this.
In the original method using an activity class, I was able to implement the bottom navigation like below:
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
setSupportActionBar(toolbar)
val navController = Navigation.findNavController(this, R.id.nav_host_fragment)
setupBottomNavMenu(navController)
setupActionBar(navController)
}
override fun onCreateOptionsMenu(menu: Menu?): Boolean {
menuInflater.inflate(R.menu.menu_toolbar, menu)
return true
}
override fun onOptionsItemSelected(item: MenuItem?): Boolean {
val navController = Navigation.findNavController(this, R.id.nav_host_fragment)
val navigated = NavigationUI.onNavDestinationSelected(item!!, navController)
return navigated || super.onOptionsItemSelected(item)
}
override fun onSupportNavigateUp(): Boolean {
return NavigationUI.navigateUp(Navigation.findNavController(this, R.id.nav_host_fragment), drawer_layout)
}
private fun setupBottomNavMenu(navController: NavController) {
bottom_nav?.let {
NavigationUI.setupWithNavController(it, navController)
}
}
private fun setupActionBar(navController: NavController) {
NavigationUI.setupActionBarWithNavController(this, navController, drawer_layout)
}
}
<?xml version="1.0" encoding="utf-8"?>
<layout
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">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:background="#color/colorMintCream"
tools:context=".activity.MainActivity">
<androidx.appcompat.widget.Toolbar
android:id="#+id/toolbar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?colorPrimary"
android:theme="#style/ToolbarTheme"/>
<fragment
android:id="#+id/nav_host_fragment"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
android:name="androidx.navigation.fragment.NavHostFragment"
app:navGraph="#navigation/main_nav_graph"
app:defaultNavHost="true"/>
<com.google.android.material.bottomnavigation.BottomNavigationView
android:id="#+id/bottom_nav"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:menu="#menu/menu_navigation"/>
</LinearLayout>
</layout>
The navigation looks like follows:
<?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" android:id="#+id/mobile_navigation.xml"
app:startDestination="#id/destination_profile">
<fragment
android:id="#+id/destination_profile"
android:name="com.example.project.profile.ProfileFragment"
android:label="Profile" />
<fragment
android:id="#+id/destination_achievements"
android:name="com.example.project.fragments.AchievementsFragment"
android:label="Achievements" />
<fragment
android:id="#+id/destination_logs"
android:name="com.example.project.fragments.LogsFragment"
android:label="Logs" />
<fragment
android:id="#+id/destination_settings"
android:name="com.example.project.fragments.SettingsFragment"
android:label="Settings" />
</navigation>
And finally the menu:
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android">
<item
android:id="#+id/destination_profile"
android:icon="#drawable/ic_person_black_24dp"
android:title="#string/profile"/>
<item
android:id="#+id/destination_logs"
android:icon="#drawable/ic_fitness_center_black_24dp"
android:title="#string/logs"/>
<item
android:id="#+id/destination_achievements"
android:icon="#drawable/ic_star_black_24dp"
android:title="#string/achievements"/>
</menu>
Similarly I tried to apply this on a fragment like below:
class MainFragment : Fragment() {
override fun onCreateView(inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?): View? {
var binding: FragmentMainBinding = DataBindingUtil.inflate(
inflater,
R.layout.fragment_main,
container,
false
)
val application = requireNotNull(this.activity).application
val dataSource = UserProfileDatabase.getInstance(application).userProfileDao
val viewModelFactory = MainViewModelFactory(dataSource, application)
val navController = this.findNavController()
NavigationUI.setupWithNavController(binding.bottomNav, navController)
return binding.root
}
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
super.onCreateOptionsMenu(menu, inflater)
inflater.inflate(R.menu.menu_toolbar, menu)
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
val navController = this.findNavController()
val navigated = NavigationUI.onNavDestinationSelected(item, navController)
return navigated || super.onOptionsItemSelected(item)
}
companion object {
fun newInstance(): MainFragment = MainFragment()
}
}
The xml is almost exactly the same as the one for activity.
I expected to invoke onOptionsItemSelected() when I clicked on the tab elements, but I wasn't able to do so. How can I implement the listeners on those tab elements so that I can navigate to the correct fragment destinations?
Why don't you keep your first implementation for your BottomNavigationView which is good and combine it with your new implementation by using addOnDestinationChangedListener.
Your FirstFragment that hold the button will hide the BottomNavigationView and the SecondFragment with the 3 tabbed bottom navigation will show it.
For example :
navController.addOnDestinationChangedListener { _, destination, _ ->
if(destination.id == R.id.first_fragment) {
// your intro fragment will hide your bottomNavigationView
bottomNavigationView.visibility = View.GONE
} else if (destination.id == R.id.second_fragment){
// your second fragment will show your bottomNavigationView
bottomNavigationView.visibility = View.VISIBLE
}
}
in that way you keep the control in your Activity instead of Fragment and by that interact better with your listeners and your NavController.
Keep your menu item's id same as your graph id and simply use 1 line code i.e
val navHostFragment =childFragmentManager.findFragmentById(R.id.fragmentContainerView) as NavHostFragment
val navController = navHostFragment.navController
binding.bottomNavBar.setupWithNavController(navController)