Weird behavior with CoordinatorLayout+SwipeRefreshLayout+Endless RecyclerView - android

I'm trying to implement a CoordinatorLayout with a ToolBar that collapses. I already have a SwipeRefreshLayout with a RecyclerView inside. This recycler view also has an onScrollListener to load more content and a custom adapter to show a loading ViewHolder when it's loading more content.
Everything was working fine before I tried adding the CoordinatorLayout. Now I have two problems:
When loading items for the first time, the loading ViewHolder shows up and it's well placed (below the ToolBar). When it finishes, the loading is removed and the items are added. The problem is that the first item is hidden. It's like the second item is actually the first one. It's completely impossible to see the first item even when it is bigger than the ToolBar. But when I use swipe to refresh, the item gets placed properly. I have no idea why this happens.
When I'm using swipe to refresh, it loads items two times. The first time is the normal load and the second is a load for more items because of the onScrollListener. However the scroll is still on the top of the list. The recycler view items stay invisible until I scroll (I think this is because I only notify the adapter of the new items but we are still at the top of the list).
However, I don't know what to change in the listener to fix this.
Here's the listener:
mRecyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() {
#Override
public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
super.onScrolled(recyclerView, dx, dy);
if(!adapter.isLoading()) {
LinearLayoutManager layoutManager = (LinearLayoutManager) recyclerView.getLayoutManager();
//position starts at 0
if (layoutManager.findLastCompletelyVisibleItemPosition() >= layoutManager.getItemCount() - 2) {
loadSubmissions(false);
}
}
}
});
My activity 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/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">
<include
android:id="#+id/toolbar"
layout="#layout/toolbar" />
</android.support.design.widget.AppBarLayout>
<android.support.v4.widget.SwipeRefreshLayout
android:id="#+id/contentView"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="horizontal"
app:layout_behavior="#string/appbar_scrolling_view_behavior">
<android.support.v7.widget.RecyclerView
android:id="#+id/recyclerView"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:clipToPadding="false"
android:paddingBottom="6dp"
android:paddingTop="6dp"
android:scrollbars="vertical"
app:layout_behavior="#string/appbar_scrolling_view_behavior" />
</android.support.v4.widget.SwipeRefreshLayout>
</android.support.design.widget.CoordinatorLayout>
My toolbar:
<?xml version="1.0" encoding="utf-8"?>
<android.support.v7.widget.Toolbar
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="#color/colorPrimary"
android:elevation="4dp"
app:layout_scrollFlags="scroll|enterAlways">
</android.support.v7.widget.Toolbar>

Both problems are fixed. For the first one, when I was removing the loading ViewHolder I was actually doing notifyItemInserted instead of notifyItemRemoved.
For the second problem it's because I'm calling notifyDatasetChanged when I'm using swipe to refresh.
#Override public void onRefresh() {
adapter.clear();
adapter.notifyDataSetChanged();
loadSubmissions(true);
}
When I clear the adapter with the swipe to refresh, I'm swiping so it means that the onScrollListener will be called.
I changed the listener and added a verification to don't load more if there are no items.
mRecyclerView.addOnScrollListener(new EndlessRecyclerOnScrollListener(linearLayoutManager) {
#Override
public void onLoadMore(int current_page) {
// Doesnt load multiples times with the same scroll and doesnt load if there are no items
if(!adapter.isLoading() && adapter.getItemCount() > 0) {
loadSubmissions(false);
}
}
});

Related

How to allow scrolling the last item of recyclerview above of the FAB?

