파도를 잡아라

<블로그 원문은 이곳에서 확인하실 수 있으며 블로그 번역 리뷰는 신정규(MachineLearning GDE)님이 참여해 주셨습니다>
지난 2주일간 가장 중요한 릴리스는 Google I/O 이후로 베타 버전으로 있다가 이번에 많은 기대를 받으며 선보인 Android Studio 3.5였습니다. 하지만 최근에 Android의 세계에서는 놓치지 말고 체크해봐야 할 다른 일들도 벌어지고 있었습니다.

Android Studio 3.5: 'Project Marble'

훌륭한 IDE는 좋은 친구와도 같습니다. 항상 함께 즐거운 시간을 보내며 날이 갈수록 더 즐거워지는 그런 친구 말입니다. 하지만 여러분의 IDE가 원하는대로 작동하지 않는다면 가끔 좋긴 하나 나를 힘들게 하는 친구인 셈이지요. 그래서 더욱더 기쁜 마음으로 Android Studio 3.5를 출시했습니다.


이전 릴리스에서, 우리는 지금은 개발자들이 Android Studio에서 사용할 수 있는 새로운 기능에 관해서 이야기했습니다. 하지만 이번 Android Studio 릴리스는 달랐습니다. 개발팀에서는 기본적인 기능 중 몇 가지가 충분히 만족스러운 수준으로 작동하지 않는다는 의견을 들었습니다. 그래서 팀원들은 그때까지 개발 중이던 멋진 새로운 기능은 모두 잠시 제쳐두고 소매를 걷어붙이고서(실은 팀원 대부분이 티셔츠를 입고 다니므로 이마저도 어려운 일임) 버그 수정 모드로 일제히 돌입했습니다.
그 과정에서 다음을 비롯해 수많은 중요한 수정 사항이 있었습니다.
  • 50여 가지 메모리 누수 수정 사항
  • 21가지 IDE 중단 수정 사항*
  • 3x CPU Usage Emulator 개선(백그라운드에서 실행 중인 에뮬레이터가 작동하지 않아 CPU가 고정되는 상황을 종종 목격한 필자에겐 너무나도 소중한 개선 사항)
  • Windows I/O 개선 사항(참고: 'Windows I/O'는 Microsoft에서 'Google I/O'에 상응하는 개념으로 내놓은 것이 아님)
  • 600여 개의 버그 수정*


*(팀에 이번 릴리스에 관한 최종 통계를 물어봤더니 'IDE 중단 21개, 버그 600개 이상'이라고 알려줬습니다. 필자는 그 의미를 좀 더 캐본 후에야 그 숫자는 중단/버그 *수정 사항*의 수를 뜻한다는 사실을 확인할 수 있었습니다. 휴.)

이번 릴리스에는 XML 편집 중에 자동 완성 기능 중에 중단 문제를 일으킨 이 버그에 대한 수정 사항도 포함됩니다. 이 문제는 커뮤니티에서 중대한 문제라는 사실이 분명해지면서 수정 사항 목록에 뒤늦게 추가되었고, 그 때문에 릴리스 발표가 2주일 정도 늦춰졌습니다. (중대한 문제가 있을 때는 꼭 알려주세요. 우리는 귀 기울여 듣고 세심히 살피며, 심지어 새 릴리스의 발표를 코앞에 둔 순간이라도 올바른 일 처리를 위해서는 발표 일정을 미루더라도 중대한 문제는 꼭 해결합니다!)
이번 릴리스에서는 주로 중대한 버그 수정 사항에만 초점을 맞췄지만, Instant Run(다시 작성해 'Apply Changes'로 이름을 바꿈), Data Binding, Annotation Processing을 비롯하여 기존 기능에 대해서도 꼭 필요한 점은 개선했습니다.
이 도구의 몇 가지 특정 개선 사항에 대해 더 자세한 내용을 알고 싶은 분은 팀원들이 이런 개선 사항에 관해 게시한 글을 읽어보시기 바랍니다.
개발팀은 Google I/O에서 (도구를 보다 효과적으로 사용하는 방법에 관한 팁과 함께) 도구 개선 사항에 대해 설명했습니다. 여기에서 (다양한 Android 세션과 함께) 그 내용을 다룬 프레젠테이션을 확인해 보세요. Jamal의 최근 기사에서 이번 릴리스에 대해 더 자세한 내용을 읽을 수도 있습니다.
마지막으로, 개발팀이 전체적인 '품질 개선'을 다한 게 아니라는 점을 말씀드리고 싶습니다. 우리가 3.5에서 Android Studio를 상당히 개선했다는 점은 분명하지만(더 빠르고, 부드럽고, 더욱 신뢰할 수 있음), 완벽하지 않다는 것도 잘 알고 있습니다. 그래서 개발팀이 다시 많은 사용자가 필요로 하는 기능을 개발하는 일로 되돌아가더라도 품질 개선 작업 역시 꾸준히 해나갈 것입니다. 향후 릴리스에서 품질 개선에 대해 더 보고해야 할 사항이 있으면 이곳에서 자세한 내용을 읽어보실 수 있을 것입니다.


