RecyclerView has lag and doesn't scroll smoothly - android

For showing my strings in a list I used RecyclerView, my strings are big and persian so for beatiful UI I want to show them justified. For that I used Text-Justify library. Now I have a beautiful justified text.
BUT because of heavy calculation of DocumentView (in simple: a justified TextView) my RecyclerView doesn't scroll smoothly and has lagging. So I decided to put these calculation before showing RecyclerView and instead show a progress. So I put setAdapter in an AsyncTask and create DocumentView from my strings but in runtime I get error that I don't be allowed to add view in another thread instead of main thread.
If I set the width and Height of DocumentView to somthing constant my RecyclerView scrolls very beautifully. How Can I calculate DocumentView width and height in another Thread so I can show a progress to user while calculating width and height.
private class myAdapter extends RecyclerView.Adapter<ItemViewHolder> {
private Context context;
private List<Item> items;
private ArrayList<DocumentView> justifiedBodies;
public ItemAdapter(Context context, List<Item> items){
this.items = items;
this.context = context;
justifiedBodies = new ArrayList<>();
for(Item item:Items){
Spanned span = Html.fromHtml(item.getBody());
justifiedBodies.add(createDocumentView(span, DocumentView.FORMATTED_TEXT));
}
}
#Override
public ItemViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
View itemView = LayoutInflater.
from(parent.getContext()).
inflate(R.layout.my_layout, parent, false);
return new ItemViewHolder(itemView);
}
#Override
public void onBindViewHolder(final ItemViewHolder holder, int position) {
final Item item = items.get(position);
DocumentView justifiedDocumentView = justifiedBodies.get(position);
if(justifiedDocumentView.getParent() != null){
((ViewGroup)justifiedDocumentView.getParent()).removeView(justifiedDocumentView);
}
holder.justifiedBody.removeAllViews();
holder.justifiedBody.addView(justifiedDocumentView);
}
#Override
public int getItemCount() {
return items.size();
}
}
public DocumentView createDocumentView(Spanned article, int type) {
final DocumentView documentView = new DocumentView(getActivity(), type);
documentView.getDocumentLayoutParams().setAntialias(true);
documentView.getDocumentLayoutParams().setHyphenated(false);
documentView.getDocumentLayoutParams().setMaxLines(100);
documentView.getDocumentLayoutParams().setOffsetX(10);
documentView.getDocumentLayoutParams().setOffsetY(10);
documentView.getDocumentLayoutParams().setTextSubPixel(true);
documentView.getDocumentLayoutParams().setWordSpacingMultiplier(5.0f);
documentView.getDocumentLayoutParams().setTextColor(0xff000000);
documentView.getDocumentLayoutParams().setTextTypeface(ResourceManager.iranTF);
documentView.getDocumentLayoutParams().setTextSize(23);
documentView.getDocumentLayoutParams().setTextAlignment(TextAlignment.JUSTIFIED);
documentView.getDocumentLayoutParams().setInsetPaddingLeft(30f);
documentView.getDocumentLayoutParams().setInsetPaddingRight(30f);
documentView.getDocumentLayoutParams().setInsetPaddingTop(30f);
documentView.getDocumentLayoutParams().setInsetPaddingBottom(30f);
documentView.getDocumentLayoutParams().setLineHeightMultiplier(1f);
documentView.getDocumentLayoutParams().setReverse(true);
documentView.setText(article);
// I want to calculate width and height and write them here
FrameLayout.LayoutParams layoutParams = new FrameLayout.LayoutParams(CALCULATED_WIDTH,
CALCULATED_HEIGHT);
documentView.setLayoutParams(layoutParams);
documentView.setCacheConfig(DocumentView.CacheConfig.AUTO_QUALITY);
return documentView;
}
public static class ItemViewHolder extends RecyclerView.ViewHolder
{
protected FrameLayout justifiedBody;
public ItemViewHolder(View itemView) {
super(itemView);
justifiedBody = (FrameLayout) itemView.findViewById(R.id.justified_body);
}
}
And this is my AsyncTask which fails:
class uiDecore extends AsyncTask<String, String, String>{
private ItemAdapter itemAdapter;
private ArrayList<Item> items;
#Override
protected void onPreExecute() {
super.onPreExecute();
progressView.start();
}
#Override
protected String doInBackground(String... params) {
// init Cards
items = DataSrc.getSelectedItem(); // query from my database
recyclerView = (RecyclerView) getActivity().findViewById(R.id.cards_recyclerview);
llm = new LinearLayoutManager(getActivity());
llm.setOrientation(LinearLayoutManager.VERTICAL);
// I don't allow this because I'm not in main thread!
recyclerView.setLayoutManager(llm);
itemAdapter = new ItemAdapter(getActivity(), items);
// in run time i get error becuse I allocate a new DocumentView
recyclerView.setAdapter(itemAdapter);
recyclerView.setHasFixedSize(true);
return null;
}
#Override
protected void onPostExecute(String s) {
super.onPostExecute(s);
progressView.stop();
}
}
I call uiDecore.execute in OnActivityCreated of my fragment.

