Reputation: 2030
I am trying out the new Android Data Binding library (1.0-rc1) and I have made a User object with three String fields (name, email, and age) and linked them to 3 EditTexts in my layout.
On the first field (name) I placed a TextWatcher. Everything seems to work well. I prevented the notifyPropertyChanged loop in the name field by checking to see if the text is different before allowing it to call setName.
The problem is, every time I type in the name field, the cursor resets to the left of the EditText after each character. I googled around for a solution but most fix suggestions for a cursor issue say to grab a reference to the EditText and adjust the cursor position manually. But I'd like to avoid doing that since I then need to findViewByID to the EditText and the point of Data Binding was to try to avoid doing that. Thank you for your help.
My layout looks like this:
<layout>
<data>
<variable name="user" type="com.carlpoole.databindingstest.User"/>
</data>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:bind="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent"
android:layout_height="match_parent" android:paddingLeft="@dimen/activity_horizontal_margin"
android:paddingRight="@dimen/activity_horizontal_margin"
android:paddingTop="@dimen/activity_vertical_margin"
android:paddingBottom="@dimen/activity_vertical_margin" tools:context=".MainActivity">
<EditText
android:layout_width="200dp"
android:layout_height="wrap_content"
android:id="@+id/name"
android:text="@{user.name}"
bind:addTextChangedListener="@{user.nameChanged}"
/>
<EditText
android:layout_width="200dp"
android:layout_height="wrap_content"
android:id="@+id/email"
android:layout_below="@+id/name"
android:text="@{user.email}"/>
<EditText
android:layout_width="200dp"
android:layout_height="wrap_content"
android:id="@+id/age"
android:layout_below="@+id/email"
android:text="@{user.age}"/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@+id/age"
android:text="@{user.name}"/>
</RelativeLayout>
My user object looks like this:
import android.databinding.BaseObservable;
import android.databinding.Bindable;
import android.text.Editable;
import android.text.TextWatcher;
public class User extends BaseObservable {
private String name;
private String email;
private String age;
public User(String name, String email, String age) {
this.name = name;
this.email = email;
this.age = age;
}
public User(){};
@Bindable
public String getName() {
return name;
}
@Bindable
public String getEmail() {
return email;
}
@Bindable
public String getAge() {
return age;
}
public final TextWatcher nameChanged = new TextWatcher() {
@Override
public void afterTextChanged(Editable s) {
if(!s.toString().equalsIgnoreCase(name))
setName(s.toString());
}
@Override
public void beforeTextChanged(CharSequence s, int start, int count, int after) {}
@Override
public void onTextChanged(CharSequence s, int start, int before, int count) {}
};
public void setName(String name) {
this.name = name;
notifyPropertyChanged(com.carlpoole.databindingstest.BR.name);
}
public void setEmail(String email) {
this.email = email;
}
public void setAge(String age) {
this.age = age;
}
}
My activity looks like this
import android.databinding.DataBindingUtil;
import android.os.Bundle;
import android.support.v7.app.AppCompatActivity;
import com.carlpoole.databindingstest.databinding.ActivityMainBinding;
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
ActivityMainBinding binding = DataBindingUtil.setContentView(this, R.layout.activity_main);
User user = new User("Carl Poole", "[email protected]", "26");
binding.setUser(user);
}
}
Upvotes: 26
Views: 8185
Reputation: 2836
The solution is quite simple. You should save the selection before text is set to your EditText
and right after setting - set selection back:
binding.editName.apply {
val currentSelection = if (isFocused) selectionEnd else 0
setText(item.name)
setSelection(currentSelection)
}
Upvotes: 0
Reputation: 546
Other friends' answers is great but I still get a little bug when I do as these methods. When I edit the text in the middle of the EditText view, the cursor will go to the end, not at the edit place. I add a fix for that problem:
int selection = mEditText.getSelectionEnd();
int updateTextLength = text == null ? 0 : text.length();
mEditText.setText(text);
mEditText.setSelection(Math.min(selection, updateTextLength));
Upvotes: 0
Reputation: 168
We can do this without any new BindingAdapter
. The below is my EditText
in XML. And viewModel
is my DataBinding
variable. And I will set the cursor in android:onTextChanged
in XML itself
<androidx.appcompat.widget.AppCompatEditText
android:id="@+id/et_enter_pincode"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="number"
android:maxLength="6"
android:text="@={viewModel.observableEditText}"
android:textSize="16sp"
android:onTextChanged="@{(text, start, before, count) -> viewModel.onPincodeTextChanged(etEnterPincode)}"
android:hint="@string/enter_pincode"/>
Inside the View Model the code looks like this
val observableEditText = ObservableField<String>("")
fun onPincodeTextChanged(
view: EditText
){
view.setSelection(view.text.length)
}
Upvotes: 0
Reputation: 421
To fix the weird data binding behaviour that resets the cursor to the start of the EditText, you can add the following InverseBindingAdapter :
@SuppressLint("RestrictedApi")
@BindingAdapter("android:text")
public static void setText(EditText view, String oldText, String text) {
TextViewBindingAdapter.setText(view, text);
if (text == null) return;
if (text.equals(oldText) || oldText == null) {
view.setSelection(text.length());
}
}
Upvotes: 17
Reputation: 41
Try this:
@BindingAdapter("android:text")
fun setStringWIthSelection(view: EditText, str : String) {
view.setText(str)
view.setSelection(view.text.length)
}
Upvotes: 4
Reputation: 31
The problem is with setter confusion: your setter, as recommended by the DataBinding documentation, calls the notifyPropertyChanged method. But the notifyPropertyChanged method, among other things, resets the cursor which is whats causing your problem. Their is no need for the setter to update the UI when it was the UI (TextWatcher) that is calling the setter. The solution then is to have setters only call the notifyPropertyChanged method when some backend calculation/manipulation should cause the UI to be updated.
Upvotes: 3
Reputation: 8094
Your best bet here is to use a custom @BindingAdapter
which will already have a reference to the EditText. That way you can avoid re-binding if the text in the EditText
matches your model, which will resolve your cursor issue.
First, change android:text="@{user.name}"
to bind:binding="@{user.name}"
. Then, add this static method anywhere in your project. We keep all of them in a class called BindingAdapters.java
. By the way, starting in RC2 you can create non-static binding adapter methods, but that probably isn't your biggest concern right now.
@BindingAdapter("binding")
public static void bindEditText(EditText editText, CharSequence value) {
if (!editText.getText().toString().equals(value.toString())) {
editText.setText(value);
}
}
Upvotes: 13