The Android Developer Challenge is back! Submit your idea before December 2.

다양한 픽셀 밀도 지원

Android 기기는 다양한 화면 크기(핸드셋, 태블릿, TV 등)로 제공될 뿐만 아니라 화면의 픽셀 크기도 다양합니다. 즉, 한 기기에서는 제곱인치당 160픽셀을 사용하지만 다른 기기에서는 같은 공간에 480픽셀을 사용합니다. 이러한 픽셀 밀도의 편차를 고려하지 않으면 시스템이 이미지를 확장하거나(결과적으로 이미지가 흐려짐) 이미지가 완전히 잘못된 크기로 표시될 수 있습니다.

이 페이지에서는 해상도 독립형 측정 단위를 사용하고 각 픽셀 밀도에 관해 대체 비트맵 리소스를 제공하여 다양한 픽셀 밀도를 지원하도록 앱을 디자인하는 방법을 보여줍니다.

이러한 기술에 관한 개요는 아래 동영상을 시청하세요.

실제 아이콘 애셋 디자인에 관한 자세한 정보는 머티리얼 디자인 아이콘 가이드라인을 참조하세요.

밀도 독립형 픽셀 사용

맨 먼저 피해야 할 함정은 픽셀을 사용하여 거리나 크기를 정의하는 것입니다. 픽셀로 크기를 정의하면 화면 크기에 따라 픽셀 밀도가 달라져서 같은 개수의 픽셀이라도 기기가 다르면 실제 크기가 달라질 수 있으므로 문제가 됩니다.

그림 1. 같은 크기의 두 화면에서 픽셀 수가 다를 수 있음

밀도가 서로 다른 화면에서 UI 표시 크기를 유지하려면 밀도 독립형 픽셀(dp)을 측정 단위로 사용해서 UI를 디자인해야 합니다. 1dp는 중밀도 화면(160dpi, '기준' 밀도)의 1픽셀과 거의 동일한 가상 픽셀 단위입니다. Android는 이 값을 밀도마다 적합한 실제 픽셀 수로 변환합니다.

예를 들어 그림 1의 두 기기를 생각해 보세요. 뷰 너비를 '100px'로 정의하면 뷰가 왼쪽 기기에서 훨씬 더 크게 표시됩니다. 따라서 '100dp'를 사용하여 뷰가 두 화면에서 동일한 크기로 표시되도록 해야 합니다.

텍스트 크기를 정의하는 경우에는 확장 가능 픽셀(sp)을 단위로 사용해야 합니다(단, 레이아웃 크기에는 sp를 사용하지 않아야 함). sp 단위는 기본적으로 dp와 같은 크기이지만, 사용자가 선호하는 텍스트 크기에 따라 크기가 조절됩니다.

예를 들어 2개 뷰 사이의 간격을 지정하는 경우에는 dp를 사용합니다.

    <Button android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="@string/clickme"
        android:layout_marginTop="20dp" />
    

텍스트 크기를 지정하는 경우에는 항상 sp를 사용합니다.

    <TextView android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:textSize="20sp" />
    

dp 단위를 픽셀 단위로 변환

크기를 dp로 표시한 후 픽셀로 변환해야 하는 경우가 있습니다. dp 단위를 화면 픽셀로 변환하는 것은 간단합니다.

px = dp * (dpi / 160)

앱에서 사용자 손가락이 16픽셀 이상 이동하면 스크롤 또는 흔들기 동작이 인식된다고 가정해보겠습니다. 기준 화면에서는 사용자가 16 pixels / 160 dpi(2.5mm 또는 1/10인치와 같음)를 이동해야 동작이 인식됩니다. 고밀도 화면(240dpi)을 사용하는 기기에서는 사용자가 16 pixels / 240 dpi(1.7mm 또는 1/15인치와 같음)를 이동해야 합니다. 거리가 훨씬 더 짧아지므로 앱이 사용자에게 더 민감하게 나타납니다.

이 문제를 해결하려면 동작 임계값을 코드에 dp로 표시한 후 실제 픽셀로 변환해야 합니다. 예:

