自定义注解-实现findViewById和onClick

编译时注解

下面通过编译时注解实现类似ButterKnife的功能

一、项目结构

这里写图片描述

  • AnnotationModule:Java Module,存放自定义注解
  • CompilerModule:Java Module,注解处理器,生成Java类。依赖如下
implementation 'com.google.auto.service:auto-service:1.0-rc2'
implementation 'com.squareup:javapoet:1.7.0'
implementation project(':AnnotationModule')
  • ViewInjectorApi:Android Module,提供Android调用的Api,调用CompilerModule生成的类。依赖AnnotationModule,其实没用到,主要是用到app中
api project(path: ':AnnotationModule')
  • app:Android Module,测试项目。app需要用到AnnotationModule中注解,这里app依赖ViewInjectorApi,所以ViewInjectorApi中需要依赖AnnotationModule,而且必须用api/compile引入 或者直接在app中直接引入AnnotationModule
implementation project(path: ':ViewInjectorApi')
annotationProcessor project(':CompilerModule')

注:Android Gradle插件2.2版本发布后,官方提供了annotationProcessor来代替android-apt。同时android-apt作者宣布不再维护,推荐使用annotationProcessor。

注:gradle 3.0.0版本以上提供了api等同与compile。implementation和api的区别是implementation依赖不会传递,只在当前module有效。具体Google查看一下

二、具体流程
1、在AnnotationModule中定义注解,定义为运行时注解,只在属性上使用,需要一个int型参数
@Retention(CLASS)
@Target(FIELD)
public @interface BindView {
    int value();
}
2、在CompilerModule中处理注解,生成类名为使用注解所在类的简单类名+$ViewInjector,具体如下
@AutoService(Processor.class)
public class ViewInjectorProcessor extends AbstractProcessor {

    private Elements elementUtils;
    private Map<TypeElement, List<VariableElement>> map = new HashMap<>();

    @Override
    public Set<String> getSupportedAnnotationTypes() {
        Set<String> set = new LinkedHashSet<>();
        set.add(BindView.class.getCanonicalName());
        return set;
    }

    @Override
    public synchronized void init(ProcessingEnvironment processingEnv) {
        super.init(processingEnv);
        elementUtils = processingEnv.getElementUtils();
    }

    @Override
    public SourceVersion getSupportedSourceVersion() {
        return SourceVersion.RELEASE_7;
    }

    @Override
    public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
        map.clear();

        Set<? extends Element> elements = roundEnvironment.getElementsAnnotatedWith(BindView.class);

        collectInfo(elements);
        generateCode();

