View event dispatch and example used in hybrid app

Case requirement:

Hybrid app is mixing native and web stuff toghther. Normal approch is using the cordova plugin to make javascript call native code to implement some native functions like open camera or retrive gps coordinates. But in this case is we need a webview and native view show on the same screen for a hybrid app. Like show an native mapview on screen but the navigation bar or other view on top of it are still using webview.

Solution:

We can make some of the webview area to be transparent, and put the native view under that area with exact same size. The main challenges including for this solution:

  • Put native view under webview with right position and size
  • Recognize the touch event and send the event to webview or native map view based inside/outside the transparent area
  • Put native view under webview with right position and size

    For the first changllene, we can put the native map under a html tag with same position and size.

  • Get html tag positon and size and pass to native side
  •     var pageRect = getPageRect();
        var rect = div.getBoundingClientRect();
        var divRect = [rect.left + pageRect.left, rect.top + pageRect.top, rect.width, rect.height];
        
  • Make all parent tags to be transparent, so that the native view can show up in that area
  •     while (div.parentNode) {
            div.style.backgroundColor = 'transparent';
            div = div.parentNode;
        }
        
  • Also get all children tags which need show above the native map
  •  
        var children = getAllChildren(div);
        for (var i = 0; i < children.length; i++) {
            element = children[i];
            elemId = "elementId" + i;
            elements.push({
                id: elemId,
                rect: getDivRect(element)
            });
            i++;
        }
        //native part, store every tag area
        touchLayout.addOverlayHtml(idStr, new RectF(marginLeft, marginTop, marginLeft + w, marginTop + h));
        

    Web side is setted up, Now, we start preparing the native side:

  • Put the webView inside a layout, we call it touchLayout
  •         touchLayout = new TouchLayout(cordova.getActivity(), mWebView);
            removeAllViews();
            touchLayout.addView(mWebView);
            nativeViewContainer.addView(touchLayout, -1, new FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT));
            //* @param index: the position at which to add the child or -1 to add last, from bottom to top, so if index is 0 it will put to bottom
        
  • Add native map under the touchLayout
  •         addViewToNative(mapView, 0, lp);
        

    After all these steps, the map can be show under the html tag which is transparent. So the next step is to solve the second problem:

    Recognize the touch event and send the event to webview or native map view based inside/outside the transparent area

    For this task we need understand the basics inside View of Android:

       public boolean dispatchTouchEvent(MotionEvent ev)
    //Pass the touch screen motion event down to the target view, or this view if it is the target.
        
    public boolean onInterceptTouchEvent(MotionEvent ev) // only ViewGroup has it, ViewGroup is subclass of View
    //Implement this method to intercept all touch screen motion events. This method is called inside dispatchTouchEvent().
        

    Events will be received in the following order:

  • You will receive the down event here.
  • The down event will be handled either by a child of this view group, or given to your own onTouchEvent() method to handle; this means you should implement onTouchEvent() to return true, so you will continue to see the rest of the gesture (instead of looking for a parent view to handle it). Also, by returning true from onTouchEvent(), you will not receive any following events in onInterceptTouchEvent() and all touch processing must happen in onTouchEvent() like normal.
  • For as long as you return false from this function, each following event (up to and including the final up) will be delivered first here and then to the target's onTouchEvent().
  • If you return true from here, you will not receive any following events: the target view will receive the same event but with the action ACTION_CANCEL, and all further events will be delivered to your onTouchEvent() method and no longer appear here.
  • public boolean onTouchEvent(MotionEvent ev)
    // Implement this method to handle touch screen motion events. Return value: True if the event was handled, false otherwise.
    

    So we can override the function of onInterceptTouchEvent() and verify if the touch event happends inside the mapView. Even inside the mapView, we also need to verify if the click event happens on some html tags which are the children of the map tag.(on top of the map).

     
        public boolean onInterceptTouchEvent(MotionEvent event) {
            int x = (int) event.getX();
            int y = (int) event.getY();
            Rect rectf = new Rect();
            MarginLayoutParams lp = (MarginLayoutParams) mMapView.getLayoutParams();
            mMapView.getLocalVisibleRect(rectf);
            rectf.offset(lp.leftMargin, lp.topMargin);
            boolean notContains = true;
            if (rectf.contains(x, y)) {
                // Is the touch point on any HTML elements?
                Set elements = this.htmlNodes.entrySet();
                Iterator> iterator = elements.iterator();
                Map.Entry entry;
                while (iterator.hasNext()) {
                    entry = iterator.next();
                    if (entry.getValue().contains(x, y)) {
                        notContains = false;
                        break;
                    }
                }
                if (!notContains) {
                    mWebView.requestFocus(View.FOCUS_DOWN);
                }
                return notContains;
            }
            return false; //outside of mapView
        }
        
        public boolean onTouchEvent(MotionEvent ev) {
            return false; // do not consume the event so it can pass to the next layer of ViewGroup
        }
    

    So the logical is clear, if onInterceptTouchEvent return true, the event will be handled by the viewGroup so that the following event will be handled by onTouchEvent(). Here we hard coded onTouchEvent to return false, so it won't consume the event so it can pass to the next layer of ViewGroup

    For the details about view event dispatch can reference here ViewGroup event dispatch