4

I'm developing an android application. One of my fragments contains a simple listview showing friend list. Each friend can have its own profile image - it is set by the Glide library. When user has no profile pic set the default image is shown. My problem is, that every time, first element on the list gets the same picture which is set on the last element of the list which is not default picture. What i mean is shown on pic:

doubled profile pic

user with name wiktor has set profile picture and as you see the first position bonzo has wiktor's profile pic ( bonzo should have default pic )

there is also a problem with deleting user form list:

list after delete

as you see, i removed majka from friend list and next elements gets her picture.

The default profile picture is set in inside row layout xml from drawables.

Here is code of my listview adapter:

public class FriendsAdapter extends ArrayAdapter<FriendData> {

static class FriendHolder {

    TextView friendName;
    TextView friendRank;
    ImageView friendIcon;
    ImageButton deleteFriendBtn;
    ImageButton banFriendBtn;
}

private List<FriendData> list;

public FriendsAdapter(Context context, int resource, List<FriendData> objects) {

    super(context, resource, objects);
    list = objects;
}

@Override
public View getView(final int position, View convertView, final ViewGroup parent) {

    final FriendData element = getItem(position);
    final FriendHolder viewHolder;
    if (convertView == null) {

        viewHolder = new FriendHolder();
        LayoutInflater inflater = LayoutInflater.from(getContext());
        convertView = inflater.inflate(R.layout.friend_layout, parent, false);
        viewHolder.friendIcon = (ImageView) convertView.findViewById(R.id.friendIcon);
        viewHolder.friendName = (TextView) convertView.findViewById(R.id.friendName);
        viewHolder.friendRank = (TextView) convertView.findViewById(R.id.friendRank);
        viewHolder.deleteFriendBtn = (ImageButton) convertView.findViewById(R.id.deleteFriendBtn);
        viewHolder.banFriendBtn = (ImageButton) convertView.findViewById(R.id.banFriendBtn);
        convertView.setTag(viewHolder);
    } else {
        viewHolder = (FriendHolder) convertView.getTag();
    }

    if (element.getPhoto() != null) {

        String photo = S3ImageHandler.SMALL_PROFILE_ICON_PREFIX + element.getPhoto();
        String url = String.format(S3ImageHandler.AMAZON_PROFILE_DOWNLOAD_LINK, photo);
        Glide.with(getContext())
                .load(url)
                .asBitmap()
                .diskCacheStrategy(DiskCacheStrategy.ALL)
                .placeholder(R.drawable.user_small)
                .into(new BitmapImageViewTarget(viewHolder.friendIcon) {
                    @Override
                    protected void setResource(Bitmap resource) {

                        RoundedBitmapDrawable circularBitmapDrawable = RoundedBitmapDrawableFactory.create(getContext().getResources(), resource);
                        circularBitmapDrawable.setCircular(true);
                        viewHolder.friendIcon.setImageDrawable(circularBitmapDrawable);
                    }
                });
    }

    viewHolder.friendName.setText(element.getId());
    viewHolder.friendRank.setText(String.format("%s %d", getContext().getString(R.string.text_rank), element.getRank()));
    viewHolder.deleteFriendBtn.setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View v) {
            confirmDelete(element, position, parent);
        }
    });
    viewHolder.banFriendBtn.setOnClickListener(new View.OnClickListener() {
        @Override
        public void onClick(View v) {
            confirmBan(element, position, parent);
        }
    });

    return convertView;
}

removing user from friend list:

        remove(element);
        notifyDataSetChanged();

does any of you see what i do wrong ? i would be very grateful for some help. thank you :)

TWiStErRob
  • 44,762
  • 26
  • 170
  • 254
Dominik
  • 87
  • 2
  • 11

1 Answers1

11

You're resuing list items (by definition of recycler view) which means that if an image was set to an item and you don't clear it, the image will remain there. So even though setText changes the labels viewHolder.friendIcon is not touched. The fix is really simple:

if (element.getPhoto() != null) {
    Glide.with(getContext())...
} else {
    Glide.clear(viewHolder.friendIcon); // tell Glide that it should forget this view
    viewHolder.friendIcon.setImageResource(R.drawable.user_small); // manually set "unknown" icon
}

Also remove the drawable from the xml, or at least change to tools:src which will help reducing the inflation time; the value is overwritten by Glide every time anyway.


To reduce complexity there's an alternative:

