6

I have a componed view in android contains several textViews and one EditText. I defined an attribute for my custom view called text and getText, setText methods. Now I want to add a 2-way data binding for my custom view in a way its bind to inner edit text so if my data gets updated edit text should be updated as well (that's works now) and when my edit text gets updated my data should be updated as well.

My binding class looks like this

@InverseBindingMethods({
        @InverseBindingMethod(type = ErrorInputLayout.class, attribute = "text"),
})
public class ErrorInputBinding {
    @BindingAdapter(value = "text")
    public static void setListener(ErrorInputLayout errorInputLayout, final InverseBindingListener textAttrChanged) {
        if (textAttrChanged != null) {
            errorInputLayout.getInputET().addTextChangedListener(new TextWatcher() {
                @Override
                public void beforeTextChanged(CharSequence charSequence, int i, int i1, int i2) {

                }

                @Override
                public void onTextChanged(CharSequence charSequence, int i, int i1, int i2) {

                }

                @Override
                public void afterTextChanged(Editable editable) {
                    textAttrChanged.onChange();
                }
            });
        }
    }
}

I tried to bind text with the code below. userInfo is an observable class.

            <ir.avalinejad.pasargadinsurance.component.ErrorInputLayout
                android:id="@+id/one_first_name"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                app:title="@string/first_name"
                app:text="@={vm.userInfo.firstName}"
                />

When I run the project I get this error

Error:(20, 13) Could not find event 'textAttrChanged' on View type 'ir.avalinejad.pasargadinsurance.component.ErrorInputLayout'

And my custom view looks like this

public class ErrorInputLayout extends LinearLayoutCompat implements TextWatcher {
    protected EditText inputET;
    protected TextView errorTV;
    protected TextView titleTV;
    protected TextView descriptionTV;

    private int defaultGravity;

    private String title;
    private String description;
    private String hint;
    private int inputType = -1;
    private int lines;
    private String text;

    private Subject<Boolean> isValidObservable = PublishSubject.create();

    private Map<Validation, String> validationMap;

    public ErrorInputLayout(Context context) {
        super(context);
        init();
    }

    public ErrorInputLayout(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
        readAttrs(attrs);
        init();
    }

    public ErrorInputLayout(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        readAttrs(attrs);
        init();
    }

    private void readAttrs(AttributeSet attrs){
        TypedArray a = getContext().getTheme().obtainStyledAttributes(
                attrs,
                R.styleable.ErrorInputLayout,
                0, 0);

        try {
            title = a.getString(R.styleable.ErrorInputLayout_title);
            description = a.getString(R.styleable.ErrorInputLayout_description);
            hint = a.getString(R.styleable.ErrorInputLayout_hint);
            inputType = a.getInt(R.styleable.ErrorInputLayout_android_inputType, -1);
            lines = a.getInt(R.styleable.ErrorInputLayout_android_lines, 1);
            text = a.getString(R.styleable.ErrorInputLayout_text);

        } finally {
            a.recycle();
        }
    }


    private void init(){
        validationMap = new HashMap<>();
        setOrientation(VERTICAL);
    }

    @Override
    protected void onFinishInflate() {
        super.onFinishInflate();

        titleTV = (TextView) LayoutInflater.from(getContext()).inflate(R.layout.error_layout_default_title_textview, null, false);
        addView(titleTV);

        descriptionTV = (TextView) LayoutInflater.from(getContext()).inflate(R.layout.error_layout_default_description_textview, null, false);
        addView(descriptionTV);

        readInputFromLayout();

        if(inputET == null) {
            inputET = (EditText) LayoutInflater.from(getContext()).inflate(R.layout.error_layout_defult_edittext, this, false);
            addView(inputET);
        }

        errorTV = (TextView) LayoutInflater.from(getContext()).inflate(R.layout.error_layout_default_error_textview, null, false);
        addView(errorTV);

        inputET.addTextChangedListener(this);
        defaultGravity = inputET.getGravity();

        //set values
        titleTV.setText(title);

        if(description != null && !description.trim().isEmpty()){
            descriptionTV.setVisibility(VISIBLE);
            descriptionTV.setText(description);
        }

        if(inputType != -1)
            inputET.setInputType(inputType);

        if(hint != null)
            inputET.setHint(hint);

        else
            inputET.setHint(title);

        inputET.setLines(lines);

        inputET.setText(text);
    }

    private void readInputFromLayout() {
        if(getChildCount() > 3){
            throw new IllegalStateException("Only one or zero view is allow in layout");
        }

        if(getChildCount() == 3){
            View view = getChildAt(2);
            if(view instanceof EditText)
                inputET = (EditText) view;
            else
                throw new IllegalStateException("only EditText is allow as child view");
        }
    }

    public void setText(String text){
        inputET.setText(text);
    }

    public String getText() {
        return text;
    }

    public void addValidation(@NonNull Validation validation, @StringRes int errorResourceId){
        addValidation(validation, getContext().getString(errorResourceId));
    }

    public void addValidation(@NonNull Validation validation, @NonNull String error){
        if(!validationMap.containsKey(validation))
            validationMap.put(validation, error);
    }

    public void remoteValidation(@NonNull Validation validation){
        if(validationMap.containsKey(validation))
            validationMap.remove(validation);
    }

    public EditText getInputET() {
        return inputET;
    }

    public TextView getErrorTV() {
        return errorTV;
    }

    @Override
    public void beforeTextChanged(CharSequence charSequence, int i, int i1, int i2) {

    }

    @Override
    public void onTextChanged(CharSequence charSequence, int i, int i1, int i2) {

    }

    @Override
    public void afterTextChanged(Editable editable) {
        checkValidity();

        if(editable.toString().length() == 0) //if hint
            inputET.setGravity(Gravity.RIGHT);
        else
            inputET.setGravity(defaultGravity);
    }

    public Subject<Boolean> getIsValidObservable() {
        return isValidObservable;
    }

    private void checkValidity(){
        //this function only shows the first matched error.
        errorTV.setVisibility(INVISIBLE);
        for(Validation validation: validationMap.keySet()){
            if(!validation.isValid(inputET.getText().toString())) {
                errorTV.setText(validationMap.get(validation));
                errorTV.setVisibility(VISIBLE);
                isValidObservable.onNext(false);
                return;
            }
        }

        isValidObservable.onNext(true);
    }
}
Alireza A. Ahmadi
  • 4,234
  • 4
  • 36
  • 57

3 Answers3

13

After hours of debugging, I found the solution. I changed my Binding class like this.

@InverseBindingMethods({
        @InverseBindingMethod(type = ErrorInputLayout.class, attribute = "text"),
})
public class ErrorInputBinding {
    @BindingAdapter(value = "textAttrChanged")
    public static void setListener(ErrorInputLayout errorInputLayout, final InverseBindingListener textAttrChanged) {
        if (textAttrChanged != null) {
            errorInputLayout.getInputET().addTextChangedListener(new TextWatcher() {
                @Override
                public void beforeTextChanged(CharSequence charSequence, int i, int i1, int i2) {

                }

                @Override
                public void onTextChanged(CharSequence charSequence, int i, int i1, int i2) {

                }

                @Override
                public void afterTextChanged(Editable editable) {
                    textAttrChanged.onChange();
                }
            });
        }
    }

    @BindingAdapter("text")
    public static void setText(ErrorInputLayout view, String value) {
        if(value != null && !value.equals(view.getText()))
            view.setText(value);
    }

    @InverseBindingAdapter(attribute = "text")
    public static String getText(ErrorInputLayout errorInputLayout) {
        return errorInputLayout.getText();
    }

First, I added AttrChanged after the text like this @BindingAdapter(value = "textAttrChanged") which is the default name for the listener and then I added getter and setter methods here as well.

Alireza A. Ahmadi
  • 4,234
  • 4
  • 36
  • 57
2

event = "android:textAttrChanged" works for me:

object DataBindingUtil {
    @BindingAdapter("emptyIfZeroText")        //replace "android:text" on EditText
    @JvmStatic
    fun setText(editText: EditText, text: String?) {
        if (text == "0" || text == "0.0") editText.setText("") else editText.setText(text)
    }

    @InverseBindingAdapter(attribute = "emptyIfZeroText", event = "android:textAttrChanged")
    @JvmStatic
    fun getText(editText: EditText): String {
        return editText.text.toString()
    }
}
Sam Chen
  • 2,491
  • 1
  • 17
  • 31
0

you need add one more function

@BindingAdapter("app:textAttrChanged")
fun ErrorInputLayout.bindTextAttrChanged(listener: InverseBindingListener) {

}