I/O 세션 애플리케이션 소스 코드



I/O 앱 스케줄링 앱. 올해는 어떤 세션에 참석합니까? 잠깐, 끝난 세션이라고요? 벌써요?

우리는 최근 Google I/O 2019 Android 애플리케이션소스 코드를 발표했습니다. 여러분도 이미 사용해보셔서 아시는 앱입니다. 어떤 세션이 언제 진행되는지 알아보고, 가고 싶은 세션에 플래그를 지정하고, 많은 사람이 동시에 참석 등록을 하는 바람에 등록하지 못했더라도 나중에 YouTube에서 놓치지 않고 챙겨 보거나 다른 세션에 참석하는 동안 라이브 스트리밍으로 확인하고, 샌드박스에 있는 누군가와 실현 가능한 앱 아키텍처 및 최적의 IDE 구성에 대한 장시간의 대화에 몰두하고, 그러다가 스케줄을 완전히 날려먹고 모든 걸 놓치기도 하는 등, 다양한 경험을 선사했던 바로 그 앱 말입니다.
(사내에서는 'IO Sched'이라 부르는) I/O 앱은 많은 최신 모범 사례와 기술을 통합한 앱으로, Developer Relations 팀으로서는 주요 연간 프로젝트입니다. 예를 들어 올해의 앱에는 Gesture navigation, Dark theme, Navigation 및 Room Architecture Component를 모두 사용되었습니다.
그 취지는 개발자가 학습을 위해 결과 코드를 사용하고 스스로 애플리케이션을 개발할 수 있도록 하는 것이 목적입니다. 바라건대 I/O Schedule 장르에서 약간 더 폭을 넓힐 수 있다면 좋겠습니다. 그래서 우리가 코드를 게시한다면 편리하지 않을까요?
Android 개발자 블로그에 게시된 Takeshi Hagikura의 기사에서 애플리케이션에 관해 더 자세한 내용을 읽어보실 수 있습니다.



Android Motion 샘플


Animation Motion 샘플 앱

애니메이션은 사용자가 애플리케이션에서 일어나는 일을 이해하도록 돕는 데 무척 중요합니다. 하지만 알맞은 애니메이션 효과를 얻는 것은 까다로울 수 있습니다.
Yuichi Araki가 다양한 애니메이션 효과를 얻는 방법을 보여주기 위해 새로운 Android Motion 샘플을 작성했습니다. 이 샘플에는 transitions, shared-element transitions, physics animations 등을 보여주는 다양한 데모가 있습니다. 이런 접근 방식과 데모 코드를 사용하면 Material Motion 가이드라인을 더 쉽게 구현할 수 있을 것입니다.

프로의 팁: Android Studio 내에서 직접 Android Motion 샘플을 로드하세요.


대부분의 Android 샘플과 마찬가지로, Android Motion은 IDE에서 직접 사용할 수 있습니다. File -> New -> Import Sample…을 클릭한 다음 Animation 카테고리에서 찾을 수 있습니다.

애니메이션에 관한 이야기…



