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);
}