导语
谷歌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包等实现方式,还可以考虑插件实现方式,目的都是更好的解耦,更方便的迭代项目.