Nick ButcherMotional Intelligence: Build smarter animations라는 글을 게시했습니다. 기사 형식으로 자신이 올해 Google I/O에서 진행한 강연을 설명하는 글입니다. 개발자는 어떻게 반응형 프로그래밍 환경에서 애니메이션을 만들어야 할까요? 우리는 매끄럽고 연속적이며 재진입 가능한 모션을 만들기 위해 어떻게 해야 할까요?


자, 그 다음은…


이번 글은 여기까지입니다. Android Studio 3.5를 다운로드하세요! I/O 2019 앱을 사용해보세요! Material과 반응형 애니메이션 코드를 작성하세요! 그리고 곧 이곳으로 다시 와서 Android 개발자 커뮤니티의 다음 업데이트 정보를 확인하세요.


vFlat의 조감도
다운로드해 사용할 수 있는 모바일 스캐닝 앱이 많이 있지만, 대부분의 앱은 평평한 문서를 디지털화하는 데 초점을 맞추고 있어 책을 펼쳤을 때처럼 굴곡진 페이지는 올바로 스캔하지 못할 때가 많습니다. 텍스트가 인쇄된 곡면 페이지를 스캔할 때, 책이나 출판물의 제본을 아예 뜯어내어 스캔하거나 그냥 있는 그대로의 이미지를 스캔한 후 불편하나마 카메라에서 캡처한 굴곡진 텍스트 이미지를 읽어내는 방법 중에서 선택해야 할 것입니다.
그게 바로 VoyagerX에서 이 문제를 딥러닝으로 해결하기 위해 vFlat Android 앱을 개발한 이유입니다. vFlat 앱은 사용자가 곡면 페이지에 대해 걱정할 필요 없이 책을 쉽사리 스캔할 수 있도록 하는 것이 목표입니다. 또한 책 페이지의 경계를 자동으로 결정함으로써 사용자의 수동 입력을 줄이고자 합니다.


왼쪽: 일반 모바일 카메라로 캡처한 책의 곡면 페이지 이미지.  오른쪽: vFlat을 사용하여 같은 이미지를 스캔한 버전


사용자가 OCR(광학 문자 인식)을 통해 책 페이지를 찍은 사진에서 텍스트를 추출할 때 이 앱을 사용하면 편리합니다. 위의 ‘왼쪽’ 이미지에서 텍스트를 추출할 때, OCR은 일부 단어와 텍스트 줄이 너무 굴곡져 있기 때문에 올바로 인식하지 못합니다. 하지만 똑같은 OCR 기술을 '오른쪽' 이미지에 적용하면 훨씬 더 높은 인식 성공률을 보이면서 오류가 거의 없는 텍스트를 추출할 수 있습니다.
vFlat 앱 내에서 이미지 B에 대한 OCR 결과


vFlat 앱의 구축 방법

우리는 곡면의 책 페이지를 평탄하게 하는 딥 러닝 모델을 개발했으며, 최상의 사용자 환경을 제공하기 위해 이 모델을 모바일에서 실행하기로 결정했습니다. vFlat 앱 내부에는 사용자가 평탄해진 책 페이지를 실시간으로 볼 수 있는 '실시간 미리보기' 기능이 있습니다(20FPS 이상). 사용자가 이 앱을 사용하는 것만으로 스캔한 페이지를 실시간으로 평탄한 이미지로 미리 볼 수 있다면 각도와 프레임을 조정한 후 사진을 찍을 수 있을 것입니다.


vFlat의 실시간 미리보기 기능