I have a RecyclerView with a FAB on top at the right bottom.
When scrolling I would like to allow the user to scroll further up such that the last item is fully visible and not covered by the FAB. It would be ok to allow an additional 48px at the bottom showing the background.
I tried adding a transparent footer item which does the trick. However, after adding sorting to the list, the transparent footer item creates some UI glitches during sorting (the divider is shown below the normal items).
I tried adding margin, but then the space is wasted all the time. It is ok that the FAB covers the last item when user views the first items at the top. Only when that last item is important than the item must not be covered.
This is the layout xml:
<android.support.design.widget.CoordinatorLayout
android:id="#+id/coordinator_layout"
android:layout_width="match_parent"
android:layout_height="match_parent">
<android.support.v7.widget.RecyclerView
android:id="#+id/recycler_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_gravity="center"
android:clipToPadding="false"
android:scrollbars="vertical"/>
<android.support.design.widget.FloatingActionButton
android:id="#+id/fab"
style="#style/floating_action_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginBottom="#dimen/fab_margin"
android:src="#drawable/ic_add_white_24dp"
app:layout_anchorGravity="bottom|center"/>
</android.support.design.widget.CoordinatorLayout>
Update
Using item decoration is the correct answer here.
I think better solution should calculate scroll range and hide the fab, if content was scroll down.
recyclerView.addOnScrollListener(new RecyclerView.OnScrollListener(){
#Override
public void onScrolled(RecyclerView recyclerView, int dx, int dy){
if (dy > 0) {
fab.hide();
return;
}
if (dy < 0) {
fab.show();
}
}
});
But if you really want to add margin at bottom you should use ItemDecoratorion.

Android - OnLoadMore in RecyclerView with wrap_content height

I have RecyclerView which its height is set to wrap_content. Now I need to implement OnLoadMore to it but there is a problem.
I used,
RecyclerView.OnScrollListener.onScrolled(RecyclerView recyclerView, int dx, int dy)
But it doesn't get invoked because my RecyclerView doesn't scroll. Its height is wrap_content.
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="#+id/root_rtl"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#color/white">
<include
android:id="#+id/tool_bar"
layout="#layout/toolbar" />
<ScrollView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_below="#id/tool_bar">
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
tools:context="com.yarima.msn.Activities.ProfileActivity">
<FrameLayout
android:id="#+id/FRAGMENT_PLACEHOLDER"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="#color/white">
<!-- Some Content That I want to scroll with recyclerview -->
</FrameLayout>
<android.support.v7.widget.RecyclerView
android:id="#+id/recyclerview_posts"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:scrollbars="vertical"
android:bellow="#id/FRAGMENT_PLACEHOLDER"/>
</RelativeLayout>
</ScrollView>
</RelativeLayout>
So I need to use another approach for loading more pages to RecyclerView.
I think the best way to do this, is calling onLoadMore event when the last item of RecyclerView become visible. I already tried to do this from onBindViewHolder method in adapter, but all pages loaded altogether.
if(getItemCount()-position == 1 && onLoadMoreListener != null){
if (recyclerView != null) {
visibleItemCount = recyclerView.getChildCount();
totalItemCount = recyclerView.getLayoutManager().getItemCount();
firstVisibleItem = ((LinearLayoutManager)recyclerView.getLayoutManager()).findFirstVisibleItemPosition();
if (loading) {
if (totalItemCount > previousTotal) {
loading = false;
previousTotal = totalItemCount;
}
}
if (!loading && (totalItemCount - visibleItemCount)
<= (firstVisibleItem + visibleThreshold)) {
loading = true;
onLoadMoreListener.onLoadMore();
}
}
}
What is the alternative way to implement onLoadMore without using scroll events?
Update:
The RecyclerView works perfectly with android:layout_height:"wrap_content" and my ScrollView scrolls smoothly.
Update 2:
My problem is when your RecyclerView height is wrap_content, scroll events of RecyclerView cannot be invoked. So I need an alternative way to find out when my RecyclerView reaches to end of its list and implement OnLoadMore event that way.
Update 3
I simplified xml before I wrote it in question... In real xml, there is ViewPager instead of the RecyclerView. And I have 4 tabs in that ViewPager that each tab contains a RecyclerView with different contents.
Above of this ViewPager I have some information about user and I want to scroll all of them together. So I put this header and ViewPager in a ScrollView and set the height of RecyclerView to wrap_content.
You can take a look at profile page of instagram. I want to this page works like that.
It's not possible to show this information in header of RecyclerView because in this way, I should add this information in each RecyclerView in every tabs.
Edited
Could look for scroll events in scrollview
https://developer.android.com/reference/android/view/View.html#onScrollChanged(int, int, int, int)
You need to keep your ViewPager and your RecyclerView inside a NestedScrollView. The final xml should look like this.
<android.support.v4.widget.NestedScrollView
android:layout_width="match_parent"
android:layout_height="wrap_content">
<ViewPager/>
<RecyclerView/>
</android.support.v4.widget.NestedScrollView>
And set the height of your RecyclerView to match_parent.
You'll face another problem here. The page will automatically scroll to bottom when the RecyclerView will be loaded. But there's a solution to it too.
Adding android:descendantFocusability="blocksDescendants" to the child layout in NestedScrollView which will prevent the automatic scrolling to the bottom.
If the above doesn't work, try setting nestedScrollView.scrollTo(0, 0); from your code after the RecyclerView is loaded with items.

