I need two TextViews to have same baseline, ascent, bottom, descent values. Is it possible to change the FontMetrics of TextView after text has been drawn?
It is so late, but I also confronted with this issue, and I solve with some tricks. (just modify baseline, not ascent, descent or something)
note that fontMetrics' values(ascent, descent, bottom, top) are depend on textSize.
so we must use fontMetrics' values after view is drawn. (if it is autoSizing or something)
to do something after view drawn, using Sa Qada's answer
baseline is always 0. descent, ascent, top, bottom is relative values.
refer this Suragch's answer.
change fontMetrics' values does not affect to UI.
to set custom baseline(text position of textview), just use padding.
below code is align to baseline with two TextView.
// this is kotlin code, but I'm sure you can understand
// align textview2 to textview1's baseline
val tv1Baseline = tv1.paddingBottom + tv1.paint.fontMetrics.bottom
val tv2Baseline = tv2.paddingBottom + tv2.paint.fontMetrics.bottom
val newPaddingOffset = tv1Baseline - tv2Baseline
tv2.setPadding(tv2.paddingLeft, tv2.paddingTop, tv2.paddingRight,
tv2.paddingBottom + newPaddingOffset)
Here's solution to align baselines (note the difference from bottom in Suragch answer and definition of return value of TextView.getBaseline()) which alignes tv2 baseline to tv1s':
private void alignWeatherTextBaseline(View tv1, View tv2) {
int tv1Baseline = tv1.getTop() + tv1.getBaseline();
int tv2Baseline = tv2.getTop() + tv2.getBaseline();
int newPaddingOffset = tv1Baseline - tv2Baseline;
if (newPaddingOffset != 0) {
tv2.setPadding(tv2.getPaddingLeft(),
tv2.getPaddingTop(), tv2.getPaddingRight(),
tv2.getPaddingBottom() - newPaddingOffset);
}
}
Be aware of the following:
if multiline text is possible, you need to write proper getLastLineBaseline() and use it instead of standard getBaseline()
you can get invalid value (0 or -1) from getBaseline() if a view hasn't been layout yet
Related
I am trying to replace my existing GridLayout with flexbox layout. I have a layout that requires me to have exactly 4 columns and two rows.
Only way I have found this to work is by code
val rootFlexboxLayout = binding.moreActionLayout
rootFlexboxLayout.flexDirection = FlexDirection.ROW
rootFlexboxLayout.justifyContent = JustifyContent.FLEX_START
rootFlexboxLayout.alignContent = AlignContent.FLEX_START
rootFlexboxLayout.alignItems = AlignItems.STRETCH
rootFlexboxLayout.flexWrap = FlexWrap.WRAP
val flexboxLayoutParams: FlexboxLayout.LayoutParams = FlexboxLayout.LayoutParams(WRAP_CONTENT, WRAP_CONTENT)
flexboxLayoutParams.flexBasisPercent = 0.23f
// Manually create views to be added to flexbox layout
tabs.forEachIndexed { index, tabViewState ->
// Add the tab to the layout
val tabView = BottomBarTabView(requireContext())
tabView.apply {
id = View.generateViewId()
layoutParams = flexboxLayoutParams
}
binding.moreActionLayout.addView(tabView, index)
}
Thing that is bothering me is magic number of 0.23f is the only thing that gets me 4 columns, if I set to 0.25 (25%) it gives me 3 columns.
My layout definition for moreActionLayout looks like this:
<com.google.android.flexbox.FlexboxLayout
android:id="#+id/moreActionLayout"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:alignContent="flex_start"
app:alignItems="center"
app:flexWrap="wrap"
app:flexDirection="row"
android:padding="8dp">
</com.google.android.flexbox.FlexboxLayout>
As I am new to android, I had to study Flex layout but seeing attributes of flex layout with identical names I got confused. This is what I did.
Flex Direction = change axis direction (row or column)
flex wrap = single or multliline flex container (no_wrap or wrap)
justifyContent = alignment along x axis (left, right, center, space
around/between/evenly)
alignItems = alignment along y axis(top, bottom, center, stretch upto
whole height)
alignContent = combination of justify + align (left + top,
right+bottom, center + center,space around + stretch ,space between +
stretch, )
I might be wrong in interpreting these attributes. I found two attributes applicable to your query i.e
layout_minWidth & layout_maxWidhth
From documentation ,
layout_minWidth
These attributes impose minimum size constraints for the children of FlexboxLayout. A child view won't shrink less than the value of these attributes (varies based on the flexDirection attribute as to which attribute imposes the size constraint along the main axis) regardless of the layout_flexShrink attribute
layout_maxWidth
These attributes impose maximum size constraints for the children of FlexboxLayout. A child view won't be expanded more than the value of these attributes (varies based on the flexDirection attribute as to which attribute imposes the size constraint along the main axis) regardless of the layout_flexGrow attribute.
So,I guess, just applying flexiWrap attribute to wrap in flexlayout and for children's applying maxWidth or minWidth to a value , you can achieve solution to your query.
To get the value of maxWidth in particular orientation, calculation is
private fun FlexboxLayout.getMaximumWidthForChild(columns: Int): Int {
val displayMetrics = this.resources.displayMetrics
val widthInPixels = displayMetrics.widthPixels - marginStart - marginEnd
val maxWidthForChildInPixels = widthInPixels / columns
val childWidthInDp = (maxWidthForChildInPixels * DisplayMetrics.DENSITY_DEFAULT)/displayMetrics.densityDpi
Log.d("myTag","Width in Pixels = $widthInPixels Max child Pixels = $maxWidthForChildInPixels ")
Log.d("myTag","Child Width in Dp = $childWidthInDp")
// px = dp * (dpi / 160)
return childWidthInDp
}
This method returns maxWidth a child can have for given x columns. As you are adding views programmatically, you can set the layout parameters on the child view to have this maxWidth or just set the width. For different orientation, you ought to call this method in onResume() and load the views again which is kind of bad but it depends upon the no. of views to be load.
Thanks Vivek for your suggestion above. That didn't really work. But since I am using flex_start I was able to obtain desired results using the following.
private fun flexPercent(): Float {
val padding: Float = binding.moreActionLayout.paddingStart.toFloat() / resources.displayMetrics.widthPixels.toFloat()
return FLEXBOX_PERCENT - padding
}
And then I used this method to set flexBasisPercent value, with FLEXBOX_PERCENT set to 0.25
flexboxLayoutParams.flexBasisPercent = flexPercent()
For example, I have a string «One two three four five» to be set in TextView.
If the text occupies only one line, there’s no change, and it’s OK
If text occupies two lines, then:
expected result is: «One two \n three four five» (exactly after
"two")
actual result is: «One two three four \n five» (the line break
can be anywhere)
Also I have a blank TextView in ConstraintLayout with zero width, so I don’t know the width of the textView before I set text.
How can I achieve this?
As far as I know, the width of text can be measured before setting text and divided by the textView's width, but unfortunately it doesn't work for me, as I don't know the width of textview.
Here's my code:
override fun setText(textView: TextView, prefix: String, postfix: String) {
var fullText = "$prefix $postfix"
val lines = getLinesCount(textView, fullText)
if (lines > 1) {
fullText = fullText.replace(postfix, "\n$postfix")
}
textView.text = fullText
}
private fun getLinesCount(textView: TextView, text: String): Int {
val paint = Paint()
paint.textSize = textView.textSize
val rect = Rect()
paint.getTextBounds(text, 0, text.length, rect)
// I'm not sure how to calculate textView actual width
return (ceil(rect.width().toFloat() / textView.width)).toInt()
}
TextView Supports getLineCount(); which return line count after textview is drawn on UI.
int lineCount = textView.getLineCount();
if(lineCount > 2) {
//your line break logic goes here.
} else {
//normal logic
}
See this doc.
You need to use Html.fromHtml() to format XML textview in html format.
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
textView.setText(Html.fromHtml("One two <br> three four five", Html.FROM_HTML_MODE_COMPACT));
} else {
textView.setText(Html.fromHtml("One two <br> three four five"));
}
You can also refer to this link
Part of my app displays a single 5-character word in a TextView which should fill the entire screen. The questions listed below address this same issue:
How to adjust text font size to fit textview
Have TextView scale its font size to fill parent?
Text size and different android screen sizes
Auto-fit TextView for Android
However, since my case requires only one 5-character word to fill the entire screen, is there a simpler way to do this? If not, would it be practical to simply provide drawable resources (assuming that the 5-character word will always be the same)?
Thanks!
I'm going to give you some code which shows the exact opposite of what you requested. The code below shrinks a line of text until it fits within the desired area. But there is the basic idea: Get the paint of the textView and then measure it. Adjust the size and remeasure it e.g. textView.getPaint().measureText("Your text")
public static float calculateTextSizeToFit(TextView textView, String desiredText, int limitSpSize, float desiredTxtPxSize) {
Paint measurePaint = new Paint(textView.getPaint());
measurePaint.setTextSize(desiredTxtPxSize);
float pWidth = measurePaint.measureText(desiredText);
float labelWidth = textView.getWidth();
int maxLines = textView.getMaxLines();
while (labelWidth > 0 && pWidth/maxLines > labelWidth-20) {
float textSize = measurePaint.getTextSize();
measurePaint.setTextSize(textSize-1);
pWidth = measurePaint.measureText(desiredText);
if (textSize < TypedValue.applyDimension(
TypedValue.COMPLEX_UNIT_SP, limitSpSize,
textView.getContext().getResources().getDisplayMetrics())) break;
}
return measurePaint.getTextSize();
}
I think this is the opposite of
Set width of TextView in terms of characters
I have a TextView where I'm showing some report data. I use a monospace TypefaceSpan for a portion of it because I want columns to line up.
I used my test Android device to figure out how many columns I could fit, but the Android emulator seems to have exactly one less column, which makes things wrap in an ugly way in portrait mode.
Is there a way to find out how many characters should fit on 1 line?
The answer would be to use the breakText() of your textView's Paint Object. Here is a Sample ,
int totalCharstoFit= textView.getPaint().breakText(fullString, 0, fullString.length(),
true, textView.getWidth(), null);
Now totalCharstoFit contains the exact characters that can be fit into one line. And now you can make a sub string of your full string and append it to the TextView like this,
String subString=fullString.substring(0,totalCharstoFit);
textView.append(substring);
And to calculate the remaining string, you can do this,
fullString=fullString.substring(subString.length(),fullString.length());
Now the full code,
Do this in a while loop,
while(fullstirng.length>0)
{
int totalCharstoFit= textView.getPaint().breakText(fullString, 0, fullString.length(),
true, textView.getWidth(), null);
String subString=fullString.substring(0,totalCharstoFit);
textView.append(substring);
fullString=fullString.substring(subString.length(),fullString.length());
}
Well you could do math to find this out, find the width of the character, divide the width of the screen by this, and you'd have what you're looking for.
But is it not possible to design it better? Are there any columns you can group together? Display as a graphic, or even exclude completely?
Another possible solution is to use something like a viewpager. (Find out how many columns' width fit on the first page, and then split the remaining tables onto the second page).
You can get total line of Textview and get string for each characters by below code.Then you can set style to each line whichever you want.
I set first line bold.
private void setLayoutListner( final TextView textView ) {
textView.getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
#Override
public void onGlobalLayout() {
textView.getViewTreeObserver().removeGlobalOnLayoutListener(this);
final Layout layout = textView.getLayout();
// Loop over all the lines and do whatever you need with
// the width of the line
for (int i = 0; i < layout.getLineCount(); i++) {
int end = layout.getLineEnd(0);
SpannableString content = new SpannableString( textView.getText().toString() );
content.setSpan(new StyleSpan(android.graphics.Typeface.BOLD), 0, end, 0);
content.setSpan(new StyleSpan(android.graphics.Typeface.NORMAL), end, content.length(), 0);
textView.setText( content );
}
}
});
}
Try this way.You can apply multiple style this way.
You can also get width of textview by:
for (int i = 0; i < layout.getLineCount(); i++) {
maxLineWidth = Math.max(maxLineWidth, layout.getLineWidth(i));
}
I'm going to display a complete text document on several pages in an android app. Each page contains a TextView control which is in charge to display text. It also contains a next button, which is in charge to update textview and follow the text just from where it ended in last displayed section.
The problem is how can i find the last displayed word in displayable area of TextView?
Note: In this app, textView is not supposed to be scrolled or use ellipsize.
It also must be noted textsize can be increased or decreased.
To understand the situation better:
Sample Text to be displayed:
This is whole text. At first Page It just ended here and must be
followed from here.
And this is the sample illustration of the its display. The question is how to find word "here" as last word displayed in page 1?
I can't try this idea now but let's give it a shot! According to this post you can use the following method to find out if a string fits in a textview.
private boolean isTooLarge (TextView text, String newText) {
float textWidth = text.getPaint().measureText(newText);
return (textWidth >= text.getMeasuredWidth ());
}
So one idea, if the text is always the same, you can define the initial values for each text view manually and then when you increase or decrease the font you recalculate it, removing or adding words.
If it's not an option to input the initial values manually you can do something like:
String wordsInTextview = new String();
int lastUsedWord= 0;
int totalNumberOfWords = text.size();
for (int i=lastUsedWord;i<totalNumberOfWords;i++) {
if !(isTooLarge(TextView,wordsInTextview)) {
wordsInTextview = wordsInTextview + text(i); // add the words to be shown
} else {
TextView.setText(wordsInTextView);
lastUsedWord= i;
wordsInTextView = new String(); // no idea if this will erase the textView but shouldn't be hard to fix
}
}
You also would need to store the position of the first word of the textView, so when you resize the text you know where to start from!
int firstWordOnTextView = TextView.getText().toString().split(" ")[0]
And when it resizes you use it in the same method to calculate the text on the screen.
lastUsedWord = firstWordOnTextView;
If you want to be even faster, you can keep a track of how many words you have on each page, make an average and after a few runs always stat your loop from there. Or a few words before to avoid having to iterate back.
I believe this a reasonable solution if you don't have to display too many pages at once!
Sorry for mistakes in the code I don't have where to try it now! Any comments about this solution? Very interesting question!
Finally I developed piece of code which can find last displayed word.
Considering that we are going to use a text view with properties width and height as its size and textSize in dp as size of text, function formatStringToFitInTextView evaluates the last displayed word in the textview and returns it to as function output.
In below sample we considered SERIF font as TextView typeface.
String formatStringToFitInTextView(int width, int heigth, String inputText,float textSize) {
String[] words;
String lastWord = "";
String finalString="";
StaticLayout stLayout;
int i,numberOfWords;
int h;
Typeface tf = Typeface.SERIF;
TextPaint tp = new TextPaint();
tp.setTypeface(tf );
tp.setTextSize(textSize); //COMPLEX_UNIT_DP
words = inputText.split(" "); //split input text to words
numberOfWords = words.length;
for (i=0;i<numberOfWords;i++){
stLayout= measure(tp,finalString+words[i]+" ",width);
h=stLayout.getHeight();
if (stLayout.getHeight() > heigth) break;
finalString += words[i]+" ";
lastWord = words[i];
}
return lastWord;
}
StaticLayout measure( TextPaint textPaint, String text, Integer wrapWidth ) {
int boundedWidth = Integer.MAX_VALUE;
if( wrapWidth != null && wrapWidth > 0 ) {
boundedWidth = wrapWidth;
}
StaticLayout layout = new StaticLayout( text, textPaint, boundedWidth, Alignment.ALIGN_NORMAL, 1.0f, 0.0f, false );
return layout;
}
The total logic to do actions is split the input text into words and add words one by one to textview till it fills up the provided height and width and then return the last added word as last displayed word.
Note: Thanks to Moritz, I used function measure from this answer.