Simple understanding and use of Android shared elements
1. Basic concepts
Andriod 5.0 and later began to support shared element animation. Two Activities or fragments can share certain controls. For example, when Activity A jumps to Activity B, a certain control of A can automatically move to the position of the corresponding control of B, resulting in animation.
2. Basic use
1. Activity to Activity jump implementation
1.1. Steps to use
1. Assign a unique transition name to each shared element view.
2. makeSceneTransitionAnimation
Add the shared element view and the transition name corresponding to the shared element view after switching.
3. Page jump.
1.2. Case Description
1. The first step is to set transitionName
the attribute of the view in ActivityA and ActivityB. The attribute value is customized, which is a string.
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent"
android:layout_height="match_parent">
<ImageView android:layout_width="100dp"
android:layout_height="100dp"
android:src="@mipmap/timg"
android:id="@+id/share_pic"
android:transitionName="@string/share_pic_str"
android:scaleType="fitXY"/>
</LinearLayout>
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical" android:layout_width="match_parent"
android:layout_height="match_parent">
<ImageView android:layout_width="50dp"
android:layout_height="50dp"
android:src="@mipmap/timg"
android:id="@+id/share_pic"
android:transitionName="@string/share_pic_str"
android:scaleType="fitXY" />
transitionName
You can also set the properties manually
ShareAnimatorActivity
ActivityOptionsCompat activityOptions = ActivityOptionsCompat.makeSceneTransitionAnimation(
MainActivity.this,
// Now we provide a list of Pair items which contain the view we can transitioning
// from, and the name of the view it is transitioning to, in the launched activity
new Pair<>(view.findViewById(R.id.imageview_item),
R.string.share_pic_str));
SecondShareAnimActivity
ViewCompat.setTransitionName(mImageView, R.string.share_pic_str);
2. Set the intent jump
Intent intent = new Intent(ShareAnimatorActivity.this,SecondShareAnimActivity.class);
Bundle bundle = ActivityOptionsCompat.makeSceneTransitionAnimation(ShareAnimatorActivity.this,shareImg,getString(R.string.share_pic_str)).toBundle();
startActivity(intent,bundle);
makeSceneTransitionAnimation()
Parameter explanation:
- Activity is the Activity that initiates the jump,
- shareElement is the id of the shared control,
- sharedElementName is the string defined in the first step.
- This method only supports sharing a single control.
The details are as follows: The Activity that initiates the jump:
public class ShareAnimatorActivity extends AppCompatActivity {
private ImageView shareImg;
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_share_animator_main);
shareImg = (ImageView) findViewById(R.id.share_pic);
shareImg.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Log.d(TAG,"onClick");
Intent intent = new Intent(ShareAnimatorActivity.this,SecondShareAnimActivity.class);
Bundle bundle = ActivityOptionsCompat.makeSceneTransitionAnimation(ShareAnimatorActivity.this,shareImg,getString(R.string.share_pic_str)).toBundle();
startActivity(intent,bundle);
}
});
}
Activity code to jump to:
public class SecondShareAnimActivity extends AppCompatActivity {
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_sencond_share_animator_main);
}
}
2. Fragment to Fragment jump implementation
2.1. Steps to use
1. Set the transitionName attribute.
The Transition framework needs a method that can associate the View of the current page with the page to be jumped to. The following are two methods for adding a Transition Name to the View:
- ViewCompat.setTransitionName() can be called directly in the code, or
setTransitionName() can be called directly in devices with Android Lolipop system version or higher. - In the layout XML file, set the android:transitionName attribute directly.
2. Set Fragment Transaction
getSupportFragmentManager()
.beginTransaction()
.addSharedElement(sharedElement, transitionName)
.replace(R.id.container, newFragment)
.addToBackStack(null)
.commit();
Just call addSharedElement()
to associate the View you want to share with the Fragment.
- The View passed as a parameter to addSharedElement() is the View in the first Fragment that you want to
share with the Fragment that is about to jump. - The TransitionName required to be filled in by the second parameter of this method is the Transition Name of the shared View in the redirected Fragment
.- For example, if the transitionName of the shared View in the current Fragment is "foo", and the transitionName of the shared View in the Fragment to be jumped
to is "bar", then the second parameter passed in addSharedElement() should be "bar".
- For example, if the transitionName of the shared View in the current Fragment is "foo", and the transitionName of the shared View in the Fragment to be jumped
3. Specify the method of adding Transition animation as a shared element:
Call
setSharedElementEnterTransition()
Specifies how the View transitions from the first Fragment to the View in the jump Fragment.Call
setSharedElementReturnTransition()
Specifies how the View returns to the first Fragment from the Jump Fragment after the user clicks the Back button.
Remember, to return to Transition, you need to call the corresponding method in the jump Fragment, otherwise you will not get the effect you want.
setEnterTransition
Of course, you can also set Transition transition animation for any non-shared View, but the calling API has changed: just call (), setExitTransition
(), setReturnTransition
(), and setReenterTransition
() methods in the corresponding Fragment.
2.2. Case Description
1、TestShareActivity
public class TestShareActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.share_test);
Fragment fragment = getSupportFragmentManager().findFragmentByTag(FragmentA.class.getName());
if (fragment == null) {
fragment = FragmentA.newInstance();
getSupportFragmentManager().beginTransaction().add(R.id.share_test,
fragment,
FragmentA.class.getName())
.commit();
}
}
}
share_test.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent" >
<FrameLayout
android:id="@+id/share_test"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</LinearLayout>
2、ShareElementsFragment1
public class FragmentA extends Fragment {
public static final String TAG = FragmentA.class.getSimpleName();
@Nullable
@Override
public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
return inflater.inflate(R.layout.fragment_a, container, false);
}
public static FragmentA newInstance() {
return new FragmentA();
}
@Override
public void onActivityCreated(@Nullable Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
final ImageView imageView = (ImageView) getView().findViewById(R.id.imageView);
getActivity().findViewById(R.id.btn_click).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
Fragment fragmentB = getFragmentManager().findFragmentByTag(TAG);
if (fragmentB == null) fragmentB = FragmentB.newInstance();
getFragmentManager()
.beginTransaction()
.addSharedElement(imageView,
ViewCompat.getTransitionName(imageView))
.addToBackStack(TAG)
.replace(R.id.share_test, fragmentB)
.commit();
}
});
}
}
fragment_a.xml
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/activity_main"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<ImageView
android:id="@+id/imageView"
android:layout_width="64dp"
android:layout_height="64dp"
android:transitionName="simple transition name"
android:background="@color/color_green" />
<Button
android:id="@+id/btn_click"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Click Me"
android:textSize="16sp" />
</LinearLayout>
3、ShareElementsFragment2
public class FragmentB extends Fragment {
@Override
public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
return inflater.inflate(R.layout.fragment_b, container, false);
}
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
setSharedElementEnterTransition(
TransitionInflater.from(getContext())
.inflateTransition(android.R.transition.move));
}
}
public static FragmentB newInstance() {
return new FragmentB();
}
}
fragment_b.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<ImageView
android:id="@+id/imageView"
android:layout_width="100dp"
android:layout_height="100dp"
android:transitionName="simple transition name"
android:background="@color/color_green"
android:layout_gravity="center_horizontal"/>
</LinearLayout>
3. Navigation + shared elements + recyclerview jump implementation
3.1. Steps to use
- 1. Set
transitionName
properties - 2. Define
FragmentNavigatorExtras
parameters - 3.
findNavController().navigate
Page jump
// 第一步
holder.item.findViewById<TextView>(R.id.user_name_text).transitionName = myDataset[position]
holder.item.findViewById<ImageView>(R.id.user_avatar_image).transitionName =
(position % listOfAvatars.size).toString()
//第二步
val extras = FragmentNavigatorExtras(
holder.item.findViewById<ImageView>(R.id.user_avatar_image)
to (position % listOfAvatars.size).toString(),
holder.item.findViewById<TextView>(R.id.user_name_text)
to myDataset[position]
)
holder.item.findNavController().navigate(
R.id.action_leaderboard_to_userProfile,
bundle,
null,
extras
)
//第三步
holder.item.findNavController().navigate(
R.id.action_leaderboard_to_userProfile,
bundle,
null,
extras
)
3.2. Case Description
Leaderboard
class Leaderboard : Fragment() {
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?): View? {
// Inflate the layout for this fragment
val view = inflater.inflate(R.layout.fragment_leaderboard, container, false)
val viewAdapter = MyAdapter(Array(6) {
"Person ${it + 1}" })
view.findViewById<RecyclerView>(R.id.leaderboard_list).run {
// use this setting to improve performance if you know that changes
// in content do not change the layout size of the RecyclerView
setHasFixedSize(true)
// specify an viewAdapter (see also next example)
adapter = viewAdapter
}
return view
}
}
class MyAdapter(private val myDataset: Array<String>) :
RecyclerView.Adapter<MyAdapter.ViewHolder>() {
class ViewHolder(val item: View) : RecyclerView.ViewHolder(item)
// Create new views (invoked by the layout manager)
override fun onCreateViewHolder(parent: ViewGroup,
viewType: Int): ViewHolder {
// create a new view
val itemView = LayoutInflater.from(parent.context)
.inflate(R.layout.list_view_item, parent, false)
return ViewHolder(itemView)
}
// Replace the contents of a view (invoked by the layout manager)
@SuppressLint("CutPasteId")
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
// - get element from your dataset at this position
// - replace the contents of the view with that element
holder.item.findViewById<TextView>(R.id.user_name_text).text = myDataset[position]
holder.item.findViewById<TextView>(R.id.user_name_text).transitionName = myDataset[position]
holder.item.findViewById<ImageView>(R.id.user_avatar_image)
.setImageResource(listOfAvatars[position % listOfAvatars.size])
holder.item.findViewById<ImageView>(R.id.user_avatar_image).transitionName =
(position % listOfAvatars.size).toString()
holder.item.setOnClickListener {
val bundle = Bundle()
bundle.apply {
putString(USERNAME_KEY, myDataset[position])
putInt(USER_AVATAR_KEY, position % listOfAvatars.size)
}
val extras = FragmentNavigatorExtras(
holder.item.findViewById<ImageView>(R.id.user_avatar_image)
to (position % listOfAvatars.size).toString(),
holder.item.findViewById<TextView>(R.id.user_name_text)
to myDataset[position]
)
holder.item.findNavController().navigate(
R.id.action_leaderboard_to_userProfile,
bundle,
null,
extras
)
}
// Return the size of your dataset (invoked by the layout manager)
override fun getItemCount() = myDataset.size
companion object {
const val USERNAME_KEY = "userName"
const val USER_AVATAR_KEY = "userAvatar"
}
}
public val listOfAvatars = listOf(
R.drawable.avatar_1_raster,
R.drawable.avatar_2_raster,
R.drawable.avatar_3_raster,
R.drawable.avatar_4_raster,
R.drawable.avatar_5_raster,
R.drawable.avatar_6_raster
)
fragment_leaderboard.xml
<?xml version="1.0" encoding="utf-8"?>
<androidx.recyclerview.widget.RecyclerView
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/leaderboard_list"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:context="com.example.android.navigationadvancedsample.listscreen.Leaderboard"
tools:listitem="@layout/list_view_item"/>
list_view_item.xml
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@android:color/background_light"
tools:layout_editor_absoluteY="81dp">
<ImageView
android:id="@+id/user_avatar_image"
android:layout_width="69dp"
android:layout_height="69dp"
android:layout_marginStart="8dp"
android:layout_marginTop="8dp"
android:layout_marginLeft="8dp"
android:layout_marginBottom="8dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
android:background="@drawable/avatar_5_raster"
app:srcCompat="@drawable/circle"
tools:background="@tools:sample/avatars"
tools:srcCompat="@drawable/circle"
android:contentDescription="@string/profile_image"/>
<TextView
android:id="@+id/user_name_text"
android:layout_width="228dp"
android:layout_height="28dp"
android:layout_marginStart="8dp"
android:layout_marginTop="8dp"
android:text=""
android:textAppearance="@style/TextAppearance.AppCompat.Subhead"
android:textSize="22sp"
app:layout_constraintBottom_toTopOf="@+id/user_points_text"
app:layout_constraintStart_toEndOf="@+id/user_avatar_image"
app:layout_constraintTop_toTopOf="parent"
tools:text="@tools:sample/full_names"
android:layout_marginLeft="8dp" />
<TextView
android:id="@+id/user_points_text"
android:layout_width="228dp"
android:layout_height="21dp"
android:layout_marginBottom="8dp"
android:layout_marginStart="8dp"
android:text="@string/user_points"
android:textSize="16sp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toEndOf="@+id/user_avatar_image"
app:layout_constraintTop_toBottomOf="@+id/user_name_text"
tools:text="10,000 pts"
android:layout_marginLeft="8dp" />
</androidx.constraintlayout.widget.ConstraintLayout>
UserProfile
class UserProfile : Fragment() {
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?): View? {
val view = inflater.inflate(R.layout.fragment_user_profile, container, false)
val name = arguments?.getString(USERNAME_KEY) ?: "Ali Connors"
val avatarPosition = arguments?.getInt(USER_AVATAR_KEY) ?: 0
view.findViewById<TextView>(R.id.profile_user_name).transitionName = name
view.findViewById<TextView>(R.id.profile_user_name).text = name
view.findViewById<ImageView>(R.id.profile_pic).transitionName = avatarPosition.toString()
view.findViewById<ImageView>(R.id.profile_pic)
.setImageResource(listOfAvatars[avatarPosition])
return view
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
sharedElementEnterTransition = TransitionInflater.from(requireContext())
.inflateTransition(android.R.transition.move)
}
}
fragment_user_profile.xml
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/profiler_constraint_layout"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/colorAccent"
tools:context="com.example.android.navigationadvancedsample.listscreen.UserProfile"
tools:layout_editor_absoluteY="81dp">
<ImageView
android:id="@+id/profile_pic"
android:layout_width="240dp"
android:layout_height="240dp"
android:layout_marginStart="8dp"
android:layout_marginTop="64dp"
android:layout_marginEnd="8dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
android:background="@drawable/avatar_6_raster"
app:srcCompat="@drawable/purple_frame"
tools:background="@tools:sample/avatars[6]"
tools:src="@drawable/purple_frame" />
<View
android:id="@+id/user_data_card"
android:layout_width="0dp"
android:layout_height="141dp"
android:layout_marginTop="64dp"
android:background="@drawable/rounded_rect"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/profile_pic"
app:layout_constraintVertical_bias="1.0"/>
<TextView
android:id="@+id/profile_user_name"
android:gravity="center_horizontal"
android:layout_width="0dp"
android:layout_height="40dp"
android:layout_marginStart="8dp"
android:layout_marginTop="16dp"
android:layout_marginEnd="8dp"
android:text="@string/profile_name_1"
android:textAlignment="center"
android:textColor="@color/colorAccent"
android:textSize="30sp"
android:textStyle="bold"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="@+id/user_data_card" />
<include
layout="@layout/user_card"
android:layout_width="0dp"
android:layout_height="120dp"
android:layout_marginStart="16dp"
android:layout_marginTop="8dp"
android:layout_marginEnd="24dp"
android:layout_marginLeft="16dp"
android:layout_marginRight="24dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/profile_user_name"/>
</androidx.constraintlayout.widget.ConstraintLayout>
reference
1. Add animation transition effects between destinations
2. Android high-level transition animation-ShareElement complete strategy