Android 共享元素过渡工具栏重叠
Android shared element transition ToolBar overlap
我已经为我的应用程序实现了共享元素转换,其中转换从主屏幕上 ViewPager 内的 Fragment(带有 RecyclerView)的图像开始,并扩展到全屏画廊视图,再次在 Fragment 中一个 ViewPager。这一切都很好,除了如果图像不完全可见,它会在扩展到全屏之前进入 TabBar 的顶部。这是正在发生的事情:
我的输入转换看起来像这样:
<?xml version="1.0" encoding="utf-8"?>
<fade xmlns:android="http://schemas.android.com/apk/res/android">
<targets>
<target android:excludeId="@android:id/statusBarBackground"/>
<target android:excludeId="@android:id/navigationBarBackground"/>
</targets>
</fade>
并退出:
<?xml version="1.0" encoding="utf-8"?>
<transitionSet xmlns:android="http://schemas.android.com/apk/res/android"
android:transitionOrdering="together"
android:duration="500">
<fade>
<targets>
<target android:excludeId="@android:id/statusBarBackground" />
<target android:excludeId="@android:id/navigationBarBackground" />
</targets>
</fade>
</transitionSet>
在调用 activity 的共享元素回调中,我得到了这个:
View navigationBar = activity.findViewById(android.R.id.navigationBarBackground);
View statusBar = activity.findViewById(android.R.id.statusBarBackground);
if (navigationBar != null) {
names.add(navigationBar.getTransitionName());
sharedElements.put(navigationBar.getTransitionName(), navigationBar);
}
if (statusBar != null) {
names.add(statusBar.getTransitionName());
sharedElements.put(statusBar.getTransitionName(), statusBar);
}
styles.xml 终于 activity 主题:
<item name="android:windowContentTransitions">true</item>
<item name="android:windowEnterTransition">@transition/details_window_enter_transition</item>
<item name="android:windowReturnTransition">@transition/details_window_return_transition</item>
我真的不明白工具栏(或操作栏)如何在没有重叠的情况下被转换排除。也许一种方法是以某种方式强制在顶部剪裁图像,以便它在工具栏下时不会完全可见,并且仅从可见矩形展开。
我试过将 <target android:excludeId="@id/action_bar_container"/>
添加到动画的目标,但同样的事情仍然发生。
欢迎提出任何建议。
我想出了一个临时解决方法。在执行共享元素转换之前,调用 activity 检查目标视图是否在 RecyclerView 的边界内。如果没有,RecyclerView 会平滑滚动以显示完整视图,一旦完成,转换就会运行。如果视图完全可见,则转换正常运行。
// Scroll to view and run animation when scrolling completes.
recycler.smoothScrollToPosition(adapterPosition);
recycler.getViewTreeObserver().addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() {
@Override
public boolean onPreDraw() {
recycler.getViewTreeObserver().removeOnPreDrawListener(this);
// Open activity here.
return true;
}
});
这是结果,在我找到更好的解决方案之前还不错:
我在下面的 project.Add 代码中发现了类似的问题。
<item name="android:windowSharedElementsUseOverlay">false</item>
对我有用。
我想我对这个问题有正确的答案。发生此问题是因为在第二个 activity 上您根本没有工具栏,因此无法将其作为共享元素添加到过渡中。所以在我的例子中,我在我的第二个 activity 中添加了一些 'fake toolbar',它的高度为 0dp。然后可以把toolbar from firstactivity添加为共享元素,并给他改变bounds动画,这样toolbar会同时崩溃,image不再是'over'toolbar.
我的 'fake toolbar' 观点:
<View
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="0dp"
android:background="@color/colorPrimary"
android:elevation="10dp"
android:outlineProvider="none"
android:transitionName="toolbar_transition" />
重要提示:
-视图必须有非透明背景
-我添加了海拔以确保我的视图是 'over' 图片
-高程导致阴影,我不想要,所以我将 outilenProvider 设置为 none
接下来您要做的就是将工具栏添加到共享元素
sharedElements.add(new Pair<>(toolbar, "toolbar_transition"));
我到处搜索,找不到任何解决方案,所以我想出了自己的办法。这是一个录制的演示(以一半的速度)显示结果(有和没有修复)。
请在此处查看完整的工作演示:
https://github.com/me-abhinav/shared-element-overlap-demo
步骤
假设我们有两个活动,即。 MainActivity
有一个带有 grid/list 缩略图的滚动容器,我们有一个 SecondActivity
全屏幻灯片显示图像。
请查看完整代码以完全理解解决方案。
- 在托管滚动容器的
MainActivity
内,在缩略图上设置点击侦听器以打开 SecondActivity
:
ImageView imageView = findViewById(R.id.image_view);
imageView.setOnClickListener(v -> {
// Set the transition name. We could also do it in the xml layout but this is to demo
// that we can choose any name generated dynamically.
String transitionName = getString(R.string.transition_name);
imageView.setTransitionName(transitionName);
// This part is important. We first need to clip this view to only its visible part.
// We will also clip the corresponding view in the SecondActivity using shared element
// callbacks.
Rect localVisibleRect = new Rect();
imageView.getLocalVisibleRect(localVisibleRect);
imageView.setClipBounds(localVisibleRect);
mClippedView = imageView;
Intent intent = new Intent(MainActivity.this, SecondActivity.class);
intent.putExtra(SecondActivity.EXTRA_TRANSITION_NAME, transitionName);
intent.putExtra(SecondActivity.EXTRA_CLIP_RECT, localVisibleRect);
ActivityOptions options = ActivityOptions.makeSceneTransitionAnimation(
MainActivity.this,
Pair.create(imageView, transitionName));
startActivity(intent, options.toBundle());
});
- 恢复您
MainActivity
的 onResume()
中的剪辑。
@Override
protected void onResume() {
super.onResume();
// This is also important. When we come back to this activity, we need to reset the clip.
if (mClippedView != null) {
mClippedView.setClipBounds(null);
}
}
- 像这样在 res 文件夹中创建过渡资源:
app/src/main/res/transition/shared_element_transition.xml
内容应该是这样的:
<?xml version="1.0" encoding="utf-8"?>
<transitionSet
xmlns:android="http://schemas.android.com/apk/res/android"
android:duration="375"
android:interpolator="@android:interpolator/fast_out_slow_in"
android:transitionOrdering="together">
<!-- This is needed to clip the invisible part of the view being transitioned. Otherwise we
will see weird transitions when the image is partially hidden behind appbar or any other
view. -->
<changeClipBounds/>
<changeTransform/>
<changeBounds/>
</transitionSet>
- 在
SecondActivity
中设置转场。
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_second);
// Setup transition
Transition transition =
TransitionInflater.from(this)
.inflateTransition(R.transition.shared_element_transition);
getWindow().setSharedElementEnterTransition(transition);
// Postpone the transition. We will start it when the slideshow is ready.
ActivityCompat.postponeEnterTransition(this);
// more code ...
// See next step below.
}
- 现在我们还需要在
SecondActivity
中剪辑共享视图。
// Setup the clips
String transitionName = getIntent().getStringExtra(EXTRA_TRANSITION_NAME);
Rect clipRect = getIntent().getParcelableExtra(EXTRA_CLIP_RECT);
setEnterSharedElementCallback(new SharedElementCallback() {
@Override
public void onSharedElementStart(List<String> sharedElementNames, List<View> sharedElements, List<View> sharedElementSnapshots) {
for (int i = 0; i < sharedElementNames.size(); i++) {
if (Objects.equals(transitionName, sharedElementNames.get(i))) {
View view = sharedElements.get(i);
view.setClipBounds(clipRect);
}
}
super.onSharedElementStart(sharedElementNames, sharedElements, sharedElementSnapshots);
}
@Override
public void onSharedElementEnd(List<String> sharedElementNames, List<View> sharedElements, List<View> sharedElementSnapshots) {
for (int i = 0; i < sharedElementNames.size(); i++) {
if (Objects.equals(transitionName, sharedElementNames.get(i))) {
View view = sharedElements.get(i);
view.setClipBounds(null);
}
}
super.onSharedElementEnd(sharedElementNames, sharedElements, sharedElementSnapshots);
}
});
我已经为我的应用程序实现了共享元素转换,其中转换从主屏幕上 ViewPager 内的 Fragment(带有 RecyclerView)的图像开始,并扩展到全屏画廊视图,再次在 Fragment 中一个 ViewPager。这一切都很好,除了如果图像不完全可见,它会在扩展到全屏之前进入 TabBar 的顶部。这是正在发生的事情:
我的输入转换看起来像这样:
<?xml version="1.0" encoding="utf-8"?>
<fade xmlns:android="http://schemas.android.com/apk/res/android">
<targets>
<target android:excludeId="@android:id/statusBarBackground"/>
<target android:excludeId="@android:id/navigationBarBackground"/>
</targets>
</fade>
并退出:
<?xml version="1.0" encoding="utf-8"?>
<transitionSet xmlns:android="http://schemas.android.com/apk/res/android"
android:transitionOrdering="together"
android:duration="500">
<fade>
<targets>
<target android:excludeId="@android:id/statusBarBackground" />
<target android:excludeId="@android:id/navigationBarBackground" />
</targets>
</fade>
</transitionSet>
在调用 activity 的共享元素回调中,我得到了这个:
View navigationBar = activity.findViewById(android.R.id.navigationBarBackground);
View statusBar = activity.findViewById(android.R.id.statusBarBackground);
if (navigationBar != null) {
names.add(navigationBar.getTransitionName());
sharedElements.put(navigationBar.getTransitionName(), navigationBar);
}
if (statusBar != null) {
names.add(statusBar.getTransitionName());
sharedElements.put(statusBar.getTransitionName(), statusBar);
}
styles.xml 终于 activity 主题:
<item name="android:windowContentTransitions">true</item>
<item name="android:windowEnterTransition">@transition/details_window_enter_transition</item>
<item name="android:windowReturnTransition">@transition/details_window_return_transition</item>
我真的不明白工具栏(或操作栏)如何在没有重叠的情况下被转换排除。也许一种方法是以某种方式强制在顶部剪裁图像,以便它在工具栏下时不会完全可见,并且仅从可见矩形展开。
我试过将 <target android:excludeId="@id/action_bar_container"/>
添加到动画的目标,但同样的事情仍然发生。
欢迎提出任何建议。
我想出了一个临时解决方法。在执行共享元素转换之前,调用 activity 检查目标视图是否在 RecyclerView 的边界内。如果没有,RecyclerView 会平滑滚动以显示完整视图,一旦完成,转换就会运行。如果视图完全可见,则转换正常运行。
// Scroll to view and run animation when scrolling completes.
recycler.smoothScrollToPosition(adapterPosition);
recycler.getViewTreeObserver().addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() {
@Override
public boolean onPreDraw() {
recycler.getViewTreeObserver().removeOnPreDrawListener(this);
// Open activity here.
return true;
}
});
这是结果,在我找到更好的解决方案之前还不错:
我在下面的 project.Add 代码中发现了类似的问题。
<item name="android:windowSharedElementsUseOverlay">false</item>
对我有用。
我想我对这个问题有正确的答案。发生此问题是因为在第二个 activity 上您根本没有工具栏,因此无法将其作为共享元素添加到过渡中。所以在我的例子中,我在我的第二个 activity 中添加了一些 'fake toolbar',它的高度为 0dp。然后可以把toolbar from firstactivity添加为共享元素,并给他改变bounds动画,这样toolbar会同时崩溃,image不再是'over'toolbar.
我的 'fake toolbar' 观点:
<View
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="0dp"
android:background="@color/colorPrimary"
android:elevation="10dp"
android:outlineProvider="none"
android:transitionName="toolbar_transition" />
重要提示:
-视图必须有非透明背景
-我添加了海拔以确保我的视图是 'over' 图片
-高程导致阴影,我不想要,所以我将 outilenProvider 设置为 none
接下来您要做的就是将工具栏添加到共享元素
sharedElements.add(new Pair<>(toolbar, "toolbar_transition"));
我到处搜索,找不到任何解决方案,所以我想出了自己的办法。这是一个录制的演示(以一半的速度)显示结果(有和没有修复)。
请在此处查看完整的工作演示: https://github.com/me-abhinav/shared-element-overlap-demo
步骤
假设我们有两个活动,即。 MainActivity
有一个带有 grid/list 缩略图的滚动容器,我们有一个 SecondActivity
全屏幻灯片显示图像。
请查看完整代码以完全理解解决方案。
- 在托管滚动容器的
MainActivity
内,在缩略图上设置点击侦听器以打开SecondActivity
:
ImageView imageView = findViewById(R.id.image_view);
imageView.setOnClickListener(v -> {
// Set the transition name. We could also do it in the xml layout but this is to demo
// that we can choose any name generated dynamically.
String transitionName = getString(R.string.transition_name);
imageView.setTransitionName(transitionName);
// This part is important. We first need to clip this view to only its visible part.
// We will also clip the corresponding view in the SecondActivity using shared element
// callbacks.
Rect localVisibleRect = new Rect();
imageView.getLocalVisibleRect(localVisibleRect);
imageView.setClipBounds(localVisibleRect);
mClippedView = imageView;
Intent intent = new Intent(MainActivity.this, SecondActivity.class);
intent.putExtra(SecondActivity.EXTRA_TRANSITION_NAME, transitionName);
intent.putExtra(SecondActivity.EXTRA_CLIP_RECT, localVisibleRect);
ActivityOptions options = ActivityOptions.makeSceneTransitionAnimation(
MainActivity.this,
Pair.create(imageView, transitionName));
startActivity(intent, options.toBundle());
});
- 恢复您
MainActivity
的onResume()
中的剪辑。
@Override
protected void onResume() {
super.onResume();
// This is also important. When we come back to this activity, we need to reset the clip.
if (mClippedView != null) {
mClippedView.setClipBounds(null);
}
}
- 像这样在 res 文件夹中创建过渡资源:
app/src/main/res/transition/shared_element_transition.xml
内容应该是这样的:
<?xml version="1.0" encoding="utf-8"?>
<transitionSet
xmlns:android="http://schemas.android.com/apk/res/android"
android:duration="375"
android:interpolator="@android:interpolator/fast_out_slow_in"
android:transitionOrdering="together">
<!-- This is needed to clip the invisible part of the view being transitioned. Otherwise we
will see weird transitions when the image is partially hidden behind appbar or any other
view. -->
<changeClipBounds/>
<changeTransform/>
<changeBounds/>
</transitionSet>
- 在
SecondActivity
中设置转场。
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_second);
// Setup transition
Transition transition =
TransitionInflater.from(this)
.inflateTransition(R.transition.shared_element_transition);
getWindow().setSharedElementEnterTransition(transition);
// Postpone the transition. We will start it when the slideshow is ready.
ActivityCompat.postponeEnterTransition(this);
// more code ...
// See next step below.
}
- 现在我们还需要在
SecondActivity
中剪辑共享视图。
// Setup the clips
String transitionName = getIntent().getStringExtra(EXTRA_TRANSITION_NAME);
Rect clipRect = getIntent().getParcelableExtra(EXTRA_CLIP_RECT);
setEnterSharedElementCallback(new SharedElementCallback() {
@Override
public void onSharedElementStart(List<String> sharedElementNames, List<View> sharedElements, List<View> sharedElementSnapshots) {
for (int i = 0; i < sharedElementNames.size(); i++) {
if (Objects.equals(transitionName, sharedElementNames.get(i))) {
View view = sharedElements.get(i);
view.setClipBounds(clipRect);
}
}
super.onSharedElementStart(sharedElementNames, sharedElements, sharedElementSnapshots);
}
@Override
public void onSharedElementEnd(List<String> sharedElementNames, List<View> sharedElements, List<View> sharedElementSnapshots) {
for (int i = 0; i < sharedElementNames.size(); i++) {
if (Objects.equals(transitionName, sharedElementNames.get(i))) {
View view = sharedElements.get(i);
view.setClipBounds(null);
}
}
super.onSharedElementEnd(sharedElementNames, sharedElements, sharedElementSnapshots);
}
});