Butter knife的使用介绍与源码分析
2015-08-15 15:53
761 查看
Butter Knife
Butter Knife是基于安卓的视图依赖注入框架,其原理是使用编译前注解处理生成相关辅助代码,在运行时进行辅助类的加载从而调用相关方法完成视图的注入。由于其是采用在源码编译时进行注解的处理,而非运行时再处理,所以对应用的性能影响不大。使用
它可以使你的代码更为整洁、优雅,同时在很大程度上加快你的编程速率,把你从繁琐的findViewById中解放出来。
下载
使用Android studio:compile 'com.jakewharton:butterknife:7.0.1'
使用方法
你可以在Activity中这样查找需要的view:class ExampleActivity extends Activity { @Bind(R.id.title) TextView title; @Bind(R.id.subtitle) TextView subtitle; @Bind(R.id.footer) TextView footer; @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.simple_activity); ButterKnife.bind(this); // TODO Use fields... } }
就如上面所看到的那样,在控件字段上使用@Bind注解并注明资源Id,butter knife就会自动地帮你注入需要的view。现在7.0.1版本
已经由以前的@InjectView改为@Bind了,所以是不是应该叫做视图绑定更为合适呢?下面我就称为视图绑定吧。
值得注意的是,需要在setContentView方法之后加上
ButterKnife.bind(this);
这样butter knife才会工作。
同样你也可以在Fragment中使用butter knife进行视图的绑定。
public class FancyFragment extends Fragment { @Bind(R.id.button1) Button button1; @Bind(R.id.button2) Button button2; @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { View view = inflater.inflate(R.layout.fancy_fragment, container, false); ButterKnife.bind(this, view); // TODO Use fields... return view; } }
不同于Activity,你需要在onCreateView方法中使用
ButterKnife.bind(this, view);
进行视图的绑定。
由于Fragment的生命周期不同于Activity,当你在onCreateView方法中绑定视图时,你需要在onDestroyView方法里面把对应的视图设置为null。
非常幸运的是,butter knife有一个unbind方法去自动做这件事。
@Override public void onDestroyView() { super.onDestroyView(); ButterKnife.unbind(this); }
在list adapter里面使用ViewHolder模式时,
public class MyAdapter extends BaseAdapter { @Override public View getView(int position, View view, ViewGroup parent) { ViewHolder holder; if (view != null) { holder = (ViewHolder) view.getTag(); } else { view = inflater.inflate(R.layout.whatever, parent, false); holder = new ViewHolder(view); view.setTag(holder); } holder.name.setText("John Doe"); // etc... return view; } static class ViewHolder { @Bind(R.id.title) TextView name; @Bind(R.id.job_title) TextView jobTitle; public ViewHolder(View view) { ButterKnife.bind(this, view); } } }
也许你会有疑问,在自定义视图时butter knife能不能使用?
答案是肯定的,你可以在onFinishInflate()方法里面调用bind方法进行视图的绑定。
butter knife提供了Action和Setter两个接口让你去处理具有相同行为的一系列view。
比如在应用的设置页面,里面有个通知设定项,通知设定项下包含消费通知、过期通知、最新推送等项。
我现在有这样一个需求,当把通知设定项关闭之后,其下的消费通知、过期通知、最新推送等项应该是处于禁用状态。
使用butter knife,你就可以这样做:
@Bind({ R.id.consume_checkbox_view, R.id.expired_checkbox_view, R.id.latest_push_checkbox_view }) List<CheckedTextView> checkedTextViews; /** 使用Action接口 */ static final ButterKnife.Action<View> DISABLE = new ButterKnife.Action<View>() { @Override public void apply(View view, int index) { view.setEnabled(false); } }; ButterKnife.apply(checkedTextViews, DISABLE); /** 使用Setter接口 */ static final ButterKnife.Setter<View, Boolean> ENABLED = new ButterKnife.Setter<View, Boolean>() { @Override public void set(View view, Boolean value, int index) { view.setEnabled(value); } }; ButterKnife.apply(checkedTextViews, ENABLED, false);
是不是感觉代码整洁优雅多了呢?
使用apply方法还可以实现view的渐变动画效果,需要 Api 14 以上版本才支持:
ButterKnife.apply(checkedTextViews, View.ALPHA, 0.0f);
使用butter knife还可以实现view的各种事件监听绑定,就行下面这样
@OnClick(R.id.submit) public void submit() { // TODO submit data to server... } @OnClick(R.id.submit) public void submit(View view) { // TODO submit data to server... } @OnClick(R.id.submit) public void sayHi(Button button) { button.setText("Hello!"); } @OnClick({ R.id.door1, R.id.door2, R.id.door3 }) public void pickDoor(DoorView door) { if (door.hasPrizeBehind()) { Toast.makeText(this, "You win!", LENGTH_SHORT).show(); } else { Toast.makeText(this, "Try again", LENGTH_SHORT).show(); } }
自定义view的事件监听绑定则是这样的
public class FancyButton extends Button { @OnClick public void onClick() { // TODO do something! } }
特别的,当一个事件的监听有多个回调函数时,可以这样处理:
@OnItemSelected(R.id.list_view) void onItemSelected(int position) { // TODO ... } @OnItemSelected(value = R.id.maybe_missing, callback = NOTHING_SELECTED) void onNothingSelected() { // TODO ... }
默认的,所有进行绑定的view都是必须的,当找不到对应的资源Id时就会抛出异常。为了处理在对应的view找不到而发生异常这种情况,butter knife
建议使用Android的 “support-annotations” 库的@Nullable注解声明当前view可为null的。
@Nullable @Bind(R.id.might_not_be_there) TextView mightNotBeThere; @Nullable @OnClick(R.id.maybe_missing) void onMaybeMissingClicked() { // TODO ... }
当然,butter knife也提供了最原始的方法让你进行view的查找
View view = LayoutInflater.from(context).inflate(R.layout.thing, null); TextView firstName = ButterKnife.findById(view, R.id.first_name); TextView lastName = ButterKnife.findById(view, R.id.last_name); ImageView photo = ButterKnife.findById(view, R.id.photo);
butter knife除了提供view和listener的绑定,还提供了各种资源文件的绑定
@BindString(R.string.login_error) String loginErrorMessage; @BindBool(R.bool.is_login) boolean isLogin; ...
下面是支持的资源文件的绑定注解列表。
New: Resource binding annotations!
*
@BindBoolbinds an
R.boolID to a
booleanfield.
*
@BindColorbinds an
R.colorID to an
intor
ColorStateListfield.
*
@BindDimenbinds an
R.dimenID to an
int(for pixel size) or
float(for exact value) field.
*
@BindDrawablebinds an
R.drawableID to a
Drawablefield.
*
@BindIntbinds an
R.intID to an
intfield.
*
@BindStringbinds an
R.stringID to a
Stringfield.
看到这里,你就不想试一试吗?
源码解析
先来看一系列的注解吧/** * Bind a field to the view for the specified ID. The view will automatically be cast to the field * type. * <pre><code> * {@literal @}Bind(R.id.title) TextView title; * </code></pre> */ @Retention(CLASS) @Target(FIELD) public @interface Bind { /** View ID to which the field will be bound. */ int[] value(); } /** * Bind a field to the specified array resource ID. The type of array will be inferred from the * annotated element. * * String array: * <pre><code> * {@literal @}BindArray(R.array.countries) String[] countries; * </code></pre> * * Int array: * <pre><code> * {@literal @}BindArray(R.array.phones) int[] phones; * </code></pre> * * Text array: * <pre><code> * {@literal @}BindArray(R.array.options) CharSequence[] options; * </code></pre> * * {@link android.content.res.TypedArray}: * <pre><code> * {@literal @}BindArray(R.array.icons) TypedArray icons; * </code></pre> */ @Retention(CLASS) @Target(FIELD) public @interface BindArray { /** Array resource ID to which the field will be bound. */ int value(); } /** * Bind a field to a {@link Bitmap} from the specified drawable resource ID. * <pre><code> * {@literal @}BindBitmap(R.drawable.logo) Bitmap logo; * </code></pre> */ @Retention(CLASS) @Target(FIELD) public @interface BindBitmap { /** Drawable resource ID from which the {@link Bitmap} will be created. */ int value(); } /** * Bind a field to the specified boolean resource ID. * <pre><code> * {@literal @}BindBool(R.bool.is_tablet) boolean isTablet; * </code></pre> */ @Retention(CLASS) @Target(FIELD) public @interface BindBool { /** Boolean resource ID to which the field will be bound. */ int value(); } /** * Bind a field to the specified color resource ID. Type can be {@code int} or * {@link android.content.res.ColorStateList}. * <pre><code> * {@literal @}BindColor(R.color.background_green) int green; * {@literal @}BindColor(R.color.background_green_selector) ColorStateList greenSelector; * </code></pre> */ @Retention(CLASS) @Target(FIELD) public @interface BindColor { /** Color resource ID to which the field will be bound. */ int value(); } /** * Bind a field to the specified dimension resource ID. Type can be {@code int} for pixel size or * {@code float} for exact amount. * <pre><code> * {@literal @}BindDimen(R.dimen.horizontal_gap) int gapPx; * {@literal @}BindDimen(R.dimen.horizontal_gap) float gap; * </code></pre> */ @Retention(CLASS) @Target(FIELD) public @interface BindDimen { /** Dimension resource ID to which the field will be bound. */ int value(); } /** * Bind a field to the specified drawable resource ID. * <pre><code> * {@literal @}BindDrawable(R.drawable.placeholder) Drawable placeholder; * </code></pre> */ @Retention(CLASS) @Target(FIELD) public @interface BindDrawable { /** Drawable resource ID to which the field will be bound. */ int value(); } /** * Bind a field to the specified integer resource ID. * <pre><code> * {@literal @}BindInt(R.int.columns) int columns; * </code></pre> */ @Retention(CLASS) @Target(FIELD) public @interface BindInt { /** Integer resource ID to which the field will be bound. */ int value(); } /** * Bind a field to the specified string resource ID. * <pre><code> * {@literal @}BindString(R.string.username_error) String usernameErrorText; * </code></pre> */ @Retention(CLASS) @Target(FIELD) public @interface BindString { /** String resource ID to which the field will be bound. */ int value(); }
上面的一系列注解配合着注释看,应该没什么问题。需要注意的是,它们的RetentionPolicy都是CLASS级别的,即编译时被处理。
下面是一系列的监听器类注解定义:
@Retention(RUNTIME) @Target(FIELD) public @interface ListenerMethod { /** Name of the listener method for which this annotation applies. */ String name(); //监听方法的名称 /** List of method parameters. If the type is not a primitive it must be fully-qualified. */ String[] parameters() default { };//方法参数 /** Primitive or fully-qualified return type of the listener method. May also be {@code void}. */ String returnType() default "void";//方法默认返回类型 /** If {@link #returnType()} is not {@code void} this value is returned when no binding exists. */ String defaultReturn() default "null";//方法默认返回值 } @Retention(RUNTIME) @Target(ANNOTATION_TYPE) public @interface ListenerClass { String targetType();//view的类型 /** Name of the setter method on the {@link #targetType() target type} for the listener. */ String setter();//设置器名称 /** Fully-qualified class name of the listener type. */ String type();//监听类名称 /** Enum which declares the listener callback methods. Mutually exclusive to {@link #method()}. */ Class<? extends Enum<?>> callbacks() default NONE.class;//监听方法可以有多个回调,默认是空回调 /** * Method data for single-method listener callbacks. Mutually exclusive with {@link #callbacks()} * and an error to specify more than one value. */ ListenerMethod[] method() default { };//监听的方法声明 /** Default value for {@link #callbacks()}. */ enum NONE { } } /** * Bind a method to an {@link OnCheckedChangeListener OnCheckedChangeListener} on the view for * each ID specified. * <pre><code> * {@literal @}OnCheckedChanged(R.id.example) void onChecked(boolean checked) { * Toast.makeText(this, checked ? "Checked!" : "Unchecked!", Toast.LENGTH_SHORT).show(); * } * </code></pre> * Any number of parameters from * {@link OnCheckedChangeListener#onCheckedChanged(android.widget.CompoundButton, boolean) * onCheckedChanged} may be used on the method. * * @see OnCheckedChangeListener */ @Target(METHOD) @Retention(CLASS) @ListenerClass( targetType = "android.widget.CompoundButton", setter = "setOnCheckedChangeListener", type = "android.widget.CompoundButton.OnCheckedChangeListener", method = @ListenerMethod( name = "onCheckedChanged", parameters = { "android.widget.CompoundButton", "boolean" } ) ) public @interface OnCheckedChanged { /** View IDs to which the method will be bound. */ int[] value() default { View.NO_ID }; } /** * Bind a method to an {@link OnClickListener OnClickListener} on the view for each ID specified. * <pre><code> * {@literal @}OnClick(R.id.example) void onClick() { * Toast.makeText(this, "Clicked!", Toast.LENGTH_SHORT).show(); * } * </code></pre> * Any number of parameters from * {@link OnClickListener#onClick(android.view.View) onClick} may be used on the * method. * * @see OnClickListener */ @Target(METHOD) @Retention(CLASS) @ListenerClass( targetType = "android.view.View", setter = "setOnClickListener", type = "butterknife.internal.DebouncingOnClickListener", method = @ListenerMethod( name = "doClick", parameters = "android.view.View" ) ) public @interface OnClick { /** View IDs to which the method will be bound. */ int[] value() default { View.NO_ID }; } /** * Bind a method to an {@link OnEditorActionListener OnEditorActionListener} on the view for each * ID specified. * <pre><code> * {@literal @}OnEditorAction(R.id.example) boolean onEditorAction(KeyEvent key) { * Toast.makeText(this, "Pressed: " + key, Toast.LENGTH_SHORT).show(); * return true; * } * </code></pre> * Any number of parameters from * {@link OnEditorActionListener#onEditorAction(android.widget.TextView, int, android.view.KeyEvent) * onEditorAction} may be used on the method. * * @see OnEditorActionListener */ @Target(METHOD) @Retention(CLASS) @ListenerClass( targetType = "android.widget.TextView", setter = "setOnEditorActionListener", type = "android.widget.TextView.OnEditorActionListener", method = @ListenerMethod( name = "onEditorAction", parameters = { "android.widget.TextView", "int", "android.view.KeyEvent" }, returnType = "boolean", defaultReturn = "false" ) ) public @interface OnEditorAction { /** View IDs to which the method will be bound. */ int[] value() default { View.NO_ID }; } /** * Bind a method to an {@link OnFocusChangeListener OnFocusChangeListener} on the view for each ID * specified. * <pre><code> * {@literal @}OnFocusChange(R.id.example) void onFocusChanged(boolean focused) { * Toast.makeText(this, focused ? "Gained focus" : "Lost focus", Toast.LENGTH_SHORT).show(); * } * </code></pre> * Any number of parameters from {@link OnFocusChangeListener#onFocusChange(android.view.View, * boolean) onFocusChange} may be used on the method. * * @see OnFocusChangeListener */ @Target(METHOD) @Retention(CLASS) @ListenerClass( targetType = "android.view.View", setter = "setOnFocusChangeListener", type = "android.view.View.OnFocusChangeListener", method = @ListenerMethod( name = "onFocusChange", parameters = { "android.view.View", "boolean" } ) ) public @interface OnFocusChange { /** View IDs to which the method will be bound. */ int[] value() default { View.NO_ID }; } /** * Bind a method to an {@link OnItemClickListener OnItemClickListener} on the view for each ID * specified. * <pre><code> * {@literal @}OnItemClick(R.id.example_list) void onItemClick(int position) { * Toast.makeText(this, "Clicked position " + position + "!", Toast.LENGTH_SHORT).show(); * } * </code></pre> * Any number of parameters from {@link OnItemClickListener#onItemClick(android.widget.AdapterView, * android.view.View, int, long) onItemClick} may be used on the method. * * @see OnItemClickListener */ @Target(METHOD) @Retention(CLASS) @ListenerClass( targetType = "android.widget.AdapterView<?>", setter = "setOnItemClickListener", type = "android.widget.AdapterView.OnItemClickListener", method = @ListenerMethod( name = "onItemClick", parameters = { "android.widget.AdapterView<?>", "android.view.View", "int", "long" } ) ) public @interface OnItemClick { /** View IDs to which the method will be bound. */ int[] value() default { View.NO_ID }; } /** * Bind a method to an {@link OnItemLongClickListener OnItemLongClickListener} on the view for each * ID specified. * <pre><code> * {@literal @}OnItemLongClick(R.id.example_list) boolean onItemLongClick(int position) { * Toast.makeText(this, "Long clicked position " + position + "!", Toast.LENGTH_SHORT).show(); * return true; * } * </code></pre> * Any number of parameters from * {@link OnItemLongClickListener#onItemLongClick(android.widget.AdapterView, android.view.View, * int, long) onItemLongClick} may be used on the method. * * @see OnItemLongClickListener */ @Target(METHOD) @Retention(CLASS) @ListenerClass( targetType = "android.widget.AdapterView<?>", setter = "setOnItemLongClickListener", type = "android.widget.AdapterView.OnItemLongClickListener", method = @ListenerMethod( name = "onItemLongClick", parameters = { "android.widget.AdapterView<?>", "android.view.View", "int", "long" }, returnType = "boolean", defaultReturn = "false" ) ) public @interface OnItemLongClick { /** View IDs to which the method will be bound. */ int[] value() default { View.NO_ID }; } /** * Bind a method to an {@link OnLongClickListener OnLongClickListener} on the view for each ID * specified. * <pre><code> * {@literal @}OnLongClick(R.id.example) boolean onLongClick() { * Toast.makeText(this, "Long clicked!", Toast.LENGTH_SHORT).show(); * return true; * } * </code></pre> * Any number of parameters from {@link OnLongClickListener#onLongClick(android.view.View)} may be * used on the method. * * @see OnLongClickListener */ @Retention(CLASS) @Target(METHOD) @ListenerClass( targetType = "android.view.View", setter = "setOnLongClickListener", type = "android.view.View.OnLongClickListener", method = @ListenerMethod( name = "onLongClick", parameters = { "android.view.View" }, returnType = "boolean", defaultReturn = "false" ) ) public @interface OnLongClick { /** View IDs to which the method will be bound. */ int[] value() default { View.NO_ID }; } /** * Bind a method to an {@link OnTouchListener OnTouchListener} on the view for each ID specified. * <pre><code> * {@literal @}OnTouch(R.id.example) boolean onTouch() { * Toast.makeText(this, "Touched!", Toast.LENGTH_SHORT).show(); * return false; * } * </code></pre> * Any number of parameters from * {@link OnTouchListener#onTouch(android.view.View, android.view.MotionEvent) onTouch} may be used * on the method. * * @see OnTouchListener */ @Target(METHOD) @Retention(CLASS) @ListenerClass( targetType = "android.view.View", setter = "setOnTouchListener", type = "android.view.View.OnTouchListener", method = @ListenerMethod( name = "onTouch", parameters = { "android.view.View", "android.view.MotionEvent" }, returnType = "boolean", defaultReturn = "false" ) ) public @interface OnTouch { /** View IDs to which the method will be bound. */ int[] value() default { View.NO_ID }; } /** * Bind a method to an {@link TextWatcher TextWatcher} on the view for each ID specified. * <pre><code> * {@literal @}OnTextChanged(R.id.example) void onTextChanged(CharSequence text) { * Toast.makeText(this, "Text changed: " + text, Toast.LENGTH_SHORT).show(); * } * </code></pre> * Any number of parameters from {@link TextWatcher#onTextChanged(CharSequence, int, int, int) * onTextChanged} may be used on the method. * <p> * To bind to methods other than {@code onTextChanged}, specify a different {@code callback}. * <pre><code> * {@literal @}OnTextChanged(value = R.id.example, callback = BEFORE_TEXT_CHANGED) * void onBeforeTextChanged(CharSequence text) { * Toast.makeText(this, "Before text changed: " + text, Toast.LENGTH_SHORT).show(); * } * </code></pre> * * @see TextWatcher */ @Target(METHOD) @Retention(CLASS) @ListenerClass( targetType = "android.widget.TextView", setter = "addTextChangedListener", type = "android.text.TextWatcher", //可以看到TextWatcher方法有三个回调 callbacks = OnTextChanged.Callback.class //默认是选择TextWatcher#onTextChanged这个回调方法,下面的OnPageChangeListener、OnItemSelectedListener类似。 ) public @interface OnTextChanged { /** View IDs to which the method will be bound. */ int[] value() default { View.NO_ID }; /** Listener callback to which the method will be bound. */ Callback callback() default Callback.TEXT_CHANGED; /** {@link TextWatcher} callback methods. */ enum Callback { /** {@link TextWatcher#onTextChanged(CharSequence, int, int, int)} */ @ListenerMethod( name = "onTextChanged", parameters = { "java.lang.CharSequence", "int", "int", "int" } ) TEXT_CHANGED, /** {@link TextWatcher#beforeTextChanged(CharSequence, int, int, int)} */ @ListenerMethod( name = "beforeTextChanged", parameters = { "java.lang.CharSequence", "int", "int", "int" } ) BEFORE_TEXT_CHANGED, /** {@link TextWatcher#afterTextChanged(android.text.Editable)} */ @ListenerMethod( name = "afterTextChanged", parameters = "android.text.Editable" ) AFTER_TEXT_CHANGED, } } /** * Bind a method to an {@code OnPageChangeListener} on the view for each ID specified. * <pre><code> * {@literal @}OnPageChange(R.id.example_pager) void onPageSelected(int position) { * Toast.makeText(this, "Selected " + position + "!", Toast.LENGTH_SHORT).show(); * } * </code></pre> * Any number of parameters from {@code onPageSelected} may be used on the method. * <p> * To bind to methods other than {@code onPageSelected}, specify a different {@code callback}. * <pre><code> * {@literal @}OnPageChange(value = R.id.example_pager, callback = PAGE_SCROLL_STATE_CHANGED) * void onPageStateChanged(int state) { * Toast.makeText(this, "State changed: " + state + "!", Toast.LENGTH_SHORT).show(); * } * </code></pre> */ @Target(METHOD) @Retention(CLASS) @ListenerClass( targetType = "android.support.v4.view.ViewPager", setter = "setOnPageChangeListener", type = "android.support.v4.view.ViewPager.OnPageChangeListener", callbacks = OnPageChange.Callback.class ) public @interface OnPageChange { /** View IDs to which the method will be bound. */ int[] value() default { View.NO_ID }; /** Listener callback to which the method will be bound. */ Callback callback() default Callback.PAGE_SELECTED; /** {@code ViewPager.OnPageChangeListener} callback methods. */ enum Callback { /** {@code onPageSelected(int)} */ @ListenerMethod( name = "onPageSelected", parameters = "int" ) PAGE_SELECTED, /** {@code onPageScrolled(int, float, int)} */ @ListenerMethod( name = "onPageScrolled", parameters = { "int", "float", "int" } ) PAGE_SCROLLED, /** {@code onPageScrollStateChanged(int)} */ @ListenerMethod( name = "onPageScrollStateChanged", parameters = "int" ) PAGE_SCROLL_STATE_CHANGED, } } /** * Bind a method to an {@link OnItemSelectedListener OnItemSelectedListener} on the view for each * ID specified. * <pre><code> * {@literal @}OnItemSelected(R.id.example_list) void onItemSelected(int position) { * Toast.makeText(this, "Selected position " + position + "!", Toast.LENGTH_SHORT).show(); * } * </code></pre> * Any number of parameters from * {@link OnItemSelectedListener#onItemSelected(android.widget.AdapterView, android.view.View, int, * long) onItemSelected} may be used on the method. * <p> * To bind to methods other than {@code onItemSelected}, specify a different {@code callback}. * <pre><code> * {@literal @}OnItemSelected(value = R.id.example_list, callback = NOTHING_SELECTED) * void onNothingSelected() { * Toast.makeText(this, "Nothing selected!", Toast.LENGTH_SHORT).show(); * } * </code></pre> * * @see OnItemSelectedListener */ @Target(METHOD) @Retention(CLASS) @ListenerClass( targetType = "android.widget.AdapterView<?>", setter = "setOnItemSelectedListener", type = "android.widget.AdapterView.OnItemSelectedListener", callbacks = OnItemSelected.Callback.class ) public @interface OnItemSelected { /** View IDs to which the method will be bound. */ int[] value() default { View.NO_ID }; /** Listener callback to which the method will be bound. */ Callback callback() default Callback.ITEM_SELECTED; /** {@link OnItemSelectedListener} callback methods. */ enum Callback { /** * {@link OnItemSelectedListener#onItemSelected(android.widget.AdapterView, android.view.View, * int, long)} */ @ListenerMethod( name = "onItemSelected", parameters = { "android.widget.AdapterView<?>", "android.view.View", "int", "long" } ) ITEM_SELECTED, /** {@link OnItemSelectedListener#onNothingSelected(android.widget.AdapterView)} */ @ListenerMethod( name = "onNothingSelected", parameters = "android.widget.AdapterView<?>" ) NOTHING_SELECTED } }
看了上面一大堆的注解定义是不是觉得晕乎乎的呢?
下面到了重头戏。
要在编译时解析Annotation,需要自定义一个类继承于javax.annotation.processing.AbstractProcessor,并且覆盖重写其中的几个方法。
private static final List<Class<? extends Annotation>> LISTENERS = Arrays.asList(// OnCheckedChanged.class, // OnClick.class, // OnEditorAction.class, // OnFocusChange.class, // OnItemClick.class, // OnItemLongClick.class, // OnItemSelected.class, // OnLongClick.class, // OnPageChange.class, // OnTextChanged.class, // OnTouch.class // ); /** 添加支持扫描的注解类型 */ @Override public Set<String> getSupportedAnnotationTypes() { Set<String> types = new LinkedHashSet<>(); types.add(Bind.class.getCanonicalName()); for (Class<? extends Annotation> listener : LISTENERS) { types.add(listener.getCanonicalName()); } types.add(BindArray.class.getCanonicalName()); types.add(BindBitmap.class.getCanonicalName()); types.add(BindBool.class.getCanonicalName()); types.add(BindColor.class.getCanonicalName()); types.add(BindDimen.class.getCanonicalName()); types.add(BindDrawable.class.getCanonicalName()); types.add(BindInt.class.getCanonicalName()); types.add(BindString.class.getCanonicalName()); return types; }
其中,处理主要逻辑的是下面这个方法
@Override public boolean process(Set<? extends TypeElement> elements, RoundEnvironment env) { /** 查找并且解析注解 */ Map<TypeElement, BindingClass> targetClassMap = findAndParseTargets(env); /** 循环拿出map中的键与值 */ for (Map.Entry<TypeElement, BindingClass> entry : targetClassMap.entrySet()) { TypeElement typeElement = entry.getKey(); BindingClass bindingClass = entry.getValue(); try { /** 写进文件,生成辅助类,这个放到后面分析 */ bindingClass.brewJava().writeTo(filer); } catch (IOException e) { error(typeElement, "Unable to write view binder for type %s: %s", typeElement, e.getMessage()); } } return true; }
可以看到,process方法中主要做了两件事:
1.查找并且解析注解,findAndParseTargets(env);
2.循环拿出map中的键与值,根据值写进文件,生成辅助类, bindingClass.brewJava().writeTo(filer)。
我们先来看看第一件事具体的处理。
private Map<TypeElement, BindingClass> findAndParseTargets(RoundEnvironment env) { Map<TypeElement, BindingClass> targetClassMap = new LinkedHashMap<>(); Set<String> erasedTargetNames = new LinkedHashSet<>(); // Process each @Bind element.解析每个@Bind元素 for (Element element : env.getElementsAnnotatedWith(Bind.class)) { try { parseBind(element, targetClassMap, erasedTargetNames); } catch (Exception e) { logParsingError(element, Bind.class, e); } } // Process each annotation that corresponds to a listener.解析每个监听器方法 for (Class<? extends Annotation> listener : LISTENERS) { findAndParseListener(env, listener, targetClassMap, erasedTargetNames); } // Process each @BindArray element.解析每个@BindArray元素 for (Element element : env.getElementsAnnotatedWith(BindArray.class)) { try { parseResourceArray(element, targetClassMap, erasedTargetNames); } catch (Exception e) { logParsingError(element, BindArray.class, e); } } // Process each @BindBitmap element.解析每个@BindBitmap元素 for (Element element : env.getElementsAnnotatedWith(BindBitmap.class)) { try { parseResourceBitmap(element, targetClassMap, erasedTargetNames); } catch (Exception e) { logParsingError(element, BindBitmap.class, e); } } // Process each @BindBool element.解析每个@BindBool元素 for (Element element : env.getElementsAnnotatedWith(BindBool.class)) { try { parseResourceBool(element, targetClassMap, erasedTargetNames); } catch (Exception e) { logParsingError(element, BindBool.class, e); } } // Process each @BindColor element.解析每个@BindColor元素 for (Element element : env.getElementsAnnotatedWith(BindColor.class)) { try { parseResourceColor(element, targetClassMap, erasedTargetNames); } catch (Exception e) { logParsingError(element, BindColor.class, e); } } // Process each @BindDimen element.解析每个@BindDimen元素 for (Element element : env.getElementsAnnotatedWith(BindDimen.class)) { try { parseResourceDimen(element, targetClassMap, erasedTargetNames); } catch (Exception e) { logParsingError(element, BindDimen.class, e); } } // Process each @BindDrawable element.解析每个@BindDrawable元素 for (Element element : env.getElementsAnnotatedWith(BindDrawable.class)) { try { parseResourceDrawable(element, targetClassMap, erasedTargetNames); } catch (Exception e) { logParsingError(element, BindDrawable.class, e); } } // Process each @BindInt element.解析每个@BindInt元素 for (Element element : env.getElementsAnnotatedWith(BindInt.class)) { try { parseResourceInt(element, targetClassMap, erasedTargetNames); } catch (Exception e) { logParsingError(element, BindInt.class, e); } } // Process each @BindString element.解析每个@BindString元素 for (Element element : env.getElementsAnnotatedWith(BindString.class)) { try { parseResourceString(element, targetClassMap, erasedTargetNames); } catch (Exception e) { logParsingError(element, BindString.class, e); } } // Try to find a parent binder for each.查找是否已有父类进行绑定 for (Map.Entry<TypeElement, BindingClass> entry : targetClassMap.entrySet()) { String parentClassFqcn = findParentFqcn(entry.getKey(), erasedTargetNames); if (parentClassFqcn != null) { entry.getValue().setParentViewBinder(parentClassFqcn + BINDING_CLASS_SUFFIX); } } return targetClassMap; }
我们先来分析最简单的一个解析处理,即解析每个@BindString元素。
private void parseResourceString(Element element, Map<TypeElement, BindingClass> targetClassMap, Set<String> erasedTargetNames) { boolean hasError = false; TypeElement enclosingElement = (TypeElement) element.getEnclosingElement(); // Verify that the target type is String.校验字段类型是否为String类型。 if (!"java.lang.String".equals(element.asType().toString())) { error(element, "@%s field type must be 'String'. (%s.%s)", BindString.class.getSimpleName(), enclosingElement.getQualifiedName(), element.getSimpleName()); hasError = true; } // Verify common generated code restrictions. hasError |= isInaccessibleViaGeneratedCode(BindString.class, "fields", element); hasError |= isBindingInWrongPackage(BindString.class, element); if (hasError) { return; } // Assemble information on the field. String name = element.getSimpleName().toString();//字段名称 int id = element.getAnnotation(BindString.class).value();//资源id BindingClass bindingClass = getOrCreateTargetClass(targetClassMap, enclosingElement); FieldResourceBinding binding = new FieldResourceBinding(id, name, "getString");//封装到FieldResourceBinding类,其中第三个参数为方法名称,对应着context.getString(resId) bindingClass.addResource(binding); erasedTargetNames.add(enclosingElement.toString()); } /** 下面是isInaccessibleViaGeneratedCode方法的实现 */ private boolean isInaccessibleViaGeneratedCode(Class<? extends Annotation> annotationClass, String targetThing, Element element) { boolean hasError = false; TypeElement enclosingElement = (TypeElement) element.getEnclosingElement(); // Verify method modifiers.方法修饰符不能为private或static Set<Modifier> modifiers = element.getModifiers(); if (modifiers.contains(PRIVATE) || modifiers.contains(STATIC)) { error(element, "@%s %s must not be private or static. (%s.%s)", annotationClass.getSimpleName(), targetThing, enclosingElement.getQualifiedName(), element.getSimpleName()); hasError = true; } // Verify containing type.注解不能用于非Class中 if (enclosingElement.getKind() != CLASS) { error(enclosingElement, "@%s %s may only be contained in classes. (%s.%s)", annotationClass.getSimpleName(), targetThing, enclosingElement.getQualifiedName(), element.getSimpleName()); hasError = true; } // Verify containing class visibility is not private.当前类修饰符不能为private if (enclosingElement.getModifiers().contains(PRIVATE)) { error(enclosingElement, "@%s %s may not be contained in private classes. (%s.%s)", annotationClass.getSimpleName(), targetThing, enclosingElement.getQualifiedName(), element.getSimpleName()); hasError = true; } return hasError; } private BindingClass getOrCreateTargetClass(Map<TypeElement, BindingClass> targetClassMap, TypeElement enclosingElement) { BindingClass bindingClass = targetClassMap.get(enclosingElement); if (bindingClass == null) { String targetType = enclosingElement.getQualifiedName().toString(); String classPackage = getPackageName(enclosingElement); String className = getClassName(enclosingElement, classPackage) + BINDING_CLASS_SUFFIX;//生成的辅助类名称为 $$ViewBinder bindingClass = new BindingClass(classPackage, className, targetType); targetClassMap.put(enclosingElement, bindingClass); } return bindingClass; }
其中,解析每个@BindBool元素的方法parseResourceBool、解析每个@BindColor元素的方法parseResourceColor、解析每个@BindDimen元素的方法parseResourceDimen、
解析每个@BindBitmap元素的方法parseResourceBitmap、解析每个@BindDrawable元素的方法parseResourceDrawable、解析每个@BindInt元素的方法parseResourceInt、
解析每个@BindArray元素的方法parseResourceArray都和parseResourceString类似。
其中,解析@Bind元素和监听器类有点不一样。
先看看@Bind元素的解析处理。
private void parseBind(Element element, Map<TypeElement, BindingClass> targetClassMap, Set<String> erasedTargetNames) { // Verify common generated code restrictions. if (isInaccessibleViaGeneratedCode(Bind.class, "fields", element) || isBindingInWrongPackage(Bind.class, element)) { return; } TypeMirror elementType = element.asType(); if (elementType.getKind() == TypeKind.ARRAY) { //array类型 parseBindMany(element, targetClassMap, erasedTargetNames); } else if (LIST_TYPE.equals(doubleErasure(elementType))) { //list类型,@Bind({ R.id.consume_checkbox_view, R.id.expired_checkbox_view, R.id.latest_push_checkbox_view })List<CheckedTextView> checkedTextViews; parseBindMany(element, targetClassMap, erasedTargetNames); } else if (isSubtypeOfType(elementType, ITERABLE_TYPE)) { error(element, "@%s must be a List or array. (%s.%s)", Bind.class.getSimpleName(), ((TypeElement) element.getEnclosingElement()).getQualifiedName(), element.getSimpleName()); } else { parseBindOne(element, targetClassMap, erasedTargetNames); } } private void parseBindOne(Element element, Map<TypeElement, BindingClass> targetClassMap, Set<String> erasedTargetNames) { boolean hasError = false; TypeElement enclosingElement = (TypeElement) element.getEnclosingElement(); // Verify that the target type extends from View. TypeMirror elementType = element.asType(); if (elementType.getKind() == TypeKind.TYPEVAR) { TypeVariable typeVariable = (TypeVariable) elementType; elementType = typeVariable.getUpperBound(); } /** 是否为view类型或者接口 */ if (!isSubtypeOfType(elementType, VIEW_TYPE) && !isInterface(elementType)) { error(element, "@%s fields must extend from View or be an interface. (%s.%s)", Bind.class.getSimpleName(), enclosingElement.getQualifiedName(), element.getSimpleName()); hasError = true; } // Assemble information on the field.只能有一个资源id int[] ids = element.getAnnotation(Bind.class).value(); if (ids.length != 1) { error(element, "@%s for a view must only specify one ID. Found: %s. (%s.%s)", Bind.class.getSimpleName(), Arrays.toString(ids), enclosingElement.getQualifiedName(), element.getSimpleName()); hasError = true; } if (hasError) { return; } int id = ids[0]; BindingClass bindingClass = targetClassMap.get(enclosingElement); if (bindingClass != null) { ViewBindings viewBindings = bindingClass.getViewBinding(id); if (viewBindings != null) { Iterator<FieldViewBinding> iterator = viewBindings.getFieldBindings().iterator(); if (iterator.hasNext()) {//当前资源id是否已经绑定过 FieldViewBinding existingBinding = iterator.next(); error(element, "Attempt to use @%s for an already bound ID %d on '%s'. (%s.%s)", Bind.class.getSimpleName(), id, existingBinding.getName(), enclosingElement.getQualifiedName(), element.getSimpleName()); return; } } } else { bindingClass = getOrCreateTargetClass(targetClassMap, enclosingElement); } String name = element.getSimpleName().toString(); String type = elementType.toString(); boolean required = isRequiredBinding(element);//是否可为空 FieldViewBinding binding = new FieldViewBinding(name, type, required);//封装到FieldResourceBinding类,其中第三个参数为是否可为空,对应着@Nullable bindingClass.addField(id, binding); // Add the type-erased version to the valid binding targets set. erasedTargetNames.add(enclosingElement.toString()); } private void parseBindMany(Element element, Map<TypeElement, BindingClass> targetClassMap, Set<String> erasedTargetNames) { boolean hasError = false; TypeElement enclosingElement = (TypeElement) element.getEnclosingElement(); // Verify that the type is a List or an array.判断类型是否为数组或者列表 TypeMirror elementType = element.asType(); String erasedType = doubleErasure(elementType); TypeMirror viewType = null; FieldCollectionViewBinding.Kind kind; if (elementType.getKind() == TypeKind.ARRAY) { ArrayType arrayType = (ArrayType) elementType; viewType = arrayType.getComponentType(); kind = FieldCollectionViewBinding.Kind.ARRAY; } else if (LIST_TYPE.equals(erasedType)) { DeclaredType declaredType = (DeclaredType) elementType; List<? extends TypeMirror> typeArguments = declaredType.getTypeArguments(); if (typeArguments.size() != 1) { error(element, "@%s List must have a generic component. (%s.%s)", Bind.class.getSimpleName(), enclosingElement.getQualifiedName(), element.getSimpleName()); hasError = true; } else { viewType = typeArguments.get(0); } kind = FieldCollectionViewBinding.Kind.LIST; } else { throw new AssertionError(); } if (viewType != null && viewType.getKind() == TypeKind.TYPEVAR) { TypeVariable typeVariable = (TypeVariable) viewType; viewType = typeVariable.getUpperBound(); } // Verify that the target type extends from View.类型判断 if (viewType != null && !isSubtypeOfType(viewType, VIEW_TYPE) && !isInterface(viewType)) { error(element, "@%s List or array type must extend from View or be an interface. (%s.%s)", Bind.class.getSimpleName(), enclosingElement.getQualifiedName(), element.getSimpleName()); hasError = true; } if (hasError) { return; } // Assemble information on the field.参数判断 String name = element.getSimpleName().toString(); int[] ids = element.getAnnotation(Bind.class).value(); if (ids.length == 0) { error(element, "@%s must specify at least one ID. (%s.%s)", Bind.class.getSimpleName(), enclosingElement.getQualifiedName(), element.getSimpleName()); return; } Integer duplicateId = findDuplicate(ids); if (duplicateId != null) { error(element, "@%s annotation contains duplicate ID %d. (%s.%s)", Bind.class.getSimpleName(), duplicateId, enclosingElement.getQualifiedName(), element.getSimpleName()); } assert viewType != null; // Always false as hasError would have been true. String type = viewType.toString(); boolean required = isRequiredBinding(element); BindingClass bindingClass = getOrCreateTargetClass(targetClassMap, enclosingElement); FieldCollectionViewBinding binding = new FieldCollectionViewBinding(name, type, kind, required); bindingClass.addFieldCollection(ids, binding); erasedTargetNames.add(enclosingElement.toString()); }
解析监听器类则更为稍复杂一点。
private void findAndParseListener(RoundEnvironment env, Class<? extends Annotation> annotationClass, Map<TypeElement, BindingClass> targetClassMap, Set<String> erasedTargetNames) { /** 循环遍历每个监听器注解 */ for (Element element : env.getElementsAnnotatedWith(annotationClass)) { try { parseListenerAnnotation(annotationClass, element, targetClassMap, erasedTargetNames); } catch (Exception e) { StringWriter stackTrace = new StringWriter(); e.printStackTrace(new PrintWriter(stackTrace)); error(element, "Unable to generate view binder for @%s.\n\n%s", annotationClass.getSimpleName(), stackTrace.toString()); } } } private void parseListenerAnnotation(Class<? extends Annotation> annotationClass, Element element, Map<TypeElement, BindingClass> targetClassMap, Set<String> erasedTargetNames) throws Exception { // This should be guarded by the annotation's @Target but it's worth a check for safe casting.注解应该作用在方法级别上 if (!(element instanceof ExecutableElement) || element.getKind() != METHOD) { throw new IllegalStateException( String.format("@%s annotation must be on a method.", annotationClass.getSimpleName())); } ExecutableElement executableElement = (ExecutableElement) element; TypeElement enclosingElement = (TypeElement) element.getEnclosingElement(); // Assemble information on the method. Annotation annotation = element.getAnnotation(annotationClass); Method annotationValue = annotationClass.getDeclaredMethod("value"); if (annotationValue.getReturnType() != int[].class) {//资源id参数值应为int数组 throw new IllegalStateException( String.format("@%s annotation value() type not int[].", annotationClass)); } int[] ids = (int[]) annotationValue.invoke(annotation); String name = executableElement.getSimpleName().toString(); boolean required = isRequiredBinding(element);//是否可为空 // Verify that the method and its containing class are accessible via generated code. boolean hasError = isInaccessibleViaGeneratedCode(annotationClass, "methods", element); hasError |= isBindingInWrongPackage(annotationClass, element); Integer duplicateId = findDuplicate(ids); if (duplicateId != null) { error(element, "@%s annotation for method contains duplicate ID %d. (%s.%s)", annotationClass.getSimpleName(), duplicateId, enclosingElement.getQualifiedName(), element.getSimpleName()); hasError = true; } ListenerClass listener = annotationClass.getAnnotation(ListenerClass.class); if (listener == null) {//监听类不可为空 throw new IllegalStateException( String.format("No @%s defined on @%s.", ListenerClass.class.getSimpleName(), annotationClass.getSimpleName())); } for (int id : ids) { if (id == View.NO_ID) { if (ids.length == 1) {//资源id数组长度为1,即View.NO_ID,则不可使用@Nullable。 if (!required) { error(element, "ID-free binding must not be annotated with @Nullable. (%s.%s)", enclosingElement.getQualifiedName(), element.getSimpleName()); hasError = true; } // Verify target type is valid for a binding without an id. String targetType = listener.targetType(); if (!isSubtypeOfType(enclosingElement.asType(), targetType) && !isInterface(enclosingElement.asType())) {//targetType类型判断 error(element, "@%s annotation without an ID may only be used with an object of type " + "\"%s\" or an interface. (%s.%s)", annotationClass.getSimpleName(), targetType, enclosingElement.getQualifiedName(), element.getSimpleName()); hasError = true; } } else { error(element, "@%s annotation contains invalid ID %d. (%s.%s)", annotationClass.getSimpleName(), id, enclosingElement.getQualifiedName(), element.getSimpleName()); hasError = true; } } } ListenerMethod method; ListenerMethod[] methods = listener.method(); if (methods.length > 1) {//监听方法处理 throw new IllegalStateException(String.format("Multiple listener methods specified on @%s.", annotationClass.getSimpleName())); } else if (methods.length == 1) { if (listener.callbacks() != ListenerClass.NONE.class) { throw new IllegalStateException( String.format("Both method() and callback() defined on @%s.", annotationClass.getSimpleName())); } method = methods[0]; } else { Method annotationCallback = annotationClass.getDeclaredMethod("callback"); Enum<?> callback = (Enum<?>) annotationCallback.invoke(annotation); Field callbackField = callback.getDeclaringClass().getField(callback.name()); method = callbackField.getAnnotation(ListenerMethod.class); if (method == null) { throw new IllegalStateException( String.format("No @%s defined on @%s's %s.%s.", ListenerMethod.class.getSimpleName(), annotationClass.getSimpleName(), callback.getDeclaringClass().getSimpleName(), callback.name())); } } // Verify that the method has equal to or less than the number of parameters as the listener. List<? extends VariableElement> methodParameters = executableElement.getParameters(); if (methodParameters.size() > method.parameters().length) { error(element, "@%s methods can have at most %s parameter(s). (%s.%s)", annotationClass.getSimpleName(), method.parameters().length, enclosingElement.getQualifiedName(), element.getSimpleName()); hasError = true; } // Verify method return type matches the listener. TypeMirror returnType = executableElement.getReturnType(); if (returnType instanceof TypeVariable) { TypeVariable typeVariable = (TypeVariable) returnType; returnType = typeVariable.getUpperBound(); } if (!returnType.toString().equals(method.returnType())) { error(element, "@%s methods must have a '%s' return type. (%s.%s)", annotationClass.getSimpleName(), method.returnType(), enclosingElement.getQualifiedName(), element.getSimpleName()); hasError = true; } if (hasError) { return; } Parameter[] parameters = Parameter.NONE; if (!methodParameters.isEmpty()) { parameters = new Parameter[methodParameters.size()]; BitSet methodParameterUsed = new BitSet(methodParameters.size()); String[] parameterTypes = method.parameters(); for (int i = 0; i < methodParameters.size(); i++) { VariableElement methodParameter = methodParameters.get(i); TypeMirror methodParameterType = methodParameter.asType(); if (methodParameterType instanceof TypeVariable) { TypeVariable typeVariable = (TypeVariable) methodParameterType; methodParameterType = typeVariable.getUpperBound(); } for (int j = 0; j < parameterTypes.length; j++) { if (methodParameterUsed.get(j)) { continue; } if (isSubtypeOfType(methodParameterType, parameterTypes[j]) || isInterface(methodParameterType)) { parameters[i] = new Parameter(j, methodParameterType.toString()); methodParameterUsed.set(j); break; } } if (parameters[i] == null) { StringBuilder builder = new StringBuilder(); builder.append("Unable to match @") .append(annotationClass.getSimpleName()) .append(" method arguments. (") .append(enclosingElement.getQualifiedName()) .append('.') .append(element.getSimpleName()) .append(')'); for (int j = 0; j < parameters.length; j++) { Parameter parameter = parameters[j]; builder.append("\n\n Parameter #") .append(j + 1) .append(": ") .append(methodParameters.get(j).asType().toString()) .append("\n "); if (parameter == null) { builder.append("did not match any listener parameters"); } else { builder.append("matched listener parameter #") .append(parameter.getListenerPosition() + 1) .append(": ") .append(parameter.getType()); } } builder.append("\n\nMethods may have up to ") .append(method.parameters().length) .append(" parameter(s):\n"); for (String parameterType : method.parameters()) { builder.append("\n ").append(parameterType); } builder.append( "\n\nThese may be listed in any order but will be searched for from top to bottom."); error(executableElement, builder.toString()); return; } } } MethodViewBinding binding = new MethodViewBinding(name, Arrays.asList(parameters), required); BindingClass bindingClass = getOrCreateTargetClass(targetClassMap, enclosingElement); for (int id : ids) { if (!bindingClass.addMethod(id, listener, method, binding)) { error(element, "Multiple listener methods with return value specified for ID %d. (%s.%s)", id, enclosingElement.getQualifiedName(), element.getSimpleName()); return; } } // Add the type-erased version to the valid binding targets set. erasedTargetNames.add(enclosingElement.toString()); }
上面的方法主要是解析注解中的各种参数,然后封装到targetClassMap中。
再来看看第二件事具体的处理。此时,targetClassMap中的值bindingClass发挥作用了。
JavaFile brewJava() { TypeSpec.Builder result = TypeSpec.classBuilder(className) .addModifiers(PUBLIC) .addTypeVariable(TypeVariableName.get("T", ClassName.bestGuess(targetClass))); if (parentViewBinder != null) {//已有父类进行绑定则继承父类 result.superclass(ParameterizedTypeName.get(ClassName.bestGuess(parentViewBinder), TypeVariableName.get("T"))); } else { result.addSuperinterface(//否则继承ButterKnife.ViewBinder接口 ParameterizedTypeName.get(ClassName.get(ButterKnife.ViewBinder.class), TypeVariableName.get("T"))); } result.addMethod(createBindMethod()); result.addMethod(createUnbindMethod()); return JavaFile.builder(classPackage, result.build()) .addFileComment("Generated code from Butter Knife. Do not modify!") .build(); } /** ButterKnife.ViewBinder接口定义 */ public interface ViewBinder<T> { void bind(Finder finder, T target, Object source); void unbind(T target); } /** 生成绑定方法 */ private MethodSpec createBindMethod() { MethodSpec.Builder result = MethodSpec.methodBuilder("bind") .addAnnotation(Override.class) .addModifiers(PUBLIC) .addParameter(ButterKnife.Finder.class, "finder", FINAL) .addParameter(TypeVariableName.get("T"), "target", FINAL) .addParameter(Object.class, "source"); // Emit a call to the superclass binder, if any. if (parentViewBinder != null) { result.addStatement("super.bind(finder, target, source)"); } if (!viewIdMap.isEmpty() || !collectionBindings.isEmpty()) { // Local variable in which all views will be temporarily stored. result.addStatement("$T view", View.class); // Loop over each view bindings and emit it. for (ViewBindings bindings : viewIdMap.values()) { addViewBindings(result, bindings); } // Loop over each collection binding and emit it. for (Map.Entry<FieldCollectionViewBinding, int[]> entry : collectionBindings.entrySet()) { emitCollectionBinding(result, entry.getKey(), entry.getValue()); } } if (requiresResources()) { result.addStatement("$T res = finder.getContext(source).getResources()", Resources.class); if (!bitmapBindings.isEmpty()) { for (FieldBitmapBinding binding : bitmapBindings) { result.addStatement("target.$L = $T.decodeResource(res, $L)", binding.getName(), BitmapFactory.class, binding.getId()); } } if (!resourceBindings.isEmpty()) { for (FieldResourceBinding binding : resourceBindings) { result.addStatement("target.$L = res.$L($L)", binding.getName(), binding.getMethod(), binding.getId()); } } } return result.build(); } /** 生成解除绑定方法 */ private MethodSpec createUnbindMethod() { MethodSpec.Builder result = MethodSpec.methodBuilder("unbind") .addAnnotation(Override.class) .addModifiers(PUBLIC) .addParameter(TypeVariableName.get("T"), "target"); if (parentViewBinder != null) { result.addStatement("super.unbind(target)"); } for (ViewBindings bindings : viewIdMap.values()) { for (FieldViewBinding fieldBinding : bindings.getFieldBindings()) { result.addStatement("target.$L = null", fieldBinding.getName()); } } for (FieldCollectionViewBinding fieldCollectionBinding : collectionBindings.keySet()) { result.addStatement("target.$L = null", fieldCollectionBinding.getName()); } return result.build(); } /** 生成的辅助类如下 */ // Generated code from Butter Knife. Do not modify! package io.github.zengzhihao.eway56.ui.unlogin; import android.view.View; import butterknife.ButterKnife.Finder; import butterknife.ButterKnife.ViewBinder; public class LoginActivity$$ViewBinder<T extends io.github.zengzhihao.eway56.ui.unlogin.LoginActivity> implements ViewBinder<T> { @Override public void bind(final Finder finder, final T target, Object source) { View view; view = finder.findRequiredView(source, 2131296337, "field 'toolbar'"); target.toolbar = finder.castView(view, 2131296337, "field 'toolbar'"); } @Override public void unbind(T target) { target.toolbar = null; } } // Generated code from Butter Knife. Do not modify! package io.github.zengzhihao.eway56.ui; import android.view.View; import butterknife.ButterKnife.Finder; import butterknife.ButterKnife.ViewBinder; public class LandingActivity$$ViewBinder<T extends io.github.zengzhihao.eway56.ui.LandingActivity> implements ViewBinder<T> { @Override public void bind(final Finder finder, final T target, Object source) { View view; view = finder.findRequiredView(source, 2131296336, "method 'onLoginClicked'"); view.setOnClickListener( new butterknife.internal.DebouncingOnClickListener() { @Override public void doClick( android.view.View p0 ) { target.onLoginClicked(); } }); } @Override public void unbind(T target) { } }
上面整体的编译时处理的流程分析就到此结束了。一开始可能看着迷糊,但是多看看源码都会明白的,毕竟看人家的源码实现自己也可以学到很多东西。
下面主要分析一下运行时butter knife是怎么工作的,并且怎么和编译时生成的辅助类进行加载与关联?
先看一系列的bind方法。
/** * Bind annotated fields and methods in the specified {@link Activity}. The current content * view is used as the view root. * * @param target Target activity for view binding. */ public static void bind(Activity target) { bind(target, target, Finder.ACTIVITY); } /** * Bind annotated fields and methods in the specified {@link View}. The view and its children * are used as the view root. * * @param target Target view for view binding. */ public static void bind(View target) { bind(target, target, Finder.VIEW); } /** * Bind annotated fields and methods in the specified {@link Dialog}. The current content * view is used as the view root. * * @param target Target dialog for view binding. */ public static void bind(Dialog target) { bind(target, target, Finder.DIALOG); } /** * Bind annotated fields and methods in the specified {@code target} using the {@code source} * {@link Activity} as the view root. * * @param target Target class for view binding. * @param source Activity on which IDs will be looked up. */ public static void bind(Object target, Activity source) { bind(target, source, Finder.ACTIVITY); } /** * Bind annotated fields and methods in the specified {@code target} using the {@code source} * {@link View} as the view root. * * @param target Target class for view binding. * @param source View root on which IDs will be looked up. */ public static void bind(Object target, View source) { bind(target, source, Finder.VIEW); } /** * Bind annotated fields and methods in the specified {@code target} using the {@code source} * {@link Dialog} as the view root. * * @param target Target class for view binding. * @param source Dialog on which IDs will be looked up. */ public static void bind(Object target, Dialog source) { bind(target, source, Finder.DIALOG); }
可以看出前面的bind方法最终都是调用bind(Object target, Object source, Finder finder)这个方法的;
我们在Activity类的onCreate方法中setContentView语句之后调用ButterKnife.bind(this),最终调用的就是bind(Object target, Object source, Finder finder)。
那么来看看这个方法的实现。
static void bind(Object target, Object source, Finder finder) { Class<?> targetClass = target.getClass(); try { if (debug) Log.d(TAG, "Looking up view binder for " + targetClass.getName()); ViewBinder<Object> viewBinder = findViewBinderForClass(targetClass); if (viewBinder != null) { viewBinder.bind(finder, target, source);//执行辅助类实例的bind方法 /** 再结合我们之前给出的生成辅助类,和下面Finder的定义,是不是已经理解了呢? */ /** * @Override public void bind(final Finder finder, final T target, Object source) { * View view; * view = finder.findRequiredView(source, 2131296337, "field 'toolbar'"); * target.toolbar = finder.castView(view, 2131296337, "field 'toolbar'"); * } */ } } catch (Exception e) { throw new RuntimeException("Unable to bind views for " + targetClass.getName(), e); } } /** 其中Finder的定义 */ /** DO NOT USE: Exposed for generated code. */ @SuppressWarnings("UnusedDeclaration") // Used by generated code. public enum Finder { VIEW { @Override protected View findView(Object source, int id) { return ((View) source).findViewById(id); } @Override public Context getContext(Object source) { return ((View) source).getContext(); } @Override protected String getResourceEntryName(Object source, int id) { final View view = (View) source; // In edit mode, getResourceEntryName() is unsupported due to use of BridgeResources if (view.isInEditMode()) { return "<unavailable while editing>"; } return super.getResourceEntryName(source, id); } }, ACTIVITY { @Override protected View findView(Object source, int id) { return ((Activity) source).findViewById(id); } @Override public Context getContext(Object source) { return (Activity) source; } }, DIALOG { @Override protected View findView(Object source, int id) { return ((Dialog) source).findViewById(id); } @Override public Context getContext(Object source) { return ((Dialog) source).getContext(); } }; private static <T> T[] filterNull(T[] views) { int end = 0; for (int i = 0; i < views.length; i++) { T view = views[i]; if (view != null) { views[end++] = view; } } return Arrays.copyOfRange(views, 0, end); } public static <T> T[] arrayOf(T... views) { return filterNull(views); } public static <T> List<T> listOf(T... views) { return new ImmutableList<>(filterNull(views)); } public <T> T findRequiredView(Object source, int id, String who) { T view = findOptionalView(source, id, who); if (view == null) { String name = getResourceEntryName(source, id); throw new IllegalStateException("Required view '" + name + "' with ID " + id + " for " + who + " was not found. If this view is optional add '@Nullable' annotation."); } return view; } public <T> T findOptionalView(Object source, int id, String who) { View view = findView(source, id); return castView(view, id, who); } @SuppressWarnings("unchecked") // That's the point. public <T> T castView(View view, int id, String who) { try { return (T) view; } catch (ClassCastException e) { if (who == null) { throw new AssertionError(); } String name = getResourceEntryName(view, id); throw new IllegalStateException("View '" + name + "' with ID " + id + " for " + who + " was of the wrong type. See cause for more info.", e); } } @SuppressWarnings("unchecked") // That's the point. public <T> T castParam(Object value, String from, int fromPosition, String to, int toPosition) { try { return (T) value; } catch (ClassCastException e) { throw new IllegalStateException("Parameter #" + (fromPosition + 1) + " of method '" + from + "' was of the wrong type for parameter #" + (toPosition + 1) + " of method '" + to + "'. See cause for more info.", e); } } protected String getResourceEntryName(Object source, int id) { return getContext(source).getResources().getResourceEntryName(id); } protected abstract View findView(Object source, int id); public abstract Context getContext(Object source); } /** 查找当前辅助类是否已存在BINDERS缓存中,已存在就直接从BINDERS中取出返回,否则新创建一个类实例,放进BINDERS缓存中,并且返回 */ private static ViewBinder<Object> findViewBinderForClass(Class<?> cls) throws IllegalAccessException, InstantiationException { ViewBinder<Object> viewBinder = BINDERS.get(cls); if (viewBinder != null) { if (debug) Log.d(TAG, "HIT: Cached in view binder map."); return viewBinder; } String clsName = cls.getName(); if (clsName.startsWith(ANDROID_PREFIX) || clsName.startsWith(JAVA_PREFIX)) { if (debug) Log.d(TAG, "MISS: Reached framework class. Abandoning search."); return NOP_VIEW_BINDER; } try { Class<?> viewBindingClass = Class.forName(clsName + BINDING_CLASS_SUFFIX);//类名为 $$ViewBinder //noinspection unchecked viewBinder = (ViewBinder<Object>) viewBindingClass.newInstance();//新创建一个类实例 if (debug) Log.d(TAG, "HIT: Loaded view binder class."); } catch (ClassNotFoundException e) { if (debug) Log.d(TAG, "Not found. Trying superclass " + cls.getSuperclass().getName()); viewBinder = findViewBinderForClass(cls.getSuperclass()); } BINDERS.put(cls, viewBinder); return viewBinder; }
unbind方法和bind方法非常类似。
public static void unbind(Object target) { Class<?> targetClass = target.getClass(); try { if (debug) Log.d(TAG, "Looking up view binder for " + targetClass.getName()); ViewBinder<Object> viewBinder = findViewBinderForClass(targetClass); if (viewBinder != null) { viewBinder.unbind(target);//调用生成的辅助类的unbind方法 /** 生成的辅助类的unbind方法 */ /** * @Override public void unbind(T target) { * target.toolbar = null; */ } } } catch (Exception e) { throw new RuntimeException("Unable to unbind views for " + targetClass.getName(), e); } }
Butter knife提供的其他三个查找view方法。
/** Simpler version of {@link View#findViewById(int)} which infers the target type. */ @SuppressWarnings({ "unchecked", "UnusedDeclaration" }) // Checked by runtime cast. Public API. public static <T extends View> T findById(View view, int id) { return (T) view.findViewById(id); } /** Simpler version of {@link Activity#findViewById(int)} which infers the target type. */ @SuppressWarnings({ "unchecked", "UnusedDeclaration" }) // Checked by runtime cast. Public API. public static <T extends View> T findById(Activity activity, int id) { return (T) activity.findViewById(id); } /** Simpler version of {@link Dialog#findViewById(int)} which infers the target type. */ @SuppressWarnings({ "unchecked", "UnusedDeclaration" }) // Checked by runtime cast. Public API. public static <T extends View> T findById(Dialog dialog, int id) { return (T) dialog.findViewById(id); }
Setter和Action接口定义
/** An action that can be applied to a list of views. */ public interface Action<T extends View> { /** Apply the action on the {@code view} which is at {@code index} in the list. */ void apply(T view, int index); } /** A setter that can apply a value to a list of views. */ public interface Setter<T extends View, V> { /** Set the {@code value} on the {@code view} which is at {@code index} in the list. */ void set(T view, V value, int index); } /** Apply the specified {@code action} across the {@code list} of views. */ public static <T extends View> void apply(List<T> list, Action<? super T> action) { for (int i = 0, count = list.size(); i < count; i++) { action.apply(list.get(i), i); } } /** Set the {@code value} using the specified {@code setter} across the {@code list} of views. */ public static <T extends View, V> void apply(List<T> list, Setter<? super T, V> setter, V value) { for (int i = 0, count = list.size(); i < count; i++) { setter.set(list.get(i), value, i); } } /** * Apply the specified {@code value} across the {@code list} of views using the {@code property}. */ @TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH) public static <T extends View, V> void apply(List<T> list, Property<? super T, V> setter, V value) { //noinspection ForLoopReplaceableByForEach for (int i = 0, count = list.size(); i < count; i++) { setter.set(list.get(i), value); } }
随便说点
通过阅读butter knife的源码,又用写博客的形式记录下来,我感觉自己的理解又更牢固了。至于写的不好,还请见谅。下一篇博客估计是介绍dagger的使用还有源码分析吧。
再下来应该是Picasso、okhttp、retrofit、rxjava、rxandroid吧。不过,估计猴年马月了。
相关文章推荐
- BufferReader与BufferWriter实现socket通信注意事项
- jsp基础学习(六)--jsp传递参数方法
- jsp/servlet相关技术 (四) --- jsp的内置对象(一)
- jsp基础学习(五)----jsp指令
- 用Json实现PHP与JavaScript间数据交换
- jsp基础学习(四)----jsp引擎工作原理
- jsp基础学习(三)----jsp工作原理
- jsp基础学习(二)----jsp内置对象
- HTML背景图的显示
- jsp基础学习(一)----jsp综合介绍
- JavaScript基础学习之-JavaScript权威指南--8.7函数属性方法构造函数
- JavaScript基础学习之-javascript权威指南--8.6函数闭包
- JS数组去重算法实现
- JavaScript基础学习之-JavaScript权威指南--8.1-8.4函数
- jQuery树形下拉菜单特效代码分享
- JavaScript向php传递json格式数据
- [leetcode][dfs] Different Ways to Add Parentheses
- feof与块读写
- js判断小数点几位
- uva 1471 Defense Lines