編輯:關於Android編程
Android注解越來越引領潮流,比如 Dagger2, ButterKnife, EventBus3 等,他們都是注解類型,而且他們都有個共同點就是編譯時生成代碼,而不是運行時利用反射,這樣大大優化了性能;而這些框架都用到了同一個工具就是:APT(Annotation Processing Tool ),可以在代碼編譯期解析注解,並且生成新的 Java 文件,減少手動的代碼輸入。
今天我們要自己實現的就是類似ButterKnife的簡單的view初始化和點擊事件;
先看下整個項目的目錄結構:
先從最簡單入手,注解moudle:
1.創建名字為viewinject-annotation的java類型module
2.該module只有兩個類:
1.BindView用來對成員變量進行注解,並且接收一個 int 類型的參數
* Created by JokAr on 16/8/6. */ @Retention(RetentionPolicy.CLASS) @Target(ElementType.FIELD) public @interface BindView { int value(); }
2.OnClick對方法進行注解,接收一個或一組 int 類型參數,相當於給一組 View 指定點擊響應事件。
/** * Created by JokAr on 16/8/6. */ @Retention(RetentionPolicy.CLASS) @Target(ElementType.METHOD) public @interface OnClick { int[] value(); }
注解module就完成了,下面看看API module
1.首先創建一個Android moudle 的inject,然後創建interface
/** * Created by JokAr on 16/8/6. */ public interface Inject{ void inject(T host, Object object, Provider provider); }
/** * Created by JokAr on 16/8/6. */ public interface Provider { Context getContext(Object object); View findView(Object object, int id); }
因為我們需要生成的文件是這麼寫的:
public class MainActivity$$ViewInject implements Inject{ @Override public void inject(final MainActivity host, Object source, Provider provider) { host.textView = (TextView)(provider.findView(source, 2131427412)); host.button1 = (Button)(provider.findView(source, 2131427413)); View.OnClickListener listener = new View.OnClickListener() { @Override public void onClick(View view) { host.click(); } } ; provider.findView(source, 2131427412).setOnClickListener(listener); } }
當然這個生成文件是根據自己需求生成,然後需要一個類來關聯自己的activity類與生成的類:
/** * Created by JokAr on 16/8/6. */ public class ViewInject { private static final ActivityProvider activityProvider = new ActivityProvider(); private static final ViewProvider viewProvider = new ViewProvider(); private static final ArrayMapinjectMap = new ArrayMap<>(); public static void inject(Activity activity) { inject(activity, activity, activityProvider); } public static void inject(View view) { inject(view, view); } private static void inject(Object host, View view) { inject(host, view, viewProvider); } private static void inject(Object host, Object object, Provider provider) { String className = host.getClass().getName(); try { Inject inject = injectMap.get(className); if (inject == null) { Class aClass = Class.forName(className + "$$ViewInject"); inject = (Inject) aClass.newInstance(); injectMap.put(className, inject); } inject.inject(host, object, provider); } catch (ClassNotFoundException e) { e.printStackTrace(); } catch (InstantiationException e) { e.printStackTrace(); } catch (IllegalAccessException e) { e.printStackTrace(); } } }
使用方法就是:
ViewInject.inject(this);host 表示注解 View 變量所在的類,也就是注解類 object 表示查找 View 的地方,Activity & View 自身就可以查找,Fragment 需要在自己的 itemView 中查找 provider 是一個接口,定義了不同對象(比如 Activity、View 等)如何去查找目標 View,項目中分別為 Activity、View 實現了 Provider 接口(具體實現參考項目代碼) 為了提高效率,避免每次注入的時候都去找 Inject 對象,用一個 Map 將第一次找到的對象緩存起來,後面用的時候直接從 Map 裡面取。
API module類就完成了
再看viewinject-compilermodule:
首先創建名為iewinject-compiler的Java module ,然後在該module的buile.gradle加上一些依賴:
compile project(':viewinject-annotation') compile 'com.squareup:javapoet:1.7.0' compile 'com.google.auto.service:auto-service:1.0-rc2'Javapoet是square一個工具,提供了各種 API 讓你用各種姿勢去生成 Java 代碼文件,避免了徒手拼接字符串的尴尬。 auto-service 主要用於注解 Processor,對其生成 META-INF 配置信息。
首先創建ViewInjectProcesser類:
/** * Created by JokAr on 16/8/8. */ @AutoService(Processor.class) public class ViewInjectProcesser extends AbstractProcessor { private Filer mFiler; //文件相關的輔助類 private Elements mElementUtils; //元素相關的輔助類 private Messager mMessager; //日志相關的輔助類 @Override public synchronized void init(ProcessingEnvironment processingEnv) { super.init(processingEnv); mFiler = processingEnv.getFiler(); mElementUtils = processingEnv.getElementUtils(); mMessager = processingEnv.getMessager(); mAnnotatedClassMap = new TreeMap<>(); } @Override public boolean process(Set annotations, RoundEnvironment roundEnv) { return false; } /** * 指定使用的 Java 版本。通常返回SourceVersion.latestSupported()。 * @return */ @Override public SourceVersion getSupportedSourceVersion() { return SourceVersion.latestSupported(); } /** * 指定哪些注解應該被注解處理器注冊 * @return */ @Override public Set用 @AutoService 來注解這個處理器,可以自動生成配置信息。 在 init() 可以初始化拿到一些實用的工具類。getSupportedAnnotationTypes() { Set types = new LinkedHashSet<>(); types.add(BindView.class.getCanonicalName()); types.add(OnClick.class.getCanonicalName()); return types; } }
這個類的的基本內容就完成了,
現在創建BindViewField類,來解析BindView注解類來獲取用該注解的相關信息
/** * Created by JokAr on 16/8/8. */ public class BindViewField { private VariableElement mVariableElement; private int mresId; public BindViewField(Element element) throws IllegalArgumentException{ if (element.getKind() != ElementKind.FIELD) { throw new IllegalArgumentException(String.format("Only fields can be annotated with @%s", BindView.class.getSimpleName())); } mVariableElement = (VariableElement) element; BindView bindView = mVariableElement.getAnnotation(BindView.class); mresId = bindView.value(); if (mresId < 0) { throw new IllegalArgumentException( String.format("value() in %s for field %s is not valid !", BindView.class.getSimpleName(), mVariableElement.getSimpleName())); } } /** * 獲取變量名稱 * @return */ public Name getFieldName() { return mVariableElement.getSimpleName(); } /** * 獲取變量id * @return */ public int getResId() { return mresId; } /** * 獲取變量類型 * @return */ public TypeMirror getFieldType() { return mVariableElement.asType(); } }
創建OnClickMethod類來解析使用OnClick注解的方法,獲取相關信息
public class OnClickMethod { private ExecutableElement mExecutableElement; private int[] resIds; private Name mMethodName; public OnClickMethod(Element element) throws IllegalArgumentException { if (element.getKind() != ElementKind.METHOD) { throw new IllegalArgumentException( String.format("Only methods can be annotated with @%s", OnClick.class.getSimpleName())); } mExecutableElement = (ExecutableElement) element; resIds = mExecutableElement.getAnnotation(OnClick.class).value(); if (resIds == null) { throw new IllegalArgumentException(String.format("Must set valid ids for @%s", OnClick.class.getSimpleName())); } else { for (int id : resIds) { if (id < 0) { throw new IllegalArgumentException(String.format("Must set valid id for @%s", OnClick.class.getSimpleName())); } } } mMethodName = mExecutableElement.getSimpleName(); List parameters = mExecutableElement.getParameters(); if (parameters.size() > 0) { throw new IllegalArgumentException( String.format("The method annotated with @%s must have no parameters", OnClick.class.getSimpleName())); } } /** * 獲取方法名稱 * @return */ public Name getMethodName() { return mMethodName; } /** * 獲取id數組 * @return */ public int[] getResIds() { return resIds; } }
然後重點就是生成Java代碼文件的類:
/** * Created by JokAr on 16/8/8. */ public class AnnotatedClass { private TypeElement mTypeElement; private ArrayListmFields; private ArrayList mMethods; private Elements mElements; public AnnotatedClass(TypeElement typeElement, Elements elements) { mTypeElement = typeElement; mElements = elements; mFields = new ArrayList<>(); mMethods = new ArrayList<>(); } public String getFullClassName() { return mTypeElement.getQualifiedName().toString(); } public void addField(BindViewField field) { mFields.add(field); } public void addMethod(OnClickMethod method) { mMethods.add(method); } public JavaFile generateFile() { //generateMethod MethodSpec.Builder injectMethod = MethodSpec.methodBuilder("inject") .addModifiers(Modifier.PUBLIC) .addAnnotation(Override.class) .addParameter(TypeName.get(mTypeElement.asType()), "host", Modifier.FINAL) .addParameter(TypeName.OBJECT, "source") .addParameter(TypeUtil.PROVIDER,"provider"); for(BindViewField field : mFields){ // find views injectMethod.addStatement("host.$N = ($T)(provider.findView(source, $L))", field.getFieldName(), ClassName.get(field.getFieldType()), field.getResId()); } for(OnClickMethod method :mMethods){ TypeSpec listener = TypeSpec.anonymousClassBuilder("") .addSuperinterface(TypeUtil.ANDROID_ON_CLICK_LISTENER) .addMethod(MethodSpec.methodBuilder("onClick") .addAnnotation(Override.class) .addModifiers(Modifier.PUBLIC) .returns(TypeName.VOID) .addParameter(TypeUtil.ANDROID_VIEW, "view") .addStatement("host.$N()", method.getMethodName()) .build()) .build(); injectMethod.addStatement("View.OnClickListener listener = $L ", listener); for (int id : method.getResIds()) { // set listeners injectMethod.addStatement("provider.findView(source, $L).setOnClickListener(listener)", id); } } //generaClass TypeSpec injectClass = TypeSpec.classBuilder(mTypeElement.getSimpleName() + "$$ViewInject") .addModifiers(Modifier.PUBLIC) .addSuperinterface(ParameterizedTypeName.get(TypeUtil.INJET, TypeName.get(mTypeElement.asType()))) .addMethod(injectMethod.build()) .build(); String packgeName = mElements.getPackageOf(mTypeElement).getQualifiedName().toString(); return JavaFile.builder(packgeName, injectClass).build(); } }
具體的可以看javapoet的API,然後我們需要完善ViewInjectProcesser類,增加:
private MapmAnnotatedClassMap; @Override public boolean process(Set annotations, RoundEnvironment roundEnv) { mAnnotatedClassMap.clear(); try { processBindView(roundEnv); processOnClick(roundEnv); } catch (IllegalArgumentException e) { e.printStackTrace(); error(e.getMessage()); } for (AnnotatedClass annotatedClass : mAnnotatedClassMap.values()) { try { annotatedClass.generateFile().writeTo(mFiler); } catch (IOException e) { error("Generate file failed, reason: %s", e.getMessage()); } } return true; } private void processBindView(RoundEnvironment roundEnv) throws IllegalArgumentException { for (Element element : roundEnv.getElementsAnnotatedWith(BindView.class)) { AnnotatedClass annotatedClass = getAnnotatedClass(element); BindViewField bindViewField = new BindViewField(element); annotatedClass.addField(bindViewField); } } private void processOnClick(RoundEnvironment roundEnv) throws IllegalArgumentException { for (Element element : roundEnv.getElementsAnnotatedWith(OnClick.class)) { AnnotatedClass annotatedClass = getAnnotatedClass(element); OnClickMethod onClickMethod = new OnClickMethod(element); annotatedClass.addMethod(onClickMethod); } } private void error(String msg, Object... args) { mMessager.printMessage(Diagnostic.Kind.ERROR, String.format(msg, args)); }
實際使用
在項目的根目錄的build.gradle添加:
classpath 'com.neenbedankt.gradle.plugins:android-apt:1.8'
在項目的主module的build.gradle添加:
apply plugin: 'com.neenbedankt.android-apt' compile project(':viewinject-annotation') compile project(':inject') apt project(':viewinject-compiler')
在自己的activity類使用:
/** * Created by JokAr on 16/8/8. */ public class MainActivity extends AppCompatActivity { @BindView(R.id.textView) TextView textView; @BindView(R.id.button1) Button button1; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); ViewInject.inject(this); } @OnClick(R.id.textView) public void click() { Toast.makeText(getApplicationContext(), "hello", Toast.LENGTH_SHORT).show(); } }
點擊makeProject 就編譯完成後就可以在主項目module的/build/generated/source/apt/debug 目錄下看到生成的java類文件了
一個學習級的apt項目就完成了。
項目源碼
package com.icq.spinnercity; import java.util.ArrayList; import java.util.List; imp
如果你有一定的Android的基礎和英語基礎, 有願意貢獻開源社區的心, 如果你對以下目錄感興趣, 歡迎加入我們協同翻譯《Embedded Android》 此次協同翻
適配器模式(Adapter):適配器模式是一種行為模式,它可以把一個類的接口轉換成為另一個所期待的另一種接口,這樣可以使原本因接口無法在一起工作的兩個類能夠在一起工作了。
這篇博文給大家介紹下,當手機屏幕旋轉時我們應當怎麼去處理,首先了解下默認情況下Android進行屏幕旋轉的原理,當手機進行旋轉時重力感應sensor起到作用,會將Acti