6월 3일의 ⁠#Android11: 베타 버전 출시 행사에 참여하세요.

앱 위젯 빌드

앱 위젯은 다른 애플리케이션(예: 홈 화면)에 삽입되어 주기적인 업데이트를 받을 수 있는 소형 애플리케이션 뷰입니다. 이러한 뷰는 사용자 인터페이스에서 위젯이라고하며 앱 위젯 공급자를 사용하여 게시할 수 있습니다. 다른 앱 위젯을 포함할 수 있는 애플리케이션 구성요소를 앱 위젯 호스트라고 합니다. 아래 스크린샷은 음악 앱 위젯을 보여줍니다.

이 문서에서는 앱 위젯 공급자를 사용하여 앱 위젯을 게시하는 방법을 설명합니다. 앱 위젯을 호스팅하기 위해 나만의 AppWidgetHost를 만드는 방법은 앱 위젯 호스트를 참조하세요.

참고: 앱 위젯을 디자인하는 방법에 관한 자세한 내용은 앱 위젯 개요를 읽어보세요.

기본 사항

앱 위젯을 만들려면 다음이 필요합니다.

AppWidgetProviderInfo 객체
앱 위젯의 레이아웃, 업데이트 빈도, AppWidgetProvider 클래스 등 앱 위젯의 메타데이터를 설명합니다. XML로 정의해야 합니다.
AppWidgetProvider 클래스 구현
브로드캐스트 이벤트를 기반으로 앱 위젯과 프로그래매틱 방식으로 접속할 수 있는 기본적인 방법을 정의합니다. 이를 통해 앱 위젯이 업데이트, 사용 설정, 사용 중지, 삭제될 때 브로드캐스트를 수신하게 됩니다.
레이아웃 보기
XML로 정의된 앱 위젯의 초기 레이아웃을 정의합니다.

또한 앱 위젯 구성 활동을 구현할 수 있습니다. 사용자가 앱 위젯을 추가할 때 실행되고 작성 시 사용자가 앱 위젯 설정을 수정할 수 있도록 하는 선택적 Activity입니다.

다음 섹션에서는 이러한 각 구성요소를 설정하는 방법을 설명합니다.

manifest에서 앱 위젯 선언

먼저 애플리케이션의 AndroidManifest.xml 파일에서 AppWidgetProvider 클래스를 선언하세요. 예:

    <receiver android:name="ExampleAppWidgetProvider" >
        <intent-filter>
            <action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
        </intent-filter>
        <meta-data android:name="android.appwidget.provider"
                   android:resource="@xml/example_appwidget_info" />
    </receiver>
    

<receiver> 요소에는 앱 위젯에서 사용하는 AppWidgetProvider를 지정하는 android:name 속성이 필요합니다.

<intent-filter> 요소에는 android:name 속성이 있는 <action> 요소를 포함해야 합니다. 이 속성은 AppWidgetProvider에서 ACTION_APPWIDGET_UPDATE 브로드캐스트를 허용한다는 것을 지정합니다. 이 브로드캐스트만 명시적으로 선언하면 됩니다. AppWidgetManager는 필요한 경우 다른 모든 앱 위젯 브로드캐스트를 AppWidgetProvider로 자동 전송합니다.

<meta-data> 요소는 AppWidgetProviderInfo 리소스를 지정하며 다음과 같은 속성이 필요합니다.

  • android:name - 메타데이터 이름을 지정합니다. 데이터를 AppWidgetProviderInfo 설명자로 표시하려면 android.appwidget.provider를 사용하세요.
  • android:resource - AppWidgetProviderInfo 리소스 위치를 지정합니다.

AppWidgetProviderInfo 메타데이터 추가

AppWidgetProviderInfo는 최소 레이아웃 크기, 초기 레이아웃 리소스, 앱 위젯을 업데이트하는 빈도, 작성 시 실행할 구성 활동(선택사항) 등 앱 위젯의 기본적인 특성을 정의합니다. XML 리소스에서 단일 <appwidget-provider> 요소를 사용하여 AppWidgetProviderInfo 객체를 정의하고 프로젝트의 res/xml/ 폴더에 저장하세요.

예:

    <appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android"
        android:minWidth="40dp"
        android:minHeight="40dp"
        android:updatePeriodMillis="86400000"
        android:previewImage="@drawable/preview"
        android:initialLayout="@layout/example_appwidget"
        android:configure="com.example.android.ExampleAppWidgetConfigure"
        android:resizeMode="horizontal|vertical"
        android:widgetCategory="home_screen">
    </appwidget-provider>
    

다음은 <appwidget-provider> 속성의 요약입니다.

  • minWidthminHeight 속성의 값은 앱 위젯에서 기본적으로 사용하는 공간의 최소 크기를 지정합니다. 기본 홈 화면은 높이와 너비가 정의된 셀의 그리드를 기반으로 창에 앱 위젯을 배치합니다. 앱 위젯의 최소 너비 또는 높이 값이 셀의 크기와 일치하지 않으면 앱 위젯 크기가 가장 가까운 셀 크기로 반올림됩니다.

    앱 위젯의 크기 조정에 관한 자세한 내용은 앱 위젯 디자인 가이드라인을 참조하세요.

    참고: 앱 위젯을 기기 간에 이동 가능하도록 만들려면 앱 위젯의 최소 크기가 4x4셀보다 크지 않아야 합니다.

  • minResizeWidthminResizeHeight 속성은 앱 위젯의 절대 최소 크기를 지정합니다. 앱 위젯의 크기가 이 값보다 작으면 앱 위젯을 알아볼 수 없거나 사용할 수 없습니다. 이러한 속성을 사용하면 사용자가 위젯의 크기를 minWidthminHeight 속성에 의해 정의된 기본 위젯 크기보다 작게 조정할 수 있습니다. Android 3.1에 도입되었습니다.

    앱 위젯의 크기 조정에 관한 자세한 내용은 앱 위젯 디자인 가이드라인을 참조하세요.

  • updatePeriodMillis 속성은 앱 위젯 프레임워크에서 onUpdate() 콜백 메서드를 호출하여 AppWidgetProvider에 업데이트를 요청해야 하는 빈도를 정의합니다. 정의된 빈도에 따라 제시간에 정확히 실제 업데이트가 발생하지 않을 수도 있으며 배터리를 절약하기 위해 업데이트 주기를 최대한 길게 하여 한 시간에 한 번만 업데이트하는 것이 좋습니다. 사용자가 구성에서 빈도를 조정하도록 할 수도 있습니다. 주식 시세를 15분마다 업데이트하고 싶은 사람도 있고 하루에 네 번만 업데이트하고 싶은 사람도 있습니다.

    참고: 업데이트가 필요할 때 기기가 대기 상태인 경우(updatePeriodMillis에 의해 정의된 대로) 업데이트를 실행하기 위해 대기 모드가 해제됩니다. 시간당 두 번 이상 업데이트하지 않으면 아마도 배터리 수명에 심각한 문제가 발생하지 않을 것입니다. 하지만 더 자주 업데이트해야 하거나 기기가 대기 상태인 동안 업데이트하지 않아도 되는 경우 기기의 대기 모드를 해제하지 않는 알람을 기반으로 업데이트를 대신 실행할 수 있습니다. 그렇게 하려면 AlarmManager를 사용하여 AppWidgetProvider가 수신하는 인텐트로 알람을 설정하세요. 알람 유형을 ELAPSED_REALTIME 또는 RTC로 설정하세요. 이렇게 하면 기기의 대기 모드가 해제된 경우에만 알람이 전달됩니다. 그런 다음 updatePeriodMillis"0"으로 설정하세요.

  • initialLayout 속성은 앱 위젯 레이아웃을 정의하는 레이아웃 리소스를 가리킵니다.
  • configure 속성은 사용자가 앱 위젯을 추가할 때 앱 위젯 속성을 구성하기 위해 실행할 수 있는 Activity를 정의합니다. 이 속성은 선택사항입니다. 아래의 앱 위젯 구성 활동 만들기를 읽어보세요.
  • previewImage 속성은 앱 위젯이 구성된 후 사용자가 앱 위젯을 선택할 때 표시되는 앱 위젯의 미리보기를 지정합니다. 미리보기가 제공되지 않은 경우 애플리케이션의 런처 아이콘이 대신 표시됩니다. 이 필드는 AndroidManifest.xml 파일에 있는 <receiver> 요소의 android:previewImage 속성에 상응합니다. previewImage 사용에 관한 자세한 내용은 미리보기 이미지 설정을 참조하세요. Android 3.0에 도입되었습니다.
  • autoAdvanceViewId 속성은 위젯의 호스트에서 자동으로 진행해야 하는 앱 위젯 하위 뷰의 뷰 ID를 지정합니다. Android 3.0에 도입되었습니다.
  • resizeMode 속성은 위젯의 크기를 조절할 수 있는 규칙을 지정합니다. 이 속성을 사용하여 홈 화면 위젯의 크기를 가로, 세로 또는 두 축 모두의 방향으로 조절 가능하도록 만들 수 있습니다. 사용자는 위젯을 길게 터치하여 크기 조절 핸들을 표시한 다음 가로 및 세로 핸들을 드래그하여 레이아웃 그리드에서 크기를 변경합니다. resizeMode 속성 값에는 'horizontal', 'vertical', 'none'이 포함됩니다. 위젯의 크기를 가로 및 세로로 조절 가능하도록 선언하려면 값을 'horizontal|vertical'로 지정하세요. Android 3.1에 도입되었습니다.
  • minResizeHeight 속성은 위젯의 크기를 조절할 수 있는 최소 높이를 dps 단위로 지정합니다. 이 필드는 minHeight보다 크거나 세로 크기 조절이 사용 설정되지 않은 경우(resizeMode 참조) 효과가 없습니다. Android 4.0에 도입되었습니다.
  • minResizeWidth 속성은 위젯의 크기를 조절할 수 있는 최소 너비를 dps 단위로 지정합니다. 이 필드는 minWidth보다 크거나 가로 크기 조절이 사용 설정되지 않은 경우(resizeMode 참조) 효과가 없습니다. Android 4.0에 도입되었습니다.
  • widgetCategory 속성은 앱 위젯을 홈 화면(home_screen), 잠금 화면(keyguard) 또는 둘 다에 표시할 수 있는지 여부를 선언합니다. 5.0 미만의 Android 버전에서만 잠금 화면 위젯을 지원합니다. Android 5.0 이상에서는 home_screen만 유효합니다.

