Home | History | Annotate | Download | only in common
      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      */
     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      */
     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      */
     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 
    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 
    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          */
    132         TouchDelegate getInstance(Rect bounds, View delegateView);
    133     }
    134 }
    135