假设我有一个垂直线性布局:

[v1]
[v2]

默认情况下,v1已经可见= GONE。我想用一个展开动画显示v1,同时下推v2。

我是这样做的:

Animation a = new Animation()
{
    int initialHeight;

    @Override
    protected void applyTransformation(float interpolatedTime, Transformation t) {
        final int newHeight = (int)(initialHeight * interpolatedTime);
        v.getLayoutParams().height = newHeight;
        v.requestLayout();
    }

    @Override
    public void initialize(int width, int height, int parentWidth, int parentHeight) {
        super.initialize(width, height, parentWidth, parentHeight);
        initialHeight = height;
    }

    @Override
    public boolean willChangeBounds() {
        return true;
    }
};

但是用这个解决方案,当动画开始时我有一个眨眼。我认为这是由于v1在应用动画之前显示全尺寸。

用javascript,这是一行jQuery!android有什么简单的方法吗?


当前回答

我看到这个问题很受欢迎,所以我发布了我的实际解决方案。主要的优点是你不需要知道扩展的高度来应用动画,一旦视图被扩展,它会在内容改变时适应高度。这对我来说很有效。

public static void expand(final View v) {
    int matchParentMeasureSpec = View.MeasureSpec.makeMeasureSpec(((View) v.getParent()).getWidth(), View.MeasureSpec.EXACTLY);
    int wrapContentMeasureSpec = View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED);
    v.measure(matchParentMeasureSpec, wrapContentMeasureSpec);
    final int targetHeight = v.getMeasuredHeight();
    
    // Older versions of android (pre API 21) cancel animations for views with a height of 0.
    v.getLayoutParams().height = 1;
    v.setVisibility(View.VISIBLE);
    Animation a = new Animation()
    {
        @Override
        protected void applyTransformation(float interpolatedTime, Transformation t) {
            v.getLayoutParams().height = interpolatedTime == 1
                    ? LayoutParams.WRAP_CONTENT
                    : (int)(targetHeight * interpolatedTime);
            v.requestLayout();
        }

        @Override
        public boolean willChangeBounds() {
            return true;
        }
    };
    
    // Expansion speed of 1dp/ms
    a.setDuration((int)(targetHeight / v.getContext().getResources().getDisplayMetrics().density));
    v.startAnimation(a);
}

public static void collapse(final View v) {
    final int initialHeight = v.getMeasuredHeight();

    Animation a = new Animation()
    {
        @Override
        protected void applyTransformation(float interpolatedTime, Transformation t) {
            if(interpolatedTime == 1){
                v.setVisibility(View.GONE);
            }else{
                v.getLayoutParams().height = initialHeight - (int)(initialHeight * interpolatedTime);
                v.requestLayout();
            }
        }

        @Override
        public boolean willChangeBounds() {
            return true;
        }
    };
    
    // Collapse speed of 1dp/ms
    a.setDuration((int)(initialHeight / v.getContext().getResources().getDisplayMetrics().density));
    v.startAnimation(a);
}

正如@Jefferson在评论中提到的,你可以通过改变动画的持续时间(以及速度)来获得更流畅的动画。目前,它已设置为1dp/ms的速度

其他回答

我今天也遇到了同样的问题,我想这个问题的真正解决方案是这样的

<LinearLayout android:id="@+id/container"
android:animateLayoutChanges="true"
...
 />

您必须为所有涉及移位的最顶层布局设置此属性。如果你现在将一个布局的可见性设置为GONE,另一个将在消失的布局释放它时占据空间。会有一个默认的动画,这是某种“淡出”,但我认为你可以改变这个-但最后一个我没有测试,现在。


如果在RecyclerView项目中使用这个,在onBindViewHolder中设置视图的可见性来展开/折叠,并调用notifyItemChanged(position)来触发转换。

override fun onBindViewHolder(holder: ItemViewHolder, position: Int) {
        ...

        holder.list.visibility = data[position].listVisibility
        holder.expandCollapse.setOnClickListener {
            data[position].listVisibility = if (data[position].listVisibility == View.GONE) View.VISIBLE else View.GONE
            notifyItemChanged(position)
        }
    }

如果你在onBindViewHolder中执行昂贵的操作,你可以使用notifyItemChanged(位置,有效载荷)优化部分更改

private const val UPDATE_LIST_VISIBILITY = 1

    override fun onBindViewHolder(holder: ItemViewHolder, position: Int, payloads: MutableList<Any>) {
        if (payloads.contains(UPDATE_LIST_VISIBILITY)) {
            holder.list.visibility = data[position].listVisibility
        } else {
            onBindViewHolder(holder, position)
        }
    }

    override fun onBindViewHolder(holder: ItemViewHolder, position: Int) {
        ...

        holder.list.visibility = data[position].listVisibility
        holder.expandCollapse.setOnClickListener {
            data[position].listVisibility = if (data[position].listVisibility == View.GONE) View.VISIBLE else View.GONE
            notifyItemChanged(position, UPDATE_LIST_VISIBILITY)
        }
    }

加上Tom Esterez的精彩回答和Erik B的精彩更新,我想我应该发布我自己的观点,将扩展和收缩方法压缩为一个。通过这种方式,你可以有这样一个动作……

button.setOnClickListener(v -> expandCollapse(view));

