人升开发日志#11 | 12/06 Android 桌面小部件

制作小部件的途中也遇到了各种坑orz,而且网络上的各种文章都不详细,摸索了一两天才写完。

准备

首先,直接使用AS新建一个APP WIDGET,进行各种配置。

之后如果想修改小部件的最低宽高可以参考这个表:

关键代码

LifeUpWidget.kt

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
    override fun onUpdate(context: Context, appWidgetManager: AppWidgetManager, appWidgetIds: IntArray) {
// There may be multiple widgets active, so update all of them
for (appWidgetId in appWidgetIds) {
updateAppWidget(context, appWidgetManager, appWidgetId)
AppWidgetManager.getInstance(context).notifyAppWidgetViewDataChanged(appWidgetId, R.id.widget_list)
}
}

override fun onEnabled(context: Context) {
// Enter relevant functionality for when the first widget is created
}

override fun onDisabled(context: Context) {
// Enter relevant functionality for when the last widget is disabled
}

override fun onReceive(context: Context, intent: Intent) {
super.onReceive(context, intent)

if (intent.hasExtra(WIDGET_IDS_KEY)) {
val ids = intent.extras.getIntArray(WIDGET_IDS_KEY)
this.onUpdate(context, AppWidgetManager.getInstance(context), ids)

if (intent.getBooleanExtra("isShowToast", false))
ToastUtils.showShortToast("成功刷新")
} else if (intent.action == FINISH_TASK) {
val extras = intent.extras
if (extras != null) {

if (extras.getBoolean("canBeFinish", false)) {
val taskId = extras.getLong("taskId")
val teamId = extras.getLong("teamId")
val item = todoService.getATodoItem(taskId)

if (teamId == -1L) {
todoService.finishTodoItem(taskId)
ToastUtils.showShortToast("成功完成事项")

if (item?.taskFrequency != 0)
todoService.repeatTask(taskId)
} else {
val activityVO = ActivityVO()
item?.let {
teamNetworkImpl.finishTeamTask(it, activityVO)
ToastUtils.showShortToast("成功完成事项")
}
}

} else {
ToastUtils.showShortToast("尚未到开始时间")
}

WidgetUtils.updateWidgets(context)
/* // Notify the widget that the list view needs to be updated.
val mgr = AppWidgetManager.getInstance(context)
val cn = ComponentName(context, LifeUpWidget::class.java)
mgr.notifyAppWidgetViewDataChanged(mgr.getAppWidgetIds(cn),
R.id.widget_list)*/
}
}
}

companion object {

const val WIDGET_IDS_KEY = "lifeupwidgetidskey"
const val FINISH_TASK = "net.sarasarasa.lifeup.action.FINISH_TASK"
private val todoService = TodoServiceImpl()

internal fun updateAppWidget(context: Context, appWidgetManager: AppWidgetManager,
appWidgetId: Int) {


// Construct the RemoteViews object
val views = RemoteViews(context.packageName, R.layout.life_up_widget)
views.setTextViewText(R.id.appwidget_text, "今日事项 0/0")

val intent = Intent(context, LifeUpRemoteViewsService::class.java)
intent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetId)

views.setRemoteAdapter(R.id.widget_list, intent)
views.setEmptyView(R.id.widget_list, R.id.tv_empty)

val finishTaskIntent = Intent(FINISH_TASK)
finishTaskIntent.setClass(context, LifeUpWidget::class.java)
val pendingIntent = PendingIntent.getBroadcast(context, 200, finishTaskIntent, PendingIntent.FLAG_UPDATE_CURRENT)
views.setPendingIntentTemplate(R.id.widget_list, pendingIntent)

val startActivityIntent = Intent(context, MainActivity::class.java)
val startActivityPendingIntent = PendingIntent.getActivity(context, 0, startActivityIntent, PendingIntent.FLAG_UPDATE_CURRENT)
views.setOnClickPendingIntent(R.id.iv_home, startActivityPendingIntent)

val addItemIntent = Intent(context, AddToDoItemActivity::class.java)
val addItemPendingIntent = PendingIntent.getActivity(context, 0, addItemIntent, PendingIntent.FLAG_UPDATE_CURRENT)
views.setOnClickPendingIntent(R.id.iv_add, addItemPendingIntent)

val man = AppWidgetManager.getInstance(context)

val ids = man.getAppWidgetIds(
ComponentName(context, LifeUpWidget::class.java))
val refreshIntent = Intent()
refreshIntent.action = AppWidgetManager.ACTION_APPWIDGET_UPDATE
refreshIntent.putExtra(LifeUpWidget.WIDGET_IDS_KEY, ids)
refreshIntent.putExtra("isShowToast", true)
val refreshPendingIntent = PendingIntent.getBroadcast(context, 199, refreshIntent, PendingIntent.FLAG_UPDATE_CURRENT)
views.setOnClickPendingIntent(R.id.iv_refresh, refreshPendingIntent)

val finishCnt = todoService.getTodayFinishCount()
val taskCnt = todoService.getTodayTaskCount()
views.setTextViewText(R.id.appwidget_text, "今日事项 ${finishCnt}/${taskCnt}")


// Instruct the widget manager to update the widget
appWidgetManager.updateAppWidget(appWidgetId, views)
}
}

