TextView when the user hides the soft keyboard (ime). It should correct it as a proper money amount: two decimal digits, decimal separator. Of course there is no way on Android to query if the keyboard is going to hide. But there are a few ways, the keyboard can be hidden by the user: backpress when it is visible, focus outside of the TextView, change container TabFragment to other Fragment. This is my solution for this:/**
* Simple subclass of an EditText that allows to capture and dispatch BackPress, when the EditText has the focus.
* This event is later used to finalise formatted text in the amount field (append trailing ".00" if necessary).
* I could not find other way to detect keyboard is about to hide.
* Created by: mikinw
*/
@SuppressWarnings("UnusedDeclaration")
public class HwKeySensibleEditText extends EditText {
public HwKeySensibleEditText(Context context) {
super(context);
}
public HwKeySensibleEditText(Context context, AttributeSet attrs) {
super(context, attrs);
}
public HwKeySensibleEditText(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
}
@Override
public boolean onKeyPreIme(int keyCode, KeyEvent event) {
return super.dispatchKeyEvent(event);
}
@Override
public Parcelable onSaveInstanceState() {
// This forces a focus changed event to allow {@link AmountReformatter} to do
// validation when user hides keyboard
onFocusChanged(false, FOCUS_BACKWARD, null);
return super.onSaveInstanceState();
}
}
This class is responsible to reformat the text inside the TextView. It handles the forced focus changed event also. I didn't delete the references to the outer Constants class, but you can find out, what those values should be.
/**
* Controller class for formatting amount field.
* Handles all automatic correction that is needed for the amount field.
* In the constructor it gets a {@link HwKeySensibleEditText} - which is a slightly modified EditText.
* Auto correction has two steps:
* - as to user types (add pound sign, don't allow multiple zeros at the beginning, etc. please check
* correctOnTheFlyText() function for this. And related unit tests)
* - on focus lost (add ".00" as necessary. please check correctFinalAmount() function and related unit tests)
*
* Also 2 listener can be attached also: OnValueChangedListener and OnEnterLeaveListener.
* Created by: mikinw
*/
public class AmountReformatter implements TextWatcher, View.OnKeyListener, View.OnFocusChangeListener {
private HwKeySensibleEditText mAmountTextView;
private OnValueChangedListener mValueChangedListener;
private OnEnterLeaveListener mEnterLeaveListener;
private static final int MAX_INTEGER_DIGITS = 15;
public interface OnValueChangedListener {
void onValueChanged (String value);
}
public interface OnEnterLeaveListener {
void onEnter();
}
private AmountReformatter(HwKeySensibleEditText amountTextView,
OnValueChangedListener valueChangedListener,
OnEnterLeaveListener enterLeaveListener) {
if (amountTextView == null) {
throw new IllegalArgumentException("amountTextView can't be null");
}
mAmountTextView = amountTextView;
mValueChangedListener = valueChangedListener;
mEnterLeaveListener = enterLeaveListener;
}
private boolean mDeleting = false;
@Override
public void beforeTextChanged(CharSequence charSequence, int start, int count, int after) {
mDeleting = after < count && mAmountTextView.hasFocus();
}
@Override
public void onTextChanged(CharSequence charSequence, int i, int i2, int i3) {
}
@Override
public void afterTextChanged(Editable editable) {
if(mAmountTextView.hasFocus()){
//update onthefly only if tv has focus, cause correctFinalAmount will be overwrited
mAmountTextView.removeTextChangedListener(this);
final String curValue = editable.toString();
String corrected = null;
if(mDeleting && curValue.length() == 0){
//don't allow user to delete pound char
corrected = Constants.POUND_STRING;
}else{
corrected = correctOnTheFlyText(curValue);
}
// prevent infinite recursive call
if ( !corrected.equals(editable.toString())) {
editable.replace(0, editable.length(), corrected);
}
mAmountTextView.addTextChangedListener(this);
}
if (mValueChangedListener != null) {
mValueChangedListener.onValueChanged(mAmountTextView.getText().toString());
}
}
@Override
public boolean onKey(View view, int keyCode, KeyEvent keyEvent) {
if (keyCode == KeyEvent.KEYCODE_BACK && keyEvent.getAction() == KeyEvent.ACTION_UP) {
mAmountTextView.clearFocus();
} else if (keyCode == KeyEvent.KEYCODE_ENTER && keyEvent.getAction() == KeyEvent.ACTION_UP) {
final InputMethodManager imm =
(InputMethodManager)view.getContext().getSystemService(Context.INPUT_METHOD_SERVICE);
imm.hideSoftInputFromWindow(view.getWindowToken(), 0);
mAmountTextView.clearFocus();
return false;
}
return false;
}
@Override
public void onFocusChange(View view, boolean focusGained) {
final String txtAmount = mAmountTextView.getText().toString();
if (focusGained) {
if (TextUtils.isEmpty(txtAmount)) {
mAmountTextView.setText(Constants.POUND_STRING);
}else{
//needs to remove thousands separator
mAmountTextView.setText(correctOnTheFlyText(txtAmount));
}
//move cursor to the end
mAmountTextView.setSelection(mAmountTextView.getText().length());
mAmountTextView.setCompoundDrawablesWithIntrinsicBounds(null, null, null, null);
if (mEnterLeaveListener != null) {
mEnterLeaveListener.onEnter();
}
} else {
//restore texthint if there is only pound char
mAmountTextView.setText(Constants.POUND_STRING.equals(txtAmount)
? ""
: correctFinalAmount(txtAmount));
}
}
/**
* Appends ".00" as needed or truncates to have exactly 2 decimals. Does not check other validity of the original
* string (eg. will accept alphaliterals). It it's value is zero, then returns empty string.
*
* @param original amount string that needs to be modified according to the above rules
* @return modified amount text containing decimal separator and two digits after that
*/
@SuppressWarnings("MagicCharacter")
static String correctFinalAmount(String original) {
// if no . add . to end
// fill the end to have 2 decimals
if (TextUtils.isEmpty(original)) {
return original;
}
final String withoutPoundSign = original.charAt(0) == Constants.POUND_CHAR ? original.substring(1) : original;
BigDecimal decimal;
try {
decimal = new BigDecimal(withoutPoundSign);
if (decimal.compareTo(new BigDecimal(0)) == 0) {
return "";
}
} catch (NumberFormatException nfe) {
return "";
}
decimal = decimal.setScale(2, RoundingMode.DOWN);
final DecimalFormat df = new DecimalFormat();
df.setDecimalFormatSymbols(Constants.FORCED_FORMAT_SYMBOLS);
df.setGroupingSize(3);
df.setGroupingUsed(true);
df.setMaximumFractionDigits(2);
df.setMinimumFractionDigits(2);
df.setPositivePrefix(Constants.POUND_STRING);
return df.format(decimal);
}
/**
* Filters out not allowed characters (eg. letters, symbols). Also truncates the leading zeros, but does allow to
* have "0.". Also adds pound symbol to the beginning. Truncates ending numbers after the first "." character to
* have at most 2 of them.
* @param original String that has to be formatted, corrected
* @return formatted string according to the rules above
*/
static String correctOnTheFlyText(String original) {
if (TextUtils.isEmpty(original)) {
return original;
}
final StringBuilder preDot = new StringBuilder(original.length() + 1);
final StringBuilder postDot = new StringBuilder(original.length() + 1);
boolean leadingZero = false;
boolean dot = false;
int i = 0;
final int to = original.length();
for (; i < to; i++) {
final char ch = original.charAt(i);
if (ch == '0') {
leadingZero = true;
} else if (ch > '0' && ch <= '9') {
break;
} else if (ch == Constants.FORCED_FORMAT_SYMBOLS.getDecimalSeparator()) {
dot = true;
leadingZero = true;
break;
}
}
for (; i < to; i++) {
final char ch = original.charAt(i);
if (ch >= '0' && ch <= '9') {
if (preDot.length() < MAX_INTEGER_DIGITS) {
preDot.append(ch);
}
}else if(ch == Constants.FORCED_FORMAT_SYMBOLS.getGroupingSeparator()){
//thousands separator ignor, will be added later
} else if (ch == Constants.FORCED_FORMAT_SYMBOLS.getDecimalSeparator()) {
dot = true;
break;
}
}
for (int decimalCount = 0; i < to && decimalCount < 2; i++) {
final char c = original.charAt(i);
if (c >= '0' && c <= '9') {
postDot.append(c);
decimalCount++;
}
}
if (leadingZero && preDot.length() == 0) {
preDot.insert(0, '0');
}
if (dot) {
preDot.append(Constants.FORCED_FORMAT_SYMBOLS.getDecimalSeparator());
}
//add thousands separator
preDot.append(postDot);
preDot.insert(0, Constants.POUND_CHAR);
return preDot.toString();
}
/** Static initiliser */
public static void setListenersFor(HwKeySensibleEditText amountTextView,
OnValueChangedListener valueChangedListener,
OnEnterLeaveListener enterListener) {
final AmountReformatter reformatter = new AmountReformatter(amountTextView, valueChangedListener, enterListener);
amountTextView.setOnFocusChangeListener(reformatter);
amountTextView.addTextChangedListener(reformatter);
amountTextView.setOnKeyListener(reformatter);
}
}
No comments:
Post a Comment