Prevent RecyclerView from scrolling under AppBarLayout before AppBarLayout is collapsed

I'm creating a RecyclerView with header where the header collapses as you scroll up the RecyclerView. I can achieve this very closely with the layout below, with a transparent AppBarLayout, and MyCoolView which is the header. The parallax effect works great.
However, if the header is still visible and I fling the RecyclerView, the RV scrolls slowly to the top and some of the items are under the Toolbar until the RV reaches the top of the view. I've been playing around with the scrollFlags but haven't achieved a desirable result. Any suggestions on how to improve the fling experience so the items don't get clipped?
View the video and watch when its flinged --- https://www.dropbox.com/s/jppd6m7zo41k23z/20160609_151309.mp4?dl=0
<android.support.design.widget.CoordinatorLayout>
<android.support.design.widget.AppBarLayout
android:background="#00000000">
<android.support.design.widget.CollapsingToolbarLayout
app:layout_scrollFlags="scroll|exitUntilCollapsed">
<com.android.myapp.MyCoolView
app:layout_collapseMode="parallax"/>
</android.support.design.widget.CollapsingToolbarLayout>
</android.support.design.widget.AppBarLayout>
<android.support.v7.widget.RecyclerView/>
</android.support.design.widget.CoordinatorLayout>
Possible solution (untested). Add an OnOffsetChangedListener to your AppBarLayout, and keep note of the offset value. First, declare this field:
private boolean shouldScroll = false;
Then, onCreate:
AppBarLayout appbar = findViewById(...);
appbar.addOnOffsetChangedListener(new OnOffsetChangedListener() {
#Override
void onOffsetChanged(AppBarLayout appbar, int offset) {
// Allow recycler scrolling only if we started collapsing.
this.shouldScroll = offset != 0;
}
});
Now, add a scroll listener to your RecyclerView. Whenever it tries to scroll, revert the scroll if the AppBarLayout is still expanded:
RecyclerView recycler = findViewById(...);
recycler.addOnScrollListener(new OnScrollListener() {
#Override
void onScrolled(RecyclerView recycler, int dx, int dy) {
// If AppBar is fully expanded, revert the scroll.
if (!shouldScroll) {
recycler.scrollTo(0,0);
}
}
});
This might need some tweaking though. I see two issues:
Possible stack overflow if scrollTo() calls onScrolled() back. Can be solved with a boolean or by removing/adding the scroll listener
Possibly you want to prevent scrolling not only when AppBarLayout is fully expanded, but more generally when AppBarLayout is not collapsed. This means you don’t have to check for offset != 0, but rather for offset == appBarLayout.getTotalScrollRange(). I think.
Maybe you can add layout_behavior="#string/appbar_scrolling_view_behavior" to your RecylerView like this.
<android.support.v7.widget.RecyclerView
android:layout_width="match_parent"
android:layout_height="match_parent" app:layout_behavior="#string/appbar_scrolling_view_behavior" />
Wrapping the RecyclerView in a FrameLayout solves this problem.
You also need move the appbar_scrolling_view_behavior from the RecyclerView to the FrameLayout so it will be positioned below the AppBarLayout properly.
<android.support.design.widget.CoordinatorLayout>
<android.support.design.widget.AppBarLayout
android:background="#00000000">
<android.support.design.widget.CollapsingToolbarLayout
app:layout_scrollFlags="scroll|exitUntilCollapsed">
<com.android.myapp.MyCoolView
app:layout_collapseMode="parallax"/>
</android.support.design.widget.CollapsingToolbarLayout>
</android.support.design.widget.AppBarLayout>
<!-- BEGIN SOLUTION -->
<!-- the layout behavior needs to be set on the FrameLayout, not the RecyclerView -->
<FrameLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_behavior="#string/appbar_scrolling_view_behavior"
>
<!--This RecyclerView MUST be wrapped in a FrameLayout-->
<!--This prevents the RecyclerView from going behind the AppBarLayout-->
<android.support.v7.widget.RecyclerView
android:layout_width="match_parent"
android:layout_height="match_parent"
/>
</FrameLayout>
<!-- END SOLUTION -->
</android.support.design.widget.CoordinatorLayout>

