NestedScrollView 机制是周三听其他组的同学分享才知道的。谈起技术分享,让我学习很多:
关于NestedScrollView 的学习,想在前人的研究基础上,总结两篇文章,一篇文章讲基础应用,一篇简单的深入分析一下源码,所谓知其然,还要知其背后的思想。
本文的写作逻辑如下:
继承该接口的类有:
该接口主要作用是:
An interface that can be implemented by Views to provide scroll related APIs.
传统的ScrollView 只是继承了帧布局,与该类相关的接口有 :
该类的作用是:
NestedScrollView is just like ScrollView, but it supports acting as both a nested scrolling parent and child on both new and old versions of Android. Nested scrolling is enabled by default.
不仅支持传统的 ScrollView功能,还可以支付嵌套的父View 与子View 的滑动,系统默认开启嵌套滑动支持。
与该接口相关的类有 :
该接口的作用:
This interface should be implemented by View subclasses that wish to support dispatching nested scrolling operations to a cooperating parent ViewGroup.
Classes implementing this interface should create a final instance of a NestedScrollingChildHelper as a field and delegate any View methods to the NestedScrollingChildHelper methods of the same signature.
与该接口相关的类有 :
该接口的作用:
This interface should be implemented by ViewGroup subclasses that wish to support scrolling operations delegated by a nested child view.
Classes implementing this interface should create a final instance of a NestedScrollingParentHelper as a field and delegate any View or ViewGroup methods to the NestedScrollingParentHelper methods of the same signature.
官方NestedScrolling机制涉及关键的类(接口),有:
NestedScrollingChild
NestedScrollingParent
NestedScrollingChildHelper
NestedScrollingParentHelper
1、publiv void setNestedScrollingEnabled(boolean enabled);
NestedScrollingChild中的方法,设置该View是否开启Nested Scrolling机制,一个相关联方法为isNestedScrollingEnabled()
2、public boolean startNestedScroll(int axes);
在给定的方向(axes)上开始一个Nested Scroll,只有返回值是true的情况下才会执行之后的方法,否则NestedScrolling机制无效. 默认实现(NestedScrollingChildHelper中的实现)会找到该View的parent,调用parent的onStartNestedScroll方法,如果parent为null或者onStartNestedScroll()返回false,则流程结束.否则调用NestedScrollingParent.onNestedScrollAccepted给其一个初始化配置的机会.
3、public boolean dispatchNestedPreScroll(int dx, int dy, int[] consumed, int[] offsetInWindow)
传统的事件分发机制有个onInterceptTouchEvent()方法来拦截事件,让ViewGroup可以优先于子View处理触摸事件,这个机制而Nested Scrolling机制也提供了类似的机制.这个机制的实现就是依靠dispatchNestedPreScroll方法.这个方法会在child消耗掉滑动事件之前调用,给parent一个率先处理滑动事件的机会.
offsetInWindow是自己本身的窗体变化,这个方法需要计算这个值做处理来修正touch事件的做标志。
4、public boolean dispatchNestedScroll(int dxConsumed, int dyConsumed, int dxUnconsumed, dyUnconsumed, int[] offsetInWindow)
分发nested scroll,这个方法会在该child处理完滑动事件之后调用.无论是否有未消耗的值都会调用. offsetInWindow 同dispatchNestedPreScroll.
5、fling相关
fling机制和scroll类似
6、public void stopNestedScroll();
停止nested scroll
1、public boolean onStartNestedScroll(View child, View target, int nestedScrollAxes);
被NestedScrollingChild的startNestedScroll调用,实现者根据具体情况决定是否使用Nested Scroll机制来返回true或false.
2、public void onNestedScrollAccepted(View child, View target, int nestedScrollAxes);
onStartNestedScroll方法返回true之后被调用,他提供了一个初始化nested scroll相关配置的机会,之所以有这个方法可能是因为onStartNestedScroll无论是否接收nested scroll都回被调用,而该方法只有在确定要执行nested scroll时才会被调用.
3、public void onNestedPreScroll(View target, int dx, int dy, int[] consumed);
响应dispatchNestedPreScroll,在child消耗滑动事件之前调用,实现滑动处理的地方.
4、public void onNestedScroll(View target, int dxConsumed, int dyConsumed,int dxUnconsumed, int dyUnconsumed);
响应dispatchNestedScroll,在child消耗滑动事件之后调用.实现滑动处理.
5、public void onStopNestedScroll(View target);
响应stopNestedScroll,可以做清理工作;
比较形象的可以聊天图:
DEMO主要涉及三个类,NestedScrollingActivity、MyNestedScrollParent、MyNestedScrollChildL;
NestedScrollingActivity:
// 隐藏actionBar
getSupportActionBar().hide();
布局文件:顶层布局是相对布局,依次是MyNestedScrollParentL、MyNestedScrollChildL
MyNestedScrollParentL.java
public class MyNestedScrollParentL extends LinearLayout implements NestedScrollingParent {
private ImageView mImage;
private TextView mTvTitle;
private MyNestedScrollChildL mMyNestedScrollChildL;
private int mImageHeight;
private int mTvTitleHeight;
private NestedScrollingParentHelper mNestedScrollingParentHelper;
public MyNestedScrollParentL(Context context) {
super(context);
init();
}
public MyNestedScrollParentL(Context context, AttributeSet attrs) {
super(context, attrs);
init();
}
private void init(){
mNestedScrollingParentHelper = new NestedScrollingParentHelper(this); //相关事情都交给NestedScrollingParentHelper做
}
@Override
protected void onFinishInflate() {
super.onFinishInflate();
mImage = (ImageView)getChildAt(0);//获取第一个视图
mTvTitle = (TextView)getChildAt(1);
mMyNestedScrollChildL = (MyNestedScrollChildL)getChildAt(2);
//OnGlobalLayoutListener 是ViewTreeObserver的内部类,当一个视图树的布局发生改变时,可以被ViewTreeObserver监听到
//获取图片的高度
mImage.getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
@Override
public void onGlobalLayout() {
if(mImageHeight <= 0){
mImageHeight = mImage.getMeasuredHeight();
Log.i("TEST", "mImageHeight:" + mImageHeight);
}
}
});
//获取标题的高度
mTvTitle.getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
@Override
public void onGlobalLayout() {
if(mTvTitleHeight <= 0){
mTvTitleHeight = mTvTitle.getMeasuredHeight();
Log.i("TEST", "mTvTitleHeight:" + mTvTitleHeight);
}
}
});
}
@Override
public boolean onStartNestedScroll(View child, View target, int nestedScrollAxes) {
Log.i("TEST", "onStartNestedScroll()");
return true;
}
@Override
public void onNestedScrollAccepted(View child, View target, int axes) {
Log.i("TEST", "onNestedScrollAccepted()");
mNestedScrollingParentHelper.onNestedScrollAccepted(child, target, axes);
}
@Override
public void onNestedScroll(View target, int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed) {
Log.i("TEST", "onNestedScrollAccepted()");
}
@Override
public void onNestedPreScroll(View target, int dx, int dy, int[] consumed) {
Log.i("TEST", "onNestedPreScroll()");
//父布局中除去child部分之外的视图
if(isShowImage(dy) || isHideImage(dy)){
consumed[1] = dy/2;
scrollBy(0, dy); // scrollBy 其实会调用我们重写的scrollTo 方法;
}
}
@Override
public void onStopNestedScroll(View child) {
Log.i("TEST", "onStopNestedScroll()");
mNestedScrollingParentHelper.onStopNestedScroll(child);
}
@Override
public boolean onNestedPreFling(View target, float velocityX, float velocityY) {
Log.i("TEST", "onNestedPreFling()");
return false;
}
@Override
public boolean onNestedFling(View target, float velocityX, float velocityY, boolean consumed) {
Log.i("TEST", "onNestedFling()");
return false;
}
@Override
public int getNestedScrollAxes() {
Log.i("TEST", "getNestedScrollAxes()");
return 0;
}
@Override
public void scrollTo(int x, int y) {
Log.i("TEST", "scrollTo():" + "x=" + x + "__y=" + y);
if( y < 0){ //父布局中除去child部分之外的视图保持不动
y = 0;
}
if(y >= mImageHeight){//父布局中除去child部分之外的视图保持图片的高度
y = mImageHeight;
}
super.scrollTo(x, y);
}
/**
* 判断下拉的时候是否需要向下滑动显示图片
* 下拉的时候 dy < 0
*/
public boolean isShowImage(int dy){
Log.i("TEST", "getScrollY():" + getScrollY());
Log.i("TEST", "mMyNestedScrollChildL.getScrollY():" + mMyNestedScrollChildL.getScrollY());
// dy< 0 表示在下拉;
if(dy < 0){
if(getScrollY() > 0 && mMyNestedScrollChildL.getScrollY() == 0){
return true;
}
}
return false;
}
/**
* 上滑的时候;
* @param dy> 0 表示在上滑
* @return
*/
public boolean isHideImage(int dy){
Log.i("TEST", "getScrollY():" + getScrollY());
Log.i("TEST", "mMyNestedScrollChildL.getScrollY():" + mMyNestedScrollChildL.getScrollY());
if(dy > 0){
if(getScrollY() < mImageHeight){
return true;
}
}
return false;
}
@Override
public boolean onTouchEvent(MotionEvent event) {
Log.i("TEST", "onTouchEvent():" + event.getAction());
return super.onTouchEvent(event);
}
@Override
public boolean dispatchTouchEvent(MotionEvent event) {
Log.i("TEST","dispatchTouchEvent _ getY():getRawY:"+event.getRawY());
return super.dispatchTouchEvent(event);
}
}
MyNestedScrollChildL .java
public class MyNestedScrollChildL extends LinearLayout implements NestedScrollingChild {
private NestedScrollingChildHelper mScrollingChildHelper;
public static final String Tag = "MyNestedScrollChildL";
private final int[] mScrollOffset = new int[2];
private final int[] mScrollConsumed = new int[2];
private final int[] mNestedOffsets = new int[2];
private int mLastTouchX;
private int mLastTouchY;
private int showHeight;
public MyNestedScrollChildL(Context context) {
super(context);
init();
}
public MyNestedScrollChildL(Context context, AttributeSet attrs) {
super(context, attrs);
init();
}
private void init(){
if (mScrollingChildHelper == null) {
mScrollingChildHelper = new NestedScrollingChildHelper(this);
mScrollingChildHelper.setNestedScrollingEnabled(true);
}
}
@Override
public void setNestedScrollingEnabled(boolean enabled) {
Log.i(Tag, "setNestedScrollingEnabled:" + enabled);
mScrollingChildHelper.setNestedScrollingEnabled(enabled);
}
@Override
public boolean isNestedScrollingEnabled() {
Log.i(Tag, "isNestedScrollingEnabled");
return mScrollingChildHelper.isNestedScrollingEnabled();
}
@Override
public boolean startNestedScroll(int axes) {
return mScrollingChildHelper.startNestedScroll(axes);
}
@Override
public void stopNestedScroll() {
Log.i(Tag, "stopNestedScroll");
mScrollingChildHelper.stopNestedScroll();
}
@Override
public boolean hasNestedScrollingParent() {
Log.i(Tag, "hasNestedScrollingParent");
return mScrollingChildHelper.hasNestedScrollingParent();
}
@Override
public boolean dispatchNestedScroll(int dxConsumed, int dyConsumed, int dxUnconsumed,
int dyUnconsumed, int[] offsetInWindow) {
Log.i(Tag, "dispatchNestedScroll()");
return mScrollingChildHelper.dispatchNestedScroll(dxConsumed, dyConsumed,
dxUnconsumed, dyUnconsumed, offsetInWindow);
}
@Override
public boolean dispatchNestedPreScroll(int dx, int dy, int[] consumed, int[] offsetInWindow) {
Log.i(Tag, "dispatchNestedPreScroll()");
return mScrollingChildHelper.dispatchNestedPreScroll(dx, dy, consumed, offsetInWindow);
}
@Override
public boolean dispatchNestedFling(float velocityX, float velocityY, boolean consumed) {
return mScrollingChildHelper.dispatchNestedFling(velocityX, velocityY, consumed);
}
@Override
public boolean dispatchNestedPreFling(float velocityX, float velocityY) {
Log.i(Tag, "dispatchNestedPreFling()");
return mScrollingChildHelper.dispatchNestedPreFling(velocityX, velocityY);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
switch (event.getAction()){
case MotionEvent.ACTION_DOWN:
mLastTouchY = (int)(event.getRawY() + 0.5f);
int nestedScrollAxis = ViewCompat.SCROLL_AXIS_NONE;
nestedScrollAxis |= ViewCompat.SCROLL_AXIS_HORIZONTAL; //按位或运算
Log.i(Tag, "nestedScrollAxis:" + nestedScrollAxis);
startNestedScroll(nestedScrollAxis);
break;
case MotionEvent.ACTION_MOVE:
Log.i(Tag, "Child--getRawY:" + event.getRawY());
int x = (int) (event.getX() + 0.5f);
int y = (int) (event.getRawY() + 0.5f);
int dx = mLastTouchX - x;
int dy = mLastTouchY - y;
Log.i(Tag, "child:dy:" + dy + ",mLastTouchY:" + mLastTouchY + ",y;" + y);
mLastTouchY = y;
mLastTouchX = x;
if (dispatchNestedPreScroll(dx, dy, mScrollConsumed, mScrollOffset)) {
dy -= mScrollConsumed[1];
if (dy == 0) {
return true;
}
} else {
scrollBy(0, dy);
}
break;
}
return true;
}
@Override
public void scrollTo(int x, int y) {
int MaxY = getMeasuredHeight() - showHeight;
if (y > MaxY) {
y = MaxY;
}
if (y < 0) {
y = 0;
}
super.scrollTo(x, y);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
if (showHeight <= 0) {
showHeight = getMeasuredHeight();
} else {
heightMeasureSpec = MeasureSpec.makeMeasureSpec(1000000, MeasureSpec.UNSPECIFIED);
}
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
}
}
执行效果:
向上滑动时,标题可以固定:
参考:
1、NestedScrollingChild
2、NestedScrollingParent
3、NestedScrolling