<appwidget-provider> 요소에 의해 허용되는 속성에 관한 자세한 내용은 AppWidgetProviderInfo 클래스를 참조하세요.

앱 위젯 레이아웃 만들기

앱 위젯의 초기 레이아웃을 XML로 정의하고 프로젝트의 res/layout/ 디렉터리에 저장해야 합니다. 아래 나열된 뷰 객체를 사용하여 앱 위젯을 디자인할 수 있지만 앱 위젯을 디자인하기 전에 앱 위젯 디자인 가이드라인 을 읽고 이해하세요.

레이아웃에 익숙한 경우 간단히 앱 위젯 레이아웃을 만들 수 있습니다. 하지만 앱 위젯 레이아웃은 모든 종류의 레이아웃 또는 뷰 위젯을 지원하지는 않는 RemoteViews을 기반한다는 것을 알아야 합니다.

RemoteViews 객체(및 결과적인 앱 위젯)는 다음과 같은 레이아웃 클래스를 지원할 수 있습니다.

다음과 같은 위젯 클래스도 지원할 수 있습니다.

이러한 클래스의 하위 항목은 지원되지 않습니다.

RemoteViews는 런타임에 레이아웃 리소스를 느리게 확장하는 데 사용할 수 있는 보이지 않고 크기가 0인 뷰인 ViewStub도 지원합니다.

앱 위젯에 여백 추가

위젯은 일반적으로 화면 가장자리까지 확장되지 않아야 하고 시각적으로 다른 위젯과 일치하지 않아야 하므로 위젯 프레임의 모든 면에 여백을 추가해야 합니다.

Android 4.0 기준으로, 위젯 프레임과 앱 위젯의 경계 상자 사이에는 사용자 홈 화면의 다른 위젯 및 아이콘과 더 잘 정렬되도록 자동으로 패딩이 추가됩니다. 이 권장되는 동작을 활용하려면 애플리케이션의 targetSdkVersion을 14 이상으로 설정하세요.

플랫폼의 이전 버전에 적용된 맞춤 여백이 있고 Android 4.0 이상을 위한 추가 여백이 없는 단일 레이아웃을 쉽게 작성할 수 있습니다.

  1. 애플리케이션의 targetSdkVersion을 14 이상으로 설정합니다.
  2. 아래의 레이아웃과 같이 여백에 관해 측정기준 리소스를 참조하는 레이아웃을 만듭니다.
        <FrameLayout
          android:layout_width="match_parent"
          android:layout_height="match_parent"
          android:padding="@dimen/widget_margin">
    
          <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:orientation="horizontal"
            android:background="@drawable/my_widget_background">
            …
          </LinearLayout>
    
        </FrameLayout>
        
  3. res/values/에 Android 4.0 이전 맞춤 여백을 제공하는 측정기준 리소스 하나 및 res/values-v14/에 Android 4.0 위젯에 추가 패딩을 제공하지 않는 측정기준 리소스 하나, 이렇게 측정기준 리소스 두 개를 만듭니다.

    res/values/dimens.xml:

    <dimen name="widget_margin">8dp</dimen>

    res/values-v14/dimens.xml:

    <dimen name="widget_margin">0dp</dimen>

또 한 가지 방법은 기본적으로 나인 패치 배경 애셋에 추가 여백을 빌드하고 API 레벨 14 이상의 경우 여백이 없는 다양한 나인 패치를 제공하는 것입니다.

AppWidgetProvider 클래스 사용

AppWidgetProvider 클래스는 BroadcastReceiver를 앱 위젯 브로드캐스트를 처리하기 위한 편의성 클래스로 확장합니다. AppWidgetProvider는 앱 위젯이 업데이트, 삭제, 사용 설정 및 사용 중지되는 경우와 같이 앱 위젯과 관련성 높은 이벤트 브로드캐스트만 수신합니다. 이러한 브로드캐스트 이벤트가 발생하면 AppWidgetProvider는 다음과 같은 메서드 호출을 수신합니다.

onUpdate()
AppWidgetProviderInfo의 updatePeriodMillis 속성에 의해 정의된 간격으로 앱 위젯을 업데이트하기 위해 호출됩니다(위의 AppWidgetProviderInfo 메타데이터 추가 참조). 이 메서드는 사용자가 앱 위젯을 추가할 때도 호출되므로 뷰의 이벤트 핸들러 정의와 같은 필수 설정을 실행하고 필요한 경우 임시 Service를 시작해야 합니다. 하지만 구성 활동을 선언한 경우 사용자가 앱 위젯을 추가할 때 이 메서드가 호출되지 않지만 후속 업데이트의 경우 호출됩니다. 구성이 완료된 경우 구성 활동에서 첫 번째 업데이트를 실행해야 합니다. 아래의 앱 위젯 구성 활동 만들기를 참조하세요.
onAppWidgetOptionsChanged()
위젯이 처음으로 배치될 때와 위젯의 크기가 조절될 때 호출됩니다. 이 콜백을 사용하여 위젯의 크기 범위를 기반으로 콘텐츠를 표시하거나 숨길 수 있습니다. 다음이 포함된 Bundle을 반환하는 getAppWidgetOptions()를 호출하여 크기 범위를 가져옵니다.

