在项目中,对Button设置点击事件监听时,大多数情况下还是习惯使用setOnClickListener设置监听,但是最近发现当在布局文件中同时使用了android:theme和android:onClick属性时,在响应点击事件时程序会发生crash,发生Crash的设备为Android 5.0及以上(7.0未测试),不限机型。在Android 5.0和Android 6.0上发生crash时Log信息不一致。
Android 6.0 Crash信息如下:
Android 5.0 Crash信息如下:
如果去掉android:theme属性,则点击事情可以正常响应,并未出现任何崩溃的情况。
查找资料发现,从Android 5.0开始,支持对单独的View设置主题。当在布局文件中设置了主题之后,ContextThemeWrapper 会被指定为View的Context,因此View的Context不再是Activity了,这时候点击事件的回调响应也就不存在了。
在使用getContext()获取view的Context时,如果在布局文件中未设置主题,返回值是当前的Activity实例MainActivity@4124。
在使用getContext()获取view的Context时,如果在布局文件中设置了主题,返回值是ContextThemeWrapper,它的成员变量mBase才是当前的Activity,因此在ContextThemeWrapper无法找到”android:onClick”中设置的方法。
Android M中代码如下:
private void parseInclude(XmlPullParser parser, Context context, View parent,
AttributeSet attrs) throws XmlPullParserException, IOException {
int type;
if (parent instanceof ViewGroup) {
// Apply a theme wrapper, if requested. This is sort of a weird
// edge case, since developers think the overwrites
// values in the AttributeSet of the included View. So, if the
// included View has a theme attribute, we'll need to ignore it.
final TypedArray ta = context.obtainStyledAttributes(attrs, ATTRS_THEME);
final int themeResId = ta.getResourceId(0, 0);
final boolean hasThemeOverride = themeResId != 0;
if (hasThemeOverride) {
context = new ContextThemeWrapper(context, themeResId);
}
ta.recycle();
// If the layout is pointing to a theme attribute, we have to
// massage the value to get a resource identifier out of it.
int layout = attrs.getAttributeResourceValue(null, ATTR_LAYOUT, 0);
if (layout == 0) {
final String value = attrs.getAttributeValue(null, ATTR_LAYOUT);
if (value == null || value.length() <= 0) {
throw new InflateException("You must specify a layout in the"
+ " include tag: ");
}
// Attempt to resolve the "?attr/name" string to an identifier.
layout = context.getResources().getIdentifier(value.substring(1), null, null);
}
...
}
...
View createViewFromTag(View parent, String name, Context context, AttributeSet attrs,
boolean ignoreThemeAttr) {
if (name.equals("view")) {
name = attrs.getAttributeValue(null, "class");
}
// Apply a theme wrapper, if allowed and one is specified.
if (!ignoreThemeAttr) {
final TypedArray ta = context.obtainStyledAttributes(attrs, ATTRS_THEME);
final int themeResId = ta.getResourceId(0, 0);
if (themeResId != 0) {
context = new ContextThemeWrapper(context, themeResId);
}
ta.recycle();
}
if (name.equals(TAG_1995)) {
// Let's party like it's 1995!
return new BlinkLayout(context, attrs);
}
}
Android K中代码如下:
private void parseInclude(XmlPullParser parser, View parent, AttributeSet attrs)
throws XmlPullParserException, IOException {
int type;
if (parent instanceof ViewGroup) {
final int layout = attrs.getAttributeResourceValue(null, "layout", 0);
if (layout == 0) {
final String value = attrs.getAttributeValue(null, "layout");
if (value == null) {
throw new InflateException("You must specifiy a layout in the"
+ " include tag: ");
} else {
throw new InflateException("You must specifiy a valid layout "
+ "reference. The layout ID " + value + " is not valid.");
}
...
}
...
View createViewFromTag(View parent, String name, AttributeSet attrs) {
if (name.equals("view")) {
name = attrs.getAttributeValue(null, "class");
}
...
}
对比源码可以发现,在M的源码中,多出了如下几号代码
final TypedArray ta = context.obtainStyledAttributes(attrs, ATTRS_THEME);
final int themeResId = ta.getResourceId(0, 0);
final boolean hasThemeOverride = themeResId != 0;
if (hasThemeOverride) {
context = new ContextThemeWrapper(context, themeResId);
}
ta.recycle();
如果对布局或者View指定了主题,那么当前的context(activity或fragment或application context)被转换为 ContextThemeWrapper的实例,因此会出现ContextThemeWrapper找不到对应方法的问题。
如果是对整个Activity设置主题,尽量不要在布局文件中设置,在Manifest配置文件中设置;
如果要求必须在布局文件中设置主题,那么不要使用属性android:onClick,使用setOnClickListener替代。
参考资料:
http://stackoverflow.com/questions/31653126/crash-when-clicking-button-with-custom-theme/31672941#31672941