I am trying to display a TextView in Android such that the text in the view is top-aligned:
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// Create container layout
FrameLayout layout = new FrameLayout(this);
// Create text label
TextView label = new TextView(this);
label.setTextSize(TypedValue.COMPLEX_UNIT_PX, 25); // 25 pixels tall
label.setGravity(Gravity.TOP + Gravity.CENTER); // Align text top-center
label.setPadding(0, 0, 0, 0); // No padding
Rect bounds = new Rect();
label.getPaint().getTextBounds("gdyl!", 0, 5, bounds); // Measure height
label.setText("good day, world! "+bounds.top+" to "+bounds.bottom);
label.setTextColor (0xFF000000); // Black text
label.setBackgroundColor(0xFF00FFFF); // Blue background
// Position text label
FrameLayout.LayoutParams layoutParams =
new FrameLayout.LayoutParams(300, 25, Gravity.LEFT + Gravity.TOP);
// also 25 pixels tall
layoutParams.setMargins(50, 50, 0, 0);
label.setLayoutParams(layoutParams);
// Compose screen
layout.addView(label);
setContentView(layout);
}
This code outputs the following image:
The things to note:
The blue box is 25 pixels tall, just like requested
The text bounds are also reported as 25 pixels tall as requested (6 - (-19) = 25)
The text does not start at the top of the label, but has some padding above it, ignoring setPadding()
This leads to the text being clipped at the bottom, even though the box technically is tall enough
How do I tell the TextView to start the text at the very top of the box?
I have two restrictions to possible answers:
I do need to keep the text top-aligned, though, so if there is some trick with bottom-aligning or centering it vertically instead, I can't use it, since I have scenarios where the TextView is taller than it needs to be.
I'm a bit of a compatibility-freak, so if possible I'd like to stick to calls that were available in the early Android APIs (preferably 1, but definitely no higher than 7).
TextViews use the abstract class android.text.Layout to draw the text on the canvas:
canvas.drawText(buf, start, end, x, lbaseline, paint);
The vertical offset lbaseline is calculated as the bottom of the line minus the font's descent:
int lbottom = getLineTop(i+1);
int lbaseline = lbottom - getLineDescent(i);
The two called functions getLineTop and getLineDescent are abstract, but a simple implementation can be found in BoringLayout (go figure... :), which simply returns its values for mBottom and mDesc. These are calculated in its init method as follows:
if (includepad) {
spacing = metrics.bottom - metrics.top;
} else {
spacing = metrics.descent - metrics.ascent;
}
if (spacingmult != 1 || spacingadd != 0) {
spacing = (int)(spacing * spacingmult + spacingadd + 0.5f);
}
mBottom = spacing;
if (includepad) {
mDesc = spacing + metrics.top;
} else {
mDesc = spacing + metrics.ascent;
}
Here, includepad is a boolean that specifies whether the text should include additional padding to allow for glyphs that extend past the specified ascent. It can be set (as #ggc pointed out) by the TextView's setIncludeFontPadding method.
If includepad is set to true (the default value), the text is positioned with its baseline given by the top-field of the font's metrics. Otherwise the text's baseline is taken from the descent-field.
So, technically, this should mean that all we need to do is to turn off IncludeFontPadding, but unfortunately this yields the following result:
The reason for this is that the font reports -23.2 as its ascent, while the bounding box reports a top-value of -19. I don't know if this is a "bug" in the font or if it's supposed to be like this. Unfortunately the FontMetrics do not provide any value that matches the 19 reported by the bounding box, even if you try to somehow incorporate the reported screen resolution of 240dpi vs. the definition of font points at 72dpi, so there is no "official" way to fix this.
But, of course, the available information can be used to hack a solution. There are two ways to do it:
with IncludeFontPadding left alone, i.e. set to true:
double top = label.getPaint().getFontMetrics().top;
label.setPadding(0, (int) (top - bounds.top - 0.5), 0, 0);
i.e. the vertical padding is set to compensate for the difference in the y-value reported from the text bounds and the font-metric's top-value. Result:
with IncludeFontPadding set to false:
double ascent = label.getPaint().getFontMetrics().ascent;
label.setPadding(0, (int) (ascent - bounds.top - 0.5), 0, 0);
label.setIncludeFontPadding(false);
i.e. the vertical padding is set to compensate for the difference in the y-value reported from the text bounds and the font-metric's ascent-value. Result:
Note that there is nothing magical about setting IncludeFontPadding to false. Both version should work. The reason they yield different results are slightly different rounding errors when the font-metric's floating-point values are converted to integers. It just so happens that in this particular case it looks better with IncludeFontPadding set to false, but for different fonts or font sizes this may be different. It is probably fairly easy to adjust the calculation of the top-padding to yield the same exact rounding errors as the calculation used by BoringLayout. I haven't done this yet since I'll rather use a "bug-free" font instead, but I might add it later if I find some time. Then, it should be truly irrelevant whether IncludeFontPadding is set to false or true.
If your TextView is inside an other layout, make sure to check if there is enough space between them. You can add a padding at the bottom of your parent view and see if you get your full text. It worked for me!
Example: you have a textView inside a FrameLayout but the FrameLayout is too small and is cutting your textView. Add a padding to your FrameLayout to see if it work.
Edit: Change this line
FrameLayout.LayoutParams layoutParams =
new FrameLayout.LayoutParams(300, 25, Gravity.LEFT + Gravity.TOP);
for this line
FrameLayout.LayoutParams layoutParams =
new FrameLayout.LayoutParams(300, 50, Gravity.LEFT + Gravity.TOP);
This will make the box bigger and, by the same way, let enough space for your text to be shown.
OR add this line
label.setIncludeFontPadding(false);
This will remove surrounding font padding and let the text be seen. But the only thing that dont work in your case is that it wont show entirely letters like 'g' that goes under the line... Maybe that you will have to change the size of the box or the text just a little (like by 2-3) if you really want it to work.
Related
I have an assignment right now about OpenStreetMaps, where one of the exercises is to display the road names at their respective roads on the map.
My problem right now, is that the coordinates we're using are so small, that even the smallest int font size is hundred times larger than what it's supposed to be.
I have tried the method deriveFont(), but it doesn't seem to have any effect.
g.setPaint(Color.black);
for (DrawnString d : model.getRoads()){
Point2D.Double p = d.getPosition();
Font font = new Font("TimesRoman", Font.PLAIN, 1);
font.deriveFont(0.0001f); //doesn't work!
g.setFont(font);
g.drawString(d.getText(), (float) p.x, (float) p.y);
}
My question is, if there's a way to decrease the font size to a small size like 0.0001f?
The deriveFont() method returns an object of type font that is a replica of the calling font with changed parameters.
So change the line to: font = font.deriveFont(0.001f); and everything works just as expected (with very tiny font)
Okay it's me who's stupid, I just missed a "font =" in front of derivedFont().
font = font.deriveFont(0.0001f);
It works now.
I use a Label with text wrap enabled. In this case, the text wraps wrong. The text wraps into 3 lines, when it should be only 2 and the Label size only reflects the 2 lines. I debugged through the code and found the reason for this to be in the Label.layout() method. Not sure if this is a bug or if I am doing something wrong.
In the code below you can see that the text is set twice to the GlyphLayout. The first time it wraps correct, the second time we use the reduced width and it wraps into more lines as before. I think the second time we set the text into the GlyphLayout, the same width should be used.
public class Label extends Widget {
public void layout () {
...
float width = getWidth(), height = getHeight();
...
GlyphLayout layout = this.layout;
float textWidth, textHeight;
if (wrap || text.indexOf("\n") != -1) {
// Set the text into the GlyphLayout. The text is wrapped correctly here
layout.setText(font, text, 0, text.length, Color.WHITE, width, lineAlign, wrap, ellipsis);
textWidth = layout.width;
textHeight = layout.height;
...
} else {
textWidth = width;
textHeight = font.getData().capHeight;
}
...
// Set the text again into the GlyphLayout. This time with the width that we got when we set it the first time
// This time the text is wrapped wrong as it uses less width as it should
layout.setText(font, text, 0, text.length, Color.WHITE, textWidth, lineAlign, wrap, ellipsis);
...
}
}
Make sure to invalidate() the label and then pack() the label so it calculates any new preferred sizes; this may or may not fix the problem.
Looking at the source code of Label then you can see that the last invocation of layout.setText() is always invoked regardless of text wrapping or not.
The previous layout.setText() invocation is used to set the textWidth and textHeight for the later call where those values are actually used.
If wrapping is on or there is a newline character then the width and height is set to that of the label and if it is off then the width is set to the actual text width and the height set to the font data height.
From the above, another problem that may be causing this to happen is if you have scaled the font and/or label. The scaling factor is not being applied from within Label.layout() which may cause the label size to be that of 2 lines whilst the actual text is 3 lines as the width doesn't allow for overflow with wrapping set to on.
If all else fails, I would ensure that your font files are correct and that there are not any characters or data in your text that may cause a new line to occur.
I would also suggest to use another font of the same glyph width and height and see if the problem persists. If it does not then at least you know it is a problem relating to the font and not the label.
Hope this helped you.
This issue has been solved in libgdx 1.9.12
I have a listView and a timer iterates through each list item. Ideally I'd like to have the currently selected item to automatically center in the listView. I've played around with smoothScrollToPosition() but am lost on how to work out centering the current item.
My corrected code (thanks to Matej Spili):
// Smooth scroll the list item
try {
scrollView.smoothScrollBy(scrollView.getChildAt().getTop() - (scrollView.getHeight() / 2) + (scrollView.getChildAt().getHeight() / 2), 1500);
}
catch (NullPointerException e)
{
System.out.println("NULL POINTER DEALT WITH");
}
First, get the height of the ListView using getHeight, which returns the height of the ListView in pixels.
Then, get the height of the row's View using the same method.
Then, use setSelectionFromTop and pass in half of the ListView's height minus half of the row's height.
Something like:
int h1 = mListView.getHeight();
int h2 = v.getHeight();
mListView.setSelectionFromTop(position, h1/2 - h2/2);
Or, instead of doing the math, you might just pick a constant for the offset from the top, but I would think it might be more fragile on different devices since the second argument for setSelectionFromTop appears to be in pixels rather than device independent pixels.
I haven't tested this code, but it should work as long as your rows are all roughly the same height.
Or in one line:
scrollView.smoothScrollTo(0, selectedView.getTop() - (scrollView.getHeight() / 2) + (selectedView.getHeight() / 2), 0);
I have an app that takes in input from the user and then on the clicking of a button displays the calculated results in a format like so:
123456
213456
214356
124365
I need a line ( preferably blue) to join each of the number 2's in the list as they make their way down the TextView.
I also need the option of not having this line if the user does not want it.
What I have tried so far:
I extended a TextView class and overrode the onDraw(Canvas) method and tried to get something working and managed to display vertical blue lines but couldnt get the joining of the number 2's. I am unsure how android decides when onDraw() is called, as I dont call it in my code but I would rather I could so I could control when it displays the lines or not.
This is only idea:
What you have:
Given the text size lets say: 20px
X and Y coordinates of the view.
Gravity supplied to the content by the TextView if any
Text size in px. getTextSize() or paint.measureText(); [rect.right on Canvas is the text size]
Text ems. getEms()
Optional: Padding or margin if any
What you need to find:
Each x-coordinate of each letter on the canvas.
Each y-coordinate of each letter on the canvas.
How to:
x = letter position in the text * font size(or ems). if there is padding or margin add padding left + padding right to the x; [rect.left on Canvas is x]
Reset letter position each new line
y = line number * font size (or ems). + padding top + padding bottom [rect.bottom on Canvas is y]
Collect all approximate x and y's to form a Point.
Match each Point in onDraw()
If you think you want to use paint.measureText("13332", 0, "13332".indexOf("2")), measured width will be approximate and absolute to itself x-coordinate of "2". Relative to its parent might be much more complex to find.
Edit:
What I wrote above can be easily obtained using paint.[getTextBounds()][1] method which will give you Rect from which you can form a Point
I would proceed this way.
The user enters the value and clicks the button
As the button is tapped hide the TextView and replace it with an ImageView or keep the TextView and put the ImageView on the top of the TextView (Overlap of the ImageView on the TextView can be achieved with a FrameLayout
Draw in the ImageView the lines and the numbers in the OnDraw() method
As the user taps on the ImageView, hide the ImageView and display the TextView again.
Finally if the position of the 2's is fixed you don't need to measure the text.
If the posision is not fixed you can put the numbers in an array. Here is some code in mixed order (not tested)
var resStr1 = Integer.toString(resultNumber);
var position = 0;
...
// METHOD 1, multiple 2's in a number
var j = 0;
for (int i = 0; i < resStr1.length(); i++){
char c = s.charAt(i);
if (c == '2') {
position[j] = i;
j++;
}
}
// METHOD 2. ONLY ONE 2 in a number
position = resStr1.indexOf('2');
// CONTINUE
...
Paint p = new Paint();
...
float left = customMarginLeft + p.measureText(resStr1.substring(0, position));
float top = customMarginTop;
...
canvas.drawRect( bla bla bla...
drawText() // ??
Get a look also to getTextBounds() if you wanna to get the text height too.
About the onDraw method, don't take care of how much time it will be called by the system, if performance is critical just mantain the scope of the variables containing the results and the precalculations global, put in the main class the properties and methods that controls the behaviour of the drawing method. The system will redraw every time the lines again and again as soon as another element overlaps your control or something happens in the system so the fact that the onDraw is called again and again is normal, otherwise your lines will not be redrawn again and could disappear from the screen if something happens.
Of course the code above can be also put in a custom control (combined control).
To clear the lines you should call the invalidate() or postInvalidate() method. This methods will clear the whole area and force the onDraw() to be called again. Then put globally a flag like
shouldRedrawLines = false;
and in the onDraw() do something like this:
if (shouldRedrawLines) { // please note that the onDraw is called again and again and this condition allows you to check if in another part of the program you decided to clear the lines
DrawLines(); // contains the code for redrawing lines
}
DrawNumbersFromResult(); // contains the code for redrawing Numbers
Simple not ?
I have some text that will be put in a TextView. I did that using setText().
Now I need to find the number of lines or height the text occupies in the TextView. I tried using getHeight(), but it always returns 0.
Is there anyway to get the height of the text present in the TextView?
Because this size is known only after the layout is finished, one way is to create a custom text view and use onSizeChanged method. Another way is this:
final TextView tv = (TextView) findViewById(R.id.myTextView);
ViewTreeObserver vto = tv.getViewTreeObserver();
vto.addOnGlobalLayoutListener(new OnGlobalLayoutListener() {
#Override
public void onGlobalLayout() {
Toast.makeText(MyActivity.this, tv.getWidth() + " x " + tv.getHeight(), Toast.LENGTH_LONG).show();
tv.getViewTreeObserver().removeGlobalOnLayoutListener(this);
}
});
This code I've found here: How to retrieve the dimensions of a view?
As John pointed out, you won't be able to get the height immediately after setText. I'm not sure using getHeight() on the TextView itself will help you much. The height of the view is not only dependant on the height of the visible text in the view, but also on the viewgroup/layout the TextView resides in. If the viewgroup tells the TextView to maximize height, getHeight() will get you nowhere even if you wait until after the text is rendered.
I see a number of ways that this could be done:
Subclass TextView and overwrite the onSizeChanged method. In there, call supers onSizeChanged, then get the number of lines within the TextView by getLineCount() and the height per line with getLineHeight(). This might or might not be better than using getHeight(), depending on your layout or whatnot.
Don't use the Textviews dimensions. Get the TextViews Paint with TextView.getPaint(), and then calculate the width and height from
Rect bounds;
paint.getTextBounds(text, 0, text.length(), bounds);
You'll now have the dimensions in bounds. You can now work with paint.breakText to see how much text you'll fit on one line. Probably too much hassle and not guaranteed (to my untrained eye) to be the same logic as used by TextView.