(원문: http://developer.android.com/guide/topics/appwidgets/index.html)

(위치: Develop > API Guides > App Components > App Widgets)

(전체 번역 글 목록으로 이동하기)


앱 위젯


앱 위젯은 (런처의 홈 스크린처럼) 다른 앱에 들어가며 주기적으로 업데이트 될 수 있는 내 앱의 독립적인 작은 뷰를 말합니다. 이러한 앱 위젯은 앱 위젯 프로바이더를 이용하여 만들 수 있습니다. 그리고, 다른 앱의 위젯을 담을 수 있는 앱 컴포넌트를 앱 위젯 호스트라고 합니다. 아래의 이미지는 국내 유명 음악 앱의 위젯입니다.

본 문서에서는 앱 위젯 프로바이더를 이용하여 앱 위젯을 만드는 방법에 대해 학습할 것입니다. 그리고 앱 위젯 호스트(AppWidgetHost)를 만드는 방법에 대해서는 앱 위젯 호스트 문서에서 학습하실 수 있습니다. 


기본 개념

앱 위젯을 만들기 위해서는 아래의 몇가지가 필요합니다:

AppWidgetProviderInfo 객체

앱 위젯의 레이아웃, 업데이트 시기와 같은 메타정보와 AppWidgetProvider 클래스에 대한 정보를 가지고 있는 객체이며, XML 파일로 정의해야 합니다.

AppWidgetProvider 클래스 구현

앱 위젯에 대한 인터페이스들을 구현하는 클래스입니다. BroadcastReceiver 클래스를 확장한 클래스로서 브로드캐스트 이벤트들을 받아서 처리합니다. 앱 위젯이 업데이트되거나, 활성화 및 비활성화되거나, 삭제될때 브로드캐스트 이벤트를 받습니다. 따라서 이 클래스를 확장하여 내 앱의 위젯이 해당 이벤트들을 받았을때 어떻게 동작할지를 구현할 수 있습니다.

View 레이아웃

앱 위젯의 레이아웃을 XML 파일로 정의해야 합니다. 

그리고, 앱 위젯에 대한 설정 액티비티를 만들 수 있습니다. 이것은 선택적으로 적용 가능한 액티비티로서, 사용자가 앱 위젯을 화면에 추가할 때 실행되어 앱 위젯에 대한 설정을 할 수 있습니다. 

이어지는 섹션들에서는 위의 컴포넌트들 각각에 대하여 설명합니다.


매니페스트 파일에서 앱 위젯 선언하기

첫번째로, 내 앱의 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> 요소의 android:name에는 앱 위젯에 대한 AppWidgetProvider 클래스를 지정해줘야 합니다. 

<intent-filter> 요소는 <action>을 포함해야하고, 이 <action>은 android:name 속성을 가지고 있어야 합니다. 위 예제는, ExampleAppWidgetProviderandroid.appwidget.action.APPWIDGET_UPDATE 액션을 갖는 인텐트에 대하여 동작한다는 것을 의미합니다. 이 액션(action)은 앱 개발자가 매니페스트 파일에 선언해야 하는 유일한 액션이며, 다른 브로드캐스트들은 AppWidgetManager에 의해 자동적으로 AppWidgetProvider에 전달됩니다. 

<meta-data> 요소는 AppWidgetProviderInfo에 대한 정보를 지정하며, 아래 속성들을 필요로 합니다:

  • android:name - 메타데이터의 이름을 지정하는 것으로서, AppWidgetProviderInfo에 대한 데이터임을 나타내기 위해 android.appwidget.provider를 사용합니다.
  • android:resource - AppWidgetProviderInfo에 대한 리소스 파일을 지정합니다.


AppWidgetProviderInfo 메타정보 추가하기

AppWidgetProviderInfo는 앱 위젯의 본질에 대하여 정의합니다. 예를 들어, 앱 위젯의 최소 크기, 레이아웃, 업데이트 빈도, 그리고 (선택적으로) 위젯 생성시점에 실행할 설정 액티비티를 정의합니다. 이것은 하나의 <appwidget-provider> 요소를 갖는 XML 파일이며 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|keyguard"
   
android:initialKeyguardLayout="@layout/example_keyguard">
</appwidget-provider>

<appwidget-provider> 요소의 각 속성들에 대하여 설명합니다:

  • minWidthminHeight 속성은 앱 위젯의 (기본적인, 최초의) 최소 크기를 지정합니다. 기본 홈 화면은 앱 위젯을 윈도우에 배치할 때 그리드 형태의 셀들에 기반하여 배치합니다. 만약 앱 위젯의 minWidth나 minHeight가 셀의 크기와 일치하지 않는다면, 근처의 셀이 앱 위젯의 영역으로 사용됩니다. 
    메모: 앱 위젯이 여러 디바이스에 호환되려면, 앱 위젯의 최소 크기가 4x4를 넘어가지 않도록 해야합니다.
  • minResizeWidthminResizeHeight 속성은 앱 위젯의 (절대적인) 최소 크기를 지정합니다. 이 값들은 앱 위젯이 너무 작아져서 내용들이 잘 보이지 않거나 사용하기 어려울 정도가 되지 않도록 지정해두는 최소값입니다. 이 값들을 이용하면 (사용자에 의해) 앱 위젯이 minWidth와 minHeight에 지정된 크기보다 더 작게 리사이즈 될 수 있습니다. (이 속성들은 안드로이드 3.1에서 소개되었습니다.)
  • updatePeriodMillis 속성은 AppWidgetProvider의 onUpdate() 콜백 메소드가 호출되는 주기를 정의합니다. 하지만 실제 업데이트 시기가 정확히 지정된 시간을 따른다고 보장할 수는 없습니다. 그리고 가능하다면 배터리의 사용량을 줄이기 위해서 한시간에 한번 이상은 업데이트를 하지 않는 것이 좋습니다. 이는 설정에서 사용자가 직접 지정하도록  할 수 있습니다. 어떤 사용자는 15분마다 업데이트 되는 주식 시세 표시 위젯을 원할 수도 있고, 또 어떤 사용자는 하루에 4번만 업데이트 되는 위젯을 원할 수도 있기 때문입니다. 
    메모: 만약 updatePeriodMillis에 지정한 시간이 지나 업데이트할 시간이 되었을 때 디바이스가 슬립 상태(화면이 꺼져 있는 상태)라면, 디바이스는 업데이트를 수행하기 위해 화면을 깨웁니다. 만약 한시간에 한번 이상 업데이틀 하지 않도록 했다면, 배터리 소모에 큰 악영향은 없겠지만, 자주 업데이트를 해야 하거나, 디바이스가 슬립 상태일 때는 업데이트를 하지 않아야 한다면, 알람(alarm)을 기반으로 개발하는 것이 나을 것입니다. 그러기 위해서는 일단 updatePeriodMillis 값을 0으로 지정하여 주기적 업데이트를 비활성화 한 후, AlarmManager를 이용하여 AppWidgetProvider가 받을 인텐트로 알람을 셋팅하면 됩니다. 알람 타입을 ELAPSED_REALTIME 또는 RTC로 지정하면 디바이스가 깨어날때만 알람이 전달됩니다.
  • initialLayout 속성은 앱 위젯의 레이아웃을 정의하는 xml 리소스 파일을 가리킵니다.
  • configure 속성은 사용자가 앱 위젯을 추가할 때 위젯 관련된 값들을 설정하기 위해 실행되는 액티비티를 정의합니다. 이것은 선택적인 사항이며, 아래의 앱 위젯의 설정 액티비티 만들기에서 자세히 설명합니다.
  • previewImage 속성은 앱 위젯이 어떤 모습일지 위젯을 고를 때 보여주는 이미지를 정의하며, 이 값이 없으면 앱 아이콘으로 대체됩니다. 이 속성은 안드로이드 3.0 버전부터 추가되었으며, 자세한 내용은 미리보기 이미지 설정하기에서 학습할 수 있습니다.
  • autoAdvanceViewId 속성은 위젯의 호스트에 의해 자동으로 변형되는 (auto-scaled) 뷰에 대한 ID를 지정합니다. (안드로이드 3.0 버전부터 추가되었습니다.)
  • resizeMode 속성은 위젯이 사용자에 의해 리사이즈 되는 것과 관련된 값을 지정하며, "horizontal"이나 "vertical"이나 "none"이 들어갈 수 있습니다. 위젯을 누르고 있으면 리사이즈 핸들이 나타나게 되고, 좌우(horizontal) 또는 상하(vertical)로 드래그하여 크기를 조정할 수 있습니다. 상하좌우 모두 리사이즈 되도록 하기 위해서는 "horizontal|vertical" 로 지정하면 됩니다. (안드로이드 3.0 버전부터 추가되었습니다.)
  • minResizeHeight 속성은 위젯이 리사이즈 될 수 있는 최소 높이를 지정합니다(dp단위로 지정). 이 값이 minHeight보다 크거나, resizeMode에 "vertical" 설정이 되어 있지 않으면 리사이즈 되지 않습니다. (안드로이드 4.0 버전부터 추가되었습니다.)
  • minResizeWidth 속성은 위젯이 리사이즈 될 수 있는 최소 너비를 지정합니다. 이 값이 minWidth보다 크거나, resizeMode에 "horizontal" 설정이 되어 있지 않으면 리사이즈 되지 않습니다. (안드로이드 4.0 버전부터 추가되었습니다.)
  • widgetCategory 속성은 앱 위젯을 홈화면에 노출할지("home_screen"), 잠금화면에 노출할지("keyguard"), 또는 둘 다 노출할지("home_screen|keyguard")를 선언합니다. 둘 다 노출되는 위젯은 위젯 클래스 작성시 디자인 가이드라인을 준수해야 하며, 자세한 내용은 잠금화면에 앱 위젯 보이기에서 학습할 수 있습니다. 이 속성의 기본값은 "home_screen"입니다. (안드로이드 4.2 버전부터 추가되었습니다. 안드로이드 5.0 버전에서 deprecated 된 것으로 보입니다.)
  • initialKeyguardLayout 속성은 잠금화면에 보여줄 앱 위젯의 레이아웃 xml 파일을 가리킵니다. 이것은 android:initialLayout과 같은 방식으로 동작합니다. 즉, 앱 위젯이 초기화될 때 보여지며 업데이트 될 수 있는 레이아웃입니다. (잠금화면용 레이아웃을 따로 지정할 수 있다는 것인데, 안드로이드 4.2 버전부터 추가되었습니다. 안드로이드 5.0 버전에서 deprecated 된 것으로 보입니다.)


앱 위젯 레이아웃 만들기

앱 위젯을 만들기 위해서는 res/layout 디렉토리에 xml 레이아웃 파일이 정의되어 있어야 하며, 아래 나열된 뷰 객체들을 사용할 수 있습니다. 

앱의 레이아웃을 만드는데 익숙하다면 앱 위젯의 레이아웃을 만드는 것도 어렵지 않습니다. 다만, 앱 위젯의 레이아웃은 RemoteViews를 기반으로 한다는 데에 주의해야 하며, 이는 모든 종류의 뷰나 레이아웃 클래스를 지원하지는 않는다는 것을 의미합니다. 

RemoteViews 객체는 아래의 레이아웃 클래스들을 지원하고:

FrameLayout
LinearLayout
RelativeLayout
GridLayout

아래의 뷰 클래스들을 지원합니다:

AnalogClock
Button
Chronometer
ImageButton
ImageView
ProgressBar
TextView
ViewFlipper
ListView
GridView
StackView
AdapterViewFlipper

위에 나열된 클래스들 이외의 클래스들은 지원되지 않으며, 심지어 위의 클래스들을 상속받아 만든 커스텀뷰도 지원되지 않습니다. 

RemoteViews는 ViewStub 도 지원하는데, 이것은 처음에는 보이지 않고 크기가 없는 상태였다가 나중에 필요시 런타임에 생성되는 (lazy inflate) 뷰입니다. 


앱 위젯에 여백 추가하기

위젯은 일반적으로 스크린 경계선까지 꽉 채워져서는 안되고 다른 위젯과 겹쳐서도 안되기 때문에, 위젯의 모든 경계 부분에는 여백(margin)이 있어야 합니다. 

안드로이드 4.0 버전부터는 앱 위젯이 홈 화면의 다른 위젯이나 앱 아이콘들과 더 잘 구분되어 보이도록 하기 위해, 위젯의 경계 부분과 위젯이 들어가는 영역의 경계 부분 사이에 공간(padding, 이하 패딩으로 지칭)을 자동으로 만들어 줍니다. 따라서 이와 같은 유용한 기능을 이용하기 위해서는 앱의 매니페스트 파일의 targetSdkVersion14 이상으로 선언해야 합니다. 

sdk 버전이 14 미만(안드로이드 4.0버전 미만)일 때는 자체적으로 정의한 패딩을 지정하고, 14 이상(안드로이드 4.0버전 이상)일 때는 자동으로 만들어진 패딩 이외에 패딩이 더 추가되지 않도록 하기 위해서 아래와 같이 하면 됩니다.

1. 앱의 매니페스트 파일의 targetSdkVersion14 또는 그 이상으로 선언합니다.

2. 아래와 같이 레이아웃 파일을 작성합니다. android:padding@dimen/widget_margin을 넣어줍니다. 

<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. 두 개의 디멘션(dimension) 리소스를 생성하는데, 하나는 안드로이드 4.0버전 미만에서 자체 패딩을 주기 위해 res/values 에 생성하고, 다른 하나는 안드로이드 4.0버전 이상에서 자체 패딩을 주지 않기 위해 res/values-v14 에 생성합니다:

res/values/dimens.xml:

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

res/values-v14/dimens.xml:

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


다른 방법으로는, 위젯의 배경을 나인패치(nine-patch) 이미지로 설정하되 안드로이드 4.0버전 미만과 그 이상을 위한 이미지를 따로 준비하면 됩니다. 


AppWidgetProvider 클래스 사용하기

AppWidgetProvider 클래스는 앱 위젯의 브로드캐스트들을 편리하게 다루기 위해 BroadcastReceiver 클래스를 상속받아 구현된 클래스입니다. AppWidgetProvider는 앱 위젯이 업데이트되거나, 삭제되거나, 활성화/비활성화될 때와 같이 위젯과 관련된 이벤트가 발생할 때 그에 대한 브로드캐스트를 받으며, 호출되는 콜백 메소드들은 아래와 같습니다:

onUpdate()

이 메소드는 AppWidgetProviderInfo에 정의된 updatePeriodMillis 만큼의 시간간격으로 주기적으로 호출되는 콜백 메소드입니다. 이 메소드는 또한 사용자가 위젯을 화면에 추가할때도 호출되며, 필요하다면 여기서 뷰에 이벤트 리스너를 달거나 서비스(Service component)를 시작하는 작업을 해야 합니다. 하지만 만약 앱 위젯에 대한 설정 액티비티를 선언했다면, 사용자가 위젯을 화면에 추가하는 시점에는 이 메소드가 호출되지 않으며, 위젯이 화면에 추가된 후에야 다시 주기적으로 호출됩니다. 따라서 설정 액티비티가 선언되어 있다면, 뷰에 이벤트 리스너를 달거나 하는 등의 초기화 작업을, 설정 액티비티에서 설정이 완료되는 시점에 하도록 해야 합니다. (앱 위젯의 설정 액티비티 만들기 참고.)

onAppWidgetOptionsChanged()

이 메소드는 위젯이 화면에 추가되어 처음 보여지거나 리사이즈될 때마다 호출되는 콜백 메소드이며, 위젯의 크기에 따라 내용을 변경해야 하는 경우 사용합니다. getAppWidgetOptions() 메소드를 호출하면 번들(Bundle)을 리턴해 주는데 여기에서 아래의 값들을 얻을 수 있습니다:

    • OPTION_APPWIDGET_MIN_WIDTH - 위젯의 현재 너비에 대한 작은 쪽 경계의 크기. dp 단위.
    • OPTION_APPWIDGET_MIN_HEIGHT - 위젯의 현재 높이에 대한 작은 쪽 경계의 크기. dp 단위.
    • OPTION_APPWIDGET_MAX_WIDTH - 위젯의 현재 너비에 대한 큰 쪽 경계의 크기. dp 단위.
    • OPTION_APPWIDGET_MAX_HEIGHT - 위젯의 현재 높이에 대한 큰 쪽 경계의 크기. dp 단위. 

이 메소드는 API 레벨 16(안드로이드 4.1버전)부터 추가되었습니다. 따라서 이 메소드를 구현하고자 한다면, 하위 버전의 디바이스에서 이 메소드가 호출되지 않는다는 점에 주의해야 합니다. 

onDeleted(Context, int[])

이 메소드는 앱 위젯이 앱 위젯 호스트에서 삭제될 때 호출되는 콜백 메소드입니다.

onEnabled(Context)

이 메소드는 앱 위젯의 인스턴스가 처음 생성될 때 호출되는 콜백 메소드입니다. 예를 들어, 사용자가 어떤 하나의 앱에 대하여 2개의 앱 위젯을 화면에 추가할 경우, 이 메소드는 첫번째 위젯이 추가될 때만 호출됩니다. 위젯에 대한 데이터베이스를 생성하거나, 해당 앱에 대한 모든 위젯에 대하여 한번만 실행되기를 바라는 부분이 있다면, 여기에 구현하면 됩니다. 

onDisabled(Context)

이 메소드는 앱 위젯 호스트에서 모든 위젯이 삭제되는 시점, 즉 마지막으로 남아있던 위젯이 삭제될 때 호출되는 콜백 메소드입니다. 여기서는 onEnabled(Context)에서 했던 일들을 정리해야(clean up) 합니다(데이터베이스 삭제 등).

onReceive(Context, Intent)

이 메소드는 BroadcastReceiver 클래스의 기본적인 콜백 메소드로서, AppWidgetProvider 클래스에서 적당한 시점에 위의 콜백 메소드들이 호출되도록 잘 구현되어 있기 때문에, 보통의 경우에는 구현할 필요가 없고, 위의 콜백 메소드들로 충분하지 않을 경우에만 구현하면 됩니다. 

AppWidgetProvider의 가장 중요한 콜백 메소드는 onUpdate() 입니다. 왜냐하면 (설정 액티비티가 선언되어 있지 않다면) 앱 위젯이 호스트에 추가될 때 호출되는 메소드이며, 여기서 위젯의 초기화 작업 및 변경사항 적용을 해야하기 때문입니다. 만약 위젯이 사용자와 상호작용을 해야 한다면, 이 콜백 메소드에서 이벤트 리스너를 등록해야 합니다. 그리고 만약 앱 위젯이 파일이나 데이터베이스를 생성하지 않고, 그 외에도 위젯이 삭제될 때 정리해야할(clean up) 무언가가 없다면, AppWidgetProvider 개발시 onUpdate()만 구현하면 됩니다. 아래의 예제 코드는, 눌렀을 때 액티비티를 실행하는 버튼으로 구성된 AppWidgetProvider의 구현 클래스입니다: 

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는 onUpdate() 콜백 메소드만 구현하고 있으며, 버튼 눌렀을 때 액티비티가 실행되도록 하기 위하여 setOnClickPendingIntent(int, PendingIntent)를 호출합니다. 그리고 화면에 ExampleAppWidgetProvider에 대한 위젯이 2개 이상 추가될 수 있기 때문에, 반복문을 통해 appWidgetIds의 모든 값에 대하여, 즉 모든 위젯에 대하여 업데이트 되는 내용을 적용해야 합니다. 그리고 위젯이 2개 이상일 경우에도 onUpdate()는 모든 위젯에 대하여 같은 시기에 호출됩니다. 예를 들어서 updatePeriodMillis가 2시간으로 정의되어 있고, 첫번째 위젯을 추가한 후 1시간 후에 두번째 위젯을 추가한 경우, onUpdate()는 첫번째 위젯이 추가된 시점을 기준으로 2시간 간격으로 호출되는 것입니다.  
메모: AppWidgetProvider는 BroadcastReceiver 클래스를 확장하여 만든 클래스이기 때문에, (BroadcastReceiver의 생명주기에 따라) 콜백 메소드가 실행된 후에 내 앱의 프로세스가 멈출 수도 있습니다. 그리고 앱 위젯의 셋업 과정이 (웹 요청 등의 원인으로) 오래 걸리는 경우에는 앱의 응답 지연 문제(Application Not Responding, ANR)를 방지하기 위하여, onUpdate() 메소드 안에서 서비스(Service)를 실행해 주는 것이 좋습니다. 


앱 위젯의 브로드캐스트 인텐트 받기

AppWidgetProvider는 브로드캐스트 인텐트의 액션값을 신경쓰지 않고 지정된 콜백 메소드들만 구현해 주면 되는 편리한 클래스입니다. 하지만 만약 브로드캐스트 인텐트를 직접 처리하고자 한다면, BroadcastReceiver의 확장 클래스를 만들거나 AppWidgetProvider의 onReceive(Context, Intent) 메소드를 오버라이드한 후 브로드캐스트 인텐트의 액션값에 대한 처리를 해주면 됩니다. 그 액션값들은 아래와 같습니다:

  • ACTION_APPWIDGET_UPDATE
  • ACTION_APPWIDGET_DELETED
  • ACTION_APPWIDGET_ENABLED
  • ACTION_APPWIDGET_DISABLED
  • ACTION_APPWIDGET_OPTIONS_CHANGED


앱 위젯의 설정 액티비티 만들기

사용자가 앱 위젯을 추가할 때 설정 액티비티를 보여줄 수 있습니다. 이 액티비티는 앱 위젯이 생성되는 시점에 앱 위젯 호스트에 의해 자동적으로 실행되며, 위젯의 색상이나 크기, 위젯 갱신 주기 등의 설정을 할 수 있습니다. 

설정 액티비티는 다른 액티비티들과 마찬가지로 매니페스트 파일에 정의되어야 하며, 앱 위젯 호스트에서 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>

액티비티의 이름은 패키지명을 포함한 전체 이름으로 선언해야 합니다. 왜냐하면 이는 내 앱의 범위 밖에서 참조되는 값이기 때문입니다. 

설정 액티비티를 만들기 위한 사전 작업은 위의 내용이 다입니다. 이제 설정 액티비티를 구현하면 되는데, 이때 기억해야할 중요한 내용이 두 개 있습니다:

  • 앱 위젯 호스트는 설정 액티비티를 실행할 때 startActivityForResult()를 호출하며, 설정 액티비티는 종료되기 전에 결과 인텐트를 셋팅해줘야 합니다. 결과 인텐트에는 앱 위젯 ID가 포함되어 있어야 하는데, 이는 설정 화면이 실행될 때 받는 인텐트에 EXTRA_APPWIDGET_ID로 담겨 있습니다. 그리고 결과 인텐트에도 같은 키값으로 저장하면 됩니다. 
  • 설정 액티비티가 실행된 경우에는, 앱 위젯이 생성될 때, 시스템이 ACTION_APPWIDGET_UPDATE 을 보내지 않기 때문에 onUpdate() 메소드가 호출되지 않습니다. 따라서, 설정 액티비티에서 그 역할을 해줘야 합니다. 위젯이 처음 생성되는 시점에 설정 액티비티에서  AppWidgetManager를 이용하여 업데이트 요청을 해야 합니다. 하지만 그 이후부터는 onUpdate()가 다시 호출됩니다. 즉, 앱 위젯이 생성되는 처음 1회만 onUpdate()가 스킵되는 것입니다. 

이어지는 내용은 설정 화면에서 위젯에 대하여 설정한 결과들을 위젯 호스트에게 어떻게 다시 전달해 주는지에 대한 내용입니다.


설정 액티비티에서 앱 위젯 업데이트하기

설정 액티비티가 실행되는 경우에는, 설정이 완료되었을 때 설정 액티비티에서 직접 위젯을 업데이트 해줘야 합니다. 이때 AppWidgetManager를 사용합니다. 

위젯을 업데이트하고 설정 액티비티를 종료하는 일련의 과정은 아래와 같습니다:

1. 설정 액티비티를 실행하는 인텐트로부터 앱 위젯 ID를 전달 받습니다:

Intent intent = getIntent();
Bundle extras = intent.getExtras();
if (extras != null) {
    mAppWidgetId
= extras.getInt(
           
AppWidgetManager.EXTRA_APPWIDGET_ID,
           
AppWidgetManager.INVALID_APPWIDGET_ID);
}

2. 설정 액티비티 내에서 위젯 관련 설정이 진행됩니다.

3. 설정이 완료되었다면, AppWidgetManager의 인스턴스를 가져옵니다:

AppWidgetManager appWidgetManager = AppWidgetManager.getInstance(context);

4. 아래와 같이 앱 위젯을 업데이트 합니다:

RemoteViews views = new RemoteViews(context.getPackageName(),
R
.layout.example_appwidget);
appWidgetManager
.updateAppWidget(mAppWidgetId, views);

5. 결과 인텐트를 만들어서 셋팅하고 설정 액티비티를 종료합니다:

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

팁: 설정이 완료되지 않고 설정 액티비티에서 벗어나는 경우에는 결과값에 RESULT_CANCELED를 셋팅합니다. 그러면 앱 위젯 호스트는 설정이 취소되었다는 결과를 받게 되고 앱 위젯은 화면에 추가되지 않을 것입니다. 

SDK에 포함되어 있는 샘플 프로젝트 중 하나인 ApiDemos에서 ExampleAppWidgetConfigure.java 파일을 참고하시기 바랍니다.


미리보기 이미지 설정하기

안드로이드 3.0 버전부터 앱 위젯이 어떻게 생겼는지를 나타내는 미리보기 이미지를 설정할 수 있으며, 이는 위젯 선택 화면에서 보여집니다. 미리보기 이미지를 따로 지정하지 않는 경우에는 앱의 아이콘이 그 역할을 대신합니다. 

예제 코드는 아래와 같습니다:

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

앱 위젯의 미리보기 이미지를 쉽게 생성해주기 위해 안드로이드 에뮬레이터에는 "Widget Preview"라는 앱이 포함되어 있습니다. 이 앱을 실행한 후 위젯을 선택하여 이미지를 생성할 수 있습니다. 


잠금화면에 앱 위젯 보이기

안드로이드 4.2버전부터 위젯을 잠금화면에도 추가할 수 있게 되었습니다. 앱 위젯이 잠금화면에도 추가될 수 있도록 하기 위해서는, AppWidgetProviderInfo를 정의하는 XML 파일에 android:widgetCategory를 선언해줘야 합니다. 여기에 들어갈 수 있는 값으로는 "home_screen"과 "keyguard"가 있고, 하나 또는 두개 모두 들어갈 수 있으며, 잠금화면에 들어가려면 "keyguard"가 포함되어 있어야 합니다. 

( * 역자주: 잠금화면에 앱 위젯을 추가하는 기능은 안드로이드 5.0버전에서 deprecated 된 것으로 보입니다. 4.2~4.4버전에서 잠금화면에 위젯을 추가하는 방법은, 잠금화면의 상단영역을 좌에서 우로 스와잎하면 위젯을 추가할 수 있는 플러스(+) 버튼이 나오는데, 5.0버전 이상에서는 해당 기능이 동작하지 않습니다. 그리고 설정 > 개인설정 > 보안 > 화면보안 섹션에 위젯 사용에 대한 항목이 있는데, 5.0버전 이상에서는 해당 항목이 보이지 않습니다. 5.0버전 이상에서는 노티피케이션바 영역에 들어가는 뷰 영역이 잠금화면에도 보여지기 때문에, 위젯을 대체할 수 있다고 판단하여 deprecated 된 것으로 보입니다. )

기본적으로 위젯은 홈 화면에 들어가는 것이었기 때문에, android:widgetCategory의 기본값은 "home_screen"입니다. 따라서 위젯이 홈 화면에 들어갈 수 있고 또 잠금화면에도 들어갈 수 있게 하려면, 아래와 같이 "keyguard"를 추가하면 됩니다:

<appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android"
   ...
   
android:widgetCategory="keyguard|home_screen">
</appwidget-provider>

위젯을 잠금화면과 홈화면에 모두 보여주고자 하는데 각각의 위젯에 다른 레이아웃을 적용하고 싶다면, 위젯이 추가되는 시점에 위젯 카테고리(widgetCategory)를 확인하여 각각에 맞는 레이아웃을 적용하면 됩니다. getAppWidgetOptions()를 호출하면 Bundle 객체를 리턴해 주는데, 여기에 OPTION_APPWIDGET_HOST_CATEGORY를 키로 하는 값이 WIDGET_CATEGORY_HOME_SCREEN 또는 WIDGET_CATEGORY_KEYGUARD 입니다. 이 값은 위젯이 속해 있는 곳의 위젯 호스트에 의해서 결정되는 값입니다. 그리고 아래 예제 코드에서와 같이 AppWidgetProvider에서 값을 확인할 수 있습니다:

AppWidgetManager appWidgetManager;
int widgetId;
Bundle myOptions = appWidgetManager.getAppWidgetOptions (widgetId);

// Get the value of OPTION_APPWIDGET_HOST_CATEGORY
int category = myOptions.getInt(AppWidgetManager.OPTION_APPWIDGET_HOST_CATEGORY, -1);

// If the value is WIDGET_CATEGORY_KEYGUARD, it's a lockscreen widget
boolean isKeyguard = category == AppWidgetProviderInfo.WIDGET_CATEGORY_KEYGUARD;

위젯 카테고리 값이 확인되었다면 이제 그에 따라 레이아웃을 다르게 적용할 수도 있는 것입니다:

int baseLayout = isKeyguard ? R.layout.keyguard_widget_layout : R.layout.widget_layout;

그리고 잠금화면에 보여질 위젯에 대한 레이아웃을 android:initialKeyguardLayout에 지정해야 합니다. 이는 잠금화면용 레이아웃이라는 것만 빼면 나머지는 android:initialLayout과 동일합니다. 위젯이 초기화되기까지 시간이 다소 걸린다면 그 동안에 보여줄 레이아웃이 여기에 들어갑니다. 


크기 조정 가이드라인

잠금화면에 추가된 위젯에 대해서는 minWidth, minHeight, minResizeWidth, minResizeHeight 속성이 무시됩니다. 홈화면에 추가된 위젯에 대해서는 여전히 적용되는 속성이지만, 잠금화면에 추가된 위젯에 대해서만 무시되는 것입니다. 

잠금화면 위젯의 너비(width)는 항상 제공되는 영역을 꽉 채웁니다. 그리고 잠금화면 위젯의 높이(height)는 아래의 규칙들을 따릅니다:

  • 위젯이 위아래로 리사이즈 가능하다고 정의되어 있지 않은 경우(android:resizeMode에 vertical이 포함되어 있지 않은 경우), 위젯의 높이는 항상 "작은" 상태가 됩니다. 
    - 세로 모드(portrait mode)의 폰에서, "작은" 상태란 잠금해제 UI가 보여지는 영역을 제외한 나머지 영역을 의미합니다. 
    - 태블릿 또는 가로 모드(landscape mode)의 폰에서, "작은" 상태란 디바이스에 따라 다르게 정의될 수 있습니다. 
  • 위젯이 위아래로 리사이즈 가능하다고 정의되어 있는 경우, 세로 모드의 폰에서는 위젯의 높이가 "작은" 상태가 되고, 그 외의 모든 경우에는 가능한 공간을 꽉 채웁니다. 


앱 위젯에 목록 보이기

안드로이드 3.0 버전부터 앱 위젯에 목록(collections)을 보여줄 수 있게 되었습니다. 컨텐트 프로바이더와 같은 것들로부터 데이터를 받아와서 그 목록을 보여주기 위해 RemoteViewsService 를 사용할 수 있습니다. RemoteViewsService로부터 제공받는 데이터들을 보여주기 위해서 아래의 뷰들 중 하나를 사용할 수 있으며, 아래의 뷰들을 이제 "콜랙션 뷰(collection views)"라고 지칭하도록 하겠습니다. 

ListView

하나의 행에 하나의 항목이 있고, 위아래로 스크롤되는 뷰입니다. Gmail 앱의 위젯이 그 예입니다.

GridView

하나의 행에 하나 이상의 항목이 있을 수 있고, 위아래로 스크롤되는 뷰입니다. 북마크 앱의 위젯이 그 예입니다.

StackView

카드가 겹쳐져 있는 형태이며 위아래로 스와잎하면 가장 위의 카드가 뒤로 넘어가는 방식의 뷰입니다. 유튜브나 도서 앱의 위젯이 그 예입니다. 

AdapterViewFlipper

2개 이상의 뷰들을 ViewAnimator로 애니메이션하는 방식의 뷰이며, 한번에 하나의 뷰만이 화면에 보여집니다. 

위에서 언급한 바와 같이, 이러한 콜렉션 뷰들은 어댑터(Adapter)를 이용하여 다른 곳으로부터 데이터를 제공 받아서 보여줍니다. 어댑터는 데이터셋과 그에 대응되는 뷰 사이에서 중간자 역할을 하며 그 둘을 연결시켜 줍니다. 콜렉션 뷰들이 어댑터를 기반으로 해야하기 때문에, 안드로이드 프레임웍에는 위젯에서도 이러한 방식이 적용될 수 있도록 별도의 모듈이 포함되어 있어야 합니다. 기존의, 앱 내에 콜렉션 뷰를 보여줄 때의 방식은 RemoteViews를 사용하는 것이 아니기 때문에, 그에 대한 방식이 따로 있어야 하는 것입니다. 따라서 앱 위젯에서는 Adapter 대신에 Adapter 인터페이스를 래핑(wrapping)한 RemoteViewsFactory를 사용합니다. 동작하는 방식은 기존에 Adapter를 사용하던 방식과 유사합니다. RemoteViewsFactory가 항목의 위치를 받아서 그에 해당하는 아이템을 찾고 그 데이터를 RemoteViews 객체에 적용하여 화면에 보여주는 것입니다. 따라서 앱 위젯에 콜렉션 뷰를 사용하기 위해서는 RemoteViewsServiceRemoteViewsFactory를 구현해야 합니다. 

RemoteViewsService는 RemoteViewsFactory 인터페이스의 구현체인 어댑터를 가지고 있으며, 그 어댑터가 각 위치에 해당하는 RemoteViews 객체를 리턴해 줍니다. RemoteViewsFactory는 (ListView나 GridView와 같은) 콜렉션 뷰와 콜렉션 뷰에 보여줘야할 데이터 셋을 중간에서 연결해 주는 어댑터에 대한 인터페이스입니다. StackView Widget 샘플앱을 살펴보면, 아래와 같은 형태로 구현하고 있음을 알 수 있습니다:

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.

}


