3 pane layout fragment animation is stuttering
Asked Answered
S

2

4

The implementation info: I have implemented a 3 pane layout following CommonsWare response posted on his own question over here: Complete Working Sample of the Gmail Three-Fragment Animation Scenario?

As a general idea, I have the layout comprised of following levels (1 through 3):

  1. MainActivity
  2. SlidingMenu (Side Drawer UI pattern) fragment hiding on the left side and a ContentFragment as the fragment that houses the 3 Pane Layout.
  3. Inside the ContentFragment: LeftListFragment (rows with 3 TextViews each), MiddleListFragment (rows with 8 TextViews each), DetailFragment.

LeftListFragment and MiddleListFragment use CursorLoaders to load data from a ContentProvider inside each list. DetailFragment also calls for a cursor with data when needed. So I didn't even implement custom Adapters (much nicer design this way). Then I have added the 3 Pane layout + animations. As far as working, it works as intended, no problems there. Animation time is 500ms.

The problem: The animations stutter a little. A few dropped frames. Both when the Left and Middle are visible and I click on a Middle list item to open a Detail; and also when I tap the Back button to see again Left and Middle lists (when nothing is actually loading).

What I've tried:

  1. Removed the code that loads the fragment in DetailView. I just tap an item in MiddleFragment and the animation begins, without any Detail actually loading. Still stutters. Also, when hitting back, nothing loads and it still stutters, so I presume the loaders/cursors are not the cause of this.
  2. I used dumpsys gfxinfo to see the average time in ms to compute each frame. Indeed, the avg time for a computation is 18ms, which is above the 16ms threshold. So does this mean the stuttering is because of the time it takes to draw again the lists, when animating ? If so, why ? I mean... I don't have any images at all inside row views. And I couldn't screw up the Adapters code, because I haven't written any...
  3. Reducing the Animation time from 500ms to 200ms. It still stutters if you watch it really carefully, it's just faster.

EDIT: I switched from rightPaneWidth to leftPaneWidth below (yes, that removes the re-dimensioning animation) and the stuttering is now gone. The list still slides to the left side, but it just doesn't get smaller in width. So if there is no more stuttering now, does that mean there is a problem with the ObjectAnimator in my code ?

ObjectAnimator.ofInt(this, "middleWidth", rightPaneWidth, leftPaneWidth)
                    .setDuration(ANIM_DURATION).start();

Thanks for your time !

Code for 3 Pane Layout:

package com.xyz.view.widget;

import android.animation.ObjectAnimator;
import android.annotation.SuppressLint;
import android.annotation.TargetApi;
import android.content.Context;
import android.util.AttributeSet;
import android.view.View;
import android.view.ViewPropertyAnimator;
import android.widget.LinearLayout;


