[原理] WebView はネストされたスライドを実現し、シルクのように滑らかな天井効果を実現し、X5 webview と完全に互換性があります

前回の記事【使い方】WebViewはネストスライドを実現し、シルキーのような天井効果を実現し、X5のwebviewと完全に互換性があり、
ネストスライドの実現方法については既に説明しましたが、本記事ではその実装原理を見ていきましょう。早速、本文に入りましょう。

序文

説明する前に、ネストされたスライドの概念について簡単に説明しましょう。(これに精通している仲間は、これを直接スキップできます)

ネストされたスライドに関しては、誰もがそれに精通しているはずです。彼は 5.0 以降に Google によって開始された NestedScroll メカニズムです。

たぶん初心者はそのような質問をしますか?従来のイベント配信メカニズムと比較して、NetstedScroll メカニズムの利点は何ですか。

従来のイベント配信メカニズムでは、View または ViewGroup がイベントを消費すると、そのイベントを親 View に渡して共同処理することは困難でした。NestedScrolling メカニズムは、この問題をうまく解決するのに役立ちます。仕様に従って対応するインターフェースを実装するだけでよく、子 View は NestedScrollingChild を実装し、親 View は NestedScrollingParent を実装し、NestedScrollingChildHelper または NestedScrollingParentHelper を介して相互作用を完了します。

NestedScrolling メカニズムについてご存じない場合は、私が数年前に書いたこの記事を読むことができます。
NestedScrolling メカニズムの詳細な分析

CoordinatorLayout と組み合わせて、天井効果など、多くのクールな効果を実現できます。

興味のある方は、これらの記事を読むことができます。

CoordinatorLayout を使用してさまざまなクールな効果を作成する

カスタム動作 —— Zhihu と同様に、FloatActionButton の非表示と表示

NestedScrolling メカニズムの詳細な分析

CoordinatorLayout のソース コードを段階的に理解する

Custom Behavior - 模倣の実現 Sina Weibo ディスカバリー ページ

ViewPager、ScrollView のネストされた ViewPager のスライディング競合の解決

カスタム動作 - 完全な模倣 QQ ブラウザー ホームページ、美団ビジネス詳細ページ

原理実現

今日は、WebView がネストされたスライドを実現する方法を見てみましょう。

写真の説明を追加してください

原則概要

現在、ネストされたスライド、NestedScrollingChild および NestedScrollingParent 用のいくつかのインターフェイスがあることがわかっています。

ACTION_MOVE アクションの場合

  • スクロールする子がスライドする前に、NestedScrollingChildHelper を使用して、レスポンシブなスクロールする親が存在するかどうかを確認します. その場合は、最初にスクロールする親に、スクロールする子の前にスライドする必要があるかどうかを尋ねます. 必要に応じて、スクロールする親はそれに応じてスライドします一定の距離を消費します。
  • その後、スクロールする子はそれに応じてスライドし、一定の距離値 dx を消費します.
    dy スクロールする子がスライドした後、スクロールする親にスライドを続行する必要があるかどうかを問い合わせ、必要に応じて対応する処理を実行します.
  • スライドが終了すると、Scrolling 子はスライドを停止し、対応する Scrolling 親に、NestedScrollingChildHelper を介してスライドを停止するように通知します。
  • 指を離したとき(Action_up)、スライド速度に応じて、フリングが対応しているかどうかを計算します

また、WebView がネストされたスライドを実現したい場合は、このメカニズムを使用できます。

達成

最初のステップは、NestedScrollChild3 インターフェイスを実装し、対応するメソッドを書き直すことです。

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);
    }
    
    // 省略
}

ステップ2:

  • ACTION_DOWN時点で、まず startNestedScroll メソッドを呼び出して、NestedScrollParent にスライドすることを伝えます。
  • 次に、その時点ACTION_MOVEdispatchNestedPreScrollメソッドを呼び出し、NestedScrollParent に事前にスライドする機会を与えてから、独自のdispatchNestedScrollメソッドアクティビティを実行します
   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;

ステップ3:ACTION_UP時、縦方向のスライド速度を計算し、分配する

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;


また、慣性滑りを処理するcomputeScrollメソッド

// 在更新 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);
    }
}

最後に、 が確実にタッチ イベントを受信onTouchEventできるようにするために、onInterceptTouchEvent

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;
}

処理後、webview は NestedScroll メカニズムを実装し、ネストされたスライドを実行できます。

[外部リンクの画像転送に失敗しました。ソース サイトには盗難防止リンク メカニズムがある可能性があります。画像を保存して直接アップロードすることをお勧めします (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 互換

コードを x5 webview に移動したときに、この時点でスワイプすると、リンクできないことがわかりました。

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

原因分析

理由は何ですか?

X5 webView のコードをクリックしたところ、webView はシステムの WebView ではなく、FrameLayout を継承していることがわかりました。

したがって、com.tencent.smtt.sdk.WebView を直接拡張してタッチ イベントをインターセプトしますが、実際には、内部の WebView をインターセプトするのではなく、FrameLayout をインターセプトします。

解決

まず、X5 webView のビュー ツリー構造を見てみましょう。X5 webView のコードはわかりにくく、コードからビュー ツリーを直接見るのは不便だからです。

したがって、コードを使用して x5 webView viewTree 構造を出力できます。

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)}")
}

上記のコードを実行すると、次の結果が得られます

X5 WebView は、WebView に基づいて FrameLayout のレイヤーをラップする必要があることがわかります。

次に、TencentWebViewProxy$InnerWebView オブジェクトを内部に取得する方法はありません。実際にはあります。彼にはgetView方法。

このオブジェクトを取得した後、onTouchEvent、onInterceptTouchEvent メソッドなど、それをインターセプトする方法はありますか?

公式ドキュメントX5 webview FAQでそのような説明を見つけました

3.10 TBS WebView の画面イベント(overScrollBy など)を書き換える方法には
setWebViewCallbackClient と setWebViewClientExtension が必要 参照コードサンプル http://res.imtt.qq.com/tbs/BrowserActivity.zip

コードの追跡とデバッグを通じて、WebViewCallBackClient のインターフェイスを見つけました。

X5 の WebView がスライドすると、対応するメソッドが呼び出されます。次に、この時点で同じことを行い、上記の NestedWebView のコード ロジックを下に移動します。

onTouchEventonInterceptTouchEvent、これらの主要なメソッドを書き換えますcomputeScroll

これにより、ネストされたスライドが実現されます。

特定のコードは、 nestedwebviewにあります。

要約する

  1. NestedScrool メカニズムの助けを借りて、ネストされたスライドを実現するのは実際には非常に簡単です. 基本的には、テンプレート コードに従って変更し、1 つのインスタンスから他のケースを推測することを学ぶだけで十分です.
  2. カスタム効果を実現したい場合は、動作を通じて実現できます. 詳細については、カスタム

参考ブログ

ScrollingViewBehavior で適切に動作する NestedWebView

X5 WebView 公式サイト

送信元アドレス

nestedwebview は、星を付けるのに役立ちます。

役に立つと思われる場合は、WeChat パブリック アカウントXu Gongでフォローしてください。ここに Android の高度な成長知識システムがあります。一緒に学び、進歩し、パブリック アカウントXu Gongに注意を払い、構築できることを願っています。コア競争力を一緒に

おすすめ

転載: blog.csdn.net/JasonXu94/article/details/130459275