Check if Android WebView is consuming touch events
AndroidWebviewAndroid Problem Overview
TL;DR
How can I detect whether Android WebView consumed a touch event? onTouchEvent
always returns true and WebViewClient
's onUnhandledInputEvent
is never triggered.
Detailed description
I have multiple WebViews inside a TwoDScrollView. As its name suggests, the TwoDScrollView can be scrolled both vertically and horizontally. The contents of TwoDScrollView can be zoomed in / out. When the user drags his finger or uses pinch-to-zoom, I want to dispatch the touch event to:
- WebView if its content is scrollable / zoomable (i.e. only the inside of the WebView will scroll / zoom)
- TwoDScrollView if the above condition is false (all contents of the TwoDScrollView will scroll / zoom)
I have partially achieved this by using the canScrollHorizontally
and canScrollVertically
methods. But these methods only work for "native scrolling". However, in some cases, some JavaScript inside the WebView consumes the touch event, for example Google Maps. In this case, the methods return false. Is there any way to find out whether the WebView's contents consumes the touch events, i.e. is scrollable / zoomable? I cannot change the contents of the WebView, therefore my question is different from this one.
I have considered checking touch handlers by executing some JavaScript inside the Webview by the evaluateJavaScript
method, but according to this answer there is no easy way to achieve this and also the page can have some other nested iframes. Any help will be appreciated.
What I've already tried
- I overrode WebView's
onTouchEvent
and readsuper.onTouchEvent()
which always returns true, no matter what. canScrollHorizontally
andcanScrollVertically
only partially solve this problem, as mentioned aboveonScrollChanged
isn't useful eitherWebViewClient.onUnhandledInputEvent
is never triggered- I considered using JavaScript via
evaluateJavaScript
, but it is a very complicated and ugly solution - I tried to trace the MotionEvent by
Debug.startMethodTracing
. I found out it is propagated as follows:android.webkit.WebView.onTouchEvent
com.android.webview.chromium.WebViewChromium.onTouchEvent
com.android.org.chromium.android_webview.AwContents.onTouchEvent
com.android.org.chromium.android_webview.AwContents$AwViewMethodsImpl.onTouchEvent
com.android.org.chromium.content.browser.ContentViewCore.onTouchEventImpl
- According to ContentViewCore's source code the touch event ends up in a native method
nativeOnTouchEvent
and I don't know what further happens with it. Anyway,onTouchEvent
always returns true and even if it was possible to find out somewhere whether the event was consumed or not, it would require using private methods which is also quite ugly.
Note
I don't need to know how to intercept touch events sent to WebView, but whether the WebView is consuming them, i.e. is using them for doing anything, such as scrolling, dragging etc.
Android Solutions
Solution 1 - Android
According to this issue report, not possible. If the web code is under your control, you can implement some JavaScriptInterface to workaround this. If not, I am afraid there is no solution here.
Solution 2 - Android
You can pass all touch
events to GestureDetector
by overriding onTouchEvent
of WebView
, so you can know when Android WebView is consuming touch events anywhere, anytime by listening to GestureDetector
.
Try like this:
public class MyWebView extends WebView {
private Context context;
private GestureDetector gestDetector;
public MyWebView(Context context) {
super(context);
this.context = context;
gestDetector = new GestureDetector(context, gestListener);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
return gd.onTouchEvent(event);
}
GestureDetector.SimpleOnGestureListener gestListener= new GestureDetector.SimpleOnGestureListener() {
public boolean onDown(MotionEvent event) {
return true;
}
public boolean onFling(MotionEvent event1, MotionEvent event2, float velocityX, float velocityY) {
//if (event1.getRawX() > event2.getRawX()) {
// show_toast("swipe left");
//} else {
// show_toast("swipe right");
//}
//you can trace any touch events here
return true;
}
};
void show_toast(final String text) {
Toast t = Toast.makeText(context, text, Toast.LENGTH_SHORT);
t.show();
}
}
I hope you be inspired.
Solution 3 - Android
This code will handle your scrolling events in a webview. This catch the click down and the click up events, and compares the positions of each one. It never minds that the content within the webview is scrollable, just compare the coordinates in the area of webview.
public class MainActivity extends Activity implements View.OnTouchListener, Handler.Callback {
private float x1,x2,y1,y2; //x1, y1 is the start of the event, x2, y2 is the end.
static final int MIN_DISTANCE = 150; //min distance for a scroll event
private static final int CLICK_ON_WEBVIEW = 1;
private static final int CLICK_ON_URL = 2;
private static final int UP_ON_WEBVIEW = 3;
private final Handler handler = new Handler(this);
public WebView webView;
private WebViewClient client;
private WebAppInterface webAppInt = new WebAppInterface(this);
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
webView = (WebView)findViewById(R.id.myWebView);
webView.setOnTouchListener(this);
client = new WebViewClient();
webView.setWebViewClient(client);
webView.loadDataWithBaseURL("file:///android_asset/", "myweb.html", "text/html", "UTF-8", "");
}
//HERE START THE IMPORTANT PART
@Override
public boolean onTouch(View v, MotionEvent event) {
if (v.getId() == R.id.myWebView && event.getAction() == MotionEvent.ACTION_DOWN){
x1 = event.getX();
y1 = event.getY();
handler.sendEmptyMessageDelayed(CLICK_ON_WEBVIEW, 200);
} else if (v.getId() == R.id.myWebView && event.getAction() == MotionEvent.ACTION_UP){
x2 = event.getX();
y2 = event.getY();
handler.sendEmptyMessageDelayed(UP_ON_WEBVIEW, 200);
}
return false;
}
@Override
public boolean handleMessage(Message msg) {
if (msg.what == CLICK_ON_URL){ //if you clic a link in the webview, thats not a scroll
handler.removeMessages(CLICK_ON_WEBVIEW);
handler.removeMessages(UP_ON_WEBVIEW);
return true;
}
if (msg.what == CLICK_ON_WEBVIEW){
//Handle the click in the webview
Toast.makeText(this, "WebView clicked", Toast.LENGTH_SHORT).show();
return true;
}
if (msg.what == UP_ON_WEBVIEW){
float deltaX = x2 - x1; //horizontal move distance
float deltaY = y2 - y1; //vertical move distance
if ((Math.abs(deltaX) > MIN_DISTANCE) && (Math.abs(deltaX) > Math.abs(deltaY)))
{
// Left to Right swipe action
if (x2 > x1)
{
//Handle the left to right swipe
Toast.makeText(this, "Left to Right swipe", Toast.LENGTH_SHORT).show ();
}
// Right to left swipe action
else
{
//Handle the right to left swipe
Toast.makeText(this, "Right to Left swipe", Toast.LENGTH_SHORT).show ();
}
}
else if ((Math.abs(deltaY) > MIN_DISTANCE) && (Math.abs(deltaY) > Math.abs(deltaX)))
{
// Top to Bottom swipe action
if (y2 > y1)
{
//Handle the top to bottom swipe
Toast.makeText(this, "Top to Bottom swipe", Toast.LENGTH_SHORT).show ();
}
// Bottom to top swipe action -- I HIDE MY ACTIONBAR ON SCROLLUP
else
{
getActionBar().hide();
Toast.makeText(this, "Bottom to Top swipe [Hide Bar]", Toast.LENGTH_SHORT).show ();
}
}
return true;
}
return false;
}
}
You can also try to control the speed of the swipe, to detect it as a real swipe or scrolling.
I hope that helps you.
Solution 4 - Android
Try to set the android:isClickable="true"
in the XML and create an onClickListener
in the Java code.
Solution 5 - Android
Actually now Touch Actions are not supported for webview. But some workarounds are available; I am going to show it with a longpress example : I am using Pointoption because i will get the coordinate of element and will use it for longpress.
public void longpress(PointOption po) {
//first you need to switch to native view
driver.switchToNativeView();
TouchAction action = new TouchAction((PerformsTouchActions) driver);
action.longPress(po).waitAction(WaitOptions.waitOptions(Duration.ofSeconds(2)));
action.release();
action.perform();
driver.switchToDefaultWebView();
}
For to get the coordinate of element i designed below methood
public PointOption getElementLocation(WebElement element) {
int elementLocationX;
int elementLocationY;
//get element location in webview
elementLocationX = element.getLocation().getX();
elementLocationY = element.getLocation().getY();
//get the center location of the element
int elementWidthCenter = element.getSize().getWidth() / 2;
int elementHeightCenter = element.getSize().getHeight() / 2;
int elementWidthCenterLocation = elementWidthCenter + elementLocationX;
int elementHeightCenterLocation = elementHeightCenter + elementLocationY;
//calculate the ratio between actual screen dimensions and webview dimensions
float ratioWidth = device.getDeviceScreenWidth() / ((MobileDevice) device)
.getWebViewWidth().intValue();
float ratioHeight = device.getDeviceScreenHeight() / ((MobileDevice) device)
.getWebViewHeight().intValue();
//calculate the actual element location on the screen , if needed you can increase this value,for example i used 115 for one of my mobile devices.
int offset = 0;
float elementCenterActualX = elementWidthCenterLocation * ratioWidth;
float elementCenterActualY = (elementHeightCenterLocation * ratioHeight) + offset;
float[] elementLocation = {elementCenterActualX, elementCenterActualY};
int elementCoordinateX, elementCoordinateY;
elementCoordinateX = (int) Math.round(elementCenterActualX);
elementCoordinateY = (int) Math.round(elementCenterActualY);
PointOption po = PointOption.point(elementCoordinateX, elementCoordinateY);
return po;
}
now you have a longpress(PointOption po) and getElementLocation(Webelement element) methods that gives you po. Now everything is ready and you can use them as below..
longpress(getElementLocation(driver.findElement(By.id("the selector can be any of them(xpath,css,classname,id etc.)")));