Skip to content

Commit

Permalink
Make shared element transitions for dribbble shots work across rotati…
Browse files Browse the repository at this point in the history
…ons.
  • Loading branch information
nickbutcher committed Jun 2, 2016
1 parent 247382e commit 77ce671
Show file tree
Hide file tree
Showing 5 changed files with 137 additions and 49 deletions.
4 changes: 4 additions & 0 deletions app/src/main/java/io/plaidapp/ui/DribbbleShot.java
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,7 @@
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;
Expand Down Expand Up @@ -639,6 +640,9 @@ public void onResponse(Call<List<Comment>> call, Response<List<Comment>> respons
}

private void expandImageAndFinish() {
final Intent resultData = new Intent();
resultData.putExtra(RESULT_EXTRA_SHOT_ID, shot.id);
setResult(RESULT_OK, resultData);
if (imageView.getOffset() != 0f) {
Animator expandImage = ObjectAnimator.ofFloat(imageView, ParallaxScrimageView.OFFSET,
0f);
Expand Down
90 changes: 69 additions & 21 deletions app/src/main/java/io/plaidapp/ui/FeedAdapter.java
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@
import android.animation.ValueAnimator;
import android.app.Activity;
import android.app.ActivityOptions;
import android.app.SharedElementCallback;
import android.content.Context;
import android.content.Intent;
import android.content.res.TypedArray;
import android.graphics.Color;
Expand Down Expand Up @@ -57,6 +59,7 @@
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;

import butterknife.BindView;
import butterknife.ButterKnife;
Expand Down Expand Up @@ -88,6 +91,8 @@
public class FeedAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder>
implements DataLoadingSubject.DataLoadingCallbacks {

public static final int REQUEST_CODE_VIEW_SHOT = 5407;

private static final int TYPE_DESIGNER_NEWS_STORY = 0;
private static final int TYPE_DRIBBBLE_SHOT = 1;
private static final int TYPE_PRODUCT_HUNT_POST = 2;
Expand Down Expand Up @@ -167,13 +172,14 @@ public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {
bindDesignerNewsStory((Story) getItem(position), (DesignerNewsStoryHolder) holder);
break;
case TYPE_DRIBBBLE_SHOT:
bindDribbbleShotHolder((Shot) getItem(position), (DribbbleShotHolder) holder);
bindDribbbleShotHolder(
(Shot) getItem(position), (DribbbleShotHolder) holder, position);
break;
case TYPE_PRODUCT_HUNT_POST:
bindProductHuntPostView((Post) getItem(position), (ProductHuntStoryHolder) holder);
break;
case TYPE_LOADING_MORE:
bindLoadingViewHolder((LoadingMoreHolder) holder);
bindLoadingViewHolder((LoadingMoreHolder) holder, position);
break;
}
}
Expand Down Expand Up @@ -251,8 +257,6 @@ private DribbbleShotHolder createDribbbleShotHolder(ViewGroup parent) {
holder.image.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
holder.image.setTransitionName(holder.itemView.getResources().getString(R
.string.transition_shot));
Intent intent = new Intent();
intent.setClass(host, DribbbleShot.class);
intent.putExtra(DribbbleShot.EXTRA_SHOT,
Expand All @@ -263,7 +267,7 @@ public void onClick(View view) {
Pair.create(view, host.getString(R.string.transition_shot)),
Pair.create(view, host.getString(R.string
.transition_shot_background)));
host.startActivity(intent, options.toBundle());
host.startActivityForResult(intent, REQUEST_CODE_VIEW_SHOT, options.toBundle());
}
});
// play animated GIFs whilst touched
Expand Down Expand Up @@ -310,7 +314,8 @@ public boolean onTouch(View v, MotionEvent event) {
}

private void bindDribbbleShotHolder(final Shot shot,
final DribbbleShotHolder holder) {
final DribbbleShotHolder holder,
int position) {
final int[] imageSize = shot.images.bestSize();
Glide.with(host)
.load(shot.images.best())
Expand Down Expand Up @@ -358,16 +363,17 @@ public boolean onException(Exception e, String model, Target<GlideDrawable>
return false;
}
})
.placeholder(shotLoadingPlaceholders[holder.getAdapterPosition() %
shotLoadingPlaceholders.length])
.placeholder(shotLoadingPlaceholders[position % shotLoadingPlaceholders.length])
.diskCacheStrategy(DiskCacheStrategy.SOURCE)
.fitCenter()
.override(imageSize[0], imageSize[1])
.into(new DribbbleTarget(holder.image, false));
// need both placeholder & background to prevent seeing through shot as it fades in
holder.image.setBackground(shotLoadingPlaceholders[holder.getAdapterPosition() %
shotLoadingPlaceholders.length]);
holder.image.setBackground(
shotLoadingPlaceholders[position % shotLoadingPlaceholders.length]);
holder.image.showBadge(shot.animated);
// need a unique transition name per shot, let's use it's url
holder.image.setTransitionName(shot.html_url);
}

