simpledialogfragments/src/main/java/eltos/simpledialogfragment/form/InputViewHolder.java
/*
* Copyright 2017 Philipp Niedermayer (github.com/eltos)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package eltos.simpledialogfragment.form;
import android.content.Context;
import android.os.Bundle;
import android.telephony.PhoneNumberFormattingTextWatcher;
import android.text.Editable;
import android.text.InputType;
import android.text.TextWatcher;
import android.view.View;
import android.view.inputmethod.EditorInfo;
import android.widget.ArrayAdapter;
import androidx.annotation.Nullable;
import com.google.android.material.textfield.TextInputLayout;
import java.util.Arrays;
import eltos.simpledialogfragment.R;
import eltos.simpledialogfragment.input.TextInputAutoCompleteTextView;
/**
* The ViewHolder class for {@link Input}
*
* This class is used to create an EditText and to maintain it's functionality
* <p>
* Created by philipp on 23.02.17.
*/
@SuppressWarnings("WeakerAccess")
class InputViewHolder extends FormElementViewHolder<Input> {
protected static final String SAVED_TEXT = "savedText";
private TextInputAutoCompleteTextView input;
private TextInputLayout inputLayout;
InputViewHolder(Input field) {
super(field);
}
@Override
protected int getContentViewLayout() {
return R.layout.simpledialogfragment_form_item_input;
}
@Override
protected void setUpView(View view, Context context, final Bundle savedInstanceState,
final SimpleFormDialog.DialogActions actions) {
input = view.findViewById(R.id.editText);
inputLayout = view.findViewById(R.id.inputLayout);
if (savedInstanceState == null) {
// Text preset
input.setText(field.getText(context));
// Select all on first focus
input.setSelectAllOnFocus(true);
input.setOnFocusChangeListener((v, hasFocus) -> {
if (hasFocus) {
input.postDelayed(() -> {
input.setSelectAllOnFocus(false);
input.setOnFocusChangeListener(null);
}, 10);
}
});
} else {
input.setText(savedInstanceState.getString(SAVED_TEXT));
}
// Hint
inputLayout.setHint(field.getHint(context));
// InputType
if ((field.inputType & InputType.TYPE_MASK_CLASS) == 0) {
// Setting TYPE_CLASS_TEXT as default is very important since the field
// is not editable, if no class is specified.
field.inputType |= InputType.TYPE_CLASS_TEXT;
}
input.setInputType(field.inputType);
if ((field.inputType & InputType.TYPE_MASK_CLASS) == InputType.TYPE_CLASS_PHONE) {
// format phone number automatically
input.addTextChangedListener(new PhoneNumberFormattingTextWatcher());
}
// PW hide/visible toggle button
if (field.passwordToggleVisible) {
inputLayout.setEndIconMode(TextInputLayout.END_ICON_PASSWORD_TOGGLE);
}
// Counter (maxLength)
if (field.maxLength > 0) {
inputLayout.setCounterMaxLength(field.maxLength);
inputLayout.setCounterEnabled(true);
}
// Multiline and wrapping
if (field.maxLines > 0) {
input.setMaxLines(field.maxLines);
}
if (field.wrap != null) {
input.setHorizontallyScrolling(!field.wrap); // can also be used to disable scrolling in multiline mode
}
// IME action
input.setImeOptions(actions.isLastFocusableElement() ? EditorInfo.IME_ACTION_DONE : EditorInfo.IME_ACTION_NEXT);
input.setOnEditorActionListener((textView, actionId, event) -> {
switch (actionId) {
case EditorInfo.IME_ACTION_NEXT:
// auto complete if suggestions forced
if (field.forceSuggestion && input.isPopupShowing()
&& input.getAdapter().getCount() > 0) {
input.setText(input.getAdapter().getItem(0).toString());
}
// fall through...
case EditorInfo.IME_ACTION_DONE:
// input.performCompletion();
actions.continueWithNextElement(true);
return true;
}
return false;
});
input.setOnFocusChangeListener((inputView, hasFocus) -> {
if (!hasFocus){
validate(inputView.getContext());
}
});
// Positive button state for single element forms
if (actions.isOnlyFocusableElement()) {
input.addTextChangedListener(new TextWatcher() {
@Override
public void beforeTextChanged(CharSequence s, int start, int count, int after) {
}
@Override
public void onTextChanged(CharSequence s, int start, int before, int count) {
}
@Override
public void afterTextChanged(Editable s) {
actions.updatePosButtonState();
}
});
}
// Auto complete suggestions
String[] suggestions = field.getSuggestions(context);
if (suggestions != null) {
if (field.isSpinner && !field.required){
suggestions = Arrays.copyOf(suggestions, suggestions.length + 1);
suggestions[suggestions.length - 1] = "";
}
ArrayAdapter<String> adapter = new ArrayAdapter<>(context,
// android.R.layout.simple_dropdown_item_1line
android.R.layout.simple_list_item_1, suggestions);
input.setAdapter(adapter);
input.setThreshold(1);
input.setOnItemClickListener((adapterView, itemView, position, id) -> {
if (field.isSpinner && !field.required && position == input.getAdapter().getCount()-1){
input.setText(null);
}
validate(itemView.getContext());
actions.updatePosButtonState();
actions.continueWithNextElement(true);
});
if (field.isSpinner || (field.forceSuggestion && !field.passwordToggleVisible)) {
inputLayout.setBoxBackgroundMode(TextInputLayout.BOX_BACKGROUND_OUTLINE); // Background none not supported in combination with dropdown
inputLayout.setEndIconMode(TextInputLayout.END_ICON_DROPDOWN_MENU);
}
if (field.isSpinner) {
input.setInputType(InputType.TYPE_NULL);
input.setKeyListener(null);
input.doNotFilter = true;
input.setOnClickListener(v -> actions.hideKeyboard());
}
}
}
@Nullable
protected String getText(){
return input.getText() != null ? input.getText().toString().trim() : null;
}
private boolean isInputEmpty(){
return getText() == null || getText().isEmpty();
}
private boolean isLengthExceeded() {
return field.maxLength > 0 && getText() != null && getText().length() > field.maxLength;
}
protected void setError(boolean enabled, @Nullable String error){
inputLayout.setError(error);
inputLayout.setErrorEnabled(enabled);
}
@Override
protected void saveState(Bundle outState) {
outState.putString(SAVED_TEXT, getText());
}
@Override
protected void putResults(Bundle results, String key) {
results.putString(key, getText());
}
@Override
protected boolean focus(final SimpleFormDialog.FocusActions actions) {
if (field.isSpinner){
actions.hideKeyboard();
input.showDropDown();
} else {
// Damn, there is an issue that hides the error hint and counter behind the keyboard
// because only the input EditText gets focused here, not the entire layout
// See: https://code.google.com/p/android/issues/detail?id=178153
// Workaround is resizing the dialog
actions.showKeyboard(input);
}
if (field.forceSuggestion){
input.showDropDown();
}
// actions.openKeyboard(input);
return input.requestFocus();
}
@Override
protected boolean posButtonEnabled(Context context) {
return !(field.required && isInputEmpty() || isLengthExceeded());
}
@Override
protected boolean validate(Context context) {
// required but empty?
if (field.required && isInputEmpty()) {
setError(true, context.getString(R.string.required));
return false;
}
// max length exceeded?
if (isLengthExceeded()){
setError(true, null);
return false;
}
// min length not reached?
if (getText() != null && getText().length() < field.minLength){
setError(true, context.getResources().getQuantityString(
R.plurals.at_least_x_chars, field.minLength, field.minLength));
return false;
}
// input not any of the provided suggestions?
if (field.forceSuggestion && !isInputEmpty()){
String[] suggestions = field.getSuggestions(context);
String text = getText();
boolean match = false;
if (suggestions != null && text != null && suggestions.length > 0){
for (String s : suggestions) {
if (s == null) continue;
if (text.equalsIgnoreCase(s)){
match = true;
input.setTextKeepState(s); // correct case
break;
}
}
if (!match){
setError(true, context.getString(R.string.input_not_a_given_option));
return false;
}
}
}
// predefined validation
String error = field.validatePattern(context, getText());
setError(error != null, error);
return error == null;
}
}