浅析ButterKnife的实现 (四) —— OnClick
2016-08-15 18:05
204 查看
相关文章:
浅析ButterKnife的实现 (一) —— 搭建开发框架
浅析ButterKnife的实现 (二) —— BindResource
浅析ButterKnife的实现 (三) —— BindView
讲完了View注解,下面来介绍怎么给View设置点击监听。
@OnClick
定义个用来设置点击监听的注解:
R.id.xxx,并且属性值是个数组,所以可以同时给多个View设置点击监听。该注解的目标对象是方法(ElementType.METHOD),并且属性有个默认值-1,这个后面会提到。有一点需要注意的是,实际开源项目中还定义了个元注解 @ListenerClass 来设置各种点击注解,这样在代码中就可以对所有监听器做统一处理,不过这样代码逻辑就变得有点复杂,这里我们只对 OnClick进行单独处理,这样代码会显得简单清晰,如果想了解更多内容可看开源源码。
再来看注解处理器的处理:
我们按步骤来说明:
1、获取注解的ID值并检测是否有重复 ID;
2、如果使用默认 NO_ID(-1),则ID列表只能存在一个并且将外围类作为要设置点击监听的对象,同样需要判断外围类的类型是 VIEW 的子类或接口
3、检测方法的参数和返回值类型,因为我们已经知道 OnClickListener接口的方法为void onClick(View v),所以我们可以根据这个来直接判断;
4、获取参数的确切类型并保存为 Parameter,参数类型必须为VIEW 的子类或接口;
5、生成 OnClickBinding 并添加到 BindingClass中;
来看下 Parameter 和 OnClickBinding 的定义:
Parameter 主要就是保存了参数的位置索引和参数类型,这个应该很好理解。
来看下代码生成:
的匿名内部类并实现 onClick 方法,最后就是在里面调用使用注解的方法了。
注意一点,一个ID是可以给多个方法进行注解的,不过要满足两点:返回值必须为void;方法必须不同,至少满足重载条件。这个也好理解,如果方法返回值不为void,那么多个方法同时调用你根本不知道该返回哪个值。第二点,如果方法名称、参数都相同,那IDE直接就提示错误了。
样例
同样来看下使用例子:
代码看起来会有点多,但功能上是没问题的。这里我再重新声明一点,正常情况下我们会把View的注入和设置监听放在一块来写,这也符合我们编码的习惯。我这里把它们拆开来写,只是纯粹让各个功能模块独立开来,这样学习的时候逻辑更简单点,思路也更清晰,更多内容可以看开源源码。
讲完这个,整个ButterKnife开源项目的设计原理其实都差不多了,其它的注解按照这个基本思路来理解应该不会有太大问题,后面暂时没想再整理这些内容了。
源码:ButterKnifeStudy
浅析ButterKnife的实现 (一) —— 搭建开发框架
浅析ButterKnife的实现 (二) —— BindResource
浅析ButterKnife的实现 (三) —— BindView
讲完了View注解,下面来介绍怎么给View设置点击监听。
@OnClick
定义个用来设置点击监听的注解:
/** * 点击注解 */ @Retention(RetentionPolicy.CLASS) @Target(ElementType.METHOD) public @interface OnClick { @IdRes int[] value() default -1; }这里同样用 @IdRes 限定了属性的取值范围为
R.id.xxx,并且属性值是个数组,所以可以同时给多个View设置点击监听。该注解的目标对象是方法(ElementType.METHOD),并且属性有个默认值-1,这个后面会提到。有一点需要注意的是,实际开源项目中还定义了个元注解 @ListenerClass 来设置各种点击注解,这样在代码中就可以对所有监听器做统一处理,不过这样代码逻辑就变得有点复杂,这里我们只对 OnClick进行单独处理,这样代码会显得简单清晰,如果想了解更多内容可看开源源码。
再来看注解处理器的处理:
@AutoService(Processor.class) public class ButterKnifeProcessor extends AbstractProcessor { @Override public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) { // 略... // 处理OnClick for (Element element : roundEnv.getElementsAnnotatedWith(OnClick.class)) { if (VerifyHelper.verifyOnClick(element, messager)) { ParseHelper.parseOnClick(element, targetClassMap, erasedTargetNames, elementUtils, typeUtils, messager); } } // 略... return true; } @Override public Set<String> getSupportedAnnotationTypes() { Set<String> annotations = new LinkedHashSet<>(); annotations.add(BindString.class.getCanonicalName()); annotations.add(BindColor.class.getCanonicalName()); annotations.add(Bind.class.getCanonicalName()); annotations.add(OnClick.class.getCanonicalName()); return annotations; } }一样的代码模板,我们只需要关注解析方法就行了,检测也没特别要解释的。直接来看解析处理:
public final class ParseHelper { static final int NO_ID = -1; /** * 解析 OnClick 资源 * * @param element 使用注解的元素 * @param targetClassMap 映射表 * @param elementUtils 元素工具类 */ public static void parseOnClick(Element element, Map<TypeElement, BindingClass> targetClassMap, Set<TypeElement> erasedTargetNames, Elements elementUtils, Types typeUtils, Messager messager) { ExecutableElement executableElement = (ExecutableElement) element; TypeElement enclosingElement = (TypeElement) element.getEnclosingElement(); int[] ids = element.getAnnotation(OnClick.class).value(); String name = executableElement.getSimpleName().toString(); // 检测是否有重复 ID Integer duplicateId = _findDuplicate(ids); if (duplicateId != null) { _error(messager, element, "@%s annotation contains duplicate ID %d. (%s.%s)", Bind.class.getSimpleName(), duplicateId, enclosingElement.getQualifiedName(), element.getSimpleName()); return; } // 如果未设置ID则默认为 NO_ID,则把外围类作为要设置点击的对象 for (int id : ids) { if (id == NO_ID) { // 有 NO_ID 则只能有一个ID if (ids.length > 1) { _error(messager, element, "@%s annotation contains invalid ID %d. (%s.%s)", OnClick.class.getSimpleName(), NO_ID, enclosingElement.getQualifiedName(), element.getSimpleName()); return; } // 判断外围类的类型是 VIEW 的子类或接口 if (!_isSubtypeOfType(enclosingElement.asType(), VIEW_TYPE) && !_isInterface(enclosingElement.asType())) { _error(messager, element, "@%s annotation without an ID may only be used with an object of type " + "\"%s\" or an interface. (%s.%s)", OnClick.class.getSimpleName(), VIEW_TYPE, enclosingElement.getQualifiedName(), element.getSimpleName()); return; } } } List<? extends VariableElement> methodParameters = executableElement.getParameters(); // OnClickListener 的方法void onClick(View v),只有一个参数 View,所有我们的方法不能超过1个参数,可以为0 if (methodParameters.size() > 1) { _error(messager, element, "@%s methods can have at most 1 parameter(s). (%s.%s)", OnClick.class.getSimpleName(), enclosingElement.getQualifiedName(), element.getSimpleName()); return; } // 返回值类型也要满足 OnClickListener 的方法void onClick(View v) TypeMirror returnType = executableElement.getReturnType(); if (returnType instanceof TypeVariable) { TypeVariable typeVariable = (TypeVariable) returnType; returnType = typeVariable.getUpperBound(); } if (returnType.getKind() != TypeKind.VOID) { _error(messager, element, "@%s methods must have a 'viod' return type. (%s.%s)", OnClick.class.getSimpleName(), enclosingElement.getQualifiedName(), element.getSimpleName()); return; } Parameter[] parameters = Parameter.NONE; // 我们已经知道方法参数最多只能有一个,且必须为 View 的子类或接口 if (!methodParameters.isEmpty()) { parameters = new Parameter[1]; TypeMirror typeMirror = methodParameters.get(0).asType(); // 泛型处理 if (typeMirror instanceof TypeVariable) { TypeVariable typeVariable = (TypeVariable) returnType; typeMirror = typeVariable.getUpperBound(); } // 必须为 View 的子类或接口 if (!_isSubtypeOfType(typeMirror, VIEW_TYPE) && !_isInterface(typeMirror)) { _error(messager, element, "Unable to match @%s method arguments. (%s.%s)", OnClick.class.getSimpleName(), enclosingElement.getQualifiedName(), element.getSimpleName()); return; } parameters[0] = new Parameter(0, TypeName.get(typeMirror)); } OnClickBinding methodViewBinding = new OnClickBinding(name, Arrays.asList(parameters)); BindingClass bindingClass = _getOrCreateTargetClass(element, targetClassMap, elementUtils); for (int id : ids) { bindingClass.addMethodBinding(id, methodViewBinding); } erasedTargetNames.add(enclosingElement); } }
我们按步骤来说明:
1、获取注解的ID值并检测是否有重复 ID;
2、如果使用默认 NO_ID(-1),则ID列表只能存在一个并且将外围类作为要设置点击监听的对象,同样需要判断外围类的类型是 VIEW 的子类或接口
3、检测方法的参数和返回值类型,因为我们已经知道 OnClickListener接口的方法为void onClick(View v),所以我们可以根据这个来直接判断;
4、获取参数的确切类型并保存为 Parameter,参数类型必须为VIEW 的子类或接口;
5、生成 OnClickBinding 并添加到 BindingClass中;
来看下 Parameter 和 OnClickBinding 的定义:
/** * 方法的参数 */ final class Parameter { static final Parameter[] NONE = new Parameter[0]; // 参数的位置索引 private final int listenerPosition; // 参数的类型 private final TypeName type; Parameter(int listenerPosition, TypeName type) { this.listenerPosition = listenerPosition; this.type = type; } int getListenerPosition() { return listenerPosition; } TypeName getType() { return type; } public boolean requiresCast(String toType) { return !type.toString().equals(toType); } }
Parameter 主要就是保存了参数的位置索引和参数类型,这个应该很好理解。
/** * 点击方法信息 */ final class OnClickBinding implements ViewBinding { private final String name; private final List<Parameter> parameters; OnClickBinding(String name, List<Parameter> parameters) { this.name = name; this.parameters = Collections.unmodifiableList(new ArrayList<>(parameters)); } public String getName() { return name; } public List<Parameter> getParameters() { return parameters; } @Override public String getDescription() { return "method '" + name + "'"; } }OnClickBinding 也很好理解,保存了方法名和它的参数。
来看下代码生成:
public final class BindingClass { private static final ClassName ON_CLICK_LISTENER = ClassName.get("android.view.View", "OnClickListener"); private final Map<Integer, Set<OnClickBinding>> clickIdMap = new LinkedHashMap<>(); /** * 创建方法 * * @return MethodSpec */ private MethodSpec _createBindMethod() { // 略... if (_hasMethodBinding()) { result.addStatement("$T view", VIEW); for (Map.Entry<Integer, Set<OnClickBinding>> entry : clickIdMap.entrySet()) { int id = entry.getKey(); Set<OnClickBinding> bindings = entry.getValue(); // 获取 View,如果为 null 则不做点击绑定 result.addStatement("view = finder.findOptionalView(source, $L, null)", id); // 条件判断 View 是否为空 result.beginControlFlow("if (view != null)"); // 匿名内部类,实现 OnClickListener TypeSpec.Builder callback = TypeSpec.anonymousClassBuilder("") .superclass(ON_CLICK_LISTENER); // 实现 onClick 方法 MethodSpec.Builder methodBuilder = MethodSpec.methodBuilder("onClick") .addAnnotation(Override.class) .addModifiers(Modifier.PUBLIC) .addParameter(VIEW, "v"); // 调用绑定的目标方法 for (OnClickBinding binding : bindings) { List<Parameter> parameters = binding.getParameters(); if (parameters.isEmpty()) { methodBuilder.addStatement("target.$L()", binding.getName()); } else { // 这里我们已知 Parameter 最多只有一个,不然要做循环处理 // TypeName typeName = parameters.get(0).getType(); 可以用来做类型转换处理, // 调用 finder.<$T>castParam(),传的参数太多我直接用 View 了 methodBuilder.addStatement("target.$L(v)", binding.getName()); } } callback.addMethod(methodBuilder.build()); // 添加 setOnClickListener,记得介绍条件判断 result.addStatement("view.setOnClickListener($L)", callback.build()); result.endControlFlow(); } } return result.build(); } /** * 添加 MethodBinding * * @param binding 资源信息 */ public void addMethodBinding(int id, OnClickBinding binding) { Set<OnClickBinding> methodViewBindings = clickIdMap.get(id); if (methodViewBindings == null) { methodViewBindings = new LinkedHashSet<>(); methodViewBindings.add(binding); clickIdMap.put(id, methodViewBindings); } } private boolean _hasMethodBinding() { return !clickIdMap.isEmpty(); } }可以看到,这里通过 finder.findOptionalView() 来查找对应的 View,注意和 finder.findRequiredView() 区分开来,前者对于找到 null 不会报错,后者会报错并停止程序运行。然后进行 if (view != null)条件判断,如果符合则创建 OnClickListener
的匿名内部类并实现 onClick 方法,最后就是在里面调用使用注解的方法了。
注意一点,一个ID是可以给多个方法进行注解的,不过要满足两点:返回值必须为void;方法必须不同,至少满足重载条件。这个也好理解,如果方法返回值不为void,那么多个方法同时调用你根本不知道该返回哪个值。第二点,如果方法名称、参数都相同,那IDE直接就提示错误了。
样例
同样来看下使用例子:
public class MainActivity extends AppCompatActivity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); ButterKnife.bind(this); } @OnClick({R.id.btn_one, R.id.btn_two}) void onClick(View v) { switch (v.getId()) { case R.id.btn_one: Toast.makeText(MainActivity.this, "Button One", Toast.LENGTH_SHORT).show(); break; case R.id.btn_two: Toast.makeText(MainActivity.this, "Button Two", Toast.LENGTH_SHORT).show(); break; } } @OnClick(R.id.btn_three) void showToast() { Toast.makeText(MainActivity.this, "Button Three", Toast.LENGTH_SHORT).show(); } }看下生成的代码:
public class MainActivity$$ViewBinder<T extends MainActivity> implements ViewBinder<T> { @Override @SuppressWarnings("ResourceType") public void bind(final Finder finder, final T target, Object source) { View view; view = finder.findOptionalView(source, 2131492945, null); if (view != null) { view.setOnClickListener(new OnClickListener() { @Override public void onClick(View v) { target.onClick(v); } }); } view = finder.findOptionalView(source, 2131492946, null); if (view != null) { view.setOnClickListener(new OnClickListener() { @Override public void onClick(View v) { target.onClick(v); } }); } view = finder.findOptionalView(source, 2131492947, null); if (view != null) { view.setOnClickListener(new OnClickListener() { @Override public void onClick(View v) { target.showToast(); } }); } } }
代码看起来会有点多,但功能上是没问题的。这里我再重新声明一点,正常情况下我们会把View的注入和设置监听放在一块来写,这也符合我们编码的习惯。我这里把它们拆开来写,只是纯粹让各个功能模块独立开来,这样学习的时候逻辑更简单点,思路也更清晰,更多内容可以看开源源码。
讲完这个,整个ButterKnife开源项目的设计原理其实都差不多了,其它的注解按照这个基本思路来理解应该不会有太大问题,后面暂时没想再整理这些内容了。
源码:ButterKnifeStudy
相关文章推荐
- 为radio类型的INPUT添加客户端脚本(附加实现JS来禁用onClick事件思路代码)
- 用onclick事件实现文章可用大字体中字体小字体显示网页教程
- button的OnClickListener的三种实现方法
- Android中三种onClick事件的实现,与对比
- Android知识整理(2)【转】android中三种onClick事件的实现方式与对比
- 通过在xml布局文件中设置android:onClick=""来实现组件单击事件
- android中三种onClick事件的实现,与对比
- A标签中通过href和onclick传递的this对象实现思路
- onTouch 事件与onClick事件发生冲突,如何在onTouch事件中实现点击事件
- onclick时实现textarea值的取得
- Android开发中onClick事件的几种实现,分析,对比
- 用src属性动态替换图片;图片预加载---鼠标事件实现图片翻转效果;随机显示图片和onClick事件
- Android开发中onClick事件的几种实现,分析,对比
- 用onclick事件实现改变文章字体大小
- 怎么改变Treeview中的图标? OnClick事件Click获取Node.text 批量处理及实现TreeView结点拖拽的实例
- [网络收集]给radio类型的INPUT添加客户端脚本 --附加实现JS来禁用onClick事件思路代码
- 菜单样式-实现onclick改变背景色效果
- Android中button实现onclicklistener事件的两种方法
- button动态传值给onclick实现页面内容的动态展现
- Android中三种onClick事件的实现与对比