20

I want to insert small pictures, like arrow icons for example, into certain positions of contents of a TextView.

The photo below depicts exactly what I want:

this is how I want to embed icons in the text

Obviously, the most naive solution is to use multiple TextView on either side of the small ImageView objects. But this approach is not scalable.

I am curious to learn if someone has overcome this problem with some simple yet smart trick. (Maybe with help from HTML or an external library)

Any efficient solution is much appreciated.

Behnam
  • 6,510
  • 6
  • 35
  • 65
  • Take a look at this project: https://github.com/JoanZapata/android-iconify – Egor Aug 27 '14 at 07:54
  • @Egor: That is great, but offers text placed only after the image, so in the case of above picture, I would have to utilize and align 5 IconTextView objects, plus 1 TextView. – Behnam Aug 27 '14 at 07:57
  • At which point is this error happening? Can't believe it's appearing immediately and in order to help you properly, you should add your current layout as well. – reVerse Aug 27 '14 at 13:32
  • I have included the layout, the problem happens only when I set more than two lines of text to my TextViewWithImage object. – Behnam Aug 28 '14 at 15:22

3 Answers3

30

You can create SpannableString and add any object to your string

TextView textView = (TextView) findViewById(R.id.textView);

ImageSpan imageSpan = new ImageSpan(this, R.drawable.ic_launcher);
SpannableString spannableString = new SpannableString(textView.getText());

int start = 3;
int end = 4;
int flag = 0;
spannableString.setSpan(imageSpan, start, end, flag);

textView.setText(spannableString);
zapdroid
  • 562
  • 5
  • 16
14

There was a similiar question a while back and someone came up with an awesome solution. I've just tweaked this one a little bit so that the image-size is always as tall as the line. So basically your icons will scale with the textSize.

Step 1 - Create a new View

Create a new Java class which extends TextView

public class TextViewWithImages extends TextView {

    public TextViewWithImages(Context context, AttributeSet attrs, int defStyle) {
        super(context, attrs, defStyle);

    }
    public TextViewWithImages(Context context, AttributeSet attrs) {
        super(context, attrs);
    }
    public TextViewWithImages(Context context) {
        super(context);
    }

    @Override
    public void setText(CharSequence text, BufferType type) {
        Spannable s = getTextWithImages(getContext(), text, this.getLineHeight());
        super.setText(s, BufferType.SPANNABLE);
    }

    private static final Spannable.Factory spannableFactory = Spannable.Factory.getInstance();

    private static boolean addImages(Context context, Spannable spannable, float height) {
        Pattern refImg = Pattern.compile("\\Q[img src=\\E([a-zA-Z0-9_]+?)\\Q/]\\E");
        boolean hasChanges = false;

        Matcher matcher = refImg.matcher(spannable);
        while (matcher.find()) {
            boolean set = true;
            for (ImageSpan span : spannable.getSpans(matcher.start(), matcher.end(), ImageSpan.class)) {
                if (spannable.getSpanStart(span) >= matcher.start()
                        && spannable.getSpanEnd(span) <= matcher.end()
                        ) {
                    spannable.removeSpan(span);
                } else {
                    set = false;
                    break;
                }
            }
            String resName = spannable.subSequence(matcher.start(1), matcher.end(1)).toString().trim();
            int id = context.getResources().getIdentifier(resName, "drawable", context.getPackageName());
            Drawable mDrawable = context.getResources().getDrawable(id);
            mDrawable.setBounds(0, 0, (int)height, (int)height);
            if (set) {
                hasChanges = true;
                spannable.setSpan(  new ImageSpan(mDrawable),
                        matcher.start(),
                        matcher.end(),
                        Spannable.SPAN_EXCLUSIVE_EXCLUSIVE
                );
            }
        }

        return hasChanges;
    }
    private static Spannable getTextWithImages(Context context, CharSequence text, float height) {
        Spannable spannable = spannableFactory.newSpannable(text);
        addImages(context, spannable, height);
        return spannable;
    }
}

Step 2 - Usage in layout

Now in your layout-xml just use the TextViewWithImages class

<com.stacko.examples.TextViewWithImages
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:textSize="14sp"
    android:text="@string/my_string_with_icons" />

Step 3 - Creating strings with icons

As you can see in the addImages(...) function of the TextViewWithImages class, a special pattern ([img src=my_icon/]) within the string is used in order to add the images. So here's a example:

<string name="my_string_with_icons">The [img src=ic_action_trash/] is used to delete an item while the [img src=ic_action_edit/] is to edit one.</string>

The output:

enter image description here

And as previously said it will scale with your textSize:

enter image description here

As initially said most of this post is taken from 18446744073709551615 answer here. I think this should be published as a library since it's a common use-case to have images in the text. :<

Community
  • 1
  • 1
reVerse
  • 35,075
  • 22
  • 89
  • 84
  • 1
    Wow, this is beautiful, gotta try! And I totally hear you on the library project. – Behnam Aug 27 '14 at 10:24
  • any idea about the crash? – Behnam Aug 28 '14 at 15:18
  • Unfortunately I can't reproduce the error. I've just set 20 lines to my `TextViewWithImages` and no crash occured. Which Android-Version does your device run on? – reVerse Aug 29 '14 at 08:01
  • 4.2.2, I need to insert up to 5 drawables. – Behnam Aug 31 '14 at 13:47
  • this seems to affect line spacing. is there a way to avoid that ? my image is at the very end of the text so i was hoping for a way to avoid disrupting line spacing. – j2emanue Mar 30 '17 at 17:02
  • 1
    @j2emanue Well the size is of the drawable is set to height of the line itself (before adding the image). It may be an option to reduce the size in the Drawable#setBounds method. – reVerse Apr 02 '17 at 12:30
  • Nice solution :) If anyone like me encounters problems using _proguard_ and `shrinkResources` (where the resource will be discarded unless you have other usages) - take a look at https://developer.android.com/studio/build/shrink-code.html -> `Customize which resources to keep` – Florian Mötz Jun 26 '17 at 12:18
  • @Behnam: Recently got this exception. The problem was I mistakenly deleted the drawable specified in the string resource. Once I placed the drawable again, it solved the issue. This problem comes because, when you run code analysis, the Lint detects that drawable as unused since its not *technically* referred from anywhere. Need to think of some way to prevent such deletion henceforth. – Mangesh Dec 06 '17 at 09:10
  • Can this be used if setting text dynamically? – seekingStillness Nov 08 '18 at 19:49
  • i want to load image from network. how to do this? Thanks – famfamfam May 12 '21 at 14:32
1

An option is to use a WebView instead of a TextView. This also allows you to display images from your asset folder.

See Android Development: Using Image From Assets In A WebView's HTML for more details.

Community
  • 1
  • 1
Lars Blumberg
  • 19,326
  • 11
  • 90
  • 127
  • That's a good idea, though I rather use drawable folder instead of assets, because I need to use different images for different Locales and screens efficiently. Anyway, I appreciate your solution. – Behnam Aug 27 '14 at 08:43