android 自定义StickyLayout

项目要求实现sticky的效果,涉及:自定义View、事件分发、滚动Scroller效果、加速滚动、计算和定位Sticky

一、效果图:

android 自定义StickyLayout_第1张图片

 

二、自定义LienarLayout实现效果

/**
 * author:白迎宾
 * time:2021/10/9
 * description: StickyLinearLayout
 * 基本思路:
 * 1、继承LinearLayout,实现OnGestureListener使用fling方法检测手势
 * 2、重写onLayout计算LinearLayout里所有的View的高度,计算可滑动的最大值
 * 3、构造方法初始化辅助类(OverScroller实现滑动监听、VelocityTracker滑动速度跟踪器、GestureDetector设置手势检测)
 *    注意:setLongClickable(true); //这里设置长按事件可用,否则dispatchTouchEvent的Down和Up事件都不会执行(也可以采取事件拦截方式)
 * 4、重写onMeasure获取HeadView的高度,并重写计算和适配展示的高度  当前高度+headView的高度
 * 5、重写dispatchTouchEvent,根据事件处理scrollBy滑动操作和加速滑动Scroller.fling()
 * 6、重写computeScroll()获取offset距离,scrollTo到滑动的位置
 * 7、重写scrollBy和scrollTo判断可滑动的值是headView的最大高度和最小高度
 * 8、获取getChildAt(2)就是可滚动的View,是否滑动到了第一条的最顶部 isTop()
 * 9、处理事件分发:根据(mCurY==mHeadHeight||mCurY>mHeadHeight)&&!isTop())
 *   (滑动的Y轴的值和HeadView的高度,以及子ScrollView是否滑动到第一条的最顶部,来确定事件交给谁处理)
 *    如果 headView 隐藏了,那么事件拦截,交给子View(ScrollView)处理
 *    如果 headView 要展示和展示的情况下,交给当前View去滑动展示headView
 * 10、headView是ViewPager,并且ViewPager的子View是ListView这种可以滚动的View,需要在ACTION_DOWN的时候,判断点击位置,并处理拦截状态
 */
public class StickyLinearLayout extends LinearLayout implements GestureDetector.OnGestureListener{

    protected OverScroller mScroller; //滚动辅助类
    protected VelocityTracker mVelocityTracker; //滑动速度跟踪器
    protected GestureDetector mGestureDetector; //手势检测
    private int mMaxVelocity; //触发fling最大滑动速度
    private int mScrollRange; //可滚动最大距离(不包含最后一个子控件marginBottom)

    private float scrollY=0;//纵向滑动时,记录y轴的坐标值
    private float lastScrollY = 0;//纵向滑动时,记录上一次y轴的坐标值

    private View mHeadView;
    private int mHeadHeight;

    private int minY = 0;//minY是Y轴可滑动的最小值,最小默认0,用来实现sticky固定
    private int maxY = 0;//maxY是Y轴可滑动的最大值,默认HeadView的高度,用来实现sticky固定
    private int mCurY;//最后移动的Y的值

    public StickyLinearLayout(Context context) {
        super(context);
        init(context);
    }

    public StickyLinearLayout(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
        init(context);
    }

    public StickyLinearLayout(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        init(context);
    }

    @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
    public StickyLinearLayout(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
        super(context, attrs, defStyleAttr, defStyleRes);
        init(context);
    }

