当您在Views 上执行翻译动画时,那些Views 并不会真正在布局内移动。它对用户来说只是视觉上的,但是在布局和/或测量时,任何翻译值都被忽略了。总是好像Views 根本没有翻译。
我猜你现在正在做的是:
- 您对点击事件做出反应并展开您想要展开的
View。
- 您计算其他视图需要移动多少才能容纳扩展的
View。
- 然后您对那些需要移动的
Views 执行平移动画。
- 然后突然有几个
Views移出屏幕。
这种方法实际上永远行不通。您始终需要记住,布局中的Views 位置只是由布局确定。你所有的翻译基本上只是为了展示。因此,当您尝试执行上述操作时,实际会发生这种情况:
- 您对点击事件做出反应并展开
View。
- 此扩展会导致 Android 开始布局和测量过程。计算所有视图的位置和大小,并将它们定位在具有新大小的新位置。
- 因为现在
Views 已经在扩展后它们将要到达的位置,您翻译动画只需将Views 进一步向下移动,超出它们应该在的位置。
- 因此,
Views 似乎无缘无故地离开了屏幕。
那么你能做些什么呢?本质上,您需要以相反的方式解决此问题。正如我上面提到的,Android 已经为您计算了所有Views 的新大小和位置,您可以利用它来发挥自己的优势。
您的问题有两种基本解决方案。您可以让 Android 使用 LayoutTransitions 为您执行动画,或者您手动执行动画。两种方式都使用称为ViewTreeObserver 的东西。它可以用来监听布局的变化或新的绘图过程。
但最重要的是:ScrollView 应该只与一个孩子一起工作。因此,为了防止将来出现任何错误或问题,请将您的所有项目放在 ScrollView 中另一个 LinearLayout 的垂直方向内。
1) 使用 LayoutTransition
这仅适用于 API 级别 16 及更高版本。低于 API 级别 16 的可见性动画和平移动画将被自动处理,但要获得高度变化动画,您需要 API 级别 16。
我必须提到的重要一点是:
-
LayoutTransition 为您动画更改。因此,如果您使用它,您可以删除所有自定义动画。如果您将自己的动画留在其中,您只会与LayoutTransition 执行的动画产生冲突。
- 如果您不喜欢
LayoutTransition 执行的动画,您可以自定义它们!我将在下面进一步解释如何做到这一点。
我通常使用这样的辅助方法来设置LayoutTransition。
public static void animateLayoutChanges(ViewGroup container) {
final LayoutTransition transition = new LayoutTransition();
transition.setDuration(300);
if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
transition.enableTransitionType(LayoutTransition.CHANGING);
transition.enableTransitionType(LayoutTransition.APPEARING);
transition.enableTransitionType(LayoutTransition.CHANGE_APPEARING);
transition.enableTransitionType(LayoutTransition.DISAPPEARING);
transition.enableTransitionType(LayoutTransition.CHANGE_DISAPPEARING);
}
container.setLayoutTransition(transition);
}
这将在 API 级别 16 及更高版本上启用所有可能的自动转换,并仅使用低于该级别的默认启用转换。像这样使用它:
AnimatorUtils.animateLayoutChanges(linearLayout);
如果您在ScrollView 中的LinearLayout 上调用此方法,那么LinearLayout 及其直接子项的高度/宽度/可见性的所有更改都将为您设置动画。还为您处理项目添加/删除动画。
要启用各种转换,如调整大小动画,您需要在代码中设置LayoutTransitions,但您可以通过设置属性启用基本转换,如项目添加/删除动画
android:animateLayoutChanges="true"
在您的 xml 布局中的 ViewGroup 上。
LayoutTransitions 上只有极少的文档,但 here 涵盖了基础知识。
如果您愿意,您可以为每个事件自定义动画,例如添加/删除 View 或更改 View 的某些内容,如下所示:
// APPEARING handles items being added to the ViewGroup
transition.setAnimator(LayoutTransition.APPEARING, someAnimator);
// CHANGING handles among other things height or width changes of items in the ViewGroup
transition.setAnimator(LayoutTransition.CHANGING, someOtherAnimator);
这是一个 DevByte 视频,它更详细地解释了LayoutTransitions:
还请注意,当父级的高度发生变化时,容器视图实际上可以截断部分动画。这不会发生在您的情况下,因为您的ScrollView 具有固定大小并且不会根据ScrollView 中的子项调整大小,但是如果您在ViewGroup 和wrap_content 中实现类似的东西,那么您需要在您尝试设置动画的Views 上方的所有容器上设置android:clipChildren="false"。您也可以在代码中使用setClipChildren()。
2) 手动为所有项目设置动画。
这比使用LayoutTransitions要困难得多,主要是你必须对布局和测量过程有很多了解,否则会出问题。不过,一旦您掌握了窍门,您就可以执行各种自定义动画。
基本流程是这样的:
- 记录当前
View状态。
- 将布局更改为动画完成后的状态
- 在 Android 完成布局和测量所有内容后,记录新值。
- 现在将
Views 从旧位置设置为新位置
这个过程的核心是监听视图层次结构的变化。这是使用ViewTreeObserver 完成的。您可以使用多种可能的回调,例如OnPreDrawListener 或OnGlobalLayoutListener。通常你会像这样实现它们:
final Animator animator = setupAnimator();
animator.setTarget(view);
// Record the current state
animator.setupStartValues();
modifyChildrenOfLinearLayout();
linearLayout.getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
@Override
public void onGlobalLayout() {
// Remove the callback immediately we only need to catch it this one time.
linearLayout.getViewTreeObserver().removeOnGlobalLayoutListener(this);
// Record the new state
animator.setupEndValues();
// Start the animation
animator.start();
}
});
OnGlobalLayoutListener 更擅长捕捉布局变化,因为它是在布局过程完成后调用的。 OnPreDrawListener 在绘制下一帧之前调用,但它们不能保证布局过程已经完成。但在实践中,这种差异可以忽略不计。更重要的是,在较旧的较慢设备上,新状态下的布局可能会短暂闪烁,因为它们需要一些时间来处理每个步骤。您可以通过使用OnPreDrawListener 并返回一次false 来防止这种情况。由于OnGlobalLayoutListener 也仅在较新的 API 级别上完全可用,因此在大多数情况下您应该使用 OnPreDrawListener。
如果LayoutTransitions 没有为您提供适当的解决方案来解决您的问题,并且您已经/想要手动实现动画,那么学习如何有效地执行动画就很重要了。可以看LayoutTransitionhere的源码。 LayoutTransition 的实现基本上完全符合我在这里解释的内容,它是最佳实践实现。我经常发现自己在查看 android.animation 包的源代码以了解有关如何高效动画的新知识,如果您想了解 Android 上的动画,我建议您也这样做!
您还可以观看一些有关动画的 Android DevBytes 视频,例如:
在此视频中,他解释了如何使用 OnPreDrawListener 为 ListView 中的扩展单元格设置动画。
请始终记住,布局引擎是您的朋友。不要试图重新发明轮子并手动做一些布局过程已经为你做的事情。并且永远不要在执行动画时调用requestLayout()!