Android的findViewById真是太烦人了,模板似的方法,要写在每个Activity,Fragment,Adapter里面。声明View和findView总是间隔着未知的行距;setOnClickListener之后,总是要寻找对应的onClick方法在何处。
难道Android就不能智能的把layout中的View相应的与对应field绑定起来?
答案是:Android本身不支持,但是我们可以通过一些hack达到目的。
基于反射的InjectView
findViewById接受一个int的id参数,就可以找到对应的View。通过annotation,我们可以把这个int类型的id声明的对应的field上面,通过Java的反射,遍历每个field,找到对应的id,就可以完成对field的赋值。
下面是具体做法:
声明Annotation
|
@
Retention
(
RetentionPolicy
.
RUNTIME
)
@
Target
(
{
ElementType
.
FIELD
}
)
public
@
interface
InjectView
{
int
value
(
)
;
}
|
测试Annotation
|
@
InjectView
(
R
.
id
.
aView
)
View
aView
;
|
Annotation就是一个标识,标识出aView是InjectView关心的字段。
进行注入
0
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
Field
[
]
declaredFields
=
getClass
(
)
.
getDeclaredFields
(
)
;
for
(
Field
field
:
declaredFields
)
{
InjectView
annotation
=
field
.
getAnnotation
(
InjectView
.
class
)
;
if
(
annotation
!=
null
)
{
int
id
=
annotation
.
value
(
)
;
View
view
=
findViewById
(
id
)
;
field
.
setAccessible
(
true
)
;
try
{
field
.
set
(
this
,
view
)
;
}
catch
(
IllegalAccessException
e
)
{
e
.
printStackTrace
(
)
;
}
}
}
|
在setContentView之后,我们对所有的field检查是否包含@InjectView标识,对包含@InjectView标识的field,进行赋值。
通过同样的方式,我们甚至可以把setContentView省去,也通过注入来实现。假设我们有一个@InjectLayout的Annotation,我们通过这个Annotation来标识setContentView对应的id.
|
@
InjectLayout
(
R
.
layout
.
activity_main
)
public
class
MainActivity
extends
FragmentActivity
{
|
|
InjectLayout
annotation
=
getClass
(
)
.
getAnnotation
(
InjectLayout
.
class
)
;
int
id
=
annotation
.
value
(
)
;
try
{
Method
setContentView
=
getClass
(
)
.
getMethod
(
"setContentView"
,
int
.
class
)
;
setContentView
.
invoke
(
this
,
id
)
;
}
catch
(
Exception
e
)
{
e
.
printStackTrace
(
)
;
}
|
把上面的这些代码封装到BaseActivity中,之后或许就再也不用在activity中写setContentView和findViewById方法了。
同样的,这样的方式也适用于Fragment,只是可能需要一些变通。但是不难的。
开源项目
Github上有一个强大开源项目RoboGuice
实现了一整套的注入功能,可以阅读它的官方WIKI进行了解:
https://github.com/roboguice/roboguice/wiki
性能
众所周知,相对于正常的方法调用,反射调用在执行效率上会有劣势,而且反射并不能得到编译时期的优化,使得性能差距更加明显。虽然现在Android手机的平均性能比最开始要好很多,但是在有些时候,性能往往还是需要考虑的一个很重要的一点。这时候可能就会纠结便利的@InjectView与反射带来的部分性能损耗,谁更重要。
一种更高效的InjectView – ButterKnife
这是一个开源项目,ButterKnife,官方介绍是通过AnnotationProcessor实现的View Injection, 而不是反射,性能上面不会有什么顾虑。
使用ButterKnife很简单,只需要像上面提到的那样,对View进行标注,然后在setContentView之后(或者对于Fragment在inflateView之后),调用适当的ButterKnife.inject(…)方法即可。
此外,ButterKnife的注入不依赖于反射,我们可以放心大胆的在Adapter#getView中使用:
0
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
|
public
View
getView
(
int
position
,
View
convertView
,
ViewGroup
parent
)
{
final
ViewHolder
viewHolder
;
if
(
convertView
==
null
)
{
convertView
=
layoutInflater
.
inflate
(
R
.
layout
.
element_picture_event
,
null
)
;
viewHolder
=
new
ViewHolder
(
convertView
)
;
convertView
.
setTag
(
viewHolder
)
;
}
else
{
viewHolder
=
(
ViewHolder
)
convertView
.
getTag
(
)
;
}
//do anything with view holder
}
.
.
.
class
ViewHolder
{
@
InjectViews
(
{
R
.
id
.
imageView0
,
R
.
id
.
imageView1
,
R
.
id
.
imageView2
,
R
.
id
.
imageView3
}
)
ImageView
[
]
imageViews
;
@
InjectView
(
R
.
id
.
nicknameView
)
TextView
nicknameView
;
@
InjectView
(
R
.
id
.
infoView
)
TextView
infoView
;
@
InjectView
(
R
.
id
.
avatarView
)
ImageView
avatarView
;
@
InjectView
(
R
.
id
.
statusView
)
TextView
statusView
;
public
ViewHolder
(
View
view
)
{
ButterKnife
.
inject
(
this
,
view
)
;
}
}
|
更多的使用方法,可以参考官方说明:
http://jakewharton.github.io/butterknife/
对IDE进行配置
在IDE里面使用ButterKnife,需要对IDE进行适当的配置才可以正常的运行程序。
比如说在,IntelliJ IDEA里面,需要开启AnnotationProcessor配置。如图:
对于Eclipse,官方也给出了相应的开启AnnotationProcessor的方式,可以参考下面的链接:
http://jakewharton.github.io/butterknife/ide-eclipse.html
对于IDE的自动格式化代码,可能会强行将Annotation单独在一行显示,比如说这样:
|
@
InjectView
(
R
.
id
.
ptrLayout
)
PullToRefreshLayout
ptrLayout
;
|
如果不喜欢上面的这种方式,可以在IDE中进行配置,比如说在IDEA中:
这样就可以保证@InjectView和定义View在一行显示了:
|
@
InjectView
(
R
.
id
.
ptrLayout
)
PullToRefreshLayout
ptrLayout
;
|
Ant中的配置
因为使用了AnnotationProcessor,在使用一些工具,比如说ant打包的时候,可能需要一些另外的配置,比如说,对于ant,需要加入一个处理Annotation的target. 参考代码如下:
0
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
<
javac
encoding
=
"UTF-8"
source
=
"1.5"
target
=
"1.5"
debug
=
"false"
extdirs
=
""
includeantruntime
=
"false"
destdir
=
"bin/classes"
bootclasspathref
=
"project.target.class.path"
verbose
=
"false"
classpathref
=
"project.javac.classpath"
fork
=
"false"
>
<
src
path
=
"src"
/
>
<
src
path
=
"gen"
/
>
<
compilerarg
line
=
""
/
>
<
compilerarg
line
=
"-processorpath libs/butterknife-6.0.0.jar"
/
>
<
/
javac
>
|
对于Maven或者Gradle,相信使用Annotation Processor + Maven或Gradle,就可以谷歌到相应的解决方案。