Android常用的工具“小插件”——Widget机制
Widget俗称“小插件”,是Android系统中一个很常用的工具。比如我们可以在Launcher中添加一个音乐播放器的Widget。
在Launcher上可以添加插件,那么是不是说只有Launcher才具备这个功能呢?
Android系统并没有具体规定谁才能充当“Widget容器”这个角色。它定义了一套完整的Widget添加/移除和显示机制,使得人人都能当“Widget提供者”,人人也都有资格做“Widget容器”。
上面我们提到了“Widget提供者”和“Widget容器”这样的概念,前者如一个天气插件,后者则如Launcher。在Widget机制中,它们都有各自的专有名词(同时也是类名),分别是AppWidgetProvider和AppWidgetHost。除此之外,我们能猜想到系统中还需要一个全局的Widget管理器。类似于WindowManagerService、WallpaperManagerService的命名方式,它叫作AppWidgetService。

“功能的提供者”——AppWidgetProvider
既然叫作Provider,言下之意就是“功能的提供者”。从Host的角度来说,它没有办法预先知晓用户会添加多少个Widget,也没有办法知晓这些添加的Widget都实现了哪些功能。所以在Host的“世界”里,一个Widget只是一个View——它只需要按照要求进行正确显示即可,具体的功能实现则由AppWidgetProvider来完成。
Host把Widget看成View的一个“变种”。
一个有效的Provider要提供至少以下几方面的内容。
- AppWidgetProviderInfo
也就是用于描述这个Widget的各种信息,包括它的layout布局、刷新频率以及下面要提到的AppWidgetProvider等。这些信息以XML格式的文件表示,Tag标志为。
- AppWidgetProvider
既然Widget最终是要被显示在Host中的,那么它的功能实现和普通应用程序就一定会有差异。AppWidgetProvider主要借助于Broadcast事件来对Widget进行“远程更新”。
- View布局
AppWidgetProviderInfo用于描述这个Widget的整体信息,而这里的Layout则是专门用于描述Widget的“显示部分”(确切地说,是初始化时的显示)。
Provider就是一个BroadcastReceiver。比如我们可以在AndroidManifest.xml中声明以下内容来定义一个AppWidgetProvider:
<receiver android:name="ExampleAppWidgetProvider" ><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>
这个receiver要接收的唯一消息,就是APPWIDGET_UPDATE;并且它还需要带有信息明确指明自己是一个"android.appwidget.provider",最后的android:resource即前面说到的AppWidgetProviderInfo。比如:
<appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android"android:minWidth="294dp"android:minHeight="72dp"android:updatePeriodMillis="86400000"android:previewImage="@drawable/preview"android:initialLayout="@layout/example_appwidget"
</appwidget-provider>
这个XML文件的最后一项属性(android:initialLayout)指定了初始的View布局为example_ appwidget,它和我们编写普通应用程序的布局语法一样。
当我们编写一个自己的Widget Provider时,首先要继承自AppWidgetProvider。后者的内部实现并不复杂,它继承自BroadcastReceiver,并在onReceive中将具体事件通过重载函数通知我们的AppWidgetProvider实例:
/*frameworks/base/core/java/android/appwidget/AppWidgetProvider.java*/
public class AppWidgetProvider extends BroadcastReceiver {…public void onReceive(Context context, Intent intent) {String action = intent.getAction();if (AppWidgetManager.ACTION_APPWIDGET_UPDATE.equals(action)) {Bundle extras = intent.getExtras();if (extras != null) {int[] appWidgetIds = extras.getIntArray(AppWidgetManager.EXTRA_APPWIDGET _IDS);if (appWidgetIds != null && appWidgetIds.length > 0) {this.onUpdate(context, AppWidgetManager.getInstance(context), appWid getIds);}}}…}…
也就是说,编写一个Widget应该根据需求来重载onReceiver(如果有需要的话),onUpdate,onAppWidgetOptionsChanged,onDeleted,onEnabled以及onDisabled。它们分别会在此Widget被更新、Option改变、被删除等情况下被调用。
不过要特别注意的是,一个Widget是可以有多个具体实例的。比如我们写了一个“天气”插件供用户使用,那么理论上并不限制用户会在Launcher中添加多少个“天气”实例。因而需要有相应的WidgetId来唯一标识每一个实例。

AppWidgetHost
上一小节我们了解了AppWidgetProvider所要做的工作,接下来再看看Host又是如何配合Provider的。简而言之,Host这个“东道主”需要提供相应的空间供Widget来展现自己的UI界面。打个比方,AppWidgetHost就好比一个展厅,而至于陈列的汽车是大众还是奔驰品牌都是没问题的——取决于Widget本身的意愿。
成为一个AppWidgetHost,它需要解决以下问题。
- 如何显示Widget的UI界面
- 如何与AppWidgetProvider通信
一个AppWidgetProvider与外界的接口就是onReceive,然后再细化为onUpdate,onEnable等事件处理。而产生这些事件的根源,除了AppWidgetService这一系统元素外,就是AppWidgetHost了。只不过后者也是要通过前者来发送事件的。

简图中的Host_Application是指扮演Host角色的应用程序,如Launcher。它在整个Widget机制中只会与AppWidgetManager进行交互而不会直接调用AppWidgetService的接口(这有点类似于ServiceManager.java的作用)。可想而知,AppWidgetManager内部还是要通过间接调用AppWidgetService来实现的。另外每个Host_Application还要持有一个AppWidgetHost,我们可以认为它是Host的代理。
当一个Host_Application创建后,它需要向AppWidgetService注册监听Widget事件,并提供一个callback实现。这个callback实际上继承自IAppWidgetHost.Stub,即一个基于AIDL的BinderServer,这就保证了AppWidgetService在事件发生时可以回调到Host。需要接收的回调事件包括:
updateAppWidget updateAppWidgetView@AppWidgetHostproviderChangedonProviderChanged@AppWidgetHostviewDataChangedviewDataChanged @AppWidgetHost
我们以updateAppWidget为例来分析其内部实现:
/*frameworks/base/core/java/android/appwidget/AppWidgetHost.java*/void updateAppWidgetView(int appWidgetId, RemoteViews views, int userId) {AppWidgetHostView v;synchronized (mViews) {v = mViews.get(appWidgetId);}if (v != null) {v.updateAppWidget(views);}}
当WidgetProvider希望更新Host中的View显示时(比如天气插件更新气温),它会通过AppWidgetManager.updateAppWidget(int appWidgetId,RemoteViews views)来指定新的View样式(RemoteViews)。这个请求最终由AppWidgetService发送给相应的Host来实现,即updateApp WidgetView。
上面代码中的mViews定义如下:
HashMap<Integer,AppWidgetHostView> mViews = new HashMap<Integer, AppWidgetHostView>();
它是一个AppWidgetHostView的集合。换句话说,是当前这个Host所包含的所有Widget的View对象。比如在Launcher中用户每添加一个Widget(或者设备刚开机时Launcher自己从保存的配置中读取需要加载显示的Widgets),就会用AppWidgetHost.createView把它加入这个集合中。另外因为Widget数量众多,必须为它们分配一个全局唯一的WidgetId。
Launcher中添加widget的操作过程
首先Host通过AppWidgetManager.getAppWidgetInfo来得到相应WidgetId的Info信息,即我们前一小节中讲到的AppWidgetProviderInfo。接着Host会通过AppWidgetHost.createView产生一个AppWidgetHostView——这个View对应的布局是由前面的initialLayout指定的。后续AppWidget Provider根据实际情况还会通过RemoteViews来实时更新它的Widget显示。
那么,createView都做了哪些工作呢?
/*frameworks/base/core/java/android/appwidget/AppWidgetHost.java*/public final AppWidgetHostView createView(Context context, int appWidgetId,AppWidgetProviderInfo appWidget) {final int userId = mContext.getUserId();AppWidgetHostView view = onCreateView(mContext, appWidgetId, appWidget);//本地的View对象view.setUserId(userId);view.setOnClickHandler(mOnClickHandler);view.setAppWidget(appWidgetId, appWidget);synchronized (mViews) {mViews.put(appWidgetId, view);}RemoteViews views;try {views = sService.getAppWidgetViews(appWidgetId, userId);//得到该Widget的RemoteViewsif (views != null) {views.setUser(new UserHandle(mContext.getUserId()));}} catch (RemoteException e) {throw new RuntimeException("system server dead?", e);}view.updateAppWidget(views);//通过RemoteViews“搭建”本地的Viewreturn view;}
第一步是要产生一个AppWidgetHostView,默认情况下onCreateView内部只是new了一个AppWidgetHostView对象然后就直接返回。如果读者有特殊需求,可以重载这个函数:
/*frameworks/base/core/java/android/appwidget/AppWidgetHostView.java*/
public class AppWidgetHostView extends FrameLayout {
…
可见,AppWidgetHostView实际上是FrameLayout的扩展子类。而setAppWidget一方面将widgetId与此AppWidgetHostView联系起来,另一方面设置了将要显示的widget的padding值,我们同样可以重载这一实现。
接下来就是widget显示的重点,即我们如何把Widget Provider定义的界面显示到Host_ Application中。
在分析源码前,我们先来打个比方。张三在北京建了一栋别墅,李四看了后很喜欢,于是也想自己在上海建一栋一模一样的。怎么办?显然不可能将张三的别墅直接挪到上海,因为它们是异地的,属于两个不同的“进程空间”。一个可行的办法就是将张三的建筑图纸完完本本地递交给李四,然后李四就可以在他自己的“进程空间”中兴建一栋一模一样的别墅了。虽然砖瓦、水泥可能用的不是一个品牌,但这丝毫不会影响大家认为“这两栋别墅的样式风格是完全一样的”。
Widget的显示也类似。我们需要在另一个进程空间(即Host_Application)中显示自己的View,那么也完全可以把View的“图纸”交给对方——这样对方只要“依葫芦画瓢”,也就不难“还原”出Widget的“真实面目”了。而这张“图纸”,就是RemoteViews:
/*RemoteViews.java*/
public class RemoteViews implements Parcelable, Filter {…
虽然它的名称中也带有“Views”,但实际上没有任何View的影子。它继承自可以跨进程传递的Parcelable类以及对数据进行约束的Filter类。
有了这些基础,我们再回头接着看前面的createView:
views = sService.getAppWidgetViews(appWidgetId);
上面这句代码根据WidgetId来得到一个RemoteViews,它借助于sService即AppWidgetService提供的接口来实现。实际上它只是简单填写了Widget的LayoutId和PackageName等,后面真正构造Widget的UI界面时才会去取“图纸”。
最后调用的updateAppWidget是真正构建widget界面的地方(分段阅读):
public void updateAppWidget(RemoteViews remoteViews) {…boolean recycled = false;View content = null;Exception exception = null;…if (remoteViews == null) {…} else {mRemoteContext = getRemoteContext(remoteViews);int layoutId = remoteViews.getLayoutId();/*Step1. 描述Widget的LayoutId*/…if (content == null) {try {content = remoteViews.apply(mContext, this, mOnClickHandler);/*Step2.创建Widget的View*/} catch (RuntimeException e) {exception = e;}}mLayoutId = layoutId;mViewMode = VIEW_MODE_CONTENT;}…if (!recycled) {prepareView(content);addView(content);/*添加Widget的View到全局管理中*/}…}
小结一下这个函数,简单来讲它做了两件事。
-
生成一个View(content变量)
这个View根据推测就是由Widget的“图纸”生成的,因而代表了Widget的UI界面。 -
将上述View加到AppWidgetHostView中
AppWidgetHostView是一个FrameLayout,它将content作为子View添加进来。这样当整个View重绘时,Widget的界面自然也就呈现出来了。
来看看上面代码段中apply函数的实现:
/*frameworks/base/core/java/android/widget/RemoteViews.java*/public View apply(Context context, ViewGroup parent, OnClickHandler handler) {RemoteViews rvToApply = getRemoteViewsToApply(context);View result;Context c = prepareContext(context);LayoutInflater inflater = (LayoutInflater)c.getSystemService(Context.LAYOUT_ INFLATER_ SERVICE);…result=inflater.inflate(rvToApply.getLayoutId(), parent, false);…return result;}
这样程序就按照Widget提供的“图纸”成功地在host进程中构造出本地的View对象了——它会和Host_Application中其他View一起,经过SurfaceFlinger的处理后最终显示到屏幕上。
相关文章:
Android常用的工具“小插件”——Widget机制
Widget俗称“小插件”,是Android系统中一个很常用的工具。比如我们可以在Launcher中添加一个音乐播放器的Widget。 在Launcher上可以添加插件,那么是不是说只有Launcher才具备这个功能呢? Android系统并没有具体规定谁才能充当“Widget容器…...
探索在云原生环境中构建的大数据驱动的智能应用程序的成功案例,并分析它们的关键要素。
文章目录 1. Netflix - 个性化推荐引擎2. Uber - 实时数据分析和决策支持3. Airbnb - 价格预测和优化5. Google - 自然语言处理和搜索优化 🎈个人主页:程序员 小侯 🎐CSDN新晋作者 🎉欢迎 👍点赞✍评论⭐收藏 ✨收录专…...
jupyter 添加中文选项
文章目录 jupyter 添加中文选项1. 下载中文包2. 选择中文重新加载一下,页面就变成中文了 jupyter 添加中文选项 1. 下载中文包 pip install jupyterlab-language-pack-zh-CN2. 选择中文 重新加载一下,页面就变成中文了 这才是设置中文的正解ÿ…...
系列十、Java操作RocketMQ之批量消息
一、概述 RocketMQ可以一次性发送一组消息,那么这一组消息会被当做一个消息进行消费。 二、案例代码 2.1、pom 同系列五 2.2、RocketMQConstant 同系列五 2.3、BatchConsumer package org.star.batch.consumer;import cn.hutool.core.util.StrUtil; import lom…...
leetcode1两数之和
题目: 给定一个整数数组 nums 和一个整数目标值 target,请你在该数组中找出 和为目标值 target 的那 两个 整数,并返回它们的数组下标。 你可以假设每种输入只会对应一个答案。但是,数组中同一个元素在答案里不能重复出现。 你…...
近年GDC服务器分享合集(四): 《火箭联盟》:为免费游玩而进行的扩展
如今,网络游戏采用免费游玩(Free to Play)加内购的比例要远大于买断制,这是因为前者能带来更低的用户门槛。甚至有游戏为了获取更多的用户,选择把原来的买断制改为免费游玩,一个典型的例子就是最近的网易的…...
android反射详解
1,反射的定义 一般情况下,我们使用某个类时必定知道它是什么类,是用来做什么的,并且能够获得此类的引用。于是我们直接对这个类进行实例化,之后使用这个类对象进行操作。 反射则是一开始并不知道我要初始化的类对象是…...
Python 反射和动态执行
反射主要应用于类的对象上,在运行时,将对象中的属性和方法反射出来,通过字符串对对象成员(属性、方法)进行查找、获取、删除、添加成员等动作,是一种基于字符串的事件驱动技术。 python是一门动态语言&…...
计算机网络常见端口号
端口号标识了一个主机上进行通信的不同的应用程序。比如网站服务器80端口一般都是开启的,等你来连接。 端口划分: (1)常用端口,公共端口(保留给公共服务所使用),端口号为0-1023之间…...
SpringBoot / Vue 对SSE的基本使用(简单上手)
一、SSE是什么? SSE技术是基于单工通信模式,只是单纯的客户端向服务端发送请求,服务端不会主动发送给客户端。服务端采取的策略是抓住这个请求不放,等数据更新的时候才返回给客户端,当客户端接收到消息后,…...
Qt串口基本设置与协议收发
前言 1.一直都想要做一个Qt上位机,趁着这个周末有时间,动手写一下 2.comboBox没有点击的信号,所以做了一个触发的功能 3.Qt的数据类型很奇怪,转来转去的我也搞得很迷糊 4.给自己挖个坑,下一期做一个查看波形的上位…...
interview3-微服务与MQ
一、SpringCloud篇 (1)服务注册 常见的注册中心:eureka、nacos、zookeeper eureka做服务注册中心: 服务注册:服务提供者需要把自己的信息注册到eureka,由eureka来保存这些信息,比如服务名称、…...
kafka详解一
kafka详解一 1、消息引擎背景 根据维基百科的定义,消息引擎系统是一组规范。企业利用这组规范在不同系统之间传递语义准确的消息,实现松耦合的异步式数据传递. 即:系统 A 发送消息给消息引擎系统,系统 B 从消息引擎系统中读取 A…...
Flutter yuv 转 rgb
1、引用yuv_converter库 yuv_converter: ^0.0.1 2、导入头文件: import package:yuv_converter/yuv_converter.dart;3、yuv转rgb YuvConverter.yuv420NV21ToRgba8888(yuvRawData, 512, 512) 根据yuv格式选择不同的api。 举个例子: void initState() …...
MySQL——子查询
2023.9.8 相关学习笔记: #子查询 /* 含义: 出现在其他语句中的select语句,称为子查询或内查询 外部的查询语句,称为主查询或外查询分类: 按子查询出现的位置:select后面:仅仅支持标量子查询fro…...
Java学习笔记---多态
面向对象三大特征之一(继承,封装,多态) 多态的应用场景:根据传递对象的不同,调用不同的show方法 一、多态的定义 同类型的对象,表现出的不同形态(对象的多种形态) 二…...
2023-09-10 LeetCode每日一题(课程表 II)
2023-09-10每日一题 一、题目编号 210. 课程表 II二、题目链接 点击跳转到题目位置 三、题目描述 现在你总共有 numCourses 门课需要选,记为 0 到 numCourses - 1。给你一个数组 prerequisites ,其中 prerequisites[i] [ai, bi] ,表示在…...
合并区间【贪心算法】
合并区间 以数组 intervals 表示若干个区间的集合,其中单个区间为 intervals[i] [starti, endi] 。请你合并所有重叠的区间,并返回 一个不重叠的区间数组,该数组需恰好覆盖输入中的所有区间 。 class Solution {public int[][] merge(int[…...
2023,软件测试人的未来在哪里?
2023年,IT行业出现空前的萧条,首先是年初一开始各大厂像着了魔似的不约而同的纷纷裁员、降薪、奖金包缩水,随之而来的是需求萎缩,HC减少或封锁等等。 而有幸未被列入裁员名单的在职人员,庆幸之余也心有余悸࿰…...
Python中的Numpy向量计算(R与Python系列第三篇)
目录 一、什么是Numpy? 二、如何导入NumPy? 三、生成NumPy数组 3.1利用序列生成 3.2使用特定函数生成NumPy数组 (1)使用np.arange() (2)使用np.linspace() 四、NumPy数组的其他常用函数 (1)np.z…...
设计模式和设计原则回顾
设计模式和设计原则回顾 23种设计模式是设计原则的完美体现,设计原则设计原则是设计模式的理论基石, 设计模式 在经典的设计模式分类中(如《设计模式:可复用面向对象软件的基础》一书中),总共有23种设计模式,分为三大类: 一、创建型模式(5种) 1. 单例模式(Sing…...
Qt Widget类解析与代码注释
#include "widget.h" #include "ui_widget.h"Widget::Widget(QWidget *parent): QWidget(parent), ui(new Ui::Widget) {ui->setupUi(this); }Widget::~Widget() {delete ui; }//解释这串代码,写上注释 当然可以!这段代码是 Qt …...
深入理解JavaScript设计模式之单例模式
目录 什么是单例模式为什么需要单例模式常见应用场景包括 单例模式实现透明单例模式实现不透明单例模式用代理实现单例模式javaScript中的单例模式使用命名空间使用闭包封装私有变量 惰性单例通用的惰性单例 结语 什么是单例模式 单例模式(Singleton Pattern&#…...
JUC笔记(上)-复习 涉及死锁 volatile synchronized CAS 原子操作
一、上下文切换 即使单核CPU也可以进行多线程执行代码,CPU会给每个线程分配CPU时间片来实现这个机制。时间片非常短,所以CPU会不断地切换线程执行,从而让我们感觉多个线程是同时执行的。时间片一般是十几毫秒(ms)。通过时间片分配算法执行。…...
3403. 从盒子中找出字典序最大的字符串 I
3403. 从盒子中找出字典序最大的字符串 I 题目链接:3403. 从盒子中找出字典序最大的字符串 I 代码如下: class Solution { public:string answerString(string word, int numFriends) {if (numFriends 1) {return word;}string res;for (int i 0;i &…...
MySQL账号权限管理指南:安全创建账户与精细授权技巧
在MySQL数据库管理中,合理创建用户账号并分配精确权限是保障数据安全的核心环节。直接使用root账号进行所有操作不仅危险且难以审计操作行为。今天我们来全面解析MySQL账号创建与权限分配的专业方法。 一、为何需要创建独立账号? 最小权限原则…...
【生成模型】视频生成论文调研
工作清单 上游应用方向:控制、速度、时长、高动态、多主体驱动 类型工作基础模型WAN / WAN-VACE / HunyuanVideo控制条件轨迹控制ATI~镜头控制ReCamMaster~多主体驱动Phantom~音频驱动Let Them Talk: Audio-Driven Multi-Person Conversational Video Generation速…...
使用Spring AI和MCP协议构建图片搜索服务
目录 使用Spring AI和MCP协议构建图片搜索服务 引言 技术栈概览 项目架构设计 架构图 服务端开发 1. 创建Spring Boot项目 2. 实现图片搜索工具 3. 配置传输模式 Stdio模式(本地调用) SSE模式(远程调用) 4. 注册工具提…...
uniapp 字符包含的相关方法
在uniapp中,如果你想检查一个字符串是否包含另一个子字符串,你可以使用JavaScript中的includes()方法或者indexOf()方法。这两种方法都可以达到目的,但它们在处理方式和返回值上有所不同。 使用includes()方法 includes()方法用于判断一个字…...
0x-3-Oracle 23 ai-sqlcl 25.1 集成安装-配置和优化
是不是受够了安装了oracle database之后sqlplus的简陋,无法删除无法上下翻页的苦恼。 可以安装readline和rlwrap插件的话,配置.bahs_profile后也能解决上下翻页这些,但是很多生产环境无法安装rpm包。 oracle提供了sqlcl免费许可,…...
