From 2048e69ef233a816f5d53b3c23c56c7262c0168f Mon Sep 17 00:00:00 2001 From: Nick Butcher Date: Thu, 1 Sep 2016 15:51:38 +0100 Subject: [PATCH] Better Dribbble enter/exit transitions. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit – Remove crufty anim code & replace with shiny transitions – Better enter animation with a distance staggered slide – Animate comment entry after loading --- .../java/io/plaidapp/ui/DribbbleShot.java | 127 ++---------------- .../ui/transitions/BackgroundFade.java | 61 +++++++++ .../DeparallaxingChangeBounds.java | 6 +- .../transitions/StaggeredDistanceSlide.java | 30 ++++- .../ui/widget/ParallaxScrimageView.java | 20 +-- .../io/plaidapp/util/TransitionUtils.java | 52 ++++--- app/src/main/res/animator/app_bar_pin.xml | 17 +-- .../layout/activity_designer_news_story.xml | 8 +- .../res/layout/activity_dribbble_shot.xml | 22 ++- .../designer_news_story_description.xml | 10 +- .../res/layout/dribbble_enter_comment.xml | 1 + .../res/layout/dribbble_shot_description.xml | 8 +- .../transition/designer_news_story_enter.xml | 3 +- .../res/transition/dribbble_shot_enter.xml | 79 +++++++++-- .../res/transition/dribbble_shot_return.xml | 36 +++-- .../transition/dribbble_shot_shared_enter.xml | 2 +- .../dribbble_shot_shared_return.xml | 11 +- 17 files changed, 288 insertions(+), 205 deletions(-) create mode 100644 app/src/main/java/io/plaidapp/ui/transitions/BackgroundFade.java diff --git a/app/src/main/java/io/plaidapp/ui/DribbbleShot.java b/app/src/main/java/io/plaidapp/ui/DribbbleShot.java index dd774a33f..88119e99e 100644 --- a/app/src/main/java/io/plaidapp/ui/DribbbleShot.java +++ b/app/src/main/java/io/plaidapp/ui/DribbbleShot.java @@ -16,10 +16,6 @@ package io.plaidapp.ui; -import android.animation.Animator; -import android.animation.AnimatorListenerAdapter; -import android.animation.ObjectAnimator; -import android.animation.PropertyValuesHolder; import android.animation.ValueAnimator; import android.annotation.TargetApi; import android.app.Activity; @@ -51,7 +47,6 @@ import android.view.View; import android.view.ViewGroup; import android.view.ViewTreeObserver; -import android.view.animation.Interpolator; import android.widget.Button; import android.widget.EditText; import android.widget.ImageButton; @@ -94,7 +89,6 @@ import io.plaidapp.util.HtmlUtils; import io.plaidapp.util.ImeUtils; import io.plaidapp.util.TransitionUtils; -import io.plaidapp.util.ViewOffsetHelper; import io.plaidapp.util.ViewUtils; import io.plaidapp.util.customtabs.CustomTabActivityHelper; import io.plaidapp.util.glide.CircleTransform; @@ -105,7 +99,6 @@ import retrofit2.Response; import static io.plaidapp.util.AnimUtils.getFastOutSlowInInterpolator; -import static io.plaidapp.util.AnimUtils.getLinearOutSlowInInterpolator; public class DribbbleShot extends Activity { @@ -153,7 +146,6 @@ protected void onCreate(final Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_dribbble_shot); dribbblePrefs = DribbblePrefs.get(this); - getWindow().getSharedElementReturnTransition().addListener(shotReturnHomeListener); circleTransform = new CircleTransform(this); ButterKnife.bind(this); shotDescription = getLayoutInflater().inflate(R.layout.dribbble_shot_description, @@ -285,13 +277,12 @@ private void bindShot(final boolean postponeEnterTransition) { shotSpacer.setOnClickListener(shotClick); if (postponeEnterTransition) postponeEnterTransition(); - imageView.getViewTreeObserver().addOnPreDrawListener(new ViewTreeObserver - .OnPreDrawListener() { + imageView.getViewTreeObserver().addOnPreDrawListener( + new ViewTreeObserver.OnPreDrawListener() { @Override public boolean onPreDraw() { imageView.getViewTreeObserver().removeOnPreDrawListener(this); calculateFabPosition(); - enterAnimation(); if (postponeEnterTransition) startPostponedEnterTransition(); return true; } @@ -387,6 +378,8 @@ public void onClick(View v) { 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); @@ -395,8 +388,6 @@ public void onClick(View v) { res.getDimensionPixelSize(R.dimen.divider_height), res.getDimensionPixelSize(R.dimen.keyline_1), ContextCompat.getColor(this, R.color.divider))); - commentAnimator = new CommentAnimator(); - commentsList.setItemAnimator(commentAnimator); if (shot.comments_count != 0) { loadComments(); } @@ -617,28 +608,6 @@ public void onClick(View view) { } }; - private Transition.TransitionListener shotReturnHomeListener = - new TransitionUtils.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); - // fade out the "toolbar" & list as we don't want them to be visible during return - // animation - back.animate() - .alpha(0f) - .setDuration(100) - .setInterpolator(getLinearOutSlowInInterpolator(DribbbleShot.this)); - imageView.setElevation(1f); - back.setElevation(0f); - commentsList.animate() - .alpha(0f) - .setDuration(50) - .setInterpolator(getLinearOutSlowInInterpolator(DribbbleShot.this)); - } - }; - private void loadComments() { final Call> commentsCall = dribbblePrefs.getApi().getComments(shot.id, 0, DribbbleService.PER_PAGE_MAX); @@ -671,82 +640,6 @@ private void calculateFabPosition() { fab.setMinOffset(imageView.getMinimumHeight() - (fab.getHeight() / 2)); } - /** - * Animate in the title, description and author – can't do this in the window enter transition - * as they get added to the RecyclerView later so do it manually. Also animate the FAB - * translation here so that it plays nicely with #calculateFabPosition - **/ - private void enterAnimation() { - Interpolator interp = getFastOutSlowInInterpolator(this); - int offset = title.getHeight(); - viewEnterAnimation(title, offset, interp); - if (description.getVisibility() == View.VISIBLE) { - offset *= 1.5f; - viewEnterAnimation(description, offset, interp); - } - offset *= 1.5f; - fabEnterAnimation(interp, offset); - offset *= 1.5f; - viewEnterAnimation(shotActions, offset, interp); - offset *= 1.5f; - viewEnterAnimation(playerName, offset, interp); - viewEnterAnimation(playerAvatar, offset, interp); - viewEnterAnimation(shotTimeAgo, offset, interp); - back.animate() - .alpha(1f) - .setDuration(600L) - .setInterpolator(interp) - .start(); - } - - private void viewEnterAnimation(View view, float offset, Interpolator interp) { - view.setTranslationY(offset); - view.setAlpha(0.6f); - view.animate() - .translationY(0f) - .alpha(1f) - .setDuration(600L) - .setInterpolator(interp) - .setListener(null) - .start(); - } - - private void fabEnterAnimation(Interpolator interp, int offset) { - // FAB should enter upwards with content and also scale/fade. As the FAB uses - // translationY to position itself on the title seam, we can animating this property. - // Instead animate the view's layout position (which is a bit more involved). - final ViewOffsetHelper fabOffset = new ViewOffsetHelper(fab); - final View.OnLayoutChangeListener fabLayout = 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) { - fabOffset.onViewLayout(); - } - }; - - fab.addOnLayoutChangeListener(fabLayout); - fabOffset.setTopAndBottomOffset(offset); - Animator fabMovement = ObjectAnimator.ofInt(fabOffset, ViewOffsetHelper.OFFSET_Y, 0); - fabMovement.setDuration(600L); - fabMovement.setInterpolator(interp); - fabMovement.addListener(new AnimatorListenerAdapter() { - @Override - public void onAnimationEnd(Animator animation) { - fab.removeOnLayoutChangeListener(fabLayout); - } - }); - fabMovement.start(); - - Animator showFab = ObjectAnimator.ofPropertyValuesHolder(fab, - PropertyValuesHolder.ofFloat(View.ALPHA, 0f, 1f), - PropertyValuesHolder.ofFloat(View.SCALE_X, 0f, 1f), - PropertyValuesHolder.ofFloat(View.SCALE_Y, 0f, 1f)); - showFab.setStartDelay(300L); - showFab.setDuration(300L); - showFab.setInterpolator(getLinearOutSlowInInterpolator(this)); - showFab.start(); - } - private void doLike() { performingLike = true; if (fab.isChecked()) { @@ -874,10 +767,10 @@ public void onTransitionEnd(Transition transition) { } void addComments(List newComments) { - comments.addAll(newComments); - loading = false; + hideLoadingIndicator(); noComments = false; - notifyDataSetChanged(); + comments.addAll(newComments); + notifyItemRangeInserted(1, newComments.size()); } void removeCommentingFooter() { @@ -1164,6 +1057,12 @@ private void bindPartialCommentChange( private Comment getComment(int adapterPosition) { return comments.get(adapterPosition - 1); // description } + + private void hideLoadingIndicator() { + if (!loading) return; + loading = false; + notifyItemRemoved(1); + } } /* package */ static class SimpleViewHolder extends RecyclerView.ViewHolder { diff --git a/app/src/main/java/io/plaidapp/ui/transitions/BackgroundFade.java b/app/src/main/java/io/plaidapp/ui/transitions/BackgroundFade.java new file mode 100644 index 000000000..fd532ef72 --- /dev/null +++ b/app/src/main/java/io/plaidapp/ui/transitions/BackgroundFade.java @@ -0,0 +1,61 @@ +/* + * Copyright 2016 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.transitions; + +import android.animation.Animator; +import android.animation.ObjectAnimator; +import android.content.Context; +import android.graphics.drawable.Drawable; +import android.support.annotation.Keep; +import android.transition.TransitionValues; +import android.transition.Visibility; +import android.util.AttributeSet; +import android.view.View; +import android.view.ViewGroup; + +import io.plaidapp.util.ViewUtils; + +/** + * A transition which fades in/out the background {@link Drawable} of a View. + */ +public class BackgroundFade extends Visibility { + + public BackgroundFade() { + super(); + } + + @Keep + public BackgroundFade(Context context, AttributeSet attrs) { + super(context, attrs); + } + + @Override + public Animator onAppear(ViewGroup sceneRoot, View view, + TransitionValues startValues, TransitionValues endValues) { + if (view == null || view.getBackground() == null) return null; + Drawable background = view.getBackground(); + background.setAlpha(0); + return ObjectAnimator.ofInt(background, ViewUtils.DRAWABLE_ALPHA, 0, 255); + } + + @Override + public Animator onDisappear(ViewGroup sceneRoot, View view, + TransitionValues startValues, TransitionValues endValues) { + if (view == null || view.getBackground() == null) return null; + return ObjectAnimator.ofInt(view.getBackground(), ViewUtils.DRAWABLE_ALPHA, 0); + } +} diff --git a/app/src/main/java/io/plaidapp/ui/transitions/DeparallaxingChangeBounds.java b/app/src/main/java/io/plaidapp/ui/transitions/DeparallaxingChangeBounds.java index 74a0ba5b0..fa51d38df 100644 --- a/app/src/main/java/io/plaidapp/ui/transitions/DeparallaxingChangeBounds.java +++ b/app/src/main/java/io/plaidapp/ui/transitions/DeparallaxingChangeBounds.java @@ -50,7 +50,7 @@ public void captureEndValues(TransitionValues transitionValues) { // as we're going to remove the offset (which drives the parallax) we need to // compensate for this by adjusting the target bounds. Rect bounds = (Rect) transitionValues.values.get(PROPNAME_BOUNDS); - bounds.offset(0, -psv.getOffset()); + bounds.offset(0, psv.getOffset()); transitionValues.values.put(PROPNAME_BOUNDS, bounds); } @@ -58,9 +58,9 @@ public void captureEndValues(TransitionValues transitionValues) { public Animator createAnimator(ViewGroup sceneRoot, TransitionValues startValues, TransitionValues endValues) { - if (startValues == null || endValues == null - || !(endValues.view instanceof ParallaxScrimageView)) return null; Animator changeBounds = super.createAnimator(sceneRoot, startValues, endValues); + if (startValues == null || endValues == null + || !(endValues.view instanceof ParallaxScrimageView)) return changeBounds; ParallaxScrimageView psv = ((ParallaxScrimageView) endValues.view); if (psv.getOffset() == 0) return changeBounds; diff --git a/app/src/main/java/io/plaidapp/ui/transitions/StaggeredDistanceSlide.java b/app/src/main/java/io/plaidapp/ui/transitions/StaggeredDistanceSlide.java index 5a04dbe53..a9f0c918f 100644 --- a/app/src/main/java/io/plaidapp/ui/transitions/StaggeredDistanceSlide.java +++ b/app/src/main/java/io/plaidapp/ui/transitions/StaggeredDistanceSlide.java @@ -17,16 +17,21 @@ package io.plaidapp.ui.transitions; import android.animation.Animator; +import android.animation.AnimatorListenerAdapter; import android.animation.ObjectAnimator; import android.content.Context; import android.content.res.TypedArray; +import android.support.annotation.Keep; import android.transition.TransitionValues; import android.transition.Visibility; import android.util.AttributeSet; import android.view.View; import android.view.ViewGroup; +import java.util.List; + import io.plaidapp.R; +import io.plaidapp.util.TransitionUtils; /** * An alternative to {@link android.transition.Slide} which staggers elements by distance @@ -43,8 +48,11 @@ public class StaggeredDistanceSlide extends Visibility { private int spread = 1; - public StaggeredDistanceSlide() { } + public StaggeredDistanceSlide() { + super(); + } + @Keep public StaggeredDistanceSlide(Context context, AttributeSet attrs) { super(context, attrs); final TypedArray a = @@ -65,15 +73,27 @@ public void setSpread(int spread) { public Animator onAppear(ViewGroup sceneRoot, View view, TransitionValues startValues, TransitionValues endValues) { int[] position = (int[]) endValues.values.get(PROPNAME_SCREEN_LOCATION); - view.setTranslationY(sceneRoot.getHeight() + (position[1] * spread)); - return ObjectAnimator.ofFloat(view, View.TRANSLATION_Y, 0f); + return createAnimator(view, sceneRoot.getHeight() + (position[1] * spread), 0f); } @Override public Animator onDisappear(ViewGroup sceneRoot, View view, TransitionValues startValues, TransitionValues endValues) { int[] position = (int[]) endValues.values.get(PROPNAME_SCREEN_LOCATION); - return ObjectAnimator.ofFloat(view, View.TRANSLATION_Y, - sceneRoot.getHeight() + (position[1] * spread)); + return createAnimator(view, 0f, sceneRoot.getHeight() + (position[1] * spread)); + } + + private Animator createAnimator( + final View view, float startTranslationY, float endTranslationY) { + view.setTranslationY(startTranslationY); + final List ancestralClipping = TransitionUtils.setAncestralClipping(view, false); + Animator transition = ObjectAnimator.ofFloat(view, View.TRANSLATION_Y, endTranslationY); + transition.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + TransitionUtils.restoreAncestralClipping(view, ancestralClipping); + } + }); + return transition; } } diff --git a/app/src/main/java/io/plaidapp/ui/widget/ParallaxScrimageView.java b/app/src/main/java/io/plaidapp/ui/widget/ParallaxScrimageView.java index c9952c8c9..e2a67a855 100644 --- a/app/src/main/java/io/plaidapp/ui/widget/ParallaxScrimageView.java +++ b/app/src/main/java/io/plaidapp/ui/widget/ParallaxScrimageView.java @@ -30,7 +30,6 @@ import io.plaidapp.R; import io.plaidapp.util.AnimUtils; import io.plaidapp.util.ColorUtils; -import io.plaidapp.util.ViewOffsetHelper; /** * An image view which supports parallax scrolling and applying a scrim onto it's content. Get it. @@ -43,7 +42,6 @@ public class ParallaxScrimageView extends FourThreeImageView { private final Paint scrimPaint; private int imageOffset; private int minOffset; - private ViewOffsetHelper offsetHelper; private Rect clipBounds = new Rect(); private float scrimAlpha = 0f; private float maxScrimAlpha = 1f; @@ -89,21 +87,21 @@ public ParallaxScrimageView(Context context, AttributeSet attrs) { scrimPaint = new Paint(); scrimPaint.setColor(ColorUtils.modifyAlpha(scrimColor, scrimAlpha)); - offsetHelper = new ViewOffsetHelper(this); } public int getOffset() { - return offsetHelper.getTopAndBottomOffset(); + return (int) getTranslationY(); } public void setOffset(int offset) { offset = Math.max(minOffset, offset); - if (offset != offsetHelper.getTopAndBottomOffset()) { - offsetHelper.setTopAndBottomOffset(offset); + if (offset != getTranslationY()) { + setTranslationY(offset); imageOffset = (int) (offset * parallaxFactor); - clipBounds.set(0, 0, getWidth(), getHeight() + Math.round(imageOffset)); + clipBounds.set(0, -offset, getWidth(), getHeight()); setClipBounds(clipBounds); - setScrimAlpha(Math.min(((float) -offset / getMinimumHeight()) * maxScrimAlpha, maxScrimAlpha)); + setScrimAlpha(Math.min( + ((float) -offset / getMinimumHeight()) * maxScrimAlpha, maxScrimAlpha)); postInvalidateOnAnimation(); } setPinned(offset == minOffset); @@ -132,12 +130,6 @@ protected void onSizeChanged(int w, int h, int oldw, int oldh) { } } - @Override - protected void onLayout(boolean changed, int left, int top, int right, int bottom) { - super.onLayout(changed, left, top, right, bottom); - offsetHelper.onViewLayout(); - } - @Override protected void onDraw(Canvas canvas) { if (imageOffset != 0) { diff --git a/app/src/main/java/io/plaidapp/util/TransitionUtils.java b/app/src/main/java/io/plaidapp/util/TransitionUtils.java index 396687a2b..33a8137a4 100644 --- a/app/src/main/java/io/plaidapp/util/TransitionUtils.java +++ b/app/src/main/java/io/plaidapp/util/TransitionUtils.java @@ -21,6 +21,12 @@ import android.support.annotation.Nullable; import android.transition.Transition; import android.transition.TransitionSet; +import android.view.View; +import android.view.ViewGroup; +import android.view.ViewParent; + +import java.util.ArrayList; +import java.util.List; /** * Utility methods for working with transitions @@ -63,31 +69,45 @@ private TransitionUtils() { } return null; } - public static class TransitionListenerAdapter implements Transition.TransitionListener { - - @Override - public void onTransitionStart(Transition transition) { + public static List setAncestralClipping(@NonNull View view, boolean clipChildren) { + return setAncestralClipping(view, clipChildren, new ArrayList()); + } + private static List setAncestralClipping( + @NonNull View view, boolean clipChildren, List was) { + if (view instanceof ViewGroup) { + ViewGroup group = (ViewGroup) view; + was.add(group.getClipChildren()); + group.setClipChildren(clipChildren); } + ViewParent parent = view.getParent(); + if (parent != null && parent instanceof ViewGroup) { + setAncestralClipping((ViewGroup) parent, clipChildren, was); + } + return was; + } - @Override - public void onTransitionEnd(Transition transition) { - + public static void restoreAncestralClipping(@NonNull View view, List was) { + if (view instanceof ViewGroup) { + ViewGroup group = (ViewGroup) view; + group.setClipChildren(was.remove(0)); } + ViewParent parent = view.getParent(); + if (parent != null && parent instanceof ViewGroup) { + restoreAncestralClipping((ViewGroup) parent, was); + } + } - @Override - public void onTransitionCancel(Transition transition) { + public static class TransitionListenerAdapter implements Transition.TransitionListener { - } + @Override public void onTransitionStart(Transition transition) { } - @Override - public void onTransitionPause(Transition transition) { + @Override public void onTransitionEnd(Transition transition) { } - } + @Override public void onTransitionCancel(Transition transition) { } - @Override - public void onTransitionResume(Transition transition) { + @Override public void onTransitionPause(Transition transition) { } - } + @Override public void onTransitionResume(Transition transition) { } } } diff --git a/app/src/main/res/animator/app_bar_pin.xml b/app/src/main/res/animator/app_bar_pin.xml index 2d3dab9d2..6eaf2f8ce 100644 --- a/app/src/main/res/animator/app_bar_pin.xml +++ b/app/src/main/res/animator/app_bar_pin.xml @@ -22,36 +22,25 @@ - - - - + android:valueTo="@dimen/z_app_bar" /> + android:valueTo="@dimen/touch_raise" /> + android:valueTo="0dp" /> 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 82e2646bd..802292c2e 100644 --- a/app/src/main/res/layout/activity_designer_news_story.xml +++ b/app/src/main/res/layout/activity_designer_news_story.xml @@ -26,8 +26,10 @@ app:dragDismissScale="0.95" tools:context="io.plaidapp.ui.DesignerNewsStory"> - + - + + + + app:parallaxFactor="-0.5" /> @@ -86,7 +95,6 @@ android:layout_gravity="end" android:layout_marginEnd="@dimen/padding_normal" android:stateListAnimator="@animator/raise" - android:src="@drawable/asl_fab_heart" - android:alpha="0" /> + android:src="@drawable/asl_fab_heart" /> 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 1d562df23..c8febd37a 100644 --- a/app/src/main/res/layout/designer_news_story_description.xml +++ b/app/src/main/res/layout/designer_news_story_description.xml @@ -23,7 +23,9 @@ android:paddingTop="@dimen/designer_news_story_comments_vertical_padding" android:elevation="@dimen/designer_news_story_comment_elevation" android:background="@color/designer_news_story_comment_background" - android:orientation="vertical"> + android:orientation="vertical" + android:transitionGroup="false" + android:clipToPadding="false"> + android:orientation="horizontal" + android:clipToPadding="false"> + android:orientation="horizontal" + android:clipToPadding="false"> @@ -33,8 +35,7 @@ android:layout_columnSpan="3" android:background="@drawable/mid_grey_bounded_ripple" /> - + + android:paddingEnd="@dimen/padding_normal" + android:clipToPadding="false">