📜 ⬆️ ⬇️

TextView and Spannable: highlighting parts of a word



Hi, Habramir! My name is Oksana and I am an Android developer in a small but very cool Trinity Digital team.

Today I will talk about a small part of a large project.
')
The project is called “School 2100” and is a collection of electronic textbooks with various features: search, bookmarks, notes, additional materials, test items, etc. And just in what are called “test tasks” lies the subject of discussion.

Among other different tests, there is a need to implement the task for the analysis of a word according to its composition (it is also a morphological analysis). It should look something like this:



In short: there is a set of words - they need to be displayed in the form of a scrollable list. At the top of the list there should be buttons that allow you to associate parts of the word with certain morphemes (prefix, root, suffix, ending, base).

Select the part of the word, click the button - the graphic designation of the morpheme is drawn. And, plus, a small cross to remove.

To make all this beauty, we need to implement a mechanism for separating parts of the word - in fact, further we deal with this task.

Allocation rules are needed such:


On top of that, there must be some distance (tracking) between the letters of the word, more than in a standard font. This is so that later it would be convenient to draw morphemes and they do not “stick together” visually.

For the implementation , the TextView + Spannable combination was chosen, which has sufficient capabilities and at the same time is quite simple in operation.

In general, Spannable is such an interface that describes the labeling of text with objects related to the formatting of this text. Formatting objects are instances of classes that implement the ParcelableSpan interface. There are ready-made implementations (for example, UnderlineSpan, ForegroundColorSpan, StrikethoughSpan and others), but we implement this interface ourselves, because we need tracking and color at the same time.

Actually, only in order to do the tracking, you already need a custom implementation (if there is a finished one, then it was not found).

DEMO


So, we turn to the vastness of a cozy demo project, which will include:




Let's start with activity_main.xml, everything is simple there:

<?xml version="1.0" encoding="utf-8"?> <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" android:paddingBottom="@dimen/activity_vertical_margin" android:paddingLeft="@dimen/activity_horizontal_margin" android:paddingRight="@dimen/activity_horizontal_margin" android:paddingTop="@dimen/activity_vertical_margin" tools:context="ru.trinitydigital.textselecting.MainActivity" android:id="@+id/container"> </RelativeLayout> 

Now MainActivity :

 package ru.trinitydigital.textselecting; import android.support.v7.app.AppCompatActivity; import android.os.Bundle; import android.widget.RelativeLayout; public class MainActivity extends AppCompatActivity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); RelativeLayout container = (RelativeLayout) findViewById(R.id.container); container.addView(new WordAnswerView(this, "hello", convertDpToPx(30))); } } 

Finally, WordAnswerView , we will disassemble in stages. Create a TextView inheritor and define the necessary properties:

 public class WordAnswerView extends TextView { //      private final String originalText; //   ,      (  ) private float tracking = convertDpToPx(16); //   private int selectionColor = Color.parseColor("#5591F6"); //      private static final int NO_SELECTION = -1; //     (   ) private int selectionBegin = NO_SELECTION, selectionEnd = NO_SELECTION; //   ,        private SelectionTrackingSpan selectionTrackingSpan = new SelectionTrackingSpan(); //  ,  ,      private int baseWidth; } 

In the constructor, this is what:

 public WordAnswerView(Context context, CharSequence text, float textSizePx) { super(context); //   originalText = text.toString(); setTextSize(textSizePx); setTextColor(Color.BLACK); //    ,       , //       setTypeface(Typeface.MONOSPACE); setPadding((int) tracking, 0, (int) tracking, 0); //      ,      SpannableString s = new SpannableString(originalText); s.setSpan(selectionTrackingSpan, 0, originalText.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); setText(s); } 

You also need to catch touch, so add the code to the constructor:

 public WordAnswerView(Context context, CharSequence text, float textSizePx) { // … setOnTouchListener(new OnTouchListener() { @Override public boolean onTouch(View v, MotionEvent event) { switch (event.getActionMasked()) { case MotionEvent.ACTION_UP: //   ,    int index = (int)(event.getX() / baseWidth); //         if (selectionBegin == index && selectionEnd == NO_SELECTION) { selectionBegin = NO_SELECTION; selectionEnd = NO_SELECTION; invalidate(); break; } if (selectionBegin == NO_SELECTION) { selectionBegin = index; } else if (selectionEnd == NO_SELECTION) { selectionEnd = index; if (selectionBegin > selectionEnd) { int tmp = selectionBegin; selectionBegin = selectionEnd; selectionEnd = tmp; } } else { selectionBegin = index; selectionEnd = NO_SELECTION; } invalidate(); break; } return false; } }); } 

By the way , in order for everything to be good with the definition of the letter we clicked on, we need to add this:

 @Override protected void onSizeChanged(int w, int h, int oldw, int oldh) { super.onSizeChanged(w, h, oldw, oldh); baseWidth = w / originalText.length(); } 

Getting to the point - we will write the SelectionTractingSpan class:

 public class SelectionTrackingSpan extends ReplacementSpan { @Override public int getSize(Paint paint, CharSequence text, int start, int end, Paint.FontMetricsInt fm) { //         +    return (int)(paint.measureText(text, start, end) + tracking * (end - start)); } @Override public void draw(Canvas canvas, CharSequence text, int start, int end, float x, int top, int y, int bottom, Paint paint) { float dx = x; for (int i = start; i < end; i++) { //       ,      if (i < selectionBegin || i >= (selectionEnd != NO_SELECTION ? selectionEnd + 1 : selectionBegin + 1)) paint.setColor(Color.BLACK); else paint.setColor(selectionColor); canvas.drawText(text, i, i + 1, dx, y, paint); dx += paint.measureText(text, i, i + 1) + tracking; } } } 

So the recipe is pretty simple:


+ do tracking

Profit :)

→ Sources in our github

Source: https://habr.com/ru/post/340232/


All Articles