package br.com.sapereaude.maskedEditText; import android.content.Context; import android.content.res.TypedArray; import android.os.Bundle; import android.os.Parcelable; import android.support.v4.text.TextUtilsCompat; import android.support.v7.widget.AppCompatEditText; import android.text.Editable; import android.text.SpannableStringBuilder; import android.text.TextWatcher; import android.text.style.ForegroundColorSpan; import android.util.AttributeSet; import android.util.Log; import android.view.KeyEvent; import android.view.View; import android.widget.TextView; import static android.content.ContentValues.TAG; public class MaskedEditText extends AppCompatEditText implements TextWatcher { public static final String SPACE = " "; private final OnEditorActionListener onEditorActionListener = new OnEditorActionListener() { @Override public boolean onEditorAction(TextView v, int actionId,KeyEvent event) { switch (actionId) { // case EditorInfo.IME_ACTION_NEXT: // fixing actionNext // return false; default: return true; } } }; private String mask; private char charRepresentation; private boolean keepHint; private int[] rawToMask; private RawText rawText; private boolean editingBefore; private boolean editingOnChanged; private boolean editingAfter; private int[] maskToRaw; private int selection; private boolean initialized; private boolean ignore; protected int maxRawLength; private int lastValidMaskPosition; private boolean selectionChanged; private OnFocusChangeListener focusChangeListener; private String allowedChars; private String deniedChars; private boolean shouldKeepText; public MaskedEditText(Context context) { super(context); init(); } public MaskedEditText(Context context, AttributeSet attrs) { super(context, attrs); init(); TypedArray attributes = context.obtainStyledAttributes(attrs, R.styleable.MaskedEditText); mask = attributes.getString(R.styleable.MaskedEditText_mask); allowedChars = attributes.getString(R.styleable.MaskedEditText_allowed_chars); deniedChars = attributes.getString(R.styleable.MaskedEditText_denied_chars); boolean enableImeAction = attributes.getBoolean(R.styleable.MaskedEditText_enable_ime_action, false); String representation = attributes.getString(R.styleable.MaskedEditText_char_representation); if(representation == null) { charRepresentation = '#'; } else { charRepresentation = representation.charAt(0); } keepHint = attributes.getBoolean(R.styleable.MaskedEditText_keep_hint, false); cleanUp(); // Ignoring enter key presses if needed if (!enableImeAction) { setOnEditorActionListener(onEditorActionListener); } else { setOnEditorActionListener(null); } attributes.recycle(); } @Override public Parcelable onSaveInstanceState() { final Parcelable superParcellable = super.onSaveInstanceState(); final Bundle state = new Bundle(); state.putParcelable("super", superParcellable); state.putString("text", getRawText()); state.putBoolean("keepHint", isKeepHint()); return state; } @Override public void onRestoreInstanceState(Parcelable state) { Bundle bundle = (Bundle) state; keepHint = bundle.getBoolean("keepHint", false); super.onRestoreInstanceState(((Bundle) state).getParcelable("super")); final String text = bundle.getString("text"); setText(text); Log.d(TAG, "onRestoreInstanceState: " + text); } @Override public void setText(CharSequence text, BufferType type) { // if (text == null || text.equals("")) return; super.setText(text, type); } /** @param listener - its onFocusChange() method will be called before performing MaskedEditText operations, * related to this event. */ @Override public void setOnFocusChangeListener(OnFocusChangeListener listener) { focusChangeListener = listener; } private void cleanUp() { initialized = false; if(mask == null || mask.isEmpty()){ return; } generatePositionArrays(); if (!shouldKeepText || rawText == null) { rawText = new RawText(); selection = rawToMask[0]; } editingBefore = true; editingOnChanged = true; editingAfter = true; if(hasHint() && rawText.length() == 0) { this.setText(makeMaskedTextWithHint()); } else { this.setText(makeMaskedText()); } editingBefore = false; editingOnChanged = false; editingAfter = false; maxRawLength = maskToRaw[previousValidPosition(mask.length() - 1)] + 1; lastValidMaskPosition = findLastValidMaskPosition(); initialized = true; super.setOnFocusChangeListener(new OnFocusChangeListener() { @Override public void onFocusChange(View v, boolean hasFocus) { if (focusChangeListener != null) { focusChangeListener.onFocusChange(v, hasFocus); } if (hasFocus()) { selectionChanged = false; MaskedEditText.this.setSelection(lastValidPosition()); } } }); } private int findLastValidMaskPosition() { for(int i = maskToRaw.length - 1; i >= 0; i--) { if(maskToRaw[i] != -1) return i; } throw new RuntimeException("Mask must contain at least one representation char"); } private boolean hasHint() { return getHint() != null; } public MaskedEditText(Context context, AttributeSet attrs, int defStyle) { super(context, attrs, defStyle); init(); } public void setShouldKeepText(boolean shouldKeepText) { this.shouldKeepText = shouldKeepText; } public boolean isKeepingText() { return shouldKeepText; } public void setMask(String mask) { this.mask = mask; cleanUp(); } public String getMask() { return this.mask; } public void setImeActionEnabled(boolean isEnabled) { if (isEnabled) setOnEditorActionListener(onEditorActionListener); else setOnEditorActionListener(null); } public String getRawText() { return this.rawText.getText(); } public void setCharRepresentation(char charRepresentation) { this.charRepresentation = charRepresentation; cleanUp(); } public char getCharRepresentation() { return this.charRepresentation; } /** * Generates positions for values characters. For instance: * Input data: mask = "+7(###)###-##-## * After method execution: * rawToMask = [3, 4, 5, 6, 8, 9, 11, 12, 14, 15] * maskToRaw = [-1, -1, -1, 0, 1, 2, -1, 3, 4, 5, -1, 6, 7, -1, 8, 9] * charsInMask = "+7()- " (and space, yes) */ private void generatePositionArrays() { int[] aux = new int[mask.length()]; maskToRaw = new int[mask.length()]; String charsInMaskAux = ""; int charIndex = 0; for(int i = 0; i < mask.length(); i++) { char currentChar = mask.charAt(i); if(currentChar == charRepresentation) { aux[charIndex] = i; maskToRaw[i] = charIndex++; } else { String charAsString = Character.toString(currentChar); if(!charsInMaskAux.contains(charAsString)) { charsInMaskAux = charsInMaskAux.concat(charAsString); } maskToRaw[i] = -1; } } if(charsInMaskAux.indexOf(' ') < 0) { charsInMaskAux = charsInMaskAux + SPACE; } char[] charsInMask = charsInMaskAux.toCharArray(); rawToMask = new int[charIndex]; System.arraycopy(aux, 0, rawToMask, 0, charIndex); } private void init() { addTextChangedListener(this); } @Override public void beforeTextChanged(CharSequence s, int start, int count, int after) { if(!editingBefore) { editingBefore = true; if(start > lastValidMaskPosition) { ignore = true; } int rangeStart = start; if(after == 0) { rangeStart = erasingStart(start); } Range range = calculateRange(rangeStart, start + count); if(range.getStart() != -1) { rawText.subtractFromString(range); } if(count > 0) { selection = previousValidPosition(start); } } } private int erasingStart(int start) { while(start > 0 && maskToRaw[start] == -1) { start--; } return start; } @Override public void onTextChanged(CharSequence s, int start, int before, int count) { if(!editingOnChanged && editingBefore) { editingOnChanged = true; if(ignore) { return; } if(count > 0) { int startingPosition = maskToRaw[nextValidPosition(start)]; String addedString = s.subSequence(start, start + count).toString(); count = rawText.addToString(clear(addedString), startingPosition, maxRawLength); if(initialized) { int currentPosition; if(startingPosition + count < rawToMask.length) currentPosition = rawToMask[startingPosition + count]; else currentPosition = lastValidMaskPosition + 1; selection = nextValidPosition(currentPosition); } } } } @Override public void afterTextChanged(Editable s) { if(!editingAfter && editingBefore && editingOnChanged) { editingAfter = true; if (hasHint() && (keepHint || rawText.length() == 0)) { setText(makeMaskedTextWithHint()); } else { setText(makeMaskedText()); } selectionChanged = false; setSelection(selection); editingBefore = false; editingOnChanged = false; editingAfter = false; ignore = false; } } public boolean isKeepHint() { return keepHint; } public void setKeepHint(boolean keepHint) { this.keepHint = keepHint; setText(getRawText()); } @Override protected void onSelectionChanged(int selStart, int selEnd) { // On Android 4+ this method is being called more than 1 time if there is a hint in the EditText, what moves the cursor to left // Using the boolean var selectionChanged to limit to one execution if(initialized ){ if(!selectionChanged) { selStart = fixSelection(selStart); selEnd = fixSelection(selEnd); // exactly in this order. If getText.length() == 0 then selStart will be -1 if (selStart > getText().length()) selStart = getText().length(); if (selStart < 0) selStart = 0; // exactly in this order. If getText.length() == 0 then selEnd will be -1 if (selEnd > getText().length()) selEnd = getText().length(); if (selEnd < 0) selEnd = 0; setSelection(selStart, selEnd); selectionChanged = true; } else{ //check to see if the current selection is outside the already entered text if(selStart > rawText.length() - 1){ final int start = fixSelection(selStart); final int end = fixSelection(selEnd); if (start >= 0 && end < getText().length()){ setSelection(start, end); } } } } super.onSelectionChanged(selStart, selEnd); } private int fixSelection(int selection) { if(selection > lastValidPosition()) { return lastValidPosition(); } else { return nextValidPosition(selection); } } private int nextValidPosition(int currentPosition) { while(currentPosition < lastValidMaskPosition && maskToRaw[currentPosition] == -1) { currentPosition++; } if(currentPosition > lastValidMaskPosition) return lastValidMaskPosition + 1; return currentPosition; } private int previousValidPosition(int currentPosition) { while(currentPosition >= 0 && maskToRaw[currentPosition] == -1) { currentPosition--; if(currentPosition < 0) { return nextValidPosition(0); } } return currentPosition; } private int lastValidPosition() { if(rawText.length() == maxRawLength) { return rawToMask[rawText.length() - 1] + 1; } return nextValidPosition(rawToMask[rawText.length()]); } private String makeMaskedText() { int maskedTextLength; if (rawText.length() < rawToMask.length) { maskedTextLength = rawToMask[rawText.length()]; } else { maskedTextLength = mask.length(); } char[] maskedText = new char[maskedTextLength]; //mask.replace(charRepresentation, ' ').toCharArray(); for (int i = 0; i < maskedText.length; i++) { int rawIndex = maskToRaw[i]; if (rawIndex == -1) { maskedText[i] = mask.charAt(i); } else { maskedText[i] = rawText.charAt(rawIndex); } } return new String(maskedText); } private CharSequence makeMaskedTextWithHint() { SpannableStringBuilder ssb = new SpannableStringBuilder(); int mtrv; int maskFirstChunkEnd = rawToMask[0]; for(int i = 0; i < mask.length(); i++) { mtrv = maskToRaw[i]; if (mtrv != -1) { if (mtrv < rawText.length()) { ssb.append(rawText.charAt(mtrv)); } else { ssb.append(getHint().charAt(maskToRaw[i])); } } else { ssb.append(mask.charAt(i)); } if ((keepHint && rawText.length() < rawToMask.length && i >= rawToMask[rawText.length()]) || (!keepHint && i >= maskFirstChunkEnd)) { ssb.setSpan(new ForegroundColorSpan(getCurrentHintTextColor()), i, i + 1, 0); } } return ssb; } private Range calculateRange(int start, int end) { Range range = new Range(); for(int i = start; i <= end && i < mask.length(); i++) { if(maskToRaw[i] != -1) { if(range.getStart() == -1) { range.setStart(maskToRaw[i]); } range.setEnd(maskToRaw[i]); } } if(end == mask.length()) { range.setEnd(rawText.length()); } if(range.getStart() == range.getEnd() && start < end) { int newStart = previousValidPosition(range.getStart() - 1); if(newStart < range.getStart()) { range.setStart(newStart); } } return range; } private String clear(String string) { if (deniedChars != null){ for(char c: deniedChars.toCharArray()){ string = string.replace(Character.toString(c), ""); } } if (allowedChars != null){ StringBuilder builder = new StringBuilder(string.length()); for(char c: string.toCharArray() ){ if (allowedChars.contains(String.valueOf(c) )){ builder.append(c); } } string = builder.toString(); } return string; } }