J'ai un RecyclerView
qui montre deux types de View
s l'un représente une publication utilisateur et l'autre représente une publication événement. Les deux ont des éléments en commun, par exemple un TextView
qui montre un horodatage. J'ai donc créé un PublicationViewHolder
qui prend cet horodatage TextView
dans une variable et le charge. Mon problème est que l'adaptateur, initialement, charge les bonnes valeurs, mais lorsque je fais défiler vers le bas, et que je fais défiler vers le haut, les valeurs dans les positions sont modifiées par les valeurs d'une autre position. Voici le code:
public class PublicationViewHolder extends RecyclerView.ViewHolder {
private TextView vTimeStamp;
public PublicationViewHolder(View itemView) {
super(itemView);
this.vTimeStamp = (TextView) itemView.findViewById(R.id.txt_view_publication_timestamp);
}
public void load(Publication publication, int i) {
load(publication);
try {
if (Publication.TYPE_USER_PUBLICATION == publication.getType()) {
load((UserPublication) publication);
} else if (Publication.TYPE_EVENT_PUBLICATION == publication.getType()) {
load((EventPublication) publication);
}
} catch (ClassCastException e) {
throw new RuntimeException("Publication type cast fail. See PublicationViewHolder.");
}
}
public void load(Publication publication) {
vTimeStamp.setText(DateFormatter.getTimeAgo(publication.getTimeStamp()));
}
public void load( UserPublication publication) {
//This method is override by UserPublicationViewHolder
};
public void load( EventPublication publication) {
//This method is override by EventPublicationViewHolder
};
}
Je vais maintenant créer mes UserPublicationViewHolder
pour les publications des utilisateurs uniquement.
public class UserPublicationViewHolder extends PublicationViewHolder {
private ImageView vImageView, vLikeButton, vDislikeButton, vFavButton, vEditPost, vDeletePost;
private TextView vText, vUsername, vLikeCount, vDislikeCount, vFavCount;
private PostImagesLayout vImagesContainer;
private TagCloudLocationFriends tagsView;
public UserPublicationViewHolder(View itemView) {
super(itemView);
vImageView = (ImageView) itemView.findViewById(R.id.img_view_publication_user);
vText = (TextView) itemView.findViewById(R.id.txt_view_publication_text);
vLikeCount = (TextView) itemView.findViewById(R.id.txt_view_like_count);
vFavCount = (TextView) itemView.findViewById(R.id.txt_view_fav_count);
vDislikeCount = (TextView) itemView.findViewById(R.id.txt_view_dislike_count);
vUsername = (TextView) itemView.findViewById(R.id.txt_view_publication_user_name);
vLikeButton = (ImageView) itemView.findViewById(R.id.img_view_like);
vDislikeButton = (ImageView) itemView.findViewById(R.id.img_view_dislike);
vFavButton = (ImageView) itemView.findViewById(R.id.img_view_fav);
vImagesContainer = (PostImagesLayout) itemView.findViewById(R.id.container_post_images);
tagsView = (TagCloudLocationFriends) itemView.findViewById(R.id.location_friends_tag);
// edit - remove icons
vDeletePost = (ImageView) itemView.findViewById(R.id.img_view_delete_post);
vEditPost = (ImageView) itemView.findViewById(R.id.img_view_edit_post);
}
@Override
public void load(UserPublication publication) {
//Load the UserPublicationViewHolder specific views.
}
}
Maintenant je vais faire de même mais pour les publications de l'événement
public class EventPublicationViewHolder extends PublicationViewHolder {
private TextView vTextViewTitle;
private TextView vTextViewText;
public EventPublicationViewHolder(View itemView) {
super(itemView);
vTextViewTitle = (TextView) itemView.findViewById(R.id.txt_view_publication_event_title);
vTextViewText = (TextView) itemView.findViewById(R.id.txt_view_publication_event_text);
}
@Override
public void load(EventPublication publication) {
//Load the EventPublicationViewHolder specifics views
}
}
Voici maintenant mon adaptateur RecyclerView:
public class PublicationAdapter extends RecyclerView.Adapter<PublicationViewHolder> {
public static final int USER_PUBLICATION_TYPE = 1;
public static final int EVENT_PUBLICATION_TYPE = 2;
private List<Publication> publications = new ArrayList<Publication>();
public List<Publication> getPublications() {
return publications;
}
public void setPublications(List<Publication> publications) {
this.publications = publications;
}
@Override
public int getItemViewType(int position) {
if (publications.get(position) instanceof UserPublication) {
return USER_PUBLICATION_TYPE;
}
if (publications.get(position) instanceof EventPublication) {
return EVENT_PUBLICATION_TYPE;
}
throw new RuntimeException("Unknown view type in PublicationAdapter");
}
@Override
public PublicationViewHolder onCreateViewHolder(ViewGroup viewGroup, int type) {
View v;
switch (type) {
case USER_PUBLICATION_TYPE:
v = LayoutInflater.from(getActivity()).inflate(R.layout.view_holder_user_publication, viewGroup, false);
return new UserPublicationViewHolder(v);
case EVENT_PUBLICATION_TYPE:
v = LayoutInflater.from(getActivity()).inflate(R.layout.view_holder_event_publication, viewGroup, false);
return new EventPublicationViewHolder(v);
}
return null;
}
@Override
public void onBindViewHolder(PublicationViewHolder aPublicationHolder, int i) {
aPublicationHolder.load(publications.get(i), i);
}
@Override
public long getItemId(int position) {
//Here I tried returning only position or 0 without luck.
//The id is unique BTW
return publications.get(position).getId();
}
@Override
public int getItemCount() {
return publications.size();
}
}
Je ne sais pas ce qui peut être faux, UserPublication et EventPublication s'étendent tous les deux de la publication. Je ne fais pas de demande ou ne recharge pas l'adaptateur. Je ne charge l'adaptateur qu'une seule fois.
Mise à jour:
BTW J'utilise ce RecyclerView à l'intérieur d'un fragment qui est chargé dans un PageAdapter qui est chargé dans un ViewPager qui est à l'intérieur d'un fragment, c'est peut-être le problème?
pdate: Ceci est l'autre code de liaison.
Il s'agit de la méthode de chargement de UserPublicationViewHolder
.
@Override
public void load(UserPublication publication) {
PicassoHelper.publicationUser(getActivity(), publication.getUser().getAvatarUrl(),
vImageView);
vText.setText(publication.getText());
vUsername.setText(publication.getUser().getName());
boolean hasLocation = false;
if (publication.getImages().length > 0) {
vImagesContainer.setImages(publication.getImages());
} else {
vImagesContainer.setVisibility(View.GONE);
}
tagsView.setTags(new ArrayList<MinikastTag>());
tagsView.drawTags();
if(publication.getLocation() != null || publication.getTaggedFriends().size() > 0){
if(publication.getLocation() != null){
hasLocation = true;
tagsView.add(new MinikastTag(1,"Post from ",1));
tagsView.add(new MinikastTag(2, publication.getLocation().getName(), 2));
}
if(publication.getTaggedFriends().size() > 0){
if(hasLocation)
tagsView.add(new MinikastTag(3," with ",1));
else
tagsView.add(new MinikastTag(3,"With ",1));
int i = 0;
for(User aUser: publication.getTaggedFriends()){
MinikastTag aTag;
if(i == publication.getTaggedFriends().size() - 1 ) {
aTag = new MinikastTag(4, aUser.getName(), 3);
aTag.setUserID(aUser.getId());
aTag.setUserName(aUser.getName());
tagsView.add(aTag);
} else {
aTag = new MinikastTag(4, aUser.getName() + ", ", 3);
aTag.setUserID(aUser.getId());
aTag.setUserName(aUser.getName());
tagsView.add(aTag);
}
i = i+1;
}
}
}
tagsView.drawTags();
// likes, dislikes, favs
if(publication.getLikesAmount() > 0)
vLikeCount.setText(String.valueOf(publication.getLikesAmount()));
if(publication.getDislikesAmount() > 0)
vDislikeCount.setText(String.valueOf(publication.getDislikesAmount()));
if(publication.getLovesAmount() > 0)
vFavCount.setText(String.valueOf(publication.getLovesAmount()));
// reset buttons
vFavButton.setPressed(false);
vDislikeButton.setPressed(false);
vLikeButton.setPressed(false);
if(publication.getRelationship().equals("LOVE"))
vFavButton.setPressed(true);
else if (publication.getRelationship().equals("LIKE"))
vLikeButton.setPressed(true);
else if (publication.getRelationship().equals("DISLIKE"))
vDislikeButton.setPressed(true);
// edit - remove icons
if(String.valueOf(publication.getUser().getId()).equals(StartupSharedPreferences.getProfileId())){
vEditPost.setVisibility(View.VISIBLE);
vDeletePost.setVisibility(View.VISIBLE);
}else{
vEditPost.setVisibility(View.INVISIBLE);
vDeletePost.setVisibility(View.INVISIBLE);
}
}
}
Et voici la méthode de chargement de EventPublicationViewHolder:
@Override
public void load(EventPublication publication) {
vTimeStamp.setVisibility(View.GONE);
itemView.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
//GoTo.eventDetail(getActivity(), publication);
}
});
vTextViewTitle.setText(publication.getTitle());
vTextViewText.setText(publication.getText());
}
J'ai commenté du code juste parce que je testais, mais comme vous pouvez le voir, je ne fais que setTexts et assing quelques images.
Et voici comment j'ai défini l'adaptateur, LinearLayoutManager, etc. dans la méthode onViewCreated du fragment.
vRecyclerView = (FixedRecyclerView) view.findViewById(R.id.recycler_view_publications);
vSwipeRefresh = (SwipeRefreshLayout) view.findViewById(R.id.swipe_container);
mFeedCallback.onScrollReady(vRecyclerView);
mLayoutManager = buildLayoutManager();
vRecyclerView.setLayoutManager(mLayoutManager);
vRecyclerView.addItemDecoration(new DividerItemDecoration(getActivity(), DividerItemDecoration.VERTICAL_LIST));
mAdapter = new PublicationAdapter();
vSwipeRefresh.setOnRefreshListener(this);
vSwipeRefresh.setColorSchemeResources(R.color._SWIPER_COLOR_1, R.color._SWIPER_COLOR_2,
R.color._SWIPER_COLOR_3, R.color._SWIPER_COLOR_4);
vRecyclerView.setAdapter(mAdapter);
BTW L'adaptateur est chargé avec l'ensemble de données dans une méthode personnalisée que j'ai, appelée onHttpClientReady, mais cela ne semble pas être le problème.
Voici quelques captures d'écran:
En haut de la liste lorsque j'entre dans l'application pour la première fois:
Puis quand je reviens:
BTW les boutons similaires, n'aiment pas et favoris, si quelqu'un a cliqué dessus plusieurs fois, affichera une valeur numérique, ces valeurs sont également mal placées si elles le sont.
MISE À JOUR: Maintenant, je sais que ce n'était pas parce que les fragments imbriqués. J'ai changé mon code de la façon dont, maintenant, chaque fragment d'onglet se trouve dans le PageStateAdapter qui est à l'intérieur du ViewPager qui est à l'intérieur d'une activité. Mais le problème est toujours là.
PDATE: J'ai trouvé que la méthode getItemId n'est jamais en cours d'exécution, IDK pourquoi encore.
Je suggère de revoir la hiérarchie et l'utilisation de votre classe. En général, si vous effectuez une opération de type type == type
Dans une classe de base, vous ne respectez pas le but de l'abstraction et de l'héritage. Quelque chose comme ça fonctionnerait pour vous:
public abstract class PublicationViewHolder extends RecyclerView.ViewHolder {
private TextView mTimeStamp;
public PublicationViewHolder(View itemView) {
mTimeStamp = (TextView)itemView.findViewById(R.id. txt_view_publication_timestamp);
}
public void bindViews(Publication publication) {
mTimeStamp.setText(DateFormatter.getTimeAgo(publication.getTimeStamp()));
}
}
Maintenant, votre "événement" ou "publications utilisateur" dérivent simplement de cette classe et implémentent le constructeur et la méthode bindViews()
. Assurez-vous d'appeler la superclasse dans les deux cas. Assurez-vous également de définir chaque vue dans la mise en page de la publication spécifique dans vos méthodes bindViews()
.
Dans votre adaptateur, il vous suffit de créer le bon support en fonction du type de publication à cette position dans votre ensemble de données:
public class PublicationAdapter extends RecyclerView.Adapter {
private ArrayList<Publication> mPubs;
// Your other code here, like
// swapPublications(), getItemCount(), etc.
...
public int getItemViewType(int position) {
return mPubs.get(position).getType();
}
public PublicationViewHolder createViewHolder(ViewGroup parent, int type) {
PublicationViewHolder ret;
View root;
LayoutInflater inflater = LayoutInflater.from(parent.getContext());
if (type == USER_PUBLICATION_TYPE) {
root =
inflater.inflate(R.layout.view_holder_user_publication,
parent,
false);
ret = new UserPubHolder(root);
} else {
root =
inflater.inflate(R.layout.view_holder_event_publication,
parent,
false);
ret = new EventPubHolder(root);
}
return ret;
}
public bindViewHolder(PublicationViewHolder holder, int position) {
holder.bindViews(mPubs.get(position));
}
}
Cela se produit généralement lorsque vous avez quelque chose comme "if (field! = Null) holder.setField (field)", sans autre chose. Le support est recyclé, cela signifie qu'il y aura des valeurs, vous devez donc nettoyer ou remplacer CHAQUE valeur, si elle est nulle, vous devez annuler, sinon, vous devez l'écrire, TOUJOURS. Il est tard, mais comme réponse pour les autres.
pour moi, la configuration de setHasStableIds(false)
a résolu le problème.
A eu le même problème avec les images chargées asynchrones, qui avaient différentes hauteurs . Ainsi, avec le débogueur, vous pouvez voir que les positions de recyclage dépendent de la taille réelle des vues.
La solution la plus simple pour moi a été de spécifier différentes tailles, afin que le système connaisse la taille exacte de tous les articles. https://developer.Android.com/reference/Android/support/v7/widget/RecyclerView.Adapter.html#getItemViewType (int)
Par exemple paysage, portrait et carré.
J'ai donc créé des vues séparées et les ai utilisées comme: (simplifié)
public class YourAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> {
// ...
public static class ViewHolderLandscape extends RecyclerView.ViewHolder { ... }
public static class ViewHolderPortrait extends RecyclerView.ViewHolder { ... }
public static class ViewHolderSquare extends RecyclerView.ViewHolder { ... }
@Override
public int getItemViewType(int position) {
return mDataset.get(position).getImageType();
}
@Override
public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
int mLayoutId = 0;
switch (viewType) {
case 0:
mLayoutId = R.layout.list_item_landscape;
break;
case 1:
mLayoutId = R.layout.list_item_portrait;
break;
case 2:
mLayoutId = R.layout.list_item_square;
break;
}
View v = LayoutInflater.from(parent.getContext()).inflate(mLayoutId, parent, false);
ButterKnife.inject(this, v);
return new ViewHolder(v);
}
}
Enfin, RecycleView ne se trompe pas sur les tailles d'élément différentes/dynamiques.
La seule grande variable de votre code de liaison est le format de votre date: DateFormatter.getTimeAgo(publication.getTimeStamp())
Sans voir directement cette classe, il est difficile de le dire avec certitude, mais il semble que si l'horodatage est immuable, mais que le formateur est basé sur l'heure actuelle, cela serait cohérent avec le changement de texte lorsque la vue est rebondie.
Je pense qu'un problème plus important (et un peu à part) est la lisibilité du code, ce qui rend difficile de repérer facilement le problème visuellement. Le modèle d'héritage et les surcharges ici rendent difficile de raisonner sur le code et de décider quel chemin est pris et s'il fait la bonne chose. Voici du code de serviette (ne l'avez pas construit ou exécuté) en utilisant une approche plus compositionnelle qui pourrait être une organisation plus claire et faciliter le débogage des problèmes:
Nouvelle classe d'assistance pour le code de détenteur de vue commune, remplace PublicationViewHolder
:
public class PublicationViewHolderHelper {
private final TextView vTimeStamp;
public PublicationViewHolder(View itemView) {
super(itemView);
this.vTimeStamp = (TextView) itemView.findViewById(R.id.txt_view_publication_timestamp);
}
/** Binds view data common to publication types. */
public void load(Publication publication) {
vTimeStamp.setText(DateFormatter.getTimeAgo(publication.getTimeStamp()));
}
}
EventPublicationViewHolder
à titre d'exemple (faites la même chose pour UserPublicationViewHolder
):
public class EventPublicationViewHolder extends ViewHolder {
private final PublicationViewHolderHelper helper;
// View fields...
public EventPublicationViewHolder(View itemView) {
super(itemView);
helper = new PublicationViewHolderHelper(itemView);
// Populated view fields...
}
@Override
public void load(EventPublication publication) {
helper.load(publication);
//Load the EventPublicationViewHolder specifics views
}
}
Notez qu'il n'y a plus de classe de base dans votre adaptateur et qu'il n'est pas nécessaire de vérifier le type, donc il y a beaucoup moins de code.
Maintenant, l'adaptateur reste le même à l'exception du type générique et onBindViewHolder
:
public class PublicationAdapter extends RecyclerView.Adapter<ViewHolder> {
...
@Override
public void onBindViewHolder(ViewHolder viewHolder, int position) {
final Publication publication = publications.get(position);
final int viewType = getItemViewType(position);
switch (viewType) {
case USER_PUBLICATION_TYPE:
((UserPublicationViewHolder) viewHolder).load((UserPublication) publication);
break;
case EVENT_PUBLICATION_TYPE:
((EventPublicationViewHolder) viewHolder).load((EventPublication) publication);
break;
default:
// Blow up in whatever way you choose.
}
}
...
}
Notez qu'il maintient un modèle très similaire à votre onCreateViewHolder
, donc il y a non seulement moins de code global mais aussi plus de cohérence interne. Ce n'est certainement pas la seule façon de le faire, juste une suggestion basée sur votre cas d'utilisation particulier.