맞춤 뷰를 대화형으로 만들기

Compose 방법 사용해 보기
Jetpack Compose는 Android에 권장되는 UI 도구 키트입니다. Compose에서 레이아웃을 사용하는 방법을 알아보세요.

UI 그리기는 맞춤 뷰 만들기 과정의 일부일 뿐입니다. 또한 개발자가 모방하고 있는 실제 행동과 매우 유사한 방식으로 뷰가 사용자 입력에 응답하도록 해야 합니다.

앱의 객체가 실제 객체처럼 작동하도록 합니다. 예를 들어 앱의 이미지가 사라졌다가 다른 곳에 다시 나타나게 해서는 안 됩니다. 실제 객체는 그렇지 않기 때문입니다. 대신 이미지를 한 곳에서 다른 곳으로 옮기세요.

사용자는 인터페이스에서 미묘한 동작이나 느낌을 감지하고 실제 세계를 모방한 미묘한 부분에 가장 잘 반응합니다. 예를 들어 사용자가 UI 객체를 플링하면 처음에 모션을 지연시키는 관성 감각을 사용자에게 제공하세요. 모션이 끝나면 물건을 플링 너머로 운반하는 운동 감각을 제공하세요.

이 페이지에서는 Android 프레임워크의 기능을 사용하여 이러한 실제 동작을 맞춤 뷰에 추가하는 방법을 보여줍니다.

관련 추가 정보는 입력 이벤트 개요속성 애니메이션 개요에서 확인할 수 있습니다.

입력 동작 처리

다른 많은 UI 프레임워크와 마찬가지로 Android에서는 입력 이벤트 모델을 지원합니다. 사용자 작업은 콜백을 트리거하는 이벤트로 전환되며, 콜백을 재정의하여 앱이 사용자에게 응답하는 방식을 맞춤설정할 수 있습니다. Android 시스템에서 가장 일반적인 입력 이벤트는 onTouchEvent(android.view.MotionEvent)를 트리거하는 터치입니다. 다음과 같이 이 메서드를 재정의하여 이벤트를 처리합니다.

Kotlin

override fun onTouchEvent(event: MotionEvent): Boolean {
    return super.onTouchEvent(event)
}

Java

@Override
   public boolean onTouchEvent(MotionEvent event) {
    return super.onTouchEvent(event);
   }

터치 이벤트 자체는 그다지 유용하지 않습니다. 최신 터치 UI는 탭, 가져오기, 푸시, 플링, 확대/축소와 같은 동작 측면에서 상호작용을 정의합니다. 원시 터치 이벤트를 동작으로 변환하기 위해 Android는 GestureDetector를 제공합니다.

GestureDetector.OnGestureListener를 구현하는 클래스의 인스턴스를 전달하여 GestureDetector를 생성합니다. 몇 가지 동작만 처리하려면 GestureDetector.OnGestureListener 인터페이스를 구현하는 대신 GestureDetector.SimpleOnGestureListener를 확장하면 됩니다. 예를 들어 다음 코드는 GestureDetector.SimpleOnGestureListener를 확장하고 onDown(MotionEvent)를 재정의하는 클래스를 만듭니다.

Kotlin

private val myListener =  object : GestureDetector.SimpleOnGestureListener() {
    override fun onDown(e: MotionEvent): Boolean {
        return true
    }
}

private val detector: GestureDetector = GestureDetector(context, myListener)

Java

class MyListener extends GestureDetector.SimpleOnGestureListener {
   @Override
   public boolean onDown(MotionEvent e) {
       return true;
   }
}
detector = new GestureDetector(getContext(), new MyListener());

GestureDetector.SimpleOnGestureListener 사용 여부와 관계없이 항상 true를 반환하는 onDown() 메서드를 구현합니다. 이렇게 해야 하는 이유는 모든 동작이 onDown() 메시지로 시작하기 때문입니다. GestureDetector.SimpleOnGestureListener와 마찬가지로 onDown()에서 false를 반환하면 시스템은 개발자가 나머지 동작을 무시하려고 하며 GestureDetector.OnGestureListener의 다른 메서드는 호출되지 않는다고 가정합니다. 전체 동작을 무시하려면 onDown()에서 false만 반환합니다.

GestureDetector.OnGestureListener를 구현하고 GestureDetector 인스턴스를 만든 후에는 GestureDetector를 사용하여 onTouchEvent()에서 수신한 터치 이벤트를 해석할 수 있습니다.

Kotlin

override fun onTouchEvent(event: MotionEvent): Boolean {
    return detector.onTouchEvent(event).let { result ->
        if (!result) {
            if (event.action == MotionEvent.ACTION_UP) {
                stopScrolling()
                true
            } else false
        } else true
    }
}

Java

@Override
public boolean onTouchEvent(MotionEvent event) {
   boolean result = detector.onTouchEvent(event);
   if (!result) {
       if (event.getAction() == MotionEvent.ACTION_UP) {
           stopScrolling();
           result = true;
       }
   }
   return result;
}

동작의 일부로 인식하지 못하는 터치 이벤트를 onTouchEvent()에 전달하면 false가 반환됩니다. 그런 다음 자체 맞춤 동작 감지 코드를 실행할 수 있습니다.

물리적으로 그럴듯한 움직임 만들기

동작은 터치스크린 기기를 제어하는 강력한 방법이지만 물리적으로 그럴듯한 결과를 내지 않는다면 직관적이지 않고 기억하기 어려울 수 있습니다.