이 콜백은 API 레벨 16(Android 4.1)에서 도입되었습니다. 이 콜백을 구현하는 경우 이전 기기에서 콜백이 호출되지 않으므로 앱이 콜백에 의존하지 않는지 확인하세요.
onDeleted(Context, int[])
앱 위젯이 앱 위젯 호스트에서 삭제될 때마다 호출됩니다.
onEnabled(Context)
앱 위젯의 인스턴스가 처음으로 생성될 때 호출됩니다. 예를 들어 사용자가 앱 위젯의 인스턴스를 두 개 추가하는 경우 처음에만 호출됩니다. 새 데이터베이스를 열거나 모든 앱 위젯 인스턴스에 한 번만 발생해야 하는 다른 설정을 실행해야 하는 경우 여기에서 실행하세요.
onDisabled(Context)
앱 위젯의 마지막 인스턴스가 앱 위젯 호스트에서 삭제될 때 호출됩니다. 여기에서 임시 데이터베이스 삭제와 같이 onEnabled(Context)에서 실행된 모든 작업을 정리해야 합니다.
onReceive(Context, Intent)
모든 브로드캐스트에서 위의 각 콜백 메서드 이전에 호출됩니다. 기본 AppWidgetProvider 구현은 모든 앱 위젯 브로드캐스트를 필터링하고 위 메서드를 적절하게 호출하므로 일반적으로 이 메서드를 구현할 필요가 없습니다.

AndroidManifest의 <receiver> 요소를 사용하여 AppWidgetProvider 클래스 구현을 broadcast receiver로 선언해야 합니다(위의 manifest에서 앱 위젯 선언을 참조).

가장 중요한 AppWidgetProvider 콜백은 구성 활동을 사용하지 않는 한 각 앱 위젯이 호스트에 추가될 때 호출되므로 onUpdate()입니다. 앱 위젯에서 사용자 상호작용 이벤트를 허용하는 경우 이 콜백에서 이벤트 핸들러를 등록해야 합니다. 앱 위젯에서 임시 파일이나 데이터베이스를 만들거나 정리가 필요한 다른 작업을 실행하지 않는 경우 onUpdate() 콜백 메서드만 정의하면 됩니다. 예를 들어 버튼을 클릭하면 활동이 실행되는 버튼이 있는 앱 위젯이 필요한 경우 다음과 같은 AppWidgetProvider 구현을 사용할 수 있습니다.

Kotlin

    class ExampleAppWidgetProvider : AppWidgetProvider() {

        override fun onUpdate(
                context: Context,
                appWidgetManager: AppWidgetManager,
                appWidgetIds: IntArray
        ) {
            // Perform this loop procedure for each App Widget that belongs to this provider
            appWidgetIds.forEach { appWidgetId ->
                // Create an Intent to launch ExampleActivity
                val pendingIntent: PendingIntent = Intent(context, ExampleActivity::class.java)
                        .let { intent ->
                            PendingIntent.getActivity(context, 0, intent, 0)
                        }

                // Get the layout for the App Widget and attach an on-click listener
                // to the button
                val views: RemoteViews = RemoteViews(
                        context.packageName,
                        R.layout.appwidget_provider_layout
                ).apply {
                    setOnClickPendingIntent(R.id.button, pendingIntent)
                }

                // Tell the AppWidgetManager to perform an update on the current app widget
                appWidgetManager.updateAppWidget(appWidgetId, views)
            }
        }
    }
    

자바

    public class ExampleAppWidgetProvider extends AppWidgetProvider {

        public void onUpdate(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds) {
            final int N = appWidgetIds.length;

            // Perform this loop procedure for each App Widget that belongs to this provider
            for (int i=0; i<N; i++) {
                int appWidgetId = appWidgetIds[i];

                // Create an Intent to launch ExampleActivity
                Intent intent = new Intent(context, ExampleActivity.class);
                PendingIntent pendingIntent = PendingIntent.getActivity(context, 0, intent, 0);

                // Get the layout for the App Widget and attach an on-click listener
                // to the button
                RemoteViews views = new RemoteViews(context.getPackageName(), R.layout.appwidget_provider_layout);
                views.setOnClickPendingIntent(R.id.button, pendingIntent);

                // Tell the AppWidgetManager to perform an update on the current app widget
                appWidgetManager.updateAppWidget(appWidgetId, views);
            }
        }
    }
    

이 AppWidgetProvider는 Activity를 실행하는 PendingIntent를 정의하고 setOnClickPendingIntent(int, PendingIntent)로 앱 위젯의 버튼에 첨부하기 위한 목적으로 onUpdate() 메서드만을 정의합니다. 여기에는 이 제공자가 만든 각 앱 위젯을 나타내는 ID의 배열인 appWidgetIds의 각 항목을 통해 반복되는 루프가 포함됩니다. 이러한 방식으로 사용자가 앱 위젯의 인스턴스를 두 개 이상 만드는 경우 모든 인스턴스가 동시에 업데이트됩니다. 하지만 앱 위젯의 모든 인스턴스에 하나의 updatePeriodMillis 일정만 관리됩니다. 예를 들어 업데이트 간격이 두 시간으로 정의되고 앱 위젯의 두 번째 인스턴스가 첫 번째 인스턴스의 한 시간 후에 추가되면 첫 번째 인스턴스에 의해 정의된 기간에 둘 다 업데이트되고 두 번째 업데이트 기간은 무시됩니다(둘 다 한 시간이 아니라 두 시간마다 업데이트됩니다).

참고: AppWidgetProviderBroadcastReceiver의 확장 프로그램이므로 콜백 메서드가 돌아간 후 프로세스가 계속 실행될 것이라고 보장되지 않습니다(브로드캐스트 수명 주기에 관한 내용은 BroadcastReceiver 참조). 앱 위젯 설정 프로세스에 몇 초가 소요될 수 있고(예를 들어 웹 요청을 실행하는 동안) 프로세스가 계속되어야 하는 경우 onUpdate() 메서드에서 Service를 시작해보세요. 서비스 내에서 애플리케이션 응답 없음(ANR) 오류로 인한 AppWidgetProvider 종료를 걱정하지 않고 앱 위젯을 직접 업데이트할 수 있습니다. Service를 실행하는 앱 위젯의 예는 Wiktionary 샘플의 AppWidgetProvider를 참조하세요.

ExampleAppWidgetProvider.java 샘플 클래스도 참조하세요.

앱 위젯 브로드캐스트 인텐트 수신

AppWidgetProvider는 편의성 클래스입니다. 앱 위젯 브로드캐스트를 직접 수신하려면 자신만의 BroadcastReceiver를 구현하거나 onReceive(Context, Intent) 콜백을 재정의하세요. 다음과 같은 인텐트에 관심을 가져야 합니다.

앱 위젯 고정

Android 8.0(API 레벨 26) 이상을 실행하는 기기에서 고정된 바로가기를 만들 수 있는 런처를 사용하여 앱 위젯을 런처에 고정할 수도 있습니다. 고정된 바로가기와 마찬가지로 고정된 위젯을 사용하면 앱의 특정 작업에 액세스할 수 있습니다.

앱에서 다음 단계를 완료하여 지원되는 런처에 위젯을 고정하도록 시스템에 요청할 수 있습니다.

  1. 다음 스니펫에 표시된 대로 앱의 manifest 파일에서 위젯을 만듭니다.
        <manifest>
        ...
          <application>
            ...
            <receiver android:name="MyAppWidgetProvider">
                <intent-filter>
                    <action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
                </intent-filter>
                <meta-data android:name="android.appwidget.provider"
                           android:resource="@xml/my_appwidget_info" />
            </receiver>
          </application>
        </manifest>
        
  2. 다음 코드 스니펫에 표시된 대로 requestPinAppWidget() 메서드를 호출합니다.

    Kotlin

        val appWidgetManager: AppWidgetManager = context.getSystemService(AppWidgetManager::class.java)
        val myProvider = ComponentName(context, MyAppWidgetProvider::class.java)
    
        val successCallback: PendingIntent? = if (appWidgetManager.isRequestPinAppWidgetSupported) {
            // Create the PendingIntent object only if your app needs to be notified
            // that the user allowed the widget to be pinned. Note that, if the pinning
            // operation fails, your app isn't notified.
            Intent(...).let { intent ->
                // Configure the intent so that your app's broadcast receiver gets
                // the callback successfully. This callback receives the ID of the
                // newly-pinned widget (EXTRA_APPWIDGET_ID).
                PendingIntent.getBroadcast(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT)
            }
        } else {
            null
        }
    
        successCallback?.also { pendingIntent ->
            appWidgetManager.requestPinAppWidget(myProvider, null, pendingIntent)
        }
        

    자바

        AppWidgetManager appWidgetManager =
                context.getSystemService(AppWidgetManager.class);
        ComponentName myProvider =
                new ComponentName(context, MyAppWidgetProvider.class);
    
        if (appWidgetManager.isRequestPinAppWidgetSupported()) {
            // Create the PendingIntent object only if your app needs to be notified
            // that the user allowed the widget to be pinned. Note that, if the pinning
            // operation fails, your app isn't notified.
            Intent pinnedWidgetCallbackIntent = new Intent( ... );
    
            // Configure the intent so that your app's broadcast receiver gets
            // the callback successfully. This callback receives the ID of the
            // newly-pinned widget (EXTRA_APPWIDGET_ID).
            PendingIntent successCallback = PendingIntent.getBroadcast(context, 0,
                    pinnedWidgetCallbackIntent, PendingIntent.FLAG_UPDATE_CURRENT);
    
            appWidgetManager.requestPinAppWidget(myProvider, null, successCallback);
        }
        