우리는 모바일 앱에서 실시간 추론을 달성하기 위해 훈련한 모델을 최적화하고 하드웨어 가속의 이점을 활용했습니다. 처음부터 스스로 OpenGL을 사용해 추론 모듈을 구현할 생각이었으므로, GLSL(OpenGL Shading Language)로 모델의 계층을 구현할 준비를 하고 있었습니다.
다행히도, 우리는 TensorFlow Lite의 GPU 지원 기능을 발견해 이를 사용해보기로 했습니다(이 글을 작성하던 당시에 ‘tensorflow-lite-gpu’ 패키지 버전이 ‘org.tensorflow:tensorflow-lite-gpu:0.0.0-nightly’로 업데이트되었음). 우리는 이 모델의 라이트 버전을 내놓기 위해 가중치 수와 복잡한 연산(ops)을 줄이고 하드웨어 가속을 위해 TFLite GPU 델리게이트(delegate)를 활용했습니다.
GPU는 CPU보다 컴퓨팅 성능이 우수하고 대량의 병렬 작업 부하를 처리할 수 있기 때문에 심층신경망이 매우 잘 맞습니다. 하지만 모바일 GPU를 사용하는 게 간단한 일은 아니며, 바로 이 지점에서 TFLite GPU 델리게이트가 필요합니다.
TFLite GPU 델리게이트는 모바일 GPU를 위한 신경망 그래프를 최적화하고 비동기식으로 실행되는 컴퓨트 셰이더(compute shader)를 생성하고 컴파일합니다. TFLite GPU 델리게이트 덕분에 자체 하드웨어 가속 추론 모듈을 구현할 필요가 없어져 수개월의 개발 시간을 절약했습니다.
TFLite GPU 델리게이트를 사용한 덕분에 시간과 에너지를 아꼈지만, 자체 모델을 TFLite 모델로 변환하고 이를 TFLite GPU 델리게이트와 통합하는 문제에 부딪혔습니다. GPU 델리게이트의 시험용 버전은 주로 MobileNet에 사용되는 연산만 지원하고 우리가 만든 모델의 일부 연산은 지원하지 않았습니다.
모델의 성능을 저하시키지 않고 GPU 델리게이트를 활용하기 위해, 우리는 전체적인 네트워크 구조는 그대로 똑같이 유지하면서도 일부 연산을 바꾸어야 했습니다. 우리는 변환 과정 중에 어려움을 겪었습니다. 그 시점에는 아직 소스 코드가 공개되지 않았으므로 우리가 만난 오류의 원인을 정확히 짚어내기 힘들었습니다. (TFLite GPU 델리게이트를 위한 코드는 현재 GitHub에 공개되어 있음)
예를 들어 TFLite GPU 델리게이트는 LeakyReLU 연산을 지원하지 않으므로, 우리는 그 대신에 지원되는 PReLU 연산을 다음과 같은 방식으로 활용해야 했습니다.


변경 전:
> tf.keras.layers.LeakyReLU(alpha=0.3)
변경 후:
> tf.keras.layers.PReLU(alpha_initializer=Constant(0.3), shared_axes=[1, 2], trainable=False)


하지만 PReLU 연산의 파라미터를 모든 축에 공유하여(shared_axes=[1,2,3]) 파라미터를 1개로 줄이려 할 때 예기치 않은 동작이 발생했습니다. 이 코드는 CPU 모드에서는 훌륭하게 작동했지만, GPU 델리게이트는 ‘Linear alpha shape does not match the number of input channels’(선형적인 알파의 크기가 입력 채널 수와 일치하지 않음)라는 오류 메시지와 함께 실패했습니다. 이런 이유 때문에 결국은 축 1과 2만 파라미터를 공유했습니다.
우리가 겪은 또 다른 문제는 네트워크의 Lambda 층에서 입력 데이터를 -1과 1 사이로 정규화할 때 발생했습니다.
> tf.keras.layers.Lambda(lambda x : (x / 127.5) — 1.0)
TFLite 컨버터로 변환한 Lambda 층의 시각화
이 코드는 GPU 델리게이트에서 작동할 것처럼 보이지만, 실제로 실행해보면 경고 없이 CPU로 대체됩니다. 일반적으로 이런 문제가 발생할 때 TFLite는 'Failed to apply delegate. Only the first M ops will run on the GPU, and the remaining N on the CPU'(델리게이트를 적용하지 못했습니다. 처음 M개의 연산만 GPU에서 실행되고 나머지 N개의 연산=은 CPU에서 실행됩니다.)와 같은 경고 메시지를 표시합니다. 따라서 Lambda 계층을 사용할 때는 주의를 기울여 항상 실제 추론 시간을 측정한 후에 처리해야 합니다.

결론

다양한 Android 기기에 구현한 vFlat 모델의 CPU 추론 시간 대비 GPU 평균 추론 시간


