[Principle] WebView realizes nested sliding, achieves a ceiling effect like silky smooth, and is perfectly compatible with X5 webview

The previous article [Usage] WebView realizes nested sliding, achieves a ceiling effect like silky, and is perfectly compatible with X5 webview. It
has already explained how to realize nested sliding. In this article, let us look at its realization principle. Without further ado, let's get into the text.

foreword

Before explaining, let's briefly talk about some concepts of nested sliding. (Buddies who are familiar with this can skip this directly)

When it comes to nested sliding, everyone should be familiar with it. He is the NestedScroll mechanism launched by Google after 5.0.

Maybe beginners will have such questions? Compared with the traditional event distribution mechanism, what are the advantages of the NetstedScroll mechanism.

In the traditional event distribution mechanism, once a View or ViewGroup consumes an event, it is difficult to pass the event to the parent View for joint processing. The NestedScrolling mechanism helps us solve this problem very well. We only need to implement the corresponding interface according to the specification. The child View implements NestedScrollingChild, the parent View implements NestedScrollingParent, and completes the interaction through NestedScrollingChildHelper or NestedScrollingParentHelper.

If you don't know about the NestedScrolling mechanism, you can read this article I wrote a few years ago.
In-depth analysis of NestedScrolling mechanism

He can achieve many cool effects in combination with CoordinatorLayout, such as the ceiling effect.

If you are interested, you can read these articles.

Use CoordinatorLayout to create various cool effects

Custom Behavior —— Like Zhihu, FloatActionButton hides and displays

In-depth analysis of NestedScrolling mechanism

Take you step by step to understand the source code of CoordinatorLayout

Custom Behavior - the realization of imitation Sina Weibo discovery page

ViewPager, ScrollView nested ViewPager sliding conflict resolution

Custom behavior - perfect imitation QQ browser homepage, meituan business details page

Principle realization

Not much nonsense, today, let's take a look at how WebView realizes nested sliding.

Please add a picture description

Principle brief

We know that there are currently several interfaces for nested sliding, NestedScrollingChild and NestedScrollingParent.

For an ACTION_MOVE action

  • Before the scrolling child slides, it will use NestedScrollingChildHelper to find out whether there is a responsive scrolling parent. If so, it will first ask the scrolling parent whether it needs to slide before the scrolling child. If necessary, the scrolling parent will slide accordingly and consume a certain amount distance;
  • Then the scrolling child slides accordingly, and consumes a certain distance value dx.
    After the dy scrolling child slides, it asks the scrolling parent whether it needs to continue to slide, and if necessary, perform corresponding processing.
  • After the sliding is over, the Scrolling child will stop sliding, and notify the corresponding Scrolling Parent to stop sliding through NestedScrollingChildHelper.
  • When the finger is lifted (Action_up), according to the sliding speed, calculate whether the fling is corresponding

And if our WebView wants to realize nested sliding, it can use this mechanism.

accomplish

The first step is to implement the NestedScroolChild3 interface and rewrite the corresponding method

public class NestedWebView extends WebView implements NestedScrollingChild3 {

 

    public NestedWebView(Context context) {
        this(context, null);
    }

    public NestedWebView(Context context, AttributeSet attrs) {
        this(context, attrs, android.R.attr.webViewStyle);
    }

    public NestedWebView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        setOverScrollMode(WebView.OVER_SCROLL_NEVER);
        initScrollView();
        mChildHelper = new NestedScrollingChildHelper(this);
        setNestedScrollingEnabled(true);
    }
    
    // 省略
}

