Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Toolbar in AppBarLayout is scrollable although RecyclerView has not enough content to scroll

Is it really intended that the Toolbar in a AppBarLayout is scrollable although the main container with the "appbar_scrolling_view_behavior" has not enough content to really scroll?

What I have tested so far:
When I use a NestedScrollView (with "wrap_content" attribute) as main container and a TextView as child, the AppBarLayout works properly and does not scroll.

However, when I use a RecyclerView with only a few entries and the "wrap_content" attribute (so that there is no need to scroll), the Toolbar in the AppBarLayout is scrollable even though the RecyclerView never receives a scroll event (tested with a OnScrollChangeListener).

Here's my layout code:

<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/coordinatorLayout"     android:layout_width="match_parent"     android:layout_height="match_parent">      <android.support.design.widget.AppBarLayout         android:id="@+id/appBarLayout"         android:layout_width="match_parent"         android:layout_height="wrap_content">          <android.support.v7.widget.Toolbar             android:id="@+id/toolbar"             android:layout_width="match_parent"             android:layout_height="?attr/actionBarSize"             android:background="?attr/colorPrimary"             app:layout_scrollFlags="scroll|enterAlways"             app:theme="@style/ToolbarStyle" />     </android.support.design.widget.AppBarLayout>      <android.support.v7.widget.RecyclerView         android:id="@+id/recycler"         android:layout_width="wrap_content"         android:layout_height="wrap_content"         app:layout_behavior="@string/appbar_scrolling_view_behavior" /> </android.support.design.widget.CoordinatorLayout> 

With the following effect that the toolbar is scrollable although it's not necessary:

I've also found a way to deal with this by checking if all RecyclerView items are visible and using the setNestedScrollingEnabled() method of the RecyclerView.
Nevertheless, it does seem more like a bug as intended to me. Any opinions? :D

EDIT #1:

For people who are might be interested in my current solution, I had to put the setNestedScrollingEnabled() logic in the postDelayed() method of a Handler with 5 ms delay due to the LayoutManager which always returned -1 when calling the methods to find out whether the first and the last item is visible.
I use this code in the onStart() method (after my RecyclerView has been initialized) and every time after a content change of the RecyclerView occurs.

