Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Focus EditText from add item in RecycleView

I have got an app where I use a RecycleView with CardViews. The CardView contains an EditText now when I add a new CardView to the RecycleView the EditText should be focused and the keyboard should appear.

How can I achieve that? I have tried to add some code in the onBindViewHolder:

public void onBindViewHolder(TodoViewHolder holder, final int position) {
    ...
    if(holder.tvDescription.requestFocus()) {
        window.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_VISIBLE);
    }
    ...
}

Or while the creation of the ViewHolder but it hasn't worked.

public class TodoViewHolder extends RecyclerView.ViewHolder {
    protected CheckBox cbDone;
    protected EditText tvDescription;
    protected FloatingActionButton btnDelete;

    public TodoViewHolder(View itemView) {
        super(itemView);

        cbDone = (CheckBox)itemView.findViewById(R.id.cbDone);
        tvDescription = (EditText) itemView.findViewById(R.id.tvDescription);
        btnDelete = (FloatingActionButton) itemView.findViewById(R.id.btnDelete);

        if(tvDescription.requestFocus()) {
            window.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_VISIBLE);
        }
    }
}

Here is my AdapterCode with a solution:

public abstract class ArrayAdapter<T, VH extends RecyclerView.ViewHolder>
        extends RecyclerView.Adapter<VH> {

    private static final String TAG = "CustomArrayAdapter";

    private List<T> mObjects;

    public ArrayAdapter(final List<T> objects) {
        mObjects = objects;
    }

    /**
     * Adds the specified object at the end of the array.
     *
     * @param object The object to add at the end of the array.
     */
    public void add(final T object) {
        mObjects.add(object);
        notifyItemInserted(getItemCount() - 1);
    }

    /**
     * Remove all elements from the list.
     */
    public void clear() {
        final int size = getItemCount();
        mObjects.clear();
        notifyItemRangeRemoved(0, size);
    }

    @Override
    public int getItemCount() {
        return mObjects.size();
    }

    public T getItem(final int position) {
        return mObjects.get(position);
    }

    public long getItemId(final int position) {
        return position;
    }

    public List<T> getItems() {
        return mObjects;
    }

    /**
     * Returns the position of the specified item in the array.
     *
     * @param item The item to retrieve the position of.
     * @return The position of the specified item.
     */
    public int getPosition(final T item) {
        return mObjects.indexOf(item);
    }

    /**
     * Inserts the specified object at the specified index in the array.
     *
     * @param object The object to insert into the array.
     * @param index  The index at which the object must be inserted.
     */
    public void insert(final T object, int index) {
        mObjects.add(index, object);
        notifyItemInserted(index);

    }

    /**
     * Removes the specified object from the array.
     *
     * @param object The object to remove.
     */
    public void remove(T object) {
        final int position = getPosition(object);
        remove(position);
    }

    public void remove(int position) {
        if (position < 0 || position >= mObjects.size()) {
            Log.e(TAG, "remove: index=" + position);
        } else {
            mObjects.remove(position);
            notifyItemRemoved(position);
        }
    }

    /**
     * Sorts the content of this adapter using the specified comparator.
     *
     * @param comparator The comparator used to sort the objects contained in this adapter.
     */
    public void sort(Comparator<? super T> comparator) {
        Collections.sort(mObjects, comparator);
        notifyItemRangeChanged(0, getItemCount());
    }
}

The implemented Adapter:

public class RecyclerViewAdapter extends ArrayAdapter<Todo, RecyclerViewAdapter.TodoViewHolder> {

    private static final String TAG = "RecyclerViewAdapter";
    private Todo selectedItem;
    private final Window window;

    public RecyclerViewAdapter(List<Todo> todos, Window window) {
        super(todos);
        this.window = window;
    }

    public Todo getSelectedItem() {
        return selectedItem;
    }

    public class TodoViewHolder extends RecyclerView.ViewHolder implements View.OnCreateContextMenuListener {
        protected CheckBox cbDone;
        protected EditText tvDescription;
        protected FloatingActionButton btnDelete;

