17

My application uses the unicode character \u2192 to display a right arrow within a TextView element. However, the arrow is shown at the very bottom line, but should be centered vertically:

enter image description here

However, if I print the unicode character using the standard output, everything is fine:

public class Main {
  public static void main(String[] args) {
    System.out.println("A" + Character.toString("\u2192".toCharArray()[0]));
  }
}

enter image description here

How I can enforce the right arrow to be centered in the TextView, too? My TextView already uses android:gravity="center_vertical|left".

Update: I use Android Studio 3.0.1 and Android SDK 26. The XML code of the TextView:

 <TextView
    android:id="@+id/my_text_view"
    android:layout_width="match_parent"
    android:fontFamily="monospace"
    android:gravity="center_vertical|left"
    android:textSize="26sp" />

Filling the TextView in the code:

    TextView textView = findViewById(R.id.my_text_view);
    textView.setText("A" + Character.toString("\u2192".toCharArray()[0]) + "B");
John Threepwood
  • 15,593
  • 27
  • 93
  • 149

3 Answers3

10

Since Android's Roboto font doesn't seem to contain arrow glyphs, this would cause it to display a character from a fallback font if possible (though I couldn't find the documentation of this behavior). I'm guessing that the "fallback" arrow on your device is lying on the baseline for some reason.

However, there's a trick that might help you: You can include an image of the special character and insert the image into your output text.

First, add a new Drawable resource file (ic_arrow.xml) in res/drawable, and copy-paste this into it:

<vector android:alpha="0.78" android:height="24dp"
  android:viewportHeight="24.0" android:viewportWidth="24.0"
  android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
  <path android:fillColor="#FF000000" android:pathData="M3,12L21.5,12"
    android:strokeColor="#000000" android:strokeWidth="1"/>
  <path android:fillColor="#FF000000" android:pathData="M21,12L17,8"
    android:strokeColor="#000000" android:strokeWidth="1"/>
  <path android:fillColor="#FF000000" android:pathData="M21,12L17,16"
    android:strokeColor="#000000" android:strokeWidth="1"/>
</vector>

Now, we'll need to embed the arrow image into a SpannableString that you can display in your TextView. I'll walk you through the (surprisingly many) lines of code that we'll need to accomplish this:

  • Get the Drawable resource so that we can make it the correct size before displaying it.

    Drawable arrow = ContextCompat.getDrawable(this, R.drawable.ic_arrow);

  • Figure out what size the arrow needs to be, based on the font metrics.

    Float ascent = textView.getPaint().getFontMetrics().ascent;

  • This is negative (for reasons), so make it positive.

    int h = (int) -ascent;

  • Finally, we can set the bounds of the Drawable to match the text size.

    arrow.setBounds(0,0,h,h);

  • Next, create a SpannableString initialized with our text. I just used * for a placeholder here, but it could be any character (or a blank space). If you're planning to concatenate different bits together along with multiple arrow images, use a SpannableStringBuilder for this.

    SpannableString stringWithImage = new SpannableString("A*B");

  • Now, we can finally insert the image into the span (using a new ImageSpan with our "arrow" image, aligned with the baseline). The 1 and 2 are zero-based start and end indices (telling where to insert the image), and SPAN_EXCLUSIVE_EXCLUSIVE indicates that the span doesn't expand to include inserted text.

    stringWithImage.setSpan(new ImageSpan(arrow, DynamicDrawableSpan.ALIGN_BASELINE), 1, 2, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);

  • Finally, we can display the text (including the custom arrow image):

    textView.setText(stringWithImage);

Altogether, the code should look like this.

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

    Drawable arrow = ContextCompat.getDrawable(this, R.drawable.ic_arrow);
    Float ascent = textView.getPaint().getFontMetrics().ascent;
    int h = (int) -ascent;
    arrow.setBounds(0,0,h,h);

    SpannableString stringWithImage = new SpannableString("A*B");
    stringWithImage.setSpan(new ImageSpan(arrow, DynamicDrawableSpan.ALIGN_BASELINE), 1, 2, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
    textView.setText(stringWithImage);

I wish Android had a better way to do this sort of thing, but at least it can be done somehow. The resulting TextView should look like this:

screenshot

Rapunzel Van Winkle
  • 5,380
  • 6
  • 31
  • 48
  • Thank you for your explanation and workaround. It really seems to be the case that the font does not support the arrow character. I use a Nexus 5X AVD for testing. Now I just tested it on my hardware device Samsung A5 (2017), everything working fine there. Both have Android 7.0. I am confused... – John Threepwood Mar 20 '18 at 23:10
  • It's a very confusing topic, and not well-documented at all (especially because different manufacturers - like Samsung - can and do install different fonts on their devices. In the course of trying to track down more information about this, I found [this interesting post](https://stackoverflow.com/questions/11815458/check-if-custom-font-can-display-character), which gives ways to check whether a given character can be displayed. I'm not sure if that will help in your particular case, but it might. – Rapunzel Van Winkle Mar 20 '18 at 23:36
  • Thank you again for the help. I just tried the hasGlyph method with textView.setTest(String.valueOf(p.hasGlyph("\u2192")) on the AVD. The result is: "true". So even the arrow the is one the base line, the method says "true". This is really unfortunate. I cannot test my UI with the AVD and have to rely on my single hardware device to test it. This way I cannot ensure that everything works fine on different devices. I guess I have to test your workaround for a solid solution. – John Threepwood Mar 21 '18 at 07:24
  • Good luck! Are you building more complicated strings than "A->B"? If so, you should probably use a [SpannableStringBuilder](https://blog.stylingandroid.com/introduction-to-spans/). – Rapunzel Van Winkle Mar 21 '18 at 08:37
  • Thank you for the hint. No, not more complicated strings than "A->B". However, SpannableStringBuilder does not help that much, see my comment to the answer of @Alessandro Verona. I just marked your answer as accepted, since I don't see there is a better solution available (at least for the moment). – John Threepwood Mar 21 '18 at 10:41
  • Yeah, I'd only use SpannableStringBuilder for more complicated Spannables. To be safe, you should probably allocate a new SpannableString each time... But you can get away with setting up the arrow Drawable just once and then reusing it. Thanks for the generous bounty! – Rapunzel Van Winkle Mar 21 '18 at 10:54
  • Your welcome. Your help and discussion helped me a lot. – John Threepwood Mar 21 '18 at 12:22
1

Try another symbols with html entity encodings. Look at this link, maybe you find here more convenient right arrow for you but I chose this for example : &#10132

String formula = "A &#10132 B";
textView.setText(Html.fromHtml(formula));

also this third party lib maybe appropriate for you

godot
  • 3,422
  • 6
  • 25
  • 42
  • 1
    Thank you. However, other arrow symbols have the same result. The math library is a good reference. It looks a bit over the top for my purposes but if I cannot find another solution I will give it a try. – John Threepwood Mar 16 '18 at 09:07
1

Try that:

SpannableString st = new SpannableString("A" + "\u2192" + "B");
st.setSpan(new RelativeSizeSpan(2f), 1, 2, 0);
textView.setText(st);

I know that however this doesn't completely answer.

Alessandro Verona
  • 1,157
  • 9
  • 23
  • Thank you. I tried your solution and it works fine on the AVD. However, on my hardware device, the arrow is now at the top. Unfortunately this solution only works if the arrow is always placed on the basline. – John Threepwood Mar 21 '18 at 07:29
  • @JohnThreepwood can you tall me the Brand of your device? – Alessandro Verona Mar 21 '18 at 08:12
  • As hardware device I use a Samsung Galaxy A5 (2017). For this device, your solutions displays the arrow at the top (too high). However, for the AVD Nexus 5X, your solutions is fine (arrow is vertically centered). – John Threepwood Mar 21 '18 at 12:25