샘플 앱

본 문서에 있는 예제 코드들은 StackView Widget 샘플앱으로부터 가져온 것들입니다:


위의 샘플 앱 위젯은 "0!"부터 "9!"까지의 문자를 보여주는 10개의 뷰의 스택으로 구성되어 있으며, 아래와 같이 동작합니다:

  • 사용자는 가장 앞에 있는 뷰를 아래로 플링(fling)하여 다음 뷰를 앞으로 가져오거나, 위로 플링하여 이전 뷰를 앞으로 가져올 수 있으며, 이는 StackView의 기본적인 동작입니다. 
  • 사용자가 뷰를 건드리지 않더라도, 슬라이드쇼처럼 적당한 시간을 주기로 자동으로 다음뷰로 넘어가도록 할 수 있습니다. 이는 res/xml/stackwidgetinfo.xml 파일에 android:autoAdvanceViewId="@id/stack_view"와 같이 선언하면 되며, @id/stack_view는 위젯 레이아웃의 StackView의 ID입니다.  
  • 사용자가 뷰를 클릭하면 "Touched view n" 이라는 토스트 메시지가 출력되며, 여기서 n은 터치된 뷰의 위치값을 나타냅니다. 이와 관련하여 더 자세한 내용은 각 항목에 기능 추가하기에서 학습하실 수 있습니다. 


