Created
May 13, 2019 15:44
-
-
Save followthemoney1/c4ce177839cd256a31e302e9695116cf to your computer and use it in GitHub Desktop.
plaid many expand adapter
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
class DesignerNewsCommentsAdapter | |
extends RecyclerView.Adapter<RecyclerView.ViewHolder> { | |
private static final int TYPE_HEADER = 0; | |
private static final int TYPE_NO_COMMENTS = 1; | |
private static final int TYPE_COMMENT = 2; | |
private static final int TYPE_COMMENT_REPLY = 3; | |
private static final int TYPE_FOOTER = 4; | |
private View header; | |
private List<Comment> comments; | |
private View footer; | |
private int expandedCommentPosition = RecyclerView.NO_POSITION; | |
private boolean replyToCommentFocused = false; | |
DesignerNewsCommentsAdapter(@NonNull View header, | |
@NonNull List<Comment> comments, | |
@NonNull View footer) { | |
this.header = header; | |
this.comments = comments; | |
this.footer = footer; | |
} | |
public void updateList(List<Comment> comments) { | |
this.comments = comments; | |
notifyDataSetChanged(); | |
} | |
@Override | |
public int getItemViewType(int position) { | |
if (position == 0) return TYPE_HEADER; | |
if (isCommentReplyExpanded() && position == expandedCommentPosition + 1) { | |
return TYPE_COMMENT_REPLY; | |
} | |
int footerPosition = hasComments() ? 1 + comments.size() // header + comments | |
: 2; // header + no comments view | |
if (isCommentReplyExpanded()) footerPosition++; | |
if (position == footerPosition) return TYPE_FOOTER; | |
return hasComments() ? TYPE_COMMENT : TYPE_NO_COMMENTS; | |
} | |
@Override | |
public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { | |
switch (viewType) { | |
case TYPE_HEADER: | |
return new HeaderHolder(header); | |
case TYPE_COMMENT: | |
return createCommentHolder(parent); | |
case TYPE_COMMENT_REPLY: | |
return createCommentReplyHolder(parent); | |
case TYPE_NO_COMMENTS: | |
return new NoCommentsHolder( | |
getLayoutInflater().inflate( | |
R.layout.designer_news_no_comments, parent, false)); | |
case TYPE_FOOTER: | |
return new FooterHolder(footer); | |
} | |
return null; | |
} | |
@Override | |
public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) { | |
switch (getItemViewType(position)) { | |
case TYPE_COMMENT: | |
bindComment((CommentViewHolder) holder, null); | |
break; | |
case TYPE_COMMENT_REPLY: | |
((CommentReplyViewHolder) holder).bindCommentReply( | |
getComment(holder.getAdapterPosition() - 1)); | |
break; | |
} // nothing to bind for header / no comment / footer views | |
} | |
@Override | |
public void onBindViewHolder(RecyclerView.ViewHolder holder, | |
int position, | |
List<Object> partialChangePayloads) { | |
switch (getItemViewType(position)) { | |
case TYPE_COMMENT: | |
bindComment((CommentViewHolder) holder, partialChangePayloads); | |
break; | |
default: | |
onBindViewHolder(holder, position); | |
} | |
} | |
@Override | |
public int getItemCount() { | |
int itemCount = 2; // header + footer | |
if (hasComments()) { | |
itemCount += comments.size(); | |
} else { | |
itemCount++; // no comments view | |
} | |
if (isCommentReplyExpanded()) itemCount++; | |
return itemCount; | |
} | |
public void addComment(Comment newComment) { | |
if (!hasComments()) { | |
notifyItemRemoved(1); // remove the no comments view | |
} | |
comments.add(newComment); | |
notifyItemInserted(commentIndexToAdapterPosition(comments.size() - 1)); | |
} | |
/** | |
* Add a new comment and return the adapter position that it was inserted at. | |
*/ | |
public int addCommentReply(Comment newComment, int inReplyToAdapterPosition) { | |
// when replying to a comment, we want to insert it after any existing replies | |
// i.e. after any following comments with the same or greater depth | |
int commentIndex = adapterPositionToCommentIndex(inReplyToAdapterPosition); | |
do { | |
commentIndex++; | |
} while (commentIndex < comments.size() && | |
comments.get(commentIndex).getDepth() >= newComment.getDepth()); | |
comments.add(commentIndex, newComment); | |
int adapterPosition = commentIndexToAdapterPosition(commentIndex); | |
notifyItemInserted(adapterPosition); | |
return adapterPosition; | |
} | |
public boolean isReplyToCommentFocused() { | |
return replyToCommentFocused; | |
} | |
private boolean hasComments() { | |
return !comments.isEmpty(); | |
} | |
private boolean isCommentReplyExpanded() { | |
return expandedCommentPosition != RecyclerView.NO_POSITION; | |
} | |
private Comment getComment(int adapterPosition) { | |
return comments.get(adapterPositionToCommentIndex(adapterPosition)); | |
} | |
private int adapterPositionToCommentIndex(int adapterPosition) { | |
int index = adapterPosition - 1; // less header | |
if (isCommentReplyExpanded() | |
&& adapterPosition > expandedCommentPosition) { | |
index--; | |
} | |
return index; | |
} | |
private int commentIndexToAdapterPosition(int index) { | |
int adapterPosition = index + 1; // header | |
if (isCommentReplyExpanded()) { | |
int expandedCommentIndex = adapterPositionToCommentIndex(expandedCommentPosition); | |
if (index > expandedCommentIndex) adapterPosition++; | |
} | |
return adapterPosition; | |
} | |
@NonNull | |
private CommentViewHolder createCommentHolder(ViewGroup parent) { | |
final CommentViewHolder holder = new CommentViewHolder( | |
getLayoutInflater().inflate(R.layout.designer_news_comment, parent, false), | |
threadWidth, threadGap); | |
holder.itemView.setOnClickListener(v -> { | |
final boolean collapsingSelf = | |
expandedCommentPosition == holder.getAdapterPosition(); | |
collapseExpandedComment(); | |
if (collapsingSelf) return; | |
// show reply below this | |
expandedCommentPosition = holder.getAdapterPosition(); | |
notifyItemInserted(expandedCommentPosition + 1); | |
notifyItemChanged(expandedCommentPosition, | |
CommentAnimator.EXPAND_COMMENT); | |
}); | |
return holder; | |
} | |
private void collapseExpandedComment() { | |
if (!isCommentReplyExpanded()) return; | |
notifyItemChanged(expandedCommentPosition, | |
CommentAnimator.COLLAPSE_COMMENT); | |
notifyItemRemoved(expandedCommentPosition + 1); | |
replyToCommentFocused = false; | |
expandedCommentPosition = RecyclerView.NO_POSITION; | |
updateFabVisibility(); | |
} | |
private void bindComment(final CommentViewHolder holder, List<Object> partialChanges) { | |
// Check if this is a partial update for expanding/collapsing a comment. If it is we | |
// can do a partial bind as the bound data has not changed. | |
if (partialChanges == null || partialChanges.isEmpty() || | |
!(partialChanges.contains(CommentAnimator.COLLAPSE_COMMENT) | |
|| partialChanges.contains( | |
CommentAnimator.EXPAND_COMMENT))) { | |
final Comment comment = getComment(holder.getAdapterPosition()); | |
ColorStateList linksColor = ContextCompat.getColorStateList(getApplicationContext(), | |
R.color.designer_news_links); | |
int highlightColor = ContextCompat.getColor(getApplicationContext(), | |
io.plaidapp.R.color.designer_news_link_highlight); | |
CharSequence commentText = HtmlUtils.parseMarkdownAndPlainLinks( | |
comment.getBody(), | |
markdown, | |
linksColor, | |
highlightColor, | |
(src, loadingSpan) -> GlideApp.with(StoryActivity.this) | |
.asBitmap() | |
.load(src) | |
.diskCacheStrategy(DiskCacheStrategy.ALL) | |
.into(new ImageSpanTarget(holder.getComment(), loadingSpan))); | |
String author = comment.getUserDisplayName() != null ? comment.getUserDisplayName().toLowerCase() : ""; | |
boolean isOriginalPoster = isOP(comment.getUserId()); | |
String timeAgo = DateUtils.getRelativeTimeSpanString( | |
comment.getCreatedAt().getTime(), | |
System.currentTimeMillis(), | |
DateUtils.SECOND_IN_MILLIS) | |
.toString().toLowerCase(); | |
CommentUiModel commentUiModel = new CommentUiModel( | |
commentText, | |
timeAgo, | |
comment.getDepth(), | |
author, | |
isOriginalPoster | |
); | |
holder.bind(commentUiModel); | |
} | |
// set/clear expanded comment state | |
holder.itemView.setActivated(holder.getAdapterPosition() == expandedCommentPosition); | |
holder.setExpanded(holder.getAdapterPosition() == expandedCommentPosition); | |
} | |
private void replyToComment(Long commentId, String reply) { | |
// TODO move the result handling in the VM | |
viewModel.commentReplyRequested(reply, commentId, result -> { | |
if (result instanceof Result.Error) { | |
Toast.makeText(getApplicationContext(), | |
"Failed to post comment :(", Toast.LENGTH_SHORT).show(); | |
} | |
return Unit.INSTANCE; | |
}); | |
} | |
private void handleCommentVotesClick(CommentReplyViewHolder holder, | |
boolean isUserLoggedIn, | |
Comment comment) { | |
if (isUserLoggedIn) { | |
if (!holder.getCommentVotes().isActivated()) { | |
viewModel.commentUpvoteRequested(story.getId(), | |
result -> { | |
if (result instanceof Result.Success) { | |
comment.setUpvoted(true); | |
// TODO fix this | |
// comment.vote_count++; | |
holder.getCommentVotes().setText(String.valueOf(comment.getUpvotesCount())); | |
holder.getCommentVotes().setActivated(true); | |
} else { | |
Toast.makeText(StoryActivity.this, "Unable to upvote comment", | |
Toast.LENGTH_LONG) | |
.show(); | |
} | |
return Unit.INSTANCE; | |
}); | |
} else { | |
comment.setUpvoted(false); | |
// TODO fix this | |
// comment.setVoteCount(comment.getVoteCount() - 1); | |
holder.getCommentVotes().setText(String.valueOf(comment.getUpvotesCount())); | |
holder.getCommentVotes().setActivated(false); | |
// TODO actually delete upvote - florina: why? | |
} | |
} else { | |
needsLogin(holder.getCommentVotes(), 0); | |
} | |
holder.getCommentReply().clearFocus(); | |
} | |
@NonNull | |
private CommentReplyViewHolder createCommentReplyHolder(ViewGroup parent) { | |
final CommentReplyViewHolder holder = new CommentReplyViewHolder(getLayoutInflater() | |
.inflate(R.layout.designer_news_comment_actions, parent, false)); | |
holder.getCommentVotes().setOnClickListener(v -> { | |
Comment comment = getComment(holder.getAdapterPosition()); | |
handleCommentVotesClick(holder, loginRepository.isLoggedIn(), comment); | |
}); | |
holder.getPostReply().setOnClickListener(v -> { | |
if (loginRepository.isLoggedIn()) { | |
String reply = holder.getCommentReply().getText().toString(); | |
if (reply.isEmpty()) return; | |
final int inReplyToCommentPosition = holder.getAdapterPosition() - 1; | |
final Comment replyingTo = getComment(inReplyToCommentPosition); | |
collapseExpandedComment(); | |
// insert a locally created comment before actually | |
// hitting the API for immediate response | |
int replyDepth = replyingTo.getDepth() + 1; | |
LoggedInUser user = loginRepository.getUser(); | |
String commentBody = holder.getCommentReply().getText().toString(); | |
final int newReplyPosition = commentsAdapter.addCommentReply( | |
new Comment( | |
0, | |
replyingTo.getId(), | |
commentBody, | |
new Date(), | |
replyDepth, | |
0, | |
user.getId(), | |
user.getDisplayName(), | |
user.getPortraitUrl(), | |
false | |
), | |
inReplyToCommentPosition); | |
replyToComment(replyingTo.getId(), reply); | |
holder.getCommentReply().getText().clear(); | |
ImeUtils.hideIme(holder.getCommentReply()); | |
commentsList.scrollToPosition(newReplyPosition); | |
} else { | |
needsLogin(holder.getPostReply(), 0); | |
} | |
holder.getCommentReply().clearFocus(); | |
}); | |
holder.getCommentReply().setOnFocusChangeListener((v, hasFocus) -> { | |
replyToCommentFocused = hasFocus; | |
if (hasFocus) { | |
holder.createCommentReplyFocusAnimator().start(); | |
} else { | |
holder.createCommentReplyFocusLossAnimator().start(); | |
} | |
updateFabVisibility(); | |
holder.getPostReply().setActivated(hasFocus); | |
}); | |
return holder; | |
} | |
} | |
/* package */ static class HeaderHolder extends RecyclerView.ViewHolder { | |
public HeaderHolder(View itemView) { | |
super(itemView); | |
} | |
} | |
/* package */ static class NoCommentsHolder extends RecyclerView.ViewHolder { | |
public NoCommentsHolder(View itemView) { | |
super(itemView); | |
} | |
} | |
/* package */ static class FooterHolder extends RecyclerView.ViewHolder { | |
public FooterHolder(View itemView) { | |
super(itemView); | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment