From e46f79b456dac4be6d0a7a10a16828fe71519ff6 Mon Sep 17 00:00:00 2001 From: Nick Butcher Date: Mon, 9 Nov 2015 15:42:17 +0000 Subject: [PATCH 1/2] Improved Designer News experience. * Updated description header to allow upvoting & sharing * Added ability to comment on the story --- .../api/designernews/DesignerNewsService.java | 10 +- .../data/api/designernews/model/Comment.java | 26 +- .../data/api/designernews/model/Story.java | 26 +- .../io/plaidapp/ui/DesignerNewsStory.java | 358 +++++++++++++++--- .../ui/widget/CollapsingTitleLayout.java | 6 +- app/src/main/res/animator/upvote.xml | 24 ++ .../res/drawable/ic_heart_full_24dp_grey.xml | 3 +- app/src/main/res/drawable/ic_no_comments.xml | 16 +- app/src/main/res/drawable/ic_upvote.xml | 45 +++ app/src/main/res/drawable/ic_upvote_anim.xml | 47 +++ ...b_up.xml => ic_upvote_empty_24dp_grey.xml} | 20 +- .../drawable/ic_upvote_filled_24dp_blue.xml | 32 ++ .../drawable/ic_upvote_filled_24dp_white.xml | 32 ++ .../activity_designer_news_story.xml | 6 +- .../designer_news_story_description.xml | 69 ---- .../designer_news_story_title_toolbar.xml | 37 ++ .../layout/activity_designer_news_story.xml | 5 +- .../activity_post_new_designer_news_story.xml | 2 +- .../layout/designer_news_enter_comment.xml | 58 +++ .../res/layout/designer_news_no_comments.xml | 6 +- .../designer_news_story_description.xml | 86 ++++- .../designer_news_story_title_toolbar.xml | 22 ++ .../res/layout/dribbble_enter_comment.xml | 3 +- app/src/main/res/values/colors.xml | 5 +- app/src/main/res/values/paths.xml | 2 + app/src/main/res/values/strings.xml | 12 +- app/src/main/res/values/styles.xml | 7 +- 27 files changed, 778 insertions(+), 187 deletions(-) create mode 100644 app/src/main/res/animator/upvote.xml create mode 100644 app/src/main/res/drawable/ic_upvote.xml create mode 100644 app/src/main/res/drawable/ic_upvote_anim.xml rename app/src/main/res/drawable/{ic_thumb_up.xml => ic_upvote_empty_24dp_grey.xml} (55%) create mode 100644 app/src/main/res/drawable/ic_upvote_filled_24dp_blue.xml create mode 100644 app/src/main/res/drawable/ic_upvote_filled_24dp_white.xml delete mode 100644 app/src/main/res/layout-land/designer_news_story_description.xml create mode 100644 app/src/main/res/layout-land/designer_news_story_title_toolbar.xml create mode 100644 app/src/main/res/layout/designer_news_enter_comment.xml create mode 100644 app/src/main/res/layout/designer_news_story_title_toolbar.xml diff --git a/app/src/main/java/io/plaidapp/data/api/designernews/DesignerNewsService.java b/app/src/main/java/io/plaidapp/data/api/designernews/DesignerNewsService.java index 6a38226b6..ed8ccd624 100644 --- a/app/src/main/java/io/plaidapp/data/api/designernews/DesignerNewsService.java +++ b/app/src/main/java/io/plaidapp/data/api/designernews/DesignerNewsService.java @@ -19,13 +19,15 @@ import java.util.Map; import io.plaidapp.data.api.designernews.model.AccessToken; +import io.plaidapp.data.api.designernews.model.Comment; import io.plaidapp.data.api.designernews.model.NewStoryRequest; import io.plaidapp.data.api.designernews.model.StoriesResponse; -import io.plaidapp.data.api.designernews.model.Story; import io.plaidapp.data.api.designernews.model.StoryResponse; import io.plaidapp.data.api.designernews.model.UserResponse; import retrofit.Callback; import retrofit.http.Body; +import retrofit.http.DELETE; +import retrofit.http.Field; import retrofit.http.FieldMap; import retrofit.http.FormUrlEncoded; import retrofit.http.GET; @@ -77,4 +79,10 @@ void upvoteStory(@Path("id") long storyId, void postStory(@Body NewStoryRequest story, Callback callback); + @FormUrlEncoded + @POST("/api/v1/stories/{id}/reply") + void comment(@Path("id") long storyId, + @Field("comment[body]") String comment, + Callback callback); + } diff --git a/app/src/main/java/io/plaidapp/data/api/designernews/model/Comment.java b/app/src/main/java/io/plaidapp/data/api/designernews/model/Comment.java index 594c4683a..b9c3eb79f 100644 --- a/app/src/main/java/io/plaidapp/data/api/designernews/model/Comment.java +++ b/app/src/main/java/io/plaidapp/data/api/designernews/model/Comment.java @@ -28,18 +28,6 @@ */ public class Comment implements Parcelable { - @SuppressWarnings("unused") - public static final Parcelable.Creator CREATOR = new Parcelable.Creator() { - @Override - public Comment createFromParcel(Parcel in) { - return new Comment(in); - } - - @Override - public Comment[] newArray(int size) { - return new Comment[size]; - } - }; public final long id; public final String body; public final String body_html; @@ -120,4 +108,18 @@ public void writeToParcel(Parcel dest, int flags) { dest.writeList(comments); } } + + @SuppressWarnings("unused") + public static final Parcelable.Creator CREATOR = new Parcelable.Creator() { + @Override + public Comment createFromParcel(Parcel in) { + return new Comment(in); + } + + @Override + public Comment[] newArray(int size) { + return new Comment[size]; + } + }; + } diff --git a/app/src/main/java/io/plaidapp/data/api/designernews/model/Story.java b/app/src/main/java/io/plaidapp/data/api/designernews/model/Story.java index be025c2b1..85b95bc55 100644 --- a/app/src/main/java/io/plaidapp/data/api/designernews/model/Story.java +++ b/app/src/main/java/io/plaidapp/data/api/designernews/model/Story.java @@ -30,18 +30,6 @@ */ public class Story extends PlaidItem implements Parcelable { - @SuppressWarnings("unused") - public static final Parcelable.Creator CREATOR = new Parcelable.Creator() { - @Override - public Story createFromParcel(Parcel in) { - return new Story(in); - } - - @Override - public Story[] newArray(int size) { - return new Story[size]; - } - }; public final String comment; public final String comment_html; public final int comment_count; @@ -141,4 +129,18 @@ public void writeToParcel(Parcel dest, int flags) { dest.writeList(comments); } } + + @SuppressWarnings("unused") + public static final Parcelable.Creator CREATOR = new Parcelable.Creator() { + @Override + public Story createFromParcel(Parcel in) { + return new Story(in); + } + + @Override + public Story[] newArray(int size) { + return new Story[size]; + } + }; + } diff --git a/app/src/main/java/io/plaidapp/ui/DesignerNewsStory.java b/app/src/main/java/io/plaidapp/ui/DesignerNewsStory.java index bd85ccdf4..849d7455c 100644 --- a/app/src/main/java/io/plaidapp/ui/DesignerNewsStory.java +++ b/app/src/main/java/io/plaidapp/ui/DesignerNewsStory.java @@ -17,10 +17,12 @@ 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; @@ -34,26 +36,35 @@ 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.Collections; import java.util.List; import butterknife.Bind; @@ -62,42 +73,63 @@ 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.ColorUtils; 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 CollapsingTitleLayout collapsingToolbar; - private PinnedOffsetView toolbarBackground; + private DesignerNewsPrefs designerNewsPrefs; + private DesignerNewsService designerNewsApi; private Bypass markdown; private CustomTabActivityHelper customTab; + private CircleTransform circleTransform; @Override protected void onCreate(Bundle savedInstanceState) { @@ -124,24 +156,28 @@ public void onDragDismissed() { .setPreImageLinebreakHeight(4) //dps .setBlockQuoteIndentSize(TypedValue.COMPLEX_UNIT_DIP, 2f) .setBlockQuoteTextColor(ContextCompat.getColor(this, R.color.designer_news_quote))); + circleTransform = new CircleTransform(this); - View storyDescription = getLayoutInflater().inflate(R.layout - .designer_news_story_description, commentsList, false); - bindDescription(storyDescription); + 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 (toolbar != null) { // portrait: collapsing toolbar - collapsingToolbar = (CollapsingTitleLayout) findViewById(R.id.backdrop_toolbar); + if (collapsingToolbar != null) { // portrait: collapsing toolbar collapsingToolbar.setTitle(story.title); - toolbarBackground = (PinnedOffsetView) findViewById(R.id.story_title_background); - commentsList.addOnScrollListener(headerScrollListener); - collapsingToolbar.addOnLayoutChangeListener(titlebarLayout); } else { // landscape: scroll toolbar with content - toolbar = (Toolbar) storyDescription.findViewById(R.id.story_toolbar); + 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 @@ -150,16 +186,21 @@ public void onClick(View view) { } }); + 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 wrapped = new ArrayList<>(story.comment_count); addComments(story.comments, 0, wrapped); - commentsList.setAdapter(new DesignerNewsCommentsAdapter(storyDescription, wrapped)); + commentsAdapter = + new DesignerNewsCommentsAdapter(header, wrapped, enterCommentView); + commentsList.setAdapter(commentsAdapter); } else { - commentsList.setAdapter( - new DesignerNewsCommentsAdapter(storyDescription, Collections.EMPTY_LIST)); + commentsAdapter = new DesignerNewsCommentsAdapter( + header, new ArrayList(0), enterCommentView); + commentsList.setAdapter(commentsAdapter); } customTab = new CustomTabActivityHelper(); customTab.setConnectionCallback(customTabConnect); @@ -181,6 +222,17 @@ protected void onResume() { 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); @@ -213,7 +265,8 @@ public static CustomTabsIntent.Builder getCustomTabIntent(@NonNull Context conte 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_thumb_up), + .setActionButton(ImageUtils.vectorToBitmap(context, + R.drawable.ic_upvote_filled_24dp_white), context.getString(R.string.upvote_story), pendingIntent, false) @@ -232,31 +285,90 @@ public void onCustomTabsConnected() { @Override public void onCustomTabsDisconnected() { } }; - private int gridScrollY = 0; private RecyclerView.OnScrollListener headerScrollListener = new RecyclerView.OnScrollListener() { @Override public void onScrolled(RecyclerView recyclerView, int dx, int dy) { - gridScrollY += dy; - collapsingToolbar.setScrollPixelOffset(gridScrollY); - toolbarBackground.setOffset(-gridScrollY); + checkScrollDependentUi(); } }; + private void checkScrollDependentUi() { + final int firstVisibleItemPosition = layoutManager.findFirstVisibleItemPosition(); + + // feed scroll events to the header + if (collapsingToolbar != null && firstVisibleItemPosition == 0) { + final int headerScroll = header.getTop() - commentsList.getPaddingTop(); + collapsingToolbar.setScrollPixelOffset(-headerScroll); + toolbarBackground.setOffset(headerScroll); + } + + checkFabVisibility(firstVisibleItemPosition); + } + + private boolean fabIsVisible = true; + private void checkFabVisibility(final int firstVisibleItemPosition) { + // 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 lastVisibleItemPosition = layoutManager.findLastVisibleItemPosition(); + final int footerPosition = commentsAdapter.getItemCount() - 1; + final boolean footerVisible = lastVisibleItemPosition == footerPosition; + + final boolean fabShouldBeVisible = + (firstVisibleItemPosition == 0 && !enterCommentFocused) || !footerVisible; + + if (!fabShouldBeVisible && fabIsVisible) { + fab.animate() + .scaleX(0f) + .scaleY(0f) + .alpha(0.6f) + .setDuration(200L) + .withLayer() + .setInterpolator(AnimationUtils.loadInterpolator(this, + android.R.interpolator.fast_out_linear_in)) + .setListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + fab.setVisibility(View.GONE); + } + }) + .start(); + } else if (fabShouldBeVisible && !fabIsVisible) { + fab.setVisibility(View.VISIBLE); + fab.animate() + .scaleX(1f) + .scaleY(1f) + .alpha(1f) + .setDuration(200L) + .withLayer() + .setInterpolator(AnimationUtils.loadInterpolator(this, + android.R.interpolator.linear_out_slow_in)) + .setListener(null) + .start(); + ImeUtils.hideIme(enterComment); + } + fabIsVisible = fabShouldBeVisible; + } + // 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) { - commentsList.setPaddingRelative(commentsList.getPaddingStart(), - collapsingToolbar.getHeight(), - commentsList.getPaddingEnd(), - commentsList.getPaddingBottom()); - commentsList.scrollToPosition(0); - gridScrollY = 0; - collapsingToolbar.setScrollPixelOffset(0); - toolbarBackground.setOffset(0); + if ((bottom - top) != (oldBottom - oldTop)) { + commentsList.setPaddingRelative(commentsList.getPaddingStart(), + collapsingToolbar.getHeight(), + commentsList.getPaddingEnd(), + commentsList.getPaddingBottom()); + commentsList.scrollToPosition(0); + collapsingToolbar.setScrollPixelOffset(0); + toolbarBackground.setOffset(0); + } + checkScrollDependentUi(); } }; @@ -283,6 +395,7 @@ public void onSharedElementEnd(List sharedElementNames, List sharedElementSnapshots) { // force a remeasure to account for shared element shenanigans if (collapsingToolbar != null) { + collapsingToolbar.addOnLayoutChangeListener(titlebarLayout); collapsingToolbar.measure( View.MeasureSpec.makeMeasureSpec(draggableFrame.getWidth(), View.MeasureSpec.AT_MOST), @@ -356,15 +469,8 @@ private void doFabExpand() { show.start(); } - private void bindDescription(View storyDescription) { - TextView storyPoster = (TextView) storyDescription.findViewById(R.id.story_poster); - storyPoster.setText(DateUtils.getRelativeTimeSpanString(story.created_at.getTime(), - System.currentTimeMillis(), - DateUtils.SECOND_IN_MILLIS) - + " by " + story.user_display_name - + ", " + story.user_job); - - final TextView storyComment = (TextView) storyDescription.findViewById(R.id.story_comment); + 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() { @@ -377,8 +483,139 @@ public void loadImage(String src, ImageLoadingSpan loadingSpan) { .into(new ImageSpanTarget(storyComment, loadingSpan)); } })); + } else { + storyComment.setVisibility(View.GONE); } - storyComment.setVisibility(TextUtils.isEmpty(story.comment) ? View.GONE : View.VISIBLE); + + 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() { + @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() { + @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 comments, int depth, List wrapped) { @@ -391,6 +628,16 @@ private void addComments(List comments, int depth, List comments; + private View footer; - DesignerNewsCommentsAdapter(@NonNull View header, @NonNull List comments) { + DesignerNewsCommentsAdapter(@NonNull View header, + @NonNull List comments, + @NonNull View footer) { this.header = header; this.comments = comments; + this.footer = footer; } private boolean hasComments() { @@ -431,6 +683,9 @@ private boolean hasComments() { 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; } @@ -448,6 +703,8 @@ public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType return new NoCommentsHolder( getLayoutInflater().inflate( R.layout.designer_news_no_comments, parent, false)); + case TYPE_FOOTER: + return new FooterHolder(footer); } return null; } @@ -456,7 +713,7 @@ public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType 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 views + } // nothing to bind for header / no comment / footer views } private void bindComment(final CommentHolder holder, final ThreadedComment comment) { @@ -486,8 +743,16 @@ public void loadImage(String src, ImageLoadingSpan loadingSpan) { @Override public int getItemCount() { - return hasComments() ? comments.size() + 1 // add one for header - : 2; // header + no comments view + // 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()); } } @@ -517,4 +782,11 @@ public NoCommentsHolder(View itemView) { super(itemView); } } + + /* package */ static class FooterHolder extends RecyclerView.ViewHolder { + + public FooterHolder(View itemView) { + super(itemView); + } + } } diff --git a/app/src/main/java/io/plaidapp/ui/widget/CollapsingTitleLayout.java b/app/src/main/java/io/plaidapp/ui/widget/CollapsingTitleLayout.java index 4849cbc5f..ddab4106f 100644 --- a/app/src/main/java/io/plaidapp/ui/widget/CollapsingTitleLayout.java +++ b/app/src/main/java/io/plaidapp/ui/widget/CollapsingTitleLayout.java @@ -73,6 +73,7 @@ public class CollapsingTitleLayout extends FrameLayout { private CollapsingTextHelper collapsingText; private StaticLayout layout; private Line[] lines; + private int calculatedWithWidth; private int lineCount; public CollapsingTitleLayout(Context context) { @@ -195,7 +196,9 @@ public void draw(Canvas canvas) { protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { final int width = MeasureSpec.getSize(widthMeasureSpec); - recalculate(width); + if (width != calculatedWithWidth) { + recalculate(width); + } final int desiredHeight = getDesiredHeight(); int height; @@ -295,6 +298,7 @@ private void recalculate(int width) { lineBottomScrollOffset + fadeDistance); } } + calculatedWithWidth = width; } private void createLayout(int width, int lineSpacingAdd) { diff --git a/app/src/main/res/animator/upvote.xml b/app/src/main/res/animator/upvote.xml new file mode 100644 index 000000000..49bb88bad --- /dev/null +++ b/app/src/main/res/animator/upvote.xml @@ -0,0 +1,24 @@ + + + + diff --git a/app/src/main/res/drawable/ic_heart_full_24dp_grey.xml b/app/src/main/res/drawable/ic_heart_full_24dp_grey.xml index 55dcaa1df..faa38aff0 100644 --- a/app/src/main/res/drawable/ic_heart_full_24dp_grey.xml +++ b/app/src/main/res/drawable/ic_heart_full_24dp_grey.xml @@ -1,5 +1,4 @@ - - + + + android:fillColor="@color/grey_300" /> + diff --git a/app/src/main/res/drawable/ic_upvote.xml b/app/src/main/res/drawable/ic_upvote.xml new file mode 100644 index 000000000..94a011955 --- /dev/null +++ b/app/src/main/res/drawable/ic_upvote.xml @@ -0,0 +1,45 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/ic_upvote_anim.xml b/app/src/main/res/drawable/ic_upvote_anim.xml new file mode 100644 index 000000000..8c8d47f73 --- /dev/null +++ b/app/src/main/res/drawable/ic_upvote_anim.xml @@ -0,0 +1,47 @@ + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/ic_thumb_up.xml b/app/src/main/res/drawable/ic_upvote_empty_24dp_grey.xml similarity index 55% rename from app/src/main/res/drawable/ic_thumb_up.xml rename to app/src/main/res/drawable/ic_upvote_empty_24dp_grey.xml index afca5f67e..a2ffbf47c 100644 --- a/app/src/main/res/drawable/ic_thumb_up.xml +++ b/app/src/main/res/drawable/ic_upvote_empty_24dp_grey.xml @@ -1,5 +1,4 @@ - - + + + android:pathData="@string/path_upvote" + android:strokeColor="@color/text_secondary_dark" + android:strokeWidth="2" + android:strokeLineJoin="round" /> + diff --git a/app/src/main/res/drawable/ic_upvote_filled_24dp_blue.xml b/app/src/main/res/drawable/ic_upvote_filled_24dp_blue.xml new file mode 100644 index 000000000..37a7e65ab --- /dev/null +++ b/app/src/main/res/drawable/ic_upvote_filled_24dp_blue.xml @@ -0,0 +1,32 @@ + + + + + + + + diff --git a/app/src/main/res/drawable/ic_upvote_filled_24dp_white.xml b/app/src/main/res/drawable/ic_upvote_filled_24dp_white.xml new file mode 100644 index 000000000..c5c4f9072 --- /dev/null +++ b/app/src/main/res/drawable/ic_upvote_filled_24dp_white.xml @@ -0,0 +1,32 @@ + + + + + + + + diff --git a/app/src/main/res/layout-land/activity_designer_news_story.xml b/app/src/main/res/layout-land/activity_designer_news_story.xml index 5f8acfb58..6358d4597 100644 --- a/app/src/main/res/layout-land/activity_designer_news_story.xml +++ b/app/src/main/res/layout-land/activity_designer_news_story.xml @@ -1,3 +1,4 @@ + + see layout-land/designer_news_story_description --> + android:scrollbars="vertical" /> diff --git a/app/src/main/res/layout-land/designer_news_story_description.xml b/app/src/main/res/layout-land/designer_news_story_description.xml deleted file mode 100644 index 7ceda433b..000000000 --- a/app/src/main/res/layout-land/designer_news_story_description.xml +++ /dev/null @@ -1,69 +0,0 @@ - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/layout-land/designer_news_story_title_toolbar.xml b/app/src/main/res/layout-land/designer_news_story_title_toolbar.xml new file mode 100644 index 000000000..2571e1d41 --- /dev/null +++ b/app/src/main/res/layout-land/designer_news_story_title_toolbar.xml @@ -0,0 +1,37 @@ + + + + + + + + diff --git a/app/src/main/res/layout/activity_designer_news_story.xml b/app/src/main/res/layout/activity_designer_news_story.xml index 0a184e0f8..fd5273656 100644 --- a/app/src/main/res/layout/activity_designer_news_story.xml +++ b/app/src/main/res/layout/activity_designer_news_story.xml @@ -38,10 +38,9 @@ android:layout_width="match_parent" android:layout_height="match_parent" android:paddingTop="@dimen/extended_height_toolbar" - android:paddingBottom="@dimen/padding_room_for_fab" + android:paddingBottom="@dimen/padding_normal" android:clipToPadding="false" - android:scrollbars="vertical" - app:layoutManager="android.support.v7.widget.LinearLayoutManager" /> + android:scrollbars="vertical" /> diff --git a/app/src/main/res/layout/designer_news_enter_comment.xml b/app/src/main/res/layout/designer_news_enter_comment.xml new file mode 100644 index 000000000..c3522197f --- /dev/null +++ b/app/src/main/res/layout/designer_news_enter_comment.xml @@ -0,0 +1,58 @@ + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/designer_news_no_comments.xml b/app/src/main/res/layout/designer_news_no_comments.xml index ec0b5a65d..78ebc373f 100644 --- a/app/src/main/res/layout/designer_news_no_comments.xml +++ b/app/src/main/res/layout/designer_news_no_comments.xml @@ -22,8 +22,10 @@ android:id="@android:id/empty" android:layout_width="match_parent" android:layout_height="wrap_content" - android:layout_marginTop="@dimen/spacing_huge" - android:layout_marginBottom="@dimen/spacing_huge" + android:paddingStart="@dimen/padding_normal" + android:paddingTop="@dimen/spacing_large" + android:paddingEnd="@dimen/padding_normal" + android:paddingBottom="@dimen/spacing_large" android:clickable="false" android:drawableTop="@drawable/ic_no_comments" android:gravity="center" diff --git a/app/src/main/res/layout/designer_news_story_description.xml b/app/src/main/res/layout/designer_news_story_description.xml index ebaee0007..645667250 100644 --- a/app/src/main/res/layout/designer_news_story_description.xml +++ b/app/src/main/res/layout/designer_news_story_description.xml @@ -1,5 +1,4 @@ - + - \ No newline at end of file + + + + +