LifeUpRemoteViewsService.Kt

1
2
3
4
5
6
7
8
9
10
11
package net.sarasarasa.lifeup.service

import android.content.Intent
import android.widget.RemoteViewsService

class LifeUpRemoteViewsService : RemoteViewsService() {
override fun onGetViewFactory(intent: Intent?): RemoteViewsFactory {
return LifeUpRemoteViewsFactory(this.applicationContext, intent)
}

}

LifeUpRemoteViewsFactory.Kt

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
package net.sarasarasa.lifeup.service

import android.content.Context
import android.content.Intent
import android.os.Bundle
import android.view.View
import android.widget.RemoteViews
import android.widget.RemoteViewsService
import net.sarasarasa.lifeup.R
import net.sarasarasa.lifeup.converter.TodoItemConverter
import net.sarasarasa.lifeup.fragment.LifeUpWidget
import net.sarasarasa.lifeup.fragment.LifeUpWidget.Companion.FINISH_TASK
import net.sarasarasa.lifeup.models.TaskModel
import net.sarasarasa.lifeup.service.impl.TodoServiceImpl
import java.text.SimpleDateFormat
import java.util.*


class LifeUpRemoteViewsFactory(context: Context, intent: Intent?) : RemoteViewsService.RemoteViewsFactory {

private val mList = ArrayList<TaskModel>()
private val mContext = context

private val todoService = TodoServiceImpl()

override fun onCreate() {
mList.clear()
mList.addAll(todoService.getUncompletedTodoList())
}

override fun getLoadingView(): RemoteViews? {
return null
}

override fun getItemId(position: Int): Long {
return position.toLong()
}

override fun onDataSetChanged() {
mList.clear()
mList.addAll(todoService.getUncompletedTodoList())
}

override fun hasStableIds(): Boolean {
return true
}

override fun getViewAt(position: Int): RemoteViews? {
if (position < 0 || position >= mList.size)
return null

val taskModel = mList[position]
val rv = RemoteViews(mContext.packageName, R.layout.item_widget_list)
var canBeFinish = true

rv.setTextViewText(R.id.tv_title, taskModel.content)
rv.setTextViewText(R.id.tv_exp, taskModel.expReward.toString() + "经验值")


val cal = Calendar.getInstance()
val simpleDateFormat = SimpleDateFormat("yyyy/MM/dd", Locale.getDefault())
val dateAndTimeFormat = SimpleDateFormat("yyyy/MM/dd HH:mm", Locale.getDefault())
val isTeamTask = when (taskModel.teamId) {
-1L -> false
else -> true
}

if (cal.timeInMillis < taskModel.startTime.time) {
//还没到开始时间的时候
rv.setViewVisibility(R.id.tv_time, View.VISIBLE)
rv.setViewVisibility(R.id.iv_time, View.VISIBLE)
rv.setTextViewText(R.id.tv_time, dateAndTimeFormat.format(taskModel.startTime) + "开始 #" + TodoItemConverter.iFrequencyToTitleString(isTeamTask, taskModel.taskFrequency))
canBeFinish = false
} else {
//设置频次标识的颜色

if (taskModel.taskExpireTime != null) {
rv.setViewVisibility(R.id.tv_time, View.VISIBLE)
rv.setViewVisibility(R.id.iv_time, View.VISIBLE)

if (taskModel.teamId != -1L) {
rv.setTextViewText(R.id.tv_time, dateAndTimeFormat.format(taskModel.endTime) + "期限 #" + TodoItemConverter.iFrequencyToTitleString(isTeamTask, taskModel.taskFrequency))
} else rv.setTextViewText(R.id.tv_time, simpleDateFormat.format(taskModel.taskExpireTime) + "期限 #" + TodoItemConverter.iFrequencyToTitleString(isTeamTask, taskModel.taskFrequency))
} else {
rv.setViewVisibility(R.id.tv_time, View.INVISIBLE)
rv.setViewVisibility(R.id.iv_time, View.INVISIBLE)
}
}

/* val extras = Bundle()
taskModel.id?.let { extras.putLong("taskId", it) }
val finishTaskIntent = Intent()
finishTaskIntent.action = LifeUpWidget.FINISH_TASK
finishTaskIntent.putExtras(extras)
rv.setOnClickFillInIntent(R.id.btn,finishTaskIntent)*/
val extras = Bundle()
taskModel.id?.let { extras.putLong("taskId", it) }
extras.putLong("teamId", taskModel.teamId)
extras.putBoolean("canBeFinish", canBeFinish)
val fillInIntent = Intent(FINISH_TASK)
fillInIntent.putExtra("NUMBER", position)
fillInIntent.setClass(mContext, LifeUpWidget::class.java)
fillInIntent.putExtras(extras)
rv.setOnClickFillInIntent(R.id.btn, fillInIntent)




return rv
}

override fun getCount(): Int {
return mList.size
}

override fun getViewTypeCount(): Int {
return 2
}

override fun onDestroy() {
mList.clear()
}


}

