ListView中使用EditText(解決EditText焦點丟失、保存數據以及滾動衝突的問題)

前幾天一同學項目中的某個功能需要ListView+EditText來實現,希望我給他寫個Demo,自己就隨手寫了一個小的Demo。後來想了想覺得這個功能其實挺常用的,而且期間也踩了幾個坑,就整理了一下決定寫成博客,希望能夠幫到大家。好了,廢話不多說了,接着就貼代碼。
一、編寫佈局文件
1.activity的佈局activity_main

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:paddingBottom="@dimen/activity_vertical_margin"
    android:paddingLeft="@dimen/activity_horizontal_margin"
    android:paddingRight="@dimen/activity_horizontal_margin"
    android:paddingTop="@dimen/activity_vertical_margin"
    tools:context="com.example.mytestdemo.MainActivity" >

    <ListView 
        android:id="@+id/list_view"
        android:layout_width="match_parent"
        android:layout_height="match_parent"/>

</RelativeLayout>

只有一個ListView,所以就不多說了。
2.item的佈局edittext_item

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/item_content"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:paddingTop="20dp"
    android:paddingBottom="20dp"
    android:orientation="horizontal" >
    <TextView 
        android:id="@+id/text_view"
        android:layout_width="wrap_content"
        android:layout_gravity="center_vertical"
        android:gravity="center_vertical"
        android:layout_height="50dp"/>
    <EditText 
        android:id="@+id/edit_text"
        android:layout_gravity="center_vertical"
        android:layout_marginLeft="30dp"
        android:layout_width="match_parent"
        android:layout_height="100dp"
        android:gravity="top"
        android:padding="5dp"
        android:background="@drawable/shape_edittext"/>

</LinearLayout>

一個水平的LinearLayout,裏面有一個TextView和一個EditText。
爲了稍微好看那麼一點,所以給EditText加了一個圓角矩形背景。
3.EditText的圓角矩形背景shape_edittext

<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android" >
    <solid android:color="#FFFFFFFF"/>
    <stroke android:width="1dp"
         android:color="#000000"/>
    <corners android:radius="5dp"/>
</shape>

OK,佈局代碼已經貼完了,接下來就看看咱們的邏輯代碼吧。
二、編寫MainActivity類

public class MainActivity extends Activity {
    private static final String TAG = "zbw";
    private static final int DATA_CAPACITY = 20;

    private ListView mListView;
    private List<String> mList = new ArrayList<String>(DATA_CAPACITY);
    private MyAdapter mAdapter;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        mListView = (ListView) findViewById(R.id.list_view);

        //填充數據
        for(int i = 0; i < DATA_CAPACITY; i++) {
            mList.add("" + i);
        }

        //設置Adapter
        mAdapter = new MyAdapter(this, mList);
        mListView.setAdapter(mAdapter);
    }
}

可以看到MainActivity的代碼邏輯頁比較簡單,主要操作就是生成了一個長度爲20的List,然後將其作爲數據源扔進Adapter裏面。好了,接下來就讓我們一起來看一下最後的Adapter類。
三、編寫MyAdapter類
好了,終於到了重頭戲,接下來咱們就一步步的編寫Adapter來解決ListView和EditText的各種衝突。
1.最普通的Adapter
首先咱們先按照以往的經驗寫一個最普通的Adapter,看一下會出現哪些問題:

public class MyAdapter extends BaseAdapter {
    private ViewHolder mViewHolder;
    private LayoutInflater mLayoutInflater;
    private List<String> mList;

    public MyAdapter(Context context, List<String> list) {
        mLayoutInflater = LayoutInflater.from(context);
        mList = list;
    }

    @Override
    public int getCount() {
        return mList.size();
    }

    @Override
    public Object getItem(int position) {
        return mList.get(position);
    }

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

    @Override
    public View getView(final int position, View convertView, ViewGroup parent) {
        if (convertView == null) {
            mViewHolder = new ViewHolder();
            convertView = mLayoutInflater.inflate(R.layout.edittext_item, null);
            mViewHolder.mTextView = (TextView) convertView.findViewById(R.id.text_view);
            mViewHolder.mEditText = (EditText) convertView.findViewById(R.id.edit_text);
            convertView.setTag(mViewHolder);
        } else {
            mViewHolder = (ViewHolder) convertView.getTag();
        }

        if (position <= 9) {
            mViewHolder.mTextView.setText("0" + (position));
        } else {
            mViewHolder.mTextView.setText("" + (position));
        }
        mViewHolder.mEditText.setText(mList.get(position));
        return convertView;
    }

    static final class ViewHolder {
        TextView mTextView;
        EditText mEditText;
    }
}

