I have a single TextView which I am updating as a counter every second. If I use wrap_content for its width, I get performance warnings that it takes 50ms to draw the view update in the layout/measure phase. When I programmatically set its width and height, it addresses the alerts, but my views no longer resize (obviously :). What am I missing here? Is there way to minimize view invalidations / resizing to be only when the text changes to a greater width? Do I have to do that programmatically and calculate the new size each time I update the view?
This is not really a solution to why it was happening in the first place, but a workout around (Which feels very anti-android since it doesn't rely on ConstraintLayout to solve what hopefully is a simple system of equations to determine the proper intrinsic TextView width without breaking the whole UI thread):
fun TextView.conditionallyChangeWidthWithNewString(newString: CharSequence) {
val calculatedWidth = getWidthOfNewText(newString.toString())
if (calculatedWidth > layoutParams.width) {
val params = getLayoutParams()
params.width = calculatedWidth
setLayoutParams(params)
}
setTextIfChanged(newString)
}
fun TextView.getWidthOfNewText(s: String): Int {
val p = Paint()
val bounds = Rect()
val plain = typeface
p.setTypeface(plain)
p.textSize = textSize
p.getTextBounds(s, 0, s.length, bounds)
val mt = p.measureText(s)
return mt.toInt()
}
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()
So I need to set up 2 things:
Icon for helper text in TextInputLayout
Center it to TOP, since helper text might be multiline
It might look something like this:
Is there easy of doing working around this, without making my own TextInputLayout?
Solving this melts down to following 2 possible solutions:
First is to change the Rect of the Drawable itself by setting the bounds (coordinates X and Y), drawback of this solution is whenever layout is redrawn (orientation change etc.), this has to be done again.
findViewById<AppCompatTextView>(R.id.textinput_helper_text)?.run {
compoundDrawables.first()?.run {
setBounds(
bounds.left,
bounds.top - this#with.height.div(2),
bounds.right,
bounds.bottom - this#with.height.div(2)
)
bottom = this#with.height.div(2)
}
}
Second solution is to add new ImageView in front of the helper TextView and add padding to it, so it's pushed up.
It's not a one liner, but it can be formed into easy to use method as such:
fun TextInputLayout.setStartHelperTextIcon(
#DrawableRes drawableRes: Int,
paddingPx: Int
) {
with(findViewById<AppCompatTextView>(R.id.textinput_helper_text)) {
(this?.parent?.parent as? LinearLayout)?.run {
if(findViewById<ImageView>(R.id.text_input_layout_helper_icon) == null) {
addView(
ImageView(context).apply {
id = R.id.text_input_layout_helper_icon
layoutParams = ViewGroup.LayoutParams(
ViewGroup.LayoutParams.WRAP_CONTENT,
ViewGroup.LayoutParams.WRAP_CONTENT
)
setPadding(
paddingPx,
0,
paddingPx,
paddingPx + this#with.height.div(2)
)
},
0
)
}
}
}
}
I want to display a box with suggested entries next to an EditText.
I figured I'd have to set LayoutParams (like suggested in this question.
However, no matter what I try, the box always ends up in the top left corner.
What I tried:
- get LayoutParams as MarginLayoutParams (which they are), and set them. Set them back in the view for good measure.
- All the answers in the link mentioned above
Change the position with TranslationX and -Y (which works, but runs into other problems like status bar might not always be 24dp and seems a hack)
val layoutParams = box?.layoutParams
//val layoutParams = ConstraintLayout.LayoutParams(WRAP_CONTENT, WRAP_CONTENT).apply{
// setMargins(left, top, 0,0)
//}
if (layoutParams !is ViewGroup.MarginLayoutParams) return#onTextChanged
//layoutParams.leftMargin = left
layoutParams.marginStart = left
layoutParams.topMargin = top
box?.layoutParams = layoutParams // Don't think this is necessary
Log.d(TAG, "$layoutParams")
// works, but don't like it
// box?.translationX = left.toFloat()
// box?.translationY = top.toFloat()
//doesn't work
//box?.setMargins(left, top, right, 1000)
the function View.Setmargins is as follows:
fun View.setMargins(left: Int, top: Int, right: Int, bottom: Int) {
if (layoutParams is MarginLayoutParams) {
val p = layoutParams as MarginLayoutParams
p.setMargins(left, top, right, bottom)
requestLayout()
}
}
Complete source of the thing I want to do here (snippet above is at line 170, edited it a bit to show things I've tried)
Other things I have tried: add box to different layouts, which in all cases results in the box being drawn in the top-left corner of that layout
For anybody having the same problem as I had:
Mike M. 's suggestion to get the LayoutParams as ConstraintLayout.LayoutParams, and also set its topToTop and startToStart fields to ConstraintLayout.LayoutParams.PARENT_ID did the trick, ie.
(box?.layoutParams as ConstraintLayout.LayoutParams).apply{
topToTop = ConstraintLayout.LayoutParams.PARENT_ID
startToStart = ConstraintLayout.LayoutParams.PARENT_ID
topMargin = top
marginStart = left
}
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
I have a TextView with an unknown maximum height, which is dependent on the device's DPI/screen resolution. So, for instance, on and MDPI device this maximum height makes it possible to show only 2 lines at a time, a value that can be increased up to an undefined number.
My issue is related with the ellipsize functionality. Let's suppose that a certain device allows for 4 lines to be displayed. If I manually set the maximum number of lines, like this...
<TextView
android:id="#+id/some_id"
android:layout_width="fill_parent"
android:layout_height="0dip"
android:ellipsize="end"
android:maxLines="4"
android:singleLine="false"
android:layout_weight="1"
android:gravity="center_vertical"
android:text="This is some really really really really really long text"
android:textSize="15sp" />
...everything works OK. If the text doesn't fit properly, then the ellipsis are added at the end of the fourth line, like this:
This is some
really really
really really
really long...
But I'd rather not set the number of lines as a static variable, as I would prefer to include support for any combination of DPI/screen resolution. So if I remove maxLines the ellipsis is no longer correctly shown at line four, showing instead an incomplete portion of text:
This is some
really really
really really
really long
If I slightly increase the TextView size, I can see that the rest of the text is still being drawn "behind" the other Views. Setting the variable maxHeight doesn't seem to work either.
I really can't seem to find a solution for this issue. Any ideas? If it helps, I'm working only with Android v4.0.3 and up (API level 15).
Calculate how many lines fit into the TextView with TextView#getHeight() and TextView#getLineHeight(). Then call TextView#setMaxLines().
ViewTreeObserver observer = textView.getViewTreeObserver();
observer.addOnGlobalLayoutListener(new OnGlobalLayoutListener() {
#Override
public void onGlobalLayout() {
int maxLines = (int) textView.getHeight()
/ textView.getLineHeight();
textView.setMaxLines(maxLines);
textView.getViewTreeObserver().removeGlobalOnLayoutListener(
this);
}
});
The accepted answer works well up to API 27. However, since API 28, if the line height was not set (by one of the follow methods), by default Android adds extra spacing between lines, but not after the last line.
Set attribute android:lineHeight=... (documentation) in your layout XML
Calls textView.setLineHeight(...) in your source code.
To find out the new line height for API 28 and above, I used textView.getLineBounds().
Kotlin
val observer = textView?.viewTreeObserver
observer?.addOnGlobalLayoutListener(object : ViewTreeObserver.OnGlobalLayoutListener {
override fun onGlobalLayout() {
textView?.let { view ->
val lineHeight: Int
lineHeight = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
val bounds = Rect()
textView.getLineBounds(0, bounds)
bounds.bottom - bounds.top
} else {
textView.lineHeight
}
val maxLines = textView.height / lineHeight
textView.maxLines = maxLines
textView.viewTreeObserver.removeOnGlobalLayoutListener(this)
}
}
})
Android Java
ViewTreeObserver observer = textView.getViewTreeObserver();
observer.addOnGlobalLayoutListener(new OnGlobalLayoutListener() {
#Override
public void onGlobalLayout() {
int lineHeight = textView.getLineHeight();
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
Rect bounds = new Rect();
textView.getLineBounds(0, bounds);
lineHeight = bounds.bottom - bounds.top;
}
int maxLines = (int) textView.getHeight() / lineHeight;
textView.setMaxLines(maxLines);
textView.getViewTreeObserver().removeGlobalOnLayoutListener(
this);
}
});