Repository: danylovolokh/ImageTransition
Branch: master
Commit: 69e28f7e0917
Files: 39
Total size: 78.1 KB
Directory structure:
gitextract_v8hwl3px/
├── .gitignore
├── README.md
├── app/
│ ├── .gitignore
│ ├── build.gradle
│ ├── proguard-rules.pro
│ └── src/
│ ├── androidTest/
│ │ └── java/
│ │ └── com/
│ │ └── volokh/
│ │ └── danylo/
│ │ └── imagetransition/
│ │ └── ApplicationTest.java
│ ├── main/
│ │ ├── AndroidManifest.xml
│ │ ├── java/
│ │ │ └── com/
│ │ │ └── volokh/
│ │ │ └── danylo/
│ │ │ └── imagetransition/
│ │ │ ├── ImageFilesCreateLoader.java
│ │ │ ├── activities/
│ │ │ │ ├── ImageDetailsActivity.java
│ │ │ │ └── ImagesListActivity.java
│ │ │ ├── activities_v21/
│ │ │ │ ├── ImageDetailsActivity_v21.java
│ │ │ │ └── ImagesListActivity_v21.java
│ │ │ ├── adapter/
│ │ │ │ ├── Image.java
│ │ │ │ ├── ImagesAdapter.java
│ │ │ │ └── ImagesViewHolder.java
│ │ │ ├── animations/
│ │ │ │ ├── EnterScreenAnimations.java
│ │ │ │ ├── ExitScreenAnimations.java
│ │ │ │ ├── MatrixEvaluator.java
│ │ │ │ ├── MatrixUtils.java
│ │ │ │ ├── ScreenAnimation.java
│ │ │ │ └── SimpleAnimationListener.java
│ │ │ └── event_bus/
│ │ │ ├── ChangeImageThumbnailVisibility.java
│ │ │ └── EventBusCreator.java
│ │ └── res/
│ │ ├── layout/
│ │ │ ├── image_details_activity_layout.xml
│ │ │ ├── image_item.xml
│ │ │ └── images_list.xml
│ │ ├── transition/
│ │ │ ├── change_image_transition.xml
│ │ │ └── enter_exit.xml
│ │ ├── values/
│ │ │ ├── colors.xml
│ │ │ └── styles.xml
│ │ └── values-v21/
│ │ └── styles.xml
│ └── test/
│ └── java/
│ └── com/
│ └── volokh/
│ └── danylo/
│ └── imagetransition/
│ └── ExampleUnitTest.java
├── build.gradle
├── gradle/
│ └── wrapper/
│ ├── gradle-wrapper.jar
│ └── gradle-wrapper.properties
├── gradle.properties
├── gradlew
├── gradlew.bat
└── settings.gradle
================================================
FILE CONTENTS
================================================
================================================
FILE: .gitignore
================================================
*.iml
.gradle
/local.properties
/.idea/workspace.xml
/.idea/libraries
.DS_Store
/build
/captures
================================================
FILE: README.md
================================================
# ImageTransition
This is a pet project that shows how to implement shared element image transition on pre-Lollipop devices on Android.
# Pre-Lolipop Demo
If you look closer there is almost no difference between the native Android Lollipop shared element aniamation
Animation and canceling animation in the middle of it.
 
# Lollipop native animation

