您的位置:首页 > 其它

基于事件分发机制,以最小代价实现listview顶部悬浮效果

2016-01-05 16:34 513 查看
先看效果图。




最近boss要在项目里面实现一个顶部悬浮的效果,在网上找了不少项目,基本上有三种方案:


1.整个布局分为上下两层,下面那层是有listview的布局,上面那层是悬浮view,而且固定在底部;一开始悬浮的view隐藏,通过监听listview的滑动状态来控制那个悬浮view 的显隐来达到“悬浮”的效果。(示例代码

2.这种也是分两层,下面还是listview的布局,上面也是一个viewgroup(LinearLayout、RelativeLayout均可),不同的是,第一种方案中悬浮的那个view实际上是有两个的,毕竟隐藏的那个悬浮view显示出来之后,下面那层的跟悬浮view相同外观的view依然会随着listview 的滚动而继续滚出屏幕,只是用户看不见了而已。

而这一种方案下的悬浮view只有一个,第一种方案是通过setVisibility控制显隐来实现“悬浮”的效果,这一种是通过addView、removeView来实现,就是当需要显示悬浮view 的时候,将悬浮view从底部那层布局中“抠”出来,添加到上面那层固定在顶部的viewgroup中;当需要隐藏悬浮view的时候,将它从上层固定的viewgroup中“抠”出来,添加到底层布局中。(示例代码)

3.github上也有人封装好了框架,没仔细研究,有兴趣可以关注下。(示例代码)



另外需要说明的是,上面三种方案都是需要将布局嵌套在scrollview的,因为只有这样才能将布局整体向上滚。

scrollview嵌套listview本身有多麻烦我就不多提了,尤其是listview的长度计算的问题,网上也有几种方案,比如手动计算每个item的长度,然后加起来;还有一种是重写listview的onMeasure方法,然后将它的高度设置成无限长。

这几种方法对于简单的item还可以,但是复杂的自定义view这些高度计算的时候却不是很准确,而且这些方案的滚动都是基于scrollview的滚动,相当于将listview变成了一个非常长的垂直liearlayout。

基于以上的种种原因,所以有了下面从事件分发的角度来处理的这种方案。这种方法外部调用很简单,只需要传一个需要显隐的view就可以,布局中也不需要嵌套scrollview,自然少了很多麻烦。当然仁者见仁智者见智,大家根据实际项目使用适合自己的方案就好。

总体思路就是,当listview的第一个item可见时,要判断是不是需要拦截掉触摸事件,如果需要拦截,则对header的显隐进行操作,如果不需要拦截,则将触摸事件直接交由listview处理。

[code]public class PinnedListView extends ListView implements AbsListView.OnScrollListener {

    /**
     * 容错值
     */
    private static final int FAULT_TOLERANCE = 3;
    private int mHeaderHeight;
    private int mThreshold;
    private View mHeaderLayout;
    private int mStartY;
    private boolean mFirstItemIsVisible;
    private boolean mShouldInterruptEvent = false;

    public PinnedListView(Context context) {
        this(context, null);
    }

    public PinnedListView(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public PinnedListView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);

        setOnScrollListener(this);
    }

    //移动超过 mHeaderHeight的三分之一时,收起header
    //反之,执行正常的操作
    @Override
    public boolean onTouchEvent(MotionEvent ev) {

        final MotionEvent event = ev;
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:

                mShouldInterruptEvent = false;
                mStartY = (int) event.getY();

                break;
            case MotionEvent.ACTION_MOVE:
                float offsetY = (int) (mStartY - event.getY());
//                v(String.format("startY=%s,endY=%s,offsetY=%s", mStartY, event.getY(), offsetY));

                if (shouldInterruptEvent(offsetY)) {

                    mShouldInterruptEvent = true;
                }
                if (isListViewOnTop() && changedHeaderLayout(offsetY)) {

                    mShouldInterruptEvent = true;
                }
                if (mShouldInterruptEvent) {
                    return true;
                }
                mStartY = (int) event.getY();//每次记录上一次的触摸位置,避免用户手指改变方向时导致判断出错

                break;
            case MotionEvent.ACTION_UP:

                if (mShouldInterruptEvent) {

                    final int currentHeight = mHeaderLayout.getLayoutParams().height;
                    if (mHeaderHeight - currentHeight < mThreshold) {
                        //如果上滑 没有 超过临界值,则强制展开header
                        mHeaderLayout.getLayoutParams().height = mHeaderHeight;
                    } else {
                        //如果上滑超过了临界值,则强制收起header
                        mHeaderLayout.getLayoutParams().height = 0;
                    }
                    mHeaderLayout.requestLayout();

                    //避免持续保留焦点,否则子view可能保持着触摸时的外观
                    clearFocus();
                    return true;
                }
                break;
        }
        return super.onTouchEvent(ev);
    }

    /**
     * listview在顶部时,继续下拉的事件将要被拦截,因为有的手机(比如vivo)有回弹效果(listview处在顶部时可以继续下拉)
     */
    private boolean shouldInterruptEvent(float offset) {

        View child = getChildAt(0);
        if (null == child) {
            return true;
        }
        final float tOffset = offset;
        //listview滑动到顶的时候下滑时,拦截事件
        if (tOffset < -FAULT_TOLERANCE && isListViewOnTop()) {

            return true;
        }
        return false;
    }

    private boolean isListViewOnTop() {

        View child = getChildAt(0);
        if (null == child) {
            return true;
        }
        //这里有两个判断,原因如下:
        //1.OnScrollListener里面可以获取当前显示的第一个可见的item,
        // 但是从下往上快滚动firstVisibleItem变成0的时候,此时第0个item并没有完全显示
        // ,但是如果我们直接让他执行展开header的操作,对用户来说这种显示效果可能并不是他们想要的
        //2.listview有复用机制,直接getChildAt(0)是无法判断是否已经滚动到顶部的
        return mFirstItemIsVisible && child.getTop() >= 0;
    }

    /**
     * FAULT_TOLERANCE作为容错值
     * 虽然理论上判断tOffset大于或小于0即可,但是手指做点击操作的时候因为实际操作可能并不总是为0
     * ,有时候可能有误差,所以这里设置容错值
     */
    private boolean changedHeaderLayout(final float offset) {
        final int headerHeight = mHeaderHeight;
        int currentHeight = mHeaderLayout.getLayoutParams().height;
        float tOffset = offset;
        float height = currentHeight;
        if (tOffset > FAULT_TOLERANCE && currentHeight > 0) { //如果往上滚
            //处理滚动过度的情况
            int restHeight = currentHeight;
            if (tOffset > restHeight) {
                tOffset = restHeight;
            }
            height = currentHeight - tOffset;
        } else if (tOffset < -FAULT_TOLERANCE && currentHeight < headerHeight) { //如果往下滚
            //处理滚动过度的情况
            tOffset = Math.abs(tOffset);
            if (tOffset + currentHeight > headerHeight) {
                tOffset = headerHeight - currentHeight;
            }
            height = currentHeight + tOffset;
        }
        //如果高度有所改变,说明该滑动事件已经被拦截了
        boolean isChanged = height != currentHeight;
        if (isChanged) {

            //避免过度刷新
            mHeaderLayout.getLayoutParams().height = (int) height;
            mHeaderLayout.requestLayout();//会引起重绘(onMesuare、onLayout、onDraw)

//        this.mHeaderLayout.getLayoutParams().height = height;
//        this.mHeaderLayout.setTranslationY(-height);
//        mHeaderLayout.setY(-height);
//        mHeaderLayout.scrollTo(0,height);
        }
        return isChanged;
    }

    public void setHeaderLayout(View view) {

        this.mHeaderLayout = view;
        this.mHeaderLayout.getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver
                .OnGlobalLayoutListener() {

            @Override
            public void onGlobalLayout() {

                mHeaderHeight = mHeaderLayout.getHeight();
                mThreshold = mHeaderHeight >> 1;
                mHeaderLayout.getViewTreeObserver().removeOnGlobalLayoutListener(this);
                //mHeaderLayout.getLayoutParams().height是-2(wrap_content)
                //-2的值在程序的逻辑判断中带来太多麻烦,所以设置成准确数值
                mHeaderLayout.getLayoutParams().height = mHeaderHeight;
            }
        });
    }

    public void v(String msg) {
        if (!TextUtils.isEmpty(msg)) {
            Log.v(getClass().getCanonicalName(), msg);
        }
    }

    @Override
    public void onScrollStateChanged(AbsListView view, int scrollState) {

    }

    @Override
    public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) {

        mFirstItemIsVisible = firstVisibleItem == 0;
    }
}


注释都写得很清楚了,就不多提了。

下载源码
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: