Android LayoutTransition

  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",或者在代码中指定,

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);
    ViewGroup root = findViewById(R.id.root);

    // 创建LayoutTransition实例
    LayoutTransition transition = new LayoutTransition();
    // 为根布局设置transition
    root.setLayoutTransition(transition);
}

接下来只要在这个根布局下的子view被add/remove或者可见性变化就会出发过渡动画效果。


LayoutTransition_1.gif

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加入)

以上5种动画中的前四种是默认开启的,

mTransitionTypes.png

第5种CHANGING在api16中加入,默认是不开启的,使用enableTransitionType(LayoutTransition.CHANGING)开启。

2. 为什么

先来看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为例,找到start()方法,在runAppearingTransition()方法里,中间clone了一下,难怪通过“ mAppearingAnim.start()”找不到,不过这个方法名也很明确了。

// 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()中被调用,

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

结构还是很清楚的,同时从下面源码也能看出5中动画执行的顺序:

// 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的动画,这个方法的源码我就不贴了,有兴趣的小伙伴可自行查阅。

综上,默认情况下DISAPPEARING和CHANGE_APPEARING类型动画会立即执行,其它类型动画则会有个延迟(这个延迟来自代码执行顺序,而非认为设置的延迟时间)。比如说,删除view,被删除的view将先执行动画消失,经过一些延迟受影响的view会进行动画补上位置;反之添加view,受影响的view将会先给添加的view腾位置执行CHANGE_APPEARING动画,经过一些时间的延迟才会执行APPEARING动画。

二、自定义LayoutTransition

通过setAnimator(int transitionType, Animator animator) 设置不同类型的动画,

public void setAnimator(int transitionType, Animator animator) {
    switch (transitionType) {
        case CHANGE_APPEARING:
            mChangingAppearingAnim = animator;
            break;
        case CHANGE_DISAPPEARING:
            mChangingDisappearingAnim = animator;
            break;
        case CHANGING:
            mChangingAnim = animator;
            break;
        case APPEARING:
            mAppearingAnim = animator;
            break;
        case DISAPPEARING:
            mDisappearingAnim = animator;
            break;
    }
}

参数说明:

  1. transitionType:动画类型,分为五种,分别是CHANGE_APPEARING,CHANGE_DISAPPEARING,CHANGING,APPEARING,DISAPPEARING。
  2. animator:相应的动画。
    setAnimator方法为每种布局动画设置具体的动画,可以设置ObjectAnimator或者包含属性动画的AnimatorSet。
    setStagger 可以设置每个子view动画的时间间隔。
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,185评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,445评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,684评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,564评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,681评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,874评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,025评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,761评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,217评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,545评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,694评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,351评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,988评论 3 315
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,778评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,007评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,427评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,580评论 2 349

推荐阅读更多精彩内容

  • 1 LayoutTransition 概述 通过对视图动画和属性动画的学习,我们现在可以对一个view进行动画操作...
    sliencexiu阅读 7,066评论 2 8
  • View Animation(视图动画) 概述: 视图动画,也叫 Tween (补间)动画可以在一个视图容器内执行...
    ZoranLee阅读 829评论 0 1
  • View Animation(视图动画) 概述: 视图动画,也叫 Tween (补间)动画可以在一个视图容器内执行...
    我要离开浪浪山阅读 883评论 0 6
  • 1 背景 不能只分析源码呀,分析的同时也要整理归纳基础知识,刚好有人微博私信让全面说说Android的动画,所以今...
    未聞椛洺阅读 2,697评论 0 10
  • 转载一篇高质量博文,原地址请戳这里转载下来方便今后查看。1 背景不能只分析源码呀,分析的同时也要整理归纳基础知识,...
    Elder阅读 1,938评论 0 24