Custom view's onMeasure: how to get width based on height - android

The previous version of my question was too wordy. People couldn't understand it, so the following is a complete rewrite. See the edit history if you are interested in the old version.
A RelativeLayout parent sends MeasureSpecs to its child view's onMeasure method in order to see how big the child would like to be. This occurs in several passes.
My custom view
I have a custom view. As the view's content increases, the view's height increases. When the view reaches the maximum height that the parent will allow, the view's width increases for any additional content (as long as wrap_content was selected for the width). Thus, the width of the custom view is directly dependant on what parent says the maximum hight must be.
An (inharmonious) parent child conversation
onMeasure pass 1
The RelativeLayout parent tells my view, "You can be any width up to 900 and any height up to 600. What do you say?"
My view says, "Well, at that height, I can fit everything with a width of 100. So I'll take a width of 100 and a height of 600."
onMeasure pass 2
The RelativeLayout parent tells my view, "You told me last time that you wanted a width of 100, so let's set that as an exact width. Now, based on that width, what kind of height would you like? Anything up to 500 is OK."
"Hey!" my view replies. "If you're only giving me a maximum hight of 500, then 100 is too narrow. I need a width of 200 for that height. But fine, have it your way. I won't break the rules (yet...). I'll take a width of 100 and a height of 500."
Final result
The RelativeLayout parent assigns the view a final size of 100 for the width and 500 for the height. This is of course too narrow for the view and part of the content gets clipped.
"Sigh," thinks my view. "Why won't my parent let me be wider? There is plenty of room. Maybe someone on Stack Overflow can give me some advice."

Update: Modified code to fix some things.
First, let me say that you asked a great question and laid out the problem very well (twice!) Here is my go at a solution:
It seems that there is a lot going on with onMeasure that, on the surface, doesn't make a lot of sense. Since that is the case, we will let onMeasure run as it will and at the end pass judgment on the View's bounds in onLayoutby setting mStickyWidth to the new minimum width we will accept. In onPreDraw, using a ViewTreeObserver.OnPreDrawListener, we will force another layout (requestLayout). From the documentation (emphasis added):
boolean onPreDraw ()
Callback method to be invoked when the view tree is about to be drawn. At this point, all views in the tree have been measured and
given a frame. Clients can use this to adjust their scroll bounds or
even to request a new layout before drawing occurs.
The new minimum width set in onLayout will now be enforced by onMeasure which is now smarter about what is possible.
I have tested this with your example code and it seems to work OK. It will need much more testing. There may be other ways to do this, but that is the gist of the approach.
CustomView.java
import android.content.Context;
import android.util.AttributeSet;
import android.util.Log;
import android.view.View;
import android.view.ViewTreeObserver;
public class CustomView extends View
implements ViewTreeObserver.OnPreDrawListener {
private int mStickyWidth = STICKY_WIDTH_UNDEFINED;
public CustomView(Context context) {
super(context);
}
public CustomView(Context context, AttributeSet attrs) {
super(context, attrs);
}
#Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
logMeasureSpecs(widthMeasureSpec, heightMeasureSpec);
int desiredHeight = 10000; // some value that is too high for the screen
int desiredWidth;
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
int widthSize = MeasureSpec.getSize(widthMeasureSpec);
int heightMode = MeasureSpec.getMode(heightMeasureSpec);
int heightSize = MeasureSpec.getSize(heightMeasureSpec);
int width;
int height;
// Height
if (heightMode == MeasureSpec.EXACTLY) {
height = heightSize;
} else if (heightMode == MeasureSpec.AT_MOST) {
height = Math.min(desiredHeight, heightSize);
} else {
height = desiredHeight;
}
// Width
if (mStickyWidth != STICKY_WIDTH_UNDEFINED) {
// This is the second time through layout and we are trying renogitiate a greater
// width (mStickyWidth) without breaking the contract with the View.
desiredWidth = mStickyWidth;
} else if (height > BREAK_HEIGHT) { // a number between onMeasure's two final height requirements
desiredWidth = ARBITRARY_WIDTH_LESSER; // arbitrary number
} else {
desiredWidth = ARBITRARY_WIDTH_GREATER; // arbitrary number
}
if (widthMode == MeasureSpec.EXACTLY) {
width = widthSize;
} else if (widthMode == MeasureSpec.AT_MOST) {
width = Math.min(desiredWidth, widthSize);
} else {
width = desiredWidth;
}
Log.d(TAG, "setMeasuredDimension(" + width + ", " + height + ")");
setMeasuredDimension(width, height);
}
#Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
int w = right - left;
int h = bottom - top;
super.onLayout(changed, left, top, right, bottom);
// Here we need to determine if the width has been unnecessarily constrained.
// We will try for a re-fit only once. If the sticky width is defined, we have
// already tried to re-fit once, so we are not going to have another go at it since it
// will (probably) have the same result.
if (h <= BREAK_HEIGHT && (w < ARBITRARY_WIDTH_GREATER)
&& (mStickyWidth == STICKY_WIDTH_UNDEFINED)) {
mStickyWidth = ARBITRARY_WIDTH_GREATER;
getViewTreeObserver().addOnPreDrawListener(this);
} else {
mStickyWidth = STICKY_WIDTH_UNDEFINED;
}
Log.d(TAG, ">>>>onLayout: w=" + w + " h=" + h + " mStickyWidth=" + mStickyWidth);
}
#Override
public boolean onPreDraw() {
getViewTreeObserver().removeOnPreDrawListener(this);
if (mStickyWidth == STICKY_WIDTH_UNDEFINED) { // Happy with the selected width.
return true;
}
Log.d(TAG, ">>>>onPreDraw() requesting new layout");
requestLayout();
return false;
}
protected void logMeasureSpecs(int widthMeasureSpec, int heightMeasureSpec) {
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
int widthSize = MeasureSpec.getSize(widthMeasureSpec);
int heightMode = MeasureSpec.getMode(heightMeasureSpec);
int heightSize = MeasureSpec.getSize(heightMeasureSpec);
String measureSpecHeight;
String measureSpecWidth;
if (heightMode == MeasureSpec.EXACTLY) {
measureSpecHeight = "EXACTLY";
} else if (heightMode == MeasureSpec.AT_MOST) {
measureSpecHeight = "AT_MOST";
} else {
measureSpecHeight = "UNSPECIFIED";
}
if (widthMode == MeasureSpec.EXACTLY) {
measureSpecWidth = "EXACTLY";
} else if (widthMode == MeasureSpec.AT_MOST) {
measureSpecWidth = "AT_MOST";
} else {
measureSpecWidth = "UNSPECIFIED";
}
Log.d(TAG, "Width: " + measureSpecWidth + ", " + widthSize + " Height: "
+ measureSpecHeight + ", " + heightSize);
}
private static final String TAG = "CustomView";
private static final int STICKY_WIDTH_UNDEFINED = -1;
private static final int BREAK_HEIGHT = 1950;
private static final int ARBITRARY_WIDTH_LESSER = 200;
private static final int ARBITRARY_WIDTH_GREATER = 800;
}

