Android: CollapsingToolbarLayout and SwipeRefreshLayout get stuck

AndroidSwiperefreshlayoutAndroid Collapsingtoolbarlayout

Android Problem Overview


I use CollapsingToolbarLayout, RecyclerView and SwipeRefreshLayout together:

Xml:

<android.support.v4.widget.DrawerLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:id="@+id/drawer_layout"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:fitsSystemWindows="true">

<android.support.design.widget.CoordinatorLayout
    android:id="@+id/coordinator_layout"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:fitsSystemWindows="true">

<android.support.design.widget.AppBarLayout
    android:id="@+id/appbar"
    android:layout_width="match_parent"
    android:layout_height="@dimen/collapse_toolbar_height"
    android:fitsSystemWindows="true"
    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"
        app:contentScrim="?attr/colorPrimary"
        android:fitsSystemWindows="true"
        app:expandedTitleMarginStart="48dp"
        app:expandedTitleMarginEnd="64dp"
        app:layout_scrollFlags="scroll|exitUntilCollapsed">

        <ImageView
            android:id="@+id/toolbar_image"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:scaleType="centerCrop"
            android:fitsSystemWindows="true"
            app:layout_collapseMode="parallax" />

        <include
            layout="@layout/activity_main_toolbar"/>

    </android.support.design.widget.CollapsingToolbarLayout>
</android.support.design.widget.AppBarLayout>

	<android.support.v4.widget.SwipeRefreshLayout
		android:id="@+id/swipe_container"
		android:layout_width="match_parent"
		android:layout_height="match_parent"
		app:layout_behavior="@string/appbar_scrolling_view_behavior">

		<cz.yetanotherview.webcamviewer.app.helper.EmptyRecyclerView
			android:id="@+id/mainList"
			android:layout_width="match_parent"
			android:layout_height="match_parent"
			android:scrollbars="vertical" />
	</android.support.v4.widget.SwipeRefreshLayout>

	<android.support.design.widget.FloatingActionButton
		android:id="@+id/floating_action_button"
		android:layout_height="wrap_content"
		android:layout_width="wrap_content"
		app:layout_anchor="@id/appbar"
		app:layout_anchorGravity="bottom|right|end"
		android:layout_margin="16dp"
		app:fabSize="mini"
		android:src="@drawable/ic_action_edit"
		android:onClick="assignSelectedWebCamsToCategory"/>

	<com.github.clans.fab.FloatingActionMenu
		android:id="@+id/floating_action_menu"
		android:layout_width="match_parent"
		android:layout_height="match_parent"
		android:layout_gravity="bottom|end"
		android:paddingRight="10dp"
		android:paddingBottom="8dp"
		android:paddingLeft="10dp"
		fab:menu_shadowColor="#37000000"
		fab:menu_colorNormal="#DA4336"
		fab:menu_colorPressed="#E75043"
		fab:menu_colorRipple="#99FFFFFF"
		fab:menu_icon="@drawable/fab_add"
		fab:menu_buttonSpacing="10dp"
		fab:menu_labels_textColor="@color/very_dark_grey"
		fab:menu_labels_textSize="14sp"
		fab:menu_labels_colorNormal="@color/white"
		fab:menu_labels_colorPressed="@color/next_grey"
		fab:menu_labels_colorRipple="#99FFFFFF"
		fab:menu_labels_margin="8dp"
		fab:menu_backgroundColor="@color/black_transparent">

		<com.github.clans.fab.FloatingActionButton
			android:layout_width="wrap_content"
			android:layout_height="wrap_content"
			android:src="@drawable/ic_action_content_import"
			fab:fab_size="mini"
			fab:fab_label="@string/pref_import_from_server"
			fab:fab_colorNormal="@color/white"
			app:fab_colorPressed="@color/next_grey"
			app:fab_colorRipple="#99FFFFFF"
			android:onClick="showSelectionDialog"/>

		<com.github.clans.fab.FloatingActionButton
			android:layout_width="wrap_content"
			android:layout_height="wrap_content"
			android:src="@drawable/ic_action_content_manually"
			fab:fab_size="mini"
			fab:fab_label="@string/create_manually"
			fab:fab_colorNormal="@color/white"
			app:fab_colorPressed="@color/next_grey"
			app:fab_colorRipple="#99FFFFFF"
			android:onClick="showAddDialog"/>

		<com.github.clans.fab.FloatingActionButton
			android:layout_width="wrap_content"
			android:layout_height="wrap_content"
			android:src="@drawable/ic_action_content_suggestion"
			fab:fab_size="mini"
			fab:fab_label="@string/submit_suggestion"
			fab:fab_colorNormal="@color/white"
			app:fab_colorPressed="@color/next_grey"
			app:fab_colorRipple="#99FFFFFF"
			android:onClick="showSuggestionDialog"/>

	</com.github.clans.fab.FloatingActionMenu>
