Computer >> 컴퓨터 >  >> 문제 해결 >> Android

안드로이드 앱에 다크 테마 추가하는 방법: 개발자를 위한 단계별 가이드

개발자 입장에서 안드로이드 앱을 만드는 일 자체는 어렵지 않습니다. 정말 까다로운 것은 매력적인 UI를 디자인하는 일이죠. 그래서 저희는 심플하면서도 매력적인 UI를 만들고자 하는 개발자들을 위해 「How to Design an Android App UI That Doesn't Suck」라는 가이드를 준비했습니다. 당시 그 가이드에서 다크 모드(야간 모드) 테마 추가에 대해 간단히 언급했었는데, 이번 글에서는 그 구현 방법을 하나하나 짚어보겠습니다.

앱에 다크/야간 모드 테마를 추가하면 사용자에게 여러모로 유익합니다. 배터리 소모를 줄여주고, 밤에는 눈의 피로도 확실히 덜어주기 때문입니다. 흰 배경 위 검은 글씨 조합이나 UI에 쓰인 '밝은' 색상은 특히 야간에 눈에 상당한 부담을 주는데요. 반면 어두운 배경에 밝은 텍스트를 사용하면 눈의 피로와 부담이 훨씬 적습니다. 이런 이유로 요즘 인기 앱들이 다크 테마를 잇달아 도입하고 있습니다. 최근에는 YouTube와 Reddit가 다크 모드를 선보였고, Facebook 역시 지난 5월 예고했던 다크 테마를 준비 중입니다.

안드로이드 앱에 언제든 전환할 수 있는 다크 모드를 손쉽게 추가하고 싶다면, 이 가이드를 순서대로 따라 해 보세요. 진행 중 막히는 부분이 있다면 댓글로 알려주세요!

1. XML 속성 파일 만들기

가장 먼저 테마를 처리할 XML 속성 파일을 생성해야 합니다. 라이트 테마와 다크 테마 각각의 리소스 세트를 통째로 APK에 포함해 용량을 늘리는 방식보다 훨씬 효율적인데요, 안드로이드는 이 XML을 통해 속성값 색상을 네이티브하게 교체할 수 있기 때문입니다.

앱의 res(resources) 폴더에 attrs.xml 파일을 만들고 아래 코드를 추가하세요. 스타일 지정이 가능한(styleable) 속성들입니다.

<declare-styleable name="CTAppTheme">
<attr name="primaryTextColor" format="color"/>
<attr name="secondaryTextColor" format="color"/>
<attr name="dividerColor" format="color"/>
<attr name="backgroundCardColor" format="color"/>
<attr name="iconPlaceholder" format="integer"/>
</declare-styleable>

롤리팝(Lollipop) 이전 버전에서는 drawable에 대한 속성 지정이 불가능해서 리소스 ID를 직접 지정해야 했지만, 이제는 그럴 필요가 없습니다.

2. 스타일 추가하기

동작하도록 만들려면 같은 베이스를 공유하는 두 개의 스타일이 필요합니다. 첫 번째는 기본이 되는 '라이트' 테마, 두 번째는 '다크' 테마입니다.

<!-- 라이트 기본 액티비티 테마 -->
<style name="ActivityTheme.Primary.Base.Light" parent="Theme.AppCompat.NoActionBar">
<item name="primaryTextColor">#F4F4F6</item>
<item name="secondaryTextColor">#96F4F4F6</item>
<item name="backgroundCardColor">#FFFF</item>
<item name="dividerColor">#F2F2F3</item>
<item name="iconPlaceholder">@drawable/i_light_plholder</item>
</style>

<!-- 다크 기본 액티비티 테마 -->
<style name="ActivityTheme.Primary.Base.Dark" parent="Theme.AppCompat.NoActionBar">
<item name="primaryTextColor">#33343B</item>
<item name="secondaryTextColor">#8033343B</item>
<item name="backgroundCardColor">#28292e</item>
<item name="dividerColor">#F2F2F3</item>
<item name="iconPlaceholder">@drawable/i_dark_pholder</item>
</style>

그리고 실제 액티비티에 적용할 스타일은 다음과 같습니다.

<!-- 피드 액티비티 전용 테마 -->
<style name="FeedActivityThemeLight" parent="ActivityTheme.Primary.Base.Light">
<item name="android:windowBackground">#F4F4F6</item>
</style>

