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) {
}
Related
I have one linear layout in that I have two views for an email address, one is Textview and another one is Edittext.
When there is empty or not proper email address then Textview's color should change to red,
if the user gets focus on edit text Textview's color should change to blue.
if the user lost focus on EditText then TextView's color should change to black.
To check how to achieve with customview, I have created a custom view of its parent view which is LinearLayout which is below
CustomLinearLayout.java file
public class CustomLinearLayout extends LinearLayout {
int editTextResourceId, textViewResourceId;
EditText editText;
TextView textView;
Context mContext;
public CustomLinearLayout(Context context) {
super(context);
mContext = context;
}
public CustomLinearLayout(Context context, AttributeSet attrs) {
super(context, attrs);
}
public CustomLinearLayout(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
mContext = context;
inflate(context, R.layout.activity_register_account_new, this);
TypedArray a = context.getTheme().obtainStyledAttributes(attrs, R.styleable.MyTextView, defStyleAttr, 0);
editTextResourceId = a.getResourceId(R.styleable.MyTextView_supportedEditText, NO_ID);
textViewResourceId = a.getResourceId(R.styleable.MyTextView_supportedTextView, NO_ID);
a.recycle();
}
#Override
public void onAttachedToWindow() {
super.onAttachedToWindow();
if (editTextResourceId != 0 && textViewResourceId != 0) {
editText = (EditText)findViewById(editTextResourceId);
editText.addTextChangedListener(new TextWatcher() {
#Override
public void afterTextChanged(Editable s) {}
#Override
public void beforeTextChanged(CharSequence s, int start,
int count, int after) {
}
#Override
public void onTextChanged(CharSequence s, int start,
int before, int count) {
if(s.length() != 0){
textView.setText(s);
}
}
});
}
}
}
activity_main.xml file looks like
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:custom="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#color/grey"
android:orientation="vertical">
<mobile.android.view.CustomLinearLayout
style="#style/RegisterItem"
android:layout_width="match_parent"
android:layout_height="wrap_content"
custom:supportedEditText="#id/edUserName"
custom:supportedTextView="#id/tvUserName"
android:orientation="horizontal">
<TextView
android:id="#+id/tvUserName"
style="#style/RegisterTextView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="#string/username" />
<EditText
android:id="#+id/edUserName"
style="#style/RegisterEditText"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:hint="#string/hint_username"
android:inputType="textPersonName" />
</mobile.android.view.CustomLinearLayout>
attris.xml file
<declare-styleable name="MyTextView">
<attr name="supportedEditText" />
<attr name="supportedTextView" />
</declare-styleable>
But whenever I debug code, it shows me 0 (resource id) instead of proper id or NO_ID.
Can anyone help me how to resolve this? thanks,
Try this code inside CustomLinearLayout.java :
private EditText findEditText() {
int i = 0;
for (; i < getChildCount(); i++) {
if (getChildAt(i) instanceof EditText) {
return (EditText) getChildAt(i);
}
}
return null;
}
private TextView findTextView() {
int i = 0;
for (; i < getChildCount(); i++) {
if (getChildAt(i) instanceof TextView) {
return (TextView) getChildAt(i);
}
}
return null;
}
#Override
public void onAttachedToWindow() {
super.onAttachedToWindow();
TextView textView = findTextView();
EditText editText = findEditText();
if (editText != null) {
editText.addTextChangedListener(new TextWatcher() {
#Override
public void afterTextChanged(Editable s) {
}
#Override
public void beforeTextChanged(CharSequence s, int start,
int count, int after) {
}
#Override
public void onTextChanged(CharSequence s, int start,
int before, int count) {
if (s.length() != 0) {
if (textView != null) {
textView.setText(s);
}
}
}
});
}
}
I built one thing like you want and I call it : EditTextComponent
This is java source code for EditTextComponent:
public class EdittextComponent extends InputComponent {
protected TextView tvTitle;
protected EditText edtInput;
protected int mInputType = InputType.TYPE_CLASS_TEXT;
protected int mMaxLine = 1;
protected InputFilter[] mInputFilter;
protected boolean hasFirstChange = false;
protected TextView tvWarning;
protected int mIDLayout = R.layout.component_edittext;
protected boolean isAlwaysShowWarning = false;
#Override
public View createView() {
LayoutInflater inflater = LayoutInflater.from(mContext);
rootView = inflater.inflate(mIDLayout, null, false);
tvTitle = rootView.findViewById(R.id.tv_title);
if (Utils.validateString(mTitle)) {
tvTitle.setText(mTitle);
}
edtInput = rootView.findViewById(R.id.edt_input);
if (Utils.validateString(mPlaceHolder)) {
edtInput.setHint(mPlaceHolder);
}
edtInput.setInputType(mInputType);
edtInput.setMaxLines(mMaxLine);
if (null != mInputFilter && mInputFilter.length > 0) {
edtInput.setFilters(mInputFilter);
}
if (Utils.validateString(mValue)) {
edtInput.setText(mValue);
}
if(null != mValueSelected){
isCompleted = true;
String value = (String) mValueSelected;
if(Utils.validateString(value)) {
edtInput.setText(value);
}
}
edtInput.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) {
if (null != s) {
hasFirstChange = true;
checkChange(s.toString().trim());
}
}
});
tvWarning = rootView.findViewById(R.id.tv_warning);
return rootView;
}
protected void checkChange(String source) {
if (null != mValidator) {
boolean newState = mValidator.validate(source);
if (isCompleted != newState) {
notifyStateChanged(newState, source);
}
} else {
if (source.length() == 0) {
if (isCompleted) {
notifyStateChanged(false, source);
}
} else if (!isCompleted) {
notifyStateChanged(true, source);
}
}
if(isCompleted){
edtInput.setBackgroundResource(R.drawable.bg_input);
tvWarning.setVisibility(View.INVISIBLE);
}
else{
showWarning(true);
}
}
protected void notifyStateChanged(boolean is_complete, String source) {
isCompleted = is_complete;
if (null != mCallBack) {
mCallBack.onChanged(isCompleted, source);
}
}
public void updateValue(String value){
mValue = value;
if(null != edtInput){
edtInput.setText(value);
}
checkChange(value);
}
#Override
public boolean validate() {
if(!isVisible){
return true;
}
if (!isCompleted ) {
showWarning(false);
}
return isCompleted;
}
protected void showWarning(boolean isCallByItSelf) {
String message = "Cannot be empty";
if (null != mValidator) {
message = mValidator.getMessage();
}
if(null != tvWarning) {
tvWarning.setText(message);
tvWarning.setVisibility(View.VISIBLE);
}
if(null != edtInput) {
edtInput.setBackgroundResource(R.drawable.bg_input_warning);
}
}
#Override
public String getValue() {
mValue = edtInput.getText().toString();
return mValue;
}
#Override
public Object getData() {
return getValue();
}
public void setInputType(int inputType) {
mInputType = inputType;
}
public void setMaxLine(int maxLine) {
mMaxLine = maxLine;
}
public void setInputFilter(InputFilter[] inputFilter) {
mInputFilter = inputFilter;
}
public void setIDLayout(int IDLayout) {
mIDLayout = IDLayout;
}
public void setAlwaysShowWarning(boolean alwaysShowWarning) {
isAlwaysShowWarning = alwaysShowWarning;
}}
And here is component_edittext.xml
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView
android:id="#+id/tv_title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentTop="true"
android:layout_marginStart="10dp"
android:layout_marginTop="5dp"
android:layout_marginEnd="10dp"
android:text="Last name"
android:textColor="#333333"
android:textSize="14sp"/>
<EditText
android:id="#+id/edt_input"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="#id/tv_title"
android:layout_marginStart="10dp"
android:layout_marginTop="9dp"
android:layout_marginEnd="10dp"
android:background="#drawable/bg_input_inactive"
android:paddingStart="19dp"
android:paddingTop="16dp"
android:paddingBottom="15dp"
android:textColor="#333333"
android:textSize="14sp"/>
<TextView
android:id="#+id/tv_warning"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="#+id/edt_input"
android:layout_alignParentEnd="true"
android:layout_marginStart="10dp"
android:layout_marginTop="5dp"
android:layout_marginEnd="10dp"
android:text="Cannot be empty"
android:textColor="#DD4033"
android:textSize="12sp"
android:visibility="invisible"/>
</RelativeLayout>
InputComponent.java
public class InputComponent extends BaseComponent {
protected String mTitle;
protected String mValue;
protected String mKey;
protected String mPlaceHolder;
protected Validator mValidator;
protected boolean isCompleted = false;
protected InputCallBack mCallBack;
protected float mWeight = 1;
public boolean validate(){
return false;
}
public String getTitle() {
return mTitle;
}
public void setTitle(String title) {
mTitle = title;
}
public String getValue() {
return mValue;
}
public void setValue(String value) {
mValue = value;
}
public String getKey() {
return mKey;
}
public void setKey(String key) {
mKey = key;
}
public void setCallBack(InputCallBack callBack) {
mCallBack = callBack;
}
public void setPlaceHolder(String placeHolder) {
mPlaceHolder = placeHolder;
}
public void setValidator(Validator validator) {
mValidator = validator;
}
public float getWeight() {
return mWeight;
}
public void setWeight(float weight) {
mWeight = weight;
}
}
BaseComponet.java
public class BaseComponent implements Comparable<BaseComponent>{
protected View rootView;
protected String mIDComponent;
protected Context mContext;
protected Object mValueSelected;
protected int mPosition;
protected boolean isVisible = true;
public BaseComponent() {
mContext = Manager.getInstance().getCurrentActivity();
}
public View createView() {
return rootView;
}
public Object getValueSelected() {
return mValueSelected;
}
public void setValueSelected(Object valueSelected) {
mValueSelected = valueSelected;
}
public String getIDComponent() {
return mIDComponent;
}
public void setIDComponent(String IDComponent) {
mIDComponent = IDComponent;
}
public void setRootView(View rootView) {
this.rootView = rootView;
}
public int getPosition() {
return mPosition;
}
public void setPosition(int position) {
mPosition = position;
}
public Object getData(){
return null;
}
public void removeAllView(){
}
public boolean isVisible() {
return isVisible;
}
public void setVisible(boolean visible) {
isVisible = visible;
}
public void updateView(){
if(null != rootView){
if(isVisible){
rootView.setVisibility(View.VISIBLE);
}
else{
rootView.setVisibility(View.GONE);
}
}
}
public View getRootView() {
return rootView;
}
#Override
public int compareTo( BaseComponent otherComponent) {
if (null != otherComponent) {
return (otherComponent.getPosition() - getPosition());
}
return 0;
}
}
And this is how you can use EditTextComponent:
EdittextComponent lastNameComponent = new EdittextComponent();
lastNameComponent.setPlaceHolder("Enter last name");
lastNameComponent.setTitle("Last name");
lastNameComponent.setKey("lastname");
lastNameComponent.setCallBack(new InputCallBack() {
#Override
public void onChanged(boolean isSelected, Object data) {
checkComplete();
}
});
listInputComponents.add(lastNameComponent);
You can use this component to create email, last name, phonecode .... and add them to listInputComponents ( an array)
Then you can add them to a linearlaylout like this:
LinearLayout llBody = findViewById(...)
for(BaseComponent component: listInputComponents)
{ llBody.addView(component.createView()
}
Friends , I have an edittext which is acting like a searchview.On text change,it is fetching data and storing in filterdNames but it is not updating the recyclerview.Kindly help, i am new to andriod.
Here is my recyclerview adapter code-
public class MyCategoryAdaptercheckbox extends RecyclerView.Adapter<MyCategoryAdaptercheckbox.ViewHolder> {
List<GetMyCategoryAdapter> getMyCategoryAdapter;
Context context;
List<String> category_name;
GetMyCategoryAdapter getMyCategoryAdapter1;
public MyCategoryAdaptercheckbox(List<GetMyCategoryAdapter> getMyCategoryAdapter, Context context, List<String> category_name) {
this.getMyCategoryAdapter = getMyCategoryAdapter;
this.context = context;
this.category_name = category_name;
}
#Override
public void onBindViewHolder(#NonNull ViewHolder viewHolder, int i) {
if (viewHolder instanceof ViewHolder){
}
getMyCategoryAdapter1 = getMyCategoryAdapter.get(i);
((ViewHolder) viewHolder).tv_categorytitle.setText(getMyCategoryAdapter1.getC_name());
((ViewHolder) viewHolder).tv_categoryid.setText(getMyCategoryAdapter1.getC_id());
((ViewHolder) viewHolder).gt= getMyCategoryAdapter1;
}
public void filterList(ArrayList<String> filterdNames) {
this.category_name = filterdNames;
notifyDataSetChanged();
}
}
Here is my GetMyCategoryAdapter class code -
public class GetMyCategoryAdapter {
String c_name,c_id;
public String getC_name() {
return c_name;
}
public void setC_name(String c_name) {
this.c_name = c_name;
}
public String getC_id() {
return c_id;
}
public void setC_id(String c_id) {
this.c_id = c_id;
}
}
And here is the fragment code -
searchView.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) {
//after the change calling the method and passing the search input
filter(editable.toString());
}
});
private void filter(String text) {
//new array list that will hold the filtered data
ArrayList<String> filterdNames = new ArrayList<>();
//looping through existing elements
for (String s : category_name) {
//if the existing elements contains the search input
if (s.toLowerCase().contains(text.toLowerCase())) {
//adding the element to filtered list
filterdNames.add(s);
}
}
//calling a method of the adapter class and passing the filtered list
((MyCategoryAdaptercheckbox) MyAdapter).filterList(filterdNames);
}
The problem is in filterList method:
you update category_name, but not getMyCategoryAdapter (which is used in onBindViewHolder). Try change getMyCategoryAdapter as well before notifyDataSetChanged
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 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();
}
Currently I use a RecyclerView to represent a dynamically configuration list form.
Every configuration item (entry at RecyclerView list) contains one EditText item.
To avoid wrong user input (some fields allow only integer, others only one digit after comma), I've implemented two different TextWatcher-filters which correct illegal input ("DecimalFilterDigitsAfterComma" and "DecimalFilterInteger").
My RecyclerView has 16 configuration items in total, but can only display maximum 8 at one time.
My problem is that the TextWatchers are assigned to specific Items (Integers and Decimal-Point TextEdit). But when I'm scrolling a bit, they change their order, so that Decimal- and Integer-Filters get swapped.
The TextWatcher items will be created inside the ConfigurationAdapter which is a RecyclerView.Adapter. I've event managed that the TextWatcher is only created once for each entry by using the mListConfigInit which is a boolean flag list for the items.
ConfigurationAdapter.java:
public class ConfigurationAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> {
/*
...
*/
private List<ConfigItem> mConfiguration = new ArrayList<>();
// make sure that DecimalFilter is only created once for each item
private List<Boolean> mListConfigInit = new ArrayList<>();
public ConfigurationAdapter() {
}
#Override
public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
View v = LayoutInflater.from(parent.getContext()).inflate(
R.layout.listitem_configuration,
parent,
false);
final ConfigurationViewHolder vh = new ConfigurationViewHolder(v);
/*
...
*/
return vh;
}
#Override
public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {
final ConfigurationViewHolder vh = (ConfigurationViewHolder) holder;
ConfigItem config = mConfiguration.get(position);
if(config.ShowValueAsFloat()) {
vh.SetTextWatcherType(ConfigurationViewHolder.TextWatcherType.type_FloatActive);
} else {
vh.SetTextWatcherType(ConfigurationViewHolder.TextWatcherType.type_IntActive);
}
// set name and unit
vh.mName.setText(config.mName);
vh.mUnit.setText(config.mUnit);
/*
...
*/
}
#Override
public int getItemCount() {
return mConfiguration.size();
}
public void addConfigItem(ConfigItem item) {
mConfiguration.add(item);
mListConfigInit.add(new Boolean(false));
notifyItemInserted(mConfiguration.size() - 1);
//notifyDataSetChanged();
}
/*
...
*/
}
ConfigurationViewHolder.java (changed according to pskink-comments):
public final class ConfigurationViewHolder extends RecyclerView.ViewHolder implements TextWatcher {
public TextView mName;
public CheckBox mCheckbox;
public SeekBar mSeekbar;
public EditText mValueEditText;
public TextView mUnit;
private List<TextWatcher> mListTextWatchers = new ArrayList<>();
public enum TextWatcherType {
type_FloatActive(0),
type_IntActive(1);
private int mValue;
TextWatcherType(int value) {
mValue = value;
}
int val() { return mValue; }
}
private TextWatcherType mTextWatcherType = TextWatcherType.type_FloatActive;
public ConfigurationViewHolder(View itemView) {
super(itemView);
mName = (TextView) itemView.findViewById(R.id.textView_configuration_name);
mValueEditText = (EditText) itemView.findViewById(R.id.editText_configuration_value);
mUnit = (TextView) itemView.findViewById(R.id.textView_configuration_unit);
mCheckbox = (CheckBox) itemView.findViewById(R.id.checkbox_configuration);
mSeekbar = (SeekBar) itemView.findViewById(R.id.seekBar_configuration);
mListTextWatchers.add(0, new DecimalFilterDigitsAfterComma(mValueEditText, 1));
mListTextWatchers.add(1, new DecimalFilterInteger(mValueEditText));
mValueEditText.addTextChangedListener(this);
}
public void SetTextWatcherType(TextWatcherType textWatcherType) {
mTextWatcherType = textWatcherType;
}
#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) {
mListTextWatchers.get(mTextWatcherType.val()).afterTextChanged(editable);
}
}
DecimalFilterInteger.java
public class DecimalFilterInteger implements TextWatcher {
private final static String TAG = ConfigurationAdapter.class.getSimpleName();
private final EditText mEditText;
private String mLastTextValue = new String("");
public DecimalFilterInteger(EditText editText) {
this.mEditText = editText;
}
#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 synchronized void afterTextChanged(final Editable text) {
String strInput = text.toString().trim();
if(strInput.isEmpty()) {
return;
}
if(strInput.equals(mLastTextValue)) { // return when same value as last time to avoid endless loop
return;
}
if ((strInput.charAt(0) == '.')) { // handle dot at beginning
strInput = "";
}
if(strInput.contains(".")){ // cut trailing comma
String numberBeforeDecimal = strInput.split("\\.")[0];
strInput = numberBeforeDecimal;
}
mEditText.removeTextChangedListener(this);
mEditText.getText().clear(); // do not use setText here to avoid changing the keyboard
mEditText.append(strInput); // back to default (e. g. from 123-mode to abc-mode),
// see: http://stackoverflow.com/questions/26365808/edittext-settext-changes-the-keyboard-type-to-default-from-123-to-abc
mLastTextValue = mEditText.getText().toString();
mEditText.setSelection(mEditText.getText().toString().trim().length());
mEditText.addTextChangedListener(this);
}
}
Many thanks in advance for your help!
The cause of the swap/switching behaviour of the two different TextWatcher-implementations inside the RecyclerView was that I called removeTextChangedListenerand addTextChangedListenerinside their afterTextChanged-methods to avoid retriggering of the afterTextChanged-method.
The best way to avoid retriggering is a simple check if the text changed since the last call:
public class DecimalFilterInteger implements TextWatcher {
private final static String TAG = ConfigurationAdapter.class.getSimpleName();
private final EditText mEditText;
private String mLastTextValue = new String("");
// ...
#Override
public synchronized void afterTextChanged(final Editable text) {
String strInput = text.toString().trim();
if(strInput.isEmpty()) {
return;
}
if(strInput.equals(mLastTextValue)) { // return when same value as last time to avoid endless loop
return;
}
if ((strInput.charAt(0) == '.')) { // handle dot at beginning
strInput = "";
}
if(strInput.contains(".")){ // cut trailing comma
String numberBeforeDecimal = strInput.split("\\.")[0];
strInput = numberBeforeDecimal;
}
//mEditText.removeTextChangedListener(this); // CAUSE OF SWAP-ERROR !!!
mEditText.getText().clear(); // do not use setText here to avoid changing the keyboard
mEditText.append(strInput); // back to default (e. g. from 123-mode to abc-mode),
// see: http://stackoverflow.com/questions/26365808/edittext-settext-changes-the-keyboard-type-to-default-from-123-to-abc
mLastTextValue = mEditText.getText().toString();
mEditText.setSelection(mEditText.getText().toString().trim().length());
//mEditText.addTextChangedListener(this); // CAUSE OF SWAP-ERROR !!!
}
}