I'm trying to follow this blog post to try and get two way data binding to work for a custom component (A constraint view with an EditText in it).
I'm able to get two standard EditText components to be in sync (both ways) with my model, but I'm having trouble getting the changes in my custom component to flow into my model (although one way data binding works).
My model:
public class Model extends BaseObservable {
private String value;
#Bindable
public String getValue() {
return value;
}
public void setValue(String value) {
this.value = value;
notifyPropertyChanged(company.com.databinding.BR.value);
}
public Model() {
value = "Value";
}
}
Activity:
#InverseBindingMethods({
#InverseBindingMethod(
type = CustomComponent.class,
attribute = "value",
method = "getValue")
})
public class MainActivity extends AppCompatActivity {
#BindingAdapter("value")
public static void setColor(CustomComponent view, String value) {
if (!value.equals(view.getValue())) {
view.setValue(value);
}
}
#BindingAdapter(
value = {"onValueChange", "valueAttrChanged"},
requireAll = false
)
public static void setListeners(CustomComponent view,
final ValueChangeListener onValueChangeListener,
final InverseBindingListener inverseBindingListener) {
ValueChangeListener newListener;
if (inverseBindingListener == null) {
newListener = onValueChangeListener;
} else {
newListener = new ValueChangeListener() {
#Override
public void onValueChange(CustomComponent view,
String value) {
if (onValueChangeListener != null) {
onValueChangeListener.onValueChange(view,
value);
}
inverseBindingListener.onChange();
}
};
}
ValueChangeListener oldListener =
ListenerUtil.trackListener(view, newListener,
R.id.textWatcher);
if (oldListener != null) {
view.removeListener(oldListener);
}
if (newListener != null) {
view.addListener(newListener);
}
}
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
//setContentView(R.layout.activity_main);
ActivityMainBinding binding = DataBindingUtil.setContentView(this, R.layout.activity_main);
binding.setModel(new Model());
}
}
Custom component:
public class CustomComponent extends ConstraintLayout {
private String value;
private EditText txt;
private TextWatcher textWatcher;
ValueChangeListener listener;
public String getValue() {
return value;
}
public void setValue(String value) {
this.value = value;
if (txt != null) {
txt.setText(value);
}
}
public CustomComponent(Context context) {
super(context);
init(context);
}
public CustomComponent(Context context, AttributeSet attrs) {
super(context, attrs);
init(context, attrs);
}
public CustomComponent(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
init(context, attrs);
}
private void init(Context context) {
}
private void init(Context context, AttributeSet attrs) {
View.inflate(context, R.layout.custom_component, this);
txt = findViewById(R.id.txt_box);
final CustomComponent self = this;
textWatcher = 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) {
if (listener != null) {
listener.onValueChange(self, editable.toString());
}
}
};
txt.addTextChangedListener(textWatcher);
}
public void addListener(ValueChangeListener listener) {
this.listener = listener;
}
public void removeListener(ValueChangeListener listener) {
this.listener = null;
}
}
public interface ValueChangeListener {
public void onValueChange(CustomComponent view, String value);
}
I think the section "Hooking The Event" in that post has gone completely over my head; I really only needed a simple setter and getter for the component, and so couldn't quite understand what was being done in that BindingAdapter. Of all of them I think it's this line that I don't get at all:
ValueChangeListener oldListener =
ListenerUtil.trackListener(view, newListener,
R.id.textWatcher);
Demo at: https://github.com/indgov/data_binding
Sorry that the ListenerUtil was confusing. That's only useful when your component supports multiple listeners. In that case, you can't just set a new listener, you must remove the old one and add the new one. ListenerUtil helps you track the old listener so it can be removed. In your case, it can be simplified:
#BindingAdapter(
value = {"onValueChange", "valueAttrChanged"},
requireAll = false
)
public static void setListeners(CustomComponent view,
final ValueChangeListener onValueChangeListener,
final InverseBindingListener inverseBindingListener) {
ValueChangeListener newListener;
if (inverseBindingListener == null) {
newListener = onValueChangeListener;
} else {
newListener = new ValueChangeListener() {
#Override
public void onValueChange(CustomComponent view,
String value) {
if (onValueChangeListener != null) {
onValueChangeListener.onValueChange(view,
value);
}
inverseBindingListener.onChange();
}
};
}
view.setListener(newListener);
}
and then replace addListener() with setListener() and you don't need the removeListener() because you can always set the listener to null.
The problem you're seeing is in your component:
public String getValue() {
return value;
}
You're returning the value that was last set by the setter and not the value that is in the EditText. To solve this:
public String getValue() {
return txt.getText().toString();
}
Related
I have implement a checkable CardView by following https://medium.com/#AlbinPoignot/checkable-cardview-in-all-android-versions-7124ca6df1ab
However, I need to let the user select just one option.
To clarify, if one is already checked, and the user select other, I need to deselect the previous option.
Furthermore, I need to when return the selected CardView keeps the checked state.
Could someone help me with this 2 tasks? Below is my implementation:
public class CheckableCardView extends CardView implements Checkable {
private static final int[] CHECKED_STATE_SET = {
android.R.attr.state_checked
};
private boolean isChecked;
private TextView itemText;
public CheckableCardView(Context context) {
super(context);
init(null);
}
public CheckableCardView(Context context, AttributeSet attrs) {
super(context, attrs);
init(attrs);
}
public CheckableCardView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init(attrs);
}
private void init(AttributeSet attrs) {
LayoutInflater.from(getContext()).inflate(R.layout.checkable_card_view, this, true);
setClickable(true);
setChecked(false);
setCardBackgroundColor(ContextCompat.getColorStateList(getContext(), R.color.selector_card_view_background));
if (attrs != null) {
TypedArray ta = getContext().obtainStyledAttributes(attrs, R.styleable.CheckableCardView, 0, 0);
try {
String text = ta.getString(R.styleable.CheckableCardView_card_text);
itemText = (TextView) findViewById(R.id.text);
if (text != null) {
setText(text);
}
} finally {
ta.recycle();
}
}
}
public void setText(String text){
itemText.setText(text);
}
#Override
protected int[] onCreateDrawableState(int extraSpace) {
final int[] drawableState = super.onCreateDrawableState(extraSpace + 1);
if (isChecked()) {
mergeDrawableStates(drawableState, CHECKED_STATE_SET);
}
return drawableState;
}
#Override
public boolean performClick() {
toggle();
return super.performClick();
}
#Override
public void setChecked(boolean checked) {
this.isChecked = checked;
}
#Override
public boolean isChecked() {
return isChecked;
}
#Override
public void toggle() {
setChecked(!this.isChecked);
}
}
You can also use the MaterialCard provided by the Material Components Library.
This card implement a Checkable interface by default.
Just use the android:checkable attribute in the xml:
<com.google.android.material.card.MaterialCardView
android:checkable="true"
..>
or setCheckable(true) in your code.
A way of switching to checked state is:
final MaterialCardView cardView = findViewById(R.id.card);
cardView.setOnClickListener(new View.OnClickListener() {
#Override public void onClick(View view) {
//cardView.setChecked(!cardView.isChecked());
cardView.toggle();
}
});
For those who are looking for an easy solution to this problem, here is the code:
cardONE.setOnClickListener {
cardONE.isChecked = true // set the current card to checked
cardTWO.isChecked = false // set the other card to unchecked
}
cardTWO.setOnClickListener {
cardTWO.isChecked = true
cardONE.isChecked = false
}
Or the function:
fun setChecked(checkCard: MaterialCardView, uncheckCard: MaterialCardView){
checkCard.isChecked = true
uncheckCard.isChecked = false
}
cardONE.setOnClickListener {
setChecked(it as MaterialCardView, cardTWO)
}
It's maybe not the most "elegant" way, but it works like a charm.
Needed Dependencies
implementation "com.google.android.material:material:1.2.0"
XML File
<com.google.android.material.card.MaterialCardView
android:id="#+id/cardONE"
<!-- THIS IS NEEDED -->
android:checkable="true"
android:clickable="true"
android:focusable="true" />
You can do that by declare:
private List<CheckableCardView> checkableCardViewList = new ArrayList<>();
then you can add your cards to your list in "onBindViewHolder"
checkableCardViewList.add(position,holder.cardView);
finally you can add a callback function like "onClick"
holder.cardView.setOnClickListener(new CheckableCardView.OnClickListener() {
#Override
public void onClick(boolean b) {
if (b) {
for(CheckableCardView checkableCardView : checkableCardViewList) {
checkableCardView.setChecked(false);
}
checkableCardViewList.get(position).setChecked(true);
notifyDataSetChanged();
}
}
});
for call back you can add this to your CheckableCardView at bottom
public void setOnClickListener(OnClickListener onClickListener) { this.onClickListener = onClickListener;}
public interface OnClickListener {
void onClick(boolean b);
}
and at the top
private OnClickListener onClickListener;
#Override
public boolean performClick() {
toggle();
onClickListener.onClick(this.isChecked);
return super.performClick();
}
I have a RecyclerView whose cells are populated through data-binding. Each cell represents an item for a cart of products. Each cell contains an EditText that is responsable for the product quantity in the cart. The quantity is reprezented as an observableInt in my ViewModel. When the quantity changes I want to make an action and I have an OnPropertyChangedCallback listener set for the observableInt parameter. If I use app:addTextChangedListener="#{cartItemVM.quantityInputTextWatcher}" to get the value and set it to the observable, its listener will be called for several times(that is also because I can change the value of the EditText from some + - buttons as well).
I run into Two-way data binding, but I still cannot make it work. This is what I have so far:
<QuantityEditText
android:id="#+id/etQuantityInput"
android:text="#{String.valueOf(cartItemVM.totalInCart)}"
quantity="#={cartItemVM.totalInCart}"
onQuantityChange="#{cartItemVM.onQuantityChange}"
This is my custom EditText:
public class QuantityEditText extends CustomEditText {
private int quantity;
private OnQuantityChangeListener onQuantityChangeListener;
public interface OnQuantityChangeListener {
void onQuantityChange(QuantityEditText view, int quantity);
}
This is my ViewModel class:
#InverseBindingMethods({
#InverseBindingMethod(type = QuantityEditText.class, attribute = "quantity")
})
public class ProductInCartObservableViewModel{
public final ObservableInt totalInCart;
#BindingAdapter(value = {"onQuantityChange", "quantityAttrChanged"},
requireAll = false)
public static void setQuantityAttrChanged(QuantityEditText view,
final QuantityEditText.OnQuantityChangeListener listener,
final InverseBindingListener quantityChange) {
if (quantityChange == null) {
view.setOnQuantityChangeListener(listener);
} else {
view.setOnQuantityChangeListener(new QuantityEditText.OnQuantityChangeListener() {
#Override
public void onQuantityChange(QuantityEditText view, int quantity) {
if (listener != null) {
listener.onQuantityChange(view, quantity);
}
view.setText(String.valueOf(quantity));
quantityChange.onChange();
}
});
}
}
#BindingAdapter("quantity")
public static void setQuantity(QuantityEditText view, int quantity) {
if (quantity != view.getQuantity()) {
view.setQuantity(quantity);
}
}
#InverseBindingAdapter(attribute = "quantity")
public static int getQuantity(QuantityEditText view) {
int val = 0;
if (!TextUtils.isEmpty(view.getText().toString())) {
try {
val = Integer.valueOf(view.getText().toString());
} catch (IllegalArgumentException e) {
Timber.e(e);
}
}
// Won't let the user remove product from cart using the editText
if (val <= 0) {
val = 1;
}
if (val > 150) {
val = 150;
}
return val;
}
public QuantityEditText.OnQuantityChangeListener onQuantityChange = new QuantityEditText.OnQuantityChangeListener() {
#Override
public void onQuantityChange(QuantityEditText view, int quantity) {
if (quantity <= 0) {
quantity = 0;
}
if (quantity > 150) {
quantity = 150;
}
totalInCart.set(quantity);
}
};
The implementation is taken from different places, but I admit I haven't fully understood the process, so an explanation will be highly appreciated too.
I can think of a couple of ways to solve the problem. The first is to avoid the QuatityEditText and just set the number directly. I prefer using conversion methods to custom two-way binding because they tend to be easier to implement:
#InverseMethod("stringToInt")
public static String intToString(TextView view, int oldValue, int value) {
NumberFormat numberFormat = getNumberFormat(view);
try {
String inView = view.getText().toString();
int parsed = numberFormat.parse(inView).intValue();
if (parsed == value) {
return view.getText().toString();
}
} catch (ParseException e) {
// old number was broken
}
return numberFormat.format(value);
}
public static int stringToInt(TextView view, int oldValue, String value) {
NumberFormat numberFormat = getNumberFormat(view);
try {
return numberFormat.parse(value).intValue();
} catch (ParseException e) {
view.setError("Improper number format");
return oldValue;
}
}
private static NumberFormat getNumberFormat(View view) {
Resources resources= view.getResources();
Locale locale = resources.getConfiguration().locale;
NumberFormat format =
NumberFormat.getNumberInstance(locale);
if (format instanceof DecimalFormat) {
DecimalFormat decimalFormat = (DecimalFormat) format;
decimalFormat.setGroupingUsed(false);
}
return format;
}
and the layout has:
<EditText ...
android:id="#+id/etQuantityInput"
android:inputType="number"
android:text="#={Converter.intToString(etQuantityInput, cartItemVM.totalInCart, cartItemVM.totalInCart)}"/>
If you want a new QuantityEditText, I think it is best to have one attribute control it rather than both the string text and int quantity:
<QuantityEditText ...
android:id="#+id/etQuantityInput"
android:inputType="number"
app:quantity="#={cartItemVM.totalInCart)}"/>
Here is the class that I put together so that quantity follows the text:
public class QuantityEditText extends android.support.v7.widget.AppCompatEditText {
public QuantityEditText(Context context) {
super(context);
initTextWatcher();
}
public QuantityEditText(Context context, AttributeSet attrs) {
super(context, attrs);
initTextWatcher();
}
public QuantityEditText(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
initTextWatcher();
}
private int quantity;
private OnQuantityChangeListener onQuantityChangeListener;
void initTextWatcher() {
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) {
updateQuantityFromText();
}
});
}
public void setQuantity(int quantity) {
if (updateQuantity(quantity)) {
setText(getNumberFormat().format(quantity));
}
}
public int getQuantity() {
return quantity;
}
private boolean updateQuantity(int newQuantity) {
if (this.quantity == newQuantity) {
return false; // nothing to do
}
this.quantity = newQuantity;
if (onQuantityChangeListener != null) {
onQuantityChangeListener.onQuantityChange(this, quantity);
}
return true;
}
void updateQuantityFromText() {
try {
String inView = getText().toString();
updateQuantity(getNumberFormat().parse(inView).intValue());
} catch (ParseException e) {
// Problem with the string format, so just don't update the quantity
}
}
private NumberFormat getNumberFormat() {
Resources resources = getResources();
Locale locale = resources.getConfiguration().locale;
NumberFormat format =
NumberFormat.getNumberInstance(locale);
if (format instanceof DecimalFormat) {
DecimalFormat decimalFormat = (DecimalFormat) format;
decimalFormat.setGroupingUsed(false);
}
return format;
}
public void setOnQuantityChangeListener(OnQuantityChangeListener listener) {
onQuantityChangeListener = listener;
}
public interface OnQuantityChangeListener {
void onQuantityChange(QuantityEditText view, int quantity);
}
}
You also need a way to set the InverseBindingListener and setup two-way data binding for quantity:
#InverseBindingMethods(
#InverseBindingMethod(type = QuantityEditText.class, attribute = "quantity")
)
public class BindingAdapters {
#BindingAdapter("quantityAttrChanged")
public static void setQuantityAttrChanged(QuantityEditText view,
final InverseBindingListener quantityChange) {
QuantityEditText.OnQuantityChangeListener listener = null;
if (quantityChange != null) {
listener = new QuantityEditText.OnQuantityChangeListener() {
#Override
public void onQuantityChange(QuantityEditText view, int quantity) {
quantityChange.onChange();
}
};
}
view.setOnQuantityChangeListener(listener);
}
}
I'm trying to create a Spinner like custom view with list of values. I've managed to get it started with the following code below.
public class SelectionTextView extends TextInputEditText implements View.OnClickListener {
private CharSequence[] entries, values;
private CharSequence value;
#Override
public void onClick(View v) {
new AlertDialog.Builder(v.getContext())
.setTitle("Title")
.setItems(entries, new DialogInterface.OnClickListener() {
#Override
public void onClick(DialogInterface dialog, int which) {
value = values[which];
SelectionTextView.super.setText(entries[which]);
}
})
.create()
.show();
}
public SelectionTextView(final Context context, #Nullable AttributeSet attrs) {
super(context, attrs);
super.setCursorVisible(false);
super.setFocusable(false);
super.setFocusableInTouchMode(false);
super.setInputType(InputType.TYPE_NULL);
super.setOnClickListener(this);
}
public void setEntries(CharSequence[] entries) {
this.entries = entries;
super.setOnClickListener(this);
}
public void setValues(CharSequence[] values) {
this.values = values;
}
public void setValue(Object value) {
this.value = value.toString();
}
public CharSequence getValue() {
return value;
}
}
However, I would like to implement something like onValueChanged, onEntryChanged. How would I go about doing this? Also how can I make the value attribute bindable through Android Data Binding.
Appreciate any help.
UPDATE: 03/13/2018
Posting my complete and working SelectionTextView.class.
package com.mycompany.myproject;
import android.content.Context;
import android.content.DialogInterface;
import android.content.res.TypedArray;
import android.support.annotation.Nullable;
import android.support.design.widget.TextInputEditText;
import android.support.v7.app.AlertDialog;
import android.text.InputType;
import android.util.AttributeSet;
import android.view.View;
public class SelectionTextView extends TextInputEditText implements View.OnClickListener {
private CharSequence[] entries, values;
private String value;
private String prompt;
private OnValueChangedListener listener;
#Override
public void onClick(View v) {
new AlertDialog.Builder(v.getContext())
.setTitle(prompt)
.setItems(entries, new DialogInterface.OnClickListener() {
#Override
public void onClick(DialogInterface dialog, int which) {
setValue(values[which].toString());
}
})
.create()
.show();
}
public SelectionTextView(final Context context, #Nullable AttributeSet attrs) {
super(context, attrs);
super.setCursorVisible(false);
super.setFocusable(false);
super.setFocusableInTouchMode(false);
super.setInputType(InputType.TYPE_NULL);
super.setOnClickListener(this);
TypedArray typedArray = context.getTheme().obtainStyledAttributes(attrs, R.styleable.SelectionTextView, 0, 0);
try {
entries = typedArray.getTextArray(R.styleable.SelectionTextView_entries);
values = typedArray.getTextArray(R.styleable.SelectionTextView_values);
value = typedArray.getString(R.styleable.SelectionTextView_value);
prompt = typedArray.getString(R.styleable.SelectionTextView_prompt);
} finally {
typedArray.recycle();
}
}
public void setOnValueChangeListener(OnValueChangedListener listener) {
setValue(this.value);
}
public void setEntries(CharSequence[] entries) {
this.entries = entries;
invalidate();
}
public void setValues(CharSequence[] values) {
this.values = values;
invalidate();
}
public void setValue(String value) {
this.value = value;
if (value != null) {
if (entries != null && values != null) {
for (int i = 0; i < entries.length; i++) {
if (values[i].toString().equals(value)) {
super.setText(entries[i].toString());
invalidate();
break;
}
}
}
}
}
public void setValue(Integer value) {
if (value != null) {
setValue(value.toString());
}
}
public String getValue() {
return value;
}
public interface OnValueChangedListener {
void onValueChange(SelectionTextView view, String value);
}
}
Then in my actual project, I just create a class with all the necessary binding.
package com.mycompany.myproject;
import android.databinding.BaseObservable;
import android.databinding.BindingAdapter;
import android.databinding.InverseBindingAdapter;
import android.databinding.InverseBindingListener;
import android.databinding.InverseBindingMethod;
import android.databinding.InverseBindingMethods;
import android.widget.TextView;
import com.mycompany.views.SelectionTextView;
#InverseBindingMethods({
#InverseBindingMethod(type = SelectionTextView.class, attribute = "value"),
})
public class BindingManager extends BaseObservable {
#BindingAdapter("android:text")
public static void setText(TextView view, Integer value) {
if (value != null) {
view.setText(Integer.toString(value));
}
}
#InverseBindingAdapter(attribute = "android:text")
public static Integer getText(TextView view) {
if (view.getText().length() == 0)
return null;
else
return Integer.parseInt(view.getText().toString());
}
#BindingAdapter(value = {"onValueChange", "valueAttrChanged"}, requireAll = false)
public static void setValueChangedListener(SelectionTextView view,
final SelectionTextView.OnValueChangedListener listener,
final InverseBindingListener valueChange) {
if (valueChange == null) {
view.setOnValueChangeListener(listener);
} else {
view.setOnValueChangeListener(new SelectionTextView.OnValueChangedListener() {
#Override
public void onValueChange(SelectionTextView view, String value) {
if (listener != null) {
listener.onValueChange(view, value);
}
valueChange.onChange();
}
});
}
}
#InverseBindingAdapter(attribute = "value")
public static Integer getValue(SelectionTextView view) {
if (view.getValue() == null) {
return null;
} else {
return Integer.parseInt(view.getValue());
}
}
#BindingAdapter("value")
public static void setValue(SelectionTextView view, Integer value) {
if (value != null) {
view.setValue(value);
} else {
view.setValue("");
}
}
#BindingAdapter("value")
public static void setValue(SelectionTextView view, String value) {
if (value != null) {
view.setValue(value);
} else {
view.setValue("");
}
}
}
In your AttributeSet constructor, add this:
TypedArray a = context.getTheme().obtainStyledAttributes(
attrs,
R.styleable.SelectionTextView, 0, 0);
value = a.getString(R.styleable.SelectionTextView_value, "");
In your res\values folder, add an attrs.xml that contains:
<resources>
<declare-styleable name="SelectionTextView">
<attr name="value" format="string" />
</declare-styleable>
</resources>
For onValueChanged, just define an interface that has the onValueChanged method and a "register" method with the interface as parameter and store that in a data member. Then, in the setValue method, call that interface.
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);
}
}
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.
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()
}
}
you need add one more function
#BindingAdapter("app:textAttrChanged")
fun ErrorInputLayout.bindTextAttrChanged(listener: InverseBindingListener) {
}
I've been using 2-way databinding for a basic application, it was going pretty well, until i start with custom views and attrs.
I want to create a custom view, with has a TextView and a EditText, and use it inside another layout:
<TextView
android:text="Holder"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:id="#+id/tvTitle"
android:layout_weight="1" />
<EditText
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:inputType="none"
android:text="Name"
android:ems="10"
android:id="#+id/etAnwser"
android:layout_weight="1" />
And i have the custom attr for it
<resources>
<declare-styleable name="form_item">
<attr name="tvTitle" format="string" />
<attr name="anwserHint" format="string" />
<attr name="anwserText" format="string" />
<attr name="android:enabled" />
</declare-styleable>
In the fragment i do the following:
<rhcloud.com.financialcontrol.tabutil.FormItem
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:enabled="#{state.get()}"
form_item:anwserText='#={expense.description}'
form_item:tvTitle="Description:" />
It works nice has 1-way databind, but whatever i change the text, he don't send me the callback in class
#InverseBindingMethods(value = {
#InverseBindingMethod(type = FormItem.class, attribute = "anwserText"),
})
public class FormItem extends LinearLayout {
private TextView tvTitle;
private EditText etAnwser;
public FormItem(#NonNull Context context) {
super(context);
LayoutInflater inflater = (LayoutInflater) getContext().getSystemService(Context.LAYOUT_INFLATER_SERVICE);
inflater.inflate(R.layout.form_item, this);
tvTitle = (TextView) findViewById(R.id.tvTitle);
etAnwser = (EditText) findViewById(R.id.etAnwser);
}
public FormItem(#NonNull Context context, #NonNull String title) {
this(context);
setTvTitle(title);
}
public FormItem(#NonNull Context context, #NonNull String title, #NonNull String hint) {
this(context, title);
setAnwserHint(hint);
}
public FormItem(#NonNull Context context, #NonNull String title, #NonNull String hint, #NonNull String anwserText) {
this(context, title, hint);
setAnwserHint(anwserText);
}
public FormItem(#NonNull Context context, #NonNull AttributeSet attrs) {
super(context, attrs);
LayoutInflater inflater = (LayoutInflater) getContext().getSystemService(Context.LAYOUT_INFLATER_SERVICE);
inflater.inflate(R.layout.form_item, this);
tvTitle = (TextView) findViewById(R.id.tvTitle);
etAnwser = (EditText) findViewById(R.id.etAnwser);
TypedArray a = context.getTheme().obtainStyledAttributes(
attrs,
R.styleable.form_item,
0, 0);
try {
setTvTitle(a.getString(R.styleable.form_item_tvTitle));
setAnwserHint(a.getString(R.styleable.form_item_anwserHint));
setAnwserText(a.getString(R.styleable.form_item_anwserText));
String isEnabled = a.getString(R.styleable.form_item_android_enabled);
if (isEnabled != null) {
setEnable(Boolean.parseBoolean(isEnabled));
}
} finally {
a.recycle();
}
}
public void setTvTitle(String title) {
tvTitle.setText(title);
}
public String getTvTitle() {
return tvTitle.getText().toString();
}
public void setAnwserHint(String hint) {
etAnwser.setHint(hint);
}
public String getAnwserHint() {
return etAnwser.getHint().toString();
}
public void setEnable(boolean isEnable) {
tvTitle.setEnabled(isEnable);
etAnwser.setEnabled(isEnable);
}
public void setAnwserText(String anwserText) {
etAnwser.setText(anwserText);
}
public String getAnwserText() {
return etAnwser.getText().toString();
}
#InverseBindingAdapter(attribute = "form_item:anwserText")
public static String setOnAnwserTextAttrChanged(final String value){
Log.d("Test","Calling InverseBindingAdapter: " + value);
return value;
}
#BindingAdapter(value = {"anwserTextAttrChanged"},
requireAll = false)
public static void setOnAnwserTextAttrChanged(final FormItem view,final InverseBindingListener anwserTextAttrChanged){
Log.d("Test","Calling BindingAdapter: " + view.getAnwserText());
if(anwserTextAttrChanged == null){
}else{
Log.d("Test","Calling here");
anwserTextAttrChanged.onChange();
}
}
#BindingAdapter(value = {"android:enabled"})
public static void customEnable(FormItem formItem, boolean isEnable) {
formItem.setEnable(isEnable);
}
}
Does anyone know how to make it work properly?
Fully code can be found at here
This works for me:
#InverseBindingMethods(value = {
#InverseBindingMethod(type = FilterPositionView.class, attribute = "bind:filterStringValue", method = "getFilterValue", event = "android:filterStringValuetAttrChanged")
})
public class FilterPositionView extends LinearLayout {
private FilterPositionBinding mBinding;
public FilterPositionView(Context context) {
super(context);
init(context);
}
public FilterPositionView(Context context, AttributeSet attrs) {
super(context, attrs);
init(context);
}
public FilterPositionView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init(context);
}
public FilterPositionView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
init(context);
}
private void init(Context context) {
mBinding = DataBindingUtil.inflate(LayoutInflater.from(context), R.layout.filter_position, this, true);
setOrientation(HORIZONTAL);
mBinding.filterPositionCheck.setOnCheckedChangeListener((buttonView, isChecked) -> {
mBinding.filterPositionValue.setEnabled(isChecked);
if (!isChecked) mBinding.filterPositionValue.setText("");
});
}
/**
* Zwraca wpisywany text
*
* #return wpisane litery tekstu
*/
public String getFilterValue() {
return mBinding.filterPositionValue.getText().toString();
}
#BindingAdapter(value = {"bind:filterTitle", "bind:filterStringValue", "bind:filterDateValue"}, requireAll = false)
public static void setFilterBinding(FilterPositionView positionView, String filterTitle,
String filterStringValue, Long filterDateValue) {
positionView.mBinding.filterPositionTitle.setText(filterTitle);
if (filterStringValue != null)
positionView.mBinding.filterPositionValue.setText(filterStringValue);
if (filterDateValue != null)
positionView.mBinding.filterPositionValue.setText(DateTimeFormatUtil.format(filterDateValue));
}
#BindingAdapter(value = {"android:afterTextChanged", "android:filterStringValuetAttrChanged"}, requireAll = false)
public static void setTextWatcher(FilterPositionView filterPositionView, final TextViewBindingAdapter.AfterTextChanged after,
final InverseBindingListener textAttrChanged) {
TextWatcher newValue = 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) {
if (after != null) {
after.afterTextChanged(s);
}
if (textAttrChanged != null) {
textAttrChanged.onChange();
}
}
};
TextWatcher oldValue = ListenerUtil.trackListener(filterPositionView.mBinding.filterPositionValue, newValue, R.id.textWatcher);
if (oldValue != null) {
filterPositionView.mBinding.filterPositionValue.removeTextChangedListener(oldValue);
}
filterPositionView.mBinding.filterPositionValue.addTextChangedListener(newValue);
}
}
Of course You have to add #={} in your XML layouts like below:
<com.example.customviews.FilterPositionView
style="#style/verticalLabeledValueStyle"
android:layout_width="match_parent"
android:layout_height="wrap_content"
bind:filterTitle="#{#string/filter_product}"
bind:filterStringValue="#={sfmodel.product}"/>