How LineSpace Affects StaticLayout Height in One Line Text - android

Consider this simple example:
I have a single line text like this: "Hello"
I wanna measure this text using StaticLayout. So I wrote something like this:
StaticLayout layout = new StaticLayout("Hello", myTextView.getPaint(), myTextView.getWidth(), Layout.Alignment.NORMAL, 1, lineSpace, false);
In above code I changed lineSpace variable in a for loop and every time log the layout's height:
for(int lineSpace=0; lineSpace<20;lineSpace++){
StaticLayout layout = new StaticLayout("Hello", myTextView.getPaint(), myTextView.getWidth(), Layout.Alignment.NORMAL, 1, lineSpace, false);
Log.d("TAG", "layout height: " + layout.getHeight());
}
When I run this code on device with android M layout't height doesn't changed with multiple values of lineSpace. But in lower android versions layout's height changed with line space respectively.
Although when your text is more than one line ,StaticLayout consider line space between two lines. But it seems Android M doesn't consider line space for last line but lower Android versions does.
My question is this: After what version of android StaticLayout consider line space for last line? Can I wrote something like this:
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
// in this version StaticLayout don't consider line space for last line
} else {
// in this version StaticLayout consider line space for last line
}

I did some quick digging in the source code, seems to be that this part is the culprit:
if (needMultiply && !lastLine) {
double ex = (below - above) * (spacingmult - 1) + spacingadd;
if (ex >= 0) {
extra = (int)(ex + EXTRA_ROUNDING);
} else {
extra = -(int)(-ex + EXTRA_ROUNDING);
}
} else {
extra = 0;
}
Older versions are missing the !lastLine condition and thus also add the spacing to the last line.
The condition was added in this commit, which, if my github foo doesn't fail me, should be included starting with Android 5.
Apparently, just like the commit mentions, this only affects single line texts, for multiline texts the height seems to be calculated correctly. So an easy fix might be to check whether the text only has a single line (using getLineCount()) and if the Android version is less than 5, and if so substract the line spacing once.

Related

Jetpack compose IntrinsicSize.Min cuts off Text - want to force Text to take up exact height and minimum width required for that height

