https://developer.android.com/develop/ui/views/appwidgets
#
AppWidgetHost
위젯을 로드하고 표시할 앱. 보통 Launcher 가 그 역할을 함.
AppWidgetProviderInfo
widget 의 layout, update frequency, AppWidgetProvider 등의 metadata 기술 xml 형태로 제공함
AppWidgetProvider
widget 의 lifecycle 관련된 method 들을 정의. widget 이 update, enable, disable, deleted 되었을 때 event 를 broadcast 로 받을 수 있음.
View layout
xml 로 정의
#
Android Studio 에서는 New > Widget > App Widget 을 통해 AppWidgetProviderInfo, AppWidgetProvider, view layout file 의 생성을 지원함.
#
Widget 에 configuration activity 를 지원할 수도 있음.
Android 12 (API Level 31) 부터는 default configuration 을 제공할 수 있으며, user 가 widget 을 나중에 재설정 할 수 있음.
Android 11 (API Level 30) 이하에서는 home screen 에 widget 이 추가될 때마다 항상 launch 됨.
<AppWidgetProviderInfo>
#
AppWidgetProviderInfo XML 은 res/xml/ 에 정의하며 예시는 아래와 같음.
<appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android"
android:minWidth="40dp"
android:minHeight="40dp"
android:targetCellWidth="1"
android:targetCellHeight="1"
android:maxResizeWidth="250dp"
android:maxResizeHeight="120dp"
android:updatePeriodMillis="86400000"
android:description="@string/example_appwidget_description"
android:previewLayout="@layout/example_appwidget_preview"
android:initialLayout="@layout/example_loading_appwidget"
android:configure="com.example.android.ExampleAppWidgetConfigurationActivity"
android:resizeMode="horizontal|vertical"
android:widgetCategory="home_screen"
android:widgetFeatures="reconfigurable|configuration_optional">
</appwidget-provider>
#
대부분의 런처는 정수값의 cell 지정을 지원함.
기본 사이즈, 위젯 최소 최대값 등을 설정할 수 있음.
#
targetCellWidth, targetCellHeight 는 Android 12 부터 사용할 수 있으며, grid cell 기준 기본 사이즈를 지정함. Android 11 이하에서는 홈스크린이 grid-based layout 을 지원하지 않으면 무시되는 값임.
minWidth, minHeight 는 dp 값으로 위젯 기본 사이즈를 지정. 만약 이 값이 cell 값과 정확히 맞지 않으면 반올림한 값에 가까운 cell count 가 적용됨.
targetCellWidth/Height 와 minWidth/Height 가 동시에 지정하는 것이 좋음.
targetCellWidth/Height 가 지원되는 경우는 이 값이 먼저 쓰이고, 그렇지 않으면 minWidth/Height 값이 차순으로 사용됨.
#
minResizeWidth, minResizeHeight 는 위젯의 절대적인 min size 를 지정함.
이 값은 위젯이 정말 지원할 수 없는 수준의 값이 지정되어야 함.
minResizeWidth > minWidth 인 경우 minResizeWidth 값은 무시됨.
horizontal resizing 이 disable 된 경우에도 이 값은 무시됨.
minResizeHeight 도 마찬가지 로직을 따름.
#
maxResizeWidth, maxResizeHeight 는 위젯의 권장되는 max size 를 지정함.
Android 12 에 도입됨.
grid cell 의 배수값이 아니라면 반올림해서 맞는 cell 값으로 할당됨.
maxResizeWidth < minWidth 인 경우 maxResizeWidth 값은 무시됨.
horizontal resizing 이 disable 된 경우에도 이 값은 무시됨.
maxResizeWidth 도 마찬가지 로직을 따름.
#
updatePeriodMillis 는 widget framework 이 얼마나 자주 AppWidgetProvider 의 onUpdate() 를 호출할지를 지정함.
그러나 이 호출주기는 정확히 보장되지 않으며, 최대한 자동갱신을 하지 않는 것이 권장됨.
1시간에 1회 초과는 배터리 절약 차원에서 비추됨.
#
initialLayout 은 widget layout 을 지정함
#
configure 는 user 가 widget 을 추가했을 때 불리는 activity 를 정의하며 widget 의 property 들을 설정함.
Android 12 부터는 이 initial config 를 skip 할 수 있음.
#
description 은 Android 12 에서 등장했으며, widget picker 에 표시될 description 을 지정함.
#
previewLayout (Android 12), previewImage(Android 11 이하).
previewLayout 은 Android 12 부터 지원하며, scale 가능한 preview 를 지정하며, 이는 기본 사이즈의 위젯에 대한 xml layout 을 지정한다.
previewImage 는 Android 11 이하에서 지원하며, 위젯이 설정된 후 어떻게 보일지의 preview 를 지정함. 제공되지 않으면 유저는 앱의 launcher icon 을 보게 됨.
previewLayout, previewImage 를 둘 다 지원하는 것이 권장됨. fallback 효과를 위해.
#
autoAdvanceViewId 는 widget host 에 의해 자동으로 다음 item 을 보여줄 widget subview 의 view ID 를 명시함.
예를 들어 StackView 를 사용하고 이 설정을 한 경우, 자동으로 다음 item 을 순환하며 보여줌
#
widgetCategory 는 위젯이 home screen(home_screen), lock screen(keyguard), 혹은 양쪽 다 노출되어야 하는지를 지정함. Android version 5.0 미만에서만 lock-screen widget 이 지원되고, 이상에서는 home_screen 만 유효함.
#
widgetFeatures 는 widget 에 의해 지원되는 기능을 명시함.
유저가 위젯 추가했을 때 기본 설정을 사용하게 하려면 configuration_optional 와 reconfigurable flag 를 설정해 줄 수 있음. 이렇게 하면 유저가 위젯을 추가했을 때 바로 configuration activity 를 호출하는 것을 피할 수 있고, 나중에 재설정 할 수 있음.
<AppWidgetProvider>
#
AppWidgetProvider 는 widget 의 broadcast 를 처리하고, widget 의 lifecycle 에 따라 update 를 하는 역할을 함.
#
AppWidgetProvider 의 manifest 정의는 아래와 같은 형태로 함.
<receiver android:name="ExampleAppWidgetProvider"
android:exported="false">
<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>
#
다른 외부 broadcast 를 받을 것이 아니라면 exported 는 false 가 되어야 함.
유일하게 꼭 받아야 하는 broadcast 는 APPWIDGET_UPDATE 이며, 이 broadcast 를 보내는 주체는 AppWidgetManager.
meta-data 에는 AppWidgetProviderInfo res 를 연결함.
#
AppWidgetProvider 는 BroadcastReceiver 를 상속하며, widget broadcast 관리를 위한 편리한 기능을 제공함.
widget 이 updated, deleted, enabled, disabled 되었는지를 event 로 받음.
#
onUpdate()
AppWidgetProviderInfo 에 정의한 updatePeriodMillis 에 지정한 시간이 되었을 때 호출됨.
updatePeriodMillis 는 위젯이 여러개일 때 첫번째 녀석의 시간주기에 따름. 예를 들어 시간주기가 2시간이고, 첫번재 위젯 추가 후 두번째 위젯을 1시간 후 추가하면, 첫번재 위젯 추가 후 2시간 후에 한번만 callback 이 불리고, 두번째 위젯의 시간주기는 무시됨. (즉 매시간 불리지 않음)
유저가 widget 을 추가했을 때도 필수 설정 할 기회를 주기 위해 불려짐. (view 설정, 표시 data loading 을 위한 job 시작 등을 할 수 있음)
configuration_optional 을 지정하지 않아 widget 생성 시 configuration activity 가 호출되는 경우에 이 함수는 불리지 않고, 이후 update 씬에서는 불림. 따라서 configuration activity 에서 first update 를 끝마칠 책임이 주어짐.
이 함수는 가장 중요한 callback 임.
AppWidgetProvider 가 BroadcastReceiver 구현체이기 때문에 해당 함수의 실행 완료가 보장되지 않음. 오래 걸리는 작업을 해야 하는 경우 WorkManager 를 사용하는 것이 권장됨. 그렇지 않으면 ANR 을 맞을 수 있음.
class ExampleAppWidgetProvider : AppWidgetProvider() {
override fun onUpdate(
context: Context,
appWidgetManager: AppWidgetManager,
appWidgetIds: IntArray
) {
// Perform this loop procedure for each widget that belongs to this provider.
appWidgetIds.forEach { appWidgetId ->
// Create an Intent to launch ExampleActivity.
val pendingIntent: PendingIntent = PendingIntent.getActivity(
/* context = */ context,
/* requestCode = */ 0,
/* intent = */ Intent(context, ExampleActivity::class.java),
/* flags = */ PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
)
// Get the layout for the 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 widget.
appWidgetManager.updateAppWidget(appWidgetId, views)
}
}
}
#
onAppWidgetOptionsChanged()
위젯이 처음 설치되거나, resize 될 때마다 불힘. 이 callback 이 불렸을 때 보여줄 contents 를 show/hide 하면 됨. getAppWidgetOptions() 를 통해 받아온 Bundle 을 통해 widget size 를 알 수 있음.
OPTION_APPWIDGET_MIN_WIDTH, OPTION_APPWIDGET_MIN_HEIGHT, OPTION_APPWIDGET_MAX_WIDTH, OPTION_APPWIDGET_MAX_HEIGHT, OPTION_APPWIDGET_SIZES (Android 12) 등의 key 로 값을 조회해 봄면 됨.
#
onDeleted(Context, int[])
widget 이 삭제되었을 때마다 불림
#
onEnabled(Context)
위젯이 Host 에 첫번째로 설치되었을 때 불림. 예를 들어 유저가 2개의 widget 을 설치하는 경우 첫번째 위젯을 설치할 때는 불리고, 두번째 위젯을 설치할 때는 불리지 않음.
#
onDisabled(Context)
마지막 위젯이 Host 에서 제거되었을 때 불림. onEnabled 에서 작업한 내용을 클리어 하기 좋은 위치
#
onReceive(Context, Intent)
callback method 들을 호출하기 전에 근원적으로 불리는 broadcaast receiver callback method. 일반적으로 이 함수를 구현할 필요가 없음. 기본적으로 알아서 parsing 해서 필요한 함수들을 불러주기 떄문.
<Widget Layout>
#
Widget Layout 들은 RemoteViews 기반으로 작동함.
RemoteViews 는 ViewStub 도 제공함.
#
Android 12 에서는 CheckBox, Switch, RadioButton 을 제공함.
사실상 이 버튼들은 stateless 이기 때문에 앱이 이 상태를 저장해야 함.
// Check the view.
remoteView.setCompoundButtonChecked(R.id.my_checkbox, true)
// Check a radio group.
remoteView.setRadioGroupChecked(R.id.my_radio_group, R.id.radio_button_2)
// Listen for check changes. The intent will have an extra with the key
// EXTRA_CHECKED that specifies the current checked state of the view.
remoteView.setOnCheckedChangeResponse(
R.id.my_checkbox,
RemoteViews.RemoteResponse.fromPendingIntent(onCheckedChangePendingIntent)
)
Android 12 에서 새로 지원하는 것들이 있기 때문에, res/layout-v31 과 그 이전버전을 위한 res/layout 두 세트를 지원하는 것이 좋음.
#
Android 12 에서는 위젯의 rounded corner 가 자동으로 적용됨.
system_app_widget_background_radius, system_app_widget_inner_radius 라는 2개의 system 값을 사용할 수 있음. 전자는 widget root container 의 rounded corner radius 값으로 사용하고, 후자는 inner view 의 rounded corner radius 값으로 사용하면 좋음.
가장 간단한 사용방법은 아래와 같이 shape 을 만들어 widget layout 의 배경으로 지정하는 것.
// res/drawable/app_widget_background.xml
<shape android:shape="rectangle">
<corners android:radius="@android:dimen/system_app_widget_background_radius">
…
</shape>
// res/drawable/app_widget_inner_view_background.xml
<shape android:shape="rectangle">
<corners android:radius="@android:dimen/system_app_widget_inner_radius">
…
</shape>
// res/layout/widget_layout.xml
<LinearLayout
android:background="@drawable/app_widget_background"
…>
<LinearLayout
android:background="@drawable/app_widget_inner_view_background"
…>
</LinearLayout>
</LinearLayout>
system_app_widget_background_radius : 28dp 이상이 될 수 없음.
system_app_widget_inner_radius : system_app_widget_background_radius 보다 8dp 적은 값. (8dp padding 과 잘 어울림)
3rd party launcher 나 제조사가 위 값들을 override 할 수 있으므로 상황마다 값이 다를 수 있음.
위젯이 @android:id/background 를 사용하지 않거나,
android:clipToOutline=true 등을 사용하지 않는다면 launcher 는 bg 를 알아서 감지하고 사각형 widget 을 16dp 까지 rounded corner 로 만들 수 있음.
이 역시 Android 12 에 도입된 것이 때문에 Style 을 통한 분기가 추천됨.
#
Android 12 에서는 다양한 기능들이 추가됨.
Android 12 에서만 지원하는 기능들은 하위 호환성을 위해 style 등을 2벌 정의하는 것이 추천됨.
Dynamic colors
device theme color 를 btn, bg 등에 적용할 수 있음.
dynamic color 를 사용하기 위해서는 root layout 에 system default theme 인 @android:style/Theme.DeviceDefault.DayNight 또는 Material 3 theme 인 Theme.Material3.DynamicColors.DayNight 을 적용해야 함.
이후 적용하고 싶은 다음 색상들을 지정해줌면 됨. ?android:attr/colorAccent, ?android:attr/colorBackground, ?android:attr/textColorPrimary, ?android:attr/textColorSecondary
Voice support
Built-in intents(BII) 라는 것이 있음. 유저의 voice command 에 의해 해당 Intent 가 발생하면 assistant 에 widget 을 보여줄 수 있음.
자세한 사항은 관련 내용을 참조.. (https://developer.android.com/develop/ui/views/appwidgets/enhance#voice)
Improve widget picker experience
Widget picker 를 scaleable 하게 할 수 있음. 기존 widget preview 가 static image 였다면 이제는 dynamic layout 가능한 xml 을 제공. 이를 통해 home screen(host)에서 어떻게 보일지를 더 잘 알 수 있게 됨.
appwidget-provider 의 android:previewLayout 에 값을 지정해줌면 됨.
일반적으로 previewLayout 과 initialLayout 을 동일하게 사용하며, 이것이 권장됨.
하위 호환성을 위해 previewLayout 과 previewImage 모두 설정하는 것이 좋음.
Description of widget
appwidget-provider 의 android:description 에 설명을 넣을 수 있음.
글자수 제한은 없지만 단말에 따라 잘려 보일 수 있기 때문에 간단하게 쓰는 것이 추천됨.
Enable smoother transitions
widget 에서 app 을 실행시킬 때 부드러운 transition 을 제공함
이를 위해 top level layout 에 @android:id/background 나 android.R.id.background 를 설정하는 것이 추천됨.
Runtime modification of RemoteViews
// Set the colors of a progress bar at runtime.
remoteView.setColorStateList(R.id.progress, "setProgressTintList", createProgressColorStateList())
// Specify exact sizes for margins.
remoteView.setViewLayoutMargin(R.id.text, RemoteViews.MARGIN_END, 8f, TypedValue.COMPLEX_UNIT_DP)
<Advanced widget>
#
Widget content update 는 무거운 작업임. 배터리 절약 차원에서 update 타이밍, 주기, 종류 등을 세심히 볼 필요가 있음.
#
Type of widget updates
Full update
AppWidgetManager.updateAppWidget(int, android.widget.RemoteViews) 는 위젯 전체를 업데이트 시킴. 가장 무거운 작업임.
Partial update
AppWidgetManager.partiallyUpdateAppWidget(int, android.widget.RemoteViews) 는 일부만 업데이트함. 기존 RemoteViews 를 새로운 RemoteViews 로 치환함.
partial update 를 사용하려면 최소한 한번 full update 가 불렸어야 함.
Collection data refresh
AppWidgetManager.notifyAppWidgetViewDataChanged 는 RemoteViewsFactory.onDataSetChanged 의 호출을 야기하여 최종적으로 collection data 를 갱신함.
#
Frequency of update
Periodically
updatePeriodMillis 값에 따라 주기적으로 update 할 수 있고, user interaction, broadcast update 등에 의해서도 update 될 수 있음.
updatePeriodMillis 조건이 충족되면 AppWidgetProvider.onUpdate 함수가 불림. 그러나 이곳에서 오래 걸리는 작업을 수행한다면 다른 방식을 생각해봐야 함. 기본적으로 AppWidgetProvider 는 BroadcastReceiver 의 구현체이기 때문에 10초 이상 thread 를 잡고 있으면 ANR 이 발생함. WorkManager 를 사용하는 것이 좋음
updatePeriodMillis 는 30분 미만으로 설정할 수 없음. periodic update 가 필요하지 않으면 0으로 설정할 수도 있음.
User interaction
app 의 activity 에서는 AppWidgetManager.updateAppWidget 을 호출할 수 있음.
notification, app widget 같은 remote interaction 에서는 PendingIntent 를 사용하고 이를 통해 시작된 Activity, Broadcast, Service 에서 어떤 처리를 하면 됨.
Broadcast
예를 들어 유저가 사진을 찍었을 때 위젯을 업데이트 하고 싶다면, addTriggerContentUri 조건을 걸어서 JobScheduler 를 실행시키는 방법이 있음.
ACTION_LOCALE_CHANGED 와 같은 event 를 받을 니즈가 있을 수도 있음.
Broadcast 의 기본 ANR 시간은 10초인데, goAsync 함수를 통해 30초까지 할당받을 수 있음.
기본적으로 broadcast 는 bg process 에서 동작함. 그래서 시스템이 바쁠 때는 broadcast 를 받는데 시간이 걸릴 수 있음. 이 경우 Intent.FLAG_RECEIVER_FOREGROUND 를 Intent 에 추가해서 fg process 로 승격(?) 시킬 수 있음.
#
Collection 사용하는 Preview 는 empty item 들을 노출하기 쉬움.
그래서 initialLayout 과 동일한 파일 대신, preview 전용 layout 을 따로 사용하는 것이 추천됨.
<Collection widgets>
#
Collection widget 은 RemoteViewsService 를 사용해서 ContentProvider 등을 통해 data 를 불러옴.
RemoteViewService 는 RemoteAdapter 가 RemoteViews 를 요청할 때 사용함.
#
ListView, GridView, StackView, AdapterViewFlipper 가 대표적인 collection view type.
Widget 에서는 Adapter 의 역할을 RemoteViewFactory 가 수행하는데, 이는 Adapter interface 를 wrapping 한 녀석. RemoteViewsFactory 가 collection item 을 RemoteViews 형태로 전달해줌.
#
https://android.googlesource.com/platform/development/+/master/samples/StackWidget 에서 사용방법을 볼 수 있음.
#
User interaction 없이 widget 이 자동으로 순서대로 다음 view 를 보여줄 수 있는데,
이는 autoAdvanceViewId 에 stack view 를 지정해주면 됨.
#
RemoteViewsService 를 manifest 에 정의할 때는 BIND_REMOTEVIEWS permission 을 지정해주어야 함.
<service android:name="MyWidgetService"
android:permission="android.permission.BIND_REMOTEVIEWS" />
#
AppWidgetProvider.onUpdate() 에서 위젯 생성 시 setRemoteAdapter() 를 호출해주어 어디서부터 data 를 가져올지 알려줘야 함.
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)
}
#
static data 가 아니면 RemoteViewsService 에 member 로 들고 있지 말자.
ContentProvider 를 통해 자료를 주고 받는 것이 좋다.
#
RemoteViewFactory 에서 구현해야 하는 가장 중요한 함수는 onCreate 와 getViewAt 이다.
시스템은 factory 를 처음 만들 때 onCreate 를 호출함. 이 때가 data source 와 연결하는 타이밍이고, widget 이 active 되었을 때 이 data source 를 이용하여 정보를 표시함.
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!") }
...
}
...
}
getViewAt 은 RemoteViews 를 return 함.
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)
}
}
#
setOnClickPendingIntent() 함수는 collection 쪽에서는 쓸 수 없음.
collection 에서는 setPendingIntentTemplate 와 setOnClickFillInIntent 조합을 사용해야 함.
보통은 바로 Activity 를 띄우기도 하지만, widget 내에서 다른 처리가 필요하다면 Broadcast 를 통해 통신할 수 있음.
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 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 ->
...
// 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 has 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)
}
}
private const val REMOTE_VIEW_COUNT: Int = 10
class StackRemoteViewsFactory(
private val context: Context,
intent: Intent
) : RemoteViewsService.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)
// 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)
...
}
}
...
}
#
data 를 계속 최신으로 유지하는 과정은 아래의 과정으로 이루어진다.
- RemoteViews.setRemoteViewsAdapter()
- AppWidgetManager.updateAppWidget()
- RemoteViewFactory.onCreate()
- AppWidgetManager.notifyAppWidgetViewDataChanged() -> RemoteViewFactory.onDataSetChanged()
- RemoteViewFactory.getCount(), getViewTypeCount(), hasStableIds(), getLoadingView()
- RemoteViewFactory.getViewAt(), getItemId()
#
getViewAt() 이 오래 걸린다면, RemoteViewsFactory.getLoadingView() 가 불리면서 loading view 가 먼저 표시된다.
#
Android 12 (API Level 31) 에서는 setRemoteAdapter(int viewId, RemoteViews.RemoteCollectionItems items) 가 추가되어, 기존과 같이 service 를 거치지 않고 collection view 를 직접 전달할 수 있다.
이를 사용할 경우 RemoteViewsFactory 에서 notifyAppWidgetViewDataChanged() 를 구현할 필요가 없다.
덧붙여 새로운 item 을 가져올 때의 latency 도 줄일 수 있다.
그러나 이 방법은 많은 Bitmaps 를 사용하여 setImageViewBitmap 함수 호출로 이어지는 경우 사용하기 어렵다.
#
Valid 한 구현인지 검증은 안 되었지만 아래와 같은 형태로 쓸 수 있는듯하다
// Get the app widget manager
AppWidgetManager appWidgetManager = AppWidgetManager.getInstance(context);
// Get the app widget IDs for your app widget
int[] appWidgetIds = appWidgetManager.getAppWidgetIds(new ComponentName(context, YourAppWidgetProvider.class));
// Iterate over the app widget IDs
for (int appWidgetId : appWidgetIds) {
// Create a RemoteViews object for your app widget
RemoteViews remoteViews = new RemoteViews(context.getPackageName(), R.layout.your_app_widget_layout);
// Set the remote adapter for the specified viewId
remoteViews.setRemoteAdapter(R.id.your_collection_view, items);
// Update the app widget
appWidgetManager.updateAppWidget(appWidgetId, remoteViews);
}
#
만약 collection 이 동일한 layout 을 사용하지 않는다면, setViewTypeCount 를 사용할 수 있다.
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()
)
<Size your widget>
#
따로 정리하지 않는다.
<Configuration activity>
#
App Widget host 에 의해 위젯 최초 생성시 자동으로 뜨거나 혹은 나중에 띄울 수 있음. (configuration options 에 따름)
#
Config Activity 정의할 때는 intent-filter action 으로 android.appwidget.action.APPWIDGET_CONFIGURATION 을 설정해주고,
AppWidgetProviderInfo 의 android:configure 에 full name 으로 명시해주어야 함. (외부에서 호출하기 때문)
#
configuration activity 는 result 를 return 해야 하는데, 여기는 config activity 를 launch 할 때 EXTRA_APPWIDGET_ID 로 전달받은 app widget id 를 넣어주어야 함.
RESULT_CANCELED 를 return 하는 경우 app widget host 는 widget 을 추가하지 않음.
updateAppWidget 을 통해 app widget view 를 update 한 경우 RESULT_OK 를 return 하면 성공적으로 widget 이 추가됨.
#
System 은 config activity 가 launch 된 경우 ACTION_APPWIDGET_UPDATE broadcast 를 날리지 않음.
이 말은 위젯이 붙었을 때 onUpdate 가 불리지 않을 것이라는 의미.
widget 을 update 하는 것은 config activity 가 해야 하며, AppWidgetManager 를 통해서 수행함.
#
AppWidgetProviderInfo 에 android:widgetFeatures 를 reconfigurable 을 정의하면
long press 로 widget 이 edit mode 로 들어갔을 때, reconfig button 이 생기고 이것을 누르면 진입할 수 있음.
이 설정은 Android 9 (API Level 8) 에 도입되었지만, Android 12 까지 잘 지원되지 않음.
#
user 가 initial config 를 스킵하도록 하려면, android:widgetFeatures 에 configuration_optional | reconfigurable 을 지정해주면, 최초 위젯이 붙을 때는 설정 화면에 들어가지 않고 나중에 진입할 수 있음.
이 기능은 Android 12 부터 지원됨.
#
Android 8 (API Level 26) 이상부터는 pinned shortcut 이 지원되고, 이와 비슷한 개념으로 pinned widget 도 제공됨.
이는 앱에서 바로 home screen 에 widget 을 추가하는 것을 말함.
예를 들면 날씨 앱의 앱 내에서 새로운 도시를 관심 도시로 추가하면, 유저에게 해당 도시의 날씨 위젯을 추가할지 물어보고 추가할 수 있음.
val appWidgetManager = AppWidgetManager.getInstance(context)
val myProvider = ComponentName(context, ExampleAppWidgetProvider::class.java)
if (appWidgetManager.isRequestPinAppWidgetSupported()) {
// Create the PendingIntent object only if your app needs to be notified when the user chooses to pin the widget.
// Note that if the pinning operation fails, your app isn't notified.
// This callback receives the ID of the newly pinned widget (EXTRA_APPWIDGET_ID).
val successCallback = PendingIntent.getBroadcast(
/* context = */ context,
/* requestCode = */ 0,
/* intent = */ Intent(...),
/* flags = */ PendingIntent.FLAG_UPDATE_CURRENT)
appWidgetManager.requestPinAppWidget(myProvider, null, successCallback)
}
끝
'프로그래밍 놀이터 > 안드로이드, Java' 카테고리의 다른 글
[android] What is MVI & Concept of Model - MVI (Model View Intent) Architecture (0) | 2023.11.29 |
---|---|
[android] ConnectionService 에 대해 알아보자 (0) | 2023.08.17 |
[android] Compose Side-effects (0) | 2023.08.16 |
[android] compose 의 stability (0) | 2023.08.15 |
[android] BlockedNumbers (수신차단) 에 대해 알아보자. (0) | 2023.08.14 |
댓글