一、前言
全部都是通过自定义 behavior 来实现的,比较有局限性,不了解自定义 behavior 的话,无法自定义动画效果和位置。这里暂时收集集中效果还不错的实现。
二、第一种
效果图:
布局:
<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:fitsSystemWindows="true">
<com.google.android.material.appbar.AppBarLayout
android:id="@+id/appbar_header_scroll_appbar"
android:layout_width="match_parent"
android:layout_height="240dp"
android:theme="@style/AppTheme.AppBarOverlay"
android:fitsSystemWindows="true">
<com.google.android.material.appbar.CollapsingToolbarLayout
android:id="@+id/ctl_header_scroll_collapsing"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:contentScrim="@color/blue_74D3FF"
app:layout_scrollFlags="scroll|exitUntilCollapsed"
android:fitsSystemWindows="true">
<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:fitsSystemWindows="true"/>
<androidx.appcompat.widget.Toolbar
android:id="@+id/ctl_header_scroll_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_header_scroll_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_header_scroll_titleico"
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>
</androidx.appcompat.widget.Toolbar>
</com.google.android.material.appbar.CollapsingToolbarLayout>
</com.google.android.material.appbar.AppBarLayout>
<ImageButton
android:id="@+id/ibtn_header_scroll_icon"
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=".design.behavior.header.UserInfoImageButtonBehavior"/>
<TextView
android:id="@+id/tv_header_scroll_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_header_scroll_icon"
app:layout_anchorGravity="bottom|center_horizontal"/>
<include layout="@layout/content_scrolling"/>
</androidx.coordinatorlayout.widget.CoordinatorLayout>
activity:
class HeaderScrollActivity: AppCompatActivity(R.layout.activity_header_scroll) {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
StatusBarUtils.initStatusBarStyle(this,false)
appbar_header_scroll_appbar.addOnOffsetChangedListener(object : AppBarStateChangeListener(){
override fun onStateChanged(appBarLayout: AppBarLayout?, state: State?, verticalOffset: Int) {
}
override fun onStateChangedAny(appBarLayout: AppBarLayout?, verticalOffset: Int) {
super.onStateChangedAny(appBarLayout, verticalOffset)
val total = appBarLayout!!.totalScrollRange * 1.0f
//计算出滑动百分比
//计算出滑动百分比
val p: Float = Math.abs(verticalOffset) / total//折叠时是 1,展开时是 0
if (p > 0.5) {
tv_header_scroll_title.alpha = 1.0f / 0.5f * (p - 0.5f)
tv_header_scroll_nick.alpha = 0f
} else {
tv_header_scroll_title.alpha = 0f
tv_header_scroll_nick.alpha = 1.0f - 1.0f / 0.5f * p
}
ibtn_header_scroll_titleico.visibility = if (p == 1f) View.VISIBLE else View.INVISIBLE
}
})
}
}
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;
@SuppressLint("PrivateResource")
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(R.dimen
.abc_action_bar_default_height_material);
//计算出状态栏的高度
statusBarHeight = StatusBarUtils.getStatusBarHeight(context);
//计算出头像居右的距离
marginRight = context.getResources().getDimensionPixelSize(R.dimen.activity_horizontal_margin);
//marginRight = 0;
}
@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;
}
}
三、第二种
效果图:
布局:
<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
xmlns:app="http://schemas.android.com/apk/res-auto">
<com.google.android.material.appbar.AppBarLayout
android:id="@+id/appbar_header2_appbar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar">
<com.google.android.material.appbar.CollapsingToolbarLayout
android:id="@+id/ctl_header2_collapsing"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:contentScrim="@color/colorPrimary"
app:expandedTitleGravity="bottom|center_horizontal"
app:layout_scrollFlags="scroll|exitUntilCollapsed|snap">
<ImageView
android:id="@+id/iv_head"
android:layout_width="match_parent"
android:layout_height="200dp"
android:scaleType="fitXY"
android:src="@drawable/ic_user_center_appbar_iv"
app:layout_collapseMode="parallax"
app:layout_collapseParallaxMultiplier="0.6"/>
<!-- 设置app:navigationIcon="@android:color/transparent"给头像预留位置 -->
<androidx.appcompat.widget.Toolbar
android:id="@+id/tb_header2_toolbar"
android:layout_width="match_parent"
android:layout_height="@dimen/toolbar_height"
app:layout_collapseMode="pin"
app:navigationIcon="@android:color/transparent"
app:theme="@style/ThemeOverlay.AppCompat.Dark"
app:title="Tom Hardy"/>
</com.google.android.material.appbar.CollapsingToolbarLayout>
</com.google.android.material.appbar.AppBarLayout>
<include layout="@layout/content_scrolling"/>
<!-- layout_anchor属性5.0以上需要设置为CollapsingToolbarLayout,不然头像最后会被覆盖 -->
<de.hdodenhof.circleimageview.CircleImageView
android:layout_width="100dp"
android:layout_height="100dp"
android:layout_margin="16dp"
android:src="@drawable/avator_default"
app:civ_border_color="@color/white"
app:civ_border_width="1dp"
app:layout_anchor="@id/ctl_header2_collapsing"
app:layout_anchorGravity="bottom|right"
app:layout_behavior=".design.behavior.header.AvatarBehavior"/>
</androidx.coordinatorlayout.widget.CoordinatorLayout>
activity: 什么都不用做
behavior:
public class AvatarBehavior extends CoordinatorLayout.Behavior<CircleImageView> {
// 缩放动画变化的支点
private static final float ANIM_CHANGE_POINT = 0.2f;
private Context mContext;
// 整个滚动的范围
private int mTotalScrollRange;
// AppBarLayout高度
private int mAppBarHeight;
// AppBarLayout宽度
private int mAppBarWidth;
// 控件原始大小
private int mOriginalSize;
// 控件最终大小
private int mFinalSize;
// 控件最终缩放的尺寸,设置坐标值需要算上该值
private float mScaleSize;
// 原始x坐标
private float mOriginalX;
// 最终x坐标
private float mFinalX;
// 起始y坐标
private float mOriginalY;
// 最终y坐标
private float mFinalY;
// ToolBar高度
private int mToolBarHeight;
// AppBar的起始Y坐标
private float mAppBarStartY;
// 滚动执行百分比[0~1]
private float mPercent;
// Y轴移动插值器
private DecelerateInterpolator mMoveYInterpolator;
// X轴移动插值器
private AccelerateInterpolator mMoveXInterpolator;
// 最终变换的视图,因为在5.0以上AppBarLayout在收缩到最终状态会覆盖变换后的视图,所以添加一个最终显示的图片
private CircleImageView mFinalView;
// 最终变换的视图离底部的大小
private int mFinalViewMarginBottom;
public AvatarBehavior(Context context, AttributeSet attrs) {
super(context, attrs);
mContext = context;
mMoveYInterpolator = new DecelerateInterpolator();
mMoveXInterpolator = new AccelerateInterpolator();
if (attrs != null) {
TypedArray a = mContext.obtainStyledAttributes(attrs, R.styleable.AvatarImageBehavior);
mFinalSize = (int) a.getDimension(R.styleable.AvatarImageBehavior_finalSize, 0);
mFinalX = a.getDimension(R.styleable.AvatarImageBehavior_finalX, 0);
mToolBarHeight = (int) a.getDimension(R.styleable.AvatarImageBehavior_toolBarHeight, 0);
a.recycle();
}
}
@Override
public boolean layoutDependsOn(CoordinatorLayout parent, CircleImageView child, View dependency) {
return dependency instanceof AppBarLayout;
}
@Override
public boolean onDependentViewChanged(CoordinatorLayout parent, CircleImageView child, View dependency) {
if (dependency instanceof AppBarLayout) {
_initVariables(child, dependency);
mPercent = (mAppBarStartY - dependency.getY()) * 1.0f / mTotalScrollRange;
float percentY = mMoveYInterpolator.getInterpolation(mPercent);
AnimHelper.setViewY(child, mOriginalY, mFinalY - mScaleSize, percentY);
if (mPercent > ANIM_CHANGE_POINT) {
float scalePercent = (mPercent - ANIM_CHANGE_POINT) / (1 - ANIM_CHANGE_POINT);
float percentX = mMoveXInterpolator.getInterpolation(scalePercent);
AnimHelper.scaleView(child, mOriginalSize, mFinalSize, scalePercent);
AnimHelper.setViewX(child, mOriginalX, mFinalX - mScaleSize, percentX);
} else {
AnimHelper.scaleView(child, mOriginalSize, mFinalSize, 0);
AnimHelper.setViewX(child, mOriginalX, mFinalX - mScaleSize, 0);
}
if (mFinalView != null) {
if (percentY == 1.0f) {
// 滚动到顶时才显示
mFinalView.setVisibility(View.VISIBLE);
} else {
mFinalView.setVisibility(View.GONE);
}
}
} else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP && dependency instanceof CollapsingToolbarLayout) {
// 大于5.0才生成新的最终的头像,因为5.0以上AppBarLayout会覆盖变换后的头像
if (mFinalView == null && mFinalSize != 0 && mFinalX != 0 && mFinalViewMarginBottom != 0) {
mFinalView = new CircleImageView(mContext);
mFinalView.setVisibility(View.GONE);
// 添加为CollapsingToolbarLayout子视图
((CollapsingToolbarLayout) dependency).addView(mFinalView);
FrameLayout.LayoutParams params = (FrameLayout.LayoutParams) mFinalView.getLayoutParams();
// 设置大小
params.width = mFinalSize;
params.height = mFinalSize;
// 设置位置,最后显示时相当于变换后的头像位置
params.gravity = Gravity.BOTTOM;
params.leftMargin = (int) mFinalX;
params.bottomMargin = mFinalViewMarginBottom;
mFinalView.setLayoutParams(params);
mFinalView.setImageDrawable(child.getDrawable());
mFinalView.setBorderColor(child.getBorderColor());
int borderWidth = (int) ((mFinalSize * 1.0f / mOriginalSize) * child.getBorderWidth());
mFinalView.setBorderWidth(borderWidth);
}
}
return true;
}
/**
* 初始化变量
* @param child
* @param dependency
*/
private void _initVariables(CircleImageView child, View dependency) {
if (mAppBarHeight == 0) {
mAppBarHeight = dependency.getHeight();
mAppBarStartY = dependency.getY();
}
if (mTotalScrollRange == 0) {
mTotalScrollRange = ((AppBarLayout) dependency).getTotalScrollRange();
}
if (mOriginalSize == 0) {
mOriginalSize = child.getWidth();
}
if (mFinalSize == 0) {
mFinalSize = mContext.getResources().getDimensionPixelSize(R.dimen.avatar_final_size);
}
if (mAppBarWidth == 0) {
mAppBarWidth = dependency.getWidth();
}
if (mOriginalX == 0) {
mOriginalX = child.getX();
}
if (mFinalX == 0) {
mFinalX = mContext.getResources().getDimensionPixelSize(R.dimen.avatar_final_x);
}
if (mOriginalY == 0) {
mOriginalY = child.getY();
}
if (mFinalY == 0) {
if (mToolBarHeight == 0) {
mToolBarHeight = mContext.getResources().getDimensionPixelSize(R.dimen.toolbar_height);
}
mFinalY = (mToolBarHeight - mFinalSize) / 2 + mAppBarStartY;
}
if (mScaleSize == 0) {
mScaleSize = (mOriginalSize - mFinalSize) * 1.0f / 2;
}
if (mFinalViewMarginBottom == 0) {
mFinalViewMarginBottom = (mToolBarHeight - mFinalSize) / 2;
}
}
}
三、地址和参考
demo
Coordinator Behavior example
CoordinatorLayout自定义Behavior的运用
CoordinatorLayout与Behavior的实际使用(二)