        public TodoViewHolder(View itemView) {
            super(itemView);

            cbDone = (CheckBox)itemView.findViewById(R.id.cbDone);
            tvDescription = (EditText) itemView.findViewById(R.id.tvDescription);
            btnDelete = (FloatingActionButton) itemView.findViewById(R.id.btnDelete);

            itemView.setOnCreateContextMenuListener(this);
        }

        @Override
        public void onCreateContextMenu(ContextMenu menu, View v, ContextMenu.ContextMenuInfo menuInfo) {
            menu.setHeaderTitle("Send to:");
            menu.add(0, v.getId(), 0, "all");

            Log.d(TAG, "view id: " + v.getId());
        }
    }

    @Override
    public void add(Todo object) {
        object.shouldBeFocused = true;
        super.add(object);
    }

    @Override
    public TodoViewHolder onCreateViewHolder(ViewGroup viewGroup, int viewType) {
        View view = LayoutInflater.from(viewGroup.getContext()).inflate(R.layout.todo_layout, viewGroup, false);
        return new TodoViewHolder(view);
    }

    @Override
    public void onBindViewHolder(final TodoViewHolder holder, final int position) {
        final Todo todo = getItem(holder.getAdapterPosition());
        holder.cbDone.setChecked(todo.isChecked);
        holder.tvDescription.setText(todo.description);

        holder.tvDescription.addTextChangedListener(new TextWatcher() {
            @Override
            public void beforeTextChanged(CharSequence s, int start, int count, int after) {
                // Do nothing
            }

            @Override
            public void onTextChanged(CharSequence s, int start, int before, int count) {
                // Do nothing
            }

            @Override
            public void afterTextChanged(Editable s) {
                todo.description = s.toString();
            }
        });

        holder.cbDone.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() {
            @Override
            public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
                Log.i(TAG, "onCheckedChanged called: isDone=" + isChecked);
                todo.isChecked = isChecked;
            }
        });

        holder.btnDelete.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                Log.i(TAG, "onClick called: remove todo.");
                remove(todo);
            }
        });

        View.OnLongClickListener onClickListener = new View.OnLongClickListener() {
            @Override
            public boolean onLongClick(View v) {
                selectedItem = todo;
                return false;
            }
        };

        holder.cbDone.setOnLongClickListener(onClickListener);
        holder.tvDescription.setOnLongClickListener(onClickListener);
        holder.btnDelete.setOnLongClickListener(onClickListener);

        if (todo.shouldBeFocused) {
            holder.tvDescription.post(new Runnable() {
                @Override
                public void run() {
                    if (holder.tvDescription.requestFocus()) {
                        window.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_VISIBLE);
                        InputMethodManager inputMethodManager = (InputMethodManager) holder.tvDescription.getContext().getSystemService(Context.INPUT_METHOD_SERVICE);
                        inputMethodManager.showSoftInput(holder.tvDescription, InputMethodManager.SHOW_IMPLICIT);
                    }
                }
            });
            todo.shouldBeFocused = false;
        }
    }
}

The Todo:

public class Todo implements Serializable {

    // The creationDate is not used at the moment
    protected Date creationDate;
    protected String description;
    protected boolean isChecked;
    protected boolean shouldBeFocused;

    public Todo(String description) {
        this.description = description;
        this.creationDate = new Date();
    }

    public Date getCreationDate() {
        return creationDate;
    }

    public String getDescription() { return description; }

    @Override
    public String toString() {
        return creationDate + ": " + description + " state[" + isChecked + "]";
    }
}

And in the MainActivity the add method:

FloatingActionButton fab = (FloatingActionButton) findViewById(R.id.fab);
fab.setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View view) {
        adapter.add(new Todo(""));
        int count = adapter.getItemCount();
        recyclerView.smoothScrollToPosition(count - 1);
    }
});

Problem when testing some solution:

Correct behavior at the beginning Still correct behavior Here you can see, that some entries are duplicated in the list

like image 335
Kevin Wallis Avatar asked Jul 14 '16 13:07

Kevin Wallis


1 Answers

