본 포스팅은 DroidKaigi 2017 ~ How to implement material design animation 을 기본으로 번역하여 작성했습니다
제 일본어 실력으로 인하여 오역이나 오타가 발생할 수 있습니다.
How to implement material design animation
takahirom
Android가 좋다
“one of the main components of what makes material, material.”
왜 애니메이션 시키는가?
Material Design Guidelines으로부터
생각할 것을 줄이고 알기쉽게
어떻게 Material Design으로 애니메이션 시킬까?
Material Design Guidelines으로부터
DroidKaigi 2017 official Android app으로부터
어떻게 구현할까?
material-element
Material Design Guidelines을 기준으로 작성
하나씩 가이드라인부터 따라가보자!
9개의 장으로 구성되어있다
Elevation & shadows
그림자에 주목
TAP시에 떠올라 보인다
DroidKaigi 2017 official Android app으로부터
Resting state | Pressed state | |
---|---|---|
Floating Action Button | 6dp | 12dp |
Button | 2dp | 8dp |
Card | 2dp | 8dp |
TAP시에 View를 6dp 올라간다
6dp = dynamic elevation offsets
Layout XML 내부
<ImageView
android:id="@+id/shot"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:elevation="@dimen/z_card"
android:stateListAnimator="@animator/raise"
android:foreground="@drawable/mid_grey_ripple"
app:badgeGravity="end|bottom"
app:badgePadding="@dimen/padding_normal" />
stateListAnimator를 쓰자
API Level 21 (Android 5.0)
(stateListAnimator는 API Level 21 미만에서 무시된다)
animator_raise.xml
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item
android:state_enabled="true"
android:state_pressed="true">
<objectAnimator
android:duration="@android:integer/config_shortAnimTime"
android:propertyName="translationZ"
android:valueTo="@dimen/touch_raise" />
</item>
<item>
<objectAnimator
android:duration="@android:integer/config_shortAnimTime"
android:propertyName="translationZ"
android:valueTo="0dp" />
</item>
</selector>
animator_raise.xml
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item
android:state_enabled="true"
android:state_pressed="true">
누르고 있을 때의 애니메이션을 지정한다
animator_raise.xml
<objectAnimator
android:duration="@android:integer/config_shortAnimTime"
android:propertyName="translationZ"
android:valueTo="@dimen/touch_raise" />
translationZ 을 6dp로 지정한다
2dp (기존의 Elevation) + 6dp (dynamic elevation offsets) = 8dp 가 된다
Duration & easing
Dynamic durations
거리가 길수록 애니메이션 시간이 길다
Duration & easing
EasingCurves는 애니메이션의 속도나 투명도, 크기 등에 적응된다
어디에서 사용할까 | |
---|---|
표준 커브 Standard curve | 화면 내의 운동 |
가속 커브 Deceleration curve | 화면 내에 들어오는 운동 |
가속 커브 Acceleration curve | 화면에서 나갈 때 운동 |
급커브 Sharp curve | 언제라도 화면에 넣는 객체의 운동 |
어떻게 구현할까 | |
---|---|
표준 커브 Standard curve | FastOutSlowInInterpolator |
가속 커브 Deceleration curve | LinearOutSlowInInterpolator |
가속 커브 Acceleration curve | FastOutLinearInInterpolator |
급 커브 Sharp curve | PathInterpolatorCompat.create(0.4f, 0, 0.6f, 1); |
ViewPropertyAnimator를 사용한 지정 방법
view.animate()
.translationX(100)
.setDuration(290)
.setInterpolator(new FastOutSlowInInterpolator())
.start();
API Level 14
Movement
현실 세계와 동일하게 아래쪽으로 중력이 있으므로 아래쪽이 불룩한 형태
가 된다
Intent intent = new Intent(context, TransformingActivity.class);
Bundle options = ActivityOptionsCompat.makeSceneTransitionAnimation(
context, fromView, getString(R.string.transition_name)).toBundle();
ActivityCompat.startActivity(context, intent, options);
ActivityOptionsCompat으로 이동 전 View와 이동 후 View를 묶어서 Activity 시작
Layout
<ImageView
android:layout_width="math_parent"
android:layout_height="wrap_content"
android:transitionName="@string/transition_name"/>
레이아웃에서 transitionName을 지정
Theme
<style name="AppTheme.NoActionBar.Detail">
<item name="android:windowSharedElementEnterTransition">@transition/default_share</item>
<item name="android:windowSharedElementReturnTransition">@transition/default_share</item>
</style>
테마에서 Transition을 지정
Transition
<?xml version="1.0" encoding="utf-8"?>
<transitionSet ..
android:duration="350"
android:interpolator="...">
<changeBounds/>
<pathMotion class="*.GravityArcMotion" />
</transitionSet>
changeBounds로 이동 + 크기를 변화
pathMotion을 지정, 이걸로 타원형이 된다!
pathMotion을 이용한다. 하지만 표준 ArcMotion 클래스를 이용하면 위쪽으로 불룩한 애니메이션으로 부적절한 움직임이 된다
// X Transition 지정 사례
<changeBounds>
<arcMotion android:minimumHorizontalAngle="15"
android:minimumVerticalAngle="0"
android:maximumAngle="90"/>
</changeBounds>
API Level 21
https://developer.android.com/reference/android/transition/ArcMotion.html
<?xml version="1.0" encoding="utf-8"?>
<transitionSet ..
android:duration="350"
android:interpolator="...">
<changeBounds/>
<pathMotion class="*.GravityArcMotion" />
</transitionSet>
※ Plaid는 Google의 Material Design의 오픈 소스 참고 구현이며 Design 뉴스 앱
material-element/GravityArcMotion.java
Transforming material
탭한 곳부터 원을 그리는 것처럼 표시된다
SharedElementTransition으로 Custom Transition
을 이용한다
TransformingMaterialActivity
Intent intent = new Intent(TransformingActivity.this, LoginActivity.class);
...
ActivityOptionsCompat optionsCompat = ActivityOptionsCompat
.makeSceneTransitionAnimation(context,
fab,
getString(R.string.transition_name_login));
ActivityCompat.startActivity(context,
intent,
optionsCompat.toBundle());
ActivityOptionsCompat으로 이동 전 View와 이동 후 View를 묶어서 Activity 시작
LoginActivity
<FrameLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:onClick="dismiss">
<LinearLayout
android:id="@+id/container"
android:transitionName="@string/transition_name_login"
...
레이아웃에서 transitionName을 지정
LoginActivity
final FabTransform sharedEnter = new FabTransform(color, icon);
activity.getWindow().setSharedElementEnterTransition(sharedEnter);
FabTransform 라는 Custom Transition을 지정
FabTransform.java
public class FabTransform extends Transition {
@Override
public void captureStartValues(TransitionValues transitionValues) {
// 처음 위치를 저장
captureValues(transitionValues);
}
@Override
public void captureEndValues(TransitionValues transitionValues) {
// 종료 위치를 저장
captureValues(transitionValues);
}
private void captureValues(TransitionValues transitionValues) {
final View view = transitionValues.view;
if (view == null || view.getWidth() <= 0 || view.getHeight() <= 0) return;
// transitionValues 에 위치를 저장
transitionValues.values.put(PROP_BOUNDS, new Rect(view.getLeft(), view.getTop(),
view.getRight(), view.getBottom()));
}
@Override
public Animator createAnimator(final ViewGroup sceneRoot,
final TransitionValues startValues,
final TransitionValues endValues) {
// 만든 Bounds를 취득
final Rect startBounds = (Rect) startValues.values.get(PROP_BOUNDS);
final Rect endBounds = (Rect) endValues.values.get(PROP_BOUNDS);
...
final View view = endValues.view;
// Bounds를 사용해 CircularReveal를 만든다
circularReveal = ViewAnimationUtils.createCircularReveal(view,
view.getWidth() / 2,
view.getHeight() / 2,
startBounds.width() / 2,
(float) Math.hypot(endBounds.width() / 2, endBounds.height() / 2));
...
final AnimatorSet transition = new AnimatorSet();
transition.playTogether(circularReveal, translate, colorFade, iconFade);
transition.playTogether(fadeContents);
...
// 이동 전의 View Bound와 이동 후의 Bound를 사용해 Animator를 만든다
return new AnimatorUtils.NoPauseAnimator(transition);
}
}
Choreography
API Level 21
시작 시간 | 종료 시간 | |
---|---|---|
Card 너비 | 0ms | 275ms |
Card 높이 | 30ms | 375ms |
SharedElement x 위치 | 0ms | 275ms |
SharedElement y 위치 | 30ms | 375ms |
표시되는 View의 투명도 | 75ms | 225ms |
Fab가 퍼지는 애니메이션과 동일하게 Shared Element Transition을 이용한다
높이와 너비 애니메이션의 시작 시간의 차이를 표현하기위해 CustomTransition을 작성
AnimatorSet animatorSet = new AnimatorSet();
...
widthAnim.setDuration(275);
heightAnim.setStartDelay(30);
heightAnim.setDuration(345);
animatorSet.playTogether(widthAnim, heightAnim);
API Level 21
Shared Element Transition을 사용한 패턴이 많으므로 좀 더 깊이 파봅니다
Activity A -> B로 이동하는 경우
Activity A | — startActivity —> | Activity B |
---|---|---|
ExitTransition | EnterTransition | |
SharedElementExitTransition | SharedElementEnterTransition |
하나의 전환에 4개의 Transition이 움직인다
API Level 21
Activity B -> A로 돌아오는 경우
Activity A | <— Back 키 — | Activity B |
---|---|---|
ReenterTransition | ReturnTransition | |
SharedElementReenterTransition | SharedElementReturnTransition |
하나의 전환에 4개의 다른 Transition이 움직인다
API Level 21
지금 어느 Transition이 움직이고 있나?
→ 모든 Transition 및 WindowAnimation을 출력
샘플 앱의 메뉴로부터 “Debug”로 활성화할 수 있다
→ 개발자용 옵션으로 Animator 재생시간 비율을 변경한다
→ BlinkDebugTransition 클래스를 이용해 Transition이 움직이고 있는지 확인해보면 좋다
<transitionSet>
<targets>
<target android:targetId="@id/image" />
</targets>
<transition class="*.BlinkDebugTransition" />
</transitionSet>
(AppCompatTheme 등 부모 Theme가 Theme.Material이면 문제 없다)
<item name="android:windowContentTransitions">true</item>
복잡하므로, 샘플 앱에서도 아직 완벽하게 움직이지 않고 솔직히 꽤 힘들다
ViewAnimationUtils.createCircularReveal로 한다
Transition을 사용하면 Pause시에 Crash (OperationNotSupportedException) 되므로, NoPauseAnimator Class 등을 만들어 Wrapper 해서 사용한다
API Level 21
참고 : https://halfthought.wordpress.com/2014/11/07/reveal-transition/
Creative customization
구체적인 도입 방법에 대해서 설명하겠습니다
※ Path 변경에 따른 Animation (PathMorth) 은 대응 API Level이 21이므로 주의한다
애니메이션 방법은 아래에 있습니다.
Qiita: 애니메이션 아이콘을 만든다
Android Icon Animator를 다뤄본다
API Level 14
Module의 build.gradle에 활성화한다
android {
...
defaultConfig {
...
vectorDrawables.useSupportLibrary true
}
}
API Level 14
Export해서 VectorDrawable 파일을 적는다
res/draweable 폴더에 넣는다
API Level 14
AppCompatActivity를 사용하는 Layout으로 ImageView에 app:srcCompat으로 이미지를 설정
<ImageView
app:srcCompat="@drawable/avd_menu_back_menu_to_back"
.../>
API Level 14
나머지는 ImageView에서 Drawable를 얻어서 start() 메소드를 호출하면 OK
샘플내의 CreativeCustomizationActivity.java
((Animatable) imageView.getDrawable()).start();
API Level 14
Android 4.x와 Android 5.x와 Android 7.x에서 동작을 확인할 것
startOffset을 넣으면 Android 4.x에서 Animation 안되므로, 대안으로 아무것도 하지 않는 Animation을 중간에 넣어 속인다
참고 : Android Open Source Projct의 이슈
Animation 지정이 안좋으면 targetSDK N 이상이면 Crash가 발생하므로 주의
API Level 14
Loading images
샘플의 AnimatorUtils.java
final ObservableColorMatrix cm = new ObservableColorMatrix();
ObjectAnimator saturation = ObjectAnimator.ofFloat(cm, ObservableColorMatrix.SATURATION, 0f, 1f);
saturation.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator valueAnimator) {
imageView.setColorFilter(new ColorMatrixColorFilter(cm));
}
});
ImageView#setColorFilter를 사용한 ColorFilter에는 setSaturation() 메소드가 있으므로 애니메이션 프레임마다 호출한다
Plaid 코드로부터
API Level 14
Navigational transitions
Elevation 변화
하는 것으로 부모 요소로부터 자식 요소에 포커스 변화를 나타낸다그림자가 점점 짙어진다
@Override
public Animator createAnimator(ViewGroup sceneRoot, TransitionValues startValues, TransitionValues endValues) {
return ObjectAnimator.ofFloat(endValues.view, View.TRANSLATION_Z, initialElevation, finalElevation);
}
API Level 21
전부 훑어봤습니다
현재 상황
Data collected during a 7-day period ending on February 6, 2017.
Dashboards : https://developer.android.com/about/dashboards/index.html
기본적으로 현재로는 상당히 어렵다
타당한 라인일지도?
균형있게 도입하면 좋을 것 같다
Material Design 애니메이션은 가능합니다
comments powered by Disqus
Subscribe to this blog via RSS.
LazyColumn/Row에서 동일한 Key를 사용하면 크래시가 발생하는 이유
Posted on 30 Nov 2024