一、效果图

iShot2020-10-25 23.15.35 (1).gif

二、使用 ScrollView 实现

2.1 实现方式一

对于图片的缩放实现都放在工具类中,实现比较简单。

    1. 布局文件 ``` <?xml version=”1.0” encoding=”utf-8”?>

  1. <TextView
  2. android:layout_width="match_parent"
  3. android:layout_height="250dp"
  4. android:background="@color/yellow_FF9B52"/>
  5. <TextView
  6. android:layout_width="match_parent"
  7. android:layout_height="250dp"
  8. android:background="@color/green_07C0C2"/>
  9. <TextView
  10. android:layout_width="match_parent"
  11. android:layout_height="250dp"
  12. android:background="@color/red_F7E6ED"/>
  13. <TextView
  14. android:layout_width="match_parent"
  15. android:layout_height="250dp"
  16. android:background="@color/black_999999"/>
  17. </LinearLayout>

  1. - 2. activity实现,实现下面这一句就可以了

ScrollZoomUtil.scrollZoom(sv_zoom_scroll_outer, iv_zoom_scroll_icon)

  1. - 3. 工具类

public class ScrollZoomUtil { //控件原始高 private static int sZoomViewOriginWidth = 0; //控件原始宽 private static int sZoomViewOriginHeight = 0; //被监听的可滚动控件按下时的纵坐标 private static float sLastY = 0; //是否开始缩放的标志位 private static boolean sStartZoom = false; // 回弹时间系数,系数越小,回弹越快 private static float mReplyRatio = 0.5f;

  1. // 滑动放大系数,系数越大,滑动时放大程度越大
  2. public static float mScaleRatio = 0.4f;
  3. // 最大的放大倍数
  4. public static float mScaleTimes = 2f;
  5. public static void scrollZoom(final View scrollView, final View zoomView) {
  6. zoomView.post(new Runnable() {
  7. @Override
  8. public void run() {
  9. //记录控件原始宽高
  10. sZoomViewOriginWidth = zoomView.getMeasuredWidth();
  11. sZoomViewOriginHeight = zoomView.getMeasuredHeight();
  12. }
  13. });
  14. scrollView.setOnTouchListener(new View.OnTouchListener() {
  15. @Override
  16. public boolean onTouch(View v, MotionEvent event) {
  17. switch (event.getAction()) {
  18. case MotionEvent.ACTION_DOWN:
  19. //当被监听的可滚动控件已经滚动到顶部时才可以进行缩放,置标志位为 true,记录下按下的纵坐标
  20. if (scrollView.getScrollY() == 0) {
  21. sStartZoom = true;
  22. sLastY = event.getY();
  23. return true;
  24. }
  25. break;
  26. case MotionEvent.ACTION_MOVE:
  27. if (sStartZoom) {
  28. //计算滑动的距离
  29. float distanceY = event.getY() - sLastY;
  30. if (distanceY > 0) {
  31. //当滑动距离大于 0 时表示下拉操作,对需要缩放的控件进行放大操作
  32. zoom(zoomView, distanceY);
  33. } else {
  34. //当滑动距离小于 0 时表示上拉操作,判断需要缩放的控件的当前宽高,如果比原始宽高大则先缩放至原始宽高
  35. if (zoomView.getMeasuredWidth() > sZoomViewOriginWidth) {
  36. zoom(zoomView, distanceY);
  37. } else {
  38. //需要缩放的控件的当前宽高与原始宽高相等时再进行上拉,则进行正常的滚动操作
  39. break;
  40. }
  41. }
  42. return true;
  43. }
  44. case MotionEvent.ACTION_UP:
  45. //手指抬起后恢复原始状态
  46. sLastY = 0;
  47. sStartZoom = false;
  48. restore(zoomView);
  49. break;
  50. }
  51. return false;
  52. }
  53. });
  54. }
  55. /**
  56. * Description:缩放
  57. * Date:2018/10/22
  58. */
  59. private static void zoom(View zoomView, float distanceY) {
  60. if (sZoomViewOriginWidth <= 0 || sZoomViewOriginHeight <= 0) {
  61. return;
  62. }
  63. //控制缩放比例
  64. float scaleTimes = (float) ((sZoomViewOriginWidth + distanceY) / (sZoomViewOriginWidth * 1.0));

// 如超过最大放大倍数,直接返回 if (scaleTimes > mScaleTimes) return;

  1. ViewGroup.LayoutParams layoutParams = zoomView.getLayoutParams();
  2. //控件高为原始高度加上滑动距离乘以一个系数(与滑动距离 1:1 的话变化太大)
  3. LogUtils.e(distanceY);
  4. layoutParams.height = (int) (sZoomViewOriginHeight + distanceY * 0.4);
  5. //控件宽等比例变化
  6. layoutParams.width = (int) ((sZoomViewOriginWidth * (sZoomViewOriginHeight + distanceY * 0.4)) / sZoomViewOriginHeight);
  7. // 设置控件水平居中
  8. ((ViewGroup.MarginLayoutParams) layoutParams).setMargins(-(layoutParams.width - sZoomViewOriginWidth) / 2, 0, 0, 0);
  9. zoomView.setLayoutParams(layoutParams);
  10. }
  11. /**
  12. * Description:恢复成初始状态
  13. * Date:2018/10/22
  14. */
  15. private static void restore(final View zoomView) {
  16. final float distance = zoomView.getMeasuredWidth() - sZoomViewOriginWidth;
  17. // 设置动画
  18. ValueAnimator anim = ObjectAnimator.ofFloat(distance, 0.0F).setDuration((long) (distance * mReplyRatio));
  19. anim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
  20. @Override
  21. public void onAnimationUpdate(ValueAnimator animation) {
  22. zoom(zoomView,(Float) animation.getAnimatedValue());
  23. }
  24. });
  25. anim.start();

// //利用属性动画恢复成原始宽高,使其有一个过渡效果 // ValueAnimator widthValueAnimator = ObjectAnimator.ofInt(zoomView.getMeasuredWidth(), sZoomViewOriginWidth); // ValueAnimator heightValueAnimator = ObjectAnimator.ofInt(zoomView.getMeasuredHeight(), sZoomViewOriginHeight); // widthValueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { // @Override // public void onAnimationUpdate(ValueAnimator animation) { // ViewGroup.LayoutParams layoutParams = zoomView.getLayoutParams(); // layoutParams.width = (int) animation.getAnimatedValue(); // zoomView.setLayoutParams(layoutParams); // } // }); // heightValueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { // @Override // public void onAnimationUpdate(ValueAnimator animation) { // ViewGroup.LayoutParams layoutParams = zoomView.getLayoutParams(); // layoutParams.height = (int) animation.getAnimatedValue(); // zoomView.setLayoutParams(layoutParams); // } // }); // AnimatorSet animatorSet = new AnimatorSet(); // animatorSet.play(widthValueAnimator).with(heightValueAnimator); // //这个动画时长是每个动画持续的时长而不是总时长 // animatorSet.setDuration(300); // animatorSet.start(); } }

  1. <a name="6ZylP"></a>
  2. #### 2.2 通过自定义 ScrollView 实现
  3. 这种方式直接在布局文件中使用即可,注意放大的view,默认为第一个子view。
  4. - 1. 自定义 View

public class HeadZoomScrollView extends ScrollView {

  1. public HeadZoomScrollView(Context context) {
  2. super(context);
  3. }
  4. public HeadZoomScrollView(Context context, AttributeSet attrs) {
  5. super(context, attrs);
  6. }
  7. public HeadZoomScrollView(Context context, AttributeSet attrs, int defStyleAttr) {
  8. super(context, attrs, defStyleAttr);
  9. }
  10. // 用于记录下拉位置
  11. private float y = 0f;
  12. // zoomView原本的宽高
  13. private int zoomViewWidth = 0;
  14. private int zoomViewHeight = 0;
  15. // 是否正在放大
  16. private boolean mScaling = false;
  17. // 放大的view,默认为第一个子view
  18. private View zoomView;
  19. public void setZoomView(View zoomView) {
  20. this.zoomView = zoomView;
  21. }
  22. // 滑动放大系数,系数越大,滑动时放大程度越大
  23. private float mScaleRatio = 0.4f;
  24. public void setmScaleRatio(float mScaleRatio) {
  25. this.mScaleRatio = mScaleRatio;
  26. }
  27. // 最大的放大倍数
  28. private float mScaleTimes = 2f;
  29. public void setmScaleTimes(int mScaleTimes) {
  30. this.mScaleTimes = mScaleTimes;
  31. }
  32. // 回弹时间系数,系数越小,回弹越快
  33. private float mReplyRatio = 0.5f;
  34. public void setmReplyRatio(float mReplyRatio) {
  35. this.mReplyRatio = mReplyRatio;
  36. }
  37. @Override
  38. protected void onFinishInflate() {
  39. super.onFinishInflate();

// 不可过度滚动,否则上移后下拉会出现部分空白的情况 setOverScrollMode(OVER_SCROLL_NEVER); // 获得默认第一个view if (getChildAt(0) != null && getChildAt(0) instanceof ViewGroup && zoomView == null) { ViewGroup vg = (ViewGroup) getChildAt(0); if (vg.getChildCount() > 0) { zoomView = vg.getChildAt(0); } } }

  1. @Override
  2. public boolean onTouchEvent(MotionEvent ev) {
  3. if (zoomViewWidth <= 0 || zoomViewHeight <= 0) {
  4. zoomViewWidth = zoomView.getMeasuredWidth();
  5. zoomViewHeight = zoomView.getMeasuredHeight();
  6. }
  7. if (zoomView == null || zoomViewWidth <= 0 || zoomViewHeight <= 0) {
  8. return super.onTouchEvent(ev);
  9. }
  10. switch (ev.getAction()) {
  11. case MotionEvent.ACTION_MOVE:
  12. if (!mScaling) {
  13. if (getScrollY() == 0) {
  14. y = ev.getY();//滑动到顶部时,记录位置
  15. } else {
  16. break;
  17. }
  18. }
  19. int distance = (int) ((ev.getY() - y) * mScaleRatio);
  20. if (distance < 0) break;//若往下滑动
  21. mScaling = true;
  22. setZoom(distance);
  23. return true;
  24. case MotionEvent.ACTION_UP:
  25. mScaling = false;
  26. replyView();
  27. break;
  28. }
  29. return super.onTouchEvent(ev);
  30. }
  31. /**
  32. * 放大view
  33. */
  34. private void setZoom(float s) {
  35. float scaleTimes = (float) ((zoomViewWidth + s) / (zoomViewWidth * 1.0));

// 如超过最大放大倍数,直接返回 if (scaleTimes > mScaleTimes) return;

  1. ViewGroup.LayoutParams layoutParams = zoomView.getLayoutParams();
  2. layoutParams.width = (int) (zoomViewWidth + s);
  3. layoutParams.height = (int) (zoomViewHeight * ((zoomViewWidth + s) / zoomViewWidth));

// 设置控件水平居中 ((MarginLayoutParams) layoutParams).setMargins(-(layoutParams.width - zoomViewWidth) / 2, 0, 0, 0); zoomView.setLayoutParams(layoutParams); }

  1. /**
  2. * 回弹
  3. */
  4. private void replyView() {
  5. final float distance = zoomView.getMeasuredWidth() - zoomViewWidth;
  6. // 设置动画
  7. ValueAnimator anim = ObjectAnimator.ofFloat(distance, 0.0F).setDuration((long) (distance * mReplyRatio));
  8. anim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
  9. @Override
  10. public void onAnimationUpdate(ValueAnimator animation) {
  11. setZoom((Float) animation.getAnimatedValue());
  12. }
  13. });
  14. anim.start();
  15. }
  16. @Override
  17. protected void onScrollChanged(int l, int t, int oldl, int oldt) {
  18. super.onScrollChanged(l, t, oldl, oldt);
  19. if (onScrollListener != null) onScrollListener.onScroll(l, t, oldl, oldt);
  20. }
  21. private OnScrollListener onScrollListener;
  22. public void setOnScrollListener(OnScrollListener onScrollListener) {
  23. this.onScrollListener = onScrollListener;
  24. }
  25. /**
  26. * 滑动监听
  27. */
  28. public interface OnScrollListener {
  29. void onScroll(int scrollX, int scrollY, int oldScrollX, int oldScrollY);
  30. }

}

  1. <a name="aZBuZ"></a>
  2. ## 三、通过 MD 实现
  3. - 1.这种实现方式通常是通过自定义 behavior 来实现,注意要放大的图片布局控件要加上android:tag="overScroll",下面是 behavior 类:

public class AppbarZoomBehavior extends AppBarLayout.Behavior {

  1. private ImageView mImageView;
  2. private int mAppbarHeight;//记录AppbarLayout原始高度
  3. private int mImageViewHeight;//记录ImageView原始高度
  4. private static final float MAX_ZOOM_HEIGHT = 500;//放大最大高度
  5. private float mTotalDy;//手指在Y轴滑动的总距离
  6. private float mScaleValue;//图片缩放比例
  7. private int mLastBottom;//Appbar的变化高度
  8. private boolean isAnimate;//是否做动画标志
  9. private static final String TAG = "overScroll";
  10. public AppbarZoomBehavior(Context context, AttributeSet attrs) {
  11. super(context, attrs);
  12. }
  13. @Override
  14. public boolean onLayoutChild(CoordinatorLayout parent, AppBarLayout abl, int layoutDirection) {
  15. boolean handled = super.onLayoutChild(parent, abl, layoutDirection);
  16. init(abl);
  17. return handled;
  18. }
  19. /**
  20. * 进行初始化操作,在这里获取到ImageView的引用,和Appbar的原始高度
  21. *
  22. * @param abl
  23. */
  24. private void init(AppBarLayout abl) {
  25. abl.setClipChildren(false);
  26. mAppbarHeight = abl.getHeight();
  27. if (mImageView == null) {
  28. mImageView = abl.findViewWithTag(TAG);
  29. }
  30. if (mImageView != null) {
  31. mImageViewHeight = mImageView.getHeight();
  32. }
  33. }
  34. /**
  35. * 是否处理嵌套滑动
  36. *
  37. * @param parent
  38. * @param child
  39. * @param directTargetChild
  40. * @param target
  41. * @param nestedScrollAxes
  42. * @param type
  43. * @return
  44. */
  45. @Override
  46. public boolean onStartNestedScroll(CoordinatorLayout parent, AppBarLayout child, View directTargetChild, View target, int nestedScrollAxes, int type) {
  47. isAnimate = true;
  48. return true;
  49. }
  50. /**
  51. * 在这里做具体的滑动处理
  52. *
  53. * @param coordinatorLayout
  54. * @param child
  55. * @param target
  56. * @param dx
  57. * @param dy
  58. * @param consumed
  59. * @param type
  60. */
  61. @Override
  62. public void onNestedPreScroll(CoordinatorLayout coordinatorLayout, AppBarLayout child, View target, int dx, int dy, int[] consumed, int type) {
  63. if (mImageView != null && child.getBottom() >= mAppbarHeight && dy < 0 && type == ViewCompat.TYPE_TOUCH) {
  64. zoomHeaderImageView(child, dy);
  65. } else {
  66. if (mImageView != null && child.getBottom() > mAppbarHeight && dy > 0 && type == ViewCompat.TYPE_TOUCH) {
  67. consumed[1] = dy;
  68. zoomHeaderImageView(child, dy);
  69. } else {
  70. if (valueAnimator == null || !valueAnimator.isRunning()) {
  71. super.onNestedPreScroll(coordinatorLayout, child, target, dx, dy, consumed, type);
  72. }
  73. }
  74. }
  75. }
  76. /**
  77. * 对ImageView进行缩放处理,对AppbarLayout进行高度的设置
  78. *
  79. * @param abl
  80. * @param dy
  81. */
  82. private void zoomHeaderImageView(AppBarLayout abl, int dy) {
  83. mTotalDy += -dy;
  84. mTotalDy = Math.min(mTotalDy, MAX_ZOOM_HEIGHT);
  85. mScaleValue = Math.max(1f, 1f + mTotalDy / MAX_ZOOM_HEIGHT);
  86. ViewCompat.setScaleX(mImageView, mScaleValue);
  87. ViewCompat.setScaleY(mImageView, mScaleValue);
  88. mLastBottom = mAppbarHeight + (int) (mImageViewHeight / 2 * (mScaleValue - 1));
  89. abl.setBottom(mLastBottom);
  90. }
  91. /**
  92. * 处理惯性滑动的情况
  93. *
  94. * @param coordinatorLayout
  95. * @param child
  96. * @param target
  97. * @param velocityX
  98. * @param velocityY
  99. * @return
  100. */
  101. @Override
  102. public boolean onNestedPreFling(CoordinatorLayout coordinatorLayout, AppBarLayout child, View target, float velocityX, float velocityY) {
  103. if (velocityY > 100) {
  104. isAnimate = false;
  105. }
  106. return super.onNestedPreFling(coordinatorLayout, child, target, velocityX, velocityY);
  107. }
  108. /**
  109. * 滑动停止的时候,恢复AppbarLayout、ImageView的原始状态
  110. *
  111. * @param coordinatorLayout
  112. * @param abl
  113. * @param target
  114. * @param type
  115. */
  116. @Override
  117. public void onStopNestedScroll(CoordinatorLayout coordinatorLayout, AppBarLayout abl, View target, int type) {
  118. recovery(abl);
  119. super.onStopNestedScroll(coordinatorLayout, abl, target, type);
  120. }
  121. ValueAnimator valueAnimator;
  122. /**
  123. * 通过属性动画的形式,恢复AppbarLayout、ImageView的原始状态
  124. *
  125. * @param abl
  126. */
  127. private void recovery(final AppBarLayout abl) {
  128. if (mTotalDy > 0) {
  129. mTotalDy = 0;
  130. if (isAnimate) {
  131. valueAnimator = ValueAnimator.ofFloat(mScaleValue, 1f).setDuration(220);
  132. valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
  133. @Override
  134. public void onAnimationUpdate(ValueAnimator animation) {
  135. float value = (float) animation.getAnimatedValue();
  136. ViewCompat.setScaleX(mImageView, value);
  137. ViewCompat.setScaleY(mImageView, value);
  138. abl.setBottom((int) (mLastBottom - (mLastBottom - mAppbarHeight) * animation.getAnimatedFraction()));
  139. }
  140. });
  141. valueAnimator.start();
  142. } else {
  143. ViewCompat.setScaleX(mImageView, 1f);
  144. ViewCompat.setScaleY(mImageView, 1f);
  145. abl.setBottom(mAppbarHeight);
  146. }
  147. }
  148. }

}

  1. - 2. 布局文件

<?xml version=”1.0” encoding=”utf-8”?>

  1. <com.google.android.material.appbar.CollapsingToolbarLayout
  2. android:id="@+id/collapsingToolbarLayout"
  3. android:layout_width="match_parent"
  4. android:layout_height="wrap_content"
  5. app:contentScrim="@color/blue_74D3FF"
  6. app:layout_scrollFlags="scroll|exitUntilCollapsed|snap"
  7. app:statusBarScrim="@color/red_FF6D84"
  8. >
  9. <ImageView
  10. android:id="@+id/siv_picture"
  11. android:layout_width="match_parent"
  12. android:layout_height="200dp"
  13. android:fitsSystemWindows="true"
  14. android:src="@mipmap/wuhuang"
  15. android:scaleType="centerCrop"
  16. android:tag="overScroll"
  17. app:layout_collapseMode="parallax"
  18. />
  19. <androidx.appcompat.widget.Toolbar
  20. android:id="@+id/toolbar"
  21. android:layout_width="match_parent"
  22. android:layout_height="?attr/actionBarSize"
  23. app:contentInsetEnd="64dp"
  24. app:layout_collapseMode="pin"
  25. app:navigationIcon="@mipmap/ic_navigation_back_white"/>
  1. </com.google.android.material.appbar.CollapsingToolbarLayout>
  2. </com.google.android.material.appbar.AppBarLayout>
  3. <include layout="@layout/content_scrolling"
  4. android:visibility="gone"/>
  5. <androidx.swiperefreshlayout.widget.SwipeRefreshLayout
  6. android:id="@+id/srl_top_back_refresh"
  7. android:layout_width="match_parent"
  8. android:layout_height="match_parent"
  9. app:layout_behavior="com.google.android.material.appbar.AppBarLayout$ScrollingViewBehavior">
  10. <androidx.recyclerview.widget.RecyclerView
  11. android:id="@+id/rv_top_back_list"
  12. android:layout_width="match_parent"
  13. android:layout_height="match_parent"
  14. />
  15. </androidx.swiperefreshlayout.widget.SwipeRefreshLayout>

  1. - 3. activity,布局文件设置好就实现了图片下拉放大,在这里不需要做任何处理

class TopBackActivity: AppCompatActivity() {

  1. val mAdapter by lazy { TopBackAdapter() }
  2. override fun onCreate(savedInstanceState: Bundle?) {
  3. super.onCreate(savedInstanceState)
  4. setContentView(R.layout.activity_top_back)

// setContentView(R.layout.activity_top_back2)

  1. val list = arrayListOf<String>("1","2","3","4","1","2","3","4","1","2","3","4","1","2","3","4","1","2","3","4")
  2. rv_top_back_list?.run {
  3. layoutManager = LinearLayoutManager(this@TopBackActivity)
  4. adapter = mAdapter
  5. }
  6. mAdapter.setList(list)
  7. srl_top_back_refresh.isEnabled = false
  8. }

}

class TopBackAdapter : BaseQuickAdapter(R.layout.item_sticky_two){ override fun convert(holder: BaseViewHolder, item: String) { holder.setText(R.id.tv_item_sticky_two_text,item) }

} ```

四、第三方库

有很多第三方库也可以实现,下面介绍一些比较不错的第三方库。

类似QQ空间,新浪微博个人主页下拉头部放大的布局效果,支持ListView,GridView,ScrollView,WebView,RecyclerView,以及其他的任意View和ViewGroup。支持头部视差动画,阻尼下拉放大,滑动过程监听。