Inserting RecyclerView items at zero position - always stay scrolled to top - android

I have a pretty standard RecyclerView with a vertical LinearLayoutManager. I keep inserting new items at the top and I'm calling notifyItemInserted(0).
I want the list to stay scrolled to the top; to always display the 0th position.
From my requirement's point of view, the LayoutManager behaves differently based on the number of items.
While all items fit on the screen, it looks and behaves as I expect: The new item always appears on top and shifts everything below it.
However, as soon as the no. of items exceeds the RecyclerView's bounds, new items are added above the currently visible one, but the visible items stay in view. The user has to scroll to see the newest item.
This behavior is totally understandable and fine for many applications, but not for a "live feed", where seeing the most recent thing is more important than "not distracting" the user with auto-scrolls.
I know this question is almost a duplicate of Adding new item to the top of the RecyclerView... but all of the proposed answers are mere workarounds (most of them quite good, admittedly).
I'm looking for a way to actually change this behavior. I want the LayoutManager to act exactly the same, no matter the number of items. I want it to always shift all of the items (just like it does for the first few additions), not to stop shifting items at some point, and compensate by smooth-scrolling the list to the top.
Basically, no smoothScrollToPosition, no RecyclerView.SmoothScroller. Subclassing LinearLayoutManager is fine. I'm already digging through its code, but without any luck so far, so I decided to ask in case someone already dealt with this. Thanks for any ideas!
EDIT: To clarify why I'm dismissing answers from the linked question: Mostly I'm concerned about animation smoothness.
Notice in the first GIF where ItemAnimator is moving other items while adding the new one, both fade-in and move animations have the same duration. But when I'm "moving" the items by smooth scrolling, I cannot easily control the speed of the scroll. Even with default ItemAnimator durations, this doesn't look as good, but in my particular case, I even needed to slow down the ItemAnimator durations, which makes it even worse:

Although I wrote this answer and this is the accepted solution, I suggest a look at the other later answers to see if they work for you before attempting this.
When an item is added to the top of the RecyclerView and the item can fit onto the screen, the item is attached to a view holder and RecyclerView undergoes an animation phase to move items down to display the new item at the top.
If the new item cannot be displayed without scrolling, a view holder is not created so there is nothing to animate. The only way to get the new item onto the screen when this happens is to scroll which causes the view holder to be created so the view can be laid out on the screen. (There does seem to be an edge case where the view is partially displayed and a view holder is created, but I will ignore this particular instance since it is not germane.)
So, the issue is that two different actions, animation of an added view and scrolling of an added view, must be made to look the same to the user. We could dive into the underlying code and figure out exactly what is going on in terms of view holder creation, animation timing, etc. But, even if we can duplicate the actions, it can break if the underlying code changes. This is what you are resisting.
An alternative is to add a header at position zero of the RecyclerView. You will always see the animation when this header is displayed and new items are added to position 1. If you don't want a header, you can make it zero height and it will not display. The following video shows this technique:
This is the code for the demo. It simply adds a dummy entry at position 0 of the items. If a dummy entry is not to your liking, there are other ways to approach this. You can search for ways to add headers to RecyclerView.
(If you do use a scrollbar, it will misbehave as you can probably tell from the demo. To fix this 100%, you will have to take over a lot of the scrollbar height and placement computation. The custom computeVerticalScrollOffset() for the LinearLayoutManager takes care of placing the scrollbar at the top when appropriate. (Code was introduced after video taken.) The scrollbar, however, jumps when scrolling down. A better placement computation would take care of this problem. See this Stack Overflow question for more information on scrollbars in the context of varying height items.)
MainActivity.java
public class MainActivity extends AppCompatActivity implements View.OnClickListener {
private TheAdapter mAdapter;
private final ArrayList<String> mItems = new ArrayList<>();
private int mItemCount = 0;
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
RecyclerView recyclerView = (RecyclerView) findViewById(R.id.recyclerView);
LinearLayoutManager layoutManager =
new LinearLayoutManager(this, LinearLayoutManager.VERTICAL, false) {
#Override
public int computeVerticalScrollOffset(RecyclerView.State state) {
if (findFirstCompletelyVisibleItemPosition() == 0) {
// Force scrollbar to top of range. When scrolling down, the scrollbar
// will jump since RecyclerView seems to assume the same height for
// all items.
return 0;
} else {
return super.computeVerticalScrollOffset(state);
}
}
};
recyclerView.setLayoutManager(layoutManager);
for (mItemCount = 0; mItemCount < 6; mItemCount++) {
mItems.add(0, "Item # " + mItemCount);
}
// Create a dummy entry that is just a placeholder.
mItems.add(0, "Dummy item that won't display");
mAdapter = new TheAdapter(mItems);
recyclerView.setAdapter(mAdapter);
}
#Override
public void onClick(View view) {
// Always at to position #1 to let animation occur.
mItems.add(1, "Item # " + mItemCount++);
mAdapter.notifyItemInserted(1);
}
}
TheAdapter.java
class TheAdapter extends RecyclerView.Adapter<TheAdapter.ItemHolder> {
private ArrayList<String> mData;
public TheAdapter(ArrayList<String> data) {
mData = data;
}
#Override
public ItemHolder onCreateViewHolder(ViewGroup parent, int viewType) {
View view;
if (viewType == 0) {
// Create a zero-height view that will sit at the top of the RecyclerView to force
// animations when items are added below it.
view = new Space(parent.getContext());
view.setLayoutParams(new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, 0));
} else {
view = LayoutInflater.from(parent.getContext())
.inflate(R.layout.list_item, parent, false);
}
return new ItemHolder(view);
}
#Override
public void onBindViewHolder(final ItemHolder holder, int position) {
if (position == 0) {
return;
}
holder.mTextView.setText(mData.get(position));
}
#Override
public int getItemViewType(int position) {
return (position == 0) ? 0 : 1;
}
#Override
public int getItemCount() {
return mData.size();
}
public static class ItemHolder extends RecyclerView.ViewHolder {
private TextView mTextView;
public ItemHolder(View itemView) {
super(itemView);
mTextView = (TextView) itemView.findViewById(R.id.textView);
}
}
}
activity_main.xml
<android.support.constraint.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
tools:context=".MainActivity">
<android.support.v7.widget.RecyclerView
android:id="#+id/recyclerView"
android:layout_width="0dp"
android:layout_height="0dp"
android:scrollbars="vertical"
app:layout_constraintBottom_toTopOf="#+id/button"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<Button
android:id="#+id/button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginBottom="8dp"
android:layout_marginEnd="8dp"
android:layout_marginStart="8dp"
android:text="Button"
android:onClick="onClick"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" />
</android.support.constraint.ConstraintLayout>
list_item.xml
<LinearLayout
android:id="#+id/list_item"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:orientation="horizontal">
<View
android:id="#+id/box"
android:layout_width="50dp"
android:layout_height="50dp"
android:layout_marginStart="16dp"
android:background="#android:color/holo_green_light"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="#+id/textView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:textSize="24sp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toEndOf="#id/box"
app:layout_constraintTop_toTopOf="parent"
tools:text="TextView" />
</LinearLayout>

