动画视图移动到布局之外

4

我有一个包含多个RelativeLayoutsLinearLayoutsScrollView,代码如下:

<ScrollView 
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/scroll"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:fillViewport="true" >

      <RelativeLayout >

      </RelativeLayout>

      <LinearLayout >

      </LinearLayout>

</ScrollView>

现在,当我点击其中的一个布局时,我希望它会展开并显示更多信息。我已经通过使用PropertyAnimator将我想要的布局垂直缩放来完成了这个操作:
relativeLayout.animate().scaleY(100).setDuration(duration).start();

同时,我使用另一个PropertyAnimator来移动任何在垂直方向上被展开的视图下面的Views,以便为展开的布局留出足够的空间。到目前为止,它是有效的。
不幸的是,移动的Views最终会出现在ScrollView的视口之外,因此我无法向下滚动并查看这些Views中的信息。实际上,这些视图的垂直平移使它们的下部无法到达,因为视口也没有扩展。
我已经在ScrollView上设置了android:fillViewPort="true"。我还尝试使用setFillViewPort()进行编程设置,但都没有任何效果。
出了什么问题?为什么它没有起作用?

1
你正在错误的方式尝试解决这个问题。你必须反过来做。步骤如下:注册一个 OnPreDrawListener 并记录 ScrollView 中所有视图的当前大小和位置,然后调整 ScrollView 中的视图大小。OnPreDrawListener 将在下一帧准备好绘制时被调用,在这种情况下,将在调整大小启动的布局过程完成后调用。在 OnPreDrawListener 中,记录 ScrollView 中所有视图的新高度和位置,但返回 false,以便不会绘制新状态。 - Xaver Kapeller
2
现在,您已经具备了所有视图的旧高度、新高度和位置,并且可以开始您的动画。最重要的部分是在回调函数中立即删除 OnPreDrawListener。您只想捕获由视图调整大小引起的一个回调。 - Xaver Kapeller
1
因此,总结一下:您调整视图大小并让Android执行一个布局过程,该过程会正确调整和重新定位布局中的所有视图。通过在OnPreDrawListener中返回false来防止绘制新状态,然后将视图从旧位置动画到新位置,同时动画化高度/宽度的变化。诀窍在于Android为您计算所有值,而在执行动画时,所有视图都已正确调整大小并重新定位在布局中。 - Xaver Kapeller
1
从某种意义上说,您让Android为您完成繁重的工作,您只需要记录前后值,然后执行动画即可。 - Xaver Kapeller
我很高兴能够帮忙!一旦我回到家里坐在电脑前,我会写出这个问题并附上一个或两个例子。在那之前,如果你有任何问题,请在评论区随意问我。 - Xaver Kapeller
显示剩余3条评论
4个回答

6
当您对视图执行翻译动画时,这些视图实际上并没有在布局中移动,这只是对用户来说是视觉效果。但是,在布局和/或测量方面,任何翻译值都将被忽略。它总是好像视图根本没有被翻译一样。
我猜想您现在正在做的是:
1. 响应点击事件并展开要展开的视图。 2. 计算其他视图需要移动多少以适应扩展的视图。 3. 然后,您通过需要移动的距离执行翻译动画。 4. 结果,突然有几个视图移出屏幕。
这种方法实际上永远不会起作用。您始终需要记住,视图在布局中的位置仅由布局确定。所有您的翻译本质上只是为了展示。因此,当您尝试执行上述操作时,实际上正在发生以下情况:
1. 您响应点击事件并展开视图。 2. 此扩展导致Android启动布局和测量过程。计算所有视图的位置和大小,并将它们定位到其新位置并设置其新大小。 3. 由于现在视图已经位于扩展后的位置,您的翻译动画只会将视图向下移动,超出它们应该在的位置。 4. 作为此操作的副作用,视图似乎无缘无故地移出屏幕。
那么您该怎么做呢?实际上,您需要从另一个方面解决这个问题。如我上面提到的,Android已经为您计算了所有视图的新大小和位置,您可以利用它。
您的问题有两种基本解决方案。要么让Android使用LayoutTransitions为您执行动画,要么手动执行动画。这两种方法都使用称为ViewTreeObserver的东西。它可用于侦听布局更改或新绘制过程。
但首先:ScrollView只能与一个子项一起使用。因此,为了防止任何未来的错误或问题,请将ScrollView中的所有项目放在具有垂直方向的另一个LinearLayout内。
1)使用LayoutTransition
这仅适用于API级别16及以上。低于API级别16的可见性动画和翻译动画将自动处理,但要获得高度更改的动画效果,则需要具有API级别16。
我必须提醒您的一件重要事情是:
  1. LayoutTransition 可以自动为你执行动画,因此如果你使用它,你可以删除所有自定义动画。如果你保留自己的动画,则会与 LayoutTransition 执行的动画发生冲突。
  2. 如果你不喜欢 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 Level 16及以上所有可能的自动转换,并仅在以下默认启用的转换中使用。只需像这样使用它:

