【问题标题】:Animated Views move outside of my layout动画视图移出我的布局
【发布时间】:2015-03-20 14:30:59
【问题描述】:

我有一个ScrollView,其中包含几个RelativeLayoutsLinearLayouts,如下所示:

<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() 以编程方式执行此操作,但都没有任何效果。

怎么了?为什么它不起作用?

【问题讨论】:

  • 您试图以错误的方式解决此问题。你必须反过来做。步骤是:注册一个OnPreDrawListener并记录你的ScrollView中所有视图的当前大小和位置,然后调整ScrollView中的视图大小。一旦准备好绘制下一帧,将调用OnPreDrawListener,在这种情况下,将在调整大小启动的布局过程完成之后。在OnPreDrawListener 中记录ScrollView 中所有视图的新高度和位置,但返回false,因此不会绘制新状态。
  • 现在你有了所有视图的新旧高度和位置,你可以开始你的动画了。最重要的部分是您在回调中立即删除OnPreDrawListener。您只想捕获由调整视图大小引起的一个回调。
  • 总结一下:您调整视图大小并让 android 执行一个布局过程,该过程可以正确调整布局上的所有视图的大小和重新定位。您可以通过在 OnPreDrawListener 中返回 false 来防止绘制这种新状态,然后将视图从旧位置动画到新位置,同时动画化想要更改高度/宽度。诀窍是 android 会为您计算所有值,并且当您执行动画时,所有视图都将在您的布局中正确调整大小和重新定位。
  • 从某种意义上说,您让android为您完成繁重的工作,您只需记录前后值,然后执行动画。
  • Xaver,我一直在寻找使用 ViewTreeObserver / onPreDrawListener 的机会,因为这是我非常缺乏的一项技能。感谢您提供非常详细的答案,我将使用余下的时间愉快地工作!

标签: java android android-animation android-scrollview


【解决方案1】:

当您在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。

我必须提到的重要一点是:

  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 级别 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 中的子项调整大小,但是如果您在ViewGroupwrap_content 中实现类似的东西,那么您需要在您尝试设置动画的Views 上方的所有容器上设置android:clipChildren="false"。您也可以在代码中使用setClipChildren()


2) 手动为所有项目设置动画。

这比使用LayoutTransitions要困难得多,主要是你必须对布局和测量过程有很多了解,否则会出问题。不过,一旦您掌握了窍门,您就可以执行各种自定义动画。

基本流程是这样的:

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

这个过程的核心是监听视图层次结构的变化。这是使用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 没有为您提供适当的解决方案来解决您的问题,并且您已经/想要手动实现动画,那么学习如何有效地执行动画就很重要了。可以看LayoutTransitionhere的源码。 LayoutTransition 的实现基本上完全符合我在这里解释的内容,它是最佳实践实现。我经常发现自己在查看 android.animation 包的源代码以了解有关如何高效动画的新知识,如果您想了解 Android 上的动画,我建议您也这样做!

您还可以观看一些有关动画的 Android DevBytes 视频,例如:

在此视频中,他解释了如何使用 OnPreDrawListenerListView 中的扩展单元格设置动画。


请始终记住,布局引擎是您的朋友。不要试图重新发明轮子并手动做一些布局过程已经为你做的事情。并且永远不要在执行动画时调用requestLayout()

【讨论】:

  • Xaver,这回答了我的问题,然后回答了一些问题!谢谢你非常详细的回答!我会检查你提供的所有信息。我真的很想掌握Android动画框架,我觉得这是Android开发的一个方面,我仍然非常缺乏技能。再一次,我不能感谢你!
  • 我已经稍微更新了我的答案!很高兴我能帮忙:)
  • 使用 'AnimatorUtils.animateLayoutChanges(linearLayout);'它让我的观点越过了动作栏,然后又回到了它应该在的地方,但首先它并不酷!第二个是它也会让其他片段出错,!请问您有什么要帮我的吗?
【解决方案2】:

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

我可以看到您在 Scroll View 中添加了多个布局作为子布局,请添加一个线性布局并在该 LinearLayout 中添加其余布局。 希望能解决你的问题

【讨论】:

  • 谢谢。我已经尝试过了,但不幸的是,它仍然无法正常工作!
【解决方案3】:

试试这个。

<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 子,并在其中添加了你的其余代码。

【讨论】:

  • 我刚试过!不幸的是,无法正常工作 - 移动的视图仍然在视口之外有内容。真的很奇怪。
  • 从滚动视图中删除 android:fillViewport="true" 并尝试。 @Antonis427
  • 仍然没有任何反应。
  • @Pooja 通过动画或类似方式进行的翻译对布局过程没有影响,无论您为视图设置什么动画,布局引擎仍然会看到视图,因为它根本没有动画。 (除非你做了一些坏事,比如不断地强制一个新的布局过程,同时改变LayoutParams或类似的东西)。
【解决方案4】:

只需将android:clipChildren="false"添加到您的父动画视图和视图之外的动画作品。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2016-08-25
    • 1970-01-01
    • 1970-01-01
    • 2013-12-04
    • 2018-08-30
    相关资源
    最近更新 更多