Custom TextView - setText() called before constructor - android

I've got a problem with my CustomTextView. I'm trying to get a custom value from my layout-xml file and use this in my setText() method. Unfortunately the setText() method gets called before the constructor and because of this I can't use the custom value in this method.
Here's my code (broken down to the relevant parts):
CustomTextView.class
public class CustomTextView extends TextView {
private float mHeight;
private final String TAG = "CustomTextView";
private static final Spannable.Factory spannableFactory = Spannable.Factory.getInstance();
public CustomTextView(Context context, AttributeSet attrs) {
super(context, attrs);
Log.d(TAG, "in CustomTextView constructor");
TypedArray values = context.obtainStyledAttributes(attrs, R.styleable.CustomTextView);
this.mHeight = values.getDimension(R.styleable.CustomTextView_cHeight, 20);
}
#Override
public void setText(CharSequence text, BufferType type) {
Log.d(TAG, "in setText function");
Spannable s = getCustomSpannableString(getContext(), text);
super.setText(s, BufferType.SPANNABLE);
}
private static Spannable getCustomSpannableString(Context context, CharSequence text) {
Spannable spannable = spannableFactory.newSpannable(text);
doSomeFancyStuff(context, spannable);
return spannable;
}
private static void doSomeFancyStuff(Context context, Spannable spannable) {
/*Here I'm trying to access the mHeight attribute.
Unfortunately it's 0 though I set it to 24 in my layout
and it's correctly set in the constructor*/
}
}
styles.xml
<declare-styleable name="CustomTextView">
<attr name="cHeight" format="dimension"/>
</declare-styleable>
layout.xml
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:ctvi="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<com.mypackage.views.CustomTextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="#string/my_fancy_string"
android:textSize="16sp"
ctvi:cHeight="24dp" />
</LinearLayout>
And just as a proof - here's the LogCat output:
30912-30912/com.mypackage.views D/CustomTextView﹕ in setText function
30912-30912/com.mypackage.views D/CustomTextView﹕ in CustomTextView constructor
So as you can see the setText() method is called before the constructor. That's kinda weird and I don't know what I need to change in order to use my custom attribute (cHeight) in the setText-method.
Thanks in advance for any help!

It's the TextView super() constructor that calls your setText() based on the attribute values.
If you really need to access your custom attribute when setting a text value, use a custom attribute for the text as well.

I don't think any of this solutions are good, IMHO. What if you just use a custom method, like setCustomText() instead of overriding the custom TextView.setText(). I think it could be much better in terms of scalability, and hacking / overriding the implementation of the TextView could lead you into future problems.
Cheers!

First of all, remember to always recycle the TypedArray after using it.
TextView calls #setText(CharSequence text, BufferType type) during its construction therefore define a delayed call to setText as so:
private Runnable mDelayedSetter;
private boolean mConstructorCallDone;
public CustomTextView(Context context, AttributeSet attrs) {
super(context, attrs);
Log.d(TAG, "in CustomTextView constructor");
TypedArray values = context.obtainStyledAttributes(attrs, R.styleable.CustomTextView);
this.mHeight = values.getDimension(R.styleable.CustomTextView_cHeight, 20);
mConstructorCallDone = true;
}
Then inside your setText-override:
public void setText(final CharSequence text, final TextView.BufferType type) {
if (!mConstructorCallDone) {
// The original call needs to be made at this point otherwise an exception will be thrown in BoringLayout if text contains \n or some other characters.
super.setText(text, type);
// Postponing setting text via XML until the constructor has finished calling
mDelayedSetter = new Runnable() {
#Override
public void run() {
CustomTextView.this.setText(text, type);
}
};
post(mDelayedSetter);
} else {
removeCallbacks(mDelayedSetter);
Spannable s = getCustomSpannableString(getContext(), text);
super.setText(s, BufferType.SPANNABLE);
}
}

