I have a recycler view within a fragment and basically I m trying to load song list in the recycler view .Each row of recycler view contains an imageview (for album art) and textview ( for song name). I am having trouble when the size of the dataset is huge, that is when there are too many songs, the recycler view lags and the app ends up giving an ANR.I am using Glide to load album arts in each row's imageview.
How is google music player able to show such large number of songs without any lag?
Edit:
This is my SongsFragment
public class SongsFragment extends Fragment {
static {
AppCompatDelegate.setCompatVectorFromResourcesEnabled(true);
}
ProgressBar progressBar; // progress bar to show after every 30 items
NestedScrollView nestedScrollView; //for smooth scrolling of recyclerview as well as to detect the end of recyclerview
RecyclerView recyclerView;
ArrayList<Song> songMainList = new ArrayList<>(); //partial list in which items are added
ArrayList<Song> songAllList = new ArrayList<>(); //Complete List of songs
SongAdapter songsAdapter;
private LinearLayoutManager layoutManager;
#Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
View rootView = inflater.inflate(R.layout.fragment_songs, container, false);
nestedScrollView = (NestedScrollView) rootView.findViewById(R.id.nestedScrollView);
progressBar = (ProgressBar) rootView.findViewById(R.id.progressBar);
String songJson = getActivity().getIntent().getStringExtra("songList");
songAllList = new Gson().fromJson(songJson, new TypeToken<ArrayList<Song>>() {
}.getType());
//Getting list of all songs in songAllList
if (songAllList.size() > 30) {
songMainList = new ArrayList<>(songAllList.subList(0,30));
} else {
songMainList = songAllList;
}
//if size of fetched songAllList>30 then add only 30 rows to songMainList
recyclerView = (RecyclerView) rootView.findViewById(R.id.songs);
int spanCount = 1; // 2 columns
int spacing = 4; // 50px
recyclerView.addItemDecoration(new GridItemDecoration(spanCount, spacing, true));
recyclerView.setHasFixedSize(true);
recyclerView.setNestedScrollingEnabled(false);
recyclerView.setLayoutManager(new LinearLayoutManager(getActivity()));
songsAdapter = new SongAdapter(getActivity(), songMainList, recyclerView);
nestedScrollView.getViewTreeObserver().addOnScrollChangedListener(new ViewTreeObserver.OnScrollChangedListener() {
#Override
public void onScrollChanged() {
View view = (View) nestedScrollView.getChildAt(nestedScrollView.getChildCount() - 1);
int diff = (view.getBottom() - (nestedScrollView.getHeight() + nestedScrollView
.getScrollY()));
if (diff == 0) { //NestedScrollView scrolled to bottom
progressBar.setVisibility(View.VISIBLE); //show progressbar
new Handler().postDelayed(new Runnable() {
#Override
public void run() {
if (songMainList.size() < songAllList.size()) {
int x = 0, y = 0;
if ((songAllList.size() - songMainList.size()) >= 30) {
x = songMainList.size();
y = x + 30;
} else {
x = songMainList.size();
y = x + songAllList.size() - songMainList.size();
}
for (int i = x; i < y; i++) {
songMainList.add(songAllList.get(i)); //Adding new items from songAllList to songMainList one by one
songsAdapter.notifyDataSetChanged();
}
}
progressBar.setVisibility(View.GONE);
}
}, 1500);
}
}
});
recyclerView.setAdapter(songsAdapter);
return rootView;
}
}
And this is my RecyclerViewAdapter along with viewholder
public class SongAdapter extends RecyclerView.Adapter {
private List<Song> songsList;
private Context c;
private RecyclerView.ViewHolder holder;
public SongAdapter(Context context) {
mainActivityContext = context;
}
public SongAdapter(Context context, List<Song> songs, RecyclerView recyclerView) {
songsList = songs;
LinearLayoutManager linearLayoutManager = (LinearLayoutManager) recyclerView.getLayoutManager();
c = context;
}
public SongAdapter getInstance() {
return SongAdapter.this;
}
#Override
public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.song_list_row, parent, false);
return new SongViewHolder(view,c);
}
#Override
public void onBindViewHolder(final RecyclerView.ViewHolder holder, int position) {
if (holder instanceof SongViewHolder) {
Song song = songsList.get(position);
this.holder = holder;
String name = song.getName();
String artist = song.getArtist();
String imagepath = song.getImagepath();
((SongViewHolder) holder).name.setText(name);
((SongViewHolder) holder).artist.setText(artist);
if (!imagepath.equalsIgnoreCase("no_image")) //if the album art has valid imagepath for this song
Glide.with(c).load(imagepath)
.centerCrop()
.into(((SongViewHolder) holder).iv);
else
((SongViewHolder) holder).iv.setImageResource(R.drawable.empty);
((SongViewHolder) holder).song = song;
}
}
#Override
public int getItemCount() {
return songsList.size();
}
static class SongViewHolder extends RecyclerView.ViewHolder{
ImageView iv;
TextView name, artist;
CardView songListCard;
private Context ctx;
private OnLongPressListener mListener;
SongViewHolder(View v, Context context) {
super(v);
this.ctx = context;
iv= (ImageView) v.findViewById(R.id.album_art);
name= (TextView) v.findViewById(R.id.name);
artist= (TextView) v.findViewById(R.id.artist_mini);
songListCard = (CardView) v.findViewById(R.id.song_list_card);
}
}
The recyclerview works fine when there are only 150-200 items but when reaching to 600-700 items , the whole app slows down. Could this be because of the way I have used glide in onBindViewHolder?
Sort answer:
LinearLayoutManager(context).apply { isAutoMeasureEnabled = false }
// or in Java
layoutManager.setAutoMeasureEnabled(false)
UPDATE 2020.08.14
Deprecated RecyclerView.LayoutManager#setAutoMeasureEnabled
This method was deprecated in API level 27.1.0.
Implementors of LayoutManager should define whether or not it uses AutoMeasure by overriding isAutoMeasureEnabled()
From the doc of RecyclerView.LayoutManager#setAutoMeasureEnabled() we know :
This method is usually called by the LayoutManager with value {#code true} if it wants to support WRAP_CONTENT
It works by calling {#link LayoutManager#onLayoutChildren(Recycler, State)} during an {#link RecyclerView#onMeasure(int, int)} call, then calculating desired dimensions based on children's positions.
If we set mAutoMeasure = true, it will call LayoutManager#onLayoutChildren(Recycler, State) during an RecyclerView#onMeasure(int, int) call. Every child view's onMeasure() method will be called, this cost too much time.
Let's look at LinearLayoutManager's constructor
public LinearLayoutManager(Context context, int orientation, boolean reverseLayout) {
setOrientation(orientation);
setReverseLayout(reverseLayout);
setAutoMeasureEnabled(true);
}
So, after we set mAutoMeasure = false, everything will be ok.
Solved the problem by removing the NestedScrollView over the recyclerview.
The nestedscrollview was not allowing the recyclerview.addOnScrollListener() to be called because of which I was getting a lag on loading more items.
Here is how i implemented the loadOnScroll for RecyclerView-
recyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() {
#Override
public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
super.onScrolled(recyclerView, dx, dy);
if (!recyclerView.canScrollVertically(1))
onScrolledToBottom();
}
});
private void onScrolledToBottom() {
if (songMainList.size() < songAllList.size()) {
int x, y;
if ((songAllList.size() - songMainList.size()) >= 50) {
x = songMainList.size();
y = x + 50;
} else {
x = songMainList.size();
y = x + songAllList.size() - songMainList.size();
}
for (int i = x; i < y; i++) {
songMainList.add(songAllList.get(i));
}
songsAdapter.notifyDataSetChanged();
}
}
Do you load the data all at once? RecycleView should not have problem, I think if you have too much data the processing itself can take too much time. You should load the data in chunks and check the scroll state of the user and load the next batch etc. kinda how Instagram or Facebook does it.
you may achieve this by using AndroidX Room & Paging
cache your data from network to local Room Database
using Paging to load your data from Room Database
Related
I have a problem with a simple multicolumn layout RecyclerView which is best reproducible on tablets. I have created a basic example with fully functional source code below, maybe it is easier to quickly run it, instead of trying to understand what I mean ;)
The main problem on tablets (where I have a grid layout) is, that items get strangely rearranged when hiding specific items and displaying them again (via notifyItemRemoved() and notifyItemInserted()). I think this can be even reproduced with removing and inserting only the first item. The layout manager inserts an extra row at the top and moves items from the first line below to fill it. (The example below removes and inserts every third item, starting with the first one)
I have different types of items (in the example red, green and blue items) and I want to toggle a specific type of items (in the example you can toggle the red items with a click on the FloatingActionButton).
The weird behavior that I want to fix can be reproduced by just clicking the FAB twice without scrolling before. This would first filter the red items and then display them again. When toggling the filter the second time you will notice that there is a row of items inserted above and items from the first row are moved to fill it.
If you didn't notice during the animation, just scroll up after toggling the filter.
I would expect that the first row stays where it is and that just the red items get displayed again.
Does anybody know how to fix that?
Here is the activity code:
public class MainActivity extends AppCompatActivity {
private RecyclerView recyclerView;
private TestAdapter adapter;
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
recyclerView = (RecyclerView) findViewById(R.id.recycler_view);
((FloatingActionButton) findViewById(R.id.fab)).setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(final View v) {
toggleFiltered();
}
});
final int columnCount = getColumnCount(300);
GridLayoutManager layoutManager = new GridLayoutManager(this, columnCount);
recyclerView.setLayoutManager(layoutManager);
adapter = new TestAdapter();
recyclerView.setAdapter(adapter);
recyclerView.setItemAnimator(new DefaultItemAnimator());
}
private void toggleFiltered() {
final boolean filtered = adapter.isFiltered();
adapter.setFiltered(!filtered);
if(filtered) {
adapter.notifyItemInserted(0);
adapter.notifyItemInserted(3);
adapter.notifyItemInserted(6);
adapter.notifyItemInserted(9);
adapter.notifyItemInserted(12);
} else {
adapter.notifyItemRemoved(0);
adapter.notifyItemRemoved(3);
adapter.notifyItemRemoved(6);
adapter.notifyItemRemoved(9);
adapter.notifyItemRemoved(12);
}
}
int getColumnCount(final int dpThreshold){
final Display display = getWindowManager().getDefaultDisplay();
final DisplayMetrics outMetrics = new DisplayMetrics();
display.getMetrics(outMetrics);
final float density = getResources().getDisplayMetrics().density;
final float dpWidth = outMetrics.widthPixels / density;
return ((int) dpWidth) / dpThreshold;
}
class TestAdapter extends RecyclerView.Adapter {
public static final int TYPE_RED = 0;
public static final int TYPE_GREEN = 1;
public static final int TYPE_BLUE = 2;
private boolean filtered = false;
public boolean isFiltered() {
return filtered;
}
public void setFiltered(final boolean filtered) {
this.filtered = filtered;
}
#Override
public RecyclerView.ViewHolder onCreateViewHolder(final ViewGroup parent, final int viewType) {
final View view = new View(parent.getContext());
view.setLayoutParams(new RelativeLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, 500));
int color = Color.RED;
if(viewType == TYPE_GREEN){
color = Color.GREEN;
} else if (viewType == TYPE_BLUE){
color = Color.BLUE;
}
return new TestViewHolder(view, color);
}
#Override
public void onBindViewHolder(final RecyclerView.ViewHolder holder, final int position) {
}
#Override
public int getItemViewType(final int position) {
if(!filtered) {
if ((position + 3) % 3 == 0) {
return TYPE_RED;
}
if ((position + 2) % 3 == 0) {
return TYPE_GREEN;
}
} else {
if ((position) % 2 == 0) {
return TYPE_GREEN;
}
}
return TYPE_BLUE;
}
#Override
public int getItemCount() {
if(filtered) {
return 10;
}
return 15;
}
class TestViewHolder extends RecyclerView.ViewHolder {
public TestViewHolder(final View itemView, final int color) {
super(itemView);
itemView.setBackgroundColor(color);
}
}
}
}
And here is the layout code:
<android.support.constraint.ConstraintLayout
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:id="#+id/root_layout"
android:layout_width="match_parent"
android:layout_height="match_parent">
<android.support.v7.widget.RecyclerView
android:id="#+id/recycler_view"
android:layout_width="0dp"
android:layout_height="0dp"
app:layout_constraintBottom_toBottomOf="#+id/root_layout"
app:layout_constraintLeft_toLeftOf="#+id/root_layout"
app:layout_constraintRight_toRightOf="#+id/root_layout"
app:layout_constraintTop_toTopOf="#+id/root_layout"/>
<android.support.design.widget.FloatingActionButton
android:id="#+id/fab"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintBottom_toBottomOf="#+id/root_layout"
app:layout_constraintRight_toRightOf="#+id/root_layout"
android:layout_margin="16dp"
/>
</android.support.constraint.ConstraintLayout>
Firstly, I should mention that the problem is not in DefaultItemAnimator. If you set your animator to null, you would have the same behavior. And actually there is no problem at all. The behavior is expected, as you're just inserting the item into the first position of your TestAdapter, and then other items are shifting, but your RecyclerView is staying at the same position.
So all you need is just add recyclerView.scrollToPosition(0); line in the end of your toggleFiltered() method, so it will scroll up, once you've applied the filtering. And it will work as you wish.
its not weird. because you notifyItemInserted(0). it is insert Item. not notifyDataSetChanged(). adapter is showing item keep going. its Green Item. you notifyItemInserted(0) insert Red, still adapter showing first item is Green(it poisition is 1). because adapter notifyItemInserted(0) has before showing first item is Green.
file with working code to test my issue, you have to add 2 items, then delete any of those and then add a new one to see how the deleted gets on top of the newly addded
https://www.dropbox.com/s/ojuyz5g5f3kaz0h/Test.zip?dl=0
I have a problem when deleting an item from the recyclerview, when ever I delete an item, IF I add a new item, the deleted item will appear in top of the newly added item, how could I get a fresh view or avod this from happening as is a big issue.
this is how i add items from my main activity
if (!resta || (diff > (3*60*1000)))
{
Ri.add(dataroot);
Integer position = adapter.getItemCount() + 1;
adapter.notifyItemInserted(position);
}
here my Adapter
public class ComandaAdapter extends RecyclerView.Adapter<ComandaAdapter.ComandaAdapterViewHolder>{
private Context mContext;
private ArrayList<Integer> lista_entradas = new ArrayList<>();
private ArrayList<Integer> lista_fondos = new ArrayList<>();
private ArrayList<Integer> lista_postres= new ArrayList<>();
private Boolean primeritem;
private ArrayList<DataRoot> Rir;
private TextView txt_comandas;
private TextView txt_entracola;
private TextView txt_fondocola;
private TextView txt_postrecola;
public ComandaAdapter(Context context, TextView tx_entracola, TextView tx_fondocola, TextView tx_postrecola, TextView tx_comandas, ArrayList<DataRoot> Rir)
{
this.mContext = context;
this.txt_comandas = tx_comandas;
this.txt_entracola = tx_entracola;
this.txt_fondocola = tx_fondocola;
this.txt_postrecola = tx_postrecola;
this.Rir= Rir;
}
#Override
public ComandaAdapter.ComandaAdapterViewHolder onCreateViewHolder(ViewGroup parent, int viewType)
{
return new ComandaAdapter.ComandaAdapterViewHolder(LayoutInflater.from(mContext).inflate(R.layout.row,parent,false));
}
#Override
public void onBindViewHolder(final ComandaAdapter.ComandaAdapterViewHolder holder, final int position)
{
DataRoot Rdata = Rir.get(position);
holder.setdata(Rdata);
}
public void delete(int position)
{
Rir.remove(position);
notifyItemRemoved(position);
}
public class ComandaAdapterViewHolder extends RecyclerView.ViewHolder
{
Button btn_cerrar;
public ComandaAdapterViewHolder(View itemView)
{
super(itemView);
btn_cerrar = (Button) itemView.findViewById(R.id.btn_cerrar);
void setData(final DataRoot Rdata)
{
btn_cerrar.setOnClickListener(new View.OnClickListener()
{
#Override
public void onClick(View view)
{
btn_cerrar.setEnabled(false);
btn_cerrar.setBackgroundTintList(mContext.getColorStateList(R.color.cboff));
updateRetrofitEstadoorden(Rdata.get_id());
updateRetrofitNrocomanda(Rdata.get_id(), txt_comanda.getText().toString());
delete(getAdapterPosition());
}
});
Rdata.gerOrder();
creaboton():
and here my recyler
private void setAdapter()
{
adapter = new ComandaAdapter(this, txt_entracola, txt_fondocola, txt_postrecola, txt_comandas, Ri);
recyclerView.getRecycledViewPool().setMaxRecycledViews(-1, Ri.size());//va en 0 supuestamente -1 es default
recyclerView.setItemViewCacheSize(Ri.size()); //ver si hay que cambiar con cada item
LinearLayoutManager linearLayoutManager = new LinearLayoutManager(this);
linearLayoutManager.setOrientation(LinearLayoutManager.HORIZONTAL);
recyclerView.setLayoutManager(linearLayoutManager);
recyclerView.setItemAnimator(new DefaultItemAnimator());
recyclerView.setAdapter(adapter);
}
Thanks in advance for any help.
Images to show the problem
You are using the Recyclerview in a very non standard way. The issue you are seeing is because the views are being recycled (as they should be in a Recyclerview) but you are not clearing out the items from the previous view.
The problem in in this method:
public void setData(String value) {
container.removeAllViews(); // Remove all previously added views
textview.setText(value);
Random r = new Random();
int i1 = r.nextInt(5 - 1) + 1;
for (int i = 0; i < i1; i++) {
be = new Button(mContext);
be.setText("Boton " + i);
be.setLayoutParams(new LinearLayout.LayoutParams(240, LinearLayout.LayoutParams.WRAP_CONTENT));
container.addView(be);
}
}
By calling container.addView(be), you are manually adding extra views to the views that the Recyclerview creates. When you remove these views, they are cached and reused the next time you press "Add". The problem is that the cached view still contains all of the manually added views so you are then adding more views under the existing ones.
As you can see in the code above, i added container.removeAllViews(); which removes the views that were added previously ensuring that "Container" is empty before you start adding your extra views to it.
Also, unless you have a very specific reason for doing so, I would removes these lines as I believe you are hurting performance by having them:
list.getRecycledViewPool().setMaxRecycledViews(-1, index);
list.setItemViewCacheSize(index);
I am new in Android TV. Please have a look to the below attached image.
on the top of this picture there is a crousel and all the red marked area are the images.
i want to make this screen in Android TV. i have use BrowseFragment for this. I have use two Presenter one for First Row and another for other row.
The code which i have used :
HomeFragment.java
private CustomListRowPresenter mListRowPresenter;
private void loadRows() {
List<Movie> list = MovieList.setupMovies();
mListRowPresenter = new CustomListRowPresenter(getActivity(),this);
mRowsAdapter = new ArrayObjectAdapter(mListRowPresenter);
CardPresenter cardPresenter = new CardPresenter(getActivity());
mNumberOfRows = NUM_ROWS;
HeaderItem gridHeader = new HeaderItem(0, "");
GridItemPresenter mGridPresenter = new GridItemPresenter();
ArrayObjectAdapter gridRowAdapter = new ArrayObjectAdapter(mGridPresenter);
gridRowAdapter.add(getResources().getString(R.string.grid_view));
gridRowAdapter.add(getString(R.string.error_fragment));
gridRowAdapter.add(getResources().getString(R.string.personal_settings));
mRowsAdapter.add(new ListRow(gridHeader, gridRowAdapter));
int i;
for (i = 1; i < NUM_ROWS; i++) {
if (i != 0) {
Collections.shuffle(list);
}
ArrayObjectAdapter listRowAdapter = new ArrayObjectAdapter(cardPresenter);
for (int j = 1; j < NUM_COLS; j++) {
listRowAdapter.add(list.get(j % 5));
}
HeaderItem header = new HeaderItem(i, MovieList.MOVIE_CATEGORY[i]);
mRowsAdapter.add(new ListRow(header, listRowAdapter));
}
setAdapter(mRowsAdapter);
}
CardPresenter.java
public class CardPresenter extends Presenter {
private static final String TAG = "CardPresenter";
private static int CARD_WIDTH = 360;
private static int CARD_HEIGHT = 160;
private static int sSelectedBackgroundColor;
private static int sDefaultBackgroundColor;
private Drawable mDefaultCardImage;
private Activity mActivity;
public CardPresenter(Activity activity){
mActivity = activity;
}
public CardPresenter(){
}
#Override
public ViewHolder onCreateViewHolder(ViewGroup parent) {
Log.d(TAG, "onCreateViewHolder");
sDefaultBackgroundColor = parent.getResources().getColor(R.color.default_background);
sSelectedBackgroundColor = parent.getResources().getColor(R.color.selected_background);
mDefaultCardImage = parent.getResources().getDrawable(R.drawable.movie);
/*
ImageCardView cardView = new ImageCardView(parent.getContext()) {
#Override
public void setSelected(boolean selected) {
updateCardBackgroundColor(this, selected);
super.setSelected(selected);
}
};
cardView.setFocusable(true);
cardView.setFocusableInTouchMode(true);
cardView
.setBackgroundResource(R.drawable.gridview_cell_border);
updateCardBackgroundColor(cardView, false);*/
View view = mActivity.getLayoutInflater().inflate(R.layout.gridview_cell_rows, parent,
false);
LinearLayout linearLayout = (LinearLayout)view.findViewById(R.id.container_grid_view_item);
linearLayout.setBackgroundResource(R.drawable.gridview_cell_border);
view.setFocusable(true);
ViewHolder viewHolder = new ViewHolder(view);
return viewHolder;
}
private static void updateCardBackgroundColor(ImageCardView view, boolean selected) {
int color = selected ? sSelectedBackgroundColor : sDefaultBackgroundColor;
// Both background colors should be set because the view's background is temporarily visible
// during animations.
view.setBackgroundColor(color);
view.findViewById(R.id.info_field).setBackgroundColor(color);
}
#Override
public void onBindViewHolder(Presenter.ViewHolder viewHolder, Object item) {
Movie movie = (Movie) item;
View cardView = (View) viewHolder.view;
ImageView imageView = (ImageView)cardView.findViewById(R.id.imageView);
LinearLayout linearLayout = (LinearLayout)cardView.findViewById(R.id.container_grid_view_item);
linearLayout.setBackgroundResource(R.drawable.gridview_cell_border);
Log.d(TAG, "onBindViewHolder");
if (movie.getCardImageUrl() != null) {
/* cardView.setTitleText(movie.getTitle());
cardView.setContentText(movie.getStudio());
cardView.setMainImageDimensions(CARD_WIDTH, CARD_HEIGHT);*/
Glide.with(viewHolder.view.getContext())
.load(movie.getCardImageUrl())
.centerCrop()
.error(mDefaultCardImage)
.into(imageView);
}
}
#Override
public void onUnbindViewHolder(Presenter.ViewHolder viewHolder) {
Log.d(TAG, "onUnbindViewHolder");
View cardView = (View) viewHolder.view;
// Remove references to images so that the garbage collector can free up memory
}
So for first row i have used CardPresenter and for other row i have used GridPresenter.
My Requirment is that this viewpager type of thing should be auto rotate in every 5 second.
So Please help me how can i design this fragment.
As discussed in Creating a Catalog Browser, if you wish to customize the header views between fragments, use a Presenter and implement the abstract methods to create, bind, and unbind the view holder. In the BrowseFragment implementation that displays the catalog browser, use the setHeaderPresenterSelector() method to set the presenter for the row header or setOnItemViewSelectedListener() method to set an item selection listener, as shown in the following example.
setHeaderPresenterSelector(new PresenterSelector() {
#Override
public Presenter getPresenter(Object o) {
return new IconHeaderItemPresenter();
}
});
Then, set time duration of each page presenter using Timer as shown in this GitHub post.
For sample code implementation, this tutorial might also help.
My adapter code:
public class BrandAdapter extends RecyclerView.Adapter<BrandAdapter.BrandViewHolder> {
private static final String TAG = BrandAdapter.class.getSimpleName();
private List<BrandItem> brands;
private Context context;
public BrandAdapter(Context context, List<BrandItem> data) {
this.context = context;
this.brands = data;
}
public void setData(List<BrandItem> dataDownload) {
this.brands = dataDownload;
}
#Override
public BrandViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.row_item_brand, null);
BrandViewHolder holder = new BrandViewHolder(view);
return holder;
}
#Override
public void onBindViewHolder(BrandViewHolder holder, int position) {
BrandItem brandItem = brands.get(position);
String name = brandItem.getName();
int count = brandItem.getCountArticles();
holder.tvName.setText(name);
if (count > 0) {
holder.tvCount.setText("" + count);
} else {
holder.tvCount.setVisibility(View.GONE);
}
}
#Override
public int getItemCount() {
return brands.size();
}
public static class BrandViewHolder extends RecyclerView.ViewHolder {
TextView tvName;
TextView tvCount;
public BrandViewHolder(View itemView) {
super(itemView);
tvName = (TextView) itemView.findViewById(R.id.tv_brand_name);
tvCount = (TextView) itemView.findViewById(R.id.tv_count_article);
}
}
}
Fragment code :
recyclerView = (RecyclerView) view.findViewById(R.id.recycleView);
linearLayoutManager = new LinearLayoutManager(getActivity());
recyclerView.setLayoutManager(linearLayoutManager);
adapter = new BrandAdapter(getActivity(), brands);
recyclerView.setAdapter(adapter);
Data for brands is downloaded from server. After downloaded finished, I just set new data for adapter by this code :
brands = downloadedBrands();
adapter.setData(brands);
adapter.notifyDataSetChanged();
Everything is Ok when data loaded for first time after the download finish. But when I scroll down the recycleview and scroll up again, data for each item is wrong now, all textview tvCount is gone. I do not know why.
Is there any problem from my code ?
Greenrobo's answer is correct but here is an explanation as to WHY you are having this issue.
You are assuming that your view is always set to the default values in your onBindViewHolder method.
The RecyclerView re-uses views that have scrolled off screen and therefore the view you are binding to may have already been previously used (and changed).
You onBindViewHolder method should always set EVERYTHING up. i.e all views reset to the exact values you want and do not assume that because you default an item to visible, it will always be so.
Please make tvCount visible when setting a non-zero count.
if (count > 0) {
holder.tvCount.setText("" + count);
holder.tvCount.setVisibility(View.VISIBLE);
} else {
holder.tvCount.setVisibility(View.GONE);
}
See if this helps.
You told that if count is less than 0, hide the view. What if count is greater than zero ? You are not making the view visible again. So simply make the below changes in your if condition:
if (count > 0) {
holder.tvCount.setText("" + count);
holder.tvCount.setVisibility(View.VISIBLE);
} else {
holder.tvCount.setVisibility(View.GONE);
}
I have created a very simple project, displaying 28 images with StaggeredGridLayoutManager by recyclerview. but as I scroll the recyclerview it moves items for example from left to right or swap the column of left and right.
codes:
import java.util.ArrayList;
import java.util.List;
import android.app.Activity;
import android.os.Bundle;
import android.support.v7.widget.RecyclerView;
import android.support.v7.widget.StaggeredGridLayoutManager;
public class MainActivity extends Activity {
String mImageDir;
private RecyclerView mRecyclerView;
private StaggeredGridLayoutManager mLayoutManager;
MyRecyclerAdapter myRecyclerAdapter;
List<ImageModel> mImageList;
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
mRecyclerView = (RecyclerView)findViewById(R.id.recyclerview_rootview);
mLayoutManager = new StaggeredGridLayoutManager(2,StaggeredGridLayoutManager.VERTICAL);
mLayoutManager.setGapStrategy(StaggeredGridLayoutManager.GAP_HANDLING_MOVE_ITEMS_BETWEEN_SPANS);
mRecyclerView.setLayoutManager(mLayoutManager);
mRecyclerView.setHasFixedSize(false);
mImageList = new ArrayList<ImageModel>();
for (int i = 1; i < 29 ; i++) {
ImageModel img = new ImageModel();
img.setTitle("Image No " + i);
int drawableResourceId = this.getResources().getIdentifier("image"+String.valueOf(i), "drawable", this.getPackageName());
img.setResId(drawableResourceId);
mImageList.add(img);
}
myRecyclerAdapter = new MyRecyclerAdapter(MainActivity.this,mImageList);
mRecyclerView.setAdapter(myRecyclerAdapter);
}
}
And the adapter:
public class MyRecyclerAdapter extends RecyclerView.Adapter<MyRecyclerAdapter.ViewHolder> {
private List<ImageModel> mItems;
Context mContext;
public MyRecyclerAdapter(Context context,List<ImageModel> objects) {
mContext = context;
mItems = objects;
}
static class ViewHolder extends RecyclerView.ViewHolder{
public ImageView mImageView;
public TextView mTextView;
public View rootView;
public ViewHolder(View itemView) {
super(itemView);
rootView = itemView;
mImageView =(ImageView)itemView.findViewById(R.id.image);
mTextView =(TextView)itemView.findViewById(R.id.title);
}
}
#Override
public int getItemCount() {
return mItems.size();
}
#Override
public void onBindViewHolder(ViewHolder holder, int position) {
ImageModel item = mItems.get(position);
Picasso.with(mContext).load(item.getResId()).into(holder.mImageView);
holder.mTextView.setText(item.getTitle());
}
#Override
public ViewHolder onCreateViewHolder(ViewGroup parent, int arg1) {
LayoutInflater inflater =
(LayoutInflater) mContext.getSystemService(Activity.LAYOUT_INFLATER_SERVICE);
View convertView = inflater.inflate(R.layout.item, parent, false);
return new ViewHolder(convertView);
}
}
and a sample moving item:
http://i.imgur.com/FUapm2K.gif?1
if you play (scroll up and down) you can discover more interesting animation :-)
How to prevent that and having stable layout like an ordinary listview?
Edit
#Override
public void onBindViewHolder(ViewHolder holder, int position) {
ImageModel item = mItems.get(position);
RelativeLayout.LayoutParams rlp = (RelativeLayout.LayoutParams)holder.mImageView.getLayoutParams();
float ratio = item.getHeight()/item.getWidth();
rlp.height = (int)(rlp.width * ratio);
holder.mImageView.setLayoutParams(rlp);
Picasso.with(mContext).load(item.getResId()).into(holder.mImageView);
holder.mTextView.setText(item.getTitle());
}
This is happening because SGLM does not keep any w/h information about the views. So each time a View is rebound, it gets the place holder size first and then the final size when the image is loaded.
Loading the actual image w/ different size (than place holder) triggers a new layout request, and during that layout request, SGLM detects that there will be gaps in the UI (or some item w/ higher position appears below an item w/ lower position) thus re-orders items w/ an animation.
You can avoid it by setting your place holder image to the dimensions of the actual image. If you don't have it ahead of time, you can save them after the image is loaded for the first-time and use it in the next onBind call.
What worked for me was to disable all animation on the recycle view when using StaggeredGridLayoutManager.
mRecyclerView.setItemAnimator(null);
You can create your own animator if you only want to restrict moving animations and keep the add and remove item animations.
You can try this
StaggeredGridLayoutManager manager = new StaggeredGridLayoutManager(2, OrientationHelper.VERTICAL);
manager.setGapStrategy(StaggeredGridLayoutManager.GAP_HANDLING_NONE);
mRecyclerView.setLayoutManager(manager);
after you set this, you'll find there is a blank at the top when you scroll to the top. continue to set this
mRecyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() {
#Override
public void onScrollStateChanged(RecyclerView recyclerView, int newState) {
super.onScrollStateChanged(recyclerView, newState);
((StaggeredGridLayoutManager)recyclerView.getLayoutManager()).invalidateSpanAssignments();
}
});
It's work for me, I get this way somewhere. I hope it can help you!
Change this line
mLayoutManager.setGapStrategy(
StaggeredGridLayoutManager.GAP_HANDLING_MOVE_ITEMS_BETWEEN_SPANS);
to this line
mLayoutManager.setGapStrategy(
StaggeredGridLayoutManager.GAP_HANDLING_NONE);
Add the following line at the end of the method:
holder.mImageView.requestLayout();
#Override
public void onBindViewHolder(ViewHolder holder, int position) {
...
holder.mImageView.requestLayout();
}
This fixed the issue for me.
Maybe this is a more efficient way:
mBrandRecyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() {
#Override
public void onScrollStateChanged(RecyclerView recyclerView, int newState) {
if(newState == RecyclerView.SCROLL_STATE_IDLE){
mBrandRecyclerView.invalidateItemDecorations();
}
}
});
For more information:
https://github.com/ibosong/CommonItemDecoration
Set your recyclerview's height fixed and item height and width wrap-content for staggered-layout-manager
First set gap strategy like following code :
mLayoutManager = new StaggeredGridLayoutManager(SPAN_COUNT, StaggeredGridLayoutManager.VERTICAL);
mLayoutManager.setGapStrategy(StaggeredGridLayoutManager.GAP_HANDLING_NONE);
and then add your item to mItems and then use:
mAdapter.notifyItemInserted(mItems.size() - 1);
this method is better than using:
mAdapter.notifyDataSetChanged();