public class ThreePaneLayout extends LinearLayout
{
    private View leftView = null;
    private View middleView = null;
    private View rightView = null;

private static final int ANIM_DURATION = 500;
private int leftPaneWidth = -1;
private int rightPaneWidth = -1;


// -------------------------------------------------------------------------------------------
// --------------   Constructor
// -------------------------------------------------------------------------------------------


public ThreePaneLayout(Context context, AttributeSet attrs)
{
    super(context, attrs);
    setOrientation(HORIZONTAL);
}

@Override
public void onFinishInflate()
{
    super.onFinishInflate();
    leftView = getChildAt(0);
    middleView = getChildAt(1);
    rightView = getChildAt(2);
}


// -------------------------------------------------------------------------------------------
// --------------   Public methods
// -------------------------------------------------------------------------------------------


public View getLeftView()
{
    return leftView;
}

public View getMiddleView()
{
    return middleView;
}

public View getRightView()
{
    return rightView;
}

@SuppressLint("NewApi")
public void hideLeft()
{
    if (leftPaneWidth == -1)
    {

        leftPaneWidth = leftView.getWidth();
        rightPaneWidth = middleView.getWidth();
        resetWidget(leftView, leftPaneWidth);
        resetWidget(middleView, rightPaneWidth);
        resetWidget(rightView, rightPaneWidth);
        requestLayout();
    }
    translateWidgets(-1 * leftPaneWidth, leftView, middleView, rightView);
    ObjectAnimator.ofInt(this, "middleWidth", rightPaneWidth, leftPaneWidth)
                    .setDuration(ANIM_DURATION).start();
}

@SuppressLint("NewApi")
public void showLeft()
{
    translateWidgets(leftPaneWidth, leftView, middleView, rightView);
    ObjectAnimator.ofInt(this, "middleWidth", leftPaneWidth, rightPaneWidth)
                    .setDuration(ANIM_DURATION)
                    .start();
}


// -------------------------------------------------------------------------------------------
// --------------   Private methods
// -------------------------------------------------------------------------------------------


private void setMiddleWidth(int value)
{
    middleView.getLayoutParams().width = value;
    requestLayout();
}

@TargetApi(12)
private void translateWidgets(int deltaX, View... views)
{
    for (final View view : views)
    {
        ViewPropertyAnimator viewPropertyAnimator = view.animate();
        viewPropertyAnimator.translationXBy(deltaX)
                            .setDuration(ANIM_DURATION);
    }
  }

  private void resetWidget(View view, int width)
  {
      LinearLayout.LayoutParams layoutParams = (LinearLayout.LayoutParams)view.getLayoutParams();  
      layoutParams.width = width;
      layoutParams.weight = 0;
  }
}

XML for the ContentFragment:

    <?xml version="1.0" encoding="utf-8"?>
<com.xyz.view.widget.ThreePaneLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/fragment_content_three_pane_layout"
    android:layout_width="match_parent"
    android:layout_height="match_parent" >

<FrameLayout
    android:id="@+id/fragment_content_framelayout_left"
    android:layout_width="0dp"
    android:layout_height="match_parent"
    android:layout_weight="3" />

<FrameLayout
    android:id="@+id/fragment_content_framelayout_middle"
    android:layout_width="0dp"
    android:layout_height="match_parent"
    android:layout_weight="7" />

<FrameLayout
    android:id="@+id/fragment_content_framelayout_right"
    android:layout_width="0dp"
    android:layout_height="match_parent" />
</com.xyz.view.widget.ThreePaneLayout>
Stagy answered 2/2, 2013 at 19:12 Comment(0)
C
10

The problem is not ObjectAnimator, but rather that your application is simply doing too much on every frame of the animation. Specifically, you are animating layout params and requesting layout on every frame. Layout is powerful and useful... but can be quite expensive in any but the most trivial of view hierarchies. It's important to avoid expensive operations per-frame during animations, and layout falls into that "expensive" category. Sliding things around is fine (translationX/Y), fading things in/out is good (alpha), but actually laying things out on every frame? Just say no.

Cordelier answered 8/2, 2013 at 19:33 Comment(3)
I presume that by "actually laying things out on every frame", you are referring to changing the width of the middle pane via the ObjectAnimator. AFAICT, the Email/Gmail do something along those lines. Am I misinterpreting what I am seeing when using those apps? Or is there a way to resize in a way that is more animation-friendly (beyond just waiting until the end)? Or are those apps also janky? The objective here is to create a reusable pattern for implementing the Email/Gmail three-pane UI animated effect. Thanks!Cystocarp
Well, systrace is not showing performTraversals() calls when I do the animation in Gmail. I am going to assume that my eyes are deceiving me and that Gmail is not resizing the list-of-conversations pane during its animation.Cystocarp
If they actually resized the container and caused text to reflow on every frame, they'd suffer the same stuttery animation problems (or more, given the complexity of a typical Gmail message UI). I don't know off-hand what they're doing, but I assume it's just a sliding animation, possibly laying out the container to the end-size prior to the animation and then simply translating it into place.Cordelier
S
0

