Delegate Taskbar touches to nearest view to ensure 48x48dp touch size

In TaskbarView#onTouchEvent(), which is only reached if a Taskbar
icon didn't already consuem the event, check each child to see if
the event occurs within a 48x48dp bounding box, and delegate the
event and subsequent events to it until UP or CANCEL.

Bug: 171917176
Change-Id: I7afafe0835828ab9213ec6abfe4e88ad7b9af3c4
diff --git a/quickstep/res/values/dimens.xml b/quickstep/res/values/dimens.xml
index f082b83..39a7d09 100644
--- a/quickstep/res/values/dimens.xml
+++ b/quickstep/res/values/dimens.xml
@@ -123,6 +123,7 @@
     <!-- Taskbar -->
     <dimen name="taskbar_size">48dp</dimen>
     <dimen name="taskbar_icon_size">32dp</dimen>
+    <dimen name="taskbar_icon_touch_size">48dp</dimen>
     <!-- Note that this applies to both sides of all icons, so visible space is double this. -->
     <dimen name="taskbar_icon_spacing">14dp</dimen>
 </resources>
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarView.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarView.java
index 0d82810..adcfaec 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarView.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarView.java
@@ -17,10 +17,13 @@
 
 import android.content.Context;
 import android.content.res.Resources;
+import android.graphics.RectF;
 import android.graphics.drawable.ColorDrawable;
 import android.util.AttributeSet;
 import android.view.LayoutInflater;
+import android.view.MotionEvent;
 import android.view.View;
+import android.view.ViewConfiguration;
 import android.widget.LinearLayout;
 
 import androidx.annotation.LayoutRes;
@@ -39,6 +42,10 @@
 
     private final ColorDrawable mBackgroundDrawable;
     private final int mItemMarginLeftRight;
+    private final int mIconTouchSize;
+    private final int mTouchSlop;
+    private final RectF mTempDelegateBounds = new RectF();
+    private final RectF mDelegateSlopBounds = new RectF();
 
     // Initialized in init().
     private int mHotseatStartIndex;
@@ -46,6 +53,10 @@
 
     private TaskbarController.TaskbarViewCallbacks mControllerCallbacks;
 
+    // Delegate touches to the closest view if within mIconTouchSize.
+    private boolean mDelegateTargeted;
+    private View mDelegateView;
+
     public TaskbarView(@NonNull Context context) {
         this(context, null);
     }
@@ -66,6 +77,8 @@
         Resources resources = getResources();
         mBackgroundDrawable = (ColorDrawable) getBackground();
         mItemMarginLeftRight = resources.getDimensionPixelSize(R.dimen.taskbar_icon_spacing);
+        mIconTouchSize = resources.getDimensionPixelSize(R.dimen.taskbar_icon_touch_size);
+        mTouchSlop = ViewConfiguration.get(context).getScaledTouchSlop();
     }
 
     protected void setCallbacks(TaskbarController.TaskbarViewCallbacks taskbarViewCallbacks) {
@@ -129,6 +142,74 @@
         }
     }
 
+    @Override
+    public boolean onTouchEvent(MotionEvent event) {
+        boolean handled = delegateTouchIfNecessary(event);
+        return super.onTouchEvent(event) || handled;
+    }
+
+    /**
+     * User touched the Taskbar background. Determine whether the touch is close enough to a view
+     * that we should forward the touches to it.
+     * @return Whether a delegate view was chosen and it handled the touch event.
+     */
+    private boolean delegateTouchIfNecessary(MotionEvent event) {
+        final float x = event.getX();
+        final float y = event.getY();
+        if (mDelegateView == null && event.getAction() == MotionEvent.ACTION_DOWN) {
+            for (int i = 0; i < getChildCount(); i++) {
+                View child = getChildAt(i);
+                if (!child.isShown() || !child.isClickable()) {
+                    continue;
+                }
+                int childCenterX = child.getLeft() + child.getWidth() / 2;
+                int childCenterY = child.getTop() + child.getHeight() / 2;
+                mTempDelegateBounds.set(
+                        childCenterX - mIconTouchSize / 2f,
+                        childCenterY - mIconTouchSize / 2f,
+                        childCenterX + mIconTouchSize / 2f,
+                        childCenterY + mIconTouchSize / 2f);
+                mDelegateTargeted = mTempDelegateBounds.contains(x, y);
+                if (mDelegateTargeted) {
+                    mDelegateView = child;
+                    mDelegateSlopBounds.set(mTempDelegateBounds);
+                    mDelegateSlopBounds.inset(-mTouchSlop, -mTouchSlop);
+                    break;
+                }
+            }
+        }
+
+        boolean sendToDelegate = mDelegateTargeted;
+        boolean inBounds = true;
+        switch (event.getAction()) {
+            case MotionEvent.ACTION_MOVE:
+                inBounds = mDelegateSlopBounds.contains(x, y);
+                break;
+            case MotionEvent.ACTION_UP:
+            case MotionEvent.ACTION_CANCEL:
+                mDelegateTargeted = false;
+                break;
+        }
+
+        boolean handled = false;
+        if (sendToDelegate) {
+            if (inBounds) {
+                // Offset event coordinates to be inside the target view
+                event.setLocation(mDelegateView.getWidth() / 2f, mDelegateView.getHeight() / 2f);
+            } else {
+                // Offset event coordinates to be outside the target view (in case it does
+                // something like tracking pressed state)
+                event.setLocation(-mTouchSlop * 2, -mTouchSlop * 2);
+            }
+            handled = mDelegateView.dispatchTouchEvent(event);
+            // Cleanup if this was the last event to send to the delegate.
+            if (!mDelegateTargeted) {
+                mDelegateView = null;
+            }
+        }
+        return handled;
+    }
+
     private View inflate(@LayoutRes int layoutResId) {
         return LayoutInflater.from(getContext()).inflate(layoutResId, this, false);
     }