项目中需要用到淘宝商品详情页面的下拉黏滞效果,刚开始的想法比较复杂,是通过投机取巧的方式来大致实现的,但是效果很不好,勉强可以使用,这怎么能行?后来自己尝试着去优化,感觉一个ListView就可以实现,于是就去用listView去实现了一下,主要用到了ListView的smoothScrollToPosition这个方法,做到最后,发现smoothScrollToPosition这个方法的一个bug。
假如当前ListView显示的是position为0,但是position为0的item只是显示了一部分,你调用smoothScrollToPosition方法,此时listView是不会滚动的,因为Android源代码认为 :你当前显示的position 0,你要滚动到position 0,这不是扯淡嘛!所以这个方法失效了,但是从StackOverFlow上面搜索,都是Android的一个bug!shit~将要实现的效果就这样泡汤了。
VelocityTracker--顾名思义即速度跟踪,在android中主要应用于touch event, VelocityTracker通过跟踪一连串事件实时计算出
当前的速度,这样的用法在android系统空间中随处可见,比如Gestures中的Fling, Scrolling等
//获取一个VelocityTracker对象, 用完后记得回收
//回收后代表你不需要使用了,系统将此对象在此分配到其他请求者
static public VelocityTracker obtain();
public void recycle();
//计算当前速度, 其中units是单位表示, 1代表px/毫秒, 1000代表px/秒, ..
//maxVelocity此次计算速度你想要的最大值
public void computeCurrentVelocity(int units, float maxVelocity);
//经过一次computeCurrentVelocity后你就可以用以下几个方法获取此次计算的值
//id是touch event触摸点的ID, 来为多点触控标识,有这个标识在计算时可以忽略
//其他触点干扰,当然干扰肯定是有的
public float getXVelocity();
public float getYVelocity();
public float getXVelocity(int id);
public float getYVelocity(int id);
2:
ViewConfiguration--该类中需要定义的是系统的一些常量,方面我们的使用,尽量和系统的保持一致,我们不用自己重复的定义这个常量,况且自己定义的不一定合适。代码如下:
/**
* 包含了方法和标准的常量用来设置UI的超时、大小和距离
*/
public class ViewConfiguration {
// 设定水平滚动条的宽度和垂直滚动条的高度,单位是像素px
private static final int SCROLL_BAR_SIZE = 10;
//定义滚动条逐渐消失的时间,单位是毫秒
private static final int SCROLL_BAR_FADE_DURATION = 250;
// 默认的滚动条多少秒之后消失,单位是毫秒
private static final int SCROLL_BAR_DEFAULT_DELAY = 300;
// 定义边缘地方褪色的长度
private static final int FADING_EDGE_LENGTH = 12;
//定义子控件按下状态的持续事件
private static final int PRESSED_STATE_DURATION = 125;
//定义一个按下状态转变成长按状态的转变时间
private static final int LONG_PRESS_TIMEOUT = 500;
//定义用户在按住适当按钮,弹出全局的对话框的持续时间
private static final int GLOBAL_ACTIONS_KEY_TIMEOUT = 500;
//定义一个touch事件中是点击事件还是一个滑动事件所需的时间,如果用户在这个时间之内滑动,那么就认为是一个点击事件
private static final int TAP_TIMEOUT = 115;
/**
* Defines the duration in milliseconds we will wait to see if a touch event
* is a jump tap. If the user does not complete the jump tap within this interval, it is
* considered to be a tap.
*/
//定义一个touch事件时候是一个点击事件。如果用户在这个时间内没有完成这个点击,那么就认为是一个点击事件
private static final int JUMP_TAP_TIMEOUT = 500;
//定义双击事件的间隔时间
private static final int DOUBLE_TAP_TIMEOUT = 300;
//定义一个缩放控制反馈到用户界面的时间
private static final int ZOOM_CONTROLS_TIMEOUT = 3000;
/**
* Inset in pixels to look for touchable content when the user touches the edge of the screen
*/
private static final int EDGE_SLOP = 12;
/**
* Distance a touch can wander before we think the user is scrolling in pixels
*/
private static final int TOUCH_SLOP = 16;
/**
* Distance a touch can wander before we think the user is attempting a paged scroll
* (in dips)
*/
private static final int PAGING_TOUCH_SLOP = TOUCH_SLOP * 2;
/**
* Distance between the first touch and second touch to still be considered a double tap
*/
private static final int DOUBLE_TAP_SLOP = 100;
/**
* Distance a touch needs to be outside of a window's bounds for it to
* count as outside for purposes of dismissing the window.
*/
private static final int WINDOW_TOUCH_SLOP = 16;
//用来初始化fling的最小速度,单位是每秒多少像素
private static final int MINIMUM_FLING_VELOCITY = 50;
//用来初始化fling的最大速度,单位是每秒多少像素
private static final int MAXIMUM_FLING_VELOCITY = 4000;
//视图绘图缓存的最大尺寸,以字节表示。在ARGB888格式下,这个尺寸应至少等于屏幕的大小
@Deprecated
private static final int MAXIMUM_DRAWING_CACHE_SIZE = 320 * 480 * 4; // HVGA screen, ARGB8888
//flings和scrolls摩擦力度大小的系数
private static float SCROLL_FRICTION = 0.015f;
/**
* Max distance to over scroll for edge effects
*/
private static final int OVERSCROLL_DISTANCE = 0;
/**
* Max distance to over fling for edge effects
*/
private static final int OVERFLING_DISTANCE = 4;
}
3:
Scroller--Android里Scroller类是为了实现View平滑滚动的一个Helper类。通常在自定义的View时使用,在View中定义一个私有成员mScroller = new Scroller(context)。设置mScroller滚动的位置时,并不会导致View的滚动,通常是用mScroller记录/计算View滚动的位置,再重写View的computeScroll(),完成实际的滚动。 相关API介绍如下:
mScroller.getCurrX() //获取mScroller当前水平滚动的位置
mScroller.getCurrY() //获取mScroller当前竖直滚动的位置
mScroller.getFinalX() //获取mScroller最终停止的水平位置
mScroller.getFinalY() //获取mScroller最终停止的竖直位置
mScroller.setFinalX(int newX) //设置mScroller最终停留的水平位置,没有动画效果,直接跳到目标位置
mScroller.setFinalY(int newY) //设置mScroller最终停留的竖直位置,没有动画效果,直接跳到目标位置
//滚动,startX, startY为开始滚动的位置,dx,dy为滚动的偏移量, duration为完成滚动的时间
mScroller.startScroll(int startX, int startY, int dx, int dy) //使用默认完成时间250ms
mScroller.startScroll(int startX, int startY, int dx, int dy, int duration)
mScroller.computeScrollOffset() //返回值为boolean,true说明滚动尚未完成,false说明滚动已经完成。这是一个很重要的方法,通常放在View.computeScroll()中,用来判断是否滚动是否结束。
下面上一段简单的代码,代码中读者可能会发现,其实最后调用的方法全是scrollTo方法。
import android.content.Context;
import android.util.AttributeSet;
import android.util.Log;
import android.view.View;
import android.widget.LinearLayout;
import android.widget.Scroller;
public class CustomView extends LinearLayout {
private static final String TAG = "Scroller";
private Scroller mScroller;
public CustomView(Context context, AttributeSet attrs) {
super(context, attrs);
mScroller = new Scroller(context);
}
//调用此方法滚动到目标位置
public void smoothScrollTo(int fx, int fy) {
int dx = fx - mScroller.getFinalX();
int dy = fy - mScroller.getFinalY();
smoothScrollBy(dx, dy);
}
//调用此方法设置滚动的相对偏移
public void smoothScrollBy(int dx, int dy) {
//设置mScroller的滚动偏移量
mScroller.startScroll(mScroller.getFinalX(), mScroller.getFinalY(), dx, dy);
invalidate();//这里必须调用invalidate()才能保证computeScroll()会被调用,否则不一定会刷新界面,看不到滚动效果
}
@Override
public void computeScroll() {
//先判断mScroller滚动是否完成
if (mScroller.computeScrollOffset()) {
//这里调用View的scrollTo()完成实际的滚动
scrollTo(mScroller.getCurrX(), mScroller.getCurrY());
//必须调用该方法,否则不一定能看到滚动效果
postInvalidate();
}
super.computeScroll();
}
}
注意看这个自定义的View是继承ViewGroup,而不是继承View,我前面一篇文章讲到了这一块,要想移动某一个View,你必须移动该View的父亲,如果一个View不是ViewGroup,你直接调用该View的scrollTo方法是一点效果也没有的,文章的链接地址如下:
http://blog.csdn.net/ly985557461/article/details/44957749
5:
主要介绍完这几个类,下面还有一个重头戏,发一个文章链接,如果读者还不了解事件的分发机制,建议先看看下面这一篇文章:
http://blog.csdn.net/ly985557461/article/details/40865199
上面的基本工作做完后,下面给出关键的代码:
//要扩大高度的listView控件
private ListView listView;
//允许滚动的最大的高度
public int mTopViewHeight;
//头部是否隐藏的标志位
private boolean isTopHidden = false;
//滚动的实现者 Scroller
private OverScroller mScroller;
//系统的类,用来记录一些常量,避免自己重复的定义
private VelocityTracker mVelocityTracker;
//头部隐藏的监听者
private TopViewHiddenListener listener;
//滑动的最小值,大于此值时,才认为时滑动
private int mTouchSlop;
//滑动停止后,惯性滑动的变量
private int mMaximumVelocity, mMinimumVelocity;
//记录上次触控点的Y
private float mLastY;
//滑动大于mTouchSlop时,认为时dragging
private boolean mDragging;
//headerView 滚动的距离
private float moveDistance = 0;
//滑动到顶部后,下拉距离大于minBoundDistance时,头部动画显示,否则反弹回去
private float minBoundDistance = 0;
//滑动的方向
private Direction direction = Direction.NONE;
enum Direction {UP, DOWN, NONE}
public StickyNavLayoutForBuyCircleInfo(Context context, AttributeSet attrs) {
super(context, attrs);
setOrientation(LinearLayout.VERTICAL);
mScroller = new OverScroller(context, new AccelerateDecelerateInterpolator());
mVelocityTracker = VelocityTracker.obtain();
mTouchSlop = ViewConfiguration.get(context).getScaledTouchSlop();
mMaximumVelocity = ViewConfiguration.get(context).getScaledMaximumFlingVelocity();
mMinimumVelocity = ViewConfiguration.get(context).getScaledMinimumFlingVelocity();
minBoundDistance = DisplayUtil.dip2px(context, 100);
}
上面就是一些变量的定义,不废话了~
@Override
protected void onFinishInflate() {
super.onFinishInflate();
//在控件初始化完毕之后在得到listView的控件,必须在此方法中调用
listView = (ListView) findViewById(R.id.goodsList);
}
在onFinishInflate方法中初始化listview,尽量在该方法中,否则可能出现listView未初始化的错误。
//此方法动态的设置头部滑动的距离,因为有些设计到头部高度不固定,需要动态的计算,所以需要动态设置高度
public void setTopViewHeight(int height) {
mTopViewHeight = height;
ViewGroup.LayoutParams params = listView.getLayoutParams();
params.height = getMeasuredHeight();
listView.setLayoutParams(params);
}
为什么需要动态的设定listView的高度呢?因为当我们向上滑动的时候,listView会跟着向上滚动,如果listView的高度不变的话,那么滚动之后,listView显示的大小还是原来的大小,就会在下方留白,所以当header的高度计算完毕之后,要给listView的高度加上该高度,这样就算header完全隐藏,listview完全显示,屏幕下方也不会留白。
//事件拦截,一次事件 从Action_Down 到Action_Up结束,此次事件结束后,下一次事件会重新调用onInterceptTouchEvent
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
//拦截的情况
//1:头部显示,用户向上滑动,头部不断缩小,需要拦截事件,自己处理
//2:头部不显示,但是listView滚动到了顶部,再向下滑动,头部将要显示,需要拦截事件,自己处理,下滑的过程中,头部不断显示
int action = ev.getAction();
float y = ev.getY();
switch (action) {
case MotionEvent.ACTION_DOWN:
mLastY = y;//记录手指点击的Y
break;
case MotionEvent.ACTION_MOVE:
float dy = y - mLastY;//滑动时记录滑动的距离
if (Math.abs(dy) > mTouchSlop) {//滑动距离大于mTouchSlop才认为时滑动
if (dy < 0) {//向上滑动
if (getScrollY() < mTopViewHeight) {//topView没有隐藏,则拦截事件,自己处理,让headerView随着手势不断缩小
return true;//返回true,则拦截事件,不向下分发,自己调用onTouch事件处理
}
} else {//向下滑动,在头部向下滑动的过程中需要拦截事件
int firstPosition = listView.getFirstVisiblePosition();//得到listView头部的位置
if (firstPosition == 0 && getScrollY() <= mTopViewHeight) {//listView滚动到顶部并且topView将要显示,则拦截事件
return true;
}
}
}
break;
}
return super.onInterceptTouchEvent(ev);
}
上面是事件拦截,在header显示的时候,我们都需要拦截事件来自己处理~详细请看注释,逻辑并不是很复杂
@Override
public boolean onTouchEvent(MotionEvent event) {
//跟踪触摸屏事件,用来展示手指抬起后,惯性滑动的效果
mVelocityTracker.addMovement(event);
int action = event.getAction();
float y = event.getY();
switch (action) {
case MotionEvent.ACTION_DOWN:
//手指按下,如果Scroller动画没有停止,停止动画
if (!mScroller.isFinished()) {
mScroller.abortAnimation();
}
//手指每次按下,清空VelocityTracker的状态
mVelocityTracker.clear();
//为VelocityTracker添加MotionEvent
mVelocityTracker.addMovement(event);
mLastY = y;
return true;
case MotionEvent.ACTION_MOVE:
//记录移动的距离
float dy = y - mLastY;
//判断是否时滑动
if (!mDragging && Math.abs(dy) > mTouchSlop) {
mDragging = true;
}
if (mDragging) {//y方向超过此范围才认为是拖动
if (dy > 0) {
//记录方向是向下滑动
direction = Direction.DOWN;
} else {
//记录方向是向上滑动
direction = Direction.UP;
}
//跟随手势移动,用来缩放headerView
scrollBy(0, (int) -dy);
mLastY = y;
}
break;
case MotionEvent.ACTION_CANCEL:
//手势取消时,停止动画
mDragging = false;
if (!mScroller.isFinished()) {
mScroller.abortAnimation();
}
break;
case MotionEvent.ACTION_UP:
mDragging = false;
//手指抬起后,计算惯性滑动速率
mVelocityTracker.computeCurrentVelocity(1000, mMaximumVelocity);
//得到Y方向的速率
int velocityY = (int) mVelocityTracker.getYVelocity();
//如果大于最小的移动速率,则手指抬起后惯性滚动一段距离
if (Math.abs(velocityY) > mMinimumVelocity) {
fling(-velocityY);
}
mVelocityTracker.clear();
//做回弹动作或者滚动到顶部,顶部隐藏了,需要下拉显示,如果下拉的距离过于小,则回弹
if (isTopHidden && listView.getFirstVisiblePosition() == 0) {
//得到headerView滚动的距离
moveDistance = Math.abs(mTopViewHeight - getScrollY());
//如果下拉的距离大于最小下拉距离
if (moveDistance > minBoundDistance) {
//滚动到顶部,显示headerView
mScroller.startScroll(0, getScrollY(), 0, -getScrollY(), 400);
isTopHidden = false;
if (listener != null) {
listener.onTopViewVisible();
}
} else {
//向上回弹,动画隐藏headerView
mScroller.startScroll(0, getScrollY(), 0, (mTopViewHeight - getScrollY()), 200);
isTopHidden = true;
}
invalidate();
}
break;
}
return super.onTouchEvent(event);
}
onTouch事件主要用来控制header的滑动
//重写LinearLayout的scrollTo方法,避免滑动过界
@Override
public void scrollTo(int x, int y) {
if (y < 0) {
y = 0;
}
if (y > mTopViewHeight) {
y = mTopViewHeight;
}
if (y != getScrollY()) {
super.scrollTo(x, y);
}
if (!isTopHidden && direction == Direction.UP && (getScrollY() == mTopViewHeight)) {
isTopHidden = true;
if (listener != null) {
listener.onTopViewHidden();
}
}
}
重写LinearLayout的scrollTo方法,避免滑动超过边界。
//重写此方法,不然直接调用Scroller的scrollto或者scrollBy方法没有效果
@Override
public void computeScroll() {
if (mScroller.computeScrollOffset()) {
scrollTo(0, mScroller.getCurrY());
invalidate();
}
}
该方法最后别忘了调用invalidate方法来进行刷新。
最后给上例子的地址:http://download.csdn.net/detail/ly985557461/8696003