1 /* 2 * Copyright (C) 2017 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 package com.android.managedprovisioning.common; 17 18 import android.graphics.Rect; 19 import android.util.DisplayMetrics; 20 import android.view.TouchDelegate; 21 import android.view.View; 22 23 import com.android.internal.annotations.VisibleForTesting; 24 25 /** 26 * Allows for expanding touch area of a {@link View} element, so it's compliant with 27 * accessibility guidelines, while not modifying the UI appearance. 28 * @see <a href="https://goo.gl/FcU5gX">Android Accessibility Guide</a> 29 */ 30 public class TouchTargetEnforcer { 31 /** Value taken from Android Accessibility Guide */ 32 @VisibleForTesting static final int MIN_TARGET_DP = 48; 33 34 /** @see DisplayMetrics#density */ 35 private final float mDensity; 36 37 private final TouchDelegateProvider mTouchDelegateProvider; 38 39 /** 40 * Allows for expanding touch area of a {@link View} element, so it's compliant with 41 * accessibility guidelines, while not modifying the UI appearance. 42 * @param density {@link DisplayMetrics#density} 43 * @see <a href="https://goo.gl/FcU5gX">Android Accessibility Guide</a> 44 */ TouchTargetEnforcer(float density)45 public TouchTargetEnforcer(float density) { 46 this(density, TouchDelegate::new); 47 } 48 49 /** 50 * Allows for expanding touch area of a {@link View} element, so it's compliant with 51 * accessibility guidelines, while not modifying the UI appearance. 52 * @param density {@link DisplayMetrics#density} 53 * @see <a href="https://goo.gl/FcU5gX">Android Accessibility Guide</a> 54 */ TouchTargetEnforcer(float density, TouchDelegateProvider touchDelegateProvider)55 TouchTargetEnforcer(float density, TouchDelegateProvider touchDelegateProvider) { 56 mDensity = density; 57 mTouchDelegateProvider = touchDelegateProvider; 58 } 59 60 /** 61 * Compares target's touch area to required minimum, and expands it if necessary. 62 * <p>FIXME: Does not honor screen boundaries, so might set touch areas outside of the screen. 63 * <p>FIXME: Does not honor ancestor boundaries, so might not work if ancestor too small. 64 * <p>FIXME: Does not work if ancestor has more than one TouchTarget set. 65 * @param target element to check for accessibility compliance 66 * @param ancestor target's ancestor - only one target per ancestor allowed 67 */ enforce(View target, View ancestor)68 public void enforce(View target, View ancestor) { 69 target.getViewTreeObserver().addOnGlobalLayoutListener( // avoids some subtle bugs 70 () -> { 71 int minTargetPx = (int) Math.ceil(dpToPx(MIN_TARGET_DP)); 72 int deltaHeight = Math.max(0, minTargetPx - target.getHeight()); 73 int deltaWidth = Math.max(0, minTargetPx - target.getWidth()); 74 if (deltaHeight <= 0 && deltaWidth <= 0) { 75 return; 76 } 77 78 ancestor.post(() -> { 79 Rect bounds = createNewBounds(target, minTargetPx, deltaWidth, deltaHeight); 80 81 synchronized (ancestor) { 82 if (ancestor.getTouchDelegate() == null) { 83 ancestor.setTouchDelegate( 84 mTouchDelegateProvider.getInstance(bounds, target)); 85 ProvisionLogger.logd(String.format( 86 "Successfully set touch delegate on ancestor %s " 87 + "delegating to target %s.", 88 ancestor, target)); 89 } else { 90 ProvisionLogger.logd(String.format( 91 "Ancestor %s already has an assigned touch delegate %s. " 92 + "Unable to assign another one. Ignoring target.", 93 ancestor, target)); 94 } 95 } 96 }); 97 }); 98 } 99 createNewBounds(View target, int minTargetPx, int deltaWidth, int deltaHeight)100 private Rect createNewBounds(View target, int minTargetPx, int deltaWidth, int deltaHeight) { 101 int deltaWidthHalf = deltaWidth / 2; 102 int deltaHeightHalf = deltaHeight / 2; 103 104 Rect result = new Rect(); 105 target.getHitRect(result); 106 result.top -= deltaHeightHalf; 107 result.bottom += deltaHeightHalf; 108 result.left -= deltaWidthHalf; 109 result.right += deltaWidthHalf; 110 111 // fix rounding errors 112 int deltaHeightRemaining = minTargetPx - (result.bottom - result.top); 113 if (deltaHeightRemaining > 0) { 114 result.bottom += deltaHeightRemaining; 115 } 116 int deltaWidthRemaining = minTargetPx - (result.right - result.left); 117 if (deltaWidthRemaining > 0) { 118 result.right += deltaWidthRemaining; 119 } 120 return result; 121 } 122 dpToPx(int dp)123 private float dpToPx(int dp) { 124 return dp * mDensity; 125 } 126 127 interface TouchDelegateProvider { 128 /** 129 * @param bounds New touch bounds 130 * @param delegateView The view that should receive motion events (target) 131 */ getInstance(Rect bounds, View delegateView)132 TouchDelegate getInstance(Rect bounds, View delegateView); 133 } 134 } 135