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,看看对上述疑问有没有进一步的优化。
这里写图片描述

0 0