... 它调用下面的方法,并让它在每次onClick()之后弄清楚要做什么…

public static void expandCollapse(View view) {

    boolean expand = view.getVisibility() == View.GONE;
    Interpolator easeInOutQuart = PathInterpolatorCompat.create(0.77f, 0f, 0.175f, 1f);

    view.measure(
        View.MeasureSpec.makeMeasureSpec(((View) view.getParent()).getWidth(), View.MeasureSpec.EXACTLY),
        View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED)
    );

    int height = view.getMeasuredHeight();
    int duration = (int) (height/view.getContext().getResources().getDisplayMetrics().density);

    Animation animation = new Animation() {
        @Override protected void applyTransformation(float interpolatedTime, Transformation t) {
            if (expand) {
                view.getLayoutParams().height = 1;
                view.setVisibility(View.VISIBLE);
                if (interpolatedTime == 1) {
                    view.getLayoutParams().height = ViewGroup.LayoutParams.WRAP_CONTENT;
                } else {
                    view.getLayoutParams().height = (int) (height * interpolatedTime);
                }
                view.requestLayout();
            } else {
                if (interpolatedTime == 1) {
                    view.setVisibility(View.GONE);
                } else {
                    view.getLayoutParams().height = height - (int) (height * interpolatedTime);
                    view.requestLayout();
                }
            }
        }
        @Override public boolean willChangeBounds() {
            return true;
        }
    };

    animation.setInterpolator(easeInOutQuart);
    animation.setDuration(duration);
    view.startAnimation(animation);

}

我试图做一个我认为非常相似的动画,并找到了一个优雅的解决方案。这段代码假设你总是从0->h或h->0 (h是最大高度)。三个构造函数参数是view =要动画的视图(在我的例子中是webview), targetHeight =视图的最大高度,以及down =一个布尔值,用于指定方向(true =展开,false =折叠)。

public class DropDownAnim extends Animation {
    private final int targetHeight;
    private final View view;
    private final boolean down;

    public DropDownAnim(View view, int targetHeight, boolean down) {
        this.view = view;
        this.targetHeight = targetHeight;
        this.down = down;
    }

    @Override
    protected void applyTransformation(float interpolatedTime, Transformation t) {
        int newHeight;
        if (down) {
            newHeight = (int) (targetHeight * interpolatedTime);
        } else {
            newHeight = (int) (targetHeight * (1 - interpolatedTime));
        }
        view.getLayoutParams().height = newHeight;
        view.requestLayout();
    }

    @Override
    public void initialize(int width, int height, int parentWidth,
            int parentHeight) {
        super.initialize(width, height, parentWidth, parentHeight);
    }

    @Override
    public boolean willChangeBounds() {
        return true;
    }
}

如果你不想扩展或折叠所有的方式-这里是一个简单的HeightAnimation -

import android.view.View;
import android.view.animation.Animation;
import android.view.animation.Transformation;

public class HeightAnimation extends Animation {
    protected final int originalHeight;
    protected final View view;
    protected float perValue;

    public HeightAnimation(View view, int fromHeight, int toHeight) {
        this.view = view;
        this.originalHeight = fromHeight;
        this.perValue = (toHeight - fromHeight);
    }

    @Override
    protected void applyTransformation(float interpolatedTime, Transformation t) {
        view.getLayoutParams().height = (int) (originalHeight + perValue * interpolatedTime);
        view.requestLayout();
    }

    @Override
    public boolean willChangeBounds() {
        return true;
    }
}

用法:

HeightAnimation heightAnim = new HeightAnimation(view, view.getHeight(), viewPager.getHeight() - otherView.getHeight());
heightAnim.setDuration(1000);
view.startAnimation(heightAnim);

利用Kotlin扩展函数这是测试和最短的答案

在任何视图上调用animateVisibility(expand/collapse)即可。

fun View.animateVisibility(setVisible: Boolean) {
    if (setVisible) expand(this) else collapse(this)
}

private fun expand(view: View) {
    view.measure(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT)
    val initialHeight = 0
    val targetHeight = view.measuredHeight

    // Older versions of Android (pre API 21) cancel animations for views with a height of 0.
    //v.getLayoutParams().height = 1;
    view.layoutParams.height = 0
    view.visibility = View.VISIBLE

    animateView(view, initialHeight, targetHeight)
}

private fun collapse(view: View) {
    val initialHeight = view.measuredHeight
    val targetHeight = 0

    animateView(view, initialHeight, targetHeight)
}

private fun animateView(v: View, initialHeight: Int, targetHeight: Int) {
    val valueAnimator = ValueAnimator.ofInt(initialHeight, targetHeight)
    valueAnimator.addUpdateListener { animation ->
        v.layoutParams.height = animation.animatedValue as Int
        v.requestLayout()
    }
    valueAnimator.addListener(object : Animator.AnimatorListener {
        override fun onAnimationEnd(animation: Animator) {
            v.layoutParams.height = targetHeight
        }

        override fun onAnimationStart(animation: Animator) {}
        override fun onAnimationCancel(animation: Animator) {}
        override fun onAnimationRepeat(animation: Animator) {}
    })
    valueAnimator.duration = 300
    valueAnimator.interpolator = DecelerateInterpolator()
    valueAnimator.start()
}