ListView and EditText solutions

Keywords: Android Java less

The reuse of ListView has many pits for EditText, such as loss of focus, value skyrocketing, scrolling problems. In this paper, two schemes are adopted to solve the problem:

First, honestly use ListView, and then trample the pit flat.

1. Focus issues

The main problem is that when you click EditText, the keyboard pops up, but there is no response to the input. You need to click again to enter the data. The reason is that when the keyboard pops up, it triggers the refresh of ListView, which causes EditText to lose focus again. This pit I have stepped on 4.4 machine, 5.0 after the focus of the acquisition mechanism is not the same, in my project later changed to scheme 2 to achieve, so no careful study.

//Android 4.4 code 
private int touchPosition = -1
@Override
public View getView(final int position, View convertView, ViewGroup parent) {
        ViewHolder holder = ViewHolder.get(context, convertView, null, R.layout.layout_item, position);

        final Bean bean = getItem(position);
        ((TextView) holder.getView(R.id.tv_id)).setText(bean.id + "");
        EditText etName = holder.getView(R.id.et_name);
        etName.setOnTouchListener(new View.OnTouchListener() {
            @Override
            public boolean onTouch(View v, MotionEvent event) {
                touchPosition = position;
                return false;
            }
        });
        etName.setText(bean.name);
        if (touchPosition == position) {
            etName.requestFocus();
            etName.setSelection(etName.length());
        } else etName.clearFocus();
        return holder.getRootView();
    }

The idea is simple and crude, that is to add an onTouch monitor to the edit box, and reset the focus when the ListView refreshes, but the scheme fails on 5.0 machines and above.

package com.wastrel.edittext;

import android.content.Context;
import android.util.SparseArray;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;


/**
 * Create a viewHolder
 */
public class ViewHolder {

    public static final String TAG = "ViewHolder";

    private SparseArray<View> childViews;

    private View rootView;

    private int position;
    private Object tag;

    public void setTag(Object tag) {
        this.tag = tag;
    }

    public Object getTag() {
        return tag;
    }
    /**
     * ViewHolder that generates an adapter
     *
     * @param context
     * @param parent
     * @param layoutId
     * @param position
     */
    private ViewHolder(Context context, ViewGroup parent, int layoutId, int position) {
        this.rootView = LayoutInflater.from(context).inflate(layoutId, parent, false);
        this.childViews = new SparseArray<>();
        this.position = position;
        rootView.setTag(this);
    }

    /**
     * Get a viewHolder
     *
     * @param context
     * @param parent
     * @param layoutId
     * @param attachToRoot Whether to add to parent
     */
    public ViewHolder(Context context, ViewGroup parent, int layoutId, boolean attachToRoot) {
        this.rootView = LayoutInflater.from(context).inflate(layoutId, parent, attachToRoot);
        this.childViews = new SparseArray<>();
        rootView.setTag(this);
    }


    /**
     * Get a viewHolder
     *
     * @param context
     * @param convertView
     * @param parent
     * @param layoutId
     * @param position
     * @return
     */
    public static ViewHolder get(Context context, View convertView, ViewGroup parent, int layoutId, int position) {
        ViewHolder holder;
        if (null == convertView) {
            holder = new ViewHolder(context, parent, layoutId, position);
        } else {
            holder = (ViewHolder) convertView.getTag();
        }
        return holder;
    }

    /**
     * Get the root container
     *
     * @return
     */
    public View getRootView() {
        return this.rootView;
    }

    /**
     * Get a control in a container
     *
     * @param id
     * @param <T>
     * @return
     */
    @SuppressWarnings("unchecked")
    public <T extends View> T getView(int id) {

       View view =childViews.get(id);
        if (view!=null)
        {
            return (T)view;
        }
        view=rootView.findViewById(id);
        if (null == view) {
            throw new IllegalArgumentException("Can't find id by" + rootView.getContext().getResources().getResourceEntryName(id) + "Controls");
        } else {
            childViews.put(id, view);
            return (T) view;
        }
    }

}

2. Problem of Value Scrambling

Solving this problem is easy to associate with using TextWatcher. Add a TextWatcher to each EditText to update the value of the data source when the user enters it, and go back to set the next time the user refreshes it. In order to save space, only key codes are posted here.

 @Override
public View getView(final int position, View convertView, ViewGroup parent) {
        ViewHolder holder = ViewHolder.get(context, convertView, null, R.layout.layout_item, position);
        final Bean bean = getItem(position);
        ((TextView) holder.getView(R.id.tv_id)).setText(bean.id + "");
        EditText etName = holder.getView(R.id.et_name);
        //This code is mainly to ensure that EditText only holds one TextWatcher, because if you use add every time, EditText holds a lot of TextWatcher. Once the text changes, it will trigger all its TextWatchers, which will not solve the problem, but make the problem more serious.
        MyTextWatcher textWatcher = (MyTextWatcher) etName.getTag();
        if (textWatcher == null) {
            textWatcher = new MyTextWatcher();
            etName.addTextChangedListener(textWatcher);
            etName.setTag(textWatcher);
        }
        //Fixed the object currently bound by EditText.
        textWatcher.update(bean);
        etName.setText(bean.name);
        return holder.getRootView();
    }

class MyTextWatcher implements TextWatcher {

        private Bean bean;

        public void update(Bean bean) {
            this.bean = bean;
        }

