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

四、源码下载
下载链接

  • 7
    点赞
  • 15
    收藏
    觉得还不错? 一键收藏
  • 11
    评论
好的,如果姓名本来就在ListView,您可以按照以下步骤来实现点击姓名拨打对应电话号码的功能: 1. 创建一个ListView布局,用于显示联系人的姓名和电话号码。 2. 创建一个自定义的Adapter类,用于将姓名和电话号码显示在ListView。 3. 在Adapter类的getView()方法,为ListView的每个列表项添加一个OnClickListener,当用户点击姓名时,使用隐式Intent拨打对应的电话号码。 下面是一个简单的示例代码片段,用于实现上述功能: ```java public class ContactListAdapter extends BaseAdapter { private List<Contact> contactList; private Context context; // 构造函数 public ContactListAdapter(Context context, List<Contact> contactList) { this.context = context; this.contactList = contactList; } @Override public View getView(final int position, View convertView, ViewGroup parent) { // 获取ListView的每个列表项布局 View view = LayoutInflater.from(context).inflate(R.layout.list_item, null); // 获取姓名和电话号码的TextView控件 TextView nameTextView = view.findViewById(R.id.nameTextView); TextView phoneTextView = view.findViewById(R.id.phoneTextView); // 显示姓名和电话号码 final Contact contact = contactList.get(position); nameTextView.setText(contact.getName()); phoneTextView.setText(contact.getPhone()); // 为姓名的TextView控件添加OnClickListener nameTextView.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { // 使用隐式Intent拨打电话 Intent intent = new Intent(Intent.ACTION_DIAL, Uri.parse("tel:" + contact.getPhone())); context.startActivity(intent); } }); return view; } // 其他方法省略 } ``` 在上述示例代码,ContactListAdapter类是自定义的Adapter类,用于将姓名和电话号码显示在ListView。在getView()方法,为每个列表项添加一个OnClickListener,当用户点击姓名时,使用隐式Intent拨打对应的电话号码。在OnClickListener使用ACTION_DIAL操作和电话号码URI来创建一个拨打电话的隐式Intent,然后使用startActivity()方法启动该Intent。 请注意,ACTION_DIAL操作只会打开拨号器应用程序,并自动填充电话号码,但不会直接拨打电话。如果您想要直接拨打电话,应使用ACTION_CALL操作,但是需要注意用户的权限授权问题
评论 11
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值