以前的文章
[Android] ObservableScrollView分析(一)—— Quick Start
[Android] ObservableScrollView分析(二)—— Samples and Basic/Advanced techniques
[Android] ObservableScrollView分析(三)—— 显示/隐藏 Toolbar
[Android] ObservableScrollView分析(四)—— 视差图像 Parallax image 实现
[Android] ObservableScrollView分析(五)—— Sticky header 顶部固定
[Android] ObservableScrollView分析(六)—— Toolbar上的弹性空白
[Android] ObservableScrollView分析(七)—— 使用图像的弹性空白布局
概述
前面的章节,我们主要介绍了如何利用开源库 ObservableScrollView 来实现出各种我们需要的滚动效果的实例和实现代码,却一直没有分析过在实现过程中所使用的一系列 ObservablexxxView
的源代码,今天我们就来看看,在开源库中 ObservableRecyclerView 的源码。
ObservableRecyclerView 关系图如下:
由图可知:ObservableRecyclerView 继承自 RecyclerView 并实现 Scrollable 接口,与此同时,ObservableRecyclerView 还持有四个类的引用,这四个类分别是:ObservableScrollViewCallbacks、ScrollState、ViewGroup 和 MotionEvent,其中:
ObservableScrollViewCallbacks 是开源库所定义的一个回调接口;
ScrollState 是一个枚举类,表示了滑动的三个状态: STOP
、 UP
和 DOWN
;
ViewGroup 用于在 Touch 事件拦截的过程中,指定父类 View;
当用户触摸屏幕时则会产生一个 MotionEvent 对象,在 重写 onTouchEvent()
方法和 onInterceptTouchEvent()
方法时,都需要传入 MotionEvent 参数。
Scrollable
接口方法
首先来看 Scrollable 接口,该接口是开源库所定义的一个接口,给所有接下来需要实现可观测,可滚动的控件(RecyclerView、ScrollView、ListView、WebView 和 GridView)提供了一个通用的应用程序接口,实现该接口必须实现以下几个方法:
- setScrollViewCallbacks(ObservableScrollViewCallbacks listener):设置一个回调监听
- addScrollViewCallbacks(ObservableScrollViewCallbacks listener):增加一个回调监听
- removeScrollViewCallbacks(ObservableScrollViewCallbacks listener):删除一个回调监听
- clearScrollViewCallbacks():清除所有回调监听
- scrollVerticallyTo(int y):垂直滚动到坐标 y 处(y 为绝对坐标)
- getCurrentScrollY():返回当前 y 坐标
- setTouchInterceptionViewGroup(ViewGroup viewGroup):设置一个触摸事件拦截的 ViewGroup ,用来将拦截事件传递到该 View 的父类 View 中去,这也是为什么 ObservableRecyclerView 类中会有 ViewGroup 类的引用的原因。
总结一下 Scrollable 接口,主要完成对滚动控件回调接口的设置、增加、删除和清理的工作,还能实现滚动的功能,返回当前垂直方向上的坐标,并且对拦截事件的处理进行相关设置。
具体实现
在 ObservableRecyclerView 中,这些方法分别是如何实现的?接下来让我们看一下:
@Override
public void setScrollViewCallbacks(ObservableScrollViewCallbackslistener) {
mCallbacks = listener;
}
@Override
public void addScrollViewCallbacks(ObservableScrollViewCallbackslistener) {
if (mCallbackCollection == null) {
mCallbackCollection = new ArrayList<>();
}
mCallbackCollection.add(listener);
}
@Override
public void removeScrollViewCallbacks(ObservableScrollViewCallbackslistener) {
if (mCallbackCollection != null) {
mCallbackCollection.remove(listener);
}
}
@Override
public void clearScrollViewCallbacks() {
if (mCallbackCollection != null) {
mCallbackCollection.clear();
}
}
@Override
public void setTouchInterceptionViewGroup(ViewGroupviewGroup) {
mTouchInterceptionViewGroup = viewGroup;
}
@Override
public void scrollVerticallyTo(int y) {
ViewfirstVisibleChild = getChildAt(0);
if (firstVisibleChild != null) {
int baseHeight = firstVisibleChild.getHeight();
int position = y / baseHeight;
scrollVerticallyToPosition(position);
}
}
@Override
public int getCurrentScrollY() {
return mScrollY;
}
ObservableRecyclerView 持有一个 ArrayList,如果需要添加多个回调接口,则将添加的接口放入该容器中,删除和清理操作也是基于该容器的操作。注意一下 scrollVerticallyTo()
方法的实现,需要结合 scrollVerticallyToPosition()
方法一起阅读:
public void scrollVerticallyToPosition(int position) {
LayoutManagerlm = getLayoutManager();
if (lm != null && lminstanceof LinearLayoutManager) {
((LinearLayoutManager) lm).scrollToPositionWithOffset(position, 0);
} else {
scrollToPosition(position);
}
}
其中,在 scrollVerticallyToPosition()
方法中,针对 LayoutManager 的类型会有一个判断,如果 LayoutManager 是 LinearLayoutManager,执行 scrollToPositionWithOffset()
方法,否则执行 scrollToPosition()
方法,完成最后的滚动操作。
ObservableScrollViewCallbacks
接口方法
ObservableRecyclerView 持有 ObservableScrollViewCallbacks 的引用,该接口也是开源库所定义的接口,当使用 ObservableRecyclerView 的时候,由用户自己定义滚动时的回调函数,该接口中的方法如下:
- onScrollChanged(int scrollY, boolean firstScroll, boolean dragging):在滚动状态发生改变的时候调用,但是不会在第一次加载页面的时候调用,如果需要在该方法中初始化布局,需要手动去调用一下。
- onDownMotionEvent():手指按下的事件发生时的回调函数。
- onUpOrCancelMotionEvent(ScrollState scrollState):手指抬起的事件发生或者滚动事件被取消时的回调函数。
具体实现
总的来说,在使用的时候,设置监听后重写回调接口中的这三个方法,已经足够实现我们所希望的滚动监听效果了。还记得最简单的那个 ActionBar 的 显示/隐藏
示例吗?首先,让 Activity 实现该接口:
public class ActionBarControlRecyclerViewActivityextends BaseActivityimplements ObservableScrollViewCallbacks ……
然后我们在 onCreate 中初始化 ObservableRecyclerView,并为它设置滚动监听:
ObservableRecyclerViewrecyclerView = (ObservableRecyclerView) findViewById(R.id.recycler);
recyclerView.setLayoutManager(new LinearLayoutManager(this));
recyclerView.setHasFixedSize(true);
recyclerView.setScrollViewCallbacks(this);
设置好监听后,必须要实现以上三个方法,为了达到 ActionBar 的 显示/隐藏
效果,需要这样实现方法:
@Override
public void onScrollChanged(int scrollY, boolean firstScroll, boolean dragging) {
}
@Override
public void onDownMotionEvent() {
}
@Override
public void onUpOrCancelMotionEvent(ScrollStatescrollState) {
ActionBarab = getSupportActionBar();
if (ab == null) {
return;
}
if (scrollState == ScrollState.UP) {
if (ab.isShowing()) {
ab.hide();
}
} else if (scrollState == ScrollState.DOWN) {
if (!ab.isShowing()) {
ab.show();
}
}
}
这样我们就完成了这个效果的实现。
注意,由于该效果的代码是放在 onUpOrCancelMotionEvent()
这个方法中,当你在滑动该页面的同时,并不会出现 显示/隐藏
的效果,而是要在手指抬起的瞬间才会产生相应的效果,如果你希望在滚动的同时产生 显示/隐藏
的效果,应该将实现该效果的代码放入 onScrollChanged()
方法中去。
ObservableRecyclerView 其他重要方法
下面我们来看一下 ObservableRecyclerView 中其他的重要的方法,其中有些是重写的父类方法,有些是自己定义的方法。
重写父类的方法有:
– onRestoreInstanceState(Parcelable state)
– onSaveInstanceState()
– onScrollChanged(int l, int t, int oldl, int oldt)
– onInterceptTouchEvent(MotionEvent ev)
– onTouchEvent(MotionEvent ev)
– getChildAdapterPosition(View child)
自己的方法:
– init()
– dispatchOnDownMotionEvent()
– dispatchOnScrollChanged(int scrollY, boolean firstScroll, boolean dragging)
– dispatchOnUpOrCancelMotionEvent(ScrollState scrollState)
– hasNoCallbacks()
以及一个内部类: SavedState
保存状态
onRestoreInstanceState()
和 onSaveInstanceState()
以及内部类 SavedState
完成一些临时性的状态的保存工作,需要保存的属性如下:
private int mPrevFirstVisiblePosition;
private int mPrevFirstVisibleChildHeight = -1;
private int mPrevScrolledChildrenHeight;
private int mPrevScrollY;
private int mScrollY;
private SparseIntArraymChildrenHeights;
在某个时刻 Activity 因为系统回收资源的问题要被杀掉,通过 onSaveInstanceState 将有机会保存其用户界面状态,使得将来用户返回到 Activity 时能通过 onCreate(Bundle) 或者 onRestoreInstanceState(Bundle) 恢复界面的状态。
onScrollChanged
onScrollChanged(int l, int t, int oldl, int oldt)重写 View 中的方法,代码如下:
@Override
protected void onScrollChanged(int l, int t, int oldl, int oldt) {
super.onScrollChanged(l, t, oldl, oldt);
if (hasNoCallbacks()) {
return;
}
if (getChildCount() > 0) {
int firstVisiblePosition = getChildAdapterPosition(getChildAt(0));
int lastVisiblePosition = getChildAdapterPosition(getChildAt(getChildCount() - 1));
for (int i = firstVisiblePosition, j = 0; i <= lastVisiblePosition; i++, j++) {
int childHeight = 0;
Viewchild = getChildAt(j);
if (child != null) {
if (mChildrenHeights.indexOfKey(i) < 0 || (child.getHeight() != mChildrenHeights.get(i))) {
childHeight = child.getHeight();
}
}
mChildrenHeights.put(i, childHeight);
}
ViewfirstVisibleChild = getChildAt(0);
if (firstVisibleChild != null) {
if (mPrevFirstVisiblePosition < firstVisiblePosition) {
// 向下滑动
int skippedChildrenHeight = 0;
if (firstVisiblePosition - mPrevFirstVisiblePosition != 1) {
for (int i = firstVisiblePosition - 1; i > mPrevFirstVisiblePosition; i--) {
if (0 < mChildrenHeights.indexOfKey(i)) {
skippedChildrenHeight += mChildrenHeights.get(i);
} else {
// 把每个 item 的高度近似为第一个可见子 View 的高度
// 这样计算也许不正确,但如果不这样做,当从底部向上滑动时scrollY会出错
skippedChildrenHeight += firstVisibleChild.getHeight();
}
}
}
mPrevScrolledChildrenHeight += mPrevFirstVisibleChildHeight + skippedChildrenHeight;
mPrevFirstVisibleChildHeight = firstVisibleChild.getHeight();
} else if (firstVisiblePosition < mPrevFirstVisiblePosition) {
// 向上滑动
int skippedChildrenHeight = 0;
if (mPrevFirstVisiblePosition - firstVisiblePosition != 1) {
for (int i = mPrevFirstVisiblePosition - 1; i > firstVisiblePosition; i--) {
if (0 < mChildrenHeights.indexOfKey(i)) {
skippedChildrenHeight += mChildrenHeights.get(i);
} else {
// 把每个 item 的高度近似为第一个可见子 View 的高度
// 这样计算也许不正确,但如果不这样做,当从底部向上滑动时 scrollY 会出错
skippedChildrenHeight += firstVisibleChild.getHeight();
}
}
}
mPrevScrolledChildrenHeight -= firstVisibleChild.getHeight() + skippedChildrenHeight;
mPrevFirstVisibleChildHeight = firstVisibleChild.getHeight();
} else if (firstVisiblePosition == 0) {
mPrevFirstVisibleChildHeight = firstVisibleChild.getHeight();
mPrevScrolledChildrenHeight = 0;
}
if (mPrevFirstVisibleChildHeight < 0) {
mPrevFirstVisibleChildHeight = 0;
}
mScrollY = mPrevScrolledChildrenHeight - firstVisibleChild.getTop() + getPaddingTop();
mPrevFirstVisiblePosition = firstVisiblePosition;
dispatchOnScrollChanged(mScrollY, mFirstScroll, mDragging);
if (mFirstScroll) {
mFirstScroll = false;
}
if (mPrevScrollY < mScrollY) {
//向下
mScrollState = ScrollState.UP;
} else if (mScrollY < mPrevScrollY) {
//向上
mScrollState = ScrollState.DOWN;
} else {
mScrollState = ScrollState.STOP;
}
mPrevScrollY = mScrollY;
}
}
}
从上面的代码我们可以得知:
1、没有设置回调的话,直接返回,不会将触摸事件进行分发;
2、子布局的数量大于0的情况下,获取第一个子布局的位置 firstVisiblePosition
和最后一个子布局的位置 lastVisiblePosition
;
3、循环遍历所有子布局,当 SparseIntArray 容器中没有保存该子布局的高度,或者保存的值跟该子布局现在的高度不一致时,将该值存入容器中;
4、比较 mPrevFirstVisiblePosition
和 firstVisiblePosition
的差值,判断此时的滑动方向(是 UP
还是 DOWN
),分别求 mPrevScrolledChildrenHeight
和 mPrevFirstVisibleChildHeight
的大小,最终求得 mScrollY
的值,调用 dispatchOnScrollChanged()
,让回调方法去实现最终的 onScrollChanged()
方法。
事件分发
onInterceptTouchEvent()
和 onTouchEvent()
重写了 ViegGroup 中的方法,前者完成对触摸事件的拦截,如果检测到手指按下,则调用方法 dispatchOnDownMotionEvent()
。在方法 dispatchOnDownMotionEvent()
中,对事件进行处理,处理方式为:如果设置了回调,就调用回调方法中的 onDownMotionEvent()
方法(具体是什么操作需要用户自己在使用时实现),如果存放回调接口的容器不为零,将遍历容器中的每一个接口,调用每个接口的 onDownMotionEvent()
方法。
onTouchEvent()
方法中,如果检测到触摸事件被取消,则调用 dispatchOnUpOrCancelMotionEvent()
方法,跟上面的 dispatchOnDownMotionEvent()
方法类似,也会调用回调方法中的 onUpOrCancelMotionEvent()
方法,或者遍历容器。
而当检测到触摸事件为 MOVE
时,情况就复杂了许多,我们看代码:
case MotionEvent.ACTION_MOVE:
if (mPrevMoveEvent == null) {
mPrevMoveEvent = ev;
}
float diffY = ev.getY() - mPrevMoveEvent.getY();
mPrevMoveEvent = MotionEvent.obtainNoHistory(ev);
if (getCurrentScrollY() - diffY <= 0) {
if (mIntercepted) {
return false;
}
final ViewGroupparent;
if (mTouchInterceptionViewGroup == null) {
parent = (ViewGroup) getParent();
} else {
parent = mTouchInterceptionViewGroup;
}
float offsetX = 0;
float offsetY = 0;
for (View v = this; v != null && v != parent; v = (View) v.getParent()) {
offsetX += v.getLeft() - v.getScrollX();
offsetY += v.getTop() - v.getScrollY();
}
final MotionEventevent = MotionEvent.obtainNoHistory(ev);
event.offsetLocation(offsetX, offsetY);
if (parent.onInterceptTouchEvent(event)) {
mIntercepted = true;
event.setAction(MotionEvent.ACTION_DOWN);
post(new Runnable() {
@Override
public void run() {
parent.dispatchTouchEvent(event);
}
});
return false;
}
return super.onTouchEvent(ev);
}
break;
在该方法中, mTouchInterceptionViewGroup
将设置好的拦截 View 赋值给 parent
,如果没有设置,则自动赋值当前 View 的父类给 parent
。得到 parent
以后,就可以完成物理坐标向逻辑坐标的转换:
event.offsetLocation(offsetX, offsetY);
如果父类已经将该事件拦截,则返回 false,并且启动线程调用父类的 dispatchTouchEvent()
方法。
getChildAdapterPosition 和 init
最后, getChildAdapterPosition()
方法重写自 View 中的方法,根据 recyclerViewLibraryVersion
的值判断是调用 getChildAdapterPosition()
还是 getChildPosition()
,而 init()
方法在构造器中被调用,它新建一个SparseIntArray(比 HashMap 效率更高,可以提高性能),同时调用
checkLibraryVersion()
检查 RecyclerView 库的版本号。
ObservableRecyclerView 的源码分析到这里也就差不多了,通过阅读 ObservableRecyclerView 的源码,又学到了许多的知识,尤其是巩固了之前学的不是很明白的 View 的事件拦截和事件处理这部分的内容,让我受益匪浅。