在上篇文章从源码角度深入理解LayoutInflater.Factory主要介绍了LayoutInflater.Factory是什么,并简单介绍了一下用Factory可以做些什么,本篇文章就具体介绍一下Factory在换肤上的具体应用。
在上一篇博文中我们在Factory中打印了一下输出后的AttributeSet信息如下:
1
2
3
4
5
|
gravity
:
0x11
background
:
#ffff0000
layout_width
:
-
1
layout_height
:
48.0dip
text
:
@
2131361814
|
这里只是打印出了View的属性值和属性名称,当然了资源名称和资源类型也可以打印出来,下面是获取属性和资源的四个相关方法:
1
2
3
4
5
6
7
|
//位于AttributeSet类中
public
String
getAttributeName
(
int
index
)
;
//属性名称
public
String
getAttributeValue
(
int
index
)
;
//属性值
//位于Resources类
public
String
getResourceEntryName
(
@AnyRes
int
resid
)
//资源名称
public
String
getResourceTypeName
(
@AnyRes
int
resid
)
//资源类型
|
在对每一个需要换肤的View进行操作的时候,资源类型一般就是两种类型,要么是color要么是一个drawable,但是属性就比较多了,有可能是一个textColor,有可能是background,如单复选按钮还可能是一个button,因此我们可以将属性相关的类设计为一个工厂类SkinFactory,根据不同的属性设置不同的属性类,如TextColorAttr或者BackgroundAttr。另外一个需要注意的就是在进行换肤的时候资源应该都是以引用的形式设置,此时输出的结果都是一个以@开始的属性值,@后面对应的值就是一个资源的ID,然后我们通过下面两个方法就可以获取资源真正的ID了。
String resName = context.getResources().getResourceEntryName(resId);
int trueResId = resources.getIdentifier(resName, “color”, skinPackageName);
换肤的核心就在这里了,只要我们将资源Resources获取到就可以获取到资源中相应的资源了,无论是应用内换肤还是插件式换肤都是一样的,只是获取以及处理资源的方式不同罢了,本篇重点讲解的是插件式换肤,类似QQ方式,可以远程下载皮肤库到本地,然后进行换肤。
首先确定属性的基类,基类中至少需要四个属性:View属性名称和值以及资源名称和类型,另外由于资源类型确定为只有两种类型:color和drawable,可以将资源类型设置为两个常量。当我们获取到插件包中的资源库以后,根据不同的属性实现类必须有不同的实现方法,如果是字体颜色apply()方法中可以是view.setTextColor()方法等等,因此在基类中可以设置一个抽象方法,所有实现该基类的子类必须实现该方法,SkinAttr基类代码如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
|
/**
* android:textColor="@color/text_default"
* View属性名称 attrName:textColor
* 资源ID attrValueId R类中对text_default对应的一个整型值
* 资源名称 attrEntryName:text_default
* 资源类型 attrEntryType:color
*/
public
abstract
class
SkinAttr
{
//资源类型color
protected
static
final
String
TYPE_NAME_COLOR
=
"color"
;
//资源类型drawable
protected
static
final
String
TYPE_NAME_DRAWABLE
=
"drawable"
;
//属性名称
public
String
attrName
;
//属性引用资源ID
public
int
attrValueId
;
//资源名称
public
String
attrEntryName
;
//资源类型
public
String
attrEntryType
;
//不同子类必须要实现的方法
public
abstract
void
apply
(
View
view
)
;
@Override
public
String
toString
(
)
{
return
"SkinAttr [attrName="
+
attrName
+
", attrValueId="
+
attrValueId
+
", attrEntryName="
+
attrEntryName
+
", attrEntryType="
+
attrEntryType
+
"]"
;
}
}
|
根据不同的属性可以创建不同的实现类,如textColor对应TextColorAttr,background对应BackgroundAttr等等。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
|
public
class
TextColorAttr
extends
SkinAttr
{
@Override
public
void
apply
(
View
view
)
{
if
(
view
instanceof
TextView
)
{
TextView
tv
=
(
TextView
)
view
;
if
(
TYPE_NAME_COLOR
.
equals
(
attrEntryType
)
)
{
tv
.
setTextColor
(
SkinManager
.
getInstance
(
)
.
getColorStateList
(
attrValueId
)
)
;
}
}
}
}
public
class
BackgroundAttr
extends
SkinAttr
{
@Override
public
void
apply
(
View
view
)
{
if
(
TYPE_NAME_COLOR
.
equals
(
attrEntryType
)
)
{
view
.
setBackgroundColor
(
SkinManager
.
getInstance
(
)
.
getColor
(
attrValueId
)
)
;
}
else
if
(
TYPE_NAME_DRAWABLE
.
equals
(
attrEntryType
)
)
{
Drawable
bg
=
SkinManager
.
getInstance
(
)
.
getDrawable
(
attrValueId
)
;
view
.
setBackgroundDrawable
(
bg
)
;
}
}
}
|
当所有的属性实现类完成后,接着设计一个工厂类AttrFactory,根据获取的属性名称不同生成不同的属性实现类,将来一旦有其它属性或者自定义属性需要实现换肤只需在该类中添加相应的实现即可。在AttFactory类中需要增加一个判断方法,因为在LayoutInflater.Factory中输出的属性是View的所有属性,但是并不是所有属性都需要实现换肤逻辑,只需要将所有需要实现换肤逻辑的属性定义为常量,在生成SkinAttr实现类之前判断一下该属性是否需要实现换肤逻辑。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
|
public
class
AttrFactory
{
public
static
final
String
BACKGROUND
=
"background"
;
public
static
final
String
TEXT_COLOR
=
"textColor"
;
public
static
final
String
LIST_SELECTOR
=
"listSelector"
;
public
static
final
String
DIVIDER
=
"divider"
;
public
static
final
String
BUTTON
=
"button"
;
public
static
SkinAttr
get
(
String
attrName
,
int
attrValueId
,
String
attrEntryName
,
String
attrEntryType
)
{
SkinAttr
mSkinAttr
=
null
;
if
(
BACKGROUND
.
equals
(
attrName
)
)
{
mSkinAttr
=
new
BackgroundAttr
(
)
;
}
else
if
(
TEXT_COLOR
.
equals
(
attrName
)
)
{
mSkinAttr
=
new
TextColorAttr
(
)
;
}
else
if
(
BUTTON
.
equals
(
attrName
)
)
{
mSkinAttr
=
new
ButtonAttr
(
)
;
}
else
if
(
LIST_SELECTOR
.
equals
(
attrName
)
)
{
mSkinAttr
=
new
ListSelectorAttr
(
)
;
}
else
if
(
DIVIDER
.
equals
(
attrName
)
)
{
mSkinAttr
=
new
DividerAttr
(
)
;
}
else
{
return
null
;
}
mSkinAttr
.
attrName
=
attrName
;
mSkinAttr
.
attrValueId
=
attrValueId
;
mSkinAttr
.
attrEntryName
=
attrEntryName
;
mSkinAttr
.
attrEntryType
=
attrEntryType
;
return
mSkinAttr
;
}
/**
* 判断是否需要换肤
* @param attrName
* @return
*/
public
static
boolean
isSupportedAttr
(
String
attrName
)
{
if
(
BACKGROUND
.
equals
(
attrName
)
)
{
return
true
;
}
if
(
BUTTON
.
equals
(
attrName
)
)
{
return
true
;
}
if
(
TEXT_COLOR
.
equals
(
attrName
)
)
{
return
true
;
}
if
(
LIST_SELECTOR
.
equals
(
attrName
)
)
{
return
true
;
}
if
(
DIVIDER
.
equals
(
attrName
)
)
{
return
true
;
}
return
false
;
}
}
|
属性的相关类基本封装好了,一个View在换肤的时候可能需要涉及到多个属性,如字体颜色、背景色等,因此我们可以将View和需要换肤的属性再封装进一个类中,在进行换肤的时候调用一个apply方法,通过资源依次赋值到View的属性中。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
|
public
class
SkinView
{
public
View
view
;
//所有涉及到需要换肤的属性
public
List
<SkinAttr>
attrs
;
public
SkinView
(
)
{
attrs
=
new
ArrayList
<>
(
)
;
}
//将属性依次赋值到View
public
void
apply
(
)
{
if
(
view
!=
null
&&
!
ListUtils
.
isEmpty
(
attrs
)
)
{
for
(
SkinAttr
attr
:
attrs
)
{
attr
.
apply
(
view
)
;
}
}
}
//在销毁时清除
public
void
clean
(
)
{
if
(
ListUtils
.
isEmpty
(
attrs
)
)
{
return
;
}
for
(
SkinAttr
at
:
attrs
)
{
at
=
null
;
}
}
}
|
走到这里属性封装好了,涉及到换肤的View也封装完毕,接下来就是逻辑实现了,针对每个属性的apply()方法该如何实现呢?我们知道在设置字体颜色的时候一般使用的是resource.getColor(@ColorRes int id)
,背景图片resource.getDrawable(@DrawableRes int id)
,只要可以获取到插件包的Resources就可以使用插件包中的资源文件了。
获取插件包中Resources,在Resources的一个构造方法中可以传入AssetManager实例,AssetManager中addAssetPath()方法可以将一个apk中的资源加载到Resources对象中,由于addAssetPath是隐藏API我们无法直接调用,所以只能通过反射。下面是它的声明,通过注释我们可以看出,传递的路径可以是zip文件也可以是一个资源目录,而apk就是一个zip,所以直接将apk的路径传给它,资源就加载到AssetManager中了。然后再通过AssetManager来创建一个新的Resources对象,通过这个对象我们就可以访问插件apk中的资源了,这样一来问题就解决了。
1
2
3
4
5
6
7
8
9
10
11
12
13
|
/**
* Add an additional set of assets to the asset manager. This can be
* either a directory or ZIP file. Not for use by applications. Returns
* the cookie of the added asset, or 0 on failure.
* {@hide}
*/
public
final
int
addAssetPath
(
String
path
)
{
synchronized
(
this
)
{
int
res
=
addAssetPathNative
(
path
)
;
makeStringBlocks
(
mStringBlocks
)
;
return
res
;
}
}
|
由于AssetManager的构造方法也是隐藏API,所以获取AssetManager实例也需要使用反射技术,然后调用addAssetPath()方法。将远程获取的资源包存入SDcard,然后使用一个异步任务来操作比较耗时的IO操作,下面就是使用AsyncTask获取资源包中的Resources对象。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
|
new
AsyncTask
<
String
,
Void
,
Resources
>
(
)
{
@Override
protected
void
onPreExecute
(
)
{
if
(
listener
!=
null
)
{
listener
.
onStart
(
)
;
}
}
@Override
protected
Resources
doInBackground
(
String
.
.
.
params
)
{
try
{
if
(
params
.
length
==
1
)
{
String
skinPkgPath
=
params
[
0
]
;
File
file
=
new
File
(
skinPkgPath
)
;
if
(
file
==
null
||
!
file
.
exists
(
)
)
{
return
null
;
}
PackageManager
mgr
=
context
.
getPackageManager
(
)
;
PackageInfo
info
=
mgr
.
getPackageArchiveInfo
(
skinPkgPath
,
PackageManager
.
GET_ACTIVITIES
)
;
skinPackageName
=
info
.
packageName
;
AssetManager
assetManager
=
AssetManager
.
class
.
newInstance
(
)
;
Method
addAssetPath
=
assetManager
.
getClass
(
)
.
getMethod
(
"addAssetPath"
,
String
.
class
)
;
addAssetPath
.
invoke
(
assetManager
,
skinPkgPath
)
;
Resources
superRes
=
context
.
getResources
(
)
;
Resources
skinResource
=
new
Resources
(
assetManager
,
superRes
.
getDisplayMetrics
(
)
,
superRes
.
getConfiguration
(
)
)
;
SkinConfig
.
saveSkinPath
(
context
,
skinPkgPath
)
;
skinPath
=
skinPkgPath
;
isDefaultSkin
=
false
;
return
skinResource
;
}
return
null
;
}
catch
(
Exception
e
)
{
e
.
printStackTrace
(
)
;
return
null
;
}
}
protected
void
onPostExecute
(
Resources
result
)
{
resources
=
result
;
if
(
resources
!=
null
)
{
if
(
listener
!=
null
)
{
listener
.
onSuccess
(
)
;
}
notifySkinUpdate
(
)
;
}
else
{
isDefaultSkin
=
true
;
if
(
listener
!=
null
)
{
listener
.
onFailed
(
)
;
}
}
}
}
.
execute
(
skinPackagePath
)
;
|
获取到资源Resources后,就可以通过Resources得到color或者drawable了。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
public
int
getColor
(
int
resId
)
{
int
originColor
=
ContextCompat
.
getColor
(
context
,
resId
)
;
if
(
resources
==
null
||
isDefaultSkin
)
{
return
originColor
;
}
String
resName
=
context
.
getResources
(
)
.
getResourceEntryName
(
resId
)
;
int
trueResId
=
resources
.
getIdentifier
(
resName
,
"color"
,
skinPackageName
)
;
int
trueColor
=
0
;
try
{
trueColor
=
ResourcesCompat
.
getColor
(
resources
,
trueResId
,
null
)
;
}
catch
(
NotFoundException
e
)
{
e
.
printStackTrace
(
)
;
trueColor
=
originColor
;
}
return
trueColor
;
}
|
由于换肤涉及到整个应用,所以我们可以为操作换肤的类设计为一个单例模式的类SkinManager,在SkinManager中传入一个Context对象,可以直接使用ApplicationContext。
1
2
3
4
5
6
7
8
9
10
|
public
void
init
(
Context
ctx
)
{
context
=
ctx
.
getApplicationContext
(
)
;
}
public
static
SkinManager
getInstance
(
)
{
return
InstanceHolder
.
holder
;
}
private
static
class
InstanceHolder
{
private
static
SkinManager
holder
=
new
SkinManager
(
)
;
}
|
由于传入的是ApplicationContext,当我们在Activity中使用进行换肤的时候,因为Application是在应用的整个生命周期内的,所以到某个Activity进行垃圾回收时不能被回收,引起内存溢出,因为该Activity持有Application的引用,所以我们需要再设计两个方法。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
@Override
public
void
attach
(
SkinObserver
observer
)
{
if
(
skinObservers
==
null
)
{
skinObservers
=
new
ArrayList
<SkinObserver>
(
)
;
}
if
(
!
skinObservers
.
contains
(
observer
)
)
{
skinObservers
.
add
(
observer
)
;
}
}
@Override
public
void
detach
(
SkinObserver
observer
)
{
if
(
skinObservers
==
null
)
return
;
if
(
skinObservers
.
contains
(
observer
)
)
{
skinObservers
.
remove
(
observer
)
;
}
}
|
当资源获取后,就可以通知Activity进行换肤回调了,如果没有回调,当我们在Activity任务栈回退操作的时候,导致上一个界面仍然保持了换肤之前的状态。所以这时候我们就知道了换肤操作实际上采用的是观察者模式,当Activity进入任务栈的时候SkinManger调用attach()方法,销毁的时候调用detach()方法。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
|
public
interface
SkinObservable
{
void
attach
(
SkinObserver
observer
)
;
void
detach
(
SkinObserver
observer
)
;
void
notifySkinUpdate
(
)
;
}
public
interface
SkinObserver
{
void
onThemeUpdate
(
)
;
}
public
class
SkinManager
implements
SkinObservable
{
.
.
.
@Override
public
void
notifySkinUpdate
(
)
{
if
(
skinObservers
==
null
)
return
;
for
(
SkinObserver
observer
:
skinObservers
)
{
observer
.
onThemeUpdate
(
)
;
}
}
}
public
class
BaseActivity
extends
AppCompatActivity
implements
SkinObserver
{
.
.
.
@Override
public
void
onThemeUpdate
(
)
{
skinFactory
.
applySkin
(
)
;
}
}
|
本篇博客暂时介绍到这里,在后续博客中我们继续介绍SkinFactory的设计,如何在SkinFactory中在不影响系统本身创建View的条件下进行换肤操作,当然了换肤操作比实际想象中的要复杂一些,这里讲解的都是通过布局文件生成的View,如果通过Java代码new的View又该如何换肤呢?这里讲解的主要是插件式换肤,但是如果是应用内换肤资源又该如何操作呢?还有App中如果使用了WebView加载的网页等等,如应用市场中网易新闻、开发者头条、知乎都涉及到了WebView对网页的换肤操作,在后续文章中都会逐一揭晓。