예를 들어 세로축을 중심으로 회전하며 뷰에 그려진 항목을 설정하는 가로 플링 동작을 구현한다고 가정해 보겠습니다. 이 동작은 사용자가 플라이휠을 밀고 회전하는 것처럼 플링 방향으로 빠르게 이동했다가 속도를 낮추는 방식으로 UI가 반응하는 경우 적합합니다.

스크롤 동작 애니메이션 처리 방법에 관한 문서에서 자체 스콜 동작을 구현하는 방법을 자세히 설명합니다. 그러나 플라이휠의 느낌을 시뮬레이션하는 것은 그리 간단한 일이 아닙니다. 플라이휠 모델이 올바로 작동하려면 많은 물리학 및 수학 지식이 필요합니다. 다행히 Android에서는 이러한 동작과 기타 동작을 시뮬레이션할 수 있는 도우미 클래스를 제공합니다. Scroller 클래스는 플라이휠 스타일의 플링 동작을 처리하기 위한 기반입니다.

플링을 시작하려면 시작 속도와 플링의 최소 및 최대 xy 값을 사용하여 fling()를 호출합니다. 속도 값의 경우 GestureDetector로 계산된 값을 사용할 수 있습니다.

Kotlin

fun onFling(e1: MotionEvent, e2: MotionEvent, velocityX: Float, velocityY: Float): Boolean {
    scroller.fling(
            currentX,
            currentY,
            (velocityX / SCALE).toInt(),
            (velocityY / SCALE).toInt(),
            minX,
            minY,
            maxX,
            maxY
    )
    postInvalidate()
    return true
}

Java

@Override
public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
   scroller.fling(currentX, currentY, velocityX / SCALE, velocityY / SCALE, minX, minY, maxX, maxY);
   postInvalidate();
    return true;
}

fling()를 호출하면 플링 동작의 물리 모델이 설정됩니다. 그런 다음 일정한 간격으로 Scroller.computeScrollOffset()를 호출하여 Scroller를 업데이트합니다. computeScrollOffset()는 현재 시간을 읽고 물리 모델을 사용해 해당 시점의 xy 위치를 계산하여 Scroller 객체의 내부 상태를 업데이트합니다. getCurrX()getCurrY()를 호출하여 이러한 값을 가져옵니다.

대부분의 뷰는 Scroller 객체의 xy 위치를 scrollTo()에 직접 전달합니다. 이 예는 약간 다릅니다. 현재 스크롤 x 위치를 사용하여 뷰의 회전 각도를 설정합니다.

Kotlin

scroller.apply {
    if (!isFinished) {
        computeScrollOffset()
        setItemRotation(currX)
    }
}

Java

if (!scroller.isFinished()) {
    scroller.computeScrollOffset();
    setItemRotation(scroller.getCurrX());
}

Scroller 클래스는 스크롤 위치를 계산하지만 이러한 위치를 뷰에 자동으로 적용하지는 않습니다. 스크롤 애니메이션이 매끄럽게 표시되도록 새 좌표를 자주 적용합니다. 여기에는 두 가지 방법이 있습니다.

  • fling()를 호출한 후 postInvalidate()를 호출하여 강제로 다시 그리세요. 이 기법을 사용하려면 onDraw()에서 스크롤 오프셋을 계산하고 스크롤 오프셋이 변경될 때마다 postInvalidate()를 호출해야 합니다.
  • ValueAnimator를 설정하여 플링 기간 동안 애니메이션을 실행하고 addUpdateListener()를 호출하여 애니메이션 업데이트를 처리하는 리스너를 추가합니다. 이 기법을 사용하면 View의 속성에 애니메이션을 적용할 수 있습니다.

원활한 전환

사용자는 최신 UI가 상태 간에 부드럽게 전환되기를 기대합니다. UI 요소가 나타났다가 사라지는 대신 페이드 인 및 아웃되며 모션이 갑자기 시작되고 멈추지 않고 부드럽게 시작하고 끝나는 것이죠. Android 속성 애니메이션 프레임워크를 사용하면 원활한 전환을 더 쉽게 할 수 있습니다.

애니메이션 시스템을 사용하려면 보기의 모양에 영향을 주는 속성이 변경될 때마다 속성을 직접 변경하지 마세요. 대신 ValueAnimator를 사용하여 변경하세요. 다음 예에서는 뷰에서 선택된 하위 구성요소를 수정하면 전체 렌더링된 뷰가 회전하여 선택 포인터가 중앙에 오게 됩니다. ValueAnimator는 새 회전 값을 즉시 설정하지 않고 수백 밀리초 동안 회전을 변경합니다.

Kotlin

autoCenterAnimator = ObjectAnimator.ofInt(this, "Rotation", 0).apply {
    setIntValues(targetAngle)
    duration = AUTOCENTER_ANIM_DURATION
    start()
}

Java

autoCenterAnimator = ObjectAnimator.ofInt(this, "Rotation", 0);
autoCenterAnimator.setIntValues(targetAngle);
autoCenterAnimator.setDuration(AUTOCENTER_ANIM_DURATION);
autoCenterAnimator.start();

변경하려는 값이 기본 View 속성 중 하나라면 다음 예와 같이 뷰에 여러 속성의 동시 애니메이션에 최적화된 ViewPropertyAnimator가 내장되어 있기 때문에 애니메이션을 더 쉽게 실행할 수 있습니다.

Kotlin

animate()
    .rotation(targetAngle)
    .duration = ANIM_DURATION
    .start()

Java

animate().rotation(targetAngle).setDuration(ANIM_DURATION).start();