前言
Android应用的界面效果越来越绚丽,有些静态展示信息界面过于单调,设计都会要求加上某些交互效果提高界面的互动性。这里来实现为带有头部大图展示的可滚动界面添加大图缩放效果,让整个布局更具有吸引力。
实现效果
实现方式
首先简单的实现布局:
<?xml version="1.0" encoding="utf-8"?>
<com.example.custom.widget.scroll.MyScrollView
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context="com.example.custom.ZoomHeaderActivity">
<LinearLayout
android:id="@+id/root_view"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent">
<!-- 包含图片控件的头部布局 -->
<FrameLayout
android:id="@+id/scroll_header_view"
android:layout_width="match_parent"
android:layout_height="150dp">
<ImageView
android:id="@+id/scroll_zoom_view"
android:src="@drawable/yellow_mountain"
android:scaleType="centerCrop"
android:layout_width="match_parent"
android:layout_height="match_parent" />
<TextView
android:layout_gravity="bottom|end"
android:layout_margin="10dp"
android:textSize="20sp"
android:textColor="@color/white"
android:text="三国总榜"
android:textStyle="bold"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
</FrameLayout>
<!-- 能够在ScrollView里展示的listView-->
<com.example.custom.widget.listview.MyListView
android:id="@+id/listView"
android:layout_width="match_parent"
android:layout_height="match_parent">
</com.example.custom.widget.listview.MyListView>
</LinearLayout>
</com.example.custom.widget.scroll.MyScrollView>
通常ListView直接放到ScrollView里只会展示一条记录,这里新增了自定义的MyListView确保所有的数据都能够展示在ScrollView里。
public class MyListView extends ListView {
public MyListView(Context context) {
this(context, null);
}
public MyListView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public MyListView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init();
}
private void init() {
setFocusable(false);
}
// 覆盖ListView的onMeasure,让它能在scrollView里展示
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
heightMeasureSpec = MeasureSpec.makeMeasureSpec(Integer.MAX_VALUE >> 2, MeasureSpec.AT_MOST);
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
}
}
考虑整个缩放操作都在图片控件处于最顶部时才会有作用,所以这个时候的ScrollView.getScrollY()也就是竖直方向的滚动值为0,此时顶部的图片控件一直都处于最上方。还可以观察到图片缩放是从中间向两边逐渐变大,并且图片下方的对象被放大的图片挤占了空间,这种缩放显然必须使用属性动画来实现,普通的View动画无法实现这样的操作。
同时需要确定下来图片缩放的最大值和最小值,记录下来图片最开始的初始大小作为最小值,如果在用户向上滑动是缩小到了初始值就不能在做缩放操作了,同时下拉过程中如果到达最大值就不再做缩放操作。前面确定了动画效果实现方式,再来讨论如何根据用户下拉的过程确定中间状态呢?用户在图片处于最上方时做下拉操作,记录下拉当前的位置距离最开始操作的距离,这段距离就可以作为目前用户下拉的中间状态。
public class MyScrollView extends ScrollView {
// 下拉的最大状态值
private static final float MAX_SCALE_RATIO = 1.8f;
private int mPullDownY = -1;
private boolean mIsPullDown = false;
// 图片初始大小
private int originHeight;
private int originWidth;
// 图片所在的头部控件
private View headerView;
public MyScrollView(Context context) {
this(context, null);
}
public MyScrollView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public MyScrollView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init();
}
private void init() {
}
@Override
protected void onFinishInflate() {
super.onFinishInflate();
// 初始化头部控件
headerView = findViewById(R.id.scroll_header_view);
}
@Override
public boolean onTouchEvent(MotionEvent ev) {
if (headerView == null) {
return super.onTouchEvent(ev);
}
switch (ev.getActionMasked()) {
case MotionEvent.ACTION_MOVE:
// 如果当前头部图片顶部没有在屏幕的顶部,与屏幕顶部有偏移,那么不应该做缩放动画
if (getScrollY() != 0) {
return super.onTouchEvent(ev);
}
if (!mIsPullDown && getScrollY() == 0 && mPullDownY == -1) {
// 如果图片顶部和屏幕顶部重合,那么此时可以监控用户触摸事件
mPullDownY = (int) ev.getY();
return super.onTouchEvent(ev);
} else if (!mIsPullDown) {
// 判断用户当前是否实在做滑动还是普通的触摸事件
int diff = (int) (ev.getY() - mPullDownY);
if (diff < 0) { // 向上推动,这时不应该做动画
return super.onTouchEvent(ev);
} else { // 向下拉动
// 记录下图片的原始大小
mIsPullDown = true;
originHeight = headerView.getHeight();
originWidth = headerView.getWidth();
}
}
// 如果用户正在滑动而且当前位置在用户开始滑动的位置下方
int diff = (int) (ev.getY() - mPullDownY);
if (mIsPullDown && diff > 0) {
// 调整layoutParams改变控件的大小
int height = (int) (originHeight + diff * 0.5f);
float ratio = (float) height / originHeight;
ratio = ratio > MAX_SCALE_RATIO ? MAX_SCALE_RATIO : ratio;
ViewGroup.MarginLayoutParams layoutParams = (ViewGroup.MarginLayoutParams) headerView.getLayoutParams();
layoutParams.width = (int) (originWidth * ratio);
layoutParams.height = (int) (originHeight * ratio);
layoutParams.leftMargin = -(layoutParams.width - originWidth) / 2;
headerView.requestLayout();
return true;
}
break;
case MotionEvent.ACTION_CANCEL:
case MotionEvent.ACTION_UP:
// 如果用户放开滑动,那么使用属性动画回到最初状态,
mIsPullDown = false;
mPullDownY = -1;
float startRatio = (float) headerView.getLayoutParams().width / originWidth;
ValueAnimator valueAnimator = ValueAnimator.ofFloat(startRatio, 1.0f).setDuration(300);
// 回去的时候先加速再减速
valueAnimator.setInterpolator(new AccelerateDecelerateInterpolator());
valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
// 根据中间状态修改图片的大小 ViewGroup.MarginLayoutParams layoutParams = (ViewGroup.MarginLayoutParams) headerView.getLayoutParams();
layoutParams.width = (int) ((float) animation.getAnimatedValue() * originWidth);
layoutParams.height = (int) ((float) animation.getAnimatedValue() * originHeight);
layoutParams.leftMargin = -(layoutParams.width - originWidth) / 2;
headerView.requestLayout();
}
});
valueAnimator.start();
break;
}
return super.onTouchEvent(ev);
}
}