大胆的 ClickableSpan 触摸
Bold ClickableSpan on touch
我有一个 TextView,其中每个单词都是一个 ClickableSpan(实际上是 ClickableSpan 的自定义子类)。当一个词被触摸时,它应该以粗体显示。 如果我在 TextView 上设置 textIsSelectable(false),它就可以正常工作。 这个词会立即变成粗体。但是,如果文本是可选的,那么它就不起作用。但是 - 如果我触摸一个词然后关闭屏幕然后重新打开,当屏幕显示重新出现时,这个词会加粗。我已经尝试了所有我能想到的强制重绘(使 TextView 无效、强制调用 Activity 的 onRestart()、TextView 上的 refreshDrawableState() 等)。我错过了什么?
这是我的 ClickableSpan 子类:
public class WordSpan extends ClickableSpan
{
int id;
private boolean marking = false;
TextPaint tp;
Typeface font;
int color = Color.BLACK;
public WordSpan(int id, Typeface font, boolean marked) {
this.id = id;
marking = marked;
this.font = font;
}
@Override
public void updateDrawState(TextPaint ds) {
ds.setColor(color);
ds.setUnderlineText(false);
if (marking)
ds.setTypeface(Typeface.create(font,Typeface.BOLD));
tp = ds;
}
@Override
public void onClick(View v) {
// Empty here -- overriden in activity
}
public void setMarking(boolean m) {
marking = m;
updateDrawState(tp);
}
public void setColor(int col) {
color = col;
}
}
这是我的Activity中的WordSpan实例化代码:
... looping through words
curSpan = new WordSpan(index,myFont,index==selectedWordId) {
@Override
public void onClick(View view) {
handleWordClick(index,this);
setMarking(true);
tvText.invalidate();
}
};
... continue loop code
这是我自定义的 MovementMethod:
public static MovementMethod createMovementMethod ( Context context ) {
final GestureDetector detector = new GestureDetector(context, new GestureDetector.SimpleOnGestureListener() {
@Override
public boolean onSingleTapUp ( MotionEvent e ) {
return true;
}
@Override
public boolean onSingleTapConfirmed ( MotionEvent e ) {
return false;
}
@Override
public boolean onDown ( MotionEvent e ) {
return false;
}
@Override
public boolean onDoubleTap ( MotionEvent e ) {
return false;
}
@Override
public void onShowPress ( MotionEvent e ) {
return;
}
});
return new ScrollingMovementMethod() {
@Override
public boolean canSelectArbitrarily () {
return true;
}
@Override
public void initialize(TextView widget, Spannable text) {
Selection.setSelection(text, text.length());
}
@Override
public void onTakeFocus(TextView view, Spannable text, int dir) {
if ((dir & (View.FOCUS_FORWARD | View.FOCUS_DOWN)) != 0) {
if (view.getLayout() == null) {
// This shouldn't be null, but do something sensible if it is.
Selection.setSelection(text, text.length());
}
} else {
Selection.setSelection(text, text.length());
}
}
@Override
public boolean onTouchEvent ( TextView widget, Spannable buffer, MotionEvent event ) {
// check if event is a single tab
boolean isClickEvent = detector.onTouchEvent(event);
// detect span that was clicked
if (isClickEvent) {
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();
int line = layout.getLineForVertical(y);
int off = layout.getOffsetForHorizontal(line, x);
WordSpan[] link = buffer.getSpans(off, off, WordSpan.class);
if (link.length != 0) {
// execute click only for first clickable span
// can be a for each loop to execute every one
if (event.getAction() == MotionEvent.ACTION_UP) {
link[0].onClick(widget);
return true;
}
else if (event.getAction() == MotionEvent.ACTION_DOWN) {
Selection.setSelection(buffer,
buffer.getSpanStart(link[0]),
buffer.getSpanEnd(link[0]));
return false;
}
}
else {
}
}
// let scroll movement handle the touch
return super.onTouchEvent(widget, buffer, event);
}
};
}
当文本设置为可选 (TextView#setTextIsSelectable(true)
) 时,您的 span 会以某种方式变得不可变。这是关于 Understanding Spans that explains mutability of spans. I also think that this post 的一篇很好的文章,有一些很好的解释
我不确定您的跨度如何变得不可变。也许它们是可变的但只是没有以某种方式显示出来?目前还不清楚。也许有人对这种行为有解释。但是,现在,这里有一个解决方法:
当您旋转设备或将其关闭并重新打开时,将重新创建或重新应用跨距。这就是你看到变化的原因。解决方法是不要尝试在单击时更改跨度,而是使用粗体字体重新应用它。这样更改就会生效。您甚至不需要调用 invalidate()
。跟踪加粗的跨度,以便稍后在单击另一个跨度时可以取消加粗。
结果如下:
这是主要内容activity。 (请原谅所有的硬编码,但这只是一个示例。)
MainActivity.java
public class MainActivity extends AppCompatActivity {
private TextView mTextView;
private WordSpan mBoldedSpan;
@Override
protected void onCreate(Bundle savedInstanceState) {
Typeface myFont = Typeface.DEFAULT;
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
mTextView = findViewById(R.id.textView);
mTextView.setTextIsSelectable(true);
mTextView.setMovementMethod(createMovementMethod(this));
SpannableString ss = new SpannableString("Hello world! ");
int[][] spanStartEnd = new int[][]{{0, 5}, {6, 12}};
for (int i = 0; i < spanStartEnd.length; i++) {
WordSpan wordSpan = new WordSpan(i, myFont, false) {
@Override
public void onClick(View view) {
// handleWordClick(index, this); // Not sure what this does.
Spannable ss = (Spannable) mTextView.getText();
if (mBoldedSpan != null) {
reapplySpan(ss, mBoldedSpan, false);
}
reapplySpan(ss, this, true);
mBoldedSpan = this;
}
private void reapplySpan(Spannable spannable, WordSpan span, boolean isBold) {
int spanStart = spannable.getSpanStart(span);
int spanEnd = spannable.getSpanEnd(span);
span.setMarking(isBold);
spannable.setSpan(span, spanStart, spanEnd, Spanned.SPAN_INCLUSIVE_INCLUSIVE);
}
};
ss.setSpan(wordSpan, spanStartEnd[i][0], spanStartEnd[i][1],
Spanned.SPAN_INCLUSIVE_INCLUSIVE);
}
mTextView.setText(ss, TextView.BufferType.SPANNABLE);
}
// All the other code follows without modification.
}
activity_main.xml
<android.support.constraint.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<TextView
android:id="@+id/textView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="30sp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_bias="0.04000002"
tools:text="Hello World!" />
</android.support.constraint.ConstraintLayout>
这是使用 StyleSpan
的版本。结果一样。
MainActivity.java
public class MainActivity extends AppCompatActivity {
private TextView mTextView;
private StyleSpan mBoldedSpan;
@Override
protected void onCreate(Bundle savedInstanceState) {
Typeface myFont = Typeface.DEFAULT;
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
mTextView = findViewById(R.id.textView);
mTextView.setTextIsSelectable(true);
mTextView.setMovementMethod(createMovementMethod(this));
mBoldedSpan = new StyleSpan(android.graphics.Typeface.BOLD);
SpannableString ss = new SpannableString("Hello world!");
int[][] spanStartEnd = new int[][]{{0, 5}, {6, 12}};
for (int i = 0; i < spanStartEnd.length; i++) {
WordSpan wordSpan = new WordSpan(i, myFont, false) {
@Override
public void onClick(View view) {
// handleWordClick(index, this); // Not sure what this does.
Spannable ss = (Spannable) mTextView.getText();
ss.setSpan(mBoldedSpan, ss.getSpanStart(this), ss.getSpanEnd(this),
Spanned.SPAN_INCLUSIVE_INCLUSIVE);
}
};
ss.setSpan(wordSpan, spanStartEnd[i][0], spanStartEnd[i][1],
Spanned.SPAN_INCLUSIVE_INCLUSIVE);
}
mTextView.setText(ss, TextView.BufferType.SPANNABLE);
}
// All the other code follows without modification.
}
我有一个 TextView,其中每个单词都是一个 ClickableSpan(实际上是 ClickableSpan 的自定义子类)。当一个词被触摸时,它应该以粗体显示。 如果我在 TextView 上设置 textIsSelectable(false),它就可以正常工作。 这个词会立即变成粗体。但是,如果文本是可选的,那么它就不起作用。但是 - 如果我触摸一个词然后关闭屏幕然后重新打开,当屏幕显示重新出现时,这个词会加粗。我已经尝试了所有我能想到的强制重绘(使 TextView 无效、强制调用 Activity 的 onRestart()、TextView 上的 refreshDrawableState() 等)。我错过了什么?
这是我的 ClickableSpan 子类:
public class WordSpan extends ClickableSpan
{
int id;
private boolean marking = false;
TextPaint tp;
Typeface font;
int color = Color.BLACK;
public WordSpan(int id, Typeface font, boolean marked) {
this.id = id;
marking = marked;
this.font = font;
}
@Override
public void updateDrawState(TextPaint ds) {
ds.setColor(color);
ds.setUnderlineText(false);
if (marking)
ds.setTypeface(Typeface.create(font,Typeface.BOLD));
tp = ds;
}
@Override
public void onClick(View v) {
// Empty here -- overriden in activity
}
public void setMarking(boolean m) {
marking = m;
updateDrawState(tp);
}
public void setColor(int col) {
color = col;
}
}
这是我的Activity中的WordSpan实例化代码:
... looping through words
curSpan = new WordSpan(index,myFont,index==selectedWordId) {
@Override
public void onClick(View view) {
handleWordClick(index,this);
setMarking(true);
tvText.invalidate();
}
};
... continue loop code
这是我自定义的 MovementMethod:
public static MovementMethod createMovementMethod ( Context context ) {
final GestureDetector detector = new GestureDetector(context, new GestureDetector.SimpleOnGestureListener() {
@Override
public boolean onSingleTapUp ( MotionEvent e ) {
return true;
}
@Override
public boolean onSingleTapConfirmed ( MotionEvent e ) {
return false;
}
@Override
public boolean onDown ( MotionEvent e ) {
return false;
}
@Override
public boolean onDoubleTap ( MotionEvent e ) {
return false;
}
@Override
public void onShowPress ( MotionEvent e ) {
return;
}
});
return new ScrollingMovementMethod() {
@Override
public boolean canSelectArbitrarily () {
return true;
}
@Override
public void initialize(TextView widget, Spannable text) {
Selection.setSelection(text, text.length());
}
@Override
public void onTakeFocus(TextView view, Spannable text, int dir) {
if ((dir & (View.FOCUS_FORWARD | View.FOCUS_DOWN)) != 0) {
if (view.getLayout() == null) {
// This shouldn't be null, but do something sensible if it is.
Selection.setSelection(text, text.length());
}
} else {
Selection.setSelection(text, text.length());
}
}
@Override
public boolean onTouchEvent ( TextView widget, Spannable buffer, MotionEvent event ) {
// check if event is a single tab
boolean isClickEvent = detector.onTouchEvent(event);
// detect span that was clicked
if (isClickEvent) {
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();
int line = layout.getLineForVertical(y);
int off = layout.getOffsetForHorizontal(line, x);
WordSpan[] link = buffer.getSpans(off, off, WordSpan.class);
if (link.length != 0) {
// execute click only for first clickable span
// can be a for each loop to execute every one
if (event.getAction() == MotionEvent.ACTION_UP) {
link[0].onClick(widget);
return true;
}
else if (event.getAction() == MotionEvent.ACTION_DOWN) {
Selection.setSelection(buffer,
buffer.getSpanStart(link[0]),
buffer.getSpanEnd(link[0]));
return false;
}
}
else {
}
}
// let scroll movement handle the touch
return super.onTouchEvent(widget, buffer, event);
}
};
}
当文本设置为可选 (TextView#setTextIsSelectable(true)
) 时,您的 span 会以某种方式变得不可变。这是关于 Understanding Spans that explains mutability of spans. I also think that this post 的一篇很好的文章,有一些很好的解释
我不确定您的跨度如何变得不可变。也许它们是可变的但只是没有以某种方式显示出来?目前还不清楚。也许有人对这种行为有解释。但是,现在,这里有一个解决方法:
当您旋转设备或将其关闭并重新打开时,将重新创建或重新应用跨距。这就是你看到变化的原因。解决方法是不要尝试在单击时更改跨度,而是使用粗体字体重新应用它。这样更改就会生效。您甚至不需要调用 invalidate()
。跟踪加粗的跨度,以便稍后在单击另一个跨度时可以取消加粗。
结果如下:
这是主要内容activity。 (请原谅所有的硬编码,但这只是一个示例。)
MainActivity.java
public class MainActivity extends AppCompatActivity {
private TextView mTextView;
private WordSpan mBoldedSpan;
@Override
protected void onCreate(Bundle savedInstanceState) {
Typeface myFont = Typeface.DEFAULT;
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
mTextView = findViewById(R.id.textView);
mTextView.setTextIsSelectable(true);
mTextView.setMovementMethod(createMovementMethod(this));
SpannableString ss = new SpannableString("Hello world! ");
int[][] spanStartEnd = new int[][]{{0, 5}, {6, 12}};
for (int i = 0; i < spanStartEnd.length; i++) {
WordSpan wordSpan = new WordSpan(i, myFont, false) {
@Override
public void onClick(View view) {
// handleWordClick(index, this); // Not sure what this does.
Spannable ss = (Spannable) mTextView.getText();
if (mBoldedSpan != null) {
reapplySpan(ss, mBoldedSpan, false);
}
reapplySpan(ss, this, true);
mBoldedSpan = this;
}
private void reapplySpan(Spannable spannable, WordSpan span, boolean isBold) {
int spanStart = spannable.getSpanStart(span);
int spanEnd = spannable.getSpanEnd(span);
span.setMarking(isBold);
spannable.setSpan(span, spanStart, spanEnd, Spanned.SPAN_INCLUSIVE_INCLUSIVE);
}
};
ss.setSpan(wordSpan, spanStartEnd[i][0], spanStartEnd[i][1],
Spanned.SPAN_INCLUSIVE_INCLUSIVE);
}
mTextView.setText(ss, TextView.BufferType.SPANNABLE);
}
// All the other code follows without modification.
}
activity_main.xml
<android.support.constraint.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<TextView
android:id="@+id/textView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="30sp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_bias="0.04000002"
tools:text="Hello World!" />
</android.support.constraint.ConstraintLayout>
这是使用 StyleSpan
的版本。结果一样。
MainActivity.java
public class MainActivity extends AppCompatActivity {
private TextView mTextView;
private StyleSpan mBoldedSpan;
@Override
protected void onCreate(Bundle savedInstanceState) {
Typeface myFont = Typeface.DEFAULT;
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
mTextView = findViewById(R.id.textView);
mTextView.setTextIsSelectable(true);
mTextView.setMovementMethod(createMovementMethod(this));
mBoldedSpan = new StyleSpan(android.graphics.Typeface.BOLD);
SpannableString ss = new SpannableString("Hello world!");
int[][] spanStartEnd = new int[][]{{0, 5}, {6, 12}};
for (int i = 0; i < spanStartEnd.length; i++) {
WordSpan wordSpan = new WordSpan(i, myFont, false) {
@Override
public void onClick(View view) {
// handleWordClick(index, this); // Not sure what this does.
Spannable ss = (Spannable) mTextView.getText();
ss.setSpan(mBoldedSpan, ss.getSpanStart(this), ss.getSpanEnd(this),
Spanned.SPAN_INCLUSIVE_INCLUSIVE);
}
};
ss.setSpan(wordSpan, spanStartEnd[i][0], spanStartEnd[i][1],
Spanned.SPAN_INCLUSIVE_INCLUSIVE);
}
mTextView.setText(ss, TextView.BufferType.SPANNABLE);
}
// All the other code follows without modification.
}