참고: 시스템에서 지원되는 런처에 위젯을 고정했는지 여부를 앱에 알리지 않아도 되는 경우 nullrequestPinAppWidget()에 세 번째 인수로 전달할 수 있습니다.

앱 위젯 구성 활동 만들기

사용자가 새 앱 위젯을 추가할 때 설정을 구성하도록 하려면 앱 위젯 구성 활동을 만드세요. 이 Activity는 앱 위젯 호스트에 의해 자동으로 실행되며 사용자가 이를 사용하여 작성 시 앱 위젯 색상, 크기, 업데이트 기간, 기타 기능 설정 등 앱 위젯에 사용 가능한 설정을 구성할 수 있습니다.

구성 활동은 Android manifest 파일에 일반 활동으로 선언해야 합니다. 하지만 ACTION_APPWIDGET_CONFIGURE 작업을 통해 앱 위젯 호스트에 의해 실행되므로 활동에서 이 인텐트를 수락해야 합니다. 예:

    <activity android:name=".ExampleAppWidgetConfigure">
        <intent-filter>
            <action android:name="android.appwidget.action.APPWIDGET_CONFIGURE"/>
        </intent-filter>
    </activity>
    

또한 AppWidgetProviderInfo XML 파일에서 android:configure 속성을 사용하여 활동을 선언해야 합니다(위의 AppWidgetProviderInfo 메타데이터 추가 참조). 예를 들어 구성 활동은 다음과 같이 선언할 수 있습니다.

    <appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android"
        ...
        android:configure="com.example.android.ExampleAppWidgetConfigure"
        ... >
    </appwidget-provider>
    

활동은 패키지 범위 외부에서 참조되므로 정규화된 네임스페이스로 선언됩니다.

활동을 선언한 후 구성 작업을 시작할 수 있습니다. 이제 실제 활동만 있으면 됩니다. 하지만 활동을 구현할 때 기억해야 할 두 가지 중요한 사항이 있습니다.

  • 앱 위젯 호스트는 구성 활동을 호출하며 구성 활동은 항상 결과를 반환해야 합니다. 결과에는 활동을 실행한 인텐트에 의해 전달된 (추가 인텐트에 EXTRA_APPWIDGET_ID로 저장된) 앱 위젯 ID가 포함되어야 합니다.
  • onUpdate() 메서드는 앱 위젯이 작성될 때 호출되지 않습니다(구성 활동이 실행될 때 시스템에서 ACTION_APPWIDGET_UPDATE 브로드캐스트를 보내지 않습니다). 앱 위젯이 처음 작성될 때 AppWidgetManager에 업데이트를 요청하는 것은 구성 활동의 책임입니다. 하지만 이후 업데이트에서는 onUpdate()가 호출됩니다. 처음에만 건너뜁니다.

구성에서 결과를 반환하고 앱 위젯을 업데이트하는 방법의 예는 다음 섹션의 코드 스니펫을 참조하세요.

구성 활동에서 앱 위젯 업데이트

App 위젯이 구성 활동을 사용하는 경우 구성이 완료된 후 앱 위젯을 업데이트하는 것은 활동의 책임입니다. AppWidgetManager에서 직접 업데이트를 요청하여 그렇게 할 수 있습니다.

다음은 앱 위젯을 제대로 업데이트하고 구성 활동을 닫는 절차를 요약한 것입니다.

  1. 먼저 활동을 실행한 인텐트에서 앱 위젯 ID를 가져옵니다.

    Kotlin

        appWidgetId = intent?.extras?.getInt(
                AppWidgetManager.EXTRA_APPWIDGET_ID,
                AppWidgetManager.INVALID_APPWIDGET_ID
        ) ?: AppWidgetManager.INVALID_APPWIDGET_ID
        

    자바

        Intent intent = getIntent();
        Bundle extras = intent.getExtras();
        if (extras != null) {
            appWidgetId = extras.getInt(
                    AppWidgetManager.EXTRA_APPWIDGET_ID,
                    AppWidgetManager.INVALID_APPWIDGET_ID);
        }
        
  2. 앱 위젯 구성을 실행합니다.
  3. 구성이 완료되면 getInstance(Context)를 호출하여 AppWidgetManager의 인스턴스를 가져옵니다.

    Kotlin

        val appWidgetManager: AppWidgetManager = AppWidgetManager.getInstance(context)
        

    자바

        AppWidgetManager appWidgetManager = AppWidgetManager.getInstance(context);
        
  4. updateAppWidget(int, RemoteViews)를 호출하여 RemoteViews 레이아웃으로 앱 위젯을 업데이트합니다.

    Kotlin

        RemoteViews(context.packageName, R.layout.example_appwidget).also { views->
            appWidgetManager.updateAppWidget(appWidgetId, views)
        }
        

    자바

        RemoteViews views = new RemoteViews(context.getPackageName(),
        R.layout.example_appwidget);
        appWidgetManager.updateAppWidget(appWidgetId, views);
        
  5. 마지막으로 반환 인텐트를 만들고 활동 결과를 사용하여 설정한 다음 활동을 완료합니다.

    Kotlin

        val resultValue = Intent().apply {
            putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetId)
        }
        setResult(Activity.RESULT_OK, resultValue)
        finish()
        

    자바

        Intent resultValue = new Intent();
        resultValue.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetId);
        setResult(RESULT_OK, resultValue);
        finish();
        

팁: 구성 활동이 처음 열리면 위의 5 단계에 표시된 대로 활동 결과를 EXTRA_APPWIDGET_ID와 함께 RESULT_CANCELED로 설정합니다. 이러한 방식으로 사용자가 끝에 도달하기 전에 활동을 취소하면 앱 위젯 호스트에 구성이 취소되었음을 알리고 앱 위젯이 추가되지 않습니다.

예를 보려면 ApiDemos의 ExampleAppWidgetConfigure.java 샘플 클래스를 참조하세요.

미리보기 이미지 설정

Android 3.0에서 앱 위젯의 미리보기를 지정하는 previewImage 필드가 도입되었습니다. 이 미리보기는 위젯 선택도구에서 사용자에게 표시됩니다. 이 필드가 제공되지 않으면 앱 위젯의 아이콘이 미리보기에 사용됩니다.

다음 방법으로 XML에서 이 설정을 지정합니다.

<appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android"
      ...
      android:previewImage="@drawable/preview">
    </appwidget-provider>

Android Emulator에는 앱 위젯에서 (previewImage 필드에 지정할 수 있는) 미리보기 이미지를 만드는 데 도움이 되는 'Widget Preview'라는 애플리케이션이 포함되어 있습니다. 미리보기 이미지를 만들려면 이 애플리케이션을 실행하고 애플리케이션의 앱 위젯을 선택하고 미리보기 이미지를 표시할 방법을 설정한 다음 저장하고 애플리케이션의 드로어블 리소스에 추가하세요.

컬렉션이 포함된 앱 위젯 사용

