android 状态栏 背景色_技术一面:说说Android动态换肤实现原理
換膚分為動態換膚和靜態換膚
靜態換膚
這種換膚的方式,也就是我們所說的內置換膚,就是在APP內部放置多套相同的資源。進行資源的切換。
這種換膚的方式有很多缺點,比如, 靈活性差,只能更換內置的資源、apk體積太大,在我們的應用Apk中等一般圖片文件能占到apk大小的一半左右。
當然了,這種方式也并不是一無是處, 比如我們的應用內,只是普通的 日夜間模式 的切換,并不需要圖片等的更換,只是更換顏色,那這樣的方式就很實用。
動態換膚
適用于大量皮膚,用戶選擇下載,像QQ、網易云音樂這種。它是將皮膚包下載到本地,皮膚包其實是個APK。
換膚包括替換圖片資源、布局顏色、字體、文字顏色、狀態欄和導航欄顏色。
動態換膚步驟包括:
采集需要換膚的控件
加載皮膚包
替換資源
實現原理
首先Activity的onCreate()方法里面我們都要去調用setContentView(int id) 來指定當前Activity的布局文件:
@Overrideprotected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
}
再往里看:
@Overridepublic void setContentView(int resId) {
ensureSubDecor();
ViewGroup contentParent = (ViewGroup) mSubDecor.findViewById(android.R.id.content);
contentParent.removeAllViews();
LayoutInflater.from(mContext).inflate(resId, contentParent);//這里實現view布局的加載
mOriginalWindowCallback.onContentChanged();
} public View inflate(@LayoutRes int resource, @Nullable ViewGroup root) {
return inflate(resource, root, root != null);
}
public View inflate(@LayoutRes int resource, @Nullable ViewGroup root, boolean attachToRoot) {
final Resources res = getContext().getResources();
if (DEBUG) {
Log.d(TAG, "INFLATING from resource: \"" + res.getResourceName(resource) + "\" ("
+ Integer.toHexString(resource) + ")");
}
final XmlResourceParser parser = res.getLayout(resource);
try {
return inflate(parser, root, attachToRoot);
} finally {
parser.close();
}
} public View inflate(XmlPullParser parser, @Nullable ViewGroup root, boolean attachToRoot) {
...
final String name = parser.getName();
final View temp = createViewFromTag(root, name, inflaterContext, attrs);
...
return temp;
}
可以看到inflate會返回具體的View對象出去,那么我們的關注焦點就放在createViewFromTag中了
/*** Creates a view from a tag name using the supplied attribute set.
*
* Note: Default visibility so the BridgeInflater can
* override it.
*
* @param parent the parent view, used to inflate layout params
* @param name the name of the XML tag used to define the view
* @param context the inflation context for the view, typically the
* {@code parent} or base layout inflater context
* @param attrs the attribute set for the XML tag used to define the view
* @param ignoreThemeAttr {@code true} to ignore the {@code android:theme}
* attribute (if set) for the view being inflated,
* {@code false} otherwise
*/
View createViewFromTag(View parent, String name, Context context, AttributeSet attrs,
boolean ignoreThemeAttr) {
try {
View view;
if (mFactory2 != null) {
view = mFactory2.onCreateView(parent, name, context, attrs);
} else if (mFactory != null) {
view = mFactory.onCreateView(name, context, attrs);
} else {
view = null;
}
return view;
} catch (Exception e) {
}
}
inflate最終調用了createViewFromTag方法來創建View,在這之中用到了factory,如果factory存在就用factory創建對象,如果不存在就由系統自己去創建。我們只需要實現我們的Factory然后設置給mFactory2就可以采集到所有的View了,這里是一個Hook點。
當我們采集完了需要換膚的view,下一步就是加載皮膚包資源。當我們拿到當前View的資源名稱時就會先去皮膚插件中的資源文件里找
Android加載資源的流程圖:
1.采集換膚控件
android解析xml創建view的步驟:
setContentView -> window.setContentView()(實現類是PhoneWindow)->mLayoutInflater.inflate() -> inflate … ->createViewFromTag().
所以我們復寫了Factory的onCreateView之后,就可以不通過系統層而是自己截獲從xml映射的View進行相關View創建的操作,包括對View的屬性進行設置(比如背景色,字體大小,顏色等)以實現換膚的效果。如果onCreateView返回null的話,會將創建View的操作交給Activity默認實現的Factory的onCreateView處理。
1.使用ActivityLifecycleCallbacks,盡可能少的去侵入代碼,在onActivityCreated中監聽每個activity的創建。
@Overridepublic void onActivityCreated(Activity activity, Bundle savedInstanceState) {
LayoutInflater layoutInflater = LayoutInflater.from(activity);
try {
//系統默認 LayoutInflater只能設置一次factory,所以利用反射解除限制
Field mFactorySet = LayoutInflater.class.getDeclaredField("mFactorySet");
mFactorySet.setAccessible(true);
mFactorySet.setBoolean(layoutInflater, false);
} catch (Exception e) {
e.printStackTrace();
}
//添加自定義創建View 工廠
SkinLayoutFactory factory = new SkinLayoutFactory(activity, skinTypeface);
layoutInflater.setFactory2(factory);
}
2.在 SkinLayoutFactory中將每個創建的view進行篩選采集
//根據tag反射獲取view@Override
public View onCreateView(View parent, String name, Context context, AttributeSet attrs) {
// 反射 classLoader
View view = createViewFromTag(name, context, attrs);
// 自定義View
if(null == view){
view = createView(name, context, attrs);
}
//篩選符合屬性View
skinAttribute.load(view, attrs);
return view;
}
3.將view封裝成對象
//view的參數對象static class SkinPain {
String attributeName;
int resId;
public SkinPain(String attributeName, int resId) {
this.attributeName = attributeName;
this.resId = resId;
}
}
//view對象
static class SkinView {
View view;
List<SkinPain> skinPains;
public SkinView(View view, List<SkinPain> skinPains) {
this.view = view;
this.skinPains = skinPains;
}
}
將屬性符合的view保存起來
public class SkinAttribute {private static final List<String> mAttributes = new ArrayList<>();
static {
mAttributes.add("background");
mAttributes.add("src");
mAttributes.add("textColor");
mAttributes.add("drawableLeft");
mAttributes.add("drawableTop");
mAttributes.add("drawableRight");
mAttributes.add("drawableBottom");
mAttributes.add("skinTypeface");
}
private List<SkinView> skinViews = new ArrayList<>();
public void load(View view, AttributeSet attrs) {
List<SkinPain> skinPains = new ArrayList<>();
for (int i = 0; i < attrs.getAttributeCount(); i++) {
//獲取屬性名字
String attributeName = attrs.getAttributeName(i);
if (mAttributes.contains(attributeName)) {
//獲取屬性對應的值
String attributeValue = attrs.getAttributeValue(i);
if (attributeValue.startsWith("#")) {
continue;
}
int resId;
//判斷前綴字符串 是否是"?"
//attributeValue = "?2130903043"
if (attributeValue.startsWith("?")) { //系統屬性值
//字符串的子字符串 從下標 1 位置開始
int attrId = Integer.parseInt(attributeValue.substring(1));
resId = SkinThemeUtils.getResId(view.getContext(), new int[]{attrId})[0];
} else {
//@1234564
resId = Integer.parseInt(attributeValue.substring(1));
}
if (resId != 0) {
SkinPain skinPain = new SkinPain(attributeName, resId);
skinPains.add(skinPain);
}
}
}
//SkinViewSupport是自定義view實現的接口,用來區分是否需要換膚
if (!skinPains.isEmpty() || view instanceof TextView || view instanceof SkinViewSupport) {
SkinView skinView = new SkinView(view, skinPains);
skinView.applySkin(mTypeface);
skinViews.add(skinView);
}
}
...
}
2.加載皮膚包
加載皮膚包需要我們動態獲取網絡下載的皮膚包資源,問題是我們如何加載皮膚包中的資源
Android訪問資源使用的是Resources這個類,但是程序里面通過getContext獲取到的Resources實例實際上是對應程序本來的資源的實例,也就是說這個實例只能加載app里面的資源,想要加載皮膚包里面的就不行了
自己構造一個Resources(這個Resources指向的資源就是我們的皮膚包)
看看Resources的構造方法,可以看到主要是需要一個AssetManager
this(null);
mResourcesImpl = new ResourcesImpl(assets, metrics, config, new DisplayAdjustments());
}
構造一個指向皮膚包的AssetManager,但是這個AssetManager是不能直接new出來的,這里就使用反射來實例化了
AssetManager assetManager = AssetManager.class.newInstance();AssetManager有一個addAssetPath方法可以指定資源的位置,可惜這個也只能用反射來調用
Method addAssetPath = assetManager.getClass().getMethod("addAssetPath", String.class);addAssetPath.invoke(assetManager, filePath);
再來看看Resources的其他兩個參數,一個是DisplayMetrics,一個是Configuration,這兩的就可以直接使用app原來的Resources里面的就可以。
具體代碼如下:
public void loadSkin(String path) {if(TextUtils.isEmpty(path)){
// 記錄使用默認皮膚
SkinPreference.getInstance().setSkin("");
//清空資源管理器, 皮膚資源屬性等
SkinResources.getInstance().reset();
} else {
try {
//反射創建AssetManager
AssetManager manager = AssetManager.class.newInstance();
// 資料路徑設置
Method addAssetPath = manager.getClass().getMethod("addAssetPath", String.class);
addAssetPath.invoke(manager, path);
Resources appResources = this.application.getResources();
Resources skinResources = new Resources(manager,
appResources.getDisplayMetrics(), appResources.getConfiguration());
//記錄當前皮膚包
SkinPreference.getInstance().setSkin(path);
//獲取外部Apk(皮膚薄) 包名
PackageManager packageManager = this.application.getPackageManager();
PackageInfo packageArchiveInfo = packageManager.getPackageArchiveInfo(path, PackageManager.GET_ACTIVITIES);
String packageName = packageArchiveInfo.packageName;
SkinResources.getInstance().applySkin(skinResources,packageName);
} catch (Exception e) {
e.printStackTrace();
}
}
setChanged();
//通知觀者者,進行替換資源
notifyObservers();
}
3.替換資源
換膚的核心操作就是替換資源,這里采用觀察者模式,被觀察者是我們的換膚管理類SkinManager,觀察者是我們之前緩存的每個頁面的LayoutInflater.Factory2
@Overridepublic void update(Observable o, Object arg) {
//狀態欄
SkinThemeUtils.updataStatusBarColor(activity);
//字體
Typeface skinTypeface = SkinThemeUtils.getSkinTypeface(activity);
skinAttribute.setTypeface(skinTypeface);
//更換皮膚
skinAttribute.applySkin();
}
applySkin()在去遍歷每個factory緩存的需要換膚的view,調用他們的換膚方法
public void applySkin() {for (SkinView mSkinView : skinViews) {
mSkinView.applySkin(mTypeface);
}
}
applySkin方法如下:
public void applySkin(Typeface typeface) {//換字體
if(view instanceof TextView){
((TextView) view).setTypeface(typeface);
}
//自定義view換膚
if(view instanceof SkinViewSupport){
((SkinViewSupport)view).applySkin();
}
for (SkinPain skinPair : skinPains) {
Drawable left = null, top = null, right = null, bottom = null;
switch (skinPair.attributeName) {
case "background":
Object background = SkinResources.getInstance().getBackground(
skinPair.resId);
//Color
if (background instanceof Integer) {
view.setBackgroundColor((Integer) background);
} else {
ViewCompat.setBackground(view, (Drawable) background);
}
break;
case "src":
background = SkinResources.getInstance().getBackground(skinPair
.resId);
if (background instanceof Integer) {
((ImageView) view).setImageDrawable(new ColorDrawable((Integer)
background));
} else {
((ImageView) view).setImageDrawable((Drawable) background);
}
break;
case "textColor":
((TextView) view).setTextColor(SkinResources.getInstance().getColorStateList
(skinPair.resId));
break;
case "drawableLeft":
left = SkinResources.getInstance().getDrawable(skinPair.resId);
break;
case "drawableTop":
top = SkinResources.getInstance().getDrawable(skinPair.resId);
break;
case "drawableRight":
right = SkinResources.getInstance().getDrawable(skinPair.resId);
break;
case "drawableBottom":
bottom = SkinResources.getInstance().getDrawable(skinPair.resId);
break;
case "skinTypeface" :
applyTypeface(SkinResources.getInstance().getTypeface(skinPair.resId));
break;
default:
break;
}
if (null != left || null != right || null != top || null != bottom) {
((TextView) view).setCompoundDrawablesWithIntrinsicBounds(left, top, right,
bottom);
}
}
}
這里能看到換膚的實現方式就是根據原始資源Id來獲取皮膚包的資源Id,從而加載資源。因此我們要保證app和皮膚包的資源名稱一致
public Drawable getDrawable(int resId) {//如果有皮膚 isDefaultSkin false 沒有就是true
if (isDefaultSkin) {
return mAppResources.getDrawable(resId);
}
int skinId = getIdentifier(resId);//查找對應的資源id
if (skinId == 0) {
return mAppResources.getDrawable(resId);
}
return mSkinResources.getDrawable(skinId);
}
//獲取皮膚包中對應資源的id
public int getIdentifier(int resId) {
if (isDefaultSkin) {
return resId;
}
//在皮膚包中的資源id不一定就是 當前程序的 id
//獲取對應id 在當前的名稱 例如colorPrimary
String resName = mAppResources.getResourceEntryName(resId);//ic_launcher /colorPrimaryDark
String resType = mAppResources.getResourceTypeName(resId);//drawable
int skinId = mSkinResources.getIdentifier(resName, resType, mSkinPkgName);//使用皮膚包的Resource
return skinId;
}
4.皮膚包的生成
其實很簡單,就是我們重新建立一個項目(這個項目里面的資源名字和需要換膚的項目的資源名字是對應的就可以),記住我們是通過名字去獲取資源,不是id
新建工程project
將換膚的資源文件添加到res文件下,無java文件
直接運行build.gradle,生成apk文件(注意,運行時Run/Redebug configurations 中Launch Options選擇launch nothing),否則build 會報 no default Activty的錯誤。
將apk文件重命名,如black.apk重命名為black.skin防止用戶點擊安裝
總結
以上是生活随笔為你收集整理的android 状态栏 背景色_技术一面:说说Android动态换肤实现原理的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: jsp里面的input的值吗_一个jsp
- 下一篇: mysql年份_【数据库_Mysql】查