지금까지 여러 가지 장애물이 있었지만, 우리는 TFLite GPU 델리게이트를 사용해 모델의 추론 시간을 절반 이상 단축했습니다. 마침내 평탄한 페이지가 실시간으로 표시되는 '실시간 미리보기' 기능을 사용자에게 제공할 수 있었습니다.
TFLite GPU 델리게이트 사용이 훌륭한 선택이었음을 자신있게 말씀드릴 수 있으며, 휴대기기에 훈련한 모델을 배포하려는 개발자는 TFLite GPU 델리게이트를 사용해보실 것을 적극 추천합니다.

자세히 알아보고 직접 사용해보려면 TensorFlow Lite GPU 델리게이트를 읽어보세요.




tl;dw?

이 32분짜리 동영상을 시청할 마음이 없는 분도 계실 것이므로, 프레젠테이션에서 다룬 주제를 글로 적어 소개해 드립니다. ☕️

#AnimationsMatter

애니메이션은 앱의 사용성에 중요한 역할을 한다고 생각합니다. 애니메이션으로 상태 변화나 전환을 설명하거나, 공간 모델을 확립하거나, 주의를 끌 수 있기 때문입니다. 애니메이션은 사용자가 앱을 올바로 이해하고 원하는 화면으로 이동하는 데 도움이 됩니다.
앱에서의 애니메이션 흐름
👈 애니메이션이 있는 경우와 없는 경우 👉
위 예제는 애니메이션 여부를 제외하고는 동일한 사용 흐름입니다. 애니메이션이 없으면 무엇이 바뀌었는지에 대한 설명 없이 상태가 갑자기 바뀐다는 느낌이 듭니다.


애니메이션이 중요하다고는 생각하지만, 현대 앱의 아키텍처 방식이 변화함에 따라 애니메이션을 구현하기 더 어려워지고 있다는 생각도 듭니다. 뷰 계층 밖에 있는 대부분의 상태 관리를 (ViewModel과 같은) 컨트롤러로 이동하고 있는데, 이런 컨트롤러는 뷰를 렌더링하는 데 필요한 앱의 현재 상태를 캡슐화하는 상태 객체(예: UiModel)를 게시합니다. 예를 들어 네트워크 요청이나 사용자가 시작한 작업이 완료될 때처럼, 데이터 모델에서 뭔가가 바뀔 때마다 업데이트된 전체 상태를 캡슐화하는 새 UI 모델을 게시합니다.
상태 객체의 스트림을 게시하는 ViewModel
상태 객체의 스트림을 게시하는 ViewModel


오늘은 이 패턴이나 그 이점에 초점을 맞추고 싶지 않습니다.이 주제에 관해서는 훌륭한 자료가 많이 있습니다. 단방향 데이터 흐름이나 MVI를 찾아보거나  MvRx 또는 Mobius와 같은 라이브러리를 찾아보세요. 반대로 뷰가 모델 스트림을 관찰하고, UI에 바인딩하는 스트림의 다른 쪽 끝 부분에 초점을 맞추고 싶습니다. 이 부분은 UI에 완전히 바인딩되는 어떤 새로운 상태가 주어지는 순수한 함수 같은 것입니다. UI의 현재 상태에 대해서는 생각하고 싶지 않습니다. 즉, UI에 대한 데이터 바인딩이 상태 비추적(Stateless) 이어야 합니다. 하지만 애니메이션은 상태 저장(Stateful)입니다. 애니메이션은 시간의 경과에 따라 하나의 값에서 다른 값으로 이동하는 것입니다. 이 점에서 본질적이 상충이 있습니다. 요즘 이런 이유로 많은 앱에서 애니메이션이 제거되어 실제로 사용성의 손실로 이어지고 있다는 점이 걱정스럽습니다.


무엇이 문제일까요?

우리가 이런 반응형 환경에서 애니메이션을 유지할 방법과 해결해야 할 과제를 구체적으로 파악하기 위해 살펴볼 수 있는 아주 작은 본보기가 있는데, 바로 로그인 화면입니다.
로그인 버튼과 진행률 표시기가 페이드 인/페이드 아웃 방식으로 나타나거나 사라지는 로그인 화면
로그인 버튼과 진행률 표시기가 페이드 인/페이드 아웃 방식으로 나타나거나 사라지는 로그인 화면