Android 3.0에서 컬렉션이 포함된 앱 위젯이 도입되었습니다. 이러한 종류의 앱 위젯에서는 RemoteViewsService를 사용하여 콘텐츠 제공업체에서 제공하는 데이터와 같이 원격 데이터에 의해 지원되는 컬렉션을 표시합니다. RemoteViewsService에서 제공하는 데이터는 '컬렉션 뷰'라고 하는 다음과 같은 뷰 유형 중 하나를 사용하여 앱 위젯에 표시됩니다.

ListView
항목을 세로로 스크롤되는 목록으로 표시하는 뷰. Gmail 앱 위젯의 예를 참조하세요.
GridView
항목을 2차원으로 스크롤되는 그리드로 표시하는 뷰. 북마크 앱 위젯의 예를 참조하세요.
StackView
사용자가 전면 카드를 위/아래로 돌려 이전/다음 카드를 볼 수 있는 스택형 카드 뷰(예: rolodex). 예를 들어 YouTube 및 도서 앱 위젯이 있습니다.
AdapterViewFlipper
두 개 이상의 뷰 간을 애니메이션하는 어댑터 지원 단순 ViewAnimator. 한 번에 하나의 하위 요소만 표시됩니다.

위에서 설명한 바와 같이 이러한 컬렉션 뷰는 원격 데이터에 의해 지원되는 컬렉션을 표시합니다. 즉 Adapter를 사용하여 사용자 인터페이스를 데이터에 바인딩합니다. Adapter는 데이터 세트의 개별 항목을 개별 View 객체에 바인딩합니다. 이러한 컬렉션 뷰는 어댑터에 의해 지원되므로 Android 프레임워크에는 앱 위젯에서 사용할 수 있도록 지원하는 추가 아키텍처가 포함되어야 합니다. 앱 위젯의 컨텍스트에서 AdapterAdapter 인터페이스를 감싸는 씬 래퍼인 RemoteViewsFactory로 대체됩니다. 컬렉션의 특정 항목을 위해 요청되는 경우 RemoteViewsFactory에서는 컬렉션을 위한 항목을 만들고 RemoteViews 객체로 반환합니다. 앱 위젯에 컬렉션 뷰를 포함하려면 RemoteViewsServiceRemoteViewsFactory를 구현해야 합니다.

RemoteViewsService는 원격 어댑터가 RemoteViews 객체를 요청할 수 있는 서비스입니다. RemoteViewsFactory는 컬렉션 뷰(예: ListView, GridView)와 뷰의 기본 데이터 사이의 어댑터를 위한 인터페이스입니다. StackWidget 샘플의 경우 이 서비스와 인터페이스를 구현하는 데 사용하는 상용구 코드의 예는 다음과 같습니다.

Kotlin

    class StackWidgetService : RemoteViewsService() {

        override fun onGetViewFactory(intent: Intent): RemoteViewsFactory {
            return StackRemoteViewsFactory(this.applicationContext, intent)
        }
    }

    class StackRemoteViewsFactory(
            private val context: Context,
            intent: Intent
    ) : RemoteViewsService.RemoteViewsFactory {

    //... include adapter-like methods here. See the StackView Widget sample.

    }
    

자바

    public class StackWidgetService extends RemoteViewsService {
        @Override
        public RemoteViewsFactory onGetViewFactory(Intent intent) {
            return new StackRemoteViewsFactory(this.getApplicationContext(), intent);
        }
    }

    class StackRemoteViewsFactory implements RemoteViewsService.RemoteViewsFactory {

    //... include adapter-like methods here. See the StackView Widget sample.

    }
    

샘플 애플리케이션

이 섹션의 발췌한 코드는 StackWidget 샘플에서 가져온 것입니다.

이 샘플은 "0!"~"9!" 값을 표시하는 뷰 10개의 스택으로 구성됩니다. 샘플 앱 위젯에는 다음과 같은 기본 동작이 있습니다.

  • 사용자는 앱 위젯의 상단 뷰를 세로로 흔들어 다음 또는 이전 뷰를 표시할 수 있습니다. 이 동작은 내장된 StackView 동작입니다.
  • 앱 위젯은 아무런 사용자 상호작용 없이 슬라이드쇼와 같이 뷰 사이를 자동으로 순차적으로 이동합니다. 이는 res/xml/stackwidgetinfo.xml 파일의 android:autoAdvanceViewId="@id/stack_view" 설정 때문입니다. 이 설정은 뷰 ID(이 경우 스택 뷰의 뷰 ID)에 적용됩니다.
  • 사용자가 상단 뷰를 터치하면 앱 위젯에서 Toast 메시지 "터치된 뷰 n"을 표시합니다. 여기서 n은 터치된 뷰의 색인(위치)입니다. 구현 방법에 관한 자세한 내용은 개별 항목에 동작 추가를 참조하세요.

컬렉션이 포함된 앱 위젯 구현

컬렉션이 포함된 앱 위젯을 구현하려면 모든 앱 위젯을 구현하는 데 사용하는 기본 단계를 따릅니다. 다음 섹션에서는 컬렉션이 포함된 앱 위젯을 구현하기 위해 실행해야 하는 추가 단계를 설명합니다.

컬렉션이 포함된 앱 위젯의 manifest

manifest에서 앱 위젯 선언에 나열된 요구사항 외에 컬렉션이 포함된 앱 위젯이 RemoteViewsService에 바인딩할 수 있도록 하려면 BIND_REMOTEVIEWS 권한을 사용하여 manifest 파일에서 서비스를 선언해야 합니다. 이렇게 하면 다른 애플리케이션이 앱 위젯의 데이터에 자유롭게 액세스할 수 없습니다. 예를 들어 RemoteViewsService를 사용하여 컬렉션 뷰를 게재하는 앱 위젯을 만드는 경우 manifest 항목이 다음과 같이 표시될 수 있습니다.

<service android:name="MyWidgetService"
    ...
    android:permission="android.permission.BIND_REMOTEVIEWS" />

android:name="MyWidgetService" 행은 RemoteViewsService의 서브클래스를 가리킵니다.

컬렉션이 포함된 앱 위젯의 레이아웃

앱 위젯 레이아웃 XML 파일의 기본 요구사항은 컬렉션 뷰 ListView, GridView, StackView, AdapterViewFlipper 중 하나를 포함해야 한다는 것입니다. 다음은 StackWidget 샘플widget_layout.xml입니다.

<?xml version="1.0" encoding="utf-8"?>

    <FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
        android:layout_width="match_parent"
        android:layout_height="match_parent">
        <StackView xmlns:android="http://schemas.android.com/apk/res/android"
            android:id="@+id/stack_view"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:gravity="center"
            android:loopViews="true" />
        <TextView xmlns:android="http://schemas.android.com/apk/res/android"
            android:id="@+id/empty_view"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:gravity="center"
            android:background="@drawable/widget_item_background"
            android:textColor="#ffffff"
            android:textStyle="bold"
            android:text="@string/empty_view_text"
            android:textSize="20sp" />
    </FrameLayout>

빈 뷰는 빈 뷰가 빈 상태를 나타내는 컬렉션 뷰의 동위 요소여야 합니다.

전체 앱 위젯의 레이아웃 파일 외에 컬렉션에 포함된 각 항목의 레이아웃(예를 들어 책 컬렉션에 포함된 각 책의 레이아웃)을 정의하는 다른 레이아웃 파일을 만들어야 합니다 모든 항목이 동일한 레이아웃을 사용하므로 StackWidget 샘플에는 레이아웃 파일이 widget_item.xml 하나뿐입니다.

컬렉션이 포함된 앱 위젯의 AppWidgetProvider 클래스

일반 앱 위젯의 경우와 마찬가지로 AppWidgetProvider 서브클래스의 코드 대부분은 일반적으로 onUpdate()에 들어갑니다. 컬렉션이 포함된 앱 위젯을 만들 때 onUpdate() 구현의 주요한 차이점은 setRemoteAdapter()를 호출해야 한다는 것입니다. 이렇게 하면 컬렉션 뷰에 데이터를 가져올 위치를 알려줍니다. 그런 다음 RemoteViewsService에서 RemoteViewsFactory 구현을 반환하고 위젯에서 적절한 데이터를 제공할 수 있습니다. 이 메서드를 호출할 때 RemoteViewsService 구현을 가리키는 인텐트와 업데이트할 앱 위젯을 지정하는 앱 위젯 ID를 전달해야 합니다.

