前言
CoordinatorLayout
功能非常强大,而他的神奇之处就在于Behavior
对象,CoordinatorLayout
自己并不控制View
,所有的控制权都在Behavior
。前面我们讲了系统自带的Behavior
使用,现在我们尝试着自定义Behavior
,实现自己想要的效果。
前段时间在项目中刚好用到了自定义Behavior
,接下来就看看如何一步步实现的,首先我们看看要实现的效果。
要自己定义CoordinatorLayout Behavior
,需要实现layoutDependsOn()
和onDependentViewChanged()
两个方法。比如AppBarLayout.Behavior
就定义了这两个关键方法。这个behavior
用于当滚动发生的时候让AppBarLayout
发生改变。
public boolean layoutDependsOn(CoordinatorLayout parent, View child, View dependency) {
return dependency instanceof AppBarLayout;
}
public boolean onDependentViewChanged(CoordinatorLayout parent, View child, View dependency) {
// check the behavior triggered
android.support.design.widget.CoordinatorLayout.Behavior behavior = ((android.support.design.widget.CoordinatorLayout.LayoutParams) dependency.getLayoutParams()).getBehavior();
if (behavior instanceof AppBarLayout.Behavior) {
// do stuff here
}
}
实现
首先我们分析一下上面的界面,这就是一个简单的用户信息界面,界面中用到了可折叠的Toolbar
,随着界面上下滑动,用户的头像可以跟随着一起移动,并伴随着变大变小。根据前面了解的知识,要实现上面的效果就需要用到的有CoordinatorLayout
、AppBarLayout
、CollapsingToolbarLayout
等。
布局代码
<?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"
tools:context="me.shihao.coordinatorlayoutusage.MainActivity">
<android.support.design.widget.AppBarLayout
android:id="@+id/app_bar"
android:layout_width="match_parent"
android:layout_height="240dp"
android:theme="@style/AppTheme.AppBarOverlay">
<android.support.design.widget.CollapsingToolbarLayout
android:id="@+id/toolbar_layout"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:contentScrim="?attr/colorPrimary"
app:layout_scrollFlags="scroll|exitUntilCollapsed">
<ImageView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:scaleType="centerCrop"
android:src="@drawable/ic_user_center_appbar_iv"
app:layout_collapseMode="parallax"/>
<android.support.v7.widget.Toolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
app:layout_collapseMode="pin"
app:navigationIcon="?attr/homeAsUpIndicator"
app:popupTheme="@style/AppTheme.PopupOverlay">
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize">
<TextView
android:id="@+id/tv_title"
style="@style/TextAppearance.Widget.AppCompat.Toolbar.Title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentLeft="true"
android:layout_centerVertical="true"
android:text="个人信息"/>
</RelativeLayout>
</android.support.v7.widget.Toolbar>
</android.support.design.widget.CollapsingToolbarLayout>
</android.support.design.widget.AppBarLayout>
<ImageButton
android:id="@+id/ibtn_ico"
android:layout_width="70dp"
android:layout_height="70dp"
android:background="@drawable/usercenter_avator_bg"
android:padding="2dp"
android:scaleType="fitCenter"
android:src="@drawable/avator_default"
app:layout_behavior="me.shihao.coordinatorlayoutusage.UserInfoImageButtonBehavior"/>
<TextView
android:id="@+id/tv_title_nick"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:maxLines="1"
android:paddingTop="32dp"
android:text="NickName"
android:textColor="@android:color/white"
android:textSize="16sp"
app:layout_anchor="@id/ibtn_ico"
app:layout_anchorGravity="bottom|center_horizontal"/>
<include layout="@layout/content_scrolling"/>
</android.support.design.widget.CoordinatorLayout>
自定义的behavior代码
public class UserInfoImageButtonBehavior extends CoordinatorLayout.Behavior<ImageButton> {
private String TAG = getClass().getSimpleName();
private int maxScrollDistance;
private float maxChildWidth;
private float minChildWidth;
private int toolbarHeight;
private int statusBarHeight;
private int appbarStartPoint;
private int marginRight;
public UserInfoImageButtonBehavior(Context context, AttributeSet attrs) {
super(context, attrs);
//计算出头像的最小宽度
minChildWidth = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 32, context.getResources()
.getDisplayMetrics());
//计算出toolbar的高度
toolbarHeight = context.getResources().getDimensionPixelSize(android.support.design.R.dimen
.abc_action_bar_default_height_material);
//计算出状态栏的高度
statusBarHeight = XStatusBarHelper.getStatusBarHeight(context);
//计算出头像居右的距离
marginRight = context.getResources().getDimensionPixelSize(R.dimen.activity_horizontal_margin);
}
@Override
public boolean layoutDependsOn(CoordinatorLayout parent, ImageButton child, View dependency) {
// Log.d(TAG, "layoutDependsOn");
//确定依赖关系,这里我们用作头像的ImageButton相依赖的是AppBarLayout,也就是ImageButton跟着AppBarLayout的变化而变化。
return dependency instanceof AppBarLayout;
}
private int startX;
private int startY;
@Override
public boolean onDependentViewChanged(CoordinatorLayout parent, ImageButton child, View dependency) {
//这里的dependency就是布局中的AppBarLayout,child即显示的头像
if (maxScrollDistance == 0) {
//也就是第一次进来时,计算出AppBarLayout的最大垂直变化距离
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT)
maxScrollDistance = dependency.getBottom() - toolbarHeight - statusBarHeight - statusBarHeight;
else
maxScrollDistance = dependency.getBottom() - toolbarHeight;
}
//计算出appbar的开始的y坐标
if (appbarStartPoint == 0)
appbarStartPoint = dependency.getBottom();
//计算出头像的宽度
if (maxChildWidth == 0)
maxChildWidth = Math.min(child.getWidth(), child.getHeight());
//计算出头像的起始x坐标
if (startX == 0)
startX = (int) (dependency.getWidth() / 2 - maxChildWidth / 2);
//计算出头像的起始y坐标
if (startY == 0)
startY = (int) (dependency.getBottom() - maxScrollDistance / 2 - maxChildWidth / 2 - toolbarHeight / 2);
//计算出appbar已经变化距离的百分比,起始位置y减去当前位置y,然后除以最大距离
float expandedPercentageFactor = (appbarStartPoint - dependency.getBottom()) * 1.0f /
(maxScrollDistance * 1.0f);
//根据上面计算出的百分比,计算出头像应该移动的y距离,通过百分比乘以最大距离
float moveY = expandedPercentageFactor * (maxScrollDistance - (appbarStartPoint - startY - toolbarHeight / 2
- minChildWidth / 2));
//根据上面计算出的百分比,计算出头像应该移动的y距离
float moveX = expandedPercentageFactor * (startX + maxChildWidth - marginRight - minChildWidth);
//更新头像的位置
child.setX(startX + moveX);
child.setY(startY - moveY);
//计算出当前头像的宽度
float nowWidth = maxChildWidth - ((maxChildWidth - minChildWidth) * expandedPercentageFactor);
//更新头像的宽高
CoordinatorLayout.LayoutParams params = (CoordinatorLayout.LayoutParams) child.getLayoutParams();
params.height = params.width = (int) nowWidth;
child.setLayoutParams(params);
return true;
}
}
在代码中有详细的注释,在这里我们用作头像的是ImageButton
,与其相依赖的是AppBarLayout
,在layoutDependsOn
我们定义了两者的依赖关系。在xml中使用时,我们通过app:layout_behavior
来设置。
app:layout_behavior="me.shihao.coordinatorlayoutusage.UserInfoImageButtonBehavior"
这里注意一下,我们引用时使用的是全路径me.shihao.coordinatorlayoutusage.UserInfoImageButtonBehavior
。
Activity代码
public class MainActivity extends AppCompatActivity {
private TextView tvTitle;
private TextView tvTitleNick;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
XStatusBarHelper.immersiveStatusBar(this);
XStatusBarHelper.setHeightAndPadding(this, toolbar);
tvTitle = (TextView) findViewById(R.id.tv_title);
tvTitleNick = (TextView) findViewById(R.id.tv_title_nick);
AppBarLayout appBar = (AppBarLayout) findViewById(R.id.app_bar);
appBar.addOnOffsetChangedListener(new AppBarLayout.OnOffsetChangedListener() {
@Override
public void onOffsetChanged(AppBarLayout appBarLayout, int verticalOffset) {
float total = appBarLayout.getTotalScrollRange() * 1.0f;
//计算出滑动百分比
float p = Math.abs(verticalOffset) / total;
if (p > 0.5) {
tvTitle.setAlpha(1.0f / 0.5f * (p - 0.5f));
tvTitleNick.setAlpha(0);
} else {
tvTitle.setAlpha(0);
tvTitleNick.setAlpha(1.0f - 1.0f / 0.5f * p);
}
}
});
}
}
这里我们通过设置AppBarLayout
的addOnOffsetChangedListener
,来监听AppBarLayout
的变化,然后通过变化来改变Title
与NickName
的显示隐藏。
基本代码就是这样,接下里我们看看实际的效果。
实际测试中却发现了还有另外一个问题,通过对比4.1与6.0上的效果,发现在6.0上当推到最上面时,头像被隐藏了,而我们实际需要的是不隐藏。为了解决这个问题,我在toolbar中增加了一个ImageButton
,用做最后显示头像,通过监听变化设置其显示隐藏。
改变后的xml布局
<?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"
tools:context="me.shihao.coordinatorlayoutusage.MainActivity">
<android.support.design.widget.AppBarLayout
android:id="@+id/app_bar"
android:layout_width="match_parent"
android:layout_height="240dp"
android:theme="@style/AppTheme.AppBarOverlay">
<android.support.design.widget.CollapsingToolbarLayout
android:id="@+id/toolbar_layout"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:contentScrim="?attr/colorPrimary"
app:layout_scrollFlags="scroll|exitUntilCollapsed">
<ImageView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:scaleType="centerCrop"
android:src="@drawable/ic_user_center_appbar_iv"
app:layout_collapseMode="parallax"/>
<android.support.v7.widget.Toolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
app:layout_collapseMode="pin"
app:navigationIcon="?attr/homeAsUpIndicator"
app:popupTheme="@style/AppTheme.PopupOverlay">
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize">
<TextView
android:id="@+id/tv_title"
style="@style/TextAppearance.Widget.AppCompat.Toolbar.Title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentLeft="true"
android:layout_centerVertical="true"
android:text="个人信息"/>
<ImageButton
android:id="@+id/ibtn_title_ico"
android:layout_width="32dp"
android:layout_height="32dp"
android:layout_alignParentRight="true"
android:layout_centerVertical="true"
android:layout_marginRight="@dimen/activity_horizontal_margin"
android:background="@drawable/usercenter_avator_bg"
android:padding="2dp"
android:scaleType="fitCenter"
android:src="@drawable/avator_default"/>
</RelativeLayout>
</android.support.v7.widget.Toolbar>
</android.support.design.widget.CollapsingToolbarLayout>
</android.support.design.widget.AppBarLayout>
<ImageButton
android:id="@+id/ibtn_ico"
android:layout_width="70dp"
android:layout_height="70dp"
android:background="@drawable/usercenter_avator_bg"
android:padding="2dp"
android:scaleType="fitCenter"
android:src="@drawable/avator_default"
app:layout_behavior="me.shihao.coordinatorlayoutusage.UserInfoImageButtonBehavior"/>
<TextView
android:id="@+id/tv_title_nick"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:maxLines="1"
android:paddingTop="32dp"
android:text="NickName"
android:textColor="@android:color/white"
android:textSize="16sp"
app:layout_anchor="@id/ibtn_ico"
app:layout_anchorGravity="bottom|center_horizontal"/>
<include layout="@layout/content_scrolling"/>
</android.support.design.widget.CoordinatorLayout>
改变后的Activity代码
public class MainActivity extends AppCompatActivity {
private TextView tvTitle;
private TextView tvTitleNick;
private ImageButton ibtnTitleIco;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
XStatusBarHelper.immersiveStatusBar(this);
XStatusBarHelper.setHeightAndPadding(this, toolbar);
tvTitle = (TextView) findViewById(R.id.tv_title);
tvTitleNick = (TextView) findViewById(R.id.tv_title_nick);
ibtnTitleIco = (ImageButton) findViewById(R.id.ibtn_title_ico);
AppBarLayout appBar = (AppBarLayout) findViewById(R.id.app_bar);
appBar.addOnOffsetChangedListener(new AppBarLayout.OnOffsetChangedListener() {
@Override
public void onOffsetChanged(AppBarLayout appBarLayout, int verticalOffset) {
float total = appBarLayout.getTotalScrollRange() * 1.0f;
//计算出滑动百分比
float p = Math.abs(verticalOffset) / total;
if (p > 0.5) {
tvTitle.setAlpha(1.0f / 0.5f * (p - 0.5f));
tvTitleNick.setAlpha(0);
} else {
tvTitle.setAlpha(0);
tvTitleNick.setAlpha(1.0f - 1.0f / 0.5f * p);
}
ibtnTitleIco.setVisibility(p == 1 ? View.VISIBLE : View.INVISIBLE);
}
});
}
}
最终的效果如下:
勉强算是解决了吧,个人能力有限,如果你有更好的方法,欢迎再下面留言。
项目中为了实现沉浸式状态栏使用了XStatusBarHelper,详细使用请前往查看。
如何实现沉浸式状态栏:http://www.jianshu.com/p/00fed1371bb0
https://github.com/fodroid/XStatusBarHelper