Unfortunately it is a limitation on Java, that requires to call super(..) in constructor before anything else. So, your only workaround is to call setText(..) again after you initialize the custom attributes.
Just remember, as setText called also before you initialize your custom attributes, they may have null value and you can get NullPointerException
Check my example of customTextView which capitalize first letter and adds double dots at the and (I use it in all my activities)
package com.example.myapp_android_box_detector;
import android.content.Context;
import android.content.res.TypedArray;
import android.support.v7.widget.AppCompatTextView;
import android.util.AttributeSet;
public class CapsTextView extends AppCompatTextView {
public Boolean doubleDot;
private Boolean inCustomText = false;
public CapsTextView(Context context){
super(context);
doubleDot = false;
setText(getText());
}
public CapsTextView(Context context, AttributeSet attrs){
super(context, attrs);
initAttrs(context, attrs);
setText(getText());
}
public CapsTextView(Context context, AttributeSet attrs, int defStyle){
super(context, attrs, defStyle);
initAttrs(context, attrs);
setText(getText());
}
public void initAttrs(Context context, AttributeSet attrs){
TypedArray a = context.getTheme().obtainStyledAttributes(attrs, R.styleable.CapsTextView, 0, 0);
doubleDot = a.getBoolean(R.styleable.CapsTextView_doubleDot, false);
a.recycle();
}
#Override
public void setText(CharSequence text, BufferType type) {
if (text.length() > 0){
text = String.valueOf(text.charAt(0)).toUpperCase() + text.subSequence(1, text.length());
// Adds double dot (:) to the end of the string
if (doubleDot != null && doubleDot){
text = text + ":";
}
}
super.setText(text, type);
}
}

Related

Android - Custom EditText with different input types

I'm trying to make a custom EditText for currency which means I need to have a prefix of it for the currency and I have to limit users' input to numbers only.
This is my custom EditText code
public OpenSansEditText(Context context, AttributeSet attrs) {
super(context, attrs);
paint = getPaint();
applyCustomFont(context, attrs);
}
public OpenSansEditText(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
paint = getPaint();
applyCustomFont(context, attrs);
}
private void applyCustomFont(Context context, AttributeSet attrs) {
TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.OpenSansET);
...
// Prefix
String prefix = a.getString(R.styleable.OpenSansET_prefix);
if (prefix != null) {
mPrefix = prefix;
} else {
mPrefix = "";
}
// Prefix Color
int prefixColor = a.getColor(R.styleable.OpenSansET_prefixColor, 0);
if (prefix != null) {
mPrefixColor = prefixColor;
} else {
mPrefixColor = ContextCompat.getColor(context, R.color.miBlack);
}
a.recycle();
}
...
#Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
if (!mPrefix.equals("")) {
getPaint().getTextBounds(mPrefix, 0, mPrefix.length(), mPrefixRect);
mPrefixRect.right += getPaint().measureText(" "); // add some offset
}
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
}
#Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
if (!mPrefix.equals("")) {
paint.setColor(mPrefixColor);
canvas.drawText(mPrefix, super.getCompoundPaddingLeft(), getBaseline(), paint);
}
}
#Override
public int getCompoundPaddingLeft() {
return mPrefix.equals("") ? super.getCompoundPaddingLeft()
: super.getCompoundPaddingLeft() + mPrefixRect.width();
}
This is how I use it in xml :
<com.asta.www.classes.OpenSansEditText
android:id="#+id/shopping_filter_priceMinRange"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="0.4"
android:gravity="center"
android:hint="#string/min"
android:textColor="#color/miBlack"
android:textColorHint="#color/miGrey"
app:prefix="$"
app:prefixColor="#color/miBlack" />
<com.asta.www.classes.OpenSansEditText
android:id="#+id/shopping_filter_priceMaxRange"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="0.4"
android:gravity="center"
android:hint="#string/max"
android:inputType="number"
android:textColorHint="#color/miGrey"
app:prefix="$"
app:prefixColor="#color/miBlack" />
Which yields :
Only the first one without inputType as number has the currency sign shown, whereas the second ET doesn't have its currency sign shown.
How to achieve currency prefix as text and still keeping inputType to numbers only for user? And I don't want to use two views, namely EditText and TextView to left of it, both inside a ViewGroup to achieve that.
For this type of scenarios I use Compound views. Please see below code for more information.
First create a layout for your custom view like below.
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="horizontal" android:layout_width="match_parent"
android:layout_height="wrap_content">
<TextView
android:id="#+id/txt_prefix"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="$" />
<EditText
android:id="#+id/et_value"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_weight="1"
android:inputType="number" />
</LinearLayout>
Now create a new class which should extends the LinearLayout. See below code.
public class OpenSansEditText extends LinearLayout {
private TextView txtPrefix;
private EditText etValue;
private String prefix = "$";
private int prefixColor = Color.BLACK;
public OpenSansEditText(Context context) {
super(context);
initializeViews(context, null);
}
public OpenSansEditText(Context context, #Nullable AttributeSet attrs) {
super(context, attrs);
initializeViews(context, attrs);
}
public OpenSansEditText(Context context, #Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
initializeViews(context, attrs);
}
private void initializeViews(Context context, AttributeSet attrs) {
LayoutInflater inflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
inflater.inflate(R.layout.opensansedittext_view, this,true);
if (attrs != null) {
TypedArray a = getContext().obtainStyledAttributes(attrs, R.styleable.OpenSansEditText);
prefix = a.getString(R.styleable.OpenSansEditText_prefix);
prefixColor = a.getColor(R.styleable.OpenSansEditText_prefixColor, Color.BLACK);
}
}
public CharSequence getValue(){
return etValue.getText();
}
public CharSequence getPrefix(){
return txtPrefix.getText();
}
#Override
protected void onFinishInflate() {
super.onFinishInflate();
txtPrefix = (TextView) findViewById(R.id.txt_prefix);
etValue = (EditText) findViewById(R.id.et_value);
txtPrefix.setText(prefix);
txtPrefix.setTextColor(prefixColor);
}
}
And then add your attributes to attribute xml file Ex: (attrs.xml in my case)
<resources>
<declare-styleable name="OpenSansEditText">
<attr name="prefix" format="string"/>
<attr name="prefixColor" format="color"/>
</declare-styleable>
</resources>
Now you can use it anywhere in the project as below
<com.asta.www.classes.OpenSansEditText
android:layout_width="match_parent"
android:layout_height="match_parent"
app:prefix="$"
app:prefixColor="#f00"/>
Hope this will help you to solve your problem. Thanks...
In the end I found this link https://gist.github.com/kennydude/5407963 which helps me in the right direction. So what it does is I think making the prefix as Drawable using this class :
private class TagDrawable extends Drawable {
public String text = "";
public void setText(String s){
text = s;
// Tell it we need to be as big as we want to be!
setBounds(0,0,getIntrinsicWidth(),getIntrinsicHeight());
invalidateSelf();
}
#Override
public void draw(#NonNull Canvas canvas) {
// I don't know why this y works here, but it does :)
// (aka if you are from Google/are Jake Wharton and I have done it wrong, please tell me!)
canvas.drawText( text, 0, mLine0Baseline + canvas.getClipBounds().top, mTextPaint );
}
#Override public void setAlpha(int i) {}
#Override public void setColorFilter(ColorFilter colorFilter) {}
#Override public int getOpacity() {return PixelFormat.UNKNOWN;}
#Override public int getIntrinsicHeight (){
return (int)mFontHeight;
}
#Override public int getIntrinsicWidth(){
return (int)mTextPaint.measureText( text );
}
}
And draw it to the left of the TextView like
TagDrawable left = new TagDrawable();
left.setText("$");
setCompoundDrawablesRelative(left, null, null, null);
The link I supplied even has suffix support which I haven't tried.