목록을 보이게 하는 앱 위젯 구현하기

앱 위젯에서 목록을 보여주기 위해서는, (위에서 학습한 바와 같이) 앱 위젯을 구현하는데 필요한 일반적인 단계에 더하여, 아래에서 설명하는 추가적인 단계들을 진행해야 합니다. 


목록 구현을 위한 매니페스트 파일 수정사항

매니페스트 파일에서 앱 위젯 선언하기 섹션에서 학습한 내용에 더하여, 앱 위젯이 RemoteViewsService를 사용하여 목록을 보여주도록 하기 위해서는, 매니페스트 파일에 서비스(service)를 BIND_REMOTEVIEWS 퍼미션과 함께 선언해줘야 합니다. 이것은 다른 앱이 내 앱 위젯의 데이터에 접근하는 것을 막아줍니다. 예제 코드는 아래와 같습니다:

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

위 코드에서 MyWidgetService는 RemoteViewsService의 서브 클래스입니다. 


목록 구현을 위한 앱 위젯의 레이아웃

목록을 보여주고자 하는 앱 위젯의 레이아웃에는 콜렉션 뷰 중의 하나가 포함되어 있어야 합니다. 콜렉션 뷰에는 ListView, GridView, StackView, AdapterViewFlipper가 있습니다. 아래 예제 코드는 StackView Widget 샘플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>

