Wear에서 레이아웃 정의

Wear OS 앱은 핸드헬드 Android 기기와 동일한 레이아웃 기법을 사용하지만, 디자인 시 특정 제약 조건이 적용됩니다. 핸드헬드 앱에서 기능과 UI를 포팅할 경우 좋은 사용자 환경을 기대할 수 없습니다.

뛰어난 웨어러블 앱의 디자인에 관한 자세한 내용은 Wear OS 디자인 가이드라인을 읽어보세요.

Wear OS 앱의 레이아웃을 만들 때는 정사각형 화면과 원형 화면의 기기를 고려해야 합니다. 원형 Wear OS 기기에서는 화면 모퉁이 부근의 콘텐츠가 잘릴 수 있습니다. 따라서 정사각형 화면에 맞게 디자인된 레이아웃은 원형 기기에서 문제가 생길 수 있습니다.

예를 들어, 그림 1은 정사각형 화면과 원형 화면에서 다음 레이아웃이 어떻게 표시되는지 보여줍니다.

그림 1. 정사각형 화면에 맞게 디자인된 레이아웃이 원형 화면에서 제대로 작동하지 않음을 보여주는 예

따라서 레이아웃에 다음 설정을 사용하면 원형 화면의 기기에서 텍스트가 올바르게 표시되지 않습니다.

    <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:tools="http://schemas.android.com/tools"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="vertical">

        <TextView
            android:id="@+id/text"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="@string/hello_square" />
    </LinearLayout>
    

이러한 문제는 다음 두 가지 방법으로 해결할 수 있습니다.

  1. 정사각형 기기와 원형 기기에 모두 Wear UI 라이브러리의 레이아웃을 사용합니다.
    • BoxInsetLayout - 이 레이아웃은 기기 화면의 모양에 따라 다른 창 인셋을 적용합니다. 두 가지 화면 모양에 비슷한 레이아웃을 사용하면서 원형 화면 가장자리 부근에서 뷰가 잘리지 않도록 하려는 경우 이 방법을 사용합니다.
    • 곡선형 레이아웃 - 원형 화면에 최적화된 항목의 세로 목록을 표시하고 조작하려는 경우 이 레이아웃을 사용합니다.
  2. 리소스 제공 가이드에 설명된 대로 정사각형 기기와 원형 기기에 대체 레이아웃 리소스를 제공합니다. Wear는 런타임 시 기기 화면의 모양을 감지하고 올바른 레이아웃을 로드합니다.

이 라이브러리로 Android 스튜디오 프로젝트를 컴파일하려면 Extras > Google Repository 패키지가 Android SDK 관리자에 설치되어 있어야 합니다. 또한, wear 모듈의 build.gradle 파일에 다음 종속 항목을 포함합니다.

    dependencies {
        compile fileTree(dir: 'libs', include: ['*.jar'])
        compile 'com.android.support:wear:26.0.0'
    }
    

BoxInsetLayout 사용

그림 2. 원형 화면의 창 인셋

Wear UI 라이브러리의 BoxInsetLayout 클래스를 사용하면 정사각형 화면과 원형 화면에서 모두 작동하는 단일 레이아웃을 정의할 수 있습니다. 이 클래스는 화면 모양에 따라 필요한 창 인셋을 적용하며, 화면의 중앙이나 가장자리 부근에서 뷰를 쉽게 정렬할 수 있도록 합니다.

참고: BoxInsetLayout 클래스는 웨어러블 지원 라이브러리에서 지원 중단된 유사한 클래스를 대체합니다.

그림 2의 회색 정사각형은 필수 창 인셋을 적용한 후 BoxInsetLayout이 원형 화면에 하위 뷰를 자동으로 배치할 수 있는 영역을 나타냅니다. 하위 뷰는 이 영역 내부에 표시되도록 boxedEdges 속성을 다음 값으로 지정합니다.

  • top, bottom, leftright를 조합하여 사용합니다. 예를 들어 "left|top" 값은 하위 뷰의 왼쪽 및 상단 가장자리를 그림 2의 회색 정사각형 내부에 배치합니다.
  • "all" 값은 모든 하위 뷰의 콘텐츠를 그림 2의 회색 정사각형 내부에 배치합니다.

정사각형 화면에서는 창 인셋이 0이고 boxedEdges 속성이 무시됩니다.

