一、前言

全部都是通过自定义 behavior 来实现的,比较有局限性,不了解自定义 behavior 的话,无法自定义动画效果和位置。这里暂时收集集中效果还不错的实现。

二、第一种

效果图:
iShot2020-08-2318.47.55.gif
布局:

  1. <?xml version="1.0" encoding="utf-8"?>
  2. <androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
  3. android:layout_width="match_parent"
  4. android:layout_height="match_parent"
  5. xmlns:app="http://schemas.android.com/apk/res-auto"
  6. android:fitsSystemWindows="true">
  7. <com.google.android.material.appbar.AppBarLayout
  8. android:id="@+id/appbar_header_scroll_appbar"
  9. android:layout_width="match_parent"
  10. android:layout_height="240dp"
  11. android:theme="@style/AppTheme.AppBarOverlay"
  12. android:fitsSystemWindows="true">
  13. <com.google.android.material.appbar.CollapsingToolbarLayout
  14. android:id="@+id/ctl_header_scroll_collapsing"
  15. android:layout_width="match_parent"
  16. android:layout_height="match_parent"
  17. app:contentScrim="@color/blue_74D3FF"
  18. app:layout_scrollFlags="scroll|exitUntilCollapsed"
  19. android:fitsSystemWindows="true">
  20. <ImageView
  21. android:layout_width="match_parent"
  22. android:layout_height="match_parent"
  23. android:scaleType="centerCrop"
  24. android:src="@drawable/ic_user_center_appbar_iv"
  25. app:layout_collapseMode="parallax"
  26. android:fitsSystemWindows="true"/>
  27. <androidx.appcompat.widget.Toolbar
  28. android:id="@+id/ctl_header_scroll_toolbar"
  29. android:layout_width="match_parent"
  30. android:layout_height="?attr/actionBarSize"
  31. app:layout_collapseMode="pin"
  32. app:navigationIcon="?attr/homeAsUpIndicator"
  33. app:popupTheme="@style/AppTheme.PopupOverlay">
  34. <RelativeLayout
  35. android:layout_width="match_parent"
  36. android:layout_height="?attr/actionBarSize">
  37. <TextView
  38. android:id="@+id/tv_header_scroll_title"
  39. style="@style/TextAppearance.Widget.AppCompat.Toolbar.Title"
  40. android:layout_width="wrap_content"
  41. android:layout_height="wrap_content"
  42. android:layout_alignParentLeft="true"
  43. android:layout_centerVertical="true"
  44. android:text="个人信息"/>
  45. <ImageButton
  46. android:id="@+id/ibtn_header_scroll_titleico"
  47. android:layout_width="32dp"
  48. android:layout_height="32dp"
  49. android:layout_alignParentRight="true"
  50. android:layout_centerVertical="true"
  51. android:layout_marginRight="@dimen/activity_horizontal_margin"
  52. android:background="@drawable/usercenter_avator_bg"
  53. android:padding="2dp"
  54. android:scaleType="fitCenter"
  55. android:src="@drawable/avator_default"/>
  56. </RelativeLayout>
  57. </androidx.appcompat.widget.Toolbar>
  58. </com.google.android.material.appbar.CollapsingToolbarLayout>
  59. </com.google.android.material.appbar.AppBarLayout>
  60. <ImageButton
  61. android:id="@+id/ibtn_header_scroll_icon"
  62. android:layout_width="70dp"
  63. android:layout_height="70dp"
  64. android:background="@drawable/usercenter_avator_bg"
  65. android:padding="2dp"
  66. android:scaleType="fitCenter"
  67. android:src="@drawable/avator_default"
  68. app:layout_behavior=".design.behavior.header.UserInfoImageButtonBehavior"/>
  69. <TextView
  70. android:id="@+id/tv_header_scroll_nick"
  71. android:layout_width="wrap_content"
  72. android:layout_height="wrap_content"
  73. android:maxLines="1"
  74. android:paddingTop="32dp"
  75. android:text="NickName"
  76. android:textColor="@android:color/white"
  77. android:textSize="16sp"
  78. app:layout_anchor="@id/ibtn_header_scroll_icon"
  79. app:layout_anchorGravity="bottom|center_horizontal"/>
  80. <include layout="@layout/content_scrolling"/>
  81. </androidx.coordinatorlayout.widget.CoordinatorLayout>