        @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) {
            bean.name = s.toString();

        }
    }

2. Use ScrollVIew+LinearLayout instead of ListView

Why use LinearLayout instead of ListVIew?

The combination of ListView and EditText will frequently cause ListView refresh. In the process of popping up the keyboard, ListView will refresh 3-4 times. When the EditText edit box changes, it will also trigger refresh, which wastes performance. In addition, the various states of EditText will be lost in the refresh process, which is quite a headache. If you use Linear Layout, these problems will be solved.

But the problem is that Linear Layout doesn't have a ListView Adapter. Next we can simulate an Adapter for LinearLayout, which is easy to use. See code:

package com.wastrel.edittext;

import android.content.Context;
import android.view.View;
import android.view.ViewGroup;
import android.widget.LinearLayout;

import java.util.List;
import java.util.Stack;

/**
 * Basic adapter <br/>
 * All subclasses must implement {@link convert (ViewHolder, Object, int)} <br/>.
 */
public abstract class BaseLinearLayoutAdapter<T> extends android.widget.BaseAdapter {

    public List<T> data;

    public Context context;

    private int layoutId;

    Stack<View> detachViews = new Stack<>();

    public LinearLayout container;

    public BaseLinearLayoutAdapter(Context context, List<T> data, LinearLayout container, int layoutId) {
        this.context = context;
        this.data = data;
        this.container = container;
        container.removeAllViews();
        this.layoutId = layoutId;
    }

    @Override
    public int getCount() {
        return null == data ? 0 : data.size();
    }

    @Override
    public T getItem(int position) {
        return null == data ? null : data.get(position);
    }

    @Override
    public long getItemId(int position) {
        return 0;
    }

    public void setList(List<T> data) {
        this.data = data;
        notifyDataSetChanged();
    }

    @Override
    public View getView(int position, View convertView, ViewGroup parent) {
        ViewHolder holder = ViewHolder.get(context, convertView, parent, layoutId, position);
         T t =  getItem(position);
        convert(holder, t, position);
        return holder.getRootView();
    }

    /**
     * Implementing Data Assignment
     *
     * @param holder
     * @param item
     * @param position
     */
    public abstract void convert(ViewHolder holder, T item, int position);


   //Renotify DataSetChanged () to satisfy LinearLayout.
    @Override
    public void notifyDataSetChanged() {
        //Get how many views are available in the current container
        int viewCount = container.getChildCount();
        int size = getCount();
        for (int i = 0; i < size; i++) {
            if (i < viewCount) {
                //When the number of display is less than the number in the current container, just take it out and reassign it.
                getView(i, container.getChildAt(i), container);
            } else {
                //When it is larger, it is fetched from the cache first, and if it is not executed, getView(i,null,container) is created.
                View v = null;
                if (detachViews.size() > 0) {
                    v = detachViews.get(0);
                    detachViews.remove(0);
                }
                v = getView(i, v, container);
                container.addView(v);
            }
        }
        //Take out the View that is not used in the container and put it in the cache.
        if (viewCount > size) {
            for (int i = viewCount - 1; i >= size; i--) {
                detachViews.add(container.getChildAt(i));
                container.removeViewAt(i);
            }

        }
    }
}

The above Adapter simply rewrites notify DataSetChange () to simulate the ListView refresh process.

Use

The layout should be satisfied: If ScrollView is used, the direction of Linear Layout should be vertical, and if Horizontal ScrollView is used, the direction of Linear Layout should be horizontal.

<!--Vertical time-->
<ScrollView
    android:layout_width="match_parent"
    android:layout_height="wrap_content">
    <LinearLayout
        android:layout_width="match_parent"
        android:orientation="vertical"
        android:layout_height="match_parent"/>
</ScrollView>
public class LinearAdapter extends BaseLinearLayoutAdapter<Bean> {
    public LinearAdapter(Context context, List<Bean> data, LinearLayout container, int layoutId) {
        super(context, data, container, layoutId);
    }

    @Override
    public void convert(ViewHolder holder, Bean bean, int position) {
        ((TextView) holder.getView(R.id.tv_id)).setText(bean.id + "");
        EditText etName = holder.getView(R.id.et_name);
        MyTextWatcher textWatcher = (MyTextWatcher) etName.getTag();
        if (textWatcher == null) {
            textWatcher = new MyTextWatcher();
            etName.addTextChangedListener(textWatcher);
            etName.setTag(textWatcher);
        }
        textWatcher.update(bean);
        etName.setText(bean.name);

    }

    class MyTextWatcher implements TextWatcher {

        private Bean bean;

        public void update(Bean bean) {
            this.bean = bean;
        }

        @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) {
            bean.name = s.toString();

        }
    }
}
 LinearAdapter adapter = new LinearAdapter(this, beans, listView, R.layout.layout_item);
adapter.notifyDataSetChanged();

Call the notifyDataSetChanged() of the Adapter when the data set changes.

The above scheme is applicable to the case that Item entries are not many and users can add and delete entries dynamically. In the process of dynamic deletion, it is unavoidable to save data quickly through TextWatcher. However, there will be no focus problem and the list will not be refreshed repeatedly. If the entry is fixed, it would be better to use the for loop directly. Then, by caching the ViewHolder, you can solve most of the problems.

Posted by wizzard on Sat, 30 Mar 2019 17:51:28 -0700