AppBarLayout does not always re-enter on scroll down

I created a collapsing transparent search bar using AppBarLayout, CollapsingToolbarLayout inside a CoordinatorLayout and a RecyclerView. It was a bit (lot) tricky to have the recyclerView appear behind the appBarLayout instead of below it ; but is working. My problem is that sometimes, the app bar does not re-enter when I scroll down. I simply stays invisible outside of the screen. Here is my layout :
<android.support.design.widget.CoordinatorLayout
android:id="#+id/main_content"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fitsSystemWindows="true"
app:expandedTitleMarginBottom="88dp">
<android.support.v7.widget.RecyclerView
android:id="#+id/services_recycler_view"
android:orientation="vertical"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_behavior="#string/appbar_scrolling_view_behavior"
app:behavior_overlapTop="88dp">
</android.support.v7.widget.RecyclerView>
<android.support.design.widget.AppBarLayout
android:id="#+id/appbar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:fitsSystemWindows="true"
android:background="#color/color_transparent"
app:elevation="0dp">
<android.support.design.widget.CollapsingToolbarLayout
android:id="#+id/collapsing_toolbar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_scrollFlags="scroll|enterAlways"
android:fitsSystemWindows="false">
<Button
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Search Location or Service"
android:id="#+id/button_search_bar"/>
</android.support.design.widget.CollapsingToolbarLayout>
</android.support.design.widget.AppBarLayout>
</android.support.design.widget.CoordinatorLayout>
Any help on solving the not re-entering issue would be great.
A side problem, is that because I am using the app:behavior_overlapTop="88dp" to make recyclerView appear behind the app bar, the whole scrolling is a little odd : it starts by scrolling the appBar and then scrolls the recycler view. Any better solution is welcome.
EDIT :
I realized that the AppBar actually re-enter on scroll down but is invisible (I can click on it, I just can't see it). I figured I would share this new clue =)
It's not the answer i just want to confirmation that what you give, it will look like this?? on scroll up
means that Search button will come on top?
Edit:
<android.support.design.widget.CollapsingToolbarLayout
android:id="#+id/collapsing_toolbar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:fitsSystemWindows="false"
app:layout_scrollFlags="scroll|exitUntilCollapsed">
add exitUntilCollapsed flag.
After a few hours of trial and error I managed to find a solution to your problem. You can set an OnScrollListener to your RecyclerView. Inside the listener you check if in onScrolled the first item of the RecyclerView is on screen.
If the first item is visible, first you change the visibility of your AppBarLayout to View.INVISIBLE and secondly you change it directly back to View.VISIBLE.
Your code may look like this (Kotlin) :
override fun onCreate(savedInstanceState : Bundle?) {
super.onCreate(savedInstanceState)
contentView(R.layout.my_activity)
val mAppbar = appbar
val mServices_recycler_view = services_recycler_view
//...
mServices_recycler_view.addOnScrollListener(object : RecyclerView.OnScrollListener() {
override fun onScrollStateChanged(recyclerView : RecyclerView, newState : Int) {
super.onScrollStateChanged(recyclerView, newState)
}
override fun onScrolled(recyclerView : RecyclerView, dx : Int, dy : Int) {
val layoutManager = LinearLayoutManager::class.java.cast(recyclerView.layoutManager)
if(layoutManager.findFirstVisibleItemPosition() == 0) {
mAppbar.visibility = View.INVISIBLE
mAppbar.visibility = View.VISIBLE
}
}
}
I am aware of the fact, that this is not very beautiful, but as long as it solves the issue I don't mind.
Additionally you want to improve the check if the first item of the RecyclerView is shown, so that it won't trigger everytime you scroll, even when it is only a little bit.
The layout you described in your question does not have to be changed.

Android L: ActionBar setHideOnContentScrollEnabled

I'm trying to use the setHideOnContentScrollEnabled and setHideOffset in the new L API. However, none of the mentioned functions seem to have any effect. Anyone else encountered the same issue?
My Activity's layout is a ScrollView with a TextView displaying a large amount of text, so there are def scrolling. I have also, as required by the documentation, added FEATURE_ACTION_BAR_OVERLAY
getWindow().requestFeature(Window.FEATURE_ACTION_BAR_OVERLAY);
setContentView(R.layout.main_activity);
getActionBar().setHideOnContentScrollEnabled(true);
getActionBar().setHideOffset(40);
Notice that:
If enabled, the action bar will scroll out of sight along with a
nested scrolling child view's content.
View.setNestedScrollingEnabled(boolean)
I was facing the same problem, using a RecyclerView, a Toolbar and trying to support API10+. I just could not get setHideOffset() or setHideOnContentScrollEnabled() on my SupportActionBar to work.
After a lot of different manual approaches on scrolling the toolbar, this is my current workaround:
I use a ScrollView only for my Toolbar. My Recycler handles its own scrolling which is being listened to.
my_activity.xml
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<!--The Recycler is in a RefreshLayout. This is optional.-->
<android.support.v4.widget.SwipeRefreshLayout
android:id="#+id/swipe"
android:layout_width="match_parent"
android:layout_height="fill_parent"
android:layout_alignParentTop="true">
<android.support.v7.widget.RecyclerView
android:id="#+id/recycler"
android:layout_width="match_parent"
android:layout_height="fill_parent"
android:scrollbars="vertical" />
</android.support.v4.widget.SwipeRefreshLayout>
<!--Draw the Recycler _below_ the Toolbar-->
<!--by defining it _before_ everything else.-->
<ScrollView
android:id="#+id/scroll_toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:layout_alignParentTop="true"
android:scrollbars="none">
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<android.support.v7.widget.Toolbar xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="#+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize" />
<!--Add a transparent View below the Toolbar
to give the ScrollView "something to scroll".
Make sure it is _not_ clickable.-->
<View
android:layout_width="match_parent"
android:layout_height="128dp"
android:clickable="false" />
</RelativeLayout>
</ScrollView>
In myActivity.class
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_room_list);
mToolbarScroller = (ScrollView) findViewById(R.id.scroll_toolbar);
mRecycler = (RecyclerView) findViewById(R.id.recycler_rooms);
// [...]
// Do not forget to give your Recycler a Layout before listening to scroll events.
mRecycler.setOnScrollListener(new RecyclerView.OnScrollListener() {
#Override
public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
super.onScrolled(recyclerView, dx, dy);
// Only handle scrolling further if there is at least one child element in the list.
if (recyclerView.getChildCount() == 0) {
mSwipeLayout.setEnabled(true);
return;
}
final boolean didReachTop = recyclerView.getChildAt(0).getTop() >= 0;
if (mToolbarScroller == null) return;
// Simply let the Toolbar follow the scrolling Recycler
// by passing on the scroll-down (positive) values of dy.
if (dy > 2) mToolbarScroller.scrollBy(0, dy);
// Let the Toolbar reappear immediately
// when scrolling up a bit or if the top has been reached.
else if (dy < -4 || didReachTop) mToolbarScroller.scrollTo(0, 0);
}
});
This leads to your Toolbar always overlapping the first element in your Recycler. If you want to avoid this, add an invisible View to your item layouts that has the size of the Toolbar. In your Adapter you simply set it to VISIBLE, if it is the first element in the list, or to GONE if it is any other element:
In myRecyclerItemAdapter.java (optional):
#Override
public void onBindViewHolder(RoomViewHolder viewHolder, Cursor cursor) {
// To compensate for the overlaying toolbar,
// offset the first element by making its spacer visible.
if (cursor.isFirst()) viewHolder.mSpacer.setVisibility(View.VISIBLE);
else viewHolder.mSpacer.setVisibility(View.GONE);
I am probably going to tweak the threshold dy values in the OnScrollListener. They are supposed to filter jittery scroll values, such as a rapid succession of -1, +1, -1, +1 that sometimes happen.
If anyone has a better way or thinks I am making huge mistakes, please let me know! I am always looking for better solutions.

Categories

Resources