導語
谷歌v7後的主題Theme其實就有意給開發者們開辟換膚的功能,我們一起手動制作一款可以換膚主題,開始撸碼吧!
一、統一自定義屬性名
attr.xml
<?xml version="1.0" encoding="utf-8"?>
<resources>
<attr name="mainColor" format="color" />
<attr name="mainPrimaryTextColor" format="color" />
<attr name="mainPrimaryLightTextColor" format="color" />
<attr name="mainBgColor" format="color" />
</resources>
屬性資源 attr 裡可以定義屬性類型:如color、float、integer、boolean、dimension(sp、dp/dip、px、pt…)、reference(指向本地資源)等等。
二、在Value檔案中自定義若幹套主題
style.xml
<resources>
<!-- Base application theme. -->
<style name="AppThemeNoAction" parent="Theme.AppCompat.Light.NoActionBar">
<!-- Customize your theme here. -->
<item name="colorPrimary">@color/colorPrimary</item>
<item name="colorPrimaryDark">@color/colorPrimaryDark</item>
<item name="colorAccent">@color/colorAccent</item>
<item name="mainBgColor">#BBDEFB</item>
<item name="mainColor">#2196F3</item>
</style>
<!--換膚-->
<style name="AppThemeNoAction.Green">
<item name="colorPrimary">#4CAF50</item>
<item name="colorPrimaryDark">#388E3C</item>
<item name="colorAccent">#F3F5F3</item>
<item name="mainBgColor">#C8E6C9</item>
<item name="mainColor">#4CAF50</item>
</style>
<style name="AppThemeNoAction.Blue">
<item name="colorPrimary">#2196F3</item>
<item name="colorPrimaryDark">#1976D2</item>
<item name="colorAccent">#607D8B</item>
<item name="mainBgColor">#BBDEFB</item>
<item name="mainColor">#2196F3</item>
</style>
<style name="AppThemeNoAction.Yellow">
<item name="colorPrimary">#FFC107</item>
<item name="colorPrimaryDark">#FF9800</item>
<item name="colorAccent">#927215</item>
<item name="mainBgColor">#D1C4E9</item>
<item name="mainColor">#FFC107</item>
</style>
<style name="AppThemeNoAction.Grey">
<item name="colorPrimary">#607D8B</item>
<item name="colorPrimaryDark">#455A64</item>
<item name="colorAccent">#58575A</item>
<item name="mainBgColor">#CFD8DC</item>
<item name="mainColor">#607D8B</item>
</style>
</resources>
三、指定自定義顔色
需要跟随主題切換顔色的控件,可以直接設定背景、字型顔色,字元串或圖檔屬性指派。
(1)xml中指派:
<?xml version="1.0" encoding="utf-8"?>
<Linerlayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?attr/mainColor"
android:minHeight="?attr/actionBarSize" />
(2)代碼中指派:
TypedArray a = obtainStyledAttributes(new int[]{R.attr.mainBgColor,
R.attr.mainColor});
int color = a.getColor(0, Color.BLACK)
//或
TypedValue typedValue = new TypedValue();
newTheme.resolveAttribute(mAttrResId, typedValue, true)
這裡隻列出了顔色;還可以使用引用主題字元串以及圖檔。
四、工具類
public class ThemeUtil {
public static class ThemeColors {
public static final int THEME_GREEN = 1;
public static final int ThEME_BLUE = 2;
public static final int THEME_GREY = 3;
public static final int THEME_YELLOW = 4;
}
public static void setBaseTheme(Context context) {
SharedPreferences sharedPreferences = context.getSharedPreferences(
"MyThemeShared", context.MODE_PRIVATE);
int themeType = sharedPreferences.getInt("theme_type", 0);
int themeId;
switch (themeType) {
case ThemeColors.THEME_GREEN:
themeId = R.style.AppThemeNoAction_Green;
break;
case ThemeColors.ThEME_BLUE:
themeId = R.style.AppThemeNoAction_Blue;
break;
case ThemeColors.THEME_GREY:
themeId = R.style.AppThemeNoAction_Grey;
break;
case ThemeColors.THEME_YELLOW:
themeId = R.style.AppThemeNoAction_Yellow;
break;
default:
themeId = R.style.AppThemeNoAction;
}
context.setTheme(themeId);
}
public static boolean setNewTheme(Context context, int theme) {
SharedPreferences sharedPreferences = context.getSharedPreferences(
"MyThemeShared", context.MODE_PRIVATE);
SharedPreferences.Editor e = sharedPreferences.edit();
e.putInt("theme_type",theme);
// e.apply();
return e.commit();//有傳回值
}
}
要在Activity的onCreate方法裡的setContextView前使用了。這裡最好寫在BaseActivity中,更具share儲存的樣式值,來動态設定theme。
五、開始切換主題
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
//設定主題
ThemeUtil.setBaseTheme(this);
setContentView(R.layout.activity_main);
}
private void setChangeTheme(){
if (ThemeUtil.setNewTheme(mContext, ThemeUtil.ThemeColors.ThEME_BLUE)){
//重新開機Activity
recreate();
}
}
當然直接設定後立即生效體驗會好點。但由于系統限制,正常情況都需要重新開機Activity,是以會出現閃屏問題。下面有個解決方案,就是動态擷取控件的atts值,對每一個控件進行操作,但是有點繁瑣。
ListView mNewsListView = (ListView) findViewById(R.id.listview);
// 為ListView設定要修改的屬性,在這裡沒有對ListView本身的屬性做修改
ViewGroupSetter listViewSetter = new ViewGroupSetter(mNewsListView, 0);
// 綁定ListView的Item View中的news_title視圖,在換膚時修改它的text_color屬性
listViewSetter.childViewTextColor(R.id.news_title, R.attr.text_color);
// 建構Colorful對象
Colorful mColorful = new Colorful.Builder(this)
.backgroundDrawable(R.id.root_view, R.attr.root_view_bg) // 設定view的背景圖檔
.backgroundColor(R.id.change_btn, R.attr.btn_bg) // 設定按鈕的背景色
.textColor(R.id.textview, R.attr.text_color) // 設定文本顔色
.setter(listViewSetter) // 手動設定setter
.create();
六、小案例,完整代碼
首先,請将前面代碼複制到項目中。基本上已經完成了百分之80了。就差一個Toolbar和popwindow。
activity_main.xml
<?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:layout_height="match_parent"
android:orientation="vertical"
tools:context="com.example.aiyang.stickydecoration.MainActivity">
<android.support.v7.widget.Toolbar
android:id="@+id/main_toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:background="?attr/mainColor"
android:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar"
app:popupTheme="@style/ThemeOverlay.AppCompat.Light">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textColor="?attr/colorAccent"
android:text="Material design" />
<View
android:id="@+id/menu_pop"
android:layout_width="1dp"
android:layout_height="1dp"
android:layout_gravity="right"
android:layout_marginRight="10dp" />
</android.support.v7.widget.Toolbar>
<Button
android:id="@+id/golistpage"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="50dp"
android:background="?attr/mainBgColor"
android:text="清單上下拉加載和分頁加載"
android:textColor="?attr/colorAccent" />
<Button
android:id="@+id/coordinatorpage"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="50dp"
android:background="?attr/mainBgColor"
android:text="頁面關聯Coordinator"
android:textColor="?attr/colorAccent" />
<Button
android:id="@+id/goshelfpage"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="50dp"
android:background="?attr/mainBgColor"
android:text="最終合成的仿美團點餐頁面"
android:textColor="?attr/colorAccent" />
</LinearLayout>
pop_window.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="vertical"
android:gravity="center"
android:paddingLeft="15dp"
android:background="@mipmap/zidongfendan_bg">
<LinearLayout
android:id="@+id/checkyellow"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center_vertical">
<View
android:layout_width="30dp"
android:layout_height="30dp"
android:layout_weight="0.3"
android:background="@color/yellow"/>
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_weight="0.7"
android:orientation="vertical">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="黃色"
android:layout_margin="10dp"
android:textSize="18sp"
android:textColor="@color/txtcolor"/>
<View
android:layout_width="match_parent"
android:layout_height="1dp"
android:background="@color/backcolor"
android:layout_marginLeft="8dp"
/>
</LinearLayout>
</LinearLayout>
<LinearLayout
android:id="@+id/checkblue"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center_vertical">
<View
android:layout_width="30dp"
android:layout_height="30dp"
android:layout_weight="0.3"
android:background="@color/blue"/>
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_weight="0.7"
android:orientation="vertical">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="藍色"
android:layout_margin="10dp"
android:textSize="18sp"
android:textColor="@color/txtcolor"/>
<View
android:layout_width="match_parent"
android:layout_height="1dp"
android:background="@color/backcolor"
android:layout_marginLeft="8dp"
/>
</LinearLayout>
</LinearLayout>
<LinearLayout
android:id="@+id/checkgrey"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center_vertical">
<View
android:layout_width="30dp"
android:layout_height="30dp"
android:layout_weight="0.3"
android:background="@color/grey"/>
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_weight="0.7"
android:orientation="vertical">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="淺灰"
android:layout_margin="10dp"
android:textSize="18sp"
android:textColor="@color/txtcolor"/>
<View
android:layout_width="match_parent"
android:layout_height="1dp"
android:background="@color/backcolor"
android:layout_marginLeft="8dp"
/>
</LinearLayout>
</LinearLayout>
<LinearLayout
android:id="@+id/checkgreen"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center_vertical">
<View
android:layout_width="30dp"
android:layout_height="30dp"
android:layout_weight="0.3"
android:background="@color/green"/>
<LinearLayout
android:layout_weight="0.7"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="vertical">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="綠色"
android:layout_margin="10dp"
android:textSize="18sp"
android:textColor="@color/txtcolor"/>
</LinearLayout>
</LinearLayout>
</LinearLayout>
colors.xml
<color name="txtcolor">#000000</color>
<color name="yellow" >#FFEB3B</color>
<color name="blue" >#03A9F4</color>
<color name="green" >#009688</color>
<color name="grey">#607D8B</color>
mipmap-mdpi檔案下圖檔
MainActivity
public class MainActivity extends AppCompatActivity {
private MainActivity mContext;
private PopupWindow popupWindow;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
//設定主題
ThemeUtil.setBaseTheme(this);
setContentView(R.layout.activity_main);
mContext = this;
Toolbar toolbar= (Toolbar) findViewById(R.id.main_toolbar);
setSupportActionBar(toolbar);
//設定不現實自帶的title文字
getSupportActionBar().setDisplayShowTitleEnabled(false);
findViewById(R.id.golistpage).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
startActivity(new Intent(MainActivity.this, ListScrollListenerActivity.class));
}
});
findViewById(R.id.goshelfpage).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
startActivity(new Intent(MainActivity.this, GoodsShelfActivity.class));
}
});
findViewById(R.id.coordinatorpage).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
startActivity(new Intent(MainActivity.this, CoordinatorActivity.class
));
}
}
);
}
@Override
public boolean onCreateOptionsMenu(Menu menu) {
getMenuInflater().inflate(R.menu.menu_main,menu);
return true;
}
@Override
public boolean onPrepareOptionsMenu(Menu menu) {
menu.findItem(R.id.settings).setIcon(android.support.v7.appcompat.R.drawable.abc_ic_menu_overflow_material);
return super.onPrepareOptionsMenu(menu);
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
if (item.getItemId() == R.id.settings){
showPopupWindow(findViewById(R.id.menu_pop));
}
return true;
}
/**
* 更多菜單選項彈框
*
* @param view
*/
private void showPopupWindow(View view) {
// 一個自定義的布局,作為顯示的内容
View contentView = LayoutInflater.from(this).inflate(
R.layout.pop_window, null);
contentView.findViewById(R.id.checkblue).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
if (ThemeUtil.setNewTheme(mContext, ThemeUtil.ThemeColors.ThEME_BLUE)){
recreate();
}
popupWindow.dismiss();
}
});
contentView.findViewById(R.id.checkyellow).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
if (ThemeUtil.setNewTheme(mContext, ThemeUtil.ThemeColors.THEME_YELLOW))
recreate();
popupWindow.dismiss();
}
});
contentView.findViewById(R.id.checkgreen).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
if (ThemeUtil.setNewTheme(mContext, ThemeUtil.ThemeColors.THEME_GREEN))
recreate();
popupWindow.dismiss();
}
});
contentView.findViewById(R.id.checkgrey).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
if (ThemeUtil.setNewTheme(mContext, ThemeUtil.ThemeColors.THEME_GREY))
recreate();
popupWindow.dismiss();
}
});
popupWindow = new PopupWindow(contentView,
ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT, true);
popupWindow.setBackgroundDrawable(new ColorDrawable(0000000000));
popupWindow.setFocusable(true);
popupWindow.setOutsideTouchable(true); // 點選外部關閉
popupWindow.setAnimationStyle(android.R.style.Animation_Dialog);
// 設定好參數之後再show
popupWindow.showAsDropDown(view, 0, 30);
}
}
其他方式的主題實作
網易雲音樂開源項目實作方法大概是添加Gradle依賴後,再建立一個擁有不同圖檔和主題風格的項目子子產品,然後切換所謂的主題切換,就能夠根據不同的方法設定,呈現不同風格的界面給使用者,也就是所謂的換膚。
APK主題方案和主題包儲存到SD卡上(墨迹,搜狗實作方式)的方案類似,隻不過是apk壓縮格式,一些資源的引用可以調用系統api。
APK主題方案的基本思路是:在Android中,所有的資源都是基于包的。資源以id進行辨別,在同一個應用中,每個資源都有唯一辨別。但在不同的應用中,可以有相同的id。是以,隻要擷取到了其他應用的Context對象,就可以通過它的getRsources擷取到其綁定的資源對象。然後,就可以使用Resources的getXXX方法擷取字元串、顔色、dimension、圖檔等。 要想擷取其他應用的Context對象,Android已經為我們提供好了接口。那就是:android.content.ContextWrapper.createPackageContext(String packageName, int flags)方法。
try {
String remotePackage = "com.your.themepackagename";
Context remoteContext = createPackageContext(remotePackage,
CONTEXT_IGNORE_SECURITY);
Resources remoteResources = remoteContext.getResources();
text.setText(remoteResources.getText(remoteResources.getIdentifier("application_name", "string", remotePackage)));
color.setTextColor(remoteResources.getColor(remoteResources.getIdentifier("color_name", "color", remotePackage)));
image.setImageDrawable(remoteResources.getDrawable(remoteResources.getIdentifier("ic_icon", "drawable", remotePackage)));
} catch (NameNotFoundException e) {
e.printStackTrace();
}
除了壓縮包,apk包等實作方式,還可以考慮插件實作方式,目的都是更好的解耦,更友善的疊代項目.