您的位置:首页 > Web前端

浅析ButterKnife的实现 (四) —— OnClick

2016-08-15 18:05 204 查看
相关文章:

浅析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
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息