class FriendData {
    // if you can't modify this class you can also put it in a `static String getPhotoUrl(Element)` somewhere
    public void String getPhotoUrl() {
        if (this.getPhoto() == null) return null;
        String photo = S3ImageHandler.SMALL_PROFILE_ICON_PREFIX + this.getPhoto();
        String url = String.format(S3ImageHandler.AMAZON_PROFILE_DOWNLOAD_LINK, photo);
        return url;
    }
}

and then replace the whole if (element.getPhoto() != null) {...} with:

Glide.with(getContext())
     .load(element.getPhotoUrl()) // this may be null --\
     .asBitmap() //                                     |
     .diskCacheStrategy(DiskCacheStrategy.ALL) //       |
     .placeholder(R.drawable.user_small) //             |
     .fallback(R.drawable.user_small) // <--------------/
     .into(new BitmapImageViewTarget(viewHolder.friendIcon) { ... })
;

This will also result in proper behavior because even though there's no image url Glide will take care of setting something, see JavaDoc or source of fallback.

As a sidenote also consider using CircleCrop. Aside from caching benefits it would also support GIFs because you can remove the .asBitmap() and the custom target.

TWiStErRob
  • 44,762
  • 26
  • 170
  • 254
  • is there a way to combine both: .fallback and the Glide.clear() – Dominik Jan 31 '16 at 14:19
  • There's no point as I see it: fallback is set when load gets null and you would clear when load would get null; so they achieve really similar goals. (I don't know what happens if you set null fallback though.) What is it you want to achieve with fallback and clear together? – TWiStErRob Jan 31 '16 at 14:24
  • you suggested to remove the image:src from the layout xml and at the end you say to remove the whole if statement. Doing that, i need to use the `.fallback` method to set the default picture and there will be no place for the `Glide.clear` – Dominik Jan 31 '16 at 14:34
  • There's a line between those two different solutions achieving the same goal. You can remove `src` from XML in both cases, because it's either set manually or via fallback in those two solutions. When you use `fallback()` that `if-else-clear-set` is hidden inside Glide. See https://github.com/bumptech/glide/issues/268 for more info. – TWiStErRob Jan 31 '16 at 14:43
  • Thanks for `tools:src` and for `.placeholder` and `.fallback`. In my case at least a ListView with LinearLayot as items became showed. Before your solution my ImageViews showed and disappeared during scrolling. But now when loading images a short shake is visible. So, I will revert back to Picasso. – CoolMind Aug 08 '16 at 08:48
  • An advice http://stackoverflow.com/a/25345980/2914140 has helped: I set `hasStableIds` to true. – CoolMind Aug 08 '16 at 08:59
  • A good advice follows there: http://stackoverflow.com/a/25931558/2914140. You can check if an ImageView already has loaded with the same url and not reload it again to avoid flickering. – CoolMind Aug 08 '16 at 09:19
  • @CoolMind no need to check the same url for Glide; if the image is currently visible, it will hit the memory cache and won't reload, so there can't be flickering. I think if you have flickering there may be something else wrong there, not related to this Q&A. – TWiStErRob Aug 08 '16 at 09:56
  • @TWiStErRob, yes, when I scroll a ListView an animation occurs (a header becomes thiner or wider), that's a required behaviour. So, while an image is still loading (about 1 second) and several secods after a ListView was flickering. Also I didn't organize a cache for Glide or OkHttp (but use diskCacheStrategy(DiskCacheStrategy.ALL)). Anyway it helped. Also I added `.listener` for loading end event and `.error`. – CoolMind Aug 08 '16 at 12:59
  • 1
    @CoolMind I'm talking about memory cache, if you didn't disable it explicitly, then it's enabled. By this limited info it seems that the flickering is likely caused by either the listener which starts animation unnecessarily (check the boolean arguments), or `TransitionDrawable`, because Glide `.crossFade()`s by default. If you want to figure out with me why it happened exaclty, you can open an issue on Github. – TWiStErRob Aug 08 '16 at 13:17
  • @TWiStErRob, yes, a memory cache is not disabled. I use a regular linear animation in the top of a screen (`Animation a = new Animation() {...}`). When a user scrolls a ListView, this header (always visible) becomes thiner and the ListView becomes taller. Also the user can press over the header and make it bigger again. In these moments images may flicker, but after several seconds everything is normal. After applying a check of the same URL I don't see this problem, at least on my device. – CoolMind Aug 08 '16 at 14:49
  • @TWiStErRob, I see, you are a good specialist, so, if will meet any problems, will open an issue. Thanks. – CoolMind Aug 08 '16 at 14:51