众所周知,android2.X版本中RemoteViews只支持FrameLayout,LinearLayout,RelativeLayout三个布局类,以及下面几个widget类:
AnalogClock
Button
Chronometer
ImageButton
ImageView
ProgressBar
TextView
ViewFlipper
Descendants of these classes are not supported.(这句很关键)
如果想要使桌面widget效果炫一些,出现一些拖动效果,那么一般只能修改sdk,编译框架才可。在网上有一个开源的launcher叫做launcherPlus,这个launcherPlus对launcher的源码进行了修改用以支持ListView。这里有一个contactWidget,效果如下:
现在公司有个小需求,就是要在上下拖动listView时,使其有弹簧的效果,于是首先想到的是修改contactWidget的源码,首先定位到修改contactWidget的gridview.xml,原来的gridview.xml中示例如下:
<GridView xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="wrap_content" android:layout_height="wrap_content"
android:cacheColorHint="#00000000" android:scrollbarStyle="outsideOverlay"
android:scrollbars="vertical" android:scrollingCache="true"
android:alwaysDrawnWithCache="true" android:persistentDrawingCache="scrolling"
android:divider="@drawable/divider_horizontal_bright"
android:fadingEdge="none" android:id="@+id/my_gridview"
android:horizontalSpacing="5dip" android:verticalSpacing="10dip"
android:numColumns="4" android:addStatesFromChildren="false"
android:choiceMode="singleChoice" android:duplicateParentState="true">
</GridView>
现在我自己实现了一个GridView类MyGridView,在gridview文件中使用自己的类,如下:
<com.zhoujie.MyGridView
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="wrap_content" android:layout_height="wrap_content"
android:cacheColorHint="#00000000" android:scrollbarStyle="outsideOverlay"
android:scrollbars="vertical" android:scrollingCache="true"
android:alwaysDrawnWithCache="true" android:persistentDrawingCache="scrolling"
android:divider="@drawable/divider_horizontal_bright"
android:fadingEdge="none" android:id="@+id/my_gridview"
android:horizontalSpacing="5dip" android:verticalSpacing="10dip"
android:numColumns="4" android:addStatesFromChildren="false"
android:choiceMode="singleChoice" android:duplicateParentState="true">
</com.zhoujie.MyGridView >
编译,运行结果却没有显示出来,如下图:
于是看logcat,
08-18 08:13:49.794: DEBUG/NewsWidget(4606): Binary XML file line #2: Error inflating class com.zhoujie.MyGridView
从log看,显然是LayoutInflater在inflate时出的错,进行一系列跟踪,可知出错的地方是在LayoutInflater的createView()方法中,如下所示:
public final View createView(String name, String prefix, AttributeSet attrs)
throws ClassNotFoundException, InflateException {
Constructor constructor = sConstructorMap.get(name);
Class clazz = null;
try {
if (constructor == null) {
// Class not found in the cache, see if it's real, and try to add it
clazz = mContext.getClassLoader().loadClass(
prefix != null ? (prefix + name) : name);
if (mFilter != null && clazz != null) {
boolean allowed = mFilter.onLoadClass(clazz);
if (!allowed) {
failNotAllowed(name, prefix, attrs);
}
}
constructor = clazz.getConstructor(mConstructorSignature);
sConstructorMap.put(name, constructor);
} else {
// If we have a filter, apply it to cached constructor
if (mFilter != null) {
// Have we seen this name before?
Boolean allowedState = mFilterMap.get(name);
if (allowedState == null) {
// New class -- remember whether it is allowed
clazz = mContext.getClassLoader().loadClass(
prefix != null ? (prefix + name) : name);
boolean allowed = clazz != null && mFilter.onLoadClass(clazz);
mFilterMap.put(name, allowed);
if (!allowed) {
failNotAllowed(name, prefix, attrs);
}
} else if (allowedState.equals(Boolean.FALSE)) {
failNotAllowed(name, prefix, attrs);
}
}
}
Object[] args = mConstructorArgs;
args[1] = attrs;
return (View) constructor.newInstance(args);
} catch (NoSuchMethodException e) {
InflateException ie = new InflateException(attrs.getPositionDescription()
+ ": Error inflating class "
+ (prefix != null ? (prefix + name) : name));
ie.initCause(e);
throw ie;
} catch (ClassNotFoundException e) {
// If loadClass fails, we should propagate the exception.
throw e;
} catch (Exception e) {
InflateException ie = new InflateException(attrs.getPositionDescription()
+ ": Error inflating class "
+ (clazz == null ? "<unknown>" : clazz.getName()));
ie.initCause(e);
throw ie;
}
}
因为这个方法中会抛出ClassNotFoundException 的异常,抛异常的位置已经红色标注,loadClass时出错了,已经知道,这个欲加载的类为“com.zhoujie.MyGridView”,也就是classLoader没有找到这个MyGridView类。通过分析,调试,发现这的mContext有问题。
那这个mContext是从哪里来的呢?下面就着重分析这个mContext的来源。
这个得从分析launcher入手,因为创建widget的过程是在launcher中实现的,长按桌面的空白区域,通过一系列的对话框选择,可以创建widget,通过查看launcherPlus的代码,发现Workspace继承了widgetspace这个类,显然在widget上上下拖动listView时,事件响应是在launcher中做的处理。
首先看launcher的启动过程
下面来看registerProvider()方法:
public void registerProvider() {
final Context context = getContext();
IntentFilter filter = new IntentFilter();
filter.addAction(LauncherIntent.Action.ACTION_START_FRAME_ANIMATION);
filter.addAction(LauncherIntent.Action.ACTION_STOP_FRAME_ANIMATION);
filter.addAction(LauncherIntent.Action.ACTION_START_TWEEN_ANIMATION);
context.registerReceiver(mAnimationProvider, filter);
IntentFilter scrollFilter = new IntentFilter();
scrollFilter.addAction(LauncherIntent.Action.ACTION_SCROLL_WIDGET_START);
scrollFilter.addAction(LauncherIntent.Action.ACTION_SCROLL_WIDGET_CLOSE);
scrollFilter.addAction(LauncherIntent.Action.ACTION_SCROLL_WIDGET_CLEAR_IMAGE_CACHE);
scrollFilter.addAction(LauncherIntent.Action.ACTION_SCROLL_WIDGET_SELECT_ITEM);
context.registerReceiver(mScrollViewProvider, scrollFilter);
}
可以看到在这个方法里注册了一系列和listView有关的intentfilter到mScrollViewProvider中。
当我们长按点击创建一个contactWidget的过程中,launcher会收到来自contactWidget的广播,这里可以从代码中看到:
在contactWidget1的onReceive()中:
在CreateMakeScrollableIntent()中可以清楚地看到,创建了一个ACTION_SCROLL_WIDGET_START 类型的intent,这个intent launcher的ScrollViewProvider可以接收到,进行处理。
ScrollViewProvider.onReceive()---->makeScrollable()
在makeScrollable()中
Context remoteContext = localContext.createPackageContext(remoteContextPackageName, Context.CONTEXT_IGNORE_SECURITY);
......................
if (intent.hasExtra(LauncherIntent.Extra.Scroll.EXTRA_LISTVIEW_REMOTEVIEWS)) {
SimpleRemoteViews rvs = (SimpleRemoteViews)intent.getParcelableExtra(LauncherIntent.Extra.Scroll.EXTRA_LISTVIEW_REMOTEVIEWS);
dummyView = rvs.apply(remoteContext, null);
...........................
通过分析可知这里的remoteContext 最终会传给LayoutInflater的mContext。
回到刚才论述的 clazz = mContext.getClassLoader().loadClass(
prefix != null ? (prefix + name) : name);
这里的这个mContext.getClassLoader() 得到的ClassLoader实际上是dalvik.system.PathClassLoader,这个PathClassLoader只加载已经安装到android系统中的apk文件,也就是/data/app目录下的apk文件,可问题是这个PathClassLoader的libPath=null,path=“.",因此最终导致加载com.zhoujie.MyGridView失败。我估计这个是android系统自己的bug,因为mContext中有个状态mPackageInfo中是有contactWidget 的apk的信息的,而mContext.getClassLoader()后得到的classloader中的path以及libPath都为空,这说明mContext.getClassLoader()这个方法遗失了data的apk的信息。