Android RTL password fields?

It appears that if you have an EditText on android with the
android:inputType="textPassword" or android:password="true
fields on them, right-to-left text does NOT appear right-to-left (stays left-to-right).
However without the password denotations the text does appear RTL.
Is this a known issue or is there a workaround?
For 17+ (4.2.x+) you can use textAlignment
android:textAlignment="viewStart"
The only solution I've found was to set the gravity programatically to LEFT or RIGHT after setting the inputType.
In my case, the problem was simply solved by changing the layout_width to wrap_content.
If you put inputType = textPassword or set a passwordTransformation method on EditText, text direction is taken as LTR. It means RTL for password is discouraged. You need to write custom TextView to override this behaviour.
Code snippet from android source for TextView.
// PasswordTransformationMethod always have LTR text direction heuristics returned by
// getTextDirectionHeuristic, needs reset
mTextDir = getTextDirectionHeuristic();
protected TextDirectionHeuristic getTextDirectionHeuristic() {
if (hasPasswordTransformationMethod()) {
// passwords fields should be LTR
return TextDirectionHeuristics.LTR;
}
In My Case both worked fine.
1) android:textAlignment="viewStart"
And
2)
https://stackoverflow.com/a/38291472/6493661
and the right answer is:
RtlEditText mUserPassword = root.findViewById(R.id.register_fragment_password);
mUserPassword.setTransformationMethod(new AsteriskPasswordTransformationMethod());
creating our own EditText!
it work prefectly only if you replace the dot with astrix by AsteriskPasswordTransformationMethod below this code.
public class RtlEditText extends EditText {
public RtlEditText(Context context) {
super(context);
}
public RtlEditText(Context context, #Nullable AttributeSet attrs) {
super(context, attrs);
}
public RtlEditText(Context context, #Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
public RtlEditText(Context context, #Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
}
#Override
public TextDirectionHeuristic getTextDirectionHeuristic() {
// passwords fields should be LTR
return TextDirectionHeuristics.ANYRTL_LTR;
}}
public class AsteriskPasswordTransformationMethod extends PasswordTransformationMethod {
#Override
public CharSequence getTransformation(CharSequence source, View view) {
return new PasswordCharSequence(source);
}
private class PasswordCharSequence implements CharSequence {
private CharSequence mSource;
public PasswordCharSequence(CharSequence source) {
mSource = source; // Store char sequence
}
public char charAt(int index) {
return '*'; // This is the important part
}
public int length() {
return mSource.length(); // Return default
}
public CharSequence subSequence(int start, int end) {
return mSource.subSequence(start, end); // Return default
}
}
}

how do you color percentage of a textview in android?

I know Spannable can help me color any specific letters in a textview. However, is it possible to color 1/2 or 1/3 of a letter. I wanted to color text within a textview by percentage instead of by letter. Thanks for reading, and please let me know if you had some idea or solution to this.
thanks
It may be easier to use Spanned thought android.text.HTML
So something like this:
Spanned text = HTML.fromHtml("Sp<span style=\"color:red\">ann</span>able");
textView.setText(text);
It can also be used to add images, but it a bit more complicated.
UPDATE
I re-read your question and thought of a solution. You could create a custom view that has two textViews in a FrameLayout (on top of each other) and then resize the one on the top relative to the percentage. Something like this:
import android.content.Context;
import android.content.res.ColorStateList;
import android.util.AttributeSet;
import android.widget.FrameLayout;
import android.widget.TextView;
public class ProgressTextView extends FrameLayout {
private TextView backgroundTextView;
private TextView foregroundTextView;
private CharSequence text;
private ProgressTextView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
init(context, attrs);
}
private ProgressTextView(Context context, AttributeSet attrs) {
super(context, attrs);
init(context, attrs);
}
private ProgressTextView(Context context) {
super(context);
init(context, null);
}
private void init(Context context, AttributeSet attrs) {
backgroundTextView = new TextView(context);
foregroundTextView = new TextView(context);
addView(backgroundTextView);
addView(foregroundTextView);
// process custom attributeSet from xml to set the colours
}
public void setBackgroundTextColor(int color) {
backgroundTextView.setTextColor(color);
}
public void setBackgroundTextColor(ColorStateList colors) {
backgroundTextView.setTextColor(colors);
}
public void setForegroundTextColor(int color) {
backgroundTextView.setTextColor(color);
}
public void setForegroundTextColor(ColorStateList colors) {
backgroundTextView.setTextColor(colors);
}
public void setPercentage(float per) {
foregroundTextView.setWidth((((float) backgroundTextView.getWidth()) / 100f) * per);
}
public void setText(CharSequence text) {
this.text = text;
backgroundTextView.setText(text);
foregroundTextView.setText(text);
}
public CharSequence getText() {
return text;
}
}
PS: not tested ;) just an idea
MORE UPDATE
Apparently, with this method the text gets shortened instead of just cropped as I expected. The maybe on creation of the foregroundTextView you could do this:
foregroundTextView = new TextView(context) {
#Override
protected void onDraw (Canvas canvas) {
super.onDraw(canvas);
Rect bounds = canvas.getClipBounds();
bounds.right (((float) bounds.right) / 100.0f) * per;
canvas.clipRect(bounds);
}
};
And also add the per variable and modify setPercentage(float per) to be just a setter:
private float per = 0.0f;
public void setPercentage(float per) {
this.per = per;
}
Hope this one works ;)
You could draw the letters yourself on a canvas. Each letter consisting of a bunch of interconnected arcs, that way you could style each individually.
Having to draw each letter would be considerably painstaking though, e.g., paint.setColor(), canvas.drawArc(), paint.setColor(), canvas.drawArc(), and so on...
create a drawable and override it's onDraw method and paint canvas how you wish and set it as background to your textview

