blob: 5c76f3a888cb1d611c8df8a1a864e0d28d9d234a [file] [log] [blame]
// Copyright 2014 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
package org.chromium.ui.widget;
import android.content.Context;
import android.os.Bundle;
import android.text.Layout;
import android.text.SpannableString;
import android.text.style.ClickableSpan;
import android.util.AttributeSet;
import android.view.Menu;
import android.view.MenuItem;
import android.view.MotionEvent;
import android.view.View;
import android.view.accessibility.AccessibilityManager;
import android.view.accessibility.AccessibilityNodeInfo;
import android.widget.PopupMenu;
import android.widget.TextView;
/**
* ClickableSpan isn't accessible by default, so we create a subclass
* of TextView that tries to handle the case where a user clicks on a view
* and not directly on one of the clickable spans. We do nothing if it's a
* touch event directly on a ClickableSpan. Otherwise if there's only one
* ClickableSpan, we activate it. If there's more than one, we pop up a
* PopupMenu to disambiguate.
*/
public class TextViewWithClickableSpans extends TextView {
private AccessibilityManager mAccessibilityManager;
public TextViewWithClickableSpans(Context context) {
super(context);
init();
}
public TextViewWithClickableSpans(Context context, AttributeSet attrs) {
super(context, attrs);
init();
}
public TextViewWithClickableSpans(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init();
}
private void init() {
mAccessibilityManager = (AccessibilityManager)
getContext().getSystemService(Context.ACCESSIBILITY_SERVICE);
setOnLongClickListener(new View.OnLongClickListener() {
@Override
public boolean onLongClick(View v) {
if (!mAccessibilityManager.isTouchExplorationEnabled()) {
return false;
}
openDisambiguationMenu();
return true;
}
});
}
@Override
public boolean performAccessibilityAction(int action, Bundle arguments) {
// BrailleBack will generate an accessibility click event directly
// on this view, make sure we handle that correctly.
if (action == AccessibilityNodeInfo.ACTION_CLICK) {
handleAccessibilityClick();
return true;
}
return super.performAccessibilityAction(action, arguments);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
boolean superResult = super.onTouchEvent(event);
if (event.getAction() != MotionEvent.ACTION_UP
&& mAccessibilityManager.isTouchExplorationEnabled()
&& !touchIntersectsAnyClickableSpans(event)) {
handleAccessibilityClick();
return true;
}
return superResult;
}
private boolean touchIntersectsAnyClickableSpans(MotionEvent event) {
// This logic is borrowed from android.text.method.LinkMovementMethod.
//
// ClickableSpan doesn't stop propagation of the event in its click handler,
// so we should only try to simplify clicking on a clickable span if the touch event
// isn't already over a clickable span.
CharSequence text = getText();
if (!(text instanceof SpannableString)) return false;
SpannableString spannable = (SpannableString) text;
int x = (int) event.getX();
int y = (int) event.getY();
x -= getTotalPaddingLeft();
y -= getTotalPaddingTop();
x += getScrollX();
y += getScrollY();
Layout layout = getLayout();
int line = layout.getLineForVertical(y);
int off = layout.getOffsetForHorizontal(line, x);
ClickableSpan[] clickableSpans =
spannable.getSpans(off, off, ClickableSpan.class);
return clickableSpans.length > 0;
}
private ClickableSpan[] getClickableSpans() {
CharSequence text = getText();
if (!(text instanceof SpannableString)) return null;
SpannableString spannable = (SpannableString) text;
return spannable.getSpans(0, spannable.length(), ClickableSpan.class);
}
private void handleAccessibilityClick() {
ClickableSpan[] clickableSpans = getClickableSpans();
if (clickableSpans == null || clickableSpans.length == 0) {
return;
} else if (clickableSpans.length == 1) {
clickableSpans[0].onClick(this);
} else {
openDisambiguationMenu();
}
}
private void openDisambiguationMenu() {
ClickableSpan[] clickableSpans = getClickableSpans();
if (clickableSpans == null || clickableSpans.length == 0)
return;
SpannableString spannable = (SpannableString) getText();
PopupMenu popup = new PopupMenu(getContext(), this);
Menu menu = popup.getMenu();
for (final ClickableSpan clickableSpan : clickableSpans) {
CharSequence itemText = spannable.subSequence(
spannable.getSpanStart(clickableSpan),
spannable.getSpanEnd(clickableSpan));
MenuItem menuItem = menu.add(itemText);
menuItem.setOnMenuItemClickListener(new MenuItem.OnMenuItemClickListener() {
@Override
public boolean onMenuItemClick(MenuItem menuItem) {
clickableSpan.onClick(TextViewWithClickableSpans.this);
return true;
}
});
}
popup.show();
}
}