I have a list of items. In each item's row I have 2 EditTexts side-by-side. EditText-2 depends on EditText-1's value. This list is bound with data-binding values in HashMap<String, ItemValues>
For Example:
Total _____1000____
Item A __1__ __200__
Item B __1__ __200__
Item C __1__ __200__
Item D __2__ __400__
First EditText is the share and the second value is its value calculated based on total and share. So, in example if I change any 1 share, all the values will be changed. So, shown in example total no of shares are = 1+1+1+2 = 5. So amount per share = 1000/5 = 200 and is calculated and shown in next EditText.
I have bound this values with two-way data binding like this:
As, this is a double value, I have added 2 binding adapters for this like this:
#BindingAdapter("android:text")
public static void setShareValue(EditText editText, double share) {
if (share != 0) {
editText.setText(String.valueOf(share));
} else {
editText.setText("");
}
}
#InverseBindingAdapter(attribute = "android:text")
public static double getShareValue(EditText editText) {
String value = editText.getText().toString();
if (!value.isEmpty()) {
return Double.valueOf(value);
} else
return 0;
}
Now, to calculate new values, I need to re-calculate whole thing after any share value is changed. So, I added android:onTextChagned method to update Calculations. But it gets me an infinite loop.
<EditText
android:text="#={items[id].share}"
android:onTextChanged="handler.needToUpdateCalculations"
.... />
public void needToUpdateCalculations(CharSequence charSequence, int i, int i1, int i2) {
updateCalculations();
}
This gets an infinete loop because when data changes, it is rebound to the EditText, and each EditText has an onTextChanged attached it will fire again and it will get really large - infinite loop.
It also updates the value of itself, ended up loosing the cursor as well.
I have also tried several other methods like adding TextWatcher when on focus and removing when losses focus. But at least it will update it self and will loose the cursor or infinite loop.
Unable to figure this problem out. Thank you for looking into this problem.
EDIT:
I have tried with the below method. But, it doesn't allow me to enter . (period).
#BindingAdapter("android:text")
public static void setDoubleValue(EditText editText, double value) {
DecimalFormat decimalFormat = new DecimalFormat("0.##");
String newValue = decimalFormat.format(value);
String currentText = editText.getText().toString();
if (!currentText.equals(newValue)) {
editText.setText("");
editText.append(newValue);
}
}
The reason you stated is correct and it will make a infinite loop definitely. And there is a way to get out from the infinite loop of this problem, android official provided a way to do so (But it is not quite obvious.)(https://developer.android.com/topic/libraries/data-binding/index.html#custom_setters)
Binding adapter methods may optionally take the old values in their
handlers. A method taking old and new values should have all old
values for the attributes come first, followed by the new values:
#BindingAdapter("android:paddingLeft")
public static void setPaddingLeft(View view, int oldPadding, int newPadding) {
if (oldPadding != newPadding) {
view.setPadding(newPadding,
view.getPaddingTop(),
view.getPaddingRight(),
view.getPaddingBottom());
}
}
You can use the old value and new value comparison to make the setText function called conditionally.
#BindingAdapter("android:text")
public static void setShareValue(EditText editText, double oldShare,double newShare) {
if(oldShare != newShare)
{
if (newShare!= 0) {
editText.setText(String.valueOf(newShare));
} else {
editText.setText("");
}
}
}
Related
First I have to say I have read similar questions and answers here on SO and this question is basically a duplicate of this question and many others but the answers given to those questions doesn't work like the way i want it.
The problem:
Setting length filter on my EditText programmatically like this:
editText.setFilters(new InputFilter[]{new LengthFilter(10)} );
The only thing it does is hide the text that go over the limit in the EditText. It still shows the long (unlimited) text in suggestion box and i have to delete (backspace) for each letter that go over before being able to delete what is shown in the EditText.
Suggested Solutions:
Setting InputType to textFilter.
Programmatically I did this:
editText.setInputType( InputType.TYPE_TEXT_VARIATION_FILTER );
It hides suggestions but the unlimited text is still present and i still have to use backspace to delete letters that shouldn't be present.
Setting InputType to textNoSuggestions|textVisiblePassword.
Programmatically I did this (had to add TYPE_CLASS_TEXT too otherwise it wouldn't work):
editText.setInputType( InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS | InputType.TYPE_TEXT_VARIATION_VISIBLE_PASSWORD );
This one does work but the problem is it stops "gesture typing" and it changes the font to monospace.
Better Solutions?
As you can see these two methods don't actually work without additional problems. Is there any other way of doing this that I missed. Should I just use a TextWatcher if I want to keep gesture typing and suggestions?
I ended up using a TextWatcher instead. I'm not sure if it is the best way to do this but it does work with suggestions and it doesn't turn off gesture typing or change the font style. Here's how I did it (I'm quite new to android so if this needs improvement feel free to let me know).
I added an example in the comments to clarify what is going on.
Make these global variables:
private boolean mWatcherIsBlocked = false;
private String mBeforeChange;
private String mFilteredString;
private int mCursorPosition = 0;
Then create the TextWatcher and add it to your EditText
final int maxLength = 10; // desired length limit
/**
* lets say our EditText is showing "abcdefgh". We select "cdef" from it and
* paste a new text "ijklmnop" in the middle. What we should get according to
* our maxLength is this:
* (1) "ab" (0th up to the letter from before_change_text we were selecting) +
* (2) "ijklmn" (part of the text we pasted minus the number of letters the whole
* after_change_text goes over the 10 letter limit) +
* (3) "gh" (last part of before_change_text that wasn't selected)
*
* so the new text has to be "abijkmngh"
*/
TextWatcher textWatcher = new TextWatcher() {
#Override
public void beforeTextChanged(CharSequence s, int start, int count, int after) {
// get before_change_text if textWatcher isn't blocked
if (!mWatcherIsBlocked) mBeforeChange = s.toString();
}
#Override
public void onTextChanged(CharSequence s, int start, int before, int count) {
if (!mWatcherIsBlocked){
// get after_change_text if textWatcher isn't blocked
String after = s.toString();
// if after_change_text's length is bigger than the limit
if (after.length() > maxLength) {
// see how much it goes over the limit
int over = after.length() - maxLength;
// add parts (1) and (2) like our example above
String st = mBeforeChange.substring(0, start) + // (1)
after.substring(start, start + count - over); // (2)
// get where the cursor position should be after pasting (
// = after the last letter we could paste = length of (1) + (2) )
mCursorPosition = st.length();
// now add part (3) of our text to the first two
st += mBeforeChange.substring(
mBeforeChange.length() - (maxLength - st.length()),
mBeforeChange.length());
// now assign this new text to a global variable
mFilteredString = st;
} else {
// if after_change_text hasn't gone over the limit assign it
// directly to our global variable
mFilteredString = s.toString();
}
}
}
#Override
public void afterTextChanged(Editable s) {
// if filtered text is not the same as unfiltered text
// or textWatcher is not blocked
if (!mFilteredString.equals(s.toString()) && !mWatcherIsBlocked) {
// block textWatcher to avoid infinite loops created by setText
// (this might not work as I well as I think!)
mWatcherIsBlocked = true;
// set new text to our EditText
editText.setText(mFilteredString);
// set its cursor position
editText.setSelection(mCursorPosition);
// unblock the textWatcher
mWatcherIsBlocked = false;
}
}
};
// add the TextWatcher to our EditText
editText.addTextChangedListener(textWatcher);
I would need a way to detect if the EditText has been changed by the user typing something or by the app changing the text programmatically. Any standard way of doing this? I guess I could always do something hackish like unsetting the TextWatcher before setText() and setting it back again afterwards, but there's got to be a better way of doing this... right?
I tried checking if the EditText is focused in the TextWatcher, but that was of little help since the EditTexts gets focused "semi-randomly" anyway when scrolling...
Background
I have a ListView with EditTexts in every listitem. I've sorted out the basic problem of storing the values for the EditTexts for reuse when the user scrolls.
I also have a TextWatcher that sums up the values in all EditTexts and displays the sum when the user edits the content of any of the EditTexts.
The problem is that when I'm scrolling the list and my custom adapter is reentering the stored values in the EditTexts on bindView(), that also triggers the TextWatchers afterTextChanged() method, causing the scrolling to lag because the summing-up-function is triggered.
This sorted itself out a long time ago, but for anyone who finds their way here looking for an answer, here's what I did:
I ended up setting the Tag of the EditText to some arbitrary value right before I'm about to change it programmatically, and changing the value, and then resetting the Tag to null. Then in my TextWatcher.afterTextChanged() method I check if the Tag is null or not to determine if it was the user or the program that changed the value. Works like a charm!
Something like this:
edit.setTag( "arbitrary value" );
edit.setText( "My Text Value" );
edit.setTag(null);
and then
public void afterTextChanged(Editable s) {
if( view.getTag() == null )
// Value changed by user
else
// Value changed by program
}
The accepted answer is perfectly valid, but I have another approach;
#Override
public void onTextChanged(CharSequence charSequence,
int start, int before, int count) {
boolean userChange = Math.abs(count - before) == 1;
if (userChange) {
}
}
It works by checking if the change was a single character.
This is not a fool-proof solution as copy-paste operations might be missed, and non-user changes of a single character will also be missed.
Depending on your use case, this might be a viable solution.
One thing that helped to me is having boolean canListenInput field. Use it inside of watcher.
email.addTextChangedListener(new TextWatcher() {
#Override
public void afterTextChanged(Editable s) {
if (canListenInput) {
emailChanged = true;
}
}
});
Clear it before changing text programmatically. Set it inside of onAttachedToWindow, (after state) restoration:
#Override
public void onAttachedToWindow() {
super.onAttachedToWindow();
canListenInput = true;
}
Depending on your use case (e.g. you are auto-populating this field when the user types into another field), you can also check if the view has focus, e.g.:
textView.doAfterTextChanged {
val isValueChangedByUser = textView.hasFocus()
// ...
}
I have created some extension methods to tackle this scenario
inline fun TextView.runTaggingCode(block: () -> Unit) {
this.setTag(R.string.tag_text_id, "set_from_code")
block()
this.setTag(R.string.tag_text_id, null)
}
fun TextView.isTaggedForCode() = this.getTag(R.string.tag_text_id) != null
where I have defined the R.string.tag_text_id as below
<string name="tag_text_id" translatable="false">dummy</string>
Now where I to use these methods, I will simply change my code as below,
override fun beforeTextChanged(
s: CharSequence, start: Int, count: Int,
after: Int,
) {
if (textView.isTaggedForCode()) {
return
}
textView.runTaggingCode {
// your logic here
}
}
But in case you don't want to change the same text view text, in it own TextWatcher you can also see the answer
You can do this by adding:
private String current = "";
#Override
public void onTextChanged(CharSequence s, int start, int before, int count) {
if(!s.toString().equals(current)){
[your_edittext].removeTextChangedListener(this);
//Format your string here...
current = formatted;
[your_edittext].setText(formatted);
[your_edittext].setSelection(formatted.length());
[your_edittext].addTextChangedListener(this);
}
I have provided an AutoCompleteTextView to show suggestions to the user. Based on the item selected by the user, I am getting the ID of the item and using it in data base side. Now my problem is to force the user to make selection only from AutoCompleteTextView (i.e. user should not enter his own text). It is a client requirement. How to do this?
Here's a pretty straightforward solution:
You can create a variable to store the selected value by setting setOnItemClickListener in your AutoCompleteTextView. Then you can null that value whenever a user types in the field by adding a TextWatcher to it. Finally, you can validate your variable is not null before continuing.
String my_var; //keep track!
AutoCompleteTextView tv = (AutoCompleteTextView) layout.findViewById(R.id.tv);
tv.setAdapter(my_adapter);
tv.setOnItemClickListener(new AdapterView.OnItemClickListener() {
#Override
public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
my_var = my_adapter.getItem(position).toString();
}
});
/**
* Unset the var whenever the user types. Validation will
* then fail. This is how we enforce selecting from the list.
*/
tv.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) {
my_var = null;
}
#Override
public void afterTextChanged(Editable s) {}
});
I happened to need such a requirement for a project I am working on, and I though I'll share you guys the way I implemented the required.
I added a on focus change listener for the auto-complete text view and checked when the user has focus changed focus from the auto-complete, and handled the situation straight forward.
autoTextViewCountry.setOnFocusChangeListener(new View.OnFocusChangeListener() {
#Override
public void onFocusChange(View view, boolean b) {
if(!b) {
// on focus off
String str = autoTextViewCountry.getText().toString();
ListAdapter listAdapter = autoTextViewCountry.getAdapter();
for(int i = 0; i < listAdapter.getCount(); i++) {
String temp = listAdapter.getItem(i).toString();
if(str.compareTo(temp) == 0) {
return;
}
}
autoTextViewCountry.setText("");
}
}
});
So my implementation is: if the typed text doesn't exist in the array adapter then on focus changed empty the text view, and later on when continuing to next stage of say registration, check if this text view is empty or not.
Hope this approach helps somebody.
Happy coding.
NiceAutoCompleteTextView will give you the ability to check whether a selection was made from the drop-down popup, by invoking isSelectionFromPopup()
Just add this property to your AutoCompleteTextView.
android:focusable="false"
My Code looks like:
<com.google.android.material.textfield.TextInputLayout
android:id="#+id/menu"
style="#style/Widget.MaterialComponents.TextInputLayout.OutlinedBox.ExposedDropdownMenu"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="Mode">
<AutoCompleteTextView
android:id="#+id/mode"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:focusable="false"
/>
</com.google.android.material.textfield.TextInputLayout>
On Java Side:
AutoCompleteTextView mode = findViewById(R.id.mode);
final List<String> modeList = new ArrayList();
modeList.add("YEARLY");
modeList.add("HALF-YEARLY");
modeList.add("QUARTER-YEARLY");
modeList.add("MONTHLY");
mode.setAdapter(new ArrayAdapter(getApplicationContext(),R.layout.list_item,modeList));
To get the Text of AutoCompleteTextView:
mode.getText().toString()
Ok I assume you would like to limit the input of the user to the texts contained in the list of items listed in the suggest box.
For instance if you have:
One
Two
Three
then the user could only type for the first character "O" and "T".
And so on according to the text entered before.
To achieve this you can utilize the setFilters method of TextView:
editBox = (TextView) findViewById(R.id.editBox);
editBox.setFilters(getFilters());
editBox.addTextChangedListener(this);
editBox.setOnFocusChangeListener(this);
Additionally you would probably need the text change listener and focus listener to react and update the filtered list when a new character is entered ... plus to update the filter.
Here is an example of decimal number filter I have used on a project:
protected InputFilter[] getFilters()
{
InputFilter[] filters = new InputFilter[1];
filters[0] = new InputFilter()
{
public CharSequence filter(CharSequence source, int start, int end, Spanned dest, int dstart, int dend)
{
// limit input to digits and decimal / thousand separator only
// in case thousand separator is pressed, change it to decimal
// separator instead
String output = "";
if (number.isFocused())
{
for (int i = start; i < end; i++)
{
char c = source.charAt(i);
if (isDecimalOrThousandSeparator(c))
{
output = output + Character.toString(decimalSeparator);
}
else if (Character.isDigit(c))
{
output = output + Character.toString(c);
}
}
return output == "" ? null : output;
}
return null;
}
};
return filters;
}
A simple solution would be to just check if the current input is one of the items in the adapter. You can do it like this:
val AutoCompleteTextView.isValid: Boolean
get() {
for (i in 0 until adapter.count) {
if (adapter.getItem(i) == text.toString()) {
return true
}
}
return false
}
here is the another solution for this proAutoCompleteTextView.Validator to ensure the valid values. Validator is invoked when edit text looses focus.
autoCompleteTextView.validator = object : AutoCompleteTextView.Validator {
override fun isValid(text: CharSequence?): Boolean {
return optionsList.contains(text.toString())
}
override fun fixText(invalidText: CharSequence?): CharSequence {
return ""
}
}
Where optionsList is list of valid values.
I was having the same requirement, so here is my implementation:
autoCompleteTextView.setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View v) {
autoCompleteTextView.showDropDown();
}
});
In the xml set focusable=false and focusableInTouchMode=false.
Happy coding
In order to have a non editable variation of the AutoCompleteTextView, you should disable user input in the AutoCompleteTextView. That can be achieved by setting android:inputType="none" on the AutoCompleteTextView.
I am trying to calculate the salary the person receives, based on any of the following inputs - hourly, daily, weekly, monthly, yearly. When one of them is entered, the others should be recalculated automatically.
Here's how I proceed:
First, I have 5 Double type variables defined at the top of the activity. They are: hourly, daily, weekly, monthly, yearly. Then I have 5 EditText fields, corresponding to these variables. I have attached a custom subclass that implements TextWatcher to these 5 EditTexts.
For example:
etHourly = (EditText) findViewById(R.id.etHourly);
etHourly.addTextChangedListener(new EditTextWatcher(etHourly));
This custom class has a constructor that accepts and stores the view that was passed to it, since the default methods of the TextWatcher class don't provide a way to find out which View invoked the change.
After saving the passed view as a local variable inside the custom subclass I then proceed with the implemented afterTextChanged inside this subclass and get the value of the passed EditText and save it as a Double to its corresponding defined variable at the top of the activity. (e.g. if the EditText passed is for the Weekly salary, I set the value of this EditText as a double to the weekly variable.
Finally, just before the end of the afterTextChanged method I call another custom method Recalculate(), that has a bunch of if()'s to check if hourly, daily, weekly, monthly or yearly is set and if it is, calculate and use setText() on the remaining EditText's. The problem is that this setText() will invoke the TextWatchers for each of these EditTexts, causing an infinite loop.
How do I overcome this?
Here's some code to have a better understanding of this. Before onCreate:
Double hourly, daily, weekly, monthly, yearly = 0.0;
EditText etHourly, etDaily, etWeekly, etMonthly, etYearly;
Inside onCreate():
etHourly = (EditText) findViewById(R.id.etHourly);
etDaily = (EditText) findViewById(R.id.etDaily);
etWeekly = (EditText) findViewById(R.id.etWeekly);
etMonthly = (EditText) findViewById(R.id.etMonthly);
etYearly = (EditText) findViewById(R.id.etYearly);
etHourly.addTextChangedListener(new EditTextWatcher(etHourly));
etDaily.addTextChangedListener(new EditTextWatcher(etDaily));
etWeekly.addTextChangedListener(new EditTextWatcher(etWeekly));
etMonthly.addTextChangedListener(new EditTextWatcher(etMonthly));
etYearly.addTextChangedListener(new EditTextWatcher(etYearly));
The EditTextWatcher subclass:
private class EditTextWatcher implements TextWatcher {
EditText v;
public EditTextWatcher(EditText view) {
this.v = view;
}
public void afterTextChanged(Editable s) {
Reinit();
// Only if the currently edited text field contains something
if (v.getText().toString().length() > 0) {
switch (v.getId()) {
case R.id.etHourly:
hourly = getTvAsDouble(etHourly);
break;
case R.id.etDaily:
daily = getTvAsDouble(etDaily);
break;
case R.id.etWeekly:
weekly = getTvAsDouble(etWeekly);
break;
case R.id.etMonthly:
monthly = getTvAsDouble(etMonthly);
break;
case R.id.etYearly:
yearly = getTvAsDouble(etYearly);
break;
default:
}
}
Recalculate();
}
public void beforeTextChanged(CharSequence s, int start, int count,
int after) {
}
public void onTextChanged(CharSequence s, int start, int before,
int count) {
}
}
Reinit():
hourly = daily = weekly = monthly = yearly = 0.0;
Recalculate():
if(hourly!=null && hourly>0.0){
etDaily.setText(String.valueOf(hourly*8));
}
// I will complete the other if's once this works
Mate !! I too was stuck here .... I was getting the StackOverflowError ...but I was assuming that maybe it is because I have instantiated TextWatcher multiple times ... but you helped me by identifying the problem, which was actually with setText() ,which was calling TextWatcher's methods infinitely .... so here I did it --- as per your code --->
if(hourly!=null && hourly>0.0){
if(etHourly.isFocused()) //this condition helped suppressing infinite loops
etDaily.setText(String.valueOf(hourly*8));}
I have an application where I need to display a list of numbers, but the numbers need to be formatted based on their value. Negative numbers are shown in normal text, positive numbers shown as bold. Also, the number needs to always appear positive in the text view. I tried extending TextView with setText overriden as such:
#Override
public void setText(CharSequence text, TextView.BufferType type) {
double number = Double.parseDouble(text.toString());
if (number > 0) {
this.setTypeface(this.getTypeface(), BOLD);
} else {
this.setTypeface(this.getTypeface(), NORMAL);
number = Math.abs(number);
}
super.setText(number + "", type);
}
This didn't quite work, as the setText was being called multiple times on the same MyTextView. This resulted in every number appearing bold, as it was positive the next time through.
I would like to keep this logic in a widget, as opposed to where the text is being set, as this is a very common occurrence in my application.
Is there a way that I can do this in a widget?
Just add a member variable to your class to check whether it was already modified or to keep the original value.
private double originalValue = 0;
#Override
public void setText(CharSequence text, TextView.BufferType type) {
if(originalValue==0) {
originalValue = Double.parseDouble(text.toString());
}
this.setTypeface(this.getTypeface(), originalValue>0 ? BOLD : NORMAL);
super.setText(Math.abs(originalValue), type);
}
Ok, I ended up just making an adapter for each list that used this special case, and took care of it in the activity for any other instance of it. Something like this:
#Override
public void bindView(View view, Context context, Cursor cursor) {
TextView text = (TextView) view.findViewById(R.id.special_text);
double amount = cursor.getDouble(cursor.getColumnIndex(DbAdapter.KEY_NUMBER));
if (amount > 0) {
amountText.setTypeface(null, Typeface.BOLD);
} else {
amountText.setTypeface(null, Typeface.NORMAL);
amount = Math.abs(amount);
}
text.setText(amount);
}