Created
February 21, 2017 14:54
-
-
Save alexandrius/e2c040de689fc1f0f91ce50c9e256465 to your computer and use it in GitHub Desktop.
DynamicMaskedEditText
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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