I use a basic and very common implementation of a LeadingMarginSpan2 to wrap text around an image (since it seems to be the easiest way and please don't suggest me to use WebViews at this point):
public class MyLeadingMarginSpan2 implements LeadingMarginSpan.LeadingMarginSpan2 {
private int margin;
private int lines;
public MyLeadingMarginSpan2(int lines, int margin) {
this.margin = margin;
this.lines = lines;
}
#Override
public int getLeadingMargin(boolean first) {
return first ? margin : 0; // <--- the issue is here
}
#Override
public void drawLeadingMargin(Canvas c, Paint p, int x, int dir,
int top, int baseline, int bottom, CharSequence text,
int start, int end, boolean first, Layout layout) {
}
#Override
public int getLeadingMarginLineCount() {
return lines;
}
}
The problem is that as soon as a paragraph occurs in the text, an unwanted margin is given to this line. I want to limit the number of times getLeadingMargin() returns the actual margin to the number of lines passed inside the constructor.
I've tried to count how many times this margin was returned and compare it against the number of lines, this didn't work however (in most cases no margin was applied, in some cases it was applied to a wrong number of lines).
Does anyone have a workaround for this issue?
Related
I am trying to get a screen capture in the Xamarin.Android platform.
public static Android.Content.Context Context { get; private set; }
public override View OnCreateView(View parent, string name, Context context, IAttributeSet attrs)
{
MainActivity.Context = context;
return base.OnCreateView(parent, name, context, attrs);
}
I am trying to find out the why the following rootView.Width and Height returns 0 all the time.
var rootView = ((Activity)MainActivity.Context).Window.DecorView.RootView;
Console.WriteLine ("{0}x{1}", rootView.Width,rootView.Height);
My ultimate goal is to capture the screenshot of the view as an image and generate pdf.
I don't know Xamarin, however it seems to be the same as native Android for this solution.
When onCreateView() is called the views have not yet been measured. To get a view dimensions you should attach a specific listener: onLayoutChangeListener.
Here is an Android native code example:
rootView.addOnLayoutChangeListener(new View.OnLayoutChangeListener() {
#Override
public void onLayoutChange(View v, int left, int top, int right, int bottom, int oldLeft, int oldTop,
int oldRight, int oldBottom) {
int width = right - left;
int height = bottom - top;
v.removeOnLayoutChangeListener(this); // Remove the listener
}
});
You could find here the listener to use for Xamarin
Hope its help ! :)
In the onCreateView the width & height of objects are not yet defined. They are defined in a later stage of the activity's lifecycle.
You have to use the treeviewobserver for this.
Example with your rootview:
rootView.ViewTreeObserver.GlobalLayout += (object sender, EventArgs e) => {
Console.WriteLine ("{0}x{1}", rootView.Width,rootView.Height);
};
In that method the width & height will be known.
Furthermore, you want to take a picture of the your rootview, the best way to do this is to use this method, this will automatically output a Bitmap of the view to variable b.
rootView.DrawingCacheEnabled = true;
Bitmap b = rootView.GetDrawingCache(true);
Hope this gets you on your way!
I am using following code to animate expandable layout:
class ExpandAnimation extends Animation {
private View _view;
private int _startHeight;
private int _finishHeight;
public ExpandAnimation( View view, int startHeight, int finishHeight ) {
_view = view;
_startHeight = startHeight;
_finishHeight = finishHeight;
setDuration(500);
System.out.println(_startHeight);
System.out.println(_finishHeight);
}
#Override
protected void applyTransformation( float interpolatedTime, Transformation t ) {
int newHeight = (int)((_finishHeight - _startHeight) * interpolatedTime + _startHeight);
_view.getLayoutParams().height = newHeight;
_view.requestLayout();
}
#Override
public void initialize( int width, int height, int parentWidth, int parentHeight ) {
super.initialize(width, height, parentWidth, parentHeight);
}
#Override
public boolean willChangeBounds( ) {
return true;
}
};
This animation is created every time I click a button. It is called properly (checked System.out.println and it prints correct values) however in emulator animation runs only like once of 15 times. To be exact hiding it works great but expanding works only once a few times (on emulator, cant get it working on phone).
What could be the problem?
Thanks in forward
EDIT: layout I am trying to animate is FrameLayout. It has TextView as child and finishHeight is measured by textView measure height. The values are correct. I have also tried calling textView.requestLayout() in apply transformation to redraw layout but it is not working. It still expands only sometimes. If you need any more code feel free to ask.
Calling
((View) toExpand.getParent()).invalidate();
just after startAnimation solved my problem. Must check it on other devices but I think it will work.
I am currently working on a Connect four game
I have managed to develop the app to point to where the game knows what column the user has selected. I am currently trying to change the colour of the gap to the respective colour of the user (token has been placed).
The problem I am having is when connectfourView.invalidate() is called I get a java.lang.NullPointerexception
Code below
public void tokenPlacement (int index, float xpos) {
int x = 0;
int lowerxpos = (int) (xpos - 10);
int higherxpos = (int) (xpos + 10);
while (x <= 6)
{
if ( lowerxpos <= ((float) (columnselects.get(x).getWidthPos())) && higherxpos >= ((float) (columnselects.get(x).getWidthPos())))
{
Log.d("In IF", Float.toString(x));
Log.d("Looking at the colour", Float.toString(gaps.get(x).getColor()));
gaps.get(x).setColor(-1);
Log.d("After change what is the colour now", Float.toString(gaps.get(x).getColor()));
connectfourView.invalidate();
break;
}
x++;
}
}
About the code. Firstly I know this will only affect the bottom layer of each column's gaps. I will fix this when the colour changing is working. This code is from Gaps.java. This view is ConnectFourView.java (instance connectfourView) where the gaps are drawn to screen. All gaps are stored in a list (gaps) and are defined in the Gap.java (x-postion, colour etc.)
Section from Gap.java
public Gap (int j, int i, int color, double diameter, double widthpos, double heightpos){
this.j = j;
this.i = i;
this.color = color;
this.diameter = diameter;
this.widthpos = widthpos;
this.heightpos = heightpos;
}
public int getJ() {return j;}
public int getI() {return i;}
public int getColor() {return color;}
public void setColor(int newColor) {this.color = newColor;}
public double getDiameter() {return diameter;}
public double getWidthPos() {return widthpos;}
public double getHeightPos() {return heightpos;}
}
This is a question I asked to get to this point
Note that the gaps are added with a button press 'New Game'
If any other information or code is need please leave a comment
Added for dmon:
This is where connectfourView is defined, it has also been imported
public class Gaps {
ConnectFourView connectfourView;
/* Other code, not related to question */
public void tokenplacement()//at bottom of class Gaps
I have noticed that when a click the exit button on the screen all the gaps do change color just before the main menu is loaded, so the problem is just trying to refresh the screen
From ConnectFourView.java
public ConnectFourView(Context context, Gaps gaps) {
super(context);
this.gaps = gaps;
setFocusable(true);
}
//used to mange the attributes of the board (different colour to background for board)
public ConnectFourView(Context context, AttributeSet attrs, Gaps gaps) {
super(context, attrs);
this.gaps = gaps;
setFocusable(true);
setMinimumWidth(getWidth());
setMinimumHeight(getHeight());
}
link to dropbox
As others have said (I gave Henry the bump), connectfourView has to be null there. You need to initialize it:
ConnectFourView connectfourView = new ConnectFourView();
I don't see where that is ever done so it's likely set to null by default.
I want to get the number of lines of a text view
textView.setText("Test line 1 Test line 2 Test line 3 Test line 4 Test line 5.............")
textView.getLineCount(); always returns zero
Then I have also tried:
ViewTreeObserver vto = this.textView.getViewTreeObserver();
vto.addOnGlobalLayoutListener(new OnGlobalLayoutListener() {
#Override
public void onGlobalLayout() {
ViewTreeObserver obs = textView.getViewTreeObserver();
obs.removeGlobalOnLayoutListener(this);
System.out.println(": " + textView.getLineCount());
}
});
It returns the exact output.
But this works only for a static layout.
When I am inflating the layout dynamically this doesn't work anymore.
How could I find the number of line in a TextView?
I was able to get getLineCount() to not return 0 using a post, like this:
textview.setText(“Some text”);
textview.post(new Runnable() {
#Override
public void run() {
int lineCount = textview.getLineCount();
// Use lineCount here
}
});
As mentioned in this post,
getLineCount() will give you the correct number of lines only after
a layout pass.
It means that you need to render the TextView first before invoking the getLineCount() method.
ViewTreeObserver is not so reliable especially when using dynamic layouts such as ListView.
Let's assume:
1. You will do some work depending on the lines of TextView.
2. The work is not very urgent and can be done later.
Here is my solution:
public class LayoutedTextView extends TextView {
public LayoutedTextView(Context context) {
super(context);
}
public LayoutedTextView(Context context, AttributeSet attrs) {
super(context, attrs);
}
public LayoutedTextView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
}
public interface OnLayoutListener {
void onLayouted(TextView view);
}
private OnLayoutListener mOnLayoutListener;
public void setOnLayoutListener(OnLayoutListener listener) {
mOnLayoutListener = listener;
}
#Override
protected void onLayout(boolean changed, int left, int top, int right,
int bottom) {
super.onLayout(changed, left, top, right, bottom);
if (mOnLayoutListener != null) {
mOnLayoutListener.onLayouted(this);
}
}
}
Usage:
LayoutedTextView tv = new LayoutedTextView(context);
tv.setOnLayoutListener(new OnLayoutListener() {
#Override
public void onLayouted(TextView view) {
int lineCount = view.getLineCount();
// do your work
}
});
textView.getViewTreeObserver().addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() {
#Override
public boolean onPreDraw() {
// Remove listener because we don't want this called before _every_ frame
textView.getViewTreeObserver().removeOnPreDrawListener(this)
// Drawing happens after layout so we can assume getLineCount() returns the correct value
if(textView.getLineCount() > 2) {
// Do whatever you want in case text view has more than 2 lines
}
return true; // true because we don't want to skip this frame
}
});
I think the crux of this question is that people want to be able to find out the size of a TextView in advance so that they can dynamically resize it to nicely fit the text. A typical use might be to create talk bubbles (at least that was what I was working on).
I tried several solutions, including use of getTextBounds() and measureText() as discussed here. Unfortunately, both methods are slightly inexact and have no way to account for line breaks and unused linespace. So, I gave up on that approach.
That leaves getLineCount(), whose problem is that you have to "render" the text before getLineCount() will give you the number of lines, which makes it a chicken-and-egg situation. I read various solutions involving listeners and layouts, but just couldn't believe that there wasn't something simpler.
After fiddling for two days, I finally found what I was looking for (at least it works for me). It all comes down to what it means to "render" the text. It doesn't mean that the text has to appear onscreen, only that it has to be prepared for display internally. This happens whenever a call is made directly to invalidate() or indirectly as when you do a setText() on your TextView, which calls invalidate() for you since the view has changed appearance.
Anyway, here's the key code (assume you already know the talk bubble's lineWidth and lineHeight of a single line based on the font):
TextView talkBubble;
// No peeking while we set the bubble up.
talkBubble.setVisibility( View.INVISIBLE );
// I use FrameLayouts so my talk bubbles can overlap
// lineHeight is just a filler at this point
talkBubble.setLayoutParams( new FrameLayout.LayoutParams( lineWidth, lineHeight ) );
// setText() calls invalidate(), which makes getLineCount() do the right thing.
talkBubble.setText( "This is the string we want to dynamically deal with." );
int lineCount = getLineCount();
// Now we set the real size of the talkBubble.
talkBubble.setLayoutParams( new FrameLayout.LayoutParams( lineWidth, lineCount * lineHeight ) );
talkBubble.setVisibility( View.VISIBLE );
Anyway, that's it. The next redraw will give a bubble tailor-made for your text.
Note: In the actual program, I use a separate bubble for determining lines of text so that I can resize my real bubble dynamically both in terms of length and width. This allows me to shrink my bubbles left-to-right for short statements, etc.
Enjoy!
You could also use PrecomputedTextCompat for getting the number of lines.
Regular method:
fun getTextLineCount(textView: TextView, text: String, lineCount: (Int) -> (Unit)) {
val params: PrecomputedTextCompat.Params = TextViewCompat.getTextMetricsParams(textView)
val ref: WeakReference<TextView>? = WeakReference(textView)
GlobalScope.launch(Dispatchers.Default) {
val text = PrecomputedTextCompat.create(text, params)
GlobalScope.launch(Dispatchers.Main) {
ref?.get()?.let { textView ->
TextViewCompat.setPrecomputedText(textView, text)
lineCount.invoke(textView.lineCount)
}
}
}
}
Call this method:
getTextLineCount(textView, "Test line 1 Test line 2 Test line 3 Test line 4 Test line 5.............") { lineCount ->
//count of lines is stored in lineCount variable
}
Or maybe you can create extension method for it like this:
fun TextView.getTextLineCount(text: String, lineCount: (Int) -> (Unit)) {
val params: PrecomputedTextCompat.Params = TextViewCompat.getTextMetricsParams(this)
val ref: WeakReference<TextView>? = WeakReference(this)
GlobalScope.launch(Dispatchers.Default) {
val text = PrecomputedTextCompat.create(text, params)
GlobalScope.launch(Dispatchers.Main) {
ref?.get()?.let { textView ->
TextViewCompat.setPrecomputedText(textView, text)
lineCount.invoke(textView.lineCount)
}
}
}
}
and then you call it like this:
textView.getTextLineCount("Test line 1 Test line 2 Test line 3 Test line 4 Test line 5.............") { lineCount ->
//count of lines is stored in lineCount variable
}
Based on #secnelis idea, there is even a more clean way if you target API 11 or higher.
Instead of extending a TextView you can use already built-in functionality if View.OnLayoutChangeListener
In ListAdapter.getView(), for instance
if (h.mTitle.getLineCount() == 0 && h.mTitle.getText().length() != 0) {
h.mTitle.addOnLayoutChangeListener(new View.OnLayoutChangeListener() {
#Override
public void onLayoutChange(final View v, final int left, final int top,
final int right, final int bottom, final int oldLeft,
final int oldTop, final int oldRight, final int oldBottom) {
h.mTitle.removeOnLayoutChangeListener(this);
final int count = h.mTitle.getLineCount();
// do stuff
}
});
} else {
final int count = h.mTitle.getLineCount();
// do stuff
}
You can also calculate the amount of lines through this function:
private fun countLines(textView: TextView): Int {
return Math.ceil(textView.paint.measureText(textView.text.toString()) /
textView.measuredWidth.toDouble()).toInt()
}
Keep in mind that It may not work very well on a RecyclerView though.
textview.getText().toString().split(System.getProperty("line.separator")).length
It works fine for me to get number of lines of TextView.
Are you doing this onCreate? The Views aren't laid out yet, so getLineCount() is 0 for a while. If you do this later in the Window LifeCycle, you'll get your line count. You'll have a hard time doing it onCreate, but onWindowFocusChanged with hasFocus=true usually has the Views measured by now.
The textView.post() suggestion is also a good one
How much text can be set in the textview for a particular text string with a particular font and size, so that the textview doesn't need to scroll. I mean how much text can be fit inside an TextView, without the need for scrolling.
This is similar to How do I determine how much text will fit in a TextView in Android?, but I am not able to find a working solution. Please help.
Assuming you have already looked for other options and there isn't an easy way to do this, here is a theoretical (I haven't tested it) hackish approach that might work.
Create a new class that extends TextView. Then, override a method that would be called after the TextView's content has been updated. There might be a better method to use but for this example let's try onDraw(). This method would check the widths and see if it needs to be able to scroll. If so, it would trim the string and set the text. It would do this in a loop until it didn't need to scroll any longer.
#Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
if(getWidth() < computeHorizontalScrollRange()){
// Requires scrolling, the string is too long
// Do whatever you need to do to trim the string, you could also grab the remaining string and do something with it.
// Set the TextView to use the trimmed string.
}
}
You would want to make sure it didn't get into a endless loop and also check if the width is zero.
You also can look at the various android:ellipsize options.
I actually needed something similar. I needed a TextView width a semi-fixed size but the text always needed to fit in there. As the size didn't really matter i created a view that changes the text size until it fits.
import android.content.Context;
import android.util.AttributeSet;
import android.util.TypedValue;
import android.widget.TextView;
/**
* The text inside this TextView will resize to fit.
* It will resize until it fits within the view and padding
*/
public class FitTextView extends TextView {
private float defaultSize = 12;
public FitTextView(Context context) {
super(context);
}
public FitTextView(Context context, AttributeSet attrs) {
super(context, attrs);
defaultSize = getTextSize();
}
public FitTextView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
defaultSize = getTextSize();
}
/**
* Set the default size. This size is set every time the
* view is resized.
* #param size
*/
public void setDefaultSize(float size) {
defaultSize = size;
}
#Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
setTextSize(TypedValue.COMPLEX_UNIT_PX, defaultSize);
fitCharsInView();
super.onSizeChanged(w, h, oldw, oldh);
}
#Override
public void setText(CharSequence text, BufferType type) {
fitCharsInView();
super.setText(text, type);
}
/**
* Decreases the text size until it fits inside the view
*/
public void fitCharsInView() {
int padding = getPaddingLeft() + getPaddingRight();
int viewWidth = getWidth() - padding;
float textWidth = getTextWidth();
int iKillInfite = 0;
int maxIteration = 10000;
while(textWidth > viewWidth && iKillInfite < maxIteration) {
iKillInfite++;
float textSize = getTextSize();
setTextSize(TypedValue.COMPLEX_UNIT_PX, textSize-1);
textWidth = getTextWidth();
}
}
/**
* Gets the width in pixels of the text
* #return
*/
private float getTextWidth() {
return getPaint().measureText(getText().toString());
}
}
This is just a quick brain storm of mine:
Measure text length in characters
Get screen dimensions and density information
Get font size, line height
Calculate approximate amount of space the given string will occupy given the above data
Split the string accordingly
I would extend the TextView class and override onLayout() method to trim the string as long as the measured height is larger than computeHorizontalScrollRange().
I believe solution is a bit better because:
It only trims the text once (when the view is being created);
You can easily store the "trimmed" text if you want to restore it in the future;
OOP is awesome.