Related

How to get access to item's Progress bar inside RecyclerVIew from MainActivity

I have 10 items inside RecyclerView. Each Item has a progressBar.How can i get access to the ProgressBar from MainActivity. This is my Adapter class.I am handling events using interface when i click on the Button of the Item in order to change the progress bar.
public class MyHospitalAdapter extends RecyclerView.Adapter<MyHospitalAdapter.MyViewHolder> {
private List<Hospital> mHospitalList;
private Context context;
public onImageClickListener mCallBack;
public interface onImageClickListener {
void clickOnImage(int id);
void onTextViewOfPriceSelected(int id, int amountOfProduction, int price, int time, int multiplier);
}
// Provide a reference to the views for each data item
// Complex data items may need more than one view per item, and
// you provide access to all the views for a data item in a view holder
public static class MyViewHolder extends RecyclerView.ViewHolder {
// each data item is just a string in this case
ImageView imageView;
ProgressBar progressBar;
TextView textViewOnProgressBar;
Button price;
TextView productionAmount;
TextView nameHospital;
public MyViewHolder(#NonNull View itemView, ImageView imageView, ProgressBar progressBar, TextView textViewOnProgressBar, Button price, TextView productionAmount, TextView nameHospital) {
super(itemView);
this.imageView = imageView;
this.progressBar = progressBar;
this.textViewOnProgressBar = textViewOnProgressBar;
this.price = price;
this.productionAmount = productionAmount;
this.nameHospital = nameHospital;
}
}
// Provide a suitable constructor (depends on the kind of dataset)
public MyHospitalAdapter(List<Hospital> mHospitalsList, Context context) {
this.mHospitalList = mHospitalsList;
this.context = context;
}
// Create new views (invoked by the layout manager)
#Override
public MyHospitalAdapter.MyViewHolder onCreateViewHolder(ViewGroup parent,
int viewType) {
OneBusinesBinding binding =
DataBindingUtil.inflate(LayoutInflater.from(parent.getContext()),
R.layout.one_busines, parent, false);
MyViewHolder vh = new MyViewHolder(binding.constraintLayout, binding.imageBusiness, binding.progressBar, binding.textViewOnProgressbar, binding.price, binding.amountofProduction, binding.nameHospital);
return vh;
}
// Replace the contents of a view (invoked by the layout manager)
#Override
public void onBindViewHolder(MyViewHolder holder, int position) {
// - get element from your dataset at this position
// - replace the contents of the view with that element
Hospital currentItem = mHospitalList.get(position);
int price = currentItem.getPrice();
int productionAmount = currentItem.getAmount();
int time = currentItem.getAmount();
int mMultiplier = currentItem.getMultiplier();
holder.imageView.setImageResource(AndroidImageAssets.getPictures().get(position));
holder.productionAmount.setText(context.getString(R.string.amount_of_production, productionAmount));
holder.price.setText(context.getString(AssetsUpgradeStrings.getStrings_On_Button_Buy().get(position), convertNumberToString(price)));
holder.progressBar.setProgressTintList(ColorStateList.valueOf(Color.GRAY));
holder.nameHospital.setText(AssetsUpgradeStrings.getHospitalsNames().get(position));
holder.textViewOnProgressBar.setText(context.getString(R.string.string_on_progressbar, position));
holder.imageView.setOnClickListener((v) -> {
mCallBack.clickOnImage(position);
});
This is part of MainActivity where i initialize my Adapter.
recyclerView = binding.recyclerView;
recyclerView.setHasFixedSize(true);
layoutManager = new LinearLayoutManager(this);
recyclerView.setLayoutManager(layoutManager);
binding.recyclerView.setHasFixedSize(true);
mAdapter = new MyHospitalAdapter(mListHospitals,getApplicationContext());
recyclerView.setAdapter(mAdapter);
recyclerView.setLayoutManager(layoutManager);
recyclerView.setAdapter(mAdapter);
I pass the List of the data of the item.
You can pass you progressbar in interface method:
public interface onImageClickListener {
void clickOnImage(int id, ProgressBar myProgressBar);
void onTextViewOfPriceSelected(int id, int amountOfProduction, int price, int time, int multiplier);
}
And use it in callback in activity:
holder.imageView.setOnClickListener((v) -> {
mCallBack.clickOnImage(position, holder.progressBar);
});
OR
You can pass your activity instance in constructor of adapter.
...
private MainActivity context;
...
public MyHospitalAdapter(List<Hospital> mHospitalsList, MainActivity context) {
this.mHospitalList = mHospitalsList;
this.context = context;
}
Run activity method on button click and pass progress bar as method parameter:
holder.imageView.setOnClickListener((v) -> {
context.yourMethodHere(holder.progressBar)
});

Setup span in StaggeredGridLayoutManager

I need to create a view like the following (this is an iOS screenshot since I've already created it).
Each blue box is part of a linear layout of a RecyclerView, while each green item is part of a staggered layout.
I'm able to create that structure. The problem is that I cannot make each gray box half of the yellow box. Using the StaggeredGridLayoutManager I'm not able to control the span (size) of the boxes that appear for the inner recycler view. Only a GridLayoutManager has the chance to set a span size lookup.
The desiderata is to calculate the boxes based on the with of the container. For example: if the screen is 99, the yellow box should be 66 while the gray 33.
Do you have any suggestions on how to achieve this?
Here a snippet of the code I'm using:
public class BoxViewAdapter extends RecyclerView.Adapter<BoxViewAdapter.BoxViewHolder> {
private List<Box> mValues;
private Context mContext;
protected BoxViewAdapter.ItemListener mListener;
public BoxViewAdapter(Context context, List<Box> values, BoxViewAdapter.ItemListener itemListener) {
mValues = values;
mContext = context;
mListener = itemListener;
}
#Override
public BoxViewAdapter.BoxViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
View view = LayoutInflater.from(mContext).inflate(R.layout.box_view_item, parent, false);
return new BoxViewAdapter.BoxViewHolder(view);
}
#Override
public void onBindViewHolder(#NonNull BoxViewAdapter.BoxViewHolder holder, int position) {
holder.bind(mValues.get(position));
}
#Override
public int getItemCount() {
return mValues.size();
}
public interface ItemListener {
void onItemClick();
}
public class BoxViewHolder extends RecyclerView.ViewHolder implements View.OnClickListener {
public Box item;
private RecyclerView recyclerView;
public BoxViewHolder(View v) {
super(v);
v.setOnClickListener(this);
StaggeredGridLayoutManager staggeredGridLayoutManager = new StaggeredGridLayoutManager(2, StaggeredGridLayoutManager.HORIZONTAL);
recyclerView = v.findViewById(R.id.cardRecyclerView);
recyclerView.setLayoutManager(staggeredGridLayoutManager);
}
public void bind(Box item) {
this.item = item;
recyclerView.setAdapter(new CardViewAdapter(itemView.getContext(), item.cards, null));
}
#Override
public void onClick(View view) {
if (mListener != null) {
mListener.onItemClick();
}
}
}
}
In the meantime, I've resolved this way.
In onBindViewHolder of CardViewAdapter I simply set LayoutParams for itemView. Since the item view is the rendered inside a RecyclerView, StaggeredGridLayoutManager knows how to deal with it.
#Override
public void onBindViewHolder(#NonNull CardViewHolder viewHolder, int position) {
if (mUseLayoutParameters) {
int singleScreenWidth = DisplayUtils.getScreenWidth() / 3;
Card card = mValues.get(position);
if (card.isDoubleSized) {
singleScreenWidth = singleScreenWidth * 2;
}
StaggeredGridLayoutManager.LayoutParams layoutParams = new StaggeredGridLayoutManager.LayoutParams(singleScreenWidth, ViewGroup.LayoutParams.MATCH_PARENT);
viewHolder.itemView.setLayoutParams(layoutParams);
}
viewHolder.bind(mValues.get(position));
}

Get item fully visible or not in Recyclerview adapter

I am making an android app in which RecyclerView consist list of GIFs, Now I want to develop such functionality like when GIF item is completely visible to the user then and then it starts loading, once it slightly looses the focus of user it should stop loading.
I have displayed GIF in recyclerview now the only part remain is how to get that such GIF is fully visible or looses focus. I want such code inside adapter which indicated that this item is fully visible and this item looses focus.
MY Adapter class
public class FeedAdapter extends RecyclerView.Adapter<FeedAdapter.MyViewHolder> {
private List<FeedModel> feedList;
Context context;
#Override
public MyViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
View itemView = LayoutInflater.from(parent.getContext())
.inflate(R.layout.single_item, parent, false);
return new MyViewHolder(itemView);
}
#Override
public void onBindViewHolder(final MyViewHolder holder, int position) {
holder.setData(feedList.get(position));
new GifDataDownloader() {
#Override
protected void onPostExecute(final byte[] bytes) {
holder.gifImageView.setBytes(bytes);
holder.gifImageView.startAnimation();
}
}.execute(feedList.get(position).getUrl());
}
public FeedAdapter(Context context, List<FeedModel> feedList) {
this.feedList = feedList;
this.context = context;
}
#Override
public int getItemCount() {
return feedList.size();
}
public void setGridData(ArrayList<FeedModel> feedList) {
this.feedList = feedList;
notifyDataSetChanged();
}
public class MyViewHolder extends RecyclerView.ViewHolder {
FeedModel item;
GifImageView gifImageView;
public MyViewHolder(View itemView) {
super(itemView);
gifImageView = itemView.findViewById(R.id.gifImageView);
}
public void setData(FeedModel item) {
this.item = item;
}
}
My activity
public class MainActivity extends AppCompatActivity {
ImageView imageView;
ImageView imageView1;
private PendingIntent pendingIntent;
RecyclerView recyclerView;
FeedAdapter mAdapter;
ProgressBar progressBar;
GridLayoutManager manager;
Spinner countrySpinner;
ArrayList<FeedModel> feedList;
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
feedList = new ArrayList<>(10);
for (int i = 0; i < 10; i++) {
FeedModel feedModel = new FeedModel();
feedModel.setUrl("https://media.tenor.com/images/925bfbaad2f947987bcf18b9167b3326/tenor.gif");
feedList.add(feedModel);
}
recyclerView = (RecyclerView) findViewById(R.id.recycler_view);
mAdapter = new FeedAdapter(getApplicationContext(), feedList);
manager = new GridLayoutManager(getApplicationContext(), 1, GridLayoutManager.VERTICAL, false);
recyclerView.setLayoutManager(manager);
recyclerView.setItemAnimator(new DefaultItemAnimator());
recyclerView.setVisibility(View.VISIBLE);
recyclerView.setAdapter(mAdapter);
}
Better way to used glide to show image and gif etc. it is take all senior that you define .
Add below dependency into app level gradle file
implementation 'com.github.bumptech.glide:glide:4.7.1'
then after used in adapter like this way..
private Context context;
// define context into constructor
public RecyclerviewAdapter(List<StepsItem> stepsItems, Context context) {
this.stepsItems = stepsItems;
this.context = context;
}
Glide.with(context).load(yoururl).into(holder.mIvIcon);
Start gif after your view is created like in onstart() method
And when you want to stop the gif when user is no more interact with app , stop the gif in onpause() method.

Android RecyclerView does not update on data changed

The RecyclerView shows only items that was in ArrayList (data of the adapter) on creation. All my attempts to add more lines seems to work, but the list stays with initial data.
It isn't duplicate, as I tried all the suggestions found on google.
What I already did:
adapter.notifyDataSetChanged();
adapter.notifyItemChanged(5);
recycleList.invalidate();
recycleList.setAdapter(adapter);
running the update on UIThread with Handler
The adapter work as it should - onCreateView do it's work (creat new lines) as well as onBindView (bind new data). getItemCount() returns correct value (larger after update). Everything looks like it should be - instead of the final view - does not change.
The only thing that will work is recreate the adapter from scratch with new dataset - completely unacceptable by design (ugly on long lists).
Here is the code:
Create in fragment:
adapter = new MyListAdapter(getContext());
adapter.addItems(initialItems); // These and only these I see on the list
LinearLayoutManager mLayoutManager=new LinearLayoutManager(getContext());
recycleList.setLayoutManager(mLayoutManager);
recycleList.setAdapter(adapter);
Trying to update from fragment:
public void onUpdateReady(ArrayList<Model> freshData) {
new Handler(Looper.getMainLooper()).post(new Runnable() {
#Override
public void run() { // UI thread
adapter.addItems(freshData);
adapter.notifyDataSetChanged();
adapter.notifyItemChanged(5);
recycleList.invalidate();
//notifyItemRangeInserted(3,freshData.size());
}
});
}
Adapter:
private Context mContext;
private ArrayList<Model> data;
public MyListAdapter(Context mContext) {
this.mContext = mContext;
data=new ArrayList<>();
}
#Override
public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
LayoutInflater inflater = LayoutInflater.from(mContext);
View view = inflater.inflate(R.layout.list_item, parent, false);
return new ViewHolder(view);
}
#Override
public void onBindViewHolder(ViewHolder viewHolder, int position) {
viewHolder.textName.setText(data.get(position).getName());
}
#Override
public int getItemCount() {
return data.size();
}
public void addItems(Collection<Model> list) {
data.addAll(list);
}
public class ViewHolder extends RecyclerView.ViewHolder {
#BindView(R.id.textName) TextView textName;
public ViewHolder(View itemView) {
super(itemView);
ButterKnife.bind(this,itemView);
}
}
UPDATE: Changed according to comments:
public void addItems(Collection<Model> list) {
ArrayList<Model> newData=new ArrayList<>(); // Make new array
newData.addAll(data); // Take old
newData.addAll(list); // Add new
data=newData; // change to data to newly created array
notifyDataSetChanged();
}
try this
In Fragment
ArrayList<Data> items=new ArrayList<>();
items.addAll(initialItems);
adapter = new MyListAdapter(getContext(),items);
LinearLayoutManager mLayoutManager=new LinearLayoutManager(getContext());
recycleList.setLayoutManager(mLayoutManager);
recycleList.setAdapter(adapter);
public void onUpdateReady(ArrayList<Model> freshData) {
new Handler(Looper.getMainLooper()).post(new Runnable() {
#Override
public void run() { // UI thread
items.addAll(freshData);
adapter.notifyDataSetChanged();
followList.invalidate();
}
});
}
private Context mContext;
private ArrayList<Model> data;
public MyListAdapter(Context mContext,ArrayList<Model> data) {
this.mContext = mContext;
this.data=data;
}
#Override
public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
LayoutInflater inflater = LayoutInflater.from(mContext);
View view = inflater.inflate(R.layout.list_item, parent, false);
return new ViewHolder(view);
}
#Override
public void onBindViewHolder(ViewHolder viewHolder, int position) {
viewHolder.textName.setText(data.get(position).getName());
}
#Override
public int getItemCount() {
return data.size();
}
public class ViewHolder extends RecyclerView.ViewHolder {
#BindView(R.id.textName) TextView textName;
public ViewHolder(View itemView) {
super(itemView);
ButterKnife.bind(this,itemView);
}
}
Change your Adapter constructor:
private ArrayList<Model> data;
public MyListAdapter(Context mContext,ArrayList<Model> data) {
this.mContext = mContext;
this.data = data;}
public void addItems(Collection<Model> list) {
data.addAll(list);
}
fragment:
adapter = new MyListAdapter(getContext(),initialItems);
I had a similar problem once and calling invalidateViews on the ListView worked, even if it was a hack. Instead of recycleList.invalidate(); try recycleList.invalidateViews();

