Last active
November 8, 2015 15:53
-
-
Save AvatarQing/1426c59c0047b6a41bfb to your computer and use it in GitHub Desktop.
PullToZoomListView改进+中文注释
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package com.matrixxun.pulltozoomlistsimple; | |
import android.content.Context; | |
import android.os.SystemClock; | |
import android.util.AttributeSet; | |
import android.util.DisplayMetrics; | |
import android.util.Log; | |
import android.view.MotionEvent; | |
import android.view.ViewGroup; | |
import android.view.WindowManager; | |
import android.view.animation.Interpolator; | |
import android.widget.AbsListView; | |
import android.widget.ImageView; | |
import android.widget.ImageView.ScaleType; | |
import android.widget.ListView; | |
import android.widget.RelativeLayout; | |
public class PullToZoomListView extends ListView implements | |
AbsListView.OnScrollListener { | |
private static final String TAG = PullToZoomListView.class.getSimpleName(); | |
/** 无效值 */ | |
private static final int INVALID_VALUE = -1; | |
// 自定义的插值器 | |
private static final Interpolator sInterpolator = new Interpolator() { | |
public float getInterpolation(float fraction) { | |
float f = fraction - 1.0F; | |
return 1.0F + f * (f * (f * (f * f))); | |
} | |
}; | |
private int mActivePointerId = INVALID_VALUE; | |
/** 头部View容器 */ | |
private RelativeLayout mHeaderContainer; | |
/** 头部View的高度 */ | |
private int mHeaderHeight; | |
/** 头部View里的图片视图 */ | |
private ImageView mHeaderImage; | |
private float mLastMotionY = INVALID_VALUE; | |
private float mLastScale = INVALID_VALUE; | |
private float mMaxScale = INVALID_VALUE; | |
private float mHeaderScrollSpeed = 1f; | |
private AbsListView.OnScrollListener mOnScrollListener; | |
private ScalingRunnalable mScalingRunnalable; | |
/** 屏幕高度 */ | |
private int mScreenHeight; | |
private ImageView mShadow; | |
public PullToZoomListView(Context context) { | |
super(context); | |
init(context); | |
} | |
public PullToZoomListView(Context context, AttributeSet paramAttributeSet) { | |
super(context, paramAttributeSet); | |
init(context); | |
} | |
public PullToZoomListView(Context context, AttributeSet paramAttributeSet, | |
int paramInt) { | |
super(context, paramAttributeSet, paramInt); | |
init(context); | |
} | |
private void endScraling() { | |
if (this.mHeaderContainer.getBottom() >= this.mHeaderHeight) { | |
Log.d(TAG, "endScraling"); | |
} | |
this.mScalingRunnalable.startAnimation(200L); | |
} | |
private void init(Context context) { | |
// 获取屏幕高度 | |
DisplayMetrics localDisplayMetrics = new DisplayMetrics(); | |
((WindowManager) context.getSystemService(Context.WINDOW_SERVICE)) | |
.getDefaultDisplay().getMetrics(localDisplayMetrics); | |
this.mScreenHeight = localDisplayMetrics.heightPixels; | |
// 生成头部View | |
this.mHeaderContainer = new RelativeLayout(context); | |
this.mHeaderImage = new ImageView(context); | |
// 获得屏幕宽度 | |
int i = localDisplayMetrics.widthPixels; | |
// 设置头部View的宽高 | |
setHeaderViewSize(i, (int) (9.0F * (i / 16.0F))); | |
// 生成阴影视图 | |
// this.mShadow = new ImageView(context); | |
// FrameLayout.LayoutParams shadowLp = new FrameLayout.LayoutParams( | |
// FrameLayout.LayoutParams.MATCH_PARENT, | |
// FrameLayout.LayoutParams.WRAP_CONTENT); | |
// shadowLp.gravity = 80; | |
// this.mShadow.setLayoutParams(shadowLp); | |
// 将图片和阴影视图添加到头部View容器 | |
RelativeLayout.LayoutParams params = new RelativeLayout.LayoutParams( | |
RelativeLayout.LayoutParams.MATCH_PARENT, | |
RelativeLayout.LayoutParams.MATCH_PARENT); | |
params.topMargin = 0; | |
this.mHeaderContainer.addView(this.mHeaderImage, params); | |
// this.mHeaderContainer.addView(this.mShadow); | |
// 将头部View添加到ListView里 | |
addHeaderView(this.mHeaderContainer); | |
this.mScalingRunnalable = new ScalingRunnalable(); | |
// 设置滑动监听 | |
super.setOnScrollListener(this); | |
} | |
private void onSecondaryPointerUp(MotionEvent ev) { | |
int i = ev.getActionIndex(); | |
if (ev.getPointerId(i) == this.mActivePointerId) { | |
if (i != 0) { | |
this.mLastMotionY = ev.getY(0); | |
this.mActivePointerId = ev.getPointerId(0); | |
return; | |
} | |
} | |
} | |
/** | |
* 重置头部View缩放相关的参数 | |
*/ | |
private void reset() { | |
this.mActivePointerId = INVALID_VALUE; | |
this.mLastMotionY = INVALID_VALUE; | |
this.mMaxScale = INVALID_VALUE; | |
this.mLastScale = INVALID_VALUE; | |
} | |
public ImageView getHeaderView() { | |
return this.mHeaderImage; | |
} | |
@Override | |
protected void onLayout(boolean changed, int l, int t, int r, int b) { | |
super.onLayout(changed, l, t, r, b); | |
if (this.mHeaderHeight == 0) { | |
// 记录头部View正常大小时的高度 | |
this.mHeaderHeight = this.mHeaderContainer.getHeight(); | |
} | |
} | |
@Override | |
public void onScroll(AbsListView view, int firstVisibleItem, | |
int visibleItemCount, int totalItemCount) { | |
Log.d("test", "onScroll()"); | |
float space = this.mHeaderHeight - this.mHeaderContainer.getBottom(); | |
Log.d(TAG, "onScroll()->Header scrolled space:" + space); | |
if ((space > 0.0F) && (space < this.mHeaderHeight)) { | |
int i = (int) (mHeaderScrollSpeed * space); | |
this.mHeaderImage.scrollTo(0, -i); | |
} else if (this.mHeaderImage.getScrollY() != 0) { | |
this.mHeaderImage.scrollTo(0, 0); | |
} | |
if (this.mOnScrollListener != null) { | |
this.mOnScrollListener.onScroll(view, firstVisibleItem, | |
visibleItemCount, totalItemCount); | |
} | |
} | |
public void onScrollStateChanged(AbsListView paramAbsListView, int paramInt) { | |
if (this.mOnScrollListener != null) { | |
this.mOnScrollListener.onScrollStateChanged(paramAbsListView, | |
paramInt); | |
} | |
} | |
@Override | |
public boolean onTouchEvent(MotionEvent ev) { | |
// 过滤掉多点触控的触控点信息(Action的高八位),获取触摸事件类型(Action的低八位) | |
int actionMasked = ev.getActionMasked(); | |
switch (actionMasked) { | |
case MotionEvent.ACTION_OUTSIDE: | |
// 用户触碰超出了正常的UI边界 | |
case MotionEvent.ACTION_DOWN: | |
// 用户开始触摸 | |
Log.d(TAG, "onTouchEvent()->ACTION_DOWN or ACTION_OUTSIDE"); | |
if (!this.mScalingRunnalable.mIsFinished) { | |
this.mScalingRunnalable.abortAnimation(); | |
} | |
// 记录开始触摸时的y坐标 | |
this.mLastMotionY = ev.getY(); | |
// 记录第一个有效触摸的手指的id | |
this.mActivePointerId = ev.getPointerId(0); | |
// 记录头部View最大的放大比例 | |
this.mMaxScale = (this.mScreenHeight / this.mHeaderHeight); | |
// 记录开始触摸时头部View已经缩放的比例 | |
this.mLastScale = (this.mHeaderContainer.getBottom() / this.mHeaderHeight); | |
break; | |
case MotionEvent.ACTION_MOVE: | |
// 用户在移动(手指或者其他) | |
Log.d(TAG, "onTouchEvent()->ACTION_MOVE"); | |
Log.d(TAG, "mActivePointerId" + mActivePointerId); | |
// 记录第一个有效触摸手指的索引 | |
int pointerIndex = ev.findPointerIndex(this.mActivePointerId); | |
// 检查手指索引的有效性 | |
if (pointerIndex == INVALID_VALUE) { | |
// 无效的手指指针索引 | |
Log.e(TAG, "Invalid pointerId=" + this.mActivePointerId | |
+ " in onTouchEvent"); | |
} else {// 有效的手指指针索引 | |
// 确保滑动时滑动前的y坐标记录正确 | |
if (this.mLastMotionY == INVALID_VALUE) { | |
this.mLastMotionY = ev.getY(pointerIndex); | |
} | |
// 如果头部达到放大标准或者已经放大(头容器的底部坐标达到或者超出了正常大小的高度),就处理放大头部View逻辑 | |
// if (this.mHeaderContainer.getBottom() >= this.mHeaderHeight) | |
// { | |
// 获取头容器当前布局参数 | |
ViewGroup.LayoutParams hlp = this.mHeaderContainer | |
.getLayoutParams(); | |
// 计算新的缩放比例 | |
float newScale = ((ev.getY(pointerIndex) - this.mLastMotionY + this.mHeaderContainer | |
.getBottom()) / this.mHeaderHeight - this.mLastScale) | |
/ 2.0F + this.mLastScale; | |
// 如果上一次的缩放比正常比例要小,并且新的缩放比也小于上一次的缩放比 | |
// XXX 为什么新的缩放比也小于上一次的缩放比? | |
if ((this.mLastScale <= 1.0f)) { | |
mHeaderImage.setScaleType(ScaleType.FIT_CENTER); | |
// // 那就恢复正常大小 | |
// hlp.height = this.mHeaderHeight; | |
// this.mHeaderContainer.setLayoutParams(hlp); | |
// // 调用父类方法处理onScroll事件 | |
// return super.onTouchEvent(ev); | |
newScale = (mHeaderContainer.getBottom() + (ev | |
.getY(pointerIndex) - this.mLastMotionY)) | |
/ this.mHeaderHeight; | |
Log.d("test", | |
"newScale:" | |
+ newScale | |
+ ",mHeaderContainer.getBottom():" | |
+ mHeaderContainer.getBottom() | |
+ ",deltaY:" | |
+ (ev.getY(pointerIndex) - this.mLastMotionY) | |
+ ",mHeaderHeight:" + mHeaderHeight); | |
} else { | |
mHeaderImage.setScaleType(ScaleType.CENTER_CROP); | |
} | |
// 记录最后一次的缩放比 | |
this.mLastScale = Math.min(Math.max(0, newScale), | |
this.mMaxScale); | |
// 计算头容器新的高度 | |
int newHeight = (int) (this.mHeaderHeight * this.mLastScale); | |
Log.d("test", "hlp.height:" + hlp.height); | |
if (newHeight <= 10) { | |
Log.d("test", "newHeight" + newHeight); | |
hlp.height = 1; | |
this.mHeaderContainer.setLayoutParams(hlp); | |
this.mLastScale = 0; | |
return true; | |
} | |
// 新的高度不能超过整个屏幕的高度 | |
if (newHeight < this.mScreenHeight) { | |
// 将新的高度参数应用到头容器上 | |
// 容器里的布局会自动伸缩 | |
hlp.height = newHeight; | |
this.mHeaderContainer.setLayoutParams(hlp); | |
} | |
// 记录最后一次手指触摸的y坐标 | |
this.mLastMotionY = ev.getY(pointerIndex); | |
// 消耗掉触摸事件 | |
Log.d("test", "this.mLastScale" + this.mLastScale); | |
if (this.mLastScale <= 0.1) { | |
return super.onTouchEvent(ev); | |
} | |
return true; | |
// } | |
// this.mLastMotionY = ev.getY(pointerIndex); | |
} | |
break; | |
case MotionEvent.ACTION_UP: | |
// 用户抬起了手指 | |
Log.d(TAG, "onTouchEvent()->ACTION_UP"); | |
reset(); | |
endScraling(); | |
break; | |
case MotionEvent.ACTION_CANCEL: | |
// 表示手势被取消了 | |
Log.d(TAG, "onTouchEvent()->ACTION_CANCEL"); | |
int i = ev.getActionIndex(); | |
this.mLastMotionY = ev.getY(i); | |
this.mActivePointerId = ev.getPointerId(i); | |
break; | |
case MotionEvent.ACTION_POINTER_DOWN: | |
// 有一个非主要的手指按下了. | |
Log.d(TAG, "onTouchEvent()->ACTION_POINTER_DOWN"); | |
onSecondaryPointerUp(ev); | |
this.mLastMotionY = ev.getY(ev | |
.findPointerIndex(this.mActivePointerId)); | |
break; | |
case MotionEvent.ACTION_POINTER_UP: | |
// 一个非主要的手指抬起来了 | |
Log.d(TAG, "onTouchEvent()->ACTION_POINTER_UP"); | |
break; | |
} | |
return super.onTouchEvent(ev); | |
} | |
/** | |
* 设置头部View的宽、高 | |
* | |
* @param width | |
* @param height | |
*/ | |
public void setHeaderViewSize(int width, int height) { | |
Object lp = this.mHeaderContainer.getLayoutParams(); | |
if (lp == null) { | |
lp = new AbsListView.LayoutParams(width, height); | |
} | |
((ViewGroup.LayoutParams) lp).width = width; | |
((ViewGroup.LayoutParams) lp).height = height; | |
this.mHeaderContainer.setLayoutParams((ViewGroup.LayoutParams) lp); | |
this.mHeaderHeight = height; | |
} | |
public void setOnScrollListener( | |
AbsListView.OnScrollListener paramOnScrollListener) { | |
this.mOnScrollListener = paramOnScrollListener; | |
} | |
public void setShadow(int paramInt) { | |
this.mShadow.setBackgroundResource(paramInt); | |
} | |
private class ScalingRunnalable implements Runnable { | |
private long mDuration; | |
private boolean mIsFinished = true; | |
private float mScale; | |
private long mStartTime; | |
ScalingRunnalable() { | |
} | |
public void abortAnimation() { | |
this.mIsFinished = true; | |
} | |
public boolean isFinished() { | |
return this.mIsFinished; | |
} | |
public void run() { | |
float scale = 0f; | |
ViewGroup.LayoutParams lp = null; | |
if ((!this.mIsFinished) && (this.mScale > 1.0D)) { | |
// 头部View缩小动画播放开始,已经逝去的时间占总时间的百分比 | |
float animatedTimePercent = ((float) SystemClock | |
.currentThreadTimeMillis() - (float) this.mStartTime) | |
/ (float) this.mDuration; | |
// 计算缩放量 | |
scale = this.mScale | |
- (this.mScale - 1.0F) | |
* PullToZoomListView.sInterpolator | |
.getInterpolation(animatedTimePercent); | |
lp = PullToZoomListView.this.mHeaderContainer.getLayoutParams(); | |
if (scale > 1.0F) { | |
Log.d(TAG, "ScalingRunnalable->run()-> scale > 1.0"); | |
// 计算头容器的高度 | |
lp.height = ((int) (scale * PullToZoomListView.this.mHeaderHeight)); | |
// 参数应用到头部View上 | |
PullToZoomListView.this.mHeaderContainer | |
.setLayoutParams(lp); | |
// 播放动画,不停的更新参数和布局 | |
PullToZoomListView.this.post(this); | |
return; | |
} | |
// 头部View缩小到原大小时停止缩小动画 | |
this.mIsFinished = true; | |
} | |
} | |
/** | |
* 以一个动画的过程将头部View缩放到正常的大小 | |
* | |
* @param duration | |
* 动画时长 | |
*/ | |
public void startAnimation(long duration) { | |
this.mStartTime = SystemClock.currentThreadTimeMillis(); | |
this.mDuration = duration; | |
this.mScale = ((float) (PullToZoomListView.this.mHeaderContainer | |
.getBottom()) / PullToZoomListView.this.mHeaderHeight); | |
this.mIsFinished = false; | |
PullToZoomListView.this.post(this); | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment