Louis Chabert
Louis Chabert

Reputation: 439

How to add marquee text on items of NavigationView?

The names of my items are quite long, so I would like to make sure that their names scroll horizontally. I have searched on several SO posts, but I have not found a solution to my problem But I can't, I tried this:

my activity_main.xml :

    <com.google.android.material.navigation.NavigationView
    android:id="@+id/nav_view"
    android:layout_width="wrap_content"
    android:layout_height="match_parent"
    android:layout_gravity="start"
    android:ellipsize="marquee"
    android:focusable="true"
    android:focusableInTouchMode="true"
    android:marqueeRepeatLimit="marquee_forever"
    android:scrollHorizontally="true"
    android:singleLine="true"
    app:headerLayout="@layout/nav_header"
    app:menu="@menu/menuDrawer">

</com.google.android.material.navigation.NavigationView>

My XML "menuDrawer":

<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/nav_welcome"
        android:icon="@drawable/ic_folder_black_24dp"
        android:title="@string/menu_welcome" />
    <item
        android:id="@+id/nav_dataset1"
        android:icon="@drawable/ic_folder_black_24dp"
        android:title="@string/menu_dataset1"/>
    <item
        android:id="@+id/nav_dataset2"
        android:icon="@drawable/ic_folder_black_24dp"
        android:title="@string/menu_dataset2" />
    <item
        android:id="@+id/nav_dataset3"
        android:icon="@drawable/ic_folder_black_24dp"
        android:title="@string/menu_dataset3" />
</group>

My java :

private AppBarConfiguration mAppBarConfiguration;
private TextView tvDataset1;
private TextView tvDataset2;
private TextView tvDataset3;

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);
    Toolbar toolbar = findViewById(R.id.toolbar);
    tvDataset1 = this.findViewById(R.id.nav_dataset1);
    tvDataset1.setSelected(true);
    setSupportActionBar(toolbar);
    DrawerLayout drawer = findViewById(R.id.drawer_layout);
    NavigationView navigationView = findViewById(R.id.nav_view);
    // Passing each menu ID as a set of Ids because each
    // menu should be considered as top level destinations.
    mAppBarConfiguration = new AppBarConfiguration.Builder(
            R.id.nav_dataset1, R.id.nav_dataset2, R.id.nav_dataset3)
            .setDrawerLayout(drawer)
            .build();
    NavController navController = Navigation.findNavController(this, R.id.nav_host_fragment);
    NavigationUI.setupActionBarWithNavController(this, navController, mAppBarConfiguration);
    NavigationUI.setupWithNavController(navigationView, navController);


}

But no change, does anyone have an idea ? Please help

EDIT :

I tried to override this :

<?xml version="1.0" encoding="utf-8"?>
<merge xmlns:android="http://schemas.android.com/apk/res/android">
 <CheckedTextView
    android:id="@+id/design_menu_item_text"
    android:layout_width="0dp"
    android:layout_height="match_parent"
    android:layout_weight="1"
    android:drawablePadding="@dimen/design_navigation_icon_padding"
    android:gravity="center_vertical|start"
    android:textAppearance="@style/TextAppearance.AppCompat.Body2"
    android:ellipsize="marquee"
    android:marqueeRepeatLimit="marquee_forever"
    android:scrollHorizontally="true"
    android:focusable="true"
    android:focusableInTouchMode="true"/>
 <ViewStub
    android:id="@+id/design_menu_item_action_area_stub"
    android:inflatedId="@+id/design_menu_item_action_area"
    android:layout="@layout/design_menu_item_action_area"
    android:layout_width="wrap_content"
    android:layout_height="match_parent" />
</merge>

I have some changes : AFTER the override:

enter image description here

BEFORE the override :

enter image description here

Upvotes: 4

Views: 619

Answers (3)

MariosP
MariosP

Reputation: 9113

There is no official way to add marquee text on items of NavigationView i think the best approach is to implement your own custom RecyclerView to act as the NavigationView but if you want to stay with the default NavigationView below i will describe a workaround:

Short Description:

The key point is to find the NavigationMenuView which is a RecyclerView containing all the Navigation Menu items and from there to find all RecyclerView.ViewHolder visible children of type of NavigationMenuItemView to be able to get access to each menu item CheckedTextView to change its properties to marquee text.

Implementation

Below i have implemented two helper functions the setNavigationViewItemsMarquee(NavigationView navigationView) which is responsible to find the NavigationMenuView from the NavigationView and the second one the setNavigationMenuViewItemsMarquee(NavigationMenuView menuRecyclerView, boolean startMarquee) which is responsible to find all CheckedTextView visible items to change their properties to marquee text.

private void setNavigationViewItemsMarquee(NavigationView navigationView){

    //find the NavigationMenuView RecyclerView id
    int designNavigationViewId = getResources().getIdentifier("design_navigation_view", "id", getPackageName());
    if(designNavigationViewId!=0) {
        NavigationMenuView menuRecyclerView = navigationView.findViewById(designNavigationViewId);
        if(menuRecyclerView!=null) {
            //register ViewTreeObserver.OnGlobalLayoutListener to be informed for changes in the global layout state or the visibility of views within the view tree
            menuRecyclerView.getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener(){
                @Override
                public void onGlobalLayout() {
                    //remove the ViewTreeObserver.OnGlobalLayoutListener() to be called only once
                    menuRecyclerView.getViewTreeObserver().removeOnGlobalLayoutListener(this);
                    //set the initial Marquee state to false for all visible items
                    setNavigationMenuViewItemsMarquee(menuRecyclerView, false);
                }
            });
            this.mMenuRecyclerView = menuRecyclerView;
        }
    }
}

