问题
appcompatV7包包含AppCompatXX视图组件,使用这些组件可以在5.0以下版本使用tint属性进行着色。
比如:
<android.support.v7.widget.AppCompatImageView
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/image"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginBottom="5dp"
android:adjustViewBounds="true"
android:onClick="onLog"
android:src="@drawable/ic_launcher"
app:tint="#f00" />
使用app:tint="#f00"
可以将图标染成红色。这是一个很实用的功能。
而在5.0以下使用AppCompatImageView
等组件有一个bug。上面代码中,我们设置了android:onClick
属性,5.0以下会发生如下崩溃:
java.lang.IllegalStateException: Could not find a method onLog(View) in the activity class android.support.v7.widget.TintContextWrapper for onClick handler on view class android.support.v7.widget.AppCompatImageView with id 'image'
at android.view.View$1.onClick(View.java:3810)
at android.view.View.performClick(View.java:4438)
at android.view.View$PerformClick.run(View.java:18422)
at android.os.Handler.handleCallback(Handler.java:733)
at android.os.Handler.dispatchMessage(Handler.java:95)
at android.os.Looper.loop(Looper.java:136)
at android.app.ActivityThread.main(ActivityThread.java:5017)
at java.lang.reflect.Method.invokeNative(Native Method)
at java.lang.reflect.Method.invoke(Method.java:515)
at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:779)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:595)
at dalvik.system.NativeStart.main(Native Method)
看到日志感觉莫名其妙,为什么程序会去android.support.v7.widget.TintContextWrapper
这个类中找onLog(View)
方法呢?不应该去我们自己的Activity中去找么?
原因
带着这个疑问,我们来翻翻源码。
首先AppCompatImageView
的构造函数当中:
public AppCompatImageView(Context context, AttributeSet attrs, int defStyleAttr) {
super(TintContextWrapper.wrap(context), attrs, defStyleAttr);
...
}
可以看到,传入的context,也就是我们自己的Activity,被TintContextWrapper
包装了一下。
然后转到View处理onClick属性的地方,api19的源码是这样的:
case R.styleable.View_onClick:
...
//handlerName就是"android:onClick"属性的值
final String handlerName = a.getString(attr);
if (handlerName != null) {
setOnClickListener(new OnClickListener() {
private Method mHandler;
public void onClick(View v) {
if (mHandler == null) {
try {
//直接使用getContext()方法,得到的是TintContextWrapper实例,所以找不到我们的onLog方法
mHandler = getContext().getClass().getMethod(handlerName,
View.class);
} catch (NoSuchMethodException e) {
int id = getId();
String idText = id == NO_ID ? "" : " with id '"
+ getContext().getResources().getResourceEntryName(
id) + "'";
//这个就是我们看到的异常信息
throw new IllegalStateException("Could not find a method " +
handlerName + "(View) in the activity "
+ getContext().getClass() + " for onClick handler"
+ " on view " + View.this.getClass() + idText, e);
}
}
...
}
});
}
break;
关于崩溃的原因,代码的注释中写的很清楚了。
那么为什么5.0以上没有问题呢?就在于对context的处理不一样,在查找onLog方法时,代码是这样的:
private void resolveMethod(@Nullable Context context, @NonNull String name) {
//首先是个循环查找
while (context != null) {
try {
if (!context.isRestricted()) {
//首次执行,context就是TintContextWrapper,所以找不到我们的方法
final Method method = context.getClass().getMethod(mMethodName, View.class);
if (method != null) {
mResolvedMethod = method;
mResolvedContext = context;
return;
}
}
} catch (NoSuchMethodException e) {
}
if (context instanceof ContextWrapper) {
//通过getBaseContext就拿到了我们的Activity,第二次循环就能找到我们的方法了
context = ((ContextWrapper) context).getBaseContext();
} else {
context = null;
}
}
...
}
}
解决方法
1、暴力解决
放弃使用AppCompatXX视图组件,不过好挫的赶脚有没有,怎么能知难而退呢?
2、异想天开
覆写View的getContext()方法,直接返回context.getBaseContext()不就行了吗?然鹅:
public final Context getContext() {
return mContext;
}
final !
3、创新才是出路
LayoutInflater全解析一文中提到,Activity中的View在inflate之前会先调用Activity的如下方法:
@Nullable
public View onCreateView(String name, Context context, AttributeSet attrs) {
return null;
}
该方法若返回null,则由LayoutInflater来构建View。这就有了处理的余地。
首先正式开发中都会有一个BaseActivity,在该Activity中覆写上面的方法:
public class BaseActivity extends AppCompatActivity {
@Override
public View onCreateView(String name, Context context, AttributeSet attrs) {
return WidgetUtil.onCreateView(super.onCreateView(name, context, attrs), name, context, attrs);
}
}
WidgetUtil.java
如下:
class WidgetUtil {
static View onCreateView(View view, String name, Context context, AttributeSet attrs) {
//5.0以上系统没有问题,直接返回
if (Build.VERSION.SDK_INT >= 21) {
return view;
}
//查找是否设置了android:onClick属性,没有设置直接返回
TypedArray a = context.obtainStyledAttributes(attrs, new int[]{android.R.attr.onClick});
String handlerName = a.getString(0);
if (handlerName == null) {
return view;
}
//view一般来说都是null
if (view == null) {
//这里注意,LayoutInflater.from(context)一定要使用这个context,不要使用BaseActivity作为参数,否则可能出问题。
view = WidgetUtil.getViewByName(LayoutInflater.from(context), name, attrs);
if (view == null) {
return null;
}
}
//重新给view设置监听器
view.setOnClickListener(new DeclaredOnClickListener(view, handlerName));
a.recycle();
return view;
}
@Nullable
private static View getViewByName(LayoutInflater inflater, String name, AttributeSet attrs) {
if (TextUtils.isEmpty(name)) {
return null;
}
/*
过滤自己App中定义的各个View的基类
*/
// String[] parts = name.split("\\.");
// String viewName = parts[parts.length - 1];
// if (!viewName.startsWith("Custom")) {
// return null;
// }
try {
//调用LayoutInflater的方法来创建view,其实就是通过反射来创建
return inflater.createView(name, null, attrs);
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
return null;
}
//监听器的源码拷贝自5.0以上View#DeclaredOnClickListener类
private static class DeclaredOnClickListener implements View.OnClickListener {
private final View mHostView;
private final String mMethodName;
private Method mResolvedMethod;
private Context mResolvedContext;
DeclaredOnClickListener(@NonNull View hostView, @NonNull String methodName) {
mHostView = hostView;
mMethodName = methodName;
}
@Override
public void onClick(@NonNull View v) {
if (mResolvedMethod == null) {
resolveMethod(mHostView.getContext(), mMethodName);
}
try {
mResolvedMethod.invoke(mResolvedContext, v);
} catch (IllegalAccessException e) {
throw new IllegalStateException(
"Could not execute non-public method for android:onClick", e);
} catch (InvocationTargetException e) {
throw new IllegalStateException(
"Could not execute method for android:onClick", e);
}
}
private void resolveMethod(@Nullable Context context, @NonNull String name) {
while (context != null) {
try {
if (!context.isRestricted()) {
Method method = context.getClass().getMethod(name, View.class);
if (method != null) {
mResolvedMethod = method;
mResolvedContext = context;
return;
}
}
} catch (NoSuchMethodException e) {
// Failed to find method, keep searching up the hierarchy.
}
if (context instanceof ContextWrapper) {
context = ((ContextWrapper) context).getBaseContext();
} else {
// Can't search up the hierarchy, null out and fail.
context = null;
}
}
int id = mHostView.getId();
String idText = id == View.NO_ID ? "" : " with id '"
+ mHostView.getContext().getResources().getResourceEntryName(id) + "'";
throw new IllegalStateException("Could not find method " + name
+ "(View) in a parent or ancestor Context for android:onClick "
+ "attribute defined on view " + mHostView.getClass() + idText);
}
}
}
OK,完美解决!
还没完!!
上面虽然解决了设置android:onClick
属性崩溃的问题,但是还有一个更加严重的问题,那就是上面提到的getContext()
函数的返回值问题!
由于在5.0以下使用AppCompat组件时,getContext()
方法返回的是TintContextWrapper
这样一个类的实例,所以类似getContext() instanceOf XXActivity
的调用全部为false
,会导致已有代码需要不小的改动。
所以,为了稳定,如果程序最小使用版本在5.0以下,还是别用AppCompatXX视图组件了。
哎,挖到最后,还是要知难而退了。
网友评论