# Details of implementation
[](https://medium.com/@v.danylo/implementing-imageview-transition-between-activities-for-pre-lollipop-devices-8b24bc387a2a)
# License
Copyright 2016 Danylo Volokh
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.
================================================
FILE: app/.gitignore
================================================
/build
================================================
FILE: app/build.gradle
================================================
apply plugin: 'com.android.application'
android {
compileSdkVersion 23
buildToolsVersion "23.0.2"
defaultConfig {
applicationId "com.volokh.danylo.imagetransition"
minSdkVersion 15
targetSdkVersion 23
versionCode 1
versionName "1.0"
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
}
}
dependencies {
compile fileTree(dir: 'libs', include: ['*.jar'])
testCompile 'junit:junit:4.12'
compile 'com.android.support:appcompat-v7:23.1.1'
compile 'com.android.support:design:23.1.1'
compile 'com.squareup.picasso:picasso:2.5.2'
compile 'com.squareup:otto:1.3.8'
}
================================================
FILE: app/proguard-rules.pro
================================================
# Add project specific ProGuard rules here.
# By default, the flags in this file are appended to flags specified
# in D:\Projects\android_sdk/tools/proguard/proguard-android.txt
# You can edit the include path and order by changing the proguardFiles
# directive in build.gradle.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
# Add any project specific keep options here:
# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}
================================================
FILE: app/src/androidTest/java/com/volokh/danylo/imagetransition/ApplicationTest.java
================================================
package com.volokh.danylo.imagetransition;
import android.app.Application;
import android.test.ApplicationTestCase;
/**
* Testing Fundamentals
*/
public class ApplicationTest extends ApplicationTestCase {
public ApplicationTest() {
super(Application.class);
}
}
================================================
FILE: app/src/main/AndroidManifest.xml
================================================
================================================
FILE: app/src/main/java/com/volokh/danylo/imagetransition/ImageFilesCreateLoader.java
================================================
package com.volokh.danylo.imagetransition;
import android.app.LoaderManager;
import android.content.AsyncTaskLoader;
import android.content.Context;
import android.content.Loader;
import android.os.Bundle;
import android.util.Log;
import android.util.SparseArray;
import com.volokh.danylo.imagetransition.adapter.Image;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
/**
* Created by danylo.volokh on 3/12/16.
*/
public class ImageFilesCreateLoader implements LoaderManager.LoaderCallbacks> {
private static final String TAG = ImageFilesCreateLoader.class.getSimpleName();
private static final String AVATAR = "avatar.png";
private static final String BEST_APP_OF_THE_YEAR = "best_app_of_the_year.png";
private static final String HB_DANYLO = "hb_danylo.png";
private static final String LENA_DANYLO_VIKTOR = "lena_danylo_victor.png";
private static final String VENECIA_LENA_DANYLO_OLYA = "venecia_lena_danylo_olya.png";
private static final String DANYLO = "danylo.jng";
private static final SparseArray mImagesResourcesList = new SparseArray<>();
static {
mImagesResourcesList.put(R.raw.avatar, AVATAR);
mImagesResourcesList.put(R.raw.best_app_of_the_year, BEST_APP_OF_THE_YEAR);
mImagesResourcesList.put(R.raw.hb_danylo, HB_DANYLO);
mImagesResourcesList.put(R.raw.lena_danylo_victor, LENA_DANYLO_VIKTOR);
mImagesResourcesList.put(R.raw.venecia_lena_danylo_olya, VENECIA_LENA_DANYLO_OLYA);
mImagesResourcesList.put(R.raw.danylo, DANYLO);
}
private final Context mContext;
private LoadFinishedCallback mLoadFinishedCallback;
public interface LoadFinishedCallback{
void onLoadFinished(List imagesList);
}
public ImageFilesCreateLoader(Context context, LoadFinishedCallback loadFinishedCallback) {
mContext = context;
mLoadFinishedCallback = loadFinishedCallback;
}
@Override
public Loader> onCreateLoader(int id, Bundle args) {
return new AsyncTaskLoader>(mContext) {
@Override
public List loadInBackground() {
Log.v(TAG, "loadInBackground");
List resultList = new ArrayList<>();
for (int resourceIndex = 0; resourceIndex < mImagesResourcesList.size(); resourceIndex++) {
writeResourceToFile(resultList, resourceIndex);
}
Log.v(TAG, "loadInBackground, resultList " + resultList);
return resultList;
}
};
}
private void writeResourceToFile(List resultList, int resourceIndex) {
File fileDir = mContext.getCacheDir();
List existingFiles = Arrays.asList(fileDir.list());
String fileName = mImagesResourcesList.valueAt(resourceIndex);
File file = new File(fileDir + File.separator + fileName);
if (existingFiles.contains(fileName)) {
resultList.add(file);
} else {
saveIntoFile(file, mImagesResourcesList.keyAt(resourceIndex));
resultList.add(file);
}
}
private void saveIntoFile(File file, Integer resource) {
try {
InputStream inputStream = mContext.getResources().openRawResource(resource);
FileOutputStream fileOutputStream = new FileOutputStream(file);
byte buf[] = new byte[1024];
int len;
while ((len = inputStream.read(buf)) > 0) {
fileOutputStream.write(buf, 0, len);
}
fileOutputStream.close();
inputStream.close();
} catch (IOException e1) {
}
}
@Override
public void onLoadFinished(Loader> loader, List data) {
Log.v(TAG, "onLoadFinished, data " + data);
fillImageList(data);
}
private void fillImageList(List data) {
List imagesList = new ArrayList<>();
int imageId = 0;
int times = 7;
while(--times > 0){
for (int i = 0; i < data.size(); i++, imageId++) {
File file = data.get(i);
Log.v(TAG, "fillImageList, imageId " + imageId);
imagesList.add(new Image(imageId, file));
}
}
mLoadFinishedCallback.onLoadFinished(imagesList);
}
@Override
public void onLoaderReset(Loader> loader) {
}
}
================================================
FILE: app/src/main/java/com/volokh/danylo/imagetransition/activities/ImageDetailsActivity.java
================================================
package com.volokh.danylo.imagetransition.activities;
import android.animation.AnimatorSet;
import android.app.Activity;
import android.content.Intent;
import android.os.Bundle;
import android.util.Log;
import android.view.View;
import android.view.ViewTreeObserver;
import android.widget.FrameLayout;
import android.widget.ImageView;
import com.squareup.otto.Bus;
import com.squareup.picasso.Callback;
import com.squareup.picasso.Picasso;
import com.volokh.danylo.imagetransition.animations.EnterScreenAnimations;
import com.volokh.danylo.imagetransition.animations.ExitScreenAnimations;
import com.volokh.danylo.imagetransition.event_bus.ChangeImageThumbnailVisibility;
import com.volokh.danylo.imagetransition.event_bus.EventBusCreator;
import com.volokh.danylo.imagetransition.R;
import java.io.File;
/**
* Created by danylo.volokh on 2/21/2016.
*/
public class ImageDetailsActivity extends Activity {
private static final String IMAGE_FILE_KEY = "IMAGE_FILE_KEY";
private static final String KEY_THUMBNAIL_INIT_TOP_POSITION = "KEY_THUMBNAIL_INIT_TOP_POSITION";
private static final String KEY_THUMBNAIL_INIT_LEFT_POSITION = "KEY_THUMBNAIL_INIT_LEFT_POSITION";
private static final String KEY_THUMBNAIL_INIT_WIDTH = "KEY_THUMBNAIL_INIT_WIDTH";
private static final String KEY_THUMBNAIL_INIT_HEIGHT = "KEY_THUMBNAIL_INIT_HEIGHT";
private static final String KEY_SCALE_TYPE = "KEY_SCALE_TYPE";
private static final String TAG = ImageDetailsActivity.class.getSimpleName();
private static final long IMAGE_TRANSLATION_DURATION = 3000;
private ImageView mEnlargedImage;
private ImageView mTransitionImage;
private Picasso mImageDownloader;
private final Bus mBus = EventBusCreator.defaultEventBus();
private AnimatorSet mExitingAnimation;
private EnterScreenAnimations mEnterScreenAnimations;
private ExitScreenAnimations mExitScreenAnimations;
@Override
protected void onCreate(final Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
overridePendingTransition(0, 0);
setContentView(R.layout.image_details_activity_layout);
mEnlargedImage = (ImageView) findViewById(R.id.enlarged_image);
mImageDownloader = Picasso.with(this);
File imageFile = (File) getIntent().getSerializableExtra(IMAGE_FILE_KEY);
final View mainContainer = findViewById(R.id.main_container);
if(savedInstanceState == null){
// We entered activity for the first time.
// Initialize Image view that will be transitioned
initializeTransitionView();
} else {
// Activity is retrieved. Main container is invisible. Make it visible
mainContainer.setAlpha(1.0f);
}
mEnterScreenAnimations = new EnterScreenAnimations(mTransitionImage, mEnlargedImage, mainContainer);
mExitScreenAnimations = new ExitScreenAnimations(mTransitionImage, mEnlargedImage, mainContainer);
initializeEnlargedImageAndRunAnimation(savedInstanceState, imageFile);
}
/**
* This method waits for the main "big" image is loaded.
* And then if activity is started for the first time - it runs "entering animation"
*
* Activity is entered fro the first time if saveInstanceState is null
*
*/
private void initializeEnlargedImageAndRunAnimation(final Bundle savedInstanceState, File imageFile) {
Log.v(TAG, "initializeEnlargedImageAndRunAnimation");
mImageDownloader.load(imageFile).into(mEnlargedImage, new Callback() {
/**
* Image is loaded when this method is called
*/
@Override
public void onSuccess() {
Log.v(TAG, "onSuccess, mEnlargedImage");
// In this callback we already have image set into ImageView and we can use it's Matrix for animation
// But we have to wait for final measurements. We use OnPreDrawListener to be sure everything is measured
if (savedInstanceState == null) {
// if savedInstanceState is null activity is started for the first time.
// run the animation
runEnteringAnimation();
} else {
// activity was retrieved from recent apps. No animation needed, just load the image
}
}
@Override
public void onError() {
// CAUTION: on error is not handled. If OutOfMemory emerged during image loading we have to handle it here
Log.v(TAG, "onError, mEnlargedImage");
}
});
}
/**
* This method does very tricky part:
*
* It sets up {@link android.view.ViewTreeObserver.OnPreDrawListener}
* When onPreDraw() method is called the layout is already measured.
* It means that we can use locations of images on the screen at tis point.
*
* 1. When first frame is rendered we start animation.
* 2. We just let second frame to render
* 3. Make a view on the previous screen invisible and remove onPreDrawListener
*
* Why do we do that:
* The Android rendering system is double-buffered.
* Similar technique is used in the SDK. See here : {@link android.app.EnterTransitionCoordinator#startSharedElementTransition}
*
* You can read more about it here : https://source.android.com/devices/graphics/architecture.html
*
*/
private void runEnteringAnimation() {
Log.v(TAG, "runEnteringAnimation, addOnPreDrawListener");
mEnlargedImage.getViewTreeObserver().addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() {
int mFrames = 0;
@Override
public boolean onPreDraw() {
// When this method is called we already have everything laid out and measured so we can start our animation
Log.v(TAG, "onPreDraw, mFrames " + mFrames);
switch (mFrames++) {
case 0:
/**
* 1. start animation on first frame
*/
final int[] finalLocationOnTheScreen = new int[2];
mEnlargedImage.getLocationOnScreen(finalLocationOnTheScreen);
mEnterScreenAnimations.playEnteringAnimation(
finalLocationOnTheScreen[0], // left
finalLocationOnTheScreen[1], // top
mEnlargedImage.getWidth(),
mEnlargedImage.getHeight());
return true;
case 1:
/**
* 2. Do nothing. We just draw this frame
*/
return true;
}
/**
* 3.
* Make view on previous screen invisible on after this drawing frame
* Here we ensure that animated view will be visible when we make the viw behind invisible
*/
Log.v(TAG, "run, onAnimationStart");
mBus.post(new ChangeImageThumbnailVisibility(false));
mEnlargedImage.getViewTreeObserver().removeOnPreDrawListener(this);
Log.v(TAG, "onPreDraw, << mFrames " + mFrames);
return true;
}
});
}
private void initializeTransitionView() {
Log.v(TAG, "initializeTransitionView");
FrameLayout androidContent = (FrameLayout) getWindow().getDecorView().findViewById(android.R.id.content);
mTransitionImage = new ImageView(this);
androidContent.addView(mTransitionImage);
Bundle bundle = getIntent().getExtras();
int thumbnailTop = bundle.getInt(KEY_THUMBNAIL_INIT_TOP_POSITION)
- getStatusBarHeight();
int thumbnailLeft = bundle.getInt(KEY_THUMBNAIL_INIT_LEFT_POSITION);
int thumbnailWidth = bundle.getInt(KEY_THUMBNAIL_INIT_WIDTH);
int thumbnailHeight = bundle.getInt(KEY_THUMBNAIL_INIT_HEIGHT);
ImageView.ScaleType scaleType = (ImageView.ScaleType) bundle.getSerializable(KEY_SCALE_TYPE);
Log.v(TAG, "initInitialThumbnail, thumbnailTop [" + thumbnailTop + "]");
Log.v(TAG, "initInitialThumbnail, thumbnailLeft [" + thumbnailLeft + "]");
Log.v(TAG, "initInitialThumbnail, thumbnailWidth [" + thumbnailWidth + "]");
Log.v(TAG, "initInitialThumbnail, thumbnailHeight [" + thumbnailHeight + "]");
Log.v(TAG, "initInitialThumbnail, scaleType " + scaleType);
// We set initial margins to the view so that it was situated at exact same spot that view from the previous screen were.
FrameLayout.LayoutParams layoutParams = (FrameLayout.LayoutParams) mTransitionImage.getLayoutParams();
layoutParams.height = thumbnailHeight;
layoutParams.width = thumbnailWidth;
layoutParams.setMargins(thumbnailLeft, thumbnailTop, 0, 0);
File imageFile = (File) getIntent().getSerializableExtra(IMAGE_FILE_KEY);
mTransitionImage.setScaleType(scaleType);
mImageDownloader.load(imageFile).noFade().into(mTransitionImage);
}
private int getStatusBarHeight() {
int result = 0;
int resourceId = getResources().getIdentifier("status_bar_height", "dimen", "android");
if (resourceId > 0) {
result = getResources().getDimensionPixelSize(resourceId);
}
return result;
}
@Override
protected void onDestroy() {
super.onDestroy();
// prevent leaking activity if image was not loaded yet
Picasso.with(this).cancelRequest(mEnlargedImage);
}
@Override
public void onBackPressed() {
// We don't call super to leave this activity on the screen when back is pressed
// super.onBackPressed();
Log.v(TAG, "onBackPressed");
mEnterScreenAnimations.cancelRunningAnimations();
Log.v(TAG, "onBackPressed, mExitingAnimation " + mExitingAnimation);
Bundle initialBundle = getIntent().getExtras();
int toTop = initialBundle.getInt(KEY_THUMBNAIL_INIT_TOP_POSITION);
int toLeft = initialBundle.getInt(KEY_THUMBNAIL_INIT_LEFT_POSITION);
int toWidth = initialBundle.getInt(KEY_THUMBNAIL_INIT_WIDTH);
int toHeight = initialBundle.getInt(KEY_THUMBNAIL_INIT_HEIGHT);
mExitScreenAnimations.playExitAnimations(
toTop,
toLeft,
toWidth,
toHeight,
mEnterScreenAnimations.getInitialThumbnailMatrixValues());
}
public static Intent getStartIntent(Activity activity, File imageFile, int left, int top, int width, int height, ImageView.ScaleType scaleType) {
Log.v(TAG, "getStartIntent, imageFile " + imageFile);
Intent startIntent = new Intent(activity, ImageDetailsActivity.class);
startIntent.putExtra(IMAGE_FILE_KEY, imageFile);
startIntent.putExtra(KEY_THUMBNAIL_INIT_TOP_POSITION, top);
startIntent.putExtra(KEY_THUMBNAIL_INIT_LEFT_POSITION, left);
startIntent.putExtra(KEY_THUMBNAIL_INIT_WIDTH, width);
startIntent.putExtra(KEY_THUMBNAIL_INIT_HEIGHT, height);
startIntent.putExtra(KEY_SCALE_TYPE, scaleType);
return startIntent;
}
@Override
protected void onResume() {
super.onResume();
Log.v(TAG, "onResume");
}
}
================================================
FILE: app/src/main/java/com/volokh/danylo/imagetransition/activities/ImagesListActivity.java
================================================
package com.volokh.danylo.imagetransition.activities;
import android.app.Activity;
import android.content.Intent;
import android.graphics.Rect;
import android.os.Build;
import android.os.Bundle;
import android.support.v7.widget.GridLayoutManager;
import android.support.v7.widget.RecyclerView;
import android.util.Log;
import android.view.View;
import android.widget.Button;
import android.widget.ImageView;
import android.widget.TextView;
import com.squareup.otto.Bus;
import com.squareup.otto.Subscribe;
import com.squareup.picasso.Picasso;
import com.volokh.danylo.imagetransition.event_bus.EventBusCreator;
import com.volokh.danylo.imagetransition.ImageFilesCreateLoader;
import com.volokh.danylo.imagetransition.adapter.ImagesAdapter;
import com.volokh.danylo.imagetransition.activities_v21.ImagesListActivity_v21;
import com.volokh.danylo.imagetransition.R;
import com.volokh.danylo.imagetransition.event_bus.ChangeImageThumbnailVisibility;
import com.volokh.danylo.imagetransition.adapter.Image;
import java.io.File;
import java.util.ArrayList;
import java.util.List;
public class ImagesListActivity extends Activity implements ImagesAdapter.ImagesAdapterCallback {
private static final String TAG = ImagesListActivity.class.getSimpleName();
private static final String IMAGE_DETAILS_IMAGE_MODEL = "IMAGE_DETAILS_IMAGE_MODEL";
private final List mImagesList = new ArrayList<>();
private static final int SPAN_COUNT = 2;
private Picasso mImageDownloader;
private RecyclerView mRecyclerView;
private GridLayoutManager mLayoutManager;
private ImagesAdapter mAdapter;
private Image mImageDetailsImageModel;
private final Bus mBus = EventBusCreator.defaultEventBus();
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.images_list);
initializeImagesDownloader();
initializeAdapter();
initializeImages();
initializeRecyclerView();
initializeTitle();
initializeSavedImageModel(savedInstanceState);
initializeSwitchButton();
}
private void initializeImagesDownloader() {
mImageDownloader = Picasso.with(this);
}
private void initializeAdapter() {
mAdapter = new ImagesAdapter(this, mImagesList, mImageDownloader, SPAN_COUNT);
}
private void initializeImages() {
// load images
getLoaderManager().initLoader(0, null, new ImageFilesCreateLoader(this, new ImageFilesCreateLoader.LoadFinishedCallback() {
@Override
public void onLoadFinished(List imagesList) {
mImagesList.addAll(imagesList);
mAdapter.notifyDataSetChanged();
}
})).forceLoad();
}
private void initializeSwitchButton() {
Button switchButton = (Button) findViewById(R.id.switch_to);
if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP){
switchButton.setVisibility(View.VISIBLE);
switchButton.setText("Switch to Lollipop List Activity");
switchButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
ImagesListActivity.this.finish();
Intent startActivity_v21_intent = new Intent(ImagesListActivity.this, ImagesListActivity_v21.class);
startActivity(startActivity_v21_intent);
}
});
}
}
private void initializeSavedImageModel(Bundle savedInstanceState) {
Log.v(TAG, "initializeSavedImageModel, savedInstanceState " + savedInstanceState);
if(savedInstanceState != null){
mImageDetailsImageModel = savedInstanceState.getParcelable(IMAGE_DETAILS_IMAGE_MODEL);
}
if(mImageDetailsImageModel != null) {
updateModel(mImageDetailsImageModel);
}
}
private void initializeTitle() {
TextView title = (TextView) findViewById(R.id.title);
title.setText("List Activity Ice Cream Sandwich");
}
private void initializeRecyclerView() {
mRecyclerView = (RecyclerView) findViewById(R.id.accounts_recycler_view);
mLayoutManager = new GridLayoutManager(this, SPAN_COUNT);
mRecyclerView.setLayoutManager(mLayoutManager);
mRecyclerView.setAdapter(mAdapter);
// we don't need animation when items are hidden or shown
mRecyclerView.setItemAnimator(null);
}
@Override
protected void onStart() {
super.onStart();
Log.v(TAG, "onStart");
mBus.register(this);
}
@Override
protected void onStop() {
super.onStop();
Log.v(TAG, "onStop");
mBus.unregister(this);
}
@Override
protected void onResume() {
super.onResume();
Log.v(TAG, "onResume");
if(mRecyclerView.getChildCount() > 1){
Log.v(TAG, "onResume, " + mRecyclerView.getChildAt(4).getVisibility());
}
}
/**
* This method is called when event is sent from {@link ImageDetailsActivity}
* via event bus.
*
* When it's called it means that transition animation started and we have to hide the image on this activity in order to look
* like image from here is transitioned to the new screen
*
*/
@Subscribe
public void hideImageThumbnail(ChangeImageThumbnailVisibility message){
Log.v(TAG, ">> hideImageThumbnail");
mImageDetailsImageModel.setVisibility(message.isVisible());
updateModel(mImageDetailsImageModel);
Log.v(TAG, "<< hideImageThumbnail");
}
/**
* This method basically changes visibility of concrete item
*/
private void updateModel(Image imageToUpdate) {
Log.v(TAG, "updateModel, imageToUpdate " + imageToUpdate);
for (Image image : mImagesList) {
if(image.equals(imageToUpdate)){
Log.v(TAG, "updateModel, found imageToUpdate " + imageToUpdate);
image.setVisibility(imageToUpdate.isVisible());
break;
}
}
int index = mImagesList.indexOf(imageToUpdate);
Log.v(TAG, "updateModel, index " + index);
mAdapter.notifyItemChanged(index);
/**
* For some reason recycler view is not always redrawn when adapter updated.
* onBindViewHolder is called but image doesn't disappear from screen
* That's why we have to do this invalidation
*/
Rect dirty = new Rect();
View viewAtPosition = mLayoutManager.findViewByPosition(index);
viewAtPosition.getDrawingRect(dirty);
mRecyclerView.invalidate(dirty);
}
@Override
protected void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
outState.putParcelable(IMAGE_DETAILS_IMAGE_MODEL, mImageDetailsImageModel);
}
@Override
public void enterImageDetails(String sharedImageTransitionName, File imageFile, final ImageView image, Image imageModel) {
Log.v(TAG, "enterImageDetails, imageFile " + imageFile);
Log.v(TAG, "enterImageDetails, image.getScaleType() " + image.getScaleType());
/**
* We store this model for two purposes:
* 1. When image on the next screen will be transitioned we have to hide the view
* representation of this model in order it to look like this exact view is transitioned.
* Transitioned view will leave empty space behind it
*
* 2. Activity might be destroyed at some point and we have to store this information to restore view
* visibility when activity is recreated.
*/
mImageDetailsImageModel = imageModel;
int[] screenLocation = new int[2];
image.getLocationInWindow(screenLocation);
Intent startIntent = ImageDetailsActivity.getStartIntent(this, imageFile,
screenLocation[0],
screenLocation[1],
image.getWidth(),
image.getHeight(),
image.getScaleType());
startActivity(startIntent);
}
}
================================================
FILE: app/src/main/java/com/volokh/danylo/imagetransition/activities_v21/ImageDetailsActivity_v21.java
================================================
package com.volokh.danylo.imagetransition.activities_v21;
import android.app.Activity;
import android.content.Intent;
import android.os.Bundle;
import android.support.v4.view.ViewCompat;
import android.view.View;
import android.widget.ImageView;
import com.squareup.picasso.Picasso;
import com.volokh.danylo.imagetransition.R;
import java.io.File;
/**
* Created by danylo.volokh on 2/21/2016.
*/
public class ImageDetailsActivity_v21 extends Activity{
public static final String SHARED_ELEMENT_IMAGE_KEY = "SHARED_ELEMENT_IMAGE_KEY";
public static final String IMAGE_FILE_KEY = "IMAGE_FILE_KEY";
private ImageView mImage;
private Picasso mImageDownloader;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.image_details_activity_layout);
mImage = (ImageView) findViewById(R.id.enlarged_image);
mImage.setVisibility(View.VISIBLE);
String imageTransitionName = getIntent().getStringExtra(SHARED_ELEMENT_IMAGE_KEY);
ViewCompat.setTransitionName(mImage, imageTransitionName);
View mainContainer = findViewById(R.id.main_container);
mainContainer.setAlpha(1.f);
mImageDownloader = Picasso.with(this);
File imageFile = (File) getIntent().getSerializableExtra(IMAGE_FILE_KEY);
mImageDownloader.load(imageFile).into(mImage);
}
public static Intent getStartIntent(Activity activity, String sharedImageTransitionName, File imageFile) {
Intent startIntent = new Intent(activity, ImageDetailsActivity_v21.class);
startIntent.putExtra(SHARED_ELEMENT_IMAGE_KEY, sharedImageTransitionName);
startIntent.putExtra(IMAGE_FILE_KEY, imageFile);
return startIntent;
}
}
================================================
FILE: app/src/main/java/com/volokh/danylo/imagetransition/activities_v21/ImagesListActivity_v21.java
================================================
package com.volokh.danylo.imagetransition.activities_v21;
import android.app.Activity;
import android.app.ActivityOptions;
import android.content.Intent;
import android.os.Build;
import android.os.Bundle;
import android.support.v7.widget.GridLayoutManager;
import android.support.v7.widget.RecyclerView;
import android.transition.ChangeImageTransform;
import android.view.View;
import android.widget.Button;
import android.widget.ImageView;
import android.widget.TextView;
import com.squareup.picasso.Picasso;
import com.volokh.danylo.imagetransition.ImageFilesCreateLoader;
import com.volokh.danylo.imagetransition.adapter.ImagesAdapter;
import com.volokh.danylo.imagetransition.R;
import com.volokh.danylo.imagetransition.activities.ImagesListActivity;
import com.volokh.danylo.imagetransition.adapter.Image;
import java.io.File;
import java.util.ArrayList;
import java.util.List;
public class ImagesListActivity_v21 extends Activity implements ImagesAdapter.ImagesAdapterCallback {
private static final String TAG = ImagesListActivity_v21.class.getSimpleName();
private final List mImagesList = new ArrayList<>();
private static final int SPAN_COUNT = 2;
private Picasso mImageDownloader;
private RecyclerView mRecyclerView;
private ImagesAdapter mAdapter;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.images_list);
mImageDownloader = Picasso.with(this);
mAdapter = new ImagesAdapter(this, mImagesList, mImageDownloader, SPAN_COUNT);
getLoaderManager().initLoader(0, null, new ImageFilesCreateLoader(this, new ImageFilesCreateLoader.LoadFinishedCallback() {
@Override
public void onLoadFinished(List imagesList) {
mImagesList.addAll(imagesList);
mAdapter.notifyDataSetChanged();
}
})).forceLoad();
mRecyclerView = (RecyclerView) findViewById(R.id.accounts_recycler_view);
mRecyclerView.setLayoutManager(new GridLayoutManager(this, SPAN_COUNT));
mRecyclerView.setAdapter(mAdapter);
TextView title = (TextView) findViewById(R.id.title);
title.setText("List Activity Lollipop");
Button switchButton = (Button) findViewById(R.id.switch_to);
if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP){
switchButton.setVisibility(View.VISIBLE);
switchButton.setText("Switch to Ice Cream Sandwich List Activity");
switchButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
ImagesListActivity_v21.this.finish();
Intent startActivityIntent = new Intent(ImagesListActivity_v21.this, ImagesListActivity.class);
startActivity(startActivityIntent);
}
});
}
}
@Override
public void enterImageDetails(String sharedImageTransitionName, File imageFile, ImageView image, Image imageModel) {
ActivityOptions activityOptions =
ActivityOptions.makeSceneTransitionAnimation(this, image, sharedImageTransitionName);
getWindow().setSharedElementEnterTransition(new ChangeImageTransform(this, null));
Intent startIntent = ImageDetailsActivity_v21.getStartIntent(this, sharedImageTransitionName, imageFile);
startActivity(startIntent, activityOptions.toBundle());
}
}
================================================
FILE: app/src/main/java/com/volokh/danylo/imagetransition/adapter/Image.java
================================================
package com.volokh.danylo.imagetransition.adapter;
import android.os.Parcel;
import android.os.Parcelable;
import java.io.File;
/**
* Created by danylo.volokh on 2/21/2016.
*
* We use Parcelable in order it to store this model in bundle.
*
* NOTE: it is better to use some cache for models.(Maybe ORM)
* Parcelable is more verbose then Serializable, but we use it because #PerfMatters
*/
public class Image implements Parcelable{
public final int imageId;
public final File imageFile;
/**
* By default the image is visible but for the purpose of the animation it will be changed to invisible at some time.
*/
private boolean mImageIsVisible = true;
public Image(int imageId, File imageFile) {
this.imageId = imageId;
this.imageFile = imageFile;
}
protected Image(Parcel in) {
imageId = in.readInt();
imageFile = (File) in.readSerializable();
mImageIsVisible = in.readByte() != 0;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Image image = (Image) o;
return imageId == image.imageId;
}
@Override
public int hashCode() {
return imageId;
}
/**
* This is generated by studio
*/
@Override
public String toString() {
return "Image{" +
"imageId=" + imageId +
", imageFile=" + imageFile +
", mImageIsVisible=" + mImageIsVisible +
'}';
}
public static final Creator CREATOR = new Creator() {
@Override
public Image createFromParcel(Parcel in) {
return new Image(in);
}
@Override
public Image[] newArray(int size) {
return new Image[size];
}
};
@Override
public int describeContents() {
return 0;
}
@Override
public void writeToParcel(Parcel dest, int flags) {
dest.writeInt(imageId);
dest.writeSerializable(imageFile);
dest.writeByte((byte) (mImageIsVisible ? 1 : 0));
}
public boolean isVisible() {
return mImageIsVisible;
}
public void setVisibility(boolean isVisible){
mImageIsVisible = isVisible;
}
}
================================================
FILE: app/src/main/java/com/volokh/danylo/imagetransition/adapter/ImagesAdapter.java
================================================
package com.volokh.danylo.imagetransition.adapter;
import android.support.v4.view.ViewCompat;
import android.support.v7.widget.RecyclerView;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageView;
import com.squareup.picasso.Picasso;
import com.volokh.danylo.imagetransition.R;
import java.io.File;
import java.util.List;
/**
* Created by danylo.volokh on 2/19/2016.
*
* This adapter class is for the list of images. There is 6 types of item types.
* Each on for different {@link android.widget.ImageView.ScaleType}
*/
public class ImagesAdapter extends RecyclerView.Adapter {
private static final String TAG = ImagesAdapter.class.getSimpleName();
private final ImagesAdapterCallback mImagesAdapterCallback;
private final List mImagesList;
private final Picasso mImageDownloader;
private final int mSpanCount;
public interface ImagesAdapterCallback{
void enterImageDetails(String sharedImageTransitionName, File imageFile, ImageView image, Image imageModel);
}
public ImagesAdapter(ImagesAdapterCallback imagesAdapterCallback, List imagesList, Picasso imageDownloader, int spanCount) {
mImagesAdapterCallback = imagesAdapterCallback;
mImagesList = imagesList;
mImageDownloader = imageDownloader;
mSpanCount = spanCount;
}
public static String createSharedImageTransitionName(int position){
return "SharedImage" + position;
}
@Override
public ImagesViewHolder onCreateViewHolder(final ViewGroup parent, int viewType) {
View item = LayoutInflater.from(parent.getContext()).inflate(R.layout.image_item, parent, false);
RecyclerView.LayoutParams layoutParams = (RecyclerView.LayoutParams) item.getLayoutParams();
layoutParams.height = item.getResources().getDisplayMetrics().widthPixels / mSpanCount;
ImagesViewHolder viewHolder = new ImagesViewHolder(item);
/**
* Depends on View type we set the according {@link android.widget.ImageView.ScaleType
*/
if(viewType == 0) {
viewHolder.image.setScaleType(ImageView.ScaleType.CENTER);
}
if(viewType == 1) {
viewHolder.image.setScaleType(ImageView.ScaleType.CENTER_CROP);
}
if(viewType == 2) {
viewHolder.image.setScaleType(ImageView.ScaleType.CENTER_INSIDE);
}
if(viewType == 3) {
viewHolder.image.setScaleType(ImageView.ScaleType.FIT_CENTER);
}
if(viewType == 4) {
viewHolder.image.setScaleType(ImageView.ScaleType.FIT_XY);
}
if(viewType == 5) {
viewHolder.image.setScaleType(ImageView.ScaleType.MATRIX);
}
return viewHolder;
}
@Override
public void onBindViewHolder(final ImagesViewHolder holder, final int position) {
final Image image = mImagesList.get(position);
Log.v(TAG, "onBindViewHolder holder.imageFile " + image.imageFile);
Log.v(TAG, "onBindViewHolder holder.image " + holder.image);
Log.v(TAG, "onBindViewHolder holder.matrix " + holder.image.getMatrix());
Log.v(TAG, "onBindViewHolder holder.scaleType " + holder.image.getScaleType());
// set transition name for sdk 21 shared element transition
final String sharedImageTransitionName = createSharedImageTransitionName(position);
ViewCompat.setTransitionName(holder.image, sharedImageTransitionName);
Log.v(TAG, "onBindViewHolder isVisible " + image.isVisible());
Log.v(TAG, "onBindViewHolder isVisible " + holder.image.getVisibility());
if(image.isVisible()){
mImageDownloader.load(image.imageFile).into(holder.image);
holder.image.setVisibility(View.VISIBLE);
} else {
/** in purpose of animation image might become invisible */
holder.image.setVisibility(View.INVISIBLE);
}
Log.v(TAG, "onBindViewHolder isVisible " + holder.image.getVisibility());
holder.image.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Log.v(TAG, "onClick image " + holder.image );
Log.v(TAG, "onClick matrix " + holder.image.getMatrix() );
mImagesAdapterCallback.enterImageDetails(sharedImageTransitionName, image.imageFile, holder.image, image);
}
});
Log.v(TAG, "onBindViewHolder position " + position);
}
@Override
public int getItemCount() {
return mImagesList.size();
}
/**
* We create 6 item types for each {@link android.widget.ImageView.ScaleType} there is,
* besides:
* {@link android.widget.ImageView.ScaleType#FIT_START}
* {@link android.widget.ImageView.ScaleType#FIT_END}
*/
@Override
public int getItemViewType(int position) {
if(position % 6 == 0) {
return 0;
}
if(position % 5 == 0) {
return 1;
}
if(position % 4 == 0) {
return 2;
}
if(position % 3 == 0) {
return 3;
}
if(position % 2 == 0) {
return 4;
}
return 5;
}
}
================================================
FILE: app/src/main/java/com/volokh/danylo/imagetransition/adapter/ImagesViewHolder.java
================================================
package com.volokh.danylo.imagetransition.adapter;
import android.support.v7.widget.RecyclerView;
import android.view.View;
import android.widget.ImageView;
import com.volokh.danylo.imagetransition.R;
public class ImagesViewHolder extends RecyclerView.ViewHolder{
public final ImageView image;
public ImagesViewHolder(View itemView) {
super(itemView);
image = (ImageView) itemView.findViewById(R.id.image);
}
}
================================================
FILE: app/src/main/java/com/volokh/danylo/imagetransition/animations/EnterScreenAnimations.java
================================================
package com.volokh.danylo.imagetransition.animations;
import android.animation.Animator;
import android.animation.AnimatorSet;
import android.animation.ObjectAnimator;
import android.animation.PropertyValuesHolder;
import android.graphics.Matrix;
import android.support.annotation.NonNull;
import android.util.Log;
import android.view.View;
import android.view.animation.AccelerateInterpolator;
import android.widget.FrameLayout;
import android.widget.ImageView;
import java.util.Arrays;
/**
* Created by danylo.volokh on 3/16/16.
*/
public class EnterScreenAnimations extends ScreenAnimation{
private static final String TAG = EnterScreenAnimations.class.getSimpleName();
/**
* Image with is initially situated on the place from where transition starts
*/
private final ImageView mImageTo;
/**
* This is image that represents of how should transitioned image look like at the end of transition
*/
private final ImageView mAnimatedImage;
private View mMainContainer;
private AnimatorSet mEnteringAnimation;
/**
* This array will contain the data about image matrix of original image.
* We will use that matrix to animate image back to original state
*/
private float[] mInitThumbnailMatrixValues = new float[9];
/**
* These values represent the final position of a Image that is translated
*/
private int mToTop;
private int mToLeft;
private int mToWidth;
private int mToHeight;
public EnterScreenAnimations(ImageView animatedImage, ImageView imageTo, View mainContainer) {
super(animatedImage.getContext());
mAnimatedImage = animatedImage;
mImageTo = imageTo;
mMainContainer = mainContainer;
}
/**
* This method combines several animations when screen is opened.
* 1. Animation of ImageView position and image matrix.
* 2. Animation of main container elements - they are fading in after image is animated to position
*/
public void playEnteringAnimation(int left, int top, int width, int height) {
Log.v(TAG, ">> playEnteringAnimation");
mToLeft = left;
mToTop = top;
mToWidth = width;
mToHeight = height;
AnimatorSet imageAnimatorSet = createEnteringImageAnimation();
Animator mainContainerFadeAnimator = createEnteringFadeAnimator();
mEnteringAnimation = new AnimatorSet();
mEnteringAnimation.setDuration(IMAGE_TRANSLATION_DURATION);
mEnteringAnimation.setInterpolator(new AccelerateInterpolator());
mEnteringAnimation.addListener(new SimpleAnimationListener() {
@Override
public void onAnimationCancel(Animator animation) {
Log.v(TAG, "onAnimationCancel, mEnteringAnimation " + mEnteringAnimation);
mEnteringAnimation = null;
}
@Override
public void onAnimationEnd(Animator animation) {
Log.v(TAG, "onAnimationEnd, mEnteringAnimation " + mEnteringAnimation);
if (mEnteringAnimation != null) {
mEnteringAnimation = null;
mImageTo.setVisibility(View.VISIBLE);
mAnimatedImage.setVisibility(View.INVISIBLE);
} else {
// Animation was cancelled. Do nothing
}
}
});
mEnteringAnimation.playTogether(
imageAnimatorSet,
mainContainerFadeAnimator
);
mEnteringAnimation.start();
Log.v(TAG, "<< playEnteringAnimation");
}
/**
* Animator returned form this method animates fade in of all other elements on the screen besides ImageView
*/
private ObjectAnimator createEnteringFadeAnimator() {
ObjectAnimator fadeInAnimator = ObjectAnimator.ofFloat(mMainContainer, "alpha", 0.0f, 1.0f);
return fadeInAnimator;
}
/**
* This method creates an animator set of 2 animations:
* 1. ImageView position animation when screen is opened
* 2. ImageView image matrix animation when screen is opened
*/
@NonNull
private AnimatorSet createEnteringImageAnimation() {
Log.v(TAG, ">> createEnteringImageAnimation");
ObjectAnimator positionAnimator = createEnteringImagePositionAnimator();
ObjectAnimator matrixAnimator = createEnteringImageMatrixAnimator();
AnimatorSet enteringImageAnimation = new AnimatorSet();
enteringImageAnimation.playTogether(positionAnimator, matrixAnimator);
Log.v(TAG, "<< createEnteringImageAnimation");
return enteringImageAnimation;
}
/**
* This method creates an animator that changes ImageView position on the screen.
* It will look like view is translated from its position on previous screen to its new position on this screen
*/
@NonNull
private ObjectAnimator createEnteringImagePositionAnimator() {
Log.v(TAG, "createEnteringImagePositionAnimator");
PropertyValuesHolder propertyLeft = PropertyValuesHolder.ofInt("left", mAnimatedImage.getLeft(), mToLeft);
PropertyValuesHolder propertyTop = PropertyValuesHolder.ofInt("top", mAnimatedImage.getTop(),
mToTop - getStatusBarHeight());
PropertyValuesHolder propertyRight = PropertyValuesHolder.ofInt("right", mAnimatedImage.getRight(), mToLeft + mToWidth);
PropertyValuesHolder propertyBottom = PropertyValuesHolder.ofInt("bottom", mAnimatedImage.getBottom(), mToTop + mToHeight - getStatusBarHeight());
ObjectAnimator animator = ObjectAnimator.ofPropertyValuesHolder(mAnimatedImage, propertyLeft, propertyTop, propertyRight, propertyBottom);
animator.addListener(new SimpleAnimationListener() {
@Override
public void onAnimationEnd(Animator animation) {
// set new parameters of animated ImageView. This will prevent blinking of view when set visibility to visible in Exit animation
FrameLayout.LayoutParams layoutParams = (FrameLayout.LayoutParams) mAnimatedImage.getLayoutParams();
layoutParams.height = mImageTo.getHeight();
layoutParams.width = mImageTo.getWidth();
layoutParams.setMargins(mToLeft, mToTop - getStatusBarHeight(), 0, 0);
}
});
return animator;
}
/**
* This method creates Animator that will animate matrix of ImageView.
* It is needed in order to show the effect when scaling of one view is smoothly changed to the scale of the second view.
*
* For example: first view can have scaleType: centerCrop, and the other one fitCenter.
* The image inside ImageView will smoothly change from one to another
*/
private ObjectAnimator createEnteringImageMatrixAnimator() {
Matrix initMatrix = MatrixUtils.getImageMatrix(mAnimatedImage);
// store the data about original matrix into array.
// this array will be used later for exit animation
initMatrix.getValues(mInitThumbnailMatrixValues);
final Matrix endMatrix = MatrixUtils.getImageMatrix(mImageTo);
Log.v(TAG, "createEnteringImageMatrixAnimator, mInitThumbnailMatrixValues " + Arrays.toString(mInitThumbnailMatrixValues));
Log.v(TAG, "createEnteringImageMatrixAnimator, initMatrix " + initMatrix);
Log.v(TAG, "createEnteringImageMatrixAnimator, endMatrix " + endMatrix);
mAnimatedImage.setScaleType(ImageView.ScaleType.MATRIX);
return ObjectAnimator.ofObject(mAnimatedImage, MatrixEvaluator.ANIMATED_TRANSFORM_PROPERTY,
new MatrixEvaluator(), initMatrix, endMatrix);
}
public float[] getInitialThumbnailMatrixValues() {
return mInitThumbnailMatrixValues;
}
public void cancelRunningAnimations() {
Log.v(TAG, "cancelRunningAnimations, mEnteringAnimation " + mEnteringAnimation);
if (mEnteringAnimation != null) {
// cancel existing animation
mEnteringAnimation.cancel();
}
}
}
================================================
FILE: app/src/main/java/com/volokh/danylo/imagetransition/animations/ExitScreenAnimations.java
================================================
package com.volokh.danylo.imagetransition.animations;
import android.animation.Animator;
import android.animation.AnimatorSet;
import android.animation.ObjectAnimator;
import android.animation.PropertyValuesHolder;
import android.app.Activity;
import android.graphics.Matrix;
import android.support.annotation.NonNull;
import android.util.Log;
import android.view.View;
import android.view.animation.AccelerateInterpolator;
import android.widget.ImageView;
import com.squareup.otto.Bus;
import com.volokh.danylo.imagetransition.event_bus.ChangeImageThumbnailVisibility;
import com.volokh.danylo.imagetransition.event_bus.EventBusCreator;
/**
* Created by danylo.volokh on 3/16/16.
*/
public class ExitScreenAnimations extends ScreenAnimation{
private static final String TAG = ExitScreenAnimations.class.getSimpleName();
private final Bus mBus = EventBusCreator.defaultEventBus();
private final ImageView mAnimatedImage;
private final ImageView mImageTo;
private final View mMainContainer;
/**
* These values represent the final position of a Image that is translated
*/
private int mToTop;
private int mToLeft;
private int mToWidth;
private int mToHeight;
private AnimatorSet mExitingAnimation;
private float[] mToThumbnailMatrixValues;
public ExitScreenAnimations(ImageView animatedImage, ImageView imageTo, View mainContainer) {
super(animatedImage.getContext());
mAnimatedImage = animatedImage;
mImageTo = imageTo;
mMainContainer = mainContainer;
}
public void playExitAnimations(int toTop, int toLeft, int toWidth, int toHeight, float[] toThumbnailMatrixValues) {
mToTop = toTop;
mToLeft = toLeft;
mToWidth = toWidth;
mToHeight = toHeight;
mToThumbnailMatrixValues = toThumbnailMatrixValues;
Log.v(TAG, "playExitAnimations, mExitingAnimation " + mExitingAnimation);
if (mExitingAnimation == null) {
playExitingAnimation();
}
}
private void playExitingAnimation() {
Log.v(TAG, "playExitingAnimation");
mAnimatedImage.setVisibility(View.VISIBLE);
mImageTo.setVisibility(View.INVISIBLE);
AnimatorSet imageAnimatorSet = createExitingImageAnimation();
Animator mainContainerFadeAnimator = createExitingFadeAnimator();
mExitingAnimation = new AnimatorSet();
mExitingAnimation.setDuration(IMAGE_TRANSLATION_DURATION);
mExitingAnimation.setInterpolator(new AccelerateInterpolator());
mExitingAnimation.addListener(new SimpleAnimationListener() {
@Override
public void onAnimationEnd(Animator animation) {
mBus.post(new ChangeImageThumbnailVisibility(true));
Log.v(TAG, "onAnimationEnd, mExitingAnimation " + mExitingAnimation);
mExitingAnimation = null;
// finish the activity when animation is finished
Activity activity = (Activity) mAnimatedImage.getContext();
activity.finish();
activity.overridePendingTransition(0, 0);
}
});
mExitingAnimation.playTogether(
imageAnimatorSet,
mainContainerFadeAnimator
);
mExitingAnimation.start();
}
/**
* This method creates an animator set of 2 animations:
* 1. ImageView position animation when screen is closed
* 2. ImageView image matrix animation when screen is closed
*/
private AnimatorSet createExitingImageAnimation() {
Log.v(TAG, ">> createExitingImageAnimation");
ObjectAnimator positionAnimator = createExitingImagePositionAnimator();
ObjectAnimator matrixAnimator = createExitingImageMatrixAnimator();
AnimatorSet exitingImageAnimation = new AnimatorSet();
exitingImageAnimation.playTogether(positionAnimator, matrixAnimator);
Log.v(TAG, "<< createExitingImageAnimation");
return exitingImageAnimation;
}
/**
* This method creates an animator that changes ImageView position on the screen.
* It will look like view is translated from its position on this screen to its position on previous screen
*/
@NonNull
private ObjectAnimator createExitingImagePositionAnimator() {
// get initial location on the screen and start animation from there
int[] locationOnScreen = new int[2];
mAnimatedImage.getLocationOnScreen(locationOnScreen);
PropertyValuesHolder propertyLeft = PropertyValuesHolder.ofInt("left",
locationOnScreen[0],
mToLeft);
PropertyValuesHolder propertyTop = PropertyValuesHolder.ofInt("top",
locationOnScreen[1] - getStatusBarHeight(),
mToTop - getStatusBarHeight());
PropertyValuesHolder propertyRight = PropertyValuesHolder.ofInt("right",
locationOnScreen[0] + mAnimatedImage.getWidth(),
mToLeft + mToWidth);
PropertyValuesHolder propertyBottom = PropertyValuesHolder.ofInt("bottom",
mAnimatedImage.getBottom(),
mToTop + mToHeight - getStatusBarHeight());
return ObjectAnimator.ofPropertyValuesHolder(mAnimatedImage, propertyLeft, propertyTop, propertyRight, propertyBottom);
}
/**
* This method creates animator that animates Matrix of ImageView.
* It is needed in order to show the effect when scaling of one view is smoothly changed to the scale of the second view.
*
* For example: first view can have scaleType: centerCrop, and the other one fitCenter.
* The image inside ImageView will smoothly change from one to another
*/
private ObjectAnimator createExitingImageMatrixAnimator() {
Matrix initialMatrix = MatrixUtils.getImageMatrix(mAnimatedImage);
Matrix endMatrix = new Matrix();
endMatrix.setValues(mToThumbnailMatrixValues);
Log.v(TAG, "createExitingImageMatrixAnimator, initialMatrix " + initialMatrix);
Log.v(TAG, "createExitingImageMatrixAnimator, endMatrix " + endMatrix);
mAnimatedImage.setScaleType(ImageView.ScaleType.MATRIX);
return ObjectAnimator.ofObject(mAnimatedImage, MatrixEvaluator.ANIMATED_TRANSFORM_PROPERTY,
new MatrixEvaluator(), initialMatrix, endMatrix);
}
private ObjectAnimator createExitingFadeAnimator() {
ObjectAnimator fadeInAnimator = ObjectAnimator.ofFloat(mMainContainer, "alpha", 1.0f, 0.0f);
return fadeInAnimator;
}
}
================================================
FILE: app/src/main/java/com/volokh/danylo/imagetransition/animations/MatrixEvaluator.java
================================================
package com.volokh.danylo.imagetransition.animations;
import android.animation.TypeEvaluator;
import android.graphics.Matrix;
import android.graphics.drawable.Drawable;
import android.util.Property;
import android.widget.ImageView;
/**
* This class is passed to ObjectAnimator in order to animate changes in ImageView image matrix
*/
public class MatrixEvaluator implements TypeEvaluator {
private static final String TAG = MatrixEvaluator.class.getSimpleName();
public static TypeEvaluator NULL_MATRIX_EVALUATOR = new TypeEvaluator() {
@Override
public Matrix evaluate(float fraction, Matrix startValue, Matrix endValue) {
return null;
}
};
/**
* This property is passed to ObjectAnimator when we are animating image matrix of ImageView
*/
public static final Property ANIMATED_TRANSFORM_PROPERTY = new Property(Matrix.class,
"animatedTransform") {
/**
* This is copy-paste form ImageView#animateTransform - method is invisible in sdk
*/
@Override
public void set(ImageView imageView, Matrix matrix) {
Drawable drawable = imageView.getDrawable();
if (drawable == null) {
return;
}
if (matrix == null) {
drawable.setBounds(0, 0, imageView.getWidth(), imageView.getHeight());
} else {
drawable.setBounds(0, 0, drawable.getIntrinsicWidth(), drawable.getIntrinsicHeight());
Matrix drawMatrix = imageView.getImageMatrix();
if (drawMatrix == null) {
drawMatrix = new Matrix();
imageView.setImageMatrix(drawMatrix);
}
imageView.setImageMatrix(matrix);
}
imageView.invalidate();
}
@Override
public Matrix get(ImageView object) {
return null;
}
};
float[] mTempStartValues = new float[9];
float[] mTempEndValues = new float[9];
Matrix mTempMatrix = new Matrix();
@Override
public Matrix evaluate(float fraction, Matrix startValue, Matrix endValue) {
startValue.getValues(mTempStartValues);
endValue.getValues(mTempEndValues);
for (int i = 0; i < 9; i++) {
float diff = mTempEndValues[i] - mTempStartValues[i];
mTempEndValues[i] = mTempStartValues[i] + (fraction * diff);
}
mTempMatrix.setValues(mTempEndValues);
return mTempMatrix;
}
}
================================================
FILE: app/src/main/java/com/volokh/danylo/imagetransition/animations/MatrixUtils.java
================================================
package com.volokh.danylo.imagetransition.animations;
import android.graphics.Matrix;
import android.graphics.Rect;
import android.graphics.drawable.Drawable;
import android.util.Log;
import android.widget.ImageView;
/**
* Created by danylo.volokh on 3/14/16.
*/
public class MatrixUtils {
private static final String TAG = MatrixUtils.class.getSimpleName();
public static Matrix getImageMatrix(ImageView imageView) {
Log.v(TAG, "getImageMatrix, imageView " + imageView);
int left = imageView.getLeft();
int top = imageView.getTop();
int right = imageView.getRight();
int bottom = imageView.getBottom();
Rect bounds = new Rect(left, top, right, bottom);
Drawable drawable = imageView.getDrawable();
Matrix matrix;
ImageView.ScaleType scaleType = imageView.getScaleType();
Log.v(TAG, "getImageMatrix, scaleType " + scaleType);
if (scaleType == ImageView.ScaleType.FIT_XY) {
matrix = imageView.getImageMatrix();
if (!matrix.isIdentity()) {
matrix = new Matrix(matrix);
} else {
int drawableWidth = drawable.getIntrinsicWidth();
int drawableHeight = drawable.getIntrinsicHeight();
if (drawableWidth > 0 && drawableHeight > 0) {
float scaleX = ((float) bounds.width()) / drawableWidth;
float scaleY = ((float) bounds.height()) / drawableHeight;
matrix = new Matrix();
matrix.setScale(scaleX, scaleY);
} else {
matrix = null;
}
}
} else {
matrix = new Matrix(imageView.getImageMatrix());
}
return matrix;
}
}
================================================
FILE: app/src/main/java/com/volokh/danylo/imagetransition/animations/ScreenAnimation.java
================================================
package com.volokh.danylo.imagetransition.animations;
import android.content.Context;
/**
* Created by danylo.volokh on 3/19/16.
*/
public abstract class ScreenAnimation {
protected static final long IMAGE_TRANSLATION_DURATION = 1000;
private final Context mContext;
protected ScreenAnimation(Context context) {
mContext = context;
}
protected int getStatusBarHeight() {
int result = 0;
int resourceId = mContext.getResources().getIdentifier("status_bar_height", "dimen", "android");
if (resourceId > 0) {
result = mContext.getResources().getDimensionPixelSize(resourceId);
}
return result;
}
}
================================================
FILE: app/src/main/java/com/volokh/danylo/imagetransition/animations/SimpleAnimationListener.java
================================================
package com.volokh.danylo.imagetransition.animations;
import android.animation.Animator;
/**
* Created by danylo.volokh on 3/9/16.
*/
public class SimpleAnimationListener implements Animator.AnimatorListener {
/**
* For overriding
*/
@Override
public void onAnimationStart(Animator animation) {
}
/**
* For overriding
*/
@Override
public void onAnimationEnd(Animator animation) {
}
/**
* For overriding
*/
@Override
public void onAnimationCancel(Animator animation) {
}
/**
* For overriding
*/
@Override
public void onAnimationRepeat(Animator animation) {
}
}
================================================
FILE: app/src/main/java/com/volokh/danylo/imagetransition/event_bus/ChangeImageThumbnailVisibility.java
================================================
package com.volokh.danylo.imagetransition.event_bus;
/**
* Created by danylo.volokh on 3/15/16.
*
* This message is sent from {@link com.volokh.danylo.imagetransition.activities.ImageDetailsActivity}
* to {@link com.volokh.danylo.imagetransition.activities.ImagesListActivity}
*
* When image transition is about to start. This message should invoke hiding of original image
* Which transition we are imitating.
*
*/
public class ChangeImageThumbnailVisibility {
private final boolean visible;
public ChangeImageThumbnailVisibility(boolean visible) {
this.visible = visible;
}
public boolean isVisible() {
return visible;
}
}
================================================
FILE: app/src/main/java/com/volokh/danylo/imagetransition/event_bus/EventBusCreator.java
================================================
package com.volokh.danylo.imagetransition.event_bus;
import com.squareup.otto.Bus;
/**
* Created by danylo.volokh on 3/14/16.
* Double checked singleton (basically we need it only in UI thread) for Otto event bus.
*/
public class EventBusCreator {
private static Bus bus;
public static Bus defaultEventBus() {
if (bus == null) {
synchronized (EventBusCreator.class) {
if (bus == null) {
bus = new Bus();
}
}
}
return bus;
}
}
================================================
FILE: app/src/main/res/layout/image_details_activity_layout.xml
================================================
================================================
FILE: app/src/main/res/layout/image_item.xml
================================================
================================================
FILE: app/src/main/res/layout/images_list.xml
================================================
================================================
FILE: app/src/main/res/transition/change_image_transition.xml
================================================
================================================
FILE: app/src/main/res/transition/enter_exit.xml
================================================
================================================
FILE: app/src/main/res/values/colors.xml
================================================
#3F51B5#303F9F#FF4081@color/material_deep_teal_500@color/material_deep_teal_200#F7D896#DDDDDD
================================================
FILE: app/src/main/res/values/styles.xml
================================================
================================================
FILE: app/src/main/res/values-v21/styles.xml
================================================
>
================================================
FILE: app/src/test/java/com/volokh/danylo/imagetransition/ExampleUnitTest.java
================================================
package com.volokh.danylo.imagetransition;
import org.junit.Test;
import static org.junit.Assert.*;
/**
* To work on unit tests, switch the Test Artifact in the Build Variants view.
*/
public class ExampleUnitTest {
@Test
public void addition_isCorrect() throws Exception {
assertEquals(4, 2 + 2);
}
}
================================================
FILE: build.gradle
================================================
// Top-level build file where you can add configuration options common to all sub-projects/modules.
buildscript {
repositories {
jcenter()
}
dependencies {
classpath 'com.android.tools.build:gradle:1.5.0'
// NOTE: Do not place your application dependencies here; they belong
// in the individual module build.gradle files
}
}
allprojects {
repositories {
jcenter()
}
}
task clean(type: Delete) {
delete rootProject.buildDir
}
================================================
FILE: gradle/wrapper/gradle-wrapper.properties
================================================
#Wed Oct 21 11:34:03 PDT 2015
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-2.8-all.zip
================================================
FILE: gradle.properties
================================================
# Project-wide Gradle settings.
# IDE (e.g. Android Studio) users:
# Gradle settings configured through the IDE *will override*
# any settings specified in this file.
# For more details on how to configure your build environment visit
# http://www.gradle.org/docs/current/userguide/build_environment.html
# Specifies the JVM arguments used for the daemon process.
# The setting is particularly useful for tweaking memory settings.
# Default value: -Xmx10248m -XX:MaxPermSize=256m
# org.gradle.jvmargs=-Xmx2048m -XX:MaxPermSize=512m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8
# When configured, Gradle will run in incubating parallel mode.
# This option should only be used with decoupled projects. More details, visit
# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
# org.gradle.parallel=true
================================================
FILE: gradlew
================================================
#!/usr/bin/env bash
##############################################################################
##
## Gradle start up script for UN*X
##
##############################################################################
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS=""
APP_NAME="Gradle"
APP_BASE_NAME=`basename "$0"`
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD="maximum"
warn ( ) {
echo "$*"
}
die ( ) {
echo
echo "$*"
echo
exit 1
}
# OS specific support (must be 'true' or 'false').
cygwin=false
msys=false
darwin=false
case "`uname`" in
CYGWIN* )
cygwin=true
;;
Darwin* )
darwin=true
;;
MINGW* )
msys=true
;;
esac
# Attempt to set APP_HOME
# Resolve links: $0 may be a link
PRG="$0"
# Need this for relative symlinks.
while [ -h "$PRG" ] ; do
ls=`ls -ld "$PRG"`
link=`expr "$ls" : '.*-> \(.*\)$'`
if expr "$link" : '/.*' > /dev/null; then
PRG="$link"
else
PRG=`dirname "$PRG"`"/$link"
fi
done
SAVED="`pwd`"
cd "`dirname \"$PRG\"`/" >/dev/null
APP_HOME="`pwd -P`"
cd "$SAVED" >/dev/null
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
# Determine the Java command to use to start the JVM.
if [ -n "$JAVA_HOME" ] ; then
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
# IBM's JDK on AIX uses strange locations for the executables
JAVACMD="$JAVA_HOME/jre/sh/java"
else
JAVACMD="$JAVA_HOME/bin/java"
fi
if [ ! -x "$JAVACMD" ] ; then
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
else
JAVACMD="java"
which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
# Increase the maximum file descriptors if we can.
if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then
MAX_FD_LIMIT=`ulimit -H -n`
if [ $? -eq 0 ] ; then
if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
MAX_FD="$MAX_FD_LIMIT"
fi
ulimit -n $MAX_FD
if [ $? -ne 0 ] ; then
warn "Could not set maximum file descriptor limit: $MAX_FD"
fi
else
warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
fi
fi
# For Darwin, add options to specify how the application appears in the dock
if $darwin; then
GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
fi
# For Cygwin, switch paths to Windows format before running java
if $cygwin ; then
APP_HOME=`cygpath --path --mixed "$APP_HOME"`
CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
JAVACMD=`cygpath --unix "$JAVACMD"`
# We build the pattern for arguments to be converted via cygpath
ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
SEP=""
for dir in $ROOTDIRSRAW ; do
ROOTDIRS="$ROOTDIRS$SEP$dir"
SEP="|"
done
OURCYGPATTERN="(^($ROOTDIRS))"
# Add a user-defined pattern to the cygpath arguments
if [ "$GRADLE_CYGPATTERN" != "" ] ; then
OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
fi
# Now convert the arguments - kludge to limit ourselves to /bin/sh
i=0
for arg in "$@" ; do
CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
else
eval `echo args$i`="\"$arg\""
fi
i=$((i+1))
done
case $i in
(0) set -- ;;
(1) set -- "$args0" ;;
(2) set -- "$args0" "$args1" ;;
(3) set -- "$args0" "$args1" "$args2" ;;
(4) set -- "$args0" "$args1" "$args2" "$args3" ;;
(5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
(6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
(7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
(8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
(9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
esac
fi
# Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules
function splitJvmOpts() {
JVM_OPTS=("$@")
}
eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS
JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME"
exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@"
================================================
FILE: gradlew.bat
================================================
@if "%DEBUG%" == "" @echo off
@rem ##########################################################################
@rem
@rem Gradle startup script for Windows
@rem
@rem ##########################################################################
@rem Set local scope for the variables with windows NT shell
if "%OS%"=="Windows_NT" setlocal
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
set DEFAULT_JVM_OPTS=
set DIRNAME=%~dp0
if "%DIRNAME%" == "" set DIRNAME=.
set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME%
@rem Find java.exe
if defined JAVA_HOME goto findJavaFromJavaHome
set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1
if "%ERRORLEVEL%" == "0" goto init
echo.
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
goto fail
:findJavaFromJavaHome
set JAVA_HOME=%JAVA_HOME:"=%
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
if exist "%JAVA_EXE%" goto init
echo.
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
goto fail
:init
@rem Get command-line arguments, handling Windowz variants
if not "%OS%" == "Windows_NT" goto win9xME_args
if "%@eval[2+2]" == "4" goto 4NT_args
:win9xME_args
@rem Slurp the command line arguments.
set CMD_LINE_ARGS=
set _SKIP=2
:win9xME_args_slurp
if "x%~1" == "x" goto execute
set CMD_LINE_ARGS=%*
goto execute
:4NT_args
@rem Get arguments from the 4NT Shell from JP Software
set CMD_LINE_ARGS=%$
:execute
@rem Setup the command line
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
@rem Execute Gradle
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
:end
@rem End local scope for the variables with windows NT shell
if "%ERRORLEVEL%"=="0" goto mainEnd
:fail
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
rem the _cmd.exe /c_ return code!
if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
exit /b 1
:mainEnd
if "%OS%"=="Windows_NT" endlocal
:omega
================================================
FILE: settings.gradle
================================================
include ':app'