컬렉션 위젯은 갤러리 앱의 사진 컬렉션, 뉴스 앱의 기사, 커뮤니케이션 앱의 메시지와 같이 동일한 유형의 여러 요소를 표시하는 데 특화되어 있습니다. 컬렉션 위젯은 일반적으로 컬렉션 탐색과 컬렉션의 요소를 세부정보 뷰로 열기라는 두 가지 사용 사례에 중점을 둡니다. 컬렉션 위젯은 세로로 스크롤할 수 있습니다.
이러한 위젯은 RemoteViewsService를 사용하여 콘텐츠 제공업체와 같은 원격 데이터로 지원되는 컬렉션을 표시합니다. 위젯은 컬렉션 뷰라고 하는 다음 뷰 유형 중 하나를 사용하여 데이터를 표시합니다.
ListView: 세로로 스크롤되는 목록에 항목을 표시하는 뷰입니다.
GridView: 2차원 스크롤 그리드에 항목을 표시하는 뷰입니다.
StackView: 롤로덱스처럼 카드가 쌓여 있는 카드 보기입니다. 사용자가 앞면 카드를 위나 아래로 넘겨 이전 카드나 다음 카드를 볼 수 있습니다.
AdapterViewFlipper: 둘 이상의 뷰 간에 애니메이션을 적용하는 어댑터 지원 ViewAnimator입니다. 한 번에 한 자녀만 표시됩니다.
이러한 컬렉션 뷰는 원격 데이터로 지원되는 컬렉션을 표시하므로 Adapter를 사용하여 사용자 인터페이스를 데이터에 바인딩합니다. Adapter는 데이터 세트의 개별 항목을 개별 View 객체에 바인딩합니다.
이러한 컬렉션 뷰는 어댑터에서 지원하므로 Android 프레임워크에는 위젯에서 사용을 지원하는 추가 아키텍처가 포함되어야 합니다. 위젯 컨텍스트에서 Adapter는 Adapter 인터페이스를 둘러싼 얇은 래퍼인 RemoteViewsFactory로 대체됩니다. 컬렉션의 특정 항목에 대해 요청되면 RemoteViewsFactory는 컬렉션의 항목을 RemoteViews 객체로 만들고 반환합니다. 위젯에 컬렉션 뷰를 포함하려면 RemoteViewsService 및 RemoteViewsFactory를 구현합니다.
RemoteViewsService는 원격 어댑터가 RemoteViews 객체를 요청할 수 있는 서비스입니다. RemoteViewsFactory는 컬렉션 뷰(예: ListView, GridView, StackView)와 해당 뷰의 기본 데이터 간 어댑터의 인터페이스입니다. StackWidget 샘플에서 이 서비스와 인터페이스를 구현하는 상용구 코드의 예시는 다음과 같습니다.
class StackWidgetService : RemoteViewsService() { override fun onGetViewFactory(intent: Intent): RemoteViewsFactory = StackRemoteViewsFactory(this.applicationContext, intent) } class StackRemoteViewsFactory( private val context: Context, intent: Intent ) : RemoteViewsService.RemoteViewsFactory { // See the RemoteViewsFactory API reference for the full list of methods to implement. }
샘플 애플리케이션
이 섹션의 코드 발췌문도 StackWidget 샘플에서 가져온 것입니다.
StackWidget.이 샘플은 0부터 9까지의 값을 표시하는 10개의 뷰 스택으로 구성됩니다. 샘플 위젯에는 다음과 같은 기본 동작이 있습니다.
사용자는 위젯의 상단 뷰를 세로로 플링하여 다음 또는 이전 뷰를 표시할 수 있습니다. 이는 기본 제공되는
StackView동작입니다.사용자 상호작용 없이 위젯은 슬라이드쇼처럼 뷰를 순서대로 자동으로 진행합니다. 이는
res/xml/stackwidgetinfo.xml파일의android:autoAdvanceViewId="@id/stack_view"설정 때문입니다. 이 설정은 뷰 ID에 적용되며, 이 경우 스택 뷰의 뷰 ID입니다.사용자가 상단 뷰를 터치하면 위젯에
Toast메시지 'Touched view n'이 표시됩니다. 여기서 n은 터치된 뷰의 색인 (위치)입니다. 동작을 구현하는 방법에 관한 자세한 내용은 개별 항목에 동작 추가 섹션을 참고하세요.
컬렉션으로 위젯 구현
컬렉션이 있는 위젯을 구현하려면 위젯 구현 절차를 따른 후 몇 가지 추가 단계(매니페스트 수정, 위젯 레이아웃에 컬렉션 뷰 추가, AppWidgetProvider 서브클래스 수정)를 따릅니다.
컬렉션이 있는 위젯의 매니페스트
매니페스트에서 위젯 선언에 나열된 요구사항 외에도 컬렉션이 있는 위젯을 RemoteViewsService에 바인딩할 수 있어야 합니다. 이렇게 하려면 BIND_REMOTEVIEWS 권한을 사용하여 매니페스트 파일에서 서비스를 선언합니다.
이렇게 하면 다른 애플리케이션이 위젯의 데이터에 자유롭게 액세스하지 못합니다.
예를 들어 RemoteViewsService를 사용하여 컬렉션 뷰를 채우는 위젯을 만들 때 매니페스트 항목은 다음과 같습니다.
<service android:name="MyWidgetService"
android:permission="android.permission.BIND_REMOTEVIEWS" />
이 예에서 android:name="MyWidgetService"은 RemoteViewsService의 서브클래스를 나타냅니다.
컬렉션이 있는 위젯의 레이아웃
위젯 레이아웃 XML 파일의 기본 요구사항은 컬렉션 뷰(ListView, GridView, StackView, AdapterViewFlipper) 중 하나를 포함하는 것입니다. 다음은 StackWidget 샘플의 widget_layout.xml 파일입니다.
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent"> <StackView android:id="@+id/stack_view" android:layout_width="match_parent" android:layout_height="match_parent" android:gravity="center" android:loopViews="true" /> <TextView 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를 위젯 컬렉션의 원격 어댑터로 설정하는 방법입니다.
override fun onUpdate( context: Context, appWidgetManager: AppWidgetManager, appWidgetIds: IntArray ) { // Update each of the widgets with the remote adapter. appWidgetIds.forEach { appWidgetId -> // Set up the intent that starts the StackViewService, which // provides the views for this collection. val intent = Intent(context, StackWidgetService::class.java).apply { // Add the 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 widget layout. val views = 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 must be in the same layout used to instantiate the // RemoteViews object. setEmptyView(R.id.stack_view, R.id.empty_view) } // Do additional processing specific to this widget. appWidgetManager.updateAppWidget(appWidgetId, views) } super.onUpdate(context, appWidgetManager, appWidgetIds) }
데이터 지속
이 페이지에 설명된 대로 RemoteViewsService 서브클래스는 원격 컬렉션 뷰를 채우는 데 사용되는 RemoteViewsFactory를 제공합니다.
구체적으로 다음 단계를 따르세요.
RemoteViewsService서브클래스.RemoteViewsService는 원격 어댑터가RemoteViews를 요청할 수 있는 서비스입니다.RemoteViewsService서브클래스에RemoteViewsFactory인터페이스를 구현하는 클래스를 포함합니다.RemoteViewsFactory는 원격 컬렉션 뷰(예:ListView,GridView,StackView)와 해당 뷰의 기본 데이터 간의 어댑터 인터페이스입니다. 구현은 데이터 세트의 각 항목에 대해RemoteViews객체를 만들어야 합니다. 이 인터페이스는Adapter의 얇은 래퍼입니다.
서비스의 단일 인스턴스 또는 여기에 포함된 데이터가 유지된다고 확신할 수 없습니다. 정적 데이터가 아닌 경우 RemoteViewsService에 데이터를 저장하지 마세요. 위젯의 데이터를 유지하려면 프로세스 수명 주기 이후에도 데이터가 유지되는 ContentProvider를 사용하는 것이 가장 좋습니다. 예를 들어 식료품점 위젯은 각 식료품 목록 항목의 상태를 SQL 데이터베이스와 같은 영구 위치에 저장할 수 있습니다.
RemoteViewsService 구현의 기본 콘텐츠는 다음 섹션에서 설명하는 RemoteViewsFactory입니다.
RemoteViewsFactory 인터페이스
RemoteViewsFactory 인터페이스를 구현하는 맞춤 클래스는 위젯에 컬렉션의 항목에 관한 데이터를 제공합니다. 이를 위해 위젯 항목 XML 레이아웃 파일을 데이터 소스와 결합합니다. 이 데이터 소스는 데이터베이스부터 배열까지 무엇이든 될 수 있습니다. StackWidget 샘플에서 데이터 소스는 WidgetItems의 배열입니다. RemoteViewsFactory는 데이터를 원격 컬렉션 뷰에 연결하는 어댑터 역할을 합니다.
RemoteViewsFactory 서브클래스에 구현해야 하는 가장 중요한 두 가지 메서드는 onCreate() 및 getViewAt()입니다.
시스템은 처음으로 팩토리를 만들 때 onCreate()를 호출합니다.
여기에서 데이터 소스에 대한 연결 또는 커서를 설정합니다. 예를 들어 StackWidget 샘플은 onCreate()를 사용하여 WidgetItem 객체 배열을 초기화합니다. 위젯이 활성 상태이면 시스템은 배열의 색인 위치를 사용하여 이러한 객체에 액세스하고 객체에 포함된 텍스트를 표시합니다.
다음은 onCreate() 메서드의 일부를 보여주는 StackWidget 샘플의 RemoteViewsFactory 구현에서 발췌한 부분입니다.
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(), set up any connections or cursors to your data // source. Heavy lifting, such as downloading or creating content, // must be deferred to onDataSetChanged() or getViewAt(). Taking // more than 20 seconds on this call results in an ANR. widgetItems = List(REMOTE_VIEW_COUNT) { index -> WidgetItem("$index!") } } }
RemoteViewsFactory 메서드 getViewAt()는 데이터 세트의 지정된 position에 있는 데이터에 해당하는 RemoteViews 객체를 반환합니다. 다음은 StackWidget 샘플의 RemoteViewsFactory 구현에서 발췌한 내용입니다.
override fun getViewAt(position: Int): RemoteViews { // Construct a remote views item based on the 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) } }
개별 항목에 동작 추가
이전 섹션에서는 데이터를 위젯 컬렉션에 바인딩하는 방법을 보여줍니다. 하지만 컬렉션 뷰의 개별 항목에 동적 동작을 추가하려면 어떻게 해야 할까요?
onUpdate() 클래스로 이벤트 처리에 설명된 대로 일반적으로 setOnClickPendingIntent()를 사용하여 객체의 클릭 동작을 설정합니다(예: 버튼이 Activity를 실행하도록 함). 하지만 이 접근 방식은 개별 컬렉션 항목의 하위 뷰에 허용되지 않습니다.
예를 들어 setOnClickPendingIntent()를 사용하여 앱을 실행하는 Gmail 위젯에서 전역 버튼을 설정할 수 있지만 개별 목록 항목에서는 설정할 수 없습니다.
대신 컬렉션의 개별 항목에 클릭 동작을 추가하려면 setOnClickFillInIntent()를 사용하세요. 컬렉션 뷰의 대기 중 인텐트 템플릿을 설정하고 RemoteViewsFactory를 사용하여 컬렉션의 각 항목에 채우기 인텐트를 설정합니다.
이 섹션에서는 StackWidget 샘플을 사용하여 개별 항목에 동작을 추가하는 방법을 설명합니다. StackWidget 샘플에서 사용자가 상단 뷰를 터치하면 위젯에 Toast 메시지 'Touched view n'이 표시됩니다. 여기서 n은 터치된 뷰의 색인 (위치)입니다. 작동 원리는 다음과 같습니다.
StackWidgetProvider(AppWidgetProvider서브클래스)는TOAST_ACTION라는 맞춤 액션으로 대기 중인 인텐트를 만듭니다.사용자가 뷰를 터치하면 인텐트가 실행되고
TOAST_ACTION를 브로드캐스트합니다.이 브로드캐스트는
StackWidgetProvider클래스의onReceive()메서드에 의해 가로채기되고 위젯은 터치된 뷰의Toast메시지를 표시합니다. 컬렉션 항목의 데이터는RemoteViewsService를 통해RemoteViewsFactory에서 제공됩니다.
대기 중인 인텐트 템플릿 설정
StackWidgetProvider (AppWidgetProvider 서브클래스)는 대기 중인 인텐트를 설정합니다. 컬렉션의 개별 항목은 자체 대기 중 인텐트를 설정할 수 없습니다. 대신 컬렉션 전체에서 대기 중인 인텐트 템플릿을 설정하고 개별 항목에서 채우기 인텐트를 설정하여 항목별로 고유한 동작을 만듭니다.
이 클래스는 사용자가 뷰를 터치할 때 전송되는 브로드캐스트도 수신합니다. onReceive() 메소드에서 이 이벤트를 처리합니다. 인텐트의 작업이 TOAST_ACTION이면 위젯은 현재 뷰에 관한 Toast 메시지를 표시합니다.
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 whether the intent's action is TOAST_ACTION. If it is, the // 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 ) // EXTRA_ITEM represents a custom value provided by the Intent // passed to the setOnClickFillInIntent() method to indicate the // position of the clicked item. See StackRemoteViewsFactory in // Set the fill-in Intent for details. 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 widgets with the remote adapter. appWidgetIds.forEach { appWidgetId -> // Sets up the intent that points to the StackViewService that // provides 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 embed // the extra sinto the data so that the extras are not 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 must 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 can't 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 has the effect of // broadcasting TOAST_ACTION. action = TOAST_ACTION putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetId) data = Uri.parse(toUri(Intent.URI_INTENT_SCHEME)) // The template must be mutable, because each item fills in its // own extras through setOnClickFillInIntent(). PendingIntent.getBroadcast( context, 0, this, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_MUTABLE ) } rv.setPendingIntentTemplate(R.id.stack_view, toastPendingIntent) appWidgetManager.updateAppWidget(appWidgetId, rv) } super.onUpdate(context, appWidgetManager, appWidgetIds) } }
입력 의도 설정
RemoteViewsFactory는 컬렉션의 각 항목에 채우기 인텐트를 설정해야 합니다. 이렇게 하면 특정 항목의 개별 클릭 시 작업을 구분할 수 있습니다. 그런 다음 채우기 인텐트가 PendingIntent 템플릿과 결합되어 항목을 탭할 때 실행되는 최종 인텐트를 결정합니다.
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(), set up any connections or cursors to your data source. // Heavy lifting, such as downloading or creating content, must be // deferred to onDataSetChanged() or getViewAt(). Taking more than 20 // seconds on this call results 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 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) // Set a fill-intent 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) // ... } } // ... }
컬렉션 데이터 최신 상태로 유지
그림 2는 컬렉션을 사용하는 위젯의 업데이트 흐름을 보여줍니다. 위젯 코드가 RemoteViewsFactory와 상호작용하는 방식과 업데이트를 트리거하는 방법을 보여줍니다.
RemoteViewsFactory와의 상호작용컬렉션을 사용하는 위젯은 사용자에게 최신 콘텐츠를 제공할 수 있습니다. 예를 들어 Gmail 위젯은 사용자에게 받은편지함의 스냅샷을 제공합니다. 이를 위해 RemoteViewsFactory 및 컬렉션 뷰를 트리거하여 새 데이터를 가져오고 표시합니다.
이렇게 하려면 AppWidgetManager를 사용하여 notifyAppWidgetViewDataChanged()를 호출합니다. 이 호출은 RemoteViewsFactory 객체의 onDataSetChanged() 메서드에 대한 콜백을 발생시켜 새 데이터를 가져올 수 있게 합니다.
onDataSetChanged() 콜백 내에서 처리 집약적인 작업을 동기식으로 실행할 수 있습니다. 이 호출은 메타데이터 또는 뷰 데이터가 RemoteViewsFactory에서 가져오기 전에 완료됩니다. getViewAt() 메서드 내에서 처리 집약적인 작업을 수행할 수도 있습니다. 이 호출에 시간이 오래 걸리면 RemoteViewsFactory 객체의 getLoadingView() 메서드로 지정된 로드 뷰가 반환될 때까지 컬렉션 뷰의 해당 위치에 표시됩니다.
RemoteCollectionItems를 사용하여 컬렉션을 직접 전달
Android 12 (API 수준 31)에는 setRemoteAdapter(int viewId,
RemoteViews.RemoteCollectionItems
items) 메서드가 추가되어 컬렉션 뷰를 채울 때 앱에서 컬렉션을 직접 전달할 수 있습니다. 이 메서드를 사용하여 어댑터를 설정하는 경우 RemoteViewsFactory를 구현할 필요가 없고 notifyAppWidgetViewDataChanged()를 호출할 필요가 없습니다.
이 접근 방식은 어댑터를 더 쉽게 채울 수 있을 뿐만 아니라 사용자가 목록을 스크롤하여 새 항목을 표시할 때 새 항목을 채우는 지연 시간도 없앱니다. 이 어댑터 설정 방식은 컬렉션 항목 집합이 비교적 작은 경우에 적합합니다. 하지만 예를 들어 컬렉션에 setImageViewBitmap에 전달되는 Bitmaps가 많이 포함된 경우에는 이 접근 방식이 제대로 작동하지 않습니다.
컬렉션에서 일정한 레이아웃 집합을 사용하지 않는 경우(즉, 일부 항목이 가끔만 표시되는 경우) setViewTypeCount를 사용하여 컬렉션에 포함할 수 있는 고유한 레이아웃의 최대 개수를 지정합니다. 이렇게 하면 앱 위젯 업데이트 전반에서 어댑터를 재사용할 수 있습니다.
다음은 간소화된 RemoteViews 컬렉션을 구현하는 방법의 예입니다.
val itemLayouts = listOf( R.layout.item_type_1, R.layout.item_type_2, // ... ) remoteView.setRemoteAdapter( R.id.list_view, RemoteViews.RemoteCollectionItems.Builder() .addItem(/* id= */ ID_1, RemoteViews(context.packageName, R.layout.item_type_1)) .addItem(/* id= */ ID_2, RemoteViews(context.packageName, R.layout.item_type_2)) // ... .setViewTypeCount(itemLayouts.count()) .build() )