To make custom layout you need to read and understand this article https://developer.android.com/guide/topics/ui/how-android-draws.html
It isn't difficult to implement behaviour you want. You just need to override onMeasure and onLayout in your custom view.
In onMeasure you will get max possible height of your custom view and call measure() for childs in cycle. After child measurement get desired height from each child and calculate is child fit in current column or not, if not increase custom view wide for new column.
In onLayout you must call layout() for all child views to set them positions within the parent. This positions you have calculated in onMeasure before.

Related

Allocate fixed space for adaptive banner ad

How should I go about allocating fixed space for adaptive ad banners?
My XML adaptive banner
<FrameLayout
android:id="#+id/ad_view_container"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_centerInParent="true"
android:layout_alignParentBottom="true"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
/>
Inside my Fragment getting adaptive ad banner dimensions
private val adSize: AdSize
get() {
val outMetrics = Resources.getSystem().displayMetrics;
val density = outMetrics.density
var adWidthPixels = adContainerView.width.toFloat()
if (adWidthPixels == 0f) {
adWidthPixels = outMetrics.widthPixels.toFloat()
}
val adWidth = (adWidthPixels / density).toInt()
return AdSize.getCurrentOrientationBannerAdSizeWithWidth(context, adWidth)
}
I would like to follow Googles guidlines and have an allocated fixed space reserved for my banner, so that if my banner "lags" for a second screen on mobile stays the same.
How should I go about it, whats the proper way?
when starting the app you can get the adaptive banner height straight away by calling something like adSize.getHeight(). This will give you banner height in dps.
You then convert that heigth dps in px and set this height to your ad frame layout programatically by calling setLayoutParams with acquired height in pixels.
This way you dont need to wait for the ad to load to apply proper height and risk ad overlapping policy violation or conent shifting (which would happen if you depend on wrap_content)
If you have a more complex behavior, like I do, you can use the following implementation. I have a ConstraintLayout that splits my screen in two parts in portrait mode. The banner is within one of these two parts and the ratio between them is not fixed but depending on some logic. So this implementation also works with that requirement, as it overwrites onMeasure to determine the best size depending on the available width.
public class AdBanner extends FrameLayout
{
private AdView mAdView;
private final DisplayMetrics mDisplayMetrics = new DisplayMetrics();
public AdBanner(Context context)
{
super(context);
}
public AdBanner(Context context, AttributeSet attrs)
{
super(context, attrs);
}
public AdBanner(Context context, AttributeSet attrs, int defStyleAttr)
{
super(context, attrs, defStyleAttr);
}
#Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec)
{
if (!isShowBanner())
{
super.onMeasure(widthMeasureSpec, MeasureSpec.makeMeasureSpec(0, MeasureSpec.AT_MOST));
return;
}
int width = MeasureSpec.getSize(widthMeasureSpec);
if (width <= 0)
{
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
return;
}
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
// That's where we determine the most accurate banner format.
AdSize adSize = AdSize.getCurrentOrientationAnchoredAdaptiveBannerAdSize(getContext(), getDpWidth(width));
int height = adSize.getHeightInPixels(getContext());
int heightMode = MeasureSpec.getMode(heightMeasureSpec);
if (heightMode != MeasureSpec.UNSPECIFIED)
{
height = Math.min(height, MeasureSpec.getSize(heightMeasureSpec));
}
setMeasuredDimension(widthMeasureSpec, MeasureSpec.makeMeasureSpec(height, MeasureSpec.getMode(heightMeasureSpec)));
}
protected int getDpWidth(int width)
{
Display display = ((WindowManager) getContext().getSystemService(Context.WINDOW_SERVICE)).getDefaultDisplay();
display.getMetrics(mDisplayMetrics);
return (int) (width / mDisplayMetrics.density);
}
protected boolean isShowBanner()
{
// Do your checks here, like whether the user payed for ad removement.
}
#Override
protected void onLayout(boolean changed, int l, int t, int r, int b)
{
if (!isShowBanner())
{
return;
}
int width = r - l;
if (width <= 0)
{
return;
}
AdSize adSize = AdSize.getCurrentOrientationAnchoredAdaptiveBannerAdSize(getContext(), getDpWidth(width));
// Prevent the ad from beeing added with each layout cicle,
// by checking, whether or not available size actually changed the format of the banner
if (mAdView == null || !adSize.equals(mAdView.getAdSize()))
{
removeAllViews();
mAdView = new AdView(getContext());
mAdView.setAdSize(adSize);
((GameApplication) getContext().getApplicationContext()).androidInjector().getAdService().loadBannerAd(getRootActivity(this), mAdView);
this.addView(mAdView);
}
mAdView.layout(0, 0, width, b - t);
}
}