위 코드에서 empty_view는 stack_view가 빈(empty) 상태일 때 텍스트("This is the empty view")를 보여주는 뷰입니다. 

위의 레이아웃 파일은 앱 위젯 자체에 대한 레이아웃이며, StackView의 각각의 항목을 나타내는 레이아웃 파일도 필요합니다. StackView Widget 샘플widget_item.xml 파일이 그것입니다. 


목록 구현을 위한 AppWidgetProvider 클래스 구현

일반적인 앱 위젯과 마찬가지로, 목록을 보여주는 경우에도 AppWidgetProvider의 서브 클래스를 구성하는 대부분의 코드는 onUpdate() 메소드입니다. 일반적인 경우와의 가장 큰 차이점은 setRemoteAdapter() 메소드를 호출한다는 것입니다. 이것은 콜렉션 뷰가 데이터를 어디에서 가져오는지를 말해줍니다. RemoteViewsService는 내부적으로 RemoteViewsFactory의 구현체를 사용하여 콜렉션 뷰 각각의 위치에 해당 데이터를 연결해주는 어댑터의 역할을 해줍니다. setRemoteAdapter(int viewId, Intent intent) 에서 viewId는 콜렉션 뷰의 ID이고, intent는 RemoteViewsService의 구현체를 가리키는 인텐트입니다. ( * 역자주: setRemoteAdapter(int appWidgetId, int viewId, Intent intent) 는 API Level 14에서 deprecated 되었기 때문에, minSdk가 14이상이라면 아래 예제 코드에서 for 루프문을 제거할 수 있습니다. )

