Bullet points in textview are cut off - android

I have a TextView in an app, the text of which is set by a hard-coded string resource in the layout. In order to get a bulleted list in the TextView, I've used the (unofficial?) support for the <li> element. This creates properly-indented bullets, as desired, but the leftmost edge of the bullets themselves are slightly cut off, as you can see:
I have tried adding left padding to these, but it did nothing to the clipped edge - just moved the whole thing inwards.
Is there any simple solution to resolve this?
Where does the resource for that bulleted list live?

Old question but for anyone else finding this late:
I've found the built in BulletSpan class has had bugs from early android versions all the way through to marshmallow:
Bullet radius and gap width don't scale depending on dp
Bullets are sometimes cut off (should be + BULLET_RADIUS, not * BULLET_RADIUS)
Warning: I've seen a few custom BulletSpan classes out there which implement ParcelableSpan like the internal class. This WILL cause crashes and is not intended to be used externally.
Here's my BulletSpanCompat:
public class BulletSpanCompat implements LeadingMarginSpan {
private final int mGapWidth;
private final boolean mWantColor;
private final int mColor;
private static final int BULLET_RADIUS = MaterialDesignUtils.dpToPx(1.5f);
private static Path sBulletPath = null;
public static final int STANDARD_GAP_WIDTH = MaterialDesignUtils.dpToPx(8);
public BulletSpanCompat() {
mGapWidth = STANDARD_GAP_WIDTH;
mWantColor = false;
mColor = 0;
}
public BulletSpanCompat(int gapWidth) {
mGapWidth = gapWidth;
mWantColor = false;
mColor = 0;
}
public BulletSpanCompat(int gapWidth, int color) {
mGapWidth = gapWidth;
mWantColor = true;
mColor = color;
}
public BulletSpanCompat(Parcel src) {
mGapWidth = src.readInt();
mWantColor = src.readInt() != 0;
mColor = src.readInt();
}
public int getLeadingMargin(boolean first) {
return 2 * BULLET_RADIUS + mGapWidth;
}
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 l) {
if (((Spanned) text).getSpanStart(this) == start) {
Paint.Style style = p.getStyle();
int oldcolor = 0;
if (mWantColor) {
oldcolor = p.getColor();
p.setColor(mColor);
}
p.setStyle(Paint.Style.FILL);
if (c.isHardwareAccelerated()) {
if (sBulletPath == null) {
sBulletPath = new Path();
// Bullet is slightly better to avoid aliasing artifacts on mdpi devices.
sBulletPath.addCircle(0.0f, 0.0f, 1.2f + BULLET_RADIUS, Path.Direction.CW);
}
c.save();
c.translate(x + dir + BULLET_RADIUS, (top + bottom) / 2.0f);
c.drawPath(sBulletPath, p);
c.restore();
} else {
c.drawCircle(x + dir + BULLET_RADIUS, (top + bottom) / 2.0f, BULLET_RADIUS, p);
}
if (mWantColor) {
p.setColor(oldcolor);
}
p.setStyle(style);
}
}
}

Try using the unicode character • (Unicode 2022)? Looks like you can just paste it into the XML.
http://www.fileformat.info/info/unicode/char/2022/index.htm

Please use below code:-
<CustomBulletTextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Point1" />
<CustomBulletTextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Point2" />
/* CustomBulletTextView.java */
public class CustomBulletTextView extends TextView {
public CustomBulletTextView(Context context) {
super(context);
addBullet();
}
public CustomBulletTextView(Context context, AttributeSet attrs) {
super(context, attrs);
addBullet();
}
public CustomBulletTextView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
addBullet();
}
public CustomBulletTextView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
addBullet();
}
private void addBullet() {
CharSequence text = getText();
if (TextUtils.isEmpty(text)) {
return;
}
SpannableString spannable = new SpannableString(text);
spannable.setSpan(new CustomBulletSpan(16), 0, text.length(), 0);
setText(spannable);
}
}
/* CustomBulletSpan.java */
public class CustomBulletSpan implements LeadingMarginSpan, ParcelableSpan {
private final int mGapWidth;
private final boolean mWantColor;
private final int mColor;
private static final int BULLET_RADIUS = 3;
private static Path sBulletPath = null;
public static final int STANDARD_GAP_WIDTH = 2;
public CustomBulletSpan(int gapWidth) {
mGapWidth = gapWidth;
mWantColor = false;
mColor = 0;
}
public int describeContents() {
return 0;
}
public void writeToParcel(Parcel dest, int flags) {
dest.writeInt(mGapWidth);
dest.writeInt(mWantColor ? 1 : 0);
dest.writeInt(mColor);
}
public int getLeadingMargin(boolean first) {
return 2 * BULLET_RADIUS + mGapWidth;
}
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 l) {
// Here I shifted the bullets to right by the given half bullet
x += mGapWidth / 2;
if (((Spanned) text).getSpanStart(this) == start) {
Paint.Style style = p.getStyle();
int oldcolor = 0;
if (mWantColor) {
oldcolor = p.getColor();
p.setColor(mColor);
}
p.setStyle(Paint.Style.FILL);
if (c.isHardwareAccelerated()) {
if (sBulletPath == null) {
sBulletPath = new Path();
// Bullet is slightly better to avoid aliasing artifacts on
// mdpi devices.
sBulletPath.addCircle(0.0f, 0.0f, 1.2f * BULLET_RADIUS, Direction.CW);
}
c.save();
c.translate(x + dir * BULLET_RADIUS, (top + bottom) / 2.0f);
c.drawPath(sBulletPath, p);
c.restore();
} else {
c.drawCircle(x + dir * BULLET_RADIUS, (top + bottom) / 2.0f, BULLET_RADIUS, p);
}
if (mWantColor) {
p.setColor(oldcolor);
}
p.setStyle(style);
}
}
#Override
public int getSpanTypeId() {
return 0;
}
}

Related

