From 2613bf2cef66d6fa5018f863f88983cabff97225 Mon Sep 17 00:00:00 2001 From: Salvador Martinez Date: Mon, 23 Jan 2017 13:32:18 -0800 Subject: [PATCH] Moved LinkTextView to settingslib This View is being used by several projects and so I moved it to settingslib so there are not multiple copies floating around. Test: Manual & robotests for projects that use it Bug: 34621372 Change-Id: I8d51a1b5383fad977754eeeb78dce545e506be04 --- .../LinkAccessibilityHelper.java | 235 ++++++++++++++++++ .../settingslib/widget/LinkTextView.java | 66 +++++ 2 files changed, 301 insertions(+) create mode 100644 packages/SettingsLib/src/com/android/settingslib/accessibility/LinkAccessibilityHelper.java create mode 100644 packages/SettingsLib/src/com/android/settingslib/widget/LinkTextView.java diff --git a/packages/SettingsLib/src/com/android/settingslib/accessibility/LinkAccessibilityHelper.java b/packages/SettingsLib/src/com/android/settingslib/accessibility/LinkAccessibilityHelper.java new file mode 100644 index 0000000000000..74b0c6b9ece1b --- /dev/null +++ b/packages/SettingsLib/src/com/android/settingslib/accessibility/LinkAccessibilityHelper.java @@ -0,0 +1,235 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.settingslib.accessibility; + +import android.graphics.Rect; +import android.os.Bundle; +import android.support.v4.view.accessibility.AccessibilityNodeInfoCompat; +import android.support.v4.widget.ExploreByTouchHelper; +import android.text.Layout; +import android.text.Spanned; +import android.text.style.ClickableSpan; +import android.util.Log; +import android.view.accessibility.AccessibilityEvent; +import android.widget.TextView; + +import java.util.List; + +/** + * COPIED FROM SETUP WIZARD An accessibility delegate that allows {@link + * android.text.style.ClickableSpan} to be focused and clicked by accessibility services. + * + *

Sample usage: + * + *

+ * LinkAccessibilityHelper mAccessibilityHelper;
+ *
+ * private void init() {
+ *     mAccessibilityHelper = new LinkAccessibilityHelper(myTextView);
+ *     ViewCompat.setAccessibilityDelegate(myTextView, mLinkHelper);
+ * }
+ *
+ * {@literal @}Override
+ * protected boolean dispatchHoverEvent({@literal @}NonNull MotionEvent event) {
+ *     if (mAccessibilityHelper != null && mAccessibilityHelper.dispatchHoverEvent(event)) {
+ *         return true;
+ *     }
+ *     return super.dispatchHoverEvent(event);
+ * }
+ * 
+ * + * @see android.support.v4.widget.ExploreByTouchHelper + */ +public class LinkAccessibilityHelper extends ExploreByTouchHelper { + + private static final String TAG = "LinkAccessibilityHelper"; + + private final TextView mView; + private final Rect mTempRect = new Rect(); + + public LinkAccessibilityHelper(TextView view) { + super(view); + mView = view; + } + + @Override + protected int getVirtualViewAt(float x, float y) { + final CharSequence text = mView.getText(); + if (text instanceof Spanned) { + final Spanned spannedText = (Spanned) text; + final int offset = getOffsetForPosition(mView, x, y); + ClickableSpan[] linkSpans = spannedText.getSpans(offset, offset, ClickableSpan.class); + if (linkSpans.length == 1) { + ClickableSpan linkSpan = linkSpans[0]; + return spannedText.getSpanStart(linkSpan); + } + } + return INVALID_ID; + } + + @Override + protected void getVisibleVirtualViews(List virtualViewIds) { + final CharSequence text = mView.getText(); + if (text instanceof Spanned) { + final Spanned spannedText = (Spanned) text; + ClickableSpan[] linkSpans = + spannedText.getSpans(0, spannedText.length(), ClickableSpan.class); + for (ClickableSpan span : linkSpans) { + virtualViewIds.add(spannedText.getSpanStart(span)); + } + } + } + + @Override + protected void onPopulateEventForVirtualView(int virtualViewId, AccessibilityEvent event) { + final ClickableSpan span = getSpanForOffset(virtualViewId); + if (span != null) { + event.setContentDescription(getTextForSpan(span)); + } else { + Log.e(TAG, "LinkSpan is null for offset: " + virtualViewId); + event.setContentDescription(mView.getText()); + } + } + + @Override + protected void onPopulateNodeForVirtualView( + int virtualViewId, AccessibilityNodeInfoCompat info) { + final ClickableSpan span = getSpanForOffset(virtualViewId); + if (span != null) { + info.setContentDescription(getTextForSpan(span)); + } else { + Log.e(TAG, "LinkSpan is null for offset: " + virtualViewId); + info.setContentDescription(mView.getText()); + } + info.setFocusable(true); + info.setClickable(true); + getBoundsForSpan(span, mTempRect); + if (mTempRect.isEmpty()) { + Log.e(TAG, "LinkSpan bounds is empty for: " + virtualViewId); + mTempRect.set(0, 0, 1, 1); + } + info.setBoundsInParent(mTempRect); + info.addAction(AccessibilityNodeInfoCompat.ACTION_CLICK); + } + + @Override + protected boolean onPerformActionForVirtualView( + int virtualViewId, int action, Bundle arguments) { + if (action == AccessibilityNodeInfoCompat.ACTION_CLICK) { + ClickableSpan span = getSpanForOffset(virtualViewId); + if (span != null) { + span.onClick(mView); + return true; + } else { + Log.e(TAG, "LinkSpan is null for offset: " + virtualViewId); + } + } + return false; + } + + private ClickableSpan getSpanForOffset(int offset) { + CharSequence text = mView.getText(); + if (text instanceof Spanned) { + Spanned spannedText = (Spanned) text; + ClickableSpan[] spans = spannedText.getSpans(offset, offset, ClickableSpan.class); + if (spans.length == 1) { + return spans[0]; + } + } + return null; + } + + private CharSequence getTextForSpan(ClickableSpan span) { + CharSequence text = mView.getText(); + if (text instanceof Spanned) { + Spanned spannedText = (Spanned) text; + return spannedText.subSequence( + spannedText.getSpanStart(span), spannedText.getSpanEnd(span)); + } + return text; + } + + // Find the bounds of a span. If it spans multiple lines, it will only return the bounds for the + // section on the first line. + private Rect getBoundsForSpan(ClickableSpan span, Rect outRect) { + CharSequence text = mView.getText(); + outRect.setEmpty(); + if (text instanceof Spanned) { + final Layout layout = mView.getLayout(); + if (layout != null) { + Spanned spannedText = (Spanned) text; + final int spanStart = spannedText.getSpanStart(span); + final int spanEnd = spannedText.getSpanEnd(span); + final float xStart = layout.getPrimaryHorizontal(spanStart); + final float xEnd = layout.getPrimaryHorizontal(spanEnd); + final int lineStart = layout.getLineForOffset(spanStart); + final int lineEnd = layout.getLineForOffset(spanEnd); + layout.getLineBounds(lineStart, outRect); + if (lineEnd == lineStart) { + // If the span is on a single line, adjust both the left and right bounds + // so outrect is exactly bounding the span. + outRect.left = (int) Math.min(xStart, xEnd); + outRect.right = (int) Math.max(xStart, xEnd); + } else { + // If the span wraps across multiple lines, only use the first line (as returned + // by layout.getLineBounds above), and adjust the "start" of outrect to where + // the span starts, leaving the "end" of outrect at the end of the line. + // ("start" being left for LTR, and right for RTL) + if (layout.getParagraphDirection(lineStart) == Layout.DIR_RIGHT_TO_LEFT) { + outRect.right = (int) xStart; + } else { + outRect.left = (int) xStart; + } + } + + // Offset for padding + outRect.offset(mView.getTotalPaddingLeft(), mView.getTotalPaddingTop()); + } + } + return outRect; + } + + // Compat implementation of TextView#getOffsetForPosition(). + + private static int getOffsetForPosition(TextView view, float x, float y) { + if (view.getLayout() == null) return -1; + final int line = getLineAtCoordinate(view, y); + return getOffsetAtCoordinate(view, line, x); + } + + private static float convertToLocalHorizontalCoordinate(TextView view, float x) { + x -= view.getTotalPaddingLeft(); + // Clamp the position to inside of the view. + x = Math.max(0.0f, x); + x = Math.min(view.getWidth() - view.getTotalPaddingRight() - 1, x); + x += view.getScrollX(); + return x; + } + + private static int getLineAtCoordinate(TextView view, float y) { + y -= view.getTotalPaddingTop(); + // Clamp the position to inside of the view. + y = Math.max(0.0f, y); + y = Math.min(view.getHeight() - view.getTotalPaddingBottom() - 1, y); + y += view.getScrollY(); + return view.getLayout().getLineForVertical((int) y); + } + + private static int getOffsetAtCoordinate(TextView view, int line, float x) { + x = convertToLocalHorizontalCoordinate(view, x); + return view.getLayout().getOffsetForHorizontal(line, x); + } +} \ No newline at end of file diff --git a/packages/SettingsLib/src/com/android/settingslib/widget/LinkTextView.java b/packages/SettingsLib/src/com/android/settingslib/widget/LinkTextView.java new file mode 100644 index 0000000000000..da865366668d4 --- /dev/null +++ b/packages/SettingsLib/src/com/android/settingslib/widget/LinkTextView.java @@ -0,0 +1,66 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.settingslib.widget; + +import android.content.Context; +import android.support.annotation.NonNull; +import android.support.v4.view.ViewCompat; +import android.text.Spanned; +import android.text.method.LinkMovementMethod; +import android.text.style.ClickableSpan; +import android.util.AttributeSet; +import android.view.MotionEvent; +import android.widget.TextView; +import com.android.settingslib.accessibility.LinkAccessibilityHelper; +/** + * Copied from setup wizard. This TextView performs two functions. The first is to make it so the + * link behaves properly and becomes clickable. The second is that it makes the link visible to + * accessibility services. + */ +public class LinkTextView extends TextView { + + private LinkAccessibilityHelper mAccessibilityHelper; + + public LinkTextView(Context context) { + this(context, null); + } + + public LinkTextView(Context context, AttributeSet attrs) { + super(context, attrs); + mAccessibilityHelper = new LinkAccessibilityHelper(this); + ViewCompat.setAccessibilityDelegate(this, mAccessibilityHelper); + } + + @Override + public void setText(CharSequence text, BufferType type) { + super.setText(text, type); + if (text instanceof Spanned) { + final ClickableSpan[] spans = + ((Spanned) text).getSpans(0, text.length(), ClickableSpan.class); + if (spans.length > 0) { + setMovementMethod(LinkMovementMethod.getInstance()); + } + } + } + + @Override + protected boolean dispatchHoverEvent(@NonNull MotionEvent event) { + if (mAccessibilityHelper.dispatchHoverEvent(event)) { + return true; + } + return super.dispatchHoverEvent(event); + } +} \ No newline at end of file