아래 예제 코드는 StackView Widget 샘플StackWidgetProvider.java의 onUpdate() 메소드 부분이며, 여기서 목록을 보여주기 위해 어댑터를 셋팅하는 부분을 볼 수 있습니다:

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(appWidgetIds[i], 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);
}


RemoveViewsService 클래스

위에서 설명한 바와 같이, RemoteViewsService의 서브 클래스는 내부적으로 RemoteViewsFactory의 구현체를 이용하여 목록의 각 항목에 대응되는 RemoteViews에 해당 데이터를 주입합니다. 

1. RemoteViewsService의 서브 클래스를 구현합니다. RemoteViewsService는 어댑터를 이용하여 RemoteViews에 데이터를 주입합니다. 

2. RemoteViewsService의 서브 클래스에는 RemoteViewsFactory 인터페이스의 구현체를 리턴해 주는 부분을 구현해야 합니다. RemoteViewsFactory는 (ListView, GridView 등의) 콜렉션 뷰와 그에 대응되는 데이터들을 연결시켜 주는 어댑터 인터페이스로서, Adapter 인터페이스의 랩퍼(wrapper) 인터페이스입니다.

다시 말해서, RemoteViewsService의 서브 클래스를 구현할 때 가장 중요한 일은 RemoteViewsFactory 인터페이스의 구현체를 리턴해 주는 것입니다. 


