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

先看效果图。

这里写图片描述

最近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处理。

package com.passerby.pinnedlistview;

import android.content.Context;
import android.text.TextUtils;
import android.util.AttributeSet;
import android.util.Log;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewTreeObserver;
import android.widget.AbsListView;
import android.widget.LinearLayout;
import android.widget.ListView;

/**
 * Created by mac on 16/1/4.
 */
public class PinnedListView extends ListView implements AbsListView.OnScrollListener {

    public 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());

            if (shouldInterruptEvent(offsetY)) {

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

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

            break;
        case MotionEvent.ACTION_UP:

            if (mShouldInterruptEvent) {

                LinearLayout.LayoutParams params = (LinearLayout.LayoutParams) mHeaderLayout.getLayoutParams();
                final int currentTopMargin = params.topMargin;
                if (currentTopMargin < -mThreshold) {
                    // 如果上滑 没有 超过临界值,则强制展开header
                    params.topMargin = -mHeaderHeight;
                } else {
                    // 如果上滑超过了临界值,则强制收起header
                    params.topMargin = 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() {

        // v("childTop= " + getChildAt(0).getTop());

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

    private boolean hideHeaderLayout(final float offset) {
        final int headerHeight = mHeaderHeight;

        LinearLayout.LayoutParams params = (LinearLayout.LayoutParams) mHeaderLayout.getLayoutParams();
        int currentTopMargin = params.topMargin;
        float topMargin = currentTopMargin;
        float tOffset = offset;

        if (tOffset > FAULT_TOLERANCE && currentTopMargin >= -headerHeight) { // 如果往上滚

            topMargin = currentTopMargin - tOffset;
            // 处理滚动过度的情况
            if (topMargin < -headerHeight) {
                topMargin = -headerHeight;
            }
        } else if (tOffset < -FAULT_TOLERANCE && currentTopMargin <= 0) { // 如果往下滚

            topMargin = currentTopMargin - tOffset;
            // 处理滚动过度的情况
            if (topMargin > 0) {
                topMargin = 0;
            }
        }
        // mHeaderLayout.getLayoutParams().topMargin = (int) topMargin;
        params.topMargin = (int) topMargin;
        mHeaderLayout.requestLayout();

        // 如果高度有所改变,说明该滑动事件已经被拦截了
        return topMargin != currentTopMargin;
    }

    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);
                    }
                });
    }

    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;
    }
}

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

下载源码

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值