3

I've got EditTexts in my rows in a ListView. When I tap on one of the EditTexts the soft keyboard appears and the focus jumps to the first EditText in the list instead of staying in the field where I tapped.

Here is a video of it:

https://youtu.be/ZwuFrX-WWBo

I created a completely stripped down app to demonstrate the problem. The full code is here: https://pastebin.com/YT8rxqKa

I'm not doing anything to alter the focus in my code:

@Override
public View getView(int position, View convertView, ViewGroup parent) {

    if (convertView == null) {
        convertView = layoutInflater.inflate(R.layout.cell_textfield, parent, false);
    }

    TextView label = (TextView) convertView.findViewById(R.id.textview1);
    EditText textfield = (EditText) convertView.findViewById(R.id.textview2);

    String text = String.format("Row %d", position);
    label.setText(text);
    textfield.setText(text);

    return convertView;
}

I found another post on StackOverflow giving a workaround for this dumb Android behavior, which involves putting an OnFocusChangedListener on all of the textfields so they can retake focus if it's taken from them improperly.

That worked to regain focus, but then I discovered that when a textfield retakes focus the cursor ends up at the start of the text instead of end, which is unnatural and annoying to my users.

Here is a video of that:

https://youtu.be/A35wLqbuIac

Here's the code for that OnFocusChangeListener. It works to fight the stupid Android behavior of moving focus, but the cursor is misplaced after it regains focus.

View.OnFocusChangeListener onFocusChangeListener = new View.OnFocusChangeListener() {
    @Override
    public void onFocusChange(View view, boolean hasFocus) {
        long t = System.currentTimeMillis();
        long delta = t - focusTime;
        if (hasFocus) {     // gained focus
            if (delta > minDeltaForReFocus) {
                focusTime = t;
                focusTarget = view;
            }
        }
        else {              // lost focus
            if (delta <= minDeltaForReFocus &&  view == focusTarget) {
                focusTarget.post(new Runnable() {   // reset focus to target
                    public void run() {
                        Log.d("BA", "requesting focus");
                        focusTarget.requestFocus();
                    }
                });
            }
        }
    }
};

I hate having to put a bandaid on a bandaid on a bandaid to try to get Android to just behave as it would naturally be expected to behave, but I'll take what I can get.

1) Is there something I can do to fix this problem at the source and not have to have the OnFocusChangeListener at all?

2) If (1) isn't possible, then how can I make sure that when I force focus back to the correct field that I make sure the cursor is placed at the end? I tried using setSelection() right after requestFocus() but since the textfield wasn't yet focused the selection is ignored.

Kenny Wyland
  • 20,844
  • 26
  • 117
  • 229
  • You got a good fix for this? I have the same issue now. – Sreeram Sunkara Sep 11 '17 at 19:55
  • @SreeramSunkara I don't have a -good- fix for it, but I did find something that worked. In short, when I needed a ListView with EditTexts in it, I would just use a basic ScrollView instead. It works well as long as you don't have too many rows. In fact, my adapter code worked well enough that I had entirely FORGOTTEN that I was cheating and that my layout didn't really have a ListView. I found this question again when working on a new project and had to dig through my old code to figure out how I solved it. I'll try to post an example answer below. – Kenny Wyland May 08 '18 at 20:27

2 Answers2

2

Here was my "solution." In short: ListViews are stupid and will always be a total nightmare when EditTexts are involved, so I changed my Fragment/Adapter code to be able to adapt to either a ListView layout or a ScrollView layout. It only works if you have a small number of rows, because the scrollview implementation isn't able to take advantage of lazy-loading and view recycling. Thankfully, any situation wherein I want EditTexts in a ListView, I rarely have more than 20 rows or so.

When inflating my view in my BaseListFragment, I get my layout id via a method that relies on a hasTextFields() method:

public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
    View view = inflater.inflate(getLayoutId(), container, false);
    return view;
}

public boolean hasTextfields() {
    return false;
}

public int getLayoutId() {
    if (hasTextfields()) {
        return R.layout.scrollfragment;
    } else {
        return R.layout.listfragment;
    }
}

