Add test for LinkAccessibilityHelper
- For the AccessibilityDelegateCompat methods, add tests that the delegation is done correctly. - Refactored LinkAccessibilityHelper to make the dependency direction clearer. Test: ./gradlew connectedAndroidTest test Change-Id: I6132c0820ee6de1b9cc71a2838bdf05a34d7d2af
This commit is contained in:
parent
1ae463059c
commit
b72f3fb459
|
@ -19,6 +19,8 @@ package com.android.setupwizardlib.util;
|
||||||
import android.graphics.Rect;
|
import android.graphics.Rect;
|
||||||
import android.os.Build;
|
import android.os.Build;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
|
import android.support.annotation.NonNull;
|
||||||
|
import android.support.annotation.VisibleForTesting;
|
||||||
import android.support.v4.view.AccessibilityDelegateCompat;
|
import android.support.v4.view.AccessibilityDelegateCompat;
|
||||||
import android.support.v4.view.accessibility.AccessibilityNodeInfoCompat;
|
import android.support.v4.view.accessibility.AccessibilityNodeInfoCompat;
|
||||||
import android.support.v4.view.accessibility.AccessibilityNodeProviderCompat;
|
import android.support.v4.view.accessibility.AccessibilityNodeProviderCompat;
|
||||||
|
@ -38,12 +40,11 @@ import java.util.List;
|
||||||
/**
|
/**
|
||||||
* An accessibility delegate that allows {@link android.text.style.ClickableSpan} to be focused and
|
* An accessibility delegate that allows {@link android.text.style.ClickableSpan} to be focused and
|
||||||
* clicked by accessibility services.
|
* clicked by accessibility services.
|
||||||
* <p>
|
|
||||||
* <strong>Note: </strong> From Android O on, there is native support for ClickableSpan
|
|
||||||
* accessibility, so this class is not needed (and indeed has no effect.)
|
|
||||||
* </p>
|
|
||||||
*
|
*
|
||||||
* <p />Sample usage:
|
* <p><strong>Note:</strong> This class is a no-op on Android O or above since there is native
|
||||||
|
* support for ClickableSpan accessibility.
|
||||||
|
*
|
||||||
|
* <p>Sample usage:
|
||||||
* <pre>
|
* <pre>
|
||||||
* LinkAccessibilityHelper mAccessibilityHelper;
|
* LinkAccessibilityHelper mAccessibilityHelper;
|
||||||
*
|
*
|
||||||
|
@ -68,294 +69,255 @@ public class LinkAccessibilityHelper extends AccessibilityDelegateCompat {
|
||||||
|
|
||||||
private static final String TAG = "LinkAccessibilityHelper";
|
private static final String TAG = "LinkAccessibilityHelper";
|
||||||
|
|
||||||
private final TextView mView;
|
private final AccessibilityDelegateCompat mDelegate;
|
||||||
private final Rect mTempRect = new Rect();
|
|
||||||
private final ExploreByTouchHelper mExploreByTouchHelper;
|
|
||||||
|
|
||||||
public LinkAccessibilityHelper(TextView view) {
|
public LinkAccessibilityHelper(TextView view) {
|
||||||
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.N_MR1) {
|
this(Build.VERSION.SDK_INT >= Build.VERSION_CODES.O
|
||||||
// Pre-O, we essentially extend ExploreByTouchHelper to expose a virtual view hierarchy
|
// Platform support was added in O. This helper will be no-op
|
||||||
mExploreByTouchHelper = new ExploreByTouchHelper(view) {
|
? new AccessibilityDelegateCompat()
|
||||||
@Override
|
// Pre-O, we extend ExploreByTouchHelper to expose a virtual view hierarchy
|
||||||
protected int getVirtualViewAt(float x, float y) {
|
: new PreOLinkAccessibilityHelper(view));
|
||||||
return LinkAccessibilityHelper.this.getVirtualViewAt(x, y);
|
}
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@VisibleForTesting
|
||||||
protected void getVisibleVirtualViews(List<Integer> virtualViewIds) {
|
LinkAccessibilityHelper(@NonNull AccessibilityDelegateCompat delegate) {
|
||||||
LinkAccessibilityHelper.this.getVisibleVirtualViews(virtualViewIds);
|
mDelegate = delegate;
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void onPopulateEventForVirtualView(int virtualViewId,
|
|
||||||
AccessibilityEvent event) {
|
|
||||||
LinkAccessibilityHelper
|
|
||||||
.this.onPopulateEventForVirtualView(virtualViewId, event);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void onPopulateNodeForVirtualView(int virtualViewId,
|
|
||||||
AccessibilityNodeInfoCompat infoCompat) {
|
|
||||||
LinkAccessibilityHelper
|
|
||||||
.this.onPopulateNodeForVirtualView(virtualViewId, infoCompat);
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected boolean onPerformActionForVirtualView(int virtualViewId, int action,
|
|
||||||
Bundle arguments) {
|
|
||||||
return LinkAccessibilityHelper.this
|
|
||||||
.onPerformActionForVirtualView(virtualViewId, action, arguments);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
} else {
|
|
||||||
mExploreByTouchHelper = null;
|
|
||||||
}
|
|
||||||
mView = view;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void sendAccessibilityEvent(View host, int eventType) {
|
public void sendAccessibilityEvent(View host, int eventType) {
|
||||||
if (mExploreByTouchHelper != null) {
|
mDelegate.sendAccessibilityEvent(host, eventType);
|
||||||
mExploreByTouchHelper.sendAccessibilityEvent(host, eventType);
|
|
||||||
} else {
|
|
||||||
super.sendAccessibilityEvent(host, eventType);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void sendAccessibilityEventUnchecked(View host, AccessibilityEvent event) {
|
public void sendAccessibilityEventUnchecked(View host, AccessibilityEvent event) {
|
||||||
if (mExploreByTouchHelper != null) {
|
mDelegate.sendAccessibilityEventUnchecked(host, event);
|
||||||
mExploreByTouchHelper.sendAccessibilityEventUnchecked(host, event);
|
|
||||||
} else {
|
|
||||||
super.sendAccessibilityEventUnchecked(host, event);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean dispatchPopulateAccessibilityEvent(View host, AccessibilityEvent event) {
|
public boolean dispatchPopulateAccessibilityEvent(View host, AccessibilityEvent event) {
|
||||||
return (mExploreByTouchHelper != null)
|
return mDelegate.dispatchPopulateAccessibilityEvent(host, event);
|
||||||
? mExploreByTouchHelper.dispatchPopulateAccessibilityEvent(host, event)
|
|
||||||
: super.dispatchPopulateAccessibilityEvent(host, event);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onPopulateAccessibilityEvent(View host, AccessibilityEvent event) {
|
public void onPopulateAccessibilityEvent(View host, AccessibilityEvent event) {
|
||||||
if (mExploreByTouchHelper != null) {
|
mDelegate.onPopulateAccessibilityEvent(host, event);
|
||||||
mExploreByTouchHelper.onPopulateAccessibilityEvent(host, event);
|
|
||||||
} else {
|
|
||||||
super.onPopulateAccessibilityEvent(host, event);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onInitializeAccessibilityEvent(View host, AccessibilityEvent event) {
|
public void onInitializeAccessibilityEvent(View host, AccessibilityEvent event) {
|
||||||
if (mExploreByTouchHelper != null) {
|
mDelegate.onInitializeAccessibilityEvent(host, event);
|
||||||
mExploreByTouchHelper.onInitializeAccessibilityEvent(host, event);
|
|
||||||
} else {
|
|
||||||
super.onInitializeAccessibilityEvent(host, event);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onInitializeAccessibilityNodeInfo(View host, AccessibilityNodeInfoCompat info) {
|
public void onInitializeAccessibilityNodeInfo(View host, AccessibilityNodeInfoCompat info) {
|
||||||
if (mExploreByTouchHelper != null) {
|
mDelegate.onInitializeAccessibilityNodeInfo(host, info);
|
||||||
mExploreByTouchHelper.onInitializeAccessibilityNodeInfo(host, info);
|
|
||||||
} else {
|
|
||||||
super.onInitializeAccessibilityNodeInfo(host, info);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean onRequestSendAccessibilityEvent(ViewGroup host, View child,
|
public boolean onRequestSendAccessibilityEvent(ViewGroup host, View child,
|
||||||
AccessibilityEvent event) {
|
AccessibilityEvent event) {
|
||||||
return (mExploreByTouchHelper != null)
|
return mDelegate.onRequestSendAccessibilityEvent(host, child, event);
|
||||||
? mExploreByTouchHelper.onRequestSendAccessibilityEvent(host, child, event)
|
|
||||||
: super.onRequestSendAccessibilityEvent(host, child, event);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public AccessibilityNodeProviderCompat getAccessibilityNodeProvider(View host) {
|
public AccessibilityNodeProviderCompat getAccessibilityNodeProvider(View host) {
|
||||||
return (mExploreByTouchHelper != null)
|
return mDelegate.getAccessibilityNodeProvider(host);
|
||||||
? mExploreByTouchHelper.getAccessibilityNodeProvider(host)
|
|
||||||
: super.getAccessibilityNodeProvider(host);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean performAccessibilityAction(View host, int action, Bundle args) {
|
public boolean performAccessibilityAction(View host, int action, Bundle args) {
|
||||||
return (mExploreByTouchHelper != null)
|
return mDelegate.performAccessibilityAction(host, action, args);
|
||||||
? mExploreByTouchHelper.performAccessibilityAction(host, action, args)
|
|
||||||
: super.performAccessibilityAction(host, action, args);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Delegated to {@link ExploreByTouchHelper}
|
* Dispatches hover event to the virtual view hierarchy. This method should be called in
|
||||||
|
* {@link View#dispatchHoverEvent(MotionEvent)}.
|
||||||
|
*
|
||||||
|
* @see ExploreByTouchHelper#dispatchHoverEvent(MotionEvent)
|
||||||
*/
|
*/
|
||||||
public final boolean dispatchHoverEvent(MotionEvent event) {
|
public final boolean dispatchHoverEvent(MotionEvent event) {
|
||||||
return (mExploreByTouchHelper != null) ? mExploreByTouchHelper.dispatchHoverEvent(event)
|
return mDelegate instanceof ExploreByTouchHelper
|
||||||
: false;
|
&& ((ExploreByTouchHelper) mDelegate).dispatchHoverEvent(event);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected int getVirtualViewAt(float x, float y) {
|
@VisibleForTesting
|
||||||
final CharSequence text = mView.getText();
|
static class PreOLinkAccessibilityHelper extends ExploreByTouchHelper {
|
||||||
if (text instanceof Spanned) {
|
|
||||||
final Spanned spannedText = (Spanned) text;
|
private final Rect mTempRect = new Rect();
|
||||||
final int offset = getOffsetForPosition(mView, x, y);
|
private final TextView mView;
|
||||||
ClickableSpan[] linkSpans = spannedText.getSpans(offset, offset, ClickableSpan.class);
|
|
||||||
if (linkSpans.length == 1) {
|
PreOLinkAccessibilityHelper(TextView view) {
|
||||||
ClickableSpan linkSpan = linkSpans[0];
|
super(view);
|
||||||
return spannedText.getSpanStart(linkSpan);
|
mView = view;
|
||||||
|
}
|
||||||
|
|
||||||
|
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 ExploreByTouchHelper.INVALID_ID;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void getVisibleVirtualViews(List<Integer> 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));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return ExploreByTouchHelper.INVALID_ID;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected void getVisibleVirtualViews(List<Integer> virtualViewIds) {
|
protected void onPopulateEventForVirtualView(int virtualViewId, AccessibilityEvent event) {
|
||||||
final CharSequence text = mView.getText();
|
final ClickableSpan span = getSpanForOffset(virtualViewId);
|
||||||
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));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected boolean onPerformActionForVirtualView(int virtualViewId, int action,
|
|
||||||
Bundle arguments) {
|
|
||||||
if (action == AccessibilityNodeInfoCompat.ACTION_CLICK) {
|
|
||||||
ClickableSpan span = getSpanForOffset(virtualViewId);
|
|
||||||
if (span != null) {
|
if (span != null) {
|
||||||
span.onClick(mView);
|
event.setContentDescription(getTextForSpan(span));
|
||||||
return true;
|
|
||||||
} else {
|
} else {
|
||||||
Log.e(TAG, "LinkSpan is null for offset: " + virtualViewId);
|
Log.e(TAG, "LinkSpan is null for offset: " + virtualViewId);
|
||||||
|
event.setContentDescription(mView.getText());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
private ClickableSpan getSpanForOffset(int offset) {
|
protected void onPopulateNodeForVirtualView(
|
||||||
CharSequence text = mView.getText();
|
int virtualViewId,
|
||||||
if (text instanceof Spanned) {
|
AccessibilityNodeInfoCompat info) {
|
||||||
Spanned spannedText = (Spanned) text;
|
final ClickableSpan span = getSpanForOffset(virtualViewId);
|
||||||
ClickableSpan[] spans = spannedText.getSpans(offset, offset, ClickableSpan.class);
|
if (span != null) {
|
||||||
if (spans.length == 1) {
|
info.setContentDescription(getTextForSpan(span));
|
||||||
return spans[0];
|
} 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);
|
||||||
}
|
}
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
private CharSequence getTextForSpan(ClickableSpan span) {
|
protected boolean onPerformActionForVirtualView(
|
||||||
CharSequence text = mView.getText();
|
int virtualViewId,
|
||||||
if (text instanceof Spanned) {
|
int action,
|
||||||
Spanned spannedText = (Spanned) text;
|
Bundle arguments) {
|
||||||
return spannedText.subSequence(spannedText.getSpanStart(span),
|
if (action == AccessibilityNodeInfoCompat.ACTION_CLICK) {
|
||||||
spannedText.getSpanEnd(span));
|
ClickableSpan span = getSpanForOffset(virtualViewId);
|
||||||
}
|
if (span != null) {
|
||||||
return text;
|
span.onClick(mView);
|
||||||
}
|
return true;
|
||||||
|
|
||||||
// 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 {
|
} else {
|
||||||
// If the span wraps across multiple lines, only use the first line (as returned
|
Log.e(TAG, "LinkSpan is null for offset: " + virtualViewId);
|
||||||
// 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 false;
|
||||||
}
|
}
|
||||||
return outRect;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Compat implementation of TextView#getOffsetForPosition().
|
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 static int getOffsetForPosition(TextView view, float x, float y) {
|
private CharSequence getTextForSpan(ClickableSpan span) {
|
||||||
if (view.getLayout() == null) return -1;
|
CharSequence text = mView.getText();
|
||||||
final int line = getLineAtCoordinate(view, y);
|
if (text instanceof Spanned) {
|
||||||
return getOffsetAtCoordinate(view, line, x);
|
Spanned spannedText = (Spanned) text;
|
||||||
}
|
return spannedText.subSequence(
|
||||||
|
spannedText.getSpanStart(span),
|
||||||
|
spannedText.getSpanEnd(span));
|
||||||
|
}
|
||||||
|
return text;
|
||||||
|
}
|
||||||
|
|
||||||
private static float convertToLocalHorizontalCoordinate(TextView view, float x) {
|
// Find the bounds of a span. If it spans multiple lines, it will only return the bounds for
|
||||||
x -= view.getTotalPaddingLeft();
|
// the section on the first line.
|
||||||
// Clamp the position to inside of the view.
|
private Rect getBoundsForSpan(ClickableSpan span, Rect outRect) {
|
||||||
x = Math.max(0.0f, x);
|
CharSequence text = mView.getText();
|
||||||
x = Math.min(view.getWidth() - view.getTotalPaddingRight() - 1, x);
|
outRect.setEmpty();
|
||||||
x += view.getScrollX();
|
if (text instanceof Spanned) {
|
||||||
return x;
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private static int getLineAtCoordinate(TextView view, float y) {
|
// Offset for padding
|
||||||
y -= view.getTotalPaddingTop();
|
outRect.offset(mView.getTotalPaddingLeft(), mView.getTotalPaddingTop());
|
||||||
// Clamp the position to inside of the view.
|
}
|
||||||
y = Math.max(0.0f, y);
|
}
|
||||||
y = Math.min(view.getHeight() - view.getTotalPaddingBottom() - 1, y);
|
return outRect;
|
||||||
y += view.getScrollY();
|
}
|
||||||
return view.getLayout().getLineForVertical((int) y);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static int getOffsetAtCoordinate(TextView view, int line, float x) {
|
// Compat implementation of TextView#getOffsetForPosition().
|
||||||
x = convertToLocalHorizontalCoordinate(view, x);
|
|
||||||
return view.getLayout().getOffsetForHorizontal(line, x);
|
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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -14,29 +14,35 @@
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package com.android.setupwizardlib.test;
|
package com.android.setupwizardlib.util;
|
||||||
|
|
||||||
import static org.junit.Assert.assertEquals;
|
import static org.junit.Assert.assertEquals;
|
||||||
import static org.junit.Assert.assertTrue;
|
import static org.junit.Assert.assertTrue;
|
||||||
|
import static org.mockito.Matchers.eq;
|
||||||
|
import static org.mockito.Matchers.same;
|
||||||
|
import static org.mockito.Mockito.mock;
|
||||||
|
import static org.mockito.Mockito.verify;
|
||||||
|
|
||||||
import android.graphics.Rect;
|
import android.graphics.Rect;
|
||||||
import android.os.Build;
|
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.support.test.InstrumentationRegistry;
|
import android.support.test.InstrumentationRegistry;
|
||||||
import android.support.test.filters.SmallTest;
|
import android.support.test.filters.SmallTest;
|
||||||
import android.support.test.runner.AndroidJUnit4;
|
import android.support.test.runner.AndroidJUnit4;
|
||||||
import android.support.v4.text.BidiFormatter;
|
import android.support.v4.text.BidiFormatter;
|
||||||
import android.support.v4.view.accessibility.AccessibilityNodeInfoCompat;
|
import android.support.v4.view.accessibility.AccessibilityNodeInfoCompat;
|
||||||
|
import android.support.v4.view.accessibility.AccessibilityNodeInfoCompat.AccessibilityActionCompat;
|
||||||
import android.support.v4.widget.ExploreByTouchHelper;
|
import android.support.v4.widget.ExploreByTouchHelper;
|
||||||
import android.text.SpannableStringBuilder;
|
import android.text.SpannableStringBuilder;
|
||||||
import android.util.DisplayMetrics;
|
import android.util.DisplayMetrics;
|
||||||
import android.util.TypedValue;
|
import android.util.TypedValue;
|
||||||
|
import android.view.MotionEvent;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
import android.view.accessibility.AccessibilityEvent;
|
import android.view.accessibility.AccessibilityEvent;
|
||||||
|
import android.widget.FrameLayout;
|
||||||
import android.widget.TextView;
|
import android.widget.TextView;
|
||||||
|
|
||||||
import com.android.setupwizardlib.span.LinkSpan;
|
import com.android.setupwizardlib.span.LinkSpan;
|
||||||
import com.android.setupwizardlib.util.LinkAccessibilityHelper;
|
import com.android.setupwizardlib.util.LinkAccessibilityHelper.PreOLinkAccessibilityHelper;
|
||||||
|
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
import org.junit.runner.RunWith;
|
import org.junit.runner.RunWith;
|
||||||
|
@ -52,13 +58,12 @@ public class LinkAccessibilityHelperTest {
|
||||||
private static final LinkSpan LINK_SPAN = new LinkSpan("foobar");
|
private static final LinkSpan LINK_SPAN = new LinkSpan("foobar");
|
||||||
|
|
||||||
private TextView mTextView;
|
private TextView mTextView;
|
||||||
private TestLinkAccessibilityHelper mHelper;
|
private TestPreOLinkAccessibilityHelper mHelper;
|
||||||
|
|
||||||
private DisplayMetrics mDisplayMetrics;
|
private DisplayMetrics mDisplayMetrics;
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testGetVirtualViewAt() {
|
public void testGetVirtualViewAt() {
|
||||||
if (Build.VERSION.SDK_INT > Build.VERSION_CODES.N_MR1) return;
|
|
||||||
initTextView();
|
initTextView();
|
||||||
final int virtualViewId = mHelper.getVirtualViewAt(dp2Px(15), dp2Px(10));
|
final int virtualViewId = mHelper.getVirtualViewAt(dp2Px(15), dp2Px(10));
|
||||||
assertEquals("Virtual view ID should be 1", 1, virtualViewId);
|
assertEquals("Virtual view ID should be 1", 1, virtualViewId);
|
||||||
|
@ -66,7 +71,6 @@ public class LinkAccessibilityHelperTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testGetVirtualViewAtHost() {
|
public void testGetVirtualViewAtHost() {
|
||||||
if (Build.VERSION.SDK_INT > Build.VERSION_CODES.N_MR1) return;
|
|
||||||
initTextView();
|
initTextView();
|
||||||
final int virtualViewId = mHelper.getVirtualViewAt(dp2Px(100), dp2Px(100));
|
final int virtualViewId = mHelper.getVirtualViewAt(dp2Px(100), dp2Px(100));
|
||||||
assertEquals("Virtual view ID should be INVALID_ID",
|
assertEquals("Virtual view ID should be INVALID_ID",
|
||||||
|
@ -75,7 +79,6 @@ public class LinkAccessibilityHelperTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testGetVisibleVirtualViews() {
|
public void testGetVisibleVirtualViews() {
|
||||||
if (Build.VERSION.SDK_INT > Build.VERSION_CODES.N_MR1) return;
|
|
||||||
initTextView();
|
initTextView();
|
||||||
List<Integer> virtualViewIds = new ArrayList<>();
|
List<Integer> virtualViewIds = new ArrayList<>();
|
||||||
mHelper.getVisibleVirtualViews(virtualViewIds);
|
mHelper.getVisibleVirtualViews(virtualViewIds);
|
||||||
|
@ -86,7 +89,6 @@ public class LinkAccessibilityHelperTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testOnPopulateEventForVirtualView() {
|
public void testOnPopulateEventForVirtualView() {
|
||||||
if (Build.VERSION.SDK_INT > Build.VERSION_CODES.N_MR1) return;
|
|
||||||
initTextView();
|
initTextView();
|
||||||
AccessibilityEvent event = AccessibilityEvent.obtain();
|
AccessibilityEvent event = AccessibilityEvent.obtain();
|
||||||
mHelper.onPopulateEventForVirtualView(1, event);
|
mHelper.onPopulateEventForVirtualView(1, event);
|
||||||
|
@ -100,7 +102,6 @@ public class LinkAccessibilityHelperTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testOnPopulateEventForVirtualViewHost() {
|
public void testOnPopulateEventForVirtualViewHost() {
|
||||||
if (Build.VERSION.SDK_INT > Build.VERSION_CODES.N_MR1) return;
|
|
||||||
initTextView();
|
initTextView();
|
||||||
AccessibilityEvent event = AccessibilityEvent.obtain();
|
AccessibilityEvent event = AccessibilityEvent.obtain();
|
||||||
mHelper.onPopulateEventForVirtualView(ExploreByTouchHelper.INVALID_ID, event);
|
mHelper.onPopulateEventForVirtualView(ExploreByTouchHelper.INVALID_ID, event);
|
||||||
|
@ -113,7 +114,6 @@ public class LinkAccessibilityHelperTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testOnPopulateNodeForVirtualView() {
|
public void testOnPopulateNodeForVirtualView() {
|
||||||
if (Build.VERSION.SDK_INT > Build.VERSION_CODES.N_MR1) return;
|
|
||||||
initTextView();
|
initTextView();
|
||||||
AccessibilityNodeInfoCompat info = AccessibilityNodeInfoCompat.obtain();
|
AccessibilityNodeInfoCompat info = AccessibilityNodeInfoCompat.obtain();
|
||||||
mHelper.onPopulateNodeForVirtualView(1, info);
|
mHelper.onPopulateNodeForVirtualView(1, info);
|
||||||
|
@ -132,7 +132,6 @@ public class LinkAccessibilityHelperTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testNullLayout() {
|
public void testNullLayout() {
|
||||||
if (Build.VERSION.SDK_INT > Build.VERSION_CODES.N_MR1) return;
|
|
||||||
initTextView();
|
initTextView();
|
||||||
// Setting the padding will cause the layout to be null-ed out.
|
// Setting the padding will cause the layout to be null-ed out.
|
||||||
mTextView.setPadding(1, 1, 1, 1);
|
mTextView.setPadding(1, 1, 1, 1);
|
||||||
|
@ -150,7 +149,6 @@ public class LinkAccessibilityHelperTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testRtlLayout() {
|
public void testRtlLayout() {
|
||||||
if (Build.VERSION.SDK_INT > Build.VERSION_CODES.N_MR1) return;
|
|
||||||
SpannableStringBuilder ssb = new SpannableStringBuilder("מכונה בתרגום");
|
SpannableStringBuilder ssb = new SpannableStringBuilder("מכונה בתרגום");
|
||||||
ssb.setSpan(LINK_SPAN, 1, 2, 0 /* flags */);
|
ssb.setSpan(LINK_SPAN, 1, 2, 0 /* flags */);
|
||||||
initTextView(ssb);
|
initTextView(ssb);
|
||||||
|
@ -170,7 +168,6 @@ public class LinkAccessibilityHelperTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testMultilineLink() {
|
public void testMultilineLink() {
|
||||||
if (Build.VERSION.SDK_INT > Build.VERSION_CODES.N_MR1) return;
|
|
||||||
SpannableStringBuilder ssb = new SpannableStringBuilder(
|
SpannableStringBuilder ssb = new SpannableStringBuilder(
|
||||||
"Lorem ipsum dolor sit amet, consectetur adipiscing elit. "
|
"Lorem ipsum dolor sit amet, consectetur adipiscing elit. "
|
||||||
+ "Praesent accumsan efficitur eros eu porttitor.");
|
+ "Praesent accumsan efficitur eros eu porttitor.");
|
||||||
|
@ -192,7 +189,6 @@ public class LinkAccessibilityHelperTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testRtlMultilineLink() {
|
public void testRtlMultilineLink() {
|
||||||
if (Build.VERSION.SDK_INT > Build.VERSION_CODES.N_MR1) return;
|
|
||||||
String iwLoremIpsum = "אחר על רביעי אקטואליה. לוח דת אחרות המקובל רומנית, מיזמים מועמדים "
|
String iwLoremIpsum = "אחר על רביעי אקטואליה. לוח דת אחרות המקובל רומנית, מיזמים מועמדים "
|
||||||
+ "האנציקלופדיה בה צ'ט. מתן מה שנורו לערוך ייִדיש, בקר או החול אנתרופולוגיה, עוד "
|
+ "האנציקלופדיה בה צ'ט. מתן מה שנורו לערוך ייִדיש, בקר או החול אנתרופולוגיה, עוד "
|
||||||
+ "דפים המחשב מיזמים ב.";
|
+ "דפים המחשב מיזמים ב.";
|
||||||
|
@ -216,7 +212,6 @@ public class LinkAccessibilityHelperTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testBidiMultilineLink() {
|
public void testBidiMultilineLink() {
|
||||||
if (Build.VERSION.SDK_INT > Build.VERSION_CODES.N_MR1) return;
|
|
||||||
String iwLoremIpsum = "אחר על רביעי אקטואליה. לוח דת אחרות המקובל רומנית, מיזמים מועמדים "
|
String iwLoremIpsum = "אחר על רביעי אקטואליה. לוח דת אחרות המקובל רומנית, מיזמים מועמדים "
|
||||||
+ "האנציקלופדיה בה צ'ט. מתן מה שנורו לערוך ייִדיש, בקר או החול אנתרופולוגיה, עוד "
|
+ "האנציקלופדיה בה צ'ט. מתן מה שנורו לערוך ייִדיש, בקר או החול אנתרופולוגיה, עוד "
|
||||||
+ "דפים המחשב מיזמים ב.";
|
+ "דפים המחשב מיזמים ב.";
|
||||||
|
@ -243,6 +238,70 @@ public class LinkAccessibilityHelperTest {
|
||||||
info.recycle();
|
info.recycle();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testMethodDelegation() {
|
||||||
|
initTextView();
|
||||||
|
ExploreByTouchHelper delegate = mock(TestPreOLinkAccessibilityHelper.class);
|
||||||
|
LinkAccessibilityHelper helper = new LinkAccessibilityHelper(delegate);
|
||||||
|
|
||||||
|
AccessibilityEvent accessibilityEvent =
|
||||||
|
AccessibilityEvent.obtain(AccessibilityEvent.TYPE_VIEW_CLICKED);
|
||||||
|
|
||||||
|
helper.sendAccessibilityEvent(mTextView, AccessibilityEvent.TYPE_VIEW_CLICKED);
|
||||||
|
verify(delegate).sendAccessibilityEvent(
|
||||||
|
same(mTextView),
|
||||||
|
eq(AccessibilityEvent.TYPE_VIEW_CLICKED));
|
||||||
|
|
||||||
|
helper.sendAccessibilityEventUnchecked(mTextView, accessibilityEvent);
|
||||||
|
verify(delegate).sendAccessibilityEventUnchecked(same(mTextView), same(accessibilityEvent));
|
||||||
|
|
||||||
|
helper.performAccessibilityAction(
|
||||||
|
mTextView,
|
||||||
|
AccessibilityActionCompat.ACTION_CLICK.getId(),
|
||||||
|
Bundle.EMPTY);
|
||||||
|
verify(delegate).performAccessibilityAction(
|
||||||
|
same(mTextView),
|
||||||
|
eq(AccessibilityActionCompat.ACTION_CLICK.getId()),
|
||||||
|
eq(Bundle.EMPTY));
|
||||||
|
|
||||||
|
helper.dispatchPopulateAccessibilityEvent(
|
||||||
|
mTextView,
|
||||||
|
accessibilityEvent);
|
||||||
|
verify(delegate).dispatchPopulateAccessibilityEvent(
|
||||||
|
same(mTextView),
|
||||||
|
same(accessibilityEvent));
|
||||||
|
|
||||||
|
MotionEvent motionEvent = MotionEvent.obtain(0, 0, 0, 0, 0, 0);
|
||||||
|
helper.dispatchHoverEvent(motionEvent);
|
||||||
|
verify(delegate).dispatchHoverEvent(eq(motionEvent));
|
||||||
|
|
||||||
|
helper.getAccessibilityNodeProvider(mTextView);
|
||||||
|
verify(delegate).getAccessibilityNodeProvider(same(mTextView));
|
||||||
|
|
||||||
|
helper.onInitializeAccessibilityEvent(mTextView, accessibilityEvent);
|
||||||
|
verify(delegate).onInitializeAccessibilityEvent(
|
||||||
|
same(mTextView),
|
||||||
|
eq(accessibilityEvent));
|
||||||
|
|
||||||
|
AccessibilityNodeInfoCompat accessibilityNodeInfo = AccessibilityNodeInfoCompat.obtain();
|
||||||
|
helper.onInitializeAccessibilityNodeInfo(mTextView, accessibilityNodeInfo);
|
||||||
|
verify(delegate).onInitializeAccessibilityNodeInfo(
|
||||||
|
same(mTextView),
|
||||||
|
same(accessibilityNodeInfo));
|
||||||
|
|
||||||
|
helper.onPopulateAccessibilityEvent(mTextView, accessibilityEvent);
|
||||||
|
verify(delegate).onPopulateAccessibilityEvent(
|
||||||
|
same(mTextView),
|
||||||
|
same(accessibilityEvent));
|
||||||
|
|
||||||
|
FrameLayout parent = new FrameLayout(InstrumentationRegistry.getTargetContext());
|
||||||
|
helper.onRequestSendAccessibilityEvent(parent, mTextView, accessibilityEvent);
|
||||||
|
verify(delegate).onRequestSendAccessibilityEvent(
|
||||||
|
same(parent),
|
||||||
|
same(mTextView),
|
||||||
|
same(accessibilityEvent));
|
||||||
|
}
|
||||||
|
|
||||||
private void initTextView() {
|
private void initTextView() {
|
||||||
SpannableStringBuilder ssb = new SpannableStringBuilder("Hello world");
|
SpannableStringBuilder ssb = new SpannableStringBuilder("Hello world");
|
||||||
ssb.setSpan(LINK_SPAN, 1, 2, 0 /* flags */);
|
ssb.setSpan(LINK_SPAN, 1, 2, 0 /* flags */);
|
||||||
|
@ -254,7 +313,7 @@ public class LinkAccessibilityHelperTest {
|
||||||
mTextView.setSingleLine(false);
|
mTextView.setSingleLine(false);
|
||||||
mTextView.setText(text);
|
mTextView.setText(text);
|
||||||
mTextView.setTextSize(TypedValue.COMPLEX_UNIT_DIP, 15);
|
mTextView.setTextSize(TypedValue.COMPLEX_UNIT_DIP, 15);
|
||||||
mHelper = new TestLinkAccessibilityHelper(mTextView);
|
mHelper = new TestPreOLinkAccessibilityHelper(mTextView);
|
||||||
|
|
||||||
int measureExactly500dp = View.MeasureSpec.makeMeasureSpec(dp2Px(500),
|
int measureExactly500dp = View.MeasureSpec.makeMeasureSpec(dp2Px(500),
|
||||||
View.MeasureSpec.EXACTLY);
|
View.MeasureSpec.EXACTLY);
|
||||||
|
@ -270,9 +329,9 @@ public class LinkAccessibilityHelperTest {
|
||||||
return (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dp, mDisplayMetrics);
|
return (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dp, mDisplayMetrics);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static class TestLinkAccessibilityHelper extends LinkAccessibilityHelper {
|
public static class TestPreOLinkAccessibilityHelper extends PreOLinkAccessibilityHelper {
|
||||||
|
|
||||||
TestLinkAccessibilityHelper(TextView view) {
|
TestPreOLinkAccessibilityHelper(TextView view) {
|
||||||
super(view);
|
super(view);
|
||||||
}
|
}
|
||||||
|
|
|
@ -16,5 +16,5 @@
|
||||||
|
|
||||||
apply from: 'standalone-rules.gradle'
|
apply from: 'standalone-rules.gradle'
|
||||||
|
|
||||||
android.compileSdkVersion 25
|
android.compileSdkVersion 26
|
||||||
android.buildToolsVersion '25.0.0'
|
android.buildToolsVersion '26.0.0'
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
// Set the default SDK and build tools version for all apps
|
// Set the default SDK and build tools version for all apps
|
||||||
compileSdkVersion 25
|
compileSdkVersion 26
|
||||||
buildToolsVersion = '25.0.0'
|
buildToolsVersion = '26.0.0'
|
||||||
|
|
||||||
// enable Java7
|
// enable Java7
|
||||||
compileOptions.sourceCompatibility JavaVersion.VERSION_1_7
|
compileOptions.sourceCompatibility JavaVersion.VERSION_1_7
|
||||||
|
|
Loading…
Reference in a new issue