RemoteViewsFactory 인터페이스

RemoteViewsFactory 인터페이스를 구현한 클래스는 앱 위젯에 데이터를 제공해 줍니다. 즉, 위젯의 콜렉션 뷰의 각 아이템에 대한 레이아웃 XML 파일에 해당 데이터를 주입할 수 있게 해줍니다. 데이터는 데이터베이스로부터 얻어온 커서(cursor) 객체도 되고, 간단한 배열(array)도 될 수 있습니다. StackView Widget 샘플에서 데이터는 WidgetItem 객체를 담는 ArrayList 였습니다. RemoteViewsFactory는 위젯의 콜렉션 뷰에 데이터를 주입해 주는 어댑터인 것입니다. 

RemoteViewsFactory 인터페이스를 구현할 때, onCreate()getViewAt() 메소드를 구현하는 것이 중요합니다. 

시스템은 RemoteViewsFactory의 구현체가 처음 생성되는 시점에 onCreate()를 호출해 주며, 여기에서 데이터를 가져오는 것과 관련된 일들을 해야 합니다. 예를 들어, StackView Widget 샘플에서는 onCreate()에서 ArrayList에 WidgetItem 객체들을 담는 일을 합니다. 그러면 위젯이 화면에 보여질 때, 시스템이 이 ArrayList에서 현재 위치에 해당하는 WidgetItem 객체를 가져와서 그 안에 셋팅되어 있는 텍스트를 뷰에 보여주는 것입니다. 