예를 들어 다음은 StackWidget 샘플이 onUpdate() 콜백 메서드를 호출하여 RemoteViewsService를 앱 위젯 컬렉션의 원격 어댑터로 설정하는 방법입니다.

Kotlin

    override fun onUpdate(
            context: Context,
            appWidgetManager: AppWidgetManager,
            appWidgetIds: IntArray
    ) {
        // update each of the app widgets with the remote adapter
        appWidgetIds.forEach { appWidgetId ->

            // Set up the intent that starts the StackViewService, which will
            // provide the views for this collection.
            val intent = Intent(context, StackWidgetService::class.java).apply {
                // Add the app widget ID to the intent extras.
                putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetId)
                data = Uri.parse(toUri(Intent.URI_INTENT_SCHEME))
            }
            // Instantiate the RemoteViews object for the app widget layout.
            val rv = RemoteViews(context.packageName, R.layout.widget_layout).apply {
                // Set up the RemoteViews object to use a RemoteViews adapter.
                // This adapter connects
                // to a RemoteViewsService  through the specified intent.
                // This is how you populate the data.
                setRemoteAdapter(R.id.stack_view, intent)

                // The empty view is displayed when the collection has no items.
                // It should be in the same layout used to instantiate the RemoteViews
                // object above.
                setEmptyView(R.id.stack_view, R.id.empty_view)
            }

            //
            // Do additional processing specific to this app widget...
            //

            appWidgetManager.updateAppWidget(appWidgetId, rv)
        }
        super.onUpdate(context, appWidgetManager, appWidgetIds)
    }
    

자바

    public void onUpdate(Context context, AppWidgetManager appWidgetManager,
    int[] appWidgetIds) {
        // update each of the app widgets with the remote adapter
        for (int i = 0; i < appWidgetIds.length; ++i) {

            // Set up the intent that starts the StackViewService, which will
            // provide the views for this collection.
            Intent intent = new Intent(context, StackWidgetService.class);
            // Add the app widget ID to the intent extras.
            intent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetIds[i]);
            intent.setData(Uri.parse(intent.toUri(Intent.URI_INTENT_SCHEME)));
            // Instantiate the RemoteViews object for the app widget layout.
            RemoteViews rv = new RemoteViews(context.getPackageName(), R.layout.widget_layout);
            // Set up the RemoteViews object to use a RemoteViews adapter.
            // This adapter connects
            // to a RemoteViewsService  through the specified intent.
            // This is how you populate the data.
            rv.setRemoteAdapter(R.id.stack_view, intent);

            // The empty view is displayed when the collection has no items.
            // It should be in the same layout used to instantiate the RemoteViews
            // object above.
            rv.setEmptyView(R.id.stack_view, R.id.empty_view);

            //
            // Do additional processing specific to this app widget...
            //

            appWidgetManager.updateAppWidget(appWidgetIds[i], rv);
        }
        super.onUpdate(context, appWidgetManager, appWidgetIds);
    }
    

RemoteViewsService 클래스

데이터 유지

위에 설명된 대로 RemoteViewsService 서브클래스는 원격 컬렉션 뷰를 게재하는 데 사용되는 RemoteViewsFactory를 제공합니다.

구체적으로 다음 단계를 실행해야 합니다.

  1. RemoteViewsService 서브클래스. 원격 어댑터는 RemoteViewsService 서비스를 통해 RemoteViews를 요청할 수 있습니다.
  2. RemoteViewsService 서브클래스에 RemoteViewsFactory 인터페이스를 구현하는 클래스를 포함하세요. RemoteViewsFactory는 원격 컬렉션 뷰(예: ListView, GridView)와 뷰의 기본 데이터 사이의 어댑터를 위한 인터페이스입니다. 구현에서 데이터 세트 내 각 항목의 RemoteViews 객체를 만들어야 합니다. 이 인터페이스는 Adapter 주위의 씬 래퍼입니다.

서비스의 단일 인스턴스나 인스턴스에 포함된 데이터에 의존하여 유지할 수 없습니다. 따라서 정적인 경우가 아닌 한 RemoteViewsService에 데이터를 저장해서는 안 됩니다. 앱 위젯의 데이터를 유지하는 데 좋은 방법은 데이터가 프로세스 수명주기를 넘어 유지되는 ContentProvider를 사용하는 것입니다.

RemoteViewsService 구현의 기본 콘텐츠는 아래에 설명된 RemoteViewsFactory입니다.

RemoteViewsFactory 인터페이스

RemoteViewsFactory 인터페이스를 구현하는 맞춤 클래스는 앱 위젯에 컬렉션에 포함된 항목의 데이터를 제공합니다. 이렇게 하기 위해 맞춤 클래스는 앱 위젯 항목 XML 레이아웃 파일을 데이터 소스와 결합합니다. 데이터베이스에서 단일 배열에 이르기까지 모든 항목이 데이터 소스가 될 수 있습니다. StackWidget 샘플에서는 WidgetItems의 배열이 데이터 소스입니다. RemoteViewsFactory는 데이터를 원격 컬렉션 뷰에 결합하는 어댑터의 역할을 합니다.

RemoteViewsFactory 서브클래스를 위해 구현해야 하는 가장 중요한 두 가지 메서드는 onCreate()getViewAt()입니다.

처음으로 팩토리를 만들 때 시스템에서는 onCreate()을 호출합니다. 여기에서 데이터 소스의 연결 및 커서를 설정합니다. 예를 들어 StackWidget 샘플에서는 onCreate()를 사용하여 WidgetItem 객체의 배열을 초기화합니다. 앱 위젯이 활성화되면 시스템에서 배열의 색인 위치를 사용하여 이 객체에 액세스하며 객체에 포함된 텍스트가 표시됩니다.

다음은 StackWidget 샘플의 RemoteViewsFactory 구현에서 onCreate() 메서드를 보여주는 부분을 발췌한 것입니다.

Kotlin

    private const val REMOTE_VIEW_COUNT: Int = 10

    class StackRemoteViewsFactory(
            private val context: Context
    ) : RemoteViewsService.RemoteViewsFactory {

        private lateinit var widgetItems: List<WidgetItem>

        override fun onCreate() {
            // In onCreate() you setup any connections / cursors to your data source. Heavy lifting,
            // for example downloading or creating content etc, should be deferred to onDataSetChanged()
            // or getViewAt(). Taking more than 20 seconds in this call will result in an ANR.
            widgetItems = List(REMOTE_VIEW_COUNT) { index -> WidgetItem("$index!") }
            ...
        }
        ...
    }
    