How to get slide menu views exact height in Xamarin.Forms for Android platform

I am using Xamarin.forms with SlideOverKit(https://github.com/XAM-Consulting/SlideOverKit) . SlideOverKit uses static values for HeightRequest &
WidthRequest for slide menu. I want to dynamically set the height as per menu content, for this I am making use of custom renderer of library & overriding the
OnMeasure() for Android.
I am suppose to get child views height by measuring them as per below code but it always return me the parent/screen height. I tried
differnet combinations of MeasureSpec mode & size but unable to get exact height of child elements.
SlideMenuDroidRenderer.cs inside SlideOverKit.Droid project:-
protected override void OnMeasure(int widthMeasureSpec, int heightMeasureSpec)
{
int mWidth = 0, mHt = 0;
int childWidthMeasureSpec = MeasureSpec.MakeMeasureSpec(MarginLayoutParams.WrapContent, MeasureSpecMode.Exactly);
int childHeightMeasureSpec = MeasureSpec.MakeMeasureSpec(MarginLayoutParams.WrapContent, MeasureSpecMode.Exactly);
for (int i = 0; i < ChildCount; i++)
{
Android.Views.View child = GetChildAt(i);
if (child.Visibility != ViewStates.Gone)
{
child.Measure(childWidthMeasureSpec, childHeightMeasureSpec);
mWidth += child.MeasuredWidth;
mHt += child.MeasuredHeight;
}
}
SetMeasuredDimension(width, height);
}
After it calls OnLayout(), I use MeasuredHeight & MeasuredWidth to set the slidemenu height & width.
If anyone knows how to get exact size of menu view in custom renderer of Xamarin.Forms ?
TIA.

SIze Imageview Placeholders

I am using recyclerview with imageviews in each cell.Each imageview loads images from the web and can be square or with more width than height or more height than width i.e any sizes.I am going to display a placeholder for each image while it loads in the background(with a progressbar).But the problem is the dimension of the images is unknown and I want to size the placeholders exactly the size of the images like the 9gag app in which the placeholders are exactly the size of the images while loading in the bacground.How do I achieve this in android ?I don't want to use wrap-content(produces a jarring effect after the image has been downloaded) or a specific height to the imageviews(crops the images).I am using UIL and currently planning to switch to Fresco or Picassa.
In case that your placeholder is just filled of some color, you can easily emulate a color drawable with exactly the same size.
/**
* The Color Drawable which has a given size.
*/
public class SizableColorDrawable extends ColorDrawable {
int mWidth = -1;
int mHeight = -1;
public SizableColorDrawable(int color, int width, int height) {
super(color);
mWidth = width;
mHeight = height;
}
#Override public int getIntrinsicWidth() {
return mWidth;
}
#Override public int getIntrinsicHeight() {
return mHeight;
}
}
To use it with Picasso:
Picasso.with(context).load(url).placeholder(new SizableColorDrawable(color, width, height)).into(imageView);
Now some tips on the ImageView:
public class DynamicImageView extends ImageView {
#Override public void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
Drawable drawable = getDrawable();
if (drawable == null) {
super.onMeasure(widthSpecureSpec, heightMeasureSpec);
return;
}
int width = MeasureSpec.getSize(widthMeasureSpec);
int height = MeasureSpec.getSize(heightMeasureSpec);
int reqWidth = drawable.getIntrinsicWidth();
int reqHeight = drawable.getIntrinsicHeight();
// Now you have the measured width and height,
// also the image's width and height,
// calculate your expected width and height;
setMeasuredDimension(targetWidth, targetHeight);
}
}
Hope this will help someone..
If you use fresco, you need to pass the width and height of your image from the web server, then you set the layout params of your drawee view with that width and height.
Like this:
RelativeLayout.LayoutParams draweeParams =
new RelativeLayout.LayoutParams(desiredImageWidthPixel,
desiredImageHeightPixel);
yourDraweeView.setLayoutParams(draweeParams);
From width and height of the image that you pass from your web server, you can calculate/resize proportionally the view as you need. Where desiredImageWidthPixel is the calculated image width that you want to show in yourDraweeView and desiredImageHeightPixel is the calculated image height that you want to show in yourDraweeView.
Don't forget to call
yourDraweeView.getHierarchy().setActualImageScaleType(ScalingUtils.ScaleType.FIT_XY);
To make yourDraweeView match the actual params that you set before.
Hope this helps
You can feed image dimensions along with the image url. (I assume you are getting image list from a source, like a JSON file or something.) And resize the ImageView holder inside the RecyclerView according to their dimension and then fire the image downloading process.

