自从5.0推出NestedScrolling这个机制一直没有去了解,之前项目中要求做这种效果后觉得还是有必要学习一下,掌握这个机制是可以做出现在很多App流行的联动滚动效果的。
比如饿了么这种效果:
原谅我随便盗个效果图~~
这个机制也是挺复杂的,需要对于view知识有一定的了解,自己刚开始学习这个,也算对于自己学习的一个总结.
贴下布局
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context="com.example.boboyuwu.nestedscrolling.MainActivity">
<com.example.boboyuwu.nestedscrolling.LinearNestedScrollParentLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<TextView
android:layout_width="match_parent"
android:layout_height="50dp"
android:background="@android:color/holo_blue_light"
android:gravity="center"
android:text="这是我的ActionBar"
android:textColor="@android:color/white"/>
<TextView
android:layout_width="match_parent"
android:layout_height="30dp"
android:background="@android:color/holo_red_light"
android:gravity="center"
android:text="这是中间隔的一个view哦"/>
<com.example.boboyuwu.nestedscrolling.LinearNestedScrollChildLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<TextView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:text="@string/content"/>
</com.example.boboyuwu.nestedscrolling.LinearNestedScrollChildLayout>
</com.example.boboyuwu.nestedscrolling.LinearNestedScrollParentLayout>
</FrameLayout>
首先我们定义一个
LinearNestedScrollParentLayout extends LinearLayout implements NestedScrollingParent
实现NestedScrollingParent接口
public boolean onStartNestedScroll(View child, View target, int nestedScrollAxes);
public void onStopNestedScroll(View target);
public void onNestedScroll(View target, int dxConsumed, int dyConsumed,
int dxUnconsumed, int dyUnconsumed);
public void onNestedPreScroll(View target, int dx, int dy, int[] consumed);
public boolean onNestedFling(View target, float velocityX, float velocityY, boolean consumed);
public boolean onNestedPreFling(View target, float velocityX, float velocityY);
public int getNestedScrollAxes();
做为一个接口特么居然这么多实现方法,额滴天啊是不是一眼就想放弃的感觉,我也是这样想的,不过还好系统给我们提供了一个helper类自动帮我们处理这些逻辑.
我们在init初始化的时候new一个helper对象
mNestedScrollingParentHelper = new NestedScrollingParentHelper(this);
然后在每个实现方法里我们这样调用
mNestedScrollingParentHelper.onNestedScrollAccepted(child,target,nestedScrollAxes);
把方法中参数传递到helper类相同方法的参数中,copy copy就行了~
ok我们复制粘贴一通终于"实现了"所有的方法.
接下来以同样的方式定义一个LinearNestedScrollChildLayout类
public class LinearNestedScrollChildLayout extends LinearLayout implements NestedScrollingChild
它实现NestedScrollingChild接口,同样我们要实现一堆的方法,这个类也给我们提供了一个helper帮助类简化我们的工作
在init的时候我们创建一个helper对象
mNestedScrollingChildHelper = new NestedScrollingChildHelper(this);
跟parent同样的方式我们使用这个helper类去实现接口中的所有方法,这里就不贴代码了跟上面是一样的.
好了,基本工作做完接下来我们就去实现一个类似的效果。
我们重写child触摸事件
@Override
public boolean onTouchEvent(MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
mStartY = event.getY();
break;
case MotionEvent.ACTION_MOVE:
float y = event.getY();
int diffY = (int) (mStartY - y);
if (startNestedScroll(View.SCROLL_AXIS_VERTICAL) && dispatchNestedPreScroll(0, diffY, consume, offset)) {
mOffsetY += offset[1];
Log.e("wwwconsume", " offset:" + mOffsetY);
} else {
scrollBy(0, diffY);
}
Log.e("wwww", diffY + " startY:" + mStartY + " y:" + y + " top:" + getTop());
mStartY = y;
break;
case MotionEvent.ACTION_UP:
break;
}
return true;
}
当我们移动的时候调用startNestedScroll(View.SCROLL_AXIS_VERTICAL)方法通知我们parent我要开始滑动的,里面传入一个参数,这里我传入一个垂直标记,parent可以根据这个判断返回boolean值,如果返回true就优先于child滑动,如果返回false,那么我们的if判断就不成立走else,自然parent也就不接受这次滚动事件,同时我们还要调用dispatchNestedPreScroll(0, diffY, consume, offset)这个方法里面参数分别是
params:
int dx -> 自己当前移动位置的x坐标
int dy ->自己当前移动位置的y坐标
int[] consumed -> 一个int[]数组 用于接收parent优先滑动消费的x,y距离,这个由parent滑动消费后指定
int[] offsetInWindow -> 一个int[]数组 用于记录parent每次事件的偏移量,如果每次child滑动距离parent全部消费了,那么consumed 里面的值是等同于
offsetInWindow 偏移量的我们在if()判断里打印下看看是不是这样
06-07 14:36:50.619 25570-25570/com.example.boboyuwu.nestedscrolling E/wwwconsume: offset:-3 consume:3
06-07 14:36:50.632 25570-25570/com.example.boboyuwu.nestedscrolling E/wwwconsume: offset:-18 consume:18
06-07 14:36:50.649 25570-25570/com.example.boboyuwu.nestedscrolling E/wwwconsume: offset:-1 consume:1
06-07 14:36:50.666 25570-25570/com.example.boboyuwu.nestedscrolling E/wwwconsume: offset:-15 consume:15
06-07 14:36:50.682 25570-25570/com.example.boboyuwu.nestedscrolling E/wwwconsume: offset:2 consume:-2
06-07 14:36:50.699 25570-25570/com.example.boboyuwu.nestedscrolling E/wwwconsume: offset:-9 consume:9
``
忽略helper帮我处理后的符号问题可以看到值都是一模一样的因为我在parent消费完了child的滚动距离
ok现在在parent中我们去实现一些细节
@Override
public boolean onStartNestedScroll(View child, View target, int nestedScrollAxes) {
Log.e("wwww","onStartNestedScroll");
return child instanceof NestedScrollingChild && nestedScrollAxes==View.SCROLL_AXIS_VERTICAL;
}
在onStartNestedScroll中我简单的判断了一下直接返回true代表parent接受这次事件在child之前滚动,这个方法调用完毕后会接着调用onNestedScrollAccepted方法我们可以在开始滚动之前进行一些设置操作,
接着child的dispatchNestedPreScroll会调用parent的
onNestedPreScroll(View target, int dx, int dy, int[] consumed) 方法,在这里
设置了2个方法
@Override
public void onNestedPreScroll(View target, int dx, int dy, int[] consumed) {
//滚动之前消费
boolean b = hasHide(dy);
boolean b1 = hasShow(dy);
Log.e("aaaaaaaaaaa","b:"+b+" b1:"+b1);
if(hasHide(dy) || hasShow(dy)){
scrollBy(0,dy);
consumed[1]=dy;
}
}
private boolean hasShow(int dy) {
if(dy<0){
if(getScrollY()>0 && mChildAt2.getScrollY()==0){
Log.e("wwwhasShow", getScrollY()+ " " +mChildAt2.getScrollY()+" "+mChildAt2.getScrollY());
return true;
}
}
return false;
}
public boolean hasHide(int dy){
if(dy>0){
if(getScrollY()<mChildAt0MeasuredHeight && mChildAt2.getScrollY()==0){
Log.e("wwwhasHide", getScrollY()+" " +mChildAt2.getScrollY());
return true;
}
}
return false;
}
当接收到child传过来的dy时,根据scrollTo特性如果是-的认为是向下滚动,如果是+的则向上滚动,所以这里dy<0的时候我们判断向下滚动,这里还需要判断一下mChildAt2.getScrollY()==0因为少了这个判断的话,我们向上滚动一段距离,后再向下滚动这时应该child优先滚动,但是由于 getScrollY()始终>0那么这个方法永远返回true, 如果我们增加判断,由于我们之前child向上滚动过一段距离那么getScrollY()!=0的所以这个时候判断不成立滚动就交给我们child了。
同样当dy>0则向上滚动,同样逻辑增加一个判断.
我们滚动自己内容的时候是要限制一下滚动区域的,本来我期望向上滚动的时候滚动actionbar title高度parent就停止滚动,如果不限制它会一直向上滚,所以我们重写一下scrollTo方法因为scrollBy内部也是调用scorllTo的,
@Override
public void scrollTo(@Px int x, @Px int y) {
Log.e("wwwwscrollTo","x:"+x+" y:"+y+" mChildAt0MeasuredHeight"+mChildAt0MeasuredHeight);
if(y<0){
y=0;
}
if(y>mChildAt0MeasuredHeight && mChildAt0MeasuredHeight!=0){
y=mChildAt0MeasuredHeight;
}
super.scrollTo(x, y);
}
这里限制一下如果如果y<0说明向下已经滚动到actionbar title高度,我们限制y=0
如果向上滚动dy>actionbar title 的height高度那么则限制它就为height
同样child里面也应该限制下滚动范围原理跟parent一样就不贴代码了,好了,大概思路已经实现了,这里实现方式比较简单主要是熟悉一下NestedScrolling滚动机制,我们看一下代码运行效果,模拟器有点卡真机流畅很多