44


I've made an app using Android Design Library, with a Toolbar and TabLayout.
Actually 2 tabs are present, both with 2 RecyclerView, that automatically collapse the Toolbar when scrolled.

My question is: can I disable Toolbar collapsing when RecyclerView has few items and completely fits the screen (like in TAB 2)?

I've seen a lot of examples like CheeseSquare, made by a Google employee where the issue is still present: even if the RecyclerView has just 1 item, the toolbar keeps hiding on scroll.

enter image description here

I think I can just find out if the first item of the RecyclerView is visible on screen and if yes disable toolbar collapsing. The former is easy to implement, what about the latter?

This is my layout:

<android.support.design.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:id="@+id/coordinator_layout"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

        <android.support.design.widget.AppBarLayout
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:fitsSystemWindows="true"
            android:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar">

        <android.support.v7.widget.Toolbar
            android:id="@+id/toolbar"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            app:layout_scrollFlags="scroll|enterAlwaysCollapsed"
            android:background="?attr/colorPrimary"
            app:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar"
            app:popupTheme="@style/ThemeOverlay.AppCompat.Light"/>

        <android.support.design.widget.TabLayout
            android:id="@+id/tab_layout"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:background="@color/glucosio_pink"
            app:tabSelectedTextColor="@android:color/white"
            app:tabIndicatorColor="@color/glucosio_accent"
            app:tabTextColor="#80ffffff"/>
        </android.support.design.widget.AppBarLayout>

        <android.support.v4.view.ViewPager
            android:id="@+id/pager"
            app:layout_behavior="@string/appbar_scrolling_view_behavior"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"/>

    <android.support.design.widget.FloatingActionButton
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:id="@+id/main_fab"
        android:layout_margin="16dp"
        android:onClick="onFabClicked"
        app:backgroundTint="@color/glucosio_accent"
        android:src="@drawable/ic_add_black_24dp"
        android:layout_gravity="bottom|right"
        />
    </android.support.design.widget.CoordinatorLayout>




Paolo Rotolo
  • 1,243
  • 1
  • 14
  • 22
  • Can you tell me how you creating this gif? – Pratik Butani Sep 16 '15 at 04:40
  • 1
    Hi @PratikButani! I've found the gif on internet (https://mzgreen.github.io/2015/06/23/How-to-hideshow-Toolbar-when-list-is-scrolling(part3)/). BTW, you can use services like https://gifs.com/ to create a gif from a YouTube video. – Paolo Rotolo Sep 16 '15 at 13:18
  • Twitted to that person with your link :) May he will help you. Thanks :) https://twitter.com/pratik13butani/status/644355243174526976 – Pratik Butani Sep 17 '15 at 03:41
  • What do expect it to work? Let's say that you collapse the Toolbar on first page, then you swipe to second page. What should happen? Toolbar is hidden and you can't show it because there is too few items. And for disabling Toolbar scrolling - it may be possible, I will play with it later. Maybe a custom behavior will be solution. – Michał Z. Sep 17 '15 at 07:17
  • @MichałZ. At least I want to disable collapsing if the Recycler has just one item. It seems that **Whatsapp** has solved the issue someway. – Paolo Rotolo Sep 17 '15 at 19:22
  • 1
    Please write your solution in a proper answer, so that it's easier to find. – PicPuc Sep 20 '15 at 12:34
  • Moved my final solution in a proper answer. Thanks. – Paolo Rotolo Sep 21 '15 at 15:54

10 Answers10

31

Final Solution (thanks Michał Z.)

Methods to turn off/on Toolbar scrolling:

public void turnOffToolbarScrolling() {
    Toolbar mToolbar = (Toolbar) findViewById(R.id.toolbar);
    AppBarLayout appBarLayout = (AppBarLayout) findViewById(R.id.appbar_layout);

    //turn off scrolling
    AppBarLayout.LayoutParams toolbarLayoutParams = (AppBarLayout.LayoutParams) mToolbar.getLayoutParams();
    toolbarLayoutParams.setScrollFlags(0);
    mToolbar.setLayoutParams(toolbarLayoutParams);

    CoordinatorLayout.LayoutParams appBarLayoutParams = (CoordinatorLayout.LayoutParams) appBarLayout.getLayoutParams();
    appBarLayoutParams.setBehavior(null);
    appBarLayout.setLayoutParams(appBarLayoutParams);
}