I'm also displaying a live feed of items, and when a new item is added, it's before the first item. But in the recycler view, I need to scroll up to see the new item.
To solve the problem, add to the Adapter a RecyclerView.AdapterDataObserver() and override the onItemRangeInserted(). When new data is added, check if the data is on at position 0, and the recycler view was on top (You don't want to autoscroll to first position if you were scrolling in the list).
Exemple :
adapter.registerAdapterDataObserver(new RecyclerView.AdapterDataObserver() {
#Override
public void onItemRangeInserted(int positionStart, int itemCount) {
super.onItemRangeInserted(positionStart, itemCount);
if (positionStart == 0 && positionStart == layoutManager.findFirstCompletelyVisibleItemPosition()) {
layoutManager.scrollToPosition(0);
}
}
});
This solution worked fine for me, with different type of Adapter, like ListAdapter and PagedListAdapter.
I firstly wanted to use a similar implementation of the accepted solution, where you add a dumb first item, but in PagedListAdapter it's impossible as list are immutable.

This worked for me:
val atTop = !recycler.canScrollVertically(-1)
adapter.addToFront(item)
adapter.notifyItemInserted(0)
if (atTop) {
recycler.scrollToPosition(0)
}

The only solution that worked for me was to reverse the recycler's layout by calling setReverseLayout() and setStackFromEnd() on its LinearLayoutManager.
This might sound stupid, but the way RecyclerView handles adding items at the end of the list is what you need at the top. The only downsize of this is that you'd have to reverse your list and start adding items to the end instead.

Use adapter.notifyDataSetChanged() instead of adater.notifyItemInserted(0). This will scroll recylerView to zero position if current scroll position is one(old zero).

Related

How to create a layout with different types of cards?

I'm trying to develop an application which will show stats. The data will be from multiple APIs and is returned as a JSONArray. I'm confused on, Should I create separate recyclerview (s) for each card, since the data is from different list/models OR shall I create one recyclerview/one adapter? What is the best way to achieve a layout which is given below. Kindly help me with a working code. I'm new to this. :
Recyclerview is used to display large number of similar items dynamically.
You have few cards with different view.
Best way to achieve this use Scroll view->Linear Layout -> Card View.
As mentioned by Md. Asaduzzaman
Visit https://stackoverflow.com/a/58298078/9186263
Why do you want to use RecyclerView for this simple view. RecyclerView is used to show list of data. You simply use ScrollView as parent and CardView for each child.
<ScrollView
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fillViewport="true"
android:scrollbars="none">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<!-- CardView -->
<!-- CardView -->
<!-- CardView -->
<!-- CardView -->
</LinearLayout>
</ScrollView>
In your adapter class Override getItemViewType like the following.
#Override
public int getItemViewType(int position) {
if (position % 3 == 0) {
return 1;
} else if (position % 2 == 0) {
return 2;
} else {
return 3;
}
}
And oncreateViewHolder method return view according to its view type. like below
#Override
public CustomViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
switch (viewType) {
case 1:
View viewONE = LayoutInflater.from(parent.getContext()).inflate(R.layout.category_template_one, parent, false);
CustomViewHolder rowONE = new CustomViewHolder(viewONE);
return rowONE;
case 2:
View viewTWO = LayoutInflater.from(parent.getContext()).inflate(R.layout.category_template_two, parent, false);
CustomViewHolder rowTWO = new CustomViewHolder(viewTWO);
return rowTWO;
case 3:
View viewTHREE = LayoutInflater.from(parent.getContext()).inflate(R.layout.category_template_three, parent, false);
CustomViewHolder rowTHREE = new CustomViewHolder(viewTHREE);
return rowTHREE;
}
return null;
}
See this for complete example.
There are two things, firstly if you know the total count of items you need to display then don't use the RecyclerView, it would be more complex for you handle it because every child has different layout in your case.
Secondly if the total count is not fixed, then for that check the follow link.
https://www.journaldev.com/12372/android-recyclerview-example
RecyclerView works great if you want to recycle views. It means to re-use pieces of views. But in your case, if the items are way too different between themselves, there is absolutely no point of doing so.
One approach, would be to have a hierarchy like suggested in other answers:
ScrollView:
A view group that enables you to organize the cards as you need
Your Cards.
You can use FlexBoxLayout as a container for your Cards, if you want some of them side-by-side. As FlexBox are really worth knowing and are quite flexible.
If this is not the case, you can go with the good old LinearLayout as suggested by Md. Asaduzzaman.

