5

I have a TextView in which I want to place a solid color block over given words of the TextView, for example:

"This is a text string, I want to put a rectangle over this WORD" - so, "WORD" would have a rectangle with a solid color over it.

To do this, I am thinking about overriding the onDraw(Canvas canvas) method, in order to draw a block over the text. My only problem is to find an efficient way to get the absolute position of a given word or character.

Basically, I am looking for something that does the exact opposite of the getOffsetForPosition(float x, float y) method

Suragch
  • 484,302
  • 314
  • 1,365
  • 1,393
Pedro Lopes
  • 2,833
  • 1
  • 30
  • 36

4 Answers4

10

Based on this post: How get coordinate of a ClickableSpan inside a TextView?, I managed to use this code in order to put a rectangle on top of the text:

protected void onDraw(Canvas canvas) {
    super.onDraw(canvas);
    Paint paint = new Paint();
    paint.setStyle(Paint.Style.FILL);
    paint.setColor(Color.WHITE);

    // Initialize global value
    TextView parentTextView = this;
    Rect parentTextViewRect = new Rect();

    // Find where the WORD is
    String targetWord = "WORD";
    int startOffsetOfClickedText = this.getText().toString().indexOf(targetWord);
    int endOffsetOfClickedText = startOffsetOfClickedText + targetWord.length();

    // Initialize values for the computing of clickedText position
    Layout textViewLayout = parentTextView.getLayout();

    double startXCoordinatesOfClickedText = textViewLayout.getPrimaryHorizontal((int)startOffsetOfClickedText);
    double endXCoordinatesOfClickedText = textViewLayout.getPrimaryHorizontal((int)endOffsetOfClickedText);

    // Get the rectangle of the clicked text
    int currentLineStartOffset = textViewLayout.getLineForOffset((int)startOffsetOfClickedText);
    int currentLineEndOffset = textViewLayout.getLineForOffset((int)endOffsetOfClickedText);
    boolean keywordIsInMultiLine = currentLineStartOffset != currentLineEndOffset;
    textViewLayout.getLineBounds(currentLineStartOffset, parentTextViewRect);

    // Update the rectangle position to his real position on screen
    int[] parentTextViewLocation = {0,0};
    parentTextView.getLocationOnScreen(parentTextViewLocation);

    double parentTextViewTopAndBottomOffset = (
        //parentTextViewLocation[1] - 
        parentTextView.getScrollY() + 
        parentTextView.getCompoundPaddingTop()
    );

    parentTextViewRect.top += parentTextViewTopAndBottomOffset;
    parentTextViewRect.bottom += parentTextViewTopAndBottomOffset;

    // In the case of multi line text, we have to choose what rectangle take
    if (keywordIsInMultiLine){

        WindowManager wm = (WindowManager) getContext().getSystemService(Context.WINDOW_SERVICE);
        Display display = wm.getDefaultDisplay();

        int screenHeight = display.getHeight();
        int dyTop = parentTextViewRect.top;
        int dyBottom = screenHeight - parentTextViewRect.bottom;
        boolean onTop = dyTop > dyBottom;

        if (onTop){
            endXCoordinatesOfClickedText = textViewLayout.getLineRight(currentLineStartOffset);
        }
        else{
            parentTextViewRect = new Rect();
            textViewLayout.getLineBounds(currentLineEndOffset, parentTextViewRect);
            parentTextViewRect.top += parentTextViewTopAndBottomOffset;
            parentTextViewRect.bottom += parentTextViewTopAndBottomOffset;
            startXCoordinatesOfClickedText = textViewLayout.getLineLeft(currentLineEndOffset);
        }

    }

    parentTextViewRect.left += (
        parentTextViewLocation[0] +
        startXCoordinatesOfClickedText + 
        parentTextView.getCompoundPaddingLeft() - 
        parentTextView.getScrollX()
    );
    parentTextViewRect.right = (int) (
        parentTextViewRect.left + 
        endXCoordinatesOfClickedText - 
        startXCoordinatesOfClickedText
    );

    canvas.drawRect(parentTextViewRect, paint);
 }
Community
  • 1
  • 1