Kotlin

    // The gesture threshold expressed in dp
    private const val GESTURE_THRESHOLD_DP = 16.0f
    ...
    private var mGestureThreshold: Int = 0
    ...
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        // Get the screen's density scale
        val scale: Float = resources.displayMetrics.density
        // Convert the dps to pixels, based on density scale
        mGestureThreshold = (GESTURE_THRESHOLD_DP * scale + 0.5f).toInt()

        // Use mGestureThreshold as a distance in pixels...
    }
    

자바

    // The gesture threshold expressed in dp
    private static final float GESTURE_THRESHOLD_DP = 16.0f;

    // Get the screen's density scale
    final float scale = getResources().getDisplayMetrics().density;
    // Convert the dps to pixels, based on density scale
    mGestureThreshold = (int) (GESTURE_THRESHOLD_DP * scale + 0.5f);

    // Use mGestureThreshold as a distance in pixels...
    

DisplayMetrics.density 필드는 현재 픽셀 밀도에 따라 dp 단위를 픽셀로 변환하는 데 사용해야 할 배율을 지정합니다. DisplayMetrics.density는 중밀도 화면의 경우 1.0, 고밀도 화면의 경우 1.5, 초고밀도 화면의 경우 2.0, 저밀도 화면의 경우 0.75입니다. 이 수치는 현재 화면에 관한 실제 픽셀 수를 구하기 위해 dp 단위에 곱해야 하는 인수입니다.

사전 크기 조정된 구성 값 사용

ViewConfiguration 클래스를 사용하면 Android 시스템에서 사용되는 공통의 거리, 속도, 시간에 액세스할 수 있습니다. 예를 들어 프레임워크에서 스크롤 임계값으로 사용되는 거리(픽셀 단위)는 getScaledTouchSlop()를 사용하여 구할 수 있습니다.

Kotlin

    private val GESTURE_THRESHOLD_DP = ViewConfiguration.get(myContext).scaledTouchSlop
    

자바

    private static final int GESTURE_THRESHOLD_DP = ViewConfiguration.get(myContext).getScaledTouchSlop();
    

ViewConfiguration에서 getScaled 접두어로 시작되는 메서드는 현재 픽셀 밀도에 관계없이 올바르게 표시되는 값(픽셀 단위)을 반환합니다.

대체 비트맵 제공

픽셀 밀도가 서로 다른 기기에서 좋은 그래픽 품질을 제공하기 위해서는 앱에 있는 각 비트맵에 관해 밀도 버킷마다 해당하는 해상도로 하나씩 여러 개의 비트맵 버전을 제공해야 합니다. 그러지 않으면 Android가 비트맵 크기를 조정해 각 화면에서 동일한 표시 공간을 사용하도록 해야 하며 블러와 같은 크기 조정 아티팩트가 발생합니다.

그림 2. 다양한 밀도 크기에서 비트맵의 상대적 크기

앱에서 사용할 수 있는 밀도 버킷은 여러 개가 있습니다. 표 1에는 사용할 수 있는 다양한 구성 한정자와 이러한 구성 한정자가 적용되는 화면 유형이 설명되어 있습니다.

표 1. 다양한 픽셀 밀도의 구성 한정자

밀도 한정자 설명
ldpi 저밀도(ldpi)의 화면(~120dpi)에 대한 리소스입니다.
mdpi 중밀도(mdpi)의 화면(~160dpi)에 대한 리소스입니다. (이것이 기준 밀도입니다.)
hdpi 고밀도(hdpi)의 화면(~240dpi)에 대한 리소스입니다.
xhdpi 초고밀도(xhdpi)의 화면(~320dpi)에 대한 리소스입니다.
xxhdpi 초초고밀도(xxhdpi)의 화면(~480dpi)에 대한 리소스입니다.
xxxhdpi 초초초고밀도(xxxhdpi) 사용(~640dpi)에 대한 리소스입니다.
nodpi 모든 밀도에 대한 리소스입니다. 이들은 밀도 독립적 리소스입니다. 이 한정자 태그가 지정된 리소스의 경우 현재 화면의 밀도에 관계없이 시스템에서 리소스 크기를 조정하지 않습니다.
tvdpi mdpi와 hdpi 사이의 화면(약 213dpi)에 대한 리소스입니다. 이 값은 '기본' 밀도 그룹으로 간주되지 않습니다. 대개의 경우 텔레비전용이며 앱에서는 대부분 필요하지 않습니다. 앱은 대부분 mdpi 및 hdpi 리소스를 제공하는 것으로 충분하며 시스템에서 필요에 따라 확장합니다. tvdpi 리소스를 제공해야 한다고 생각되는 경우 1.33*mdpi로 크기를 지정합니다. 예를 들어 mdpi 화면의 100px x 100px 이미지가 tvdpi에서는 133px x 133px입니다.

