一般我们在Android 的开发中,我们在Activity中都会涉及到大量的findViewById操作,这个时候我们可以采用一些开源的框架来帮我们省很多的苦力,例如大名鼎鼎的ButterKnife就是我经常使用的。ButterKnife是一种非常流行的注解框架。主要有以下几个优点:
1.强大的View绑定和Click事件处理功能,简化代码,提升开发效率
2.方便的处理Adapter里的ViewHolder绑定问题
3.运行时不会影响APP效率,使用配置方便(采用的是编译时注解)
4.代码清晰,可读性强
注意事项:
1 必须在setContentView()之后绑定
2 由于采用了编译时注解,一代工程大量使用了之后编译时间会增加不少
关于使用可以参考下面这篇博客
http://blog.csdn.net/itjianghuxiaoxiong/article/details/50177549
下面我们要仿照ButterKnife自己来实现一个IOC注解框架,毕竟我们自己要学会慢慢造轮子。
首先我们要明白,类似于ButterKnife注解框架,它的基本思想就是利用反射区拿到对应的注解值,这个值一般是R.id.xxx。然后根据这个域去设置对应的值,如果是方法就去设置这个View的onClickListener方法。步骤如下:
对于属性注入
Bind(R.id.xxx)
View view
思路如下:
利用反射去 获取Annotation –> value –> findViewById –> 反射注入属性
对于事件注入:
@OnClick(R.id.xxx)
public void submit(View view) {
// TODO submit data to server...
}
利用反射去 获取Annotation –> value –> findViewById –> setOnclickListener –> 反射执行该方法
当然,ButterKnifer比这个功能强大很多,但是基本思想是类似的。我们今天就来一个简单的例子,比如我们只思想findViewById和设置点击事件的功能。并且,我们是运行时注解,不是编译时注解,虽然会稍微影响性能,但是这个影响非常非常小,几乎可以忽略不计。
我们的绑定view就叫它ViewById吧,事件就叫OnClick。
首先我们定义一个注解ViewById
package com.qiyei.baselibrary.ioc;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* Email: [email protected]
* Created by qiyei2015 on 2017/5/2.
* Version: 1.0
* Description: View注解的Annotation
*/
/**
* @Target(ElementType.FIELD) 代表Annotation的位置 FIELD属性 TYPE类上 CONSTRUCTOR 构造函数上
* @Retention(RetentionPolicy.CLASS) 什么时候生效 CLASS编译时 RUNTIME运行时 SOURCE源码资源
*/
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface ViewById {
/**
* --> @ViewById(R.id.xxx)
* @return
*/
int value();
}
这里需要有两点需要注意:一是指定生效的是Filed和RUNTIME,表示我们是需要注解在Field上的,并且在运行时生效。另一个就是定义一个变量int value();
接着,我们来定义注解处理器ViewUtils。
package com.qiyei.baselibrary.ioc;
import android.app.Activity;
import android.content.Context;
import android.net.ConnectivityManager;
import android.net.NetworkInfo;
import android.util.Log;
import android.view.View;
import android.widget.Toast;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
/**
* Email: [email protected]
* Created by qiyei2015 on 2017/5/2.
* Version: 1.0
* Description:
*/
public class ViewUtils {
private static final String TAG = ViewUtils.class.getSimpleName();
/**
* Activity中使用
* @param activity
*/
public static void inject(Activity activity){
inject(new ViewFinder(activity));
}
/**
* 实际处理者
* @param finder
*/
private static void inject(ViewFinder finder){
injectField(finder);
}
/**
* 注入域
* @param finder
*/
private static void injectField(ViewFinder finder){
//获取类里面所有的属性
Class> clazz = finder.findClass();
Field[] fields = clazz.getDeclaredFields();
Log.d(TAG,"name:" + clazz.getSimpleName() + " fields.length:" + fields.length);
//依次遍历并获取域上的ViewById注解
for (Field field : fields){
//获取ViewById注解
ViewById viewById = field.getAnnotation(ViewById.class);
if (viewById != null){
//获取id值,对应R.id.xxx
int id = viewById.value();
if (id != 0){
View view = finder.finderViewById(id);
if (view != null){
//能够注入所有修饰符 private public protect
field.setAccessible(true);
try {
//动态的注入找到的View
field.set(finder.finderObject(),view);
} catch (IllegalAccessException e) {
e.printStackTrace();
}
}
}
}
}
}
}
这里可以看到,我们首先定义了一个方法inject(Activity activity),这个方法我们会调用一个我们的ViewUtils辅助类。ViewFinder主要是辅助我们找到View,Object(反射时调用的对象的),class等
/**
* Email: [email protected]
* Created by qiyei2015 on 2017/5/2.
* Version: 1.0
* Description: View的findViewById的辅助类
*/
public class ViewFinder {
/**
* 所保存的activity实例
*/
private Activity mActivity;
/**
* class实例
*/
private Class> mClass;
public ViewFinder(Activity activity){
mActivity = activity;
mClass = activity.getClass();
}
/**
* 根据id找到对应的View
* @param id
* @return
*/
public View finderViewById(int id){
return mActivity != null ? mActivity.findViewById(id) : null;
}
/**
* 返回对应的对象
* @return
*/
public Object finderObject(){
if (mActivity != null){
return mActivity;
}
return null;
}
/**
* 返回对应的Class实例
* @return
*/
public Class> findClass(){
return mClass;
}
}
ViewFinder主要有以下几个功能:
1 找到View
2 找到Object
3 找到Object对应的class
整个IOC框架比较重要的点:
1 在反射的时候,我们需要调用getDeclaredFields()获取所有的变量,包括public 和private的
2 在ViewFinder中对mClass赋值一定是activity.getClass()而不是Activity.class。因为我们我要获取的反射对象的类是我们的自定义Activity(继承Activity)。如果传入的是Activity类型的class将会报空指针,不信可以试试。
我们在MainActivty 中代码如下:
public class MainActivity extends AppCompatActivity {
static {
System.loadLibrary("native-lib");
}
@ViewById(R.id.test_tv1)
private TextView mTextView1;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
ViewUtils.inject(this);
// Example of a call to a native method
TextView tv = (TextView) findViewById(R.id.sample_text);
//tv.setText(stringFromJNI());
tv.setText("");
mTextView1.setText("这是IOC注解生成的");
}
/**
* A native method that is implemented by the 'native-lib' native library,
* which is packaged with this application.
*/
public native String stringFromJNI();
}
因为我建工程的时候选择了支持c++,所以会多了一个c++的提示消息。将它屏蔽了。效果如下:
有了前面注入域的例子,我们要实现点击事件的注入也很简单,步骤如下:
1 通过反射拿到所有的方法
2 根据注解OnClick找到这个方法
3 根据id值找到对应的View
4 设置这个View的onClickListener
5 在onClickListener中反射调用这个方法
OnClick注解定义如下:
package com.qiyei.baselibrary.ioc;
/**
* Email: [email protected]
* Created by qiyei2015 on 2017/5/3.
* Version: 1.0
* Description: 方法上设置点击事件的注解
*/
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* @Target(ElementType.FIELD) 代表Annotation的位置 FIELD属性 TYPE类上 CONSTRUCTOR 构造函数上
* @Retention(RetentionPolicy.CLASS) 什么时候生效 CLASS编译时 RUNTIME运行时 SOURCE源码资源
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface OnClick {
int[] value();
}
因为我们要支持多个id绑定到一个方法上,所以定义了一个数组变量
ViewUtils部分代码如下:
/**
* 实际处理者
* @param finder
*/
private static void inject(ViewFinder finder){
injectField(finder);
injectEvent(finder);
}
/**
* 注入事件
* @param finder
*/
private static void injectEvent(ViewFinder finder){
//获取所有的方法
Class> clazz = finder.findClass();
Method[] methods = clazz.getDeclaredMethods();
//依次遍历并获取方法的ViewById注解
for (Method method : methods){
//获取OnClick注解
OnClick onClick = method.getAnnotation(OnClick.class);
if (onClick != null){
//获取id值,对应R.id.xxx
int[] ids = onClick.value();
for (int id : ids){
View view = finder.finderViewById(id);
if (view != null){
view.setOnClickListener(new DeclaredOnClickListener(method,finder.finderObject());
}
}
}
}
}
/**
* 设置OnClickListener的监听器
*/
private static class DeclaredOnClickListener implements View.OnClickListener{
/**
* 反射时方法调用的对象
*/
private Object mObject;
/**
* 反射的方法
*/
private Method mMethod;
public DeclaredOnClickListener(Method method , Object object){
mMethod = method;
mObject = object;
}
@Override
public void onClick(View v) {
//反射该方法
try {
mMethod.setAccessible(true);
mMethod.invoke(mObject,v);
} catch (Exception e) {
e.printStackTrace();
try {
mMethod.invoke(mObject,new Object[]{});
} catch (IllegalAccessException e1) {
e1.printStackTrace();
} catch (InvocationTargetException e1) {
e1.printStackTrace();
}
}
}
}
....
注意:这里支持了注解方法有参和无参两种类型,也就是说我们的注解方法支持一下两种写法:
@OnClick(R.id.test_tv1)
private void textClick(View view){
Toast.makeText(this,"点击了:" + view.getId(),Toast.LENGTH_LONG).show();
}
@OnClick(R.id.test_tv1)
private void textClick(){
Toast.makeText(this,"点击了:",Toast.LENGTH_LONG).show();
}
接下来我们在MainActivity 中修改如下:
@OnClick(R.id.test_tv1)
private void textClick(View view){
Toast.makeText(this,"点击了:" + view.getId(),Toast.LENGTH_LONG).show();
}
上面我们已经初步的建立了一个IOC框架的原型,但是还远远不够完善,我们需要再完善一下。
功能扩展
1 支持Fragment与View中使用,因此我们需要增加两个方法,并且需要修改ViewFinder
2 增加监测网络的功能两个,比如有些需求中,没有网络的条件下我们是无法去点击某个按钮的,因此需要在点击事件中增加监测网络的功能
3 增加生成ViewHolder 的功能
4 增加其他事件,例如长按事件,双击事件等
这些功能会逐步的添加到IOC框架中,欢迎访问我的githubhttps://github.com/qiyei2015/EssayJoke
总结:
IOC框架主要是省去了我们去findViewById和设置点击事件。所用的知识点主要是反射和注解。我们可以参照XUtils,ButterKnife等框架去完善它,慢慢的提高我们自己的编程能力
参考
http://www.jianshu.com/p/2570c2de028b
http://blog.csdn.net/itjianghuxiaoxiong/article/details/50177549