Android ButterKnife 注解框架的使用详解和原理分析
ButterKnife简介
ButterKnife是JakeWharton大神开发的一个开源库,官方对这个库的介绍为:
Butter Knife
Field and method binding for Android views
ButterKnife是一个使用注解方式来为Android中的View视图绑定字段和方法,能通过自动解析注解来搜索资源文件并赋值给Activity中的字段,如使用@BindView,@BindColor替代原生的findViewById,getColor等方法,或者给View视图的监听器绑定方法,如使用@OnClick 替代 setOnClickListener等方法。ButterKnife通过注解方式为我们封装了很多原生操作,我们只需要编写少量的代码就能实现跟原生代码一样的操作,减少了开发者的很多工作,提高了工作效率。
ButterKnife是通过使用注解方式来自动生成模板代码,从而来将Activity中的字段和方法与View绑定在一起。目前ButterKnife支持如下的特性:
- 使用@BindView 来代替findViewById 完成View的引用。
- 将多个View组合成list或者array,使用actions,setters或者属性来同时操作它们。
- 使用@OnClick等注解字段来注解方法,从而来代替监听器中的匿名内部类。
- 使用@BindString等注解字段来注解字段,从而来代替Context.getString等获取资源的方式。
这样说很抽象,下面就使用代码来实践说明一下。
Android Studio配置
从外部引入ButterKnife库很简单,只需要在项目的App包中的build.gradle文件中的dependencies中加入依赖即可:
dependencies {.....//省略 compile 'com.jakewharton:butterknife:8.5.1' annotationProcessor 'com.jakewharton:butterknife-compiler:8.5.1'}
最新版本的8.5.1,如需了解到最新的版本号可以参考Github:
https://github.com/JakeWharton/butterknife
View绑定
我们一般在Activity中的onCreate中使用findViewById方式来引用布局文件中的View对象,而且还需要进行强制类型转换成所声明的变量的类型。当有多个View时,就需要调用多次findViewById方法,代码工作量大,而且也不简洁。ButterKnife使用@BindView(View的ID)注解字段来注解变量,这样ButterKnife就能自动引用布局中的View资源并能转换为所声明的相应的View类型。
@BindView(R.id.MainActivity_Btn)Button btn; @BindView(R.id.MainActivity_Text)TextView text; @BindView(R.id.MainActivity_RecyclerView)RecyclerView recyclerView; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); ButterKnife.bind(this); }
注意还需要在onCreate方法调用ButterKnife.bind(this)来绑定Activity和ButterKnife。
以前一直以为ButterKnife是使用反射机制的,所以一直对ButterKnife的性能有点担忧。现在才了解到ButterKnife并不是使用反射机制的,而是解析注解字段来自动生成代码来执行查询和绑定的,说到底就是为我们封装了一些代码,减少我们的工作量。具体生成的代码文件可以查看app包中的build-generated-source-apt-debug-包名中的类文件,里面的类文件命名规则为调用ButterKnife的bind方法的class类加上”_ViewBinding”,具体如下:
点击MainActivity_ViewBinding文件进去查看源码,可以看到变量对应我们在MainActivity中声明的View变量一致:
在MainActivity_ViewBinding方法进行变量的初始化和监听绑定:
在其中的代码中可以看到 Utils.findOptionalViewAsType来引用布局中的View并进行类型转换的:
target.text = Utils.findOptionalViewAsType(source, R.id.MainActivity_Text, "field 'text'", TextView.class); target.recyclerView = Utils.findRequiredViewAsType(source, R.id.MainActivity_RecyclerView, "field 'recyclerView'", RecyclerView.class);
对于监听器的绑定,则直接调用了对应的setOnXXXX方法,然后在监听器方法里面调用target即Activity里面的被ButterKnife注解的事件处理方法:
其中的部分源代码:
view.setOnClickListener(new DebouncingOnClickListener() { @Override public void doClick(View p0) { target.multiHandleClick(Utils.castParam(p0, "doClick", 0, "multiHandleClick", 0)); } }); view = Utils.findRequiredView(source, R.id.MainActivity_EditText, "method 'onTextChanged', method 'beforeTextChanged', and method 'afterTextChanged'"); view2131427424 = view; view2131427424TextWatcher = new TextWatcher() { @Override public void onTextChanged(CharSequence p0, int p1, int p2, int p3) { target.onTextChanged(p0, p1, p2, p3); } @Override public void beforeTextChanged(CharSequence p0, int p1, int p2, int p3) { target.beforeTextChanged(p0, p1, p2, p3); } @Override public void afterTextChanged(Editable p0) { target.afterTextChanged(p0); } }; ((TextView) view).addTextChangedListener(view2131427424TextWatcher);
从上面的分析中就可以总结出ButterKnife的原理了,简单来说就是ButterKnife框架解析注解字段,然后自动生成代码来引用View和为View绑定事件监听器。
Resources资源绑定
除了使用@BindView来引用view,ButterKnife还对引用resource资源文件也提供了相应的支持。可以使用@BindBool,@BindColor,@BindDimen,@BindDrawable,@BindInt,@BindString等注解字段来引用预定义的resource资源。
如:
@BindString(R.string.app_name)String sub; @BindColor(R.color.TextColor)int textColor;
这个资源绑定的原理也很简单,可以参考上面的分析,在MainActivity_ViewBinding文件中的MainActivity_ViewBinding方法中可以看到:
Context context = source.getContext(); Resources res = context.getResources(); target.textColor = ContextCompat.getColor(context, R.color.TextColor); target.sub = res.getString(R.string.app_name);
NON-Activity绑定
除了可以在Activity中使用 ButterKnife来进行绑定,还可以在Fragment,RecyclerView的Adapter等任意对象上使用,前提是要提供这个对象上绑定的根视图View Root。
使用 ButterKnife.bind(Object,View);方法来绑定对象和ButterKnife,如下为Fragment中的使用:
public class MainFragment extends Fragment{ @BindView(R.id.MainFragment_Btn)Button btn; @BindView(R.id.MainFragment_Text)TextView text; @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState){ View view=inflater.inflate(R.layout.fragement_main,container,false); ButterKnife.bind(this,view); return view; }}
在Adapter中使用ButterKnife来简化ViewHolder模式:
public class ListAdapter extends RecyclerView.Adapter<ListAdapter.ListHolder> { private ArrayList data; public ListAdapter(ArrayList list){ this.data=list; } @Override public ListHolder onCreateViewHolder(ViewGroup parent, int viewType) { return new ListHolder(LayoutInflater.from(parent.getContext()).inflate(R.layout.list_item,parent,false)); } @Override public void onBindViewHolder(ListHolder holder, int position) { holder.text.setText("Text: no"+data.get(position)); } @Override public int getItemCount() { return data.size(); } static class ListHolder extends RecyclerView.ViewHolder{ @BindView(R.id.MainActivity_ListItem_Btn)Button btn; @BindView(R.id.MainActivity_ListItem_Text)TextView text; public ListHolder(View itemView) { super(itemView); ButterKnife.bind(this,itemView); } }}
View List
对于多个有相同作用的View时,我们可以将这些View组合成一个List或者Array,然后只需调用一个方法就可以对这些View实行同一个操作。
使用@BindViews( { View.ID,View.ID,View.ID…} )方法来初始化,注意有花括号 { }:
@BindViews({R.id.MainActivity_FirstName,R.id.MainActivity_MiddleName,R.id.MainActivity_LastName}) List nameTexts;
对同一组中的View实行同一操作可以使用apply方法,首先我们来了解下两个重要的接口 Action和Setter,这两个接口可以指定对View的行为。在Activity中初始化这两个接口:
static final ButterKnife.Action DISABLE=new ButterKnife.Action() { @Override public void apply(@NonNull View view, int index) { view.setEnabled(false); } }; static final ButterKnife.Setter ENABLE=new ButterKnife.Setter() { @Override public void set(@NonNull View view, Boolean value, int index) { view.setEnabled(value); } }; static final ButterKnife.Setter ChangeColor=new ButterKnife.Setter() { @Override public void set(@NonNull TextView view, Integer value, int index) { view.setTextColor(value); } };
可以看到Action和Setter方法的区别,Action接口只能获取到View对象,但是不能传递参数;而Setter接口使用了泛型,可以在实现接口的时候指定需要传递的数据类型,然后在其中的set方法的第二个参数中获取传递参数。
使用apply就可以执行这些定义中的操作了:
ButterKnife.apply(nameTexts,DISABLE); ButterKnife.apply(nameTexts,ENABLE,false); ButterKnife.apply(nameTexts,ChangeColor,textColor);
ButterKnife还允许在apply方法中直接使用Android中的属性,例如:
ButterKnife.apply(nameViews, View.ALPHA, 0.0f);
监听绑定
一般我们为View设置监听需要调用View.addOnXXXXXListener或者setOnXXXXXListener方法来绑定事件监听器,这样每次都要新建匿名内部类,而且有时还必须实现监听器接口中我们并不需要使用的事件方法。ButterKnife也对事件监听器绑定提供了支持,只需要使用相应的注解字段,如@OnClick,@OnTextChanged字段,来注解一个已声明的方法,然后ButterKnife就会自动将这个方法与事件监听器绑定在一起。
对于单个View的事件监听方法,方法参数可有可无:
@OnClick(R.id.MainActivity_Btn) public void click(View view){ Log.e("ButterKnife","Click:"+view.getId()); } @OnClick(R.id.MainActivity_Btn) public void click(){ Log.e("ButterKnife","Click no arguments"); } @OnClick(R.id.MainActivity_Btn) public void click(Button button){//指定View类型,会被自动类型转换 Log.e("ButterKnife","button Click:"+button.getId()); }
我们也可以在OnClick方法参数中声明多个View.ID,然后就可以对这一组View绑定同一监听器来实现相同的事件处理:
@OnClick({R.id.MainActivity_FirstName,R.id.MainActivity_MiddleName,R.id.MainActivity_LastName}) public void multiHandleClick(TextView textView){ Log.e("ButterKnife","multiHandleClick:"+textView.getId()); }
在自定义View中,可以不指定View.ID来绑定监听器:
public class FancyButton extends Button { @OnClick public void onClick() { // TODO do something! }}
监听器多事件处理
当一个View的监听器中有多个事件处理函数时,如TextWatch监听器,我们可以指定其中的一个事件处理函数来绑定方法。ButterKnife为每一个方法注解字段都默认绑定了一个事件处理函数,我们可以使用callback 参数来为方法注解字段指定相应的事件处理函数。
例如@OnTextChanged方法注解字段默认绑定的是TextWatch中的onTextChanged事件处理函数,但我们可以使用callback = OnTextChanged.Callback.BEFORE_TEXT_CHANGED来为@OnTextChanged注解绑定beforeTextChanged事件处理函数,使用callback = OnTextChanged.Callback.AFTER_TEXT_CHANGED来为@OnTextChanged注解绑定afterTextChanged事件处理函数:
@OnTextChanged(R.id.MainActivity_EditText) public void onTextChanged(CharSequence s, int start, int before, int count){ Log.e("onTextChanged",String.valueOf(s)); } @OnTextChanged(value = R.id.MainActivity_EditText,callback = OnTextChanged.Callback.BEFORE_TEXT_CHANGED) public void beforeTextChanged(CharSequence s, int start, int count, int after){ Log.e("beforeTextChanged",String.valueOf(s)); } @OnTextChanged(value = R.id.MainActivity_EditText,callback = OnTextChanged.Callback.AFTER_TEXT_CHANGED) public void afterTextChanged(Editable s){ Log.e("afterTextChanged",String.valueOf(s.toString())); }
解除绑定
Fragment跟Activity一样,有自己对应的生命周期。在Fragment的onCreateView方法中绑定View,在onDestroyView将view置为null。当我们在onCreateView中调用ButterKnife.bind(this,view);时,这个方法会返回一个Unbinder实例对象,可以在适当的生命周期回调函数中调用Unbinder的unbind方法来解除View和ButterKnife的绑定,释放资源。
public class MainFragment extends Fragment{ @BindView(R.id.MainFragment_Btn)Button btn; @BindView(R.id.MainFragment_Text)TextView text; private Unbinder unbinder; @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState){ View view=inflater.inflate(R.layout.fragement_main,container,false); unbinder=ButterKnife.bind(this,view); return view; } @Override public void onDestroyView(){ super.onDestroyView(); unbinder.unbind(); }}
防止异常发生
当使用@Bind来引用View和资源或者绑定View事件处理函数时,如果目标view是null就会抛出异常。为了防止异常的发生和创建可选择性的绑定,在变量上使用@Nullable注解,在方法上使用@Optional注解。
@Nullable @BindView(R.id.MainActivity_Btn)Button btn; @Nullable @BindView(R.id.MainActivity_Text)TextView text; @Optional @OnClick(R.id.MainActivity_Btn) public void click(View view){ Log.e("ButterKnife","Click:"+view.getId()); }
使用findById
除了使用@BindView来简化findViewById的使用,我们还可以使用ButterKnife.findById来简化在View,Activity或者Dialog中来引用View的代码。ButterKnife使用了泛型来推断返回的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);
参考:
http://jakewharton.github.io/butterknife/
更多相关文章
- 32、adb INSTALL_FAILED_TEST_ONLY 处理方法
- js调Android与IOS方法
- android studio 添加按钮点击事件的三种方法
- 在android中运行java main方法
- React-Native 调用原生方法,弹出自定义对话框
- Android截屏截图的几种方法总结
- Ubuntu adb devices : no permissions 解决方法