사용자가 로그인 버튼을 누르면 로그인 버튼을 숨기고 진행률 표시기를 표시하고 싶지만, 이때 로그인 버튼을 페이드 아웃하고 진행률 표시기를 페이드 인하는 방식으로 처리하고 싶다고 해봅시다.
이 화면과 (정적) 바인딩 로직에 대한 상태 객체는 다음과 같은 모습일 수 있습니다.

이런 변화를 애니메이션 으로 표현하고 싶을 경우 최초의 시도는 다음과 같은 모습일 수 있으며, 여기서 알파 속성을 애니메이션하고 마지막으로 페이드 아웃하는 경우 표시 유형 값을 설정합니다.
하지만 이는 다음과 같이 예기치 않은 결과로 이어질 수 있습니다.


반응형 앱에 애니메이션을 추가할 때 발생할 수 있는 문제
반응형 앱에 애니메이션을 추가할 때 발생할 수 있는 문제


여기서는 키를 누를 때마다 새로운 UI 모델이 게시되지만, 예상과는 달리 진행률 표시기가 계속 표시되는 것을 볼 수 있습니다! 또는 (데모를 위해 애니메이션 지속 시간을 과하게 늘린 상태에서) 제출 버튼을 누르면 결국 버튼과 진행률 표시기가 둘 다 사라져버리는 잘못된 상태로 이어질 수 있습니다. 이는 애니메이션에 올바로 처리되지 않는 종료 리스너와 같은 역효과가 있기 때문입니다.


이 반응형 환경에서 애니메이션을 작성할 때 애니메이션 코드에 필요한 몇 가지 특성이 있습니다. 이런 특성을 다음과 같이 분류해봤습니다.
  • 재진입
  • 연속
  • 매끄러움


재진입

재진입은 애니메이션을 언제든지 중단했다가 다시 호출할 수 있어야 한다는 의미입니다. 새로운 상태 객체가 게시되는 경우, 애니메이션이 실행 중인 동안에도 새로운 상태에 맞는 애니메이션이 바인딩될 수 있도록 애니메이션을 준비해야 합니다. 이를 위해서는 실행 중인 애니메이션을 취소 또는 다시 타게팅할 수 있거나, 잠재적인 역효과(예: 리스너)를 모두 없앨 수 있어야 합니다.


연속

연속성은 애니메이션되는 값에 갑작스러운 변화가 생기지 않도록 하는 것입니다.이 속성을 입증하려면 누르거나 놓을 때 크기와 색상에 애니메이션 효과를 주는 뷰를 고려하세요.
누를 때 크기와 색상에 애니메이션 효과 부여
누를 때 크기와 색상에 애니메이션 효과 부여


애니메이션을 끝까지 실행하면 모든 모습이 보기 좋지만, 그러지 않고 빠르게 탭하면 애니메이션의 크기와 색상이 불연속적으로 변합니다. 이는 바인딩 코드에서 가정을 한 결과입니다(예: 페이드 애니메이션은 항상 0 알파에서 시작하는 것으로 가정함).


매끄러움

이 속성을 이해하려면 다음과 같이 뷰가 어떤 이벤트에 대한 반응으로 상단 왼쪽이나 오른쪽으로 움직이는 예를 생각해보세요.
더듬거리는 애니메이션
더듬거리는 애니메이션


뷰를 상단 오른쪽으로 두 차례 연속으로 빠르게 보내는 경우 뷰가 중간에서 멈춘 후에 목적지를 향해 계속 천천히 움직입니다. 이동 중간에 목적지를 바꾸면 뷰가 다시 멈추었다가 갑자기 방향을 바꿉니다. 이런 종류의 갑작스러운 정지나 방향 변화는 부자연스러워 보입니다. 현실 세계에서는 이런 식으로 움직이지 않기 때문입니다. 따라서 애니메이션을 계속 매끄럽게 유지하려면 이런 종류의 동작을 피하도록 해야 합니다.


Fixme

