Skip to content

Instantly share code, notes, and snippets.

@aitorvs
Created August 21, 2016 20:04
Show Gist options
  • Save aitorvs/fb6812eec9205d4ec48efd5b515b5dbe to your computer and use it in GitHub Desktop.
Save aitorvs/fb6812eec9205d4ec48efd5b515b5dbe to your computer and use it in GitHub Desktop.
package com.aitorvs.android.rsspace.ui.activity;
import android.app.Activity;
import android.content.Context;
import android.content.Intent;
import android.graphics.Bitmap;
import android.graphics.drawable.BitmapDrawable;
import android.os.Bundle;
import android.support.annotation.NonNull;
import android.support.v4.content.ContextCompat;
import android.support.v7.app.AppCompatActivity;
import android.support.v7.graphics.Palette;
import android.text.TextUtils;
import android.view.View;
import android.view.ViewTreeObserver;
import android.view.animation.AccelerateDecelerateInterpolator;
import android.widget.FrameLayout;
import android.widget.ImageView;
import com.aitorvs.android.rsspace.MyApplication;
import com.aitorvs.android.rsspace.R;
import com.aitorvs.android.rsspace.util.ImageViewUtil;
import com.squareup.picasso.Callback;
import com.squareup.picasso.Picasso;
import java.util.Locale;
import javax.inject.Inject;
import timber.log.Timber;
public class ArticleImageActivity extends AppCompatActivity {
private static final String IMAGE_URL_EXTRA = "IMAGE_URL_EXTRA";
private static final String VIEW_INFO_EXTRA = "VIEW_INFO_EXTRA";
// Animation constants
private static final AccelerateDecelerateInterpolator DEFAULT_INTERPOLATOR = new AccelerateDecelerateInterpolator();
private static final int DEFAULT_DURATION = 300;
// reference to the image vie
private ImageView mDestinationView;
// reference to the image view container
private FrameLayout mImageViewContainer;
// image URL
private String mImageUrl;
// Bundle that will contain the transition start values
private Bundle mStartValues;
// Bundle that will contain the transition end values
final private Bundle mEndValues = new Bundle();
// Picasso object
@Inject
protected Picasso mPicasso;
// transition properties
private static String PROPNAME_SCREENLOCATION_LEFT = "rsspace:location:left";
private static String PROPNAME_SCREENLOCATION_TOP = "rsspace:location:top";
private static String PROPNAME_WIDTH = "rsspace:width";
private static String PROPNAME_HEIGHT = "rsspace:height";
private Palette mImagePalette;
public static void startActivityFromImage(
@NonNull Context context,
@NonNull String imageUrl,
@NonNull View originView) {
//noinspection ConstantConditions
if (context == null || !(context instanceof Activity) || TextUtils.isEmpty(imageUrl) || originView == null) {
Timber.e("Invalid params");
return;
}
Intent intent = new Intent(context, ArticleImageActivity.class);
intent.putExtra(IMAGE_URL_EXTRA, imageUrl);
intent.putExtra(VIEW_INFO_EXTRA, /* start values */ captureValues(originView));
context.startActivity(intent);
((Activity) context).overridePendingTransition(0, 0);
}
/**
* Helper method to capture the view values to animate
*
* @param view target view
* @return Bundle with the captured values
*/
private static Bundle captureValues(@NonNull View view) {
Bundle b = new Bundle();
captureScaleValues(b, view);
captureScreenLocationValues(b, view);
return b;
}
private static void captureScaleValues(@NonNull Bundle b, @NonNull View view) {
if (view instanceof ImageView) {
int[] size = ImageViewUtil.getDisplayedImageLocation((ImageView) view);
b.putInt(PROPNAME_WIDTH, size[2]);
b.putInt(PROPNAME_HEIGHT, size[3]);
} else {
b.putInt(PROPNAME_WIDTH, view.getWidth());
b.putInt(PROPNAME_HEIGHT, view.getHeight());
}
}
private static void captureScreenLocationValues(@NonNull Bundle b, @NonNull View view) {
if (view instanceof ImageView) {
int[] size = ImageViewUtil.getDisplayedImageLocation((ImageView) view);
b.putInt(PROPNAME_SCREENLOCATION_LEFT, size[0]);
b.putInt(PROPNAME_SCREENLOCATION_TOP, size[1]);
} else {
int[] screenLocation = new int[2];
view.getLocationOnScreen(screenLocation);
b.putInt(PROPNAME_SCREENLOCATION_LEFT, screenLocation[0]);
b.putInt(PROPNAME_SCREENLOCATION_TOP, screenLocation[1]);
}
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_article_image);
// inject dependencies
MyApplication.getComponent().inject(this);
// make sure we're not visible yet
mDestinationView = (ImageView) findViewById(R.id.article_image);
if (mDestinationView != null) {
// we need to make INVISIBLE so that we can get the view coordinates
mDestinationView.setVisibility(View.INVISIBLE);
}
mImageViewContainer = (FrameLayout) findViewById(R.id.image_container);
// extract all the info from the bundle
extractViewInfoFromBundle(getIntent());
// only now we load the image
mPicasso.load(mImageUrl)
.into(mDestinationView, new Callback() {
@Override
public void onSuccess() {
// get the image palette
Bitmap bitmap = ((BitmapDrawable) mDestinationView.getDrawable()).getBitmap();
mImagePalette = Palette.from(bitmap).generate();
// we've got the image loaded, start the animation
onUiReady();
}
@Override
public void onError() {
// no-op
}
});
}
@Override
public void onBackPressed() {
// run the exit animation
runExitAnimation();
}
/**
* Call this method to notify the view has all elements ready (e.g. images fetched etc)
*/
private void onUiReady() {
mDestinationView.getViewTreeObserver().addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() {
@Override
public boolean onPreDraw() {
// remove previous listener
mDestinationView.getViewTreeObserver().removeOnPreDrawListener(this);
// prep the scene
prepareScene();
// run the animation
runEnterAnimation();
return true;
}
});
}
/**
* This method preps the scene. Captures the end values, calculates deltas with start values and
* reposition the view in the target layout
*/
private void prepareScene() {
// do the first capture to scale the image
captureScaleValues(mEndValues, mDestinationView);
// calculate the scale factors
float scaleX = scaleDelta(mStartValues, mEndValues, PROPNAME_WIDTH);
float scaleY = scaleDelta(mStartValues, mEndValues, PROPNAME_HEIGHT);
// scale the image
mDestinationView.setScaleX(scaleX);
mDestinationView.setScaleY(scaleY);
// as scaling the image will change the top and left coordinates, we need to re-capture
// the values to proper figure out the translation deltas w.r.t. to start view
captureScreenLocationValues(mEndValues, mDestinationView);
int deltaX = translationDelta(mStartValues, mEndValues, PROPNAME_SCREENLOCATION_LEFT);
int deltaY = translationDelta(mStartValues, mEndValues, PROPNAME_SCREENLOCATION_TOP);
// finally, translate the end view to where the start view was
mDestinationView.setTranslationX(deltaX);
mDestinationView.setTranslationY(deltaY);
}
/**
* This method will run the entry animation
*/
private void runEnterAnimation() {
// We can now make it visible
mDestinationView.setVisibility(View.VISIBLE);
// set the image container background using palette
mImageViewContainer.setBackgroundColor(mImagePalette.getDarkVibrantColor(ContextCompat.getColor(this, android.R.color.black)));
// finally, run the animation
mDestinationView.animate()
.setDuration(DEFAULT_DURATION)
.setInterpolator(DEFAULT_INTERPOLATOR)
.scaleX(1f)
.scaleY(1f)
.translationX(0)
.translationY(0)
.start();
}
/**
* Call this method to run the exit transition
*/
private void runExitAnimation() {
// re-calculate deltas
int deltaX = translationDelta(mStartValues, mEndValues, PROPNAME_SCREENLOCATION_LEFT);
int deltaY = translationDelta(mStartValues, mEndValues, PROPNAME_SCREENLOCATION_TOP);
float scaleX = scaleDelta(mStartValues, mEndValues, PROPNAME_WIDTH);
float scaleY = scaleDelta(mStartValues, mEndValues, PROPNAME_HEIGHT);
mDestinationView.animate()
.setDuration(DEFAULT_DURATION)
.setInterpolator(DEFAULT_INTERPOLATOR)
.scaleX(scaleX)
.scaleY(scaleY)
.translationX(deltaX)
.translationY(deltaY)
.withEndAction(new Runnable() {
@Override
public void run() {
finish();
overridePendingTransition(0, 0);
}
}).start();
}
private void extractViewInfoFromBundle(Intent intent) {
mImageUrl = intent.getStringExtra(IMAGE_URL_EXTRA);
mStartValues = intent.getBundleExtra(VIEW_INFO_EXTRA);
}
/**
* Helper method to calculate the scale delta given start and end values
*
* @param startValues start values {@link Bundle}
* @param endValues end values {@link Bundle}
* @param propertyName property name
* @return scale delta value
*/
private float scaleDelta(
@NonNull Bundle startValues,
@NonNull Bundle endValues,
@NonNull String propertyName) {
int startValue = startValues.getInt(propertyName);
int endValue = endValues.getInt(propertyName);
float delta = (float) startValue / endValue;
Timber.d(String.format(Locale.US, "%s: startValue = %d, endValue = %d, delta = %f", propertyName, startValue, endValue, delta));
return delta;
}
/**
* Helper method to calculate the translation deltas given start and end values
*
* @param startValues start values {@link Bundle}
* @param endValues end values {@link Bundle}
* @param propertyName property name
* @return translation delta between start and end values
*/
private int translationDelta(
@NonNull Bundle startValues,
@NonNull Bundle endValues,
@NonNull String propertyName) {
int startValue = startValues.getInt(propertyName);
int endValue = endValues.getInt(propertyName);
int delta = startValue - endValue;
Timber.d(String.format(Locale.US, "%s: startValue = %d, endValue = %d, delta = %d", propertyName, startValue, endValue, delta));
return delta;
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment