Жирный ClickableSpan при касании

У меня есть TextView, в котором каждое слово является ClickableSpan (на самом деле это настраиваемый подкласс ClickableSpan). Когда слово затронуто, оно должно быть показано жирным шрифтом. Если я устанавливаю textIsSelectable(false) в TextView, все работает нормально. Слово сразу выделяется жирным шрифтом. Но если текст выбирается, то это не работает. НО - если я коснусь слова, а затем выключу и снова включу экран, когда экран снова появится, слово будет выделено жирным шрифтом. Я пробовал все, что мог придумать, чтобы принудительно перерисовать (аннулировать TextView, принудительно вызвать Activity onRestart(), refreshDrawableState() в TextView и т. д.). Что мне не хватает?

Вот мой подкласс 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;
    }
}

Вот код создания экземпляра 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);
        }
    };
}

person Matt Robertson    schedule 05.07.2018    source источник
comment
управляйте им в onPause, чтобы слова не выделялись жирным шрифтом.   -  person    schedule 14.07.2018


Ответы (1)


Ваши диапазоны каким-то образом становятся неизменными, когда текст установлен как выбираемый (TextView#setTextIsSelectable(true)). Вот хорошая статья о Понимание спанов, в которой объясняется изменчивость спанов. Я также думаю, что в этом сообщении есть несколько хороших объяснений

Я не уверен, как ваши интервалы становятся неизменными. Может быть, они изменчивы, но просто как-то не отображаются? Это неясно. Может у кого есть объяснение такому поведению. Но пока вот исправление:

Когда вы поворачиваете устройство или выключаете и снова включаете, диапазоны воссоздаются или просто повторно применяются. Вот почему вы видите изменение. Исправление состоит в том, чтобы не пытаться изменить интервалы при нажатии, а повторно применить его с полужирным шрифтом. Таким образом, изменение вступит в силу. Вам даже не нужно будет звонить invalidate(). Следите за выделенным жирным шрифтом диапазоном, чтобы его можно было убрать позже, когда будет нажат другой диапазон.

Вот результат:

введите описание изображения здесь

Вот основная деятельность. (Пожалуйста, простите за жесткое кодирование, но это всего лишь пример.)

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.
}
person Cheticamp    schedule 11.07.2018
comment
Возможно, это было непонятно из моих опубликованных фрагментов, но каждое отдельное слово должно быть привязано к экземпляру ClickableSpan — жирным шрифтом будет выделено только слово, которое было нажато последним. Поэтому я не могу удалить ClickableSpan, но вариант вашего ответа может состоять в том, чтобы поддерживать один TextAppearanceSpan и просто сбрасывать его начальные/конечные значения при нажатии нового слова. Я попробую это решение и дам вам ответ/награду, если оно сработает (чего я и ожидаю). - person Matt Robertson; 12.07.2018
comment
@MattRobertson Обновлен ответ, выделенный жирным шрифтом или выделенный жирным шрифтом по мере необходимости. - person Cheticamp; 14.07.2018