Make custom EditText numeric-only

I have a custom EditText which I got from here.
PROBLEM: This by default, opens the normal keyboard. I want to open the numeric keyboard. I tried adding inputType="number" in the XML, but then it stops showing the placeholder lines.
How can I make this open the numeric keyboard while still showing the placeholder lines ?
Also, how is it possible to set maxLength from inside the class ?
Below is the code:
public class PinEntryEditText extends android.support.v7.widget.AppCompatEditText {
public static final String XML_NAMESPACE_ANDROID = "http://schemas.android.com/apk/res/android";
private float mSpace = 10; //24 dp by default, space between the lines
private float mCharSize;
private float mNumChars = 6;
private float mLineSpacing = 8; //8dp by default, height of the text from our lines
private int mMaxLength = 6;
private int pinLength;
private OnClickListener mClickListener;
private float mLineStroke = 1; //1dp by default
private float mLineStrokeSelected = 2; //2dp by default
private Paint mLinesPaint;
int[][] mStates = new int[][]{
new int[]{android.R.attr.state_selected}, // selected
new int[]{android.R.attr.state_focused}, // focused
new int[]{-android.R.attr.state_focused}, // unfocused
};
int[] mColors = new int[]{
Color.GREEN,
Color.BLACK,
Color.GRAY
};
ColorStateList mColorStates = new ColorStateList(mStates, mColors);
public PinEntryEditText(Context context) {
super(context);
}
public PinEntryEditText(Context context, AttributeSet attrs) {
super(context, attrs);
TypedArray ta = context.obtainStyledAttributes(attrs, R.styleable.PinEntryEditText , 0, 0);
try {
pinLength = ta.getInteger(R.styleable.PinEntryEditText_pinLength , mMaxLength);
} finally {
ta.recycle();
}
mNumChars = pinLength;
mMaxLength = pinLength;
init(context, attrs);
}
public PinEntryEditText(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init(context, attrs);
}
private void init(Context context, AttributeSet attrs) {
float multi = context.getResources().getDisplayMetrics().density;
mLineStroke = multi * mLineStroke;
mLineStrokeSelected = multi * mLineStrokeSelected;
mLinesPaint = new Paint(getPaint());
mLinesPaint.setStrokeWidth(mLineStroke);
if (!isInEditMode()) {
TypedValue outValue = new TypedValue();
context.getTheme().resolveAttribute(R.attr.colorControlActivated,
outValue, true);
final int colorActivated = outValue.data;
mColors[0] = colorActivated;
context.getTheme().resolveAttribute(R.attr.colorPrimaryDark,
outValue, true);
final int colorDark = outValue.data;
mColors[1] = colorDark;
context.getTheme().resolveAttribute(R.attr.colorControlHighlight,
outValue, true);
final int colorHighlight = outValue.data;
mColors[2] = colorHighlight;
}
setBackgroundResource(0);
mSpace = multi * mSpace; //convert to pixels for our density
mLineSpacing = multi * mLineSpacing; //convert to pixels for our density
mMaxLength = attrs.getAttributeIntValue(XML_NAMESPACE_ANDROID, "maxLength", mMaxLength);
mNumChars = mMaxLength;
//Disable copy paste
super.setCustomSelectionActionModeCallback(new ActionMode.Callback() {
public boolean onPrepareActionMode(ActionMode mode, Menu menu) {
return false;
}
public void onDestroyActionMode(ActionMode mode) {
}
public boolean onCreateActionMode(ActionMode mode, Menu menu) {
return false;
}
public boolean onActionItemClicked(ActionMode mode, MenuItem item) {
return false;
}
});
// When tapped, move cursor to end of text.
super.setOnClickListener(new OnClickListener() {
#Override
public void onClick(View v) {
setSelection(getText().length());
if (mClickListener != null) {
mClickListener.onClick(v);
}
}
});
}
#Override
public void setOnClickListener(OnClickListener l) {
mClickListener = l;
}
#Override
public void setCustomSelectionActionModeCallback(ActionMode.Callback actionModeCallback) {
throw new RuntimeException("setCustomSelectionActionModeCallback() not supported.");
}
#Override
protected void onDraw(Canvas canvas) {
//super.onDraw(canvas);
int availableWidth = getWidth() - getPaddingRight() - getPaddingLeft();
if (mSpace < 0) {
mCharSize = (availableWidth / (mNumChars * 2 - 1));
} else {
mCharSize = (availableWidth - (mSpace * (mNumChars - 1))) / mNumChars;
}
int startX = getPaddingLeft();
int bottom = getHeight() - getPaddingBottom();
//Text Width
Editable text = getText();
int textLength = text.length();
float[] textWidths = new float[textLength];
getPaint().getTextWidths(getText(), 0, textLength, textWidths);
for (int i = 0; i < mNumChars; i++) {
updateColorForLines(i == textLength);
canvas.drawLine(startX, bottom, startX + mCharSize, bottom, mLinesPaint);
if (getText().length() > i) {
float middle = startX + mCharSize / 2;
canvas.drawText(text, i, i + 1, middle - textWidths[0] / 2, bottom - mLineSpacing, getPaint());
}
if (mSpace < 0) {
startX += mCharSize * 2;
} else {
startX += mCharSize + mSpace;
}
}
}
private int getColorForState(int... states) {
return mColorStates.getColorForState(states, Color.GRAY);
}
/**
* #param next Is the current char the next character to be input?
*/
private void updateColorForLines(boolean next) {
if (isFocused()) {
mLinesPaint.setStrokeWidth(mLineStrokeSelected);
mLinesPaint.setColor(getColorForState(android.R.attr.state_focused));
if (next) {
mLinesPaint.setColor(getColorForState(android.R.attr.state_selected));
}
} else {
mLinesPaint.setStrokeWidth(mLineStroke);
mLinesPaint.setColor(getColorForState(-android.R.attr.state_focused));
}
}
}
XML:
<com.mridulahuja.kudamm.tools.PinEntryEditText
android:id="#+id/txtToken"
android:layout_width="0dp"
android:layout_height="55dp"
android:ems="10"
pin:pinLength="6"
android:gravity="center"
app:layout_constraintTop_toTopOf="parent"
android:layout_marginTop="8dp"
app:layout_constraintRight_toRightOf="parent"
android:layout_marginRight="15dp"
android:layout_marginEnd="15dp"
android:layout_marginLeft="15dp"
android:layout_marginStart="15dp"
app:layout_constraintLeft_toLeftOf="parent"/>
Default length is set from code:
private int mMaxLength = 6;
But this library reads value from xml too:
mMaxLength = attrs.getAttributeIntValue(XML_NAMESPACE_ANDROID, "maxLength", mMaxLength);
so you can use both approaches.
In order to make placeholders visible you should add the _ symbol like this:
android:digits="0123456789_"
May be you'll need to add some other symbols, based on your needs.

