換湯不換藥,用了仍是好! 新手也能看得懂的好文章!java
簡單說:兩個APK,一個安裝包(main.apk),一個皮膚包(skin.apk);當咱們的main.apk須要換膚的時候就經過資源的名字去skin.apk中取相同名字的資源而後進行替換操做。android
資源文件的獲取
通常狀況下,咱們都是直接調用獲取資源文件的代碼來獲取資源:程序員
context.getResources().getColor(R.color.colorPrimary);
複製代碼
那麼究竟是什麼在幫咱們來進行資源的獲取操做的?
老規矩,扒一下源碼小姐姐:web
動手擼碼前,忽然想到一個問題——雖然是寫demo,可是個人換膚操做難道要在每一個Activity中都實現一邊嗎???固然不行!不偷懶的必定是個假程序員! 果斷抽象一個BaseActivity出來。架構
public class BaseActivity extends AppCompatActivity {
private LayoutFactory layoutFactory;
private FrameLayout frameLayout;
private FrameLayout.LayoutParams layoutParams = new FrameLayout.LayoutParams(
FrameLayout.LayoutParams.MATCH_PARENT
, FrameLayout.LayoutParams.MATCH_PARENT);
private Unbinder unbinder;
private Toast toast;
private View childActivityView;
@Override
protected void onCreate(Bundle savedInstanceState) {
layoutFactory = new LayoutFactory();
LayoutInflaterCompat.setFactory2(getLayoutInflater(), layoutFactory);
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_base);
//狀態欄透明
setTransParentStatusBar();
//初始化Activity界面的容器
frameLayout = findViewById(R.id.baseViewContainer);
}
@Override
protected void onResume() {
super.onResume();
}
@Override
protected void onDestroy() {
super.onDestroy();
//ButterKnife移除回調
if (unbinder != null) unbinder.unbind();
//從容器中移除Activity界面
removeChildView();
}
/** * 返回Activity的View * * @return emptyView : childActivityView */
public View getChildActivityView() {
if (childActivityView == null) {
Log.e("BaseActivity", "getChildActivityView() is error:Have not create an instance of Activity View");
return new View(this);
}
return childActivityView;
}
/** * 添加Activity佈局到界面中 * * @param layoutResId 子Activity佈局文件資源ID * @return 子Activity佈局生成的View */
protected void addContentView(@LayoutRes int layoutResId) {
removeChildView();
childActivityView = getLayoutInflater().inflate(layoutResId, frameLayout, false);
frameLayout.addView(childActivityView, layoutParams);
//綁定ButterKnife
unbinder = ButterKnife.bind(this);
}
/** * 移除ChildView */
private void removeChildView() {
int childCount = frameLayout.getChildCount();
if (childCount > 0) {
frameLayout.removeAllViews();
}
childActivityView = null;
}
/** * 通用toast * * @param msg 信息 */
protected void toast(String msg) {
if (toast != null) toast.cancel();
toast = Toast.makeText(this, msg, Toast.LENGTH_SHORT);
toast.show();
}
/** * 狀態欄按鈕點擊事件 * 單獨來用,這個方法沒有@OnClick註解,ButterKnife是不會生成相關點擊事件代碼的 * 可是咱們的子Activity中ButterKnife綁定的點擊事件回調方法中能夠利用super.onViewClicked(view.getId())將ID傳遞過* 來,這樣就能夠一塊兒處理一些公用的控件點擊事件(這裏處理狀態欄中的返回、用戶按鈕) * @param viewId viewId */
protected void onViewClicked(int viewId) {
switch (viewId) {
case R.id.ivBack:
finish();
toast("點擊了返回按鈕");
break;
case R.id.ivUser:
toast("點擊了用戶頭像");
}
}
/** * 設置透明狀態欄(PS:別忘了配合android:fitsSystemWindows="true") */
private void setTransParentStatusBar() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
Window window = getWindow();
window.clearFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS
| WindowManager.LayoutParams.FLAG_TRANSLUCENT_NAVIGATION);
window.getDecorView().setSystemUiVisibility(View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
| View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
| View.SYSTEM_UI_FLAG_LAYOUT_STABLE);
window.addFlags(WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS);
window.setStatusBarColor(Color.TRANSPARENT);
window.setNavigationBarColor(Color.TRANSPARENT);
} else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
Window window = getWindow();
window.setFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS,
WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS);
}
}
}
複製代碼
極爲簡單的佈局文件,只有一個自定義的ActionBarapp
<androidx.constraintlayout.widget.ConstraintLayout 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:layout_height="match_parent" tools:context=".BaseActivity">
<FrameLayout android:id="@+id/actionbarLayout" android:layout_width="match_parent" android:layout_height="?attr/actionBarSize" android:background="@color/skin_actionBarBg" android:fitsSystemWindows="true" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent">
<ImageView android:id="@+id/ivBack" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_gravity="center_vertical|left" android:src="@drawable/skin_back" />
<TextView android:id="@+id/tvTitle" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_gravity="center" android:text="BaseActivity" android:textColor="@color/skin_actionBarTextColor" />
<ImageView android:id="@+id/ivUser" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_gravity="right|center_vertical" android:src="@drawable/skin_user" />
</FrameLayout>
<!--全部的Activity界面都添加在NestedScrollView中的FrameLayout中-->
<androidx.core.widget.NestedScrollView android:layout_width="match_parent" android:layout_height="0dp" android:fillViewport="true" android:background="#ffffff" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintTop_toBottomOf="@+id/actionbarLayout">
<FrameLayout android:id="@+id/baseViewContainer" android:layout_width="match_parent" android:layout_height="wrap_content" />
</androidx.core.widget.NestedScrollView>
</androidx.constraintlayout.widget.ConstraintLayout>
複製代碼
LayoutInflater.from(this).inflate(R.layout.activity_main,parent,false);
複製代碼
這行代碼你們不陌生吧,就是這行代碼將咱們的xml轉成了咱們所須要的View!因此,爲了證實我是對的,扒一下源碼!一層一層往下看!
首先 LayoutInflater.from(this) :框架
上一步咱們已經實現了View生成的監聽,這裏咱們實現View的篩選
在咱們建立的 LayoutFactory2 中進行篩選:ide
public View onCreateView(@Nullable View view, @NonNull String s, @NonNull Context context, @NonNull AttributeSet attributeSet) {
// 注意這裏必定要本身根據傳遞過來的**attributeSet**參數實現View建立,而不能直接使用**view**進行篩選判斷
// 緣由就是這個 view 並非咱們想要的 View 而是他的ParentView,因此這個View和attributeSet是不匹配的
}
複製代碼
完整的建立源碼:佈局
public class LayoutFactory implements LayoutInflater.Factory2 {
private List<SkinView> skinViewList = new ArrayList<>();
private final String[] prefixs = {"android.widget.", "android.view.", "android.webkit."};
@Nullable
@Override
public View onCreateView(@Nullable View view, @NonNull String s, @NonNull Context context, @NonNull AttributeSet attributeSet) {
View viewInstance = null;
//s就是xml中
// <TextView
// ****
// ****
// />
//的Textview字段
//因爲咱們建立View是利用的反射,因此建立的時候須要 包名.TextView這樣的格式進行實例化
if (s.contains(".")) {//包含 . 說明是自定義View,直接能夠用這個
viewInstance = onCreateView(s, context, attributeSet);
} else {
//不是自定義View的則遍歷前綴集合進行實例化,若是實例化爲空則說明不是該前綴下的控件
//包含View的包也就這三個吧 "android.widget.", "android.view.", "android.webkit."
for (String prefix : prefixs) {
viewInstance = onCreateView(prefix + s, context, attributeSet);
if (viewInstance != null) {
addSkinView(viewInstance, attributeSet);
break;
}
}
}
return viewInstance;
}
@Nullable
@Override
public View onCreateView(@NonNull String s, @NonNull Context context, @NonNull AttributeSet attributeSet) {
View view = null;
try {
Class aClass = context.getClassLoader().loadClass(s);
Constructor<? extends View> constructor = aClass.getConstructor(Context.class, AttributeSet.class);
view = constructor.newInstance(context, attributeSet);
} catch (Exception e) {
e.printStackTrace();
}
return view;
}
/** * 條件篩選後添加須要換膚的View * * @param view Activity中的view */
void addSkinView(@Nullable View view, @NonNull AttributeSet attributeSet) {
if (view == null) {
return;
}
List<SkinAttr> skinAttrs = new ArrayList<>();
String idName = "";
//遍歷View的屬性而且判斷該View是否須要應用換膚功能
for (int i = 0; i < attributeSet.getAttributeCount(); i++) {
//資源ID的具體數值,引用資源文件獲得的資源ID格式是@123456
String valueString = attributeSet.getAttributeValue(i);
//若是不是直接引用了資源文件的屬性則忽略
if (!valueString.startsWith("@")) {
continue;
}
//資源值
int value = Integer.parseInt(valueString.substring(1));
//資源ID的名字
String valueName = view.getResources().getResourceEntryName(value);
//屬性名
String name = attributeSet.getAttributeName(i);
//資源ID的類型
String type = view.getResources().getResourceTypeName(value);
//找到了view的Id,取Id的name
if (type.equals("id")) {
idName = valueName;
}
//以 skin_ 爲資源名開頭的則說明須要換膚
if (valueName.indexOf("skin_") == 0) {
skinAttrs.add(new SkinAttr(idName, name, type, valueName, value));
}
}
if (skinAttrs.size() > 0) {
SkinView skinView = new SkinView(view, skinAttrs);
skinViewList.add(skinView);
}
}
private String getSimpleName() {
return LayoutFactory.class.getSimpleName();
}
/** * 換膚操做 */
public void changeNewSkin(Context context,String skinResourcePath) {
SkinResourceManager.getInstance().setContext(context);
SkinResourceManager.getInstance().loadSkin(skinResourcePath);
if (skinViewList.size() == 0) {
return;
}
for (SkinView skinView : skinViewList) {
skinView.changeNewSkin();
}
}
/** * 須要換膚的View的封裝 */
class SkinView {
//須要換膚的View
private View view;
//這個View中須要替換成皮膚包中資源的屬性集合
List<SkinAttr> skinAttrList;
public SkinView(View view, List<SkinAttr> skinAttrList) {
this.view = view;
this.skinAttrList = skinAttrList;
}
//該View進行換膚操做
public void changeNewSkin() {
for (SkinAttr skinAttr : skinAttrList) {
if (skinAttr.name.equals("background")) {//設置背景
if (skinAttr.type.equals("color")) {
view.setBackgroundColor(SkinResourceManager.getInstance().getColor(skinAttr.value));
}
if (skinAttr.type.equals("drawable")) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
view.setBackground(SkinResourceManager.getInstance().getDrawable(skinAttr.value));
} else {
view.setBackgroundDrawable(SkinResourceManager.getInstance().getDrawable(skinAttr.value));
}
}
} else if (skinAttr.name.equals("textColor") && view instanceof TextView) { //設置字體顏色
((TextView) view).setTextColor(SkinResourceManager.getInstance().getColor(skinAttr.value));
} else if (skinAttr.name.equals("text") && view instanceof TextView) {//設置文字
((TextView) view).setText(SkinResourceManager.getInstance().getString(skinAttr.value));
} else if (skinAttr.name.equals("src") && view instanceof ImageView) {//設置圖片資源
((ImageView) view).setImageDrawable(SkinResourceManager.getInstance().getDrawable(skinAttr.value));
}
}
}
}
/** * 單條控件屬性元素封裝 */
class SkinAttr {
//View Id的名字
private String idName;
//屬性名,eg:background,textColor..
private String name;
//屬性類型,eg:@color,@drawable,@String
private String type;
//資源Id的name
private String valueName;
//資源ID
private int value;
public SkinAttr(String idName, String name, String type, String valueName, int value) {
this.idName = idName;
this.name = name;
this.type = type;
this.valueName = valueName;
this.value = value;
}
public String getIdName() {
return idName;
}
public String getName() {
return name;
}
public String getType() {
return type;
}
public String getValueName() {
return valueName;
}
public int getValue() {
return value;
}
}
}
複製代碼
從皮膚包中獲取資源的類:字體
public class SkinResourceManager {
private static final SkinResourceManager skinResourceManager = new SkinResourceManager();
/** * 皮膚包的包名 */
private String mPackageName;
public static SkinResourceManager getInstance() {
return skinResourceManager;
}
private Context mContext;
public Resources mSkinResources;
private String apkPath;
private SkinResourceManager() {
}
public void setContext(Context context) {
mContext = context.getApplicationContext();
}
public void loadSkin(String skinResourcePtah) {
if (TextUtils.isEmpty(skinResourcePtah)){
mPackageName=mContext.getPackageName();
}else {
try {
AssetManager manager = AssetManager.class.newInstance();
Method method = AssetManager.class.getDeclaredMethod("addAssetPath", String.class);
method.invoke(manager, skinResourcePtah);
//當前應用的resources對象,獲取到屏幕相關的參數和配置
Resources res = mContext.getResources();
//getResources()方法經過 AssetManager的addAssetPath方法,構造出Resource對象,因爲是Library層的代碼,因此須要用到反射
mSkinResources = new Resources(manager, res.getDisplayMetrics(), res.getConfiguration());
mPackageName = mContext.getPackageManager().getPackageArchiveInfo(skinResourcePtah, PackageManager.GET_ACTIVITIES).packageName;
} catch (Exception e) {
e.printStackTrace();
}
}
}
//經過ID獲取drawable對象
public Drawable getDrawable(int id) {
Drawable drawable = mContext.getResources().getDrawable(id);
if (mSkinResources != null) {
String name = mContext.getResources().getResourceEntryName(id);
Log.i(SkinResourceManager.class.getSimpleName(), "getDrawable()--name=" + name + "--packageName=" + mPackageName);
int resId = mSkinResources.getIdentifier(name, "drawable", mPackageName);
if (resId > 0) {
return mSkinResources.getDrawable(resId);
}
}
return drawable;
}
//經過ID獲取顏色值
public int getColor(int id) {
int color = mContext.getResources().getColor(id);
if (mSkinResources != null) {
String name = mContext.getResources().getResourceEntryName(id);
Log.i(SkinResourceManager.class.getSimpleName(), "getColor()--name=" + name + "--packageName=" + mPackageName);
int resId = mSkinResources.getIdentifier(name, "color", mPackageName);
if (resId > 0) {
return mSkinResources.getColor(resId);
}
}
return color;
}
public String getString(int id) {
String str = mContext.getResources().getString(id);
if (mSkinResources != null) {
String name = mContext.getResources().getResourceEntryName(id);
Log.i(SkinResourceManager.class.getSimpleName(), "getDrawable()--name=" + name + "--packageName=" + mPackageName);
int resId = mSkinResources.getIdentifier(name, "string", mPackageName);
if (resId > 0) {
Log.i(SkinResourceManager.class.getSimpleName(), "getDrawable()--name=" + name + "--packageName=" + mPackageName+"--get="+mSkinResources.getString(resId));
return mSkinResources.getString(resId);
}
}
return str;
}
}
複製代碼
新建一個 skin_test module,該module是 application 類型,能夠 build 成 apk
將生成的皮膚apk更名並放到對應手機的目錄中:
新建一個Main2Activity用於換膚操做
當點擊換膚按鈕時,將會切換至藍色皮膚樣式,點擊換膚默認按鈕時恢復默認紅色皮膚
界面樣式以下:
BaseActivity中新建換膚方法
Main2Activity中應用換膚操做
這裏用 SP 來持久保存當前應用的皮膚資源路徑
其餘Activity中同時也應用換膚 在 onResume() 中判斷一下是否須要換膚便可
至此整套換膚流程就結束了