Eu sei que já existem um milhão de respostas para isso, com uma aceita. No entanto, existem inúmeros erros na resposta aceita e a maioria dos demais simplesmente corrige um (ou talvez dois) deles, sem expandir para todos os casos de uso possíveis.
Então, eu basicamente compilei a maioria das correções sugeridas nas respostas de suporte, além de adicionar um método para permitir a entrada contínua de números fora do intervalo na direção de 0 (se o intervalo não começar em 0), pelo menos até que seja certo de que não pode mais estar no intervalo. Porque, para ficar claro, este é o único momento que realmente causa problemas em muitas das outras soluções.
Aqui está a correção:
public class InputFilterIntRange implements InputFilter, View.OnFocusChangeListener {
private final int min, max;
public InputFilterIntRange(int min, int max) {
if (min > max) {
// Input sanitation for the filter itself
int mid = max;
max = min;
min = mid;
}
this.min = min;
this.max = max;
}
@Override
public CharSequence filter(CharSequence source, int start, int end, Spanned dest, int dstart, int dend) {
// Determine the final string that will result from the attempted input
String destString = dest.toString();
String inputString = destString.substring(0, dstart) + source.toString() + destString.substring(dstart);
// Don't prevent - sign from being entered first if min is negative
if (inputString.equalsIgnoreCase("-") && min < 0) return null;
try {
int input = Integer.parseInt(inputString);
if (mightBeInRange(input))
return null;
} catch (NumberFormatException nfe) {}
return "";
}
@Override
public void onFocusChange(View v, boolean hasFocus) {
// Since we can't actively filter all values
// (ex: range 25 -> 350, input "15" - could be working on typing "150"),
// lock values to range after text loses focus
if (!hasFocus) {
if (v instanceof EditText) sanitizeValues((EditText) v);
}
}
private boolean mightBeInRange(int value) {
// Quick "fail"
if (value >= 0 && value > max) return false;
if (value >= 0 && value >= min) return true;
if (value < 0 && value < min) return false;
if (value < 0 && value <= max) return true;
boolean negativeInput = value < 0;
// If min and max have the same number of digits, we can actively filter
if (numberOfDigits(min) == numberOfDigits(max)) {
if (!negativeInput) {
if (numberOfDigits(value) >= numberOfDigits(min) && value < min) return false;
} else {
if (numberOfDigits(value) >= numberOfDigits(max) && value > max) return false;
}
}
return true;
}
private int numberOfDigits(int n) {
return String.valueOf(n).replace("-", "").length();
}
private void sanitizeValues(EditText valueText) {
try {
int value = Integer.parseInt(valueText.getText().toString());
// If value is outside the range, bring it up/down to the endpoint
if (value < min) {
value = min;
valueText.setText(String.valueOf(value));
} else if (value > max) {
value = max;
valueText.setText(String.valueOf(value));
}
} catch (NumberFormatException nfe) {
valueText.setText("");
}
}
}
Observe que é impossível lidar com alguns casos de entrada "ativamente" (ou seja, como o usuário está inserindo), portanto, devemos ignorá-los e tratá-los depois que o usuário terminar de editar o texto.
Veja como você pode usá-lo:
EditText myEditText = findViewById(R.id.my_edit_text);
InputFilterIntRange rangeFilter = new InputFilterIntRange(25, 350);
myEditText.setFilters(new InputFilter[]{rangeFilter});
// Following line is only necessary if your range is like [25, 350] or [-350, -25].
// If your range has 0 as an endpoint or allows some negative AND positive numbers,
// all cases will be handled pre-emptively.
myEditText.setOnFocusChangeListener(rangeFilter);
Agora, quando o usuário tentar digitar um número mais próximo de 0 do que o intervalo permite, uma das duas coisas acontecerá:
Se min
e max
tiverem o mesmo número de dígitos, eles não terão permissão para inseri-lo assim que chegarem ao dígito final.
Se um número fora do intervalo for deixado no campo quando o texto perder o foco, ele será ajustado automaticamente para o limite mais próximo.
E, é claro, o usuário nunca poderá inserir um valor além de 0 do que o intervalo permite, nem é possível que um número como esse "acidentalmente" esteja no campo de texto por esse motivo.
Problemas conhecidos
- Isso só funciona se o
EditText
foco for perdido quando o usuário terminar.
A outra opção é desinfetar quando o usuário pressiona a tecla "concluído" / retornar, mas em muitos ou até na maioria dos casos, isso causa uma perda de foco de qualquer maneira.
No entanto, fechar o teclado virtual não desvira o foco automaticamente do elemento. Tenho certeza de que 99,99% dos desenvolvedores do Android gostariam que sim (e que o manuseio do foco nos EditText
elementos era menos desagradável em geral), mas até o momento não há nenhuma funcionalidade integrada para isso. O método mais fácil que eu encontrei para contornar isso, se necessário, é estender EditText
algo assim:
public class EditTextCloseEvent extends AppCompatEditText {
public EditTextCloseEvent(Context context) {
super(context);
}
public EditTextCloseEvent(Context context, AttributeSet attrs) {
super(context, attrs);
}
public EditTextCloseEvent(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
}
@Override
public boolean onKeyPreIme(int keyCode, KeyEvent event) {
if (event.getKeyCode() == KeyEvent.KEYCODE_BACK) {
for (InputFilter filter : this.getFilters()) {
if (filter instanceof InputFilterIntRange)
((InputFilterIntRange) filter).onFocusChange(this, false);
}
}
return super.dispatchKeyEvent(event);
}
}
Isso "engana" o filtro para higienizar a entrada, mesmo que a visualização não tenha realmente perdido o foco. Se, posteriormente, a visão perder o foco por conta própria, o saneamento de entrada será acionado novamente, mas nada mudará, pois já foi corrigido.
Encerramento
Ufa. Isso foi muito. O que originalmente parecia ser um problema trivialmente fácil acabou descobrindo muitos pedaços feios de baunilha Android (pelo menos em Java). E mais uma vez, você só precisa adicionar o ouvinte e estender EditText
se o seu intervalo não incluir 0 de alguma forma. (E realisticamente, se seu intervalo não incluir 0, mas começar em 1 ou -1, você também não terá problemas.)
Como última nota, isso funciona apenas para ints . Certamente, existe uma maneira de implementá-lo para trabalhar com decimais ( double
, float
), mas, como nem eu nem o solicitante original precisamos disso, não quero aprofundar muito nisso. Seria muito fácil simplesmente usar a filtragem pós-conclusão junto com as seguintes linhas:
// Quick "fail"
if (value >= 0 && value > max) return false;
if (value >= 0 && value >= min) return true;
if (value < 0 && value < min) return false;
if (value < 0 && value <= max) return true;
Você teria apenas que mudar de int
para float
(ou double
), permitir a inserção de um único .
(ou ,
, dependendo do país?) E analisar como um dos tipos decimais em vez de um int
.
Isso lida com a maior parte do trabalho de qualquer maneira, portanto, funcionaria de maneira muito semelhante.