Dispatch touch event from a view to his WebView si

2019-02-17 13:44发布

问题:

Resume of the problem : dispatch touch event on a layout to his WebView sibling achieving the same scroll than the default WebView scroll (with fling)

I have a frameLayout over an WebView following this xml :

<RelativeLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent" android:layout_height="match_parent">


    <com.app.ObservableWebView
        android:layout_width="match_parent" android:layout_height="match_parent"
        android:id="@+id/observableWebView">

    </com.app.ObservableWebView>


    <FrameLayout
        android:layout_width="match_parent" android:layout_height="200dp"
        android:id="@+id/frameLayout">

    </FrameLayout>

</RelativeLayout>

During the begining of the app, an empty html placeholder is placed somewhere in the WebView Dom and his position is given by an JavascriptInterface. This position is converted with pixel ratio and the FrameLayout is placed above the frameLayout. When the WebView content moved, the placeholder moved an so with the FrameLayout (event sended from the Observable WebView). So far, everything is working as it should.

In the frameLayout I need to listen a touch click on it so I've setted a TouchListener following this step :

private boolean mIsNativeClick = false;
private float mStartNativeX;
private float mStartNativeY;
private final float SCROLL_THRESHOLD = 10;

private void init(){
    mRootFrameLayout.setOnTouchListener(this);
}


@Override
public boolean onTouch(View v, MotionEvent event) {
    switch (event.getAction() & MotionEvent.ACTION_MASK) {
        case MotionEvent.ACTION_DOWN:
            mStartNativeX = event.getX();
            mStartNativeY = event.getY();
            mIsNativeClick = true;
            return true;
        case MotionEvent.ACTION_MOVE:
            if (mIsNativeClick && (Math.abs(mStartNativeX - event.getX()) > SCROLL_THRESHOLD
                    || Math.abs(mStartNativeY - event.getY()) > SCROLL_THRESHOLD)) {
                mIsNativeClick = false;
            }
            break;
        case MotionEvent.ACTION_UP:
            if (mIsNativeClick) {
                // my action on touch up goes here
                return true;
            }
    }

    return false;
}

The touch event arrive correctly to the listener and everything was working fine. Except that the WebView is not scrolling when I scroll the FrameLayout because it's a sibling of the frameLayout : it's not a parent/child.

The obvious solution will be to set correclty return true/false for View.onTouchEvent or in the listener so Android dispatch the event to the next view in three Latout Tree. But because I need to handle down/up event in the FrameLayout, starting to return true for MotionEvent.ACTION_DOWN stop dispatching the next event.

Searching on StackOverFlow during the last week, I've achieve a solution working at 50%. Let's describe it :


Step 1 : making the WebView scroll on FrameLayout move event

The solution consist of intercepting event on the FrameLayout and setting the scroll position of the WebView according the the scroll movement/event. The issue with this solution is that the fling event of the scroll is not managed (fling : when user swipe and remove his finger from the screen it produced an inertia effect of the scroll continuing to move for some ms). Adding the fling is explained in step 2.

This is the code snippet : (part for the FrameLayout custom view)

private int movY;
private float mStartNativeX;
private float mStartNativeY;

@Override
public boolean onInterceptTouchEvent ( MotionEvent event ) {
    switch (event.getAction()) {
        case MotionEvent.ACTION_DOWN:
            mStartNativeX = event.getX();
            mStartNativeY = event.getY();
            break;
        case MotionEvent.ACTION_SCROLL:
            Log.d(LOG_TAG, " scroll event");
            break;
    }
    return super.onInterceptTouchEvent(event);
}


@Override
public boolean onTouchEvent(MotionEvent event) {

    switch (event.getAction()) {
        case MotionEvent.ACTION_DOWN:
            Log.d(LOG_TAG, "down event : " + Float.toString(mStartNativeY));
            break;
        case MotionEvent.ACTION_MOVE:
            movY = (int) ((int) mStartNativeY - event.getY());

            mWebView.scrollBy(0, movY);

            Log.d(LOG_TAG, "move event : " + Integer.toString(movY));
            return true;
        case MotionEvent.ACTION_UP:
            Log.d(LOG_TAG, "up event : ");
            break;
        default:
            break;
    }
    return true;
}

Step 2 : the fling

Adding the fling to the manul scrollBy is not so easy. The basic is to add a GestureDetector and a Scroller. So I've a class implementaing Runnable with a Scroller which managing the fling effect. This class is called on the onFling of the GestureDetector Listener.

private float mStartNativeY;
private GestureDetector mGestureDetector;
int movY;

@Override
public boolean onInterceptTouchEvent ( MotionEvent event ) {
    switch (event.getAction()) {
        case MotionEvent.ACTION_DOWN:
            mStartNativeY = event.getY();
            break;
        case MotionEvent.ACTION_SCROLL:
            Log.d(LOG_TAG, " scroll event");
            break;
    }
    return super.onInterceptTouchEvent(event);
}

@Override
public boolean onTouchEvent(MotionEvent event) {

    mGestureDetector.onTouchEvent(event);

    // 1.) remember DOWN event ALWAYS as this is important start for every gesture
    switch (event.getAction()) {
        case MotionEvent.ACTION_DOWN:
            Log.d(LOG_TAG, "down event : " + Float.toString(mStartNativeY));
            break;
        case MotionEvent.ACTION_MOVE:
            movY = (int) ((int) mStartNativeY - event.getY());
            mWebView.scrollBy(0, movY);
            Log.d(LOG_TAG, "move event : " + Integer.toString(movY));
            return true;
        case MotionEvent.ACTION_UP:
            Log.d(LOG_TAG, "up event : " + Integer.toString(mWebView.getScrollY()));
            return false;
        default:
            break;
    }
    return true;
}

