• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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