Android edittext cursor not seen despite edittext.isCursorVisible() returning true

In my xml I have already added following attributes.
<com.project.current.widget.AutoResizeEditText
android:id="#+id/edittext_add_text_new"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:focusable="true"
android:focusableInTouchMode="true"
android:hint="#string/text_sticker_hint"
android:inputType="textNoSuggestions|textVisiblePassword|textMultiLine"
android:isScrollContainer="false"
android:padding="#dimen/dimen_margin_small"
android:textColor="#color/black"
android:textColorHint="#color/blue_popover"
android:textSize="#dimen/text_font_edit_text_default"
android:layout_alignParentTop="true"
android:layout_centerHorizontal="true"
android:background="#drawable/bg_text_sticker"
android:layout_toRightOf="#+id/resize_icon_left"
android:cursorVisible="true"
android:textCursorDrawable="#null"
android:textIsSelectable="true" />
I have set Focusable and FocusableOnTouch to true. I also have cursorVisible set to true. And just to make sure the cursor isn't taking white color(the background is whitish), I have set the textCursorDrawable to '#null'
But the cursor still isnt visible in the view. I have a touchListener implemented to the edittext (since I also need to move it around the parent layout). So I have set the keyboard to pop up at ACTION_UP. At that point I when I check
editTextViewNew.isCursorVisible()
It returns true. But the cursor is actually not visible. How do I make it visible and the text within my edittext selectable? (The long press option to 'paste' copied text also doesn't work.)
I am using the following Custom AutoResizeEditText to resize the text within editText as per my need.
public class AutoResizeEditText extends android.support.v7.widget.AppCompatEditText {
private static final int NO_LINE_LIMIT = -1;
private final RectF _availableSpaceRect = new RectF();
private final SparseIntArray _textCachedSizes = new SparseIntArray();
private final SizeTester _sizeTester;
private float _maxTextSize;
private float _spacingMult = 1.0f;
private float _spacingAdd = 0.0f;
private float _minTextSize;
private int _widthLimit;
private int _maxLines;
private boolean _enableSizeCache = true;
private boolean _initiallized = false;
private TextPaint paint;
//set Boolean for disabling auto-resize when user is resizing using controls
public static boolean isUserTyping = false;
//disable autoresizing when user is not typing text
public static void checkIfUserTyping(boolean isActive){
isUserTyping = isActive;
}
private interface SizeTester {
/**
* AutoResizeEditText
*
* #param suggestedSize
* Size of text to be tested
* #param availableSpace
* available space in which text must fit
* #return an integer < 0 if after applying {#code suggestedSize} to
* text, it takes less space than {#code availableSpace}, > 0
* otherwise
*/
public int onTestSize(int suggestedSize, RectF availableSpace);
}
public AutoResizeEditText(final Context context) {
this(context, null, 0);
}
public AutoResizeEditText(final Context context, final AttributeSet attrs) {
this(context, attrs, 0);
}
public AutoResizeEditText(final Context context, final AttributeSet attrs,
final int defStyle) {
super(context, attrs, defStyle);
// using the minimal recommended font size
_minTextSize = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP,
12, getResources().getDisplayMetrics());
_maxTextSize = getTextSize();
if (_maxLines == 0)
// no value was assigned during construction
_maxLines = NO_LINE_LIMIT;
// prepare size tester:
_sizeTester = new SizeTester() {
final RectF textRect = new RectF();
#TargetApi(Build.VERSION_CODES.JELLY_BEAN)
#Override
public int onTestSize(final int suggestedSize,
final RectF availableSPace) {
paint.setTextSize(suggestedSize);
final String text = getText().toString();
final boolean singleline = getMaxLines() == 1;
if (singleline) {
textRect.bottom = paint.getFontSpacing();
textRect.right = paint.measureText(text);
} else {
final StaticLayout layout = new StaticLayout(text, paint,
_widthLimit, Layout.Alignment.ALIGN_NORMAL, _spacingMult,
_spacingAdd, true);
// return early if we have more lines
Log.d("NLN", "Current Lines = " + Integer.toString(layout.getLineCount()));
Log.d("NLN", "Max Lines = " + Integer.toString(getMaxLines()));
if (getMaxLines() != NO_LINE_LIMIT
&& layout.getLineCount() > getMaxLines())
return 1;
textRect.bottom = layout.getHeight();
int maxWidth = -1;
for (int i = 0; i < layout.getLineCount(); i++)
if (maxWidth < layout.getLineWidth(i))
maxWidth = (int) layout.getLineWidth(i);
textRect.right = maxWidth;
}
textRect.offsetTo(0, 0);
if (availableSPace.contains(textRect))
// may be too small, don't worry we will find the best match
return -1;
// else, too big
return 1;
}
};
setEnabled(true);
setFocusableInTouchMode(true);
setFocusable(true);
setEnableSizeCache(false);
setMovementMethod(null);
_initiallized = true;
}
#Override
public void setTypeface(final Typeface tf) {
if (paint == null)
paint = new TextPaint(getPaint());
paint.setTypeface(tf);
super.setTypeface(tf);
}
#Override
public void setTextSize(final float size) {
_maxTextSize = size;
_textCachedSizes.clear();
adjustTextSize();
}
#Override
public void setMaxLines(final int maxlines) {
super.setMaxLines(maxlines);
_maxLines = maxlines;
reAdjust();
}
#Override
public int getMaxLines() {
return _maxLines;
}
#Override
public void setSingleLine() {
super.setSingleLine();
_maxLines = 1;
reAdjust();
}
#Override
public void setSingleLine(final boolean singleLine) {
super.setSingleLine(singleLine);
if (singleLine)
_maxLines = 1;
else
_maxLines = NO_LINE_LIMIT;
reAdjust();
}
#Override
public void setLines(final int lines) {
super.setLines(lines);
_maxLines = lines;
reAdjust();
}
#Override
public void setTextSize(final int unit, final float size) {
final Context c = getContext();
Resources r;
if (c == null)
r = Resources.getSystem();
else
r = c.getResources();
_maxTextSize = TypedValue.applyDimension(unit, size,
r.getDisplayMetrics());
_textCachedSizes.clear();
adjustTextSize();
}
#Override
public void setLineSpacing(final float add, final float mult) {
super.setLineSpacing(add, mult);
_spacingMult = mult;
_spacingAdd = add;
}
/**
* Set the lower text size limit and invalidate the view
*
* #param
*/
public void setMinTextSize(final float minTextSize) {
_minTextSize = minTextSize;
reAdjust();
}
private void reAdjust() {
adjustTextSize();
}
private void adjustTextSize() {
if (!_initiallized)
return;
Log.d("IsUserTyping"," "+isUserTyping);
if(!isUserTyping)
return;
final int startSize = (int) _minTextSize;
/*final int heightLimit = getMeasuredHeight()
- getCompoundPaddingBottom() - getCompoundPaddingTop();
_widthLimit = getMeasuredWidth() - getCompoundPaddingLeft()
- getCompoundPaddingRight();*/
//User maxWidth and maxHeight the textBox can attain to calculate resize values
final int heightLimit = getMaxHeight()
- getCompoundPaddingBottom() - getCompoundPaddingTop();
_widthLimit = getMaxWidth() - getCompoundPaddingLeft()
- getCompoundPaddingRight();
if (_widthLimit <= 0)
return;
_availableSpaceRect.right = _widthLimit;
_availableSpaceRect.bottom = heightLimit;
super.setTextSize(
TypedValue.COMPLEX_UNIT_PX,
efficientTextSizeSearch(startSize, (int) _maxTextSize,
_sizeTester, _availableSpaceRect));
}
/**
* Enables or disables size caching, enabling it will improve performance
* where you are animating a value inside TextView. This stores the font
* size against getText().length() Be careful though while enabling it as 0
* takes more space than 1 on some fonts and so on.
*
* #param enable
* enable font size caching
*/
public void setEnableSizeCache(final boolean enable) {
_enableSizeCache = enable;
_textCachedSizes.clear();
adjustTextSize();
}
private int efficientTextSizeSearch(final int start, final int end,
final SizeTester sizeTester, final RectF availableSpace) {
if (!_enableSizeCache)
return binarySearch(start, end, sizeTester, availableSpace);
final String text = getText().toString();
final int key = text == null ? 0 : text.length();
int size = _textCachedSizes.get(key);
if (size != 0)
return size;
size = binarySearch(start, end, sizeTester, availableSpace);
_textCachedSizes.put(key, size);
return size;
}
private int binarySearch(final int start, final int end,
final SizeTester sizeTester, final RectF availableSpace) {
int lastBest = start;
int lo = start;
int hi = end - 1;
int mid = 0;
while (lo <= hi) {
mid = lo + hi >>> 1;
final int midValCmp = sizeTester.onTestSize(mid, availableSpace);
if (midValCmp < 0) {
lastBest = lo;
lo = mid + 1;
} else if (midValCmp > 0) {
hi = mid - 1;
lastBest = hi;
} else
return mid;
}
// make sure to return last best
// this is what should always be returned
return lastBest;
}
#Override
protected void onTextChanged(final CharSequence text, final int start,
final int before, final int after) {
super.onTextChanged(text, start, before, after);
reAdjust();
}
#Override
protected void onSizeChanged(final int width, final int height,
final int oldwidth, final int oldheight) {
_textCachedSizes.clear();
super.onSizeChanged(width, height, oldwidth, oldheight);
if (width != oldwidth || height != oldheight)
reAdjust();
}
}
I found these details implemented here, which was easy to emulate on my end.
android:textIsSelectable="true"
Set it to false if you wanna see your cursor
you don't have to set textCursorDrawable to #null because the color only depends on your choice of accent color
Please remove the line that has setMovementMethod from null, this will appear the cursor to change the cursor drawable you can use textCursorDrawable