I ended up removing the ObjectAnimator completely... It still has the slide animation, of course, but not the smooth re-dimensioning at the same time. Not that it was actually smooth...

Anyway, if somebody does come up with an actual solution to this problem, feel free to share. Thanks.

package com.anfuddle.view.widget;

import android.annotation.TargetApi;
import android.content.Context;
import android.os.Build;
import android.util.AttributeSet;
import android.view.View;
import android.view.ViewPropertyAnimator;
import android.widget.LinearLayout;


public class ThreePaneLayout extends LinearLayout
{
    private View leftView = null;
    private View middleView = null;
    private View rightView = null;

private static final int ANIM_DURATION = 500;
private int rootWidth = -1;
private int leftPaneWidth = -1;
private int rightPaneWidth = -1;


// -------------------------------------------------------------------------------------------
// --------------   Constructor
// -------------------------------------------------------------------------------------------


public ThreePaneLayout(Context context, AttributeSet attrs)
{
    super(context, attrs);
    setOrientation(HORIZONTAL);
}

@Override
public void onFinishInflate()
{
    super.onFinishInflate();

    leftView = getChildAt(0);
    middleView = getChildAt(1);
    rightView = getChildAt(2);
}


// -------------------------------------------------------------------------------------------
// --------------   Public methods
// -------------------------------------------------------------------------------------------


public View getLeftView()
{
    return leftView;
}

public View getMiddleView()
{
    return middleView;
}

public View getRightView()
{
    return rightView;
}

@TargetApi(Build.VERSION_CODES.HONEYCOMB)
public void hideLeftAndMiddle()
{
    if (leftPaneWidth == -1)
    {
        rootWidth = getWidth();
        leftPaneWidth = leftView.getWidth();
        rightPaneWidth = middleView.getWidth();
    }
    resetWidget(leftView, leftPaneWidth);
    resetWidget(middleView, rightPaneWidth);
    resetWidget(rightView, rootWidth);
    requestLayout();

    translateWidgets(-1 * rootWidth, leftView, middleView, rightView);
}

@TargetApi(Build.VERSION_CODES.HONEYCOMB)
public void hideLeft()
{
    if (leftPaneWidth == -1)
    {
        leftPaneWidth = leftView.getWidth();
        rightPaneWidth = middleView.getWidth();
    }
    resetWidget(leftView, leftPaneWidth);
    resetWidget(middleView, leftPaneWidth);
    resetWidget(rightView, rightPaneWidth);
    requestLayout();
    translateWidgets(-1 * leftPaneWidth, leftView, middleView, rightView);
}

@TargetApi(Build.VERSION_CODES.HONEYCOMB)
public void showLeftAndMiddle()
{
    translateWidgets(rootWidth, leftView, middleView, rightView);
}

@TargetApi(Build.VERSION_CODES.HONEYCOMB)
public void showLeft()
{
    resetWidget(leftView, leftPaneWidth);
    resetWidget(middleView, rightPaneWidth);
    resetWidget(rightView, rightPaneWidth);
    requestLayout();
    translateWidgets(leftPaneWidth, leftView, middleView, rightView);
}


// -------------------------------------------------------------------------------------------
// --------------   Private methods
// -------------------------------------------------------------------------------------------


@TargetApi(12)
private void translateWidgets(int deltaX, View... views)
{
    for (final View view : views)
    {
        ViewPropertyAnimator viewPropertyAnimator = view.animate();
        viewPropertyAnimator.translationXBy(deltaX)
                            .setDuration(ANIM_DURATION);
    }
  }

  private void resetWidget(View view, int width)
  {
      LinearLayout.LayoutParams layoutParams = (LinearLayout.LayoutParams)view.getLayoutParams();  
      layoutParams.width = width;
      layoutParams.weight = 0;
  }
}
Stagy answered 4/2, 2013 at 11:48 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.