private void setNavigationMenuViewItemsMarquee(NavigationMenuView menuRecyclerView, boolean startMarquee){

    if(menuRecyclerView!=null){
        //for every visible child in RecyclerView get its ViewHolder as NavigationMenuItemView and from there find the CheckedTextView using the design_menu_item_text id
        for (int i = 0; i < menuRecyclerView.getChildCount(); i++) {
            View child = menuRecyclerView.getChildAt(i);
            if (child!=null) {
                RecyclerView.ViewHolder holder = menuRecyclerView.getChildViewHolder(child);
                if(holder!=null && holder.itemView instanceof NavigationMenuItemView){
                    NavigationMenuItemView navigationMenuItemView = (NavigationMenuItemView)holder.itemView;
                    int designMenuItemTextId = getResources().getIdentifier("design_menu_item_text", "id", getPackageName());
                    if(designMenuItemTextId!=0){
                        //CheckedTextView found change it to MARQUEE
                        CheckedTextView textView = navigationMenuItemView.findViewById(designMenuItemTextId);
                        if(textView!=null) {
                            textView.setSingleLine(true);
                            textView.setEllipsize(TextUtils.TruncateAt.MARQUEE);
                            textView.setHorizontalFadingEdgeEnabled(true);
                            textView.setMarqueeRepeatLimit(startMarquee ? -1 : 0);
                            textView.setSelected(startMarquee);
                        }
                    }
                }
            }
        }
    }
}

Usage:

private NavigationMenuView mMenuRecyclerView;

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);
    setSupportActionBar(findViewById(R.id.toolbar));

    //get DrawerLayout and NavigationView
    DrawerLayout drawer = findViewById(R.id.drawer_layout);
    NavigationView navigationView = findViewById(R.id.nav_view);

    //set the NavController with AppBarConfiguration
    mAppBarConfiguration = new AppBarConfiguration.Builder(
            R.id.nav_home, R.id.nav_gallery, R.id.nav_slideshow)
            .setOpenableLayout(drawer)
            .build();
    NavController navController = Navigation.findNavController(this, R.id.nav_host_fragment_content_main);
    NavigationUI.setupActionBarWithNavController(this, navController, mAppBarConfiguration);
    NavigationUI.setupWithNavController(navigationView, navController);

    //set NavigationView Items Marquee
    setNavigationViewItemsMarquee(navigationView);

    //add also the DrawerLayout.DrawerListener to start/stop the Marquee based on onDrawerOpened/onDrawerClosed callbacks
    drawer.addDrawerListener(new DrawerLayout.DrawerListener() {
        @Override
        public void onDrawerSlide(@NonNull View view, float v) {}

        @Override
        public void onDrawerStateChanged(int i) {}

        @Override
        public void onDrawerOpened(@NonNull View view) {
            //start Marquee effect for all visible items
            setNavigationMenuViewItemsMarquee(mMenuRecyclerView, true);
        }

        @Override
        public void onDrawerClosed(@NonNull View view) {
            //stop Marquee effect for all visible items
            setNavigationMenuViewItemsMarquee(mMenuRecyclerView, false);
        }
    });
}

From the above code i have used the DrawerLayout.DrawerListener to start the marquee effect only when the Drawer is opened and to stop the marquee effect when the Drawer is closed.

In case also you have menu items which are not visible currently in the screen and you need to add the marquee effect during a RecyclerView scroll you can listen to RecyclerView.OnScrollListener() like: mMenuRecyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() and in onScrolled() callback you can start the marque effect to the new visible items using the above helper like setNavigationMenuViewItemsMarquee(mMenuRecyclerView, true); Of Course you can modify further the code based on your needs.

Result:

drawer

Upvotes: 2

GensaGames
GensaGames

Reputation: 5788

The trick is using styles. Attribute works for direct item, for ex, when you have a TextView where you want to marquee, you add these changes directly onto the item.

In examples, where your view is actually custom (for ex. as you may know custom view might be set of mutliple different base views) you have to change it in styles in order for these attributes to propagate to corresponding items, in your example TextView.

So the changes should be in styles, something like next. Add a new style to the styles.xml:

res / values / styles.xml

<style name="TextAppearance">
    <item name="android:ellipsize">marquee</item>
    <item name="android:focusable">true</item>
    <item name="android:focusableInTouchMode>true</item>
    <item name="android:marqueeRepeatLimit">marquee_forever</item>

</style>

Set the new style to the NavigationView:

res / layout / activity_main.xml

<android.support.design.widget.NavigationView
    android:id="@+id/nav_view"
    android:layout_width="wrap_content"
    android:layout_height="match_parent"
    android:layout_gravity="start"
    android:fitsSystemWindows="true"
    app:headerLayout="@layout/nav_header_main"
    app:menu="@menu/activity_main_drawer"
    app:theme="@style/TextAppearance" />

PS. Another very simple approach it's by using custom entries for Navigation View. You can find more information on how to build it from here.

Upvotes: 1

Martin Zeitler
Martin Zeitler

Reputation: 76809

Styling Widget.MaterialComponents.NavigationView will not cause it to scroll.

One would have to replace the whole NavigationView with a ListView or RecyclerView.
And OnNavigationItemSelectedListener(MenuItem item) would need to be implemented.

Upvotes: 1

Related Questions