In my various subclasses of my BaseListFragment, if I need to have an EditText in one of my fields, I just override the hasTextFields() method to return true and then my fragment/adapter switchs over to using the basic scrollview implementation.

From there, it's a matter of making sure that the Adapter handles the standard ListView actions for both the ListView and the ScrollView scenarios. Like this:

public void notifyDataSetChanged() {
    // If scrollContainer is not null, that means we're in a ScrollView setup
    if (this.scrollContainer != null) {
        // intentionally not calling super
        this.scrollContainer.removeAllViews();
        this.setupRows();
    } else {
        // use the real ListView
        super.notifyDataSetChanged();
    }
}

public void setupRows() {
    for (int i = 0; i < this.getCount(); i++) {
        View view = this.getView(i, null, this.scrollContainer);
        view.setOnClickListener(myItemClickListener);
        this.scrollContainer.addView(view);
    }
}

One issue that the click listener presented is that a ListView wants an AdapterView.OnItemClickListener, but arbitrary Views inside a ScrollView want a simple View.OnClickListener. So, I made my ItemClickListener also implement View.OnClickListener and then just dispatched the OnClick to the OnItemClick method:

public class MyItemClickListener implements AdapterView.OnItemClickListener, View.OnClickListener {

    @Override
    public void onClick(View v) {
        // You can either have your Adapter set the tag on the View to be its position
        // or you could have your click listener use v.getParent() and iterate through
        // the children to find the position. I find its faster and easier to have my
        // adapter set the Tag on the view.
        int position = v.getTag();
        this.onItemClick(null, v, config.getPosition(), 0);
    }

    @Override
    public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
        // ...
    }
}

Then in MyEditTextListFragment, I create the adapter like this:

listener = createClickListener();
adapter = createListAdapter();

if (scrollContainer != null) {
    adapter.setScrollContainer(scrollContainer);
    adapter.setMenuItemClickListener(listener);
    adapter.setupRows();

} else {
    getListView().setOnItemClickListener(listener);
    getListView().setAdapter(adapter);
}

Here is my scrollfragment.xml for reference:

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="#fff"
    android:clickable="true"
    >

    <!-- 
        The following LinearLayout as a focus catcher that won't cause the keyboard to 
        show without it, the virtual keyboard shows up immediately/always which means we 
        never get to the enjoy the full size of our screen while scrolling, and 
        that sucks.
    -->
    <LinearLayout
        android:focusable="true"
        android:focusableInTouchMode="true"
        android:layout_width="0px"
        android:layout_height="0px"/>

    <!--
        This ListView is still included in the layout but set to visibility=gone. List 
        fragments require a standard ListView in the layout, so this gets us past that
        check and allows us to use the same adapter code in both listview and scrollview
        situations.
    -->
    <ListView android:id="@id/android:list"
              android:layout_width="match_parent"
              android:layout_height="match_parent"
              android:layout_weight="1"
              android:drawSelectorOnTop="false"
              android:background="@null"
              android:layout_alignParentTop="true"
              android:descendantFocusability="afterDescendants"

              android:visibility="gone"
        />


    <!-- 
        This scrollview will act as our fake listview so that we don't have to deal with
        all the stupid crap that comes along with having EditTexts inside a ListView.
    -->
    <ScrollView
        android:id="@+id/scrollView"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:descendantFocusability="afterDescendants"
        >

        <LinearLayout
            android:id="@+id/scrollContainer"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:orientation="vertical"
            >

        </LinearLayout>
    </ScrollView>
</RelativeLayout>
Kenny Wyland
  • 20,844
  • 26
  • 117
  • 229
0

Try this once, it worked for me:

public void setCursorPosition() {
                focusTarget.requestFocus();
                focusTarget.setCursorVisible(true);
                other.setCursorVisible(false);
            } else {
                other.setCursorVisible(true);
                focusTarget.setCursorVisible(false);
            }
        }
Stuti Kasliwal
  • 771
  • 9
  • 20