心想才能事成!
成功只会降临在那些自我感觉会成功的人的身上。 —-拿破仑·希尔
概述
在讲解之前,我们先来看一张成品图吧。
是不是很漂亮啊!那两个音乐页和相机页是怎么加上去的呢?是如何实现跨进程显示view的? 目前我知道两种方式:
1. RemoteViews
RemoteViews描述的是一个跨进程显示的view。
RemoteViews使用最多的场合是通知栏和桌面小插件
但是RemoteView并不支持所有的View:
布局:FrameLayout、LinearLayout、RelativeLayout、GridLayout
组件:Button、ImageButton、ImageView、TextView、ListView、GridView、ViewStub等
2. 通过Context的createPackageContext方法,获取其它包的上下文Context,然后获取其他包的布局view。
由于RemoteView并不支持所有的View,所以我们这个实例采用的是借壳方式。所谓借壳就是采用AppWidget结构,用createPackageContext 获取的上下文来加载的其他包的view来替代RemoteView,下面就开始做详细的介绍吧,下面就称为“专属widget”。
准备工作
- 准备音乐和相机两个widget应用。因为这两个应用要独立拿出来管理,所以在AndroidManifest.xml中添加信息:
<intent-filter>
<action android:name="android.appwidget.action.PRIVATE_PAGE" />
</intent-filter>
用来检索这两个widget。同时这两个widget还要包含一些各自的信息,所以要在AndroidManifest.xml中添加要表达的信息:
<meta-data
android:name="dqf.private.page.info"
android:resource="@xml/private_info" />
方便在Launcher中loadXmlMetaData获取相关信息。 - 对Launcher架构要熟悉,这两个widget要和Launcher中其他的widget分开,在Launcher中创建widget的LauncherAppWidgetInfo的时候,用appWidgetId来区分,普通的widget的appWidgetId由AppWidgetHost.allocateAppWidgetId()申请获得或者由AppWidgetHostView.getAppWidgetId()获得。而这两个特殊的widget由我们自己制定一个特殊的值,比如:-110。
解析和加载专属widget
系统层解析加载一个widget主要的类有:
1、AppWidgetHostView
AppWidgetHostView是一个view,是容纳APP传来的View的容器。而RemoteViews就是用来传递APP的View的。
2、AppWidgetHos
3、AppWidgetProvider
4、AppWidgetServiceImpl
5、AppWidgetManager
6、AppWidgetProviderInfo
在Launcher上显示widget,我们需要关心的是AppWidgetHostView和AppWidgetProviderInfo两个文件。
因为我们要独立出来两个特殊的widget,所以在这两个Widget的AndroidManifest.xml的AppWidgetProvider生命中不添加
<intent-filter>
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
</intent-filter>
这样的话,AppWidgetServiceImpl系统中就不会加载到这两个widget了。
我们需要单独定义一个action,如下:
<intent-filter>
<action android:name="android.appwidget.action.PRIVATE_PAGE" />
</intent-filter>
第一步:解析专属widget的信息:
1. workspace部分的解析:
workspace直接显示这两个widget,是最主要的部分。
synchronized (mPrivateWidgets) {
mPrivateWidgets.clear();
//在上文中,我提到要独立赛选出这两个widget,就用"android.appwidget.action.PRIVATE_PAGE"
List privatewidgets = mPackageManager.queryBroadcastReceivers(new Intent(
"android.appwidget.action.PRIVATE_PAGE"), PackageManager.GET_META_DATA);
if(privatewidgets != null && privatewidgets.size() > 0) {
for(int i = 0; i < privatewidgets.size(); i++){
try {
ResolveInfo info = (ResolveInfo) privatewidgets.get(i);
ComponentName componentname = new ComponentName(
info.activityInfo.applicationInfo.packageName,
info.activityInfo.name);
//创建DqfAppWidgetProviderInfo对象,该类中包含了widget的相关信息
DqfAppWidgetProviderInfo dqfappwidgetproviderinfo = new DqfAppWidgetProviderInfo (
mLauncher, componentname);
mPrivateWidgets.add(dqfappwidgetproviderinfo );
} catch (Exception e) {
e.printStackTrace();
}
}
}
因为我们的widget不再走系统的那套流程,所以我们要建一个AppWidgetProviderInfo的子类,需要解析这两个widget的信息。
所以我们定义了一个类DqfAppWidgetProviderInfo,继承AppWidgetProviderInfo。在类的构造函数中解析指定ComponentName的widget信息。
和AppWidgetServiceImpl 的 parseProviderInfoXml的解析是一样的,需要了解的可以去看一下这个函数,
就是用来解析Widget应用的AndroidManifest.xml中的
<meta-data
android:name="android.appwidget.provider"
android:resource="@xml/widget_info" />
解析xml文件widget_info。从widget_info.xml中,我们要获取
1、minWidth 和minHeight:App Widget布局需要的最小区域
2、updatePeriodMillis:widget 的更新频率
3、initialLayout:指向 widget 的布局资源文件
4、previewImage:指定预览图,该预览图在用户选择 widget 时出现。
2. Hotseat和Indicator部分的解析:
如图:
两个专属widget,hotseat部分分别对应不同的应用。page_indicator部分也对应了不同的小角标。这些信息都来自专属widget的AndroidManifest.xml中的
<meta-data
android:name="dqf.private.page.info"
android:resource="@xml/private_info" />
private_info.xml:
<?xml version="1.0" encoding="utf-8"?>
<privatepage xmlns:privatepage="http://schemas.android.com/apk/res/com.dqf.moodalbum"
privatepage:dockIntent1="@string/dockIntent1"
privatepage:dockIntent2="@string/dockIntent2"
privatepage:dockIntent3="@string/dockIntent3"
privatepage:pointIndicator="@drawable/private_icon"
privatepage:pointIndicatorFocus="@drawable/private_icon_focus" />
解析private_info.xml文件,同样需要定义一个类来管理这些信息PrivatePageInfo。解析:
public PrivatePageInfo(Context context, ApplicationInfo application)
throws XmlPullParserException, IOException {
mApplicationInfo = application;
mPm = context.getPackageManager();
XmlResourceParser parser = null;
try {
parser = application.loadXmlMetaData(mPm, "dqf.private.page.info");
}
if (parser == null) {
throw new XmlPullParserException(
"No dqf.private.page.info meta-data");
}
int type;
while ((type = parser.next()) != XmlPullParser.END_DOCUMENT
&& type != XmlPullParser.START_TAG) {
}
String nodeName = parser.getName();
Log.d(TAG, "nodeName =" + nodeName);
if (!"privatepage".equals(nodeName)) {
throw new XmlPullParserException(
"Meta-data does not start with LockInfo tag");
}
String namespace = parser.getAttributeNamespace(0);
//mPointIndicatorId 和mPointIndicatorFocusId是page_indicator部分对应的小图标的资源id。然后再通过BitmapFactory.decodeResource(
localResources, mPointIndicatorId)获取图片资源(localResources是专属widget应用的上下文资源哦)。
mPointIndicatorId = parser.getAttributeResourceValue(namespace, "pointIndicator", -1);
mPointIndicatorFocusId = parser.getAttributeResourceValue(namespace, "pointIndicatorFocus", -1);
//mDockIntent1 mDockIntent2 mDockIntent3是hotseat部分对应的三个应用的信息的ID,然后再通过mPm.getText(mApplicationInfo.packageName, mDockIntent1,mApplicationInfo)获取具体的信息(是三个String,信息内容是ComponentName)。
mDockIntent1 = parser.getAttributeResourceValue(namespace, "dockIntent1", -1);
mDockIntent2 = parser.getAttributeResourceValue(namespace, "dockIntent2", -1);
mDockIntent3 = parser.getAttributeResourceValue(namespace, "dockIntent3", -1);
if (parser != null) {
parser.close();
}
} catch (Exception localException) {
throw new XmlPullParserException("Unable to create context for: "
+ mApplicationInfo.packageName);
} finally {
if (parser != null) {
parser.close();
}
}
}
第二步:加载专属widget到Launcher界面:
专属widget预览界面的显示
如图:
这两个图片就是widget的预览图,就是上文我们解析出来的previewImage。在Adapter中加载,在布局中显示出来。专属widget在workspace的显示
workspace显示的是AppWidgetHostView中通过RemoteViews传递过来的widget的view。
Launcher中定义了LauncherAppWidgetHostView类,继承AppWidgetHostView。
然后使用方法:
launcherInfo.hostView = (LauncherAppWidgetHostView)mAppWidgetHost.createView(this, appWidgetId, appWidgetInfo);
launcherInfo.hostView.setAppWidget(appWidgetId, appWidgetInfo);
哈哈哈!
其实这里的createView是获取不到RemoteView的,因为专属widget没有加载到系统里(上文已经分析了过了),并且专属widget的appWidgetId不是系统allocate的,是我们自己定义的-110,所以AppWidgetServiceImpl找不到ID为-110对应的RemoteViews。
怎么办?? 方法:
View view = ((LayoutInflater) mContext.getSystemService( Context.LAYOUT_INFLATER_SERVICE)).inflate(resource, parent);
mInfo.hostView.addView(view);
mInfo.hostView.setChildView(view);
OK,借壳成功。
然后就是Launcher的流程了:
mWorkspace.addInScreen(launcherInfo.hostView, container, screenId, cellXY[0], cellXY[1],
launcherInfo.spanX, launcherInfo.spanY, isWorkspaceLocked());
addInScreen到桌面上。这是显示这一部分的主要代码,中间的一些细节,就不介绍了。还有专属widget的删除和添加等等。都不难,只要你对Launcher的流程很熟悉,都不在话下。Hotseat部分的显示
如图:
专属widget的DockBar部分创建好之后,下面就开始解决滑动页面,DockBar跟着变化的问题了。因为专属widget有两个,可能还有更多,所以要建立一个类PrivatePageDockBarManager来管理专属widget的DockBar部分,这个类的主要功能是添加、删除、获取、查询DockBar。
DockBar如何添加到桌面,采用Launcher的DragLayer来addView到Launcher的底部。
那么如何让不同专属widget对应的DockBar跟着页面的滑动变化呢?在Launcher的onCreate中,给Workspace注册一个PageSwitchListener的监听事件:
mWorkspace.setPageSwitchListener(new PagedView.PageSwitchListener() {
public void onPageSwitch(View view, int i) {
changeHotseatVisibility(i);
}
});
在changeHotseatVisibility函数中,写DockBar的切换显示。
- Indicator部分图标的跟换
这部分就更简单了,随着页面的滑动,更新专属widget的indicator指示图就可以了。PageIndicator类中做好更改。
结束语
发现写技术博客好困难,之前也没写过。只懂得如果写代码,表达出来还真是困难。但是,在写博客的过程中,真的有很大收获,因为写博客的过程就是对专业知识的巩固,对开发功能的再次审核,对开发功能的细节再次思考。