이제 바인딩 함수로 돌아가서 이런 문제를 수정해 봅시다. 먼저 연속성을 살펴봅시다. 알파 애니메이션은 항상 초기값부터 최종값까지(예: 페이드 인을 위해 0부터 1까지) 실행된다는 점을 알 수 있습니다. 그 대신에 초기값을 생략하고 최종값만 제공할 수 있습니다.

초기값을 생략하면 애니메이터가 현재 값을 읽고 그곳부터 시작합니다.이것이 바로 정확히 우리가 원하는 바로, 애니메이션 처리되는 속성에서 갑작스러운 변화를 방지해 줍니다.
이제는 함수를 재진입 가능하게 만들어 언제든 안심하고 다시 호출할 수 있도록 해봅시다.먼저 수행할 필요가 없는 작업은 어떤 것도 수행하지 않도록 해두면 안심할 수 있을 것입니다. 뷰가 이미 목표 값에 있는 경우에는 일찍 돌아올 수 있습니다.

다음으로, 실행 중인 애니메이터와 리스너를 저장하여 새 애니메이션을 시작하기 전에 이들을 취소할 수 있도록 해야 합니다. 이를 저장할 논리적 위치는 뷰 자체 내에 있지만, View는 이미 이를 수행하기 위한 편리한 메커니즘인ViewPropertyAnimator를 제공합니다. 이 메커니즘은 View.animate()에 대한 호출로 반환되는 객체로, 어떤 속성에 대해 새 애니메이션을 시작할 경우 그 속성에서 현재 실행 중인 모든 애니메이션을 자동으로 취소합니다. 대단하죠! 또한 ViewPropertyAnimator는 애니메이션이 끝까지 정상적으로 실행되는 경우에만 실행되고 애니메이션이 취소되는 경우에는 실행되지 않는 withEndAction 메서드도 제공합니다.이 역시 정확히 우리가 원하는 동작으로, 새로 입력된 목표 값으로 인해 애니메이션이 취소되는 경우 어떤 역효과(예: 표시 유형 변경)도 실행되지 않을 것이라는 뜻입니다. ViewPropertyAnimator로의 전환은 함수를 재진입 가능하게 만듭니다.
ViewPropertyAnimator는 같은 속성에 대해 실행 중인 애니메이션을 취소하고 새 애니메이션을 시작할 것이라 말씀드렸습니다. 이는 부드러움 속성에 위배되고 앞서 본 것처럼 한 애니메이션이 갑자기 멈추고 다른 애니메이션이 시작되는 부자연스러운 애니메이션 문제로 이어질 수 있습니다(거리는 더 짧아지지만 지속 시간은 같음). 이 문제를 해결하기 위해 애니메이션 라이브러리를 살펴볼 수 있는데, 이 라이브러리에 익숙한 개발자가 많지 않다고 생각합니다.


Springterlude

스프링은 ‘동적 애니메이션’ Jetpack 라이브러리의 일부입니다. 많은 사람이 매우 탄력있는 애니메이션 예시를 보면서 이 라이브러리를 그냥 건너뛰었을지도 모른다고 생각합니다. 이 효과를 유용하게 활용할 수도 있지만, 늘 필요하거나 바람직한 것은 아니라서 그럴 겁니다. 하지만 이런 탄력성은 비활성화하고 애니매이션을 중단하고 다시 시작하는 기능 및 일반적인 애니메이션에 유용한 여러 가지 속성을 가진 물리 애니메이션 시스템으로 사용할 수 있습니다. 


이전의 예로 다시 돌아가 스프링 애니메이션으로 다시 구현하면 부드러움 문제가 생기지 않는다는 점을 확인할 수 있습니다. 대신에 목적지 변경과 반복되는 시작 문제를 처리하여 현재의 속도를 지킴으로써 부드러운 애니메이션을 생성합니다.
리타게팅 시 속도를 유지하는 스프링 기반 애니메이션
리타게팅 시 속도를 유지하는 스프링 기반 애니메이션


