[Android] ItemTouchHelper로 swipe 구현하기

Happy Jiwon·2023년 8월 2일
1

Android

목록 보기
11/13

위 그림처럼 swipe 했을때 삭제할 수 있도록 구현해보고자 한다.

💡참고💡
위 그림에 대한 깃허브 주소 👉🏻 링크


😹 시작하기 전...

ItemTouchHelper 란?

ItemTouchHelper 는 RecyclerView의 아이템 삭제를 위한 스와이프 및 drag &drop 지원을 추가하는 유틸리티 클래스이다.

지원하는 기능에 따라 onMoveonSwiped 를 재정의해야 한다.
또한 기본적으로 ItemTouchHelper는 항목의 translateX/Y 속성을 이동하여 위치를 변경한다. onChildDraw 또는 onChildDrawOver 를 재정의하여 이러한 동작을 사용자 지정할 수 있다.

ItemTouchHelper 는 사용자가 액션을 수행할 때 이벤트를 수신하는 RecyclerView 및 이벤트에 반응하는 콜백 메소드가 선언되어 있는 Callback 클래스와 함께 사용한다.

쉽게 말해서 RecyclerView와 ItemTouchHelper.Callback을 ItemTouchHelper가 연결시켜주는 것이라고 이해하면 된다.

method

attachToRecyclerView

제공된 RecyclerView에 ItemTouchHelper를 연결한다.

public void attachToRecyclerView(@Nullable RecyclerView recyclerView)

😿 ItemTouchHelper.Callback 란?

이 클래스는 ItemTouchHelper애플리케이션 간의 계약이다.
각 ViewHolder별로 활성화되는 터치 동작을 제어하고 사용자가 이러한 작업을 수행할 때 콜백을 받을 수 있다.

methods

1. getMovementFlags(RecyclerView, ViewHolder)

각 뷰에 수행할 수 있는 작업을 하기 위해서 오버라이딩 한 후 방향을 정의하는 복합 플래그를 반환해야 한다.

2. onMove(recyclerView, dragged, target)

아이템을 이전 위치에서 새 위치로 이동하려고 할 때(즉, 사용자가 아이템을 드래그할 때) 호출된다.
이 콜백 메소드를 받으면 어댑터에서 아이템을 이전 위치(dragged.getAdapterPosition())에서 새로운 위치(target.getAdapterPosition())로 이동해야 하고, RecyclerView의 Adapter에서 notifyItemMoved(int, int)를 호출해야 합니다.

3. onSwiped(ViewHolder, int)

사용자가 ViewHolder를 스와이프할 때 호출된다.
스와이프될 때 ItemTouchHelper는 범위를 벗어날 때까지 View를 애니메이션화한 다음 onSwiped 메소드를 호출한다.

이때 여기서 어댑터를 업데이트 해야하고 관련된 Adapter의 notify 이벤트를 호출해야한다.


🦧 예제는 여기!!

완성 예제는 하단에 이씀!!

1. 구현하고 싶은 화면 설정

save_item.xml

스와이프할 뷰와 백그라운드 뷰를 다음과 같이 나타내었다.

해당 뷰를 스와이프 했을 때 삭제 버튼을 그릴 예정이다.


ItemTouchHelper.callback 구현

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 는 스와이프 이벤트가 발생됬을때 호출된다.


SaveFragment.java

리사이클러뷰가 배치된 Fragment(or Activity) 에서 연결해준다.

 // ItemTouchHelper 생성
helper = new ItemTouchHelper(new ItemTouchHelperCallback(recyclerViewAdapter));
// RecyclerView에 ItemTouchHelper 붙이기
helper.attachToRecyclerView(recyclerView);

여기까지 한다면 고정되지 않고 스와이프 되는 것을 볼 수 있다.

❗️ 해결
아이템을 스와이프 했을 때 삭제 버튼을 그려주기 위해 스와이프가 됐는지 확인하는 코드를 작성할 예정이다.


2. 아이템을 스와이프할 때 뷰에 변화가 생길 경우 함수 불러오기

ItemTouchHelperCallback.java

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

setTouchListener / up / down

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

3. Interface 작성

public interface ItemTouchHelperListener {
    void onDeleteClick(int position, RecyclerView.ViewHolder viewHolder);
}

onDeleteClick 인터페이스를 생성해준 후 Adapter에서 오버라이딩하여 작업해준다.

Adapter.java

@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); // 아이템 삭제 후 업데이트 알림
	}
}

🏋🏻‍♀️ 결과 바로보기

profile
공부가 조은 안드로이드 개발자

1개의 댓글

comment-user-thumbnail
2023년 8월 2일

개발자로서 성장하는 데 큰 도움이 된 글이었습니다. 감사합니다.

답글 달기