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. ![support_animation](https://cloud.githubusercontent.com/assets/2686355/13902301/783b604c-ee4c-11e5-8428-bab7a67f6fff.gif) ![cancel support animation in the middle](https://cloud.githubusercontent.com/assets/2686355/13902305/80beb502-ee4c-11e5-8dde-fcfade1eb93c.gif) # Lollipop native animation ![lollipop animation](https://cloud.githubusercontent.com/assets/2686355/13902304/7d066c52-ee4c-11e5-9329-a455c0baa440.gif) # Details of implementation [![Medium](https://img.shields.io/badge/Meduim-Implementing%20ImageView%20transition%20between%20activities%20for%20pre--Lollipop%20devices.-blue.svg)](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 ================================================