</android.support.design.widget.CoordinatorLayout>

<include
    layout="@layout/activity_main_drawer"/>
</android.support.v4.widget.DrawerLayout>

Code:

    swipeRefreshLayout = (SwipeRefreshLayout) findViewById(R.id.swipe_container);
    swipeRefreshLayout.setOnRefreshListener(this);

How to allow swipe refresh action only when collapsing toolbar layout is fully expanded and scrollview (recyclerview) on top? The similar behavior like in Google+ or Inbox application.

Wrong:

enter image description here

Good:

enter image description here

Android Solutions


Solution 1 - Android

Update: This issue has been resolved in the latest version of the support library (23.1.1+). If you are using an older version of the support library either upgrade or continue reading.

If you're using an older version of the support library, add an offset change listener to your AppBarLayout to enable or disable your swipe to refresh layout accordingly. Additional code available here:

https://gist.github.com/blackcj/001a90c7775765ad5212

Relevant changes:

public class MainActivity extends AppCompatActivity implements AppBarLayout.OnOffsetChangedListener {
    ...

    private AppBarLayout appBarLayout;
    private SwipeRefreshLayout mSwipeRefreshLayout;

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

        ...
        mSwipeRefreshLayout = (SwipeRefreshLayout) findViewById(R.id.contentView);
        appBarLayout = (AppBarLayout) findViewById(R.id.appBarLayout);

    }

    @Override
    public void onOffsetChanged(AppBarLayout appBarLayout, int i) {
        //The Refresh must be only active when the offset is zero :
        mSwipeRefreshLayout.setEnabled(i == 0);
    }

    @Override
    protected void onResume() {
        super.onResume();
        appBarLayout.addOnOffsetChangedListener(this);
    }

    @Override
    protected void onPause() {
        super.onPause();
        appBarLayout.removeOnOffsetChangedListener(this);
    }
}

Solution 2 - Android

Finally,

I found that SwipeRefreshLayout works without any "hacks" from Support Library version 23.1.1.

Simply use in your layout:

 <android.support.v4.widget.SwipeRefreshLayout
    android:id="@+id/swipe_refresh_layout"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    app:layout_behavior="@string/appbar_scrolling_view_behavior">

        <android.support.v7.widget.RecyclerView
            android:id="@+id/recycler_view"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:scrollbars="vertical" />
</android.support.v4.widget.SwipeRefreshLayout>

and in code:

SwipeRefreshLayout swipeRefreshLayout = (SwipeRefreshLayout) findViewById(R.id.swipe_refresh_layout);
swipeRefreshLayout.setColorSchemeResources(R.color.green, R.color.red, R.color.yellow);
swipeRefreshLayout.setOnRefreshListener(new SwipeRefreshLayout.OnRefreshListener() {
    @Override
    public void onRefresh() {
       //Your refresh code here
    }
});

And don't forget to use:

swipeRefreshLayout.setRefreshing(false);

after using your code logic ;)

Solution 3 - Android

If I understand you correctly, you want to start refreshing only after toolbar is expanded, right? So first CollapsingToolbarLayout needs to be opened and then start refreshing. I managed it by the following 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/coordinator_layout"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:fitsSystemWindows="true">

        <android.support.design.widget.AppBarLayout
            android:id="@+id/app_bar_layout"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:fitsSystemWindows="true"
            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="wrap_content"
                android:fitsSystemWindows="true"
                app:layout_scrollFlags="scroll|enterAlways|enterAlwaysCollapsed">

                <LinearLayout
                    android:layout_width="match_parent"
                    android:layout_height="wrap_content"
                    android:orientation="vertical">
                      <!--PUT HERE WHAT EVER YOU WANT TO COLLAPSE, A TOOLBAR, ETC...-->
                </LinearLayout>
            </android.support.design.widget.CollapsingToolbarLayout>
        </android.support.design.widget.AppBarLayout>
     <android.support.v4.widget.SwipeRefreshLayout
        android:id="@+id/swipe_refresh_layout"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        app:layout_behavior="@string/appbar_scrolling_view_behavior">

            <android.support.v7.widget.RecyclerView
                android:id="@+id/recycler_view"
                android:layout_width="match_parent"
                android:layout_height="match_parent"
                android:clipToPadding="false"
                android:fadeScrollbars="false"
                android:scrollbars="vertical"
                app:layout_behavior="@string/appbar_scrolling_view_behavior" />
    </android.support.v4.widget.SwipeRefreshLayout>
