LinkAccessibilityHelper.java revision cf90658b5c16018c9f3db7fd1d872025cff5d1dc
1d832154e333a3a45b5faecd518b664ddd297183cMaurice Lam/* 2d832154e333a3a45b5faecd518b664ddd297183cMaurice Lam * Copyright (C) 2016 The Android Open Source Project 3d832154e333a3a45b5faecd518b664ddd297183cMaurice Lam * 4d832154e333a3a45b5faecd518b664ddd297183cMaurice Lam * Licensed under the Apache License, Version 2.0 (the "License"); 5d832154e333a3a45b5faecd518b664ddd297183cMaurice Lam * you may not use this file except in compliance with the License. 6d832154e333a3a45b5faecd518b664ddd297183cMaurice Lam * You may obtain a copy of the License at 7d832154e333a3a45b5faecd518b664ddd297183cMaurice Lam * 8d832154e333a3a45b5faecd518b664ddd297183cMaurice Lam * http://www.apache.org/licenses/LICENSE-2.0 9d832154e333a3a45b5faecd518b664ddd297183cMaurice Lam * 10d832154e333a3a45b5faecd518b664ddd297183cMaurice Lam * Unless required by applicable law or agreed to in writing, software 11d832154e333a3a45b5faecd518b664ddd297183cMaurice Lam * distributed under the License is distributed on an "AS IS" BASIS, 12d832154e333a3a45b5faecd518b664ddd297183cMaurice Lam * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13d832154e333a3a45b5faecd518b664ddd297183cMaurice Lam * See the License for the specific language governing permissions and 14d832154e333a3a45b5faecd518b664ddd297183cMaurice Lam * limitations under the License. 15d832154e333a3a45b5faecd518b664ddd297183cMaurice Lam */ 16d832154e333a3a45b5faecd518b664ddd297183cMaurice Lam 17d832154e333a3a45b5faecd518b664ddd297183cMaurice Lampackage com.android.setupwizardlib.util; 18d832154e333a3a45b5faecd518b664ddd297183cMaurice Lam 19d832154e333a3a45b5faecd518b664ddd297183cMaurice Lamimport android.graphics.Rect; 20d832154e333a3a45b5faecd518b664ddd297183cMaurice Lamimport android.os.Bundle; 21d832154e333a3a45b5faecd518b664ddd297183cMaurice Lamimport android.support.v4.view.accessibility.AccessibilityNodeInfoCompat; 22d832154e333a3a45b5faecd518b664ddd297183cMaurice Lamimport android.support.v4.widget.ExploreByTouchHelper; 23d832154e333a3a45b5faecd518b664ddd297183cMaurice Lamimport android.text.Layout; 24d832154e333a3a45b5faecd518b664ddd297183cMaurice Lamimport android.text.Spanned; 25d832154e333a3a45b5faecd518b664ddd297183cMaurice Lamimport android.text.style.ClickableSpan; 26d832154e333a3a45b5faecd518b664ddd297183cMaurice Lamimport android.util.Log; 27d832154e333a3a45b5faecd518b664ddd297183cMaurice Lamimport android.view.accessibility.AccessibilityEvent; 28d832154e333a3a45b5faecd518b664ddd297183cMaurice Lamimport android.widget.TextView; 29d832154e333a3a45b5faecd518b664ddd297183cMaurice Lam 30d832154e333a3a45b5faecd518b664ddd297183cMaurice Lamimport java.util.List; 31d832154e333a3a45b5faecd518b664ddd297183cMaurice Lam 32d832154e333a3a45b5faecd518b664ddd297183cMaurice Lam/** 33d832154e333a3a45b5faecd518b664ddd297183cMaurice Lam * An accessibility delegate that allows {@link android.text.style.ClickableSpan} to be focused and 34d832154e333a3a45b5faecd518b664ddd297183cMaurice Lam * clicked by accessibility services. 35d832154e333a3a45b5faecd518b664ddd297183cMaurice Lam * 36d832154e333a3a45b5faecd518b664ddd297183cMaurice Lam * <p />Sample usage: 37d832154e333a3a45b5faecd518b664ddd297183cMaurice Lam * <pre> 38d832154e333a3a45b5faecd518b664ddd297183cMaurice Lam * LinkAccessibilityHelper mAccessibilityHelper; 39d832154e333a3a45b5faecd518b664ddd297183cMaurice Lam * 40d832154e333a3a45b5faecd518b664ddd297183cMaurice Lam * private void init() { 41d832154e333a3a45b5faecd518b664ddd297183cMaurice Lam * mAccessibilityHelper = new LinkAccessibilityHelper(myTextView); 42d832154e333a3a45b5faecd518b664ddd297183cMaurice Lam * ViewCompat.setAccessibilityDelegate(myTextView, mLinkHelper); 43d832154e333a3a45b5faecd518b664ddd297183cMaurice Lam * } 44d832154e333a3a45b5faecd518b664ddd297183cMaurice Lam * 45d832154e333a3a45b5faecd518b664ddd297183cMaurice Lam * {@literal @}Override 46d832154e333a3a45b5faecd518b664ddd297183cMaurice Lam * protected boolean dispatchHoverEvent({@literal @}NonNull MotionEvent event) { 47d832154e333a3a45b5faecd518b664ddd297183cMaurice Lam * if (mAccessibilityHelper != null && mAccessibilityHelper.dispatchHoverEvent(event)) { 48d832154e333a3a45b5faecd518b664ddd297183cMaurice Lam * return true; 49d832154e333a3a45b5faecd518b664ddd297183cMaurice Lam * } 50d832154e333a3a45b5faecd518b664ddd297183cMaurice Lam * return super.dispatchHoverEvent(event); 51d832154e333a3a45b5faecd518b664ddd297183cMaurice Lam * } 52d832154e333a3a45b5faecd518b664ddd297183cMaurice Lam * </pre> 53d832154e333a3a45b5faecd518b664ddd297183cMaurice Lam * 54d832154e333a3a45b5faecd518b664ddd297183cMaurice Lam * @see com.android.setupwizardlib.view.RichTextView 55d832154e333a3a45b5faecd518b664ddd297183cMaurice Lam * @see android.support.v4.widget.ExploreByTouchHelper 56d832154e333a3a45b5faecd518b664ddd297183cMaurice Lam */ 57d832154e333a3a45b5faecd518b664ddd297183cMaurice Lampublic class LinkAccessibilityHelper extends ExploreByTouchHelper { 58d832154e333a3a45b5faecd518b664ddd297183cMaurice Lam 59d832154e333a3a45b5faecd518b664ddd297183cMaurice Lam private static final String TAG = "LinkAccessibilityHelper"; 60d832154e333a3a45b5faecd518b664ddd297183cMaurice Lam 61d832154e333a3a45b5faecd518b664ddd297183cMaurice Lam private final TextView mView; 62d832154e333a3a45b5faecd518b664ddd297183cMaurice Lam private final Rect mTempRect = new Rect(); 63d832154e333a3a45b5faecd518b664ddd297183cMaurice Lam 64d832154e333a3a45b5faecd518b664ddd297183cMaurice Lam public LinkAccessibilityHelper(TextView view) { 65d832154e333a3a45b5faecd518b664ddd297183cMaurice Lam super(view); 66d832154e333a3a45b5faecd518b664ddd297183cMaurice Lam mView = view; 67d832154e333a3a45b5faecd518b664ddd297183cMaurice Lam } 68d832154e333a3a45b5faecd518b664ddd297183cMaurice Lam 69d832154e333a3a45b5faecd518b664ddd297183cMaurice Lam @Override 70d832154e333a3a45b5faecd518b664ddd297183cMaurice Lam protected int getVirtualViewAt(float x, float y) { 71d832154e333a3a45b5faecd518b664ddd297183cMaurice Lam final CharSequence text = mView.getText(); 72d832154e333a3a45b5faecd518b664ddd297183cMaurice Lam if (text instanceof Spanned) { 73d832154e333a3a45b5faecd518b664ddd297183cMaurice Lam final Spanned spannedText = (Spanned) text; 74d832154e333a3a45b5faecd518b664ddd297183cMaurice Lam final int offset = getOffsetForPosition(mView, x, y); 75d832154e333a3a45b5faecd518b664ddd297183cMaurice Lam ClickableSpan[] linkSpans = spannedText.getSpans(offset, offset, ClickableSpan.class); 76d832154e333a3a45b5faecd518b664ddd297183cMaurice Lam if (linkSpans.length == 1) { 77d832154e333a3a45b5faecd518b664ddd297183cMaurice Lam ClickableSpan linkSpan = linkSpans[0]; 78d832154e333a3a45b5faecd518b664ddd297183cMaurice Lam return spannedText.getSpanStart(linkSpan); 79d832154e333a3a45b5faecd518b664ddd297183cMaurice Lam } 80d832154e333a3a45b5faecd518b664ddd297183cMaurice Lam } 81d832154e333a3a45b5faecd518b664ddd297183cMaurice Lam return INVALID_ID; 82d832154e333a3a45b5faecd518b664ddd297183cMaurice Lam } 83d832154e333a3a45b5faecd518b664ddd297183cMaurice Lam 84d832154e333a3a45b5faecd518b664ddd297183cMaurice Lam @Override 85d832154e333a3a45b5faecd518b664ddd297183cMaurice Lam protected void getVisibleVirtualViews(List<Integer> virtualViewIds) { 86d832154e333a3a45b5faecd518b664ddd297183cMaurice Lam final CharSequence text = mView.getText(); 87d832154e333a3a45b5faecd518b664ddd297183cMaurice Lam if (text instanceof Spanned) { 88d832154e333a3a45b5faecd518b664ddd297183cMaurice Lam final Spanned spannedText = (Spanned) text; 89d832154e333a3a45b5faecd518b664ddd297183cMaurice Lam ClickableSpan[] linkSpans = spannedText.getSpans(0, spannedText.length(), 90d832154e333a3a45b5faecd518b664ddd297183cMaurice Lam ClickableSpan.class); 91d832154e333a3a45b5faecd518b664ddd297183cMaurice Lam for (ClickableSpan span : linkSpans) { 92d832154e333a3a45b5faecd518b664ddd297183cMaurice Lam virtualViewIds.add(spannedText.getSpanStart(span)); 93d832154e333a3a45b5faecd518b664ddd297183cMaurice Lam } 94d832154e333a3a45b5faecd518b664ddd297183cMaurice Lam } 95d832154e333a3a45b5faecd518b664ddd297183cMaurice Lam } 96d832154e333a3a45b5faecd518b664ddd297183cMaurice Lam 97d832154e333a3a45b5faecd518b664ddd297183cMaurice Lam @Override 98d832154e333a3a45b5faecd518b664ddd297183cMaurice Lam protected void onPopulateEventForVirtualView(int virtualViewId, AccessibilityEvent event) { 99d832154e333a3a45b5faecd518b664ddd297183cMaurice Lam final ClickableSpan span = getSpanForOffset(virtualViewId); 100d832154e333a3a45b5faecd518b664ddd297183cMaurice Lam if (span != null) { 101d832154e333a3a45b5faecd518b664ddd297183cMaurice Lam event.setContentDescription(getTextForSpan(span)); 102d832154e333a3a45b5faecd518b664ddd297183cMaurice Lam } else { 103d832154e333a3a45b5faecd518b664ddd297183cMaurice Lam Log.e(TAG, "LinkSpan is null for offset: " + virtualViewId); 104d832154e333a3a45b5faecd518b664ddd297183cMaurice Lam event.setContentDescription(mView.getText()); 105d832154e333a3a45b5faecd518b664ddd297183cMaurice Lam } 106d832154e333a3a45b5faecd518b664ddd297183cMaurice Lam } 107d832154e333a3a45b5faecd518b664ddd297183cMaurice Lam 108d832154e333a3a45b5faecd518b664ddd297183cMaurice Lam @Override 109d832154e333a3a45b5faecd518b664ddd297183cMaurice Lam protected void onPopulateNodeForVirtualView(int virtualViewId, 110d832154e333a3a45b5faecd518b664ddd297183cMaurice Lam AccessibilityNodeInfoCompat info) { 111d832154e333a3a45b5faecd518b664ddd297183cMaurice Lam final ClickableSpan span = getSpanForOffset(virtualViewId); 112d832154e333a3a45b5faecd518b664ddd297183cMaurice Lam if (span != null) { 113d832154e333a3a45b5faecd518b664ddd297183cMaurice Lam info.setContentDescription(getTextForSpan(span)); 114d832154e333a3a45b5faecd518b664ddd297183cMaurice Lam } else { 115d832154e333a3a45b5faecd518b664ddd297183cMaurice Lam Log.e(TAG, "LinkSpan is null for offset: " + virtualViewId); 116d832154e333a3a45b5faecd518b664ddd297183cMaurice Lam info.setContentDescription(mView.getText()); 117d832154e333a3a45b5faecd518b664ddd297183cMaurice Lam } 118d832154e333a3a45b5faecd518b664ddd297183cMaurice Lam info.setFocusable(true); 119d832154e333a3a45b5faecd518b664ddd297183cMaurice Lam info.setClickable(true); 120d832154e333a3a45b5faecd518b664ddd297183cMaurice Lam getBoundsForSpan(span, mTempRect); 121cf90658b5c16018c9f3db7fd1d872025cff5d1dcMaurice Lam if (mTempRect.isEmpty()) { 122d832154e333a3a45b5faecd518b664ddd297183cMaurice Lam Log.e(TAG, "LinkSpan bounds is empty for: " + virtualViewId); 123d832154e333a3a45b5faecd518b664ddd297183cMaurice Lam mTempRect.set(0, 0, 1, 1); 124d832154e333a3a45b5faecd518b664ddd297183cMaurice Lam } 125cf90658b5c16018c9f3db7fd1d872025cff5d1dcMaurice Lam info.setBoundsInParent(mTempRect); 126d832154e333a3a45b5faecd518b664ddd297183cMaurice Lam info.addAction(AccessibilityNodeInfoCompat.ACTION_CLICK); 127d832154e333a3a45b5faecd518b664ddd297183cMaurice Lam } 128d832154e333a3a45b5faecd518b664ddd297183cMaurice Lam 129d832154e333a3a45b5faecd518b664ddd297183cMaurice Lam @Override 130d832154e333a3a45b5faecd518b664ddd297183cMaurice Lam protected boolean onPerformActionForVirtualView(int virtualViewId, int action, 131d832154e333a3a45b5faecd518b664ddd297183cMaurice Lam Bundle arguments) { 132d832154e333a3a45b5faecd518b664ddd297183cMaurice Lam if (action == AccessibilityNodeInfoCompat.ACTION_CLICK) { 133d832154e333a3a45b5faecd518b664ddd297183cMaurice Lam ClickableSpan span = getSpanForOffset(virtualViewId); 134d832154e333a3a45b5faecd518b664ddd297183cMaurice Lam if (span != null) { 135d832154e333a3a45b5faecd518b664ddd297183cMaurice Lam span.onClick(mView); 136d832154e333a3a45b5faecd518b664ddd297183cMaurice Lam return true; 137d832154e333a3a45b5faecd518b664ddd297183cMaurice Lam } else { 138d832154e333a3a45b5faecd518b664ddd297183cMaurice Lam Log.e(TAG, "LinkSpan is null for offset: " + virtualViewId); 139d832154e333a3a45b5faecd518b664ddd297183cMaurice Lam } 140d832154e333a3a45b5faecd518b664ddd297183cMaurice Lam } 141d832154e333a3a45b5faecd518b664ddd297183cMaurice Lam return false; 142d832154e333a3a45b5faecd518b664ddd297183cMaurice Lam } 143d832154e333a3a45b5faecd518b664ddd297183cMaurice Lam 144d832154e333a3a45b5faecd518b664ddd297183cMaurice Lam private ClickableSpan getSpanForOffset(int offset) { 145d832154e333a3a45b5faecd518b664ddd297183cMaurice Lam CharSequence text = mView.getText(); 146d832154e333a3a45b5faecd518b664ddd297183cMaurice Lam if (text instanceof Spanned) { 147d832154e333a3a45b5faecd518b664ddd297183cMaurice Lam Spanned spannedText = (Spanned) text; 148d832154e333a3a45b5faecd518b664ddd297183cMaurice Lam ClickableSpan[] spans = spannedText.getSpans(offset, offset, ClickableSpan.class); 149d832154e333a3a45b5faecd518b664ddd297183cMaurice Lam if (spans.length == 1) { 150d832154e333a3a45b5faecd518b664ddd297183cMaurice Lam return spans[0]; 151d832154e333a3a45b5faecd518b664ddd297183cMaurice Lam } 152d832154e333a3a45b5faecd518b664ddd297183cMaurice Lam } 153d832154e333a3a45b5faecd518b664ddd297183cMaurice Lam return null; 154d832154e333a3a45b5faecd518b664ddd297183cMaurice Lam } 155d832154e333a3a45b5faecd518b664ddd297183cMaurice Lam 156d832154e333a3a45b5faecd518b664ddd297183cMaurice Lam private CharSequence getTextForSpan(ClickableSpan span) { 157d832154e333a3a45b5faecd518b664ddd297183cMaurice Lam CharSequence text = mView.getText(); 158d832154e333a3a45b5faecd518b664ddd297183cMaurice Lam if (text instanceof Spanned) { 159d832154e333a3a45b5faecd518b664ddd297183cMaurice Lam Spanned spannedText = (Spanned) text; 160d832154e333a3a45b5faecd518b664ddd297183cMaurice Lam return spannedText.subSequence(spannedText.getSpanStart(span), 161d832154e333a3a45b5faecd518b664ddd297183cMaurice Lam spannedText.getSpanEnd(span)); 162d832154e333a3a45b5faecd518b664ddd297183cMaurice Lam } 163d832154e333a3a45b5faecd518b664ddd297183cMaurice Lam return text; 164d832154e333a3a45b5faecd518b664ddd297183cMaurice Lam } 165d832154e333a3a45b5faecd518b664ddd297183cMaurice Lam 166d832154e333a3a45b5faecd518b664ddd297183cMaurice Lam // Find the bounds of a span. If it spans multiple lines, it will only return the bounds for the 167d832154e333a3a45b5faecd518b664ddd297183cMaurice Lam // section on the first line. 168d832154e333a3a45b5faecd518b664ddd297183cMaurice Lam private Rect getBoundsForSpan(ClickableSpan span, Rect outRect) { 169d832154e333a3a45b5faecd518b664ddd297183cMaurice Lam CharSequence text = mView.getText(); 170d832154e333a3a45b5faecd518b664ddd297183cMaurice Lam outRect.setEmpty(); 171d832154e333a3a45b5faecd518b664ddd297183cMaurice Lam if (text instanceof Spanned) { 172d832154e333a3a45b5faecd518b664ddd297183cMaurice Lam final Layout layout = mView.getLayout(); 173cf90658b5c16018c9f3db7fd1d872025cff5d1dcMaurice Lam if (layout != null) { 174cf90658b5c16018c9f3db7fd1d872025cff5d1dcMaurice Lam Spanned spannedText = (Spanned) text; 175cf90658b5c16018c9f3db7fd1d872025cff5d1dcMaurice Lam final int spanStart = spannedText.getSpanStart(span); 176cf90658b5c16018c9f3db7fd1d872025cff5d1dcMaurice Lam final int spanEnd = spannedText.getSpanEnd(span); 177cf90658b5c16018c9f3db7fd1d872025cff5d1dcMaurice Lam final float xStart = layout.getPrimaryHorizontal(spanStart); 178cf90658b5c16018c9f3db7fd1d872025cff5d1dcMaurice Lam final float xEnd = layout.getPrimaryHorizontal(spanEnd); 179cf90658b5c16018c9f3db7fd1d872025cff5d1dcMaurice Lam final int lineStart = layout.getLineForOffset(spanStart); 180cf90658b5c16018c9f3db7fd1d872025cff5d1dcMaurice Lam final int lineEnd = layout.getLineForOffset(spanEnd); 181cf90658b5c16018c9f3db7fd1d872025cff5d1dcMaurice Lam layout.getLineBounds(lineStart, outRect); 182cf90658b5c16018c9f3db7fd1d872025cff5d1dcMaurice Lam outRect.left = (int) xStart; 183cf90658b5c16018c9f3db7fd1d872025cff5d1dcMaurice Lam if (lineEnd == lineStart) { 184cf90658b5c16018c9f3db7fd1d872025cff5d1dcMaurice Lam outRect.right = (int) xEnd; 185cf90658b5c16018c9f3db7fd1d872025cff5d1dcMaurice Lam } // otherwise just leave it at the end of the start line 186cf90658b5c16018c9f3db7fd1d872025cff5d1dcMaurice Lam 187cf90658b5c16018c9f3db7fd1d872025cff5d1dcMaurice Lam // Offset for padding 188cf90658b5c16018c9f3db7fd1d872025cff5d1dcMaurice Lam outRect.offset(mView.getTotalPaddingLeft(), mView.getTotalPaddingTop()); 189cf90658b5c16018c9f3db7fd1d872025cff5d1dcMaurice Lam } 190d832154e333a3a45b5faecd518b664ddd297183cMaurice Lam } 191d832154e333a3a45b5faecd518b664ddd297183cMaurice Lam return outRect; 192d832154e333a3a45b5faecd518b664ddd297183cMaurice Lam } 193d832154e333a3a45b5faecd518b664ddd297183cMaurice Lam 194d832154e333a3a45b5faecd518b664ddd297183cMaurice Lam // Compat implementation of TextView#getOffsetForPosition(). 195d832154e333a3a45b5faecd518b664ddd297183cMaurice Lam 196d832154e333a3a45b5faecd518b664ddd297183cMaurice Lam private static int getOffsetForPosition(TextView view, float x, float y) { 197d832154e333a3a45b5faecd518b664ddd297183cMaurice Lam if (view.getLayout() == null) return -1; 198d832154e333a3a45b5faecd518b664ddd297183cMaurice Lam final int line = getLineAtCoordinate(view, y); 199d832154e333a3a45b5faecd518b664ddd297183cMaurice Lam return getOffsetAtCoordinate(view, line, x); 200d832154e333a3a45b5faecd518b664ddd297183cMaurice Lam } 201d832154e333a3a45b5faecd518b664ddd297183cMaurice Lam 202d832154e333a3a45b5faecd518b664ddd297183cMaurice Lam private static float convertToLocalHorizontalCoordinate(TextView view, float x) { 203d832154e333a3a45b5faecd518b664ddd297183cMaurice Lam x -= view.getTotalPaddingLeft(); 204d832154e333a3a45b5faecd518b664ddd297183cMaurice Lam // Clamp the position to inside of the view. 205d832154e333a3a45b5faecd518b664ddd297183cMaurice Lam x = Math.max(0.0f, x); 206d832154e333a3a45b5faecd518b664ddd297183cMaurice Lam x = Math.min(view.getWidth() - view.getTotalPaddingRight() - 1, x); 207d832154e333a3a45b5faecd518b664ddd297183cMaurice Lam x += view.getScrollX(); 208d832154e333a3a45b5faecd518b664ddd297183cMaurice Lam return x; 209d832154e333a3a45b5faecd518b664ddd297183cMaurice Lam } 210d832154e333a3a45b5faecd518b664ddd297183cMaurice Lam 211d832154e333a3a45b5faecd518b664ddd297183cMaurice Lam private static int getLineAtCoordinate(TextView view, float y) { 212d832154e333a3a45b5faecd518b664ddd297183cMaurice Lam y -= view.getTotalPaddingTop(); 213d832154e333a3a45b5faecd518b664ddd297183cMaurice Lam // Clamp the position to inside of the view. 214d832154e333a3a45b5faecd518b664ddd297183cMaurice Lam y = Math.max(0.0f, y); 215d832154e333a3a45b5faecd518b664ddd297183cMaurice Lam y = Math.min(view.getHeight() - view.getTotalPaddingBottom() - 1, y); 216d832154e333a3a45b5faecd518b664ddd297183cMaurice Lam y += view.getScrollY(); 217d832154e333a3a45b5faecd518b664ddd297183cMaurice Lam return view.getLayout().getLineForVertical((int) y); 218d832154e333a3a45b5faecd518b664ddd297183cMaurice Lam } 219d832154e333a3a45b5faecd518b664ddd297183cMaurice Lam 220d832154e333a3a45b5faecd518b664ddd297183cMaurice Lam private static int getOffsetAtCoordinate(TextView view, int line, float x) { 221d832154e333a3a45b5faecd518b664ddd297183cMaurice Lam x = convertToLocalHorizontalCoordinate(view, x); 222d832154e333a3a45b5faecd518b664ddd297183cMaurice Lam return view.getLayout().getOffsetForHorizontal(line, x); 223d832154e333a3a45b5faecd518b664ddd297183cMaurice Lam } 224d832154e333a3a45b5faecd518b664ddd297183cMaurice Lam} 225