今天要写的这个效果属于刷新类,比较实用,像很多流行的 app 都是用了这种效果,大家熟知的QQ空间、微博个人主页等。
本篇思路其实是完全按照android中已有的思路去实现的这种效果。
1.那么在开始之前,请大家看一下实现的效果图。
2.那么我们一起来分析下实现的原理吧!
图片放大的原理是什么呢?
通过改变图片显示控件 ImageView 的父控件的高度,比如这里的头部 View 是一个 FrameLayout;
FrameLayout 中再通过 add 方法把图片添加进去:addView(ImageView);
其中ImageView有几个属性是要特别注意的,ImageView 的放缩类型为从中间截取
setScaleType(ImageView.ScaleType.CENTER_CROP);
并且宽高设为匹配父控件;所以想要图片有放大效果,只需设置 FrameLayout 的 LayoutParams 中的 height值,通过改变 height 的值从而改变 ImageView 的显示高度。
如果你是对手势事件处理很了解的朋友,对这个效果的实现应该没有什么难度,唯一的一点就是判断何时该放大图片,何时该滚动ListView。
3.原理分析完了,那么大家一起来看下代码吧:
/**
* Created by guaju on 16/09/17.
*/
public class PullZoomListView extends ListView {/*头部View 的容器*/
private FrameLayout mHeaderContainer;
/*头部View 的图片*/
private ImageView mHeaderImg;
/*屏幕的高度*/
private int mScreenHeight;
/*屏幕的宽度*/
private int mScreenWidth;private int mHeaderHeight;/*无效的点*/
private static final int INVALID_POINTER = -1;
/*滑动动画执行的时间*/
private static final int MIN_SETTLE_DURATION = 200; // ms
/*定义了一个时间插值器,根据ViewPage控件来定义的*/
private static final Interpolator sInterpolator = new Interpolator() {public float getInterpolation(float t) {t -= 1.0f;return t * t * t * t * t + 1.0f;}
};/*记录上一次手指触摸的点*/
private float mLastMotionX;
private float mLastMotionY;/*当前活动的点Id,有效的点的Id*/
protected int mActivePointerId = INVALID_POINTER;
/*开始滑动的标志距离*/
private int mTouchSlop;/*放大的倍数*/
private float mScale;
/*上一次放大的倍数*/
private float mLastScale;/*最大放大的倍数*/
private final float mMaxScale = 2.0f;
/*是否需要禁止ListView 的事件响应*/
private boolean isNeedCancelParent;/*这个不解释*/
private OnScrollListener mScrollListener ;/*下拉刷新的阈值*/
private final float REFRESH_SCALE = 1.20F;/*下拉刷新监听*/
private OnRefreshListener mRefreshListener ;public PullZoomListView(Context context) {super(context);init(context);
}public PullZoomListView(Context context, AttributeSet attrs) {super(context, attrs);init(context);
}public PullZoomListView(Context context, AttributeSet attrs, int defStyleAttr) {super(context, attrs, defStyleAttr);init(context);
}private void init(Context context) {/*这里获取的是一个无用值,可忽略*/final ViewConfiguration configuration = ViewConfiguration.get(context);mTouchSlop = ViewConfigurationCompat.getScaledPagingTouchSlop(configuration);/*创建头部View 的容器*/mHeaderContainer = new FrameLayout(context);/*获取屏幕的像素值*/DisplayMetrics metrics = new DisplayMetrics();((Activity) context).getWindowManager().getDefaultDisplay().getMetrics(metrics);mScreenHeight = metrics.heightPixels;mScreenWidth = metrics.widthPixels;/*设置头部View 的初始大小*/mHeaderHeight = (int) ((9 * 1.0f / 16) * mScreenWidth);LayoutParams absLayoutParams = new LayoutParams(mScreenWidth, mHeaderHeight);mHeaderContainer.setLayoutParams(absLayoutParams);/*创建图片显示的View*/mHeaderImg = new ImageView(context);FrameLayout.LayoutParams imgLayoutParams = new FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT);mHeaderImg.setScaleType(ImageView.ScaleType.CENTER_CROP);mHeaderImg.setLayoutParams(imgLayoutParams);mHeaderContainer.addView(mHeaderImg);/*增加头部View*/addHeaderView(mHeaderContainer);/*设置监听事件*/super.setOnScrollListener(new InternalScrollerListener() );}
/*处理事件用*/@Override
public boolean onTouchEvent(MotionEvent ev) {final int action = ev.getAction() & MotionEventCompat.ACTION_MASK;switch (action) {case MotionEvent.ACTION_DOWN:/*计算 x,y 的距离*/int index = MotionEventCompat.getActionIndex(ev);mActivePointerId = MotionEventCompat.getPointerId(ev, index);if (mActivePointerId == INVALID_POINTER)break;mLastMotionX = MotionEventCompat.getX(ev, index);mLastMotionY = MotionEventCompat.getY(ev, index);// 结束动画,目前没做处理,可忽略abortAnimation();/*计算算一次放缩的比例*/mLastScale = (this.mHeaderContainer.getBottom() / this.mHeaderHeight);/*当按下的时候把这个标志为设为有效*/isNeedCancelParent = true ;break;case MotionEvent.ACTION_MOVE:int indexMove = MotionEventCompat.getActionIndex(ev);mActivePointerId = MotionEventCompat.getPointerId(ev, indexMove);if (mActivePointerId == INVALID_POINTER) {/*这里相当于松手*/finishPull();isNeedCancelParent = true ;} else {/*这是一个关键值,通过这个值来判断是否需要放大图片*/if (mHeaderContainer.getBottom() >= mHeaderHeight) {ViewGroup.LayoutParams params = this.mHeaderContainer.getLayoutParams();final float y = MotionEventCompat.getY(ev, indexMove);float dy = y - mLastMotionY;float f = ((y - this.mLastMotionY + this.mHeaderContainer.getBottom()) / this.mHeaderHeight - this.mLastScale)/ 2.0F + this.mLastScale;if ((this.mLastScale <= 1.0D) && (f <= this.mLastScale)) {params.height = this.mHeaderHeight;this.mHeaderContainer.setLayoutParams(params);return super.onTouchEvent(ev);}/*这里设置紧凑度*/dy = dy * 0.5f * (mHeaderHeight * 1.0f / params.height);mLastScale = (dy + params.height) * 1.0f / mHeaderHeight;mScale = clamp(mLastScale, 1.0f, mMaxScale);
// Log.v(“zgy”, “=======mScale=====” + mLastScale+”,f = “+f);params.height = (int) (mHeaderHeight * mScale);mHeaderContainer.setLayoutParams(params);mLastMotionY = y;/*这里,如果图片有放大,则屏蔽ListView 的其他事件响应*/if(isNeedCancelParent ){isNeedCancelParent = false;MotionEvent motionEvent = MotionEvent.obtain(ev);motionEvent.setAction(MotionEvent.ACTION_CANCEL);super.onTouchEvent(motionEvent);}return true;}mLastMotionY = MotionEventCompat.getY(ev, indexMove);}break;case MotionEvent.ACTION_UP:/*结束事件响应,做相应的操作*/finishPull();break;case MotionEvent.ACTION_POINTER_UP:/*这里需要注意,多点处理,这里的处理方式是:如果有两个手指按下,抬起的是后按下的手指,则不做处理* 如果抬起的是最先按下的手指,则复原图片效果。* */int pointUpIndex = MotionEventCompat.getActionIndex(ev);int pointId = MotionEventCompat.getPointerId(ev, pointUpIndex);if (pointId == mActivePointerId) {/*松手执行结束拖拽操作*//*结束事件响应,做相应的操作*/finishPull();}break;}return super.onTouchEvent(ev);
}@Override
public void setOnScrollListener(OnScrollListener l) {mScrollListener = l ;
}private void abortAnimation() {/*啥都没做,暂时没做而已*/
}private void finishPull() {mActivePointerId = INVALID_POINTER;/*这是一个阈值,如果成立,则表示图片已经放大了,在手指抬起的时候需要复原图片*/if (mHeaderContainer.getBottom() > mHeaderHeight){
// Log.v(“zgy”, “===super====onTouchEvent========”);
/这里是下拉刷新的阈值,当达到了,则表示需要刷新,可以添加刷新动画/
if (mScale > REFRESH_SCALE){
if (mRefreshListener != null){
mRefreshListener.onRefresh();
}
}
/图片复原动画/
pullBackAnimation();
}
}/*** 这是属性动画的知识,不懂的可以去看看属性动画的知识*/
private void pullBackAnimation(){ValueAnimator pullBack = ValueAnimator.ofFloat(mScale , 1.0f);pullBack.setInterpolator(sInterpolator);pullBack.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {@Overridepublic void onAnimationUpdate(ValueAnimator animation) {float value = (float) animation.getAnimatedValue();LayoutParams params = (LayoutParams) mHeaderContainer.getLayoutParams();params.height = (int) (mHeaderHeight * value);mHeaderContainer.setLayoutParams(params);}});pullBack.setDuration((long) (MIN_SETTLE_DURATION*mScale));pullBack.start();}/*** 通过事件和点的 id 来获取点的索引** @param ev* @param id* @return*/
private int getPointerIndex(MotionEvent ev, int id) {int activePointerIndex = MotionEventCompat.findPointerIndex(ev, id);if (activePointerIndex == -1)mActivePointerId = INVALID_POINTER;return activePointerIndex;
}public void setOnRefreshListener(OnRefreshListener l){mRefreshListener = l ;
}public ImageView getHeaderImageView() {return this.mHeaderImg;
}private float clamp(float value, float min, float max) {return Math.min(Math.max(value, min), max);
}private class InternalScrollerListener implements OnScrollListener{@Overridepublic void onScrollStateChanged(AbsListView view, int scrollState) {if (mScrollListener != null){mScrollListener.onScrollStateChanged(view,scrollState);}}@Overridepublic void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) {float diff = mHeaderHeight - mHeaderContainer.getBottom();if ((diff > 0.0F) && (diff < mHeaderHeight)) {int i = (int) (0.3D * diff);mHeaderImg.scrollTo(0, -i);} else if (mHeaderImg.getScrollY() != 0) {mHeaderImg.scrollTo(0, 0);}Log.v("zgy","=========height==="+getScrolledY());if (mScrollListener != null){mScrollListener.onScroll(view, firstVisibleItem, visibleItemCount, totalItemCount);}}
}public int getScrolledY() {View c = getChildAt(0);if (c == null) {return 0;}int firstVisiblePosition = getFirstVisiblePosition();int top = c.getTop();int headerHeight = 0;if (firstVisiblePosition >= 1) {headerHeight = mHeaderHeight;}return -top + firstVisiblePosition * c.getHeight() + headerHeight;
}public interface OnRefreshListener {void onRefresh() ;
}public void computeRefresh(){if (mActivePointerId != INVALID_POINTER){}
}
}