| /* |
| * Copyright (C) 2008 The Android Open Source Project |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| package android.view; |
| |
| import android.annotation.NonNull; |
| import android.annotation.UnsupportedAppUsage; |
| import android.graphics.Rect; |
| import android.graphics.Region; |
| import android.util.ArrayMap; |
| import android.view.accessibility.AccessibilityNodeInfo.TouchDelegateInfo; |
| |
| /** |
| * Helper class to handle situations where you want a view to have a larger touch area than its |
| * actual view bounds. The view whose touch area is changed is called the delegate view. This |
| * class should be used by an ancestor of the delegate. To use a TouchDelegate, first create an |
| * instance that specifies the bounds that should be mapped to the delegate and the delegate |
| * view itself. |
| * <p> |
| * The ancestor should then forward all of its touch events received in its |
| * {@link android.view.View#onTouchEvent(MotionEvent)} to {@link #onTouchEvent(MotionEvent)}. |
| * </p> |
| */ |
| public class TouchDelegate { |
| |
| /** |
| * View that should receive forwarded touch events |
| */ |
| private View mDelegateView; |
| |
| /** |
| * Bounds in local coordinates of the containing view that should be mapped to the delegate |
| * view. This rect is used for initial hit testing. |
| */ |
| private Rect mBounds; |
| |
| /** |
| * mBounds inflated to include some slop. This rect is to track whether the motion events |
| * should be considered to be within the delegate view. |
| */ |
| private Rect mSlopBounds; |
| |
| /** |
| * True if the delegate had been targeted on a down event (intersected mBounds). |
| */ |
| @UnsupportedAppUsage |
| private boolean mDelegateTargeted; |
| |
| /** |
| * The touchable region of the View extends above its actual extent. |
| */ |
| public static final int ABOVE = 1; |
| |
| /** |
| * The touchable region of the View extends below its actual extent. |
| */ |
| public static final int BELOW = 2; |
| |
| /** |
| * The touchable region of the View extends to the left of its actual extent. |
| */ |
| public static final int TO_LEFT = 4; |
| |
| /** |
| * The touchable region of the View extends to the right of its actual extent. |
| */ |
| public static final int TO_RIGHT = 8; |
| |
| private int mSlop; |
| |
| /** |
| * Touch delegate information for accessibility |
| */ |
| private TouchDelegateInfo mTouchDelegateInfo; |
| |
| /** |
| * Constructor |
| * |
| * @param bounds Bounds in local coordinates of the containing view that should be mapped to |
| * the delegate view |
| * @param delegateView The view that should receive motion events |
| */ |
| public TouchDelegate(Rect bounds, View delegateView) { |
| mBounds = bounds; |
| |
| mSlop = ViewConfiguration.get(delegateView.getContext()).getScaledTouchSlop(); |
| mSlopBounds = new Rect(bounds); |
| mSlopBounds.inset(-mSlop, -mSlop); |
| mDelegateView = delegateView; |
| } |
| |
| /** |
| * Forward touch events to the delegate view if the event is within the bounds |
| * specified in the constructor. |
| * |
| * @param event The touch event to forward |
| * @return True if the event was consumed by the delegate, false otherwise. |
| */ |
| public boolean onTouchEvent(@NonNull MotionEvent event) { |
| int x = (int)event.getX(); |
| int y = (int)event.getY(); |
| boolean sendToDelegate = false; |
| boolean hit = true; |
| boolean handled = false; |
| |
| switch (event.getActionMasked()) { |
| case MotionEvent.ACTION_DOWN: |
| mDelegateTargeted = mBounds.contains(x, y); |
| sendToDelegate = mDelegateTargeted; |
| break; |
| case MotionEvent.ACTION_POINTER_DOWN: |
| case MotionEvent.ACTION_POINTER_UP: |
| case MotionEvent.ACTION_UP: |
| case MotionEvent.ACTION_MOVE: |
| sendToDelegate = mDelegateTargeted; |
| if (sendToDelegate) { |
| Rect slopBounds = mSlopBounds; |
| if (!slopBounds.contains(x, y)) { |
| hit = false; |
| } |
| } |
| break; |
| case MotionEvent.ACTION_CANCEL: |
| sendToDelegate = mDelegateTargeted; |
| mDelegateTargeted = false; |
| break; |
| } |
| if (sendToDelegate) { |
| if (hit) { |
| // Offset event coordinates to be inside the target view |
| event.setLocation(mDelegateView.getWidth() / 2, mDelegateView.getHeight() / 2); |
| } else { |
| // Offset event coordinates to be outside the target view (in case it does |
| // something like tracking pressed state) |
| int slop = mSlop; |
| event.setLocation(-(slop * 2), -(slop * 2)); |
| } |
| handled = mDelegateView.dispatchTouchEvent(event); |
| } |
| return handled; |
| } |
| |
| /** |
| * Forward hover events to the delegate view if the event is within the bounds |
| * specified in the constructor and touch exploration is enabled. |
| * |
| * @param event The hover event to forward |
| * @return True if the event was consumed by the delegate, false otherwise. |
| * |
| * @see android.view.accessibility.AccessibilityManager#isTouchExplorationEnabled |
| */ |
| public boolean onTouchExplorationHoverEvent(@NonNull MotionEvent event) { |
| if (mBounds == null) { |
| return false; |
| } |
| |
| final int x = (int) event.getX(); |
| final int y = (int) event.getY(); |
| boolean hit = true; |
| boolean handled = false; |
| |
| final boolean isInbound = mBounds.contains(x, y); |
| switch (event.getActionMasked()) { |
| case MotionEvent.ACTION_HOVER_ENTER: |
| mDelegateTargeted = isInbound; |
| break; |
| case MotionEvent.ACTION_HOVER_MOVE: |
| if (isInbound) { |
| mDelegateTargeted = true; |
| } else { |
| // delegated previously |
| if (mDelegateTargeted && !mSlopBounds.contains(x, y)) { |
| hit = false; |
| } |
| } |
| break; |
| case MotionEvent.ACTION_HOVER_EXIT: |
| mDelegateTargeted = true; |
| break; |
| } |
| if (mDelegateTargeted) { |
| if (hit) { |
| event.setLocation(mDelegateView.getWidth() / 2, mDelegateView.getHeight() / 2); |
| } else { |
| mDelegateTargeted = false; |
| } |
| handled = mDelegateView.dispatchHoverEvent(event); |
| } |
| return handled; |
| } |
| |
| /** |
| * Return a {@link TouchDelegateInfo} mapping from regions (in view coordinates) to |
| * delegated views for accessibility usage. |
| * |
| * @return A TouchDelegateInfo. |
| */ |
| @NonNull |
| public TouchDelegateInfo getTouchDelegateInfo() { |
| if (mTouchDelegateInfo == null) { |
| final ArrayMap<Region, View> targetMap = new ArrayMap<>(1); |
| Rect bounds = mBounds; |
| if (bounds == null) { |
| bounds = new Rect(); |
| } |
| targetMap.put(new Region(bounds), mDelegateView); |
| mTouchDelegateInfo = new TouchDelegateInfo(targetMap); |
| } |
| return mTouchDelegateInfo; |
| } |
| } |