MultipleTheme换肤功能详解
来源:互联网 发布:js继承构造函数 编辑:程序博客网 时间:2024/05/20 15:10
前段时间刚好看到一篇换肤开源框架,MultipleTheme,这边来研究研究到底怎么实现的:
Android每个页面都有自己的主题风格,而主题样式可以在Style.xml里面自定义。自然就可以在这里面做文章,并且便于管理。
首先在attrs.xml里面定义属性:
<?xml version="1.0" encoding="utf-8"?><resources> <attr name="main_bg" format="reference|color"/> <attr name="main_textcolor" format="reference|color"/> <attr name="second_bg" format="reference|color"/> <attr name="second_textcolor" format="reference|color"/></resources>
然后在style.xml里面设置相应的属性值:
- 这里分为theme1, theme2。分别对应不同的主题风格;
<?xml version="1.0" encoding="utf-8"?><resources> <style name="theme_1" > <item name="main_bg">@color/bg_main_normal</item> <item name="main_textcolor">@color/textcolor_main_normal</item> <item name="second_bg">@color/bg_second_normal</item> <item name="second_textcolor">@color/textcolor_second_normal</item> </style> <style name="theme_2"> <item name="main_bg">@color/bg_main_dark</item> <item name="main_textcolor">@color/textcolor_main_dark</item> <item name="second_bg">@color/bg_second_dark</item> <item name="second_textcolor">@color/textcolor_second_dark</item> </style></resources>
相应的颜色值color.xml中去定义:
<?xml version="1.0" encoding="utf-8"?><resources> <color name="bg_main_normal">#ffffff</color> <color name="textcolor_main_normal">#ff0000</color> <color name="bg_main_dark">#000000</color> <color name="textcolor_main_dark">#ffffff</color> <color name="bg_second_normal">#0000ff</color> <color name="textcolor_second_normal">#00ff00</color> <color name="bg_second_dark">#ffffff</color> <color name="textcolor_second_dark">#000000</color></resources>
重点是怎么去用主题,具体讲解如下:
所有的Activity都继承BaseActivity,在oncreate()创建Activity实例时,会设置该Activity的Theme(主题),然后布局文件各元素会自定获取Style.xml定义好的属性进行展示;
package derson.com.multipletheme;import android.app.Activity;import android.os.Bundle;import android.os.PersistableBundle;import derson.com.multipletheme.colorUi.util.SharedPreferencesMgr;/** * Created by chengli on 15/6/14. */public class BaseActivity extends Activity{ @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); if(SharedPreferencesMgr.getInt("theme", 0) == 1) { setTheme(R.style.theme_2); } else { setTheme(R.style.theme_1); } }}
相应的布局文件
<derson.com.multipletheme.colorUi.widget.ColorRelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" xmlns:app="http://schemas.android.com/apk/res-auto" android:layout_width="match_parent" android:layout_height="match_parent" android:background="?attr/main_bg" android:paddingBottom="@dimen/activity_vertical_margin" android:paddingLeft="@dimen/activity_horizontal_margin" android:paddingRight="@dimen/activity_horizontal_margin" android:paddingTop="@dimen/activity_vertical_margin"> <derson.com.multipletheme.colorUi.widget.ColorTextView android:textColor="?attr/main_textcolor" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="@string/hello_world" /> <derson.com.multipletheme.colorUi.widget.ColorButton android:id="@+id/btn" android:text="换肤" android:layout_centerInParent="true" android:textColor="?attr/main_textcolor" android:layout_width="100dip" android:layout_height="80dip" /> <derson.com.multipletheme.colorUi.widget.ColorButton android:id="@+id/btn_2" android:layout_centerHorizontal="true" android:text="下一页" android:layout_below="@id/btn" android:layout_marginTop="30dip" android:textColor="?attr/main_textcolor" android:layout_width="100dip" android:layout_height="80dip" /></derson.com.multipletheme.colorUi.widget.ColorRelativeLayout>
我们来看看MainActivity:
package derson.com.multipletheme;import android.animation.Animator;import android.app.Activity;import android.content.Intent;import android.graphics.Bitmap;import android.graphics.drawable.BitmapDrawable;import android.os.Build;import android.os.Bundle;import android.view.Menu;import android.view.MenuItem;import android.view.View;import android.view.ViewGroup;import derson.com.multipletheme.colorUi.util.ColorUiUtil;import derson.com.multipletheme.colorUi.util.SharedPreferencesMgr;import derson.com.multipletheme.colorUi.widget.ColorButton;public class MainActivity extends BaseActivity { ColorButton btn,btn_next; @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); btn = (ColorButton)findViewById(R.id.btn); btn.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { if(SharedPreferencesMgr.getInt("theme", 0) == 1) { SharedPreferencesMgr.setInt("theme", 0); setTheme(R.style.theme_1); } else { SharedPreferencesMgr.setInt("theme", 1); setTheme(R.style.theme_2); } final View rootView = getWindow().getDecorView(); if(Build.VERSION.SDK_INT >= 14) { rootView.setDrawingCacheEnabled(true); rootView.buildDrawingCache(true); final Bitmap localBitmap = Bitmap.createBitmap(rootView.getDrawingCache()); rootView.setDrawingCacheEnabled(false); if (null != localBitmap && rootView instanceof ViewGroup) { final View localView2 = new View(getApplicationContext()); localView2.setBackgroundDrawable(new BitmapDrawable(getResources(), localBitmap)); ViewGroup.LayoutParams params = new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT); ((ViewGroup) rootView).addView(localView2, params); localView2.animate().alpha(0).setDuration(400).setListener(new Animator.AnimatorListener() { @Override public void onAnimationStart(Animator animation) { ColorUiUtil.changeTheme(rootView, getTheme()); } @Override public void onAnimationEnd(Animator animation) { ((ViewGroup) rootView).removeView(localView2); localBitmap.recycle(); } @Override public void onAnimationCancel(Animator animation) { } @Override public void onAnimationRepeat(Animator animation) { } }).start(); } } else { ColorUiUtil.changeTheme(rootView, getTheme()); } } }); btn_next = (ColorButton)findViewById(R.id.btn_2); btn_next.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { startActivity(new Intent(MainActivity.this, SecondActivity.class)); } }); } @Override public boolean onCreateOptionsMenu(Menu menu) { // Inflate the menu; this adds items to the action bar if it is present. getMenuInflater().inflate(R.menu.menu_main, menu); return true; } @Override public boolean onOptionsItemSelected(MenuItem item) { // Handle action bar item clicks here. The action bar will // automatically handle clicks on the Home/Up button, so long // as you specify a parent activity in AndroidManifest.xml. int id = item.getItemId(); //noinspection SimplifiableIfStatement if (id == R.id.action_settings) { return true; } return super.onOptionsItemSelected(item); }}
在MainActivity中,我们设置一个切换主题点击事件。这里我们会把主题标志保存到本地,这里用到的是SharedPreferencesMgr:
package derson.com.multipletheme.colorUi.util;import android.content.Context;import android.content.SharedPreferences;/** * SharedPreferences管理类 */public class SharedPreferencesMgr { private static Context context; private static SharedPreferences sPrefs; private SharedPreferencesMgr(Context context, String fileName) { this.context = context; sPrefs = context.getSharedPreferences( fileName, Context.MODE_WORLD_READABLE); } public static void init(Context context, String fileName) { new SharedPreferencesMgr(context, fileName); } public static String fileName; public static int getInt(String key, int defaultValue) { return sPrefs.getInt(key, defaultValue); } public static void setInt(String key, int value) { sPrefs.edit().putInt(key, value).commit(); } public static boolean getBoolean(String key, boolean defaultValue) { return sPrefs.getBoolean(key, defaultValue); } public static void setBoolean(String key, boolean value) { sPrefs.edit().putBoolean(key, value).commit(); } public static String getString(String key, String defaultValue) { if (sPrefs == null) return defaultValue; return sPrefs.getString(key, defaultValue); } public static void setString(String key, String value) { if (sPrefs == null) return; sPrefs.edit().putString(key, value).commit(); } public static void clearAll() { if (sPrefs == null) return; sPrefs.edit().clear().commit(); }}
在切换主题点击事件中逻辑看起来有点复杂,我们来一起分析分析吧;
- 根据主题标志设置相应主题
if(SharedPreferencesMgr.getInt("theme", 0) == 1) { SharedPreferencesMgr.setInt("theme", 0); setTheme(R.style.theme_1); } else { SharedPreferencesMgr.setInt("theme", 1); setTheme(R.style.theme_2); }
- 根据版本不同设置动画,这里偷懒没有导入niceoldandroids ,所以做了版本区别。会得到DecorView的视图,这里会复制当前RootView(根视图)覆盖到当前视图上面,然后调用动画改变其透明度。这里注册动画监听函数(AnimatorListener),在onAnimatorStart[动画开始执行]会根据改变的主题样式去同步改变各控件样式,在onAnimatorEnd[动画执行完毕]会删掉复制的RootView;
final View rootView = getWindow().getDecorView(); if(Build.VERSION.SDK_INT >= 14) { rootView.setDrawingCacheEnabled(true); rootView.buildDrawingCache(true); final Bitmap localBitmap = Bitmap.createBitmap(rootView.getDrawingCache()); rootView.setDrawingCacheEnabled(false); if (null != localBitmap && rootView instanceof ViewGroup) { final View localView2 = new View(getApplicationContext()); localView2.setBackgroundDrawable(new BitmapDrawable(getResources(), localBitmap)); ViewGroup.LayoutParams params = new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT); ((ViewGroup) rootView).addView(localView2, params); localView2.animate().alpha(0).setDuration(400).setListener(new Animator.AnimatorListener() { @Override public void onAnimationStart(Animator animation) { ColorUiUtil.changeTheme(rootView, getTheme()); } @Override public void onAnimationEnd(Animator animation) { ((ViewGroup) rootView).removeView(localView2); localBitmap.recycle(); } @Override public void onAnimationCancel(Animator animation) { } @Override public void onAnimationRepeat(Animator animation) { } }).start(); } } else { ColorUiUtil.changeTheme(rootView, getTheme()); }
这里同步改变各控件主题样式的触发操作主要由ColorUiUtil完成:
package derson.com.multipletheme.colorUi.util;import android.app.Activity;import android.content.res.Resources;import android.provider.Settings;import android.view.View;import android.view.ViewGroup;import android.view.Window;import android.view.WindowManager;import android.widget.AbsListView;import java.lang.reflect.Field;import java.lang.reflect.InvocationTargetException;import java.lang.reflect.Method;import derson.com.multipletheme.colorUi.ColorUiInterface;/** * Created by chengli on 15/6/10. */public class ColorUiUtil { /** * 切换应用主题 * * @param rootView */ public static void changeTheme(View rootView, Resources.Theme theme) { if (rootView instanceof ColorUiInterface) { ((ColorUiInterface) rootView).setTheme(theme); if (rootView instanceof ViewGroup) { int count = ((ViewGroup) rootView).getChildCount(); for (int i = 0; i < count; i++) { changeTheme(((ViewGroup) rootView).getChildAt(i), theme); } } if (rootView instanceof AbsListView) { try { Field localField = AbsListView.class.getDeclaredField("mRecycler"); localField.setAccessible(true); Method localMethod = Class.forName("android.widget.AbsListView$RecycleBin").getDeclaredMethod("clear", new Class[0]); localMethod.setAccessible(true); localMethod.invoke(localField.get(rootView), new Object[0]); } catch (NoSuchFieldException e1) { e1.printStackTrace(); } catch (ClassNotFoundException e2) { e2.printStackTrace(); } catch (NoSuchMethodException e3) { e3.printStackTrace(); } catch (IllegalAccessException e4) { e4.printStackTrace(); } catch (InvocationTargetException e5) { e5.printStackTrace(); } } } else { if (rootView instanceof ViewGroup) { int count = ((ViewGroup) rootView).getChildCount(); for (int i = 0; i < count; i++) { changeTheme(((ViewGroup) rootView).getChildAt(i), theme); } } if (rootView instanceof AbsListView) { try { Field localField = AbsListView.class.getDeclaredField("mRecycler"); localField.setAccessible(true); Method localMethod = Class.forName("android.widget.AbsListView$RecycleBin").getDeclaredMethod("clear", new Class[0]); localMethod.setAccessible(true); localMethod.invoke(localField.get(rootView), new Object[0]); } catch (NoSuchFieldException e1) { e1.printStackTrace(); } catch (ClassNotFoundException e2) { e2.printStackTrace(); } catch (NoSuchMethodException e3) { e3.printStackTrace(); } catch (IllegalAccessException e4) { e4.printStackTrace(); } catch (InvocationTargetException e5) { e5.printStackTrace(); } } } }}
这里逻辑还是比较清晰的,会去遍历所有子视图,如果实现了ColorUiInterface接口,就会通过多态形式回调给实现该接口的实体,这里一点需要注意就是如果该控件是AbsListView的实体,这里会清空ReclycleBin里面的视图缓存(为什么这样做,目前我不是很清晰);
- 例如:
package derson.com.multipletheme.colorUi.widget;import android.content.Context;import android.content.res.Resources;import android.util.AttributeSet;import android.view.View;import derson.com.multipletheme.colorUi.ColorUiInterface;import derson.com.multipletheme.colorUi.util.ViewAttributeUtil;/** * Created by chengli on 15/6/8. */public class ColorView extends View implements ColorUiInterface { private int attr_background = -1; public ColorView(Context context) { super(context); } public ColorView(Context context, AttributeSet attrs) { super(context, attrs); this.attr_background = ViewAttributeUtil.getBackgroundAttibute(attrs); } public ColorView(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); this.attr_background = ViewAttributeUtil.getBackgroundAttibute(attrs); } @Override public View getView() { return this; } @Override public void setTheme(Resources.Theme themeId) { if(attr_background != -1) { ViewAttributeUtil.applyBackgroundDrawable(this, themeId, attr_background); } }}
主要是通过setTheme去改变该空间的样式,这里就要看一下ViewAttributeUtil干了些什么:
package derson.com.multipletheme.colorUi.util;import android.content.res.Resources;import android.content.res.TypedArray;import android.graphics.drawable.Drawable;import android.util.AttributeSet;import android.widget.ImageView;import android.widget.TextView;import derson.com.multipletheme.colorUi.ColorUiInterface;/** * Created by chengli on 15/6/8. */public class ViewAttributeUtil { public static int getAttributeValue(AttributeSet attr, int paramInt) { int value = -1; int count = attr.getAttributeCount(); for(int i = 0; i <count;i++) { if(attr.getAttributeNameResource(i) == paramInt) { String str = attr.getAttributeValue(i); if(null != str && str.startsWith("?")) { value = Integer.valueOf(str.substring(1,str.length())).intValue(); return value; } } } return value; } public static int getBackgroundAttibute(AttributeSet attr) { return getAttributeValue(attr , android.R.attr.background); } public static int getCheckMarkAttribute(AttributeSet attr) { return getAttributeValue(attr, android.R.attr.checkMark); } public static int getSrcAttribute(AttributeSet attr) { return getAttributeValue(attr, android.R.attr.src); } public static int getTextApperanceAttribute(AttributeSet attr) { return getAttributeValue(attr, android.R.attr.textAppearance); } public static int getDividerAttribute(AttributeSet attr) { return getAttributeValue(attr, android.R.attr.divider); } public static int getTextColorAttribute(AttributeSet attr) { return getAttributeValue(attr, android.R.attr.textColor); } public static void applyBackgroundDrawable(ColorUiInterface ci, Resources.Theme theme, int paramInt) { TypedArray ta = theme.obtainStyledAttributes(new int[]{paramInt}); Drawable drawable = ta.getDrawable(0); if(null != ci) { (ci.getView()).setBackgroundDrawable(drawable); } ta.recycle(); } public static void applyImageDrawable(ColorUiInterface ci, Resources.Theme theme, int paramInt) { TypedArray ta = theme.obtainStyledAttributes(new int[]{paramInt}); Drawable drawable = ta.getDrawable(0); if(null != ci && ci instanceof ImageView) { ((ImageView)ci.getView()).setImageDrawable(drawable); } ta.recycle(); } public static void applyTextAppearance(ColorUiInterface ci, Resources.Theme theme, int paramInt) { TypedArray ta = theme.obtainStyledAttributes(new int[]{paramInt}); int resourceId = ta.getResourceId(0,0); if(null != ci && ci instanceof TextView) { ((TextView)ci.getView()).setTextAppearance(ci.getView().getContext(), resourceId); } ta.recycle(); } public static void applyTextColor(ColorUiInterface ci, Resources.Theme theme, int paramInt) { TypedArray ta = theme.obtainStyledAttributes(new int[]{paramInt}); int resourceId = ta.getColor(0,0); ((TextView)ci.getView()).setTextColor(resourceId); if(null != ci && ci instanceof TextView) { } ta.recycle(); }}
从代码中我们可以看出通过paramInt去获取当前主题中对应的属性值,然后根据属性值进行进行操作;
- 而paramInt是怎么被赋值的呢,这里我们还是来看一下各个自定义组件的构造函数,已经相应的赋值函数:
public ColorRadioButton(Context context, AttributeSet attrs) { super(context, attrs); this.attr_background = ViewAttributeUtil.getBackgroundAttibute(attrs); this.attr_textAppearance = ViewAttributeUtil.getTextApperanceAttribute(attrs); } public ColorRadioButton(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); this.attr_background = ViewAttributeUtil.getBackgroundAttibute(attrs); this.attr_textAppearance = ViewAttributeUtil.getTextApperanceAttribute(attrs); }
public static int getAttributeValue(AttributeSet attr, int paramInt) { int value = -1; int count = attr.getAttributeCount(); for(int i = 0; i <count;i++) { if(attr.getAttributeNameResource(i) == paramInt) { String str = attr.getAttributeValue(i); if(null != str && str.startsWith("?")) { value = Integer.valueOf(str.substring(1,str.length())).intValue(); return value; } } } return value; }
一目了然,主要是通过AttributeSet去获取paramInt相应的参数;
最重要的一点是需要动态变换主题的组件都要使用自定义组件,个别没有的根据规则可以自己实现:
大体的功能应该就这么多吧。
这个框架对换肤操作的确管用,但是需要自己去设置对应的自定义组件,感觉代码量颇大,可定制型不是很强,而且功能解耦不是很清晰。
抽个时间我们看下ColorFul,看看对上述疑问有没有进一步的优化。
- MultipleTheme换肤功能详解
- 43.使用MultipleTheme换肤框架实现日、夜间模式的切换
- 实现换肤功能
- 换肤功能实现!!!
- android 换肤功能
- 页面换肤功能浅析
- 10.2 实时换肤功能
- Coolite 换肤功能实现
- 页面换肤功能浅析
- android换肤功能实现
- Android 实现换肤功能
- Android实现换肤功能
- Android 换肤功能实现
- qt换肤功能-qss
- 换肤功能的实现
- android app换肤详解
- 使用Ajax实现换肤功能
- 使用Ajax实现换肤功能
- 性能测试基本知识
- 使用Android Studio阅读整个Android源码
- 程序员必须掌握的8大排序算法(OC版)
- 类对象的使用
- centos 配置IP和解除自动待机休眠
- MultipleTheme换肤功能详解
- centos安装zabbix3.0
- 嵌入式系统中RAM和ROM的疑惑澄清
- HYSBZ 2243 染色 树链剖分
- VRRP协议介绍
- spring配置二
- 黑马程序员——C语言基础---指针1
- Android入门简介
- 关于 tomcat 集群中 session 共享的三种方法