자바

    class StackRemoteViewsFactory implements
    RemoteViewsService.RemoteViewsFactory {
        private static final int count = 10;
        private List<WidgetItem> widgetItems = new ArrayList<WidgetItem>();
        private Context context;
        private int appWidgetId;

        public StackRemoteViewsFactory(Context context, Intent intent) {
            this.context = context;
            appWidgetId = intent.getIntExtra(AppWidgetManager.EXTRA_APPWIDGET_ID,
                    AppWidgetManager.INVALID_APPWIDGET_ID);
        }

        public void onCreate() {
            // In onCreate() you setup any connections / cursors to your data source. Heavy lifting,
            // for example downloading or creating content etc, should be deferred to onDataSetChanged()
            // or getViewAt(). Taking more than 20 seconds in this call will result in an ANR.
            for (int i = 0; i < count; i++) {
                widgetItems.add(new WidgetItem(i + "!"));
            }
            ...
        }
    ...
    

RemoteViewsFactory 메서드 getViewAt()는 데이터 세트의 지정된 position에 있는 데이터에 상응하는 RemoteViews 객체를 반환합니다. 다음은 StackWidget 샘플의 RemoteViewsFactory 구현에서 발췌한 것입니다.

Kotlin

    override fun getViewAt(position: Int): RemoteViews {
        // Construct a remote views item based on the app widget item XML file,
        // and set the text based on the position.
        return RemoteViews(context.packageName, R.layout.widget_item).apply {
            setTextViewText(R.id.widget_item, widgetItems[position].text)
        }
    }
    

자바

    public RemoteViews getViewAt(int position) {

        // Construct a remote views item based on the app widget item XML file,
        // and set the text based on the position.
        RemoteViews rv = new RemoteViews(context.getPackageName(), R.layout.widget_item);
        rv.setTextViewText(R.id.widget_item, widgetItems.get(position).text);

        ...
        // Return the remote views object.
        return rv;
    }
    

개별 항목에 동작 추가

위 섹션에서는 앱 위젯 컬렉션에 데이터를 바인딩하는 방법을 보여줍니다. 하지만 컬렉션 뷰의 개별 항목에 동적 동작을 추가하려면 어떻게 해야 하나요?

AppWidgetProvider 클래스 사용에 설명된 대로 일반적으로 setOnClickPendingIntent()를 사용하여 객체의 클릭 동작(예: 버튼을 클릭하면 Activity가 실행됨)을 설정합니다. 하지만 개별 컬렉션 항목의 하위 뷰에서는 이 방법을 사용할 수 없습니다(다시 말해, setOnClickPendingIntent()를 사용하여 개별 목록 항목이 아니라 앱을 실행하는 Gmail 앱 위젯에서 전역 버튼을 설정할 수 있습니다). 대신 컬렉션의 개별 항목에 클릭 동작을 추가하려면 setOnClickFillInIntent()를 사용하세요. 여기에는 컬렉션 뷰의 대기중 인텐트 템플릿을 설정한 다음 RemoteViewsFactory를 통해 컬렉션의 각 항목에 채우기 인텐트를 설정하는 작업이 수반됩니다.

이 섹션에서는 StackWidget 샘플을 사용하여 개별 항목에 동작을 추가하는 방법을 설명합니다. StackWidget sample 샘플에서 사용자가 상단 뷰를 터치하면 앱 위젯에서 Toast 메시지 "터치된 뷰 n"을 표시합니다. 여기서 n은 터치된 뷰의 색인(위치)입니다. 작동 방식은 다음과 같습니다.

  • StackWidgetProvider(AppWidgetProvider 서브클래스)는 TOAST_ACTION이라고 하는 맞춤 작업이 있는 대기중 인텐트를 만듭니다.
  • 사용자가 뷰를 터치하면 인텐트가 실행되고 TOAST_ACTION을 브로드캐스트합니다.
  • 이 브로드캐스트는 StackWidgetProvideronReceive() 메서드에 의해 가로채기 당하며 앱 위젯에서 터치된 뷰의 Toast 메시지를 표시합니다. 컬렉션 항목의 데이터는 RemoteViewsFactory에서 RemoteViewsService를 통해 제공합니다.

참고: StackWidget 샘플 은 브로드캐스트를 사용하지만 일반적으로 앱 위젯은 간단히 이와 같은 시나리오에서 활동을 시작합니다.

대기중 인텐트 템플릿 설정

StackWidgetProvider(AppWidgetProvider 서브클래스)는 대기중 인텐트를 설정합니다. 컬렉션의 개별 항목은 자체 대기중 인텐트를 설정할 수 없습니다. 대신 컬렉션은 대기중 인텐트 템플릿을 설정하고 개별 항목은 채우기 인텐트를 설정하여 항목별로 고유한 동작을 만듭니다.

이 클래스는 사용자가 뷰를 터치할 때 전송되는 브로드캐스트도 수신하며, onReceive() 메소드에서 이 이벤트를 처리합니다. 인텐트의 액션이 TOAST_ACTION인 경우 앱 위젯에서 현재 뷰의 Toast 메시지를 표시합니다.

Kotlin

    const val TOAST_ACTION = "com.example.android.stackwidget.TOAST_ACTION"
    const val EXTRA_ITEM = "com.example.android.stackwidget.EXTRA_ITEM"

    class StackWidgetProvider : AppWidgetProvider() {

        ...

        // Called when the BroadcastReceiver receives an Intent broadcast.
        // Checks to see whether the intent's action is TOAST_ACTION. If it is, the app widget
        // displays a Toast message for the current item.
        override fun onReceive(context: Context, intent: Intent) {
            val mgr: AppWidgetManager = AppWidgetManager.getInstance(context)
            if (intent.action == TOAST_ACTION) {
                val appWidgetId: Int = intent.getIntExtra(
                        AppWidgetManager.EXTRA_APPWIDGET_ID,
                        AppWidgetManager.INVALID_APPWIDGET_ID
                )
                val viewIndex: Int = intent.getIntExtra(EXTRA_ITEM, 0)
                Toast.makeText(context, "Touched view $viewIndex", Toast.LENGTH_SHORT).show()
            }
            super.onReceive(context, intent)
        }

        override fun onUpdate(
                context: Context,
                appWidgetManager: AppWidgetManager,
                appWidgetIds: IntArray
        ) {
            // update each of the app widgets with the remote adapter
            appWidgetIds.forEach { appWidgetId ->

                // Sets up the intent that points to the StackViewService that will
                // provide the views for this collection.
                val intent = Intent(context, StackWidgetService::class.java).apply {
                    putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetId)
                    // When intents are compared, the extras are ignored, so we need to embed the extras
                    // into the data so that the extras will not be ignored.
                    data = Uri.parse(toUri(Intent.URI_INTENT_SCHEME))
                }
                val rv = RemoteViews(context.packageName, R.layout.widget_layout).apply {
                    setRemoteAdapter(R.id.stack_view, intent)

                    // The empty view is displayed when the collection has no items. It should be a
                    // sibling of the collection view.
                    setEmptyView(R.id.stack_view, R.id.empty_view)
                }

                // This section makes it possible for items to have individualized behavior.
                // It does this by setting up a pending intent template. Individuals items of a
                // collection cannot set up their own pending intents. Instead, the collection as a
                // whole sets up a pending intent template, and the individual items set a fillInIntent
                // to create unique behavior on an item-by-item basis.
                val toastPendingIntent: PendingIntent = Intent(
                        context,
                        StackWidgetProvider::class.java
                ).run {
                    // Set the action for the intent.
                    // When the user touches a particular view, it will have the effect of
                    // broadcasting TOAST_ACTION.
                    action = TOAST_ACTION
                    putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetId)
                    data = Uri.parse(toUri(Intent.URI_INTENT_SCHEME))

                    PendingIntent.getBroadcast(context, 0, this, PendingIntent.FLAG_UPDATE_CURRENT)
                }
                rv.setPendingIntentTemplate(R.id.stack_view, toastPendingIntent)

                appWidgetManager.updateAppWidget(appWidgetId, rv)
            }
            super.onUpdate(context, appWidgetManager, appWidgetIds)
        }
    }
    