    private void init(Context context){
        //这里设置长按事件可用,否则dispatchTouchEvent的Down和Up事件都不会执行(也可以采取事件拦截方式)
        setLongClickable(true);
        mScroller = new OverScroller(context);
        mGestureDetector = new GestureDetector(context, this);
        mMaxVelocity = ViewConfiguration.get(context).getScaledMaximumFlingVelocity();
    }

    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        super.onLayout(changed, l, t, r, b);
        if (changed) {                                 //当布局发生改变时计算滚动范围
            Rect rect = new Rect();
            getGlobalVisibleRect(rect);
            View lastChild = getChildAt(getChildCount() - 1);
            if (null != lastChild){
                //获取当前View可滚动距离
                mScrollRange = lastChild.getBottom() - (rect.bottom-rect.top);
            }
        }
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        //第一个View作为headView,并且测量headView高度
        mHeadView = getChildAt(0);
        measureChildWithMargins(mHeadView, widthMeasureSpec, 0, MeasureSpec.UNSPECIFIED, 0);
        maxY = mHeadView.getMeasuredHeight();
        mHeadHeight = mHeadView.getMeasuredHeight();
        //重新适配展示的高度  当前高度+headView的高度
        super.onMeasure(widthMeasureSpec, MeasureSpec.makeMeasureSpec(MeasureSpec.getSize(heightMeasureSpec) + maxY, MeasureSpec.EXACTLY));
    }

    private boolean isIntercepted = false;
    @Override
    public boolean dispatchTouchEvent(MotionEvent event) {
        if (null == mVelocityTracker) {
            mVelocityTracker = VelocityTracker.obtain();
        }
        mVelocityTracker.addMovement(event);
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                if (!mScroller.isFinished()) {
                    mScroller.abortAnimation();
                }

                scrollY = event.getY();
                lastScrollY = scrollY;

                //****************************************************************************************************
                //*****这是的拦截是为了适配headView是ViewPager的时候,并且ViewPager的View还是ListView这种可滚动的View*****
                //*****如果headView不是ViewPager或者ViewPager里的View不是可滚动的View,可以去掉这个拦截判断***************
                if(getChildAt(0) instanceof ViewPager) {
                    //这两个判断是为了适配顶部是ViewPager的情况,如果没有ViewPager可以直接执行结构体就行
                    if (isVerticalInView(event, getChildAt(0))) {
                        isIntercepted = true;
                        break;
                    }
                }
                isIntercepted = false;
                //****************************************************************************************************

                break;
            case MotionEvent.ACTION_MOVE:
                scrollY = event.getY();
                //判断当前Y的值是否等于或者大于HeadView的高度,并且子滚动ScrollView没有滑动到最顶部的时候:屏蔽当前滚动事件,事件交给ScrollView处理
                if((mCurY==mHeadHeight||mCurY>mHeadHeight)&&!isTop()){
                    break;
                }

                //****************************************************************************************************
                //*****这是的拦截是为了适配headView是ViewPager的时候,并且ViewPager的View还是ListView这种可滚动的View*****
                //*****如果headView不是ViewPager或者ViewPager里的View不是可滚动的View,可以去掉这个拦截判断***************
                if(isIntercepted){
                    break;
                }
                //****************************************************************************************************

                int scrolledY = (int) (lastScrollY - scrollY);
                if (scrolledY < 0 && getScrollY() + scrolledY < 0) {                      //向下滚动
                    scrolledY = -getScrollY();
                }
                if (scrolledY > 0 && getScrollY() + scrolledY > mScrollRange) {          //向上滚动
                    scrolledY = mScrollRange - getScrollY();
                }
                if (mScrollRange > 0 ) {                                                  //可滚动距离必须大于0
                    scrollBy(0, scrolledY);
                    invalidate();
                }
                lastScrollY = scrollY;
                break;
            case MotionEvent.ACTION_UP:
            case MotionEvent.ACTION_CANCEL:
                if (mVelocityTracker != null && mScrollRange > 0) {
                    mVelocityTracker.computeCurrentVelocity(1000, mMaxVelocity);
                    mScroller.fling(getScrollX()
                            , getScrollY()                                             //松开滑动垂直开始坐标
                            , (int) mVelocityTracker.getXVelocity(0)
                            , -(int) mVelocityTracker.getYVelocity(0)                  //垂直方向滑动加速度,0表示第一个按下手指
                            , 0                                                        //松开滑动水平最小坐标
                            , 0                                                        //松开滑动垂直最小坐标
                            , 0                                                        //松开滑动水平最大坐标
                            , mScrollRange);                                           //松开滑动垂直最大坐标(这里就是最大滑动范围)
                    invalidate();                                                      //这必须调用刷新否则看不到效果
                }
                releaseVelocityTracker();
                break;
        }
        return super.dispatchTouchEvent(event);
    }

    @Override
    public void computeScroll() {
        //判断当前Y的值是否等于或者大于HeadView的高度,也就是HeadView隐藏的时候,不执行scrollTo当前View不可滑动,交给子View(ScrollView滑动)
        if((mCurY==mHeadHeight||mCurY>mHeadHeight)){
            return;
        }
        //这里不管是不是滑动到顶部,都需要执行scrollTo,否则headView会直接弹出来效果不对
        if (mScroller.computeScrollOffset()) {

            //****************************************************************************************************
            //*****这是的拦截是为了适配headView是ViewPager的时候,并且ViewPager的View还是ListView这种可滚动的View*****
            //*****如果headView不是ViewPager或者ViewPager里的View不是可滚动的View,可以去掉这个拦截判断***************
            if(isIntercepted){
                return;
            }
            //****************************************************************************************************

            scrollTo(mScroller.getCurrX(), mScroller.getCurrY());
            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;
        }
        mCurY = y;
        super.scrollTo(x, y);
    }

    public boolean isTop(){
        View scrollView = getChildAt(2);
        if(scrollView instanceof ScrollView){
            return isScrollViewTop((ScrollView) scrollView);
        }
        if(scrollView instanceof AdapterView){
            return isAdapterViewTop((AdapterView) scrollView);
        }
        return false;
    }

    private boolean isScrollViewTop(ScrollView scrollView) {
        if (scrollView != null) {
            int scrollViewY = scrollView.getScrollY();
            Log.d(this.getClass().getSimpleName(),"scrollViewY ===>" + scrollViewY);
            return scrollViewY <= 0;
        } else {
            Log.d(this.getClass().getSimpleName(),"scrollView is null");
        }
        return false;
    }

    private boolean isAdapterViewTop(AdapterView adapterView) {
        if (adapterView != null) {
            int firstVisiblePosition = adapterView.getFirstVisiblePosition();
            View childAt = adapterView.getChildAt(0);
            return childAt == null || (firstVisiblePosition == 0 && childAt.getTop() == 0);
        }
        return false;
    }

    /**
     * mVelocityTracker回收
     */
    private void releaseVelocityTracker() {
        if (null != mVelocityTracker) {
            mVelocityTracker.clear();
            mVelocityTracker.recycle();
            mVelocityTracker = null;
        }
    }

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

    @Override
    public boolean onDown(MotionEvent e) {
        return false;
    }

    @Override
    public void onShowPress(MotionEvent e) {}

    @Override
    public boolean onSingleTapUp(MotionEvent e) {
        return false;
    }

    @Override
    public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
        return false;
    }

    @Override
    public void onLongPress(MotionEvent e) {}

    @Override
    public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
        return false;
    }
}