<style name="FeedActivityThemeDark" parent="ActivityTheme.Primary.Base.Dark">
<item name="android:windowBackground">#33343B</item>
</style>

이 중 하나를 매니페스트(Manifest)에서 액티비티 테마로 지정해야 합니다. 여기서는 FeedActivity가 대상입니다.

3. 뷰(View) 스타일링하기

뷰를 스타일링하는 예시입니다. 이 예시는 '페이지를 찾을 수 없음(Page not found)' 오류 화면을 위한 것입니다.

<ImageView
android:id="@+id/placeHolderAssetImageView"
android:layout_width="100dp"
android:layout_height="100dp"
android:src="?attr/iconPlaceholder"
android:layout_marginBottom="8dp" />

<TextView
android:id="@+id/placeHolderTitleTextView"
android:text="Page not found."
android:textSize="16sp"
android:textColor="?attr/primaryTextColor"
android:layout_margin="16dp" />

4. 동적 테마 전환 구현하기

동적 테마 전환을 구현하는 가장 좋고 효율적인 방법은, 설정 값을 저장하는 SharedPreferences 값을 싱글턴(Singleton) 패턴으로 Application 인스턴스에 로드하는 것입니다. 이렇게 하면 매 액티비티가 시작될 때마다 값을 새로 읽어올 필요가 없습니다.

public class App extends Application {
public static final String TAG = "App";

private boolean isNightModeEnabled = false;

@Override
public void onCreate() {
super.onCreate();

// 여기서 야간 모드 상태를 로드합니다
SharedPreferences mPrefs = PreferenceManager.getDefaultSharedPreferences(this);
this.isNightModeEnabled = mPrefs.getBoolean("NIGHT_MODE", false);
}

public boolean isNightModeEnabled() {
return isNightModeEnabled;
}

public void setIsNightModeEnabled(boolean isNightModeEnabled) {
this.isNightModeEnabled = isNightModeEnabled;
}
}

이 인스턴스는 앱의 어떤 것보다 먼저 실행되기 때문에, 앱이 열린 이후에는 소속된 어떤 액티비티에서든 언제든 isNightModeEnabled() 불리언 값을 호출할 수 있습니다.

public final class FeedActivity extends AppCompatActivity {
private final static String TAG = "FeedActivity";

@Override
protected void onCreate(Bundle savedInstanceState) {
if (MyApplication.getInstance().isNightModeEnabled()) {
setTheme(R.style.FeedActivityThemeDark);
}
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_feed);
}
}

단, 매니페스트에 지정된 테마와 다른 테마를 적용하려면 반드시 부모 onCreate() 메서드를 호출하기 전에 setTheme()를 실행해야 한다는 점을 기억하세요.

5. AppCompat으로 테마 상태 설정하기

AppCompat은 다양한 작업에서 유용하게 쓰이며, 여기서는 모드 상태를 설정하는 데 활용합니다. 이 방식을 쓰면 앱 인스턴스로 상태를 캐싱할 필요가 없으므로 앱 캐시 낭비도 줄어듭니다(물론 그 폭은 아주 미미합니다).

AppCompatDelegate
.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_YES);

사용할 수 있는 옵션은 다음과 같습니다.

  • MODE_NIGHT_YES – 항상 야간 모드 적용
  • MODE_NIGHT_NO – 항상 라이트 모드 적용
  • MODE_NIGHT_AUTO – 시간대에 따라 자동 전환

상태 조회하기

AppCompatDelegate.getDefaultNightMode();

public final class FeedActivity extends AppCompatActivity {
private final static String TAG = "FeedActivity";

@Override
protected void onCreate(Bundle savedInstanceState) {
if (AppCompatDelegate.getDefaultNightMode()
== AppCompatDelegate.MODE_NIGHT_YES) {
setTheme(R.style.FeedActivityThemeDark);
}
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_feed);
}
}

이것으로 기본적인 구현이 끝났습니다. 이후에는 취향에 맞게 값들을 계속 커스터마이징하고 다듬으면 됩니다. 이 가이드만으로도 안드로이드 앱에 네이티브 다크 테마를 도입하는 데 충분한 출발점이 될 것입니다.