12d9ad3273f375a84cf32ea33b55a9bcde43d7bc7Jakub Gielzak/* 22d9ad3273f375a84cf32ea33b55a9bcde43d7bc7Jakub Gielzak * Copyright (C) 2017 The Android Open Source Project 32d9ad3273f375a84cf32ea33b55a9bcde43d7bc7Jakub Gielzak * 42d9ad3273f375a84cf32ea33b55a9bcde43d7bc7Jakub Gielzak * Licensed under the Apache License, Version 2.0 (the "License"); 52d9ad3273f375a84cf32ea33b55a9bcde43d7bc7Jakub Gielzak * you may not use this file except in compliance with the License. 62d9ad3273f375a84cf32ea33b55a9bcde43d7bc7Jakub Gielzak * You may obtain a copy of the License at 72d9ad3273f375a84cf32ea33b55a9bcde43d7bc7Jakub Gielzak * 82d9ad3273f375a84cf32ea33b55a9bcde43d7bc7Jakub Gielzak * http://www.apache.org/licenses/LICENSE-2.0 92d9ad3273f375a84cf32ea33b55a9bcde43d7bc7Jakub Gielzak * 102d9ad3273f375a84cf32ea33b55a9bcde43d7bc7Jakub Gielzak * Unless required by applicable law or agreed to in writing, software 112d9ad3273f375a84cf32ea33b55a9bcde43d7bc7Jakub Gielzak * distributed under the License is distributed on an "AS IS" BASIS, 122d9ad3273f375a84cf32ea33b55a9bcde43d7bc7Jakub Gielzak * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 132d9ad3273f375a84cf32ea33b55a9bcde43d7bc7Jakub Gielzak * See the License for the specific language governing permissions and 142d9ad3273f375a84cf32ea33b55a9bcde43d7bc7Jakub Gielzak * limitations under the License. 152d9ad3273f375a84cf32ea33b55a9bcde43d7bc7Jakub Gielzak */ 162d9ad3273f375a84cf32ea33b55a9bcde43d7bc7Jakub Gielzakpackage com.android.managedprovisioning.common; 172d9ad3273f375a84cf32ea33b55a9bcde43d7bc7Jakub Gielzak 182d9ad3273f375a84cf32ea33b55a9bcde43d7bc7Jakub Gielzakimport android.graphics.Rect; 192d9ad3273f375a84cf32ea33b55a9bcde43d7bc7Jakub Gielzakimport android.util.DisplayMetrics; 202d9ad3273f375a84cf32ea33b55a9bcde43d7bc7Jakub Gielzakimport android.view.TouchDelegate; 212d9ad3273f375a84cf32ea33b55a9bcde43d7bc7Jakub Gielzakimport android.view.View; 222d9ad3273f375a84cf32ea33b55a9bcde43d7bc7Jakub Gielzak 232d9ad3273f375a84cf32ea33b55a9bcde43d7bc7Jakub Gielzakimport com.android.internal.annotations.VisibleForTesting; 242d9ad3273f375a84cf32ea33b55a9bcde43d7bc7Jakub Gielzak 252d9ad3273f375a84cf32ea33b55a9bcde43d7bc7Jakub Gielzak/** 262d9ad3273f375a84cf32ea33b55a9bcde43d7bc7Jakub Gielzak * Allows for expanding touch area of a {@link View} element, so it's compliant with 272d9ad3273f375a84cf32ea33b55a9bcde43d7bc7Jakub Gielzak * accessibility guidelines, while not modifying the UI appearance. 282d9ad3273f375a84cf32ea33b55a9bcde43d7bc7Jakub Gielzak * @see <a href="https://goo.gl/FcU5gX">Android Accessibility Guide</a> 292d9ad3273f375a84cf32ea33b55a9bcde43d7bc7Jakub Gielzak */ 302d9ad3273f375a84cf32ea33b55a9bcde43d7bc7Jakub Gielzakpublic class TouchTargetEnforcer { 312d9ad3273f375a84cf32ea33b55a9bcde43d7bc7Jakub Gielzak /** Value taken from Android Accessibility Guide */ 322d9ad3273f375a84cf32ea33b55a9bcde43d7bc7Jakub Gielzak @VisibleForTesting static final int MIN_TARGET_DP = 48; 332d9ad3273f375a84cf32ea33b55a9bcde43d7bc7Jakub Gielzak 342d9ad3273f375a84cf32ea33b55a9bcde43d7bc7Jakub Gielzak /** @see DisplayMetrics#density */ 352d9ad3273f375a84cf32ea33b55a9bcde43d7bc7Jakub Gielzak private final float mDensity; 362d9ad3273f375a84cf32ea33b55a9bcde43d7bc7Jakub Gielzak 372d9ad3273f375a84cf32ea33b55a9bcde43d7bc7Jakub Gielzak private final TouchDelegateProvider mTouchDelegateProvider; 382d9ad3273f375a84cf32ea33b55a9bcde43d7bc7Jakub Gielzak 392d9ad3273f375a84cf32ea33b55a9bcde43d7bc7Jakub Gielzak /** 402d9ad3273f375a84cf32ea33b55a9bcde43d7bc7Jakub Gielzak * Allows for expanding touch area of a {@link View} element, so it's compliant with 412d9ad3273f375a84cf32ea33b55a9bcde43d7bc7Jakub Gielzak * accessibility guidelines, while not modifying the UI appearance. 422d9ad3273f375a84cf32ea33b55a9bcde43d7bc7Jakub Gielzak * @param density {@link DisplayMetrics#density} 432d9ad3273f375a84cf32ea33b55a9bcde43d7bc7Jakub Gielzak * @see <a href="https://goo.gl/FcU5gX">Android Accessibility Guide</a> 442d9ad3273f375a84cf32ea33b55a9bcde43d7bc7Jakub Gielzak */ 452d9ad3273f375a84cf32ea33b55a9bcde43d7bc7Jakub Gielzak public TouchTargetEnforcer(float density) { 462d9ad3273f375a84cf32ea33b55a9bcde43d7bc7Jakub Gielzak this(density, TouchDelegate::new); 472d9ad3273f375a84cf32ea33b55a9bcde43d7bc7Jakub Gielzak } 482d9ad3273f375a84cf32ea33b55a9bcde43d7bc7Jakub Gielzak 492d9ad3273f375a84cf32ea33b55a9bcde43d7bc7Jakub Gielzak /** 502d9ad3273f375a84cf32ea33b55a9bcde43d7bc7Jakub Gielzak * Allows for expanding touch area of a {@link View} element, so it's compliant with 512d9ad3273f375a84cf32ea33b55a9bcde43d7bc7Jakub Gielzak * accessibility guidelines, while not modifying the UI appearance. 522d9ad3273f375a84cf32ea33b55a9bcde43d7bc7Jakub Gielzak * @param density {@link DisplayMetrics#density} 532d9ad3273f375a84cf32ea33b55a9bcde43d7bc7Jakub Gielzak * @see <a href="https://goo.gl/FcU5gX">Android Accessibility Guide</a> 542d9ad3273f375a84cf32ea33b55a9bcde43d7bc7Jakub Gielzak */ 552d9ad3273f375a84cf32ea33b55a9bcde43d7bc7Jakub Gielzak TouchTargetEnforcer(float density, TouchDelegateProvider touchDelegateProvider) { 562d9ad3273f375a84cf32ea33b55a9bcde43d7bc7Jakub Gielzak mDensity = density; 572d9ad3273f375a84cf32ea33b55a9bcde43d7bc7Jakub Gielzak mTouchDelegateProvider = touchDelegateProvider; 582d9ad3273f375a84cf32ea33b55a9bcde43d7bc7Jakub Gielzak } 592d9ad3273f375a84cf32ea33b55a9bcde43d7bc7Jakub Gielzak 602d9ad3273f375a84cf32ea33b55a9bcde43d7bc7Jakub Gielzak /** 612d9ad3273f375a84cf32ea33b55a9bcde43d7bc7Jakub Gielzak * Compares target's touch area to required minimum, and expands it if necessary. 622d9ad3273f375a84cf32ea33b55a9bcde43d7bc7Jakub Gielzak * <p>FIXME: Does not honor screen boundaries, so might set touch areas outside of the screen. 632d9ad3273f375a84cf32ea33b55a9bcde43d7bc7Jakub Gielzak * <p>FIXME: Does not honor ancestor boundaries, so might not work if ancestor too small. 642d9ad3273f375a84cf32ea33b55a9bcde43d7bc7Jakub Gielzak * <p>FIXME: Does not work if ancestor has more than one TouchTarget set. 652d9ad3273f375a84cf32ea33b55a9bcde43d7bc7Jakub Gielzak * @param target element to check for accessibility compliance 662d9ad3273f375a84cf32ea33b55a9bcde43d7bc7Jakub Gielzak * @param ancestor target's ancestor - only one target per ancestor allowed 672d9ad3273f375a84cf32ea33b55a9bcde43d7bc7Jakub Gielzak */ 682d9ad3273f375a84cf32ea33b55a9bcde43d7bc7Jakub Gielzak public void enforce(View target, View ancestor) { 692d9ad3273f375a84cf32ea33b55a9bcde43d7bc7Jakub Gielzak target.getViewTreeObserver().addOnGlobalLayoutListener( // avoids some subtle bugs 702d9ad3273f375a84cf32ea33b55a9bcde43d7bc7Jakub Gielzak () -> { 712d9ad3273f375a84cf32ea33b55a9bcde43d7bc7Jakub Gielzak int minTargetPx = (int) Math.ceil(dpToPx(MIN_TARGET_DP)); 722d9ad3273f375a84cf32ea33b55a9bcde43d7bc7Jakub Gielzak int deltaHeight = Math.max(0, minTargetPx - target.getHeight()); 732d9ad3273f375a84cf32ea33b55a9bcde43d7bc7Jakub Gielzak int deltaWidth = Math.max(0, minTargetPx - target.getWidth()); 742d9ad3273f375a84cf32ea33b55a9bcde43d7bc7Jakub Gielzak if (deltaHeight <= 0 && deltaWidth <= 0) { 752d9ad3273f375a84cf32ea33b55a9bcde43d7bc7Jakub Gielzak return; 762d9ad3273f375a84cf32ea33b55a9bcde43d7bc7Jakub Gielzak } 772d9ad3273f375a84cf32ea33b55a9bcde43d7bc7Jakub Gielzak 782d9ad3273f375a84cf32ea33b55a9bcde43d7bc7Jakub Gielzak ancestor.post(() -> { 792d9ad3273f375a84cf32ea33b55a9bcde43d7bc7Jakub Gielzak Rect bounds = createNewBounds(target, minTargetPx, deltaWidth, deltaHeight); 802d9ad3273f375a84cf32ea33b55a9bcde43d7bc7Jakub Gielzak 812d9ad3273f375a84cf32ea33b55a9bcde43d7bc7Jakub Gielzak synchronized (ancestor) { 822d9ad3273f375a84cf32ea33b55a9bcde43d7bc7Jakub Gielzak if (ancestor.getTouchDelegate() == null) { 832d9ad3273f375a84cf32ea33b55a9bcde43d7bc7Jakub Gielzak ancestor.setTouchDelegate( 842d9ad3273f375a84cf32ea33b55a9bcde43d7bc7Jakub Gielzak mTouchDelegateProvider.getInstance(bounds, target)); 852d9ad3273f375a84cf32ea33b55a9bcde43d7bc7Jakub Gielzak ProvisionLogger.logd(String.format( 862d9ad3273f375a84cf32ea33b55a9bcde43d7bc7Jakub Gielzak "Successfully set touch delegate on ancestor %s " 872d9ad3273f375a84cf32ea33b55a9bcde43d7bc7Jakub Gielzak + "delegating to target %s.", 882d9ad3273f375a84cf32ea33b55a9bcde43d7bc7Jakub Gielzak ancestor, target)); 892d9ad3273f375a84cf32ea33b55a9bcde43d7bc7Jakub Gielzak } else { 902d9ad3273f375a84cf32ea33b55a9bcde43d7bc7Jakub Gielzak ProvisionLogger.logd(String.format( 912d9ad3273f375a84cf32ea33b55a9bcde43d7bc7Jakub Gielzak "Ancestor %s already has an assigned touch delegate %s. " 922d9ad3273f375a84cf32ea33b55a9bcde43d7bc7Jakub Gielzak + "Unable to assign another one. Ignoring target.", 932d9ad3273f375a84cf32ea33b55a9bcde43d7bc7Jakub Gielzak ancestor, target)); 942d9ad3273f375a84cf32ea33b55a9bcde43d7bc7Jakub Gielzak } 952d9ad3273f375a84cf32ea33b55a9bcde43d7bc7Jakub Gielzak } 962d9ad3273f375a84cf32ea33b55a9bcde43d7bc7Jakub Gielzak }); 972d9ad3273f375a84cf32ea33b55a9bcde43d7bc7Jakub Gielzak }); 982d9ad3273f375a84cf32ea33b55a9bcde43d7bc7Jakub Gielzak } 992d9ad3273f375a84cf32ea33b55a9bcde43d7bc7Jakub Gielzak 1002d9ad3273f375a84cf32ea33b55a9bcde43d7bc7Jakub Gielzak private Rect createNewBounds(View target, int minTargetPx, int deltaWidth, int deltaHeight) { 1012d9ad3273f375a84cf32ea33b55a9bcde43d7bc7Jakub Gielzak int deltaWidthHalf = deltaWidth / 2; 1022d9ad3273f375a84cf32ea33b55a9bcde43d7bc7Jakub Gielzak int deltaHeightHalf = deltaHeight / 2; 1032d9ad3273f375a84cf32ea33b55a9bcde43d7bc7Jakub Gielzak 1042d9ad3273f375a84cf32ea33b55a9bcde43d7bc7Jakub Gielzak Rect result = new Rect(); 1052d9ad3273f375a84cf32ea33b55a9bcde43d7bc7Jakub Gielzak target.getHitRect(result); 1062d9ad3273f375a84cf32ea33b55a9bcde43d7bc7Jakub Gielzak result.top -= deltaHeightHalf; 1072d9ad3273f375a84cf32ea33b55a9bcde43d7bc7Jakub Gielzak result.bottom += deltaHeightHalf; 1082d9ad3273f375a84cf32ea33b55a9bcde43d7bc7Jakub Gielzak result.left -= deltaWidthHalf; 1092d9ad3273f375a84cf32ea33b55a9bcde43d7bc7Jakub Gielzak result.right += deltaWidthHalf; 1102d9ad3273f375a84cf32ea33b55a9bcde43d7bc7Jakub Gielzak 1112d9ad3273f375a84cf32ea33b55a9bcde43d7bc7Jakub Gielzak // fix rounding errors 1122d9ad3273f375a84cf32ea33b55a9bcde43d7bc7Jakub Gielzak int deltaHeightRemaining = minTargetPx - (result.bottom - result.top); 1132d9ad3273f375a84cf32ea33b55a9bcde43d7bc7Jakub Gielzak if (deltaHeightRemaining > 0) { 1142d9ad3273f375a84cf32ea33b55a9bcde43d7bc7Jakub Gielzak result.bottom += deltaHeightRemaining; 1152d9ad3273f375a84cf32ea33b55a9bcde43d7bc7Jakub Gielzak } 1162d9ad3273f375a84cf32ea33b55a9bcde43d7bc7Jakub Gielzak int deltaWidthRemaining = minTargetPx - (result.right - result.left); 1172d9ad3273f375a84cf32ea33b55a9bcde43d7bc7Jakub Gielzak if (deltaWidthRemaining > 0) { 1182d9ad3273f375a84cf32ea33b55a9bcde43d7bc7Jakub Gielzak result.right += deltaWidthRemaining; 1192d9ad3273f375a84cf32ea33b55a9bcde43d7bc7Jakub Gielzak } 1202d9ad3273f375a84cf32ea33b55a9bcde43d7bc7Jakub Gielzak return result; 1212d9ad3273f375a84cf32ea33b55a9bcde43d7bc7Jakub Gielzak } 1222d9ad3273f375a84cf32ea33b55a9bcde43d7bc7Jakub Gielzak 1232d9ad3273f375a84cf32ea33b55a9bcde43d7bc7Jakub Gielzak private float dpToPx(int dp) { 1242d9ad3273f375a84cf32ea33b55a9bcde43d7bc7Jakub Gielzak return dp * mDensity; 1252d9ad3273f375a84cf32ea33b55a9bcde43d7bc7Jakub Gielzak } 1262d9ad3273f375a84cf32ea33b55a9bcde43d7bc7Jakub Gielzak 1272d9ad3273f375a84cf32ea33b55a9bcde43d7bc7Jakub Gielzak interface TouchDelegateProvider { 1282d9ad3273f375a84cf32ea33b55a9bcde43d7bc7Jakub Gielzak /** 1292d9ad3273f375a84cf32ea33b55a9bcde43d7bc7Jakub Gielzak * @param bounds New touch bounds 1302d9ad3273f375a84cf32ea33b55a9bcde43d7bc7Jakub Gielzak * @param delegateView The view that should receive motion events (target) 1312d9ad3273f375a84cf32ea33b55a9bcde43d7bc7Jakub Gielzak */ 1322d9ad3273f375a84cf32ea33b55a9bcde43d7bc7Jakub Gielzak TouchDelegate getInstance(Rect bounds, View delegateView); 1332d9ad3273f375a84cf32ea33b55a9bcde43d7bc7Jakub Gielzak } 1342d9ad3273f375a84cf32ea33b55a9bcde43d7bc7Jakub Gielzak} 135