三、布局xml使用




    

        





        

        

        
        

    


四、activity测试stickyView

public class StickyLinearLayoutActivity extends AppCompatActivity {
    private ListView motListView;
    private BaseViewPager baseViewPager;

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_android_view_sticky_linearlayout_mot_page);
        motListView = (ListView) findViewById(R.id.motListView);
//        baseViewPager = (BaseViewPager)findViewById(R.id.baseViewPager);
    }

    @Override
    protected void onResume() {
        super.onResume();
//        initViewPager();
        initMoreListView();
    }

    private void initMoreListView(){
        List 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);
    }

    private void initViewPager(){

        LayoutInflater layoutInflater = LayoutInflater.from(this);
        View firstPageView = layoutInflater.inflate(R.layout.viewpager_first_layout,null);
        View secondPageView = layoutInflater.inflate(R.layout.viewpager_second_layout,null);
        View thirdPageView = layoutInflater.inflate(R.layout.viewpager_third_layout,null);

        BaseListView firstBaseListView =firstPageView.findViewById(R.id.baseFirstListView);
        BaseListView secondBaseListView =secondPageView.findViewById(R.id.baseSecondListView);
        BaseListView thirdBaseListView =thirdPageView.findViewById(R.id.baseThirdListView);

        ArrayAdapter testArrayAdapter = new ArrayAdapter(this,R.layout.list_item_layout,R.id.showText,getTempListData());

        firstBaseListView.setAdapter(testArrayAdapter);
        secondBaseListView.setAdapter(testArrayAdapter);
        thirdBaseListView.setAdapter(testArrayAdapter);

        List views = new ArrayList<>();
        views.add(firstPageView);
        views.add(secondPageView);
        views.add(thirdPageView);

        baseViewPager.setAdapter(new ListPagerAdapter(views));
    }

    private List getTempListData(){
        List resourceList = new ArrayList<>();
        resourceList.add("0000000000000000000000000000");
        resourceList.add("111111111111111111111111");
        resourceList.add("2222222222222222222222222");
        resourceList.add("33333333333333333333333333");
        resourceList.add("4444444444444444444444444");
        resourceList.add("5555555555555555555555555");
        resourceList.add("6666666666666666666666666666");
        resourceList.add("777777777777777777777777777777");
        resourceList.add("88888888888888888888888888888");
        resourceList.add("9999999999999999999999999999");
        resourceList.add("*****************************");
        resourceList.add("");
        return resourceList;
    }

    /**
     * viewpager 适配器
     */
    private static class ListPagerAdapter extends PagerAdapter {

        private List mListViews=new ArrayList<>();

        public ListPagerAdapter(List listViews) {
            mListViews = listViews;
        }

        @Override
        public int getCount() {
            return mListViews.size();
        }

        @Override
        public boolean isViewFromObject(@NonNull View view, @NonNull Object object) {
            return view==object;
        }

        @NonNull
        @Override
        public Object instantiateItem(@NonNull ViewGroup container, int position) {
            container.addView(mListViews.get(position));
            return mListViews.get(position);
        }

        @Override
        public void destroyItem(@NonNull ViewGroup container, int position, @NonNull Object object) {
            container.removeView(mListViews.get(position));
        }
    }

}

关键点:

1、ViewGroup的事件分发很重要

2、思路清晰很重要

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

你可能感兴趣的:(android,android,android,studio)