Android TextView: Phantom Padding When Padding Set To 0

I have TextView:
text.setTypeface(font);
text.setWidth((int)width);
text.setGravity(Gravity.CENTER);
text.setHeight((int)height);
text.setIncludeFontPadding(false);
text.setPadding(0,0,0,0);
However even with the padding set to 0, and setIncludeFontPadding to false and I set the font size to the height of the TextView I still get this:
If I set the padding to have negative values like -30, It fixes it:
text.setPadding(0,-30,0,-30);
My question is why do I have to do this? Where is this phantom padding coming from? And where am I finding this arbitrary (-30 in this specific case) value I need to set it to, so that the text fills the height?
Update
Trying #Mike M. 's solution I got these results. When it was a positive number, it was a smaller size than when it was a negative one. Both of them still had padding:
Update 2 Here the custom class in full:
import android.annotation.TargetApi;
import android.content.Context;
import android.content.res.Resources;
import android.graphics.Canvas;
import android.graphics.Rect;
import android.graphics.RectF;
import android.os.Build;
import android.text.Layout;
import android.text.Layout.Alignment;
import android.text.StaticLayout;
import android.text.TextPaint;
import android.util.AttributeSet;
import android.util.Log;
import android.util.SparseIntArray;
import android.util.TypedValue;
import android.widget.TextView;
/**
* Special textview which gracefully handles resizing.
*/
public class iTextView extends TextView
{
// Set true to remove phantom padding
public boolean trimPadding = false;
//region Interfaces
private interface SizeTester
{
/**
* Interface for scaling text to fit.
* #param suggestedSize Size of text to be tested.
* #param availableSpace Available space in which text must fit.
* #return An integer < 0 if after applying {#code suggestedSize} to
* text, it takes less space than {#code availableSpace}, > 0
* otherwise.
*/
int onTestSize(int suggestedSize, RectF availableSpace);
}
//endregion
//region Variables
private static final int NO_LINE_LIMIT = -1;
private RectF _textRect = new RectF();
private RectF _availableSpaceRect;
private SparseIntArray _textCachedSizes;
private TextPaint _paint;
private float _maxTextSize;
private float _spacingMult;
private float _spacingAdd;
private float _minTextSize;
private int _widthLimit;
private int _maxLines;
private boolean _enableSizeCache;
private boolean _initialized;
//endregion
//region Constructors
public iTextView(Context context)
{
super(context);
initialize();
}
public iTextView(Context context, AttributeSet attrs)
{
super(context, attrs);
initialize();
}
public iTextView(Context context, AttributeSet attrs, int defStyle)
{
super(context, attrs, defStyle);
initialize();
}
//endregion
//region Initialization
private void initialize()
{
_spacingMult = 1.0f;
_spacingAdd = 0.0f;
_minTextSize = 20;
_enableSizeCache = true;
_paint = new TextPaint(getPaint());
_maxTextSize = getTextSize();
_availableSpaceRect = new RectF();
_textCachedSizes = new SparseIntArray();
if (_maxLines == 0)
{
// No value was assigned during construction
_maxLines = NO_LINE_LIMIT;
}
_initialized = true;
}
//endregion
//region Text Value
#Override
public void setText(final CharSequence text, BufferType type)
{
super.setText(text, type);
adjustTextSize(text.toString());
}
#Override
protected void onTextChanged(final CharSequence text, final int start, final int before,
final int after)
{
super.onTextChanged(text, start, before, after);
reAdjust();
}
//endregion
//region Text Sizing
#Override
public void setTextSize(float size)
{
_maxTextSize = size;
_textCachedSizes.clear();
adjustTextSize(getText().toString());
}
/**
* Ensures the text is as big as possible for the text area.
*/
public void setTextSizeToMaxFit()
{
_maxTextSize = 999;
_textCachedSizes.clear();
adjustTextSize(getText().toString());
}
#Override
public void setTextSize(int unit, float size)
{
Context context = getContext();
Resources resources;
if (context == null)
{
resources = Resources.getSystem();
} else
{
resources = context.getResources();
}
_maxTextSize = TypedValue.applyDimension(unit, size, resources.getDisplayMetrics());
_textCachedSizes.clear();
adjustTextSize(getText().toString());
}
/**
* Set the lower text size limit and invalidate the view
*
* #param minTextSize
*/
public void setMinTextSize(float minTextSize)
{
_minTextSize = minTextSize;
reAdjust();
}
private void reAdjust() {
adjustTextSize(getText().toString());
}
private void adjustTextSize(String string)
{
if (!_initialized)
{
return;
}
int startSize = (int) _minTextSize;
int heightLimit = getMeasuredHeight() - getCompoundPaddingBottom() - getCompoundPaddingTop();
_widthLimit = getMeasuredWidth() - getCompoundPaddingLeft() - getCompoundPaddingRight();
_availableSpaceRect.right = _widthLimit;
_availableSpaceRect.bottom = heightLimit;
super.setTextSize
(
TypedValue.COMPLEX_UNIT_PX,
efficientTextSizeSearch(startSize, (int)_maxTextSize, mSizeTester, _availableSpaceRect)
);
}
private final SizeTester mSizeTester = new SizeTester()
{
#TargetApi(Build.VERSION_CODES.JELLY_BEAN)
#Override
public int onTestSize(int suggestedSize, RectF availableSPace)
{
_paint.setTextSize(suggestedSize);
String text = getText().toString();
boolean singleLine = getMaxLines() == 1;
if (singleLine)
{
_textRect.bottom = _paint.getFontSpacing();
_textRect.right = _paint.measureText(text);
} else
{
StaticLayout layout = new StaticLayout
(
text,
_paint,
_widthLimit,
Alignment.ALIGN_NORMAL,
_spacingMult,
_spacingAdd,
true
);
// Return early if no more lines
if (getMaxLines() != NO_LINE_LIMIT && layout.getLineCount() > getMaxLines())
{
return 1;
}
_textRect.bottom = layout.getHeight();
int maxWidth = -1;
for (int i = 0; i < layout.getLineCount(); i++)
{
if (maxWidth < layout.getLineWidth(i))
{
maxWidth = (int)layout.getLineWidth(i);
}
}
_textRect.right = maxWidth;
}
_textRect.offsetTo(0, 0);
if (availableSPace.contains(_textRect))
{
// May be too small, Will find the best match later
return -1;
} else
{
// Too big
return 1;
}
}
};
/**
* Enables or disables size caching, enabling it will improve performance
* where its animating a value inside TextView. This stores the font
* size against getText().length() Enabling it as 0
* takes more space than 1 on some fonts and so on.
* #param enable Enable font size caching
*/
public void enableSizeCache(boolean enable)
{
_enableSizeCache = enable;
_textCachedSizes.clear();
adjustTextSize(getText().toString());
}
private int efficientTextSizeSearch(int start, int end, SizeTester sizeTester, RectF availableSpace)
{
if (!_enableSizeCache)
{
return binarySearch(start, end, sizeTester, availableSpace);
}
String text = getText().toString();
int key = text == null ? 0 : text.length();
int size = _textCachedSizes.get(key);
if (size != 0)
{
return size;
}
size = binarySearch(start, end, sizeTester, availableSpace);
_textCachedSizes.put(key, size);
return size;
}
#Override
protected void onSizeChanged(int width, int height, int oldwidth,int oldheight)
{
_textCachedSizes.clear();
super.onSizeChanged(width, height, oldwidth, oldheight);
if (width != oldwidth || height != oldheight)
{
reAdjust();
}
}
private static int binarySearch(int start, int end, SizeTester sizeTester,RectF availableSpace)
{
int lastBest = start;
int low = start;
int high = end - 1;
int mid = 0;
while (low <= high)
{
mid = (low + high) >>> 1;
int midValCmp = sizeTester.onTestSize(mid, availableSpace);
if (midValCmp < 0)
{
lastBest = low;
low = mid + 1;
} else if (midValCmp > 0)
{
high = mid - 1;
lastBest = high;
} else
{
return mid;
}
}
// Make sure to return last best
// This is what should always be returned
return lastBest;
}
//endregion
//region Text Lines
#Override
public void setMaxLines(int maxlines)
{
super.setMaxLines(maxlines);
_maxLines = maxlines;
reAdjust();
}
public int getMaxLines()
{
return _maxLines;
}
#Override
public void setSingleLine()
{
super.setSingleLine();
_maxLines = 1;
reAdjust();
}
#Override
public void setSingleLine(boolean singleLine)
{
super.setSingleLine(singleLine);
if (singleLine)
{
_maxLines = 1;
} else
{
_maxLines = NO_LINE_LIMIT;
}
reAdjust();
}
#Override
public void setLines(int lines)
{
super.setLines(lines);
_maxLines = lines;
reAdjust();
}
#Override
public void setLineSpacing(float add, float mult)
{
super.setLineSpacing(add, mult);
_spacingMult = mult;
_spacingAdd = add;
}
//endregion
//region Padding Fix
#Override
protected void onDraw(Canvas canvas)
{
if (trimPadding)
{
trimVertical();
}
super.onDraw(canvas);
}
private void trimVertical()
{
final Layout layout = getLayout();
final Rect textBounds = new Rect();
if (layout == null)
{
Log.d("Layout is null","" + layout);
return;
}
int baseline = layout.getLineBaseline(0);
getTextBounds(0, textBounds);
final int pTop = baseline + textBounds.top;
final int lastLine = getLineCount() - 1;
baseline = layout.getLineBaseline(lastLine);
getTextBounds(lastLine, textBounds);
final int pBottom = layout.getHeight() - baseline - textBounds.bottom + 1;
setPadding(getPaddingLeft(), -pTop, getPaddingRight(), -pBottom);
}
private void getTextBounds(int line, Rect bounds)
{
final String s = getText().toString();
final int start = getLayout().getLineStart(line);
final int end = getLayout().getLineEnd(line);
getPaint().getTextBounds(s, start, end, bounds);
}
//endregion
}