여러 밀도의 대체 비트맵 드로어블을 만들려면 6가지 기본 밀도 간에 3:4:6:8:12:16 크기 조정 비율을 따라야 합니다. 예를 들어 중밀도 화면에 대한 48x48픽셀의 비트맵 드로어블이 있는 경우 모든 다양한 크기는 다음과 같아야 합니다.

  • 저밀도(ldpi)의 경우 36x36(0.75x)
  • 중밀도(mdpi)의 경우 48x48(1.0x 기준)
  • 고밀도(hdpi)의 경우 72x72(1.5x)
  • 초고밀도(xhdpi)의 경우 96x96(2.0x)
  • 초초고밀도(xxhdpi)의 경우 144x144(3.0x)
  • 초초초고밀도(xxxhdpi)의 경우 192x192(4.0x)

다음으로, 생성된 이미지 파일을 res/의 적절한 하위 디렉터리에 배치하면 앱이 실행되는 기기의 픽셀 밀도에 따라 시스템에서 자동으로 알맞은 크기를 선택합니다.

    res/
      drawable-xxxhdpi/
        awesome-image.png
      drawable-xxhdpi/
        awesome-image.png
      drawable-xhdpi/
        awesome-image.png
      drawable-hdpi/
        awesome-image.png
      drawable-mdpi/
        awesome-image.png
    

다음으로, @drawable/awesomeimage를 참조할 때마다 시스템은 화면의 dpi를 기반으로 적절한 비트맵을 선택합니다. 해당 밀도의 밀도 특정 리소스를 제공하지 않으면 시스템이 다음 최적의 일치를 선택하고 화면에 맞게 크기를 조정합니다.

도움말: 시스템이 크기를 조정해서는 안 되는 드로어블 리소스가 있는 경우(예를 들어, 런타임에 개발자가 직접 이미지를 조정하는 경우) nodpi 구성 한정자를 통해 디렉터리에 이러한 리소스를 배치해야 합니다. 이 한정자가 있는 리소스는 밀도를 알 수 없는 것으로 간주되며, 시스템에서 이 리소스의 크기를 조정하지 않습니다.

Android가 현재 화면 구성에 적합한 리소스를 선택하는 방법과 다른 구성 한정자에 관한 자세한 정보는 리소스 제공을 참조하세요.

mipmap 디렉터리에 앱 아이콘 지정

다른 모든 비트맵 애셋과 마찬가지로 앱 아이콘의 밀도 특정 버전을 제공해야 합니다. 하지만 일부 앱 런처는 기기의 밀도 버킷에서 요구하는 것보다 최대 25% 더 크게 앱 아이콘을 표시합니다.

예를 들어 기기의 밀도 버킷이 xxhdpi이고 제공되는 최대 앱 아이콘이 drawable-xxhdpi에 있는 경우 런처 앱이 이 아이콘을 확장하여 아이콘이 덜 선명하게 표시됩니다. 그러므로 더 높은 밀도의 런처 아이콘을 mipmap-xxxhdpi 디렉터리에 제공해야 합니다. 이제 런처는 xxxhdpi 애셋을 사용할 수 있습니다.

앱 아이콘이 이렇게 확장될 수 있기 때문에 모든 앱 아이콘을 drawable 디렉터리가 아닌 mipmap 디렉터리에 지정해야 합니다. drawable 디렉터리와 달리, mipmap 디렉터리는 모두 밀도 특정 APK를 빌드하는 경우에도 APK에 유지됩니다. 이렇게 하면 런처 앱이 최적의 해상도 아이콘을 선택하여 홈 화면에 표시할 수 있습니다.

    res/
      mipmap-xxxhdpi/
        launcher-icon.png
      mipmap-xxhdpi/
        launcher-icon.png
      mipmap-xhdpi/
        launcher-icon.png
      mipmap-hdpi/
        launcher-icon.png
      mipmap-mdpi/
        launcher-icon.png
    

아이콘 디자인 가이드라인은 아이콘에 대한 주요 가이드를 참조하세요.

앱 아이콘 빌드에 대한 도움말은 Image Asset Studio로 앱 아이콘 만들기를 참조하세요.

벡터 그래픽 사용

이미지에 관해 여러 밀도 특정 버전을 만드는 대신 벡터 그래픽 하나만 만들 수 있습니다. 벡터 그래픽은 픽셀 비트맵이 아니라 경로와 색상을 정의하는 XML을 사용하여 이미지를 만듭니다. 이렇게 벡터 그래픽은 사진이 아닌 아이콘과 같은 일러스트레이션에 일반적으로 가장 적합하지만 아티팩트를 조정하지 않고 원하는 크기로 확장할 수 있습니다.

벡터 그래픽은 SVG(Scalable Vector Graphics) 파일로 제공되는 경우도 있지만 Android는 이 형식을 지원하지 않으므로 SVG 파일을 Android 벡터 드로어블 형식으로 변환해야 합니다.

Android 스튜디오에서 Vector Asset Studio를 사용하여 다음과 같이 쉽게 SVG를 벡터 드로어블로 변경할 수 있습니다.

  1. 프로젝트 창에서 res 디렉터리를 마우스 오른쪽 버튼으로 클릭하고 새로 만들기 > 벡터 애셋을 선택합니다.
  2. 로컬 파일(SVG, PSD)을 선택합니다.
  3. 가져와 조정할 파일을 찾습니다.

    그림 3. Android 스튜디오로 SVG 가져오기

    Asset Studio 창에 벡터 드로어블이 지원하지 않는 파일의 일부 속성을 보여주는 오류가 표시될 수 있습니다. 하지만 오류가 표시되어도 파일을 가져올 수 있습니다. 지원되지 않는 속성은 그냥 무시됩니다.

  4. 다음을 클릭합니다.

  5. 다음 화면에서 프로젝트에 파일을 사용하려는 소스 세트를 확인하고 완료를 클릭합니다.

    하나의 벡터 드로어블을 모든 픽셀 밀도에서 사용할 수 있기 때문에 이 파일은 기본 드로어블 디렉터리에 저장됩니다(밀도 특정 디렉터리를 사용할 필요가 없음).

        res/
          drawable/
            ic_android_launcher.xml
        

벡터 그래픽 만들기에 관한 자세한 정보는 벡터 드로어블 문서를 참조하세요.

특수한 밀도 문제에 관한 조언

이 섹션에서는 Android가 다양한 픽셀 밀도에 맞춰 비트맵의 크기를 조정하는 방법과 다양한 밀도에서 비트맵을 그리는 방법을 어떻게 추가로 제어할 수 있는지 자세히 설명합니다. 앱에서 그래픽을 조작하지 않거나 다양한 픽셀 밀도에서 실행될 때 문제가 발생하지 않는 경우 이 섹션을 무시할 수 있습니다.