RecyclerView inside NestedScrollView causes RecyclerView to inflate all elements

I'm having an issue with placing a RecyclerView inside a NestedScrollView, which causes ALL elements of the RecyclerView's adapter to be rendered.
This is a rather large issue, as the lists that the RecyclerView is showing can contain several hundred elements.
This is at the moment causing quite a lot of lag (obviously) as it has to render all views at once, and can't reuse any already inflated views as the RecyclerView normally does.
This is my current XML (Removed some bloat to minimize it):
<?xml version="1.0" encoding="utf-8"?>
<android.support.v4.widget.NestedScrollView
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fillViewport="true"
android:overScrollMode="never">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="90dp">
<!-- Some content -->
</RelativeLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<!-- Some more content -->
</LinearLayout>
<!-- Product list -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<TextView
android:id="#+id/title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:padding="12dp"/>
<android.support.v7.widget.RecyclerView
android:id="#+id/recyclerview"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:overScrollMode="never"/>
</LinearLayout>
</LinearLayout>
</android.support.v4.widget.NestedScrollView>
This is my onCreateView() from the Fragment that is inflating the view containing the NestedScrollView and RecyclerView:
#Override
public View onCreateView(LayoutInflater inflater, #Nullable ViewGroup container, #Nullable Bundle savedInstanceState) {
View root = inflater.inflate(R.layout.category_content_fragment, container, false);
ButterKnife.bind(this, root);
List<Product> products = new ArrayList<>(); //This is populated by other means, not relevant to the issue
productsRecyclerView.setNestedScrollingEnabled(false);
productsRecyclerView.setHasFixedSize(true);
productsRecyclerView.setLayoutManager(new LinearLayoutManager(getContext()));
ProductsContentAdapter productsContentAdapter = new ProductsContentAdapter(products);
productsRecyclerView.setAdapter(productsContentAdapter);
return root;
}
I have seen this post about the issue:
How to put RecyclerView inside NestedScrollView?
But it doesn't mention a final fix to the issue sadly.
To clarify:
The RecyclerView scrolls perfectly, it shows at the correct time, but the issue is that it renders ALL of its children instantly, meaning possible several hundreds of elements, even though the screen only shows 5-6 at a time at max.
Please feel free to ask questions if more information is needed.
------- EDIT -------
After many failed attempts of other solutions, i ended up using Jeeva Nandhan's solution.
Prior to asking this question i knew that was a possible solution, but i had 11 different possible views that needed to fit into the RecyclerView, so i would've liked to avoid it.
After using different ViewTypes, it worked perfectly. I was afraid it would be very inefficient due to the high amount of ViewTypes, but it's buttery smooth.
I too have come across this issue... This is because both scrollview and RecyclerView are different in loading data, since the ScrollView acts as the parent in this case and we are using the below line in our code.
setNestedScrollingEnabled(false);
This will make the scroll slow and hang issue based on the Recyclerview data.
One way which I have used to solve this issue is adding header to the Recyclerview..
Here I'll explain it clearly.
lets assume this recyclerview is in our activity.
<android.support.v7.widget.RecyclerView
android:id="#+id/recyclerview"
android:layout_width="match_parent"
android:layout_height="match_parent"
/>
The adapter class will be like this, where we will add the header
public class SampleAdapter extends RecyclerView.Adapter {
private final int BODY = 1;
private final int HEADER = 2;
private List<String> data = null;
SampleAdapter(List<String> data) {
this.data = data;
}
#Override
public int getItemViewType(int position) {
if (position == 0) {
return HEADER;
}
return BODY;
}
#Override
public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
View view;
switch (viewType) {
case HEADER:
view = LayoutInflater.from(parent.getContext()).inflate(R.layout.inflate_header_layout, parent, false);
return new HeaderViewHolder(view);
default:
//Setting the Body view...
view = LayoutInflater.from(parent.getContext()).inflate(R.layout.inflate_details, parent, false);
return new BodyViewHolder(view);
}
}
#Override
public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {
if (holder instanceof BodyViewHolder) {
//Set the body content....
if (data != null && data.size() > 0) {
/** Since we have added one cell for header,
* we need to decrement the position and render the body view.
*
*/
int bodyPosition = position - 1;
}
} else if (holder instanceof HeaderViewHolder) {
//Set the header content...
}
}
#Override
public int getItemCount() {
//Sice we are going to add header, we are supposed increase the count by one...
return data.size() + 1;
}
}
by this there is no need for NestedScrollView and all the view will work in RecyclerView behavior...
Hope this is helpful :)
If you have large amount of data to display,show only some numbers of data first time than on scroll use loadMoreListener to get next data.

