Attach TextWatcher Class to Multiple EditText without Inifinite Loop - android

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));}

Related

Applying auto-calculation to edit text

If the user enters any value like 1234 in the edit text box, then all the values of the edit text box are Addition in each other and the answer is shown in a text box.
For example if user enter values such as 1234 in edit text and after addition (1+2+3+4=10) their answer that is 10 show in text box.
You could probably use data binding, either one or two way depending on how you want to do things.
Make sure to read the documentation around data-binding before starting to successfully implement!
Using one-way data binding, you can set a value on an attribute and set a listener that reacts to a change in that attribute - android docs
First set your binding adapters on your new SumNumberTextView,
Then in your parent view's layout file, if you want to use the more verbose one way data binding, you'd reference something like:
android:numbers="#{viewmodel.numbers}"
and
android:onNumbersChanged="#{() => viewmodel.onNumbersChanged()}"
Then you can define a function on your view model that does your numeric operations whenever you input numbers.
You can just use textwatcher on edittext to get update when text change and do your operation of addition just like this
editText.addTextChangedListener(new TextWatcher() {
#Override
public void onTextChanged(CharSequence cs, int arg1, int arg2, int arg3) {
// call function from here if you want to perform operation as user provide input
add(cs.toString());
}
#Override
public void beforeTextChanged(CharSequence arg0, int arg1, int arg2, int arg3) {
Toast.makeText(getApplicationContext(),"before text change",Toast.LENGTH_LONG).show();
}
#Override
public void afterTextChanged(Editable arg0) {
// call function from here if you want to perform operation only user stop input
add(arg0.getText().toString());
}
});
private void add(String input){
if(input != null && input != ""){
String[] numbers = input.split("")
int total = 0;
for(int counter = 0 ; counter < numbers.length ; counter++){
total += Integer.parseInt(numbers[counter]);
}
Log.e("total",total);
}
}

android - EditText length filter not working as it should

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);

Two-way data-binding infinite loop

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("");
}
}
}

How to set text of a TextView using EditText? [duplicate]

This question already has an answer here:
automatically update my activity to show which was written
(1 answer)
Closed 8 years ago.
Using a text field, how would I set the text of a TextView while constantly updating the text? For example: The user begins to type information into the text field, this changes some text in in the activity they're in, however, the user does not need to manually update the text, instead the text automatically refreshes.
I have tried doing this myself and searched for other dilemmas, yet nothing appears to work. Additionally, I'm working with fragments that could possibly cause the problem. The code is below, partitioned into areas, before onCreateView(), in onCreateView() and after onCreateView().
Before onCreateView():
// Edit Text
EditText exerciseOneTitle;
// String value, contains value of exerciseOneTitle
String value;
// titleOne, what I want to the user to be able to change
TextView titleOne;
During onCreateView():
// Edit text
exerciseOneTitle = (EditText) rootView.findViewById(R.id.exercise_text);
// Get Edit Text value
value = exerciseOneTitle.getText().toString();
// Set title so there is always an initial value
titleOne.setText(R.string.exercise_one);
// Checks if edit text is focused
exerciseOneTitle.setOnFocusChangeListener(new OnFocusChangeListener() {
public void onFocusChange(View v, boolean hasFocus) {
// While edit text is focused, update titleOne via refresh()
while (hasFocus == true) {
refresh();
}
}
});
After onCreateView():
// Get the updated "value" and set it as titleOne text
// Additionaly, will be using it in other situations
public void refresh() {
value = exerciseOneTitle.getText().toString();
titleOne.setText(value);
}
Any thoughts or ideas? Thanks!
You can use TextWatcher.. See the link
According to the docs
onTextChanged() is called to notify you that, within s, the count
characters beginning at start have just replaced old text that had
length before. It is an error to attempt to make changes to s from
this callback.
Try this..
tv = (TextView)findViewById(R.id.charCounts);
textMessage = (EditText)findViewById(R.id.textMessage);
textMessage.addTextChangedListener(new TextWatcher(){
public void afterTextChanged(Editable s) {
tv.setText(textMessage.getText().toString());
}
public void beforeTextChanged(CharSequence s, int start, int count, int after){}
public void onTextChanged(CharSequence s, int start, int before, int count){}
});

