实现Android Widget 桌面小部件
Android实现桌面小部件
可以切换标题显示不同列表,列表中可以显示不同类型布局以及显示和隐藏操作菜单。
图4显示不同类型的item显示的效果一样。
想体验效果可以下载apk运行体验:app-release.apk 提取码:uduw
运行程序后找到widget,拖到桌面就可以体验了
实现方法
1.创建一个布局文件file_app_widget.xml用于显示整个widget的布局,代码如下:
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@drawable/file_app_widget_bg"
android:padding="@dimen/widget_margin"
android:theme="@style/ThemeOverlay.Nxfilemanager.AppWidgetContainer">
<LinearLayout
android:id="@+id/widget_title_lay"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:paddingTop="4dp"
android:paddingBottom="4dp"
android:paddingRight="16dp"
android:paddingLeft="16dp">
<TextView
android:id="@+id/widget_title1"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:paddingTop="10dp"
android:paddingBottom="10dp"
android:gravity="center"
android:text="@string/widget_title1"
android:textColor="@color/widget_font_black"
android:textSize="12sp" />
<TextView
android:id="@+id/widget_title2"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:paddingTop="10dp"
android:paddingBottom="10dp"
android:gravity="center"
android:text="@string/widget_title2"
android:textColor="@color/widget_font_black"
android:textSize="12sp" />
<TextView
android:id="@+id/widget_title3"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:paddingTop="10dp"
android:paddingBottom="10dp"
android:gravity="center"
android:text="@string/widget_title3"
android:textColor="@color/widget_font_black"
android:textSize="12sp" />
<TextView
android:id="@+id/widget_title4"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:paddingTop="10dp"
android:paddingBottom="10dp"
android:gravity="center"
android:text="@string/widget_title4"
android:textColor="@color/widget_font_black"
android:textSize="12sp" />
</LinearLayout>
<ListView
android:id="@+id/widget_list"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_below="@id/widget_title_lay"
android:paddingLeft="18dp"
android:paddingRight="18dp" />
</RelativeLayout>
布局中有4个TextView用于显示4个标题,一个ListView用于显示列表。
2.创建一个item布局文件,用于显示列表里的item信息,在这就不插入代码了。
3.创建一个资源文件file_app_widget_info.xml,放在res/xml目录下,file_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/file_app_widget"
android:initialLayout="@layout/file_app_widget"
android:minWidth="250dp"
android:minHeight="250dp"
android:previewImage="@drawable/example_appwidget_preview"
android:resizeMode="horizontal|vertical"
android:updatePeriodMillis="86400000"
android:widgetCategory="home_screen"></appwidget-provider>
属性含义:
initialLayout表示要初始化显示的布局信息;minWidth和minHeight表示要显示的最小占屏幕的,250dp表示占桌面的44格子,计算公式为 (70 * n - 30),其中n表示占屏幕的几格;previewImage表示在查找预览widget时显示的图;resizeMode表示widget小部件是否可改变长宽的大小。
4.创建FileAppWidget.kt文件用于创建和更新widget小部件,代码如下:
class FileAppWidget : AppWidgetProvider() {
override fun onUpdate(
context: Context,
appWidgetManager: AppWidgetManager,
appWidgetIds: IntArray
) {
Logger.t(TAG).v("onUpdate")
// There may be multiple widgets active, so update all of them
for (appWidgetId in appWidgetIds) {
updateAppWidget(context, appWidgetManager, appWidgetId)
}
}
override fun onAppWidgetOptionsChanged(
context: Context?,
appWidgetManager: AppWidgetManager?,
appWidgetId: Int,
newOptions: Bundle?
) {
super.onAppWidgetOptionsChanged(context, appWidgetManager, appWidgetId, newOptions)
Logger.t(TAG).v("onAppWidgetOptionsChanged")
}
override fun onDeleted(context: Context?, appWidgetIds: IntArray?) {
super.onDeleted(context, appWidgetIds)
Logger.t(TAG).v("onDeleted")
}
override fun onRestored(context: Context?, oldWidgetIds: IntArray?, newWidgetIds: IntArray?) {
super.onRestored(context, oldWidgetIds, newWidgetIds)
Logger.t(TAG).v("onRestored")
}
override fun onEnabled(context: Context) {
// Enter relevant functionality for when the first widget is created
Logger.t(TAG).v("onEnabled")
}
override fun onDisabled(context: Context) {
// Enter relevant functionality for when the last widget is disabled
Logger.t(TAG).v("onDisabled")
}
override fun onReceive(context: Context?, intent: Intent?) {
Logger.t(TAG).v("onReceive")
intent?.also {
val action = intent.action
action?.also {
when (action) {
WIDGET_COLLECTION_TITLE_ACTION -> {//标题
...
}
WIDGET_COLLECTION_VIEW_ACTION -> {//列表item
...
}
}
}
}
super.onReceive(context, intent)
}
companion object {
val TAG = "FileAppWidget-"
const val WIDGET_COLLECTION_TITLE_ACTION =
"WIDGET_COLLECTION_TITLE_ACTION"
const val WIDGET_COLLECTION_TITLE_EXTRA =
"WIDGET_COLLECTION_TITLE_EXTRA"
const val WIDGET_COLLECTION_VIEW_ACTION =
"WIDGET_COLLECTION_VIEW_ACTION"
const val WIDGET_COLLECTION_VIEW_EXTRA =
"WIDGET_COLLECTION_VIEW_EXTRA"
const val WIDGET_COLLECTION_VIEW_EXTRA_POSITION =
"WIDGET_COLLECTION_VIEW_EXTRA_POSITION"
const val WIDGET_COLLECTION_VIEW_EXTRA_PATH =
"WIDGET_COLLECTION_VIEW_EXTRA_PATH"
}
}
...
internal fun refreshWidget(context: Context, remoteViews: RemoteViews, refreshList: Boolean) {
val appWidgetManager = AppWidgetManager.getInstance(context)
val componentName = ComponentName(context, FileAppWidget::class.java)
appWidgetManager.updateAppWidget(componentName, remoteViews)
if (refreshList) {
appWidgetManager.notifyAppWidgetViewDataChanged(
appWidgetManager.getAppWidgetIds(
componentName
), R.id.widget_list
)
}
}
internal fun refreshList(context: Context) {
val appWidgetManager = AppWidgetManager.getInstance(context)
val componentName = ComponentName(context, FileAppWidget::class.java)
appWidgetManager.notifyAppWidgetViewDataChanged(
appWidgetManager.getAppWidgetIds(componentName),
R.id.widget_list
)
}
internal fun updateAppWidget(
context: Context,
appWidgetManager: AppWidgetManager,
appWidgetId: Int
) {
// Construct the RemoteViews object
val views = RemoteViews(context.packageName, R.layout.file_app_widget)
views.setTextColor(R.id.widget_title1, context.getColor(R.color.widget_font_blue))
//标题1
val title1Intent = Intent(context, FileAppWidget::class.java)
title1Intent.action = WIDGET_COLLECTION_TITLE_ACTION
title1Intent.data = Uri.parse(title1Intent.toUri(Intent.URI_INTENT_SCHEME))
title1Intent.putExtra(WIDGET_COLLECTION_TITLE_EXTRA, 0)
val uDiskPendingIntent =
PendingIntent.getBroadcast(context, 0, title1Intent, PendingIntent.FLAG_UPDATE_CURRENT)
views.setOnClickPendingIntent(R.id.widget_title1, uDiskPendingIntent)
//标题2
val title2Intent = Intent(context, FileAppWidget::class.java)
title2Intent.action = WIDGET_COLLECTION_TITLE_ACTION
title2Intent.data = Uri.parse(title2Intent.toUri(Intent.URI_INTENT_SCHEME))
title2Intent.putExtra(WIDGET_COLLECTION_TITLE_EXTRA, 1)
val favoritePendingIntent =
PendingIntent.getBroadcast(context, 1, title2Intent, PendingIntent.FLAG_UPDATE_CURRENT)
views.setOnClickPendingIntent(R.id.widget_title2, favoritePendingIntent)
//标题3
val title3Intent = Intent(context, FileAppWidget::class.java)
title3Intent.action = WIDGET_COLLECTION_TITLE_ACTION
title3Intent.data = Uri.parse(title3Intent.toUri(Intent.URI_INTENT_SCHEME))
title3Intent.putExtra(WIDGET_COLLECTION_TITLE_EXTRA, 2)
val downloadPendingIntent =
PendingIntent.getBroadcast(context, 2, title3Intent, PendingIntent.FLAG_UPDATE_CURRENT)
views.setOnClickPendingIntent(R.id.widget_title3, downloadPendingIntent)
//标题4
val title4Intent = Intent(context, FileAppWidget::class.java)
title4Intent.action = WIDGET_COLLECTION_TITLE_ACTION
title4Intent.data = Uri.parse(title4Intent.toUri(Intent.URI_INTENT_SCHEME))
title4Intent.putExtra(WIDGET_COLLECTION_TITLE_EXTRA, 3)
val recentPendingIntent =
PendingIntent.getBroadcast(context, 3, title4Intent, PendingIntent.FLAG_UPDATE_CURRENT)
views.setOnClickPendingIntent(R.id.widget_title4, recentPendingIntent)
//列表适配器
val serviceIntent = Intent(context, FileAppWidgetService::class.java)
serviceIntent.data = Uri.parse(serviceIntent.toUri(Intent.URI_INTENT_SCHEME))
views.setRemoteAdapter(R.id.widget_list, serviceIntent)
//列表item点击
val listIntent = Intent(context, FileAppWidget::class.java)
listIntent.action = WIDGET_COLLECTION_VIEW_ACTION
listIntent.data = Uri.parse(listIntent.toUri(Intent.URI_INTENT_SCHEME))
val listPendingIntent =
PendingIntent.getBroadcast(context, 5, listIntent, PendingIntent.FLAG_UPDATE_CURRENT)
views.setPendingIntentTemplate(R.id.widget_list, listPendingIntent)
// Instruct the widget manager to update the widget
appWidgetManager.updateAppWidget(appWidgetId, views)
}
5.创建一个FileAppWidgetFactory.kt文件用于处理ListView列表的item,代码如下:
class FileAppWidgetFactory(
val context: Context,
val intent: Intent?
) : RemoteViewsService.RemoteViewsFactory {
override fun onCreate() {
Logger.t(TAG).d("onCreate")
mContext = context
titleType = 0
}
override fun getLoadingView(): RemoteViews? {
Logger.t(TAG).d("getLoadingView")
return null
}
override fun getItemId(position: Int): Long {
Logger.t(TAG).d("getItemId")
return position.toLong()
}
override fun onDataSetChanged() {
Logger.t(TAG).d("onDataSetChanged")
if (isClickFill) {
isClickFill = false
} else {
initData()
}
}
override fun hasStableIds(): Boolean {
Logger.t(TAG).d("hasStableIds")
return true
}
override fun getViewAt(position: Int): RemoteViews {
Logger.t(TAG).d("getViewAt")
val views = RemoteViews(context.packageName, R.layout.widget_item_view)
val bean = fileWidgetBeans[position]
//设置item布局信息
...
//菜单点击
...
return views
}
override fun getCount(): Int {
Logger.t(TAG).d("getCount")
return fileWidgetBeans.size
}
override fun getViewTypeCount(): Int {
Logger.t(TAG).d("getViewTypeCount")
return 1
}
override fun onDestroy() {
Logger.t(TAG).d("onDestroy")
titleType = -1
fileWidgetBeans.clear()
}
companion object {
private val TAG = "FileAppWidgetFactory"
private lateinit var mContext: Context
var titleType = 0
var isClickFill = false
private var fileWidgetBeans = mutableListOf<FileWidgetBean>()
private var subscribe: Disposable? = null
fun showMenu(position: Int) {
if (position >= 0 && fileWidgetBeans.isNotEmpty()) {
if (fileWidgetBeans[position].isShowMenu) {
fileWidgetBeans[position].isShowMenu = false
dispose()
} else {
hideMenu()
fileWidgetBeans[position].isShowMenu = true
downTime()
}
}
}
fun hideMenu(position: Int) {
if (position >= 0 && fileWidgetBeans.isNotEmpty()) {
fileWidgetBeans[position].isShowMenu = false
dispose()
}
}
private fun hideMenu() {
for (bean in fileWidgetBeans) {
bean.isShowMenu = false
}
}
...
}
private var isLoading = true
private fun initData() {
setData(titleType)
}
private fun setData(type: Int) {
isLoading = true
fileWidgetBeans.clear()
when (type) {
0 -> {//标题1
setTitle1Data()
}
1 -> {//标题2
setTitle2Data()
}
2 -> {//标题3
setTitle3Data()
}
3 -> {//标题4
setTitle4Data()
}
else -> isLoading = false
}
}
...
}
6.创建一个FileAppWidgetService.kt文件用于创建RemoteViewsService.RemoteViewsFactory,代码如下:
class FileAppWidgetService : RemoteViewsService() {
override fun onGetViewFactory(intent: Intent?): RemoteViewsFactory {
return FileAppWidgetFactory(this, intent)
}
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
return Service.START_STICKY
}
}
7.最后还需要在AndroidManifest.xml文件中注册receiver和server,代码如下:
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.example.desktopwidget">
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/AppTheme">
<activity android:name=".MainActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<receiver android:name=".widget.FileAppWidget">
<intent-filter>
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
</intent-filter>
<meta-data
android:name="android.appwidget.provider"
android:resource="@xml/file_app_widget_info" />
</receiver>
<service
android:name=".widget.FileAppWidgetService"
android:permission="android.permission.BIND_REMOTEVIEWS" />
</application>
</manifest>
到这代码基本完成了。
在实习widget的过程中要注意一些问题
在代码实现后,运行程序发现widget中的界面显示异常或者一直显示在加载中,这个时候就去布局看一下,是否使用了RemoteViews不支持的布局,大部分都是这个问题导致的,我在实际编写过程中也碰到过这个问题,所以与大家分享一下,避免入坑。
RemoteViews 对象(因而应用微件)可以支持以下布局类:
FrameLayout
LinearLayout
RelativeLayout
GridLayout
以及以下微件类:
AnalogClock
Button
Chronometer
ImageButton
ImageView
ProgressBar
TextView
ViewFlipper
ListView
GridView
StackView
AdapterViewFlipper
不支持这些类的后代。
RemoteViews 还支持 ViewStub
,它是一个大小为零的不可见视图,您可以使用它在运行时以懒散的方式扩充布局资源。