머티리얼 디자인의 핵심 개념
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