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!
* @BindBool
binds an R.bool
ID to a boolean
field.
* @BindColor
binds an R.color
ID to an int
or ColorStateList
field.
* @BindDimen
binds an R.dimen
ID to an int
(for pixel size) or float
(for exact value) field.
* @BindDrawable
binds an R.drawable
ID to a Drawable
field.
* @BindInt
binds an R.int
ID to an int
field.
* @BindString
binds an R.string
ID to a String
field.
看到这里,你就不想试一试吗?
源码解析
先来看一系列的注解吧
/**
* 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吧。不过,估计猴年马月了。