0

I made a custom View that is used to show feedback in the UI (usually in response to an action being taken). When FeedbackView.showText is called, it will animate the View in for 2 seconds, and then animate it out. This is done using translationY.

If I apply a negative margin to it that is greater than its height the first time FeedbackView.showText is called it doesn't animate in correctly; it just appears (or in some cases doesn't display at all). Subsequent calls to FeedbackView.showText cause the correct animation.

In activity_main.xml below, the FeedbackView has a margin top of -36dp, which is greater than its height (when non-negated). If the margin top is changed to -35dp it animates correctly even the first time FeedbackView.showText is called.

Does anyone know why something like this would happen?

Romain Guy has said that it is OK to use negative margins on LinearLayout and RelativeLayout. My only guess is that they are not OK with FrameLayouts.

FeedbackView.java

public class FeedbackView extends FrameLayout {
  public static final int DEFAULT_SHOW_DURATION = 2000;

  private AtomicBoolean showing = new AtomicBoolean(false);
  private AtomicBoolean animating = new AtomicBoolean(false);

  private float heightOffset;

  private Runnable animateOutRunnable = new Runnable() {
    @Override
    public void run() {
      animateContainerOut();
    }
  };

  public FeedbackView(Context context) {
    super(context);
    init();
  }

  public FeedbackView(Context context, AttributeSet attrs) {
    super(context, attrs);
    init();
  }

  public FeedbackView(Context context, AttributeSet attrs, int defStyleAttr) {
    super(context, attrs, defStyleAttr);
    init();
  }

  @TargetApi(Build.VERSION_CODES.LOLLIPOP)
  public FeedbackView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
    super(context, attrs, defStyleAttr, defStyleRes);
    init();
  }

  private void init() {
    setAlpha(0);
  }

  public boolean isShowing() {
    return showing.get();
  }

  public void showText(Context context, String text) {
    removeCallbacks(animateOutRunnable);

    heightOffset = getMeasuredHeight();

    removeAllViews();

    final TextView tv = new TextView(context);
    tv.setGravity(Gravity.CENTER);
    tv.setTextColor(Color.WHITE);
    tv.setTextSize(TypedValue.COMPLEX_UNIT_SP, 14);
    tv.setText(text);

    addView(tv);

    if(!showing.getAndSet(true)) {
      animateContainerIn();
    }
    else {
      tv.setTranslationY(-getHeight());
      tv.animate().translationY(0).start();
    }

    postDelayed(animateOutRunnable, DEFAULT_SHOW_DURATION);
  }

  private void animateContainerIn() {
    if(animating.getAndSet(true)) {
      animate().cancel();
    }

    ViewPropertyAnimator animator = animate();
    long startDelay = animator.getDuration() / 2;

    animate()
        .alpha(1)
        .setStartDelay(startDelay)
        .start();

    animate()
        .translationY(heightOffset)
        .setStartDelay(0)
        .withEndAction(new Runnable() {
          @Override
          public void run() {
            animating.set(false);
            showing.set(true);
          }
        })
        .start();
  }

  private void animateContainerOut() {
    showing.set(false);

    if(animating.getAndSet(true)) {
      animate().cancel();
    }

    ViewPropertyAnimator animator = animate();
    long duration = animator.getDuration();

    animate()
        .alpha(0)
        .setDuration(duration / 2)
        .start();

    animate()
        .translationY(-heightOffset)
        .setDuration(duration)
        .withEndAction(new Runnable() {
          @Override
          public void run() {
            animating.set(false);
          }
        })
        .start();
  }
}

MainActivity.java

public class MainActivity extends Activity {
  private FeedbackView feedbackView;

  @Override
  protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);

    feedbackView = (FeedbackView) findViewById(R.id.feedback);

    findViewById(R.id.show_feedback).setOnClickListener(new View.OnClickListener() {
      @Override
      public void onClick(View v) {
        feedbackView.showText(MainActivity.this, "Feedback");
      }
    });
  }
}

activity_main.xml

<LinearLayout
  xmlns:android="http://schemas.android.com/apk/res/android"
  android:layout_width="match_parent"
  android:layout_height="match_parent"
  android:orientation="vertical">

  <FrameLayout
    android:layout_width="match_parent"
    android:layout_height="70dp"
    android:background="#000"
    android:clickable="true"/>

  <FrameLayout
    android:layout_width="match_parent"
    android:layout_height="wrap_content">

    <FrameLayout
      android:layout_width="match_parent"
      android:layout_height="90dp"/>

    <FrameLayout
      android:layout_width="match_parent"
      android:layout_height="match_parent"
      android:clickable="true"
      android:background="#e9e9e9"/>

    <negative.margin.FeedbackView
      android:id="@+id/feedback"
      android:layout_width="match_parent"
      android:layout_height="35dp"
      android:layout_marginTop="-36dp"
      android:background="#20ACE0"/>

  </FrameLayout>

  <Button
    android:id="@+id/show_feedback"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:layout_gravity="bottom|center"
    android:text="Show Feedback"/>

</LinearLayout>
Community
  • 1
  • 1
Eliezer
  • 7,209
  • 12
  • 56
  • 103

1 Answers1

0

My guess would be that the negative margin is not the direct cause of your animation failing.

You would probably achieve the same (undesired) effect - animation not being performed - if you set for example: layout_marginLeft to the value equal to the Activity width (so a positive value).

The problem is that your View is completely "outside" of the visible area therefore when your Activity is being created, the View is not being rendered right away. And running an animation on a View that has not been rendered yet, will not be performed.

More information (for example) here.

What you can do to fix it is:

  • Rebuild your layout in the way that your View is within the rendered area (so basically is within the visible area), but its visibility is set to View.INVISIBLE. At the start of the animation (use an AnimationListener or AnimatorListener or something ;) ) set its visibility to View.VISIBLE.

  • Rebuild your animation so it does not use ViewPropertyAnimator (the animate() method call), but an Animation Object. And start it on a different View (on one that you are sure has already been rendered) - for example on the View's ViewParent (which you can get with getParent()).

  • You can try (my guts tell me that should work, but you would need to test it) to set your layouts clipChildren and clipToPadding to false, forcing your views to be rendered even when outside of the visibile area. If you try that solution (and I think you should, because you won't have to change that much - just add android:clipChildren="false",android:clipToPadding="false" to all of your layouts in this Activity) please tell me if it worked.

Community
  • 1
  • 1
Bartek Lipinski
  • 30,698
  • 10
  • 94
  • 132