SpringAnimation 작성은 일반적인 애니메이터와 많은 면에서 흡사해 보이는데, start()를 호출하는 게 아니라 animateToFinalPosition 메서드를 사용하는 점이 큰 장점입니다. 이 메서드는 애니메이션이 아직 시작되지 않은 경우 애니메이션을 시작하는 역할을 하지만, 애니메이션이 실행 중인 경우에는 애니메이션을 새 목적지로 리타게팅하여 갑자기 변경하지 않고 모멘텀을 유지한다는 점이 중요합니다.


View.animate처럼 스프링을 사용하기에 편리한 View API가 없다는 점이 아쉽긴 하지만(Jetpack에서만 지원됨), 다음과 같이 확장 함수로 빌드해 사용할 수 있습니다.
이렇게 하면 주어진 ViewProperty(평행이동, 회전 등)에 대한 스프링을 생성하거나 불러와 뷰의 태그에 저장할 수 있습니다.그러면 손쉽게 animateToFinalPosition 메서드를 사용하여 실행 중인 애니메이션을 업데이트할 수 있습니다.다음과 같이 이를 표시 유형 바인딩 함수에 사용합니다.


또한 종료 작업에서 스프링 애니메이션 종료 리스너를 사용하도록 전환해야 합니다. 이 gist에서 이에 대한 전체 코드를 찾을 수 있습니다. 또한 애니메이션을 어느 정도 구성할 수 있었으면 하는 생각도 들 것입니다. 지속 시간과 보간기를 지정하는 일반 애니메이션과는 달리, 스프링은 강성 및 감쇠비 설정을 통해 구성할 수 있습니다. 호출 사이트를 쉽게 구성할 수 있도록 하되, 실용적인 기본값을 제공하기 위해 적절한 매개변수를 허용하도록 확장 함수를 개선할 수 있습니다. 좀 더 완전한 구현에 대한 설명은 여기를 참조하세요.


바인딩 함수가 재진입, 연속성, 매끄러움이라는 특성을 지니도록 만들었습니다.이런 목적을 달성하기 위해 이 모든 바인딩 함수가 관련되어 있는 것처럼 보일 수 있지만, 실제로는 그중 몇 가지만 있으면 애플리케이션 전체에 사용할 수 있습니다. 여기에서 이 스프링 기법을 쉽게 사용할 수 있도록 패키지로 묶은 라이브러리를 찾을 수 있습니다.


Item Animator

이런 종류의 애니메이션을 사용하는 또 다른 예를 살펴봅시다. 이번에는 이런 원리를 RecyclerView.ItemAnimator에 적용해 봅니다.
DefaultItemAnimator와 스프링 기반 ItemAnimator
👈 DefaultItemAnimator와 스프링 기반 ItemAnimator 👉


이 예에서는 애니메이션이 셔플 버튼을 사용하여 실행 중인 동안 데이터 세트에 발생하는 업데이트를 시뮬레이션합니다. 버튼을 두 번 연속으로 빠르게 누를 때 스프링 기반 애니메이터의 매끄러움이 큰 차이를 만들어낸다는 점에 주목하세요.왼쪽에서는 상자가 잠시 멈춘 다음에 방향을 바꾸지만,오른쪽에서는 매끄럽게 방향을 바꿉니다. 대부분의 앱이 아마도 네트워크의 많은 소스로부터 정보를 로드하여 RecyclerView에 표시할 것이라 장담합니다.이런 종류의 유연한 애니메이션을 사용하면 애플리케이션의 완성도가 더욱 높아져 훨씬 더 부드러운 사용 환경을 만들어 줄 것입니다. 여기에서 Plaid 샘플에 이런 종류의 애니메이터를 추가하는 PR을 찾을 수 있습니다.

더욱 똑똑한 애니메이션

이 글에서 설명한 원리가 반응형 앱에서 자연스럽게 애니메이션을 적용해 사용성을 개선하는 데 도움이 되기를 바랍니다. 실제로 설명한 원리에는 중요도에 차이가 있습니다..


재진입은 정확성에 관한 속성입니다. 이 속성이 없으면 애니메이션이 단절될 수 있습니다.ViewPropertyAnimator를 사용하거나 애니메이션 코드에 주의를 기울여 애니메이션이 중단되었다가 다시 호출될 수도 있는 상황을 적절히 처리하세요.