294 lines
9.5 KiB
Java
294 lines
9.5 KiB
Java
![]() |
/*
|
||
|
* Copyright (C) 2006 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 android.text.method;
|
||
|
|
||
|
import android.compat.annotation.UnsupportedAppUsage;
|
||
|
import android.os.Build;
|
||
|
import android.text.Layout;
|
||
|
import android.text.NoCopySpan;
|
||
|
import android.text.Selection;
|
||
|
import android.text.Spannable;
|
||
|
import android.text.style.ClickableSpan;
|
||
|
import android.view.KeyEvent;
|
||
|
import android.view.MotionEvent;
|
||
|
import android.view.View;
|
||
|
import android.view.textclassifier.TextLinks.TextLinkSpan;
|
||
|
import android.widget.TextView;
|
||
|
|
||
|
/**
|
||
|
* A movement method that traverses links in the text buffer and scrolls if necessary.
|
||
|
* Supports clicking on links with DPad Center or Enter.
|
||
|
*/
|
||
|
public class LinkMovementMethod extends ScrollingMovementMethod {
|
||
|
private static final int CLICK = 1;
|
||
|
private static final int UP = 2;
|
||
|
private static final int DOWN = 3;
|
||
|
|
||
|
private static final int HIDE_FLOATING_TOOLBAR_DELAY_MS = 200;
|
||
|
|
||
|
@Override
|
||
|
public boolean canSelectArbitrarily() {
|
||
|
return true;
|
||
|
}
|
||
|
|
||
|
@Override
|
||
|
protected boolean handleMovementKey(TextView widget, Spannable buffer, int keyCode,
|
||
|
int movementMetaState, KeyEvent event) {
|
||
|
switch (keyCode) {
|
||
|
case KeyEvent.KEYCODE_DPAD_CENTER:
|
||
|
case KeyEvent.KEYCODE_ENTER:
|
||
|
if (KeyEvent.metaStateHasNoModifiers(movementMetaState)) {
|
||
|
if (event.getAction() == KeyEvent.ACTION_DOWN &&
|
||
|
event.getRepeatCount() == 0 && action(CLICK, widget, buffer)) {
|
||
|
return true;
|
||
|
}
|
||
|
}
|
||
|
break;
|
||
|
}
|
||
|
return super.handleMovementKey(widget, buffer, keyCode, movementMetaState, event);
|
||
|
}
|
||
|
|
||
|
@Override
|
||
|
protected boolean up(TextView widget, Spannable buffer) {
|
||
|
if (action(UP, widget, buffer)) {
|
||
|
return true;
|
||
|
}
|
||
|
|
||
|
return super.up(widget, buffer);
|
||
|
}
|
||
|
|
||
|
@Override
|
||
|
protected boolean down(TextView widget, Spannable buffer) {
|
||
|
if (action(DOWN, widget, buffer)) {
|
||
|
return true;
|
||
|
}
|
||
|
|
||
|
return super.down(widget, buffer);
|
||
|
}
|
||
|
|
||
|
@Override
|
||
|
protected boolean left(TextView widget, Spannable buffer) {
|
||
|
if (action(UP, widget, buffer)) {
|
||
|
return true;
|
||
|
}
|
||
|
|
||
|
return super.left(widget, buffer);
|
||
|
}
|
||
|
|
||
|
@Override
|
||
|
protected boolean right(TextView widget, Spannable buffer) {
|
||
|
if (action(DOWN, widget, buffer)) {
|
||
|
return true;
|
||
|
}
|
||
|
|
||
|
return super.right(widget, buffer);
|
||
|
}
|
||
|
|
||
|
private boolean action(int what, TextView widget, Spannable buffer) {
|
||
|
Layout layout = widget.getLayout();
|
||
|
if (widget.isOffsetMappingAvailable()) {
|
||
|
// The text in the layout is transformed and has OffsetMapping, don't do anything.
|
||
|
return false;
|
||
|
}
|
||
|
|
||
|
int padding = widget.getTotalPaddingTop() +
|
||
|
widget.getTotalPaddingBottom();
|
||
|
int areaTop = widget.getScrollY();
|
||
|
int areaBot = areaTop + widget.getHeight() - padding;
|
||
|
|
||
|
int lineTop = layout.getLineForVertical(areaTop);
|
||
|
int lineBot = layout.getLineForVertical(areaBot);
|
||
|
|
||
|
int first = layout.getLineStart(lineTop);
|
||
|
int last = layout.getLineEnd(lineBot);
|
||
|
|
||
|
ClickableSpan[] candidates = buffer.getSpans(first, last, ClickableSpan.class);
|
||
|
|
||
|
int a = Selection.getSelectionStart(buffer);
|
||
|
int b = Selection.getSelectionEnd(buffer);
|
||
|
|
||
|
int selStart = Math.min(a, b);
|
||
|
int selEnd = Math.max(a, b);
|
||
|
|
||
|
if (selStart < 0) {
|
||
|
if (buffer.getSpanStart(FROM_BELOW) >= 0) {
|
||
|
selStart = selEnd = buffer.length();
|
||
|
}
|
||
|
}
|
||
|
|
||
|
if (selStart > last)
|
||
|
selStart = selEnd = Integer.MAX_VALUE;
|
||
|
if (selEnd < first)
|
||
|
selStart = selEnd = -1;
|
||
|
|
||
|
switch (what) {
|
||
|
case CLICK:
|
||
|
if (selStart == selEnd) {
|
||
|
return false;
|
||
|
}
|
||
|
|
||
|
ClickableSpan[] links = buffer.getSpans(selStart, selEnd, ClickableSpan.class);
|
||
|
|
||
|
if (links.length != 1) {
|
||
|
return false;
|
||
|
}
|
||
|
|
||
|
ClickableSpan link = links[0];
|
||
|
if (link instanceof TextLinkSpan) {
|
||
|
((TextLinkSpan) link).onClick(widget, TextLinkSpan.INVOCATION_METHOD_KEYBOARD);
|
||
|
} else {
|
||
|
link.onClick(widget);
|
||
|
}
|
||
|
break;
|
||
|
|
||
|
case UP:
|
||
|
int bestStart, bestEnd;
|
||
|
|
||
|
bestStart = -1;
|
||
|
bestEnd = -1;
|
||
|
|
||
|
for (int i = 0; i < candidates.length; i++) {
|
||
|
int end = buffer.getSpanEnd(candidates[i]);
|
||
|
|
||
|
if (end < selEnd || selStart == selEnd) {
|
||
|
if (end > bestEnd) {
|
||
|
bestStart = buffer.getSpanStart(candidates[i]);
|
||
|
bestEnd = end;
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
if (bestStart >= 0) {
|
||
|
Selection.setSelection(buffer, bestEnd, bestStart);
|
||
|
return true;
|
||
|
}
|
||
|
|
||
|
break;
|
||
|
|
||
|
case DOWN:
|
||
|
bestStart = Integer.MAX_VALUE;
|
||
|
bestEnd = Integer.MAX_VALUE;
|
||
|
|
||
|
for (int i = 0; i < candidates.length; i++) {
|
||
|
int start = buffer.getSpanStart(candidates[i]);
|
||
|
|
||
|
if (start > selStart || selStart == selEnd) {
|
||
|
if (start < bestStart) {
|
||
|
bestStart = start;
|
||
|
bestEnd = buffer.getSpanEnd(candidates[i]);
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
if (bestEnd < Integer.MAX_VALUE) {
|
||
|
Selection.setSelection(buffer, bestStart, bestEnd);
|
||
|
return true;
|
||
|
}
|
||
|
|
||
|
break;
|
||
|
}
|
||
|
|
||
|
return false;
|
||
|
}
|
||
|
|
||
|
@Override
|
||
|
public boolean onTouchEvent(TextView widget, Spannable buffer,
|
||
|
MotionEvent event) {
|
||
|
int action = event.getAction();
|
||
|
|
||
|
if (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_DOWN) {
|
||
|
int x = (int) event.getX();
|
||
|
int y = (int) event.getY();
|
||
|
|
||
|
x -= widget.getTotalPaddingLeft();
|
||
|
y -= widget.getTotalPaddingTop();
|
||
|
|
||
|
x += widget.getScrollX();
|
||
|
y += widget.getScrollY();
|
||
|
|
||
|
Layout layout = widget.getLayout();
|
||
|
ClickableSpan[] links;
|
||
|
if (y < 0 || y > layout.getHeight()) {
|
||
|
links = null;
|
||
|
} else {
|
||
|
int line = layout.getLineForVertical(y);
|
||
|
if (x < layout.getLineLeft(line) || x > layout.getLineRight(line)) {
|
||
|
links = null;
|
||
|
} else {
|
||
|
int off = layout.getOffsetForHorizontal(line, x);
|
||
|
links = buffer.getSpans(off, off, ClickableSpan.class);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
if (links != null && links.length != 0) {
|
||
|
ClickableSpan link = links[0];
|
||
|
if (action == MotionEvent.ACTION_UP) {
|
||
|
if (link instanceof TextLinkSpan) {
|
||
|
((TextLinkSpan) link).onClick(
|
||
|
widget, TextLinkSpan.INVOCATION_METHOD_TOUCH);
|
||
|
} else {
|
||
|
link.onClick(widget);
|
||
|
}
|
||
|
} else if (action == MotionEvent.ACTION_DOWN) {
|
||
|
if (widget.getContext().getApplicationInfo().targetSdkVersion
|
||
|
>= Build.VERSION_CODES.P) {
|
||
|
// Selection change will reposition the toolbar. Hide it for a few ms for a
|
||
|
// smoother transition.
|
||
|
widget.hideFloatingToolbar(HIDE_FLOATING_TOOLBAR_DELAY_MS);
|
||
|
}
|
||
|
Selection.setSelection(buffer,
|
||
|
buffer.getSpanStart(link),
|
||
|
buffer.getSpanEnd(link));
|
||
|
}
|
||
|
return true;
|
||
|
} else {
|
||
|
Selection.removeSelection(buffer);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
return super.onTouchEvent(widget, buffer, event);
|
||
|
}
|
||
|
|
||
|
@Override
|
||
|
public void initialize(TextView widget, Spannable text) {
|
||
|
Selection.removeSelection(text);
|
||
|
text.removeSpan(FROM_BELOW);
|
||
|
}
|
||
|
|
||
|
@Override
|
||
|
public void onTakeFocus(TextView view, Spannable text, int dir) {
|
||
|
Selection.removeSelection(text);
|
||
|
|
||
|
if ((dir & View.FOCUS_BACKWARD) != 0) {
|
||
|
text.setSpan(FROM_BELOW, 0, 0, Spannable.SPAN_POINT_POINT);
|
||
|
} else {
|
||
|
text.removeSpan(FROM_BELOW);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
public static MovementMethod getInstance() {
|
||
|
if (sInstance == null)
|
||
|
sInstance = new LinkMovementMethod();
|
||
|
|
||
|
return sInstance;
|
||
|
}
|
||
|
|
||
|
@UnsupportedAppUsage
|
||
|
private static LinkMovementMethod sInstance;
|
||
|
private static Object FROM_BELOW = new NoCopySpan.Concrete();
|
||
|
}
|