Prevent RecyclerView from scrolling under AppBarLayout before AppBarLayout is collapsed - android

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>

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.

RecyclerView inside CoordinatorLayout,AppBarLayout Scrolling issue

I have this xml code in fragment:
<CoordinatorLayout 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="match_parent" android:id="#+id/coordinatorLayout" android:fitsSystemWindows="true">
<android.support.design.widget.AppBarLayout
android:id="#+id/appBarLayout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:theme="#style/AppTheme"
app:elevation="0dp">
<android.support.design.widget.CollapsingToolbarLayout
android:layout_width="match_parent"
android:layout_height="300dp"
app:layout_scrollFlags="scroll"
android:id="#+id/collapsingToolbarLayout"
app:statusBarScrim="#color/bestColor">
<LinearLayout></LinearLayout> <!--this elements hide then appbar is collapsed-->
</android.support.design.widget.CollapsingToolbarLayout>
<LinearLayout>
<ImageButton>
android:id="#+id/profile_header_trophies"
</ImageButton><!-- this elements like a tab,visible if appbar collapsed-->
</LinearLayout>
</android.support.design.widget.AppBarLayout>
<android.support.v7.widget.RecyclerView
android:id="#+id/profile_recyclerView"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_behavior="#string/appbar_scrolling_view_behavior"/>
</android.support.design.widget.CoordinatorLayout>
in Java Class on Item set ClickListener:
#OnClick(R.id.profile_header_trophies)
public void profile_header_trophies_clicked() {
if (myProfile != null) {
appBarLayout.setExpanded(false, false);
if (myProfile.getBests().size() == 0) {
profile_recyclerView.smoothScrollToPosition(3);
} else {
profile_recyclerView.smoothScrollToPosition(2 + 20);
}
}
When I click to ImageButton, my RecyclerView scrolls to position, everything looks fine.
But if I put finger on AppBarLayout section (ImageButton) which visible(sticky) on top, and drag to bottom I have a bad scrolling.
My appbar start expanded, while my Recycler have some elements on top (they are hidden when scrolled).
I think this problem is setting behavoir. Because if I scrolling recycler first, AppBar doesnt start expanding, while Recycler not rich top of elements.
Thanks for your answers.
The bad scrolling once happened to me, it was happening because I was using RecyclerView inside another RecyclerView.
So when I try to scroll contents in the main RecyclerView it gave me this issue.
To resolve that issue I added this to RecyclerView:
recyclerView.setNestedScrollingEnabled(false);
For doing this in XML you can use:
android:nestedScrollingEnabled="false"
with this, you tell it to "merge" the scrolling of the RecyclerView with the scrolling of its parent
app:layout_behavior="#string/appbar_scrolling_view_behavior"/>
if I well understood, you want to have the following scrolling behaviour:
if you are scrolling by touching outside the RecyclerView, it collapses the AppBar
if you are scrolling by touching inside it, it ignores the RecyclerView's scroll and collapses the AppBar, and once the AppBar is collapsed, it scrolls inside the RecyclerView
Could you confirm this is the desired behaviour please?
In such case, you may look at this answer, maybe it will help
I think you need wrap content in NestedScrollView and set app:layout_behavior="#string/appbar_scrolling_view_behavior" in NestedScrollView
If you are using RecyclerView inside ViewPager then add this line to ViewPager: android:nestedScrollingEnabled="false"
It will solve your problem.
It can be tricky and there's a few things you need to have in order for this to work.
You should use app:layout_scrollFlags="scroll|enterAlwaysCollapsed" in your CollapsingToolbarLayout instead of just scroll.
It's not clear where the tabs or buttons are in your XML layout, but if they are supposed to stay on screen then you need to pin them, so you would use app:layout_collapseMode="pin" for that element. Perhaps this is in the LinearLayout or ImageView?
If the LinearLayout holds something else then you should add some scroll flags to that too, probably best would be app:layout_scrollFlags="scroll|enterAlwaysCollapsed" if it is supposed to scroll off screen.
Lastly, make sure you are not disabling nested scrolling in your RecyclerView.

Weird behavior with CoordinatorLayout+SwipeRefreshLayout+Endless RecyclerView

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);
}
}
});

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