AnimatorUtils.animateLayoutChanges(linearLayout);

如果您在ScrollView中调用此方法,则可以为LinearLayout及其直接子项的高度/宽度/可见性进行动画处理。此外,添加/删除项目的动画也会自动处理。
要启用各种过渡效果(如大小调整动画),您需要在代码中设置LayoutTransitions,但是您可以通过设置属性来启用基本过渡效果(如项目添加/删除动画)。
android:animateLayoutChanges="true"

在您的xml布局中的ViewGroup上。

关于LayoutTransitions,只有极少量的文档可用,但基础知识在这里被涵盖。

如果您想要自定义每个事件的动画,比如添加/删除一个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);

以下是更详细讲解LayoutTransition的DevByte视频:

还要注意,当父容器的高度改变时,容器视图可以基本上剪切掉部分动画。由于您的ScrollView具有固定大小并且不根据ScrollView内的子项调整大小,因此不会发生这种情况,但是如果您在ViewGroup中实现了类似的内容,则需要在所有包含要尝试动画化的View上设置android:clipChildren =“false”。您也可以使用代码中的setClipChildren()


2) 手动动画所有项。

与使用LayoutTransition相比,这要困难得多,主要是因为您必须对布局和测量过程有很多了解,否则您将会引起问题。然而,一旦您掌握了它,就可以执行各种自定义动画。

基本过程如下:

  1. 记录当前View状态。
  2. 更改布局以在动画完成后达到的状态
  3. 在Android完成布局和测量所有内容后记录新值。
  4. 现在将View从其旧位置动画到新位置

这个过程的核心是监听视图层次结构中的更改。这是使用ViewTreeObserver完成的。您可以使用多个可能的回调,例如OnPreDrawListenerOnGlobalLayoutListener。通常,您会像这样实现它们:

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无法为您提供适当的解决方案,并且您必须/想要手动实现动画,则学习如何有效地执行动画非常重要。您可以查看LayoutTransition的源代码here。实际上,LayoutTransition的实现正是我一直在这里解释的内容,并且它是最佳实践的实现。我经常浏览android.animation包的源代码,以了解如何高效地进行动画,如果您想了解Android上的动画,则建议您也这样做!
您还可以观看一些有关动画的Android DevBytes视频,比如这个:
- ListView Expanding Cells Animation 在这个视频中,他讲解了如何通过使用OnPreDrawListener来为ListView中的扩展单元格添加动画。
记住,布局引擎是您的朋友。不要试图重复造轮子并手动执行布局过程会为您执行的操作。在执行动画时永远不要调用requestLayout()

Xaver,这个回答不仅解决了我的问题,还提供了更多信息!非常感谢你详细的回答!我会查看你提供的所有信息。我真的想掌握Android动画框架,因为我感觉这是Android开发中我技能仍然非常欠缺的一个方面。再次感谢你! - Antonis427
我稍微更新了我的答案!很高兴能帮到你 :) - Xaver Kapeller
使用“AnimatorUtils.animateLayoutChanges(linearLayout);”会使我的视图超过操作栏,然后闪回到它应该在的位置,但首先这不太好!其次,它也会使其他片段出现故障。你有什么帮助我解决这个问题的方法吗? - Muhammad Naderi

0

试试这个。

<ScrollView 
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/scroll"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:fillViewport="true" >

      <LinearLayout
              android:layout_width="match_parent"
              android:layout_height="match_parent"
              android:orientation="vertical" >


                <RelativeLayout >

                     </RelativeLayout>

                <LinearLayout >

                     </LinearLayout>

      </LinearLayout>

</ScrollView>

ScrollView只能有一个子元素。因此,我创建了一个LinearLayout子元素,并将您的其余代码添加到其中。


我刚试了一下!不幸的是,它没有起作用 - 移动的视图仍然有内容超出视口。这真的很奇怪。 - Antonis427
从ScrollView中删除 android:fillViewport="true" 并尝试。@Antonis427 - Pooja
仍然没有任何事情发生。 - Antonis427
@Pooja 通过动画或其他方式进行的翻译对布局过程没有影响,无论您对视图进行何种动画,布局引擎仍将视图视为未经动画处理。 (除非您做了一些不好的事情,例如在同时更改LayoutParams时不断强制进行新的布局过程)。 - Xaver Kapeller

0

来自Android开发者网站: ScrollView是一个FrameLayout,这意味着您应该在其中放置一个包含要滚动的整个内容的子项;这个子项本身可以是具有复杂对象层次结构的布局管理器。经常使用的子项是垂直方向上的LinearLayout,它呈现了用户可以滚动的顶级项目的垂直数组。

我看到您已经将多个布局添加为ScrollView的子项,请添加一个LinearLayout并将其余布局添加到该LinearLayout中。 希望它能解决您的问题。


谢谢。我尝试过了,但不幸的是,它仍然没有起作用! - Antonis427

0
只需在您的父动画视图中添加android:clipChildren="false",动画即可在视图外部工作。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接