一、效果图
二、使用 ScrollView 实现
2.1 实现方式一
对于图片的缩放实现都放在工具类中,实现比较简单。
布局文件 ``` <?xml version=”1.0” encoding=”utf-8”?>
<TextViewandroid:layout_width="match_parent"android:layout_height="250dp"android:background="@color/yellow_FF9B52"/><TextViewandroid:layout_width="match_parent"android:layout_height="250dp"android:background="@color/green_07C0C2"/><TextViewandroid:layout_width="match_parent"android:layout_height="250dp"android:background="@color/red_F7E6ED"/><TextViewandroid:layout_width="match_parent"android:layout_height="250dp"android:background="@color/black_999999"/></LinearLayout>
- 2. activity实现,实现下面这一句就可以了
ScrollZoomUtil.scrollZoom(sv_zoom_scroll_outer, iv_zoom_scroll_icon)
- 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;
// 滑动放大系数,系数越大,滑动时放大程度越大public static float mScaleRatio = 0.4f;// 最大的放大倍数public static float mScaleTimes = 2f;public static void scrollZoom(final View scrollView, final View zoomView) {zoomView.post(new Runnable() {@Overridepublic void run() {//记录控件原始宽高sZoomViewOriginWidth = zoomView.getMeasuredWidth();sZoomViewOriginHeight = zoomView.getMeasuredHeight();}});scrollView.setOnTouchListener(new View.OnTouchListener() {@Overridepublic boolean onTouch(View v, MotionEvent event) {switch (event.getAction()) {case MotionEvent.ACTION_DOWN://当被监听的可滚动控件已经滚动到顶部时才可以进行缩放,置标志位为 true,记录下按下的纵坐标if (scrollView.getScrollY() == 0) {sStartZoom = true;sLastY = event.getY();return true;}break;case MotionEvent.ACTION_MOVE:if (sStartZoom) {//计算滑动的距离float distanceY = event.getY() - sLastY;if (distanceY > 0) {//当滑动距离大于 0 时表示下拉操作,对需要缩放的控件进行放大操作zoom(zoomView, distanceY);} else {//当滑动距离小于 0 时表示上拉操作,判断需要缩放的控件的当前宽高,如果比原始宽高大则先缩放至原始宽高if (zoomView.getMeasuredWidth() > sZoomViewOriginWidth) {zoom(zoomView, distanceY);} else {//需要缩放的控件的当前宽高与原始宽高相等时再进行上拉,则进行正常的滚动操作break;}}return true;}case MotionEvent.ACTION_UP://手指抬起后恢复原始状态sLastY = 0;sStartZoom = false;restore(zoomView);break;}return false;}});}/*** Description:缩放* Date:2018/10/22*/private static void zoom(View zoomView, float distanceY) {if (sZoomViewOriginWidth <= 0 || sZoomViewOriginHeight <= 0) {return;}//控制缩放比例float scaleTimes = (float) ((sZoomViewOriginWidth + distanceY) / (sZoomViewOriginWidth * 1.0));
// 如超过最大放大倍数,直接返回 if (scaleTimes > mScaleTimes) return;
ViewGroup.LayoutParams layoutParams = zoomView.getLayoutParams();//控件高为原始高度加上滑动距离乘以一个系数(与滑动距离 1:1 的话变化太大)LogUtils.e(distanceY);layoutParams.height = (int) (sZoomViewOriginHeight + distanceY * 0.4);//控件宽等比例变化layoutParams.width = (int) ((sZoomViewOriginWidth * (sZoomViewOriginHeight + distanceY * 0.4)) / sZoomViewOriginHeight);// 设置控件水平居中((ViewGroup.MarginLayoutParams) layoutParams).setMargins(-(layoutParams.width - sZoomViewOriginWidth) / 2, 0, 0, 0);zoomView.setLayoutParams(layoutParams);}/*** Description:恢复成初始状态* Date:2018/10/22*/private static void restore(final View zoomView) {final float distance = zoomView.getMeasuredWidth() - sZoomViewOriginWidth;// 设置动画ValueAnimator anim = ObjectAnimator.ofFloat(distance, 0.0F).setDuration((long) (distance * mReplyRatio));anim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {@Overridepublic void onAnimationUpdate(ValueAnimator animation) {zoom(zoomView,(Float) animation.getAnimatedValue());}});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(); } }
<a name="6ZylP"></a>#### 2.2 通过自定义 ScrollView 实现这种方式直接在布局文件中使用即可,注意放大的view,默认为第一个子view。- 1. 自定义 View
public class HeadZoomScrollView extends ScrollView {
public HeadZoomScrollView(Context context) {super(context);}public HeadZoomScrollView(Context context, AttributeSet attrs) {super(context, attrs);}public HeadZoomScrollView(Context context, AttributeSet attrs, int defStyleAttr) {super(context, attrs, defStyleAttr);}// 用于记录下拉位置private float y = 0f;// zoomView原本的宽高private int zoomViewWidth = 0;private int zoomViewHeight = 0;// 是否正在放大private boolean mScaling = false;// 放大的view,默认为第一个子viewprivate View zoomView;public void setZoomView(View zoomView) {this.zoomView = zoomView;}// 滑动放大系数,系数越大,滑动时放大程度越大private float mScaleRatio = 0.4f;public void setmScaleRatio(float mScaleRatio) {this.mScaleRatio = mScaleRatio;}// 最大的放大倍数private float mScaleTimes = 2f;public void setmScaleTimes(int mScaleTimes) {this.mScaleTimes = mScaleTimes;}// 回弹时间系数,系数越小,回弹越快private float mReplyRatio = 0.5f;public void setmReplyRatio(float mReplyRatio) {this.mReplyRatio = mReplyRatio;}@Overrideprotected void onFinishInflate() {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); } } }
@Overridepublic boolean onTouchEvent(MotionEvent ev) {if (zoomViewWidth <= 0 || zoomViewHeight <= 0) {zoomViewWidth = zoomView.getMeasuredWidth();zoomViewHeight = zoomView.getMeasuredHeight();}if (zoomView == null || zoomViewWidth <= 0 || zoomViewHeight <= 0) {return super.onTouchEvent(ev);}switch (ev.getAction()) {case MotionEvent.ACTION_MOVE:if (!mScaling) {if (getScrollY() == 0) {y = ev.getY();//滑动到顶部时,记录位置} else {break;}}int distance = (int) ((ev.getY() - y) * mScaleRatio);if (distance < 0) break;//若往下滑动mScaling = true;setZoom(distance);return true;case MotionEvent.ACTION_UP:mScaling = false;replyView();break;}return super.onTouchEvent(ev);}/*** 放大view*/private void setZoom(float s) {float scaleTimes = (float) ((zoomViewWidth + s) / (zoomViewWidth * 1.0));
// 如超过最大放大倍数,直接返回 if (scaleTimes > mScaleTimes) return;
ViewGroup.LayoutParams layoutParams = zoomView.getLayoutParams();layoutParams.width = (int) (zoomViewWidth + s);layoutParams.height = (int) (zoomViewHeight * ((zoomViewWidth + s) / zoomViewWidth));
// 设置控件水平居中 ((MarginLayoutParams) layoutParams).setMargins(-(layoutParams.width - zoomViewWidth) / 2, 0, 0, 0); zoomView.setLayoutParams(layoutParams); }
/*** 回弹*/private void replyView() {final float distance = zoomView.getMeasuredWidth() - zoomViewWidth;// 设置动画ValueAnimator anim = ObjectAnimator.ofFloat(distance, 0.0F).setDuration((long) (distance * mReplyRatio));anim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {@Overridepublic void onAnimationUpdate(ValueAnimator animation) {setZoom((Float) animation.getAnimatedValue());}});anim.start();}@Overrideprotected void onScrollChanged(int l, int t, int oldl, int oldt) {super.onScrollChanged(l, t, oldl, oldt);if (onScrollListener != null) onScrollListener.onScroll(l, t, oldl, oldt);}private OnScrollListener onScrollListener;public void setOnScrollListener(OnScrollListener onScrollListener) {this.onScrollListener = onScrollListener;}/*** 滑动监听*/public interface OnScrollListener {void onScroll(int scrollX, int scrollY, int oldScrollX, int oldScrollY);}
}
<a name="aZBuZ"></a>## 三、通过 MD 实现- 1.这种实现方式通常是通过自定义 behavior 来实现,注意要放大的图片布局控件要加上android:tag="overScroll",下面是 behavior 类:
public class AppbarZoomBehavior extends AppBarLayout.Behavior {
private ImageView mImageView;private int mAppbarHeight;//记录AppbarLayout原始高度private int mImageViewHeight;//记录ImageView原始高度private static final float MAX_ZOOM_HEIGHT = 500;//放大最大高度private float mTotalDy;//手指在Y轴滑动的总距离private float mScaleValue;//图片缩放比例private int mLastBottom;//Appbar的变化高度private boolean isAnimate;//是否做动画标志private static final String TAG = "overScroll";public AppbarZoomBehavior(Context context, AttributeSet attrs) {super(context, attrs);}@Overridepublic boolean onLayoutChild(CoordinatorLayout parent, AppBarLayout abl, int layoutDirection) {boolean handled = super.onLayoutChild(parent, abl, layoutDirection);init(abl);return handled;}/*** 进行初始化操作,在这里获取到ImageView的引用,和Appbar的原始高度** @param abl*/private void init(AppBarLayout abl) {abl.setClipChildren(false);mAppbarHeight = abl.getHeight();if (mImageView == null) {mImageView = abl.findViewWithTag(TAG);}if (mImageView != null) {mImageViewHeight = mImageView.getHeight();}}/*** 是否处理嵌套滑动** @param parent* @param child* @param directTargetChild* @param target* @param nestedScrollAxes* @param type* @return*/@Overridepublic boolean onStartNestedScroll(CoordinatorLayout parent, AppBarLayout child, View directTargetChild, View target, int nestedScrollAxes, int type) {isAnimate = true;return true;}/*** 在这里做具体的滑动处理** @param coordinatorLayout* @param child* @param target* @param dx* @param dy* @param consumed* @param type*/@Overridepublic void onNestedPreScroll(CoordinatorLayout coordinatorLayout, AppBarLayout child, View target, int dx, int dy, int[] consumed, int type) {if (mImageView != null && child.getBottom() >= mAppbarHeight && dy < 0 && type == ViewCompat.TYPE_TOUCH) {zoomHeaderImageView(child, dy);} else {if (mImageView != null && child.getBottom() > mAppbarHeight && dy > 0 && type == ViewCompat.TYPE_TOUCH) {consumed[1] = dy;zoomHeaderImageView(child, dy);} else {if (valueAnimator == null || !valueAnimator.isRunning()) {super.onNestedPreScroll(coordinatorLayout, child, target, dx, dy, consumed, type);}}}}/*** 对ImageView进行缩放处理,对AppbarLayout进行高度的设置** @param abl* @param dy*/private void zoomHeaderImageView(AppBarLayout abl, int dy) {mTotalDy += -dy;mTotalDy = Math.min(mTotalDy, MAX_ZOOM_HEIGHT);mScaleValue = Math.max(1f, 1f + mTotalDy / MAX_ZOOM_HEIGHT);ViewCompat.setScaleX(mImageView, mScaleValue);ViewCompat.setScaleY(mImageView, mScaleValue);mLastBottom = mAppbarHeight + (int) (mImageViewHeight / 2 * (mScaleValue - 1));abl.setBottom(mLastBottom);}/*** 处理惯性滑动的情况** @param coordinatorLayout* @param child* @param target* @param velocityX* @param velocityY* @return*/@Overridepublic boolean onNestedPreFling(CoordinatorLayout coordinatorLayout, AppBarLayout child, View target, float velocityX, float velocityY) {if (velocityY > 100) {isAnimate = false;}return super.onNestedPreFling(coordinatorLayout, child, target, velocityX, velocityY);}/*** 滑动停止的时候,恢复AppbarLayout、ImageView的原始状态** @param coordinatorLayout* @param abl* @param target* @param type*/@Overridepublic void onStopNestedScroll(CoordinatorLayout coordinatorLayout, AppBarLayout abl, View target, int type) {recovery(abl);super.onStopNestedScroll(coordinatorLayout, abl, target, type);}ValueAnimator valueAnimator;/*** 通过属性动画的形式,恢复AppbarLayout、ImageView的原始状态** @param abl*/private void recovery(final AppBarLayout abl) {if (mTotalDy > 0) {mTotalDy = 0;if (isAnimate) {valueAnimator = ValueAnimator.ofFloat(mScaleValue, 1f).setDuration(220);valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {@Overridepublic void onAnimationUpdate(ValueAnimator animation) {float value = (float) animation.getAnimatedValue();ViewCompat.setScaleX(mImageView, value);ViewCompat.setScaleY(mImageView, value);abl.setBottom((int) (mLastBottom - (mLastBottom - mAppbarHeight) * animation.getAnimatedFraction()));}});valueAnimator.start();} else {ViewCompat.setScaleX(mImageView, 1f);ViewCompat.setScaleY(mImageView, 1f);abl.setBottom(mAppbarHeight);}}}
}
- 2. 布局文件
<?xml version=”1.0” encoding=”utf-8”?>
<com.google.android.material.appbar.CollapsingToolbarLayoutandroid:id="@+id/collapsingToolbarLayout"android:layout_width="match_parent"android:layout_height="wrap_content"app:contentScrim="@color/blue_74D3FF"app:layout_scrollFlags="scroll|exitUntilCollapsed|snap"app:statusBarScrim="@color/red_FF6D84"><ImageViewandroid:id="@+id/siv_picture"android:layout_width="match_parent"android:layout_height="200dp"android:fitsSystemWindows="true"android:src="@mipmap/wuhuang"android:scaleType="centerCrop"android:tag="overScroll"app:layout_collapseMode="parallax"/><androidx.appcompat.widget.Toolbarandroid:id="@+id/toolbar"android:layout_width="match_parent"android:layout_height="?attr/actionBarSize"app:contentInsetEnd="64dp"app:layout_collapseMode="pin"app:navigationIcon="@mipmap/ic_navigation_back_white"/>
</com.google.android.material.appbar.CollapsingToolbarLayout></com.google.android.material.appbar.AppBarLayout><include layout="@layout/content_scrolling"android:visibility="gone"/><androidx.swiperefreshlayout.widget.SwipeRefreshLayoutandroid:id="@+id/srl_top_back_refresh"android:layout_width="match_parent"android:layout_height="match_parent"app:layout_behavior="com.google.android.material.appbar.AppBarLayout$ScrollingViewBehavior"><androidx.recyclerview.widget.RecyclerViewandroid:id="@+id/rv_top_back_list"android:layout_width="match_parent"android:layout_height="match_parent"/></androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
- 3. activity,布局文件设置好就实现了图片下拉放大,在这里不需要做任何处理
class TopBackActivity: AppCompatActivity() {
val mAdapter by lazy { TopBackAdapter() }override fun onCreate(savedInstanceState: Bundle?) {super.onCreate(savedInstanceState)setContentView(R.layout.activity_top_back)
// setContentView(R.layout.activity_top_back2)
val list = arrayListOf<String>("1","2","3","4","1","2","3","4","1","2","3","4","1","2","3","4","1","2","3","4")rv_top_back_list?.run {layoutManager = LinearLayoutManager(this@TopBackActivity)adapter = mAdapter}mAdapter.setList(list)srl_top_back_refresh.isEnabled = false}
}
class TopBackAdapter : BaseQuickAdapter
四、第三方库
有很多第三方库也可以实现,下面介绍一些比较不错的第三方库。
类似QQ空间,新浪微博个人主页下拉头部放大的布局效果,支持ListView,GridView,ScrollView,WebView,RecyclerView,以及其他的任意View和ViewGroup。支持头部视差动画,阻尼下拉放大,滑动过程监听。
- 下拉刷新
- ScrollView
- RecyclerView
- CoordinatorLayout (其他Layout需要处理改Layout的onTouchEvent事件,否则可能无法使用)
