前言
書接上回,上回說到庫里對戰湖人三分10投0中,真真氣煞我庫也,這下把氣全撒在鵜鶘身上,一口氣轟下破紀錄的13記三分。 上回說到Java基礎回歸之注解Annotation【基礎篇】,這回我們來真刀真槍實戰。相信很多做安卓的同學都用過至少聽過ButterKnife,沒錯,就是大神JakeWharton的黃油刀。本篇博文將結合注解和反射,實現類似Jake Wharton大神的ButterKnife注入框架,需要說明的是ButterKnife實現方式和我們的實現方式是有區別的,它里面是用到了APT技術,他是基于編譯期的注入,所以效率比我們用反射高,本文是基于運行期的。但是這并不妨礙我們造輪子。
ps:對反射不熟悉可以看博主另外一篇博文:Java基礎回歸之反射Reflection,本文不講解ButterKnife的用法。
取名
既然是低仿大名鼎鼎ButterKnife(黃油刀),那我們項目的名字也要低仿,就叫Shaver(剃須刀)吧..
Shaver的功能點
- @Bind(R.id.btn1):用于注入view,代替繁瑣的findViewById操作
- @ContentView(R.layout.activity_main):用于代替注入contentView
- @StringRes(R.string.string_shaver):用于注入String資源文件
- @OnClick({R.id.btn1,R.id.btn2}):用于綁定view的監聽事件
目標效果
開始造輪子
我先幫大家捋一遍思路,其實說白了就是編寫上述4個注解類,然后編寫一個處理這四種注解的核心類。例如要處理綁定view的@Bind注解,我們需要將activity傳入到核心類中,核心類反射獲取到標注有@Bind注解的成員變量field,然后獲取該注解的value,即view的id,最后將id利用activity.findViewById(value)獲取到view,然后反射將獲取到的view賦值給改成員變量filed,這樣我們就成功將view注入進去了,其他三個注解同理,下面show you the code,代碼注釋很詳細,請仔細看。
- 首先理所應當,我們編寫4個注解類:
- @Bind注解類
package com.youzhi.shaver.core;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/** * 代替findViewById的注解 */
@Target(ElementType.FIELD)//標注目標為成員變量
@Retention(RetentionPolicy.RUNTIME)//生命周期為運行時
public @interface Bind {
int value();//用于保存控件id
}
- @ContentView注解類
package com.youzhi.shaver.core;import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/** * 代替setContentView的注解 */
@Target(ElementType.TYPE)//標注目標為類前
@Retention(RetentionPolicy.RUNTIME)//生命周期為運行時
public @interface ContentView {
int value();//用于保存layoutId
}
- @StringRes注解類
package com.youzhi.shaver.core;import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/** * 代替getResource().getString(R.string.xx) */
@Retention(RetentionPolicy.RUNTIME)//生命周期為運行時
@Target(ElementType.FIELD)//標注目標為成員變量
public @interface StringRes {
int value();//用于保存string的id
}
- OnClick注解類
package com.youzhi.shaver.core;import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/** * 代替setOnClickListener的注解 */
@Retention(RetentionPolicy.RUNTIME)//生命周期為運行時
@Target(ElementType.METHOD)//標注目標為方法上
public @interface OnClick {
int[] value();//用于保存控件id集
}
- 接下來是最核心的處理類,所有邏輯都在這個處理類上面。(ps:可以優化,例如用Map將view緩存起來...這里留給大家)
package com.youzhi.shaver.core;
import android.app.Activity;
import android.view.View;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
/** * 注入接口 */
public class Shaver {
public static void bind(Activity activity) {
try {
bindContentView(activity);
bindStringRes(activity);
bindViews(activity);
bindClicks(activity);
} catch (Exception e) {
e.printStackTrace();
}
}
/**
* 注入contentView
* @param activity
*/
private static void bindContentView(Activity activity) {
Class<? extends Activity> aClass = activity.getClass();
ContentView annotation = aClass.getAnnotation(ContentView.class);
if(null != annotation){
int layoutId = annotation.value();//獲得注解上的layoutId值
activity.setContentView(layoutId);//將layoutId設置給activity
//activity.setContentView(layoutId)也可用反射實現,但效率低且麻煩,所以直接使用上面setContentView(layoutId)方法,此處只是順便說一下反射調方法
//aClass.getMethod("setContentView",int.class).invoke(activity,layoutId);
}
}
/**
* 注入String資源
* @param activity
*/
private static void bindStringRes(Activity activity) throws IllegalAccessException {
Class<? extends Activity> aClass = activity.getClass();
Field[] fields = aClass.getDeclaredFields();
//遍歷成員變量取出被StringRes注解的field
for (Field field : fields) {
if(field.isAnnotationPresent(StringRes.class)){
StringRes annotation = field.getAnnotation(StringRes.class);
int stringId = annotation.value();//string資源文件id
String stringValue = activity.getString(stringId);//獲取到相應資源文件string值
//反射賦值
field.setAccessible(true);//破封裝
field.set(activity,stringValue);
}
}
}
/**
* 注入view
* @param activity
* @throws IllegalAccessException
*/
private static void bindViews(Activity activity) throws IllegalAccessException/*, NoSuchMethodException, InvocationTargetException */{
//反射拿到@Bind注解的成員變量
Class<? extends Activity> aClass = activity.getClass();
Field[] fields = aClass.getDeclaredFields();//拿到所有成員變量
for (Field field : fields) {
//遍歷成員變量,判斷成員變量上是否有@Bind注解
if (field.isAnnotationPresent(Bind.class)) {
//如果有,拿出注解的value值,即控件id
Bind bind = field.getAnnotation(Bind.class);
int viewId = bind.value();
View view = activity.findViewById(viewId);//獲取到view對象
//activity.findViewById(viewId)也可用反射實現,但效率低且麻煩,所以直接使用上面find方法,此處只是順便說一下反射調方法
//View view= (View) aClass.getMethod("findViewById",int.class).invoke(activity,viewId);
field.setAccessible(true);//破封裝
field.set(activity, view);//將view設置給該成員變量
}
}
}
/**
* 綁定監聽事件
* @param activity
*/
private static void bindClicks(final Activity activity) {
Class<? extends Activity> aClass = activity.getClass();
Method[] declaredMethods = aClass.getDeclaredMethods();//反射獲取方法
//遍歷方法,判斷方法上是否有@OnClick注解
for (final Method method : declaredMethods) {
if(method.isAnnotationPresent(OnClick.class)){
OnClick annotation = method.getAnnotation(OnClick.class);
int[] viewIds = annotation.value();//拿到該方法上注解的view的id集
for (int viewId : viewIds) {
final View view = activity.findViewById(viewId);
if(null != view){
view.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
try {
method.setAccessible(true);//破封裝
method.invoke(activity,view);//調起該帶有@OnClick注解方法
} catch (Exception e) {
e.printStackTrace();
}
}
});
}
}
}
}
}
}
我們看到,Shaver類里面定義了一個入口方法bind(Activity activity) ,然后bind方法里面我們調用了4個方法,這4個方法分別是處理4個注解邏輯,其思路上面已經說了,就是通過反射掃描帶有這4個注解的編程元素(類/方法/成員變量),然后獲取注解上的value,拿到這些id后,我們就可以做很多事了。代碼本身不復雜,這里就不多說了。
- Shaver使用
package com.youzhi.shaver;
//省略各種導包
@ContentView(R.layout.activity_main)
public class MainActivity extends AppCompatActivity {
@Bind(R.id.btn1)
Button btn1;
@Bind(R.id.btn2)
Button btn2;
@Bind(R.id.tv1)
TextView tv1;
@Bind(R.id.et1)
EditText et1;
@StringRes(R.string.string_shaver)
String stringRes;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
Shaver.bind(this);
Log.e("MainActivity", btn1.getText().toString() + " , " + btn2.getText().toString() + " , " + tv1.getText().toString() + " , " + et1.getText().toString());
tv1.setText(stringRes);
}
@OnClick({R.id.btn1,R.id.btn2})
public void onClicks(View view){
switch (view.getId()){
case R.id.btn1:
Toast.makeText(this, "點擊了btn1", Toast.LENGTH_SHORT).show();
break;
case R.id.btn2:
Toast.makeText(this, "點擊了btn2", Toast.LENGTH_SHORT).show();
break;
}
}
}
然后我們在String文件中有一個string_shaver,用于例子中注入@StringRes(R.string.string_shaver)
布局文件:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/activity_main"
android:layout_width="match_parent"
android:orientation="vertical"
android:gravity="center_horizontal"
android:layout_height="match_parent">
<Button
android:id="@+id/btn1"
android:layout_width="150dp"
android:background="#33ff00ff"
android:layout_marginBottom="10dp"
android:text="按鈕1"
android:layout_height="30dp"/>
<Button
android:id="@+id/btn2"
android:text="按鈕2"
android:layout_width="150dp"
android:background="#33ffff00"
android:layout_marginBottom="10dp"
android:layout_height="30dp"/>
<TextView
android:id="@+id/tv1"
android:layout_width="150dp"
android:background="#4433bb00"
android:layout_marginBottom="10dp"
android:gravity="center"
android:text="文本1"
android:layout_height="30dp"/>
<EditText
android:id="@+id/et1"
android:layout_width="150dp"
android:background="#443300ff"
android:layout_marginBottom="10dp"
android:gravity="center"
android:text="輸入文本框1"
android:layout_height="30dp"/>
</LinearLayout>
- 運行結果及打印的log
我們成功獲取到各個view的文本,以及設置上點擊事件,說明我們的Shaver起作用了!It works!
這里需要注意打印出來的文本框為什么運行結果是“我是字符串資源”而log是“文本1”?大家看仔細點,我們布局文件里面text是“文本1”,然后我們在MainActivity里面打印出來后,我們重新set了一次,改成string文件里的值了。
The End
我們的低仿ButterKnife到此結束,相信講解的已經夠仔細了,到這里我相信現在對反射及注解有更深入的理解和鞏固。
最后,轉載請注明出處。