@NonNull
Expand Down Expand Up @@ -407,11 +413,11 @@ private void bindProductHuntPostView(final Post item, ProductHuntStoryHolder hol
holder.comments.setText(String.valueOf(item.comments_count));
}

private void bindLoadingViewHolder(LoadingMoreHolder holder) {
private void bindLoadingViewHolder(LoadingMoreHolder holder, int position) {
// only show the infinite load progress spinner if there are already items in the
// grid i.e. it's not the first item & data is being loaded
holder.progress.setVisibility((holder.getAdapterPosition() > 0
&& dataLoading.isDataLoading()) ? View.VISIBLE : View.INVISIBLE);
holder.progress.setVisibility((position > 0 && dataLoading.isDataLoading())
? View.VISIBLE : View.INVISIBLE);
}

@Override
Expand Down Expand Up @@ -581,28 +587,41 @@ public long getItemId(int position) {
return getItem(position).id;
}

public int getItemPosition(final long itemId) {
for (int position = 0; position < items.size(); position++) {
if (getItem(position).id == itemId) return position;
}
return RecyclerView.NO_POSITION;
}

@Override
public int getItemCount() {
return getDataItemCount() + (showLoadingMore ? 1 : 0);
}

/**
* The shared element transition to dribbble shots & dn stories can intersect with the FAB.
* This can cause a strange layers-passing-through-each-other effect, especially on return.
* In this situation, hide the FAB on exit and re-show it on return.
* This can cause a strange layers-passing-through-each-other effect. On return hide the FAB
* and animate it back in after the transition.
*/
private void setGridItemContentTransitions(View gridItem) {
if (!ViewUtils.viewsIntersect(gridItem, host.findViewById(R.id.fab))) return;
final View fab = host.findViewById(R.id.fab);
if (!ViewUtils.viewsIntersect(gridItem, fab)) return;

final TransitionInflater ti = TransitionInflater.from(host);
host.getWindow().setExitTransition(
ti.inflateTransition(R.transition.home_content_item_exit));
final Transition reenter = ti.inflateTransition(R.transition.home_content_item_reenter);
final Transition reenter = TransitionInflater.from(host)
.inflateTransition(R.transition.home_content_item_reenter);
// we only want these content transitions in certain cases so clear out when done.
reenter.addListener(new AnimUtils.TransitionListenerAdapter() {

@Override
public void onTransitionStart(Transition transition) {
fab.setAlpha(0f);
fab.setScaleX(0f);
fab.setScaleY(0f);
}

@Override
public void onTransitionEnd(Transition transition) {
host.getWindow().setExitTransition(null);
host.getWindow().setReenterTransition(null);
}
});
Expand Down Expand Up @@ -639,6 +658,35 @@ public void dataFinishedLoading() {
notifyItemRemoved(loadingPos);
}

public static SharedElementCallback createSharedElementReenterCallback(
@NonNull Context context) {
final String shotTransitionName = context.getString(R.string.transition_shot);
final String shotBackgroundTransitionName =
context.getString(R.string.transition_shot_background);
return new SharedElementCallback() {

/**
* We're performing a slightly unusual shared element transition i.e. from one view
* (image in the grid) to two views (the image & also the background of the details
* view, to produce the expand effect). After changing orientation, the transition
* system seems unable to map both shared elements (only seems to map the shot, not
* the background) so in this situation we manually map the background to the
* same view.
*/
@Override
public void onMapSharedElements(List<String> names, Map<String, View> sharedElements) {
if (sharedElements.size() != names.size()) {
// couldn't map all shared elements
final View sharedShot = sharedElements.get(shotTransitionName);
if (sharedShot != null) {
// has shot so add shot background, mapped to same view
sharedElements.put(shotBackgroundTransitionName, sharedShot);
}
}
}
};
}

/* package */ static class DribbbleShotHolder extends RecyclerView.ViewHolder {

BadgedFourThreeImageView image;
Expand Down
33 changes: 33 additions & 0 deletions app/src/main/java/io/plaidapp/ui/HomeActivity.java
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
import android.app.Activity;
import android.app.ActivityManager;
import android.app.ActivityOptions;
import android.app.SharedElementCallback;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
Expand Down Expand Up @@ -74,6 +75,7 @@
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Map;

import butterknife.BindInt;
import butterknife.BindView;
Expand Down Expand Up @@ -143,6 +145,7 @@ protected void onCreate(Bundle savedInstanceState) {
if (savedInstanceState == null) {
animateToolbar();
}
setExitSharedElementCallback(FeedAdapter.createSharedElementReenterCallback(this));

dribbblePrefs = DribbblePrefs.get(this);
designerNewsPrefs = DesignerNewsPrefs.get(this);
Expand All @@ -169,6 +172,7 @@ public void onDataLoaded(List<? extends PlaidItem> data) {
}
};
adapter = new FeedAdapter(this, dataManager, columns, PocketUtils.isPocketInstalled(this));

grid.setAdapter(adapter);
layoutManager = new GridLayoutManager(this, columns);
layoutManager.setSpanSizeLookup(new GridLayoutManager.SpanSizeLookup() {
Expand Down Expand Up @@ -276,6 +280,35 @@ protected void onPause() {
super.onPause();
}

@Override
public void onActivityReenter(int resultCode, Intent data) {
if (data == null || resultCode != RESULT_OK
|| !data.hasExtra(DribbbleShot.RESULT_EXTRA_SHOT_ID)) return;

// When reentering, if the shared element is no longer on screen (e.g. after an
// orientation change) then scroll it into view.
final long sharedShotId = data.getLongExtra(DribbbleShot.RESULT_EXTRA_SHOT_ID, -1l);
if (sharedShotId != -1l // returning from a shot
&& adapter.getDataItemCount() > 0 // grid populated
&& grid.findViewHolderForItemId(sharedShotId) == null) { // view not attached
final int position = adapter.getItemPosition(sharedShotId);
if (position == RecyclerView.NO_POSITION) return;

// delay the transition until our shared element is on-screen i.e. has been laid out
postponeEnterTransition();
grid.addOnLayoutChangeListener(new View.OnLayoutChangeListener() {
@Override
public void onLayoutChange(View v, int l, int t, int r, int b,
int oL, int oT, int oR, int oB) {
grid.removeOnLayoutChangeListener(this);
startPostponedEnterTransition();
}
});
grid.scrollToPosition(position);
toolbar.setTranslationZ(-1f);
}
}

@Override
public boolean onCreateOptionsMenu(Menu menu) {
getMenuInflater().inflate(R.menu.main, menu);
Expand Down
31 changes: 31 additions & 0 deletions app/src/main/java/io/plaidapp/ui/PlayerActivity.java
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@

import android.app.Activity;
import android.app.ActivityOptions;
import android.app.SharedElementCallback;
import android.content.Intent;
import android.content.res.Resources;
import android.graphics.drawable.AnimatedVectorDrawable;
Expand All @@ -40,6 +41,7 @@

import java.text.NumberFormat;
import java.util.List;
import java.util.Map;

import butterknife.BindInt;
import butterknife.BindView;
Expand Down Expand Up @@ -138,6 +140,7 @@ public WindowInsets onApplyWindowInsets(View v, WindowInsets insets) {
return insets;
}
});
setExitSharedElementCallback(FeedAdapter.createSharedElementReenterCallback(this));
}

@Override
Expand All @@ -160,6 +163,34 @@ protected void onDestroy() {
super.onDestroy();
}

@Override
public void onActivityReenter(int resultCode, Intent data) {
if (data == null || resultCode != RESULT_OK
|| !data.hasExtra(DribbbleShot.RESULT_EXTRA_SHOT_ID)) return;

// When reentering, if the shared element is no longer on screen (e.g. after an
// orientation change) then scroll it into view.
final long sharedShotId = data.getLongExtra(DribbbleShot.RESULT_EXTRA_SHOT_ID, -1l);
if (sharedShotId != -1l // returning from a shot
&& adapter.getDataItemCount() > 0 // grid populated
&& shots.findViewHolderForItemId(sharedShotId) == null) { // view not attached
final int position = adapter.getItemPosition(sharedShotId);
if (position == RecyclerView.NO_POSITION) return;

// delay the transition until our shared element is on-screen i.e. has been laid out
postponeEnterTransition();
shots.addOnLayoutChangeListener(new View.OnLayoutChangeListener() {
@Override
public void onLayoutChange(View v, int l, int t, int r, int b,
int oL, int oT, int oR, int oB) {
shots.removeOnLayoutChangeListener(this);
startPostponedEnterTransition();
}
});
shots.scrollToPosition(position);
}
}

private void bindPlayer() {
if (player == null) {
return;
Expand Down
28 changes: 0 additions & 28 deletions app/src/main/res/transition/home_content_item_exit.xml

This file was deleted.

0 comments on commit 77ce671

Please sign in to comment.