Giới thiệu
Bài viết này hướng dẫn cách tạo EditText có thể focus và nhập liệu bên trong ListView trên Android. Vấn đề chính: ListView mặc định không cho phép child view focus được (để cuộn mượt), cần cấu hình descendantFocusability và quản lý focus thủ công.
Các bước thực hiện
Bước 1: Tạo dự án mới
Mở Android Studio, chọn File → New Project và điền thông tin cần thiết.
Bước 2: Layout chính (res/layout/activity_main.xml)
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="https://schemas.android.com/apk/res/android"
xmlns:tools="https://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:padding="4dp"
tools:context=".MainActivity">
<ListView
android:id="@+id/listView"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:descendantFocusability="beforeDescendants" />
</LinearLayout>
Bước 3: Layout item (res/layout/listitem.xml)
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="https://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="8dp">
<EditText
android:id="@+id/itemCaption"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:hint="Nhập nội dung..."
android:inputType="text"
android:singleLine="true" />
</LinearLayout>
Bước 4: Adapter tùy chỉnh (MainActivity.java)
import androidx.appcompat.app.AppCompatActivity;
import android.annotation.SuppressLint;
import android.content.Context;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.BaseAdapter;
import android.widget.EditText;
import android.widget.ListView;
import java.util.ArrayList;
import java.util.List;
public class MainActivity extends AppCompatActivity {
private ListView listView;
private MyAdapter myAdapter;
private List items = new ArrayList<>();
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
listView = findViewById(R.id.listView);
// Quan trọng: Cho phép item focus
listView.setItemsCanFocus(true);
// Dữ liệu mẫu
for (int i = 0; i < 7; i++) {
items.add("Caption " + i);
}
myAdapter = new MyAdapter();
listView.setAdapter(myAdapter);
}
private class MyAdapter extends BaseAdapter {
private final LayoutInflater inflater;
MyAdapter() {
inflater = (LayoutInflater) getSystemService(Context.LAYOUT_INFLATER_SERVICE);
}
@Override
public int getCount() {
return items.size();
}
@Override
public Object getItem(int position) {
return items.get(position);
}
@Override
public long getItemId(int position) {
return position;
}
@SuppressLint("InflateParams")
@Override
public View getView(int position, View convertView, ViewGroup parent) {
ViewHolder holder;
if (convertView == null) {
convertView = inflater.inflate(R.layout.listitem, parent, false);
holder = new ViewHolder();
holder.caption = convertView.findViewById(R.id.itemCaption);
convertView.setTag(holder);
} else {
holder = (ViewHolder) convertView.getTag();
}
// Set data
holder.caption.setText(items.get(position));
holder.caption.setTag(position); // Lưu position để dùng trong listener
// Xử lý khi mất focus -> lưu dữ liệu
holder.caption.setOnFocusChangeListener((v, hasFocus) -> {
if (!hasFocus) {
int pos = (int) v.getTag();
String text = ((EditText) v).getText().toString();
items.set(pos, text);
// TODO: Lưu vào database, SharedPreferences, hoặc notifyDataSetChanged()
}
});
return convertView;
}
static class ViewHolder {
EditText caption;
}
}
}
Bước 5: Cấu hình Manifest (androidManifest.xml)
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="https://schemas.android.com/apk/res/android"
package="app.com.sample">
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/AppTheme">
<activity
android:name=".MainActivity"
android:windowSoftInputMode="adjustPan|adjustResize">
<intent-filter>
</intent-filter>
</activity>
</application>
</manifest>
Chạy ứng dụng
Kết nối thiết bị, nhấn Run. Chạm vào EditText bất kỳ để focus, nhập text, chạm ra ngoài hoặc cuộn để mất focus - dữ liệu sẽ được lưu vào list.
Giải thích các thuộc tính quan trọng
| Thuộc tính | Giá trị | Chức năng |
|---|---|---|
android:descendantFocusability |
beforeDescendants |
Cho phép child view nhận focus trước khi ListView xử lý touch |
android:descendantFocusability |
afterDescendants |
ListView xử lý touch trước, child nhận focus sau (mặc định) |
android:descendantFocusability |
blocksDescendants |
Chặn hoàn toàn child nhận focus (mặc định của ListView cũ) |
listView.setItemsCanFocus(true) |
Java code | Bật cho phép item focus (tương đương beforeDescendants) |
android:windowSoftInputMode |
adjustPan|adjustResize |
Điều chỉnh layout khi bàn phím hiện: pan hoặc resize |
Vấn đề thường gặp & Giải pháp
- Bàn phím che EditText: Dùng
adjustResizetrong Manifest, hoặcandroid:windowSoftInputMode="adjustPan". - Mất dữ liệu khi cuộn (recycle view): Luôn lưu data trong
onFocusChange(hasFocus=false)hoặcTextWatcher. - Focus nhảy vọt: Set
android:focusableInTouchMode="true"cho EditText, vàandroid:focusable="true". - ListView recycle sai position: Dùng
holder.caption.setTag(position)để lấy đúng position.
Khuyên dùng: RecyclerView thay vì ListView
ListView đã lỗi thời. RecyclerView xử lý focus tốt hơn, hiệu năng cao hơn, hỗ trợ LayoutManager linh hoạt.
// RecyclerView.Adapter - ViewHolder pattern bắt buộc, view recycling tự động // Item focus xử lý tương tự, nhưng RecyclerView có ViewHolder pattern chuẩn hơn // Nên dùng RecyclerView cho mọi danh sách mới
Lưu ý
- Test kỹ trên nhiều thiết bị - hành vi focus khác nhau giữa OEM.
- Cân nhắc dùng
TextWatcher(afterTextChanged) để auto-save thay vì chờ mất focus. - Đối với form phức tạp: dùng
RecyclerView+DiffUtil+ ViewBinding.