activity:

  1. class HeaderScrollActivity: AppCompatActivity(R.layout.activity_header_scroll) {
  2. override fun onCreate(savedInstanceState: Bundle?) {
  3. super.onCreate(savedInstanceState)
  4. StatusBarUtils.initStatusBarStyle(this,false)
  5. appbar_header_scroll_appbar.addOnOffsetChangedListener(object : AppBarStateChangeListener(){
  6. override fun onStateChanged(appBarLayout: AppBarLayout?, state: State?, verticalOffset: Int) {
  7. }
  8. override fun onStateChangedAny(appBarLayout: AppBarLayout?, verticalOffset: Int) {
  9. super.onStateChangedAny(appBarLayout, verticalOffset)
  10. val total = appBarLayout!!.totalScrollRange * 1.0f
  11. //计算出滑动百分比
  12. //计算出滑动百分比
  13. val p: Float = Math.abs(verticalOffset) / total//折叠时是 1,展开时是 0
  14. if (p > 0.5) {
  15. tv_header_scroll_title.alpha = 1.0f / 0.5f * (p - 0.5f)
  16. tv_header_scroll_nick.alpha = 0f
  17. } else {
  18. tv_header_scroll_title.alpha = 0f
  19. tv_header_scroll_nick.alpha = 1.0f - 1.0f / 0.5f * p
  20. }
  21. ibtn_header_scroll_titleico.visibility = if (p == 1f) View.VISIBLE else View.INVISIBLE
  22. }
  23. })
  24. }
  25. }

behavior:

  1. public class UserInfoImageButtonBehavior extends CoordinatorLayout.Behavior<ImageButton> {
  2. private String TAG = getClass().getSimpleName();
  3. private int maxScrollDistance;
  4. private float maxChildWidth;
  5. private float minChildWidth;
  6. private int toolbarHeight;
  7. private int statusBarHeight;
  8. private int appbarStartPoint;
  9. private int marginRight;
  10. @SuppressLint("PrivateResource")
  11. public UserInfoImageButtonBehavior(Context context, AttributeSet attrs) {
  12. super(context, attrs);
  13. //计算出头像的最小宽度
  14. minChildWidth = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 32, context.getResources()
  15. .getDisplayMetrics());
  16. //计算出toolbar的高度
  17. toolbarHeight = context.getResources().getDimensionPixelSize(R.dimen
  18. .abc_action_bar_default_height_material);
  19. //计算出状态栏的高度
  20. statusBarHeight = StatusBarUtils.getStatusBarHeight(context);
  21. //计算出头像居右的距离
  22. marginRight = context.getResources().getDimensionPixelSize(R.dimen.activity_horizontal_margin);
  23. //marginRight = 0;
  24. }
  25. @Override
  26. public boolean layoutDependsOn(CoordinatorLayout parent, ImageButton child, View dependency) {
  27. // Log.d(TAG, "layoutDependsOn");
  28. //确定依赖关系,这里我们用作头像的ImageButton相依赖的是AppBarLayout,也就是ImageButton跟着AppBarLayout的变化而变化。
  29. return dependency instanceof AppBarLayout;
  30. }
  31. private int startX;
  32. private int startY;
  33. @Override
  34. public boolean onDependentViewChanged(CoordinatorLayout parent, ImageButton child, View dependency) {
  35. //这里的dependency就是布局中的AppBarLayout,child即显示的头像
  36. if (maxScrollDistance == 0) {
  37. //也就是第一次进来时,计算出AppBarLayout的最大垂直变化距离
  38. if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT)
  39. maxScrollDistance = dependency.getBottom() - toolbarHeight - statusBarHeight - statusBarHeight;
  40. else
  41. maxScrollDistance = dependency.getBottom() - toolbarHeight;
  42. }
  43. //计算出appbar的开始的y坐标
  44. if (appbarStartPoint == 0)
  45. appbarStartPoint = dependency.getBottom();
  46. //计算出头像的宽度
  47. if (maxChildWidth == 0)
  48. maxChildWidth = Math.min(child.getWidth(), child.getHeight());
  49. //计算出头像的起始x坐标
  50. if (startX == 0)
  51. startX = (int) (dependency.getWidth() / 2 - maxChildWidth / 2);
  52. //计算出头像的起始y坐标
  53. if (startY == 0)
  54. startY = (int) (dependency.getBottom() - maxScrollDistance / 2 - maxChildWidth / 2 - toolbarHeight / 2);
  55. //计算出appbar已经变化距离的百分比,起始位置y减去当前位置y,然后除以最大距离
  56. float expandedPercentageFactor = (appbarStartPoint - dependency.getBottom()) * 1.0f /
  57. (maxScrollDistance * 1.0f);
  58. //根据上面计算出的百分比,计算出头像应该移动的y距离,通过百分比乘以最大距离
  59. float moveY = expandedPercentageFactor * (maxScrollDistance - (appbarStartPoint - startY - toolbarHeight / 2
  60. - minChildWidth / 2));
  61. //根据上面计算出的百分比,计算出头像应该移动的y距离
  62. float moveX = expandedPercentageFactor * (startX + maxChildWidth - marginRight - minChildWidth);
  63. //更新头像的位置
  64. child.setX(startX + moveX);
  65. child.setY(startY - moveY);
  66. //计算出当前头像的宽度
  67. float nowWidth = maxChildWidth - ((maxChildWidth - minChildWidth) * expandedPercentageFactor);
  68. //更新头像的宽高
  69. CoordinatorLayout.LayoutParams params = (CoordinatorLayout.LayoutParams) child.getLayoutParams();
  70. params.height = params.width = (int) nowWidth;
  71. child.setLayoutParams(params);
  72. return true;
  73. }
  74. }

