仿淘宝商品详情页面下拉黏滞效果

来源:互联网 发布:网络大电影的宣发 编辑:程序博客网 时间:2024/04/30 05:24

项目中需要用到淘宝商品详情页面的下拉黏滞效果,刚开始的想法比较复杂,是通过投机取巧的方式来大致实现的,但是效果很不好,勉强可以使用,这怎么能行?后来自己尝试着去优化,感觉一个ListView就可以实现,于是就去用listView去实现了一下,主要用到了ListView的smoothScrollToPosition这个方法,做到最后,发现smoothScrollToPosition这个方法的一个bug。

假如当前ListView显示的是position为0,但是position为0的item只是显示了一部分,你调用smoothScrollToPosition方法,此时listView是不会滚动的,因为Android源代码认为 :你当前显示的position 0,你要滚动到position 0,这不是扯淡嘛!所以这个方法失效了,但是从StackOverFlow上面搜索,都是Android的一个bug!shit~将要实现的效果就这样泡汤了。

后来发现了一种新的思路:
1:自定义一个LinearLayout,自己去处理事件,然后根据事件调用Scroller的相关方法去滚动头部。
2:自定义一个HeaderView。
3:HeaderView下面是一个ListView。

想要实现的效果描述如下:
1:Header显示的时候,向上滑动,Header不断隐藏,Header完全隐藏后,listView才可以滑动。
2:Header显示的时候,向下滑动,Header不断显示,Header完全显示后,在向下滑动,无效果。
3:Header完全隐藏的时候,如果listView的firstVisiblePosition不是0,则滑动事件交给listView处理。
4:Header完全隐藏的时候,如果listView的firstVisiblePosition是0,则滑动事件交给LinearLayout,屏蔽listView的事件处理。
5:Header完全隐藏并且listView的firstVisiblePosition是0,不断下拉,header不断显示增大,如果手指抬起后,header显示的部分小于一定距离的话,header要反弹隐藏;Header显示超过一定距离,播放动画让header完全显示。

基本上面说的这几种情况就是我们自定义的LinearLayout需要处理的几种情况,主要涉及到事件的拦截onInterceptTouchEvent方法,和onTouch方法。
好了,在介绍实现代码之前,我们先介绍几个类:
1:

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) //使用默认完成时间250msmScroller.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()会被调用,否则不一定会刷新界面,看不到滚动效果}@Overridepublic void computeScroll() {//先判断mScroller滚动是否完成if (mScroller.computeScrollOffset()) {//这里调用View的scrollTo()完成实际的滚动scrollTo(mScroller.getCurrX(), mScroller.getCurrY());//必须调用该方法,否则不一定能看到滚动效果postInvalidate();}super.computeScroll();}}


4:
注意看这个自定义的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;//滚动的实现者 Scrollerprivate OverScroller mScroller;//系统的类,用来记录一些常量,避免自己重复的定义private VelocityTracker mVelocityTracker;//头部隐藏的监听者private TopViewHiddenListener listener;//滑动的最小值,大于此值时,才认为时滑动private int mTouchSlop;//滑动停止后,惯性滑动的变量private int mMaximumVelocity, mMinimumVelocity;//记录上次触控点的Yprivate float mLastY;//滑动大于mTouchSlop时,认为时draggingprivate 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);}
上面就是一些变量的定义,不废话了~

@Overrideprotected 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@Overridepublic boolean onInterceptTouchEvent(MotionEvent ev) {//拦截的情况//1:头部显示,用户向上滑动,头部不断缩小,需要拦截事件,自己处理//2:头部不显示,但是listView滚动到了顶部,再向下滑动,头部将要显示,需要拦截事件,自己处理,下滑的过程中,头部不断显示int action = ev.getAction();float y = ev.getY();switch (action) {case MotionEvent.ACTION_DOWN:mLastY = y;//记录手指点击的Ybreak;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显示的时候,我们都需要拦截事件来自己处理~详细请看注释,逻辑并不是很复杂

@Overridepublic 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添加MotionEventmVelocityTracker.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;}//跟随手势移动,用来缩放headerViewscrollBy(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) {//滚动到顶部,显示headerViewmScroller.startScroll(0, getScrollY(), 0, -getScrollY(), 400);isTopHidden = false;if (listener != null) {listener.onTopViewVisible();}} else {//向上回弹,动画隐藏headerViewmScroller.startScroll(0, getScrollY(), 0, (mTopViewHeight - getScrollY()), 200);isTopHidden = true;}invalidate();}break;}return super.onTouchEvent(event);}

onTouch事件主要用来控制header的滑动

//重写LinearLayout的scrollTo方法,避免滑动过界@Overridepublic 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方法没有效果@Overridepublic void computeScroll() {if (mScroller.computeScrollOffset()) {scrollTo(0, mScroller.getCurrY());invalidate();}}

该方法最后别忘了调用invalidate方法来进行刷新。

最后给上例子的地址:http://download.csdn.net/detail/ly985557461/8696003


0 0
原创粉丝点击