그림 3. 정사각형 화면과 원형 화면에서 모두 작동하는 레이아웃 정의

그림 3의 레이아웃은 <BoxInsetLayout> 요소를 사용하며 정사각형 화면 및 원형 화면에서 작동합니다.

    <androidx.wear.widget.BoxInsetLayout
        xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:app="http://schemas.android.com/apk/res-auto"
        android:layout_height="match_parent"
        android:layout_width="match_parent"
        android:padding="15dp">

        <FrameLayout
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:padding="5dp"
            app:boxedEdges="all">

            <TextView
                android:gravity="center"
                android:layout_height="wrap_content"
                android:layout_width="match_parent"
                android:text="@string/sometext"
                android:textColor="@color/black" />

            <ImageButton
                android:background="@null"
                android:layout_gravity="bottom|left"
                android:layout_height="50dp"
                android:layout_width="50dp"
                android:src="@drawable/ok" />

            <ImageButton
                android:background="@null"
                android:layout_gravity="bottom|right"
                android:layout_height="50dp"
                android:layout_width="50dp"
                android:src="@drawable/cancel" />
        </FrameLayout>
    </androidx.wear.widget.BoxInsetLayout>
    

굵게 표시된 레이아웃 부분을 확인하세요.

  • android:padding="15dp"

    이 줄은 <BoxInsetLayout> 요소에 패딩을 할당합니다. 원형 기기의 창 인셋은 15dp보다 크므로 이 패딩은 정사각형 화면에만 적용됩니다.

  • android:padding="5dp"

    이 줄은 내부 FrameLayout 요소에 패딩을 할당합니다. 이 패딩은 정사각형 화면과 원형 화면에 모두 적용됩니다. 버튼과 창 인셋 간의 총 패딩은 정사각형 화면에서는 20dp(15+5)이고 원형 화면에서는 5dp입니다.

  • app:boxedEdges="all"

    이 줄은 FrameLayout 요소 및 하위 요소가 원형 화면의 창 인셋에 의해 정의된 영역 내부에서 박스로 표시되도록 합니다. 이 줄은 정사각형 화면에는 영향을 주지 않습니다.

곡선형 레이아웃 사용

Wear UI 라이브러리의 WearableRecyclerView 클래스를 사용하면 원형 화면에 최적화된 곡선형 레이아웃을 선택할 수 있습니다. 앱의 스크롤 가능한 목록에 곡선형 레이아웃을 사용하려면 곡선형 레이아웃 만들기를 참조하세요.

정사각형 화면과 원형 화면에 다른 레이아웃 사용

Wear 기기의 화면은 정사각형일 수도 있고 원형일 수도 있습니다. 앱은 두 가지 기기 설정 중 하나를 지원할 수 있어야 합니다. 이를 위해 대체 리소스를 제공해야 합니다. 레이아웃, 크기 또는 기타 리소스 유형에 -round-notround 리소스 한정자를 적용합니다.

예를 들어 다음과 같이 레이아웃을 구성해볼 수 있습니다.

  • layout/ 디렉터리에는 원형 시계와 정사각형 시계에서 모두 작동하는 레이아웃을 포함합니다.
  • layout-round/layout-notround/ 디렉터리에는 화면의 모양과 관련된 레이아웃을 포함합니다.

res/values, res/values-roundres/values-notround 리소스 디렉터리를 사용할 수도 있습니다. 이 방식으로 리소스를 구성하면 기기 유형을 기반으로 특정 속성만 변경하면서 단일 레이아웃을 공유할 수 있습니다.

값 변경

원형 시계와 정사각형 시계용으로 손쉽게 빌드하는 방법은 values/dimens.xmlvalues-round/dimens.xml을 사용하는 것입니다. 패딩 설정을 서로 다르게 지정하면 단일 layout.xml 파일 하나와 dimens.xml 파일 두 개로 다음과 같은 레이아웃을 만들 수 있습니다.

    <dimen name="header_start_padding">36dp</dimen>
    <dimen name="header_end_padding">22dp</dimen>
    <dimen name="list_start_padding">36dp</dimen>
    <dimen name="list_end_padding">22dp</dimen>
    
values-round/dimens.xml 사용

