美文网首页Android控件篇Android-ui效果自定义控件
悬浮导航栏StickyNavLayout的实现--基本滑动的实现

悬浮导航栏StickyNavLayout的实现--基本滑动的实现

作者: 皮球二二 | 来源:发表于2016-05-27 17:17 被阅读1384次

    致敬鸿洋,这篇文章是基于他的项目改造而成的

    本次主题将分成3个部分进行讲解,前2部分基本上是对鸿洋的代码解读以及一些小bug的处理,第三部分是针对鸿洋Android-StickyNavLayout不支持的功能进行优化处理。本文对应的项目地址在Github

    啥都先不说,先看看真是场景下的效果图


    豌豆荚效果图

    为什么要改他的项目?鸿洋这个确实非常好,对在viewpager下的listview、recyclerview以及scrollview都做了很好的处理,看上去什么问题都没有。但是在实际使用过程中我发现,不可能每个列表都如预期一样充满屏幕,有的列表由于数据不足,不满足原有代码中一些滑动条件,最终造成一系列不可预知的问题出现。本篇文章就是在鸿洋的基础上,对之前的代码进行分析,并且添加之前未处理部分逻辑

    预热

    在开始进行代码书写工作前,自行把这三个事件流传递过程了解一下,有不明白的地方我们一起探讨

    public boolean dispatchTouchEvent(MotionEvent ev); 
    public boolean onInterceptTouchEvent(MotionEvent ev); 
    public boolean onTouchEvent(MotionEvent ev);
    

    准备开始

    我们首先定义个view,名作StickyNavLayoutView

    public class StickyNavLayoutView extends LinearLayout {
    
        LinearLayout id_topview;
        LinearLayout id_indicatorview;
        ListView id_bottomview;
    
        public StickyNavLayoutView(Context context) {    
            this(context, null);
        }
        public StickyNavLayoutView(Context context, AttributeSet attrs) {    
            super(context, attrs);    
            init();
        }
        private void init() {    
            setOrientation(VERTICAL);
        }
        @Overridepublic boolean dispatchTouchEvent(MotionEvent ev) {    
            return super.dispatchTouchEvent(ev);
        }
        @Overridepublic boolean onInterceptTouchEvent(MotionEvent ev) {    
            return super.onInterceptTouchEvent(ev);
        }
        @Overridepublic boolean onTouchEvent(MotionEvent event) {    
            return super.onTouchEvent(event);
        }
    
        @Override
        protected void onFinishInflate() {    
            super.onFinishInflate();    
            id_topview= (LinearLayout) findViewById(R.id.id_topview);    
            id_indicatorview= (LinearLayout) findViewById(R.id.id_indicatorview);    
            id_bottomview= (ListView) findViewById(R.id.id_bottomview);
        }
    
        @Overridepublic void computeScroll() {    
            super.computeScroll();
        }
    }
    

    这里就是将事件都初始化好,并且设置线型布局的方向是向下的

    随后我们上xml布局文件

    <?xml version="1.0" encoding="utf-8"?>
    <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"    
        android:layout_width="match_parent"    
        android:layout_height="match_parent" >    
        <com.rg.stickynavlayout.myview.StickyNavLayoutView        
            android:id="@+id/stickyNavLayout"        
            android:layout_width="match_parent"        
            android:layout_height="match_parent">        
            <LinearLayout            
                android:id="@id/id_topview"            
                android:background="@android:color/holo_blue_light"          
                android:layout_width="match_parent"            
                android:layout_height="200dip"            
                android:orientation="horizontal">            
                <TextView                
                    android:layout_width="match_parent"                
                    android:layout_height="match_parent"                
                    android:gravity="center"                
                    android:text="这个是头"/>        
            </LinearLayout>        
            <LinearLayout            
                android:id="@id/id_indicatorview"    
                android:background="@android:color/holo_orange_light"      
                android:orientation="horizontal"           
                android:layout_width="match_parent"            
                android:layout_height="100dip">        
            </LinearLayout>        
            <ListView            
                android:id="@id/id_bottomview"         
                android:background="@android:color/holo_green_dark"            
                android:layout_width="match_parent"            
                android:layout_height="match_parent">        
            </ListView>    
        </com.rg.stickynavlayout.myview.StickyNavLayoutView>
    </RelativeLayout>
    

    这边也没有什么,只是将id写死在ids下,方便我直接在自定义view里面使用而已。目录结构定义成上中下三层。我没有使用鸿洋的viewpager嵌套,因为我的需求不需要左右滑动,而且listview你都会了,recyclerview跟scrollview甚至webview你还不会吗?


    布局层次

    初始化

    有些重要的数值我们要先了解一下

    1. ViewGroup的滑动范围 这个范围应该是0~蓝色区域的高度。超过这个范围,黄色悬浮栏置顶并且绿色listview得到并处理事件,ViewGroup没有权利去干预;没有超过这个范围,ViewGroup拦截事件,交由自身TouchEvent去处理滑动事件
    2. ListView的高度 在当前布局下如果你不动态去修改listview的高度,那么你滚动的时候,凭onMeasure测量出的高度是达不到悬停时listview撑满布局的要求的,所以listview的高度应该是总高度减去黄色区域的高度
    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {    
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);    
        mTopViewHeight=id_topview.getMeasuredHeight();    
        LayoutParams params= (LayoutParams) id_bottomview.getLayoutParams();      
        params.height=getMeasuredHeight()-id_indicatorview.getMeasuredHeight();
    }
    

    剩下几个不重要的参数初始化,都是滑动时候的一些常量:允许控件最小滑动距离临界值、允许fling动作的最大最小值

    
    int touchSlop;
    int maxFling;
    int minFling;
    OverScroller scroller;
    VelocityTracker tracker;
    
    private void init(Context context) {    
        setOrientation(VERTICAL);    
    
        scroller=new OverScroller(context);
        touchSlop= ViewConfiguration.get(context).getScaledTouchSlop();    
        maxFling=ViewConfiguration.get(context).getScaledMaximumFlingVelocity();    
        minFling=ViewConfiguration.get(context).getScaledMinimumFlingVelocity();
    }
    
    @Override
    public void computeScroll() {    
        super.computeScroll();    
        if (scroller.computeScrollOffset()) {        
            scrollTo(0, scroller.getCurrY());        
            invalidate();    
        }
    }
    
    private void initVelocityTrackerIfNotExists() {    
        if (tracker==null) {        
            tracker=VelocityTracker.obtain();    
        }
    }
    
    private void recycleVelocityTracker() {    
        if (tracker!=null) {        
            tracker.recycle();        
            tracker=null;    
        }
    }
    

    基本滑动事件编写

    首先定义一个属性 isTopHidden,这个值是滑动事件用来判断ViewGroup是否已经到达可移动的最大距离

    boolean isTopHidden=false;
    

    看看onIntercepTouchEvent是怎么处理的

    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {    
        int y= (int) ev.getY();    
        switch (ev.getAction()) {        
            case MotionEvent.ACTION_DOWN:            
                lastY=y;            
                break;        
            case MotionEvent.ACTION_MOVE:            
                View view=id_bottomview.getChildAt(id_bottomview.getFirstVisiblePosition());        
                //当顶部蓝色区域在显示的时候,ViewGroup接管事件
                //当顶部蓝色区域不显示,达到悬浮条件的时候,如果ListView在滑动到最顶部并且继续向下滑动,ViewGroup接管事件   
                if (!isTopHidden || 
                         (isTopHidden && (y-lastY)>0 && view!=null && view.getTop()==0) ) {                
                    lastY=y;                
                    return true;            
                }            
                lastY=y;            
                break;        
            case MotionEvent.ACTION_CANCEL:        
            case MotionEvent.ACTION_UP:            
                break;    
        }    
        return super.onInterceptTouchEvent(ev);
    }
    

    这里面也就注释所描述的地方非常重要,他关系着到底何时允许ViewGroup滑动。这边比之前的代码少了一个判断,我用模拟器的时候遇到滑动事件小于touchSlop时候出现listview在那边滑动的尴尬,所以我简单做了调整

    @Override
    public boolean onTouchEvent(MotionEvent event) {    
        initVelocityTrackerIfNotExists();    
        tracker.addMovement(event);    
        int y= (int) event.getY();    
        switch (event.getAction()) {        
            case MotionEvent.ACTION_DOWN:            
                if (!scroller.isFinished()) {                
                    scroller.abortAnimation();            
                }            
                lastY=y;            
                return true;        
            case MotionEvent.ACTION_MOVE:            
                scrollBy(0, lastY-y);            
                lastY=y;            
                break;        
            case MotionEvent.ACTION_CANCEL:            
                recycleVelocityTracker();            
                if (!scroller.isFinished()) {                
                    scroller.abortAnimation();            
                }            
                break;        
            case MotionEvent.ACTION_UP:            
                tracker.computeCurrentVelocity(1000, maxFling);            
                if (Math.abs(tracker.getYVelocity())>minFling) {                
                    fling(-tracker.getYVelocity());            
                }            
                recycleVelocityTracker();            
                break;    
            }    
        return super.onTouchEvent(event);
    }
    

    这边就是基本的滑动,也没什么好说的,滑动结束之后,有一个fling操作,稍微有点惯性

    private void fling(float v) {    
        scroller.fling(0, getScrollY(), 0, (int) v, 0, 0, 0, mTopViewHeight);
    }
    

    最终执行滚动的方法,限定了滚动的范围为蓝色区域,同时给出了临界点条件的判断条件:ViewGroup滑动的距离等于蓝色区域的高度

    @Override
    public void scrollTo(int x, int y) {    
        if (y<0) {        
            y=0;    
        }   
        if (y>mTopViewHeight) {        
            y=mTopViewHeight;    
        }    
        if (y!=getScrollY()) {        
            super.scrollTo(0, y);    
        }    
        isTopHidden=getScrollY()==mTopViewHeight;
    }
    

    到目前为止,我们看下效果

    初步效果

    相关文章

      网友评论

      • GoAllOutInWork:如果把中间层改成tabLyaout 下层改成viewpager中fragment再嵌套listview这样的方式 可不可以使用呢?
        皮球二二: @GoAllOutInWork 怎么不符合?
        GoAllOutInWork:@r17171709 可coordinatorLayout不符合我的实现要求,我找了好久就看到你这个基本上最贴近我的需要,不知道是不是我不会用CoordinatorLayout
        皮球二二: @GoAllOutInWork 那你可以直接使用coordinatorLayout,毕竟这种写法是在design出来之前使用的
      • xiasuhuei321:鸿洋大神的确分享了超多的干货
      • l1zheng: :grin: 换成RecyclerView top高度总是计算的不正确
        40d5f596705d:@l1zheng 头像不错。
        皮球二二:@l1zheng 是的,方法不一样,这边可以直接拷贝鸿洋的计算方法

      本文标题:悬浮导航栏StickyNavLayout的实现--基本滑动的实现

      本文链接:https://www.haomeiwen.com/subject/jqzxdttx.html