note: I've created a GitHub repo containing the repro for this bug here. Feel free to clone and try the app out yourself to see the bug. The relevant code is here: with the commented part kept in a comment it works fine, uncomment it and you will experience the bug.
I'm building a source code editor app for Android. I have a custom Editable type that wraps SpannableStringBuilder (which will henceforth be referred to as SSB). Here is its code:
package com.bluejay.myapplication;
import android.text.Editable;
import android.text.InputFilter;
import android.text.SpannableStringBuilder;
public class ColoredText implements Editable {
private final SpannableStringBuilder builder;
public ColoredText(String rawText) {
assert rawText != null;
this.builder = new SpannableStringBuilder(rawText);
}
#Override
public Editable replace(int st, int en, CharSequence source, int start, int end) {
this.builder.replace(st, en, source, start, end);
return this;
}
#Override
public Editable replace(int st, int en, CharSequence text) {
this.builder.replace(st, en, text);
return this;
}
#Override
public Editable insert(int where, CharSequence text, int start, int end) {
this.builder.insert(where, text, start, end);
return this;
}
#Override
public Editable insert(int where, CharSequence text) {
this.builder.insert(where, text);
return this;
}
#Override
public Editable delete(int st, int en) {
this.builder.delete(st, en);
return this;
}
#Override
public Editable append(CharSequence text) {
this.builder.append(text);
return this;
}
#Override
public Editable append(CharSequence text, int start, int end) {
this.builder.append(text, start, end);
return this;
}
#Override
public Editable append(char text) {
this.builder.append(text);
return this;
}
#Override
public void clear() {
this.builder.clear();
}
#Override
public void clearSpans() {
this.builder.clearSpans();
}
#Override
public void setFilters(InputFilter[] filters) {
this.builder.setFilters(filters);
}
#Override
public InputFilter[] getFilters() {
return this.builder.getFilters();
}
#Override
public void getChars(int start, int end, char[] dest, int destoff) {
this.builder.getChars(start, end, dest, destoff);
}
#Override
public void setSpan(Object what, int start, int end, int flags) {
this.builder.setSpan(what, start, end, flags);
}
#Override
public void removeSpan(Object what) {
this.builder.removeSpan(what);
}
#Override
public <T> T[] getSpans(int start, int end, Class<T> type) {
return this.builder.getSpans(start, end, type);
}
#Override
public int getSpanStart(Object tag) {
return this.builder.getSpanStart(tag);
}
#Override
public int getSpanEnd(Object tag) {
return this.builder.getSpanEnd(tag);
}
#Override
public int getSpanFlags(Object tag) {
return this.builder.getSpanFlags(tag);
}
#Override
public int nextSpanTransition(int start, int limit, Class type) {
return this.builder.nextSpanTransition(start, limit, type);
}
#Override
public int length() {
return this.builder.length();
}
#Override
public char charAt(int index) {
return this.builder.charAt(index);
}
#Override
public CharSequence subSequence(int start, int end) {
return this.builder.subSequence(start, end);
}
}
As you can see, this type is a simple wrapper for SSB. new ColoredText(str) creates the underlying SSB from str, and all of its method calls (with the exception of append, delete, etc. which return this instead of the SSB) simply forward to the SSB.
Now when I have an EditText and I try to set the ColoredText as the underlying text of the EditText, like so
EditText editText = (EditText) findViewById(R.id.editText);
// By default, setText() will attempt to copy the passed CharSequence into a new SSB.
// See https://github.com/android/platform_frameworks_base/blob/master/core/java/android/widget/TextView.java#L4396
// and https://github.com/android/platform_frameworks_base/blob/master/core/java/android/text/Editable.java#L143
// I want to prevent this and have the ColoredText instead of an SSB be the EditText's
// underlying text, that is, I want the mText member to be of type ColoredText.
editText.setEditableFactory(new Editable.Factory() {
#Override
public Editable newEditable(CharSequence source) {
return (Editable) source; // source is ColoredText
}
});
ColoredText text = new ColoredText("Hello world!\nHello world again!");
editText.setText(text, TextView.BufferType.EDITABLE);
The EditText will behave quite glitchy when edited. In the above example, tap anywhere on the first line with Hello world! and start typing random characters. The second line will be affected, and somehow (even if you don't touch a newline or arrow keys) the cursor will eventually spill over into the second line. And some of the chars you type may not get displayed, even though the cursor will move.
Now if you comment out the setEditableFactory part, so the text is copied into an SSB during setText(), and you run the app again, you will see there are no glitches.
It even works if you leave the setEditableFactory part intact, but replace the variable initialization of text with
SpannableStringBuilder text = new SpannableStringBuilder("Hello world!\nHello world again!");
Clearly, although setText() says it'll accept any Editable, it doesn't work well when dealing with anything other than an SSB. Why does this happen and how can I fix it? Thanks.
By digging the source code of SpannableStringBuilder I figured out that it not only fulfils the responsibilities defined by the Interfaces Editable, etc. but also reports the span change by calling SpanWatcher.onSpanChanged() by passing this. DynamicLayout (the real workhorse of EditText) responds to onSpanChanged() by checking the equality of passed in reference with it's member (which is our actual ColoredSpan instance). Obviously they are different and I suspect that this is a problem.
Actually SpannableStringBuilder is not just Editable, but more than that. If you need a custom Editable subclassing SpannableStringBuilder may work.
Related
I have a number picker set up with strings but I am having a problem with getting the string that is being displayed.
Here is the code for the construction of the number picker.
fractionPicker is my Number picker .
void fractionPickerFunction(){
final String[] arrayString= new String[]{"None", "1/8", "1/4", "3/8", "1/2", "5/8", "3/4", "7/8"};
fractionPicker.setMinValue(0);
fractionPicker.setMaxValue(arrayString.length-1);
fractionPicker.setFormatter(new NumberPicker.Formatter() {
#Override
public String format(int value) {
return arrayString[value];
}
});
}
Set a change listener if you need to just get the result. If you need to modify the variable with the setFormatter still then just use both. call your setFormatter, then also add the change listener. It will then return to you, your modified variable.
Change
void fractionPickerFunction(){
final String[] arrayString= new String[]{"None", "1/8", "1/4", "3/8", "1/2", "5/8", "3/4", "7/8"};
fractionPicker.setMinValue(0);
fractionPicker.setMaxValue(arrayString.length-1);
fractionPicker.setFormatter(new NumberPicker.Formatter() {
#Override
public String format(int value) {
return arrayString[value];
}
});
}
To this:
fractionPicker.setOnValueChangedListener(new OnValueChangeListener() {
#Override
public void onValueChange(NumberPicker picker, int oldVal, int newVal) {
// TODO Auto-generated method stub
String Old = "Old Value : ";
String New = "New Value : ";
}
});
You should refer to the android documentation on the NumberPicker. In short,
you are going to want to set a OnValueChangeListener by calling setOnValueChangedListener on your NumberPicker. You can do that with the following:
fractionPicker.setOnValueChangedListener(new OnValueChangeListener() {
#Override
public void onValueChange(NumberPicker picker, int oldVal, int newVal) {
// Do your work here
// Parameters
// picker NumberPicker: The NumberPicker associated with this listener.
// oldVal int: The previous value.
// newVal int: The new value.
}
});
I'm experiencing issues trying to use a custom type that implements Editable as the basis for an EditText. Here is my code:
// This is a custom type that implements Editable.
// It's just a wrapper over a SpannableStringBuilder, with some
// irrelevant added functionality.
public class ColoredText implements Editable {
private final SpannableStringBuilder builder;
public ColoredText(String text) {
this.builder = new SpannableStringBuilder(text);
}
// All Editable methods are implemented through this.builder.
#Override
public Editable replace(int i, int i1, CharSequence charSequence, int i2, int i3) {
this.builder.replace(i, i1, charSequence, i2, i3);
return this;
}
...
}
public class NoCopyEditableFactory extends Editable.Factory {
#Override
public Editable newEditable(CharSequence source) {
return (Editable) source;
}
}
// In MainActivity.java
#Override
protected void onCreate(Bundle bundle) {
super.onCreate(bundle);
setContentView(R.layout.main);
String line = "EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE";
String text = TextUtils.join("\n", Collections.nCopies(100, line));
Editable e = new ColoredText(text);
EditText et = (EditText) findViewById(R.id.editText);
et.setEditableFactory(new NoCopyEditableFactory()); // This line is causing trouble.
et.setText(e, TextView.BufferType.EDITABLE);
}
// In the MainActivity layout file
<EditText
android:id="#+id/editText"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
For some reason, when I comment out the NoCopyEditableFactory, the cursor updates just fine. However, when I uncomment the line forcing the EditText not to copy the ColoredText into a new SpannableStringBuilder, the cursor does not update when I click on new places in the text. Why is this?
The problem turned out to be with my Editable implementation. It works fine after I deleted some misbehaving code.
I need to block the user from inputing the . (period) character from the keyboard on a number EditText, but I need to be able to use it on the same EditText via the setText method.
I've tried using InputFilter but when I call setText the . character don't show. I've also tried setting the digits parameter in the xml but the setText doesn't work either.
Is there a way to do this?
Here's my InputFilter code:
public static InputFilter blockPeriod(){
return new InputFilter() {
#Override
public CharSequence filter(CharSequence charSequence, int i, int i1, Spanned spanned, int i2, int i3) {
for (int j = i; j <i1 ; j++) {
char c = charSequence.charAt(j);
if (!allowed(c)){
return "";
}
}
return null;
}
private boolean allowed(char c){
return c != '.';
}
};
}
You can do it with your InputFilter, but you need to allow it for a while when you calling setText() method.
private static class AllowableInputFilter implements InputFilter {
private boolean mAllowDot;
public void setAllowDot(boolean toAllow) {
mAllowDot = toAllow;
}
....
private boolean allowed(char c){
return mAllowDot || c != '.';
}
}
public void forceText(String text) {
mInputFilter.setAllowDot(true);
mEditText.setText(text);
mInputFilter.setAllowDot(false);
}
mInputFilter = new AllowableInputFilter();
mInputFilter.setAllowDot(false);
mEditText.setFilters(new InputFilter[] {mInputFilter});
forceText("Okaaayy....");
try this out :
if (textedit.getText().toString().startsWith(".")){
//Do something about it
}
now entering . you can show a message or something else.also put this code under onTextChanged method(text watcher).
Hope it helps you out.
user digits property inside xml in EditText
android:digits="0123456789"
so now edittext only allow digit from 0 to 9
I've recycler view with 0 items, I've added an option to manually add the items, my stracture is simple :
RV_Item.xml (contains EditText).
MyItem, which is an Object for RV ( contains private String Text; ).
MainActivity.java, where the stuff happen.
// My List<Object>
List<MyItem> Items = new ArrayList<>();
// For Adding, I've added FAB-Button, When Clicked, it does the following :
Items.add(new MyItem());
CheckForEmptyItems();
mAdapter.notifyItemInserted(0);
Now, When the user click the save button, i want to take all the edittext in all the items he had added, i'm doing in the following way :
for(MyItem items : Items){
Log.i("PrintingInfo", items.getText() );
}
The problem is, i'm not getting the text he entered in all EditText fields, and it's returning Null in all of them, What's the issue in this ?
So, i don't know why always i know the answer after posting, but here's how you gonna know what the user typed :
in your Adapter Class, in onBindViewHolder method, add textlistener for the EditText, here's an example :
holder.MyEditText.addTextChangedListener(new TextWatcher() {
#Override
public void beforeTextChanged(CharSequence s, int start, int count, int after) {
}
#Override
public void onTextChanged(CharSequence s, int start, int before, int count) {
}
#Override
public void afterTextChanged(Editable s) {
itemList.get(position).setText(s.toString());
}
});
Hope that helps you!
All- I have been struggling with this for a while and have looked at all the other questions about this sort of thing but I just can't figure it out: I have an edttext field that needs to be formatted for currency. I have tried the code in all of the other questions relating to this but no matter what I try it just doesn't work. Here is my code:
EditText text = (EditText)findViewById(R.id.edit_bill);
text.setRawInputType(Configuration.KEYBOARD_12KEY);
text.addTextChangedListener(new TextWatcher(){
EditText text = (EditText)findViewById(R.id.edit_bill);
DecimalFormat dec = new DecimalFormat("0.00");
public void afterTextChanged(Editable arg0) {
}
public void beforeTextChanged(CharSequence s, int start,
int count, int after) {
}
public void onTextChanged(CharSequence s, int start,
int before, int count) {
if(!s.toString().matches("^\\$(\\d{1,3}(\\,\\d{3})*|(\\d+))(\\.\\d{2})?$"))
{
String userInput= ""+s.toString().replaceAll("[^\\d]", "");
if (userInput.length() > 0) {
Float in=Float.parseFloat(userInput);
float percen = in/100;
text.setText("$"+dec.format(percen));
text.setSelection(text.getText().length());
}
}
}
});
This code is inside of an onClick method. Does that make a difference (eg. the text will only be formatted when the user clicks the button)?. Thanks in advance!
I have used this method in the past for formatting money.
static public String customFormat(String pattern, double s ) {
DecimalFormat myFormatter = new DecimalFormat(pattern);
String stringFormatOutput = myFormatter.format(s);
return stringFormatOutput;
}
it can be used like this:
String strPrice = customFormat("$###,##0.00", 300.2568);
mTxt.setText(strPrice);
just change the "300.2568" to be your price. This probably could work just as well for floats instead of doubles.