一、效果图
二、使用 ScrollView 实现
2.1 实现方式一
对于图片的缩放实现都放在工具类中,实现比较简单。
布局文件 ``` <?xml version=”1.0” encoding=”utf-8”?>
<TextView
android:layout_width="match_parent"
android:layout_height="250dp"
android:background="@color/yellow_FF9B52"/>
<TextView
android:layout_width="match_parent"
android:layout_height="250dp"
android:background="@color/green_07C0C2"/>
<TextView
android:layout_width="match_parent"
android:layout_height="250dp"
android:background="@color/red_F7E6ED"/>
<TextView
android: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() {
@Override
public void run() {
//记录控件原始宽高
sZoomViewOriginWidth = zoomView.getMeasuredWidth();
sZoomViewOriginHeight = zoomView.getMeasuredHeight();
}
});
scrollView.setOnTouchListener(new View.OnTouchListener() {
@Override
public 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() {
@Override
public 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,默认为第一个子view
private 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;
}
@Override
protected 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); } } }
@Override
public 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() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
setZoom((Float) animation.getAnimatedValue());
}
});
anim.start();
}
@Override
protected 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);
}
@Override
public 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
*/
@Override
public 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
*/
@Override
public 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
*/
@Override
public 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
*/
@Override
public 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() {
@Override
public 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.CollapsingToolbarLayout
android: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"
>
<ImageView
android: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.Toolbar
android: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.SwipeRefreshLayout
android: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.RecyclerView
android: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事件,否则可能无法使用)