Prevent ToggleButton from switching state

I have a ToggleButton, when you click it, I don't want the state to change. I will handle state changes myself when after I receive feedback from whatever the button toggled. How might I prevent the state change on click?
You can implement your own ToggleButton with overriden toggle() method with empty body.
You could simply use the CheckedTextView instead.
Of course, you need to set a background image and a text based on the state, but other than those (which you might have used already), it's a nice alternative solution.
here's a sample code in case you miss the textOn and textOff attributes:
CheckableTextView.java :
public class CheckableTextView extends CheckedTextView {
private CharSequence mTextOn, mTextOff;
public CheckableTextView (final Context context, final AttributeSet attrs, final int defStyle) {
super(context, attrs, defStyle);
final TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.CheckableTextView, defStyle, 0);
mTextOn = a.getString(R.styleable.CheckableTextView_textOn);
mTextOff = a.getString(R.styleable.CheckableTextView_textOff);
a.recycle();
}
public CheckableTextView(final Context context, final AttributeSet attrs) {
this(context, attrs, 0);
}
public CheckableTextView(final Context context) {
this(context, null, 0);
}
#Override
public void setChecked(final boolean checked) {
super.setChecked(checked);
if (mTextOn == null && mTextOff == null)
return;
if (checked)
super.setText(mTextOn);
else
super.setText(mTextOff);
}
public void setTextOff(final CharSequence textOff) {
this.mTextOff = textOff;
}
public void setTextOn(final CharSequence textOn) {
this.mTextOn = textOn;
}
public CharSequence getTextOff() {
return this.mTextOff;
}
public CharSequence getTextOn() {
return this.mTextOn;
}
}
in res/values/attr.xml :
<declare-styleable name="SyncMeCheckableTextView">
<attr name="textOn" format="reference|string" />
<attr name="textOff" format="reference|string" />
</declare-styleable>
another possible solution would be to use setClickable(false) on the ToggleButton, and handle onTouchListener when the motion action is ACTION_UP .
While I think you can just mark it as disabled, I don't think it is a good idea, as users are used to a certain semantic of such a button.
If you only want to show some state, why don't you use an ImageView and show different images depending on state?