아래 예제 코드는 StackView Widget 샘플RemoteViewsFactory 구현체의 일부로서, onCreate()에 주목하시기 바랍니다:

class StackRemoteViewsFactory implements
RemoteViewsService.RemoteViewsFactory {
   
private static final int mCount = 10;
   
private List<WidgetItem> mWidgetItems = new ArrayList<WidgetItem>();
   
private Context mContext;
   
private int mAppWidgetId;

   
public StackRemoteViewsFactory(Context context, Intent intent) {
        mContext
= context;
        mAppWidgetId
= 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 < mCount; i++) {
            mWidgetItems
.add(new WidgetItem(i + "!"));
       
}
       
...
   
}
...

RemoteViewsFactory의 getViewAt(int position) 메소드는 position에 해당하는 데이터가 주입된 RemoteViews 객체를 리턴해 줍니다. 예제 코드는 아래와 같습니다:

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(mContext.getPackageName(), R.layout.widget_item);
    rv
.setTextViewText(R.id.widget_item, mWidgetItems.get(position).text);

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


각 항목에 기능 추가하기

위에서는 위젯의 콜렉션 뷰에 데이터를 어떻게 바인드할까에 대하여 설명했습니다. 그렇다면 콜렉션 뷰의 각 아이템들의 동작 방식은 어떻게 구현할 수 있을까요? 

