Android 머티리얼 디자인 구현 실전: UI 요소부터 부드러운 애니메이션까지

머티리얼 디자인의 핵심 개념

Android 5.0(API 21)에서 도입된 머티리얼 디자인은 플랫 디자인의 단순함과 물리적 세계의 깊이감을 결합한 시각 언어입니다. 이 디자인 체계는 종이와 잉크라는 메타포를 기반으로 하며, 그림자, 레이어, 의미 있는 모션을 통해 계층 구조와 상호작용을 표현합니다.

핵심 컴포넌트 구현

앱 바 구성: Toolbar 활용

기존 ActionBar를 대체하는 Toolbar는 유연한 커스터마이징이 가능합니다. 아래는 DrawerLayout과 연동하는 기본 구성입니다.

<androidx.coordinatorlayout.widget.CoordinatorLayout
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <com.google.android.material.appbar.AppBarLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content">

        <androidx.appcompat.widget.Toolbar
            android:id="@+id/appBar"
            android:layout_width="match_parent"
            android:layout_height="?attr/actionBarSize"
            android:background="?attr/colorPrimary"
            app:layout_scrollFlags="scroll|enterAlways"/>
    </com.google.android.material.appbar.AppBarLayout>

    <androidx.recyclerview.widget.RecyclerView
        android:id="@+id/contentList"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        app:layout_behavior="@string/appbar_scrolling_view_behavior"/>

</androidx.coordinatorlayout.widget.CoordinatorLayout>

Java/Kotlin에서 툴바 설정:

private void setupAppBar() {
    Toolbar appBar = findViewById(R.id.appBar);
    setSupportActionBar(appBar);
    
    DrawerLayout sideMenu = findViewById(R.id.sideMenu);
    ActionBarDrawerToggle toggle = new ActionBarDrawerToggle(
        this, sideMenu, appBar, 
        R.string.menu_open, R.string.menu_close
    );
    sideMenu.addDrawerListener(toggle);
    toggle.syncState();
}

리스트 구현: RecyclerView 최적화

RecyclerView의 성능을 극대화하려면 ViewHolder 패턴과 효율적인 레이아웃 매니저 선택이 중요합니다. 다음은 커스텀 애니메이터를 적용한 예시입니다.

public class SlideInAnimator extends DefaultItemAnimator {
    
    @Override
    public boolean animateAdd(RecyclerView.ViewHolder holder) {
        final View item = holder.itemView;
        item.setTranslationY(item.getHeight());
        item.setAlpha(0f);
        
        item.animate()
            .translationY(0f)
            .alpha(1f)
            .setDuration(250)
            .setInterpolator(new FastOutSlowInInterpolator())
            .setListener(new AnimatorListenerAdapter() {
                @Override
                public void onAnimationEnd(Animator anim) {
                    dispatchAddFinished(holder);
                }
            })
            .start();
        
        return true;
    }

    @Override
    public boolean animateRemove(RecyclerView.ViewHolder holder) {
        holder.itemView.animate()
            .translationX(holder.itemView.getWidth())
            .alpha(0f)
            .setDuration(200)
            .setListener(new AnimatorListenerAdapter() {
                @Override
                public void onAnimationEnd(Animator anim) {
                    dispatchRemoveFinished(holder);
                }
            })
            .start();
        return true;
    }
}

적용 방법:

RecyclerView contentList = findViewById(R.id.contentList);
contentList.setItemAnimator(new SlideInAnimator());
contentList.setLayoutManager(new LinearLayoutManager(this));

카드 레이아웃: CardView 심화

CardView는 elevation 속성으로 그림자를 생성하며, contentPadding으로 내부 여백을 조정합니다.

<androidx.cardview.widget.CardView
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:layout_margin="8dp"
    app:cardCornerRadius="12dp"
    app:cardElevation="4dp"
    app:cardBackgroundColor="@android:color/white"
    android:foreground="?attr/selectableItemBackground">

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:orientation="horizontal"
        android:padding="16dp">

        <ImageView
            android:id="@+id/thumbImg"
            android:layout_width="64dp"
            android:layout_height="64dp"
            android:scaleType="centerCrop"/>

        <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:orientation="vertical"
            android:layout_marginStart="16dp">

            <TextView
                android:id="@+id/titleTxt"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:textAppearance="?attr/textAppearanceHeadline6"/>

            <TextView
                android:id="@+id/descTxt"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:textAppearance="?attr/textAppearanceBody2"
                android:alpha="0.7"/>
        </LinearLayout>
    </LinearLayout>
</androidx.cardview.widget.CardView>

상호작용과 피드백

플로팅 액션 버튼 동작

