自定义ScollView+ListView实现sticky的效果,涉及:自定义View、事件分发、计算和定位Sticky

一、效果图:

二、自定义ScrollView实现效果

/**
 * author:白迎宾
 * time:2021/10/12
 * description: StickyScrollView
 *
 * 1、继承ScrollView
 * 2、重写onLayout计算LinearLayout里所有的View的高度,计算headView的高度,计算ListView可滑动的最大值
 * 3、重写onScrollChanged来判断ScrollView滑动到什么位置,也就是固定sticky
 * 4、重写computeScroll()获取offset距离,scrollTo到滑动的位置
 * 5、重写scrollBy和scrollTo判断可滑动的值是headView的最大高度和最小高度
 * 6、重写dispatchTouchEvent,处理事件分发:
 *    第一:
 *      满足(currentY > maxY || currentY==maxY)表示隐藏headView的时候,也就是sticky的时候
 *      并且DOWN和MOVE的位置点在子View(ListView)上的时候
 *      上面这两种情况都满足的时候,我们把事件交给子View(ListView)处理
 *    第二:
 *      剔除,子View(ListView滑动到最顶部,还要向下滑动的时候)我们把事件交给父View(ScrollView)处理
 * 7、headView是ViewPager,并且ViewPager的子View是ListView这种可以滚动的View,需要在ACTION_DOWN的时候,判断点击位置,并处理拦截状态
 */
public class StickyScrollView extends ScrollView {

    private int minY = 0;//minY是Y轴可滑动的最小值,最小默认0,用来实现sticky固定
    private int maxY = 600;//maxY是Y轴可滑动的最大值,默认HeadView的高度,用来实现sticky固定
    private int currentY = 0;//当前滚动的Y轴的值,用来计算sticky的位置
    private int scrollChildValidHeight = 0;//scrollView的最大有效高度

    private ViewGroup containerView=null;//ScrollView里的唯一ViewGroup(LinearLayout)

    private boolean isListScrollTop = false;//用作处理子View(ListView)滑动到顶部的时候,事件分发给父view(ScrollView)

    public StickyScrollView(Context context) {
        super(context);
    }

