前言
闲来无事,掏出AS熟练的new一个project,发现默认出来的activity点击之后有如下动画
感觉很神奇,于是分析以下,这是怎么做到的?
这个activity的style是@style/AppTheme.NoActionBar,布局如下所示,这是目前android比较推崇的做法,基本理念就是脱离actionbar,在自己的布局里写toolbar,这样会提高更大的自由度,toolbar就变成了一个普通的view,我想放哪里放哪里,想怎么放怎么放(其实默认actionbar也是toolbar实现的,view层次结构可以参考AppCompatActivity的View树)。
<?xml version="1.0" encoding="utf-8"?>
<android.support.design.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fitsSystemWindows="true"
tools:context="com.fish.a2.MainActivity">
<android.support.design.widget.AppBarLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:theme="@style/AppTheme.AppBarOverlay">
<android.support.v7.widget.Toolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:background="?attr/colorPrimary"
app:popupTheme="@style/AppTheme.PopupOverlay" />
</android.support.design.widget.AppBarLayout>
<include layout="@layout/content_main" />
<android.support.design.widget.FloatingActionButton
android:id="@+id/fab"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="bottom|end"
android:layout_margin="@dimen/fab_margin"
android:src="@android:drawable/ic_dialog_email" />
</android.support.design.widget.CoordinatorLayout>
这一个xml中,CoordinatorLayout、FloatingActionButton、AppBarLayout都是陌生的。FloatingActionButton就是右下角的带阴影圆形按钮,可以简单看做一个ImageButton,如果对此控件有兴趣,可以阅读http://blog.csdn.net/lmj623565791/article/details/46678867,此文内鸿洋大神还自己实现了阴影效果。
我们再来看自动生成的部分代码,FloatingActionButton(后边简称fab)的点击就是弹出一个Snackbar,又出来一个新玩意,Snackbar可以看做是Toast的升级版本,没什么高深的,下边代码的意思就是点击fab弹出一个Snackbar,Snackbar像toast一样几秒后就消失。
FloatingActionButton fab = (FloatingActionButton) findViewById(R.id.fab);
fab.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
Snackbar.make(view, "Replace with your own action", Snackbar.LENGTH_SHORT)
.setAction("Action", null).show();
}
});
这些看起来都很简单,但是Snackbar弹出的时候,为何fab为何为往上跑呢?然后过了几秒钟,Snackbar消失了,fab又自动往下滑了,这2个动画是怎么做到的?从代码里看不出任何端倪.
CoordinatorLayout
先简单介绍下CoordinatorLayout,看官方文档
CoordinatorLayout is a super-powered FrameLayout.
CoordinatorLayout is intended for two primary use cases:
1.As a top-level application decor or chrome layout
2.As a container for a specific interaction with one or more child views
By specifying Behaviors for child views of a CoordinatorLayout you can provide many different interactions within a single parent and those views can also interact with one another. View classes can specify a default behavior when used as a child of a CoordinatorLayout using the DefaultBehavior annotation.
Behaviors may be used to implement a variety of interactions and additional layout modifications ranging from sliding drawers and panels to swipe-dismissable elements and buttons that stick to other elements as they move and animate.
CoordinatorLayout是个超级强大的FrameLayout(其实他并不继承FrameLayout,只是里面的view布局和FrameLayout一样,一层层叠上去)
CoordinatorLayout主要有2种用法
1.作为app的rootview(contentview的child)
2.作为一个容器,包含几个view,这些view有特殊行为,互相作用
给CoordinatorLayout的子view指定behavior可以让他们互相作用互相依赖
behavior
概述
Behavior是一种新的view关系描述,CoordinatorLayout的一个子view可以依赖于另一个子view做出响应,比如有子view A和B,A依赖于B,一旦B发生变化(比如大小位置可见性改变),A就可以知道并且响应,这其实就是观察者模式。一个子view可以依赖于多个子view,比如A可以依赖于B,C这个依赖关系会在一个类内写明,这个类是CoordinatorLayout.Behavior的子类,可以自定义。
例子解释
还是有点难以理解,看上文的实际例子吧。
fab会因为Snackbar的弹出而上移,随着Snackbar的消失而下移,是因为fab依赖于Snackbar。 这种依赖关系由FloatingActionButton内部的Behavior类决定的。
Behavior内有如下代码,看layoutDependsOn代码,dependency是SnackbarLayout就返回true(SNACKBAR_BEHAVIOR_ENABLED前提下),这就说明了fab依赖于SnackbarLayout,一旦SnackbarLayout发生变化,fab就会立刻知道并作出反应。
@Override
public boolean layoutDependsOn(CoordinatorLayout parent,
FloatingActionButton child, View dependency) {
// We're dependent on all SnackbarLayouts (if enabled)
return SNACKBAR_BEHAVIOR_ENABLED && dependency instanceof Snackbar.SnackbarLayout;
}
做出什么反应呢?来看onDependentViewChanged的代码,主要调用updateFabTranslationForSnackbar,注意updateFabTranslationForSnackbar 的L34、L35,这里触发了平移动画。这就是为什么snackbar出现或消失的时候,fab会有平移动画。
@Override
public boolean onDependentViewChanged(CoordinatorLayout parent, FloatingActionButton child,
View dependency) {
if (dependency instanceof Snackbar.SnackbarLayout) {
updateFabTranslationForSnackbar(parent, child, dependency);
}
return false;
}
private void updateFabTranslationForSnackbar(CoordinatorLayout parent,
final FloatingActionButton fab, View snackbar) {
final float targetTransY = getFabTranslationYForSnackbar(parent, fab);
if (mFabTranslationY == targetTransY) {
// We're already at (or currently animating to) the target value, return...
return;
}
final float currentTransY = ViewCompat.getTranslationY(fab);
// Make sure that any current animation is cancelled
if (mFabTranslationYAnimator != null && mFabTranslationYAnimator.isRunning()) {
mFabTranslationYAnimator.cancel();
}
if (fab.isShown()
&& Math.abs(currentTransY - targetTransY) > (fab.getHeight() * 0.667f)) {
// If the FAB will be travelling by more than 2/3 of it's height, let's animate
// it instead
if (mFabTranslationYAnimator == null) {
mFabTranslationYAnimator = ViewUtils.createAnimator();
mFabTranslationYAnimator.setInterpolator(
AnimationUtils.FAST_OUT_SLOW_IN_INTERPOLATOR);
mFabTranslationYAnimator.setUpdateListener(
new ValueAnimatorCompat.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimatorCompat animator) {
ViewCompat.setTranslationY(fab,
animator.getAnimatedFloatValue());
}
});
}
//这里制造动画来完成上移或者下移
mFabTranslationYAnimator.setFloatValues(currentTransY, targetTransY);
mFabTranslationYAnimator.start();
} else {
// Now update the translation Y
ViewCompat.setTranslationY(fab, targetTransY);
}
mFabTranslationY = targetTransY;
}
我们再具体分析下,为何snackbar出现的时候,fab上移,而snackbar消失的时候fab下移呢?
在snackbar出现的时候,会调用onDependentViewChanged()然后调用updateFabTranslationForSnackbar(),这里重点是L34代码,意思就是从currentTransY平移到targetTransY,关注下这2个变量。currentTransY怎么来的,看L9就是fab本身的TranslationY,那targetTransY呢?看L3,由getFabTranslationYForSnackbar得到。
所以看一下getFabTranslationYForSnackbar的代码,L5获取CoordinatorLayout内部被依赖的子view,在这个CoordinatorLayout内只有fab依赖于Snackbar.SnackbarLayout,所以Snackbar.SnackbarLayout是被依赖的子view,也只有这一个。看L10,此时view就是Snackbar.SnackbarLayout ,minOffset为0,ViewCompat.getTranslationY(view) 为0,所以最终得到的minOffset就是- view.getHeight(),其实就是snackbar的高度的相反数。我们假设-200好了。再回到updateFabTranslationForSnackbar内,此时targetTransY为-200,而currentTransY为0,那会发生什么?从0上移到-200,所以snackbar出现的时候,fab做了一个上移动画。
private float getFabTranslationYForSnackbar(CoordinatorLayout parent,
FloatingActionButton fab) {
float minOffset = 0;
//获取被依赖的子view
final List<View> dependencies = parent.getDependencies(fab);
for (int i = 0, z = dependencies.size(); i < z; i++) {
final View view = dependencies.get(i);
if (view instanceof Snackbar.SnackbarLayout && parent.doViewsOverlap(fab, view)) {
//重点是这句代码
minOffset = Math.min(minOffset,
ViewCompat.getTranslationY(view) - view.getHeight());
}
}
return minOffset;
}
下面我们再可以分析snackbar消失的时候的场景,注意这里稍有不同,snackbar 出现调用了onDependentViewChanged,但是snackbar消失调用的是onDependentViewRemoved, 但最终调的还是updateFabTranslationForSnackbar ,殊途同归啊,此时getFabTranslationYForSnackbar内dependencies为空(因为snackbar 不见了),所以最终返回0,所以targetTransY为0,而currentTransY位-200,所以就从-200下移到了0,下移就是这么产生的。
@Override
public void onDependentViewRemoved(CoordinatorLayout parent, FloatingActionButton child,
View dependency) {
if (dependency instanceof Snackbar.SnackbarLayout) {
updateFabTranslationForSnackbar(parent, child, dependency);
}
}
snackbar出现回调到onDependentViewChanged,然后调用updateFabTranslationForSnackbar完成上移。
snackbar消失回调到onDependentViewRemoved,然后调用updateFabTranslationForSnackbar完成上移。
总结
简单的说CoordinatorLayout是容器,behavior是核心。behavior封装了CoordinatorLayout的子view随着其他子view的变化而变化的规则,主要是通过layoutDependsOn和onDependentViewChanged这2个方法。再看看CoordinatorLayout的名字,就是协调布局的意思,协调子view之间的关系,被协调的2个view必须是CoordinatorLayout的直接子view
dependency概念
fab依赖于Snackbar.SnackbarLayout,那我们就可以说fab的dependency是Snackbar.SnackbarLayout。