实现Android Widget 桌面小部件

实现Android Widget 桌面小部件

Android实现桌面小部件

可以切换标题显示不同列表,列表中可以显示不同类型布局以及显示和隐藏操作菜单。


title1
title2
title3
title4

图4显示不同类型的item显示的效果一样。

想体验效果可以下载apk运行体验:app-release.apk
提取码:uduw

运行程序后找到widget,拖到桌面就可以体验了

找到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,它是一个大小为零的不可见视图,您可以使用它在运行时以懒散的方式扩充布局资源。


具体源码可下载参考 源码

  • 2
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值