一、NestedScrollingParent & NestedScrollingChild
1.基础
NestedScrollingParent & NestedScrollingChild
dispatchNestedPreScroll:
在子view的onInterceptTouchEvent或者onTouch中(一般在MontionEvent.ACTION_MOVE事件里),调用该方法通知父view滑动的距离。该方法的第三、第四个参数返回父view消费掉的scroll长度和子view的窗体偏移量。如果这个scroll没有被消费完,则子view进行处理剩下的一些距离,由于窗体进行了移动,如果你记录了手指最后的位置,需要根据第四个参数offsetInWindow计算偏移量,才能保证下一次的touch事件的计算是正确的。如果父view接受了它的滚动参数,进行了部分消费,则这个函数返回true,否则为false。这个函数一般在子view处理scroll前调用。
2.Demo
布局文件:
<?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.example.androidtest_20200329.NestedParentView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_margin="200px"
android:background="@color/colorPrimary">
<com.example.androidtest_20200329.NestedChildView
android:layout_width="match_parent"
android:layout_height="500px"
android:layout_marginTop="200px"
android:background="@color/colorAccent"/>
</com.example.androidtest_20200329.NestedParentView>
</RelativeLayout>
NestedChildView:
public class NestedChildView extends View implements NestedScrollingChild {
private static final String TAG = NestedChildView.class.getSimpleName();
private final NestedScrollingChildHelper childHelper = new NestedScrollingChildHelper(this);
private float downY;
private int[] consumed = new int[2];
private int[] offsetInWindow = new int[2];
public NestedChildView(Context context) {
super(context);
init();
}
public NestedChildView(Context context, AttributeSet attrs) {
super(context, attrs);
init();
}
private void init() {
setNestedScrollingEnabled(true);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
int action = event.getAction();
switch (action) {
case MotionEvent.ACTION_DOWN:
downY = event.getY();
Log.d(TAG, String.format("ACTION_DOWN, downY: %f", downY));
//通知父View,开始滑动
startNestedScroll(ViewCompat.SCROLL_AXIS_VERTICAL);
break;
case MotionEvent.ACTION_MOVE:
//计算出滑动的偏移量
float deltaY = event.getY() - downY;
Log.d(TAG, String.format("ACTION_MOVE, before dispatchNestedPreScroll, deltaY: %f", deltaY));
//通知父View,子View想滑动deltaY个偏移量,父View要不要先滑一下,然后把父View滑了多少,告诉子View一下
//下面这个方法的前两个参数为在x,y方向上想要滑动的偏移量
//第三个参数为一个长度为2的整型数组,父View将消费掉的距离放置在这个数组里面
//第四个参数为一个长度为2的整型数组,子View在屏幕里面的偏移量放置在这个数组里面
//返回值为true,表示父View有消费任何的滑动
if (dispatchNestedPreScroll(0, (int) deltaY, consumed, offsetInWindow)) {
//偏移量需要减掉被父View消费掉的
deltaY -= consumed[1];
Log.d(TAG, String.format("ACTION_MOVE, after dispatchNestedPreScroll, deltaY: %f", deltaY));
}
//上面的 (int)deltaY 会造成精度丢失,这里把精度给舍弃掉
if (Math.floor(Math.abs(deltaY)) == 0) {
deltaY = 0;
}
//父View滑动后,子View进行滑动
float newY = getY() + deltaY;
if(newY < 0) {
setY(0);
} else if (newY > ((View) getParent()).getHeight() - getHeight()) {
setY(((View) getParent()).getHeight() - getHeight());
} else {
setY(newY);
}
break;
}
return true;
}
@Override
public void setNestedScrollingEnabled(boolean enabled) {
Log.d(TAG, String.format("setNestedScrollingEnabled, enabled: %b", enabled));
childHelper.setNestedScrollingEnabled(enabled);
}
@Override
public boolean isNestedScrollingEnabled() {
Log.d(TAG, "isNestedScrollingEnabled");
return childHelper.isNestedScrollingEnabled();
}
@Override
public boolean startNestedScroll(int axes) {
Log.d(TAG, String.format("startNestedScroll, axes: %d", axes));
return childHelper.startNestedScroll(axes);
}
@Override
public void stopNestedScroll() {
Log.d(TAG, "stopNestedScroll");
childHelper.stopNestedScroll();
}
@Override
public boolean hasNestedScrollingParent() {
Log.d(TAG, "hasNestedScrollingParent");
return childHelper.hasNestedScrollingParent();
}
@Override
public boolean dispatchNestedScroll(int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed, int[] offsetInWindow) {
final boolean b = childHelper.dispatchNestedScroll(dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed, offsetInWindow);
Log.d(TAG, String.format("dispatchNestedScroll, dxConsumed: %d, dyConsumed: %d, dxUnconsumed: %d, dyUnconsumed: %d, offsetInWindow: %s", dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed, Arrays.toString(offsetInWindow)));
return b;
}
@Override
public boolean dispatchNestedPreScroll(int dx, int dy, int[] consumed, int[] offsetInWindow) {
final boolean b = childHelper.dispatchNestedPreScroll(dx, dy, consumed, offsetInWindow);
Log.d(TAG, String.format("dispatchNestedPreScroll, dx: %d, dy: %d, consumed: %s, offsetInWindow: %s", dx, dy, Arrays.toString(consumed), Arrays.toString(offsetInWindow)));
return b;
}
@Override
public boolean dispatchNestedFling(float velocityX, float velocityY, boolean consumed) {
Log.d(TAG, String.format("dispatchNestedFling, velocityX: %f, velocityY: %f, consumed: %b", velocityX, velocityY, consumed));
return childHelper.dispatchNestedFling(velocityX, velocityY, consumed);
}
@Override
public boolean dispatchNestedPreFling(float velocityX, float velocityY) {
Log.d(TAG, String.format("dispatchNestedPreFling, velocityX = %f, velocityY = %f", velocityX, velocityY));
return childHelper.dispatchNestedPreFling(velocityX, velocityY);
}
}
NestedParentView:
public class NestedParentView extends FrameLayout implements NestedScrollingParent {
private static final String TAG = NestedParentView.class.getSimpleName();
private NestedScrollingParentHelper parentHelper = new NestedScrollingParentHelper(this);
public NestedParentView(Context context) {
super(context);
}
public NestedParentView(Context context, AttributeSet attrs) {
super(context, attrs);
}
@Override
public boolean onStartNestedScroll(View child, View target, int nestedScrollAxes) {
Log.d(TAG, String.format("onStartNestedScroll, child: %s, target: %s, nestedScrollAxes: %d", child, target, nestedScrollAxes));
return true;
}
@Override
public void onNestedScrollAccepted(View child, View target, int nestedScrollAxes) {
Log.d(TAG, String.format("onNestedScrollAccepted, child: %s, target: %s, nestedScrollAxes: %d", child, target, nestedScrollAxes));
parentHelper.onNestedScrollAccepted(child, target, nestedScrollAxes);
}
@Override
public void onStopNestedScroll(View target) {
Log.d(TAG, "onStopNestedScroll");
parentHelper.onStopNestedScroll(target);
}
@Override
public void onNestedScroll(View target, int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed) {
Log.d(TAG, String.format("onNestedScroll, dxConsumed: %d, dyConsumed: %d, dxUnconsumed: %d, dyUnconsumed: %d", dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed));
}
@Override
public void onNestedPreScroll(View target, int dx, int dy, int[] consumed) {
//应该移动的Y距离
final float shouldMoveY = getY() + dy;
//父View实际消费的Y距离
int consumedY;
if (shouldMoveY < 0) {
consumedY = -(int) getY();
} else if (shouldMoveY > ((View) getParent()).getHeight() - getHeight()) {
consumedY = (int) (((View) getParent()).getHeight() - getHeight() - getY());
} else {
consumedY = dy;
}
//父View进行滑动
setY(getY() + consumedY);
//将父View消费掉的放入consumed数组中
consumed[1] = consumedY;
Log.d(TAG, String.format("onNestedPreScroll, dx: %d, dy: %d, consumed: %s", dx, dy, Arrays.toString(consumed)));
}
@Override
public boolean onNestedFling(View target, float velocityX, float velocityY, boolean consumed) {
Log.d(TAG, String.format("onNestedFling, velocityX: %f, velocityY: %f, consumed: %b", velocityX, velocityY, consumed));
return true;
}
@Override
public boolean onNestedPreFling(View target, float velocityX, float velocityY) {
Log.d(TAG, String.format("onNestedPreFling, velocityX: %f, velocityY: %f", velocityX, velocityY));
return true;
}
@Override
public int getNestedScrollAxes() {
Log.d(TAG, "getNestedScrollAxes");
return parentHelper.getNestedScrollAxes();
}
}
二、NestedScrollingParent2 & NestedScrollingChild2
1.基础
NestedScrollingParent2 & NestedScrollingChild2
2.Demo
需要处理的场景:
- 上方:WebView的scroll及fling事件处理
- 下方:RecyclerView的scroll及fling事件处理
- 中间:不可滚动的TextView的touch事件处理
- 外层:父View的fling事件处理
注意:
- scroll指的是内容滚动,fling指的是内容惯性滚动
- 控件高度与控件内容高度需要区分开来
主界面布局文件:
activity_main.xml
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent">
<com.example.androidtest_20200329.NestedScrollingDetailContainer
android:id="@+id/nested_container"
android:layout_width="0dp"
android:layout_height="0dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent">
<com.example.androidtest_20200329.NestedScrollingWebView
android:id="@+id/nested_webview"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:tag="nested_scrolling_webview" />
<TextView
android:layout_width="match_parent"
android:layout_height="400dp"
android:background="@color/colorAccent"
android:gravity="center"
android:text="不可滑动的View"
android:textSize="30dp" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/nested_recyclerview"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:tag="nested_scrolling_recyclerview" />
</com.example.androidtest_20200329.NestedScrollingDetailContainer>
</androidx.constraintlayout.widget.ConstraintLayout>
RecyclerView适配器:
RecyclerViewAdapter
public class RecyclerViewAdapter extends RecyclerView.Adapter {
private List<RecyclerViewBean> mData;
private LayoutInflater mInflater;
public RecyclerViewAdapter(Context context, List<RecyclerViewBean> data) {
this.mData = data;
this.mInflater = LayoutInflater.from(context);
}
@NonNull
@Override
public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup viewGroup, int viewType) {
switch (viewType) {
case RecyclerViewBean.TYPE_ITEM:
return new ContentHolder(mInflater.inflate(R.layout.recyclerview_content_layout, viewGroup, false));
case RecyclerViewBean.TYPE_TITLE:
return new TitleHolder(mInflater.inflate(R.layout.recyclerview_title_layout, viewGroup, false));
default:
return null;
}
}
@Override
public void onBindViewHolder(@NonNull RecyclerView.ViewHolder viewHolder, int i) {
RecyclerViewBean infoBean = mData.get(i);
int viewType = getItemViewType(i);
switch (viewType) {
case RecyclerViewBean.TYPE_ITEM:
ContentHolder contentHolder = (ContentHolder) viewHolder;
contentHolder.tvTitle.setText(infoBean.title);
contentHolder.tvContent.setText(infoBean.content);
break;
case RecyclerViewBean.TYPE_TITLE:
TitleHolder titleHolder = (TitleHolder) viewHolder;
titleHolder.tvTitle.setText(infoBean.title);
break;
default:
break;
}
}
@Override
public int getItemCount() {
return mData == null ? 0 : mData.size();
}
@Override
public int getItemViewType(int position) {
return mData.get(position).type;
}
public static class TitleHolder extends RecyclerView.ViewHolder {
TextView tvTitle;
TitleHolder(@NonNull View itemView) {
super(itemView);
tvTitle = itemView.findViewById(R.id.tv_title);
}
}
public static class ContentHolder extends RecyclerView.ViewHolder {
TextView tvTitle;
TextView tvContent;
ContentHolder(@NonNull View itemView) {
super(itemView);
tvTitle = itemView.findViewById(R.id.tv_title);
tvContent = itemView.findViewById(R.id.tv_content);
}
}
}
RecyclerView数据对象:
RecyclerViewBean
public class RecyclerViewBean {
public static final int TYPE_TITLE = 1;
public static final int TYPE_ITEM = 2;
public int type;
public String title;
public String content;
}
RecyclerView布局文件:
recyclerview_title_layout.xml
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:background="#63BB0B"
android:layout_height="40dp">
<TextView
android:id="@+id/tv_title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginLeft="15dp"
android:textSize="20dp"
android:textStyle="bold"
android:textColor="#000000"
android:text="评论"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
recyclerview_content_layout.xml
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<TextView
android:id="@+id/tv_title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginLeft="15dp"
android:textColor="#000000"
android:textSize="16dp"
android:text="评论"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/tv_content"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginLeft="15dp"
android:layout_marginTop="5dp"
android:layout_marginBottom="5dp"
android:textColor="#000000"
android:textSize="16dp"
android:text="内容"
app:layout_constraintTop_toBottomOf="@+id/tv_title"
app:layout_constraintLeft_toLeftOf="parent" />
<View
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toBottomOf="@+id/tv_content"
android:layout_marginTop="5dp"
android:layout_width="0dp"
android:layout_height="5dp"
android:background="#686868"/>
</androidx.constraintlayout.widget.ConstraintLayout>
主界面:
MainActivity
public class MainActivity extends AppCompatActivity {
private NestedScrollingWebView mWebView;
private RecyclerView mRecyclerView;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
initWebView();
initRecyclerView();
}
private void initWebView() {
mWebView = findViewById(R.id.nested_webview);
mWebView.setWebViewClient(new WebViewClient());
mWebView.setWebChromeClient(new WebChromeClient());
mWebView.loadUrl("https://github.com/wangzhengyi/Android-NestedDetail");
}
private void initRecyclerView() {
mRecyclerView = findViewById(R.id.nested_recyclerview);
mRecyclerView.setLayoutManager(new LinearLayoutManager(this));
List<RecyclerViewBean> data = getCommentData();
RecyclerViewAdapter rvAdapter = new RecyclerViewAdapter(this, data);
mRecyclerView.setAdapter(rvAdapter);
}
private List<RecyclerViewBean> getCommentData() {
List<RecyclerViewBean> commentList = new ArrayList<>();
RecyclerViewBean titleBean = new RecyclerViewBean();
titleBean.type = RecyclerViewBean.TYPE_TITLE;
titleBean.title = "评论列表";
commentList.add(titleBean);
for (int i = 0; i < 40; i++) {
RecyclerViewBean contentBean = new RecyclerViewBean();
contentBean.type = RecyclerViewBean.TYPE_ITEM;
contentBean.title = "评论标题" + i;
contentBean.content = "评论内容" + i;
commentList.add(contentBean);
}
return commentList;
}
}
支持嵌套滑动的WebView控件:
NestedScrollingWebView
public class NestedScrollingWebView extends WebView implements NestedScrollingChild2 {
private static final String TAG = NestedScrollingWebView.class.getSimpleName();
private final float DENSITY;
private final int TOUCH_SLOP;
private int mFirstY;
private int mLastY;
private int mMaxVelocity;
private boolean mIsParentFling;
private final int[] mScrollConsumed = new int[2];
private NestedScrollingChildHelper mChildHelper;
private NestedScrollingDetailContainer mParentView;
private Scroller mScroller;
private VelocityTracker mVelocityTracker;
public NestedScrollingWebView(Context context) {
this(context, null);
}
public NestedScrollingWebView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public NestedScrollingWebView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
mChildHelper = new NestedScrollingChildHelper(this);
setNestedScrollingEnabled(true);
mScroller = new Scroller(getContext());
ViewConfiguration configuration = ViewConfiguration.get(getContext());
mMaxVelocity = configuration.getScaledMaximumFlingVelocity();
TOUCH_SLOP = configuration.getScaledTouchSlop();
DENSITY = context.getResources().getDisplayMetrics().density;
}
public int getWebViewContentHeight() {
int webViewContentHeight = (int) (getContentHeight() * DENSITY);
Log.d(TAG, String.format("WebView content height: %d", webViewContentHeight));
return webViewContentHeight;
}
public int getWebViewMaxScrollHeight() {
int webViewMaxScrollHeight = getWebViewContentHeight() - getHeight();
Log.d(TAG, String.format("WebView max scroll height: %d", webViewMaxScrollHeight));
return webViewMaxScrollHeight;
}
public boolean canScrollDown() {
int maxScrollHeight = getWebViewMaxScrollHeight();
if (maxScrollHeight <= 0) { //WebView内容最大滚动高度为小于等于0
Log.d(TAG, String.format("WebView can not scroll down, max scroll height <= 0"));
return false;
}
boolean canScrollDown = getScrollY() + TOUCH_SLOP < maxScrollHeight; //WebView内容当前滚动的距离加上TOUCH_SLOP的值小于WebView内容最大滚动高度,则WebView内容可以滚动
Log.d(TAG, String.format("WebView can scroll down: %b", canScrollDown));
return canScrollDown;
}
@Override
public boolean onTouchEvent(MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
Log.d(TAG, String.format("onTouchEvent, ACTION_DOWN"));
mLastY = (int) event.getRawY();
mFirstY = mLastY;
if (!mScroller.isFinished()) {
mScroller.abortAnimation();
}
initOrResetVelocityTracker();
mIsParentFling = false;
startNestedScroll(ViewCompat.SCROLL_AXIS_VERTICAL); //开始嵌套滚动
if (getParent() != null) {
getParent().requestDisallowInterceptTouchEvent(true);
}
break;
case MotionEvent.ACTION_MOVE:
Log.d(TAG, String.format("onTouchEvent, ACTION_MOVE"));
if (mVelocityTracker != null) {
mVelocityTracker.addMovement(event);
}
int y = (int) event.getRawY();
int dy = y - mLastY;
mLastY = y;
if (getParent() != null) {
getParent().requestDisallowInterceptTouchEvent(true);
}
if (!dispatchNestedPreScroll(0, -dy, mScrollConsumed, null)) { //将滚动事件先交给父View处理
Log.d(TAG, String.format("after dispatchNestedPreScroll, parent view not consume, WebView scroll internal"));
scrollBy(0, -dy);
} else {
Log.d(TAG, String.format("after dispatchNestedPreScroll, parent view consumed, mScrollConsumed: %s", Arrays.toString(mScrollConsumed)));
}
if (Math.abs(mFirstY - y) > TOUCH_SLOP) {
event.setAction(MotionEvent.ACTION_CANCEL); //屏蔽WebView本身的滑动,滑动事件自己处理,不交给父类(WebView)处理
Log.d(TAG, String.format("onTouchEvent, set ACTION_CANCEL"));
}
break;
case MotionEvent.ACTION_UP:
case MotionEvent.ACTION_CANCEL:
Log.d(TAG, String.format("onTouchEvent, ACTION_UP or ACTION_CANCEL"));
if (isParentResetScroll() && mVelocityTracker != null) { //注意:对于WebView的ACTION_UP或ACTION_CANCEL事件,需要父View的内容没有进行过滚动,才能进行惯性滚动
mVelocityTracker.computeCurrentVelocity(1000, mMaxVelocity);
int yVelocity = (int) -mVelocityTracker.getYVelocity();
recycleVelocityTracker();
Log.d(TAG, String.format("mIsSelfFling: true, yVelocity: %d,", yVelocity));
flingScroll(0, yVelocity); //开始惯性滚动
}
break;
}
super.onTouchEvent(event);
return true;
}
@Override
protected void onDetachedFromWindow() {
super.onDetachedFromWindow();
recycleVelocityTracker();
stopScroll();
mChildHelper = null;
mScroller = null;
mParentView = null;
}
@Override
public void flingScroll(int vx, int vy) {
Log.d(TAG, String.format("WebView flingScroll"));
mScroller.fling(0, getScrollY(), 0, vy, 0, 0, Integer.MIN_VALUE, Integer.MAX_VALUE);
invalidate();
}
@Override
public void computeScroll() {
if (mScroller.computeScrollOffset()) {
final int currY = mScroller.getCurrY();
Log.d(TAG, String.format("computeScroll, currY: %d", currY));
if (isWebViewCanScroll()) {
scrollTo(0, currY);
invalidate();
}
if (mScroller.getStartY() < currY && !canScrollDown() && !mIsParentFling) { //WebView内容向上滚动,且已经滚动底部,则将惯性滚动传给父View
Log.d(TAG, "computeScroll, dispatch fling to parent view");
startNestedScroll(ViewCompat.SCROLL_AXIS_VERTICAL);
dispatchNestedPreFling(0, mScroller.getCurrVelocity());
mIsParentFling = true;
}
}
}
@Override
public void scrollTo(int x, int y) {
Log.d(TAG, String.format("scrollTo, y: %d", y));
if (y < 0) {
y = 0;
}
int maxScrollHeight = getWebViewMaxScrollHeight();
if (maxScrollHeight > 0 && y > maxScrollHeight) {
y = maxScrollHeight;
}
if (isParentResetScroll()) { //注意:需要父View的内容没有进行过滚动,WebView的内容才可以滚动
Log.d(TAG, String.format("actually scrollTo, y: %d", y));
super.scrollTo(x, y);
}
}
public void scrollToBottom() {
Log.d(TAG, String.format("WebView scroll to bottom"));
super.scrollTo(0, getWebViewMaxScrollHeight());
}
private NestedScrollingChildHelper getNestedScrollingHelper() {
if (mChildHelper == null) {
mChildHelper = new NestedScrollingChildHelper(this);
}
return mChildHelper;
}
private void initOrResetVelocityTracker() {
if (mVelocityTracker == null) {
mVelocityTracker = VelocityTracker.obtain();
} else {
mVelocityTracker.clear();
}
}
private void recycleVelocityTracker() {
if (mVelocityTracker != null) {
mVelocityTracker.recycle();
mVelocityTracker = null;
}
}
private void initWebViewParent() {
if (mParentView != null) {
return;
}
View parent = (View) getParent();
while (parent != null) {
if (parent instanceof NestedScrollingDetailContainer) {
mParentView = (NestedScrollingDetailContainer) parent;
break;
} else {
parent = (View) parent.getParent();
}
}
}
private boolean isParentResetScroll() {
boolean isParentResetScroll = true;
if (mParentView == null) {
initWebViewParent();
}
if (mParentView != null) {
isParentResetScroll = mParentView.getScrollY() == 0;
}
Log.d(TAG, String.format("isParentResetScroll: %b", isParentResetScroll));
return isParentResetScroll;
}
private void stopScroll() {
if (mScroller != null && !mScroller.isFinished()) {
mScroller.abortAnimation();
}
}
private boolean isWebViewCanScroll() {
boolean isWebViewCanScroll = getWebViewContentHeight() > getHeight();
Log.d(TAG, String.format("isWebViewCanScroll: %b", isWebViewCanScroll));
return isWebViewCanScroll;
}
/****** NestedScrollingChild BEGIN ******/
@Override
public void setNestedScrollingEnabled(boolean enabled) {
getNestedScrollingHelper().setNestedScrollingEnabled(enabled);
}
@Override
public boolean isNestedScrollingEnabled() {
return getNestedScrollingHelper().isNestedScrollingEnabled();
}
@Override
public boolean startNestedScroll(int axes) {
Log.d(TAG, String.format("startNestedScroll, axes: %d", axes));
return getNestedScrollingHelper().startNestedScroll(axes);
}
@Override
public void stopNestedScroll() {
getNestedScrollingHelper().stopNestedScroll();
}
@Override
public boolean hasNestedScrollingParent() {
return getNestedScrollingHelper().hasNestedScrollingParent();
}
@Override
public boolean dispatchNestedPreScroll(int dx, int dy, @Nullable int[] consumed, @Nullable int[] offsetInWindow) {
Log.d(TAG, String.format("before dispatchNestedPreScroll, dy: %d", dy));
return getNestedScrollingHelper().dispatchNestedPreScroll(dx, dy, consumed, offsetInWindow);
}
@Override
public boolean dispatchNestedScroll(int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed, @Nullable int[] offsetInWindow) {
return getNestedScrollingHelper().dispatchNestedScroll(dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed, offsetInWindow);
}
@Override
public boolean dispatchNestedPreFling(float velocityX, float velocityY) {
Log.d(TAG, String.format("before dispatchNestedPreFling, velocityY: %f", velocityY));
return getNestedScrollingHelper().dispatchNestedPreFling(velocityX, velocityY);
}
@Override
public boolean dispatchNestedFling(float velocityX, float velocityY, boolean consumed) {
return getNestedScrollingHelper().dispatchNestedFling(velocityX, velocityY, consumed);
}
@Override
public boolean startNestedScroll(int axes, int type) {
return getNestedScrollingHelper().startNestedScroll(axes, type);
}
@Override
public void stopNestedScroll(int type) {
getNestedScrollingHelper().stopNestedScroll(type);
}
@Override
public boolean hasNestedScrollingParent(int type) {
return getNestedScrollingHelper().hasNestedScrollingParent(type);
}
@Override
public boolean dispatchNestedPreScroll(int dx, int dy, @Nullable int[] consumed, @Nullable int[] offsetInWindow, int type) {
return getNestedScrollingHelper().dispatchNestedPreScroll(dx, dy, consumed, offsetInWindow, type);
}
@Override
public boolean dispatchNestedScroll(int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed, @Nullable int[] offsetInWindow, int type) {
return getNestedScrollingHelper().dispatchNestedScroll(dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed, offsetInWindow, type);
}
}
支持嵌套滑动的父布局控件:
NestedScrollingDetailContainer
public class NestedScrollingDetailContainer extends ViewGroup implements NestedScrollingParent2 {
private static final String TAG = NestedScrollingDetailContainer.class.getSimpleName();
private static final String TAG_NESTED_SCROLLING_WEBVIEW = "nested_scrolling_webview";
private static final String TAG_NESTED_SCROLLING_RECYCLERVIEW = "nested_scrolling_recyclerview";
private static final int FLING_FROM_WEBVIEW_TO_PARENT = 0;
private static final int FLING_FROM_RECYCLERVIEW_TO_PARENT = 1;
private static final int FLING_FROM_PARENT_TO_RECYCLERVIEW = 2;
private static final int FLING_FROM_PARENT_TO_WEBVIEW = 3;
private final int TOUCH_SLOP;
private boolean mIsSetFling;
private boolean mIsBeingDragged;
private int mMaxVelocity;
private int mCurFlingType;
private int mInnerScrollHeight; //父View内容的最大滑动距离
private int mScreenWidth;
private int mLastY;
private int mLastMotionY;
private NestedScrollingWebView mChildWebView;
private RecyclerView mChildRecyclerView;
private NestedScrollingParentHelper mParentHelper;
private Scroller mScroller;
private VelocityTracker mVelocityTracker;
public NestedScrollingDetailContainer(Context context) {
this(context, null);
}
public NestedScrollingDetailContainer(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public NestedScrollingDetailContainer(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
mParentHelper = new NestedScrollingParentHelper(this);
mScroller = new Scroller(getContext());
ViewConfiguration viewConfiguration = ViewConfiguration.get(getContext());
mMaxVelocity = viewConfiguration.getScaledMaximumFlingVelocity();
TOUCH_SLOP = viewConfiguration.getScaledTouchSlop();
mScreenWidth = getResources().getDisplayMetrics().widthPixels;
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int width;
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
int measureWidth = MeasureSpec.getSize(widthMeasureSpec);
int measureHeight = MeasureSpec.getSize(heightMeasureSpec);
if (widthMode == MeasureSpec.EXACTLY) {
width = measureWidth;
} else {
width = mScreenWidth;
}
int left = getPaddingLeft();
int right = getPaddingRight();
int top = getPaddingTop();
int bottom = getPaddingBottom();
int count = getChildCount();
for (int i = 0; i < count; i++) {
View child = getChildAt(i);
LayoutParams params = child.getLayoutParams();
int childWidthMeasureSpec = getChildMeasureSpec(widthMeasureSpec, left + right, params.width);
int childHeightMeasureSpec = getChildMeasureSpec(heightMeasureSpec, top + bottom, params.height);
measureChild(child, childWidthMeasureSpec, childHeightMeasureSpec);
}
setMeasuredDimension(width, measureHeight);
findWebView(this);
findRecyclerView(this);
Log.d(TAG, String.format("onMeasure, width: %d, height: %d", width, measureHeight));
}
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
int childTotalHeight = 0;
mInnerScrollHeight = 0;
for (int i = 0; i < getChildCount(); i++) {
View child = getChildAt(i);
int childWidth = child.getMeasuredWidth();
int childHeight = child.getMeasuredHeight();
child.layout(0, childTotalHeight, childWidth, childHeight + childTotalHeight);
childTotalHeight += childHeight;
mInnerScrollHeight += childHeight;
}
mInnerScrollHeight -= getMeasuredHeight();
Log.d(TAG, String.format("onLayout, mInnerScrollHeight: %d", mInnerScrollHeight));
}
@Override
public boolean dispatchTouchEvent(MotionEvent ev) { //父View的惯性滚动事件处理
int action = ev.getAction();
switch (action) {
case MotionEvent.ACTION_DOWN:
Log.d(TAG, String.format("dispatchTouchEvent, ACTION_DOWN"));
mIsSetFling = false;
initOrResetVelocityTracker();
resetScroller();
dealWithError();
break;
case MotionEvent.ACTION_MOVE:
Log.d(TAG, String.format("dispatchTouchEvent, ACTION_MOVE"));
if (mVelocityTracker != null) {
mVelocityTracker.addMovement(ev);
}
break;
case MotionEvent.ACTION_UP:
case MotionEvent.ACTION_CANCEL:
Log.d(TAG, String.format("dispatchTouchEvent, ACTION_UP or ACTION_CANCEL"));
if (isParentCenter() && mVelocityTracker != null) { ///注意:对于ACTION_UP或ACTION_CANCEL事件,如果父View的内容滚动过,则父View进行惯性滚动
mVelocityTracker.computeCurrentVelocity(1000, mMaxVelocity);
int yVelocity = (int) -mVelocityTracker.getYVelocity();
Log.d(TAG, String.format("parent view is in center and start fling, yVelocity: %d", yVelocity));
if (yVelocity > 0) { //内容向上滚动
mCurFlingType = FLING_FROM_PARENT_TO_RECYCLERVIEW;
Log.d(TAG, String.format("FLING_FROM_PARENT_TO_RECYCLERVIEW"));
} else { //内容向下滚动
mCurFlingType = FLING_FROM_PARENT_TO_WEBVIEW;
Log.d(TAG, String.format("FLING_FROM_PARENT_TO_WEBVIEW"));
}
recycleVelocityTracker();
parentFling(yVelocity); //开始惯性滚动
}
break;
}
return super.dispatchTouchEvent(ev);
}
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
final int action = ev.getAction();
switch (action) {
case MotionEvent.ACTION_DOWN:
Log.d(TAG, String.format("onInterceptTouchEvent, ACTION_DOWN"));
mLastMotionY = (int) ev.getY();
mIsBeingDragged = false;
break;
case MotionEvent.ACTION_MOVE:
Log.d(TAG, String.format("onInterceptTouchEvent, ACTION_MOVE"));
final int y = (int) ev.getY();
final int yDiff = Math.abs(y - mLastMotionY);
boolean isInNestedChildViewArea = isTouchNestedInnerView((int) ev.getRawX(), (int) ev.getRawY()); //拦截落在不可滚动的TextView上的ACTION_MOVE事件
Log.d(TAG, String.format("isInNestedChildViewArea: %b", isInNestedChildViewArea));
if (yDiff > TOUCH_SLOP && !isInNestedChildViewArea) {
Log.d(TAG, String.format("touch event on TextView, handle by parent view"));
mIsBeingDragged = true;
mLastMotionY = y;
final ViewParent parent = getParent();
if (parent != null) {
parent.requestDisallowInterceptTouchEvent(true);
}
}
break;
case MotionEvent.ACTION_UP:
case MotionEvent.ACTION_CANCEL:
mIsBeingDragged = false;
break;
}
return mIsBeingDragged;
}
private boolean isTouchNestedInnerView(int x, int y) {
List<View> innerView = new ArrayList<>();
if (mChildWebView != null) {
innerView.add(mChildWebView);
}
if (mChildRecyclerView != null) {
innerView.add(mChildRecyclerView);
}
for (View nestedView : innerView) {
if (nestedView.getVisibility() != View.VISIBLE) {
continue;
}
int[] location = new int[2];
nestedView.getLocationOnScreen(location);
int left = location[0];
int top = location[1];
int right = left + nestedView.getMeasuredWidth();
int bottom = top + nestedView.getMeasuredHeight();
Log.d(TAG, String.format("left: %d, top: %d, right: %d, bottom: %d, view: %s", left, top, right, bottom, nestedView));
if (y >= top && y <= bottom && x >= left && x <= right) {
return true;
}
}
return false;
}
@Override
public boolean onTouchEvent(MotionEvent event) { //不可滚动的TextView的滑动事件处理,由父View进行拦截处理,进行滚动父View的内容
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
Log.d(TAG, String.format("onTouchEvent, ACTION_DOWN"));
mLastY = (int) event.getY();
break;
case MotionEvent.ACTION_MOVE:
Log.d(TAG, String.format("onTouchEvent, ACTION_MOVE"));
if (mLastY == 0) {
mLastY = (int) event.getY();
return true;
}
int y = (int) event.getY();
int dy = y - mLastY;
mLastY = y;
Log.d(TAG, String.format("parent view scroll internal: %d", -dy));
scrollBy(0, -dy); //父View内容进行滚动
break;
case MotionEvent.ACTION_UP:
case MotionEvent.ACTION_CANCEL:
Log.d(TAG, String.format("onTouchEvent, ACTION_UP or ACTION_CANCEL"));
mLastY = 0;
break;
}
return true;
}
@Override
protected void onDetachedFromWindow() {
super.onDetachedFromWindow();
if (mScroller != null) {
mScroller.abortAnimation();
mScroller = null;
}
mVelocityTracker = null;
mChildRecyclerView = null;
mChildWebView = null;
mParentHelper = null;
}
private void parentFling(float velocityY) {
Log.d(TAG, String.format("parentFling, velocityY: %f", velocityY));
mScroller.fling(0, getScrollY(), 0, (int) velocityY, 0, 0, Integer.MIN_VALUE, Integer.MAX_VALUE);
invalidate();
}
@Override
public void computeScroll() {
if (mScroller.computeScrollOffset()) {
int currY = mScroller.getCurrY();
switch (mCurFlingType) {
case FLING_FROM_WEBVIEW_TO_PARENT: //WebView内容向上滚到底部,父View内容继续向上滚动
case FLING_FROM_PARENT_TO_RECYCLERVIEW: //父View内容向上滚动
Log.d(TAG, String.format("computeScroll, FLING_FROM_WEBVIEW_TO_PARENT"));
scrollTo(0, currY);
invalidate();
checkRecyclerViewTop();
if (getScrollY() >= getInnerScrollHeight() && !mIsSetFling) { //父View内容向上滚到底部,RecyclerView内容继续向上滚动
mIsSetFling = true;
Log.d(TAG, String.format("recyclerViewFling, velocity: %f", mScroller.getCurrVelocity()));
recyclerViewFling((int) mScroller.getCurrVelocity());
}
break;
case FLING_FROM_RECYCLERVIEW_TO_PARENT: //RecyclerView内容向下滑到顶部,继续向下滑动父View内容
//注意:RecyclerView的惯性滚动事件最终会转换成scroll事件进行处理,因此这里不需要调用scrollTo方法
Log.d(TAG, String.format("computeScroll, FLING_FROM_RECYCLERVIEW_TO_PARENT"));
if (getScrollY() <= 0 && !mIsSetFling) { //父View内容向下滚到顶部,WebView内容继续向下滚动
mIsSetFling = true;
Log.d(TAG, String.format("webViewFling, velocity: -%f", mScroller.getCurrVelocity()));
webViewFling((int) -mScroller.getCurrVelocity());
}
break;
case FLING_FROM_PARENT_TO_WEBVIEW: //父View内容向下滚动
Log.d(TAG, String.format("computeScroll, FLING_FROM_PARENT_TO_WEBVIEW"));
scrollTo(0, currY);
invalidate();
if (currY <= 0 && !mIsSetFling) { //父View内容向下滚到顶部,WebView内容继续向下滚动
mIsSetFling = true;
Log.d(TAG, String.format("webViewFling, velocity: -%f", mScroller.getCurrVelocity()));
webViewFling((int) -mScroller.getCurrVelocity());
}
break;
}
}
}
@Override
public void scrollTo(int x, int y) {
Log.d(TAG, String.format("scrollTo, y: %d", y));
if (y < 0) {
y = 0;
}
if (y > getInnerScrollHeight()) {
y = getInnerScrollHeight();
}
Log.d(TAG, String.format("actually scrollTo, y: %d", y));
super.scrollTo(x, y);
}
private void webViewFling(int velocityY) {
Log.d(TAG, String.format("webViewFling, velocityY: %d", velocityY));
if (mChildWebView != null) {
mChildWebView.flingScroll(0, velocityY);
}
}
private void recyclerViewFling(int velocityY) {
Log.d(TAG, String.format("recyclerViewFling, velocityY: %d", velocityY));
if (mChildRecyclerView != null) {
mChildRecyclerView.fling(0, velocityY);
}
}
private void findWebView(ViewGroup parent) {
if (mChildWebView != null) {
return;
}
int count = parent.getChildCount();
for (int i = 0; i < count; i++) {
View child = parent.getChildAt(i);
if (child instanceof NestedScrollingWebView && TAG_NESTED_SCROLLING_WEBVIEW.equals(child.getTag())) {
mChildWebView = (NestedScrollingWebView) child;
break;
}
if (child instanceof ViewGroup) {
findWebView((ViewGroup) child);
}
}
}
private void findRecyclerView(ViewGroup parent) {
if (mChildRecyclerView != null) {
return;
}
int count = parent.getChildCount();
for (int i = 0; i < count; i++) {
View child = parent.getChildAt(i);
if (child instanceof RecyclerView && TAG_NESTED_SCROLLING_RECYCLERVIEW.equals(child.getTag())) {
mChildRecyclerView = (RecyclerView) child;
break;
}
if (child instanceof ViewGroup) {
findRecyclerView((ViewGroup) child);
}
}
}
private void checkRecyclerViewTop() {
if (isParentCenter() && !isRecyclerViewTop()) {
recyclerViewScrollToPosition(0);
}
}
private boolean isParentCenter() {
return getScrollY() > 0 && getScrollY() < getInnerScrollHeight();
}
private boolean isRecyclerViewTop() {
return mChildRecyclerView != null && !mChildRecyclerView.canScrollVertically(-1);
}
private void recyclerViewScrollToPosition(int position) {
if (mChildRecyclerView == null) {
return;
}
mChildRecyclerView.scrollToPosition(position);
RecyclerView.LayoutManager manager = mChildRecyclerView.getLayoutManager();
if (manager instanceof LinearLayoutManager) {
((LinearLayoutManager) manager).scrollToPositionWithOffset(position, 0);
}
}
private boolean canWebViewScrollDown() {
return mChildWebView != null && mChildWebView.canScrollDown();
}
private void scrollToWebViewBottom() {
if (mChildWebView != null) {
mChildWebView.scrollToBottom();
}
}
private int getInnerScrollHeight() {
Log.d(TAG, String.format("getInnerScrollHeight: %d", mInnerScrollHeight));
return mInnerScrollHeight;
}
private void initOrResetVelocityTracker() {
if (mVelocityTracker == null) {
mVelocityTracker = VelocityTracker.obtain();
} else {
mVelocityTracker.clear();
}
}
private void recycleVelocityTracker() {
if (mVelocityTracker != null) {
mVelocityTracker.recycle();
mVelocityTracker = null;
}
}
private void resetScroller() {
if (!mScroller.isFinished()) {
mScroller.abortAnimation();
}
if (mChildRecyclerView != null) {
mChildRecyclerView.stopScroll();
}
}
/**
* 处理未知的错误情况
*/
private void dealWithError() {
//当父View内容有偏移,但是WebView内容却不在底部时,属于异常情况,需要进行修复
//有两种修复方案:1.将WebView手动滑动到底部;2.将父控件的scroll位置重置为0
//目前的测试中没有出现这种异常,此代码作为异常防御
if (isParentCenter() && canWebViewScrollDown()) {
if (getScrollY() > getMeasuredHeight() / 4) {
scrollToWebViewBottom();
} else {
scrollTo(0, 0);
}
}
}
private NestedScrollingParentHelper getNestedScrollingHelper() {
if (mParentHelper == null) {
mParentHelper = new NestedScrollingParentHelper(this);
}
return mParentHelper;
}
/****** NestedScrollingParent2 BEGIN ******/
@Override
public boolean onStartNestedScroll(@NonNull View child, @NonNull View target, int axes, int type) {
return (axes & ViewCompat.SCROLL_AXIS_VERTICAL) != 0;
}
@Override
public int getNestedScrollAxes() {
return getNestedScrollingHelper().getNestedScrollAxes();
}
@Override
public void onNestedScrollAccepted(@NonNull View child, @NonNull View target, int axes, int type) {
getNestedScrollingHelper().onNestedScrollAccepted(child, target, axes, type);
}
@Override
public void onStopNestedScroll(@NonNull View target, int type) {
getNestedScrollingHelper().onStopNestedScroll(target);
}
@Override
public boolean onNestedPreFling(View target, float velocityX, float velocityY) {
if (target instanceof NestedScrollingWebView) { //WebView内容向上滑到底部,继续向上滑动父View内容
mCurFlingType = FLING_FROM_WEBVIEW_TO_PARENT;
Log.d(TAG, String.format("onNestedPreFling, WebView already fling to bottom, FLING_FROM_WEBVIEW_TO_PARENT"));
parentFling(velocityY);
} else if (target instanceof RecyclerView && velocityY < 0 && getScrollY() >= getInnerScrollHeight()) { //RecyclerView内容向下滑到顶部,继续向下滑动父View内容
mCurFlingType = FLING_FROM_RECYCLERVIEW_TO_PARENT;
Log.d(TAG, String.format("onNestedPreFling, RecyclerView already fling to top, FLING_FROM_RECYCLERVIEW_TO_PARENT"));
parentFling((int) velocityY);
}
return false;
}
@Override
public boolean onNestedFling(View target, float velocityX, float velocityY, boolean consumed) {
Log.d(TAG, String.format("onNestedFling, parent view return false, target: %s", target));
return false;
}
@Override
public void onNestedPreScroll(@NonNull View target, int dx, int dy, @Nullable int[] consumed, int type) {
boolean isWebViewBottom = !canWebViewScrollDown();
boolean isCenter = isParentCenter();
Log.d(TAG, String.format("onNestedPreScroll, isWebViewBottom: %b, isParentCenter: %b", isWebViewBottom, isCenter));
if (dy > 0 && isWebViewBottom && getScrollY() < getInnerScrollHeight()) { //内容向上滚动,WebView内容已经滚到底部,继续向上滚动父View的内容
scrollBy(0, dy);
if (consumed != null) {
consumed[1] = dy;
}
Log.d(TAG, String.format("onNestedPreScroll, WebView already scroll to bottom, continue to scroll up parent view"));
} else if (dy < 0 && isCenter) { //内容向下滚动,父View的内容在中间,继续向下滚动父View的内容
scrollBy(0, dy);
if (consumed != null) {
consumed[1] = dy;
}
Log.d(TAG, String.format("onNestedPreScroll, parent view is in center, continue to scroll down parent view"));
}
if (isCenter && !isWebViewBottom) { //父View的内容在中间,且WebView内容没有滚到底部,此为异常情况,特殊处理
scrollToWebViewBottom();
}
}
@Override
public void onNestedScroll(@NonNull View target, int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed, int type) {
Log.d(TAG, String.format("onNestedScroll, dyUnconsumed: %d, target: %s", dyUnconsumed, target));
if (dyUnconsumed < 0) { //RecyclerView内容已经滚到顶部,还有距离未消费,继续向下滚动父View的内容
scrollBy(0, dyUnconsumed);
}
}
}
3.参考链接
NestedScrolling机制
Android-NestedDetail
网友评论