Step two:

  • At the time ACTION_DOWNof , first call the startNestedScroll method to tell NestedScrollParent that I am going to slide
  • Then, ACTION_MOVEat the time, call dispatchNestedPreScrollthe method, let NestedScrollParent have the opportunity to slide in advance, and then call its own dispatchNestedScrollmethod to carry out the activity
   public boolean onTouchEvent(MotionEvent ev) {
        initVelocityTrackerIfNotExists();

        MotionEvent vtev = MotionEvent.obtain(ev);

        final int actionMasked = ev.getActionMasked();

        if (actionMasked == MotionEvent.ACTION_DOWN) {
            mNestedYOffset = 0;
        }
        vtev.offsetLocation(0, mNestedYOffset);

        switch (actionMasked) {
            case MotionEvent.ACTION_DOWN:
                if ((mIsBeingDragged = !mScroller.isFinished())) {
                    final ViewParent parent = getParent();
                    if (parent != null) {
                        parent.requestDisallowInterceptTouchEvent(true);
                    }
                }

                if (!mScroller.isFinished()) {
                    abortAnimatedScroll();
                }

                mLastMotionY = (int) ev.getY();
                mActivePointerId = ev.getPointerId(0);
                startNestedScroll(ViewCompat.SCROLL_AXIS_VERTICAL, ViewCompat.TYPE_TOUCH);
                break;
            case MotionEvent.ACTION_MOVE:
                final int activePointerIndex = ev.findPointerIndex(mActivePointerId);
                if (activePointerIndex == -1) {
                    Log.e(TAG, "Invalid pointerId=" + mActivePointerId + " in onTouchEvent");
                    break;
                }

                final int y = (int) ev.getY(activePointerIndex);
                int deltaY = mLastMotionY - y;
                if (dispatchNestedPreScroll(0, deltaY, mScrollConsumed, mScrollOffset,
                        ViewCompat.TYPE_TOUCH)) {
                    deltaY -= mScrollConsumed[1];
                    mNestedYOffset += mScrollOffset[1];
                }
                if (!mIsBeingDragged && Math.abs(deltaY) > mTouchSlop) {
                    final ViewParent parent = getParent();
                    if (parent != null) {
                        parent.requestDisallowInterceptTouchEvent(true);
                    }
                    mIsBeingDragged = true;
                    if (deltaY > 0) {
                        deltaY -= mTouchSlop;
                    } else {
                        deltaY += mTouchSlop;
                    }
                }
                if (mIsBeingDragged) {
                    mLastMotionY = y - mScrollOffset[1];

                    final int oldY = getScrollY();
                    final int range = getScrollRange();

                    // Calling overScrollByCompat will call onOverScrolled, which
                    // calls onScrollChanged if applicable.
                    if (overScrollByCompat(0, deltaY, 0, oldY, 0, range, 0,
                            0, true) && !hasNestedScrollingParent(ViewCompat.TYPE_TOUCH)) {
                        mVelocityTracker.clear();
                    }

                    final int scrolledDeltaY = getScrollY() - oldY;
                    final int unconsumedY = deltaY - scrolledDeltaY;

                    mScrollConsumed[1] = 0;

                    dispatchNestedScroll(0, scrolledDeltaY, 0, unconsumedY, mScrollOffset,
                            ViewCompat.TYPE_TOUCH, mScrollConsumed);

                    mLastMotionY -= mScrollOffset[1];
                    mNestedYOffset += mScrollOffset[1];
                }
                break;

Step 3: When ACTION_UP, calculate the sliding speed in the vertical direction and distribute it

case MotionEvent.ACTION_UP:
    final VelocityTracker velocityTracker = mVelocityTracker;
    velocityTracker.computeCurrentVelocity(1000, mMaximumVelocity);
    int initialVelocity = (int) velocityTracker.getYVelocity(mActivePointerId);
    if ((Math.abs(initialVelocity) > mMinimumVelocity)) {
        if (!dispatchNestedPreFling(0, -initialVelocity)) {
            dispatchNestedFling(0, -initialVelocity, true);
            fling(-initialVelocity);
        }
    } else if (mScroller.springBack(getScrollX(), getScrollY(), 0, 0, 0,
            getScrollRange())) {
        ViewCompat.postInvalidateOnAnimation(this);
    }
    mActivePointerId = INVALID_POINTER;
    endDrag();
    break;


Also rewrite computeScrollthe method to handle inertial sliding

// 在更新 mScrollX 和 mScrollY 的时候会调用
public void computeScroll() {
    if (mScroller.isFinished()) {
        return;
    }

    mScroller.computeScrollOffset();
    final int y = mScroller.getCurrY();
    int unconsumed = y - mLastScrollerY;
    mLastScrollerY = y;

    // Nested Scrolling Pre Pass
    mScrollConsumed[1] = 0;
    dispatchNestedPreScroll(0, unconsumed, mScrollConsumed, null,
            ViewCompat.TYPE_NON_TOUCH);
    unconsumed -= mScrollConsumed[1];


    if (unconsumed != 0) {
        // Internal Scroll
        final int oldScrollY = getScrollY();
        overScrollByCompat(0, unconsumed, getScrollX(), oldScrollY, 0, getScrollRange(),
                0, 0, false);
        final int scrolledByMe = getScrollY() - oldScrollY;
        unconsumed -= scrolledByMe;

        // Nested Scrolling Post Pass
        mScrollConsumed[1] = 0;
        dispatchNestedScroll(0, 0, 0, unconsumed, mScrollOffset,
                ViewCompat.TYPE_NON_TOUCH, mScrollConsumed);
        unconsumed -= mScrollConsumed[1];
    }

    if (unconsumed != 0) {
        abortAnimatedScroll();
    }

    // 判断是否滑动完成,没有完成的话,继续滑动 mScroller
    if (!mScroller.isFinished()) {
        ViewCompat.postInvalidateOnAnimation(this);
    }
}

Finally, in order to ensure onTouchEventthat can receive touch events, we onInterceptTouchEventintercept in

public boolean onInterceptTouchEvent(MotionEvent ev) {
    final int action = ev.getAction();
    if ((action == MotionEvent.ACTION_MOVE) && (mIsBeingDragged)) { // most common
        return true;
    }

    switch (action & MotionEvent.ACTION_MASK) {
        case MotionEvent.ACTION_MOVE:
             
             
            final int y = (int) ev.getY(pointerIndex);
            final int yDiff = Math.abs(y - mLastMotionY);
            // 判断一下滑动距离并且是竖直方向的滑动
            if (yDiff > mTouchSlop
                    && (getNestedScrollAxes() & ViewCompat.SCROLL_AXIS_VERTICAL) == 0) {
                // 代表药进行拦截
                mIsBeingDragged = true;
                mLastMotionY = y;
                initVelocityTrackerIfNotExists();
                mVelocityTracker.addMovement(ev);
                mNestedYOffset = 0;
                
                // 请求父类不要拦截事件
                final ViewParent parent = getParent();
                if (parent != null) {
                    parent.requestDisallowInterceptTouchEvent(true);
                }
            }
            break;
        case MotionEvent.ACTION_DOWN:
            mLastMotionY = (int) ev.getY();
            mActivePointerId = ev.getPointerId(0);

            initOrResetVelocityTracker();
            mVelocityTracker.addMovement(ev);

            mScroller.computeScrollOffset();
            mIsBeingDragged = !mScroller.isFinished();

            startNestedScroll(ViewCompat.SCROLL_AXIS_VERTICAL);
            break;
            
    return mIsBeingDragged;
}

After processing, our webview implements the NestedScrol mechanism, and nested sliding can be performed.

[External link picture transfer failed, the source site may have an anti-theft link mechanism, it is recommended to save the picture and upload it directly (img-ZLoWwHcf-1663672759560)(https://raw.githubusercontent.com/gdutxiaoxu/blog_image/master/22/04 /webview%20%E5%B5%8C%E5%A5%97%E6%BB%91%E5%8A%A8.gif)]

X5 webView compatible

When I moved the code to x5 webview, I swipe at this time and found that it cannot be linked.

class NestedWebView extends com.tencent.smtt.sdk.WebView implements NestedScrollingChild3

Cause Analysis

What is the reason?

We clicked into the code in X5 webView and found that webView inherits FrameLayout, not the system WebView.

Therefore, we directly extend com.tencent.smtt.sdk.WebView to intercept touch events. In fact, we intercept FrameLayout instead of intercepting WebView inside, which will definitely not achieve nested sliding.

solution

Let's take a look at the View Tree structure of X5 webView first, because the code of X5 webView is confusing, and it is not convenient for us to directly see its View Tree through the code.

So, we can print out the x5 webView viewTree structure through code

webView = view.findViewById<WebView>(R.id.webview)
val childCount = webView.childCount
Log.i(TAG, "onViewCreated: webView  is $webView, childCount is $childCount")

for (i in 0 until childCount) {
    Log.i(TAG, "x5 webView: childView[$i]  is ${webView.getChildAt(i)}")
}

Run the above code and get the following result

It can be seen that X5 WebView should wrap a layer of FrameLayout on the basis of WebView.

Then we have no way to get the TencentWebViewProxy$InnerWebView object inside, in fact, there are. He has getViewa .

After getting this object, do we have a way to intercept it, like onTouchEvent, onInterceptTouchEvent method?

We found such a description in the official documentation X5 webview FAQ

3.10 How to rewrite the screen events of TBS WebView (such as overScrollBy)
requires setWebViewCallbackClient and setWebViewClientExtension Reference code sample http://res.imtt.qq.com/tbs/BrowserActivity.zip

Through code tracking & debugging, we found the interface of WebViewCallBackClient

When the webview in X5 slides, the corresponding method will be called. Then, we can do the same at this time and move the code logic of the above NestedWebView down.

Rewrite onTouchEvent, onInterceptTouchEvent, computeScrollthese key methods.

This achieves nested sliding.

The specific code can be found in nestedwebview

Summarize

  1. With the help of the NestedScrool mechanism, it is actually quite simple to realize nested sliding. Basically, it is enough to change it according to the template code, and learn to infer other cases from one instance.
  2. If you want to achieve some custom effects, then we can achieve it through Behavior. For details, please refer to Custom behavior - perfect imitation QQ browser homepage, meituan business details page

reference blog

NestedWebView working properly with ScrollingViewBehavior

X5 WebView official website

source address

nestedwebview , can help give a star.

If you think it is helpful to you, you can follow me on my WeChat public account Xu Gong , here is an Android advanced growth knowledge system, I hope we can learn and progress together, pay attention to the public account Xu Gong , and build core competitiveness together

Guess you like

Origin blog.csdn.net/JasonXu94/article/details/130459275