Android布局动画介绍

382 阅读5分钟

Android动画大致可以分为帧动画(FrameAnimation),补间动画(TweenAnimation)和属性动画(Animator),这三类是Android最基础的动画,可以通过这三类动画实现更高级的动画样式,本篇文章就来讲讲Android的过渡动画(TransitionAnimation)

一、LayoutTransition

当我们对一个view进行操作的时候(仅限添加view,移除view或改变view的可见性),希望这个view和其他依赖其布局的view的变化过渡自然,如果单一使用属性动画实现起来相当繁琐,LayoutTransition则可以完美解决这个问题。

LayoutTransition字面翻译是布局的过渡也就是布局动画,这个类可以实现ViewGroup的布局改变时自动执行动画,LayoutTransition 从api11开始提供。给ViewGroup设置动画很简单,只需要生成一个LayoutTransition实例,然后调用ViewGroup的setLayoutTransition(LayoutTransition)函数就可以了。当设置了布局动画的ViewGroup添加或者删除内部view时就会触发动画。如果要设置定制的动画,需要调用setAnimator()方法。

  1. 接入布局动画

在根布局添加android:animateLayoutChanges="true"

<!-- activity_main.xml -->

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:animateLayoutChanges="true" >

</androidx.constraintlayout.widget.ConstraintLayout>

或者在代码中指定,

// MainActivity.kt

class MainActivity : Activity() {

    private val mViewBinding = ActivityMainBinding.inflate(layoutInflater)

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(mViewBinding.root)

        // 设置默认的布局动画
        mViewBinding.root.layoutTransition = LayoutTransition()
    }
}

LayoutTransition定义了5种动画,分别是:

  1. APPEARING: view被添加(可见)到ViewGroup会触发的该view的动画。
  2. CHANGE_APPEARING: view被添加(可见)到ViewGroup,会影响其他View,此时其它View会触发的动画。
  3. DISAPPEARING: view被移除(不可见)ViewGroup会触发的该view的动画。
  4. CHANGE_DISAPPEARING: view被移除(不可见)ViewGroup,会影响其他View,此时其它View会触发的动画。
  5. CHANGING: 非以上四种的其他布局变化触发的动画,比如view边距,大小改变等。(api16加入)

以上类型对应LayoutTransition源码中的这5个动画,

其中前四种动画是默认开启的,CHANGING默认不开启,需要手动开启,调用enableTransitionType开启指定动画类型,

mViewBinding.root.layoutTransition = LayoutTransition().apply {
enableTransitionType(LayoutTransition. CHANGING )
} 

同时LayoutTransition也提供了禁用某个动画的接口disableTransitionType().

  1. 常用接口

方法说明
void enableTransitionType(int transitionType)启用transitionType类型的动画,参数APPEARINGCHANGE_APPEARINGDISAPPEARINGCHANGE_DISAPPEARINGCHANGING之一。
void disableTransitionType(int transitionType)禁用transitionType类型的动画,参数同上。
void setDuration(long duration)设置动画时长。
void setDuration(int transitionType, long duration)设置transitionType类型动画时长。
void setStartDelay(int transitionType, long delay)设置transitionType类型动画延迟执行毫秒数。
void setStagger(int transitionType, long duration)设置transitionType类型动画之间间隔时长毫秒数。参数CHANGE_APPEARINGCHANGE_DISAPPEARINGCHANGING之一。
void setInterpolator(int transitionType, TimeInterpolator interpolator)设置transitionType类型动画插值器。
void setAnimator(int transitionType, Animator animator)自定义transitionType类型的布局动画。
void addTransitionListener(TransitionListener listener)设置布局动画监听。
  1. 自定义布局动画

LayoutTransition提供了自定义特定类型动画的接口setAnimator(int transitionType, Animator animator)。假如我希望View在变得不可见的时候背景色从黑色过渡到白色,动画时长2s;显示的时候从红色过渡到绿色,动画时长1s

mViewBinding.root.layoutTransition = LayoutTransition().apply {
    setAnimator(
        LayoutTransition.APPEARING,
        ObjectAnimator.ofArgb(null, "backgroundColor", Color.RED, Color.GREEN)
    )
    setAnimator(
        LayoutTransition.DISAPPEARING,
        ObjectAnimator.ofArgb(null, "backgroundColor", Color.BLACK, Color.WHITE)
    )
    setDuration(LayoutTransition.DISAPPEARING, 2000)
    setDuration(LayoutTransition.APPEARING, 1000)
} 

二、原理

LayoutTransition只有一个构造方法,在构造方法里创建了默认的五种属性动画,

public LayoutTransition() {
    // 在没有自定义动画的时候,生成默认动画
    if (defaultChangeIn == null) {
        // "left" is just a placeholder; we'll put real properties/values in when needed
        // 定义需要执行动画的属性,以及属性动画数值
        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);
        // 默认进入(显示)动画
        defaultChangeIn = ObjectAnimator.ofPropertyValuesHolder((Object)null,
                pvhLeft, pvhTop, pvhRight, pvhBottom, pvhScrollX, pvhScrollY);
        // DEFAULT_DURATION = 300
        defaultChangeIn.setDuration(DEFAULT_DURATION);
        // mChangingAppearingDelay = 0
        defaultChangeIn.setStartDelay(mChangingAppearingDelay);
        // mChangingAppearingInterpolator  = DecelerateInterpolator()
        defaultChangeIn.setInterpolator(mChangingAppearingInterpolator);
        // 默认退出(消失)动画,由defaultChangeIn复制而来
        defaultChangeOut = defaultChangeIn.clone();
        defaultChangeOut.setStartDelay(mChangingDisappearingDelay);
        defaultChangeOut.setInterpolator(mChangingDisappearingInterpolator);
        defaultChange = defaultChangeIn.clone();
        defaultChange.setStartDelay(mChangingDelay);
        defaultChange.setInterpolator(mChangingInterpolator);
        // 默认淡入动画
        defaultFadeIn = ObjectAnimator.ofFloat(null, "alpha", 0f, 1f);
        defaultFadeIn.setDuration(DEFAULT_DURATION);
        defaultFadeIn.setStartDelay(mAppearingDelay);
        defaultFadeIn.setInterpolator(mAppearingInterpolator);
        // 默认淡出动画
        defaultFadeOut = ObjectAnimator.ofFloat(null, "alpha", 1f, 0f);
        defaultFadeOut.setDuration(DEFAULT_DURATION);
        defaultFadeOut.setStartDelay(mDisappearingDelay);
        defaultFadeOut.setInterpolator(mDisappearingInterpolator);
    }
    // 赋值给5种动画(APPEARING,DISAPPEARING,CHANGE_APPEARING ,CHANGE_DISAPPEARING,CHANGING)
    mChangingAppearingAnim = defaultChangeIn;
    mChangingDisappearingAnim = defaultChangeOut;
    mChangingAnim = defaultChange;
    mAppearingAnim = defaultFadeIn;
    mDisappearingAnim = defaultFadeOut;
}

接下来看动画在哪里触发,以mAppearingAnim为例,在runAppearingTransition()方法里,

// LayoutTransition
private void runAppearingTransition(final ViewGroup parent, final View child) {
    Animator currentAnimation = currentDisappearingAnimations.get(child);
    // 先取消当前动画
    if (currentAnimation != null) {
        currentAnimation.cancel();
    }
    // 如果未设置,则直接回调动画结束
    if (mAppearingAnim == null) {
        if (hasListeners()) {
            ArrayList<TransitionListener> listeners = (ArrayList<TransitionListener>) mListeners.clone();
            for (TransitionListener listener : listeners) {
                listener.endTransition(LayoutTransition.this, parent, child, APPEARING);
            }
        }
        return;
    }
    Animator anim = mAppearingAnim.clone();
    // 设置动画对象View
    anim.setTarget(child);
    anim.setStartDelay(mAppearingDelay);
    anim.setDuration(mAppearingDuration);
    if (mAppearingInterpolator != sAppearingInterpolator) {
        anim.setInterpolator(mAppearingInterpolator);
    }
    if (anim instanceof ObjectAnimator) {
        ((ObjectAnimator) anim).setCurrentPlayTime(0);
    }
    // 设置动画监听
    anim.addListener(new AnimatorListenerAdapter() {
        @Override
        public void onAnimationEnd(Animator anim) {
            currentAppearingAnimations.remove(child);
            if (hasListeners()) {
                ArrayList<TransitionListener> listeners = (ArrayList<TransitionListener>) mListeners.clone();
                for (TransitionListener listener : listeners) {
                    listener.endTransition(LayoutTransition.this, parent, child, APPEARING);
                }
            }
        }
    });
    currentAppearingAnimations.put(child, anim);
    // 启动动画
    anim.start();}