Pedro Lopes
  • 2,833
  • 1
  • 30
  • 36
  • 1
    I am not getting this, how do you use this..?? I mean If I select text, this code will be executed by it self?? Or we need to do something else? I have created my custom text view and put this method in it. How will it be executed? – Nir Patel Sep 22 '17 at 07:05
  • @NirPatel this code draws a rectangle on top of the `targetWord`, that's the algorithm input. If you use the code without any changes and write on you custom text view the `targetWord` (in this case "WORD"), a rectangle will be drawn on top of it. If you would like to place the rectangle on top of the user's selected text, then the `startOffsetOfClickedText `, and `endXCoordinatesOfClickedText ` would have to be calculated from whichever selection span is made by the user, however I believe this wont work well for multiple target words, since it only draws one rectangle. – Pedro Lopes Sep 23 '17 at 07:55
5

You can use spans for that. First you create a spannable for your text, like this:

Spannable span = new SpannableString(text);

Then you put a span around the word that you want to highlight, somewhat like this:

span.setSpan(new UnderlineSpan(), start, end, 
                     Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);

Unfortunately I don't know of an existing span that puts a border around a word. I found UnderlineSpan, and also BackgroundColorSpan, perhaps these are also useful for you, or you can have a look at the code and see if you can create a BorderSpan based on one of those.

user114676
  • 482
  • 5
  • 9
0

Instead of drawing a rectangle over the WORD, you could simply replace its characters with an appropriate unicode symbol like U+25AE (▮ Black vertical rectangle).

So you'd get

"This is a text string, I want to put a rectangle over this ▮▮▮▮"

If that is sufficient. See for example Wikipedia for a wast list of unicode symbols.

If you actually need to paint that black box you can do the following as long as your text is in a single line:

Calculate the width of the text part before 'WORD' as explained here to find the left edge of the box and calcuate the width of 'WORD' using the same method to find the width of the box.

For a multiline text the explained method might also work but I think you'll have to do quite a lot of work here.

Community
  • 1
  • 1
Ridcully
  • 23,362
  • 7
  • 71
  • 86
  • Thanks for your reply, first of all. Since it is multiline, maybe I could use a technique where I would scan every line for the WORD, but I do not know if that is very efficient – Pedro Lopes Nov 15 '12 at 00:46
  • Nah, that will not work well. You'd have to rebuild the text wrapping process of the TextView to find out whhat text is in which line. – Ridcully Nov 15 '12 at 06:26
0

use getLayout().getLineBottom and textpaint.measureText to manually do the reverse calculation of getOffsetForPosition.

below is an example of using the calculated x,y for some textOffset to position the handle drawable when the textview gets clicked.

class TextViewCustom extends TextView{
    float lastX,lastY;
    @Override
    public boolean onTouchEvent(MotionEvent event) {
        boolean ret = super.onTouchEvent(event);
        lastX=event.getX();
        lastY=event.getY();
        return ret;
    }

    BreakIterator boundary;
    Drawable handleLeft;
    private void init() {// call  it in constructors
        boundary = BreakIterator.getWordInstance();
handleLeft=getResources().getDrawable(R.drawable.abc_text_select_handle_left_mtrl_dark);
        setOnClickListener(new OnClickListener() {
            @Override
            public void onClick(View v) {
                int line = getLayout().getLineForVertical((int) lastY);
                int offset = getLayout().getOffsetForHorizontal(line, lastX);
                int wordEnd = boundary.following(offset);
                int wordStart = boundary.previous();
                CMN.Log(getText().subSequence(wordStart, wordEnd));

                int y = getLayout().getLineBottom(line);
                int trimA = getLayout().getLineStart(line);
                float x = getPaddingLeft()+getPaint().measureText(getText(), trimA, wordStart);


                x-=handleLeft.getIntrinsicWidth()*1.f*9/12;
                handleLeft.setBounds((int)x,y,(int)(x+handleLeft.getIntrinsicWidth()),y+handleLeft.getIntrinsicHeight());
                invalidate();
            }
        });
    }

    @Override
    public void setText(CharSequence text, BufferType type) {
        super.setText(text, type);
        if(boundary!=null)
            boundary.setText(text.toString());
    }

}
KnIfER
  • 712
  • 7
  • 13