三、第二种

效果图:
iShot2020-08-2318.52.07.gif
布局:

  1. <?xml version="1.0" encoding="utf-8"?>
  2. <androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
  3. android:layout_width="match_parent"
  4. android:layout_height="match_parent"
  5. xmlns:app="http://schemas.android.com/apk/res-auto">
  6. <com.google.android.material.appbar.AppBarLayout
  7. android:id="@+id/appbar_header2_appbar"
  8. android:layout_width="match_parent"
  9. android:layout_height="wrap_content"
  10. android:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar">
  11. <com.google.android.material.appbar.CollapsingToolbarLayout
  12. android:id="@+id/ctl_header2_collapsing"
  13. android:layout_width="match_parent"
  14. android:layout_height="match_parent"
  15. app:contentScrim="@color/colorPrimary"
  16. app:expandedTitleGravity="bottom|center_horizontal"
  17. app:layout_scrollFlags="scroll|exitUntilCollapsed|snap">
  18. <ImageView
  19. android:id="@+id/iv_head"
  20. android:layout_width="match_parent"
  21. android:layout_height="200dp"
  22. android:scaleType="fitXY"
  23. android:src="@drawable/ic_user_center_appbar_iv"
  24. app:layout_collapseMode="parallax"
  25. app:layout_collapseParallaxMultiplier="0.6"/>
  26. <!-- 设置app:navigationIcon="@android:color/transparent"给头像预留位置 -->
  27. <androidx.appcompat.widget.Toolbar
  28. android:id="@+id/tb_header2_toolbar"
  29. android:layout_width="match_parent"
  30. android:layout_height="@dimen/toolbar_height"
  31. app:layout_collapseMode="pin"
  32. app:navigationIcon="@android:color/transparent"
  33. app:theme="@style/ThemeOverlay.AppCompat.Dark"
  34. app:title="Tom Hardy"/>
  35. </com.google.android.material.appbar.CollapsingToolbarLayout>
  36. </com.google.android.material.appbar.AppBarLayout>
  37. <include layout="@layout/content_scrolling"/>
  38. <!-- layout_anchor属性5.0以上需要设置为CollapsingToolbarLayout,不然头像最后会被覆盖 -->
  39. <de.hdodenhof.circleimageview.CircleImageView
  40. android:layout_width="100dp"
  41. android:layout_height="100dp"
  42. android:layout_margin="16dp"
  43. android:src="@drawable/avator_default"
  44. app:civ_border_color="@color/white"
  45. app:civ_border_width="1dp"
  46. app:layout_anchor="@id/ctl_header2_collapsing"
  47. app:layout_anchorGravity="bottom|right"
  48. app:layout_behavior=".design.behavior.header.AvatarBehavior"/>
  49. </androidx.coordinatorlayout.widget.CoordinatorLayout>