How to avoid character wrap of text in TextView, for auto-resizing text?

Background
After a lot of searching for the best solution of auto-resizing TextView (according to content, size, min&max lines, and font-size restrictions), I've made a merged solution for it all, here.
NOTE: I don't use other solutions because they don't work well, each has its own issues (something isn't supported, text goes outside of TextView, text get truncated,...) .
Demonstration of it works:
https://raw.githubusercontent.com/AndroidDeveloperLB/AutoFitTextView/master/animationPreview.gif
The problem
On some cases, the last character of one line wraps to the next line, as such:
Green is the boundaries of the TextView, red is outside of it.
The code
Basically, given the size of the TextView, its min&max font size and min&max lines, and the content (text) that's supposed to be within, it finds (using binary search) what font size should fit within the boundaries of the TextView.
The code is available in Github already, but here it is just in case :
public class AutoResizeTextView extends AppCompatTextView {
private static final int NO_LINE_LIMIT = -1;
private final RectF _availableSpaceRect = new RectF();
private final SizeTester _sizeTester;
private float _maxTextSize, _spacingMult = 1.0f, _spacingAdd = 0.0f, _minTextSize;
private int _widthLimit, _maxLines;
private boolean _initialized = false;
private TextPaint _paint;
private interface SizeTester {
/**
* #param suggestedSize Size of text to be tested
* #param availableSpace available space in which text must fit
* #return an integer < 0 if after applying {#code suggestedSize} to
* text, it takes less space than {#code availableSpace}, > 0
* otherwise
*/
int onTestSize(int suggestedSize, RectF availableSpace);
}
public AutoResizeTextView(final Context context) {
this(context, null, android.R.attr.textViewStyle);
}
public AutoResizeTextView(final Context context, final AttributeSet attrs) {
this(context, attrs, android.R.attr.textViewStyle);
}
public AutoResizeTextView(final Context context, final AttributeSet attrs, final int defStyle) {
super(context, attrs, defStyle);
// using the minimal recommended font size
_minTextSize = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP, 12, getResources().getDisplayMetrics());
_maxTextSize = getTextSize();
_paint = new TextPaint(getPaint());
if (_maxLines == 0)
// no value was assigned during construction
_maxLines = NO_LINE_LIMIT;
// prepare size tester:
_sizeTester = new SizeTester() {
final RectF textRect = new RectF();
#TargetApi(Build.VERSION_CODES.JELLY_BEAN)
#Override
public int onTestSize(final int suggestedSize, final RectF availableSPace) {
_paint.setTextSize(suggestedSize);
final TransformationMethod transformationMethod = getTransformationMethod();
final String text;
if (transformationMethod != null)
text = transformationMethod.getTransformation(getText(), AutoResizeTextView.this).toString();
else
text = getText().toString();
final boolean singleLine = getMaxLines() == 1;
if (singleLine) {
textRect.bottom = _paint.getFontSpacing();
textRect.right = _paint.measureText(text);
} else {
final StaticLayout layout = new StaticLayout(text, _paint, _widthLimit, Alignment.ALIGN_NORMAL, _spacingMult, _spacingAdd, true);
// return early if we have more lines
if (getMaxLines() != NO_LINE_LIMIT && layout.getLineCount() > getMaxLines())
return 1;
textRect.bottom = layout.getHeight();
int maxWidth = -1;
for (int i = 0; i < layout.getLineCount(); i++)
if (maxWidth < layout.getLineRight(i) - layout.getLineLeft(i))
maxWidth = (int) layout.getLineRight(i) - (int) layout.getLineLeft(i);
textRect.right = maxWidth;
}
textRect.offsetTo(0, 0);
if (availableSPace.contains(textRect))
// may be too small, don't worry we will find the best match
return -1;
// else, too big
return 1;
}
};
_initialized = true;
}
#Override
public void setAllCaps(boolean allCaps) {
super.setAllCaps(allCaps);
adjustTextSize();
}
#Override
public void setTypeface(final Typeface tf) {
super.setTypeface(tf);
adjustTextSize();
}
#Override
public void setTextSize(final float size) {
_maxTextSize = size;
adjustTextSize();
}
#Override
public void setMaxLines(final int maxlines) {
super.setMaxLines(maxlines);
_maxLines = maxlines;
adjustTextSize();
}
#Override
public int getMaxLines() {
return _maxLines;
}
#Override
public void setSingleLine() {
super.setSingleLine();
_maxLines = 1;
adjustTextSize();
}
#Override
public void setSingleLine(final boolean singleLine) {
super.setSingleLine(singleLine);
if (singleLine)
_maxLines = 1;
else _maxLines = NO_LINE_LIMIT;
adjustTextSize();
}
#Override
public void setLines(final int lines) {
super.setLines(lines);
_maxLines = lines;
adjustTextSize();
}
#Override
public void setTextSize(final int unit, final float size) {
final Context c = getContext();
Resources r;
if (c == null)
r = Resources.getSystem();
else r = c.getResources();
_maxTextSize = TypedValue.applyDimension(unit, size, r.getDisplayMetrics());
adjustTextSize();
}
#Override
public void setLineSpacing(final float add, final float mult) {
super.setLineSpacing(add, mult);
_spacingMult = mult;
_spacingAdd = add;
}
/**
* Set the lower text size limit and invalidate the view
*
* #param minTextSize
*/
public void setMinTextSize(final float minTextSize) {
_minTextSize = minTextSize;
adjustTextSize();
}
private void adjustTextSize() {
// This is a workaround for truncated text issue on ListView, as shown here: https://github.com/AndroidDeveloperLB/AutoFitTextView/pull/14
// TODO think of a nicer, elegant solution.
// post(new Runnable()
// {
// #Override
// public void run()
// {
if (!_initialized)
return;
final int startSize = (int) _minTextSize;
final int heightLimit = getMeasuredHeight() - getCompoundPaddingBottom() - getCompoundPaddingTop();
_widthLimit = getMeasuredWidth() - getCompoundPaddingLeft() - getCompoundPaddingRight();
if (_widthLimit <= 0)
return;
_paint = new TextPaint(getPaint());
_availableSpaceRect.right = _widthLimit;
_availableSpaceRect.bottom = heightLimit;
superSetTextSize(startSize);
// }
// });
}
private void superSetTextSize(int startSize) {
int textSize = binarySearch(startSize, (int) _maxTextSize, _sizeTester, _availableSpaceRect);
super.setTextSize(TypedValue.COMPLEX_UNIT_PX, textSize);
}
private int binarySearch(final int start, final int end, final SizeTester sizeTester, final RectF availableSpace) {
int lastBest = start, lo = start, hi = end - 1, mid;
while (lo <= hi) {
mid = lo + hi >>> 1;
final int midValCmp = sizeTester.onTestSize(mid, availableSpace);
if (midValCmp < 0) {
lastBest = lo;
lo = mid + 1;
} else if (midValCmp > 0) {
hi = mid - 1;
lastBest = hi;
} else return mid;
}
// make sure to return last best
// this is what should always be returned
return lastBest;
}
#Override
protected void onTextChanged(final CharSequence text, final int start, final int before, final int after) {
super.onTextChanged(text, start, before, after);
adjustTextSize();
}
#Override
protected void onSizeChanged(final int width, final int height, final int oldwidth, final int oldheight) {
super.onSizeChanged(width, height, oldwidth, oldheight);
if (width != oldwidth || height != oldheight)
adjustTextSize();
}
}
The question
Why does it occur? What can I do to fix this?
Seems it's possible using support library:
<TextView
android:layout_width="250dp" android:layout_height="wrap_content" android:background="#f00"
android:breakStrategy="balanced" android:hyphenationFrequency="none"
android:text="This is an example text" android:textSize="30dp" app:autoSizeTextType="uniform"/>
Sadly, it has 2 disadvantages:
Doesn't always spread words nicely. Reported here.
Requires Android API 23 and above (here).
More information here.
I had similar problem in my project. Long-time Googling, StackOverflow (with your question among others). Nothing.
And my final "solution" was BreakIterator for words + measure them all to check this situation.
UPDATE (2018-08-10):
static public boolean isTextFitWidth(final #Nullable String source, final #NonNull BreakIterator bi, final #NonNull TextPaint paint, final int width, final float textSize)
{
if (null == source || source.length() <= 0) {
return true;
}
TextPaint paintCopy = new TextPaint();
paintCopy.set(paint);
paintCopy.setTextSize(textSize);
bi.setText(source);
int start = bi.first();
for (int end = bi.next(); BreakIterator.DONE != end; start = end, end = bi.next()) {
int wordWidth = (int)Math.ceil(paintCopy.measureText(source, start, end));
if (wordWidth > width) {
return false;
}
}
return true;
}
static public boolean isTextFitWidth(final #NonNull TextView textView, final #NonNull BreakIterator bi, final int width, final #Nullable Float textSize)
{
final int textWidth = width - textView.getPaddingLeft() - textView.getPaddingRight();
return isTextFitWidth(textView.getText().toString(), bi, textView.getPaint(), textWidth,
null != textSize ? textSize : textView.getTextSize());
}

how to set hintcolor programmatically

I this specific example I want set hint text color progrommatically but I couldn't change it.
public class FloatingHintEditText extends EditText {
private static enum Animation { NONE, SHRINK, GROW }
private final Paint mFloatingHintPaint = new Paint();
private final ColorStateList mHintColors;
private final float mHintScale;
private final int mAnimationSteps;
private boolean mWasEmpty;
private int mAnimationFrame;
private Animation mAnimation = Animation.NONE;
public FloatingHintEditText(Context context) {
this(context, null);
}
public FloatingHintEditText(Context context, AttributeSet attrs) {
this(context, attrs, R.attr.floatingHintEditTextStyle);
}
public FloatingHintEditText(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
TypedValue typedValue = new TypedValue();
getResources().getValue(R.dimen.floatinghintedittext_hint_scale, typedValue, true);
mHintScale = typedValue.getFloat();
mAnimationSteps = getResources().getInteger(R.dimen.floatinghintedittext_animation_steps);
mHintColors = getHintTextColors();
mWasEmpty = TextUtils.isEmpty(getText());
}
#Override
public int getCompoundPaddingTop() {
final FontMetricsInt metrics = getPaint().getFontMetricsInt();
final int floatingHintHeight = (int) ((metrics.bottom - metrics.top) * mHintScale);
return super.getCompoundPaddingTop() + floatingHintHeight;
}
#Override
protected void onTextChanged(CharSequence text, int start, int lengthBefore, int lengthAfter) {
super.onTextChanged(text, start, lengthBefore, lengthAfter);
final boolean isEmpty = TextUtils.isEmpty(getText());
// The empty state hasn't changed, so the hint stays the same.
if (mWasEmpty == isEmpty) {
return;
}
mWasEmpty = isEmpty;
// Don't animate if we aren't visible.
if (!isShown()) {
return;
}
if (isEmpty) {
mAnimation = Animation.GROW;
setHintTextColor(Color.TRANSPARENT);
} else {
mAnimation = Animation.SHRINK;
}
}
#Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
if (TextUtils.isEmpty(getHint())) {
return;
}
final boolean isAnimating = mAnimation != Animation.NONE;
// The large hint is drawn by Android, so do nothing.
if (!isAnimating && TextUtils.isEmpty(getText())) {
return;
}
mFloatingHintPaint.set(getPaint());
mFloatingHintPaint.setColor(
mHintColors.getColorForState(getDrawableState(), mHintColors.getDefaultColor()));
final float hintPosX = getCompoundPaddingLeft() + getScrollX();
final float normalHintPosY = getBaseline();
final float floatingHintPosY = normalHintPosY + getPaint().getFontMetricsInt().top + getScrollY();
final float normalHintSize = getTextSize();
final float floatingHintSize = normalHintSize * mHintScale;
// If we're not animating, we're showing the floating hint, so draw it and bail.
if (!isAnimating) {
mFloatingHintPaint.setTextSize(floatingHintSize);
canvas.drawText(getHint().toString(), hintPosX, floatingHintPosY, mFloatingHintPaint);
return;
}
if (mAnimation == Animation.SHRINK) {
drawAnimationFrame(canvas, normalHintSize, floatingHintSize,
hintPosX, normalHintPosY, floatingHintPosY);
} else {
drawAnimationFrame(canvas, floatingHintSize, normalHintSize,
hintPosX, floatingHintPosY, normalHintPosY);
}
mAnimationFrame++;
if (mAnimationFrame == mAnimationSteps) {
if (mAnimation == Animation.GROW) {
setHintTextColor(mHintColors);
}
mAnimation = Animation.NONE;
mAnimationFrame = 0;
}
invalidate();
}
private void drawAnimationFrame(Canvas canvas, float fromSize, float toSize,
float hintPosX, float fromY, float toY) {
final float textSize = lerp(fromSize, toSize);
final float hintPosY = lerp(fromY, toY);
mFloatingHintPaint.setTextSize(textSize);
canvas.drawText(getHint().toString(), hintPosX, hintPosY, mFloatingHintPaint);
}
private float lerp(float from, float to) {
final float alpha = (float) mAnimationFrame / (mAnimationSteps - 1);
return from * (1 - alpha) + to * alpha;
}
}
So when I call from my activity mEditText.setHintTextColor(getResources().getColor(R.color.red)) the color doesn't change
The color changes when I set only from xml.
I tried to do mEditText.invalidate() but it doesn't help too.
What should I do here to make my hintTextColor red.
Make your fields not final and try this:
public FloatingHintEditText(Context context) {
super(context);
init();
}
public FloatingHintEditText(Context context, AttributeSet attrs) {
super(context, attrs);
init();
}
public FloatingHintEditText(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
init();
}
private void init() {
TypedValue typedValue = new TypedValue();
getResources().getValue(R.dimen.floatinghintedittext_hint_scale, typedValue, true);
mHintScale = typedValue.getFloat();
mAnimationSteps = getResources().getInteger(R.dimen.floatinghintedittext_animation_steps);
mHintColors = getHintTextColors();
mWasEmpty = TextUtils.isEmpty(getText());
}

Categories

Resources