How to change letter spacing in a Textview?

How can i change letter spacing in a textview?
Will it help if I have HTML text in it (I cannot use webview in my code).
P.S. I'm using my own typeface in the textview with HTML text.
Since API 21 there is an option set letter spacing. You can call method setLetterSpacing or set it in XML with attribute letterSpacing.
More space:
android:letterSpacing="0.1"
Less space:
android:letterSpacing="-0.07"
check out android:textScaleX
Depending on how much spacing you need, this might help. That's the only thing remotely related to letter-spacing in the TextView.
Edit: please see #JerabekJakub's response below for an updated, better method to do this starting with api 21 (Lollipop)
This answer is based on Pedro's answer but adjusted so it also works if text attribute is already set:
package nl.raakict.android.spc.widget;
import android.content.Context;
import android.text.Spannable;
import android.text.SpannableString;
import android.text.style.ScaleXSpan;
import android.util.AttributeSet;
import android.widget.TextView;
public class LetterSpacingTextView extends TextView {
private float letterSpacing = LetterSpacing.BIGGEST;
private CharSequence originalText = "";
public LetterSpacingTextView(Context context) {
super(context);
}
public LetterSpacingTextView(Context context, AttributeSet attrs){
super(context, attrs);
originalText = super.getText();
applyLetterSpacing();
this.invalidate();
}
public LetterSpacingTextView(Context context, AttributeSet attrs, int defStyle){
super(context, attrs, defStyle);
}
public float getLetterSpacing() {
return letterSpacing;
}
public void setLetterSpacing(float letterSpacing) {
this.letterSpacing = letterSpacing;
applyLetterSpacing();
}
#Override
public void setText(CharSequence text, BufferType type) {
originalText = text;
applyLetterSpacing();
}
#Override
public CharSequence getText() {
return originalText;
}
private void applyLetterSpacing() {
if (this == null || this.originalText == null) return;
StringBuilder builder = new StringBuilder();
for(int i = 0; i < originalText.length(); i++) {
String c = ""+ originalText.charAt(i);
builder.append(c.toLowerCase());
if(i+1 < originalText.length()) {
builder.append("\u00A0");
}
}
SpannableString finalText = new SpannableString(builder.toString());
if(builder.toString().length() > 1) {
for(int i = 1; i < builder.toString().length(); i+=2) {
finalText.setSpan(new ScaleXSpan((letterSpacing+1)/10), i, i+1, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
}
}
super.setText(finalText, BufferType.SPANNABLE);
}
public class LetterSpacing {
public final static float NORMAL = 0;
public final static float NORMALBIG = (float)0.025;
public final static float BIG = (float)0.05;
public final static float BIGGEST = (float)0.2;
}
}
If you want to use it programatically:
LetterSpacingTextView textView = new LetterSpacingTextView(context);
textView.setSpacing(10); //Or any float. To reset to normal, use 0 or LetterSpacingTextView.Spacing.NORMAL
textView.setText("My text");
//Add the textView in a layout, for instance:
((LinearLayout) findViewById(R.id.myLinearLayout)).addView(textView);
after API >=21 there is inbuild method provided by TextView called setLetterSpacing
check this for more
I built a custom class that extends TextView and solves this problem... Check out my answer here =)
As android doesn't support such a thing, you can do it manually with FontCreator. It has good options for font modifying.
I used this tool to build a custom font, even if it takes some times but you can always use it in your projects.
For embedding HTML text in your textview you can use Html.fromHTML() syntax.
More information you will get from http://developer.android.com/reference/android/text/Html.html#fromHtml%28java.lang.String%29

Categories

Resources