activity: 什么都不用做
behavior:

  1. public class AvatarBehavior extends CoordinatorLayout.Behavior<CircleImageView> {
  2. // 缩放动画变化的支点
  3. private static final float ANIM_CHANGE_POINT = 0.2f;
  4. private Context mContext;
  5. // 整个滚动的范围
  6. private int mTotalScrollRange;
  7. // AppBarLayout高度
  8. private int mAppBarHeight;
  9. // AppBarLayout宽度
  10. private int mAppBarWidth;
  11. // 控件原始大小
  12. private int mOriginalSize;
  13. // 控件最终大小
  14. private int mFinalSize;
  15. // 控件最终缩放的尺寸,设置坐标值需要算上该值
  16. private float mScaleSize;
  17. // 原始x坐标
  18. private float mOriginalX;
  19. // 最终x坐标
  20. private float mFinalX;
  21. // 起始y坐标
  22. private float mOriginalY;
  23. // 最终y坐标
  24. private float mFinalY;
  25. // ToolBar高度
  26. private int mToolBarHeight;
  27. // AppBar的起始Y坐标
  28. private float mAppBarStartY;
  29. // 滚动执行百分比[0~1]
  30. private float mPercent;
  31. // Y轴移动插值器
  32. private DecelerateInterpolator mMoveYInterpolator;
  33. // X轴移动插值器
  34. private AccelerateInterpolator mMoveXInterpolator;
  35. // 最终变换的视图,因为在5.0以上AppBarLayout在收缩到最终状态会覆盖变换后的视图,所以添加一个最终显示的图片
  36. private CircleImageView mFinalView;
  37. // 最终变换的视图离底部的大小
  38. private int mFinalViewMarginBottom;
  39. public AvatarBehavior(Context context, AttributeSet attrs) {
  40. super(context, attrs);
  41. mContext = context;
  42. mMoveYInterpolator = new DecelerateInterpolator();
  43. mMoveXInterpolator = new AccelerateInterpolator();
  44. if (attrs != null) {
  45. TypedArray a = mContext.obtainStyledAttributes(attrs, R.styleable.AvatarImageBehavior);
  46. mFinalSize = (int) a.getDimension(R.styleable.AvatarImageBehavior_finalSize, 0);
  47. mFinalX = a.getDimension(R.styleable.AvatarImageBehavior_finalX, 0);
  48. mToolBarHeight = (int) a.getDimension(R.styleable.AvatarImageBehavior_toolBarHeight, 0);
  49. a.recycle();
  50. }
  51. }
  52. @Override
  53. public boolean layoutDependsOn(CoordinatorLayout parent, CircleImageView child, View dependency) {
  54. return dependency instanceof AppBarLayout;
  55. }
  56. @Override
  57. public boolean onDependentViewChanged(CoordinatorLayout parent, CircleImageView child, View dependency) {
  58. if (dependency instanceof AppBarLayout) {
  59. _initVariables(child, dependency);
  60. mPercent = (mAppBarStartY - dependency.getY()) * 1.0f / mTotalScrollRange;
  61. float percentY = mMoveYInterpolator.getInterpolation(mPercent);
  62. AnimHelper.setViewY(child, mOriginalY, mFinalY - mScaleSize, percentY);
  63. if (mPercent > ANIM_CHANGE_POINT) {
  64. float scalePercent = (mPercent - ANIM_CHANGE_POINT) / (1 - ANIM_CHANGE_POINT);
  65. float percentX = mMoveXInterpolator.getInterpolation(scalePercent);
  66. AnimHelper.scaleView(child, mOriginalSize, mFinalSize, scalePercent);
  67. AnimHelper.setViewX(child, mOriginalX, mFinalX - mScaleSize, percentX);
  68. } else {
  69. AnimHelper.scaleView(child, mOriginalSize, mFinalSize, 0);
  70. AnimHelper.setViewX(child, mOriginalX, mFinalX - mScaleSize, 0);
  71. }
  72. if (mFinalView != null) {
  73. if (percentY == 1.0f) {
  74. // 滚动到顶时才显示
  75. mFinalView.setVisibility(View.VISIBLE);
  76. } else {
  77. mFinalView.setVisibility(View.GONE);
  78. }
  79. }
  80. } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP && dependency instanceof CollapsingToolbarLayout) {
  81. // 大于5.0才生成新的最终的头像,因为5.0以上AppBarLayout会覆盖变换后的头像
  82. if (mFinalView == null && mFinalSize != 0 && mFinalX != 0 && mFinalViewMarginBottom != 0) {
  83. mFinalView = new CircleImageView(mContext);
  84. mFinalView.setVisibility(View.GONE);
  85. // 添加为CollapsingToolbarLayout子视图
  86. ((CollapsingToolbarLayout) dependency).addView(mFinalView);
  87. FrameLayout.LayoutParams params = (FrameLayout.LayoutParams) mFinalView.getLayoutParams();
  88. // 设置大小
  89. params.width = mFinalSize;
  90. params.height = mFinalSize;
  91. // 设置位置,最后显示时相当于变换后的头像位置
  92. params.gravity = Gravity.BOTTOM;
  93. params.leftMargin = (int) mFinalX;
  94. params.bottomMargin = mFinalViewMarginBottom;
  95. mFinalView.setLayoutParams(params);
  96. mFinalView.setImageDrawable(child.getDrawable());
  97. mFinalView.setBorderColor(child.getBorderColor());
  98. int borderWidth = (int) ((mFinalSize * 1.0f / mOriginalSize) * child.getBorderWidth());
  99. mFinalView.setBorderWidth(borderWidth);
  100. }
  101. }
  102. return true;
  103. }
  104. /**
  105. * 初始化变量
  106. * @param child
  107. * @param dependency
  108. */
  109. private void _initVariables(CircleImageView child, View dependency) {
  110. if (mAppBarHeight == 0) {
  111. mAppBarHeight = dependency.getHeight();
  112. mAppBarStartY = dependency.getY();
  113. }
  114. if (mTotalScrollRange == 0) {
  115. mTotalScrollRange = ((AppBarLayout) dependency).getTotalScrollRange();
  116. }
  117. if (mOriginalSize == 0) {
  118. mOriginalSize = child.getWidth();
  119. }
  120. if (mFinalSize == 0) {
  121. mFinalSize = mContext.getResources().getDimensionPixelSize(R.dimen.avatar_final_size);
  122. }
  123. if (mAppBarWidth == 0) {
  124. mAppBarWidth = dependency.getWidth();
  125. }
  126. if (mOriginalX == 0) {
  127. mOriginalX = child.getX();
  128. }
  129. if (mFinalX == 0) {
  130. mFinalX = mContext.getResources().getDimensionPixelSize(R.dimen.avatar_final_x);
  131. }
  132. if (mOriginalY == 0) {
  133. mOriginalY = child.getY();
  134. }
  135. if (mFinalY == 0) {
  136. if (mToolBarHeight == 0) {
  137. mToolBarHeight = mContext.getResources().getDimensionPixelSize(R.dimen.toolbar_height);
  138. }
  139. mFinalY = (mToolBarHeight - mFinalSize) / 2 + mAppBarStartY;
  140. }
  141. if (mScaleSize == 0) {
  142. mScaleSize = (mOriginalSize - mFinalSize) * 1.0f / 2;
  143. }
  144. if (mFinalViewMarginBottom == 0) {
  145. mFinalViewMarginBottom = (mToolBarHeight - mFinalSize) / 2;
  146. }
  147. }
  148. }

三、地址和参考

demo
Coordinator Behavior example
CoordinatorLayout自定义Behavior的运用
CoordinatorLayout与Behavior的实际使用(二)