In the login panel of my app, I divided the country calling code and the remaining numbers in two editable TextView as below:
I want to use international formatting standard in the TextView on the right. If a user who has a phone number as +905444444444 types in number in these boxes, I want to see "90" in the box on the left and "544 444 4444" on the right.
For this reason, I tried to use the following implementation that uses libphonenumber:
/**
* Watches a {@link android.widget.TextView} and if a phone number is entered
* will format it.
* <p>
* Stop formatting when the user
* <ul>
* <li>Inputs non-dialable characters</li>
* <li>Removes the separator in the middle of string.</li>
* </ul>
* <p>
* The formatting will be restarted once the text is cleared.
*/
public class PhoneNumberFormattingTextWatcher implements TextWatcher {
/**
* Indicates the change was caused by ourselves.
*/
private boolean mSelfChange = false;
/**
* Indicates the formatting has been stopped.
*/
private boolean mStopFormatting;
private AsYouTypeFormatter mFormatter;
private String code;
/**
* The formatting is based on the current system locale and future locale changes
* may not take effect on this instance.
*/
public PhoneNumberFormattingTextWatcher() {
this(Locale.getDefault().getCountry());
}
/**
* The formatting is based on the given <code>countryCode</code>.
*
* @param countryCode the ISO 3166-1 two-letter country code that indicates the country/region
* where the phone number is being entered.
*/
public PhoneNumberFormattingTextWatcher(String countryCode) {
if (countryCode == null) throw new IllegalArgumentException();
mFormatter = PhoneNumberUtil.getInstance().getAsYouTypeFormatter(countryCode);
}
@Override
public void beforeTextChanged(CharSequence s, int start, int count,
int after) {
if (mSelfChange || mStopFormatting) {
return;
}
// If the user manually deleted any non-dialable characters, stop formatting
if (count > 0 && hasSeparator(s, start, count)) {
stopFormatting();
}
}
@Override
public void onTextChanged(CharSequence s, int start, int before, int count) {
if (mSelfChange || mStopFormatting) {
return;
}
// If the user inserted any non-dialable characters, stop formatting
if (count > 0 && hasSeparator(s, start, count)) {
stopFormatting();
}
}
@Override
public synchronized void afterTextChanged(Editable s) {
if (mStopFormatting) {
// Restart the formatting when all texts were clear.
mStopFormatting = !(s.length() == 0);
return;
}
if (mSelfChange) {
// Ignore the change caused by s.replace().
return;
}
String formatted = reformat(s, Selection.getSelectionEnd(s));
if (formatted != null) {
int rememberedPos = mFormatter.getRememberedPosition();
mSelfChange = true;
s.replace(0, s.length(), formatted, 0, formatted.length());
// The text could be changed by other TextWatcher after we changed it. If we found the
// text is not the one we were expecting, just give up calling setSelection().
if (formatted.equals(s.toString())) {
Selection.setSelection(s, rememberedPos);
}
mSelfChange = false;
}
// PhoneNumberUtils.ttsSpanAsPhoneNumber(s, 0, s.length());
}
/**
* Generate the formatted number by ignoring all non-dialable chars and stick the cursor to the
* nearest dialable char to the left. For instance, if the number is (650) 123-45678 and '4' is
* removed then the cursor should be behind '3' instead of '-'.
*/
private String reformat(CharSequence s, int cursor) {
// The index of char to the leftward of the cursor.
int curIndex = cursor - 1;
String formatted = null;
mFormatter.clear();
char lastNonSeparator = 0;
boolean hasCursor = false;
int len = s.length();
for (int i = 0; i < len; i++) {
char c = s.charAt(i);
if (PhoneNumberUtils.isNonSeparator(c)) {
if (lastNonSeparator != 0) {
formatted = getFormattedNumber(lastNonSeparator, hasCursor);
hasCursor = false;
}
lastNonSeparator = c;
}
if (i == curIndex) {
hasCursor = true;
}
}
if (lastNonSeparator != 0) {
formatted = getFormattedNumber(lastNonSeparator, hasCursor);
}
return formatted;
}
private String getFormattedNumber(char lastNonSeparator, boolean hasCursor) {
return hasCursor ? mFormatter.inputDigitAndRememberPosition(lastNonSeparator)
: mFormatter.inputDigit(lastNonSeparator);
}
private void stopFormatting() {
mStopFormatting = true;
mFormatter.clear();
}
private boolean hasSeparator(final CharSequence s, final int start, final int count) {
for (int i = start; i < start + count; i++) {
char c = s.charAt(i);
if (!PhoneNumberUtils.isNonSeparator(c)) {
return true;
}
}
return false;
}
}
However, this TextWatcher formats the numbers includes the calling code. In other words, it successfully formats "+905444444444" but cannot format "54444444444". How can I achieve to get the same result when the input phone number includes the country code in the TextView on the right? Needless to say but I want to get the following output:
- 5
- 54
- 544
- 544 4
- 544 44
- 544 444
- 544 444 4
- 544 444 44 ...
I edited reformat(charSequence, cursor)
method and achieved to get the internationally formatted phone numbers without country calling code at last. If you want to get the same result, you can see the edited code below:
/**
* Watches a {@link android.widget.TextView} and if a phone number is entered
* will format it.
* <p>
* Stop formatting when the user
* <ul>
* <li>Inputs non-dialable characters</li>
* <li>Removes the separator in the middle of string.</li>
* </ul>
* <p>
* The formatting will be restarted once the text is cleared.
*/
public class PhoneNumberFormattingTextWatcher implements TextWatcher {
/**
* Indicates the change was caused by ourselves.
*/
private boolean mSelfChange = false;
/**
* Indicates the formatting has been stopped.
*/
private boolean mStopFormatting;
private AsYouTypeFormatter mFormatter;
private String countryCode;
/**
* The formatting is based on the current system locale and future locale changes
* may not take effect on this instance.
*/
public PhoneNumberFormattingTextWatcher() {
this(Locale.getDefault().getCountry());
}
/**
* The formatting is based on the given <code>countryCode</code>.
*
* @param countryCode the ISO 3166-1 two-letter country code that indicates the country/region
* where the phone number is being entered.
*
* @hide
*/
public PhoneNumberFormattingTextWatcher(String countryCode) {
if (countryCode == null) throw new IllegalArgumentException();
mFormatter = PhoneNumberUtil.getInstance().getAsYouTypeFormatter(countryCode);
this.countryCode = countryCode;
}
@Override
public void beforeTextChanged(CharSequence s, int start, int count,
int after) {
if (mSelfChange || mStopFormatting) {
return;
}
// If the user manually deleted any non-dialable characters, stop formatting
if (count > 0 && hasSeparator(s, start, count)) {
stopFormatting();
}
}
@Override
public void onTextChanged(CharSequence s, int start, int before, int count) {
if (mSelfChange || mStopFormatting) {
return;
}
// If the user inserted any non-dialable characters, stop formatting
if (count > 0 && hasSeparator(s, start, count)) {
stopFormatting();
}
}
@Override
public synchronized void afterTextChanged(Editable s) {
if (mStopFormatting) {
// Restart the formatting when all texts were clear.
mStopFormatting = !(s.length() == 0);
return;
}
if (mSelfChange) {
// Ignore the change caused by s.replace().
return;
}
String formatted = reformat(s, Selection.getSelectionEnd(s));
if (formatted != null) {
int rememberedPos = formatted.length();
Log.v("rememberedPos", "" + rememberedPos);
mSelfChange = true;
s.replace(0, s.length(), formatted, 0, formatted.length());
// The text could be changed by other TextWatcher after we changed it. If we found the
// text is not the one we were expecting, just give up calling setSelection().
if (formatted.equals(s.toString())) {
Selection.setSelection(s, rememberedPos);
}
mSelfChange = false;
}
}
/**
* Generate the formatted number by ignoring all non-dialable chars and stick the cursor to the
* nearest dialable char to the left. For instance, if the number is (650) 123-45678 and '4' is
* removed then the cursor should be behind '3' instead of '-'.
*/
private String reformat(CharSequence s, int cursor) {
// The index of char to the leftward of the cursor.
int curIndex = cursor - 1;
String formatted = null;
mFormatter.clear();
char lastNonSeparator = 0;
boolean hasCursor = false;
String countryCallingCode = "+" + CountryCodesAdapter.getCode(countryCode);
s = countryCallingCode + s;
int len = s.length();
for (int i = 0; i < len; i++) {
char c = s.charAt(i);
if (PhoneNumberUtils.isNonSeparator(c)) {
if (lastNonSeparator != 0) {
formatted = getFormattedNumber(lastNonSeparator, hasCursor);
hasCursor = false;
}
lastNonSeparator = c;
}
if (i == curIndex) {
hasCursor = true;
}
}
if (lastNonSeparator != 0) {
Log.v("lastNonSeparator", "" + lastNonSeparator);
formatted = getFormattedNumber(lastNonSeparator, hasCursor);
}
if (formatted.length() > countryCallingCode.length()) {
if (formatted.charAt(countryCallingCode.length()) == ' ')
return formatted.substring(countryCallingCode.length() + 1);
return formatted.substring(countryCallingCode.length());
}
return formatted.substring(formatted.length());
}
private String getFormattedNumber(char lastNonSeparator, boolean hasCursor) {
return hasCursor ? mFormatter.inputDigitAndRememberPosition(lastNonSeparator)
: mFormatter.inputDigit(lastNonSeparator);
}
private void stopFormatting() {
mStopFormatting = true;
mFormatter.clear();
}
private boolean hasSeparator(final CharSequence s, final int start, final int count) {
for (int i = start; i < start + count; i++) {
char c = s.charAt(i);
if (!PhoneNumberUtils.isNonSeparator(c)) {
return true;
}
}
return false;
}
}
Works OK but... Cursor is not set on proper position. When user change cursor inside edit text and enter number, cursor goes to the end. I've added class holding formatted number and position and return it from reformat method.
return new InputFormatted(TextUtils.isEmpty(formatted) ? "" : formatted,
mFormatter.getRememberedPosition());
After that only set
Selection.setSelection(s, formatted.getPosition());
Thank you @Dorukhan Arslan and @NixSam for the answers. The accepted answer is working well but the problem occurs when user changes the digit somewhere in middle. The other answer helps there, but for some edge case, it was not behaving as I wanted. So I thought to solve it in a different way. This solution uses "digitsBeforeCursor" to maintain the correct cursor position every time [hopefully:-)].
For all those who are facing the problem, there are two options for you to solve this.
1. Easy and Ready to GO option
If you are planning to take international phone input, you can use CCP Library which can give you total power for the full international number with ease and flexibility. It will allow you to do something like this. It will handle formatting along with the country selector (bonus).
2. Custom option
If you want to implement things from the scratch here you go.
- Add Optimized Android port of libphonenumber by Michael Rozumyanskiy to your project by adding following in your gradle file.
dependencies {
compile 'io.michaelrocks:libphonenumber-android:8.9.0'
}
- Create a new class named
InternationalPhoneTextWatcher
Add following code to that class. CCP uses this class here. Then use object of this class to the editText. This will take country name code and phone code in constructor. and will update formatting automatically when updateCountry() is called to change the country.
public class InternationalPhoneTextWatcher implements TextWatcher {
// Reference https://stackoverflow.com/questions/32661363/using-phonenumberformattingtextwatcher-without-typing-country-calling-code to solve formatting issue
// Check parent project of this class at https://github.com/hbb20/CountryCodePickerProject
private static final String TAG = "Int'l Phone TextWatcher";
PhoneNumberUtil phoneNumberUtil;
/**
* Indicates the change was caused by ourselves.
*/
private boolean mSelfChange = false;
/**
* Indicates the formatting has been stopped.
*/
private boolean mStopFormatting;
private AsYouTypeFormatter mFormatter;
private String countryNameCode;
Editable lastFormatted = null;
private int countryPhoneCode;
//when country is changed, we update the number.
//at this point this will avoid "stopFormatting"
private boolean needUpdateForCountryChange = false;
/**
* @param context
* @param countryNameCode ISO 3166-1 two-letter country code that indicates the country/region
* where the phone number is being entered.
* @param countryPhoneCode Phone code of country. https://countrycode.org/
*/
public InternationalPhoneTextWatcher(Context context, String countryNameCode, int countryPhoneCode) {
if (countryNameCode == null || countryNameCode.length() == 0)
throw new IllegalArgumentException();
phoneNumberUtil = PhoneNumberUtil.createInstance(context);
updateCountry(countryNameCode, countryPhoneCode);
}
public void updateCountry(String countryNameCode, int countryPhoneCode) {
this.countryNameCode = countryNameCode;
this.countryPhoneCode = countryPhoneCode;
mFormatter = phoneNumberUtil.getAsYouTypeFormatter(countryNameCode);
mFormatter.clear();
if (lastFormatted != null) {
needUpdateForCountryChange = true;
String onlyDigits = phoneNumberUtil.normalizeDigitsOnly(lastFormatted);
lastFormatted.replace(0, lastFormatted.length(), onlyDigits, 0, onlyDigits.length());
needUpdateForCountryChange = false;
}
}
@Override
public void beforeTextChanged(CharSequence s, int start, int count,
int after) {
if (mSelfChange || mStopFormatting) {
return;
}
// If the user manually deleted any non-dialable characters, stop formatting
if (count > 0 && hasSeparator(s, start, count) && !needUpdateForCountryChange) {
stopFormatting();
}
}
@Override
public void onTextChanged(CharSequence s, int start, int before, int count) {
if (mSelfChange || mStopFormatting) {
return;
}
// If the user inserted any non-dialable characters, stop formatting
if (count > 0 && hasSeparator(s, start, count)) {
stopFormatting();
}
}
@Override
public synchronized void afterTextChanged(Editable s) {
if (mStopFormatting) {
// Restart the formatting when all texts were clear.
mStopFormatting = !(s.length() == 0);
return;
}
if (mSelfChange) {
// Ignore the change caused by s.replace().
return;
}
//calculate few things that will be helpful later
int selectionEnd = Selection.getSelectionEnd(s);
boolean isCursorAtEnd = (selectionEnd == s.length());
//get formatted text for this number
String formatted = reformat(s);
//now calculate cursor position in formatted text
int finalCursorPosition = 0;
if (formatted.equals(s.toString())) {
//means there is no change while formatting don't move cursor
finalCursorPosition = selectionEnd;
} else if (isCursorAtEnd) {
//if cursor was already at the end, put it at the end.
finalCursorPosition = formatted.length();
} else {
// if no earlier case matched, we will use "digitBeforeCursor" way to figure out the cursor position
int digitsBeforeCursor = 0;
for (int i = 0; i < s.length(); i++) {
if (i >= selectionEnd) {
break;
}
if (PhoneNumberUtils.isNonSeparator(s.charAt(i))) {
digitsBeforeCursor++;
}
}
//at this point we will have digitsBeforeCursor calculated.
// now find this position in formatted text
for (int i = 0, digitPassed = 0; i < formatted.length(); i++) {
if (digitPassed == digitsBeforeCursor) {
finalCursorPosition = i;
break;
}
if (PhoneNumberUtils.isNonSeparator(formatted.charAt(i))) {
digitPassed++;
}
}
}
//if this ends right before separator, we might wish to move it further so user do not delete separator by mistake.
// because deletion of separator will cause stop formatting that should not happen by mistake
if (!isCursorAtEnd) {
while (0 < finalCursorPosition - 1 && !PhoneNumberUtils.isNonSeparator(formatted.charAt(finalCursorPosition - 1))) {
finalCursorPosition--;
}
}
//Now we have everything calculated, set this values in
if (formatted != null) {
mSelfChange = true;
s.replace(0, s.length(), formatted, 0, formatted.length());
mSelfChange = false;
lastFormatted = s;
Selection.setSelection(s, finalCursorPosition);
}
}
/**
* this will format the number in international format (only).
*/
private String reformat(CharSequence s) {
String internationalFormatted = "";
mFormatter.clear();
char lastNonSeparator = 0;
String countryCallingCode = "+" + countryPhoneCode;
//to have number formatted as international format, add country code before that
s = countryCallingCode + s;
int len = s.length();
for (int i = 0; i < len; i++) {
char c = s.charAt(i);
if (PhoneNumberUtils.isNonSeparator(c)) {
if (lastNonSeparator != 0) {
internationalFormatted = mFormatter.inputDigit(lastNonSeparator);
}
lastNonSeparator = c;
}
}
if (lastNonSeparator != 0) {
internationalFormatted = mFormatter.inputDigit(lastNonSeparator);
}
internationalFormatted = internationalFormatted.trim();
if (internationalFormatted.length() > countryCallingCode.length()) {
if (internationalFormatted.charAt(countryCallingCode.length()) == ' ')
internationalFormatted = internationalFormatted.substring(countryCallingCode.length() + 1);
else
internationalFormatted = internationalFormatted.substring(countryCallingCode.length());
} else {
internationalFormatted = "";
}
return TextUtils.isEmpty(internationalFormatted) ? "" : internationalFormatted;
}
private void stopFormatting() {
mStopFormatting = true;
mFormatter.clear();
}
private boolean hasSeparator(final CharSequence s, final int start, final int count) {
for (int i = start; i < start + count; i++) {
char c = s.charAt(i);
if (!PhoneNumberUtils.isNonSeparator(c)) {
return true;
}
}
return false;
}
}