Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Two way binding on custom view

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);
    }
}
like image 781
Alireza A. Ahmadi Avatar asked Jan 11 '18 16:01

Alireza A. Ahmadi


People also ask

What is 2way binding?

In a two-way binding, the data flow is bi-directional. This means that the flow of code is from ts file to Html file as well as from Html file to ts file. In order to achieve a two-way binding, we will use ngModel or banana in a box syntax.

What is 2 way binding in Android?

Stay organized with collections Save and categorize content based on your preferences. The @={} notation, which importantly includes the "=" sign, receives data changes to the property and listen to user updates at the same time. // Avoids infinite loops.

How do you make a view binding?

To enable view binding, configure viewBinding in your module-level build. gradle file. Once enabled for a project, view binding will generate a binding class for all of your layouts automatically. You don't have to make changes to your XML — it'll automatically work with your existing layouts.

What is two-way data binding in Angular?

The two-way data binding in Angular is used to display information to the end user and allows the end user to make changes to the underlying data using the UI. This makes a two-way connection between the view (the template) and the component class. This is similar to the two-way binding in WPF.


2 Answers

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.

like image 189
Alireza A. Ahmadi Avatar answered Oct 14 '22 23:10

Alireza A. Ahmadi


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()
    }
}
like image 2
Sam Chen Avatar answered Oct 15 '22 01:10

Sam Chen