致敬鸿洋,这篇文章是基于他的项目改造而成的
本次主题将分成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你还不会吗?
布局层次
初始化
有些重要的数值我们要先了解一下
-
ViewGroup的滑动范围
这个范围应该是0~蓝色区域的高度。超过这个范围,黄色悬浮栏置顶并且绿色listview得到并处理事件,ViewGroup没有权利去干预;没有超过这个范围,ViewGroup拦截事件,交由自身TouchEvent去处理滑动事件 -
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;
}
到目前为止,我们看下效果
初步效果
网友评论