    public StickyScrollView(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

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

    public StickyScrollView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
        super(context, attrs, defStyleAttr, defStyleRes);
    }

    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        super.onLayout(changed, l, t, r, b);
        if (changed) {                                 //当布局发生改变时计算滚动范围
            //获取当前ScrollView的最大可视View的尺寸,用来计算LinearLayout里下面的可滚动View(ListView)的有效高度
            Rect rect = new Rect();
            getGlobalVisibleRect(rect);
            int mainHeight = rect.bottom-rect.top;
            //获取ScrollView里的唯一ViewGroup(LinearLayout)
            containerView = (ViewGroup) getChildAt(0);
            if (null != containerView){
                //获取headView的Y轴高度值,用来限制sticky
                View headView = containerView.getChildAt(0);
                Rect childRect = new Rect();
                headView.getGlobalVisibleRect(childRect);
                maxY = childRect.bottom-childRect.top;

                //获取ScrollView里的LinearLayout里面的stickyView的高度,用来计算LinearLayout里下面的可滚动View(ListView)的有效高度
                View stickyView = containerView.getChildAt(1);
                Rect childStickyRect = new Rect();
                stickyView.getGlobalVisibleRect(childStickyRect);
                int stickyHeight = childStickyRect.bottom-childStickyRect.top;

                //计算LinearLayout里下面的可滚动View(ListView)的有效高度
                //headView的高度是滚动隐藏的高度,所以sticky的时候,要把这个高度加上
                scrollChildValidHeight = mainHeight-stickyHeight;
            }
            setScrollViewHeight();
        }
    }

    /**
     * 滚动的时候动态设置 LinearLayout里下面的可滚动View(ListView)的有效高度
     */
    public void setScrollViewHeight(){
        if(null != containerView){
            ListView listView = (ListView) containerView.getChildAt(containerView.getChildCount()-1);
            if(null!=listView){
                ViewGroup.LayoutParams params = listView.getLayoutParams();
                // 最后再加上分割线的高度和padding高度,否则显示不完整。
                params.height = scrollChildValidHeight+listView.getPaddingTop()+listView.getPaddingBottom();
                listView.setLayoutParams(params);

                listView.setOnScrollListener(new AbsListView.OnScrollListener() {
                    @Override
                    public void onScrollStateChanged(AbsListView view, int scrollState) {
                        if(scrollState == AbsListView.OnScrollListener.SCROLL_STATE_IDLE){
                            // 当不滚动时,处理ListView滚动到最顶部的时候事件
                            if(listView.getFirstVisiblePosition() == 0){
                                //当headView隐藏的时候,并且子View(ListView滑动到最顶部的时候)把事件交给当前View(ScrollView处理)
                                isListScrollTop=true;
                            }
                        }
                    }

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

    @Override
    public boolean dispatchTouchEvent(MotionEvent ev) {
        switch (ev.getAction()) {
            case MotionEvent.ACTION_DOWN:
            case MotionEvent.ACTION_MOVE:
                if((currentY > maxY || currentY==maxY) ){//表示隐藏headView的时候
                    if(isVerticalInView(ev,containerView.getChildAt(2))) {//表示点击在滚动视图内部的时候
                        if(!isListScrollTop){
                            requestDisallowInterceptTouchEvent(true);
                        }
                    }
                }
                break;
        }
        return super.dispatchTouchEvent(ev);
    }

    @Override
    protected void onScrollChanged(int scrollX, int scrollY, int oldScrollX, int oldScrollY) {
        //滚动Y轴改变的时候,确定当前的滚动有效值,也就是 小于和等于sticky的高度Y值的时候是可以滚动的
        if(scrollY > maxY || scrollY==maxY){
            scrollY = maxY;
            //当headView隐藏的时候,把事件交给子View(ListView)处理
            isListScrollTop = false;
        }
        currentY = scrollY;
        super.onScrollChanged(scrollX, scrollY, oldScrollX, oldScrollY);
    }

    @Override
    public void computeScroll() {
        //这里不管是不是滑动到顶部,都需要执行scrollTo,否则headView会直接弹出来效果不对
        if (computeVerticalScrollOffset()!=0) {
            scrollTo(getScrollX(), currentY);
            postInvalidate(); //这必须调用刷新否则看不到效果
        }
    }

    @Override
    public void scrollBy(int x, int y) {
        int scrollY = getScrollY();
        int toY = scrollY + y;
        //maxY是HeadView的高度,用来实现sticky固定
        if (toY >= maxY) {
            toY = maxY;
        } else if (toY <= minY) {
            toY = minY;
        }
        y = toY - scrollY;
        super.scrollBy(x, y);
    }

    /**
     * 整个控件的高度就是展示在屏幕上的高度加上header控件的高度,所以最多只能向下滑动header的高度
     *
     * @param x //因为不能左右滑,所以始终未0
     * @param y //纵向滑动的y值,判断sticky固定的位置
     */
    @Override
    public void scrollTo(int x, int y) {
        //maxY是HeadView的高度,用来实现sticky固定
        if (y >= maxY) {
            y = maxY;
        } else if (y <= minY) {
            y = minY;
        }
        super.scrollTo(x, y);
    }

    private boolean isVerticalInView(MotionEvent ev, View view){
        if(view == null){
            return false;
        }

        //Y轴移动的值 (正值代表向上移动,负值代表向下移动)
        int scrollY = getScrollY();

        int listTop = view.getTop()-scrollY ;
        int listBtm = view.getBottom() -scrollY;
        int listLeft = view.getLeft();
        int listR = view.getRight();
        int y = (int) ev.getY();
        int x = (int) ev.getX();

        return x > listLeft && x < listR && y > listTop && y < listBtm;
    }
}

三、布局xml使用

<?xml version="1.0" encoding="utf-8"?>
<androidx.appcompat.widget.LinearLayoutCompat xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical">

    <com.mot.android_view.views.sticky.scroll_sticky.StickyScrollView
        android:layout_width="match_parent"
        android:layout_height="match_parent">

        <androidx.appcompat.widget.LinearLayoutCompat
            android:layout_width="match_parent"
            android:orientation="vertical"
            android:layout_height="match_parent">

            <TextView
                android:id="@+id/headView"
                android:layout_width="match_parent"
                android:layout_height="300dp"
                android:gravity="center"
                android:text="TextView水电费水电费胜多负少的发送到发大水水电费水电费水电费是水电费水电费胜多负少的水电费水电费胜多负少的展示HeadView\n\n可以是ViewPager\nViewPager的子View可以是ListView" />

            <TextView
                android:id="@+id/stickyView"
                android:layout_width="match_parent"
                android:layout_height="50dp"
                android:background="@color/color_red"
                android:gravity="center"
                android:text="StickyView用作展示,也可以换成其他的View"
                android:textColor="@color/color_white" />

            <!--   这里可以是ScrollView、ListView、RecyclerView等可滚动的Viwe     -->
            <ListView
                android:id="@+id/motListView"
                android:layout_width="match_parent"
                android:layout_height="match_parent"/>

        </androidx.appcompat.widget.LinearLayoutCompat>

    </com.mot.android_view.views.sticky.scroll_sticky.StickyScrollView>


</androidx.appcompat.widget.LinearLayoutCompat>

四、activity测试stickyView

public class ScrollGroupStickyActivity extends AppCompatActivity {
    private ListView motListView;
    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_android_view_sticky_scroll_group_page);
        motListView = (ListView) findViewById(R.id.motListView);
    }
    @Override
    protected void onResume() {
        super.onResume();
        initMoreListView();
    }

    private void initMoreListView(){
        List<ListBean> listBeans = new ArrayList<>();
        ListBean listBean = new ListBean();
        listBean.setName("漠天");
        listBean.setPhone("15201498667");
        listBeans.add(listBean);
        ListBean listBean1 = new ListBean();
        listBean1.setName("黑天河");
        listBean1.setPhone("13831048122");
        listBeans.add(listBean1);
        ListBean listBean2 = new ListBean();
        listBean2.setName("堕落风");
        listBean2.setPhone("12323248844");
        listBeans.add(listBean2);
        ListBean listBean3 = new ListBean();
        listBean3.setName("湖天地");
        listBean3.setPhone("999999999999");
        listBeans.add(listBean3);
        listBeans.add(listBean2);
        listBeans.add(listBean1);
        listBeans.add(listBean3);
        listBeans.add(listBean2);
        listBeans.add(listBean3);
        listBeans.add(listBean2);
        listBeans.add(listBean1);
        listBeans.add(listBean3);
        listBeans.add(listBean2);
        listBeans.add(listBean3);
        listBeans.add(listBean2);
        listBeans.add(listBean1);
        listBeans.add(listBean3);
        listBeans.add(listBean2);
        ConflictListViewAdapter conflictListViewAdapter = new ConflictListViewAdapter();
        motListView.setAdapter(conflictListViewAdapter);
        conflictListViewAdapter.addListBean(listBeans);
    }
}

关键点:

1、ViewGroup的事件分发很重要

2、思路清晰很重要

3、scrollBy和scrollTo的区别,实现sticky效果

4、ScrollView和ListView的常用方法和事件要熟悉

Logo

华为开发者空间,是为全球开发者打造的专属开发空间,汇聚了华为优质开发资源及工具,致力于让每一位开发者拥有一台云主机,基于华为根生态开发、创新。

更多推荐