RecyclerView使用LayoutTransition实现布局过渡动画

时间:2024-03-15 21:10:51

一、目标

RecyclerView使用LayoutTransition实现布局过渡动画
上图为普通状态和编辑状态的界面图。

普通状态切换到编辑状态,需要切换删除按钮和拖拽按钮的可见性,并实现切换过程的动画效果。

二、体验地址

神马笔记最新版本:【神马笔记 版本1.3.0.apk

三、功能设计

如果不设置动画效果,整个界面的切换过程将会非常生硬。

因此,我们添加如下的过渡动画。

  • 切换到编辑状态
    • 删除按钮——从左侧淡入(translationX + alpha)
    • 拖拽按钮——从右侧进入(translationX)
  • 切换到普通状态
    • 删除按钮——从左侧淡出(translationX + alpha)
    • 拖拽按钮——从右侧退出(translationX)

四、准备工作

1. LayoutTransition

ViewGroup使用LayoutTransition即可为布局变化应用动画效果。

  • ViewGroup#setLayoutTransition(LayoutTransition)
  • android:animateLayoutChanges="true"

通过代码或者资源文件方式,都可以启用ViewGroup的布局过渡动画效果。

  • LayoutTransition支持5中情景的动画,但经常使用的只有4种。
常量定义 动画效果
CHANGE_APPEARING 其他控件出现时,自身的变化动画
CHANGE_DISAPPEARING 其他控件消失时,自身的变化动画
APPEARING 自身出现时的动画
DISAPPEARING 自身消失时的动画
CHANGING ViewGroup自身布局发生变化时,而不是子控件发生变化。默认关闭。
private int mTransitionTypes = FLAG_CHANGE_APPEARING | FLAG_CHANGE_DISAPPEARING |
    FLAG_APPEARING | FLAG_DISAPPEARING;
  • LayoutTransition动画的默认值。
动画常量 Delay Interpolator Animator
CHANGE_APPEARING 0 DECEL CHANGE动画
CHANGE_DISAPPEARING 300 DECEL CHANGE动画
APPEARING 300 ACCEL_DECEL ObjectAnimator.ofFloat(null, “alpha”, 0f, 1f)
DISAPPEARING 0 ACCEL_DECEL ObjectAnimator.ofFloat(null, “alpha”, 1f, 0f)
CHANGING 0 DECEL CHANGE动画

CHANGE动画

PropertyValuesHolder pvhLeft = PropertyValuesHolder.ofInt("left", 0, 1);
PropertyValuesHolder pvhTop = PropertyValuesHolder.ofInt("top", 0, 1);
PropertyValuesHolder pvhRight = PropertyValuesHolder.ofInt("right", 0, 1);
PropertyValuesHolder pvhBottom = PropertyValuesHolder.ofInt("bottom", 0, 1);
PropertyValuesHolder pvhScrollX = PropertyValuesHolder.ofInt("scrollX", 0, 1);
PropertyValuesHolder pvhScrollY = PropertyValuesHolder.ofInt("scrollY", 0, 1);

ObjectAnimator.ofPropertyValuesHolder((Object)null,
                                      pvhLeft, pvhTop, pvhRight, pvhBottom, 
                                      pvhScrollX, pvhScrollY);

LayoutTransition提供了set/get接口,可以修改动画属性。

  • 注意事项
    1. 控件APPEARING时,默认有300s的延迟,等到CHANGE_APPEARING空出位置后才会开始动画;
    2. 控件CHANGE_DISAPPEARING时,默认有300s的延迟,等到DISAPPEARING释放出位置才会开始动画;
    3. APPEARINGDISAPPEARING的动画效果为淡入淡出,因此决定了以上2个默认值;
    4. DISAPPEARING动画结束后,会恢复动画前的alpha值;
anim.addListener(new AnimatorListenerAdapter() {
            @Override
            public void onAnimationEnd(Animator anim) {
                currentDisappearingAnimations.remove(child);
                child.setAlpha(preAnimAlpha);
                
                // ……
            }
        });
  • 总结

LayoutTransition默认的动画效果已经非常不错,通常不需要进行其他设置。

如果需要自身设置动画,除了设置Animator之外,还需要注意设置StartDelayInterpolatorStagger等参数。

2. TransitionListener

我门设计的2组的动画效果。

  • 切换到编辑状态
    • 从左侧淡入(translationX + alpha)
    • 从右侧进入(translationX)
  • 切换到普通状态
    • 从左侧淡出(translationX + alpha)
    • 从右侧退出(translationX)

显然LayoutTransition默认的APPEARINGDISAPPEARING动画效果,无法满足需求。