런타임에 그래픽을 조작할 때 여러 밀도를 지원할 수 있는 방법을 잘 이해하려면 시스템에서 다음과 같은 방법으로 비트맵의 크기가 적절하게 조정되도록 한다는 점을 알고 있어야 합니다.

  1. 리소스 사전 크기 조정(예: 비트맵 드로어블)

    시스템에서 현재 화면의 밀도를 기반으로 앱의 밀도 특정 리소스를 사용합니다. 올바른 밀도의 리소스를 사용할 수 없는 경우 시스템에서 기본 리소스를 로드하고 필요에 따라 리소스를 확장하거나 축소합니다. 시스템에서는 기본 리소스(구성 한정자가 없는 디렉터리에 있는 리소스)가 기준 픽셀 밀도(mdpi)용으로 디자인되었으며 현재 픽셀 밀도에 맞는 크기로 해당 비트맵 크기를 조절한다고 가정합니다.

    사전 크기 조정된 리소스의 크기를 요청하면 시스템에서 크기 조정 이후 크기를 나타내는 값을 반환합니다. 예를 들어 mdpi 화면에 맞게 50x50픽셀로 디자인한 비트맵은 hdpi 화면에서 75x75픽셀로 조정되며(hdpi에 대한 대체 리소스가 없는 경우) 시스템에서는 이와 같이 크기를 보고합니다.

    Android에서 리소스를 사전 크기 조정하지 않게 하려는 경우가 있습니다. 사전 크기 조정을 방지하는 가장 쉬운 방법은 nodpi 구성 한정자가 있는 리소스 디렉터리에 리소스를 넣는 것입니다. 예:

    res/drawable-nodpi/icon.png

    시스템에서 이 폴더의 icon.png 비트맵을 사용할 때는 현재 기기 밀도를 기준으로 비트맵의 크기를 조정하지 않습니다.

  2. 픽셀 크기 및 좌표 자동 크기 조정

    manifest에서 android:anyDensity"false"로 설정하거나 Bitmap에 관해 프로그래밍 방식으로 inScaled"false"로 설정하여 크기 및 이미지 사전 크기 조정을 사용 안함으로 설정할 수 있습니다. 이런 경우 시스템에서는 그리기 시간에 절대 픽셀 좌표 및 픽셀 크기 값을 자동으로 조정합니다. 이렇게 하면 픽셀로 정의된 화면 요소가 기준 픽셀 밀도(mdpi)와 거의 동일한 실제 크기로 계속 표시되도록 합니다. 시스템에서는 앱에 대한 이 크기 조정을 투명하게 처리하고 실제 픽셀 크기가 아니라 앱에 관해 크기 조정한 픽셀 크기를 보고합니다.

    예를 들어 480x800인 WVGA 고밀도 화면(기존의 HVGA 화면과 거의 동일한 크기)이 달린 기기가 있고 사전 크기 조정이 사용 안함으로 설정된 앱이 이 기기에서 실행된다고 가정해보겠습니다. 이런 경우 시스템에서는 앱의 화면 크기 쿼리에 관해 '잘못된 정보를 알리며' 320x533(픽셀 밀도의 대략적인 mdpi 변환 값)을 보고합니다. 이 경우 앱에서 (10,10)~(100,100) 직사각형을 무효화하는 등의 그리기 작업을 수행하면 시스템에서는 적절한 양만큼 크기를 조정해 좌표를 변환한 후 실제로 (15,15)~(150, 150) 영역을 무효화합니다. 이 차이로 인해 앱이 크기 조정된 비트맵을 직접 조작하면 예기치 못한 동작이 발생할 수 있으나, 이는 앱 성능을 최대한 양호하게 유지하는 데 필요한 합당한 수준의 단점이라고 간주됩니다. 이러한 상황이 발생하는 경우 dp 단위를 pixel 단위로 변환을 참조하세요.

    일반적으로 사전 크기 조정을 사용하지 않도록 설정해서는 안 됩니다. 여러 화면을 지원하는 가장 좋은 방법은 이 문서에서 설명하는 기본 기술을 따르는 것입니다.

비트맵을 조작하거나 다른 방식으로 화면의 픽셀과 직접적으로 상호작용하는 앱의 경우 다양한 화면 밀도를 지원하도록 추가 단계를 수행해야 할 수 있습니다. 예를 들어 손가락이 지나가는 픽셀 수를 카운트하여 터치 동작에 반응하려는 경우 실제 픽셀을 사용하는 대신 적절한 밀도 독립형 픽셀 값을 사용해야 하며 손쉽게 dp 및 px 값 간에 변환할 수 있습니다.

모든 픽셀 밀도에 관해 테스트

여러 기기에서 다양한 픽셀 밀도로 앱을 테스트하여 UI 크기 조정이 제대로 이루어지도록 하는 것이 중요합니다. 실제 기기에서 테스트하는 것은 쉽지만 모든 다양한 픽셀 밀도의 실제 기기에 액세스할 수 없는 경우 Android 에뮬레이터를 사용할 수도 있습니다.

실제 기기에서 테스트하고 싶지만 기기를 구매하고 싶지 않은 경우에는 Firebase Test Lab을 사용하여 Google 데이터 센터의 기기에 액세스할 수 있습니다.