</android.support.design.widget.CoordinatorLayout>   

And then, in your fragment/activity make it implement AppBarLayout.OnOffsetChangedListener (Now the refreshing is enabled when toolbar is fully expanded):

    @Override
    public void onOffsetChanged(AppBarLayout appBarLayout, int verticalOffset) {
        if (collapsingToolbarLayout.getHeight() + verticalOffset < 2 * ViewCompat.getMinimumHeight(collapsingToolbarLayout)) {
            swipeRefreshLayout.setEnabled(false);
        } else {
            swipeRefreshLayout.setEnabled(true);
        }
    }

Override onPause() & onResume() as in @blackcj answer:

  @Override
    public void onResume() {
        super.onResume();
        appBarLayout.addOnOffsetChangedListener(this);
    }

    @Override
    public void onPause() {
        super.onPause();
        appBarLayout.removeOnOffsetChangedListener(this);
    }

Then set LinearLayoutManager to your recyclerView:

    LinearLayoutManager layoutManager = new LinearLayoutManager(getActivity());
    layoutManager.setOrientation(LinearLayoutManager.VERTICAL);
    recyclerView.setLayoutManager(layoutManager);

For me this worked as a charm, first appBarlayout gets expanded and only then swipeRefreshLayout triggers refreshing.

Solution 4 - Android

I had to make the RecyclerView the main child of the SwipeRefreshLayout in order to remove the issue using Support Library 23.2.0. Could not fixed it having an include layout inside of the SwipeRefreshLayout

<android.support.v4.widget.SwipeRefreshLayout
        android:id="@+id/my_swipeRefreshLayout"
        android:layout_width="match_parent"
        android:layout_height="match_parent">

        <!--<include layout="@layout/my_RecyclerView_layout"/> issue for me here -->

        <android.support.v7.widget.RecyclerView
            android:id="@+id/my_recyclerView"
            android:layout_width="match_parent"
            android:layout_height="match_parent">
        </android.support.v7.widget.RecyclerView>

    </android.support.v4.widget.SwipeRefreshLayout>

Solution 5 - Android

It worked with offsetChangedListener for me, just a little better-performing kotlin onOffsetChanged() implementation

val toEnable = verticalOffset == 0
if (!swipeRefresh.isEnabled.xor(toEnable)) return
swipeRefresh.isEnabled = toEnable

Solution 6 - Android

The Above Answer is Perfect for AppCompatActivity but If you are used Fragment then following snippet will help you.

Just put NestedScrollView in xml of fragment

<android.support.v4.widget.NestedScrollView 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:layout_gravity="fill_vertical"
    android:clipToPadding="false"
    android:isScrollContainer="false"
    app:layout_behavior="@string/appbar_scrolling_view_behavior">

    <!-- A RecyclerView with some commonly used attributes -->
        <android.support.v4.widget.SwipeRefreshLayout xmlns:android="http://schemas.android.com/apk/res/android"
            android:id="@+id/album_timeline_swipe_refresh_layout"
            android:layout_width="match_parent"
            android:layout_height="match_parent">

            <android.support.v7.widget.RecyclerView
                android:id="@+id/album_timeline_recyclerview"
                android:layout_width="match_parent"
                android:layout_height="match_parent"
                android:clickable="true" />
        </android.support.v4.widget.SwipeRefreshLayout>

</android.support.v4.widget.NestedScrollView>

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
QuestionTomasView Question on Stackoverflow
Solution 1 - AndroidblackcjView Answer on Stackoverflow
Solution 2 - AndroidTomasView Answer on Stackoverflow
Solution 3 - AndroidhkopView Answer on Stackoverflow
Solution 4 - AndroidRensodarwinView Answer on Stackoverflow
Solution 5 - AndroidSidakpreet NView Answer on Stackoverflow
Solution 6 - AndroidPratik ButaniView Answer on Stackoverflow