先来看下以下的一个简单布局:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:orientation="vertical"
android:gravity="center_horizontal"
android:layout_height="match_parent">
<Button
android:id="@+id/btnTest1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="test" />
<Button
android:id="@+id/btnTest2"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="test" />
<Button
android:id="@+id/btnTest3"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="test" />
<Button
android:id="@+id/btnTest4"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="test" />
<Button
android:id="@+id/btnTest5"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="test" />
<Button
android:id="@+id/btnTest6"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="test" />
<Button
android:id="@+id/btnTest7"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="test" />
</LinearLayout>
这就是一个布局有放了七个Button.
在Activity中获取,如下:
public class ActivityTest extends AppCompatActivity {
private Button btnTest1;
private Button btnTest2;
private Button btnTest3;
private Button btnTest4;
private Button btnTest5;
private Button btnTest6;
private Button btnTest7;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
btnTest1 = findViewById(R.id.btnTest1);
btnTest2 = findViewById(R.id.btnTest2);
btnTest3 = findViewById(R.id.btnTest3);
btnTest4 = findViewById(R.id.btnTest4);
btnTest5 = findViewById(R.id.btnTest5);
btnTest6 = findViewById(R.id.btnTest6);
btnTest7 = findViewById(R.id.btnTest7);
}
}
这样要写7次的findViewById,如果还有更多的控件,还要写更多次,那有没有省去findViewById的式?
不过有人会说,可以用ButterKnife,进行控件的绑定,这个也是目前用的比较多的。不过这次以另外的方法来解决这个问题。
先来看下findViewById这个方法:
有没有记得很早之前写findViewById的时候,比如
btnTest1 = findViewById(R.id.btnTest1),要是在很早之前这样写是会报错的,需要进行类型转换才能正常
btnTest1 = (Button)findViewById(R.id.btnTest1)。
而现在并不会报错,
@Override
public <T extends View> T findViewById(@IdRes int id) {
return getDelegate().findViewById(id);
}
原因就是有返回个泛型。
这个方法返回的是一个继承View的泛型。
我们再来看一个Resources中的方法:
/**
* Return a resource identifier for the given resource name. A fully
* qualified resource name is of the form "package:type/entry". The first
* two components (package and type) are optional if defType and
* defPackage, respectively, are specified here.
*
* <p>Note: use of this function is discouraged. It is much more
* efficient to retrieve resources by identifier than by name.
*
* @param name The name of the desired resource.
* @param defType Optional default resource type to find, if "type/" is
* not included in the name. Can be null to require an
* explicit type.
* @param defPackage Optional default package to find, if "package:" is
* not included in the name. Can be null to require an
* explicit package.
*
* @return int The associated resource identifier. Returns 0 if no such
* resource was found. (0 is not a valid resource ID.)
*/
public int getIdentifier(String name, String defType, String defPackage) {
return mResourcesImpl.getIdentifier(name, defType, defPackage);
}
name就是要获取资源的名字
defType 资源的类型,如R.layout.activity_main,defType 就是”layout”,R.id.button,
defType 就是”id”,
defPackage 就是app的包名。
return的返回值就是一个资源所对应的int值,如
public static final int btnOk = 0x7f050025;
做如下代码的改动:
package com.example.rxjavatest;
import android.content.Context;
import android.os.Bundle;
import android.view.View;
import android.widget.Button;
import androidx.appcompat.app.AppCompatActivity;
public class ActivityTest extends AppCompatActivity {
private Button btnTest1;
private Button btnTest2;
private Button btnTest3;
private Button btnTest4;
private Button btnTest5;
private Button btnTest6;
private Button btnTest7;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
int btnId = getIdResId(this,getPackageName(),"btnTest1");
if(btnId > 0){
btnTest1 = findViewById(btnId);
}
btnId = getIdResId(this,getPackageName(),"btnTest2");
if(btnId > 0){
btnTest2 = findViewById(btnId);
}
btnId = getIdResId(this,getPackageName(),"btnTest3");
if(btnId > 0){
btnTest3 = findViewById(btnId);
}
btnId = getIdResId(this,getPackageName(),"btnTest4");
if(btnId > 0){
btnTest4 = findViewById(btnId);
}
btnId = getIdResId(this,getPackageName(),"btnTest5");
if(btnId > 0){
btnTest5 = findViewById(btnId);
}
btnId = getIdResId(this,getPackageName(),"btnTest6");
if(btnId > 0){
btnTest6 = findViewById(btnId);
}
btnId = getIdResId(this,getPackageName(),"btnTest7");
if(btnId > 0){
btnTest7 = findViewById(btnId);
}
}
// 得到资源id
public static int getResId(Context context, String pageName, String resName, String type){
return context.getResources().getIdentifier(resName, type, pageName);
}
/**
* 取得指定名称的资源id
* @param idName
* @return
*/
public static int getIdResId(Context context, String pageName, String idName){
return getResId(context, pageName, idName, "id");
}
}
这样也是可以正常获取Button。
我人来现在具体看下
int btnId = getIdResId(this,getPackageName(),"btnTest1");
if(btnId > 0){
btnTest1 = findViewById(btnId);
}
如果把Button btnTest1,直接转成"btnTest1"就可以不用直接写字符串了。
如果可以向for循环那样遍历Button定义的变量,这样处理起来是不是也方便是很多了。
要实现这个两个功能,要用到反射。
先看反射中的一些方法:
1.Class用反射获取类中的变量。
public native Field[] getDeclaredFields();
2.Field获取变量的名称:
public String getName()
- Field设置变量:
public native void set(Object obj, Object value)
有了这三个方法,就可以做如下的实现
定义一个类叫做ViewUtil
public class ViewUtil {
/**
*
* @param cls 类的名称
* @param obj 类的实例
* @param mainView 布局Viwe,
*/
public static void setViewItem(Class<?> cls, Object obj, View mainView){
Field[] fields = cls.getDeclaredFields();//获取定义的类
if(fields != null) {
for (Field field : fields) {
field.setAccessible(true);//可以访问private变量
String name = field.getName();//变量的名称
String declaringClassName = field.getType().getName();//定义的类
//用于判断是否是View或是View的子类。
if(!declaringClassName.contains("android.view") &&
!declaringClassName.contains("android.widget")){
continue;
}
if(name == null){
continue;
}
Context context = mainView.getContext();
int id = getIdResId(context,context.getPackageName(),name);//通过名称获取id
if(id <= 0){
continue;
}
try {
View view = mainView.findViewById(id);
if(view != null){
field.set(obj,view);//给变量赋值
}
} catch (IllegalAccessException e) {
e.printStackTrace();
}
}
}
}
// 得到资源id
public static int getResId(Context context, String pageName, String resName, String type){
return context.getResources().getIdentifier(resName, type, pageName);
}
/**
* 取得指定名称的资源id
* @param idName
* @return
*/
public static int getIdResId(Context context, String pageName, String idName){
return getResId(context, pageName, idName, "id");
}
}
setViewItem这里不传Activity,而传个View主要是为了通用考虑,像Dialog,悬浮的窗口等,是没有Activity的。
public class ActivityTest extends AppCompatActivity {
private Button btnTest1;
private Button btnTest2;
private Button btnTest3;
private Button btnTest4;
private Button btnTest5;
private Button btnTest6;
private Button btnTest7;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
View mainView = LayoutInflater.from(this).inflate(R.layout.activity_main,null);
setContentView(mainView);
ViewUtil.setViewItem(this.getClass(),this,mainView);
btnTest1.setText("btnTest1");
btnTest2.setText("btnTest2");
btnTest3.setText("btnTest3");
btnTest4.setText("btnTest4");
btnTest5.setText("btnTest5");
btnTest6.setText("btnTest6");
btnTest7.setText("btnTest7");
}
private void btnTest(){
int btnId = ViewUtil.getIdResId(this,getPackageName(),"btnTest1");
if(btnId > 0){
btnTest1 = findViewById(btnId);
}
btnId = ViewUtil.getIdResId(this,getPackageName(),"btnTest2");
if(btnId > 0){
btnTest2 = findViewById(btnId);
}
btnId = ViewUtil.getIdResId(this,getPackageName(),"btnTest3");
if(btnId > 0){
btnTest3 = findViewById(btnId);
}
btnId = ViewUtil.getIdResId(this,getPackageName(),"btnTest4");
if(btnId > 0){
btnTest4 = findViewById(btnId);
}
btnId =ViewUtil.getIdResId(this,getPackageName(),"btnTest5");
if(btnId > 0){
btnTest5 = findViewById(btnId);
}
btnId = ViewUtil.getIdResId(this,getPackageName(),"btnTest6");
if(btnId > 0){
btnTest6 = findViewById(btnId);
}
btnId = ViewUtil.getIdResId(this,getPackageName(),"btnTest7");
if(btnId > 0){
btnTest7 = findViewById(btnId);
}
}
}
这个看起来,获取布局控件是方便多了,但是还是有几个问题:
- 如果是自定义的控件,包名都是不确定的,要如何区分是布局的变量还是其他的变量?
- 如果控件名称和布局id的名称不一致要怎么处理?
- 如果不是控件了,是像String,Color,Dimen这些,也是可以用类似的方法的,但要怎么在一个里做区分?
解决这个几个问题,可以用到运行时注解。
先定义一个enum
public enum FieldType {
ID(0,"id",""),
STRING(1,"string","getString"),
COLOR(2,"color","getColor"),
DINEN(3,"dimen","getDimension"),
DINEN_PIXEL_SIZE(4,"dimen","getDimensionPixelSize"),
ANIM(5,"anim","getAnimation");
private String defType;
private int defTypeId = 0;
private String methodName = "";
private FieldType(int type,String typeName,String methodName){
defTypeId = type;
defType = typeName;
this.methodName = methodName;
}
public String getDefType(){
return defType;
}
public String getMethodName(){
return methodName;
}
}
像这个emun中methodName对应的是Resources里对应的方法,比如
public String getString(@StringRes int id) throws NotFoundException,
public int getColor(@ColorRes int id) throws NotFoundException,
public float getDimension(@DimenRes int id) throws NotFoundException,
public int getDimensionPixelSize(@DimenRes int id) throws NotFoundException
public XmlResourceParser getAnimation(@AnimatorRes @AnimRes int id) throws NotFoundException
这些相对应的函数名称。
再定义一个注解:
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface ViewField {
int resId() default -1;//默认ViewId为-1
FieldType fieldType() default FieldType.ID;//哪种类型
}
resId默认为-1,表示直接用变量的名称来获取相对资源的值。
fieldType表示是要获取哪类资源的值,默认是获取View的id.
具体的实现如下
/**
*
* @param cls 类的名称
* @param obj 类的实例
* @param mainView 布局Viwe
*/
public static void setResItem(Class<?> cls, Object obj, Context context,View mainView){
Field[] fields = cls.getDeclaredFields();//获取定义的类
if(fields != null) {
for (Field field : fields) {
Log.e("cl1"," field is "+field);
field.setAccessible(true);
ViewField annotation = field.getAnnotation(ViewField.class);
Log.e("cl1"," annotation is "+annotation+" fieldName is "+ field.getName());
if(annotation == null) {
continue;
}
FieldType fieldType = annotation.fieldType();
int resId = annotation.resId();
if(resId != -1){
}else{
String name = field.getName();//变量的名称
Log.e("cl"," name is "+name);
if(name == null){
continue;
}
String defType = fieldType.getDefType();
/*
String defType = "";
switch (fieldType){
case STRING:
defType = "string";
break;
case COLOR:
defType = "color";
break;
case ANIM:
defType = "anim";
break;
case DINEN:
defType = "dimen";
break;
}*/
resId= getResId(context,context.getPackageName(),name,defType);//通过名称获取id
if(resId <= 0){
continue;
}
}
Object res = null;
Resources resources = context.getResources();
if(fieldType == FieldType.ID){
res = mainView.findViewById(resId);
}else{
/*
switch (fieldType){
case STRING:
res = resources.getString(resId);
break;
case COLOR:
res = resources.getColor(resId);
break;
case ANIM:
res = resources.getAnimation(resId);
break;
case DINEN:
res = resources.getDimension(resId);
break;
}*/
try {
Log.e("cl"," methodName is "+fieldType.getMethodName());
Method method = Resources.class.getMethod(fieldType.getMethodName(),int.class);
Log.e("cl"," method is "+method);
if(method != null){
res = method.invoke(resources,resId);
}
} catch (NoSuchMethodException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
}
}
try {
if(res != null){
field.set(obj,res);//给变量赋值
}
} catch (IllegalAccessException e) {
e.printStackTrace();
}
}
}
}
先看下以的代码:
String defType = fieldType.getDefType();
resId= getResId(context,context.getPackageName(),name,defType);//通过名称获取id
还有一种比较简单粗暴的写好,一般大家都能想的到,就是用switch语句:
String defType = "";
switch (fieldType){
case STRING:
defType = "string";
break;
case COLOR:
defType = "color";
break;
case ANIM:
defType = "anim";
break;
case DINEN:
defType = "dimen";
break;
}
resId= getResId(context,context.getPackageName(),name,defType);//通过名称获取id
if(resId <= 0){
continue;
}
这个能实现相同的功能,但是有个问题是,如果我要加个其他类型的,还是要再加个case语句才能进行扩展,扩展性就变差了。所以选择在emun中定义类型,这样我只需要增加emun中的类型就能进行扩展了。
同样的还有如下的代码:
try {
Method method = Resources.class.getMethod(fieldType.getMethodName(),int.class);
if(method != null){
Resources resources = context.getResources();
res = method.invoke(resources,resId);
}
} catch (NoSuchMethodException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
}
也可以写与如下的switc代码:
switch (fieldType){
case STRING:
res = resources.getString(resId);
break;
case COLOR:
res = resources.getColor(resId);
break;
case ANIM:
res = resources.getAnimation(resId);
break;
case DINEN:
res = resources.getDimension(resId);
break;
}
同样也是有扩展性的问题,所以把方法直接定义在了emun中。
这样就基本实现了一个比较简单的获取资源和布局的一种方法,但是这种方式,如果是用变量名去获取id值的话,代码混淆的时候,不能把变量给混淆了。
gitHub地址:git@github.com:fzhFidt/ResUtil.git