代碼如上,相信大家都寫過無數遍這樣類似的代碼,讓我們一起看一下這段代碼會出什麼問題。運行效果如圖所示:
這裏寫圖片描述
操作a:點擊“0”,光標定位到“0”,彈出軟鍵盤,“0”處的光標丟失;
操作b:再次點擊“0”,光標重新定位到“0”;
之後我又重複了一遍此步驟,不過點擊的是“1”的位置。
那麼爲什麼會出現這種效果呢?點擊“0”的時候大家看的可能不是太明顯,但點擊“1”的時候大家應該能明顯看出來ListView移動了一下。沒錯,正如大家所知道的那樣,軟鍵盤彈出的時候會重新繪製界面,因此ListView進行了一次重新繪製,重新走了一邊getView方法,生成了一個新的EditText,而之前展示光標的EditText被銷燬,所以才造成了EditText的焦點丟失。既然我們已經知道了這個問題的原因,那麼接下來我們就來解決掉它吧。
2.解決焦點丟失的問題
解決思路:既然焦點丟失是因爲ListView的重繪導致的,那我們就可以定義一個變量mTouchItemPosition來記錄用戶觸碰的EditText的位置,然後在getView方法中去判斷當前的position是否和用戶觸碰的位置相等,如果相等則讓其獲得焦點,否則清除焦點。而mTouchItemPosition的值可以在EditText的OnTouch事件中獲取。
代碼實現:

    //定義成員變量mTouchItemPosition,用來記錄手指觸摸的EditText的位置
    private int mTouchItemPosition = -1;
    ...
    @Override
    public View getView(int position, View convertView, ViewGroup parent) {
        if (convertView == null) {
            mViewHolder = new ViewHolder();
            convertView = mLayoutInflater.inflate(R.layout.edittext_item, null);
            mViewHolder.mTextView = (TextView) convertView.findViewById(R.id.text_view);
            mViewHolder.mEditText = (EditText) convertView.findViewById(R.id.edit_text);

            mViewHolder.mEditText.setOnTouchListener(new OnTouchListener() {
                @Override
                public boolean onTouch(View view, MotionEvent event) {
                    //注意,此處必須使用getTag的方式,不能將position定義爲final,寫成mTouchItemPosition = position
                    mTouchItemPosition = (Integer) view.getTag();
                    return false;
                }
            });

            convertView.setTag(mViewHolder);
        } else {
            mViewHolder = (ViewHolder) convertView.getTag();
        }

        if (position <= 9) {
            mViewHolder.mTextView.setText("0" + (position));
        } else {
            mViewHolder.mTextView.setText("" + (position));
        }
        mViewHolder.mEditText.setText(mList.get(position));

        mViewHolder.mEditText.setTag(position);

        if (mTouchItemPosition == position) {
            mViewHolder.mEditText.requestFocus();
            mViewHolder.mEditText.setSelection(mViewHolder.mEditText.getText().length());
        } else {
            mViewHolder.mEditText.clearFocus();
        }

        return convertView;
    }

讓我們重新運行看一下效果:
這裏寫圖片描述
可以看到焦點丟失這個問題已經被我們解決了。接下來就讓我們給EditText增加保存數據的功能。
3.添加保存數據的功能
首先讓我們來分析一下怎麼保存EditText中的數據。其實保存數據比較簡單,我們只需要做兩步就可以了,第一步我們需要拿到EditText變化之後的數據;第二步我們將這些數據替換掉之前的就大功告成了。
讓我們再次對MyAdapter類進行修改,而用於TextWatcher的afterTextChanged方法中獲取不到當前position,所以我們需要新建一個內部類MyTextWatcher實現TextWatcher接口並持有一個position,其次在ViewHolder中需要持有一個MyTextWatcher的引用來動態更新其position的值,代碼如下:

@Override
    public View getView(int position, View convertView, ViewGroup parent) {
        if (convertView == null) {
            mViewHolder = new ViewHolder();
            convertView = mLayoutInflater.inflate(R.layout.edittext_item, null);
            mViewHolder.mTextView = (TextView) convertView.findViewById(R.id.text_view);
            mViewHolder.mEditText = (EditText) convertView.findViewById(R.id.edit_text);

            mViewHolder.mEditText.setOnTouchListener(new OnTouchListener() {

                @Override
                public boolean onTouch(View view, MotionEvent event) {
                    //注意,此處必須使用getTag的方式,不能將position定義爲final,寫成mTouchItemPosition = position
                    mTouchItemPosition = (Integer) view.getTag();
                    return false;
                }
            });

            // 讓ViewHolder持有一個TextWathcer,動態更新position來防治數據錯亂;不能將position定義成final直接使用,必須動態更新
            mViewHolder.mTextWatcher = new MyTextWatcher();
            mViewHolder.mEditText.addTextChangedListener(mViewHolder.mTextWatcher);
            mViewHolder.updatePosition(position);

            convertView.setTag(mViewHolder);
        } else {
            mViewHolder = (ViewHolder) convertView.getTag();
            //動態更新TextWathcer的position
            mViewHolder.updatePosition(position);
        }

        if (position <= 9) {
            mViewHolder.mTextView.setText("0" + (position));
        } else {
            mViewHolder.mTextView.setText("" + (position));
        }
        mViewHolder.mEditText.setText(mList.get(position));

        mViewHolder.mEditText.setTag(position);

        if (mTouchItemPosition == position) {
            mViewHolder.mEditText.requestFocus();
            mViewHolder.mEditText.setSelection(mViewHolder.mEditText.getText().length());
        } else {
            mViewHolder.mEditText.clearFocus();
        }

        return convertView;
    }

    static final class ViewHolder {
        TextView mTextView;
        EditText mEditText;
        MyTextWatcher mTextWatcher;

        //動態更新TextWathcer的position
        public void updatePosition(int position) {
            mTextWatcher.updatePosition(position);
        }
    }


    class MyTextWatcher implements TextWatcher {
        //由於TextWatcher的afterTextChanged中拿不到對應的position值,所以自己創建一個子類
        private int mPosition;

        public void updatePosition(int position) {
            mPosition = position;
        }

        @Override
        public void onTextChanged(CharSequence s, int start, int before, int count) {

        }

        @Override
        public void beforeTextChanged(CharSequence s, int start, int count, int after) {

        }

        @Override
        public void afterTextChanged(Editable s) {
            mList.set(mPosition, s.toString());
        }
    };

