/* * Copyright (C) 2017 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 com.android.managedprovisioning.common; import android.graphics.Rect; import android.util.DisplayMetrics; import android.view.TouchDelegate; import android.view.View; import com.android.internal.annotations.VisibleForTesting; /** * Allows for expanding touch area of a {@link View} element, so it's compliant with * accessibility guidelines, while not modifying the UI appearance. * @see Android Accessibility Guide */ public class TouchTargetEnforcer { /** Value taken from Android Accessibility Guide */ @VisibleForTesting static final int MIN_TARGET_DP = 48; /** @see DisplayMetrics#density */ private final float mDensity; private final TouchDelegateProvider mTouchDelegateProvider; /** * Allows for expanding touch area of a {@link View} element, so it's compliant with * accessibility guidelines, while not modifying the UI appearance. * @param density {@link DisplayMetrics#density} * @see Android Accessibility Guide */ public TouchTargetEnforcer(float density) { this(density, TouchDelegate::new); } /** * Allows for expanding touch area of a {@link View} element, so it's compliant with * accessibility guidelines, while not modifying the UI appearance. * @param density {@link DisplayMetrics#density} * @see Android Accessibility Guide */ TouchTargetEnforcer(float density, TouchDelegateProvider touchDelegateProvider) { mDensity = density; mTouchDelegateProvider = touchDelegateProvider; } /** * Compares target's touch area to required minimum, and expands it if necessary. *

FIXME: Does not honor screen boundaries, so might set touch areas outside of the screen. *

FIXME: Does not honor ancestor boundaries, so might not work if ancestor too small. *

FIXME: Does not work if ancestor has more than one TouchTarget set. * @param target element to check for accessibility compliance * @param ancestor target's ancestor - only one target per ancestor allowed */ public void enforce(View target, View ancestor) { target.getViewTreeObserver().addOnGlobalLayoutListener( // avoids some subtle bugs () -> { int minTargetPx = (int) Math.ceil(dpToPx(MIN_TARGET_DP)); int deltaHeight = Math.max(0, minTargetPx - target.getHeight()); int deltaWidth = Math.max(0, minTargetPx - target.getWidth()); if (deltaHeight <= 0 && deltaWidth <= 0) { return; } ancestor.post(() -> { Rect bounds = createNewBounds(target, minTargetPx, deltaWidth, deltaHeight); synchronized (ancestor) { if (ancestor.getTouchDelegate() == null) { ancestor.setTouchDelegate( mTouchDelegateProvider.getInstance(bounds, target)); ProvisionLogger.logd(String.format( "Successfully set touch delegate on ancestor %s " + "delegating to target %s.", ancestor, target)); } else { ProvisionLogger.logd(String.format( "Ancestor %s already has an assigned touch delegate %s. " + "Unable to assign another one. Ignoring target.", ancestor, target)); } } }); }); } private Rect createNewBounds(View target, int minTargetPx, int deltaWidth, int deltaHeight) { int deltaWidthHalf = deltaWidth / 2; int deltaHeightHalf = deltaHeight / 2; Rect result = new Rect(); target.getHitRect(result); result.top -= deltaHeightHalf; result.bottom += deltaHeightHalf; result.left -= deltaWidthHalf; result.right += deltaWidthHalf; // fix rounding errors int deltaHeightRemaining = minTargetPx - (result.bottom - result.top); if (deltaHeightRemaining > 0) { result.bottom += deltaHeightRemaining; } int deltaWidthRemaining = minTargetPx - (result.right - result.left); if (deltaWidthRemaining > 0) { result.right += deltaWidthRemaining; } return result; } private float dpToPx(int dp) { return dp * mDensity; } interface TouchDelegateProvider { /** * @param bounds New touch bounds * @param delegateView The view that should receive motion events (target) */ TouchDelegate getInstance(Rect bounds, View delegateView); } }