大胆的 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.
}