Android 共享元素的简单理解和使用
1 、基本概念
Andriod 5.0及之后,开始支持共享元素动画,两个Activity或者fragment可以共享某些控件,例如Activity A跳转到Activity B的时候,A的某个控件能自动移动到B的相应控件的位置,产生动画。
2、基本使用
1、Activity to Activity跳转实现
1.1、使用步骤
1、为每个共享元素视图分配一个唯一的过渡名称。
2、makeSceneTransitionAnimation
添加共享元素视图和切换后对应共享元素视图的过渡名称。
3、 页面跳转。
1.2、案例说明
1、第一步,设置ActivityA、ActivityB中view的transitionName
属性, 属性值是自定义的,就是一个字符串。
<?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
属性
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、设置intent跳转
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()
参数解释:
- activity就是发起跳转的Activity,
- shareElement就是共享的控件的id,
- sharedElementName就是第一步定义的字符串。
- 这个方式只支持共享单个控件。
具体如下:发起跳转的Activity:
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代码:
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跳转实现
2.1、使用步骤
1、设置transitionName属性
Transition 框架需要一个能将当前页面的 View 关联到将要跳转到的页面的办法,下面是为 View 添加 Transition Name 的两种方法:
- 可以在代码中直接调用 ViewCompat.setTransitionName(),也可以在 Android Lolipop
系统版本以上的设备中直接调用 setTransitionName()。 - 在布局的 XML 文件中,直接设置 android:transitionName 属性。
2、设置FragmentTransaction
getSupportFragmentManager()
.beginTransaction()
.addSharedElement(sharedElement, transitionName)
.replace(R.id.container, newFragment)
.addToBackStack(null)
.commit();
只需要调用 addSharedElement()
来关联要与 Fragment 共享的 View。
- 作为参数传递给 addSharedElement() 的 View ,就是第一个 Fragment 中你所想要与即将跳转的
Fragment 共享的 View。 - 而这个方法的第二个参数要求填入的 TransitionName,就是在跳转的 Fragment 中的该共享 View 的
Transition Name。- 例如,当前 Fragment 中共享 View 的 transitionName 为 “foo”,即将跳转的 Fragment 中的共享
View 的 transitionName 为 “bar”,那么在 addSharedElement() 中传递的第二个参数就应该是“bar”。
- 例如,当前 Fragment 中共享 View 的 transitionName 为 “foo”,即将跳转的 Fragment 中的共享
3、指定 Transition 动画为共享元素添加的办法:
调用
setSharedElementEnterTransition()
指定 View 如何从第一个 Fragment 转换为跳转 Fragment 中的 View。调用
setSharedElementReturnTransition()
指定 View 在用户点击返回按钮后如何从跳转 Fragment 中回到第一个 Fragment 中。
记住,返回 Transition 需要在跳转 Fragment 中调用相应的方法,要不然你得不到你想要的效果的。
当然了,你也可以为任何非共享的 View 设置 Transition 过渡动画,只不过调用的 API 变了:在对应的 Fragment 中调用 setEnterTransition
(), setExitTransition
(), setReturnTransition
(),和 setReenterTransition
() 这些方法就可以了。
2.2、案例说明
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 + 共享元素+ recyclerview 跳转实现
3.1、使用步骤
- 1、设置
transitionName
属性 - 2、定义
FragmentNavigatorExtras
参数 - 3、
findNavController().navigate
页面跳转
// 第一步
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、案例说明
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>