現在保存數據的問題也已經完成了,接下來讓我們看最後一個滾動衝突的問題。
4.解決滾動衝突的問題
其實這個問題我在 完美解決EditText和ScrollView的滾動衝突(上)完美解決EditText和ScrollView的滾動衝突(下)這兩篇博客中詳細的講過,原理都是一樣的,所以這兒就不多說了,直接將原來的代碼拷過來就可以了。感興趣的同學可以去看一下之前的兩篇博客。

@Override
    public View getView(int position, View convertView, ViewGroup parent) {
        if (convertView == null) {
            mViewHolder = new ViewHolder();
            convertView = mLayoutInflater.inflate(R.layout.edittext_item, null);
            mViewHolder.mTextView = (TextView) convertView.findViewById(R.id.text_view);
            mViewHolder.mEditText = (EditText) convertView.findViewById(R.id.edit_text);

            mViewHolder.mEditText.setOnTouchListener(new OnTouchListener() {

                @Override
                public boolean onTouch(View view, MotionEvent event) {
                    //注意,此處必須使用getTag的方式,不能將position定義爲final,寫成mTouchItemPosition = position
                    mTouchItemPosition = (Integer) view.getTag();

                    //觸摸的是EditText並且當前EditText可以滾動則將事件交給EditText處理;否則將事件交由其父類處理
                    if ((view.getId() == R.id.edit_text && canVerticalScroll((EditText)view))) {
                        view.getParent().requestDisallowInterceptTouchEvent(true);
                        if (event.getAction() == MotionEvent.ACTION_UP) {
                            view.getParent().requestDisallowInterceptTouchEvent(false);
                        }
                    }
                    return false;
                }
            });

            // 讓ViewHolder持有一個TextWathcer,動態更新position來防治數據錯亂;不能將position定義成final直接使用,必須動態更新
            mViewHolder.mTextWatcher = new MyTextWatcher();
            mViewHolder.mEditText.addTextChangedListener(mViewHolder.mTextWatcher);
            mViewHolder.updatePosition(position);

            convertView.setTag(mViewHolder);
        } else {
            mViewHolder = (ViewHolder) convertView.getTag();
            //動態更新TextWathcer的position
            mViewHolder.updatePosition(position);
        }

        if (position <= 9) {
            mViewHolder.mTextView.setText("0" + (position));
        } else {
            mViewHolder.mTextView.setText("" + (position));
        }
        mViewHolder.mEditText.setText(mList.get(position));

        mViewHolder.mEditText.setTag(position);

        if (mTouchItemPosition == position) {
            mViewHolder.mEditText.requestFocus();
            mViewHolder.mEditText.setSelection(mViewHolder.mEditText.getText().length());
        } else {
            mViewHolder.mEditText.clearFocus();
        }

        return convertView;
    }

    /**
     * EditText豎直方向是否可以滾動
     * @param editText  需要判斷的EditText
     * @return  true:可以滾動   false:不可以滾動
     */
    private boolean canVerticalScroll(EditText editText) {
        //滾動的距離
        int scrollY = editText.getScrollY();
        //控件內容的總高度
        int scrollRange = editText.getLayout().getHeight();
        //控件實際顯示的高度
        int scrollExtent = editText.getHeight() - editText.getCompoundPaddingTop() -editText.getCompoundPaddingBottom();
        //控件內容總高度與實際顯示高度的差值
        int scrollDifference = scrollRange - scrollExtent;

        if(scrollDifference == 0) {
            return false;
        }

        return (scrollY > 0) || (scrollY < scrollDifference - 1);
    }

四、源碼下載
下載鏈接

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章