final LinearLayoutManager layoutManager = (LinearLayoutManager) mRecyclerView.getLayoutManager(); new Handler().postDelayed(new Runnable() {     @Override     public void run() {         //no items in the RecyclerView         if (mRecyclerView.getAdapter().getItemCount() == 0)             mRecyclerView.setNestedScrollingEnabled(false);         //if the first and the last item is visible         else if (layoutManager.findFirstCompletelyVisibleItemPosition() == 0                 && layoutManager.findLastCompletelyVisibleItemPosition() == mRecyclerView.getAdapter().getItemCount() - 1)             mRecyclerView.setNestedScrollingEnabled(false);         else             mRecyclerView.setNestedScrollingEnabled(true);     } }, 5); 

EDIT #2:

I just played around with a new app and it seems that this (unintended) behavior has been fixed in support library version 23.3.0 (or even earlier). Thus, there is no need for workarounds anymore!

like image 678
eickeee Avatar asked Sep 04 '15 12:09

eickeee


People also ask

What are the different types of scrolling behavior in appbars?

App bars contains four main aspects, that plays huge role in scrolling behavior. They are, Example of a status bar, navigation bar, tab/search bar, and flexible space. Image from material.io AppBar scrolling behavior enriches the way contents in a page presented.

What happens when the toolbar scrolls all the way back?

The whole app bar scrolls off. When the user reverse scrolls, the toolbar returns anchored to the top. When scrolling all the way back, the flexible space and the title grow into place again.

How to change the scrolling behavior of a tablayout in Android?

To achieve this, we need to add TabLayout inside the AppBarLayout and provide the layout_scrollFlags inside TabLayout. That will be enough to achieve this and we can play around with the scrolling behavior like above examples by just altering the layout_scrollFlags.

How to scroll off-screen with appbar?

Once content starts scrolling, the app bar will scroll faster than the content until it gets out of the overlapping content view. Once the content reaches top, app bar comes upside of the content and content goes underneath and scrolls smoothly. The whole AppBar can scroll off-screen along with content and can be returned while reverse scrolling.


2 Answers

Edit 2:

Turns out the only way to ensure Toolbar is not scrollable when RecyclerView is not scrollable is to set setScrollFlags programmatically which requires to check if RecyclerView's is scrollable. This check has to be done every time adapter is modified.

Interface to communicate with the Activity:

public interface LayoutController {     void enableScroll();     void disableScroll(); } 

MainActivity:

public class MainActivity extends AppCompatActivity implements      LayoutController {      private CollapsingToolbarLayout collapsingToolbarLayout;      @Override     protected void onCreate(Bundle savedInstanceState) {         super.onCreate(savedInstanceState);         setContentView(R.layout.activity_main);          Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);         setSupportActionBar(toolbar);          collapsingToolbarLayout =                (CollapsingToolbarLayout) findViewById(R.id.collapsing_toolbar);          final FragmentManager manager = getSupportFragmentManager();         final Fragment fragment = new CheeseListFragment();         manager.beginTransaction()                 .replace(R.id.root_content, fragment)                 .commit();     }      @Override     public void enableScroll() {         final AppBarLayout.LayoutParams params = (AppBarLayout.LayoutParams)                                   collapsingToolbarLayout.getLayoutParams();         params.setScrollFlags(                 AppBarLayout.LayoutParams.SCROLL_FLAG_SCROLL                  | AppBarLayout.LayoutParams.SCROLL_FLAG_ENTER_ALWAYS         );         collapsingToolbarLayout.setLayoutParams(params);     }      @Override     public void disableScroll() {         final AppBarLayout.LayoutParams params = (AppBarLayout.LayoutParams)                                   collapsingToolbarLayout.getLayoutParams();         params.setScrollFlags(0);         collapsingToolbarLayout.setLayoutParams(params);     } } 

activity_main.xml:

<android.support.v4.widget.DrawerLayout     xmlns:android="http://schemas.android.com/apk/res/android"     android:id="@+id/drawer_layout"     android:layout_height="match_parent"     android:layout_width="match_parent"     android:fitsSystemWindows="true">      <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/main_content"         android:layout_width="match_parent"         android:layout_height="match_parent">          <android.support.design.widget.AppBarLayout             android:id="@+id/appbar"             android:layout_width="match_parent"             android:layout_height="wrap_content"             android:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar">              <android.support.design.widget.CollapsingToolbarLayout                 android:id="@+id/collapsing_toolbar"                 android:layout_width="match_parent"                 android:layout_height="match_parent"                 android:fitsSystemWindows="true"                 app:contentScrim="?attr/colorPrimary">                  <android.support.v7.widget.Toolbar                     android:id="@+id/toolbar"                     android:layout_width="match_parent"                     android:layout_height="?attr/actionBarSize"                     android:background="?attr/colorPrimary"                     app:popupTheme="@style/ThemeOverlay.AppCompat.Light"/>              </android.support.design.widget.CollapsingToolbarLayout>          </android.support.design.widget.AppBarLayout>          <FrameLayout             android:id="@+id/root_content"             android:layout_width="match_parent"             android:layout_height="match_parent"             android:layout_gravity="fill_vertical"             app:layout_behavior="@string/appbar_scrolling_view_behavior"/>      </android.support.design.widget.CoordinatorLayout>  </android.support.v4.widget.DrawerLayout> 

Test Fragment:

public class CheeseListFragment extends Fragment {      private static final int DOWN = 1;     private static final int UP = 0;      private LayoutController controller;     private RecyclerView rv;      @Override     public void onAttach(Context context) {         super.onAttach(context);          try {             controller = (MainActivity) getActivity();         } catch (ClassCastException e) {             throw new RuntimeException(getActivity().getLocalClassName()                     + "must implement controller.", e);         }     }      @Nullable     @Override     public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {         rv = (RecyclerView) inflater.inflate(                 R.layout.fragment_cheese_list, container, false);         setupRecyclerView(rv);          // Find out if RecyclerView are scrollable, delay required         final Handler handler = new Handler();         handler.postDelayed(new Runnable() {             @Override             public void run() {                 if (rv.canScrollVertically(DOWN) || rv.canScrollVertically(UP)) {                     controller.enableScroll();                 } else {                     controller.disableScroll();                 }             }         }, 100);          return rv;     }      private void setupRecyclerView(RecyclerView recyclerView) {         final LinearLayoutManager layoutManager = new LinearLayoutManager(recyclerView.getContext());          recyclerView.setLayoutManager(layoutManager);          final SimpleStringRecyclerViewAdapter adapter =                 new SimpleStringRecyclerViewAdapter(                         getActivity(),                         // Test ToolBar scroll                         getRandomList(/* with enough items to scroll */)                         // Test ToolBar pin                         getRandomList(/* with only 3 items*/)                 );          recyclerView.setAdapter(adapter);     } } 

Sources:

  • Change scroll flags programmatically
  • Original code by Chris Banes
  • Need a postDelayed to ensure RecyclerView children are ready for calculations

Edit:

You should CollapsingToolbarLayout to control the behaviour.

Adding a Toolbar directly to an AppBarLayout gives you access to the enterAlwaysCollapsed and exitUntilCollapsed scroll flags, but not the detailed control on how different elements react to collapsing. [...] setup uses CollapsingToolbarLayout’s app:layout_collapseMode="pin" to ensure that the Toolbar itself remains pinned to the top of the screen while the view collapses.http://android-developers.blogspot.com.tr/2015/05/android-design-support-library.html

<android.support.design.widget.CollapsingToolbarLayout         android:layout_width="match_parent"         android:layout_height="match_parent"         app:layout_scrollFlags="scroll|exitUntilCollapsed">      <android.support.v7.widget.Toolbar         android:id="@+id/drawer_toolbar"         android:layout_width="match_parent"         android:layout_height="?attr/actionBarSize"         app:layout_collapseMode="pin"/>  </android.support.design.widget.CollapsingToolbarLayout> 

Add

app:layout_collapseMode="pin" 

to your Toolbar in xml.

    <android.support.v7.widget.Toolbar         android:id="@+id/toolbar"         android:layout_width="match_parent"         android:layout_height="?attr/actionBarSize"         android:background="?attr/colorPrimary"         app:layout_scrollFlags="scroll|enterAlways"         app:layout_collapseMode="pin"         app:theme="@style/ToolbarStyle" /> 
like image 129
user3623735 Avatar answered Oct 14 '22 03:10

user3623735


So, proper credit, this answer almost solved it for me https://stackoverflow.com/a/32923226/5050087. But since it was not showing the toolbar when you actually had an scrollable recyclerview and its last item was visible (it would not show the toolbar on the first scroll up), I decided to modify it and adapt it for an easier implementation and for dynamic adapters.

First, you must create a custom layout behavior for you appbar:

public class ToolbarBehavior extends AppBarLayout.Behavior{  private boolean scrollableRecyclerView = false; private int count;  public ToolbarBehavior() { }  public ToolbarBehavior(Context context, AttributeSet attrs) {     super(context, attrs); }  @Override public boolean onInterceptTouchEvent(CoordinatorLayout parent, AppBarLayout child, MotionEvent ev) {     return scrollableRecyclerView && super.onInterceptTouchEvent(parent, child, ev); }  @Override public boolean onStartNestedScroll(CoordinatorLayout parent, AppBarLayout child, View directTargetChild, View target, int nestedScrollAxes, int type) {     updatedScrollable(directTargetChild);     return scrollableRecyclerView && super.onStartNestedScroll(parent, child, directTargetChild, target, nestedScrollAxes, type); }  @Override public boolean onNestedFling(CoordinatorLayout coordinatorLayout, AppBarLayout child, View target, float velocityX, float velocityY, boolean consumed) {     return scrollableRecyclerView && super.onNestedFling(coordinatorLayout, child, target, velocityX, velocityY, consumed); }  private void updatedScrollable(View directTargetChild) {     if (directTargetChild instanceof RecyclerView) {         RecyclerView recyclerView = (RecyclerView) directTargetChild;         RecyclerView.Adapter adapter = recyclerView.getAdapter();         if (adapter != null) {             if (adapter.getItemCount()!= count) {                 scrollableRecyclerView = false;                 count = adapter.getItemCount();                 RecyclerView.LayoutManager layoutManager = recyclerView.getLayoutManager();                 if (layoutManager != null) {                     int lastVisibleItem = 0;                     if (layoutManager instanceof LinearLayoutManager) {                         LinearLayoutManager linearLayoutManager = (LinearLayoutManager) layoutManager;                         lastVisibleItem = Math.abs(linearLayoutManager.findLastCompletelyVisibleItemPosition());                     } else if (layoutManager instanceof StaggeredGridLayoutManager) {                         StaggeredGridLayoutManager staggeredGridLayoutManager = (StaggeredGridLayoutManager) layoutManager;                         int[] lastItems = staggeredGridLayoutManager.findLastCompletelyVisibleItemPositions(new int[staggeredGridLayoutManager.getSpanCount()]);                         lastVisibleItem = Math.abs(lastItems[lastItems.length - 1]);                     }                     scrollableRecyclerView = lastVisibleItem < count - 1;                 }             }         }     } else scrollableRecyclerView = true;   } } 

Then, you only need to define this behavior for you appbar in your layout file:

<android.support.design.widget.AppBarLayout     android:layout_width="match_parent"     android:layout_height="wrap_content"     android:fitsSystemWindows="true"     app:layout_behavior="com.yourappname.whateverdir.ToolbarBehavior"     > 

I haven't tested it for screen rotation so let me know if it works like this. I guess it should work as I don't think the count variable is saved when the rotation happens, but let me know if it doesn't.

This was the easiest and cleanest implementation for me, enjoy it.

like image 25
emirua Avatar answered Oct 14 '22 01:10

emirua