Handle drag and drop in RecyclerView
Bài đăng này đã không được cập nhật trong 4 năm
Trước khi vào bài viết các bạn hãy xem ví dụ sau
Đây là ứng dụng Trello, một ứng dụng tương đối quen thuộc đối với sinh viên thường được sử dụng khi để quản lý công việc trong team. Như các bạn thấy, trong khi mình kéo thả các task thì các task đã tự sắp xếp lại để lại khoảng trống cho vị trí mới của task đang được kéo.
Khi thả tay task đang được kéo sẽ được update vào lại vị trí vào vị trí mới của mình. Bài viết hôm nay mình sẽ hướng dẫn các bạn xử lý tính năng này.
1) ItemTouchHelper.Callback
ItemTouchHelper.Callback là một abstract class sử dụng để liên lạc giữa ItemTouchHelper và ứng dụng của bạn. Nó cho phép bạn kiểm soát ViewHolder nào được tương tác và trả về callback mỗi khi người dùng thực hiện hành động.
Một số phương thức thường sử dụng:
- int getMovementFlags( @NonNull recyclerView: RecyclerView, @NonNull viewHolder: RecyclerView.ViewHolder)
Trả về giá trị dùng để xác định hướng có thể di chuyển của ViewHolder. và bạn sử dụng 2 method là makeFlag(int, int) và makeMovementFlag(int, int) để tạo giá trị trả về cho hàm này
Note: Nếu bạn muốn ViewHolder không thể drag hay sử dụng makeFlag(0,0) hoặc makeMovementFlag(0, 0)
- boolean onMove(@NonNull recyclerView: RecyclerView, @NonNull viewHolder: RecyclerView.ViewHolder, @NonNull target: RecyclerView.ViewHolder)
Được gọi khi người dùng di chuyển 1 item đã được chọn từ vị trí cũ sang vị trí mới với viewholder là viewHolder của item đang drag và target là ViewHolder đích đến hiện tại của item đó, trả về giá trị true thì ItemTouchHelper sẽ hiểu rằng item đã được di chuyển sang vị trí mới
Hàm này sẽ được gọi thi bạn thả tay item đang drop vào vị trí mới
2) Ví dụ
Note: Trong ví dụ này mình sẽ chia ra làm 4 trường hợp:
- 1 list & 1 column in 1 recyclerView
- 1 list & 2 column in 1 recyclerView
- 2 list & 1 column in 1 recyclerView
- 2 list & 2 column in 1 recyclerView
Toàn bộ code demo mình sẽ gửi link ở cuối bài viết. Trong bài này mình chỉ demo và giải thích trường hợp 2 list & 2 column và các class chính sẽ sử dụng nhé.
1. activity_main.xml
<?xml version="1.0" encoding="utf-8"?>
<layout>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recyclerView"
android:layout_width="0dp"
android:layout_height="0dp"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:listitem="@layout/item_one_column" />
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>
2. ItemTouchListener.java
public class ItemTouchListener extends ItemTouchHelper.Callback {
private RecyclerView.Adapter itemAdapter;
public ItemTouchListener(RecyclerView.Adapter itemAdapter) {
this.itemAdapter = itemAdapter;
}
@Override
public int getMovementFlags(@NonNull RecyclerView recyclerView, @NonNull RecyclerView.ViewHolder viewHolder) {
if (viewHolder instanceof TitleViewHolder) {
return makeFlag(0, 0); //Kiểm tra xem ViewHolder đang action có phải title hay không. Nếu là title thì sẽ không có action gì cả
}
return makeFlag(ItemTouchHelper.ACTION_STATE_DRAG,
ItemTouchHelper.DOWN | ItemTouchHelper.UP | ItemTouchHelper.START | ItemTouchHelper.END); //Trong trường hợp không phải title thì có thể di chuyển lên xuống trái phải để tiến tới các viewholder đích
}
@Override
public boolean onMove(@NonNull RecyclerView recyclerView, @NonNull RecyclerView.ViewHolder viewHolder, @NonNull RecyclerView.ViewHolder target) {
if (target instanceof TitleViewHolder) { //Kiểm tra xem đích đến có phải title hay không, nếu là title thì không có action gì cả
return false;
}
int currentPosition = viewHolder.getAdapterPosition(); //lấy vị trí hiện tại của item đang drag
int newPosition = target.getAdapterPosition(); // lấy vị trí mới của item đang drop
if (currentPosition < newPosition) { //Thưc hiện đổi chỗ trong đoạn từ vị trí hiện tại đến vị trí đích
for (int i = currentPosition; i < newPosition; i++) {
if (itemAdapter instanceof OneColumnItemAdapter) {
Collections.swap(((OneColumnItemAdapter) itemAdapter).getData(), i, i + 1);
}
if (itemAdapter instanceof TwoColumnItemAdapter) {
Collections.swap(((TwoColumnItemAdapter) itemAdapter).getData(), i, i + 1);
}
if (itemAdapter instanceof OneColumnWithViewTypeAdapter) {
Collections.swap(((OneColumnWithViewTypeAdapter) itemAdapter).getData(), i, i + 1);
}
if (itemAdapter instanceof TwoColumnWithViewTypeAdapter) {
Collections.swap(((TwoColumnWithViewTypeAdapter) itemAdapter).getData(), i, i + 1);
}
}
} else {
for (int i = currentPosition; i > newPosition; i--) {
if (itemAdapter instanceof OneColumnItemAdapter) {
Collections.swap(((OneColumnItemAdapter) itemAdapter).getData(), i, i - 1);
}
if (itemAdapter instanceof TwoColumnItemAdapter) {
Collections.swap(((TwoColumnItemAdapter) itemAdapter).getData(), i, i - 1);
}
if (itemAdapter instanceof OneColumnWithViewTypeAdapter) {
Collections.swap(((OneColumnWithViewTypeAdapter) itemAdapter).getData(), i, i - 1);
}
if (itemAdapter instanceof TwoColumnWithViewTypeAdapter) {
Collections.swap(((TwoColumnWithViewTypeAdapter) itemAdapter).getData(), i, i - 1);
}
}
}
itemAdapter.notifyItemMoved(currentPosition, newPosition); //đổi chỗ vị trí hiện tại với vị trí mới
return true;
}
@Override
public void clearView(@NonNull RecyclerView recyclerView, @NonNull RecyclerView.ViewHolder viewHolder) {
super.clearView(recyclerView, viewHolder);
//Thực hiện call api, lưu database,... update vị trí mới cho item đã drag với vị trí mới là viewholder.getAdapterPosition()
}
@Override
public void onSwiped(@NonNull RecyclerView.ViewHolder viewHolder, int direction) {
}
}
3. TwoColumnWithViewTypeAdapter.java
public class TwoColumnWithViewTypeAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> {
private List<String> data;
public TwoColumnWithViewTypeAdapter(List<String> data) {
this.data = new ArrayList<>();
this.data.add("List 1");
this.data.addAll(data);
this.data.add(10, "List 2");
}
@Override
public int getItemViewType(int position) {
if (isTitle(position)) {
return ViewType.VIEW_TYPE_TITLE;
}
return ViewType.VIEW_TYPE_DATA;
}
private boolean isTitle(int position) {
return data.get(position).equals("List 1") || data.get(position).equals("List 2");
}
@NonNull
@Override
public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
if (viewType == ViewType.VIEW_TYPE_DATA) {
ItemTwoColumnBinding binding = ItemTwoColumnBinding.inflate(LayoutInflater.from(parent.getContext()), parent, false);
return new ItemTwoColumnViewHolder(binding);
}
ItemTitleBinding binding = ItemTitleBinding.inflate(LayoutInflater.from(parent.getContext()), parent, false);
return new TitleViewHolder(binding);
}
@Override
public void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, int position) {
if (holder instanceof ItemTwoColumnViewHolder) {
((ItemTwoColumnViewHolder) holder).bindData(data.get(position));
}
if (holder instanceof TitleViewHolder) {
((TitleViewHolder) holder).bindData(data.get(position));
}
}
@Override
public int getItemCount() {
return data == null ? 0 : data.size();
}
public List<String> getData() {
return data;
}
}
4. MainActivity.java
public class MainActivity extends AppCompatActivity implements DialogInterface.OnClickListener {
private static final String FORMAT_ITEM = "Item %d";
private static final int NUMBER_ITEM = 20;
private ActivityMainBinding binding;
private List<String> data = new ArrayList<>();
private RecyclerView.Adapter adapter;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
binding = DataBindingUtil.setContentView(this, R.layout.activity_main);
showData();
}
private void initData() {
data.clear();
for (int i = 1; i <= NUMBER_ITEM; i++) {
data.add(String.format(Locale.getDefault(), FORMAT_ITEM, i));
}
}
private void showData() {
new AlertDialog.Builder(this)
.setCancelable(false)
.setItems(R.array.selection, this)
.create()
.show();
}
@Override
public void onClick(DialogInterface dialog, int which) {
initData();
switch (which) {
case 0:
adapter = new OneColumnItemAdapter(data);
break;
case 1:
adapter = new TwoColumnItemAdapter(data);
binding.recyclerView.setLayoutManager(new GridLayoutManager(this, 2));
break;
case 2:
adapter = new OneColumnWithViewTypeAdapter(data);
break;
case 3:
adapter = new TwoColumnWithViewTypeAdapter(data);
GridLayoutManager gridLayoutManager = new GridLayoutManager(this, 2);
gridLayoutManager.setSpanSizeLookup(new GridLayoutManager.SpanSizeLookup() {
@Override
public int getSpanSize(int position) {
if (adapter.getItemViewType(position) == ViewType.VIEW_TYPE_TITLE) { //Kiểm tra xem vị trí position có phải là title hay không? Nếu là title sẽ chiếm 2 khoảng không của RecyclerView
return 2;
}
return 1;
}
});
binding.recyclerView.setLayoutManager(gridLayoutManager);
break;
}
binding.recyclerView.setAdapter(adapter);
ItemTouchListener itemTouchListener = new ItemTouchListener(adapter);
ItemTouchHelper itemTouchHelper = new ItemTouchHelper(itemTouchListener);
itemTouchHelper.attachToRecyclerView(binding.recyclerView);
}
}
Và đây là kết quả
Bạn có thể tham khảo thêm code của mình tại đây
TÀI LIỆU THAM KHẢO
https://developer.android.com/reference/kotlin/androidx/recyclerview/widget/ItemTouchHelper.Callback
All rights reserved