위의 AppWidgetProvider 클래스 사용하기 섹션에서 설명한 바와 같이, 버튼을 클릭했을 때 액티비티를 실행하는 등의 동작을 하기 위해서 setOnClickPendingIntent() 메소드를 사용하는 것이 일반적입니다. 하지만 이러한 방식은 콜렉션 뷰의 각 아이템에 대해서는 적용할 수 없으며, 대신에 setOnClickFillInIntent() 메소드를 사용합니다. 콜렉션 뷰 자체에 대해서는 setPendingIntentTemplate() 메소드를 호출하여 콜렉션 뷰를 클릭했을 때 어떤 동작을 하게되는지에 대해서 정의하고, 콜렉션 뷰의 아이템 뷰에 대해서는 setOnClickFillInIntent() 메소드를 호출하여 위치값(position) 등의 데이터를 전달합니다. 

이번 섹션에서도 콜렉션 뷰의 아이템들의 동작을 추가하는 방식을 설명하기 위하여 StackView Widget 샘플의 코드를 인용합니다. 사용자가 위젯의 맨 앞의 뷰를 클릭하면, "Touched view n"이라는 토스트 메시지가 출력됩니다(n에는 터치한 뷰의 position값이 들어갑니다). 동작하는 흐름을 간단히 살펴보면 아래와 같습니다: 

  • (AppWidgetProvider의 서브클래스인) StackWidgetProviderTOAST_ACTION이라는 액션값을 갖는 펜딩 인텐트를 생성합니다.  
  • 사용자가 위젯을 터치하면, TOAST_ACTION이 브로드캐스트 됩니다.
  • StackWidgetProvider의 onReceive()에 해당 브로드캐스트 인텐트가 전달되고, 토스트 메시지를 출력합니다. 터치한 뷰의 position값은 RemoteViewsFactory의 getViewAt()에서 setOnClickFillInIntent()를 통해 전달된 값입니다. 


setPendingIntentTemplate() 호출하기

StackWidgetProvider는 펜딩 인텐트를 셋팅합니다. 하지만 콜렉션 뷰의 아이템 뷰들에 대하여 각각의 펜딩 인텐트를 셋팅할 수는 없기 때문에, 콜렉션 뷰 전체에 대해서는 펜딩 인텐트 템플릿을 셋팅하고(setPendingIntentTemplate() 호출), 아이템 뷰 각각에 대해서는 데이터 전달용 인텐트(fill-in intent)를 셋팅합니다(setOnClickFillInIntent() 호출). 

아래 예제 코드에서는, 사용자가 뷰를 터치하면 TOAST_ACTION으로 브로드캐스트하고, 그것을 onReceive()에서 받아서, 터치된 뷰에 대한 토스트 메시지를 출력해 주고 있습니다:

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);
   
}
}


setOnClickFillInIntent() 호출하기

RemoteViewsFactory에서는 콜렉션 뷰의 각 아이템에 데이터 전달용 인텐트(fill-in intent)를 셋팅해야 합니다. 이것은 어느 아이템이 클릭되었는지에 대한 정보를 전달해 주는 것입니다. 데이터 전달용 인텐트에 담긴 데이터는, 아이템이 클릭되어 브로드캐스트 하는 시점에 최종적으로 전달되는 인텐트에 추가됩니다. 

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 mCount = 10;
   
private List<WidgetItem> mWidgetItems = new ArrayList<WidgetItem>();
   
private Context mContext;
   
private int mAppWidgetId;

   
public StackRemoteViewsFactory(Context context, Intent intent) {
        mContext
= context;
        mAppWidgetId
= 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 < mCount; i++) {
                mWidgetItems
.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(mContext.getPackageName(), R.layout.widget_item);
            rv
.setTextViewText(R.id.widget_item, mWidgetItems.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와 어떻게 상호작용을 하는지 보여줍니다:



위젯에 목록을 보여주는 이유 중 하나는 어떤 최신 컨텐츠 일부를 보여주기 위해서일 것입니다. 예를 들어, Gmail 앱의 위젯은 받은 편지함의 최근 편지 몇개를 보여줍니다. 그러기 위해서는, RemoteViewsFactory가 새로 받은 편지를 보여주기 위해 콜렉션 뷰를 갱신할 필요가 있으며, 이를 위해 AppWidgetManager의 notifyAppWidgetViewDataChanged() 메소드를 호출해야 합니다. 그러면 RemoteViewsFactory의 onDataChanged() 콜백 메소드가 호출되고, 여기서 최신 데이터를 가져오는 작업을 할 수 있습니다. onDataChanged()는 동기적으로 동작합니다. 즉, onDataChanged()에서 데이터 가져오는 일이 끝나야 그 데이터를 아이템 뷰에 적용하는(fetch data) 메소드가 호출되는 것입니다. onDataChanged()getViewAt()에서는 시간이 꽤 오래 걸리는 작업들(processing-intensive operations)이 실행될 수 있을 것입니다. 따라서 그럴 때는 RemoteViewsFactory의 getLoadingView()를 구현함으로써 실행 중에 로딩뷰가 보여지도록 할 수 있습니다. 


Posted by 개발자 김태우
,