我们都知道给view设置点击事件有三种方式:
第一种:View.setOnClickLintener(new OnClicklistener(...));
第二种:View.setOnClickListener(this); 然后让class去实现点击事件
第三种:xml文件中写onClick="XXXX";然后在activity中写方法XXX(View view)
其实前两种方法本质上没有什么区别,第二种就是有时候可以避免内存泄漏(不是一定可以避免,只有在activity中可以),今天我们来分析view源码是如何通过第三种xml方式设置点击事件的。
首先我们看看View的构造器:
public View(Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) {
this(context);
final TypedArray a = context.obtainStyledAttributes(
attrs, com.android.internal.R.styleable.View, defStyleAttr, defStyleRes);
//此处省略n行无关代码
final int N = a.getIndexCount();
for (int i = 0; i < N; i++) {
int attr = a.getIndex(i);
switch (attr) {
//此处省略N行无关代码
case R.styleable.View_onClick:
if (context.isRestricted()) {
throw new IllegalStateException("The android:onClick attribute cannot "
+ "be used within a restricted context");
}
final String handlerName = a.getString(attr);
if (handlerName != null) {
setOnClickListener(new DeclaredOnClickListener(this, handlerName));
}
break;
//此处省略N行无关代码
}
//此处省略N行无关代码
}
//此处省略N行无关代码
}
我们会发现,在构造器中会去读取onClick属性是否存在,如果存在则会读取到String类型的name,即handlerName,然后立即给我们设置了点击事件,原来系统帮我们设置了点击事件,那么又是如何传递到activity中的呢?为什么我们activity中要写handlerName名字的方法呢?我们就需要进入DeclaredOnClickListener()这个方法看看。很简单,代码如下:
/**
* An implementation of OnClickListener that attempts to lazily load a
* named click handling method from a parent or ancestor context.
*/
private static class DeclaredOnClickListener implements OnClickListener {
private final View mHostView;
private final String mMethodName;
private Method mResolvedMethod;
private Context mResolvedContext;
public 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);
}
}
@NonNull
private void resolveMethod(@Nullable Context context, @NonNull String name) {
while (context != null) {
try {
if (!context.isRestricted()) {
final Method method = context.getClass().getMethod(mMethodName, 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;
}
}
final int id = mHostView.getId();
final String idText = id == NO_ID ? "" : " with id '"
+ mHostView.getContext().getResources().getResourceEntryName(id) + "'";
throw new IllegalStateException("Could not find method " + mMethodName
+ "(View) in a parent or ancestor Context for android:onClick "
+ "attribute defined on view " + mHostView.getClass() + idText);
}
}
他是view里面的一个静态内部类,实现了onClickListener接口,很简单,构造器传过来了点击的View和hanlderName,然后我们看onClick(View v)方法,会进入resolveMethod这个方法,进去看看吧。这个方法好像就是通过反射获取到了方法,然后通过context=null跳出while循环,如果找了半天发现没有找到这个方法,那就直接挂了!然后又回到onClick方法中执行了此方法,完了,就这么简单。。。但是,问题来了,那么是如何知道在activity中写的方法呢?假如我把此方法写到fragment中可不可以呢?答案是不可以的,这里我们会看到一个关键的context上下文,其实这个上下文就是activity,这就是为什么我们在activity中写此方法会被调用了。我们可以做个试验,假如说我在fragment的xml布局中添加点击事件,我把方法写到activity中,是否会执行呢?肯定会的,但是写到fragment中就不行。好了,很简单就这么多......