一、前言
全部都是通过自定义 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.AppBarLayoutandroid: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.CollapsingToolbarLayoutandroid: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"><ImageViewandroid: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.Toolbarandroid: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"><RelativeLayoutandroid:layout_width="match_parent"android:layout_height="?attr/actionBarSize"><TextViewandroid: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="个人信息"/><ImageButtonandroid: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><ImageButtonandroid: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"/><TextViewandroid: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,展开时是 0if (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 = 0ftv_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;}@Overridepublic 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;@Overridepublic 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;elsemaxScrollDistance = 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.AppBarLayoutandroid: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.CollapsingToolbarLayoutandroid: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"><ImageViewandroid: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.Toolbarandroid: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.CircleImageViewandroid: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();}}@Overridepublic boolean layoutDependsOn(CoordinatorLayout parent, CircleImageView child, View dependency) {return dependency instanceof AppBarLayout;}@Overridepublic 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的实际使用(二)
