/*
* Copyright 2015 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.plaidapp.ui;
import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.animation.AnimatorSet;
import android.animation.ObjectAnimator;
import android.annotation.TargetApi;
import android.app.Activity;
import android.app.ActivityOptions;
import android.app.PendingIntent;
import android.app.SharedElementCallback;
import android.app.assist.AssistContent;
import android.content.Context;
import android.content.Intent;
import android.graphics.Path;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.customtabs.CustomTabsIntent;
import android.support.customtabs.CustomTabsSession;
import android.support.v4.app.ShareCompat;
import android.support.v4.content.ContextCompat;
import android.support.v7.widget.LinearLayoutManager;
import android.support.v7.widget.RecyclerView;
import android.text.SpannableString;
import android.text.Spanned;
import android.text.TextUtils;
import android.text.format.DateUtils;
import android.text.style.TextAppearanceSpan;
import android.transition.ArcMotion;
import android.transition.Transition;
import android.util.TypedValue;
import android.view.View;
import android.view.ViewAnimationUtils;
import android.view.ViewGroup;
import android.view.animation.AnimationUtils;
import android.widget.Button;
import android.widget.EditText;
import android.widget.ImageButton;
import android.widget.ImageView;
import android.widget.TextView;
import android.widget.Toast;
import android.widget.Toolbar;
import com.bumptech.glide.Glide;
import com.bumptech.glide.load.engine.DiskCacheStrategy;
import java.text.NumberFormat;
import java.util.ArrayList;
import java.util.List;
import butterknife.Bind;
import butterknife.BindDimen;
import butterknife.BindInt;
import butterknife.ButterKnife;
import in.uncod.android.bypass.Bypass;
import in.uncod.android.bypass.style.ImageLoadingSpan;
import io.plaidapp.BuildConfig;
import io.plaidapp.R;
import io.plaidapp.data.api.ClientAuthInterceptor;
import io.plaidapp.data.api.designernews.DesignerNewsService;
import io.plaidapp.data.api.designernews.UpvoteStoryService;
import io.plaidapp.data.api.designernews.model.Comment;
import io.plaidapp.data.api.designernews.model.Story;
import io.plaidapp.data.api.designernews.model.StoryResponse;
import io.plaidapp.data.prefs.DesignerNewsPrefs;
import io.plaidapp.ui.drawable.ThreadedCommentDrawable;
import io.plaidapp.ui.transitions.FabDialogMorphSetup;
import io.plaidapp.ui.widget.AuthorTextView;
import io.plaidapp.ui.widget.CollapsingTitleLayout;
import io.plaidapp.ui.widget.ElasticDragDismissFrameLayout;
import io.plaidapp.ui.widget.FontTextView;
import io.plaidapp.ui.widget.PinnedOffsetView;
import io.plaidapp.util.AnimUtils;
import io.plaidapp.util.HtmlUtils;
import io.plaidapp.util.ImageUtils;
import io.plaidapp.util.ImeUtils;
import io.plaidapp.util.ViewUtils;
import io.plaidapp.util.customtabs.CustomTabActivityHelper;
import io.plaidapp.util.glide.CircleTransform;
import io.plaidapp.util.glide.ImageSpanTarget;
import retrofit.Callback;
import retrofit.RestAdapter;
import retrofit.RetrofitError;
import retrofit.client.Response;
public class DesignerNewsStory extends Activity {
protected static final String EXTRA_STORY = "story";
private static final int RC_LOGIN_UPVOTE = 7;
private View header;
@Bind(R.id.comments_list) RecyclerView commentsList;
private LinearLayoutManager layoutManager;
private DesignerNewsCommentsAdapter commentsAdapter;
@Bind(R.id.fab) ImageButton fab;
@Bind(R.id.fab_expand) View fabExpand;
@Bind(R.id.comments_container) ElasticDragDismissFrameLayout draggableFrame;
private ElasticDragDismissFrameLayout.SystemChromeFader chromeFader;
@Nullable @Bind(R.id.backdrop_toolbar) CollapsingTitleLayout collapsingToolbar;
@Nullable @Bind(R.id.story_title_background) PinnedOffsetView toolbarBackground;
private Button upvoteStory;
private EditText enterComment;
private ImageButton postComment;
@BindInt(R.integer.fab_expand_duration) int fabExpandDuration;
@BindDimen(R.dimen.comment_thread_width) int threadWidth;
@BindDimen(R.dimen.comment_thread_gap) int threadGap;
private Story story;
private DesignerNewsPrefs designerNewsPrefs;
private DesignerNewsService designerNewsApi;
private Bypass markdown;
private CustomTabActivityHelper customTab;
private CircleTransform circleTransform;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_designer_news_story);
ButterKnife.bind(this);
getWindow().getSharedElementReturnTransition().addListener(returnHomeListener);
story = getIntent().getParcelableExtra(EXTRA_STORY);
fab.setOnClickListener(fabClick);
chromeFader = new ElasticDragDismissFrameLayout.SystemChromeFader(getWindow()) {
@Override
public void onDragDismissed() {
finishAfterTransition();
}
};
markdown = new Bypass(this, new Bypass.Options()
.setBlockQuoteLineColor(
ContextCompat.getColor(this, R.color.designer_news_quote_line))
.setBlockQuoteLineWidth(2) // dps
.setBlockQuoteLineIndent(8) // dps
.setPreImageLinebreakHeight(4) //dps
.setBlockQuoteIndentSize(TypedValue.COMPLEX_UNIT_DIP, 2f)
.setBlockQuoteTextColor(ContextCompat.getColor(this, R.color.designer_news_quote)));
circleTransform = new CircleTransform(this);
designerNewsPrefs = DesignerNewsPrefs.get(this);
createDesignerNewsApi();
layoutManager = new LinearLayoutManager(this);
commentsList.setLayoutManager(layoutManager);
header = getLayoutInflater().inflate(
R.layout.designer_news_story_description, commentsList, false);
bindDescription();
// setup toolbar
Toolbar toolbar = (Toolbar) findViewById(R.id.story_toolbar);
if (collapsingToolbar != null) { // portrait: collapsing toolbar
collapsingToolbar.setTitle(story.title);
collapsingToolbar.addOnLayoutChangeListener(titlebarLayout);
} else { // landscape: scroll toolbar with content
toolbar = (Toolbar) header.findViewById(R.id.story_toolbar);
FontTextView title = (FontTextView) toolbar.findViewById(R.id.story_title);
title.setText(story.title);
}
commentsList.addOnScrollListener(headerScrollListener);
toolbar.setNavigationOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
finishAfterTransition();
}
});
View enterCommentView = setupCommentField();
if (story.comment_count > 0) {
// flatten the comments from a nested structure {@see Comment#comments} to an
// array for our adapter (saving the depth).
List<ThreadedComment> wrapped = new ArrayList<>(story.comment_count);
addComments(story.comments, 0, wrapped);
commentsAdapter =
new DesignerNewsCommentsAdapter(header, wrapped, enterCommentView);
commentsList.setAdapter(commentsAdapter);
} else {
commentsAdapter = new DesignerNewsCommentsAdapter(
header, new ArrayList<ThreadedComment>(0), enterCommentView);
commentsList.setAdapter(commentsAdapter);
}
customTab = new CustomTabActivityHelper();
customTab.setConnectionCallback(customTabConnect);
setEnterSharedElementCallback(sharedEnterCallback);
}
@Override
protected void onStart() {
super.onStart();
customTab.bindCustomTabsService(this);
}
@Override
protected void onResume() {
super.onResume();
// clean up after any fab expansion
fab.setAlpha(1f);
fabExpand.setVisibility(View.INVISIBLE);
draggableFrame.addListener(chromeFader);
}
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
switch (requestCode) {
case RC_LOGIN_UPVOTE:
if (resultCode == RESULT_OK) {
upvoteStory();
}
break;
}
}
@Override
protected void onPause() {
draggableFrame.removeListener(chromeFader);
super.onPause();
}
@Override
protected void onStop() {
customTab.unbindCustomTabsService(this);
super.onStop();
}
@Override
protected void onDestroy() {
customTab.setConnectionCallback(null);
super.onDestroy();
}
@Override @TargetApi(Build.VERSION_CODES.M)
public void onProvideAssistContent(AssistContent outContent) {
outContent.setWebUri(Uri.parse(story.url));
}
public static CustomTabsIntent.Builder getCustomTabIntent(@NonNull Context context,
@NonNull Story story,
@Nullable CustomTabsSession session) {
Intent upvoteStory = new Intent(context, UpvoteStoryService.class);
upvoteStory.setAction(UpvoteStoryService.ACTION_UPVOTE);
upvoteStory.putExtra(UpvoteStoryService.EXTRA_STORY_ID, story.id);
PendingIntent pendingIntent = PendingIntent.getService(context, 0, upvoteStory, 0);
return new CustomTabsIntent.Builder(session)
.setToolbarColor(ContextCompat.getColor(context, R.color.designer_news))
.setActionButton(ImageUtils.vectorToBitmap(context,
R.drawable.ic_upvote_filled_24dp_white),
context.getString(R.string.upvote_story),
pendingIntent,
false)
.setShowTitle(true)
.enableUrlBarHiding();
}
private final CustomTabActivityHelper.ConnectionCallback customTabConnect
= new CustomTabActivityHelper.ConnectionCallback() {
@Override
public void onCustomTabsConnected() {
customTab.mayLaunchUrl(Uri.parse(story.url), null, null);
}
@Override public void onCustomTabsDisconnected() { }
};
private RecyclerView.OnScrollListener headerScrollListener
= new RecyclerView.OnScrollListener() {
@Override
public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
updateScrollDependentUi();
}
};
private void updateScrollDependentUi() {
// feed scroll events to the header
if (collapsingToolbar != null) {
final int headerScroll = header.getTop() - commentsList.getPaddingTop();
collapsingToolbar.setScrollPixelOffset(-headerScroll);
toolbarBackground.setOffset(headerScroll);
}
updateFabVisibility();
}
private boolean fabIsVisible = true;
private void updateFabVisibility() {
// the FAB position can interfere with the enter comment field. Hide the FAB if:
// - The comment field is scrolled onto screen
// - The comment field is focused (i.e. stories with no/few comments might not push the
// enter comment field off-screen so need to make sure the button is accessible
final boolean enterCommentFocused = enterComment.isFocused();
final int firstVisibleItemPosition = layoutManager.findFirstVisibleItemPosition();
final int lastVisibleItemPosition = layoutManager.findLastVisibleItemPosition();
final int footerPosition = commentsAdapter.getItemCount() - 1;
final boolean footerVisible = lastVisibleItemPosition == footerPosition;
final boolean fabShouldBeVisible =
(firstVisibleItemPosition == 0 && !enterCommentFocused) || !footerVisible;
if (!fabShouldBeVisible && fabIsVisible) {
fabIsVisible = false;
fab.animate()
.scaleX(0f)
.scaleY(0f)
.alpha(0.6f)
.setDuration(200L)
.setInterpolator(AnimationUtils.loadInterpolator(this,
android.R.interpolator.fast_out_linear_in))
.withLayer()
.setListener(postHideFab)
.start();
} else if (fabShouldBeVisible && !fabIsVisible) {
fabIsVisible = true;
fab.animate()
.scaleX(1f)
.scaleY(1f)
.alpha(1f)
.setDuration(200L)
.setInterpolator(AnimationUtils.loadInterpolator(this,
android.R.interpolator.linear_out_slow_in))
.withLayer()
.setListener(preShowFab)
.start();
ImeUtils.hideIme(enterComment);
}
}
private AnimatorListenerAdapter preShowFab = new AnimatorListenerAdapter() {
@Override
public void onAnimationStart(Animator animation) {
fab.setVisibility(View.VISIBLE);
}
};
private AnimatorListenerAdapter postHideFab = new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
fab.setVisibility(View.GONE);
}
};
// title can expand up to a max number of lines. If it does then adjust the list padding
// & reset scroll trackers
private View.OnLayoutChangeListener titlebarLayout = new View.OnLayoutChangeListener() {
@Override
public void onLayoutChange(View v, int left, int top, int right, int bottom, int
oldLeft, int oldTop, int oldRight, int oldBottom) {
if ((bottom - top) != (oldBottom - oldTop)) {
commentsList.setPaddingRelative(commentsList.getPaddingStart(),
collapsingToolbar.getHeight(),
commentsList.getPaddingEnd(),
commentsList.getPaddingBottom());
commentsList.scrollToPosition(0);
collapsingToolbar.setScrollPixelOffset(0);
toolbarBackground.setOffset(0);
}
updateScrollDependentUi();
}
};
private View.OnClickListener fabClick = new View.OnClickListener() {
@Override
public void onClick(View v) {
doFabExpand();
CustomTabActivityHelper.openCustomTab(
DesignerNewsStory.this,
getCustomTabIntent(DesignerNewsStory.this, story,
customTab.getSession())
.setStartAnimations(getApplicationContext(),
R.anim.chrome_custom_tab_enter,
R.anim.fade_out_rapidly)
.build(),
Uri.parse(story.url));
}
};
private SharedElementCallback sharedEnterCallback = new SharedElementCallback() {
@Override
public void onSharedElementEnd(List<String> sharedElementNames,
List<View> sharedElements,
List<View> sharedElementSnapshots) {
// force a remeasure to account for shared element shenanigans
if (collapsingToolbar != null) {
collapsingToolbar.measure(
View.MeasureSpec.makeMeasureSpec(draggableFrame.getWidth(),
View.MeasureSpec.AT_MOST),
View.MeasureSpec.makeMeasureSpec(draggableFrame.getWidth(),
View.MeasureSpec.AT_MOST));
collapsingToolbar.requestLayout();
}
if (toolbarBackground != null) {
toolbarBackground.measure(
View.MeasureSpec.makeMeasureSpec(draggableFrame.getWidth(),
View.MeasureSpec.AT_MOST),
View.MeasureSpec.makeMeasureSpec(draggableFrame.getWidth(),
View.MeasureSpec.AT_MOST));
toolbarBackground.requestLayout();
}
}
};
private Transition.TransitionListener returnHomeListener = new AnimUtils
.TransitionListenerAdapter() {
@Override
public void onTransitionStart(Transition transition) {
super.onTransitionStart(transition);
// hide the fab as for some reason it jumps position?? TODO work out why
fab.setVisibility(View.INVISIBLE);
}
};
private void doFabExpand() {
// translate the chrome placeholder ui so that it is centered on the FAB
int fabCenterX = (fab.getLeft() + fab.getRight()) / 2;
int fabCenterY = ((fab.getTop() + fab.getBottom()) / 2) - fabExpand.getTop();
int translateX = fabCenterX - (fabExpand.getWidth() / 2);
int translateY = fabCenterY - (fabExpand.getHeight() / 2);
fabExpand.setTranslationX(translateX);
fabExpand.setTranslationY(translateY);
// then reveal the placeholder ui, starting from the center & same dimens as fab
fabExpand.setVisibility(View.VISIBLE);
Animator reveal = ViewAnimationUtils.createCircularReveal(
fabExpand,
fabExpand.getWidth() / 2,
fabExpand.getHeight() / 2,
fab.getWidth() / 2,
(int) Math.hypot(fabExpand.getWidth() / 2, fabExpand.getHeight() / 2))
.setDuration(fabExpandDuration);
// translate the placeholder ui back into position along an arc
ArcMotion arcMotion = new ArcMotion();
arcMotion.setMinimumVerticalAngle(70f);
Path motionPath = arcMotion.getPath(translateX, translateY, 0, 0);
Animator position = ObjectAnimator.ofFloat(fabExpand, View.TRANSLATION_X, View
.TRANSLATION_Y, motionPath)
.setDuration(fabExpandDuration);
// animate from the FAB colour to the placeholder background color
Animator background = ObjectAnimator.ofArgb(fabExpand,
ViewUtils.BACKGROUND_COLOR,
ContextCompat.getColor(this, R.color.designer_news),
ContextCompat.getColor(this, R.color.background_light))
.setDuration(fabExpandDuration);
// fade out the fab (rapidly)
Animator fadeOutFab = ObjectAnimator.ofFloat(fab, View.ALPHA, 0f)
.setDuration(60);
// play 'em all together with the material interpolator
AnimatorSet show = new AnimatorSet();
show.setInterpolator(AnimUtils.getMaterialInterpolator(DesignerNewsStory.this));
show.playTogether(reveal, background, position, fadeOutFab);
show.start();
}
private void bindDescription() {
final TextView storyComment = (TextView) header.findViewById(R.id.story_comment);
if (!TextUtils.isEmpty(story.comment)) {
HtmlUtils.setTextWithNiceLinks(storyComment, markdown.markdownToSpannable(story
.comment, storyComment, new Bypass.LoadImageCallback() {
@Override
public void loadImage(String src, ImageLoadingSpan loadingSpan) {
Glide.with(DesignerNewsStory.this)
.load(src)
.asBitmap()
.diskCacheStrategy(DiskCacheStrategy.ALL)
.into(new ImageSpanTarget(storyComment, loadingSpan));
}
}));
} else {
storyComment.setVisibility(View.GONE);
}
upvoteStory = (Button) header.findViewById(R.id.story_vote_action);
upvoteStory.setText(getResources().getQuantityString(R.plurals.upvotes, story.vote_count,
NumberFormat.getInstance().format(story.vote_count)));
upvoteStory.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(final View v) {
upvoteStory();
}
});
Button share = (Button) header.findViewById(R.id.story_share_action);
share.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
startActivity(ShareCompat.IntentBuilder.from(DesignerNewsStory.this)
.setText(story.url)
.setType("text/plain")
.getIntent());
}
});
TextView storyPosterTime = (TextView) header.findViewById(R.id.story_poster_time);
SpannableString poster = new SpannableString("–" + story.user_display_name);
poster.setSpan(new TextAppearanceSpan(this, R.style.TextAppearance_CommentAuthor),
0, poster.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
CharSequence job = !TextUtils.isEmpty(story.user_job) ? "\n" + story.user_job : "";
CharSequence timeAgo = DateUtils.getRelativeTimeSpanString(story.created_at.getTime(),
System.currentTimeMillis(),
DateUtils.SECOND_IN_MILLIS);
storyPosterTime.setText(TextUtils.concat(poster, job, "\n", timeAgo));
ImageView avatar = (ImageView) header.findViewById(R.id.story_poster_avatar);
if (!TextUtils.isEmpty(story.user_portrait_url)) {
Glide.with(this)
.load(story.user_portrait_url)
.placeholder(R.drawable.avatar_placeholder)
.transform(circleTransform)
.into(avatar);
} else {
avatar.setVisibility(View.GONE);
}
}
@NonNull
private View setupCommentField() {
View enterCommentView = getLayoutInflater()
.inflate(R.layout.designer_news_enter_comment, commentsList, false);
enterComment = (EditText) enterCommentView.findViewById(R.id.comment);
postComment = (ImageButton) enterCommentView.findViewById(R.id.post_comment);
postComment.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
if (designerNewsPrefs.isLoggedIn()) {
if (TextUtils.isEmpty(enterComment.getText())) return;
enterComment.setEnabled(false);
postComment.setEnabled(false);
designerNewsApi.comment(story.id, enterComment.getText().toString(),
new Callback<Comment>() {
@Override
public void success(Comment comment, Response response) {
enterComment.getText().clear();
enterComment.setEnabled(true);
postComment.setEnabled(true);
((DesignerNewsCommentsAdapter) commentsList.getAdapter())
.addComment(new ThreadedComment(0, comment));
}
@Override
public void failure(RetrofitError error) {
Toast.makeText(getApplicationContext(),
"Failed to post comment :(", Toast.LENGTH_SHORT).show();
enterComment.setEnabled(true);
postComment.setEnabled(true);
}
});
} else {
needsLogin(postComment, 0);
}
enterComment.clearFocus();
}
});
enterComment.setOnFocusChangeListener(enterCommentFocus);
return enterCommentView;
}
private void upvoteStory() {
if (designerNewsPrefs.isLoggedIn()) {
if (!upvoteStory.isActivated()) {
upvoteStory.setActivated(true);
designerNewsApi.upvoteStory(story.id, "",
new Callback<StoryResponse>() {
@Override
public void success(StoryResponse storyResponse, Response
response) {
final int newUpvoteCount = storyResponse.story.vote_count;
upvoteStory.setText(getResources().getQuantityString(
R.plurals.upvotes, newUpvoteCount,
NumberFormat.getInstance().format(newUpvoteCount)));
}
@Override public void failure(RetrofitError error) { }
});
} else {
upvoteStory.setActivated(false);
// TODO delete upvote. Not available in v1 API.
}
} else {
needsLogin(upvoteStory, RC_LOGIN_UPVOTE);
}
}
private void needsLogin(View triggeringView, int requestCode) {
Intent login = new Intent(DesignerNewsStory.this,
DesignerNewsLogin.class);
login.putExtra(FabDialogMorphSetup.EXTRA_SHARED_ELEMENT_START_COLOR,
ContextCompat.getColor(DesignerNewsStory.this, R.color.background_light));
ActivityOptions options = ActivityOptions.makeSceneTransitionAnimation(
DesignerNewsStory.this,
triggeringView, getString(R.string.transition_designer_news_login));
startActivityForResult(login, requestCode, options.toBundle());
}
private void createDesignerNewsApi() {
designerNewsApi = new RestAdapter.Builder()
.setEndpoint(DesignerNewsService.ENDPOINT)
.setRequestInterceptor(new ClientAuthInterceptor(designerNewsPrefs.getAccessToken(),
BuildConfig.DESIGNER_NEWS_CLIENT_ID))
.build()
.create(DesignerNewsService.class);
}
private void addComments(List<Comment> comments, int depth, List<ThreadedComment> wrapped) {
for (Comment comment : comments) {
wrapped.add(new ThreadedComment(depth, comment));
// todo move this to after downloading so only done once
if (comment.comments != null && comment.comments.size() > 0) {
addComments(comment.comments, depth + 1, wrapped);
}
}
}
private View.OnFocusChangeListener enterCommentFocus = new View.OnFocusChangeListener() {
@Override
public void onFocusChange(View view, boolean hasFocus) {
// kick off an anim (via animated state list) on the post button. see
// @drawable/ic_add_comment_state
postComment.setActivated(hasFocus);
updateFabVisibility();
}
};
private boolean isOP(Long userId) {
return userId.equals(story.user_id);
}
// convenience class used to convert nested comment structure returned from the API to a flat
// structure with a depth attribute, suitable for showing in a list.
protected class ThreadedComment {
final int depth;
final Comment comment;
ThreadedComment(int depth,
Comment comment) {
this.depth = depth;
this.comment = comment;
}
}
/* package */ 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_FOOTER = 3;
private View header;
private List<ThreadedComment> comments;
private View footer;
DesignerNewsCommentsAdapter(@NonNull View header,
@NonNull List<ThreadedComment> comments,
@NonNull View footer) {
this.header = header;
this.comments = comments;
this.footer = footer;
}
private boolean hasComments() {
return !comments.isEmpty();
}
@Override
public int getItemViewType(int position) {
if (position == 0) {
return TYPE_HEADER;
} else if ((hasComments() && position == comments.size() + 1)
|| (!hasComments() && position == 2)) {
return TYPE_FOOTER;
} else {
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 new CommentHolder(
getLayoutInflater().inflate(R.layout.designer_news_comment, parent, false));
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) {
if (getItemViewType(position) == TYPE_COMMENT) {
bindComment((CommentHolder) holder, comments.get(position - 1)); // minus header
} // nothing to bind for header / no comment / footer views
}
private void bindComment(final CommentHolder holder, final ThreadedComment comment) {
HtmlUtils.setTextWithNiceLinks(holder.comment, markdown.markdownToSpannable(comment
.comment.body, holder.comment, new Bypass.LoadImageCallback() {
@Override
public void loadImage(String src, ImageLoadingSpan loadingSpan) {
Glide.with(DesignerNewsStory.this)
.load(src)
.asBitmap()
.diskCacheStrategy(DiskCacheStrategy.ALL)
.into(new ImageSpanTarget(holder.comment, loadingSpan));
}
}));
holder.author.setText(comment.comment.user_display_name);
holder.author.setOriginalPoster(isOP(comment
.comment.user_id));
holder.timeAgo.setText(
DateUtils.getRelativeTimeSpanString(comment.comment.created_at.getTime(),
System.currentTimeMillis(),
DateUtils.SECOND_IN_MILLIS));
ThreadedCommentDrawable depthDrawable = new ThreadedCommentDrawable(threadWidth,
threadGap);
depthDrawable.setDepth(comment.depth);
holder.threadDepth.setImageDrawable(depthDrawable);
}
@Override
public int getItemCount() {
// include header & footer (+ no comments view)
return hasComments() ? comments.size() + 2 : 3;
}
public void addComment(ThreadedComment newComment) {
if (!hasComments()) {
notifyItemRemoved(1); // remove the no comments view
}
comments.add(newComment);
notifyItemInserted(comments.size());
}
}
/* package */ static class CommentHolder extends RecyclerView.ViewHolder {
@Bind(R.id.depth) ImageView threadDepth;
@Bind(R.id.comment_author) AuthorTextView author;
@Bind(R.id.comment_time_ago) TextView timeAgo;
@Bind(R.id.comment_text) TextView comment;
public CommentHolder(View itemView) {
super(itemView);
ButterKnife.bind(this, itemView);
}
}
/* 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);
}
}
}