FAB는 주요 액션을 강조하며, CoordinatorLayout과 연동해 스크롤 시 숨김/표시가 가능합니다.

<com.google.android.material.floatingactionbutton.FloatingActionButton
    android:id="@+id/mainAction"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:layout_gravity="bottom|end"
    android:layout_margin="16dp"
    app:srcCompat="@drawable/ic_add"
    app:backgroundTint="?attr/colorAccent"
    app:layout_behavior="com.google.android.material.behavior.HideBottomViewOnScrollBehavior"/>

스낵바와 연동한 피드백:

FloatingActionButton mainAction = findViewById(R.id.mainAction);
mainAction.setOnClickListener(v -> {
    Snackbar.make(findViewById(R.id.rootView), 
        "작업이 완료되었습니다", Snackbar.LENGTH_LONG)
        .setAction("실행 취소", undoView -> {
            // 되돌리기 로직
        })
        .setAnchorView(mainAction)
        .show();
});

화면 전환 애니메이션

공유 요소 전환

두 화면 간 연속적인 시각 흐름을 위해 공유 요소 전환을 적용합니다.

// 출발 액티비티
public void openDetail(ItemData data, View sharedView) {
    Intent intent = new Intent(this, DetailActivity.class);
    intent.putExtra("item_data", data);
    
    ActivityOptions opts = ActivityOptions.makeSceneTransitionAnimation(
        this,
        android.util.Pair.create(sharedView, "hero_image"),
        android.util.Pair.create(findViewById(R.id.appBar), "top_bar")
    );
    startActivity(intent, opts.toBundle());
}

도착 액티비티에서 전환 설정:

@Override
protected void onCreate(Bundle savedInstanceState) {
    getWindow().setSharedElementEnterTransition(
        TransitionInflater.from(this)
            .inflateTransition(R.transition.hero_enter));
    super.onCreate(savedInstanceState);
    // ...
}

원형 확장 효과

터치 지점에서 시작하는 원형 확장 애니메이션 구현:

@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
private void performReveal(View target, int centerX, int centerY) {
    int startRadius = 0;
    int endRadius = (int) Math.hypot(target.getWidth(), target.getHeight());
    
    Animator reveal = ViewAnimationUtils.createCircularReveal(
        target, centerX, centerY, startRadius, endRadius);
    reveal.setDuration(400);
    reveal.setInterpolator(new AccelerateDecelerateInterpolator());
    reveal.start();
}

테마 시스템 구성

일관된 색상 체계를 위해 테마 계층을 정의합니다.

<!-- colors.xml -->
<color name="primaryBrand">#6200EE</color>
<color name="primaryBrandDark">#3700B3</color>
<color name="secondaryBrand">#03DAC6</color>
<color name="surfaceLight">#FFFFFF</color>
<color name="errorState">#B00020</color>

<!-- themes.xml -->
<style name="Base.Theme.App" parent="Theme.Material3.Light.NoActionBar">
    <item name="colorPrimary">@color/primaryBrand</item>
    <item name="colorPrimaryDark">@color/primaryBrandDark</item>
    <item name="colorAccent">@color/secondaryBrand</item>
    <item name="colorSurface">@color/surfaceLight</item>
    <item name="colorError">@color/errorState</item>
    <item name="android:statusBarColor">@android:color/transparent</item>
</style>

의존성 설정

dependencies {
    implementation 'com.google.android.material:material:1.11.0'
    implementation 'androidx.appcompat:appcompat:1.6.1'
    implementation 'androidx.recyclerview:recyclerview:1.3.2'
    implementation 'androidx.cardview:cardview:1.0.0'
    implementation 'androidx.coordinatorlayout:coordinatorlayout:1.2.0'
}

성능 최적화 체크리스트

  • 오버드로 감소: android:background 중복 설정 제거
  • 그림자 비용: elevation이 높은 뷰는 별도 하드웨어 레이어 사용
  • 애니메이션 프레임: Choreographer로 16ms 프레임 시간 준수 확인
  • 리스트 스크롤: RecyclerView에 setHasFixedSize(true) 설정
  • 리플 효과: foreground 대신 background에 RippleDrawable 적용

참고 구현: LollipopShowcase

머티리얼 디자인 컴포넌트의 실제 동작을 확인할 수 있는 오픈소스 참고 프로젝트입니다. RecyclerView 애니메이션, DrawerLayout 연동, 공유 요소 전환 등 12가지 핵심 패턴이 구현되어 있습니다.

프로젝트 경로: https://gitcode.com/gh_mirrors/lo/LollipopShowcase

태그: Android Material Design RecyclerView CardView CoordinatorLayout

8월 19일 04:35에 게시됨