I have a recycler view item that has few Views. I have a setting that will set the visibility of one Image view after the Recycler view is displayed. What is the best way to achieve this behavior?
What I tried, in the adapter ViewHolder class, I set a boolean that would hide the element and then call notifysetDataChanged()
public static class ViewHolder extends RecyclerView.ViewHolder {
public ImageView imageView;
public ViewHolder(View itemView) {
super(itemView);
.....
}
public void bind(item: Model){
//check and set visibility
if(showImage) imageview.setVisibility
else imageview.setVisibility
}
}
Calling this bind function from onBindViewHolder.
Is there any better way to achieve this as all the data remain the same only one visibility is changed?
your bind method is fine, but calling notifyDataSetChanged() will make your whole list redraw, all items. it's way better for perfomance to redraw only these items which has changed, use then notifyItemChanged(int position). calling it will make only single onBindViewHolder call with desired position, thus only one desired item will be redrawn (your bind call)
note there are more methods for preventing whole list redraw:
notifyItemChanged(int)
notifyItemInserted(int)
notifyItemRemoved(int)
notifyItemRangeChanged(int, int)
notifyItemRangeInserted(int, int)
notifyItemRangeRemoved(int, int)
Are you try this?
android:visibility="gone"
I have a RecyclerView adapter that looks like this:
public class RecyclerAdapter extends RecyclerView.Adapter<RecyclerAdapter.ViewHolder> {
private static Context context;
private List<Message> mDataset;
public RecyclerAdapter(Context context, List<Message> myDataset) {
this.context = context;
this.mDataset = myDataset;
}
public static class ViewHolder extends RecyclerView.ViewHolder implements View.OnCreateContextMenuListener, View.OnClickListener {
public TextView title;
public LinearLayout placeholder;
public ViewHolder(View view) {
super(view);
view.setOnCreateContextMenuListener(this);
title = (TextView) view.findViewById(R.id.title);
placeholder = (LinearLayout) view.findViewById(R.id.placeholder);
}
}
#Override
public RecyclerAdapter.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.message_layout, parent, false);
ViewHolder vh = new ViewHolder((LinearLayout) view);
return vh;
}
#Override
public void onBindViewHolder(ViewHolder holder, int position) {
Message item = mDataset.get(position);
holder.title.setText(item.getTitle());
int numImages = item.getImages().size();
if (numImages > 0) {
View test = LayoutInflater.from(holder.placeholder.getContext()).inflate(R.layout.images, holder.placeholder, false);
ImageView image = (ImageView) test.findViewById(R.id.image);
Glide.with(context)
.load("http://www.website.com/test.png")
.fitCenter()
.into(image);
holder.placeholder.addView(test);
}
}
#Override
public int getItemCount() {
return mDataset.size();
}
}
However, some of the items in the RecyclerView are showing images when they shouldn't be. How can I stop this from happening?
I do the check if (numImages > 0) { in onBindViewHolder(), but that's still not stopping it from showing images for items that shouldn't have images.
You should set imageView.setImageDrawable (null)
In onBindViewHolder() before setting the image using glide.
Setting image drawable to null fix the issue.
Hope it helps!
The problem is in onBindViewHolder, here:
if (numImages > 0) {
View test = LayoutInflater.from(holder.placeholder.getContext()).inflate(R.layout.images, holder.placeholder, false);
ImageView image = (ImageView) test.findViewById(R.id.image);
Glide.with(context)
.load("http://www.website.com/test.png")
.fitCenter()
.into(image);
holder.placeholder.addView(test);
}
If numImages is equal to 0, you're simply allowing the previously started load into the view you're reusing to continue. When it finishes, it will still load the old image into your view. To prevent this, tell Glide to cancel the previous load by calling clear:
if (numImages > 0) {
View test = LayoutInflater.from(holder.placeholder.getContext()).inflate(R.layout.images, holder.placeholder, false);
ImageView image = (ImageView) test.findViewById(R.id.image);
Glide.with(context)
.load("http://www.website.com/test.png")
.fitCenter()
.into(image);
holder.placeholder.addView(test);
} else {
Glide.clear(image);
}
When you call into(), Glide handles canceling the old load for you. If you're not going to call into(), you must call clear() yourself.
Every call to onBindViewHolder must include either a load() call or a clear() call.
I also had issues with RecyclerView showing wrong images. This happens because RecyclerView is not inflating view for every new list item: instead list items are being recycled.
By recycling views we can ruffly understand cloning views. A cloned view might have an image set from the previous interaction.
This is especially fair if your are using Picasso, Glide, or some other lib for async loading. These libs hold reference to an ImageView, and set an image on that refference when image is loaded.
By the time the image gets loaded, the item view might have gotten cloned, and the image is going to be set to the wrong clone.
To make a long story short, I solved this problem by restricting RecyclerView from cloning my item views:
setIsRecyclable(false)in ViewHolder constructor.
Now RecyclerView is working a bit slower, but at least the images are set right.
Or else cansel loading image in onViewRecycled(ViewHolder holde)
The issue here is that, as you are working with views that are going to be recycled, you'll need to handle all the possible scenarios at the time your binding your view.
For example, if you're adding the ImageView to the LinearLayout on position 0 of the data source, then, if position 4 doesn't met the condition, its view will most likely have the ImageView added when binding position 0.
You can add the content of R.layout.images content inside your
R.layout.message_layout layout's R.id.placeholder and showing/hiding the placeholder depending on the case.
So, your onBindViewHolder method would be something like:
#Override
public void onBindViewHolder(ViewHolder holder, int position) {
Message item = mDataset.get(position);
holder.title.setText(item.getTitle());
int numImages = item.getImages().size();
if (numImages > 0) {
holder.placeholder.setVisivility(View.VISIBLE);
ImageView image = (ImageView)holder.placeholder.findViewById(R.id.image);
Glide.with(context)
.load("http://www.website.com/test.png")
.fitCenter()
.into(image);
}else{
holder.placeholder.setVisibility(View.INVISIBLE);
}
}
Sometimes when using RecyclerView, a View may be re-used and retain the size from a previous position that will be changed for the current position. To handle those cases, you can create a new [ViewTarget and pass in true for waitForLayout]:
#Override
public void onBindViewHolder(VH holder, int position) {
Glide.with(fragment)
.load(urls.get(position))
.into(new DrawableImageViewTarget(holder.imageView,/*waitForLayout=*/ true));
https://bumptech.github.io/glide/doc/targets.html
I also had the same problem and ended with below solution and it working fine for me..
Have your hands on this solution might be work for you too (Put below code in your adapter class)-
If you are using Kotlin -
override fun getItemId(position: Int): Long {
return position.toLong()
}
override fun getItemViewType(position: Int): Int {
return position
}
If you are using JAVA -
#Override
public long getItemId(int position) {
return position;
}
#Override
public int getItemViewType(int position) {
return position;
}
This works for me in onBindViewHolder!
if(!m.getPicture().isEmpty())
{
holder.setIsRecyclable(false);
Picasso.with(holder.profile_pic.getContext()).load(m.getPicture()).placeholder(R.mipmap.ic_launcher_round).into(holder.profile_pic);
Animation fadeOut = new AlphaAnimation(0, 1);
fadeOut.setInterpolator(new AccelerateInterpolator());
fadeOut.setDuration(1000);
holder.profile_pic.startAnimation(fadeOut);
}
else
{
holder.setIsRecyclable(true);
}
I was having same issue I solved by writing holder.setIsRecyclable(false).Worked for me.
#Override
public void onBindViewHolder(#NonNull RecylerViewHolder holder, int position) {
NewsFeed currentFeed = newsFeeds.get(position);
holder.textView.setText(currentFeed.getNewsTitle());
holder.sectionView.setText(currentFeed.getNewsSection());
if(currentFeed.getImageId() == "NOIMG") {
holder.setIsRecyclable(false);
Log.v("ImageLoad","Image not loaded");
} else {
Picasso.get().load(currentFeed.getImageId()).into(holder.imageView);
Log.v("ImageLoad","Image id "+ currentFeed.getImageId());
}
holder.dateView.setText(getModifiedDate(currentFeed.getDate()));
}
override fun getItemId(position: Int): Long {
return position.toLong()
}
override fun getItemViewType(position: Int): Int {
return position
}
This Works for Me
I Had the same issue and i fixed it like this:
GOAL : onViewAttachedToWindow
#Override
public void onViewAttachedToWindow(Holder holder) {
super.onViewAttachedToWindow(holder);
StructAllItems sfi = mArrayList.get(position);
if (!sfi.getPicHayatParking().isEmpty()) {
holder.viewFliperMelk.addSlider(new TextSliderView(mContext.getApplicationContext()).image(T.GET_MELK_IMAGE + '/' + sfi.getPicHayatParking() + ".jpg").setScaleType(BaseSliderView.ScaleType.CenterCrop));
}
if (!sfi.getPicSleepRoom().isEmpty()) {
holder.viewFliperMelk.addSlider(new TextSliderView(mContext.getApplicationContext()).image(T.GET_MELK_IMAGE + '/' + sfi.getPicSleepRoom() + ".jpg").setScaleType(BaseSliderView.ScaleType.CenterCrop));
}
if (!sfi.getPicSalonPazirayi().isEmpty()) {
holder.viewFliperMelk.addSlider(new TextSliderView(mContext.getApplicationContext()).image(T.GET_MELK_IMAGE + '/' + sfi.getPicSalonPazirayi() + ".jpg").setScaleType(BaseSliderView.ScaleType.CenterCrop));
}
if (!sfi.getPicNamayeStruct().isEmpty()) {
holder.viewFliperMelk.addSlider(new TextSliderView(mContext.getApplicationContext()).image(T.GET_MELK_IMAGE + '/' + sfi.getPicNamayeStruct() + ".jpg").setScaleType(BaseSliderView.ScaleType.CenterCrop));
}
}
I had a similar issue when getting pictures from the photo gallery and putting them in a recyclerview with GridLayoutManager(never had the issue with Glide). So in the adapter onBindViewHolder use a HashMap or SparseIntArray to put the current hashcode(this is the common thing that the recycled views have in common) and adapter position inside it. Then call your background task and then once it's done and before you set the image, check to see if the hashcode key - which will always have the current adapter position as the value - still has the same value (adapter position) as when you first called the background task.
(Global variable)
private SparseIntArray hashMap = new SparseIntArray();
onBindViewHolder(ViewHolder holder, int position){
holder.imageView.setImageResource(R.drawable.grey_square);
hashMap.put(holder.hashCode(), position);
yourBackgroundTask(ViewHolder holder, int position);
}
yourBackGroundTask(ViewHolder holder, int holderPosition){
do some stuff in the background.....
*if you want to stop to image from downloading / or in my case
fetching the image from MediaStore then do -
if(hashMap.get(holder.hashCode())!=(holderPos)){
return null;
}
- in the background task, before the call to get the
image
onPostExecute{
if(hashMap.get(holder.hashCode())==(holderPosition)){
holder.imageView.setImageBitmap(result);
}
}
}
So i am just providing an extension to this answer since there is not much space to leave it as comment.
After trying out like mentioned in one of above solutions i found out that, the real issue can still be addressed even if you are using a static resource(is not being downloaded and is available locally)
So basically on onBindViewHolder event i just converted the resource to drawable and added it like below :
imageView.setImageDrawable(ContextCompat.getDrawable(context,R.drawable.album_art_unknown));
this way you wont have an empty space on the view while glide/async downloader is loading the actual image from network.
plus looking at that being reloaded every time i also added below code while calling the recycler adapter class;
recyclerView.setItemViewCacheSize(10);
recyclerView.setDrawingCacheEnabled(true);
so by using above way you wont need to set setIsRecyclable(false) which is degrading if you have larger datasets.
By doing this i you will have a flicker free loading of recyclerview of course except for the initial loads.
I would like to say that if you send the ImageView and any load-async command (for instance loading from S3), the recycler view does get confused.
I did set the bitmap null in the onViewRecycled and tested with attach and detach views etc. the issue never went away.
The issue is that if a holderView gets used for image-1, image-10 and stops at the scroll with image-19, what the user sees is image-1, then image-10 and then image-19.
One method that worked for me is to keep a hash_map that helps know what is the latest image that needs to be displayed on that ImageView.
Remember, the holder is recycled, so the hash for that view is persistent.
1- Create this map for storing what image should be displayed,
public static HashMap<Integer, String> VIEW_SYNCHER = new HashMap<Integer, String>();
2- In your Adapter, onBindViewHolder,
String thumbnailCacheKey = "img-url";
GLOBALS.VIEW_SYNCHER.put(holder.thumbnailImage.hashCode(), thumbnailCacheKey);
3- Then you have some async call to make the network call and load the image in the view right ?
In that code after loading the image from S3, you test to make sure what goes into the View,
// The ImageView in the network data loader, get its hash.
int viewCode = iim.imView[0].hashCode();
if (GLOBALS.VIEW_SYNCHER.containsKey(viewCode))
if (GLOBALS.VIEW_SYNCHER.get(viewCode).equals(bitmapKey))
iim.imView[0].setImageBitmap(GLOBALS.BITMAP_CACHE.get(bitmapKey).bitmapData);
So essentially, you make sure what is the last image key that should go into a view, then when you download the image you check to make sure that's the last image URL that goes in that view.
This solution worked for me.
i want to ask, why my code doesnt work when running on device, but when i run on emulator android like a Genymotion, it's working perfectly..
someone said on this link like this : you cannot call methods on the Activity superclass until after super.onCreate(), except in certain situations. Please postpone your initialization of promptsView until after super.onCreate() has been called.
i still dont get it, please tell me if you have the same problems..
anyway , i'm sorry if my explanation is bad..
public class DestinationListAdapter extends ArrayAdapter<DestinationModel> {
Context context;
int layoutResourceId;
List<DestinationModel> data = Collections.emptyList();
public DestinationListAdapter(Context context, int layoutResourceId, List<DestinationModel> data) {
super(context, layoutResourceId, data);
this.layoutResourceId = layoutResourceId;
this.context = context;
this.data = data;
}
#Override
public View getView(int position, View convertView, ViewGroup parent) {
View row = convertView;
DestinationHolder holder = null;
if(row == null)
{
// Predicted Error At Line Below
LayoutInflater inflater = ((Activity)context).getLayoutInflater();
row = inflater.inflate(layoutResourceId, parent, false);
holder = new DestinationHolder();
holder.destination_id = (TextView) row.findViewById(R.id.destination_id);
holder.destination_name = (TextView) row.findViewById(R.id.destination_name);
row.setTag(holder);
}
else
{
holder = (DestinationHolder)row.getTag();
}
DestinationModel weather = data.get(position);
holder.destination_id.setText(weather.getDestination_id());
holder.destination_name.setText(weather.getDestination_name());
return row;
}
for make sure when return back & continuing previous activity
i just Added & checking for getContext()!=null
Here's an good example :
Before block
adapter = new ExampleAdapter(getContext());
adapter.setData(items);
listView.setAdapter(adapter);
And better replace for getActivity()!=null
For example:
if (getActivity()!=null){
adapter = new ExampleAdapter(getActivity());
adapter.setData(items);
listView.setAdapter(adapter);
}
I think this is solved all problem which got the same error like my problems !
Be conscious of where in the lifecycle you are. The value of getContext() may not be available yet.
For example, in a DialogFragment, the context will not be available until onCreateDialog() is called. So don't try and create an adapter in the constructor, because the context will still be null at that point.
Actually this issue come when something comes as null byte.
and when we are using recycler view then it comes mostly. Because most people confuse in onCreateView of RecyclerViewAdpater.
Some code writers write Layout inflater as the layout of Tab. but here we will use another layout where we will mention our internal Recyclerview layout.
view= LayoutInflater.from(mcontext).inflate(R.layout.**item_tab14**,parent,false);
In the app I've been working on, I have a custom class DeviceListAdapter extending BaseAdapter which gets passed to my ListView. In my DeviceListAdapter class, I keep my own ArrayList<Device> which I use to generate the list's views with View getView(... ). Whenever the app causes a change in the data, I use custom methods in DeviceListAdapter to update the ArrayList<Device> to reflect the changes. I've used the debugger and many print statements to check that the data does get changed how I expect it to, adding and removing Device objects as specified. However, after each change to the data I also call notifyDataSetChanged(), but on the UI none of the elements get updated. In the debugger, I found that after calling notifyDataSetChanged(), the getView(... ) method was not being called, which explains why the ListView wasn't being redrawn. To figure out why, I used the debugger's "step into" function to trace where the program execution went into the android framework since I have the SDK sources downloaded. What I found was very interesting. The path of execution went like this:
DeviceListAdapter.notifyDataSetChanged()
BaseAdapter.notifyDataSetChanged()
DataSetObservable.notifyChanged()
AbsListView.onInvalidated()
Rather calling the onChanged() method, it jumped tracks and executed the onInvalidated() method once it reached AbsListView. Initially I thought this was an error with the debugger perhaps reading the wrong line number, but I restarted my Android Studio as well as totally uninstalled and reinstalled the app, but the result is the same. Can anybody tell me if this is legitimately a problem with Android's framework or if the debugger is unreliable for tracing execution outside of my own project files?
More on my implementation of notifyDataSetChanged()... I created the local method to override BaseAdapter's notifyDataSetChanged() so that I could set a boolean flag mForceRedraw inside of my DeviceListAdapter as to whether I should force redraw my list entries. In the getView(... ) method, I typically check if the second parameter, View convertView is null, if it is then I redraw the view and if not then I pass convertView through and return it. However, when 'mForceRedraw' is true, I never return convertView, I explicitly redraw the view. The problem that arises is caused by my earlier concern, which is that getView() is not called after I execute notifyDataSetChanged().
EDIT: Here's a code snippet of my DeviceListAdapter:
/**
* Serves data about current Device data to the mDeviceListView. Manages the dynamic and
* persistent storage of the configured Devices and constructs views of each individual
* list item for placement in the list.
*/
private class DeviceListAdapter extends BaseAdapter {
private boolean mForceRedraw = false;
/**
* Dynamic array that keeps track of all devices currently being managed.
* This is held in memory and is readily accessible so that system calls
* requesting View updates can be satisfied quickly.
*/
private List<Device> mDeviceEntries;
private Context mContext;
public DeviceListAdapter(Context context) {
this.mContext = context;
this.mDeviceEntries = new ArrayList<>();
populateFromStorage();
}
/**
* Inserts the given device into storage and notifies the mDeviceListView of a data update.
* #param newDevice The device to add to memory.
*/
public void put(Device newDevice) {
Preconditions.checkNotNull(newDevice);
boolean flagUpdatedExisting = false;
for (Device device : mDeviceEntries) {
if (newDevice.isVersionOf(device)) {
int index = mDeviceEntries.indexOf(device);
if(index != -1) {
mDeviceEntries.set(index, newDevice);
flagUpdatedExisting = true;
break;
} else {
throw new IllegalStateException();
}
}
//If an existing device was not updated, then this is a new device, add it to the list
if (!flagUpdatedExisting) {
mDeviceEntries.add(newDevice);
}
TECDataAdapter.setDevices(mDeviceEntries);
notifyDataSetChanged();
}
/**
* If the given device exists in storage, delete it and remove it from the mDeviceListView.
* #param device
*/
public void delete(Device device) {
Preconditions.checkNotNull(device);
//Remove device from mDeviceEntries
Iterator iterator = mDeviceEntries.iterator();
while(iterator.hasNext()) {
Device d = (Device) iterator.next();
if(device.isVersionOf(d)) {
iterator.remove();
}
}
TECDataAdapter.setDevices(mDeviceEntries);
notifyDataSetChanged();
}
/**
* Retrieves Device entries from persistent storage and loads them into the dynamic
* array responsible for displaying the entries in the listView.
*/
public void populateFromStorage() {
List<Device> temp = Preconditions.checkNotNull(TECDataAdapter.getDevices());
mDeviceEntries = temp;
notifyDataSetChanged();
}
public int getCount() {
if (mDeviceEntries != null) {
return mDeviceEntries.size();
}
return 0;
}
public Object getItem(int position) {
return mDeviceEntries.get(position);
}
public long getItemId(int position) {
return position;
}
public View getView(final int position, View convertView, ViewGroup parent) {
LinearLayout view;
if (convertView == null || mForceRedraw) //Regenerate the view
{
/* Draws my views */
} else //Reuse the view
{
view = (LinearLayout) convertView;
}
return view;
}
#Override
public void notifyDataSetChanged() {
mForceRedraw = true;
super.notifyDataSetChanged();
mForceRedraw = false;
}
}
You are in the adapter and calling notify dataset changed.This would ideally not even be needed.Because you are modifying the dataset which is used internally by your adapter.The getView method of your adapter will always be called whenever a view needs to be rendered.
The convertView approach is to solely recycle a view(not the data).It merely provides you an alternative to the expensive process of view inflation.
So what your code should be :
public View getView(final int position, View convertView, ViewGroup parent) {
LinearLayout view;
if (convertView == null) //Regenerate the view
{
/* inflate Draws my views */
} else
{
view = (LinearLayout) convertView;
}
//repopulate this view with the data that needs to appear at this position using getItem(position)
return view;
}
There are many bugs with notifyDataSetChanged() and usually they appear if you try doing some complex work with your list data.
Mostly, it is because the method is lazy and can't distinguish changes, so to avoid this problem, test your code with this scenario:
delete changing rows
call notifyDataSetChanged()
add changed rows at their indexes
again call notifyDataSetChanged()
and, tell me if it did'nt solve your problem.
Edit: After adapter code is put, I saw the flaw in your code.
Sorry for late the response:
convertView is the view which you had populated before after initializing it.
When in method getView() you get an instance of convertView, you must populate it before returning it.
so to be clear, do something like this:
public View getView(final int position, View convertView, ViewGroup parent) {
View view;
if (convertView == null) //Regenerate the view
{
/* Instantiate your view */
} else {
view = convertView;
}
// populate the elements in view like EditText, TextView, ImageView and etc
return view;
}
I have 92 images and I would like to have an indicator such as a check mark to indicate that an image is unlocked. I have the check mark in a .png file and what I tried at first was to just make a seperate copy of each image with the check mark put on top of the image in photoshop. However I know that there must be a simpler way of just adding the check mark file on top of the image that is already there instead of having a copy of the image with the check mark already on it.
I have a GridViewAdapter class responsible for loading the original images into a gridview:
#Override public View getView(int position, View convertView, ViewGroup parent) {
SquaredImageView view = (SquaredImageView) convertView;
if (view == null) {
view = new SquaredImageView(context);
view.setScaleType(CENTER_CROP);
}
// Get the image URL for the current position.
Integer url = getItem(position);
// Trigger the download of the URL asynchronously into the image view.
Picasso.with(context) //
.load(url) //
//
.error(R.drawable.error) //
.fit() //
.into(view);
return view;
}
where url is a list that contains the references to each image to be loaded
the mentioned SquaredImageView class is:
/** An image view which always remains square with respect to its width. */
final class SquaredImageView extends ImageView {
public SquaredImageView(Context context) {
super(context);
}
public SquaredImageView(Context context, AttributeSet attrs) {
super(context, attrs);
}
#Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
setMeasuredDimension(getMeasuredWidth(), getMeasuredWidth());
}
}
Any suggestions are appreciated
I can think of a few ways to do this:
Option 1:
Wrap the SquaredImageView in a Framelayout and place another SqauredImageView inside of it. Set the check mark on the second image view.
Option 2:
Wrap the SquaredImageView in a FrameLayout and set the checkmark image on the foreground property of the FrameLayout (using setForeground).
Option 3:
Create a ForegroundSquaredImageView which supports overlaying an image (similar to the foreground property of FrameLayout).
The code for something like this can be found here: https://gist.github.com/JakeWharton/0a251d67649305d84e8a. You'll need to change it to extend from your SquaredImageView.