public void turnOnToolbarScrolling() {
    Toolbar mToolbar = (Toolbar) findViewById(R.id.toolbar);
    AppBarLayout appBarLayout = (AppBarLayout) findViewById(R.id.appbar_layout);

    //turn on scrolling
    AppBarLayout.LayoutParams toolbarLayoutParams = (AppBarLayout.LayoutParams) mToolbar.getLayoutParams();
    toolbarLayoutParams.setScrollFlags(AppBarLayout.LayoutParams.SCROLL_FLAG_SCROLL | AppBarLayout.LayoutParams.SCROLL_FLAG_ENTER_ALWAYS);
    mToolbar.setLayoutParams(toolbarLayoutParams);

    CoordinatorLayout.LayoutParams appBarLayoutParams = (CoordinatorLayout.LayoutParams) appBarLayout.getLayoutParams();
    appBarLayoutParams.setBehavior(new AppBarLayout.Behavior());
    appBarLayout.setLayoutParams(appBarLayoutParams);
}


Find out if last item of RecyclerView is visible in my Fragment.
If yes, disable scrolling:

public void updateToolbarBehaviour(){
    if (mLayoutManager.findLastCompletelyVisibleItemPosition() == items.size()-1) {
        ((MainActivity) getActivity()).turnOffToolbarScrolling();
    } else {
        ((MainActivity)getActivity()).turnOnToolbarScrolling();
    }
}
Paolo Rotolo
  • 1,243
  • 1
  • 14
  • 22
  • 3
    You need to call `updateToolbarBehaviour()` with a delay (using Handler-Runnable) for the first time. Otherwise, the method `findLastCompletelyVisibleItemPosition()` returns "-1" initially. – Ugurcan Yildirim Jan 06 '17 at 12:11
  • Still this solution is working. And for collapsing tool bar also. – Pankaj Kumar Sep 04 '18 at 07:17
  • Hell lot of code for a simple logical stuff. It gets even more complicated while using fragment inside an activity with a toolbar. – Sai Sep 29 '18 at 08:23
14

RecyclerView now (since version 23.2) supports wrap_content. Just use wrap_content as the height.

Eric Cochran
  • 8,414
  • 5
  • 50
  • 91
  • 4
    Unfortunately it does not work for me when I wrap `RecyclerView` with `SwipeRefreshLayout` – Artem_Iens Apr 28 '17 at 16:16
  • This worked for me using a NestedScrollView. Thanks! – rjr-apps Sep 15 '17 at 19:46
  • This looks like the best solution. I'm not sure this is a completely assumed feature of the coordinator/recyclerView, but it definitely works with no dev. When in wrap_content, the recycler does not trigger scrolling events if its viewport is not filled with elements. – AlexG Jan 17 '20 at 09:59
  • Also working for me! It's important to note that the RecyclerView must be the first child of your CoordinatorLayout (mine was in a nested scroll view first, which was preventing android to understand that scrolling wasn't necessary). – A.Mamode Jan 25 '21 at 17:34
8

You can check if the last item in RecyclerView is visible. If it's not then turn off scrolling programmatically using this method:

            //turn off scrolling
            AppBarLayout.LayoutParams toolbarLayoutParams = (AppBarLayout.LayoutParams) mToolbar.getLayoutParams();
            toolbarLayoutParams.setScrollFlags(0);
            mToolbar.setLayoutParams(toolbarLayoutParams);

            CoordinatorLayout.LayoutParams appBarLayoutParams = (CoordinatorLayout.LayoutParams) appBarLayout.getLayoutParams();
            appBarLayoutParams.setBehavior(null);
            appBarLayout.setLayoutParams(appBarLayoutParams);
Michał Z.
  • 4,119
  • 1
  • 22
  • 32
  • Works well, thanks! I've updated my question with both off and on methods. – Paolo Rotolo Sep 19 '15 at 19:55
  • Works well, but if the content of the view that has app:layout_behavior="@string/appbar_scrolling_view_behavior", and also has layout_height="match_parent" the content won't center after turning off the scrolling – Earwin delos Santos Oct 01 '15 at 04:36
  • 1
    this works well just to disable the scroll, but if to re-enable the same for different fragment wont work, on setting the scroll Flags progrmatically. – pallavi Sep 20 '16 at 07:15
6

I took a slightly different approach to solve this.

I created a custom AppBarBehavior that disables its self based on cells.

public class CustomAppBarBehavior extends AppBarLayout.Behavior {

    private RecyclerView recyclerView;
    private boolean enabled;