Permanent hint in EditText

I have a situation where I would like the user to complete a sentence for me. For example, consider a EditText with a hint of "The last time I ". Normally, when a user clicks an EditText, the hint disappears, but I would like it to stay. Additionally, I would like the text to be permanent, so that it cannot be erased... leaving the user with only one option... complete the sentence.
The first part is fairly simple, just use the setText() method of EditText to place the hint. The difficult part is the latter. How can I have text in an EditText that the user cannot erase?
Well couldn't you do it in code? Some algorithim like, if the text is less than 16 characters (length of "The last time I ") then set the text to that. Therefore whenever they clicked it, if they tried to erase it, it would just go back to the default text.
Also, another idea..why don't you just make a TextView thats right edge aligns with the left edge of the EditText box, the user would never know that it was another box. This is acutally the best solution, if you don't want the text ever to be edited, just make it a TextView
Described problem can be solved using android.text.TextWatcher.
public class CompleteSentenceWathcher implements TextWatcher {
private final String initialText;
private int start;
private int after;
private int count;
public CompleteSentenceWathcher(String initialText) {
this.initialText = initialText;
}
#Override
public void beforeTextChanged(CharSequence s, int start, int count, int after) {
this.start = start;
this.count = count;
this.after = after;
}
#Override
public void onTextChanged(CharSequence s, int start, int before, int count) {
}
#Override
public void afterTextChanged(Editable s) {
if(start < initialText.length()) {
if(s.toString().startsWith(initialText)) {
return;
}
if(count >= 1 && after == 0) {
if(start+count+1 <= initialText.length()) {
s.replace(start, start+count, initialText.substring(start, start+count+1));
} else {
s.replace(start, start, initialText.substring(start, start+1));
}
} else if(count == 0 && after >= 1) {
s.delete(start, start+after);
}
}
}
}
Create an instance of EditText and add the TextWatcher.
EditText editText = new EditText(this);
editText.setText("I love");
editText.addTextChangedListener(new CompleteSentenceWathcher(editText.getText().toString()));
I've implemented this with an InputFilter, where _PERMANENT_HINT_TEXT is the text at the end of the EditText that I don't want the user to be able to modify. I recommend adding a color span to it, so that it is grayed out to hopefully look like a hint/disabled section of text. This should hopefully improve the UX as they should automatically assume it is unmodifiable, and not just wonder why some part of the EditText (that they usually can completely change) isn't "working". This approach allowed the text to be set after
the InputFilter was set on the EditText, which was a requirement for me since I used this on an EditTextPreference.
To be clear, I needed the permanent text to exist at the end of the EditText, instead of the beginning, but that should be symmetrical to my implementation.
new InputFilter() {
#Override
public CharSequence filter(CharSequence source, int source_start, int source_end,
Spanned destination, int destination_start, int destination_end) {
final int protected_text_start = (TextUtils.isEmpty(destination)? source.length() : destination.length()) - _PERMANENT_HINT_TEXT.length();
// Allows input into unprotected region
if (source_start + destination_start - source_end < protected_text_start)
return null;
// Prevents deletion of protected region
else if (TextUtils.isEmpty(source))
return destination.subSequence(destination_start, destination_end);
// Ignores insertion into protected region
else
return "";
}
}
use EditText.setFilters(new InputFilters[] { /* InputFilter goes here */ }; to add it to the desired EditText.
Just checking for the length wouldn't be adequate... I could type "This is a really long text I put into the box" and it would accept it even though it doesn't begin with "The last time I" string.
Personally, I would probably go for the prevention method suggested of using a TextView over that of a check on the way out. But if you're going to validate it afterwards, you'd actually need to check the beginning of the returned string.

Categories

Resources