在 APP 开发过程中,给一个 View 设置监听点击事件是常见的需求。一般两种实现方式:
1、设置 listener
view.setOnClickListener(onClickListener);
2、在 XML 文件中设置对应的 view 点击时候的回调方法,当然需要在 Activity 中编写用于回调的方法
// Activity
public void onClickView(){
// do something
}
在 XML 设置 View 的 android:onClick 属性
<View
android:layout_width="match_parent"
android:layout_height="match_parent"
android:onClick="onClickView" />
有的时候从 XML 布局里直接设定点击事件会比较方便(特别是在写 DEMO 项目的时候),这种做法平时用的人并不多,从使用方式上大致能猜出来,View 应该是在运行的时候,使用反射的方式从 Activity 找到 “onClickView” 方法并调用,因为这种做法并没有用到任何接口。
接下来,我们可以从源码中分析出 View 是怎么触发回调方法的。
我们知道,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);
for (int i = 0; i < N; i++) {
int attr = a.getIndex(i);
switch (attr) {
……
// 处理 onClick 属性
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) {
// 给当前 View 实例设置一个 DeclaredOnClickListener 监听器
setOnClickListener(new DeclaredOnClickListener(this, handlerName));
}
break;
}
}
}
处理 onClick 属性的时候,先判断 View 的 Context 是否 isRestricted,如果是就抛出一个 IllegalStateException 异常。看看 isRestricted 方法。
a restricted context may disable specific features. For instance, a View associated with a restricted context would ignore particular XML attributes.
按照官方的说法,isRestricted 是用于判断当前的 Context 实例是否处于被限制的状态,处于被限制状态的 Context,会忽略某些特点的功能,比如 XML 的某些属性,很明显,我们在研究的 android:onClick 属性也会被忽略。
/**
* Indicates whether this Context is restricted.
*
* @return {@code true} if this Context is restricted, {@code false} otherwise.
*
* @see #CONTEXT_RESTRICTED
*/
public boolean isRestricted() {
return false;
}
接着,final String handlerName = a.getString(attr);
其实就是拿到了 android:onClick="onClickView"
中的 “onClickView” 这个字符串,然后使用了当前 View 的实例和 “onClickView” 创建了一个 DeclaredOnClickListener 实例,并设置为当前 View 的点击监听器。
/**
* 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 mMethod;
public DeclaredOnClickListener(@NonNull View hostView, @NonNull String methodName) {
mHostView = hostView;
mMethodName = methodName;
}
@Override
public void onClick(@NonNull View v) {
if (mMethod == null) {
mMethod = resolveMethod(mHostView.getContext(), mMethodName);
}
try {
mMethod.invoke(mHostView.getContext(), 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 Method resolveMethod(@Nullable Context context, @NonNull String name) {
while (context != null) {
try {
if (!context.isRestricted()) {
return context.getClass().getMethod(mMethodName, View.class);
}
} 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 的时候,DeclaredOnClickListener 实例的 “onClick” 方法会被调用,接着会调用 “resolveMethod” 方法,使用反射的方式从 View 的 Context 中找一个叫 “onClickView” 方法。要注意的是,“onClickView” 方法必须是 public 类型的,不然反射调用时会抛出 IllegalAccessException 异常。
同时从源码也能看出,使用 android:onClick 设置点击事件的方式是从 Context 里面查找回调方法的,所以如果对于在 Fragment 的 XML 里创建的 View,是无法通过这种方式绑定 Fragment 中的回调方法的,因为 Fragment 自身并不是一个 Context,这里的 View 的 Context 其实是 FragmentActivity。
此外,从 DeclaredOnClickListener 类的注释也能看出 android:onClick 的功能,主要是起到懒加载的作用,只有到点击 View 的时候,才会知道哪个方法是用于点击回调的。
最后,特别需要补充说明的是,使用 android:onClick 给 View 设置点击事件,就意味着要在 Activity 里添加一个非接口的 public 方法。现在 Android 的开发趋势是 “不要把业务逻辑写在 Activity 类里面”,这样做有利于项目的维护,防止 Activity 爆炸,所以尽量不要在 Activity 里出现非接口、非生命周期的 public 方法。因此,随意使用 android:onClick 可能会“污染” Activity。
------至所有正在努力奋斗的程序猿们!加油!!
有码走遍天下 无码寸步难行
1024 - 梦想,永不止步!
爱编程 不爱Bug
爱加班 不爱黑眼圈
固执 但不偏执
疯狂 但不疯癫
生活里的菜鸟
工作中的大神
身怀宝藏,一心憧憬星辰大海
追求极致,目标始于高山之巅
一群怀揣好奇,梦想改变世界的孩子
一群追日逐浪,正在改变世界的极客
你们用最美的语言,诠释着科技的力量
你们用极速的创新,引领着时代的变迁
——乐于分享,共同进步,欢迎补充
——Treat Warnings As Errors
——Any comments greatly appreciated
——Talking is cheap, show me the code
——诚心欢迎各位交流讨论!QQ:1138517609
——CSDN:https://blog.csdn.net/u011489043
——GitHub:https://github.com/selfconzrr