/*
* 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.ValueAnimator;
import android.annotation.TargetApi;
import android.app.Activity;
import android.app.ActivityOptions;
import android.app.assist.AssistContent;
import android.content.Intent;
import android.content.res.Resources;
import android.graphics.Bitmap;
import android.graphics.drawable.AnimatedVectorDrawable;
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.design.widget.Snackbar;
import android.support.v4.content.ContextCompat;
import android.support.v7.graphics.Palette;
import android.support.v7.widget.RecyclerView;
import android.text.Spanned;
import android.text.TextUtils;
import android.text.format.DateUtils;
import android.transition.AutoTransition;
import android.transition.Transition;
import android.transition.TransitionManager;
import android.util.Pair;
import android.util.TypedValue;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
import android.view.ViewTreeObserver;
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 com.bumptech.glide.Glide;
import com.bumptech.glide.Priority;
import com.bumptech.glide.load.engine.DiskCacheStrategy;
import com.bumptech.glide.load.resource.drawable.GlideDrawable;
import com.bumptech.glide.request.RequestListener;
import com.bumptech.glide.request.target.Target;
import java.text.NumberFormat;
import java.util.ArrayList;
import java.util.List;
import butterknife.BindDimen;
import butterknife.BindView;
import butterknife.ButterKnife;
import io.plaidapp.R;
import io.plaidapp.data.api.dribbble.DribbbleService;
import io.plaidapp.data.api.dribbble.model.Comment;
import io.plaidapp.data.api.dribbble.model.Like;
import io.plaidapp.data.api.dribbble.model.Shot;
import io.plaidapp.data.prefs.DribbblePrefs;
import io.plaidapp.ui.recyclerview.InsetDividerDecoration;
import io.plaidapp.ui.recyclerview.SlideInItemAnimator;
import io.plaidapp.ui.transitions.FabTransform;
import io.plaidapp.ui.widget.AuthorTextView;
import io.plaidapp.ui.widget.CheckableImageButton;
import io.plaidapp.ui.widget.ElasticDragDismissFrameLayout;
import io.plaidapp.ui.widget.FABToggle;
import io.plaidapp.ui.widget.FabOverlapTextView;
import io.plaidapp.ui.widget.ForegroundImageView;
import io.plaidapp.ui.widget.ParallaxScrimageView;
import io.plaidapp.util.ColorUtils;
import io.plaidapp.util.HtmlUtils;
import io.plaidapp.util.ImeUtils;
import io.plaidapp.util.TransitionUtils;
import io.plaidapp.util.ViewUtils;
import io.plaidapp.util.customtabs.CustomTabActivityHelper;
import io.plaidapp.util.glide.CircleTransform;
import io.plaidapp.util.glide.GlideUtils;
import okhttp3.HttpUrl;
import retrofit2.Call;
import retrofit2.Callback;
import retrofit2.Response;
import static io.plaidapp.util.AnimUtils.getFastOutSlowInInterpolator;
public class DribbbleShot extends Activity {
public final static String EXTRA_SHOT = "EXTRA_SHOT";
public final static String RESULT_EXTRA_SHOT_ID = "RESULT_EXTRA_SHOT_ID";
private static final int RC_LOGIN_LIKE = 0;
private static final int RC_LOGIN_COMMENT = 1;
private static final float SCRIM_ADJUSTMENT = 0.075f;
@BindView(R.id.draggable_frame) ElasticDragDismissFrameLayout draggableFrame;
@BindView(R.id.back) ImageButton back;
@BindView(R.id.shot) ParallaxScrimageView imageView;
@BindView(R.id.dribbble_comments) RecyclerView commentsList;
@BindView(R.id.fab_heart) FABToggle fab;
View shotDescription;
View shotSpacer;
Button likeCount;
Button viewCount;
Button share;
ImageView playerAvatar;
EditText enterComment;
ImageButton postComment;
private View title;
private View description;
private TextView playerName;
private TextView shotTimeAgo;
private View commentFooter;
private ImageView userAvatar;
private ElasticDragDismissFrameLayout.SystemChromeFader chromeFader;
Shot shot;
int fabOffset;
DribbblePrefs dribbblePrefs;
boolean performingLike;
boolean allowComment;
CircleTransform circleTransform;
CommentsAdapter adapter;
CommentAnimator commentAnimator;
@BindDimen(R.dimen.large_avatar_size) int largeAvatarSize;
@BindDimen(R.dimen.z_card) int cardElevation;
@Override
protected void onCreate(final Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_dribbble_shot);
dribbblePrefs = DribbblePrefs.get(this);
circleTransform = new CircleTransform(this);
ButterKnife.bind(this);
shotDescription = getLayoutInflater().inflate(R.layout.dribbble_shot_description,
commentsList, false);
shotSpacer = shotDescription.findViewById(R.id.shot_spacer);
title = shotDescription.findViewById(R.id.shot_title);
description = shotDescription.findViewById(R.id.shot_description);
likeCount = (Button) shotDescription.findViewById(R.id.shot_like_count);
viewCount = (Button) shotDescription.findViewById(R.id.shot_view_count);
share = (Button) shotDescription.findViewById(R.id.shot_share_action);
playerName = (TextView) shotDescription.findViewById(R.id.player_name);
playerAvatar = (ImageView) shotDescription.findViewById(R.id.player_avatar);
shotTimeAgo = (TextView) shotDescription.findViewById(R.id.shot_time_ago);
setupCommenting();
commentsList.addOnScrollListener(scrollListener);
commentsList.setOnFlingListener(flingListener);
back.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
setResultAndFinish();
}
});
fab.setOnClickListener(fabClick);
chromeFader = new ElasticDragDismissFrameLayout.SystemChromeFader(this) {
@Override
public void onDragDismissed() {
setResultAndFinish();
}
};
final Intent intent = getIntent();
if (intent.hasExtra(EXTRA_SHOT)) {
shot = intent.getParcelableExtra(EXTRA_SHOT);
bindShot(true);
} else if (intent.getData() != null) {
final HttpUrl url = HttpUrl.parse(intent.getDataString());
if (url.pathSize() == 2 && url.pathSegments().get(0).equals("shots")) {
try {
final String shotPath = url.pathSegments().get(1);
final long id = Long.parseLong(shotPath.substring(0, shotPath.indexOf("-")));
final Call<Shot> shotCall = dribbblePrefs.getApi().getShot(id);
shotCall.enqueue(new Callback<Shot>() {
@Override
public void onResponse(Call<Shot> call, Response<Shot> response) {
shot = response.body();
bindShot(false);
}
@Override
public void onFailure(Call<Shot> call, Throwable t) {
reportUrlError();
}
});
} catch (NumberFormatException|StringIndexOutOfBoundsException ex) {
reportUrlError();
}
} else {
reportUrlError();
}
}
}
@Override
protected void onResume() {
super.onResume();
if (!performingLike) {
checkLiked();
}
draggableFrame.addListener(chromeFader);
}
@Override
protected void onPause() {
draggableFrame.removeListener(chromeFader);
super.onPause();
}
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
switch (requestCode) {
case RC_LOGIN_LIKE:
if (resultCode == RESULT_OK) {
// TODO when we add more authenticated actions will need to keep track of what
// the user was trying to do when forced to login
fab.setChecked(true);
doLike();
setupCommenting();
}
break;
case RC_LOGIN_COMMENT:
if (resultCode == RESULT_OK) {
setupCommenting();
}
}
}
@Override
public void onBackPressed() {
setResultAndFinish();
}
@Override
public boolean onNavigateUp() {
setResultAndFinish();
return true;
}
@Override @TargetApi(Build.VERSION_CODES.M)
public void onProvideAssistContent(AssistContent outContent) {
outContent.setWebUri(Uri.parse(shot.url));
}
public void postComment(View view) {
if (dribbblePrefs.isLoggedIn()) {
if (TextUtils.isEmpty(enterComment.getText())) return;
enterComment.setEnabled(false);
final Call<Comment> postCommentCall = dribbblePrefs.getApi().postComment(
shot.id, enterComment.getText().toString().trim());
postCommentCall.enqueue(new Callback<Comment>() {
@Override
public void onResponse(Call<Comment> call, Response<Comment> response) {
loadComments();
enterComment.getText().clear();
enterComment.setEnabled(true);
}
@Override
public void onFailure(Call<Comment> call, Throwable t) {
enterComment.setEnabled(true);
}
});
} else {
Intent login = new Intent(DribbbleShot.this, DribbbleLogin.class);
FabTransform.addExtras(login, ContextCompat.getColor(
DribbbleShot.this, R.color.background_light), R.drawable.ic_comment_add);
ActivityOptions options = ActivityOptions.makeSceneTransitionAnimation(
DribbbleShot.this, postComment, getString(R.string.transition_dribbble_login));
startActivityForResult(login, RC_LOGIN_COMMENT, options.toBundle());
}
}
void bindShot(final boolean postponeEnterTransition) {
final Resources res = getResources();
// load the main image
final int[] imageSize = shot.images.bestSize();
Glide.with(this)
.load(shot.images.best())
.listener(shotLoadListener)
.diskCacheStrategy(DiskCacheStrategy.SOURCE)
.priority(Priority.IMMEDIATE)
.override(imageSize[0], imageSize[1])
.into(imageView);
imageView.setOnClickListener(shotClick);
shotSpacer.setOnClickListener(shotClick);
if (postponeEnterTransition) postponeEnterTransition();
imageView.getViewTreeObserver().addOnPreDrawListener(
new ViewTreeObserver.OnPreDrawListener() {
@Override
public boolean onPreDraw() {
imageView.getViewTreeObserver().removeOnPreDrawListener(this);
calculateFabPosition();
if (postponeEnterTransition) startPostponedEnterTransition();
return true;
}
});
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
((FabOverlapTextView) title).setText(shot.title);
} else {
((TextView) title).setText(shot.title);
}
if (!TextUtils.isEmpty(shot.description)) {
final Spanned descText = shot.getParsedDescription(
ContextCompat.getColorStateList(this, R.color.dribbble_links),
ContextCompat.getColor(this, R.color.dribbble_link_highlight));
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
((FabOverlapTextView) description).setText(descText);
} else {
HtmlUtils.setTextWithNiceLinks((TextView) description, descText);
}
} else {
description.setVisibility(View.GONE);
}
NumberFormat nf = NumberFormat.getInstance();
likeCount.setText(
res.getQuantityString(R.plurals.likes,
(int) shot.likes_count,
nf.format(shot.likes_count)));
likeCount.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
((AnimatedVectorDrawable) likeCount.getCompoundDrawables()[1]).start();
if (shot.likes_count > 0) {
PlayerSheet.start(DribbbleShot.this, shot);
}
}
});
if (shot.likes_count == 0) {
likeCount.setBackground(null); // clear touch ripple if doesn't do anything
}
viewCount.setText(
res.getQuantityString(R.plurals.views,
(int) shot.views_count,
nf.format(shot.views_count)));
viewCount.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
((AnimatedVectorDrawable) viewCount.getCompoundDrawables()[1]).start();
}
});
share.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
((AnimatedVectorDrawable) share.getCompoundDrawables()[1]).start();
new ShareDribbbleImageTask(DribbbleShot.this, shot).execute();
}
});
if (shot.user != null) {
playerName.setText(shot.user.name.toLowerCase());
Glide.with(this)
.load(shot.user.getHighQualityAvatarUrl())
.transform(circleTransform)
.placeholder(R.drawable.avatar_placeholder)
.override(largeAvatarSize, largeAvatarSize)
.into(playerAvatar);
View.OnClickListener playerClick = new View.OnClickListener() {
@Override
public void onClick(View v) {
Intent player = new Intent(DribbbleShot.this, PlayerActivity.class);
if (shot.user.shots_count > 0) { // legit user object
player.putExtra(PlayerActivity.EXTRA_PLAYER, shot.user);
} else {
// search doesn't fully populate the user object,
// in this case send the ID not the full user
player.putExtra(PlayerActivity.EXTRA_PLAYER_NAME, shot.user.username);
player.putExtra(PlayerActivity.EXTRA_PLAYER_ID, shot.user.id);
}
ActivityOptions options =
ActivityOptions.makeSceneTransitionAnimation(DribbbleShot.this,
playerAvatar, getString(R.string.transition_player_avatar));
startActivity(player, options.toBundle());
}
};
playerAvatar.setOnClickListener(playerClick);
playerName.setOnClickListener(playerClick);
if (shot.created_at != null) {
shotTimeAgo.setText(DateUtils.getRelativeTimeSpanString(shot.created_at.getTime(),
System.currentTimeMillis(),
DateUtils.SECOND_IN_MILLIS).toString().toLowerCase());
}
} else {
playerName.setVisibility(View.GONE);
playerAvatar.setVisibility(View.GONE);
shotTimeAgo.setVisibility(View.GONE);
}
commentAnimator = new CommentAnimator();
commentsList.setItemAnimator(commentAnimator);
adapter = new CommentsAdapter(shotDescription, commentFooter, shot.comments_count,
getResources().getInteger(R.integer.comment_expand_collapse_duration));
commentsList.setAdapter(adapter);
commentsList.addItemDecoration(new InsetDividerDecoration(
CommentViewHolder.class,
res.getDimensionPixelSize(R.dimen.divider_height),
res.getDimensionPixelSize(R.dimen.keyline_1),
ContextCompat.getColor(this, R.color.divider)));
if (shot.comments_count != 0) {
loadComments();
}
checkLiked();
}
void reportUrlError() {
Snackbar.make(draggableFrame, R.string.bad_dribbble_shot_url, Snackbar.LENGTH_SHORT).show();
draggableFrame.postDelayed(new Runnable() {
@Override
public void run() {
finishAfterTransition();
}
}, 3000L);
}
private View.OnClickListener shotClick = new View.OnClickListener() {
@Override
public void onClick(View view) {
openLink(shot.url);
}
};
/**
* We run a transition to expand/collapse comments. Scrolling the RecyclerView while this is
* running causes issues, so we consume touch events while the transition runs.
*/
View.OnTouchListener touchEater = new View.OnTouchListener() {
@Override
public boolean onTouch(View view, MotionEvent motionEvent) {
return true;
}
};
void openLink(String url) {
CustomTabActivityHelper.openCustomTab(
DribbbleShot.this,
new CustomTabsIntent.Builder()
.setToolbarColor(ContextCompat.getColor(DribbbleShot.this, R.color.dribbble))
.addDefaultShareMenuItem()
.build(),
Uri.parse(url));
}
private RequestListener shotLoadListener = new RequestListener<String, GlideDrawable>() {
@Override
public boolean onResourceReady(GlideDrawable resource, String model,
Target<GlideDrawable> target, boolean isFromMemoryCache,
boolean isFirstResource) {
final Bitmap bitmap = GlideUtils.getBitmap(resource);
final int twentyFourDip = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP,
24, DribbbleShot.this.getResources().getDisplayMetrics());
Palette.from(bitmap)
.maximumColorCount(3)
.clearFilters() /* by default palette ignore certain hues
(e.g. pure black/white) but we don't want this. */
.setRegion(0, 0, bitmap.getWidth() - 1, twentyFourDip) /* - 1 to work around
https://code.google.com/p/android/issues/detail?id=191013 */
.generate(new Palette.PaletteAsyncListener() {
@Override
public void onGenerated(Palette palette) {
boolean isDark;
@ColorUtils.Lightness int lightness = ColorUtils.isDark(palette);
if (lightness == ColorUtils.LIGHTNESS_UNKNOWN) {
isDark = ColorUtils.isDark(bitmap, bitmap.getWidth() / 2, 0);
} else {
isDark = lightness == ColorUtils.IS_DARK;
}
if (!isDark) { // make back icon dark on light images
back.setColorFilter(ContextCompat.getColor(
DribbbleShot.this, R.color.dark_icon));
}
// color the status bar. Set a complementary dark color on L,
// light or dark color on M (with matching status bar icons)
int statusBarColor = getWindow().getStatusBarColor();
final Palette.Swatch topColor =
ColorUtils.getMostPopulousSwatch(palette);
if (topColor != null &&
(isDark || Build.VERSION.SDK_INT >= Build.VERSION_CODES.M)) {
statusBarColor = ColorUtils.scrimify(topColor.getRgb(),
isDark, SCRIM_ADJUSTMENT);
// set a light status bar on M+
if (!isDark && Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
ViewUtils.setLightStatusBar(imageView);
}
}
if (statusBarColor != getWindow().getStatusBarColor()) {
imageView.setScrimColor(statusBarColor);
ValueAnimator statusBarColorAnim = ValueAnimator.ofArgb(
getWindow().getStatusBarColor(), statusBarColor);
statusBarColorAnim.addUpdateListener(new ValueAnimator
.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
getWindow().setStatusBarColor(
(int) animation.getAnimatedValue());
}
});
statusBarColorAnim.setDuration(1000L);
statusBarColorAnim.setInterpolator(
getFastOutSlowInInterpolator(DribbbleShot.this));
statusBarColorAnim.start();
}
}
});
Palette.from(bitmap)
.clearFilters()
.generate(new Palette.PaletteAsyncListener() {
@Override
public void onGenerated(Palette palette) {
// color the ripple on the image spacer (default is grey)
shotSpacer.setBackground(
ViewUtils.createRipple(palette, 0.25f, 0.5f,
ContextCompat.getColor(DribbbleShot.this, R.color.mid_grey),
true));
// slightly more opaque ripple on the pinned image to compensate
// for the scrim
imageView.setForeground(
ViewUtils.createRipple(palette, 0.3f, 0.6f,
ContextCompat.getColor(DribbbleShot.this, R.color.mid_grey),
true));
}
});
// TODO should keep the background if the image contains transparency?!
imageView.setBackground(null);
return false;
}
@Override
public boolean onException(Exception e, String model, Target<GlideDrawable> target,
boolean isFirstResource) {
return false;
}
};
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
postComment.setActivated(hasFocus);
// prevent content hovering over image when not pinned.
if(hasFocus) {
imageView.bringToFront();
imageView.setOffset(-imageView.getHeight());
imageView.setImmediatePin(true);
}
}
};
private RecyclerView.OnScrollListener scrollListener = new RecyclerView.OnScrollListener() {
@Override
public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
final int scrollY = shotDescription.getTop();
imageView.setOffset(scrollY);
fab.setOffset(fabOffset + scrollY);
}
@Override
public void onScrollStateChanged(RecyclerView recyclerView, int newState) {
// as we animate the main image's elevation change when it 'pins' at it's min height
// a fling can cause the title to go over the image before the animation has a chance to
// run. In this case we short circuit the animation and just jump to state.
imageView.setImmediatePin(newState == RecyclerView.SCROLL_STATE_SETTLING);
}
};
private RecyclerView.OnFlingListener flingListener = new RecyclerView.OnFlingListener() {
@Override
public boolean onFling(int velocityX, int velocityY) {
imageView.setImmediatePin(true);
return false;
}
};
private View.OnClickListener fabClick = new View.OnClickListener() {
@Override
public void onClick(View view) {
if (dribbblePrefs.isLoggedIn()) {
fab.toggle();
doLike();
} else {
final Intent login = new Intent(DribbbleShot.this, DribbbleLogin.class);
FabTransform.addExtras(login, ContextCompat.getColor(DribbbleShot.this, R
.color.dribbble), R.drawable.ic_heart_empty_56dp);
ActivityOptions options = ActivityOptions.makeSceneTransitionAnimation
(DribbbleShot.this, fab, getString(R.string.transition_dribbble_login));
startActivityForResult(login, RC_LOGIN_LIKE, options.toBundle());
}
}
};
void loadComments() {
final Call<List<Comment>> commentsCall =
dribbblePrefs.getApi().getComments(shot.id, 0, DribbbleService.PER_PAGE_MAX);
commentsCall.enqueue(new Callback<List<Comment>>() {
@Override
public void onResponse(Call<List<Comment>> call, Response<List<Comment>> response) {
final List<Comment> comments = response.body();
if (comments != null && !comments.isEmpty()) {
adapter.addComments(comments);
}
}
@Override public void onFailure(Call<List<Comment>> call, Throwable t) { }
});
}
void setResultAndFinish() {
final Intent resultData = new Intent();
resultData.putExtra(RESULT_EXTRA_SHOT_ID, shot.id);
setResult(RESULT_OK, resultData);
finishAfterTransition();
}
void calculateFabPosition() {
// calculate 'natural' position i.e. with full height image. Store it for use when scrolling
fabOffset = imageView.getHeight() + title.getHeight() - (fab.getHeight() / 2);
fab.setOffset(fabOffset);
// calculate min position i.e. pinned to the collapsed image when scrolled
fab.setMinOffset(imageView.getMinimumHeight() - (fab.getHeight() / 2));
}
void doLike() {
performingLike = true;
if (fab.isChecked()) {
final Call<Like> likeCall = dribbblePrefs.getApi().like(shot.id);
likeCall.enqueue(new Callback<Like>() {
@Override
public void onResponse(Call<Like> call, Response<Like> response) {
performingLike = false;
}
@Override
public void onFailure(Call<Like> call, Throwable t) {
performingLike = false;
}
});
} else {
final Call<Void> unlikeCall = dribbblePrefs.getApi().unlike(shot.id);
unlikeCall.enqueue(new Callback<Void>() {
@Override
public void onResponse(Call<Void> call, Response<Void> response) {
performingLike = false;
}
@Override
public void onFailure(Call<Void> call, Throwable t) {
performingLike = false;
}
});
}
}
boolean isOP(long playerId) {
return shot.user != null && shot.user.id == playerId;
}
private void checkLiked() {
if (shot != null && dribbblePrefs.isLoggedIn()) {
final Call<Like> likedCall = dribbblePrefs.getApi().liked(shot.id);
likedCall.enqueue(new Callback<Like>() {
@Override
public void onResponse(Call<Like> call, Response<Like> response) {
// note that like.user will be null here
fab.setChecked(response.body() != null);
}
@Override
public void onFailure(Call<Like> call, Throwable t) {
// 404 is expected if shot is not liked
fab.setChecked(false);
fab.jumpDrawablesToCurrentState();
}
});
}
}
private void setupCommenting() {
allowComment = !dribbblePrefs.isLoggedIn()
|| (dribbblePrefs.isLoggedIn() && dribbblePrefs.userCanPost());
if (allowComment && commentFooter == null) {
commentFooter = getLayoutInflater().inflate(R.layout.dribbble_enter_comment,
commentsList, false);
userAvatar = (ForegroundImageView) commentFooter.findViewById(R.id.avatar);
enterComment = (EditText) commentFooter.findViewById(R.id.comment);
postComment = (ImageButton) commentFooter.findViewById(R.id.post_comment);
enterComment.setOnFocusChangeListener(enterCommentFocus);
} else if (!allowComment && commentFooter != null) {
adapter.removeCommentingFooter();
commentFooter = null;
Toast.makeText(getApplicationContext(),
R.string.prospects_cant_post, Toast.LENGTH_SHORT).show();
}
if (allowComment
&& dribbblePrefs.isLoggedIn()
&& !TextUtils.isEmpty(dribbblePrefs.getUserAvatar())) {
Glide.with(this)
.load(dribbblePrefs.getUserAvatar())
.transform(circleTransform)
.placeholder(R.drawable.ic_player)
.into(userAvatar);
}
}
class CommentsAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> {
private static final int EXPAND = 0x1;
private static final int COLLAPSE = 0x2;
private static final int COMMENT_LIKE = 0x3;
private static final int REPLY = 0x4;
private final List<Comment> comments = new ArrayList<>();
final Transition expandCollapse;
private final View description;
private View footer;
private boolean loading;
private boolean noComments;
int expandedCommentPosition = RecyclerView.NO_POSITION;
CommentsAdapter(
@NonNull View description,
@Nullable View footer,
long commentCount,
long expandDuration) {
this.description = description;
this.footer = footer;
noComments = commentCount == 0L;
loading = !noComments;
expandCollapse = new AutoTransition();
expandCollapse.setDuration(expandDuration);
expandCollapse.setInterpolator(getFastOutSlowInInterpolator(DribbbleShot.this));
expandCollapse.addListener(new TransitionUtils.TransitionListenerAdapter() {
@Override
public void onTransitionStart(Transition transition) {
commentsList.setOnTouchListener(touchEater);
}
@Override
public void onTransitionEnd(Transition transition) {
commentAnimator.setAnimateMoves(true);
commentsList.setOnTouchListener(null);
}
});
}
@Override
public int getItemViewType(int position) {
if (position == 0) return R.layout.dribbble_shot_description;
if (position == 1) {
if (loading) return R.layout.loading;
if (noComments) return R.layout.dribbble_no_comments;
}
if (footer != null) {
int footerPos = (loading || noComments) ? 2 : comments.size() + 1;
if (position == footerPos) return R.layout.dribbble_enter_comment;
}
return R.layout.dribbble_comment;
}
@Override
public int getItemCount() {
int count = 1; // description
if (!comments.isEmpty()) {
count += comments.size();
} else {
count++; // either loading or no comments
}
if (footer != null) count++;
return count;
}
@Override
public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
switch (viewType) {
case R.layout.dribbble_shot_description:
return new SimpleViewHolder(description);
case R.layout.dribbble_comment:
return createCommentHolder(parent, viewType);
case R.layout.loading:
case R.layout.dribbble_no_comments:
return new SimpleViewHolder(
getLayoutInflater().inflate(viewType, parent, false));
case R.layout.dribbble_enter_comment:
return new SimpleViewHolder(footer);
}
throw new IllegalArgumentException();
}
@Override
public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {
switch (getItemViewType(position)) {
case R.layout.dribbble_comment:
bindComment((CommentViewHolder) holder, getComment(position));
break;
}
}
@Override
public void onBindViewHolder(RecyclerView.ViewHolder holder, int position,
List<Object> partialChangePayloads) {
if (holder instanceof CommentViewHolder) {
bindPartialCommentChange(
(CommentViewHolder) holder, position, partialChangePayloads);
} else {
onBindViewHolder(holder, position);
}
}
Comment getComment(int adapterPosition) {
return comments.get(adapterPosition - 1); // description
}
void addComments(List<Comment> newComments) {
hideLoadingIndicator();
noComments = false;
comments.addAll(newComments);
notifyItemRangeInserted(1, newComments.size());
}
void removeCommentingFooter() {
if (footer == null) return;
int footerPos = getItemCount() - 1;
footer = null;
notifyItemRemoved(footerPos);
}
private CommentViewHolder createCommentHolder(ViewGroup parent, int viewType) {
final CommentViewHolder holder = new CommentViewHolder(
getLayoutInflater().inflate(viewType, parent, false));
holder.itemView.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
final int position = holder.getAdapterPosition();
if (position == RecyclerView.NO_POSITION) return;
final Comment comment = getComment(position);
TransitionManager.beginDelayedTransition(commentsList, expandCollapse);
commentAnimator.setAnimateMoves(false);
// collapse any currently expanded items
if (RecyclerView.NO_POSITION != expandedCommentPosition) {
notifyItemChanged(expandedCommentPosition, COLLAPSE);
}
// expand this item (if it wasn't already)
if (expandedCommentPosition != position) {
expandedCommentPosition = position;
notifyItemChanged(position, EXPAND);
if (comment.liked == null) {
final Call<Like> liked = dribbblePrefs.getApi()
.likedComment(shot.id, comment.id);
liked.enqueue(new Callback<Like>() {
@Override
public void onResponse(Call<Like> call, Response<Like> response) {
comment.liked = response.isSuccessful();
holder.likeHeart.setChecked(comment.liked);
holder.likeHeart.jumpDrawablesToCurrentState();
}
@Override public void onFailure(Call<Like> call, Throwable t) { }
});
}
if (enterComment != null && enterComment.hasFocus()) {
enterComment.clearFocus();
ImeUtils.hideIme(enterComment);
}
holder.itemView.requestFocus();
} else {
expandedCommentPosition = RecyclerView.NO_POSITION;
}
}
});
holder.avatar.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
final int position = holder.getAdapterPosition();
if (position == RecyclerView.NO_POSITION) return;
final Comment comment = getComment(position);
final Intent player = new Intent(DribbbleShot.this, PlayerActivity.class);
player.putExtra(PlayerActivity.EXTRA_PLAYER, comment.user);
ActivityOptions options =
ActivityOptions.makeSceneTransitionAnimation(DribbbleShot.this,
Pair.create(holder.itemView,
getString(R.string.transition_player_background)),
Pair.create((View) holder.avatar,
getString(R.string.transition_player_avatar)));
startActivity(player, options.toBundle());
}
});
holder.reply.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
final int position = holder.getAdapterPosition();
if (position == RecyclerView.NO_POSITION) return;
final Comment comment = getComment(position);
enterComment.setText("@" + comment.user.username + " ");
enterComment.setSelection(enterComment.getText().length());
// collapse the comment and scroll the reply box (in the footer) into view
expandedCommentPosition = RecyclerView.NO_POSITION;
notifyItemChanged(position, REPLY);
holder.reply.jumpDrawablesToCurrentState();
enterComment.requestFocus();
commentsList.smoothScrollToPosition(getItemCount() - 1);
}
});
holder.likeHeart.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
if (dribbblePrefs.isLoggedIn()) {
final int position = holder.getAdapterPosition();
if (position == RecyclerView.NO_POSITION) return;
final Comment comment = getComment(position);
if (comment.liked == null || !comment.liked) {
comment.liked = true;
comment.likes_count++;
holder.likesCount.setText(String.valueOf(comment.likes_count));
notifyItemChanged(position, COMMENT_LIKE);
final Call<Like> likeCommentCall =
dribbblePrefs.getApi().likeComment(shot.id, comment.id);
likeCommentCall.enqueue(new Callback<Like>() {
@Override
public void onResponse(Call<Like> call, Response<Like> response) { }
@Override
public void onFailure(Call<Like> call, Throwable t) { }
});
} else {
comment.liked = false;
comment.likes_count--;
holder.likesCount.setText(String.valueOf(comment.likes_count));
notifyItemChanged(position, COMMENT_LIKE);
final Call<Void> unlikeCommentCall =
dribbblePrefs.getApi().unlikeComment(shot.id, comment.id);
unlikeCommentCall.enqueue(new Callback<Void>() {
@Override
public void onResponse(Call<Void> call, Response<Void> response) { }
@Override
public void onFailure(Call<Void> call, Throwable t) { }
});
}
} else {
holder.likeHeart.setChecked(false);
startActivityForResult(new Intent(DribbbleShot.this,
DribbbleLogin.class), RC_LOGIN_LIKE);
}
}
});
holder.likesCount.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
final int position = holder.getAdapterPosition();
if (position == RecyclerView.NO_POSITION) return;
final Comment comment = getComment(position);
final Call<List<Like>> commentLikesCall =
dribbblePrefs.getApi().getCommentLikes(shot.id, comment.id);
commentLikesCall.enqueue(new Callback<List<Like>>() {
@Override
public void onResponse(Call<List<Like>> call,
Response<List<Like>> response) {
// TODO something better than this.
StringBuilder sb = new StringBuilder("Liked by:\n\n");
for (Like like : response.body()) {
if (like.user != null) {
sb.append("@");
sb.append(like.user.username);
sb.append("\n");
}
}
Toast.makeText(getApplicationContext(), sb.toString(), Toast
.LENGTH_SHORT).show();
}
@Override
public void onFailure(Call<List<Like>> call, Throwable t) { }
});
}
});
return holder;
}
private void bindComment(CommentViewHolder holder, Comment comment) {
final int position = holder.getAdapterPosition();
final boolean isExpanded = position == expandedCommentPosition;
Glide.with(DribbbleShot.this)
.load(comment.user.getHighQualityAvatarUrl())
.transform(circleTransform)
.placeholder(R.drawable.avatar_placeholder)
.override(largeAvatarSize, largeAvatarSize)
.into(holder.avatar);
holder.author.setText(comment.user.name.toLowerCase());
holder.author.setOriginalPoster(isOP(comment.user.id));
holder.timeAgo.setText(comment.created_at == null ? "" :
DateUtils.getRelativeTimeSpanString(comment.created_at.getTime(),
System.currentTimeMillis(),
DateUtils.SECOND_IN_MILLIS)
.toString().toLowerCase());
HtmlUtils.setTextWithNiceLinks(holder.commentBody,
comment.getParsedBody(holder.commentBody));
holder.likeHeart.setChecked(comment.liked != null && comment.liked);
holder.likeHeart.setEnabled(comment.user.id != dribbblePrefs.getUserId());
holder.likesCount.setText(String.valueOf(comment.likes_count));
setExpanded(holder, isExpanded);
}
private void setExpanded(CommentViewHolder holder, boolean isExpanded) {
holder.itemView.setActivated(isExpanded);
holder.reply.setVisibility((isExpanded && allowComment) ? View.VISIBLE : View.GONE);
holder.likeHeart.setVisibility(isExpanded ? View.VISIBLE : View.GONE);
holder.likesCount.setVisibility(isExpanded ? View.VISIBLE : View.GONE);
}
private void bindPartialCommentChange(
CommentViewHolder holder, int position, List<Object> partialChangePayloads) {
// for certain changes we don't need to rebind data, just update some view state
if ((partialChangePayloads.contains(EXPAND)
|| partialChangePayloads.contains(COLLAPSE))
|| partialChangePayloads.contains(REPLY)) {
setExpanded(holder, position == expandedCommentPosition);
} else if (partialChangePayloads.contains(COMMENT_LIKE)) {
return; // nothing to do
} else {
onBindViewHolder(holder, position);
}
}
private void hideLoadingIndicator() {
if (!loading) return;
loading = false;
notifyItemRemoved(1);
}
}
static class SimpleViewHolder extends RecyclerView.ViewHolder {
SimpleViewHolder(View itemView) {
super(itemView);
}
}
static class CommentViewHolder extends RecyclerView.ViewHolder {
@BindView(R.id.player_avatar) ImageView avatar;
@BindView(R.id.comment_author) AuthorTextView author;
@BindView(R.id.comment_time_ago) TextView timeAgo;
@BindView(R.id.comment_text) TextView commentBody;
@BindView(R.id.comment_reply) ImageButton reply;
@BindView(R.id.comment_like) CheckableImageButton likeHeart;
@BindView(R.id.comment_likes_count) TextView likesCount;
CommentViewHolder(View itemView) {
super(itemView);
ButterKnife.bind(this, itemView);
}
}
/**
* A {@link RecyclerView.ItemAnimator} which allows disabling move animations. RecyclerView
* does not like animating item height changes. {@link android.transition.ChangeBounds} allows
* this but in order to simultaneously collapse one item and expand another, we need to run the
* Transition on the entire RecyclerView. As such it attempts to move views around. This
* custom item animator allows us to stop RecyclerView from trying to handle this for us while
* the transition is running.
*/
static class CommentAnimator extends SlideInItemAnimator {
private boolean animateMoves = false;
CommentAnimator() {
super();
}
void setAnimateMoves(boolean animateMoves) {
this.animateMoves = animateMoves;
}
@Override
public boolean animateMove(
RecyclerView.ViewHolder holder, int fromX, int fromY, int toX, int toY) {
if (!animateMoves) {
dispatchMoveFinished(holder);
return false;
}
return super.animateMove(holder, fromX, fromY, toX, toY);
}
}
}