Calling TextView.setText() redraws entire screen in spite of view hierarchy

In my app I have a time display which updates every second. Each time the TextView used for the seconds field changes, the Developer Options->Show surface updates tool flashes the entire screen. I've looked around and can really only find this question which pretty well clarifies that there is no way to prevent the TextView from causing a relayout for at least part of the window. So I was sure to verify that my TextView's are wrapped in their own container but I still have the same issue. Every call to setText() causes the entire view to flash.
My hierarchy is as follows:
Fragment
RelativeLayout (Fragment Root View)
LinearLayout
RelativeLayout
My Time TextViews
Various other view components which change rarely
I would like to fix this if possible. I do need to try and reduce my view count if possible and I plan on working on it but this is still a problem I would like to remove from the app.
Show surface updates flashes the entire screen when hardware acceleration is on but it does not mean the entire window was redrawn. There is another option you can use that shows you exactly what part of the screen was redrawn when hardware acceleration is on ("Show GPU view updates").
I encountered a similar problem where I was updating a timer which caused a measure pass every time. If you have something that is expensive to measure, like a ListView, it affects performance. In my case, I had a ListView that didn't need to change size when the timer updated, so I made a custom ListView holder to stop the the measure from cascading down to the ListView. Now it runs smoothly!
public class CustomListviewHolderLayout extends FrameLayout {
public int currentWidth = 0;
public int currentHeight = 0;
public CustomListviewHolderLayout(Context context) {
super(context);
}
public CustomListviewHolderLayout(Context context, AttributeSet attrs) {
super(context, attrs);
}
public CustomListviewHolderLayout(Context context, AttributeSet attrs,
int defStyle) {
super(context, attrs, defStyle);
}
#Override
protected void onMeasure(final int widthMeasureSpec, final int heightMeasureSpec) {
//Debug Prints
String wMode = "";
switch(MeasureSpec.getMode(widthMeasureSpec)){
case MeasureSpec.AT_MOST:
wMode = "AT_MOST"; break;
case MeasureSpec.EXACTLY:
wMode = "EXACTLY"; break;
case MeasureSpec.UNSPECIFIED:
wMode = "UNSPECIFIED"; break;
default:
wMode = "";
}
String hMode = "";
switch(MeasureSpec.getMode(heightMeasureSpec)){
case MeasureSpec.AT_MOST:
hMode = "AT_MOST"; break;
case MeasureSpec.EXACTLY:
hMode = "EXACTLY"; break;
case MeasureSpec.UNSPECIFIED:
hMode = "UNSPECIFIED"; break;
default:
hMode = "";
}
Log.i("RandomCustomListViewHolder", "wMode = " + wMode + ", size = " + MeasureSpec.getSize(widthMeasureSpec));
Log.i("RandomCustomListViewHolder", "hMode = " + hMode + ", size = " + MeasureSpec.getSize(heightMeasureSpec));
//only remeasure child listview when the size changes (e.g. orientation change)
if(getChildCount() == 1){
if(((MeasureSpec.getMode(widthMeasureSpec) == MeasureSpec.EXACTLY) ||
(MeasureSpec.getMode(widthMeasureSpec) == MeasureSpec.AT_MOST))
&&((MeasureSpec.getMode(heightMeasureSpec) == MeasureSpec.EXACTLY) ||
(MeasureSpec.getMode(heightMeasureSpec) == MeasureSpec.AT_MOST))){
//check if height dimensions are different than before
int newWidth = MeasureSpec.getSize(widthMeasureSpec);
int newHeight = MeasureSpec.getSize(heightMeasureSpec);
if((newWidth != currentWidth) || (newHeight != currentHeight)){
//remeasure if different
Log.i("RandomCustomListViewHolder", "measuring listView");
View childView = getChildAt(0);
childView.measure(widthMeasureSpec, heightMeasureSpec);
currentWidth = newWidth;
currentHeight = newHeight;
this.setMeasuredDimension(newWidth, newHeight);
} else {
//still set this view's measured dimension
this.setMeasuredDimension(newWidth, newHeight);
}
} else {
//Specify match parent if one of the dimensions is unspecified
this.setMeasuredDimension(ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.MATCH_PARENT);
}
} else {
//view does not have the listview child, measure normally
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
currentWidth = 0;
currentHeight = 0;
}
}
}