    public CustomAppBarBehavior() {
    }

    public CustomAppBarBehavior(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    @Override
    public boolean onInterceptTouchEvent(CoordinatorLayout parent, AppBarLayout child, MotionEvent ev) {
        updatedEnabled();
        return enabled && super.onInterceptTouchEvent(parent, child, ev);
    }

    @Override
    public boolean onStartNestedScroll(CoordinatorLayout parent, AppBarLayout child, View directTargetChild, View target, int nestedScrollAxes) {
        return enabled && super.onStartNestedScroll(parent, child, directTargetChild, target, nestedScrollAxes);
    }

    @Override
    public boolean onNestedFling(CoordinatorLayout coordinatorLayout, AppBarLayout child, View target, float velocityX, float velocityY, boolean consumed) {
        return enabled && super.onNestedFling(coordinatorLayout, child, target, velocityX, velocityY, consumed);
    }

    private void updatedEnabled() {
        enabled = false;
        if(recyclerView != null) {
            RecyclerView.Adapter adapter = recyclerView.getAdapter();
            if (adapter != null) {
                int count = adapter.getItemCount();
                RecyclerView.LayoutManager layoutManager = recyclerView.getLayoutManager();
                if (layoutManager != null) {
                    int lastItem = 0;
                    if (layoutManager instanceof LinearLayoutManager) {
                        LinearLayoutManager linearLayoutManager = (LinearLayoutManager) layoutManager;
                        lastItem = Math.abs(linearLayoutManager.findLastCompletelyVisibleItemPosition());
                    } else if (layoutManager instanceof StaggeredGridLayoutManager) {
                        StaggeredGridLayoutManager staggeredGridLayoutManager = (StaggeredGridLayoutManager) layoutManager;
                        int[] lastItems = staggeredGridLayoutManager.findLastCompletelyVisibleItemPositions(new int[staggeredGridLayoutManager.getSpanCount()]);
                        lastItem = Math.abs(lastItems[lastItems.length - 1]);
                    }
                    enabled = lastItem < count - 1;
                }
            }
        }
    }

    public void setRecyclerView(RecyclerView recyclerView) {
        this.recyclerView = recyclerView;
    }
}

Then set the custom behavior on the app bar layout

appBarBehavior = new CustomAppBarBehavior();
CoordinatorLayout.LayoutParams appBarLayoutParams = (CoordinatorLayout.LayoutParams) appBarLayout.getLayoutParams();
appBarLayoutParams.setBehavior(appBarBehavior);
appBarLayout.setLayoutParams(appBarLayoutParams);

Last on page change of the view pager updated the RecyclerView on the behavior

private ViewPager.OnPageChangeListener pageChangeListener = new ViewPager.OnPageChangeListener() {
        @Override
        public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) { }

        @Override
        public void onPageSelected(final int position) {             
            appBarLayout.setExpanded(true, true);
            appBarLayout.post(new Runnable() {
                @Override
                public void run() {
                    appBarBehavior.setRecyclerView(childFragments.get(position).getRecyclerView());
                }
            });
        }

        @Override
        public void onPageScrollStateChanged(int state) { }
    };

This should work with changing datasets.

Stuart Campbell
  • 1,141
  • 11
  • 13
3

Add this code after you change data in your adapter:

recyclerView.afterMeasured {
    val isTurnedOff = recyclerView.turnOffNestedScrollingIfEnoughItems()
    if (isTurnedOff) appBarLayout.setExpanded(true)
}

And this is the functions:

inline fun <T: View> T.afterMeasured(crossinline action: T.() -> Unit) {
    viewTreeObserver.addOnGlobalLayoutListener(object : ViewTreeObserver.OnGlobalLayoutListener {
        override fun onGlobalLayout() {             
            viewTreeObserver.removeOnGlobalLayoutListener(this)
            action()
        }
    })
}


fun RecyclerView.turnOffNestedScrollingIfEnoughItems(): Boolean {
    val lm = (layoutManager as LinearLayoutManager)
    val count = if (lm.itemCount <= 0) 0 else lm.itemCount - 1
    val isFirstVisible = lm.findFirstCompletelyVisibleItemPosition() == 0
    val isLastItemVisible = lm.findLastCompletelyVisibleItemPosition() == count

    isNestedScrollingEnabled = !(isLastItemVisible && isFirstVisible)
    return isNestedScrollingEnabled.not()
}
Artur Dumchev
  • 1,252
  • 14
  • 17
1

