Search code examples
javaandroidxmlandroid-animationobjectanimator

Animating views parallely


I'm designing a login/signup interface. The idea is like this:

The XML

I've got two EditText views: one for username, the other for password, like this:

[USERNAME]
[PASSWORD]

[SIGN IN BUTTON]

The XML for both elements:

<EditText
android:layout_width="270dp"
android:layout_height="@dimen/ui_element_height"
android:id="@+id/usernameField"
android:background="@drawable/field_start_screen"
android:layout_gravity="center_horizontal"
android:paddingLeft="@dimen/field_padding_left"
android:drawableLeft="@drawable/ic_user"
android:hint="@string/hint_username"/>

The hidden EditText

I've got another EditText, whose code is exactly the same as the one above, but with another property:

android:visibility="gone"

That's because, when the user wants to sign up, that "gone" field will appear. Up to now, the layout would be something like this:

(USERNAME)
(PASSWORD)
(HIDDEN REPEAT PASSWORD)

(SIGN IN BUTTON)

The problem

The real thing here is that I've got a TextView prompting the user to sign up. When he touches that text, I want the two EditText (Username and password), to translate a few pixels to the top, so the newer EditText (The repeat password field) can occupy the Password field.

The distance both EditText have to translate is exactly the height of the edittext. In other words, the repeat password field has to be EXACTLY in the same place the password field was, and right above the repeat password field.

Here's what my View.OnclickListener does:

ObjectAnimator animationUsernameField = ObjectAnimator.ofFloat(usernameField, "TranslationY", 0, -usernameField.getHeight());  
ObjectAnimator animationPasswordField = ObjectAnimator.ofFloat(passwordField, "TranslationY", 0, -passwordField.getHeight());  
ObjectAnimator animationRepeatPassword = ObjectAnimator.ofFloat(repeatPasswordField, "Alpha", 0.0f, 1.0f);

AnimatorSet animatorSet = new AnimatorSet();
animatorSet.playTogether(
        animationUsernameField,
        animationPasswordField,
        animationRepeatPassword
);
animatorSet.setDuration(1000);
animatorSet.start();
repeatPasswordField.setVisibility(View.VISIBLE);

I'm also applying an alpha effect to the repeat password field.

And everything is messed up

Because when I touch the SignUp, the thing is like this:

(USERNAME)
(PASSWORD)
ANNOYING SPACE
(REPEAT PASSWORD)

(SIGN UP BUTTON)

The annoying space is exactly the place where Password should be. I mean, password is translated *2 * height*, when I need it translated height (because repeat password is in the original space where password was before touching the textview)

Another important thing

Everything is in a unique LinearLayout

Can anyone help me with this problem? What am I doing wrong? I tried a lot of things but always end up in the same situation, with that annoying space between the Password field and the repeat password field.

I think I didn't forget any details. If you need more info, please tell me. Thank you a lot!!


Solution

  • First of all I'd like to explain why you're not getting the expected result.

    When you translate a view, you're not actually changing the layout. This means that when you show the "Repeat Password" field, that field is supposed to be where it's being displayed, because as far as the layout is concerned, the two other views hasn't moved at all. The reason the layout does not update when using a translation / scale or other animation, is because recalculating the layout on every animation frame, would slow down the animation.

    Solution

    My solution might not be exactly what you're looking for, as it does assume that you have the possibility of adding a bit of empty space above your form. The reason for tihs is because you specifically require that the "Repeat Password" field is reveal in place of the "Password" field, pushing it upwards, rather than revealing it below the field.

    Create a new class called RevealInPlaceAnimation

    This class handles the actual animation, making the code in your Activity or Fragment cleaner.

    package net.njensen.stackoverflow;
    
    import android.animation.Animator;
    import android.animation.AnimatorListenerAdapter;
    import android.animation.AnimatorSet;
    import android.animation.ObjectAnimator;
    import android.animation.TimeInterpolator;
    import android.animation.ValueAnimator;
    import android.view.View;
    import android.view.ViewGroup;
    
    /**
     * Created by Nicklas Jensen on 03/05/15.
     */
    public class RevealInPlaceAnimation {
    
        private final View mContainerView;
        private final View mRevealView;
        private final AnimatorSet mAnimations;
    
        public RevealInPlaceAnimation(View containerView, View revealView) {
            mContainerView = containerView;
            mRevealView = revealView;
    
            mAnimations = new AnimatorSet();
            mAnimations.addListener(getAnimationListener());
        }
    
        public void setDuration(long duration) {
            mAnimations.setDuration(duration);
        }
    
        public void setInterpolator(TimeInterpolator interpolator) {
            mAnimations.setInterpolator(interpolator);
        }
    
        public void addListener(Animator.AnimatorListener listener) {
            mAnimations.addListener(listener);
        }
    
        public void removeListener(Animator.AnimatorListener listener) {
            mAnimations.removeListener(listener);
        }
    
        public void start() {
            mAnimations.playTogether(getContainerViewAnimation(), getRevealViewAnimation());
            mAnimations.start();
        }
    
        private float getExpectedRevealViewHeight() {
            return mRevealView.getHeight();
        }
    
        private float getRevealViewMarginTop() {
            ViewGroup.MarginLayoutParams params = (ViewGroup.MarginLayoutParams) mRevealView.getLayoutParams();
            return params.topMargin;
        }
    
        private float getRevealViewOffset() {
            return getExpectedRevealViewHeight() + getRevealViewMarginTop();
        }
    
        private ValueAnimator getContainerViewAnimation() {
            float translationY = -getRevealViewOffset();
            return ObjectAnimator.ofFloat(mContainerView, "translationY", 0.0f, translationY);
        }
    
        private ValueAnimator getRevealViewAnimation() {
            return ObjectAnimator.ofFloat(mRevealView, "alpha", 0.0f, 1.0f);
        }
    
        private Animator.AnimatorListener getAnimationListener() {
            return new AnimatorListenerAdapter() {
                @Override
                public void onAnimationStart(Animator animation) {
                    super.onAnimationStart(animation);
                    mRevealView.setVisibility(View.VISIBLE);
                }
            };
        }
    
    }
    

    Now you'll need to setup your layout like so:

    1. Wrap the form elements above the "Repeat Password" field in a LinearLayout of their own.
    2. Wrap the new layout (step 1) and the "Repeat Password" field in a RelativeLayout, and set the below attributes on the "Repeat Password" field.

      • android:layout_alignBottom="@+id/of_the_new_layout"
      • android:visibility="invisible"
      • android:alpha="0.0"
    3. You will need to set android:layout_paddingTop on the RelativeLayout, equal or greater than the height of the "Repeat Password" field + any top margin it might have.

    4. If you want to use a form that's centered on the screen, you will want to set android:paddingBottom equal to the same value you've set on the RelativeLayout, to even out the padding.
      • It sounds like you're using a top-aligned LinearLayout, in which case you shouldn't set it.
    5. Finally, set android:clipToPadding="false" on the RelativeLayout (step 2).

    Now your layout should look something like this:

    <ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:tools="http://schemas.android.com/tools"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:paddingLeft="@dimen/activity_horizontal_margin"
        android:paddingRight="@dimen/activity_horizontal_margin"
        android:paddingTop="@dimen/activity_vertical_margin"
        android:paddingBottom="@dimen/activity_vertical_margin"
        android:orientation="vertical"
        tools:context=".MainActivityFragment">
    
        <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:paddingBottom="48dp"
            android:orientation="vertical">
    
            <RelativeLayout
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:paddingTop="48dp"
                android:clipToPadding="false">
    
                <LinearLayout
                    android:id="@+id/username_password_container"
                    android:layout_width="match_parent"
                    android:layout_height="wrap_content"
                    android:orientation="vertical">
    
                    <EditText
                        android:id="@+id/et_username"
                        android:layout_width="match_parent"
                        android:layout_height="wrap_content"
                        android:hint="Username" />
    
                    <EditText
                        android:id="@+id/et_password"
                        android:layout_width="match_parent"
                        android:layout_height="wrap_content"
                        android:inputType="textPassword"
                        android:layout_marginTop="8dp"
                        android:hint="Password"/>
    
                </LinearLayout>
    
                <EditText
                    android:id="@+id/et_repeat_password"
                    android:layout_width="match_parent"
                    android:layout_height="wrap_content"
                    android:layout_alignBottom="@+id/username_password_container"
                    android:layout_marginTop="8dp"
                    android:inputType="textPassword"
                    android:hint="Repeat Password"
                    android:visibility="invisible"
                    android:alpha="0.0"/>
    
            </RelativeLayout>
    
            <Button
                android:id="@+id/btn_register"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:layout_marginTop="8dp"
                android:text="Register"/>
    
        </LinearLayout>
    
    </ScrollView>
    

    The only thing left to do is trigger the animation. Here's an example:

    package net.njensen.stackoverflow;
    
    import android.content.res.Resources;
    import android.os.Bundle;
    import android.support.v4.app.Fragment;
    import android.util.TypedValue;
    import android.view.LayoutInflater;
    import android.view.View;
    import android.view.ViewGroup;
    import android.view.animation.DecelerateInterpolator;
    import android.widget.Button;
    import android.widget.EditText;
    
    
    /**
     * Created by Nicklas Jensen on 03/05/15.
     */
    public class MainActivityFragment extends Fragment {
    
        private EditText mEtUsername;
        private EditText mEtPassword;
        private EditText mEtRepeatPassword;
        private Button mBtnRegister;
        private View mUsernamePasswordContainer;
    
        private final View.OnClickListener mDisplayRepeatPasswordListener = new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                if (!isRepeatPasswordVisible()) {
                    showRepeatPasswordAnimated();
                }
            }
        };
    
        @Override
        public View onCreateView(LayoutInflater inflater, ViewGroup container,
                                 Bundle savedInstanceState) {
            return inflater.inflate(R.layout.fragment_main, container, false);
        }
    
        @Override
        public void onViewCreated(View view, Bundle savedInstanceState) {
            super.onViewCreated(view, savedInstanceState);
    
            mEtUsername = (EditText) view.findViewById(R.id.et_username);
            mEtPassword = (EditText) view.findViewById(R.id.et_password);
            mEtRepeatPassword = (EditText) view.findViewById(R.id.et_repeat_password);
            mBtnRegister = (Button) view.findViewById(R.id.btn_register);
            mUsernamePasswordContainer = view.findViewById(R.id.username_password_container);
    
            mBtnRegister.setOnClickListener(mDisplayRepeatPasswordListener);
        }
    
        private boolean isRepeatPasswordVisible() {
            return mEtRepeatPassword.getVisibility() == View.VISIBLE;
        }
    
        private int getRepeatPasswordAnimationDuration() {
            return getResources().getInteger(android.R.integer.config_mediumAnimTime);
        }
    
        private void showRepeatPasswordAnimated() {
            RevealInPlaceAnimation animation = new RevealInPlaceAnimation(mUsernamePasswordContainer, mEtRepeatPassword);
            animation.setInterpolator(new DecelerateInterpolator());
            animation.setDuration(getRepeatPasswordAnimationDuration());
            animation.start();
        }
    }
    

    Improvements

    If you want to improve upon the solution, here's a suggestion:

    1. Set android:paddingTop on the wrapper RelativeLayout dynamically when the view is drawn, to ensure the correct padding.