AndroidManifest.xml

在application中加入:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<receiver android:name=".fragment.LifeUpWidget">
<intent-filter>
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
<action android:name="net.sarasarasa.lifeup.action.FINISH_TASK" />
</intent-filter>

<meta-data
android:name="android.appwidget.provider"
android:resource="@xml/life_up_widget_info" />
</receiver>

<service
android:name=".service.LifeUpRemoteViewsService"
android:exported="false"
android:permission="android.permission.BIND_REMOTEVIEWS"></service>

坑点

只支持一部分View

A RemoteViews object (and, consequently, an App Widget) can support the following layout classes:

  • FrameLayout
  • LinearLayout
  • RelativeLayout
  • GridLayout

And the following widget classes:

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

如果使用其他VIew的话,直接显示不出来。

WidgetProvier中设置ListView

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
const val FINISH_TASK = "net.sarasarasa.lifeup.action.FINISH_TASK"
//要在AndroidManifest中加上 <action android:name="net.sarasarasa.lifeup.action.FINISH_TASK" />

val views = RemoteViews(context.packageName, R.layout.life_up_widget)

val intent = Intent(context, LifeUpRemoteViewsService::class.java)
intent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetId)

views.setRemoteAdapter(R.id.widget_list, intent)

//这里的EmptyView指的是R.layout.life_up_widget的一个view,在ListView为空的时候才显示出来
//并不是单独的一个View
views.setEmptyView(R.id.widget_list, R.id.tv_empty)

//如果你的ListView要发送Intent的话,这里要设置IntentTemplate
val finishTaskIntent = Intent(FINISH_TASK)
finishTaskIntent.setClass(context, LifeUpWidget::class.java)
val pendingIntent = PendingIntent.getBroadcast(context, 200, finishTaskIntent, PendingIntent.FLAG_UPDATE_CURRENT)
views.setPendingIntentTemplate(R.id.widget_list, pendingIntent)

然后在LifeUpRemoteViewsFactory.Kt的getViewAt方法中

1
2
3
4
5
6
7
8
9
val extras = Bundle()
taskModel.id?.let { extras.putLong("taskId", it) }
extras.putLong("teamId", taskModel.teamId)
extras.putBoolean("canBeFinish", canBeFinish)
val fillInIntent = Intent(FINISH_TASK)
fillInIntent.putExtra("NUMBER", position)
fillInIntent.setClass(mContext, LifeUpWidget::class.java)
fillInIntent.putExtras(extras)
rv.setOnClickFillInIntent(R.id.btn, fillInIntent)

然后再在WidgetProvider的OnReceive中处理这个广播

1
2
3
4
5
6
7
override fun onReceive(context: Context, intent: Intent) {
super.onReceive(context, intent)

if (intent.action == FINISH_TASK) {
//Do something...
}
}

刷新ListView的数据

  1. 首先,重写onDataSetChanged方法

    1
    2
    3
    4
    override fun onDataSetChanged() {
    mList.clear()
    mList.addAll(todoService.getUncompletedTodoList())
    }
  2. 然后在WidgetProvier恰当的地方调用notifyAppWidgetViewDataChanged方法

    1
    AppWidgetManager.getInstance(context).notifyAppWidgetViewDataChanged(appWidgetId, R.id.widget_list)

##在应用中通知刷新

这是一个StackOverFlow中有人推荐的方法:

  1. 在 WidgetProvier 中:
1
2
3
4
5
6
7
8
9
10
11
12
13
const val WIDGET_IDS_KEY = "lifeupwidgetidskey"

override fun onReceive(context: Context, intent: Intent) {
super.onReceive(context, intent)

if (intent.hasExtra(WIDGET_IDS_KEY)) {
val ids = intent.extras.getIntArray(WIDGET_IDS_KEY)
this.onUpdate(context, AppWidgetManager.getInstance(context), ids)

if (intent.getBooleanExtra("isShowToast", false))
ToastUtils.showShortToast("成功刷新")
}
}
  1. 写一个工具类:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
package net.sarasarasa.lifeup.utils

import android.appwidget.AppWidgetManager
import android.content.ComponentName
import android.content.Context
import android.content.Intent
import net.sarasarasa.lifeup.fragment.LifeUpWidget


class WidgetUtils {
companion object {
fun updateWidgets(context: Context) {
val man = AppWidgetManager.getInstance(context)
val ids = man.getAppWidgetIds(
ComponentName(context, LifeUpWidget::class.java))
val updateIntent = Intent()
updateIntent.action = AppWidgetManager.ACTION_APPWIDGET_UPDATE
updateIntent.putExtra(LifeUpWidget.WIDGET_IDS_KEY, ids)
// updateIntent.putExtra(MyWidgetProvider.WIDGET_DATA_KEY, data)
context.sendBroadcast(updateIntent)
}
}
}

人升开发日志#11 | 12/06 Android 桌面小部件

http://sarasarasa.net/post/f5d060f4.html

作者

AyagiKei

发布于

2018-12-06

更新于

2021-08-10

许可协议

评论