COCSOS - guitar, computer, etc
191205 App Widget 정리 - 1 본문
안드로이드의 App Widget이 팁이 부족해서 따로 간단하게 남기기 위해 작성.
제 글이 당신의 숙면에 도움되길.
#App Widget?
원래 Widget은 안드로이드에서 기본 뷰를 뜻하는 말이다.
ex) androidx.constraintlayout.widget.ConstraintLayout 처럼
바탕화면에 설치하는 우리가 말하는 위젯은 App Widget이라고 함.
그런데 위젯은 AppWidgetManager가 관리하기 때문에 복잡한 동작은 힘들다.
따라서... RecyclerView 안됨, ConstraintLayout 안됨
또한 <include /> 는 가능해도 참조하는 xml 내부에 ConstraintLayout이 있어도 동작하지 않는 듯.
DataBinding 사용 불가한 듯.
왜냐면 View 객체를 만드는게 아니고, (setContentView() 혹은 layoutInflator.inflate() 방식이 아님)
RemoteViews(context.packageName, R.layout.app_widget_small)
를 이용한... 해괴한 방식이기 때문.
참고로 Custom Notification Layout을 만들때도 RemoteViews를 사용한다.
#AndroidManifest.xml에 추가할 것
필수적으로 1개, 기능에 따라 더 필요하다.
나는 위젯, 위젯 내부 어댑터, 설정 화면 총 3블럭이 필요했다.
<!--반드시 필요한 앱 위젯 프로바이더-->
<receiver android:name=".widget.SmallWidgetProvider"
android:label="@string/small_widget">
<intent-filter>
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
</intent-filter>
<meta-data android:name="android.appwidget.provider"
android:resource="@xml/small_app_widget_info" />
</receiver>
<!-- 앱 내에서 ListView/Stack View/... 가 필요하다면 추가-->
<service android:name=".widget.WidgetRemoteViewService"
android:permission="android.permission.BIND_REMOTEVIEWS"
android:exported="false"/>
<!-- 앱 위젯의 Configuration Activity가 필요하다면 추가 -->
<activity android:name=".ui.activity.WidgetConfigureActivity"
android:screenOrientation="portrait"
android:theme="@style/Theme.Transparent">
<intent-filter>
<action android:name="android.appwidget.action.APPWIDGET_CONFIGURE"/>
</intent-filter>
</activity>
나는 widget 폴더에 SmallWidgetProvider라는 이름으로 AppWidgetProvider를 추가함.
App Widget이 2개 이상일때는 label 태그에 넣어진 대로 이름이 나온다.
#xml폴더에 추가할 파일
manifest에 small_app_widget_info를 resource에 넣었는데,
그 파일은 xml(res 폴더 내에 생성함) 폴더 내부에 small_app_widget_info.xml이름으로 추가함.
<?xml version="1.0" encoding="utf-8"?>
<appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android"
android:initialKeyguardLayout="@layout/app_widget_large"
android:initialLayout="@layout/app_widget_large"
android:minWidth="110dp"
android:minHeight="110dp"
android:minResizeHeight="110dp"
android:minResizeWidth="110dp"
android:configure="com.melog.welllog.ui.activity.WidgetConfigureActivity"
android:previewImage="@drawable/app_widget_preview_small"
android:resizeMode="vertical|horizontal"
android:updatePeriodMillis="1800000"
android:widgetCategory="keyguard|home_screen"/>
!!resizeMode에 설정된 대로 minWidth나 minResizeHeight가 되어있지 않으면 App Widget이 위젯 리스트에 뜨지 않는 현상때문에 똥을 한바가지 써야했다.
*updatePeriodMillis는 갱신 시간인데 최소 30분이며, 0으로 넣으면 갱신하지 않는다고 한다.
나는 다른 App Widget을 여러개 만들기 위해서 xml 폴더 내부에 다른 이름(Large_어쩌고)으로 또 만들었다.
WidgetProvider도 하나 더 만들고, manifest에 새로 만든 prover를 추가해주었다.
WidgetRemoteViewsService와 ConfigurationActivity는 하나로 만들 수 있었다.
#AppWidgetProvider?
App Widget을 생성하는 BroadcastReceiver를 상속한 클래스.
BroadcastReceiver를 상속했기때문에 onReceive() 함수를 override해야하며 원래의 BroadcastReceiver의 역할을 잘 수행해낸다.
다음은 앱 내부에서 위젯을 강제로 갱신할 때 사용한 코드.
val manager = AppWidgetManager.getInstance(context)
val smallAppWidgetIds = manager.getAppWidgetIds(ComponentName(context, SmallWidgetProvider::class.java))
if(smallAppWidgetIds.isNotEmpty()) {
val update = Intent(context, SmallWidgetProvider::class.java)
update.action = SmallWidgetProvider.REFRESH_ALL
update.putExtra(AppWidgetManager.EXTRA_APPWIDGET_IDS, smallAppWidgetIds)
context.sendBroadcast(update)
}
REFRESH_ALL이라는 문자열을 action에 넣어 onReceive에서 when으로 걸러 사용함.
또 하는 일은 위젯이 생성되거나, 삭제될때, 업데이트 될 때 를 처리해준다.
RemoteViews는 static(Kotlin에서는 companion object) updateAppWidget() 함수 내부에서 생성해주면 되고
그렇게 만든 RemoteViews를 넣어 updateAppWidget을 넣어주는 것이 가장 중요한 듯.
val remoteViews = RemoteViews(context.packageName, R.layout.app_widget_small)
...
val openIntent = Intent(context, StartActivity::class.java)
val pendingIntent = PendingIntent.getActivity(context, 0, openIntent, 0)
remoteViews.setOnClickPendingIntent(R.id.tv_date, pendingIntent)
appWidgetManager.updateAppWidget(appWidgetId, remoteViews)
appWidgetManager.updateAppWidget(appWidgetId, remoteViews)
#터치 이벤트
또한 위젯을 터치할때는 그 리스너를 이곳에서 터치 이벤트를 달아준다.
그러나 RemoteViews는 우리가 만든 앱이 관리하지 않고 AppWidgetManager가 관리하기 때문에
setOnClickListener가 아닌
setOnClickPendingIntent를 설정해주어야한다.
실행하라는 명령을 담은 intent를
담은 pendingIntent를
뷰에 설정한다.
무엇?
이것은 일반적인 TextView, Button,ImageView의 경우이며
간단하지 않은 동작... 리스트뷰 내부의 아이템을 클릭했을 때는
//listView의 클릭 템플릿 설정
val itemIntent = Intent(context, WidgetInputActivity::class.java)
itemIntent.action = CLICK_ACTION
itemIntent.putExtra(EXTRA_APPWIDGET_ID, appWidgetId)
intent.data = Uri.parse(intent.toUri(Intent.URI_INTENT_SCHEME))
val itemPendingIntent = PendingIntent.getActivity(context, 0, itemIntent, 0)
remoteViews.setPendingIntentTemplate(R.id.lv_item, itemPendingIntent)
리스트 뷰(R.id.lv_item)에는 setPendingIntentTemplate를 담는다.
템플릿! 즉 각 아이템이 클릭되어도 템플릿을 따라서 실행된다는 것.
(참고로 나는 WidgetInputActivity를 실행하는 템플릿)
각 아이템을 구분하는 동작은 Provider에서 하지 않고 WidgetRemoteViewService내부에서 지정해줘야한다.
그런데 Service역시 지정해줘야하는데
val intent = Intent(context, WidgetRemoteViewService::class.java).apply {
putExtra(EXTRA_APPWIDGET_ID, appWidgetId)
// extra가 무시되지 않도록 조치
data = Uri.parse(toUri(Intent.URI_INTENT_SCHEME))
}
remoteViews.setRemoteAdapter(R.id.lv_item, intent)
그니까 ListView에는 remoteViews에서 WidgetRemoteViewService 내부에 구현된 어댑터를 참조해주기 위해setRemoteAdapter(R.id.lv_item,intent)로 한 번
템플릿을 설정하기위해 setPendingIntentTemplate(R.id.lv_item,pendingIntent)로 또 한 번 설정해줘야 한다.
개복잡ㅎ
#뷰 조작하기
짜증나는 것은, 일반 View가 아닌 RemoteViews인 만큼 내가 원하는대로 조작할 수 없다는 것...
val remoteViews = RemoteViews(pkgName,R.id.레이아웃_위젯 )
로 만든 remoteViews 내부에 있는 텍스트 뷰를 조작하려면
remoteViews.setTextViewText(R.id.레이아웃_내부_textview_id,"텍스트으")로
remoteViews가 제공하는 함수를 거쳐야한다.
즉 제공하는 것 밖에 못쓴다는 이야기.
setTextColor,
setViewVisibility,
setImageViewBitmap,
등
여기서 제공되지 않는 꼼수는 https://stackoverflow.com/questions/13403306/android-remoteviews-custom-font
Android remoteViews custom font
How I can set custom font in my widget ? remoteViews.setTextViewText(R.id.text1,""+ days); Can someone show me an example of how to set the font for this?
stackoverflow.com
!!백그라운드 투명도를 주기 위해서 똥을 두 세 바가지 싸야했다.
1. LinearLayout의 background를 없애고 RelativeLayout으로 만든 뒤,
imageView를 w:matchparent, h = matchParent로 만든다.
2. setInt함수를 이용하여 조작한다.
remoteViews.setInt(R.id.iv_background, "setColorFilter", Color.parseColor(backgroundColor))
remoteViews.setInt(R.id.iv_background, "setImageAlpha", transparency.toInt())
*"setAlpha" 함수는 버전에 따라 안되기도 했고
*background엔 alpha가 안먹혀서 ImageView의 src에 drawable을 넣어줘야했다.ㅠ
AppWidgetProvider내부에서 onUpdate() 등 을 구현해준다.
나의 경우 하루에 한 번 업데이트를 해야했지만 그 시각이 자정이어야했기 때문에
onEnabled() 함수 내부에서
매일 알람을 울리기위해 현재 구현중인 Provider를 불러야했다
val manager = getInstance(context)
val appWidgetId = manager.getAppWidgetIds(ComponentName(context, LargeWidgetProvider::class.java)).last()
//Log.d(TAG, "onEnabled: set alarm for $appWidgetId")
val i = Intent(context, LargeWidgetProvider::class.java)
i.action = REFRESH_NEW_DAY
i.putExtra(EXTRA_APPWIDGET_ID, appWidgetId)
val pendingIntent = PendingIntent.getBroadcast(context, appWidgetId, i, PendingIntent.FLAG_UPDATE_CURRENT)
val midnight = Calendar.getInstance()
midnight.set(Calendar.HOUR_OF_DAY, 0)
midnight.set(Calendar.MINUTE, 0)
midnight.set(Calendar.SECOND, 0)
midnight.set(Calendar.MILLISECOND, 0)
val alarmManager = context.getSystemService(Context.ALARM_SERVICE) as AlarmManager
alarmManager.setInexactRepeating(AlarmManager.RTC, midnight.timeInMillis,
24 * 60 * 60 * 1000, pendingIntent)
*알람을 지울때는 위젯이 삭제될 때는 onDelete()에서 같은 채널을 가진 pendingIntent를 만들어서 지워야한다.
pendingIntent를 외부변수를 Provider에 저장해두지 않는편이 좋을 것이다.
언제 Provider가 지워질 지 모르기 때문.
ex) alarmManager?.cancel(pendingIntent)
정리하면 내 Provider 구조는 이렇다
내 Provider:AppWidgetProvider(){
fun onReceive(){
when(action){
UPDATE->mgr.notifyAppWidgetViewDataChanged(it, R.id.lv_item)
NEW_DAY->updateAppWidget()
}
}
fun onUpdate() {
for (appWidgetId in appWidgetIds) {
updateAppWidget(context, appWidgetManager, appWidgetId)
}
}
fun onEnabled(){자정 알람 추가}
fun onDisabled(){자정 알람 삭제}
companion object {
fun updateAppWidget() {
val views = createRemoteViews(context, appWidgetId)
appWidgetManager.updateAppWidget(appWidgetId, views)
}
}
}
이제 남은건 WidgetRemoteViewService인가
'Computer > android' 카테고리의 다른 글
220405 애니메이션 훑기 (0) | 2022.04.05 |
---|---|
181004 RxJava2+Retrofit2+lambda정리 (0) | 2018.10.05 |
181002 Retrofit2 설명, 차이와 사용법 (0) | 2018.10.04 |