一、引言
作为一个车机的Launcher开发,总结了下自己认为的难点技术:ApppWidget、拖拽、RemoteAnimation(应用打开/关闭动画)。今天就其中的AppWidget简述一下。
二、AppWidget简介
- Android widget 也称为桌面插件,其是android系统应用开发层面的一部分,但是又有特殊用途,而且会成为整个android系统的亮点。Android中的AppWidget与google widget和中移动的widget并不是一个概念,这里的AppWidget只是把一个进程的控件嵌入到别外一个进程的窗口里的一种方法。
- AppWidget的服务核心在AppWidgetService中,它是系统应用,在SystemServer进程中。
- AppWidget的提供方由应用提供(对大部分应用开发者来说,了解操作这一块就够了)。
- AppWidget的显示方,基本上运行在Launcher中。
- AppWidget支持的控件是由局限性的,比如不支持RecyclerView等。
- RemoteViews 在Android中的使用场景主要有:自定义通知栏和桌面小部件。
如下图红色箭头所指的都是 AppWidget
三、Launcher3 AppWidget的启动添加流程
1. Launcher3启动添加
Launcher启动onCreate()方法初始化mAppWidgetManager, mAppWidgetHost对象,AppWidgetHost是launcher承载AppWidgetView的宿主。
public void onCreate() {
...
//得到AppWidget管理实例 : AppWidgetManager , AppWidgetHost , AppWidgetHostView三个类的关系
mAppWidgetManager = AppWidgetManagerCompat.getInstance(this); //1
mAppWidgetHost = new LauncherAppWidgetHost(this); //2
// Host启动监听,监听LauncherProvider中的数据改变
mAppWidgetHost.startListening(); //3
...
}
- AppWidgetManagerCompat 管理类是一个单例模式的兼容类
public static AppWidgetManagerCompat getInstance(Context context) {
synchronized (sInstanceLock) {
if (sInstance == null) {
if (Utilities.ATLEAST_OREO) {
sInstance = new AppWidgetManagerCompatVO(context.getApplicationContext());
} else {
sInstance = new AppWidgetManagerCompatVL(context.getApplicationContext());
}
}
return sInstance;
}
}
- LauncherAppWidgetHost extends AppWidgetHost 由其父类完成初始化对象,创建用于回调的Callbacks服务类IAppWidgetHost.Stub, 绑定服务bindService,得到IAppWidgetService对象,进行launcher和AppWidgetService之间的调用
public AppWidgetHost(Context context, int hostId, OnClickHandler handler, Looper looper) {
mContextOpPackageName = context.getOpPackageName();
mHostId = hostId;
mOnClickHandler = handler;
mHandler = new UpdateHandler(looper);
mCallbacks = new Callbacks(mHandler);
mDisplayMetrics = context.getResources().getDisplayMetrics();
bindService(context);
}
private static void bindService(Context context) {
synchronized (sServiceLock) {
if (sServiceInitialized) {
return;
}
sServiceInitialized = true;
PackageManager packageManager = context.getPackageManager();
if (!packageManager.hasSystemFeature(PackageManager.FEATURE_APP_WIDGETS)
&& !context.getResources().getBoolean(R.bool.config_enableAppWidgetService)) {
return;
}
IBinder b = ServiceManager.getService(Context.APPWIDGET_SERVICE);
sService = IAppWidgetService.Stub.asInterface(b);
}
}
- 在startListening 方法中 ,通过IAppWidgetService.startListening 方法解析Launcher中的AppWidget信息保存到系统服务成员变量中。
public void startListening() {
if (sService == null) {
return;
}
final int[] idsToUpdate;
synchronized (mViews) {
int N = mViews.size();
idsToUpdate = new int[N];
for (int i = 0; i < N; i++) {
idsToUpdate[i] = mViews.keyAt(i);
}
}
List updates;
try {
updates = sService.startListening(
mCallbacks, mContextOpPackageName, mHostId, idsToUpdate).getList();
}
catch (RemoteException e) {
throw new RuntimeException("system server dead?", e);
}
int N = updates.size();
for (int i = 0; i < N; i++) {
PendingHostUpdate update = updates.get(i);
switch (update.type) {
case PendingHostUpdate.TYPE_VIEWS_UPDATE:
updateAppWidgetView(update.appWidgetId, update.views);
break;
case PendingHostUpdate.TYPE_PROVIDER_CHANGED:
onProviderChanged(update.appWidgetId, update.widgetInfo);
break;
case PendingHostUpdate.TYPE_VIEW_DATA_CHANGED:
viewDataChanged(update.appWidgetId, update.viewId);
}
}
}
- 当添加AppWidget时,首页返回到Launcher中的onActivityResult中,在handleActivityResult中创建添加小部件意图,之后返回到onActivityForResult,调用completeAddAppWidget,通过IAppWidgetService.getAppWidgetInfo,获取AppWidgetProviderInfo,保存到本地数据库中addItemToDatabase(),并创建AppWidgetHostView 对象,mAppWidgetHost.createView,返回RemoteView对象,IAppWidgetService。getAppWidgetViews(),调用AppWidgetHostView.updateAppWidget(views);更新View到launcher界面上mWorkspace.addInScreen(hostView, launcherInfo);
@Thunk void completeAddAppWidget(int appWidgetId, ItemInfo itemInfo,
AppWidgetHostView hostView, LauncherAppWidgetProviderInfo appWidgetInfo) {
if (appWidgetInfo == null) {
appWidgetInfo = mAppWidgetManager.getLauncherAppWidgetInfo(appWidgetId);
}
LauncherAppWidgetInfo launcherInfo;
launcherInfo = new LauncherAppWidgetInfo(appWidgetId, appWidgetInfo.provider);
launcherInfo.spanX = itemInfo.spanX;
launcherInfo.spanY = itemInfo.spanY;
launcherInfo.minSpanX = itemInfo.minSpanX;
launcherInfo.minSpanY = itemInfo.minSpanY;
launcherInfo.user = appWidgetInfo.getProfile();
getModelWriter().addItemToDatabase(launcherInfo,
itemInfo.container, itemInfo.screenId, itemInfo.cellX, itemInfo.cellY);
if (hostView == null) {
// Perform actual inflation because we're live
hostView = mAppWidgetHost.createView(this, appWidgetId, appWidgetInfo);
}
hostView.setVisibility(View.VISIBLE);
prepareAppWidget(hostView, launcherInfo);
mWorkspace.addInScreen(hostView, launcherInfo);
}
- 当AppWidgetProvider获得更新的广播,并执行onUpdate(),onUpdate()中创建了RemoteViews并通过AppWidgetManager.updateAppWidget()更新到AppWidgetService之后,AppWidgetService会通过注册的IAppWidgetHost的回调,执行AppWidgetHost的更新。
2. Lancher3 预置 AppWidget
- 添加权限
- 在res/xml/default_workspace_4x4.xml、default_workspace_5x5.xml等中添加
其中
- launcher:container="-100",表示添加在 desktop 中,如果是-101那就是在 HotSeat 中,但是这里我们的 widget 是要添加在 desktop,所以是-100;
- launcher:packageName=“com.android.deskclock”,这个没啥说的,就是widget的包名,我这里添加的是数字时钟,所以这里填写的是 时钟模块 的包名;
- launcher:className=“com.android.alarmclock.DigitalAppWidgetProvider”,这个是 widget 所在的类,这是是数字时钟,如果要添加 表盘时钟(指针时钟),就填写com.android.alarmclock.AnalogAppWidgetProvider;
- launcher:screen=“0”,这个是添加在哪一屏;
- launcher:spanX=“5”,这个表示 widget 在 x 方向上占位多少,我的launcher是 x 方向可以放5个APP图标,所以这里widget是占满整个 x 方向;
- launcher:spanY=“2”,这个表示 widget 在 y 方向上站位多少,2表示占用相当于两个APP图标的高度;
- launcher:x=“0”,这个表示 widget 的 x 方向上的位置,这里0表示从屏幕最左侧开始显示;
- launcher:y=“2”,这个表示 widget 的 y 方向上的位置,这里3表示从上往下第3个位置开始显示(从0开始,所以2就是第3个)。
四、AppWidget的使用
1. 大致思路:
- 在AndroidManifest中声明AppWidget。
- 在xml目录中定义AppWidget的配置文件。
- 在layout目录中定义Widget的布局文件。
- 新建一个类,继承AppWidgetProvider类,实现具体的widget业务逻辑
2. 具体使用步骤:
1. 在 AndroidManifest 中声明 App Widget
2. 在 xml 目录定义 App Widget 的初始化 xml 文件
- minWidth & minHeight:定义了 Widget 的最小宽高,当 minWidth 和 minHeight 不是桌面 cell 的整数倍时,Widget 的宽高会被阔至与其最接近的 cells 大小。Google 官方给出了一个大致估算 minWidth & minHeight 的公式,根据 Widget 所占的 cell 数量来计算宽高:70 × n − 30,n 是所占的 cell 数量。
- updatePeriodMillis:定义了 Widget 的刷新频率,也就是 App Widget Framework 多久请求一次 AppWidgetProvider 的 onUpdate() 回调函数。该时间间隔并不保证精确,出于节约用户电量的考虑,Android 系统默认最小更新周期是 30 分钟,也就是说:如果您的程序需要实时更新数据,设置这个更新周期是 2 秒,那么您的程序是不会每隔 2 秒就收到更新通知的,而是要等到 30 分钟以上才可以,要想实时的更新 Widget,一般可以采用 Service 和 AlarmManager 对 Widget 进行更新。
- previewImage:当用户选择添加 Widget 时的预览图片。如果该属性没有定义,则展示 application 的 launcher icon。该属性是在 3.0 以后引入的。
- initialLayout:Widget 的布局 Layout 文件。
- configure:定义了用户在添加 Widget 时弹出的配置页面的 Activity,用户可以在此进行 Widget 的一些配置,该 Activity 是可选的,如果不需要可以不进行声明。
- resizeMode:Widget 在水平和垂直方向是否可以调整大小,值可以为:horizontal(水平方向可以调整大小),vertical(垂直方向可以调整大小),none(不可以调整大小),也可以 horizontal|vertical 组合表示水平和垂直方向均可以调整大小。
- widgetCategory:表示 Widget 可以显示的位置,包括 home_screen(桌面),keyguard(锁屏),keyguard 属性需要 5.0 或以上 Android 版本才可以。
3. layout文件布局
仅支持以下布局类:
FrameLayout、LinearLayout 、RelativeLayout 、GridLayout 、AnalogClock 、Button 、Chronometer 、ImageButton 、ImageView 、ProgressBar 、TextView 、ViewFlipper 、 ListView 、 GridView 、StackView 、AdapterViewFlipper 、 ViewStub 不支持这些类的后代。
4. 自定义一个类 继承 AppWidgetProvider 类
AppWidgetProvider 继承自 BroadcastReceiver,内部逻辑非常简单,就是在 onReceive() 中处理 Widget 相关的广播事件,分发到各个回调函数中(onUpdate(), onDeleted(), onEnabled(), onDisabled, onAppWidgetOptionsChanged())。
- onUpdate():是最重要的回调函数,根据 updatePeriodMillis 定义的定期刷新操作会调用该函数,此外当用户添加 Widget 时 也会调用该函数,可以在这里进行必要的初始化操作。但如果在
中声明了 android:configure 的 Activity,在用户添加 Widget 时,不会调用 onUpdate(),需要由 configure Activity 去负责去调用 AppWidgetManager.updateAppWidget() 完成 Widget 更新,后续的定时更新还是会继续调用 onUpdate() 的。 - onDeleted():当 Widget 被删除时调用该方法。
- onEnabled():当 Widget 第一次被添加时调用,例如用户添加了两个你的 Widget,那么只有在添加第一个 Widget 时该方法会被调用。所以该方法比较适合执行你所有 Widgets 只需进行一次的操作。
- onDisabled():与 onEnabled 恰好相反,当你的最后一个 Widget 被删除时调用该方法,所以这里用来清理之前在 onEnabled() 中进行的操作。
- onAppWidgetOptionsChanged():当 Widget 第一次被添加或者大小发生变化时调用该方法,可以在此控制 Widget 元素的显示和隐藏。
public class MyAppWidgetProvider extends AppWidgetProvider {
private static final String TAG = MyAppWidgetProvider.class.getSimpleName();
public static final String REFRESH_WIDGET = "com.oitsme.REFRESH_WIDGET";
private Context mContext;
private static final Handler mHandler = new Handler();
private final Runnable runnable = new Runnable() {
@Override
public void run() {
hideLoading(mContext);
}
};
@Override
public void onReceive(Context context, Intent intent) {
super.onReceive(context, intent);
String action = intent.getAction();
Log.i(TAG, "onReceive");
if (action.equals(REFRESH_WIDGET)) {
// 接受“bt_refresh”的点击事件的广播
showLoading(context);
mHandler.postDelayed(runnable, 2000);
}
}
/**
* 到达指定的更新时间或者当用户向桌面添加AppWidget时被调用
* appWidgetIds:桌面上所有的widget都会被分配一个唯一的ID标识,这个数组就是他们的列表
*
* @param context
* @param appWidgetManager
* @param appWidgetIds
*/
@Override
@Override
public void onUpdate(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds) {
super.onUpdate(context, appWidgetManager, appWidgetIds);
this.mContext = context;
Log.i(TAG, "onUpdate");
// 获取AppWidget对应的视图
RemoteViews remoteViews = new RemoteViews(context.getPackageName(), R.layout.appwidget_layout);
// 设置响应 “按钮(bt_refresh)” 的intent
Intent btIntent = new Intent(context, MyAppWidgetProvider.class);
btIntent.setAction(REFRESH_WIDGET);
// btIntent.putExtra(REFRESH_WIDGET,"REFRESH_WIDGET");
PendingIntent btPendingIntent = PendingIntent.getBroadcast(context, 0, btIntent, PendingIntent.FLAG_UPDATE_CURRENT);
remoteViews.setOnClickPendingIntent(R.id.tv_refresh, btPendingIntent);
// 调用集合管理器对集合进行更新
appWidgetManager.updateAppWidget(appWidgetIds, remoteViews);
}
/**
* 显示加载loading
*/
private void showLoading(Context context) {
RemoteViews remoteViews = new RemoteViews(context.getPackageName(), R.layout.appwidget_layout);
remoteViews.setViewVisibility(R.id.tv_refresh, View.VISIBLE);
remoteViews.setViewVisibility(R.id.progress_bar, View.VISIBLE);
remoteViews.setTextViewText(R.id.tv_refresh, "正在刷新...");
refreshWidget(context, remoteViews, false);
}
/**
* 隐藏加载loading
*/
private void hideLoading(Context context) {
RemoteViews remoteViews = new RemoteViews(context.getPackageName(), R.layout.appwidget_layout);
remoteViews.setViewVisibility(R.id.progress_bar, View.GONE);
remoteViews.setTextViewText(R.id.tv_refresh, "刷新");
refreshWidget(context, remoteViews, false);
}
/**
* 刷新Widget
*/
private void refreshWidget(Context context, RemoteViews remoteViews, boolean refreshList) {
AppWidgetManager appWidgetManager = AppWidgetManager.getInstance(context);
ComponentName componentName = new ComponentName(context, MyAppWidgetProvider.class);
appWidgetManager.updateAppWidget(componentName, remoteViews);
}
}
- onUpdate()方法中首先需要new一个RemoteViews,构造方法里需要传递两个参数,一个是包名(context.getPacakgeName),一个是布局文件(layout_widget)。
然后通过remoteViews.setOnClickPendingIntent()设置按钮的点击事件。setOnClickPendingIntent()中需要传递两个参数:一个是id(比如需要被点击的button),一个是PendingIntent。PendingIntent是未来的意图。
于是我们需要事先构造一个PendingIntent,这个需要通过 PendingIntent.getBroadcast()来构造。getBroadcast()方法中需要传递四个参数,其中有一个是Intent。于是我们需要构造一个Intent。在intent里发送广播,并设置Action。按钮点击完了之后,记得调用appWidgetManager.updateAppWidget(int[] appWidgetIds, RemoteViews views)方法更新一下,第一个参数就是onUpdate方法中的参数,代表的是所有的控件。在onUpdate()方法中通过intent发送按钮点击时间的广播之后,我们需要在onReceive()方法中进行广播的接收。 - onReceive()方法中当intent的action匹配成功时,开始执行做点击时间之后的setText,不过这里需要重新new 一个 RemoteViews,而不能共用onUpdate()方法中的RemoteViews(这是一个很大的坑)。执行完点击事件之后的setText之后,记得调用appWidgetManager.updateAppWidget(ComponentName, RemoteViews)方法,第一个参数为组件名,需要我们自己new一下,第二个参数很好解释。
5. 如何显示在桌面
-
桌面长按桌面空白部分弹框选择 Widgets
-
选择自己的小组件长按拖拽到桌面