Android控件篇Android-ui效果自定义控件

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

2016-05-27  本文已影响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;
}

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

初步效果
上一篇下一篇

猜你喜欢

热点阅读