According to android documentation, shared elements transition determine how views that are shared between two activities transition between these activities. For example, if two activities have the same image in different positions and sizes, the changeImageTransform shared element transition translates and scales the image smoothly between these activities.
This thread looks at examples and libraries relating to Shared Transitions in android. Feel free to contribute more examples, links and libraries.
Easily Implement Shared Transition on Images
You can easily implement shared element transition on images using this library known as Transitional ImageView.
Let’s show you how to.
Step 1 – Install the Library
First register jitpack as a repository in your app level build.gradle:
allprojects {
repositories {
...
maven { url 'https://jitpack.io' }
}
}
Then install the library:
implementation 'com.github.mostafaaryan:transitional-imageview:v0.2.2'
Step 2
Create Transitional ImageView in your layout by pasting the following code:
<com.mostafaaryan.transitionalimageview.TransitionalImageView
android:id="@+id/transitional_image"
android:layout_width="100dp"
android:layout_height="wrap_content"
android:scaleType="fitXY"
android:adjustViewBounds="true"
app:res_id="@drawable/sample_image" />
Step 3
Now build a TransitionImageObject and set it to the TransitionalImageView:
TransitionalImageView transitionalImageView = (TransitionalImageView) findViewById(R.id.transitional_image);
TransitionalImage transitionalImage = new TransitionalImage.Builder()
.duration(500)
.backgroundColor(ContextCompat.getColor(MainActivity.this, R.color.color))
.image(R.drawable.sample_image)
/* or */
.image(bitmap)
.create();
transitionalImageView.setTransitionalImage(transitionalImage);
Full Example
Here is a beautiful example for using this library.
(a). Shoe.java
The model class to define a single shoe.
public class Shoe {
private String Title;
private String imageUrl;
public Shoe(String title, String imageUrl) {
Title = title;
this.imageUrl = imageUrl;
}
public String getTitle() {
return Title;
}
public String getImageUrl() {
return imageUrl;
}
}
(b). ShoeAdapter.java
Then the recyclerview adapter.
import android.app.Activity;
import android.content.Context;
import android.graphics.Bitmap;
import android.os.AsyncTask;
import android.support.v4.content.ContextCompat;
import android.support.v7.widget.RecyclerView;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.TextView;
import com.ariannejad.mostafa.transitional_imageview_implementation.R;
import com.ariannejad.mostafa.transitional_imageview_implementation.controller.MainActivity;
import com.ariannejad.mostafa.transitional_imageview_implementation.model.Shoe;
import com.mostafaaryan.transitionalimageview.TransitionalImageView;
import com.mostafaaryan.transitionalimageview.model.TransitionalImage;
import com.squareup.picasso.Picasso;
import java.io.IOException;
import java.util.ArrayList;
/**
* Created by Mostafa Aryan Nejad on 8/11/17.
*/
public class ShoeAdapter extends RecyclerView.Adapter<ShoeAdapter.ViewHolder> {
Context mContext;
ArrayList<Shoe> shoes = new ArrayList<>();
public ShoeAdapter(Context context, ArrayList<Shoe> shoes) {
mContext = context;
this.shoes = shoes;
}
@Override
public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
View view = LayoutInflater.from(parent.getContext())
.inflate(R.layout.item_shoe, parent, false);
return new ViewHolder(view);
}
@Override
public void onBindViewHolder(final ViewHolder holder, final int position) {
final Shoe shoe = shoes.get(position);
AsyncTask.execute(new Runnable() {
@Override
public void run() {
try{
final Bitmap bitmap = Picasso.with(mContext).load(shoe.getImageUrl()).get();
((Activity) mContext).runOnUiThread(new Runnable() {
@Override
public void run() {
TransitionalImage transitionalImage = new TransitionalImage.Builder()
.duration(500)
/*.backgroundColor(ContextCompat.getColor(, R.color.colorAccent))*/
/*.image(R.drawable.sample_image)*/
.image(bitmap)
.create();
holder.image.setTransitionalImage(transitionalImage);
bitmap.recycle();
}
});
} catch (IOException e){e.printStackTrace();}
}
});
holder.title.setText(shoe.getTitle());
holder.sizes.setText("37,38,39,40");
}
@Override
public int getItemCount() {
return shoes.size();
}
public static class ViewHolder extends RecyclerView.ViewHolder {
public TextView title;
public TextView sizes;
public TransitionalImageView image;
public ViewHolder(View itemView) {
super(itemView);
title = (TextView) itemView.findViewById(R.id.shoe_title);
sizes = (TextView) itemView.findViewById(R.id.shoe_sizes);
image = (TransitionalImageView) itemView.findViewById(R.id.shoe_image);
}
}
}
(c). ShoeListActivity.java
The shoe list activity:
import android.support.design.widget.AppBarLayout;
import android.support.design.widget.CollapsingToolbarLayout;
import android.support.design.widget.TabLayout;
import android.support.v7.app.ActionBar;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.support.v7.widget.LinearLayoutManager;
import android.support.v7.widget.RecyclerView;
import android.support.v7.widget.Toolbar;
import com.ariannejad.mostafa.transitional_imageview_implementation.R;
import com.ariannejad.mostafa.transitional_imageview_implementation.adapter.ShoeAdapter;
import com.ariannejad.mostafa.transitional_imageview_implementation.model.Shoe;
import java.util.ArrayList;
public class ShoeListActivity extends AppCompatActivity {
private RecyclerView shoeRecyclerView;
private ArrayList<Shoe> shoes = new ArrayList<>();
private ActionBar actionBar;
private AppBarLayout appBarLayout;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_shoe_list);
shoeRecyclerView = (RecyclerView) findViewById(R.id.shoe_recycler_view);
CollapsingToolbarLayout collapsingToolbar =
(CollapsingToolbarLayout) findViewById(R.id.collapsing_toolbar);
appBarLayout = (AppBarLayout) findViewById(R.id.app_bar_layout);
setOnOffsetChangedListener();
collapsingToolbar.setTitleEnabled(false);
Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
setSupportActionBar(toolbar);
actionBar = getSupportActionBar();
if(actionBar != null) actionBar.setTitle("");
populateList();
}
private void populateList() {
shoes.add(new Shoe("Skechers Relaxed Fit Empire Game On Walking Shoe",
"https://www.shoes.com/pm/skech/skech800828_42965_hd2.jpg"));
shoes.add(new Shoe("Skechers After Burn Memory Fit Geardo High Top Trainer",
"https://www.shoes.com//pm/skech/skech798492_42965_hd2.jpg"));
shoes.add(new Shoe("New Balance Fresh Foam Zante v3 Running Shoe",
"https://www.shoes.com/pi/newba/hd/newba805216_436896_hd.jpg"));
for(int i = 0 ; i <= 5 ; i++ ) {
shoes.addAll(shoes);
}
displayList();
}
private void displayList() {
RecyclerView.LayoutManager layoutManager = new LinearLayoutManager(this);
RecyclerView.Adapter adapter = new ShoeAdapter(this, shoes);
shoeRecyclerView.setLayoutManager(layoutManager);
shoeRecyclerView.setAdapter(adapter);
}
private void setOnOffsetChangedListener() {
appBarLayout.addOnOffsetChangedListener(new AppBarLayout.OnOffsetChangedListener() {
boolean isDisplayed = false;
@Override
public void onOffsetChanged(AppBarLayout appBarLayout, int verticalOffset) {
int totalScroll = appBarLayout.getTotalScrollRange();
if (totalScroll + verticalOffset == 0) {
if (actionBar != null) {
actionBar.setTitle("Sneakers");
}
isDisplayed = true;
} else if (isDisplayed) {
if (actionBar != null)
actionBar.setTitle("");
isDisplayed = false;
}
}
});
}
}
(d). MainActivity.java
And finally the main activity.
import android.content.Intent;
import android.graphics.Bitmap;
import android.os.AsyncTask;
import android.support.v4.content.ContextCompat;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.view.View;
import com.ariannejad.mostafa.transitional_imageview_implementation.R;
import com.mostafaaryan.transitionalimageview.TransitionalImageView;
import com.mostafaaryan.transitionalimageview.model.TransitionalImage;
import com.squareup.picasso.Picasso;
import java.io.IOException;
public class MainActivity extends AppCompatActivity {
private String imageUrl = "https://image.freepik.com/free-icon/android-logo_318-54237.jpg";
TransitionalImageView tiv;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
tiv = (TransitionalImageView) findViewById(R.id.sample_image);
loadImage();
}
private void loadImage() {
/*
ImageLoader imageLoader;
imageLoader = ImageLoader.getInstance();
imageLoader.init(ImageLoaderConfiguration.createDefault(this));
AsyncTask.execute(new Runnable() {
@Override
public void run() {
DisplayImageOptions dio = new DisplayImageOptions.Builder()
.cacheInMemory(false).build();
final Bitmap bmp = imageLoader.loadImageSync(imageUrl, dio);
runOnUiThread(new Runnable() {
@Override
public void run() {
tiv.setImage(bmp);
}
});
}
});*/
/* Glide.with(this).asBitmap().load(imageUrl).into(new SimpleTarget<Bitmap>() {
@Override
public void onResourceReady(Bitmap resource, Transition<? super Bitmap> transition) {
tiv.setImage(resource);
}
}); */
AsyncTask.execute(new Runnable() {
@Override
public void run() {
try {
final Bitmap b = Picasso.with(MainActivity.this).load(imageUrl).get();
runOnUiThread(new Runnable() {
@Override
public void run() {
// tiv.setImage(b);
TransitionalImage transitionalImage = new TransitionalImage.Builder()
.duration(500)
.backgroundColor(ContextCompat.getColor(MainActivity.this, R.color.colorAccent))
//.image(R.drawable.sample_image)
.image(b)
.create();
tiv.setTransitionalImage(transitionalImage);
}
});
} catch (IOException e) {e.printStackTrace();}
}
});
}
public void onClickShoes(View view) {
startActivity(new Intent(this, ShoeListActivity.class));
}
}
Demo
Here is the demo of what you get when you run the project.
Download
Here are the download links.
Kotlin Shared Transition RecyclerView and Fragments
This is also a simple shared transitions example written in Kotlin. This time round however a recyclerview is the shared element among two fragments.
Tools
Here are the things to keep in mind:
- Programming Language – Kotlin
- Minimum SDK – 21
1. Create Transitions
In a folder known as transitions under resources add the following:
(a). change_bounds.xml
<?xml version="1.0" encoding="utf-8"?>
<transitionSet>
<changeBounds />
</transitionSet>
(b). change_image_transform.xml
Then:
<?xml version="1.0" encoding="utf-8"?>
<transitionSet>
<changeImageTransform />
</transitionSet>
2. Design Layouts
You will find the layouts in the code.
3. Write Code
Code is written in Kotlin in this case.
(a). Fragment1.kt
Here is the code for the first fragment.
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.Fragment
import androidx.recyclerview.widget.LinearLayoutManager
import kotlinx.android.synthetic.main.activity_main.*
import android.transition.ChangeBounds
import android.transition.ChangeImageTransform
class Fragment1: Fragment() {
private lateinit var lm: LinearLayoutManager
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
return inflater.inflate(R.layout.activity_main, container, false)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
lm = LinearLayoutManager(context, LinearLayoutManager.VERTICAL,
false)
rv.layoutManager = lm
val adapter = MainActivity.Adapter()
rv.adapter = adapter
btn.setOnClickListener {
// val changeImageTransform =
// TransitionInflater.from(context).inflateTransition(R.transition.change_image_transform)
// val changeBoundsTransform =
// TransitionInflater.from(context).inflateTransition(R.transition.change_bounds)
sharedElementReturnTransition = ChangeBounds()
sharedElementEnterTransition = ChangeImageTransform()
exitTransition = ChangeBounds()
val fragment2 = Fragment2()
// Setup transition on second fragment
fragment2.sharedElementEnterTransition = ChangeBounds()
fragment2.enterTransition = ChangeBounds();
val firstVisiblePosition = lm.findFirstVisibleItemPosition()
val lastVisiblePosition = lm.findLastVisibleItemPosition()
val transaction = fragmentManager!!.beginTransaction()
.replace(R.id.container, fragment2, fragment2::class.java.simpleName)
.addToBackStack("name")
for (i in firstVisiblePosition..lastVisiblePosition) {
val holderForAdapterPosition =
rv.findViewHolderForAdapterPosition(i) as MainActivity.Adapter.Holder
val itemView = holderForAdapterPosition.itemView
transaction.addSharedElement(itemView, "unique_key_$i")
}
transaction.commit()
}
}
}
(b). Fragment2.kt
Add the following code in second fragment.
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.Fragment
import androidx.recyclerview.widget.LinearLayoutManager
import kotlinx.android.synthetic.main.activity_main.*
class Fragment2: Fragment() {
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
return inflater.inflate(R.layout.activity_main, container, false)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
val lm = LinearLayoutManager(context, LinearLayoutManager.HORIZONTAL,
false)
rv.layoutManager = lm
val adapter = MainActivity.Adapter()
rv.adapter = adapter
btn.setOnClickListener {
}
postponeEnterTransition()
}
override fun onStart() {
super.onStart()
rv.post {
startPostponedEnterTransition()
}
}
}
(c). ScndActivity.kt
Then the second activity.
import android.os.Bundle
import android.os.PersistableBundle
import androidx.appcompat.app.AppCompatActivity
import androidx.recyclerview.widget.LinearLayoutManager
import kotlinx.android.synthetic.main.activity_main.*
class ScndActivity: AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val lm = LinearLayoutManager(this, LinearLayoutManager.HORIZONTAL,
false)
rv.layoutManager = lm
val adapter = MainActivity.Adapter()
rv.adapter = adapter
btn.setOnClickListener {
// val currentOrientation = lm.orientation
// if (currentOrientation == LinearLayoutManager.VERTICAL) {
// lm.orientation = LinearLayoutManager.HORIZONTAL
// } else {
// lm.orientation = LinearLayoutManager.VERTICAL
// }
// adapter.notifyItemRangeChanged(1, adapter?.itemCount ?: 0)
}
supportPostponeEnterTransition()
rv.post {
supportStartPostponedEnterTransition()
}
}
}
(d). MainActivity.kt
And lastly the main activity,
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
class MainActivity : AppCompatActivity() {
private lateinit var lm: LinearLayoutManager
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.cont)
val fragment1 = Fragment1()
supportFragmentManager.beginTransaction()
.add(R.id.container, fragment1, Fragment1::class.java.simpleName)
.commit()
// lm = LinearLayoutManager(this, LinearLayoutManager.VERTICAL,
// false)
// rv.layoutManager = lm
// val adapter = Adapter()
// rv.adapter = adapter
// btn.setOnClickListener {
// val firstVisiblePosition = lm.findFirstVisibleItemPosition()
// val lastVisiblePosition = lm.findLastVisibleItemPosition()
// val pairs = ArrayList<Pair<View, String>>()
// for (i in firstVisiblePosition..lastVisiblePosition) {
// val holderForAdapterPosition =
// rv.findViewHolderForAdapterPosition(i) as Adapter.Holder
// val itemView = holderForAdapterPosition.itemView
// pairs.add(Pair(itemView, "unique_key_$i"))
// }
// val bundle = ActivityOptions.makeSceneTransitionAnimation(
// this,
// *pairs.toTypedArray()
// ).toBundle()
// val fragment1 = Fragment1()
// supportFragmentManager.beginTransaction()
// .add(fragment1, Fragment1::class.java.simpleName)
// .commit()
// startActivity(Intent(this, ScndActivity::class.java), bundle)
// }
}
override fun onResume() {
super.onResume()
}
class Adapter : RecyclerView.Adapter<Adapter.Holder>() {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): Holder =
Holder(
LayoutInflater.from(parent.context).inflate(
R.layout.item_item,
parent,
false
)
)
override fun getItemCount(): Int = 10
override fun onBindViewHolder(holder: Holder, position: Int) {
holder.bind(position)
}
class Holder(view: View) : RecyclerView.ViewHolder(view) {
fun bind(position: Int) {
itemView.transitionName = "unique_key_$position"
}
}
}
}
Demo
Here is what you get when you run the project.
Download
Java Shared Transition with Fragments and FloatingActionButton
This is a simple one-class example to utilize a shared element transition within fragments in an android activity. The programming language is Java. While it is not written in androidx, you can easily update it to androidx fragments and it doesn’t utilize any third partt library.
shared
Transitions
These are written in XML. Tyically you create a transition resource directory and place the XML.
(a). shared_enter_transition.xml
Here is the code:
<?xml version="1.0" encoding="utf-8"?>
<transitionSet xmlns:android="http://schemas.android.com/apk/res/android"
android:duration="@integer/default_anim_duration">
<changeTransform/>
<arcMotion
android:minimumHorizontalAngle="0"
android:minimumVerticalAngle="15"
android:maximumAngle="90" />
<changeBounds />
</transitionSet>
Activities
Here are the activities
(a). MainActivity.java
Here is the main activity:
import android.animation.Animator;
import android.app.Fragment;
import android.os.Bundle;
import android.support.v4.view.ViewCompat;
import android.support.v7.app.ActionBarActivity;
import android.transition.Fade;
import android.transition.Transition;
import android.transition.TransitionInflater;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewAnimationUtils;
import android.view.ViewGroup;
import android.view.animation.AccelerateInterpolator;
public class FabActivity extends ActionBarActivity {
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_fab);
getFragmentManager()
.beginTransaction()
.add(R.id.frag_content, TitleFragment.newInstance())
.commit();
}
public static class TitleFragment extends Fragment {
public static TitleFragment newInstance() {
return new TitleFragment();
}
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
final View view = inflater.inflate(R.layout.fragment_fab_title, container, false);
final View fabbutton = view.findViewById(R.id.fab);
fabbutton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
final ControlsFragment controlsFragment = ControlsFragment.newInstance();
setupSharedElementTransition(controlsFragment);
Fade f = new Fade();
f.setStartDelay(250);
setExitTransition(f);
getFragmentManager()
.beginTransaction()
.replace(R.id.frag_content, controlsFragment)
.addToBackStack("controls")
.addSharedElement(fabbutton, "pause_button")
.commit();
}
});
return view;
}
private void setupSharedElementTransition(final ControlsFragment controlsFragment) {
Transition sharedTransition = TransitionInflater.from(getActivity()).inflateTransition(R.transition.shared_enter_transition);
controlsFragment.setSharedElementEnterTransition(sharedTransition);
controlsFragment.setSharedElementReturnTransition(sharedTransition);
sharedTransition.addListener(new Transition.TransitionListener() {
@Override
public void onTransitionEnd(Transition transition) {
controlsFragment.revealContent();
}
@Override
public void onTransitionStart(Transition transition) {
}
@Override
public void onTransitionCancel(Transition transition) {
}
@Override
public void onTransitionPause(Transition transition) {
}
@Override
public void onTransitionResume(Transition transition) {
}
});
}
}
public static class ControlsFragment extends Fragment {
public static ControlsFragment newInstance() {
return new ControlsFragment();
}
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
return inflater.inflate(R.layout.fragment_fab_controls, container, false);
}
public void revealContent() {
View layout = getView().findViewById(R.id.controls_layout);
animateRevealColor(layout);
}
private void animateRevealColor(View targetView) {
int cx = (targetView.getLeft() + targetView.getRight()) / 2;
int cy = (targetView.getTop() + targetView.getBottom()) / 2;
cx += targetView.getTranslationX();
cy += targetView.getTranslationY();
int finalRadius = Math.max(targetView.getWidth(), targetView.getHeight());
Animator anim = ViewAnimationUtils.createCircularReveal(targetView, cx, cy, 0, finalRadius);
targetView.setBackgroundColor(getResources().getColor(R.color.accent_material_light));
anim.setDuration(getResources().getInteger(R.integer.default_anim_duration));
anim.setInterpolator(new AccelerateInterpolator());
anim.addListener(new Animator.AnimatorListener() {
@Override
public void onAnimationEnd(Animator animator) {
animateScaleButton(getView().findViewById(R.id.ff_button));
animateScaleButton(getView().findViewById(R.id.rew_button));
}
@Override
public void onAnimationStart(Animator animator) {
}
@Override
public void onAnimationCancel(Animator animator) {
}
@Override
public void onAnimationRepeat(Animator animator) {
}
});
anim.start();
}
private void animateScaleButton(View view) {
ViewCompat.animate(view)
.scaleX(1)
.scaleY(1)
.setDuration(250)
.start();
}
}
}
Demo
Here is the demo of what you get when you run the project.