Android font size not scaling on tablet [duplicate]

I'm trying to create a method for resizing multi-line text in a TextView such that it fits within the bounds (both the X and Y dimensions) of the TextView.
At present, I have something, but all it does is resize the text such that just the first letter/character of the text fills the dimensions of the TextView (i.e. only the first letter is viewable, and it's huge). I need it to fit all the lines of the text within the bounds of the TextView.
Here is what I have so far:
public static void autoScaleTextViewTextToHeight(TextView tv)
{
final float initSize = tv.getTextSize();
//get the width of the view's back image (unscaled)....
float minViewHeight;
if(tv.getBackground()!=null)
{
minViewHeight = tv.getBackground().getIntrinsicHeight();
}
else
{
minViewHeight = 10f;//some min.
}
final float maxViewHeight = tv.getHeight() - (tv.getPaddingBottom()+tv.getPaddingTop())-12;// -12 just to be sure
final String s = tv.getText().toString();
//System.out.println(""+tv.getPaddingTop()+"/"+tv.getPaddingBottom());
if(minViewHeight >0 && maxViewHeight >2)
{
Rect currentBounds = new Rect();
tv.getPaint().getTextBounds(s, 0, s.length(), currentBounds);
//System.out.println(""+initSize);
//System.out.println(""+maxViewHeight);
//System.out.println(""+(currentBounds.height()));
float resultingSize = 1;
while(currentBounds.height() < maxViewHeight)
{
resultingSize ++;
tv.setTextSize(resultingSize);
tv.getPaint().getTextBounds(s, 0, s.length(), currentBounds);
//System.out.println(""+(currentBounds.height()+tv.getPaddingBottom()+tv.getPaddingTop()));
//System.out.println("Resulting: "+resultingSize);
}
if(currentBounds.height()>=maxViewHeight)
{
//just to be sure, reduce the value
tv.setTextSize(resultingSize-1);
}
}
}
I think the problem is in the use of tv.getPaint().getTextBounds(...). It always returns small numbers for the text bounds... small relative to the tv.getWidth() and tv.getHeight() values... even if the text size is far larger than the width or height of the TextView.
The AutofitTextView library from MavenCentral handles this nicely. The source hosted on Github(1k+ stars) at https://github.com/grantland/android-autofittextview
Add the following to your app/build.gradle
repositories {
mavenCentral()
}
dependencies {
implementation 'me.grantland:autofittextview:0.2.+'
}
Enable any View extending TextView in code:
AutofitHelper.create(textView);
Enable any View extending TextView in XML:
<me.grantland.widget.AutofitLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
>
<Button
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:singleLine="true"
/>
</me.grantland.widget.AutofitLayout>
Use the built in Widget in code or XML:
<me.grantland.widget.AutofitTextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:singleLine="true"
/>
New since Android O:
https://developer.android.com/preview/features/autosizing-textview.html
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:autoSizeTextType="uniform"
android:autoSizeMinTextSize="12sp"
android:autoSizeMaxTextSize="100sp"
android:autoSizeStepGranularity="2sp"
/>
I have played with this for quite some time, trying to get my font sizes correct on a wide variety of 7" tablets (kindle fire, Nexus7, and some inexpensive ones in China with low-res screens) and devices.
The approach that finally worked for me is as follows. The "32" is an arbitrary factor that basically gives about 70+ characters across a 7" tablet horizontal line, which is a font size I was looking for. Adjust accordingly.
textView.setTextSize(getFontSize(activity));
public static int getFontSize (Activity activity) {
DisplayMetrics dMetrics = new DisplayMetrics();
activity.getWindowManager().getDefaultDisplay().getMetrics(dMetrics);
// lets try to get them back a font size realtive to the pixel width of the screen
final float WIDE = activity.getResources().getDisplayMetrics().widthPixels;
int valueWide = (int)(WIDE / 32.0f / (dMetrics.scaledDensity));
return valueWide;
}
I was able to answer my own question using the following code (see below), but my solution was very specific to the application. For instance, this will probably only look good and/or work for a TextView sized to approx. 1/2 the screen (with also a 40px top margin and 20px side margins... no bottom margin).
The using this approach though, you can create your own similar implementation. The static method basically just looks at the number of characters and determines a scaling factor to apply to the TextView's text size, and then incrementally increases the text size until the overall height (an estimated height -- using the width of the text, the text height, and the width of the TextView) is just below that of the TextView. The parameters necessary to determine the scaling factor (i.e. the if/else if statements) were set by guess-and-check. You'll likely have to play around with the numbers to make it work for your particular application.
This isn't the most elegant solution, though it was easy to code and it works for me. Does anyone have a better approach?
public static void autoScaleTextViewTextToHeight(final TextView tv, String s)
{
float currentWidth=tv.getPaint().measureText(s);
int scalingFactor = 0;
final int characters = s.length();
//scale based on # of characters in the string
if(characters<5)
{
scalingFactor = 1;
}
else if(characters>=5 && characters<10)
{
scalingFactor = 2;
}
else if(characters>=10 && characters<15)
{
scalingFactor = 3;
}
else if(characters>=15 && characters<20)
{
scalingFactor = 3;
}
else if(characters>=20 && characters<25)
{
scalingFactor = 3;
}
else if(characters>=25 && characters<30)
{
scalingFactor = 3;
}
else if(characters>=30 && characters<35)
{
scalingFactor = 3;
}
else if(characters>=35 && characters<40)
{
scalingFactor = 3;
}
else if(characters>=40 && characters<45)
{
scalingFactor = 3;
}
else if(characters>=45 && characters<50)
{
scalingFactor = 3;
}
else if(characters>=50 && characters<55)
{
scalingFactor = 3;
}
else if(characters>=55 && characters<60)
{
scalingFactor = 3;
}
else if(characters>=60 && characters<65)
{
scalingFactor = 3;
}
else if(characters>=65 && characters<70)
{
scalingFactor = 3;
}
else if(characters>=70 && characters<75)
{
scalingFactor = 3;
}
else if(characters>=75)
{
scalingFactor = 5;
}
//System.out.println(((int)Math.ceil(currentWidth)/tv.getWidth()+scalingFactor));
//the +scalingFactor is important... increase this if nec. later
while((((int)Math.ceil(currentWidth)/tv.getWidth()+scalingFactor)*tv.getTextSize())<tv.getHeight())
{
tv.setTextSize(TypedValue.COMPLEX_UNIT_SP, tv.getTextSize()+0.25f);
currentWidth=tv.getPaint().measureText(s);
//System.out.println(((int)Math.ceil(currentWidth)/tv.getWidth()+scalingFactor));
}
tv.setText(s);
}
Thanks.
I had the same problem and wrote a class that seems to work for me. Basically, I used a static layout to draw the text in a separate canvas and remeasure until I find a font size that fits. You can see the class posted in the topic below. I hope it helps.
Auto Scale TextView Text to Fit within Bounds
Stumbled upon this whilst looking for a solution myself... I'd tried all the other solutions out there that I could see on stack overflow etc but none really worked so I wrote my own.
Basically by wrapping the text view in a custom linear layout I've been able to successfully measure the text properly by ensuring it is measured with a fixed width.
<!-- TextView wrapped in the custom LinearLayout that expects one child TextView -->
<!-- This view should specify the size you would want the text view to be displayed at -->
<com.custom.ResizeView
android:layout_width="fill_parent"
android:layout_height="0dp"
android:layout_margin="10dp"
android:layout_weight="1"
android:orientation="horizontal" >
<TextView
android:id="#+id/CustomTextView"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
</com.custom.ResizeView>
Then the linear layout code
public class ResizeView extends LinearLayout {
public ResizeView(Context context, AttributeSet attrs) {
super(context, attrs);
}
public ResizeView(Context context) {
super(context);
}
#Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
super.onLayout(changed, left, top, right, bottom);
// oldWidth used as a fixed width when measuring the size of the text
// view at different font sizes
final int oldWidth = getMeasuredWidth() - getPaddingBottom() - getPaddingTop();
final int oldHeight = getMeasuredHeight() - getPaddingLeft() - getPaddingRight();
// Assume we only have one child and it is the text view to scale
TextView textView = (TextView) getChildAt(0);
// This is the maximum font size... we iterate down from this
// I've specified the sizes in pixels, but sp can be used, just modify
// the call to setTextSize
float size = getResources().getDimensionPixelSize(R.dimen.solutions_view_max_font_size);
for (int textViewHeight = Integer.MAX_VALUE; textViewHeight > oldHeight; size -= 0.1f) {
textView.setTextSize(TypedValue.COMPLEX_UNIT_PX, size);
// measure the text views size using a fixed width and an
// unspecified height - the unspecified height means measure
// returns the textviews ideal height
textView.measure(MeasureSpec.makeMeasureSpec(oldWidth, MeasureSpec.EXACTLY), MeasureSpec.UNSPECIFIED);
textViewHeight = textView.getMeasuredHeight();
}
}
}
Hope this helps someone.
maybe try setting setHoriztonallyScrolling() to true before taking text measurements so that the textView doesn't try to layout your text on multiple lines
One way would be to specify different sp dimensions for each of the generalized screen sizes. For instance, provide 8sp for small screens, 12sp for normal screens, 16 sp for large and 20 sp for xlarge. Then just have your layouts refer to #dimen text_size or whatever and you can rest assured, as density is taken care of via the sp unit. See the following link for more info on this approach.
http://www.developer.android.com/guide/topics/resources/more-resources.html#Dimension
I must note, however, that supporting more languages means more work during the testing phase, especially if you're interested in keeping text on one line, as some languages have much longer words. In that case, make a dimens.xml file in the values-de-large folder, for example, and tweak the value manually. Hope this helps.
Here is a solution that I created based on some other feedback. This solution allows you to set the size of the text in XML which will be the max size and it will adjust itself to fit the view height.
Size Adjusting TextView
private float findNewTextSize(int width, int height, CharSequence text) {
TextPaint textPaint = new TextPaint(getPaint());
float targetTextSize = textPaint.getTextSize();
int textHeight = getTextHeight(text, textPaint, width, targetTextSize);
while(textHeight > height && targetTextSize > mMinTextSize) {
targetTextSize = Math.max(targetTextSize - 1, mMinTextSize);
textHeight = getTextHeight(text, textPaint, width, targetTextSize);
}
return targetTextSize;
}
private int getTextHeight(CharSequence source, TextPaint paint, int width, float textSize) {
paint.setTextSize(textSize);
StaticLayout layout = new StaticLayout(source, paint, width, Alignment.ALIGN_NORMAL, mSpacingMult, mSpacingAdd, true);
return layout.getHeight();
}
If your only requirement is to have the text automatically split and continue in the next line and the height is not important then just have it like this.
<TextView
android:layout_height="wrap_content"
android:layout_width="wrap_content"
android:maxEms="integer"
android:width="integer"/>
This will have your TextView wrap to it's content vertically depending on your maxEms value.
Check if my solution helps you:
Auto Scale TextView Text to Fit within Bounds
I found that this worked well for me. see: https://play.google.com/store/apps/details?id=au.id.rupert.chauffeurs_name_board&hl=en
Source Code at http://www.rupert.id.au/chauffeurs_name_board/verson2.php
http://catchthecows.com/?p=72 and https://github.com/catchthecows/BigTextButton
This is based on mattmook's answer. It worked well on some devices, but not on all. I moved the resizing to the measuring step, made the maximum font size a custom attribute, took margins into account, and extended FrameLayout instead of LineairLayout.
public class ResizeView extends FrameLayout {
protected float max_font_size;
public ResizeView(Context context, AttributeSet attrs) {
super(context, attrs);
TypedArray a = context.getTheme().obtainStyledAttributes(
attrs,
R.styleable.ResizeView,
0, 0);
max_font_size = a.getDimension(R.styleable.ResizeView_maxFontSize, 30.0f);
}
public ResizeView(Context context) {
super(context);
}
#Override
protected void onMeasure(final int widthMeasureSpec, final int heightMeasureSpec) {
// Use the parent's code for the first measure
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
// Assume we only have one child and it is the text view to scale
final TextView textView = (TextView) getChildAt(0);
// Check if the default measure resulted in a fitting textView
LayoutParams childLayout = (LayoutParams) textView.getLayoutParams();
final int textHeightAvailable = getMeasuredHeight() - getPaddingTop() - getPaddingBottom() - childLayout.topMargin - childLayout.bottomMargin;
int textViewHeight = textView.getMeasuredHeight();
if (textViewHeight < textHeightAvailable) {
return;
}
final int textWidthSpec = MeasureSpec.makeMeasureSpec(
MeasureSpec.getSize(widthMeasureSpec) - getPaddingLeft() - getPaddingRight() - childLayout.leftMargin - childLayout.rightMargin,
MeasureSpec.EXACTLY);
final int textHeightSpec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED);
for (float size = max_font_size; size >= 1.05f; size-=0.1f) {
textView.setTextSize(TypedValue.COMPLEX_UNIT_PX, size);
textView.measure(textWidthSpec, textHeightSpec);
textViewHeight = textView.getMeasuredHeight();
if (textViewHeight <= textHeightAvailable) {
break;
}
}
}
}
And this in attrs.xml:
<declare-styleable name="ResizeView">
<attr name="maxFontSize" format="reference|dimension"/>
</declare-styleable>
And finally used like this:
<PACKAGE_NAME.ui.ResizeView xmlns:custom="http://schemas.android.com/apk/res/PACKAGE_NAME"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="start|center_vertical"
android:padding="5dp"
custom:maxFontSize="#dimen/normal_text">
<TextView android:id="#+id/tabTitle2"
android:layout_width="match_parent"
android:layout_height="match_parent"/>
</PACKAGE_NAME.ui.ResizeView>
Try this...
tv.setText("Give a very large text anc check , this xample is very usefull");
countLine=tv.getLineHeight();
System.out.println("LineCount " + countLine);
if (countLine>=40){
tv.setTextSize(15);
}

Categories

Resources