How to scroll recylcerView and bottom button simultaneously

I have a recylcer view having grid images and I have a button at the bottom of the recycler view. This button should be at the bottom of the recyclerview, not the bottom of the parent.
I tried code for recyclerView. But the button stays as parent bottom not scrolling along with the recycler view.
<android.support.v7.widget.RecyclerView
android:id="#+id/recycler_view"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
android:text="#string/hello_blank_fragment"/>
I have found the solution.
I can create a separate view with Button and can add in the recyclerView Adapter.But there will be one problem - Since My Layout manager is GridLayout manager and having span of 2, I have to change the span to 1 when the current view is Button.
The changing of span when the view is Button is below:
final GridLayoutManager mGridManager = new GridLayoutManager(getActivity(),2);
mGridManager.setSpanSizeLookup(new GridLayoutManager.SpanSizeLookup() {
#Override
public int getSpanSize(int position) {
return adapter.isPositionHeader(position) ? mGridManager.getSpanCount() : 1;
}
});
recyclerView.setLayoutManager(mGridManager);
recyclerView.setAdapter(adapter);
This code I had to add in recyclerView Adapter, this below code means when button is the view return true, otherwise return false
public boolean isPositionHeader(int position) {
if(ADD_SOURCE_BUTTON == getItemViewType(position)){
return true;
} else {
return false;
}
}
Am not sure this is a good method, but it worked for me.
Try putting RecyclerView inside a ScrollView and place the button below RecyclerView. It will work fine now. Earlier it had some issues of scrolling but now its fixed by google I think and we can place RecyclerView inside scroll view, but am not sure its a good method, but it will work.

Endless RecyclerView with ProgressBar for pagination