The problem is because you call requestFocus() too early, cause your view doesn't appear on the screen yet. Also you should add some flag, when you're adding a new element - should you request focus on this view or not, to prevent all previous views in RecyclerView be focused. Assuming you add a new CardView to the end of RecyclerView, so your add method of Adapter should be like this:

public void addToEnd(Model item) {
    item.shouldBeFocused = true;
    dataset.add(item);
    notifyItemInserted(dataset.size() - 1);
}

And then in your onBindViewHolder() do something like this:

Model item = dataset.get(position);
...
if (item.shouldBeFocused) {
    holder.tvDescription.post(new Runnable() {
        @Override
        public void run() {
            if (holder.tvDescription.requestFocus()) {
                window.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_VISIBLE);
                InputMethodManager inputMethodManager = (InputMethodManager) holder.tvDescription.getContext().getSystemService(Context.INPUT_METHOD_SERVICE);
                inputMethodManager.showSoftInput(holder.tvDescription, InputMethodManager.SHOW_IMPLICIT);
            }
        }
    });
    item.shouldBeFocused = false;
}
...

Also you would probably need to scroll to the last position of your RecyclerView to call your onBindViewHolder() for the new added element. You can do this for example by setStackFromEnd = true line.

UPDATE:

Your problem is that you're adding TextWatcher inside onBindViewHolder method, firstly it's very expensive operation, and secondly you're saving entered text to the final reference, that's why your RecyclerView gives inappropriate results after.

So, try to create your custom TextWatcher, that keeps position of current item in Adapter first:

private static class PositionTextWatcher implements TextWatcher {
    private int position;

    public void updatePosition(int position) {
        this.position = position;
    }

    @Override
    public void beforeTextChanged(CharSequence charSequence, int i, int i2, int i3) {
        // no op
    }

    @Override
    public void onTextChanged(CharSequence charSequence, int i, int i2, int i3) {
        final Todo todo = getItem(position);
        todo.description = charSequence.toString();
    }

    @Override
    public void afterTextChanged(Editable editable) {
        // no op
    }
}

Then add it to your EditText in ViewHolder's constructor, when onCreateViewHolder will be called:

@Override
public TodoViewHolder onCreateViewHolder(ViewGroup viewGroup, int viewType) {
    View view = LayoutInflater.from(viewGroup.getContext()).inflate(R.layout.todo_layout, viewGroup, false);
    return new TodoViewHolder(view, new PositionTextWatcher());
}

public class TodoViewHolder extends RecyclerView.ViewHolder implements View.OnCreateContextMenuListener {
    protected CheckBox cbDone;
    protected EditText tvDescription;
    protected FloatingActionButton btnDelete;
    protected PositionTextWatcher positionTextWatcher;

     public TodoViewHolder(View itemView, PositionTextWatcher positionTextWatcher) {
        super(itemView);

        cbDone = (CheckBox)itemView.findViewById(R.id.cbDone);
        tvDescription = (EditText) itemView.findViewById(R.id.tvDescription);
        btnDelete = (FloatingActionButton) itemView.findViewById(R.id.btnDelete);
        this.positionTextWatcher = positionTextWatcher;
        tvDescription.addTextChangedListener(this.positionTextWatcher);
        itemView.setOnCreateContextMenuListener(this);
    }

    @Override
    public void onCreateContextMenu(ContextMenu menu, View v, ContextMenu.ContextMenuInfo menuInfo) {
        menu.setHeaderTitle("Send to:");
        menu.add(0, v.getId(), 0, "all");

        Log.d(TAG, "view id: " + v.getId());
    }
}

And finally, instead of adding a new TextWatcher each time in onBindViewHolder(), just update the position in your custom TextWatcher:

@Override
public void onBindViewHolder(final TodoViewHolder holder, final int position) {
    final Todo todo = getItem(holder.getAdapterPosition());
    ...
    holder.cbDone.setChecked(todo.isChecked);
    holder.positionTextWatcher.updatePosition(position);
    holder.tvDescription.setText(todo.description);
    ...
}

This should work like a charm! Got that solution from this perfect answer, so check it for more background.

like image 124
romtsn Avatar answered Oct 07 '22 03:10

romtsn