RecyclerView元素更新+异步网络调用
RecyclerView element update + async network call
我有一个按预期工作的 recyclerview。我在布局中有一个按钮可以填充列表。该按钮应该进行异步调用,结果,我更改了按钮的外观。这一切都很好。
但是当我单击按钮并快速向下滚动列表时,异步调用的结果会更新新视图的按钮(代替旧视图的视图)。我该如何处理?我可以处理特定视图何时被重用吗?
更新:
执行异步调用和 ui 更新的适配器 class 的代码片段。
@Override
public void onBindViewHolder(CommentsViewHolder holder, int position) {
try {
Comments comment = comments.get(position);
holder.bindView(comment,position);
}
catch(Exception ex){ex.printStackTrace();}
}
@Override
public int getItemCount() {
if(comments==null)
{return 0;}
return comments.size();
//return comments.length();
}
public class CommentsViewHolder extends RecyclerView.ViewHolder {
TextView score ;
TextView commentText;
TextView commentTime;
TextView avatarId;
ImageButton minusOne;
ImageButton plusOne;
ParseObject model;
public CommentsViewHolder(View itemView) {
super(itemView);
//itemView.setBackgroundColor(Color.DKGRAY);
minusOne =(ImageButton)itemView.findViewById(R.id.decScore);
plusOne =(ImageButton)itemView.findViewById(R.id.incScore);
commentText = (TextView)itemView.findViewById(R.id.comment);
score = (TextView)itemView.findViewById(R.id.commentScore);
commentTime =(TextView)itemView.findViewById(R.id.commentTime);
avatarId = (TextView)itemView.findViewById(R.id.ivUserAvatar);
}
public void bindView(Comments comment, int position) {
commentText.setText(comment.getCommentText());
score.setText(Integer.toString(comment.getScore()));
String timeText = DateUtils.getRelativeTimeSpanString( comment.getCreatedAt().getTime(), System.currentTimeMillis(), DateUtils.SECOND_IN_MILLIS).toString();
timeText = timeText.replace("hours","hrs");
timeText = timeText.replace("seconds","secs");
timeText = timeText.replace("minutes","mins");
commentTime.setText(timeText);
int commentHandler = comment.getCommenterHandle();
String commenterNumber = "";
if(commentHandler==0)
{
commenterNumber = "OP";
}
else{
commenterNumber = "#"+commentHandler;
}
avatarId.setText( commenterNumber);
model = comment;
String choice = "none";
minusOne.setEnabled(true);
plusOne.setEnabled(true);
minusOne.setVisibility(View.VISIBLE);
plusOne.setVisibility(View.VISIBLE);
for (ParseObject choiceIter : choices) {
if ((choiceIter.getParseObject("comment").getObjectId()).equals(comment.getObjectId())) {
choice = choiceIter.getString("userChoice");
break;
}
}
Log.i("debug",comment.getCommentText()+" "+comment.getScore()+" "+choice);
switch (choice) {
case "plusOne":
Log.i("darkplus","setting darkplus");
plusOne.setImageResource(R.drawable.ic_add_circle_black_18dp);
plusOne.setOnClickListener(reversePlusOneOnClickListener);
//minusOne.setOnClickListener(minusOneOnClickListener);
minusOne.setVisibility(View.GONE);
break;
case "minusOne":
Log.i("darkminus","setting darkminus");
minusOne.setImageResource(R.drawable.ic_remove_circle_black_18dp);
minusOne.setOnClickListener(reverseMinusOneOnClickListener);
//plusOne.setOnClickListener(plusOneOnClickListener);
plusOne.setVisibility(View.GONE);
break;
case "none":
Log.i("darkregular","setting regular");
minusOne.setImageResource(R.drawable.ic_remove_black_18dp);
plusOne.setImageResource(R.drawable.ic_add_black_18dp);
plusOne.setOnClickListener(plusOneOnClickListener);
minusOne.setOnClickListener(minusOneOnClickListener);
break;
}
}
View.OnClickListener reversePlusOneOnClickListener = new View.OnClickListener() {
@Override
public void onClick(View v) {
if (!FourUtils.isConnected(v.getContext())) {
return;
}
minusOne.setEnabled(false);
plusOne.setEnabled(false);
model.increment("plusOne", -1);
model.increment("score", -1);
model.saveEventually(new SaveCallback() {
@Override
public void done(ParseException e) {
if (e == null) {
ParseQuery<ParseObject> query = ParseQuery.getQuery("CommentChoice");
query.whereEqualTo("user", ParseUser.getCurrentUser());
query.whereEqualTo("comment", model);
query.fromPin(Four.COMMENT_CHOICE);
query.getFirstInBackground(new GetCallback<ParseObject>() {
@Override
public void done(ParseObject parseObject, ParseException e) {
if (e == null) {
if (parseObject == null) {
parseObject = ParseObject.create("CommentChoice");
parseObject.put("comment", model);
parseObject.put("user", ParseUser.getCurrentUser());
}
parseObject.put("userChoice", "none");
parseObject.pinInBackground(Four.COMMENT_CHOICE, new SaveCallback() {
@Override
public void done(ParseException e) {
if (e == null) {
score.setText(Integer.toString(model.getInt("score")));
//votes.setText((model.getInt("minusOne") + model.getInt("plusOne")) + " votes");
minusOne.setVisibility(View.VISIBLE);
plusOne.setImageResource(R.drawable.ic_add_black_18dp);
plusOne.setOnClickListener(plusOneOnClickListener);
minusOne.setEnabled(true);
plusOne.setEnabled(true);
// minusOne.setOnClickListener(minusOneOnClickListener);
BusProvider.getInstance().post(new NewCommentChoicesAdded());
} else {
e.printStackTrace();
}
}
});
}
else{e.printStackTrace();}
}
});
} else {
e.printStackTrace();
Log.i("plus1 error", e.getMessage());
}
}
});
}
};
如果没有代码可供查看,人们将很难(如果不是不可能的话)提供准确的答案。但是,根据此描述,听起来好像您的异步网络加载(使用 AsyncTask
或自定义 Loader
?)并未专门绑定到您的适配器正在跟踪的元素。由于 RecyclerView
显示的 child View
objects 是 re-used 以提高效率,因此您需要某种方式将两者联系在一起。这也意味着如果 View
被重用并且有一个活跃的异步操作与之相关联,则需要取消该异步操作。否则你会看到你现在看到的:错误的 child View
正在更新为旧异步调用的内容。
来自 Google 的 Chet Haase 在 this DevBytes video 中讨论了您的确切问题。
简而言之,需要通知框架其中一个View处于"transient"状态。一旦收到通知,框架将不会回收此视图,直到其 "transient" 标志被清除。
在您的情况下,在执行异步操作之前,在异步操作完成时应该更改的子视图上调用 setHasTransientState(true)
。在您明确调用 setHasTransientState(false)
之前,此视图不会被回收。
题外话:
看起来您可能正在从后台线程操纵 UI 元素。不要那样做!如果您可以引用 Activity
,那么请改用它的 runOnUiThread(Runnable action)
API。如果很难获得对 Activity
的引用,您可以获取 UI 线程的 Handler
并使用其 post(Runnable action)
API.
完成异步代码后,您应该更新数据,而不是视图。更新数据后,告诉适配器数据改变了。 RecyclerView 会注意到这一点并重新呈现您的视图。
使用回收视图(ListView 或 RecyclerView)时,您无法知道视图代表的是什么项目。在您的情况下,该视图在异步工作完成之前被回收并分配给您的数据的不同项目。
所以永远不要修改视图。始终修改数据并通知适配器。 bindView 应该是你处理这些情况的地方。
我有一个按预期工作的 recyclerview。我在布局中有一个按钮可以填充列表。该按钮应该进行异步调用,结果,我更改了按钮的外观。这一切都很好。
但是当我单击按钮并快速向下滚动列表时,异步调用的结果会更新新视图的按钮(代替旧视图的视图)。我该如何处理?我可以处理特定视图何时被重用吗?
更新:
执行异步调用和 ui 更新的适配器 class 的代码片段。
@Override
public void onBindViewHolder(CommentsViewHolder holder, int position) {
try {
Comments comment = comments.get(position);
holder.bindView(comment,position);
}
catch(Exception ex){ex.printStackTrace();}
}
@Override
public int getItemCount() {
if(comments==null)
{return 0;}
return comments.size();
//return comments.length();
}
public class CommentsViewHolder extends RecyclerView.ViewHolder {
TextView score ;
TextView commentText;
TextView commentTime;
TextView avatarId;
ImageButton minusOne;
ImageButton plusOne;
ParseObject model;
public CommentsViewHolder(View itemView) {
super(itemView);
//itemView.setBackgroundColor(Color.DKGRAY);
minusOne =(ImageButton)itemView.findViewById(R.id.decScore);
plusOne =(ImageButton)itemView.findViewById(R.id.incScore);
commentText = (TextView)itemView.findViewById(R.id.comment);
score = (TextView)itemView.findViewById(R.id.commentScore);
commentTime =(TextView)itemView.findViewById(R.id.commentTime);
avatarId = (TextView)itemView.findViewById(R.id.ivUserAvatar);
}
public void bindView(Comments comment, int position) {
commentText.setText(comment.getCommentText());
score.setText(Integer.toString(comment.getScore()));
String timeText = DateUtils.getRelativeTimeSpanString( comment.getCreatedAt().getTime(), System.currentTimeMillis(), DateUtils.SECOND_IN_MILLIS).toString();
timeText = timeText.replace("hours","hrs");
timeText = timeText.replace("seconds","secs");
timeText = timeText.replace("minutes","mins");
commentTime.setText(timeText);
int commentHandler = comment.getCommenterHandle();
String commenterNumber = "";
if(commentHandler==0)
{
commenterNumber = "OP";
}
else{
commenterNumber = "#"+commentHandler;
}
avatarId.setText( commenterNumber);
model = comment;
String choice = "none";
minusOne.setEnabled(true);
plusOne.setEnabled(true);
minusOne.setVisibility(View.VISIBLE);
plusOne.setVisibility(View.VISIBLE);
for (ParseObject choiceIter : choices) {
if ((choiceIter.getParseObject("comment").getObjectId()).equals(comment.getObjectId())) {
choice = choiceIter.getString("userChoice");
break;
}
}
Log.i("debug",comment.getCommentText()+" "+comment.getScore()+" "+choice);
switch (choice) {
case "plusOne":
Log.i("darkplus","setting darkplus");
plusOne.setImageResource(R.drawable.ic_add_circle_black_18dp);
plusOne.setOnClickListener(reversePlusOneOnClickListener);
//minusOne.setOnClickListener(minusOneOnClickListener);
minusOne.setVisibility(View.GONE);
break;
case "minusOne":
Log.i("darkminus","setting darkminus");
minusOne.setImageResource(R.drawable.ic_remove_circle_black_18dp);
minusOne.setOnClickListener(reverseMinusOneOnClickListener);
//plusOne.setOnClickListener(plusOneOnClickListener);
plusOne.setVisibility(View.GONE);
break;
case "none":
Log.i("darkregular","setting regular");
minusOne.setImageResource(R.drawable.ic_remove_black_18dp);
plusOne.setImageResource(R.drawable.ic_add_black_18dp);
plusOne.setOnClickListener(plusOneOnClickListener);
minusOne.setOnClickListener(minusOneOnClickListener);
break;
}
}
View.OnClickListener reversePlusOneOnClickListener = new View.OnClickListener() {
@Override
public void onClick(View v) {
if (!FourUtils.isConnected(v.getContext())) {
return;
}
minusOne.setEnabled(false);
plusOne.setEnabled(false);
model.increment("plusOne", -1);
model.increment("score", -1);
model.saveEventually(new SaveCallback() {
@Override
public void done(ParseException e) {
if (e == null) {
ParseQuery<ParseObject> query = ParseQuery.getQuery("CommentChoice");
query.whereEqualTo("user", ParseUser.getCurrentUser());
query.whereEqualTo("comment", model);
query.fromPin(Four.COMMENT_CHOICE);
query.getFirstInBackground(new GetCallback<ParseObject>() {
@Override
public void done(ParseObject parseObject, ParseException e) {
if (e == null) {
if (parseObject == null) {
parseObject = ParseObject.create("CommentChoice");
parseObject.put("comment", model);
parseObject.put("user", ParseUser.getCurrentUser());
}
parseObject.put("userChoice", "none");
parseObject.pinInBackground(Four.COMMENT_CHOICE, new SaveCallback() {
@Override
public void done(ParseException e) {
if (e == null) {
score.setText(Integer.toString(model.getInt("score")));
//votes.setText((model.getInt("minusOne") + model.getInt("plusOne")) + " votes");
minusOne.setVisibility(View.VISIBLE);
plusOne.setImageResource(R.drawable.ic_add_black_18dp);
plusOne.setOnClickListener(plusOneOnClickListener);
minusOne.setEnabled(true);
plusOne.setEnabled(true);
// minusOne.setOnClickListener(minusOneOnClickListener);
BusProvider.getInstance().post(new NewCommentChoicesAdded());
} else {
e.printStackTrace();
}
}
});
}
else{e.printStackTrace();}
}
});
} else {
e.printStackTrace();
Log.i("plus1 error", e.getMessage());
}
}
});
}
};
如果没有代码可供查看,人们将很难(如果不是不可能的话)提供准确的答案。但是,根据此描述,听起来好像您的异步网络加载(使用 AsyncTask
或自定义 Loader
?)并未专门绑定到您的适配器正在跟踪的元素。由于 RecyclerView
显示的 child View
objects 是 re-used 以提高效率,因此您需要某种方式将两者联系在一起。这也意味着如果 View
被重用并且有一个活跃的异步操作与之相关联,则需要取消该异步操作。否则你会看到你现在看到的:错误的 child View
正在更新为旧异步调用的内容。
来自 Google 的 Chet Haase 在 this DevBytes video 中讨论了您的确切问题。
简而言之,需要通知框架其中一个View处于"transient"状态。一旦收到通知,框架将不会回收此视图,直到其 "transient" 标志被清除。
在您的情况下,在执行异步操作之前,在异步操作完成时应该更改的子视图上调用 setHasTransientState(true)
。在您明确调用 setHasTransientState(false)
之前,此视图不会被回收。
题外话:
看起来您可能正在从后台线程操纵 UI 元素。不要那样做!如果您可以引用 Activity
,那么请改用它的 runOnUiThread(Runnable action)
API。如果很难获得对 Activity
的引用,您可以获取 UI 线程的 Handler
并使用其 post(Runnable action)
API.
完成异步代码后,您应该更新数据,而不是视图。更新数据后,告诉适配器数据改变了。 RecyclerView 会注意到这一点并重新呈现您的视图。
使用回收视图(ListView 或 RecyclerView)时,您无法知道视图代表的是什么项目。在您的情况下,该视图在异步工作完成之前被回收并分配给您的数据的不同项目。
所以永远不要修改视图。始终修改数据并通知适配器。 bindView 应该是你处理这些情况的地方。