第一次接触CoordinateLayout的时候深深的被其炫酷的特效所吸引;想着何时在实际项目中可使用一下,无奈实际项目因行业特色,并不须要使用到CoordinateLayout这么高端的交互体验;因此本着学习的态度,便用CoordinateLayout模仿了一下知乎首页效果,这也是如今掘金APP首页的效果;这种效果其实很友好,能让用户最大限度的使用到手机屏幕。javascript
好了,废话很少说,先看看模仿效果。php
这里用到的icon 大部分来源于iconfont。java
使用模拟器截取gif貌似永远都是这样模糊不清,有兴趣的同窗能够点github查看源码,实际运行在手机上效果会比这里好一些。android
CoordinateLayout是Android Design Support Library提供的一种布局方式。git
查看源码咱们能够看到 CoordinateLayout继承自ViewGroup,是一个“超级强大”的FrameLayout,FrameLayout 相信你们都很熟悉,使用也很简单,FrameLayout能够说是让Android布局中有了“层”的概念,那么这个CoordinateLayout又有什么神奇之处呢,下面咱们就学习一下。github
Coordinate 按照字面意思理解,就是协调。它能够方便的实现布局内view协调app
那么到底是怎么个调节法呢,咱们来看一下。ide
关于CoordinateLayout最经典的例子就是其结合Snackbar的使用了。函数
<?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" android:layout_width="match_parent" android:layout_height="match_parent">
<android.support.design.widget.FloatingActionButton android:id="@+id/fab" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_gravity="end|bottom" android:layout_margin="16dp" android:src="@drawable/ic_done" />
</android.support.design.widget.CoordinatorLayout>复制代码
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
findViewById(R.id.fab).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
Snackbar.make(view,"FAB",Snackbar.LENGTH_LONG)
.setAction("cancel", new View.OnClickListener() {
@Override
public void onClick(View v) {
}
})
.show();
}
});
}复制代码
上面这份代码应该是不少人对CoordinateLayout的第一印象,这也是关于CoordinateLayout最直观的解释。布局
这里实现的效果就是如上面第三幅动图message中那样,底部弹出一个SnackBar,FloatingActionButton自动上移;这就是所谓的协调,协调FloatingActionButton上移,不被顶部弹出的SnackBar所遮挡。
这里若是没有使用CoordinateLayout做为根布局,而是使用LinearLayout或RelativeLayout等,若是FloatingActionButton距离底部太近,那么它将会被底部弹出的Snackbar所遮挡。
说到CoordinateLayout就不得不提这个AppBarLayout,他们俩简直就是天生一对,两者结合使用,那画面真是太美了,想一想都以为刺激。
这里看一下咱们模仿首页顶部搜索栏的代码:
<?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:id="@+id/coordinatorLayout" android:layout_width="match_parent" android:layout_height="match_parent" tools:context=".fragments.IndexFragment">
<android.support.design.widget.AppBarLayout android:id="@+id/index_app_bar" android:layout_width="match_parent" android:layout_height="wrap_content" android:theme="@style/AppTheme.AppBarOverlay">
<RelativeLayout android:layout_width="match_parent" android:layout_height="?attr/actionBarSize" android:background="@color/colorPrimary" app:layout_scrollFlags="scroll|enterAlways">
<ImageView android:id="@+id/live" android:layout_width="24dp" android:layout_height="24dp" android:layout_alignParentRight="true" android:layout_centerVertical="true" android:layout_marginRight="5dp" android:src="@drawable/live_button" />
<RelativeLayout android:layout_width="match_parent" android:layout_height="match_parent" android:layout_margin="10dp" android:layout_toLeftOf="@id/live" android:background="@color/searchmenu">
<ImageView android:id="@+id/search" android:layout_width="24dp" android:layout_height="24dp" android:layout_centerVertical="true" android:layout_marginLeft="10dp" android:src="@drawable/ic_search" />
<TextView android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_centerVertical="true" android:layout_marginLeft="10dp" android:layout_toRightOf="@id/search" android:text="搜索话题、问题或人" android:textSize="16sp" />
</RelativeLayout>
</RelativeLayout>
</android.support.design.widget.AppBarLayout>
.......
</android.support.design.widget.CoordinatorLayout>复制代码
这里咱们在AppBarLayout内部嵌套了一个RelativeLayout,在这个RelativeLayout中咱们模仿了顶部的搜索栏的布局效果,这个很简单。这里最核心的东西就是
app:layout_scrollFlags="scroll|enterAlways"复制代码
这行代码。什么意思呢?这个app:layout_scrollFlags有下面几个值:
scroll: 全部想滚动出屏幕的view都须要设置这个flag, 没有设置这个flag的view将被固定在屏幕顶部。
enterAlways: 设置这个flag时,向下的滚动都会致使该view变为可见。
enterAlwaysCollapsed: 当你的视图已经设置minHeight属性又使用此标志时,你的视图只能已最小高度进入,只有当滚动视图到达顶部时才扩大到完整高度。
exitUntilCollapsed: 滚动退出屏幕,最后折叠在顶端。
snap: 视图在滚动时会有一种“就近原则”,怎么说呢,就是当视图展开时,若是滑动中展 开的内容超过视图的75%,那么视图依旧会保持展开;当视图处于关闭时,若是滑动中展开的部分小于视图的25%,那么视图会保持关闭。总的来讲,就是会让动画有一种弹性的视觉效果。
这里咱们使用了scroll 和 enterAlways ,就很容易的实现了向下滑动时顶部隐藏,向下滑动时顶部出现的效果。
注意,这里所说的TabLayout是android.support.design.widget.TabLayout,不是好久之前的那个TabLayout。
使用这个TabLayout能够产生一种滑动时tab 悬停的效果,这里咱们模仿的时候,用于种种缘由没能使用TabLayout的动态效果,只是简单的结合ViewPager使用了一下,第二个页面discovery就是使用TabLayout做为顶部的Indicator使用;这个很简单,就不展开说了;具体实现看查看源码。
我的感受,这是整个CoordinateLayout中最拉风的动画特效,主要是实现一种“折叠”的动画效果。咱们在模仿我的中心的时候就是用到了这个功能:
看一下我的中心的布局文件:
<?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" android:layout_width="match_parent" android:layout_height="match_parent">
<android.support.design.widget.AppBarLayout android:id="@+id/appbar" android:layout_width="match_parent" android:layout_height="256dp" android:fitsSystemWindows="true" android:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar">
<android.support.design.widget.CollapsingToolbarLayout android:id="@+id/collapsing_toolbar" android:layout_width="match_parent" android:layout_height="match_parent" android:fitsSystemWindows="true" app:contentScrim="?attr/colorPrimary" app:expandedTitleMarginEnd="64dp" app:expandedTitleMarginStart="48dp" app:layout_scrollFlags="scroll|exitUntilCollapsed|snap">
<RelativeLayout android:layout_width="match_parent" android:layout_height="match_parent" android:background="@drawable/user_bg" app:layout_collapseMode="parallax">
<de.hdodenhof.circleimageview.CircleImageView android:layout_width="68dp" android:layout_height="68dp" android:layout_centerInParent="true" android:src="@drawable/profile" />
</RelativeLayout>
<android.support.v7.widget.Toolbar android:id="@+id/toolbar" android:layout_width="match_parent" android:layout_height="?attr/actionBarSize" app:layout_collapseMode="pin" app:popupTheme="@style/ThemeOverlay.AppCompat.Light" />
</android.support.design.widget.CollapsingToolbarLayout>
</android.support.design.widget.AppBarLayout>
<android.support.v4.widget.NestedScrollView android:layout_width="match_parent" android:layout_height="wrap_content" app:layout_behavior="@string/appbar_scrolling_view_behavior">
<LinearLayout android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="vertical" android:paddingTop="10dp">
<ImageView android:layout_width="match_parent" android:layout_height="match_parent" android:scaleType="fitStart" android:src="@drawable/fake" />
</LinearLayout>
</android.support.v4.widget.NestedScrollView>
<android.support.design.widget.FloatingActionButton android:id="@+id/btn" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_margin="16dp" android:clickable="true" android:src="@drawable/ic_edit" app:layout_anchor="@id/appbar" app:layout_anchorGravity="bottom|right|end" />
</android.support.design.widget.CoordinatorLayout>复制代码
app:layout_scrollFlags="scroll|exitUntilCollapsed|snap"复制代码
这个属性前面介绍过了,这里三种属性结合就可实现滚动中“折叠视差”的效果了。
app:layout_collapseMode="parallax"复制代码
这个layout_collapseMode就是用来设置整个RelativeLayout的折叠效果的,有两种取值,“pin”:固定模式,在折叠的时候最后固定在顶端;“parallax”:视差模式,在折叠的时候会有个视差折叠的效果。
这里须要注意,这个时候,咱们须要将AppBarLayout的高度设置为固定值
CoordinatorLayout 还提供了一个 layout_anchor 的属性,连同 layout_anchorGravity 一块儿,能够用来放置与其余视图关联在一块儿的悬浮视图(如 FloatingActionButton)。
这里若是咱们将floatingActionButton设置为:
android:layout_gravity="bottom|right|end"复制代码
FloatingActionButton将位于整个屏幕的右下角。
这里须要注意的是,使用AppBarLayout时,为了实现其滚动时的效果,在其下面必须有一个可滚动的View,而且须要为其设置app:layout_behavior属性。
好比咱们在结合结合CollapsingToolbarLayout使用时,
在AppBarLayout的下面放置了一个NestedScrollView,并设置其app:layout_behavior="@string/appbar_scrolling_view_behavior"。
而在其余页面,咱们AppBarLayout的下面放置了ViewPager或者是FrameLayout都设置了相应的属性;具体可参考源码。
上面咱们提到了layout_behavior,这是个什么意思呢?
这里就不得不说这个Behavior了,能够说Behavior是整个CoordinateLayout最核心的东西。还记得咱们最开始的列子吗?FloatingActionButton会随着Snackbar的出现,自动的调节本身的位置,这是怎样的实现的呢?
咱们经过追踪查看 Snackbar 的 show() 这个方法,最终会在Snack的源码中找到以下实现:
final void showView() {
if (mView.getParent() == null) {
final ViewGroup.LayoutParams lp = mView.getLayoutParams();
if (lp instanceof CoordinatorLayout.LayoutParams) {
// If our LayoutParams are from a CoordinatorLayout, we'll setup our Behavior
final CoordinatorLayout.LayoutParams clp = (CoordinatorLayout.LayoutParams) lp;
final Behavior behavior = new Behavior();
behavior.setStartAlphaSwipeDistance(0.1f);
behavior.setEndAlphaSwipeDistance(0.6f);
behavior.setSwipeDirection(SwipeDismissBehavior.SWIPE_DIRECTION_START_TO_END);
behavior.setListener(new SwipeDismissBehavior.OnDismissListener() {
@Override
public void onDismiss(View view) {
view.setVisibility(View.GONE);
dispatchDismiss(Callback.DISMISS_EVENT_SWIPE);
}
@Override
public void onDragStateChanged(int state) {
switch (state) {
case SwipeDismissBehavior.STATE_DRAGGING:
case SwipeDismissBehavior.STATE_SETTLING:
// If the view is being dragged or settling, cancel the timeout
SnackbarManager.getInstance().cancelTimeout(mManagerCallback);
break;
case SwipeDismissBehavior.STATE_IDLE:
// If the view has been released and is idle, restore the timeout
SnackbarManager.getInstance().restoreTimeout(mManagerCallback);
break;
}
}
});
clp.setBehavior(behavior);
// Also set the inset edge so that views can dodge the snackbar correctly
clp.insetEdge = Gravity.BOTTOM;
}
mTargetParent.addView(mView);
}
......
}复制代码
咱们能够看到,当Snack执行show方法的时候,会生成一个Behavior对象,而后将这个对象set给CoordinateLayout,而CoordinateLayout会根据这个Behavior执行动做。这个方法下面省略的代码大致上就是一个Translation属性动画的实现,这里就不展开来讲了。
回到咱们以前所说,咱们须要为带有滚动属性的view设置layout_behavior这个属性,咱们为其设置的值
app:layout_behavior="@string/appbar_scrolling_view_behavior"复制代码
<string name="appbar_scrolling_view_behavior" translatable="false">android.support.design.widget.AppBarLayout$ScrollingViewBehavior</string>复制代码
咱们能够在AppBarLayout的源码中找到这个ScrollingViewBehavior,其最终也是继承自Behavior实现了特定的效果。
如今或许你有疑问,这个神秘的Behavior究竟是个什么鬼?
Behavior 就是行为,他定义了View的行为。
CoordinatorLayout的工做原理是搜索定义了CoordinatorLayout Behavior的子view,不论是经过在xml中使用app:layout_behavior标签仍是经过在代码中对view类使用@DefaultBehavior修饰符来添加注解。当滚动发生的时候,CoordinatorLayout会尝试触发那些声明了依赖的子view。
Behavior最基础的两个方法是:layoutDependsOn() 和onDependentViewChanged();这两个方法的说明以下:
public boolean layoutDependsOn(CoordinatorLayout parent, T child, View dependency) {
boolean result;
//返回false表示child不依赖dependency,ture表示依赖
return result;
}复制代码
/** * 当dependency发生改变时(位置、宽高等),执行这个函数 * 返回true表示child的位置或者是宽高要发生改变,不然就返回false */
@Override
public boolean onDependentViewChanged(CoordinatorLayout parent, T child, View dependency) {
//child要执行的具体动做
return true;
}复制代码
咱们用Android Studio查看FloatingActionButton的源码,会发现他包含了一个Behavior的注解:
@CoordinatorLayout.DefaultBehavior(FloatingActionButton.Behavior.class)
public class FloatingActionButton extends VisibilityAwareImageButton {
.....
}复制代码
这里咱们看一下FloatingActionButton的注解参数FloatingActionButton.Behavior.class 是怎样实现的。经过源码咱们发现这个FloatingActionButton的Behavior继承自CoordinateLayout的Behavior,并且只实现了onDependentViewChanged方法,咱们看一下:
public static class Behavior extends CoordinatorLayout.Behavior<FloatingActionButton> {
private static final boolean AUTO_HIDE_DEFAULT = true;
private Rect mTmpRect;
private OnVisibilityChangedListener mInternalAutoHideListener;
private boolean mAutoHideEnabled;
public Behavior() {
super();
mAutoHideEnabled = AUTO_HIDE_DEFAULT;
}
public Behavior(Context context, AttributeSet attrs) {
super(context, attrs);
TypedArray a = context.obtainStyledAttributes(attrs,
R.styleable.FloatingActionButton_Behavior_Layout);
mAutoHideEnabled = a.getBoolean(
R.styleable.FloatingActionButton_Behavior_Layout_behavior_autoHide,
AUTO_HIDE_DEFAULT);
a.recycle();
}
@Override
public boolean onDependentViewChanged(CoordinatorLayout parent, FloatingActionButton child, View dependency) {
if (dependency instanceof AppBarLayout) {
// If we're depending on an AppBarLayout we will show/hide it automatically
// if the FAB is anchored to the AppBarLayout
updateFabVisibilityForAppBarLayout(parent, (AppBarLayout) dependency, child);
} else if (isBottomSheet(dependency)) {
updateFabVisibilityForBottomSheet(dependency, child);
}
return false;
}
}复制代码
能够看到他在onDependentViewChanged中直接判断了当前依赖的view。咱们在模仿我的中心时,设置的FloatingActionButton的dependency就是AppBarLayout。而在这个方法中,他就会根据此执行特定的操做,也就是updateFabVisibilityForAppBarLayout 这个方法中的内容。
好了,说了这么多,下面咱们说一下自定义Behavior。咱们在模仿知乎底部用于切换Fragment的Tab时,便使用了一个自定义的Behavior。
public class BottomViewBehavior extends CoordinatorLayout.Behavior<View> {
public BottomViewBehavior(Context context, AttributeSet attrs) {
super(context, attrs);
}
@Override
public boolean layoutDependsOn(CoordinatorLayout parent, View child, View dependency) {
return dependency instanceof AppBarLayout;
}
@Override
public boolean onDependentViewChanged(CoordinatorLayout parent, View child, View dependency) {
float translationY = Math.abs(dependency.getTop());
child.setTranslationY(translationY);
return true;
}
}复制代码
这里咱们的思路很简单,就是咱们的View 要依赖于顶部的AppBarLayout,而用其距离屏幕的距离,做为底部(tab)相对于屏幕的距离,这样当顶部的AppBarLayout 滑动出屏幕时,底部也将作相应的位移,固然这里底部tab 的高度是须要作限制的,不能大于顶部AppBarLayout的高度。
<LinearLayout android:id="@+id/bottom" android:layout_width="match_parent" android:layout_height="48dp" android:layout_gravity="bottom" android:background="@color/white" android:orientation="horizontal" app:layout_behavior="home.smart.fly.zhihuindex.behavior.BottomViewBehavior">
<RadioGroup android:id="@+id/tabs_rg" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="horizontal" android:paddingLeft="5dp" android:paddingRight="5dp">
<RadioButton android:id="@+id/home_tab" android:layout_width="0dp" android:layout_height="match_parent" android:layout_weight="1" android:background="#00000000" android:button="@null" android:checked="true" android:drawableTop="@drawable/home_sel" android:gravity="center|bottom" android:paddingTop="5dp" />
<RadioButton android:id="@+id/explore_tab" android:layout_width="0dp" android:layout_height="match_parent" android:layout_weight="1" android:background="#00000000" android:button="@null" android:drawableTop="@drawable/explore_sel" android:gravity="center|bottom" android:paddingTop="5dp" />
<RadioButton android:id="@+id/notify_tab" android:layout_width="0dp" android:layout_height="match_parent" android:layout_weight="1" android:background="#00000000" android:button="@null" android:drawableTop="@drawable/notify_sel" android:gravity="center|bottom" android:paddingTop="5dp" />
<RadioButton android:id="@+id/user_tab" android:layout_width="0dp" android:layout_height="match_parent" android:layout_weight="1" android:background="#00000000" android:button="@null" android:drawableTop="@drawable/user_sel" android:gravity="center|bottom" android:paddingTop="5dp" />
</RadioGroup>
</LinearLayout>复制代码
咱们将自定义的Behavior设置为这个bottom的app:layout_behavior就能够实现相似于知乎首页的那种效果了。
这里咱们用到的FloatingActionButton也能够自定义Behavior。
public class FabBehavior extends CoordinatorLayout.Behavior<View> {
private static final Interpolator INTERPOLATOR = new FastOutSlowInInterpolator();
/** * 控件距离coordinatorLayout底部距离 */
private float viewDistance;
private boolean aninmating;
public FabBehavior(Context context, AttributeSet attrs) {
super(context, attrs);
}
@Override
public boolean onStartNestedScroll(CoordinatorLayout coordinatorLayout, View child, View directTargetChild, View target, int nestedScrollAxes) {
if(child.getVisibility() == View.VISIBLE&& viewDistance ==0){
//获取控件距离父布局(coordinatorLayout)底部距离
viewDistance =coordinatorLayout.getHeight()-child.getY();
}
return (nestedScrollAxes & ViewCompat.SCROLL_AXIS_VERTICAL) != 0;//判断是否竖直滚动
}
@Override
public void onNestedPreScroll(CoordinatorLayout coordinatorLayout, View child, View target, int dx, int dy, int[] consumed) {
//dy大于0是向上滚动 小于0是向下滚动
if (dy >=0&&!aninmating &&child.getVisibility()==View.VISIBLE) {
hide(child);
} else if (dy <0&&!aninmating &&child.getVisibility()==View.GONE) {
show(child);
}
}
private void hide(final View view) {
ViewPropertyAnimator animator = view.animate().translationY(viewDistance).setInterpolator(INTERPOLATOR).setDuration(200);
animator.setListener(new Animator.AnimatorListener() {
@Override
public void onAnimationStart(Animator animator) {
aninmating =true;
}
@Override
public void onAnimationEnd(Animator animator) {
view.setVisibility(View.GONE);
aninmating =false;
}
@Override
public void onAnimationCancel(Animator animator) {
show(view);
}
@Override
public void onAnimationRepeat(Animator animator) {
}
});
animator.start();
}
private void show(final View view) {
ViewPropertyAnimator animator = view.animate().translationY(0).setInterpolator(INTERPOLATOR).setDuration(200);
animator.setListener(new Animator.AnimatorListener() {
@Override
public void onAnimationStart(Animator animator) {
view.setVisibility(View.VISIBLE);
aninmating =true;
}
@Override
public void onAnimationEnd(Animator animator) {
aninmating =false;
}
@Override
public void onAnimationCancel(Animator animator) {
hide(view);
}
@Override
public void onAnimationRepeat(Animator animator) {
}
});
animator.start();
}
}复制代码
这里咱们并无去实现layoutDependsOn() 和onDependentViewChanged()这两个方法,前面咱们看FloatingActionButton源码的时候知道他已经实现了onDependentViewChanged,咱们这里咱们从自身需求出发,就其滑动时的特性,作了一个滑动屏幕时FloatingActionButton快速从底部弹出或隐藏的Behavior。结合注释,代码很容易理解。
好了,这就是全部关于CoordinateLayout的东西了,能够看到Behavior是这个控件的核心,也是最难理解的东西。自定义Behavior可让咱们的滑动动画有无限的可能。
关于这个模仿知乎首页的实现,最初真的只是想研究一下“首页”是怎么实现的。结果随着Demo的展开,加上轻微强迫症的做祟,便成了如今这个样子。
到这里,不得不说一下,我的感受,真正的知乎首页应该是没有使用CoordinateLayout;由于按如今这种Activity+n*Fragment 的套路,使用CoordinateLayout彻底是给本身添乱,由于CoordinateLayout是滑动特性是没法嵌套使用的(或者说很复杂,我没发现),当我在最外层的Activity中使用了CoordinateLayout后,内部的Fragment中再次使用CoordinateLayout时,就会发生意想不到的各类bug,因此你会发现咱们模拟的我的中心是有问题的,这里就是嵌套CoordinateLayout后外部的CoordinateLayout失效了,致使底部的Behavior也失效。
不过在整个模仿的过程,也算是对CoordinateLayout的一次深刻了解吧,顺便也对SwipeRefreshLayout的内容和Tween Animation的使用作了一次巩固。首页RecycleView item中仿照Toolbar的弹出菜单,真的是耗费了很多时间。
源码地址
好了,按照惯例再次给出github地址,欢迎star&fork。
以前学习CoordinateLayout模仿知乎首页的效果,断断续续的大概用了一周时间;如今回过头来再看,其实关于CoordinateLayout的使用很简单,甚至有些无聊;由于app:layout_scrollFlags和 app:layout_collapseMode 这两个属性的可选值都也就那么几个;这实际上是一种局限性,你们在应用中使用这个东西,产生的滑动视差效果几乎就是类似的,惟一就是颜色及主题风格的差别;这很容易让用户产生审美疲劳;惟一变化的就是Behavior,这是咱们能够自定义的东西,所以这也是最难掌握的东西。知乎首页以及掘金首页的效果比我在这里实现的要更加柔和舒服,至于其是否使用了CoordinateLayout就不得而知了。