Dispatch touch event from a view to his WebView sibling
Asked Answered
F

1

7

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.

layout schema

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.

Forenoon answered 22/12, 2014 at 9:58 Comment(13)
Why do you need to set up a FrameLayout just to handle touch events from a specific part of your WebView? You can assign it to the WebView itself (with some logic for determining the area), and this problem wouldn't exist. Since you are already synchronizing the FrameLayout position with the WebView scroll, this logic would be much simpler in any case.Shellback
@Shellback it's a good idea but the problem is that the frameLayout is a custom FrameLayout used in many ViewGroup (ScrollView, etc). Anyway, if I return true to MotionEvent.ACTION_DOWN and return false in MOVE event, will the WebView scroll ?Forenoon
If you track click events from the WebView itself, then there will be no need to override it's touch handling logic, and it should work as normal. What is the purpose of this custom FrameLayout other than tracking clicks?Shellback
It's displaying a video over the webview, so I need click and drag/scroll. I will try to change the app structure for matching this solution. Btw, it will be great if you find why the flinger is not working as it should because it's also one of the problem with the current methodForenoon
Well, if you need the FrameLayout, then you should probably use some custom touch dispatching/redirecting logic in there or the parent to have the touch dispatched to both views.Shellback
returning TRUE means you have handled the event and FALSE means you have not and is then propagated to the view hierarchy. If you want to handle the event further, then return FALSE.Donatist
@RameshPrasad I need to return true to DOWN event because it's necessary to continue to handle the event on the view and so to have MOVE event.Forenoon
@Shellback This is what I'm doing right know, no ?Forenoon
@HugoGresse: You are currently managing a custom touch event handler that scrolls the WebView in the parent, using a combination of onInterceptTouchEvent() and onTouchEvent() callbacks. What I was suggesting was to utilize the native touch handling logic by ensuring that onTouchEvent() always gets called on both children with all the touch events by overriding the logic in the dispatchTouchEvent() method on the parent.Shellback
@Shellback The problem is that I cannot extend the parent view (a ScrollView or something else) : I'm working on a SDK and try so simplified the integration of it for developers.Forenoon
@HugoGresse: OK, then you can redirect touch events to the WebView back from the FrameLayout. Add a reference to the WebView in the FrameLayout and chain a call to the WebView's onTouchEvent() from the FrameLayout's onTouchEvent() callback.Shellback
@Shellback If I call mWebView.onTouchEvent(event); in the FrameLayout.onTouchEvent, the event is send to the webView so it's scrolling but not as if it was manage directly from the WebView. The distance when it scroll is less than what it should be. I will try to manage touch event directly in the WebView but it's not a good way to do it because touch event on a view should be manage on the view itself and not on another view. Still open if someone find how to make the flinger work properly...Forenoon
Possible duplicate of Dispatch touch event to a sibling viewCommix
P
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);
    }
});
Pence answered 3/1, 2015 at 15:40 Comment(7)
Thank you for your answer but if I call mWebView.onTouchEvent(event); in the FrameLayout.onTouchEvent, the event is send to the webView so it's scrolling but not as if it was manage directly from the WebView. The distance when it scroll is less than what it should be.Forenoon
are your FrameLayout and WebView of different width/height?Pence
If you need WebView to scroll faster, that can easily be done with scaling the X and Y coordinates of your MotionEvent. Although I still don't understand why the new problem is happening, I've posted an example of the algorithm in my answer.Pence
I've also posted in original question your method and saying that a flicker happens when doing this. Also changing the scale on MotionEvent does not seem a good way to do it correctly.Forenoon
Is the webpage displayed scaled or in full size? That might cause the slowing. If not, and regarding the flicker, I can't help much unless I'm using a debugger. Something may be resetting, or going out of scope while automatically handled, etc. You'll need to use more detailed analysis and maybe even a debugger. If you fix one problem, the other reveals itself.Pence
Let us continue this discussion in chat.Forenoon
I'm sorry, but I'm way too busy and stressed for that much. Good luck though.Pence

© 2022 - 2024 — McMap. All rights reserved.