// GestureDetector Listener

@Override
public void onLongPress(MotionEvent e) {

}

@Override
public boolean onDown(MotionEvent e) {
    Log.d(LOG_TAG, "onDown");
    return true; // else won't work

}

@Override
public boolean onScroll(MotionEvent e1, MotionEvent e2,
                        float velocitX, float veloctiyY){

    return true;
}

@Override
public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {

    new Flinger().start((int)velocityY);
    invalidate();

    return true;
}

@Override
public void onShowPress(MotionEvent e) {

}

@Override
public boolean onSingleTapUp(MotionEvent e) {
    return false;
}


private class Flinger implements Runnable {
    private final Scroller scroller;

    private int lastY = 0;

    Flinger() {
        scroller = new Scroller(getContext());
    }

    void start(int initialVelocity) {
        int initialY = mWebView.getScrollY();
        int maxY = Integer.MAX_VALUE; // or some appropriate max value in your code
        scroller.fling(0, initialY, 0, initialVelocity, 0, 10, 0, maxY);
        Log.i(LOG_TAG, "starting fling at " + initialY + ", velocity is " + initialVelocity + "");

        lastY = initialY;
        mWebView.post(this);
    }

    public void run() {
        if (scroller.isFinished()) {
            Log.i(LOG_TAG, "scroller is finished, done with fling");
            return;
        }

        boolean more = scroller.computeScrollOffset();
        int y = scroller.getCurrY();
        int diff = lastY - y;

        Log.d(LOG_TAG, "finger run : lasty : " + lastY +" y: " + y + " diff: "+Integer.toString(diff));

        if (diff != 0) {
            mWebView.scrollTo(0, scroller.getCurrY());
            lastY = y;
        }

        if (more) {
            mWebView.post(this);
        }
    }

    boolean isFlinging() {
        return !scroller.isFinished();
    }

    void forceFinished() {
        if (!scroller.isFinished()) {
            scroller.forceFinished(true);
        }
    }
}

Issue : the fling is not working everytime as it should. so if I start the fling with a velocity of 1009, the log says :

starting fling at 3032, velocity is 1009
up event : 3032
finger run : lasty : 3032 y: 3047 diff: -15
finger run : lasty : 3047 y: 3063 diff: -16
finger run : lasty : 3063 y: 3078 diff: -15
finger run : lasty : 3078 y: 3090 diff: -12
finger run : lasty : 3090 y: 3102 diff: -12
finger run : lasty : 3102 y: 3106 diff: -4
finger run : lasty : 3106 y: 3110 diff: -4
finger run : lasty : 3110 y: 3113 diff: -3
finger run : lasty : 3113 y: 3116 diff: -3
finger run : lasty : 3116 y: 3118 diff: -2
finger run : lasty : 3118 y: 3119 diff: -1
finger run : lasty : 3119 y: 3120 diff: -1 

According to the log, the fling theory is working but the starting point of scroll (-15) is not enough, it should be more than ~100


EDIT : Alternative solution : dispatch event to WebView

Another solution explained in comment should be to send the MotionEvent from the FrameLayout to the his WebView sibling with

mFrameLayout.setOnTouchListener(new OnTouchListener() {
    @Override
    public boolean onTouchEvent(MotionEvent ev) {
        return mWebView.onTouchEvent(MotionEvent ev);
    }
});

The problem with this solution is that the scroll distance is not the same as the default WebView onTouchEvent. It's too slow : the distance when it scroll is less than what it should be and some flicker happen.


Any advice or other solution is taken.

回答1:

Every view has its onTouchEvent method, that can be called independently.

So, in order to pass a touch event to the sibiling, we just need to call the sibiling's onTouchEvent with the first view's MotionEvent parameter.

yourView.setOnTouchListener(new OnTouchListener() {
    @Override
    public boolean onTouchEvent(MotionEvent ev) {
        //a sibiling MUST be final to be used in an OnTouchListener
        //if it's not, create a new final reference to it
        return sibiling.onTouchEvent(MotionEvent ev);
    }
});

UPDATE:

According to the conversation in comments, you want to synchronize scrolls of 2 different views of different dimensions. Being of different dimensions, simply passing MotionEvent to the webView is not enough. But you can modify the MotionEvent's X and Y coordinate before sending it to a sibiling, using this method:

ev.setLocation(float x, float y);

You'll need to scale X and Y of Fragment according to the dimensions of your Fragment and your WebView. I don't know what type of scrolling thus scaling you precisely need, so mathematical algorithm may be different from what I wrote below (but that's up to you anyway). If that's the case, consider this a demonstration on how to scale the MotionEvent.

//you first need all dimensions
//if they're changing during runtime, you can do all this in OnTouchListener
final int fragWidth = fragment.getWidth();
final int fragHeight =  fragment.getHeight();
final int webWidith = webView.getWidth();
final int webHeight = webView.getHeight();

//then you calculate scale factors
final float scaleX = (float)webWidth / (float)fragWidth;
final float scaleY = (float)webHeight / (float)fragHeight;

//same old OnTouchListener
fragment.setOnTouchListener(new OnTouchListener() {
    @Override
    public boolean onTouchEvent(MotionEvent ev) {

        //calculate scaled coordinates
        float newX = ev.getX() * scaleX;
        float newY = ev.getY() * scaleY;

        //MODIFY the MotionEvent by setting the scaled coordinates
        ev.setLocation(newX, newY);

        //call WebView's onTouchEvent with the modified MotionEvent
        return webView.onTouchEvent(MotionEvent ev);
    }
});