자바

    public class StackWidgetProvider extends AppWidgetProvider {
        public static final String TOAST_ACTION = "com.example.android.stackwidget.TOAST_ACTION";
        public static final String EXTRA_ITEM = "com.example.android.stackwidget.EXTRA_ITEM";

        ...

        // Called when the BroadcastReceiver receives an Intent broadcast.
        // Checks to see whether the intent's action is TOAST_ACTION. If it is, the app widget
        // displays a Toast message for the current item.
        @Override
        public void onReceive(Context context, Intent intent) {
            AppWidgetManager mgr = AppWidgetManager.getInstance(context);
            if (intent.getAction().equals(TOAST_ACTION)) {
                int appWidgetId = intent.getIntExtra(AppWidgetManager.EXTRA_APPWIDGET_ID,
                    AppWidgetManager.INVALID_APPWIDGET_ID);
                int viewIndex = intent.getIntExtra(EXTRA_ITEM, 0);
                Toast.makeText(context, "Touched view " + viewIndex, Toast.LENGTH_SHORT).show();
            }
            super.onReceive(context, intent);
        }

        @Override
        public void onUpdate(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds) {
            // update each of the app widgets with the remote adapter
            for (int i = 0; i < appWidgetIds.length; ++i) {

                // Sets up the intent that points to the StackViewService that will
                // provide the views for this collection.
                Intent intent = new Intent(context, StackWidgetService.class);
                intent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetIds[i]);
                // When intents are compared, the extras are ignored, so we need to embed the extras
                // into the data so that the extras will not be ignored.
                intent.setData(Uri.parse(intent.toUri(Intent.URI_INTENT_SCHEME)));
                RemoteViews rv = new RemoteViews(context.getPackageName(), R.layout.widget_layout);
                rv.setRemoteAdapter(appWidgetIds[i], R.id.stack_view, intent);

                // The empty view is displayed when the collection has no items. It should be a sibling
                // of the collection view.
                rv.setEmptyView(R.id.stack_view, R.id.empty_view);

                // This section makes it possible for items to have individualized behavior.
                // It does this by setting up a pending intent template. Individuals items of a collection
                // cannot set up their own pending intents. Instead, the collection as a whole sets
                // up a pending intent template, and the individual items set a fillInIntent
                // to create unique behavior on an item-by-item basis.
                Intent toastIntent = new Intent(context, StackWidgetProvider.class);
                // Set the action for the intent.
                // When the user touches a particular view, it will have the effect of
                // broadcasting TOAST_ACTION.
                toastIntent.setAction(StackWidgetProvider.TOAST_ACTION);
                toastIntent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetIds[i]);
                intent.setData(Uri.parse(intent.toUri(Intent.URI_INTENT_SCHEME)));
                PendingIntent toastPendingIntent = PendingIntent.getBroadcast(context, 0, toastIntent,
                    PendingIntent.FLAG_UPDATE_CURRENT);
                rv.setPendingIntentTemplate(R.id.stack_view, toastPendingIntent);

                appWidgetManager.updateAppWidget(appWidgetIds[i], rv);
            }
        super.onUpdate(context, appWidgetManager, appWidgetIds);
        }
    }
    
채우기 인텐트 설정

RemoteViewsFactory에서 컬렉션에 있는 각 항목의 채우기 인텐트를 설정해야 합니다. 이렇게 하면 주어진 항목의 개별 클릭 작업을 구분할 수 있습니다. 그런 다음 채우기 인텐트가 PendingIntent 템플릿과 결합되어 항목이 클릭될 때 실행될 최종 인텐트를 결정합니다.

Kotlin

    private const val REMOTE_VIEW_COUNT: Int = 10

    class StackRemoteViewsFactory(
            private val context: Context,
            intent: Intent
    ) : RemoteViewsService.RemoteViewsFactory {

        private lateinit var widgetItems: List<WidgetItem>
        private val appWidgetId: Int = intent.getIntExtra(
                AppWidgetManager.EXTRA_APPWIDGET_ID,
                AppWidgetManager.INVALID_APPWIDGET_ID
        )

        override fun onCreate() {
            // In onCreate() you setup any connections / cursors to your data source. Heavy lifting,
            // for example downloading or creating content etc, should be deferred to onDataSetChanged()
            // or getViewAt(). Taking more than 20 seconds in this call will result in an ANR.
            widgetItems = List(REMOTE_VIEW_COUNT) { index -> WidgetItem("$index!") }
            ...
        }
        ...

        override fun getViewAt(position: Int): RemoteViews {
            // Construct a remote views item based on the app widget item XML file,
            // and set the text based on the position.
            return RemoteViews(context.packageName, R.layout.widget_item).apply {
                setTextViewText(R.id.widget_item, widgetItems[position].text)

                // Next, set a fill-intent, which will be used to fill in the pending intent template
                // that is set on the collection view in StackWidgetProvider.
                val fillInIntent = Intent().apply {
                    Bundle().also { extras ->
                        extras.putInt(EXTRA_ITEM, position)
                        putExtras(extras)
                    }
                }
                // Make it possible to distinguish the individual on-click
                // action of a given item
                setOnClickFillInIntent(R.id.widget_item, fillInIntent)
                ...
            }
        }
        ...
    }
    

자바

    public class StackWidgetService extends RemoteViewsService {
        @Override
        public RemoteViewsFactory onGetViewFactory(Intent intent) {
            return new StackRemoteViewsFactory(this.getApplicationContext(), intent);
        }
    }

    class StackRemoteViewsFactory implements RemoteViewsService.RemoteViewsFactory {
        private static final int count = 10;
        private List<WidgetItem> widgetItems = new ArrayList<WidgetItem>();
        private Context context;
        private int appWidgetId;

        public StackRemoteViewsFactory(Context context, Intent intent) {
            this.context = context;
            appWidgetId = intent.getIntExtra(AppWidgetManager.EXTRA_APPWIDGET_ID,
                    AppWidgetManager.INVALID_APPWIDGET_ID);
        }

        // Initialize the data set.
            public void onCreate() {
                // In onCreate() you set up any connections / cursors to your data source. Heavy lifting,
                // for example downloading or creating content etc, should be deferred to onDataSetChanged()
                // or getViewAt(). Taking more than 20 seconds in this call will result in an ANR.
                for (int i = 0; i < count; i++) {
                    widgetItems.add(new WidgetItem(i + "!"));
                }
               ...
            }
            ...

            // Given the position (index) of a WidgetItem in the array, use the item's text value in
            // combination with the app widget item XML file to construct a RemoteViews object.
            public RemoteViews getViewAt(int position) {
                // position will always range from 0 to getCount() - 1.

                // Construct a RemoteViews item based on the app widget item XML file, and set the
                // text based on the position.
                RemoteViews rv = new RemoteViews(context.getPackageName(), R.layout.widget_item);
                rv.setTextViewText(R.id.widget_item, widgetItems.get(position).text);

                // Next, set a fill-intent, which will be used to fill in the pending intent template
                // that is set on the collection view in StackWidgetProvider.
                Bundle extras = new Bundle();
                extras.putInt(StackWidgetProvider.EXTRA_ITEM, position);
                Intent fillInIntent = new Intent();
                fillInIntent.putExtras(extras);
                // Make it possible to distinguish the individual on-click
                // action of a given item
                rv.setOnClickFillInIntent(R.id.widget_item, fillInIntent);

                ...

                // Return the RemoteViews object.
                return rv;
            }
        ...
        }
    

컬렉션 데이터를 최신 상태로 유지

다음 그림은 업데이트가 발생할 때 컬렉션을 사용하는 앱 위젯에서 발생하는 흐름을 보여줍니다. 앱 위젯 코드가 RemoteViewsFactory와 상호작용하는 방식, 업데이트를 트리거할 수 있는 방법을 보여줍니다.

컬렉션을 사용하는 앱 위젯의 한 가지 기능은 사용자에게 최신 콘텐츠를 제공하는 것입니다. 사용자에게 받은편지함을 스냅샷으로 보여주는 Android 3.0 Gmail 앱 위젯을 예로 들어 보겠습니다. 이 작업을 실행하려면 RemoteViewsFactory 및 컬렉션 뷰를 트리거하여 새 데이터를 가져와 표시할 수 있어야 합니다. 이 작업은 AppWidgetManager 호출 notifyAppWidgetViewDataChanged()를 사용하여 실행합니다. 이 호출을 실행하면 RemoteViewsFactoryonDataSetChanged() 메서드에 콜백이 전송되어 새 데이터를 가져올 수 있습니다. onDataSetChanged() 콜백 내에서 처리 집약적인 작업을 동시에 실행할 수 있습니다. 이 호출은 RemoteViewsFactory에서 메타데이터 또는 뷰 데이터를 가져오기 전에 완료됩니다. 또한 getViewAt() 메서드 내에서 처리 집약적인 작업을 실행할 수 있습니다. 이 호출에 시간이 오래 걸리는 경우 RemoteViewsFactorygetLoadingView() 메서드에 의해 지정된 로딩 뷰는 호출이 반환될 때까지 컬렉션 뷰의 상응하는 위치에 표시됩니다.