Skip to content

Instantly share code, notes, and snippets.

@alexandrius
Created February 21, 2017 14:54
Show Gist options
  • Save alexandrius/e2c040de689fc1f0f91ce50c9e256465 to your computer and use it in GitHub Desktop.
Save alexandrius/e2c040de689fc1f0f91ce50c9e256465 to your computer and use it in GitHub Desktop.
DynamicMaskedEditText
package ge.bog.mobilebank.ui.views.widgets;
/**
* Created by alex on 2/21/17.
*/
import android.content.Context;
import android.support.v7.widget.AppCompatEditText;
import android.text.Editable;
import android.text.InputFilter;
import android.text.InputType;
import android.text.SpannableStringBuilder;
import android.text.Spanned;
import android.text.TextUtils;
import android.text.TextWatcher;
import android.text.method.DigitsKeyListener;
import android.util.AttributeSet;
import android.view.View;
import java.util.ArrayList;
import java.util.ListIterator;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* @author safronov
* @version 02/12/15.
*/
public class MaskedEditText2 extends AppCompatEditText implements TextWatcher {
private String mask;
private String notMaskedSymbol;
private String deleteChar;
private String replacementChar;
private String format;
private boolean required;
private ArrayList<Integer> listValidCursorPositions = new ArrayList<>();
private Integer firstAllowedPosition = 0;
private Integer lastAllowedPosition = 0;
private OnFocusChangeListener onFocusChangeListener;
private String filteredMask;
private String cleanMask;
private MaskedInputFilter maskedInputFilter;
private boolean ignoreSelection;
public MaskedEditText2(Context context) {
super(context);
}
public MaskedEditText2(Context context, AttributeSet attrs) {
super(context, attrs);
}
public MaskedEditText2(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
private MaskedEditText2.AfterTextMaskedListener afterTextMaskedListener;
@Override
public void onTextChanged(CharSequence text, int start, int lengthBefore, int lengthAfter) {
}
@Override
public void beforeTextChanged(CharSequence s, int start, int count, int after) {
}
@Override
public void afterTextChanged(Editable s) {
if (afterTextMaskedListener != null)
afterTextMaskedListener.afterTextMasked();
}
public interface AfterTextMaskedListener {
void afterTextMasked();
}
public void setAfterTextMaskedListener(AfterTextMaskedListener afterTextMaskedListener) {
this.afterTextMaskedListener = afterTextMaskedListener;
}
public void initMask(String mask, String notMaskedSymbol) {
this.mask = mask;
this.notMaskedSymbol = notMaskedSymbol;
addTextChangedListener(this);
init();
this.setLongClickable(false);
this.setSingleLine(true);
this.setFocusable(true);
this.setFocusableInTouchMode(true);
}
private void init() {
if (!TextUtils.isEmpty(mask) && !TextUtils.isEmpty(notMaskedSymbol)) {
if (deleteChar == null) deleteChar = " ";
if (replacementChar == null) replacementChar = " ";
if (format == null) format = "";
initListValidCursorPositions(mask, notMaskedSymbol);
filteredMask = this.mask.replace(this.notMaskedSymbol, replacementChar);
cleanMask = this.mask.replace(this.notMaskedSymbol, "");
this.setText(filteredMask, BufferType.NORMAL);
maskedInputFilter = new MaskedInputFilter();
this.setFilters(new InputFilter[]{maskedInputFilter});
} else {
System.err.println("Mask not correct initialised ");
}
}
@Override
protected void onSelectionChanged(int selStart, int selEnd) {
if (mask == null || ignoreSelection) {
ignoreSelection = false;
super.onSelectionChanged(selStart, selEnd);
return;
}
String text = getText().toString();
if (selStart == selEnd) {
if (text.equals(filteredMask)) {
selStart = 0;
selEnd = 0;
} else {
selStart = getLastValidPosition(selStart);
selEnd = selStart;
}
setSelection(selStart, selEnd);
} else {
selEnd = getLastValidPosition(selEnd);
setSelection(selStart, selEnd);
}
super.onSelectionChanged(selStart, selEnd);
}
private int getLastValidPosition(int before) {
String text = getText().toString();
if (before > 0) {
for (int i = before; i > 0; i--) {
if (i < text.length())
if (cleanMask.contains(String.valueOf(text.charAt(i))) && !String.valueOf(text.charAt(i - 1)).equals(replacementChar)) {
return i + 1;
}
if (!String.valueOf(text.charAt(i - 1)).equals(replacementChar) && listValidCursorPositions.contains(i - 1))
return i;
}
}
return 0;
}
private void initListValidCursorPositions(String mask, String charSequence) {
char[] chars = mask.toCharArray();
char maskedSymbol = charSequence.charAt(0);
for (int i = 0; i < mask.length(); i++) {
if (chars[i] == maskedSymbol) {
listValidCursorPositions.add(i);
}
}
firstAllowedPosition = listValidCursorPositions.get(0);
lastAllowedPosition = listValidCursorPositions.get(listValidCursorPositions.size() - 1);
}
@Override
public void setInputType(int type) {
if (type == -1) {
type = InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS | InputType.TYPE_TEXT_VARIATION_PASSWORD;
}
if (type == InputType.TYPE_CLASS_NUMBER ||
type == InputType.TYPE_NUMBER_FLAG_SIGNED ||
type == InputType.TYPE_NUMBER_FLAG_DECIMAL ||
type == InputType.TYPE_CLASS_PHONE) {
final String symbolExceptions = getSymbolExceptions();
this.setKeyListener(DigitsKeyListener.getInstance("0123456789." + symbolExceptions));
} else {
super.setInputType(type);
}
}
/**
* Generate symbol exception for inputType = number
*/
private String getSymbolExceptions() {
if (TextUtils.isEmpty(filteredMask)) return "";
StringBuilder maskSymbolException = new StringBuilder();
for (char c : filteredMask.toCharArray()) {
if (!Character.isDigit(c) && maskSymbolException.indexOf(String.valueOf(c)) == -1) {
maskSymbolException.append(c);
}
}
maskSymbolException.append(replacementChar);
return maskSymbolException.toString();
}
public String getUnmaskedText() {
Editable text = super.getText();
if (mask != null && !mask.isEmpty()) {
Editable unMaskedText = new SpannableStringBuilder();
for (Integer index : listValidCursorPositions) {
if (text != null) {
unMaskedText.append(text.charAt(index));
}
}
if (format != null && !format.isEmpty())
return formatText(unMaskedText.toString(), format);
else
return unMaskedText.toString().trim();
}
return text.toString().trim();
}
public void setMaskedText(String input) {
if (input != null) {
StringBuilder filteredInputBuilder = new StringBuilder(input);
if (filteredInputBuilder.length() < listValidCursorPositions.size()) {
while (filteredInputBuilder.length() < listValidCursorPositions.size()) {
filteredInputBuilder.append(deleteChar);
}
} else if (filteredInputBuilder.length() > listValidCursorPositions.size()) {
filteredInputBuilder.replace(listValidCursorPositions.size(), filteredInputBuilder.length(), "");
}
StringBuilder buffer = new StringBuilder(filteredInputBuilder);
Editable text = this.getText();
if (text != null) {
for (int i = 0; i < mask.length(); i++) {
if (!listValidCursorPositions.contains(i)) {
buffer.insert(i, String.valueOf(mask.charAt(i)));
}
}
maskedInputFilter.setTextSetup(true);
this.setText(buffer.toString());
maskedInputFilter.setTextSetup(false);
}
}
}
private String formatText(String input, String pattern) {
String regularExpression = "(\\[[\\d]+\\])";
Pattern p = Pattern.compile(regularExpression);
Matcher m = p.matcher(pattern);
StringBuffer sb = new StringBuffer();
while (m.find()) {
m.appendReplacement(sb, getSymbol(input, m.group()));
}
return sb.toString();
}
private String getSymbol(String input, String group) {
int i = Integer.valueOf(group.replace("[", "").replace("]", ""));
return String.valueOf(input.toCharArray()[i - 1]);
}
public void setFormat(String format) {
this.format = format;
}
public boolean isRequired() {
return required;
}
public void setRequired(boolean required) {
this.required = required;
}
public void setMask(String mask) {
this.mask = mask;
}
@Override
public void setOnFocusChangeListener(final OnFocusChangeListener onFocusChangeListener) {
this.onFocusChangeListener = onFocusChangeListener;
super.setOnFocusChangeListener(new OnFocusChangeListener() {
@Override
public void onFocusChange(View v, boolean hasFocus) {
if (MaskedEditText2.this.onFocusChangeListener != null) {
MaskedEditText2.this.onFocusChangeListener.onFocusChange(v, hasFocus);
}
}
});
}
@Override
public void setSelection(int index) {
ignoreSelection = true;
super.setSelection(index);
}
private class MaskedInputFilter implements InputFilter {
private boolean isUserInput = true;
private boolean textSetup = false;
@Override
public CharSequence filter(CharSequence source, int start, int end, Spanned dest, int dstart, int dend) {
if (textSetup) return source;
if (!(source instanceof SpannableStringBuilder)) {
StringBuilder filteredStringBuilder = new StringBuilder();
final boolean charAllowed = isCharAllowed(dstart);
for (int i = start; i < end; i++) {
char currentChar = source.charAt(i);
if (charAllowed) {
isUserInput = false;
MaskedEditText2.this.getText().replace(dstart, dstart + 1, "");
isUserInput = true;
filteredStringBuilder.append(currentChar);
int index;
if (!isCharAllowed(dstart + 1))
index = dstart + 1;
else
index = dstart;
skipSymbol(index, true);
} else {
if (dstart != mask.length()) {
int index;
if (!isCharAllowed(dstart))
index = dstart + 1;
else
index = dstart;
int position = skipSymbol(index, true);
MaskedEditText2.this.getText().replace(position, position, Character.toString(currentChar));
}
}
}
if (isUserInput && TextUtils.isEmpty(source)) {//deletion detection
if (dend != 0) {
if (charAllowed) {
filteredStringBuilder.append(deleteChar);
skipSymbolAfterDeletion(dstart);
} else {
filteredStringBuilder.append(mask.charAt(dstart));
skipSymbolAfterDeletion(dstart);
}
}
}
return filteredStringBuilder.toString();
}
return source;
}
private int skipSymbol(int index, boolean performSkip) {
int position = getNextAvailablePosition(index, false);
if (position > lastAllowedPosition)
position = lastAllowedPosition;
if (performSkip)
setSelection(position);
return position;
}
private void skipSymbolAfterDeletion(int index) {
final int position = getNextAvailablePosition(index, true);
setSelection(position);
}
private int getNextAvailablePosition(int index, boolean isDeletion) {
if (listValidCursorPositions.contains(index)) {
final int i = listValidCursorPositions.indexOf(index);
final ListIterator<Integer> iterator = listValidCursorPositions.listIterator(i);
if (isDeletion) {
if (iterator.hasPrevious()) return iterator.previous() + 1;
} else {
if (iterator.hasNext()) return iterator.next();
}
return index;
} else {
return findCloserIndex(index, isDeletion);
}
}
private int findCloserIndex(int index, boolean isDeletion) {
ListIterator<Integer> iterator;
if (isDeletion) {
iterator = listValidCursorPositions.listIterator(listValidCursorPositions.size() - 1);
while (iterator.hasPrevious()) {
final Integer previous = iterator.previous();
if (previous <= index)
return previous + 1;
}
return firstAllowedPosition;
} else {
if (index > firstAllowedPosition) {
iterator = listValidCursorPositions.listIterator();
while (iterator.hasNext()) {
final Integer next = iterator.next();
if (next >= index)
return next - 1;
}
return lastAllowedPosition;
} else {
return firstAllowedPosition;
}
}
}
private boolean isCharAllowed(int index) {
return index < mask.length() && mask.charAt(index) == notMaskedSymbol.toCharArray()[0];
}
public void setTextSetup(boolean textSetup) {
this.textSetup = textSetup;
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment