Edittext cursor resetting to left after Android Data Binding update
Asked Answered
O

7

26

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);

    }
}
Outdoor answered 31/8, 2015 at 23:33 Comment(4)
"I prevented the notifyPropertyChanged loop in the name field by checking to see if the text is different before allowing it to call setName" -- however, if I were a betting man, I'd be that this is what is causing your cursor problems. Why do you have the TextWatcher in the first place?Osteomalacia
@Osteomalacia the TextWatcher listens for the update in the EditText so that it can fire the update to the name variable in the User object. Without it, it will not know. If I remove the setName call from the afterTextChanged method it seems to work fine so it seems the problem might be caused by how I'm using the Data Binding libraryOutdoor
"Without it, it will not know" -- update the model when the user tells you to update the model (clicks a "DONE" button, leaves the activity via HOME, etc.).Osteomalacia
@carlpoole This is a fantastic answer for me even though it is a question. Thanks so much for posting your entire solution. I was able to look at it and get my databinding working and I couldn't find any other example anywhere which was so clear. I searched books, SO, google, android docs. phew...I'll never get those hours back. SuperMegaUpvote for the clear and detailed question-answer. :)Frightful
A
13

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);
  }
}
Afterdeck answered 2/9, 2015 at 19:35 Comment(4)
The data binding guide still refers to RC1. Is RC2 out already or is it still in the works?Intrigant
RC2 isn't available yet. Coming soon to a support library near you.Afterdeck
I had to use a TextWatcher in order to have databinding along with a custom view, and found the same problem. Your solution worked perfectly for me. Thanks.Pigfish
I has similar problem, I solved it by checking if the value is same as the previous one, just like the code in the answer...Keith
L
17

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());
    }
  }
Lopeared answered 30/6, 2017 at 13:55 Comment(4)
This looks like a clean solutionDemilune
Thank you very much. This is great But if you type in middle of text, the position of cursor will go to rightmost position.Thingumabob
Great solution, thank you very much. It is kind of weird that it is overlooked with built-in binding adapterHeaviness
Make a fix for the middle edit problem. LINKAudit
A
13

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);
  }
}
Afterdeck answered 2/9, 2015 at 19:35 Comment(4)
The data binding guide still refers to RC1. Is RC2 out already or is it still in the works?Intrigant
RC2 isn't available yet. Coming soon to a support library near you.Afterdeck
I had to use a TextWatcher in order to have databinding along with a custom view, and found the same problem. Your solution worked perfectly for me. Thanks.Pigfish
I has similar problem, I solved it by checking if the value is same as the previous one, just like the code in the answer...Keith
T
3

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.

Torchier answered 22/10, 2015 at 0:24 Comment(0)
F
3

Try this:

@BindingAdapter("android:text")
fun setStringWIthSelection(view: EditText, str : String) {
    view.setText(str)
    view.setSelection(view.text.length)
}
Frumenty answered 6/3, 2019 at 9:4 Comment(0)
V
0

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)
}
Vagus answered 20/1, 2020 at 7:11 Comment(0)
A
0

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));
Audit answered 19/1, 2021 at 15:42 Comment(0)
R
0

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)
}
Reinsure answered 9/10, 2023 at 7:55 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.