I am using a RecyclerView and fetching objects from an API in batches of ten. For pagination, I use EndlessRecyclerOnScrollListener.
It's all working properly. Now all that's left is to add a progress spinner at the bottom of the list while the next batch of objects is fetched by the API. Here is a screenshot of the Google Play Store app, showing a ProgressBar in what is surely a RecyclerView:
The problem is, neither the RecyclerView nor the EndlessRecyclerOnScrollListener have built-in support for showing a ProgressBar at the bottom while the next batch of objects is being fetched.
I have already seen the following answers:
1. Put an indeterminate ProgressBar as footer in a RecyclerView grid.
2. Adding items to Endless Scroll RecyclerView with ProgressBar at bottom.
I am not satisfied with those answers (both by the same person). This involves shoehorning a null object into the data-set midway while the user is scrolling and then taking it out after the next batch is delivered. It looks like a hack that sidesteps the main problem which may or may not work properly. And it causes a bit of jarring and distortion in the list
Using SwipeRefreshLayout is not a solution here. SwipeRefreshLayout involves pulling from the top to fetch the newest items, and it does not show a progress view anyway.
Can someone please provide a good solution for this? I am interested in knowing how Google has implemented this for their own apps (the Gmail app has it too). Are there any articles where this is shown in detail? All answers & comments will be appreciated. Thank you.
Some other references:
1. Pagination with RecyclerView. (Superb overview ...)
2. RecyclerView header and footer. (More of the same ...)
3. Endless RecyclerView with ProgressBar at bottom.
HERE IS SIMPLER AND CLEANER APPROACH.
Implement Endless Scrolling from this Codepath Guide and then follow the following steps.
1. Add progress bar under the RecyclerView.
<android.support.v7.widget.RecyclerView
android:id="#+id/rv_movie_grid"
android:layout_width="0dp"
android:layout_height="0dp"
android:paddingBottom="50dp"
android:clipToPadding="false"
android:background="#android:color/black"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent">
</android.support.v7.widget.RecyclerView>
<ProgressBar
android:id="#+id/progressBar"
style="?android:attr/progressBarStyle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:visibility="invisible"
android:background="#android:color/transparent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" />
Here android:paddingBottom="50dp" and android:clipToPadding="false" are very important.
2. Get a reference to the progress bar.
progressBar = findViewById(R.id.progressBar);
3. Define methods to show and hide progress bar.
void showProgressView() {
progressBar.setVisibility(View.VISIBLE);
}
void hideProgressView() {
progressBar.setVisibility(View.INVISIBLE);
}
I implemented this on my old project, I did it as follows...
I've created an interface as the guys of your examples did
public interface LoadMoreItems {
void LoadItems();
}
Then I add added an addOnScrollListener() on my Adapter
recyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() {
#Override
public void onScrolled(RecyclerView recyclerView,
int dx, int dy) {
super.onScrolled(recyclerView, dx, dy);
totalItemCount = linearLayoutManager.getItemCount();
lastVisibleItem = linearLayoutManager
.findLastVisibleItemPosition();
if (!loading
&& totalItemCount <= (lastVisibleItem + visibleThreshold)) {
//End of the items
if (onLoadMoreListener != null) {
onLoadMoreListener.LoadItems();
}
loading = true;
}
}
});
The onCreateViewHolder() is where I put the ProgressBar or not.
#Override
public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent,
int viewType) {
RecyclerView.ViewHolder vh;
if (viewType == VIEW_ITEM) {
View v = LayoutInflater.from(parent.getContext()).inflate(
R.layout.list_row, parent, false);
vh = new StudentViewHolder(v);
} else {
View v = LayoutInflater.from(parent.getContext()).inflate(
R.layout.progressbar_item, parent, false);
vh = new ProgressViewHolder(v);
}
return vh;
}
On my MainActivity that is where I put the LoadItems() to add the others items is :
mAdapter.setOnLoadMoreListener(new LoadMoreItems() {
#Override
public void LoadItems() {
DataItemsList.add(null);
mAdapter.notifyItemInserted(DataItemsList.size() - 1);
handler.postDelayed(new Runnable() {
#Override
public void run() {
// remove progress item
DataItemsList.remove(DataItemsList.size() - 1);
mAdapter.notifyItemRemoved(DataItemsList.size());
//add items one by one
//When you've added the items call the setLoaded()
mAdapter.setLoaded();
//if you put all of the items at once call
// mAdapter.notifyDataSetChanged();
}
}, 2000); //time 2 seconds
}
});
For more information I just followed this Github repository(Note: this is using AsyncTask maybe it's useful as my answer, since I did it manually not with data from API but it should work as well) also this post was helpful to me endless-recyclerview-with-progress-bar
Also I don't know if you named it but I also found this post infinite_scrolling_recyclerview, maybe it could also help to you.
If it's not what you are looking for, let me know and tell me what's wrong with this code and I'll try to modify it as your like.
Hope it helps.
EDIT
Since you don't want to remove an item... I found I guess one guy that removes the footer only on this post : diseño-android-endless-recyclerview.
This is for ListView but I know you can adapt it to RecyclerView he's not deleting any item he's just putting Visible/Invisible the ProgressBar take a look : detecting-end-of-listview
Also take a look to : this question android-implementing-progressbar-and-loading-for-endless-list-like-android
There is another way to do this.
First your adapter's getItemCount returns listItems.size() + 1
return VIEW_TYPE_LOADING in getItemViewType() for position >= listItems.size(). This way the loader will only be shown at the end of the recycler view list. The only problem with this solution is even after reaching the last page, the loader will be shown, so in order to fix that you store the x-pagination-total-count in the adapter, and
then you change the condition to return view type to
(position >= listItem.size())&&(listItem.size <= xPaginationTotalCount) .
I just came up with this idea now what do you think?
here is my workaround without adding a fake item (in Kotlin but simple):
in your adapter add:
private var isLoading = false
private val VIEWTYPE_FORECAST = 1
private val VIEWTYPE_PROGRESS = 2
override fun getItemCount(): Int {
if (isLoading)
return items.size + 1
else
return items.size
}
override fun getItemViewType(position: Int): Int {
if (position == items.size - 1 && isLoading)
return VIEWTYPE_PROGRESS
else
return VIEWTYPE_FORECAST
}
override fun onCreateViewHolder(parent: ViewGroup?, viewType: Int): RecyclerView.ViewHolder {
if (viewType == VIEWTYPE_FORECAST)
return ForecastHolder(LayoutInflater.from(context).inflate(R.layout.item_forecast, parent, false))
else
return ProgressHolder(LayoutInflater.from(context).inflate(R.layout.item_progress, parent, false))
}
override fun onBindViewHolder(holder: RecyclerView.ViewHolder?, position: Int) {
if (holder is ForecastHolder) {
//init your item
}
}
public fun showProgress() {
isLoading = true
}
public fun hideProgress() {
isLoading = false
}
now you can easily call showProgress() before API call. and hideProgress() after API call was done.
I like the idea of adding a progress view holder to an adapter, but it tends to lead to some ugly logic manipulation to get what you want. The view holder approach forces you to guard against the additional footer item by fidgeting with the return values of getItemCount(), getItemViewType(), getItemId(position) and any kind of getItem(position) method that you may want to include.
An alternative approach is to manage the ProgressBar visibility at the Fragment or Activity level by showing or hiding the ProgressBar below the RecyclerView when loading starts and ends respectively. This can be achieved by including the ProgressBar directly in the view layout or by adding it to a custom RecyclerView ViewGroup class. This solution will generally lead to less maintenance and fewer bugs.
UPDATE: My suggestion poses a problem when you scroll the view back up while the content is loading. The ProgressBar will stick to the bottom of the view layout. This is probably not the behavior you want. For this reason, adding a progress view holder to your adapter is probably the best, functional solution. Just don't forget to guard your item accessor methods. :)
Another possible solution is to use the ConcatAdapter available from RecyclerView 1.2.0. The drawback is that this library version is yet in alpha.
Using this approach, separate adapter is used for progress indicator, concatenated with the main adapter.
val concatAdapter = ConcatAdapter(dataAdapter, progressAdapter)
progressAdapter should return 0 or 1 from getItemCount() method, depending on the loading state.
More info: https://medium.com/androiddevelopers/merge-adapters-sequentially-with-mergeadapter-294d2942127a
And check the current stable version of recyclerview library, might be already in a stable version at the time of reading: https://developer.android.com/jetpack/androidx/releases/recyclerview
Another viable approach would be to use recycler view item decorations. Using this approach would also save from modifying the ViewHolders.
Animating the decorator is also possible, see for example: https://medium.com/mobile-app-development-publication/animating-recycler-view-decorator-9b15fa4b2c23
Basically, item decorator is added when loading indicator should be present with recyclerView.addItemDecoration() function, and then removed with recyclerView.removeItemDecoration(). Constantly invalidateItemDecorations() on the recyclerView while item decoration is shown to make animation run.
A third possibility would be to use Paging library from Google (part of Jetpack), header and footer adapters available from v3 (still in beta)
https://www.youtube.com/watch?v=1cwqGOku2a4
This solution is inspired by Akshar Patels solution on this page. I modified it a bit.
When loading the first items it looks nice to have the ProgressBar centered.
I didn't like the remaining empty padding at the bottom when there existed no more items to load. That has been removed with this solution.
First the XML:
<android.support.v7.widget.RecyclerView
android:id="#+id/video_list"
android:paddingBottom="60dp"
android:clipToPadding="false"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent">
</android.support.v7.widget.RecyclerView>
<ProgressBar
android:id="#+id/progressBar2"
style="?android:attr/progressBarStyle"
android:layout_marginTop="10dp"
android:layout_width="wrap_content"
android:layout_centerHorizontal="true"
android:layout_height="wrap_content"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"/>
Then I added the following programmatically.
When first results been loaded, add this to your onScrollListener. It moves the ProgressBar from center to the bottom:
ConstraintLayout.LayoutParams layoutParams = (ConstraintLayout.LayoutParams) loadingVideos.getLayoutParams();
layoutParams.topToTop = ConstraintLayout.LayoutParams.UNSET;
loadingVideos.setLayoutParams(layoutParams);
When no more items exist, remove the padding at the bottom like this:
recyclerView.setPadding(0,0,0,0);
Hide and show your ProgressBar as usual.
Try this simple code :
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout 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"
tools:context=".MainActivity">
<androidx.recyclerview.widget.RecyclerView
android:id="#+id/recyclerview_cities"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_above="#id/preogressbar"
/>
<ProgressBar
android:id="#+id/preogressbar"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:visibility="gone"
android:layout_alignParentBottom="true"
android:layout_centerHorizontal="true"
/>
</RelativeLayout>
Make the progress bar visible when your list items already scrolled and hide when you get data from your service.
You can use layout_above tag in recycleView like this:
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginLeft="16dp"
android:layout_marginRight="16dp"
android:orientation="vertical">
<androidx.recyclerview.widget.RecyclerView
android:id="#+id/rv"
android:layout_below="#+id/tv2"
android:layout_width="match_parent"
android:focusable="false"
android:layout_height="wrap_content"
android:layout_marginTop="24dp"
android:layout_above="#+id/pb_pagination"/>
<ProgressBar
android:id="#+id/pb_pagination"
style="#style/Widget.AppCompat.ProgressBar"
android:layout_width="30dp"
android:layout_height="30dp"
android:indeterminate="true"
android:visibility="gone"
android:layout_alignParentBottom="true"
android:layout_centerHorizontal="true" />
</RelativeLayout>
Add in Adapter
Insert a new item to the RecyclerView on a predefined position
public void insert(int position, JobModel data) {
joblist.add(position, data);
notifyItemInserted(position);
}.
public void updateList( ArrayList<JobModel> data) {
try {
for (int i = joblist.size(); i < data.size(); i++)
insert(i, data.get(i));
}catch (Exception e){} }.
call from activity when page==2
apiSaveJobsAdapter.updateList(joblist);
Different approach would be to start the API call inside onBindViewHolder and initialy place into items view some progress indicator. After call is finished, you update the view (hide progress and showing received data). For example with Picasso for image loading, onBindViewHolder method would look like this
#Override
public void onBindViewHolder(final MovieViewHolder holder, final int position) {
final Movie movie = items.get(position);
holder.imageProgress.setVisibility(View.VISIBLE);
Picasso.with(context)
.load(NetworkingUtils.getMovieImageUrl(movie.getPosterPath()))
.into(holder.movieThumbImage, new Callback() {
#Override
public void onSuccess() {
holder.imageProgress.setVisibility(View.GONE);
}
#Override
public void onError() {
}
});
}
As I see it, there are two cases which can appear:
where you download all items in light version with one call (e.g. the adapter knows immediately that he’ll have to deal with 40 pictures, but downloads it on demand —> case which I showed previously with Picasso)
where you are working with real lazy loading and you are asking server to give you additional chunk of data. In this case, first prerequisite is to have adequate response from server with necessary information. Fore example
{
"offset": 0,
"total": 100,
"items": [{items}]
}
There response means that you received first chunk of total 100 data. My approach would be something like this:
View
After getting first chunk of data (e.g. 10) add them into adapter.
RecyclerView.Adapter.getItemCount
As long as the current amount of available items is lower than total amount (e.g. available 10; total 100), in getItemCount method you will return items.size() + 1
RecyclerView.Adapter.getItemViewType
if total amount of data is greater than amount of available items in adapter and the position = items.size() (i.e. you’ve fictively added item in getItemCount method), as view type you return some progress-indicator. Otherwise you’ll return normal layout type
RecyclerView.Adapter.onCreateViewHolder
When you are asked to use progress-indicator view type, all you need to do is to ask your presenter to get additional chunk of items and update the adapter
So basically, this is approach where you don’t have to add/remove items from the list and where you have control over situation when lazy loading will be triggered.
Here is the code example:
public class ForecastListAdapter extends RecyclerView.Adapter<ForecastListAdapter.ForecastVH> {
private final Context context;
private List<Forecast> items;
private ILazyLoading lazyLoadingListener;
public static final int VIEW_TYPE_FIRST = 0;
public static final int VIEW_TYPE_REST = 1;
public static final int VIEW_TYPE_PROGRESS = 2;
public static final int totalItemsCount = 14;
public ForecastListAdapter(List<Forecast> items, Context context, ILazyLoading lazyLoadingListener) {
this.items = items;
this.context = context;
this.lazyLoadingListener = lazyLoadingListener;
}
public void addItems(List<Forecast> additionalItems){
this.items.addAll(additionalItems);
notifyDataSetChanged();
}
#Override
public int getItemViewType(int position) {
if(totalItemsCount > items.size() && position == items.size()){
return VIEW_TYPE_PROGRESS;
}
switch (position){
case VIEW_TYPE_FIRST:
return VIEW_TYPE_FIRST;
default:
return VIEW_TYPE_REST;
}
}
#Override
public ForecastVH onCreateViewHolder(ViewGroup parent, int viewType) {
View v;
switch (viewType){
case VIEW_TYPE_PROGRESS:
v = LayoutInflater.from(parent.getContext()).inflate(R.layout.forecast_list_item_progress, parent, false);
if (lazyLoadingListener != null) {
lazyLoadingListener.getAdditionalItems();
}
break;
case VIEW_TYPE_FIRST:
v = LayoutInflater.from(parent.getContext()).inflate(R.layout.forecast_list_item_first, parent, false);
break;
default:
v = LayoutInflater.from(parent.getContext()).inflate(R.layout.forecast_list_item_rest, parent, false);
break;
}
return new ForecastVH(v);
}
#Override
public void onBindViewHolder(ForecastVH holder, int position) {
if(position < items.size()){
Forecast item = items.get(position);
holder.date.setText(FormattingUtils.formatTimeStamp(item.getDt()));
holder.minTemperature.setText(FormattingUtils.getRoundedTemperature(item.getTemp().getMin()));
holder.maxTemperature.setText(FormattingUtils.getRoundedTemperature(item.getTemp().getMax()));
}
}
#Override
public long getItemId(int position) {
long i = super.getItemId(position);
return i;
}
#Override
public int getItemCount() {
if (items == null) {
return 0;
}
if(items.size() < totalItemsCount){
return items.size() + 1;
}else{
return items.size();
}
}
public class ForecastVH extends RecyclerView.ViewHolder{
#BindView(R.id.forecast_date)TextView date;
#BindView(R.id.min_temperature)TextView minTemperature;
#BindView(R.id.max_temperature) TextView maxTemperature;
public ForecastVH(View itemView) {
super(itemView);
ButterKnife.bind(this, itemView);
}
}
public interface ILazyLoading{
public void getAdditionalItems();
}}
Maybe this'll inspire you to make something that will suit your needs

ViewHolder in RecyclerView.Adapter not specific to position

The following is part of my code for onBindViewHolder (inside MyAdapter extends RecyclerView.Adapter<MyAdapter.ViewHolder>)
#Override
public void onBindViewHolder(ViewHolder holder, int position) {
// - get element from your dataset at this position
StatusItem item = mDataset.get(position);
//......
//Add content and timing to the textview
String content = item.getContent();
holder.mTextViewTime.setText(timing);
//Set the img
holder.imgViewIcon.setImageDrawable(item.getProfileDrawable());
//Set content image (for Instagram)
holder.mImageViewContentPic.setImageDrawable(item.getContentDrawable());
//HIDE THE VIEW Start
if(item.getContentDrawable() == null){
holder.mImageViewContentPic.setVisibility(View.GONE);
}
//HIDE THE VIEW End
}
The part HIDE THE VIEW is not working as expected.
When I am scrolling downwards, the views are working normally. However, when I start to scroll upwards, i.e. revisited the previous views, the ImageViews that are supposed to be VISIBLE becomes GONE, although I checked my dataset and verified that it has not been modified. Try calling other methods on the views also give erratic results(positions and items in dataset do not match).
It seems that the view holders are not binded to specific positions inside the RecyclerView.
The code works as expected if I remove the HIDE THE VIEW part.
Is there any way to solve this issue and dynamically hide views in my case?
Note: I used some AsyncTasks to update the dataset and call notifyDataSetChanged(), if that is relevant.
###This is the solution to your problem:###
holder.mImageViewContentPic.setVisibility(View.VISIBLE);
if(item.getContentDrawable() == null){
holder.mImageViewContentPic.setVisibility(View.GONE);
}
Because RecyclerView use recycle very well, ViewHolder A may be used to be ViewHolder B, so you need to specific every attribute of a ViewHolder in case of some attributes attach to a wrong object.

Categories

Resources