Home | History | Annotate | Download | only in util
      1 /*
      2  * Copyright (C) 2016 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 
     17 package com.android.setupwizardlib.util;
     18 
     19 import android.graphics.Rect;
     20 import android.os.Bundle;
     21 import android.support.v4.view.accessibility.AccessibilityNodeInfoCompat;
     22 import android.support.v4.widget.ExploreByTouchHelper;
     23 import android.text.Layout;
     24 import android.text.Spanned;
     25 import android.text.style.ClickableSpan;
     26 import android.util.Log;
     27 import android.view.accessibility.AccessibilityEvent;
     28 import android.widget.TextView;
     29 
     30 import java.util.List;
     31 
     32 /**
     33  * An accessibility delegate that allows {@link android.text.style.ClickableSpan} to be focused and
     34  * clicked by accessibility services.
     35  *
     36  * <p />Sample usage:
     37  * <pre>
     38  * LinkAccessibilityHelper mAccessibilityHelper;
     39  *
     40  * private void init() {
     41  *     mAccessibilityHelper = new LinkAccessibilityHelper(myTextView);
     42  *     ViewCompat.setAccessibilityDelegate(myTextView, mLinkHelper);
     43  * }
     44  *
     45  * {@literal @}Override
     46  * protected boolean dispatchHoverEvent({@literal @}NonNull MotionEvent event) {
     47  *     if (mAccessibilityHelper != null && mAccessibilityHelper.dispatchHoverEvent(event)) {
     48  *         return true;
     49  *     }
     50  *     return super.dispatchHoverEvent(event);
     51  * }
     52  * </pre>
     53  *
     54  * @see com.android.setupwizardlib.view.RichTextView
     55  * @see android.support.v4.widget.ExploreByTouchHelper
     56  */
     57 public class LinkAccessibilityHelper extends ExploreByTouchHelper {
     58 
     59     private static final String TAG = "LinkAccessibilityHelper";
     60 
     61     private final TextView mView;
     62     private final Rect mTempRect = new Rect();
     63 
     64     public LinkAccessibilityHelper(TextView view) {
     65         super(view);
     66         mView = view;
     67     }
     68 
     69     @Override
     70     protected int getVirtualViewAt(float x, float y) {
     71         final CharSequence text = mView.getText();
     72         if (text instanceof Spanned) {
     73             final Spanned spannedText = (Spanned) text;
     74             final int offset = getOffsetForPosition(mView, x, y);
     75             ClickableSpan[] linkSpans = spannedText.getSpans(offset, offset, ClickableSpan.class);
     76             if (linkSpans.length == 1) {
     77                 ClickableSpan linkSpan = linkSpans[0];
     78                 return spannedText.getSpanStart(linkSpan);
     79             }
     80         }
     81         return INVALID_ID;
     82     }
     83 
     84     @Override
     85     protected void getVisibleVirtualViews(List<Integer> virtualViewIds) {
     86         final CharSequence text = mView.getText();
     87         if (text instanceof Spanned) {
     88             final Spanned spannedText = (Spanned) text;
     89             ClickableSpan[] linkSpans = spannedText.getSpans(0, spannedText.length(),
     90                     ClickableSpan.class);
     91             for (ClickableSpan span : linkSpans) {
     92                 virtualViewIds.add(spannedText.getSpanStart(span));
     93             }
     94         }
     95     }
     96 
     97     @Override
     98     protected void onPopulateEventForVirtualView(int virtualViewId, AccessibilityEvent event) {
     99         final ClickableSpan span = getSpanForOffset(virtualViewId);
    100         if (span != null) {
    101             event.setContentDescription(getTextForSpan(span));
    102         } else {
    103             Log.e(TAG, "LinkSpan is null for offset: " + virtualViewId);
    104             event.setContentDescription(mView.getText());
    105         }
    106     }
    107 
    108     @Override
    109     protected void onPopulateNodeForVirtualView(int virtualViewId,
    110             AccessibilityNodeInfoCompat info) {
    111         final ClickableSpan span = getSpanForOffset(virtualViewId);
    112         if (span != null) {
    113             info.setContentDescription(getTextForSpan(span));
    114         } else {
    115             Log.e(TAG, "LinkSpan is null for offset: " + virtualViewId);
    116             info.setContentDescription(mView.getText());
    117         }
    118         info.setFocusable(true);
    119         info.setClickable(true);
    120         getBoundsForSpan(span, mTempRect);
    121         if (mTempRect.isEmpty()) {
    122             Log.e(TAG, "LinkSpan bounds is empty for: " + virtualViewId);
    123             mTempRect.set(0, 0, 1, 1);
    124         }
    125         info.setBoundsInParent(mTempRect);
    126         info.addAction(AccessibilityNodeInfoCompat.ACTION_CLICK);
    127     }
    128 
    129     @Override
    130     protected boolean onPerformActionForVirtualView(int virtualViewId, int action,
    131             Bundle arguments) {
    132         if (action == AccessibilityNodeInfoCompat.ACTION_CLICK) {
    133             ClickableSpan span = getSpanForOffset(virtualViewId);
    134             if (span != null) {
    135                 span.onClick(mView);
    136                 return true;
    137             } else {
    138                 Log.e(TAG, "LinkSpan is null for offset: " + virtualViewId);
    139             }
    140         }
    141         return false;
    142     }
    143 
    144     private ClickableSpan getSpanForOffset(int offset) {
    145         CharSequence text = mView.getText();
    146         if (text instanceof Spanned) {
    147             Spanned spannedText = (Spanned) text;
    148             ClickableSpan[] spans = spannedText.getSpans(offset, offset, ClickableSpan.class);
    149             if (spans.length == 1) {
    150                 return spans[0];
    151             }
    152         }
    153         return null;
    154     }
    155 
    156     private CharSequence getTextForSpan(ClickableSpan span) {
    157         CharSequence text = mView.getText();
    158         if (text instanceof Spanned) {
    159             Spanned spannedText = (Spanned) text;
    160             return spannedText.subSequence(spannedText.getSpanStart(span),
    161                     spannedText.getSpanEnd(span));
    162         }
    163         return text;
    164     }
    165 
    166     // Find the bounds of a span. If it spans multiple lines, it will only return the bounds for the
    167     // section on the first line.
    168     private Rect getBoundsForSpan(ClickableSpan span, Rect outRect) {
    169         CharSequence text = mView.getText();
    170         outRect.setEmpty();
    171         if (text instanceof Spanned) {
    172             final Layout layout = mView.getLayout();
    173             if (layout != null) {
    174                 Spanned spannedText = (Spanned) text;
    175                 final int spanStart = spannedText.getSpanStart(span);
    176                 final int spanEnd = spannedText.getSpanEnd(span);
    177                 final float xStart = layout.getPrimaryHorizontal(spanStart);
    178                 final float xEnd = layout.getPrimaryHorizontal(spanEnd);
    179                 final int lineStart = layout.getLineForOffset(spanStart);
    180                 final int lineEnd = layout.getLineForOffset(spanEnd);
    181                 layout.getLineBounds(lineStart, outRect);
    182                 if (lineEnd == lineStart) {
    183                     // If the span is on a single line, adjust both the left and right bounds
    184                     // so outrect is exactly bounding the span.
    185                     outRect.left = (int) Math.min(xStart, xEnd);
    186                     outRect.right = (int) Math.max(xStart, xEnd);
    187                 } else {
    188                     // If the span wraps across multiple lines, only use the first line (as returned
    189                     // by layout.getLineBounds above), and adjust the "start" of outrect to where
    190                     // the span starts, leaving the "end" of outrect at the end of the line.
    191                     // ("start" being left for LTR, and right for RTL)
    192                     if (layout.getParagraphDirection(lineStart) == Layout.DIR_RIGHT_TO_LEFT) {
    193                         outRect.right = (int) xStart;
    194                     } else {
    195                         outRect.left = (int) xStart;
    196                     }
    197                 }
    198 
    199                 // Offset for padding
    200                 outRect.offset(mView.getTotalPaddingLeft(), mView.getTotalPaddingTop());
    201             }
    202         }
    203         return outRect;
    204     }
    205 
    206     // Compat implementation of TextView#getOffsetForPosition().
    207 
    208     private static int getOffsetForPosition(TextView view, float x, float y) {
    209         if (view.getLayout() == null) return -1;
    210         final int line = getLineAtCoordinate(view, y);
    211         return getOffsetAtCoordinate(view, line, x);
    212     }
    213 
    214     private static float convertToLocalHorizontalCoordinate(TextView view, float x) {
    215         x -= view.getTotalPaddingLeft();
    216         // Clamp the position to inside of the view.
    217         x = Math.max(0.0f, x);
    218         x = Math.min(view.getWidth() - view.getTotalPaddingRight() - 1, x);
    219         x += view.getScrollX();
    220         return x;
    221     }
    222 
    223     private static int getLineAtCoordinate(TextView view, float y) {
    224         y -= view.getTotalPaddingTop();
    225         // Clamp the position to inside of the view.
    226         y = Math.max(0.0f, y);
    227         y = Math.min(view.getHeight() - view.getTotalPaddingBottom() - 1, y);
    228         y += view.getScrollY();
    229         return view.getLayout().getLineForVertical((int) y);
    230     }
    231 
    232     private static int getOffsetAtCoordinate(TextView view, int line, float x) {
    233         x = convertToLocalHorizontalCoordinate(view, x);
    234         return view.getLayout().getOffsetForHorizontal(line, x);
    235     }
    236 }
    237