«

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

时间:2024-3-2 17:27     作者:韩俊     分类: Android


项目中需要用到淘宝商品详情页面的下拉黏滞效果,刚开始的想法比较复杂,是通过投机取巧的方式来大致实现的,但是效果很不好,勉强可以使用,这怎么能行?后来自己尝试着去优化,感觉一个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) //使用默认完成时间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();
    }
}


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;
    //滚动的实现者 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


标签: android

热门推荐