        return true;
    }

    private void collectInfo(Set<? extends Element> elements) {
        for (Element element : elements) {
            VariableElement variableElement = (VariableElement) element;
            TypeElement typeElement = (TypeElement) variableElement.getEnclosingElement();

            List<VariableElement> variableElementList = map.get(typeElement);
            if (variableElementList == null) {
                variableElementList = new ArrayList<>();
                map.put(typeElement, variableElementList);
            }
            variableElementList.add(variableElement);
        }
    }

    private void generateCode() {
        for (TypeElement typeElement : map.keySet()) {
            MethodSpec.Builder methodBuilder = MethodSpec.constructorBuilder()
                    .addModifiers(Modifier.PUBLIC)
                    .addParameter(ClassName.get(typeElement.asType()), "activity");

            List<VariableElement> variableElementList = map.get(typeElement);
            for (VariableElement variableElement : variableElementList) {
                String varName = variableElement.getSimpleName().toString();
                String varType = variableElement.asType().toString();
                BindView bindView = variableElement.getAnnotation(BindView.class);
                int params = bindView.value();
                methodBuilder.addStatement("activity.$L = ($L) activity.findViewById($L)", varName, varType, params);
            }

            final String pkgName = getPackageName(typeElement);
            final String clsName = typeElement.getSimpleName().toString() + "$ViewInjector";

            TypeSpec typeSpec = TypeSpec.classBuilder(clsName)
                    .addModifiers(Modifier.PUBLIC)
                    .addMethod(methodBuilder.build())
                    .build();

            JavaFile javaFile = JavaFile.builder(pkgName, typeSpec)
                    .build();

            try {
                javaFile.writeTo(processingEnv.getFiler());
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

    private String getPackageName(TypeElement type) {
        return elementUtils.getPackageOf(type).getQualifiedName().toString();
    }

}
3-1、这个时候其实也可以算完成了,因为xxx$ViewInjector已经生成了。如果不写ViewInjectorApi则可以在app的build.gradle中的dependencies下添加
implementation project(path: ':AnnotationModule')
annotationProcessor project(':CompilerModule')
在app中使用如下
public class MainActivity extends AppCompatActivity {

    @BindView(R.id.tv_name)
    TextView nameTv;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        MainActivity$ViewInjector injector = new MainActivity$ViewInjector(this);

        nameTv.setText("Bill");

    }
}
生成的类如下:
public class MainActivity$ViewInjector {
  public MainActivity$ViewInjector(MainActivity activity) {
    activity.nameTv = (android.widget.TextView) activity.findViewById(2131165304);
  }
}
3-2、我们模仿ButterKnife写个调用注解的Module,去掉3-1中的代码,在ViewInjectorApi中新建类ViewInjector,通过反射调用我们生成的MainActivity$ViewInjector类
public static void bind(Activity activity) {
        String clsName = activity.getClass().getName();
        try {
            Class<?> cls = Class.forName(clsName + "$ViewInjector");
            Constructor constructor = cls.getConstructor(activity.getClass());
            constructor.newInstance(activity);
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        } catch (NoSuchMethodException e) {
            e.printStackTrace();
        } catch (InstantiationException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        } catch (InvocationTargetException e) {
            e.printStackTrace();
        }
    }
在app的build.gradle中的dependencies下添加
implementation project(path: ':ViewInjectorApi')
annotationProcessor project(':CompilerModule')
在app中使用如下
public class MainActivity extends AppCompatActivity {

    @BindView(R.id.tv_name)
    TextView nameTv;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        ViewInjector.bind(this);

        nameTv.setText("Bill");
    }
}
三、上面模仿ButterKnife实现了在Activity中可以省略findViewById的步骤,使用xml中View前需要调用ViewInjector的bind(Activity activity)方法将当前Activity对象传入。但是当在Adapter中使用时就不可以了,因为我们在Adapter中获取Id时需要用当前View获取Id,如下:,所以我们把上面的改进一下,支持传入View。
this.textView = (TextView) itemView.findViewById(R.id.textview);
1、修改ViewInjectorProcessor类的generateCode()方法,如下代码
    private void generateCode() {
        for (TypeElement typeElement : map.keySet()) {
            MethodSpec.Builder methodBuilder = MethodSpec.constructorBuilder()
                    .addModifiers(Modifier.PUBLIC)
                    .addParameter(ClassName.get(typeElement.asType()), "target")
                    .addParameter(ClassName.get("android.view", "View"), "view");

            List<VariableElement> variableElementList = map.get(typeElement);
            for (VariableElement variableElement : variableElementList) {
                String varName = variableElement.getSimpleName().toString();
                String varType = variableElement.asType().toString();
                BindView bindView = variableElement.getAnnotation(BindView.class);
                int params = bindView.value();
                methodBuilder.addStatement("target.$L = ($L) view.findViewById($L)", varName, varType, params);
            }

            final String pkgName = getPackageName(typeElement);
            final String clsName = getClassName(typeElement, pkgName) + "$ViewInjector";

            TypeSpec typeSpec = TypeSpec.classBuilder(clsName)
                    .addModifiers(Modifier.PUBLIC)
                    .addMethod(methodBuilder.build())
                    .build();

            JavaFile javaFile = JavaFile.builder(pkgName, typeSpec)
                    .build();

            try {
                javaFile.writeTo(processingEnv.getFiler());
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

    private String getClassName(TypeElement type, String pkgName) {
        int packageLength = pkgName.length() + 1;
        return type.getQualifiedName().toString().substring(packageLength).replace('.', '$');
    }
主要改动的地方如下:
  • 之前生成的类有一个参数Activity,在Adapter中也可能是ViewHolder,这里将第一个参数改为名target,类型还是通过ClassName.get(typeElement.asType())获取,再添加一个参数View,如下:
.addParameter(ClassName.get(typeElement.asType()), "target")
.addParameter(ClassName.get("android.view", "View"), "view")
  • 赋值时将activity改为target,获取id是将activity改为view,如下:
methodBuilder.addStatement("target.$L = ($L) view.findViewById($L)", varName, varType, params);
  • 生成类的类名改变
final String clsName = typeElement.getSimpleName().toString() + "$ViewInjector"; // 以前的
final String clsName = getClassName(typeElement, pkgName) + "$ViewInjector"; // 改为这个

private String getClassName(TypeElement type, String pkgName) {
    int packageLength = pkgName.length() + 1;
    return type.getQualifiedName().toString().substring(packageLength).replace('.', '$');
}
getQualifiedName获取类的全限定名,即包名+类名,getSimpleName获取简单类名,为什么要改这里呢,比如MyAdapter的内部类ViewHolder,在内部类中通过getSimpleName获取的名是ViewHolder,但其实编译后类名是MyAdapter$ViewHolder,而通过getQualifiedName获取的名为 包名+MyAdapter$ViewHolder,截取掉包名后就是MyAdapter$ViewHolder是正确的。
现在编译后生成的代码如下:
public class MainActivity$ViewInjector {
  public MainActivity$ViewInjector(MainActivity target, View view) {
    target.nameTv = (android.widget.TextView) view.findViewById(2131165304);
  }
}
  • 在ViewInjector中调用时如下:
Class<?> bindingClass = Class.forName(cls.getName() + "$ViewInjector");
Constructor constructor = bindingClass.getDeclaredConstructor(cls, View.class);
constructor.setAccessible(true);
constructor.newInstance(target, view);
  • 在调用时在Adapter中传入当前ViewHolder和View即可,在Activity中传入当前Activity和activity.getWindow().getDecorView()即可。
四、模仿ButterKnife对ViewInjector简单封装一下,加个缓存
public class ViewInjector {

    static final Map<Class<?>, Constructor> BINDINGS = new LinkedHashMap<>();

    public static void bind(Activity activity) {
        bind(activity, activity.getWindow().getDecorView());
    }

    public static void bind(Object target, View view) {
        Constructor constructor = findBindingConstructorForClass(target.getClass());
        try {
            constructor.newInstance(target, view);
        } catch (InstantiationException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        } catch (InvocationTargetException e) {
            e.printStackTrace();
        }
    }

    private static Constructor findBindingConstructorForClass(Class<?> cls) {
        Constructor constructor = BINDINGS.get(cls);
        if (constructor == null) {
            try {
                Class<?> bindingClass = Class.forName(cls.getName() + "$ViewInjector");
                constructor = bindingClass.getDeclaredConstructor(cls, View.class);
                constructor.setAccessible(true);
                BINDINGS.put(cls, constructor);
            } catch (ClassNotFoundException e) {
                e.printStackTrace();
            } catch (NoSuchMethodException e) {
                e.printStackTrace();
            }
        }
        return constructor;
    }

}
五、添加OnClick监听
  • 定义注解类,定义为运行时注解,只在方法上使用,需要一个int型参数
@Target(METHOD)
@Retention(CLASS)
public @interface OnClick {
    int value();
}
  • 修改ViewInjectorProcessor类,主要修改generateCode方法,通过ElementKind kind = element.getKind();判断注解在属性上还是方法上,然后通过下面代码生成点击事件
    ExecutableElement executableElement = (ExecutableElement) element;
    OnClick clickView = executableElement.getAnnotation(OnClick.class);
    int params = clickView.value();
    methodBuilder.addStatement("android.view.View cView = (android.view.View) view.findViewById($L)", params);
    MethodSpec innerMethodSpec = MethodSpec.methodBuilder("onClick")
            .addAnnotation(Override.class)
            .addModifiers(Modifier.PUBLIC)
            .returns(void.class)
            .addParameter(ClassName.get("android.view", "View"), "v")
            .addStatement("target.$L()", executableElement.getSimpleName().toString())
            .build();
    TypeSpec innerTypeSpec = TypeSpec.anonymousClassBuilder("")
            .addSuperinterface(ClassName.bestGuess("View.OnClickListener"))
            .addMethod(innerMethodSpec)
            .build();
    methodBuilder.addStatement("cView.setOnClickListener($L)", innerTypeSpec);
生成的代码如下:
public class MainActivity$ViewInjector {
  public MainActivity$ViewInjector(final MainActivity target, View view) {
    target.nameTv = (android.widget.TextView) view.findViewById(2131165304);
    android.view.View cView = (android.view.View) view.findViewById(2131165217);
    cView.setOnClickListener(new View.OnClickListener() {
      @Override
      public void onClick(View v) {
        target.myClick();
      }
    });
  }
}
源码地址
  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
实现圆弧进度条的自定义ProgressBar,可以使用Canvas和Paint来绘制。 首先,创建一个自定义的ProgressBar,继承自ProgressBar,并实现构造方法和onDraw方法: ``` public class CircleProgressBar extends ProgressBar { private Paint paint; // 画笔 private int roundColor; // 圆环颜色 private int progressColor; // 进度条颜色 private int textColor; // 文字颜色 private float textSize; // 文字大小 private float roundWidth; // 圆环宽度 private int max; // 最大进度 private boolean textIsDisplayable; // 是否显示进度文字 private int style; // 进度条样式 public static final int STROKE = 0; public static final int FILL = 1; public CircleProgressBar(Context context) { this(context, null); } public CircleProgressBar(Context context, AttributeSet attrs) { this(context, attrs, 0); } public CircleProgressBar(Context context, AttributeSet attrs, int defStyle) { super(context, attrs, defStyle); // 获取自定义属性的值 TypedArray mTypedArray = context.obtainStyledAttributes(attrs, R.styleable.CircleProgressBar); roundColor = mTypedArray.getColor(R.styleable.CircleProgressBar_roundColor, Color.RED); progressColor = mTypedArray.getColor(R.styleable.CircleProgressBar_progressColor, Color.GREEN); textColor = mTypedArray.getColor(R.styleable.CircleProgressBar_textColor, Color.GREEN); textSize = mTypedArray.getDimension(R.styleable.CircleProgressBar_textSize, 15); roundWidth = mTypedArray.getDimension(R.styleable.CircleProgressBar_roundWidth, 5); max = mTypedArray.getInteger(R.styleable.CircleProgressBar_max, 100); textIsDisplayable = mTypedArray.getBoolean(R.styleable.CircleProgressBar_textIsDisplayable, true); style = mTypedArray.getInt(R.styleable.CircleProgressBar_style, 0); mTypedArray.recycle(); // 初始化画笔 paint = new Paint(); } @Override protected synchronized void onDraw(Canvas canvas) { super.onDraw(canvas); // 获取圆心坐标和半径 int centerX = getWidth() / 2; int centerY = getHeight() / 2; int radius = (int) (centerX - roundWidth / 2); // 绘制圆环 paint.setColor(roundColor); paint.setStyle(Paint.Style.STROKE); paint.setStrokeWidth(roundWidth); paint.setAntiAlias(true); canvas.drawCircle(centerX, centerY, radius, paint); // 绘制进度条 paint.setStrokeWidth(roundWidth); paint.setColor(progressColor); RectF oval = new RectF(centerX - radius, centerY - radius, centerX + radius, centerY + radius); switch (style) { case STROKE: paint.setStyle(Paint.Style.STROKE); canvas.drawArc(oval, 0, 360 * getProgress() / getMax(), false, paint); break; case FILL: paint.setStyle(Paint.Style.FILL_AND_STROKE); if (getProgress() != 0) canvas.drawArc(oval, 0, 360 * getProgress() / getMax(), true, paint); break; } // 绘制文字 paint.setStrokeWidth(0); paint.setColor(textColor); paint.setTextSize(textSize); paint.setTypeface(Typeface.DEFAULT_BOLD); int percent = (int) (((float) getProgress() / (float) getMax()) * 100); if (textIsDisplayable && percent >= 0) { String text = percent + "%"; float textWidth = paint.measureText(text); canvas.drawText(text, centerX - textWidth / 2, centerY + textSize / 2, paint); } } } ``` 在这个中,我们定义了几个自定义属性,包括圆环颜色、进度条颜色、文字颜色、文字大小、圆环宽度、最大进度、是否显示进度文字、进度条样式等。在构造方法中,我们获取了这些属性的值,并初始化了画笔。在onDraw方法中,我们首先获取了圆心坐标和半径,然后使用画笔绘制了圆环和进度条,最后绘制了进度文字。 接下来,在布局文件中使用这个自定义ProgressBar: ``` <com.example.circleprogressbar.CircleProgressBar android:id="@+id/circle_progressbar" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_centerInParent="true" app:roundColor="#cccccc" app:roundWidth="5dp" app:progressColor="#FF4081" app:textColor="#FF4081" app:textSize="20sp" app:textIsDisplayable="true" app:style="STROKE" /> ``` 最后,在Java代码中设置进度值即可: ``` CircleProgressBar circleProgressBar = findViewById(R.id.circle_progressbar); circleProgressBar.setProgress(50); // 设置进度为50% ``` 这样就完成了自定义的圆弧进度条的实现
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值