美文网首页
嵌套滑动机制 -- NestedScrolling

嵌套滑动机制 -- NestedScrolling

作者: TomyZhang | 来源:发表于2020-06-15 22:59 被阅读0次

    一、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

    4.相关链接

    ScrollView嵌套WebView与原生控件

    相关文章

      网友评论

          本文标题:嵌套滑动机制 -- NestedScrolling

          本文链接:https://www.haomeiwen.com/subject/poekxktx.html