package br.com.concretesolutions.canarinho.watcher; import android.text.Editable; import android.text.Selection; import android.text.TextWatcher; import br.com.concretesolutions.canarinho.formatador.Formatador; import br.com.concretesolutions.canarinho.validator.Validador; import br.com.concretesolutions.canarinho.watcher.evento.EventoDeValidacao; /** * Classe base para Watchers que possuem máscara e efetuam validação. * * @see Validador */ public abstract class BaseCanarinhoTextWatcher implements TextWatcher { private boolean mudancaInterna = false; private int tamanhoAnterior = 0; private EventoDeValidacao eventoDeValidacao; @Override public void beforeTextChanged(CharSequence s, int start, int count, int after) { // Não faz nada aqui } @Override public void onTextChanged(CharSequence s, int start, int before, int count) { // Não faz nada aqui } public boolean isMudancaInterna() { return mudancaInterna; } public EventoDeValidacao getEventoDeValidacao() { return eventoDeValidacao; } public void setEventoDeValidacao(EventoDeValidacao eventoDeValidacao) { this.eventoDeValidacao = eventoDeValidacao; } /** * Utilitário para implementações de Watcher customizadas. * Verifica se a ação foi de apagar um caracter * * @param s o Editable em uso * @return True case a ação foi uma deleção e false caso contrário */ protected boolean isApagouCaracter(Editable s) { return tamanhoAnterior > s.length(); } /** * Utilitário para implementações de Watcher customizadas. * Utilitário para atualizar o Editable com flags de atualização. * * @param validador Validador utilizado para verificar o input * @param resultadoParcial Objeto de validação * @param s Editable em uso * @param builder Valor atual da string */ // Usa o Editable para atualizar o Editable // O cursor SEMPRE sera posicionado no final do conteúdo protected void atualizaTexto(Validador validador, Validador.ResultadoParcial resultadoParcial, Editable s, StringBuilder builder) { tamanhoAnterior = builder.length(); mudancaInterna = true; s.replace(0, s.length(), builder, 0, builder.length()); if (builder.toString().equals(s.toString())) { // TODO: estudar implantar a manutenção da posição do cursor Selection.setSelection(s, builder.length()); } efetuaValidacao(validador, resultadoParcial, s); mudancaInterna = false; } /** * Método que efetua a validação em si. * * @param validador Validador utilizado para verificar o input * @param resultadoParcial Objeto de validação * @param s Editable em uso */ // CUIDADO AO ATUALIZAR O Editable AQUI!!! protected void efetuaValidacao(Validador validador, Validador.ResultadoParcial resultadoParcial, Editable s) { if (validador == null) { return; } if (eventoDeValidacao == null) { validador.ehValido(s.toString()); return; } validador.ehValido(s, resultadoParcial); if (!resultadoParcial.isParcialmenteValido()) { eventoDeValidacao.invalido(s.toString(), resultadoParcial.getMensagem()); } else if (!resultadoParcial.isValido()) { eventoDeValidacao.parcialmenteValido(s.toString()); } else { eventoDeValidacao.totalmenteValido(s.toString()); } } /** * Implementação genérica para adição ou remoção de caracter. * * @param s Editable em uso * @param mascara máscara do Watcher * @return Builder com o valor final */ protected StringBuilder trataAdicaoRemocaoDeCaracter(Editable s, char[] mascara) { return isApagouCaracter(s) ? trataRemocaoDeCaracter(s, mascara) : trataAdicaoDeCaracter(s, mascara); } private StringBuilder trataAdicaoDeCaracter(Editable s, char[] mascara) { return carregarMascara(s.toString(), mascara); } // Só é chamado após uma deleção, portanto, é seguro chamar mascara[s.length()] private StringBuilder trataRemocaoDeCaracter(Editable s, char[] mascara) { final StringBuilder builder = new StringBuilder(s); // Obtém a posição do último caracter excluído final int posicaoUltimoCaracter = mascara.length > s.length() ? s.length() : mascara.length - 1; // Verifica se o último caracter que foi excluído fazia parte da máscara final boolean ultimoCaracterEraMascara = mascara[posicaoUltimoCaracter] != '#'; // Se o último caracter excluído fazia parte da máscara, // deve excluir até o primeiro caracter que não faz parte da máscara if (ultimoCaracterEraMascara) { boolean encontrouCaracterValido = false; while (builder.length() > 0 && !encontrouCaracterValido) { encontrouCaracterValido = mascara[builder.length() - 1] == '#'; builder.deleteCharAt(builder.length() - 1); } } // Caso haja mais de um caracter de formatação (da máscara) faz um loop // até chegar em um caracter que não seja de formatação while (builder.length() > 0 && mascara[builder.length() - 1] != '#') { builder.deleteCharAt(builder.length() - 1); } return carregarMascara(builder.toString(), mascara); } private StringBuilder carregarMascara(String s, char[] mascara) { final StringBuilder builder = new StringBuilder(); final String str = Formatador.Padroes.PADRAO_SOMENTE_NUMEROS.matcher(s).replaceAll(""); // Só carregará a máscara se existir algum valor informado if (str.length() > 0) { int j = 0; // Acompanha a posição nos dígitos // É recomendado não usar enhanced for em Android for (int i = 0; i < mascara.length; i++) { final char charMascara = mascara[i]; if (charMascara != '#') { // '#' -> caracter de formatação builder.append(charMascara); continue; } if (j >= str.length()) { break; } builder.append(str.charAt(j)); j++; } } return builder; } }