并行动画视图

Animating views parallely

我正在设计一个 login/signup 界面。思路是这样的:

XML

我有两个 EditText 视图:一个用于用户名,另一个用于密码,如下所示:

[用户名]
[密码]

[登录按钮]

两个元素的XML:

<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"/>

隐藏的EditText

我有另一个EditText,它的代码和上面的一模一样,只是多了一个属性:

android:visibility="gone"

那是因为,当用户想要注册时,"gone" 字段会出现。 到目前为止,布局将是这样的:

(用户名)
(密码)
(隐藏重复密码)

(登录按钮)

问题

真正的事情是我有一个 TextView 提示用户注册。当他触摸那个文本时,我希望两个 EditText(用户名和密码)向顶部平移几个像素,以便较新的 EditText(重复密码字段)可以占据密码字段。

两个 EditText 必须平移的距离正好是 edittext 的高度。换句话说,重复密码字段必须完全密码字段的相同位置, 以及 重复密码字段 .

的正上方

这是我的 View.OnclickListener 所做的:

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);

我还对重复密码字段应用了 alpha 效果。

一切都搞砸了

因为当我点击SignUp时,事情是这样的:

(用户名)
(密码)
烦人SPACE
(重复密码)

(注册按钮)

烦人的space正是Password应该在的地方。我的意思是,密码翻译 *2 * height*,当我需要它翻译 height (因为重复密码在原始 space 中,密码在触摸 textview 之前)

还有一件重要的事情

一切都在一个独特的线性布局中

谁能帮我解决这个问题?我究竟做错了什么?我尝试了很多方法,但总是以相同的情况结束,密码字段和重复密码字段之间令人讨厌的 space。

我想我没有忘记任何细节。如果您需要更多信息,请告诉我。 非常感谢!!

首先我想解释一下为什么你没有得到预期的结果。

当您翻译视图时,实际上并没有更改布局。这意味着当您显示 "Repeat Password" 字段时,该字段应该在显示它的位置,因为就布局而言,其他两个视图根本没有移动。使用平移/缩放或其他动画时布局不更新的原因是因为在每个动画帧上重新计算布局会减慢动画速度。

解决方案

我的解决方案可能不完全符合您的要求,因为它假定您可以在表单上方添加一些空白 space。 tihs 的原因是因为您特别要求显示 "Repeat Password" 字段而不是 "Password" 字段,将其向上推,而不是将其显示在字段下方。

创建一个名为 RevealInPlaceAnimation

的新 class

此 class 处理实际动画,使 ActivityFragment 中的代码更清晰。

package net.njensen.Whosebug;

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);
            }
        };
    }

}

现在您需要像这样设置布局:

  1. 将 "Repeat Password" 字段上方的表单元素包装在自己的 LinearLayout 中。
  2. 将新布局(第 1 步)和 "Repeat Password" 字段包装在 RelativeLayout 中,并在 "Repeat Password" 字段上设置以下属性。

    • android:layout_alignBottom="@+id/of_the_new_layout"
    • android:visibility="invisible"
    • android:alpha="0.0"
  3. 您需要在 RelativeLayout 上设置 android:layout_paddingTop,等于或大于 "Repeat Password" 字段的高度 + 它可能具有的任何上边距。

  4. 如果您想使用屏幕居中的表单,您需要将 android:paddingBottom 设置为与您在 RelativeLayout 上设置的相同的值,以使填充均匀。
    • 听起来您正在使用顶部对齐的 LinearLayout,在这种情况下您不应该设置它。
  5. 最后,在 RelativeLayout 上设置 android:clipToPadding="false"(第 2 步)。

现在您的布局应如下所示:

<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>

唯一剩下要做的就是触发动画。这是一个例子:

package net.njensen.Whosebug;

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();
    }
}

改进

如果您想改进解决方案,这里有一个建议:

  1. 在绘制视图时动态地在包装器 RelativeLayout 上设置 android:paddingTop,以确保正确的填充。