Tags

, , , , , ,

Firefox has a tabs button overlapping the menu button, to show a nice little tail. Though the curves are easier to implement in Android, the buttons are still rectangular. What that translates to is, the tabs button becomes the touch target for the overlapping portion, even though the overlap is just a curvy tail. If the z-order of the buttons are changed to have the menu button to be on top of tabs button, we are still left with the same problem that menu steals the target from the tabs button. The image shows the problem and the fix (the white dot being the actual touch on the device):

Since the hit area is a rectangle, after quite a bit of struggle, I came up with few options:

  1. While processing a touch event, get the bitmap of the tabs button and check if the pixel beneath the x-y coordinates is transparent or not. This is costlier.
  2. Sandwich the tabs button between an invisible button that steals the event and the menu button. The invisible button tunnels the event to the menu button. This would work fine, however will have accessibility related issues.

And then came TouchDelegate approach. The idea is same as two, but without explicitly having a View which can steal focus. However, the way Android logic works is:

  1. If the touch is in my bounds, find the target for the event and deliver to it.
  2. If no target is found in any of my children, see if there is a delegate and ask it to use it.
  3. If it’s not within delegates bounds, see if there is any TouchListener available and give it to that.
  4. If all fails, try using it. If that fails, don’t use it.

As we can see, again, Tabs button will be the target always and the touch will never be given to the menu button, if the URL bar has to define a delegate. So, how do we fix this? Well, the Tabs button can delegate a part of it, the portion over the tail, to the menu button.

// Set a touch delegate to top button, so the touch events on the overlap
// are passed to the bottom button.
parent.post(new Runnable() {
    @Override
    public void run() {
        int width = _top_button_width_;
        int height = _top_button_height_;
        int overlapping_portion = _find_somehow_;
        Rect bounds = new Rect(width - overlapping_portion, 0, width, height);
        topButton.setTouchDelegate(new TouchDelegate(bounds, bottomButton));
    }
});

The reason behind posting it to parent is, the layout might not be ready when this is hit, if this is done on onCreate(). Hence it is better to post to the UI thread, which will execute after layout is complete. While we expect everything to work fine, there is a bug in Android code. When an ACTION_DOWN is received any time after the first delegation, the topButton will always give the ACTION_UP to the bottom button. Which means, topButton will receive ACTION_DOWN while bottomButton will receive ACTION_UP, and topButton can never process a click event.

The hacky way to fix this is to override the behavior of TouchDelegate with a custom class:

public class MyTouchDelegate extends TouchDelegate {
    public MyTouchDelegate(Rect bounds, View delegateView) {
        super(bounds, delegateView);
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                // Android bug 36445: Send a fake ACTION_CANCEL to delegated target,
                // if the ACTION_DOWN was not processed by it.
                if (!super.onTouchEvent(event)) {
                    MotionEvent cancelEvent = MotionEvent.obtain(event);
                    cancelEvent.setAction(MotionEvent.ACTION_CANCEL);
                    super.onTouchEvent(cancelEvent);
                    return false;
                 } else {
                    return true;
                 }
            default:
                return super.onTouchEvent(event);
        }
    }
}
Advertisements