그림 4. values-round/dimens.xml 사용

    <dimen name="header_start_padding">16dp</dimen>
    <dimen name="header_end_padding">16dp</dimen>
    <dimen name="list_start_padding">10dp</dimen>
    <dimen name="list_end_padding">10dp</dimen>
    
values/dimens.xml 사용

그림 5. values/dimens.xml 사용

무엇이 가장 효과적인지 알아보기 위해 서로 다른 값으로 실험해야 합니다.

XML을 사용하여 하단부 보완

어떤 시계에는 원형 화면에 인셋('하단부'라고도 함)이 있습니다. 일부 디자인은 보완하지 않을 경우 하단부에 의해 가려질 수 있습니다.

다음과 같은 디자인을 예로 들어보겠습니다.

기본 하트 디자인

그림 6. 기본 하트 디자인

다음 activity_main.xml 스니펫에서는 레이아웃을 정의합니다.

    <FrameLayout
      ...>
      <androidx.wear.widget.RoundedDrawable
        android:id="@+id/androidbtn"
        android:src="@drawable/ic_android"
        .../>
       <ImageButton
        android:id="@+id/lovebtn"
        android:src="@drawable/ic_favourite"
        android:paddingTop="5dp"
        android:paddingBottom="5dp"
        android:layout_gravity="bottom"
        .../>
    </FrameLayout>
    

아무것도 수정하지 않으면 디자인의 일부가 하단부로 사라집니다.

기본 하트 디자인

그림 7. 수정하지 않은 상태

fitsSystemWindows 속성을 사용하여 하단부를 피하도록 패딩을 설정할 수 있습니다. 다음 activity_main.xml 스니펫은 fitsSystemWindows 사용 방법을 보여줍니다.

    <ImageButton
      android:id="@+id/lovebtn"
      android:src="@drawable/ic_favourite"
      android:paddingTop="5dp"
      android:paddingBottom="5dp"
      android:fitsSystemWindows="true"
      .../>
    
fitsSystemWindows 사용

그림 8. fitsSystemWindows 속성 사용

정의한 상단 및 하단 패딩 값은 모든 항목이 시스템 창에 맞도록 재정의됩니다. 수정하는 방법은 InsetDrawable을 사용하여 패딩 값을 대체하는 것입니다.

inset_favourite.xml 파일을 만들어 패딩 값을 정의합니다.

    <inset
      xmlns:android="http://schemas.android.com/apk/res/android"
      android:drawable="@drawable/ic_favourite"
      android:insetTop="5dp"
      android:insetBottom="5dp" />
    

activity_main.xml에서 패딩을 삭제합니다.

    <ImageButton
      android:id="@+id/lovebtn"
      android:src="@drawable/inset_favourite"
      android:paddingTop="5dp"
      android:paddingBottom="5dp"
      android:fitsSystemWindows="true"
      .../>
    
InsetDrawables 사용

그림 9. InsetDrawables 사용

프로그래매틱 방식으로 하단부 관리

XML을 사용하는 선언적 방식에서 가능한 것보다 더 많이 레이아웃을 제어해야 하는 경우 프로그래매틱 방식으로 레이아웃을 조정할 수 있습니다. 하단부의 크기를 확보하려면 레이아웃의 가장 바깥쪽 뷰에 View.OnApplyWindowInsetsListener를 첨부해야 합니다.

다음을 MainActivity.java에 추가합니다.

Kotlin

    private var chinSize: Int = 0

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        // find the outermost element
        findViewById<View>(R.id.outer_container).apply {
            // attach a View.OnApplyWindowInsetsListener
            setOnApplyWindowInsetsListener { v, insets ->
                chinSize = insets.systemWindowInsetBottom
                // The following line is important for inner elements which react to insets
                v.onApplyWindowInsets(insets)
                insets
            }
        }
    }
    

자바

    private int chinSize;
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        // find the outermost element
        final View container = findViewById(R.id.outer_container);
        // attach a View.OnApplyWindowInsetsListener
        container.setOnApplyWindowInsetsListener(new View.OnApplyWindowInsetsListener() {
            @Override
            public WindowInsets onApplyWindowInsets(View v, WindowInsets insets) {
                chinSize = insets.getSystemWindowInsetBottom();
                // The following line is important for inner elements which react to insets
                v.onApplyWindowInsets(insets);
                return insets;
            }
        });
    }