RecycleView displays data only first time

I have a RecycleView to which I give an Array of Post objects. When it's loaded first time, everything works fine but trying to replace the data or just entering in the Fragment again then the RecycleView does not show anything. Also I have checked the size of the new data and it's different from zero. Here is my code:
// Init the list
list = (RecyclerView) view.findViewById(R.id.list);
list.setLayoutManager(new LinearLayoutManager(mContext));
...
if (adapter != null) {
adapter.replaceCurrentPosts(new ArrayList<>(postList));
}
else {
adapter = new PostAdapter(mContext, new ArrayList<>(postList), this);
list.setAdapter(adapter);
}
...
public PostAdapter(Context context, List<Post> posts, onPostElementClickListener listener) {
this.context = context;
this.posts = posts;
this.mListener = listener;
}
public void replaceCurrentPosts(List<Post> newPosts) {
this.posts.clear();
this.posts.addAll(newPosts);
this.notifyDataSetChanged();
}
#Override
public int getItemCount() {
return posts.size();
}
package com.example.jiteshmohite.myapplication;
public class PostAdapter extends RecyclerView.Adapter {
private Context context;
private List<Post> posts;
private onPostElementClickListener mListener;
// Provide a reference to the views for each data item
// Complex data items may need more than one view per item, and
// you provide access to all the views for a data item in a view holder
public static class ViewHolder extends RecyclerView.ViewHolder {
// each data item is just a string in this case
public TextView mTextView;
public ViewHolder(View v) {
super(v);
mTextView = (TextView) v.findViewById(R.id.info_text);
}
}
public PostAdapter(Context context, List<Post> posts, onPostElementClickListener listener) {
this.context = context;
this.posts = posts;
this.mListener = listener;
}
public void replaceCurrentPosts(List<Post> newPosts) {
posts.clear();
posts = newPosts;
this.notifyDataSetChanged();
}
#Override
public PostAdapter.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
// create a new view
View v = LayoutInflater.from(parent.getContext())
.inflate(R.layout.my_text_view, parent, false);
// set the view's size, margins, paddings and layout parameters
ViewHolder vh = new ViewHolder(v);
return vh;
}
#Override
public void onBindViewHolder(PostAdapter.ViewHolder holder, final int position) {
// - get element from your dataset at this position
// - replace the contents of the view with that element
holder.mTextView.setText(posts.get(position).getName());
holder.mTextView.setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View v) {
mListener.onClick(position);
}
});
}
#Override
public int getItemCount() {
return posts.size();
}
interface onPostElementClickListener {
public void onClick(int pos);
}
}
private RecyclerView list;
private PostAdapter postAdapter;
private RecyclerView.LayoutManager layoutManager;
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_recycler_example);
list = (RecyclerView) findViewById(R.id.list);
layoutManager = new LinearLayoutManager(getApplicationContext());
list.setLayoutManager(layoutManager);
postAdapter = new PostAdapter(getApplicationContext(), getPostData(), listener);
list.setAdapter(postAdapter);
}
private PostAdapter.onPostElementClickListener listener = new PostAdapter.onPostElementClickListener() {
#Override
public void onClick(int pos) {
Toast.makeText(getApplicationContext(), pos + "" , Toast.LENGTH_SHORT).show();
List<Post> posts = getPostData();
posts.get(pos).setName("abc");
postAdapter.replaceCurrentPosts(posts);
}
};
// get post list which contains name
public List<Post> getPostData() {
Post post = new Post();
post.setName("xyz");
List<Post> posts = new ArrayList<Post>();
posts.add(post);
posts.add(post);
posts.add(post);
posts.add(post);
posts.add(post);
return posts;
}
}
I suggesting rather then notifyDataSetChanged() you should refer notifyItemChange which modify specific positions.
please let me know if it is not work for you.
why don't you try to add one element (or maybe more) instead of remove all and re-add all items?

Categories

Resources