顺藤摸瓜,发现runAppearingTransition()addChild()方法里被调用,而addChild()最终在ViewGroup的addViewInner()中被调用。

// LayoutTransition
private void addChild(ViewGroup parent, View child, boolean changesLayout) {
    ...
    if ((mTransitionTypes & FLAG_APPEARING) == FLAG_APPEARING) {
        runAppearingTransition(parent, child);
    }
}
// ViewGroup
private void addViewInner(View child, int index, LayoutParams params, boolean preventRequestLayout) {
    if (mTransition != null) {
    // Don't prevent other add transitions from completing, but cancel remove
    // transitions to let them complete the process before we add to the container
        mTransition.cancel(LayoutTransition.DISAPPEARING);
    }
    if (child.getParent() != null) {
        throw new IllegalStateException("The specified child already has a parent. " +
            "You must call removeView() on the child's parent first.");
    }
    // addView时触发transition动画
    if (mTransition != null) {
       mTransition.addChild(this, child);
    }
    ...
}

同样的,我们也可以在ViewGroup的onChildVisibilityChanged()方法中看到LayoutTransition被调用执行,

// ViewGroup
protected void onChildVisibilityChanged(View child, int oldVisibility, int newVisibility) {
    if (mTransition != null) {
        if (newVisibility == VISIBLE) {
            mTransition.showChild(this, child, oldVisibility);
        } else {
            mTransition.hideChild(this, child, newVisibility);
            ...
        }
    }
}

mTransition.showChild()内部调用了addChild()mTransition.hideChild()内部调用了removeChild()

// LayoutTransition
private void addChild(ViewGroup parent, View child, boolean changesLayout) {
    ...
    if (changesLayout && (mTransitionTypes & FLAG_CHANGE_APPEARING) == FLAG_CHANGE_APPEARING) {
        runChangeTransition(parent, child, APPEARING);
    }
    if ((mTransitionTypes & FLAG_APPEARING) == FLAG_APPEARING) {
        runAppearingTransition(parent, child);
    }
}
// LayoutTransition
private void removeChild(ViewGroup parent, View child, boolean changesLayout) {
    ...
    if (changesLayout &&(mTransitionTypes & FLAG_CHANGE_DISAPPEARING) == FLAG_CHANGE_DISAPPEARING) {
        runChangeTransition(parent, child, DISAPPEARING);
    }
    if ((mTransitionTypes & FLAG_DISAPPEARING) == FLAG_DISAPPEARING) {
        runDisappearingTransition(parent, child);
    }
}

在上面两个方法中都调用了 runChangeTransition()这个方法,这个方法里处理了其他受影响的view的动画,

private void runChangeTransition(final ViewGroup parent, View newView, final int changeReason) {
    ...
    int numChildren = parent.getChildCount();
    
    for (int i = 0; i < numChildren; ++i) {
        final View child = parent.getChildAt(i);
        
        // only animate the views not being added or removed
        if (child != newView) {
            setupChangeAnimation(parent, changeReason, baseAnimator, duration, child);
        }
    }
    if (mAnimateParentHierarchy) {
        ViewGroup tempParent = parent;
        while (tempParent != null) {
            ViewParent parentParent = tempParent.getParent();
            if (parentParent instanceof ViewGroup) {
                setupChangeAnimation((ViewGroup)parentParent, changeReason, parentAnimator,
duration, tempParent);
                tempParent = (ViewGroup) parentParent;
            } else {
                tempParent = null;
            }
        }
    }
    ...
}

通过setupChangeAnimation()来触发那些受影响的view的动画,这个方法的源码我就不贴了,有兴趣的小伙伴可自行查阅。