另外,2个按钮应用了不同的动画效果,需要分开来设置Animator才能达到效果。

因此,我们需要设置TransitionListener动态设置参数。

public interface TransitionListener {

    public void startTransition(LayoutTransition transition, ViewGroup container,
                                View view, int transitionType);

    public void endTransition(LayoutTransition transition, ViewGroup container,
                              View view, int transitionType);
}

五、组合起来

1. 设置LayoutTransition参数

this.layoutTransition = new LayoutTransition();
layoutTransition.addTransitionListener(this);

layoutTransition.setInterpolator(LayoutTransition.APPEARING,
                            layoutTransition.getInterpolator(LayoutTransition.CHANGE_APPEARING));
layoutTransition.setInterpolator(LayoutTransition.DISAPPEARING,
                                 layoutTransition.getInterpolator(LayoutTransition.CHANGE_DISAPPEARING));

layoutTransition.setStartDelay(LayoutTransition.APPEARING, 0);
layoutTransition.setStartDelay(LayoutTransition.CHANGE_DISAPPEARING, 0);
  • 因为从左右同时出现,将APPEARINGCHANGE_DISAPPEARING的延迟时间均设置为0。
  • 将Interpolator设置为相同值,保持动画一致。

2. 设置初始值

view.addOnLayoutChangeListener((v, left, top, right, bottom, oldLeft, oldTop, oldRight, oldBottom) -> {

    if (!isEdit) {
        removeBtn.setAlpha(0);
        removeBtn.setTranslationX(-iconView.getRight());

        dragBtn.setTranslationX(iconView.getRight());
    }

});

因为removeBtn和dragBtn默认的可见性为GONE,不会被布局。如果使用默认值,动画前后的数值是相同的,不会产生动画效果。

因此,必须要布局完成后,设置初始值。

这里使用iconView来设置初始值,也可以使用其他方式。

3. 设置动画

public void startTransition(LayoutTransition transition, ViewGroup container, View view, int transitionType) {

    if (view == removeBtn) {

        Animator animator = null;
        if (transitionType == LayoutTransition.APPEARING) {
            animator = ObjectAnimator.ofPropertyValuesHolder(view,
                    PropertyValuesHolder.ofFloat(View.ALPHA, 1.f),
                    PropertyValuesHolder.ofFloat(View.TRANSLATION_X, 0.f));

        } else if (transitionType == LayoutTransition.DISAPPEARING) {
            animator = ObjectAnimator.ofPropertyValuesHolder(view,
                    PropertyValuesHolder.ofFloat(View.ALPHA, 0.f),
                    PropertyValuesHolder.ofFloat(View.TRANSLATION_X, -view.getRight()));

        }

        transition.setAnimator(transitionType, animator);
    }

    if (view == dragBtn) {

        ObjectAnimator animator = null;
        if (transitionType == LayoutTransition.APPEARING) {
            animator = ObjectAnimator.ofPropertyValuesHolder(view,
                    PropertyValuesHolder.ofFloat(View.TRANSLATION_X, 0));

        } else if (transitionType == LayoutTransition.DISAPPEARING) {
            animator = ObjectAnimator.ofPropertyValuesHolder(view,
                    PropertyValuesHolder.ofFloat(View.TRANSLATION_X, itemView.getWidth() - view.getLeft()));
        }

        transition.setAnimator(transitionType, animator);
    }
}

因为removeBtn和dragBtn的动画不相同,因此需要在startTransition事件中进行设置。

4. 处理Alpha

public void endTransition(LayoutTransition transition, ViewGroup container, View view, int transitionType) {
    if (view == removeBtn) {
        if (transitionType == LayoutTransition.DISAPPEARING) {
            view.setAlpha(0);
        }
    }

    super.endTransition(transition, container, view, transitionType);
}

由于DISAPPEARING动画结束后,会将alpha设置会默认值。

因此动画结束后,重新将alpha设置为0。

5. 检查动画效果

void animateEdit(boolean value) {

    {
        ViewGroup layout = itemView.findViewById(R.id.swipe_view);
        if (layout.getLayoutTransition() == null) {
            layout.setLayoutTransition(layoutTransition);
        }
    }

    {
        removeBtn.setVisibility(value? View.VISIBLE: View.GONE);
    }

    {
        dragBtn.setVisibility(value? View.VISIBLE: View.GONE);
    }
}

调用animateEdit实现切换编辑时的动画效果。

六、Finally

在动画完成后必须调用RecyclerView.Adapter#notifyDataSetChanged()通知数据发生变化,否则滑动RecyclerView会出现控件错误的情况。

因为RecyclerView对控件进行缓存,所有必须要调用该接口。

~一觉游仙好梦~任它竹冷松寒~