I have a Text component that I want the width to expand as big as it wants to accommodate the supplied text and to be exactly 2 lines tall.
I have attempted to implement this by calculating the height using my font size and the number of lines that I want:
val textHeight = textStyle.lineHeight.value * 2
Then, based on various articles (such as this one https://www.answertopia.com/jetpack-compose/jetpack-compose-intrinsicsize-tutorial/) and what I understand of IntrinsicSize, shouldn't I be able to constrain the height and use IntrinsicSize.Min to set the text's width to fit everything at the minimum width required? This actually just cuts off the text.
Text(
modifier = Modifier.height(textHeight).width(IntrinsicSize.Min),
text = "Fabrication et assemblage soignes",
style = textStyle,
)
For some reason ends up rendering as
I want it to look like
Fabrication et
assemblage soigne
On two lines, wrapped to the minimum width.
How can I achieve the result I want?
Also note using .wrapContentWidth() doesn't work because then the text just takes up 1 line and leaves the extra height space empty.
If it's just for that specific text you want, which I highly doubt, you could insert a line break inside the string as follows: "Fabrication et\nassemblage soignes".
If it's for more general sentences, then it's not clear how you would want the Text component to distribute the words between the two lines.
Assuming you would want the width of the lines to be as close as possible (like this strategy), the most 'elegant' way I can think of (which is not very elegant) is implementing an algorithm which would measure the entire width of the text, find the whitespace closest to the middle and replace it with a line break.
I've implemented something simpler which does manage to divide at the position you want:
fun String.attemptToDivideEqually(): String {
if (this.length <= 2) return this
val idealLineBreakIndex = this.closestWhiteSpaceToIndex((this.length + 1) / 2) ?: return this
return this.substring(0, idealLineBreakIndex) + "\n" + this.substring(
idealLineBreakIndex + 1,
this.length
)
}
fun String.closestWhiteSpaceToIndex(index: Int): Int? {
if (index !in 0 .. this.lastIndex) return null
val maxDistance = maxOf(index, this.lastIndex - index)
for (i in 0 .. maxDistance) {
if (index + i <= this.lastIndex && this[index + i] == ' ') return index + i
if (index - i >= 0 && this[index - i] == ' ') return index - i
}
return null
}
However, I don't recommend using it in production code for the following reasons:
Characters can differ greatly in width, so if for whatever reason, the first 'half' of the string has significantly more 'narrow' letters, than the latter, it may look odd.
If there are only a few number of words, it may look odd.
There are probably more reasons I cannot currently think of + I have not tested it thoroughly.
It was an interesting question nonetheless!

How to detect emoji support on Android by code

By code, I can make a button that inserts these 3 emojis into the text: ⚽️😈🐺
On many phones when the user clicks the button, though, the problem is that ⚽️😈🐺 displays as [X][X][X]. Or even worse, it displays only three empty spaces.
I would like to disable and hide my own built-in emoji-keypad on Android devices that do not display emojis correctly. Does anyone knows or have a tip on how to detect in code if a device has emoji support?
I have read that emoji is supported from android 4.1, but that is not my experience....
I just implemented a solution for this problem myself. The nice thing with Android is that it is open source so that when you come around problems like these, there's a good chance you can find an approach to help you.
In the Android Open Source Project, you can find a method where they use Paint.hasGlyph to detect whether a font exists for a given emoji. However, as this method is not available before API 23, they also do test renders and compare the result against the width of 'tofu' (the [x] character you mention in your post.)
There are some other failings with this approach, but it should be enough to get you started.
Google source:
https://android.googlesource.com/platform/packages/inputmethods/LatinIME/+/master/java/src/com/android/inputmethod/keyboard/emoji/EmojiCategory.java#441
https://android.googlesource.com/platform/packages/inputmethods/LatinIME/+/master/java/src/com/android/inputmethod/keyboard/KeyboardLayoutSet.java
Based on Jason Gore answer:
For example create boolean canShowFlagEmoji:
private static boolean canShowFlagEmoji() {
Paint paint = new Paint();
String switzerland = "\uD83C\uDDE8\uD83C\uDDED"; // Here enter Surrogates of Emoji
try {
return paint.hasGlyph(switzerland);
} catch (NoSuchMethodError e) {
// Compare display width of single-codepoint emoji to width of flag emoji to determine
// whether flag is rendered as single glyph or two adjacent regional indicator symbols.
float flagWidth = paint.measureText(switzerland);
float standardWidth = paint.measureText("\uD83D\uDC27"); // U+1F427 Penguin
return flagWidth < standardWidth * 1.25;
// This assumes that a valid glyph for the flag emoji must be less than 1.25 times
// the width of the penguin.
}
}
And then in code whenever when you need to check if emoji is available:
if (canShowFlagEmoji()){
// Code when FlagEmoji is available
} else {
// And when not
}
Surrogates of emoji you can get here, when you click on detail.
An alternative option might be to include the Android "Emoji Compatibility" library, which would detect and add any required Emoji characters to apps running on Android 4.4 (API 19) and later: https://developer.android.com/topic/libraries/support-library/preview/emoji-compat.html
final Paint paint = new Paint();
final boolean isEmojiRendered;
if (VERSION.SDK_INT >= VERSION_CODES.M) {
isEmojiRendered = paint.hasGlyph(emoji);
}
else{
isEmojiRendered = paint.measureText(emoji) > 7;
}
The width > 7 part is particularly hacky, I would expect the value to be 0.0 for non-renderable emoji, but across a few devices, I found that the value actually ranged around 3.0 to 6.0 for non-renderable, and 12.0 to 15.0 for renderable. Your results may vary so you might want to test that. I believe the font size also has an effect on the output of measureText() so keep that in mind.
The second part was answerd by RogueBaneling here how can I check if my device is capable to render Emoji images correctly?

iText image alignment on second page

I am creating a PDF document with images and text in Android using iText. Each page has an image at the top followed by some text. On the first page the image is correctly aligned to the top margin of the page, but on subsequent pages there is a gap of approximately 10 points between the top margin and the top of the image.
Here's my code:
// Create PDF document object
float pageMargin = 72;
document = new com.itextpdf.text.Document(PageSize.A4, pageMargin, pageMargin, pageMargin, pageMargin);
PdfWriter pdfWriter = PdfWriter.getInstance(document, new FileOutputStream(myFile.getAbsoluteFile()));
document.open();
PdfContentByte cb = pdfWriter.getDirectContent();
for (PicturePage picPage : picPageList)
{
// Draw a border on the page
cb.moveTo(pageMargin, pageMargin);
cb.lineTo(pageMargin, (pageHeight - pageMargin));
cb.lineTo((pageWidth - pageMargin), (pageHeight - pageMargin));
cb.lineTo((pageWidth - pageMargin), pageMargin);
cb.lineTo(pageMargin, pageMargin);
cb.stroke();
// Get an image from the file system and scale to required size
String imgFileName = picPage.getImagePath();
image = Image.getInstance(imgFileName);
float fitWidth = 400;
float fitHeight = 300;
image.scaleToFit(fitWidth, fitHeight);
image.setAlignment(Image.ALIGN_CENTER | Image.ALIGN_TOP);
document.add(image);
// Add the text to the page.
String theText = picPage.getText();
String[] arrParagraphs = theText.split("\n");
for (int i=0; i<arrParagraphs.length; i++)
{
String paragraphText = arrParagraphs[i];
Paragraph p = new Paragraph(paragraphText);
document.add(p);
}
// Start a new page
document.newPage();
}
I have tried various combinations of Image.ALIGN... and Image.TEXTWRAP but none of them remove the gap. I tried changing the order of placing the image and the border but no change. I have also tried removing the text and the border but the placement of the image is still the same.
Any ideas how to fix this?
Thanks,
Declan
I hope you won't mind if I share my opinion, but I don't like your code. There are much better ways to render images followed by a caption than the way you do it.
Now let me explain what causes the small gap to appear.
When you first create your document, the value of the leading is zero. (The leading is the distance between the baselines of two consecutive lines). This value changes as soon as you add the first object that defines a leading. In your case, this object is a Paragraph.
Although you do not define a leading explicitly, your paragraph uses a default font (Helvetica) and a default font size (12). The default leading is 1.5 times the font size (18).
Now when you go to the next page, that leading is used, introducing a small gap preceding the image. You could solve this by adding an empty Paragraph with leading 0 before triggering a new page:
document.add(new Paragraph(0));
(Add this before document.newPage();.)
However: if I were you, I'd throw away my code, and I'd add my image and my caption using a PdfPTable with a fixed width and fixed heights for the cells. You can add this table either using document.add(), or using the writeSelectedRows() method. Alternatively I could add the image at an absolute position and add the caption using a ColumnText object. There are many different ways to achieve what you want. The way you do it may work, but it's not optimal.

Prevent Android's TextView from breaking links

This question is probably the same as this one, but since none of its answers really solve the problem, I'll ask again.
My app has a TextView that occasionally will display very long URLs. For aesthetic reasons (and since the URLs contain no spaces), the desirable behaviour would be to fill each line completely before jumping to the next one, something like this:
|http://www.domain.com/som|
|ething/otherthing/foobar/|
|helloworld |
What happens instead, is the URL being broke near the bars, as if they were spaces.
|http://www.domain.com/ |
|something/otherthing/ |
|foobar/helloworld |
I tried extending the TextView class and adding a modified version of the breakManually method (found here) to trick the TextView and do what I need, calling it on onSizeChanged (overridden). It works fine, except for the fact that the TextView is inside a ListView. When this custom TextView is hidden by the scrolling and brought back, its content goes back to the original breaking behaviour, due to the view being re-drawn without onSizeChanged being called.
I was able to work-around this problem by calling breakManually inside onDraw. This presents the expected behaviour at all times, but at a high performance cost: since onDraw is called whenever the ListView is scrolled and the breakManually method isn't exactly "lightweight", the scrolling gets unacceptably laggy, even on a high-end quad-core device.
Next step was to crawl through the TextView source code, trying to figure where and how it splits the text, and hopefully override it. This was a complete failure. I (a newbie) spent the whole day fruitlessly looking at code I mostly couldn't understand.
And that brings me here. Can someone please point me the right direction about what I should override (assuming that it's possible)? Or maybe there is a simpler way of achieving what I want?
Here's the breakManually method I mentioned. Due to the use of getWidth(), it only works if called after the view is measured.
private CharSequence breakManually (CharSequence text) {
int width = getWidth() - getPaddingLeft() - getPaddingRight();
// Can't break with a width of 0.
if (width == 0) return text;
Editable editable = new SpannableStringBuilder(text);
//creates an array with the width of each character
float[] widths = new float[editable.length()];
Paint p = getPaint();
p.getTextWidths(editable.toString(), widths);
float currentWidth = 0.0f;
int position = 0;
int insertCount = 0;
int initialLength = editable.length();
while (position < initialLength) {
currentWidth += widths[position];
char curChar = editable.charAt(position + insertCount);
if (curChar == '\n') {
currentWidth = 0.0f;
} else if (currentWidth > width) {
editable.insert(position + insertCount , "\n");
insertCount++;
currentWidth = widths[position];
}
position++;
}
return editable.toString();
}
To everyone who bothered reading this, thanks for your time.
If you're not using a monospace font, then in some cases it's not even possible to align it very well. Since you have no whitespaces in a URL, it's not likely that a Justify-like alignment will solve the problem. I suggest that you use a monospace font for that particular TextView. Then, decide on a fixed number of characters per line and break the string to display with "\n" after those many characters.
This isn't answering your question but it's the smoothest way possible, I guess.

missing some letters and number in textview in android 4.3+

I create a view like below image in my monodroid application with code. While every thing is right when I test the app in android 4.1 but when I test it on the android 4.3 and 4.4.2 I faced with below screen. I do not test it on the android 4.2
In the textviews any numbers are not showing. It looks that I typed space. Also about some letters.
What is wrong?! What has been changed in the android 4.3 + ?
There is still space for the letters, so perhaps it's an issue with the styling.
Change the style to default, do the letters show up?
Try adding a shadow to the textviews and see if the shadow exists (maybe the letters are somehow transparent, or the same color as the background?)
by referring to #ArieDov comment I post this answer:
Below method that I used caused the problem:
public static void AddShadowEffect (TextView textview)
{
try {
if(RltXmlSettings.Instance.getVal ("shadow_enabled")=="1")
{
float[] direction = new float[] {0.3f, -1.0f, 0.0f};
MaskFilter filter = new EmbossMaskFilter (direction, 0.8f, 15f, 5f);
textview.Paint.SetMaskFilter (filter);
textview.Invalidate ();
}
} catch (Exception ex) {
RltLog.HandleException (ex);
}
}
the purpose of this method was adding the shadows you see in the question image. and I fixed the problem by changing the style of textviews just like what #Matt said. I used below code for my textviews:
txtlName.SetTypeface (null, TypefaceStyle.Bold);
and this fixed my problem.

Categories

Resources