위 그림처럼 swipe 했을때 삭제할 수 있도록 구현해보고자 한다.
💡참고💡
위 그림에 대한 깃허브 주소 👉🏻 링크
ItemTouchHelper
는 RecyclerView의 아이템 삭제를 위한 스와이프 및 drag &drop 지원을 추가하는 유틸리티 클래스이다.
지원하는 기능에 따라 onMove
와 onSwiped
를 재정의해야 한다.
또한 기본적으로 ItemTouchHelper는 항목의 translateX/Y 속성을 이동하여 위치를 변경한다. onChildDraw
또는 onChildDrawOver
를 재정의하여 이러한 동작을 사용자 지정할 수 있다.
ItemTouchHelper
는 사용자가 액션을 수행할 때 이벤트를 수신하는 RecyclerView 및 이벤트에 반응하는 콜백 메소드가 선언되어 있는 Callback 클래스와 함께 사용한다.
쉽게 말해서 RecyclerView와 ItemTouchHelper.Callback을 ItemTouchHelper가 연결시켜주는 것이라고 이해하면 된다.
제공된 RecyclerView에 ItemTouchHelper를 연결한다.
public void attachToRecyclerView(@Nullable RecyclerView recyclerView)
이 클래스는 ItemTouchHelper
와 애플리케이션
간의 계약이다.
각 ViewHolder별로 활성화되는 터치 동작을 제어하고 사용자가 이러한 작업을 수행할 때 콜백을 받을 수 있다.
각 뷰에 수행할 수 있는 작업을 하기 위해서 오버라이딩 한 후 방향을 정의하는 복합 플래그
를 반환해야 한다.
아이템을 이전 위치에서 새 위치로 이동하려고 할 때(즉, 사용자가 아이템을 드래그할 때) 호출된다.
이 콜백 메소드를 받으면 어댑터에서 아이템을 이전 위치(dragged.getAdapterPosition())에서 새로운 위치(target.getAdapterPosition())로 이동해야 하고, RecyclerView의 Adapter에서 notifyItemMoved(int, int)를 호출해야 합니다.
사용자가 ViewHolder를 스와이프할 때 호출된다.
스와이프될 때 ItemTouchHelper는 범위를 벗어날 때까지 View를 애니메이션화한 다음 onSwiped 메소드를 호출한다.
이때 여기서 어댑터를 업데이트 해야하고 관련된 Adapter의 notify 이벤트를 호출해야한다.
완성 예제는 하단에 이씀!!
스와이프할 뷰와 백그라운드 뷰를 다음과 같이 나타내었다.
해당 뷰를 스와이프 했을 때 삭제 버튼을 그릴 예정이다.
ItemTouchHelper.callback을 구현할 클래스, ItemTouchHelperCallback.java
ItemTouchHelper.callback을 구현하려면 반드시 아래 세 메소드를 작성해야 한다.
// 이동 방향 결정, 스와이프 시 항상 onChildDraw 보다 먼저 호출!
@Override
public int getMovementFlags(@NonNull RecyclerView recyclerView, @NonNull RecyclerView.ViewHolder viewHolder) {
return makeMovementFlags(0, ItemTouchHelper.LEFT);
}
ItemTouchHelper.LEFT(or START)
는 오른쪽에서 왼쪽으로만 이동하도록하는 코드이다.
왼쪽과 오른쪽을 이동하고 싶다면
|
를 사용해 나타내면 된다.
// drag
@Override
public boolean onMove(@NonNull RecyclerView recyclerView, @NonNull RecyclerView.ViewHolder viewHolder, @NonNull RecyclerView.ViewHolder target) {
return false;
}
onMove
는 드래그시에 호출되는 함수로, 이번엔 사용하지 않는다.
@Override
public void onSwiped(@NonNull RecyclerView.ViewHolder viewHolder, int direction) {
}
onSwiped
는 스와이프 이벤트가 발생됬을때 호출된다.
리사이클러뷰가 배치된 Fragment(or Activity) 에서 연결해준다.
// ItemTouchHelper 생성
helper = new ItemTouchHelper(new ItemTouchHelperCallback(recyclerViewAdapter));
// RecyclerView에 ItemTouchHelper 붙이기
helper.attachToRecyclerView(recyclerView);
여기까지 한다면 고정되지 않고 스와이프 되는 것을 볼 수 있다.
❗️ 해결
아이템을 스와이프 했을 때 삭제 버튼을 그려주기 위해 스와이프가 됐는지 확인하는 코드를 작성할 예정이다.
@Override
public void onChildDraw(@NonNull Canvas c, @NonNull RecyclerView recyclerView, @NonNull RecyclerView.ViewHolder viewHolder, float dX, float dY, int actionState, boolean isCurrentlyActive) {
//아이템이 스와이프 됐을경우 버튼을 그려주기 위해서 스와이프가 됐는지 확인
if (actionState == ItemTouchHelper.ACTION_STATE_SWIPE) {
if (buttonsShowedState != ButtonState.GONE) {
if (buttonsShowedState == ButtonState.DELETE_VISIBLE)
dX = Math.min(dX, -buttonWidth);
super.onChildDraw(c, recyclerView, viewHolder, dX, dY, actionState, isCurrentlyActive);
} else {
setTouchListener(c, recyclerView, viewHolder, dX, dY, actionState, isCurrentlyActive);
}
if (buttonsShowedState == ButtonState.GONE) {
super.onChildDraw(c, recyclerView, viewHolder, dX, dY, actionState, isCurrentlyActive);
}
}
currentItemViewHolder = viewHolder;
// 삭제 버튼 그리기
drawButtons(c, currentItemViewHolder);
}
private void drawButtons(Canvas c, RecyclerView.ViewHolder viewHolder) {
float corners = 5;
View itemView = viewHolder.itemView;
Paint paint = new Paint();
buttonInstance = null;
// 왼쪽으로 스와이프 했을 경우에만 보여지도록
if (buttonsShowedState == ButtonState.DELETE_VISIBLE) {
RectF rightButton = new RectF(itemView.getRight() - buttonWidth, itemView.getTop() + 10, itemView.getRight(),
itemView.getBottom() - 30);
paint.setColor(Color.RED);
c.drawRoundRect(rightButton, corners, corners, paint);
drawText("삭제", c, rightButton, paint);
buttonInstance = rightButton;
}
}
private void drawText(String text, Canvas c, RectF button, Paint p) {
float textSize = 32;
p.setColor(Color.WHITE);
p.setAntiAlias(true);
p.setTextSize(textSize);
float textWidth = p.measureText(text);
c.drawText(text, button.centerX() - (textWidth / 2), button.centerY() + (textSize / 2), p);
}
@Override
public int convertToAbsoluteDirection(int flags, int layoutDirection) {
if (swipeBack) {
swipeBack = false;
return 0;
}
return super.convertToAbsoluteDirection(flags, layoutDirection);
}
@SuppressLint("ClickableViewAccessibility")
private void setTouchListener(final Canvas c, final RecyclerView recyclerView, final RecyclerView.ViewHolder viewHolder, final float dX, final float dY, final int actionState, final boolean isCurrentlyActive) {
recyclerView.setOnTouchListener((v, event) -> {
// ACTION_CANCEL >> 현재 제스처 중단, ACTION_UP >> 최종 릴리스 위치와 마지막 다운, 이동 이벤트 이후의 모든 중간 지점이 포함
swipeBack = event.getAction() == MotionEvent.ACTION_CANCEL || event.getAction() == MotionEvent.ACTION_UP;
if (swipeBack) {
// state 설정
if (dX < -buttonWidth) buttonsShowedState = ButtonState.DELETE_VISIBLE;
if (buttonsShowedState != ButtonState.GONE) {
setTouchDownListener(c, recyclerView, viewHolder, dX, dY, actionState, isCurrentlyActive);
setItemsClickable(recyclerView, false);
}
}
return false;
});
}
@SuppressLint("ClickableViewAccessibility")
private void setTouchDownListener(final Canvas c, final RecyclerView recyclerView, final RecyclerView.ViewHolder viewHolder, final float dX, final float dY, final int actionState, final boolean isCurrentlyActive) {
recyclerView.setOnTouchListener((v, event) -> {
// 누른 제스처가 시작, 초기 시작 위치가 포함
if (event.getAction() == MotionEvent.ACTION_DOWN) {
setTouchUpListener(c, recyclerView, viewHolder, dX, dY, actionState, isCurrentlyActive);
}
return false;
});
}
@SuppressLint("ClickableViewAccessibility")
private void setTouchUpListener(final Canvas c, final RecyclerView recyclerView, final RecyclerView.ViewHolder viewHolder, final float dX, final float dY, final int actionState, final boolean isCurrentlyActive) {
recyclerView.setOnTouchListener((v, event) -> {
super.onChildDraw(c, recyclerView, viewHolder, 0F, dY, actionState, isCurrentlyActive);
recyclerView.setOnTouchListener((v1, event1) -> false);
setItemsClickable(recyclerView, true);
swipeBack = false;
if (listener != null && buttonInstance != null && buttonInstance.contains(event.getX(), event.getY())) {
if (buttonsShowedState == ButtonState.DELETE_VISIBLE) {
listener.onDeleteClick(viewHolder.getAdapterPosition(), viewHolder);
}
}
buttonsShowedState = ButtonState.GONE;
currentItemViewHolder = null;
return false;
});
}
private void setItemsClickable(RecyclerView recyclerView, boolean isClickable) {
for (int i = 0; i < recyclerView.getChildCount(); i++) {
recyclerView.getChildAt(i).setClickable(isClickable);
}
}
public interface ItemTouchHelperListener {
void onDeleteClick(int position, RecyclerView.ViewHolder viewHolder);
}
onDeleteClick
인터페이스를 생성해준 후 Adapter에서 오버라이딩하여 작업해준다.
@Override
public void onDeleteClick(int position, RecyclerView.ViewHolder viewHolder) {
if (position == 0) {
Toast.makeText(context, "기본 그룹은 삭제할 수 없습니다.", Toast.LENGTH_SHORT).show();
} else {
Toast.makeText(context, "삭제되었습니다.", Toast.LENGTH_SHORT).show();
itemLists.remove(position);
notifyItemRemoved(position); // 아이템 삭제 후 업데이트 알림
}
}
개발자로서 성장하는 데 큰 도움이 된 글이었습니다. 감사합니다.