I guess it's the best solution. You need to define your custom AppBarLayout behavior:

class CustomScrollingViewBehavior : AppBarLayout.Behavior {

    constructor() : super()
    constructor(context: Context, attrs: AttributeSet?) : super(context, attrs)

    var shouldScroll = true

    override fun onStartNestedScroll(coordinatorLayout: CoordinatorLayout, child: AppBarLayout, directTargetChild: View, target: View, axes: Int, type: Int): Boolean {
        return shouldScroll && when (target) {
            is NestedScrollView, is ScrollView, is RecyclerView -> {
                return target.canScrollVertically(1) || target.canScrollVertically(-1)
            }
            else -> super.onStartNestedScroll(coordinatorLayout, child, directTargetChild, target, axes, type)
        }
    }
}

And then you just need to use it in your layout as attribute of AppBarLayout:

...

 <com.google.android.material.appbar.AppBarLayout
            android:id="@+id/appbar"
            android:layout_width="match_parent"
            android:layout_height="56dp"
            app:layout_behavior=".CustomScrollingViewBehavior">

...

That's it.

Note: custom behavior also supports full disabling of the scrolling - you just need to set shouldScroll flag to false

customScrollingViewBehavior.shouldScroll = false
0

If helps anyome in Kotlin based on correct answer, I did this for Kotlin:

fun changeToolbarScroll(isToScrolling: Boolean){

    val params = toolbar.layoutParams as AppBarLayout.LayoutParams
    val appBarLayoutParams = appBarLayout.layoutParams as 
                             CoordinatorLayout.LayoutParams
    params.scrollFlags = 0
    toolbar.layoutParams = params
    appBarLayoutParams.behavior = null
    appBarLayout.layoutParams = appBarLayoutParams

    if(isToScrolling){
        params.scrollFlags =
            AppBarLayout.LayoutParams.SCROLL_FLAG_SCROLL or 
            AppBarLayout.LayoutParams.SCROLL_FLAG_ENTER_ALWAYS
        toolbar.layoutParams = params

        appBarLayoutParams.behavior = AppBarLayout.Behavior()
        appBarLayout.layoutParams = appBarLayoutParams
    }
}

In my case, I have a problem with a MainActivity that's manage navigation, toolbar and other shared things by 2 Fragments, the first Framgent use a RecyclerView and second is for show the deatil. The problems is when I set a Menu and change MenuItem from MainAcitivity

It can sound silly and totally logical but remember Always make chages to the Menu or MenuItems before call supportFragmentManager.beginTransaction() when change fragments otherwise don't work, or dont update properly, no matter changes in .add, .replace(), show()...

fun showDetailImageFragment(searchImage: SearchImage) {
    val searchFragment = 
    supportFragmentManager.findFragmentByTag(SEARCH_IMAGES)

    changeToolbarScroll(false)

    if (supportActionBar != null) {
        supportActionBar!!.collapseActionView()
        supportActionBar!!.setDisplayHomeAsUpEnabled(true)
        supportActionBar!!.title = getString(R.string.detail_image_title)
    }

    actionSearch.isVisible = false
    actionNighMOde.isVisible = false
    actionAppSetings.isVisible = false
    actionAbout.isVisible = false

    supportFragmentManager.beginTransaction()
        .setCustomAnimations(
            R.animator.fade_in,
            R.animator.fade_out,
            R.animator.fade_in,
            R.animator.fade_out
        )
        .hide(searchFragment!!)
        .add(
            R.id.frameLayout,
            DetailImageFragment().newInstance(searchImage)
        ).addToBackStack(null)
        .commit()
}
meleAstur
  • 11
  • 2
-2

Just remove scroll from

app:layout_scrollFlags="scroll|enterAlways"

So it should be

app:layout_scrollFlags="enterAlways"
Mikhail Valuyskiy
  • 1,238
  • 2
  • 16
  • 31
-2

You can add within your XML, the property layout_behaviour with value @string/appbar_scrolling_view_behavior this way:

app:layout_behavior="@string/appbar_scrolling_view_behavior"
Gratien Asimbahwe
  • 1,606
  • 4
  • 19
  • 30
-3
//turn off scrolling
AppBarLayout.LayoutParams toolbarLayoutParams = (AppBarLayout.LayoutParams) mToolbar.getLayoutParams();
toolbarLayoutParams.setScrollFlags(0);
mToolbar.setLayoutParams(toolbarLayoutParams);
hata
  • 11,633
  • 6
  • 46
  • 69