博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
Android 实现切换主题皮肤功能(类似于众多app中的 夜间模式,主题包等)
阅读量:4308 次
发布时间:2019-06-06

本文共 17703 字,大约阅读时间需要 59 分钟。

首先来个最简单的一键切换主题功能,就做个白天和晚上的主题好了。

先看我们的styles文件:

 
1 
2 3
4
10
14
15
16 17
18
22 23
24
28 29 30
 

好,然后我们来看看主要activity

 
package com.example.administrator.mainchangethemeapp;import android.content.SharedPreferences;import android.os.Bundle;import android.support.design.widget.FloatingActionButton;import android.support.design.widget.Snackbar;import android.support.v7.app.AppCompatActivity;import android.support.v7.widget.Toolbar;import android.util.Log;import android.view.View;import android.view.Menu;import android.view.MenuItem;import android.widget.TextView;public class MainActivity extends AppCompatActivity {    private TextView tv;    @Override    protected void onCreate(Bundle savedInstanceState) {        //sputils是對SharedPreferences的封裝,代碼就不上了,大家理解意思就行了        if(SPUtils.get(this,"theme","dayTheme").equals("dayTheme"))        {            //默認是白天主題            setTheme(R.style.dayTheme);        }else        {            //否则是晚上主題            setTheme(R.style.nightTheme);        }        super.onCreate(savedInstanceState);        setContentView(R.layout.activity_main);        tv=(TextView)this.findViewById(R.id.tv);        tv.setOnClickListener(new View.OnClickListener(){            @Override            public void onClick(View v) {                if(SPUtils.get(MainActivity.this,"theme","dayTheme").equals("dayTheme"))                {                    SPUtils.put(MainActivity.this,"theme","nightTheme");                }else                {                    SPUtils.put(MainActivity.this, "theme", "dayTheme");                }                recreate();            }        });    }    @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);    }}
 

然后来看下效果:

 

当然了,上面这个demo实际上是有缺陷的。有人会说了 你看人家新浪微博,那么多主题,你要是用这种方法,你这个app得有多大,

能做成微博那样要用什么就去下载什么么?答案是可以的。其实方案也很简单。

这种方案的思路就是,把主题包 额外做成一个apk,注意这个apk 是不会在桌面显示的。你只能在设置里的app列表里找到他。

然后在你的主activity里 取这个apk里的资源 即可。这里我也把这种方案的代码写一遍。注意下载apk 安装apk的代码我就不写了。

我们就假设 我们要切换的主题就是系统自带的主题就行了,我们的主activity 使用的是白天主题。然后我们的子app里面

为了简化代码,我们就不放自定义主题了,就使用android studio 原始的代码,一步步来,

先放上子app里的styles文件,其实这个里面一行代码都没更改,全是ide自己生成的:

 
1 
2 3
4
10
14
15
16 17 18
 

然后就是子activity里的 配置文件manifest:

 
 

注意这里 

的这行代码 我是没写的。这样就是保证在这个apk 安装好以后不会在桌面出现。 然后我们来看看主activity代码
 
1 package com.example.administrator.mainchangethemeapp; 2  3 import android.content.Context; 4 import android.content.SharedPreferences; 5 import android.content.pm.PackageManager; 6 import android.content.res.TypedArray; 7 import android.os.Bundle; 8 import android.support.design.widget.FloatingActionButton; 9 import android.support.design.widget.Snackbar;10 import android.support.v4.app.Fragment;11 import android.support.v7.app.AppCompatActivity;12 import android.support.v7.widget.Toolbar;13 import android.util.Log;14 import android.util.TypedValue;15 import android.view.View;16 import android.view.Menu;17 import android.view.MenuItem;18 import android.widget.TextView;19 20 public class MainActivity extends AppCompatActivity {21 22     private TextView tv;23 24     @Override25     protected void onCreate(Bundle savedInstanceState) {26         //sputils是對SharedPreferences的封裝,代碼就不上了,大家理解意思就行了27         if (SPUtils.get(this, "theme", "dayTheme").equals("dayTheme")) {28             //默認是白天主題29             setTheme(R.style.dayTheme);30         } else {31             //否则是晚上主題,這裡晚上主題我們就去加載我們晚上主題apk里的資源32             int resourceId = getResourceId(getPackageContext(this, "com.example.administrator.sonproject"), "style", "AppTheme");33             setTheme(resourceId);34         }35         super.onCreate(savedInstanceState);36         setContentView(R.layout.activity_main);37         tv = (TextView) this.findViewById(R.id.tv);38         tv.setOnClickListener(new View.OnClickListener() {39 40             @Override41             public void onClick(View v) {42                 if (SPUtils.get(MainActivity.this, "theme", "dayTheme").equals("dayTheme")) {43                     SPUtils.put(MainActivity.this, "theme", "nightTheme");44                 } else {45                     SPUtils.put(MainActivity.this, "theme", "dayTheme");46                 }47                 recreate();48             }49         });50     }51 52     @Override53     public boolean onCreateOptionsMenu(Menu menu) {54         // Inflate the menu; this adds items to the action bar if it is present.55         getMenuInflater().inflate(R.menu.menu_main, menu);56         return true;57     }58 59     @Override60     public boolean onOptionsItemSelected(MenuItem item) {61         // Handle action bar item clicks here. The action bar will62         // automatically handle clicks on the Home/Up button, so long63         // as you specify a parent activity in AndroidManifest.xml.64         int id = item.getItemId();65 66         //noinspection SimplifiableIfStatement67         if (id == R.id.action_settings) {68             return true;69         }70 71         return super.onOptionsItemSelected(item);72     }73 74 75     /**76      * 获取其他apk的context77      * @param context78      * @param packageName79      * @return80      */81     public static Context getPackageContext(Context context, String packageName) {82         try {83             return context.createPackageContext(packageName, Context.CONTEXT_INCLUDE_CODE | Context.CONTEXT_IGNORE_SECURITY);84         } catch (PackageManager.NameNotFoundException e) {85             e.printStackTrace();86         }87         return null;88     }89 90     //获取指定context的 type里面的 name属性91     public static int getResourceId(Context context, String type, String name) {92         return context.getResources().getIdentifier(name, type, context.getPackageName());93     }94 }
 
 

你看,到这里 我们就实现了,动态加载主题的方式了。当然了,到这里依旧是不完美的,因为settheme方法大家都知道一定要重启activity才有效啊。这样就不好了。

体验并非最佳。总是给人很突兀的感觉。而且我们都知道重启activity的成本很大。要考虑很多生命周期之类的东西。那我们继续往下看,看看有什么比较好的解决方案能解决这个问题。

首先 我们可以考虑一下这个问题,所谓的切换主题 之类的,无非就是把你那些控件的 背景色啊 字体颜色之类的 改变了一下。对于一个app来说,用户一般只有一个界面 会有切换主题的这个入口,

换句话说,我们的app里面 只有这一个activity 需要实现 不重启activity就切换 控件style的功能,其他activity我们是不需要实现这个功能的,因为再切换过去的时候基本上都会走oncreate。所以

我们只需要考虑这个 有切换主题按钮的 这个activity能实现 不重启activity就换皮肤的功能就可以了。其他activity不需要考虑。so 这样一想 这个功能就简单清晰了很多。

首先我们可以自定义2个属性,我们把他放在attrs 这个xml里面

 
1 
2
3
4
5
6
7
 

 

然后我们去定义一下我们的主题:

 
1 
2 3
4
5
9 10
11
15
 

然后我们来写一下mainactivity的xml布局文件,我们假设这个布局是非常简单的:

 
1 
2
8 9
18 19
 

你看这个布局里面 就一个relativelayout和一个textview。 和以前xml唯一的区别就是这里backaground和textcolor使用的值 是我们前面定义好的属性了(但是要注意 这属性是没有值的 赋值的操作放在java代码里实现)。同时这2个控件 也并非是系统控件 而是自定义控件。然后我们看看这个自定义控件是怎么写的。注意我这里就做了2个自定义控件,如果你们的那个切换主题的入口页面里面有其他控件的话,就要学着下面的方法 自己拓展一下了,其实也很简单的。

首先呢,我们来定义一个接口,谁实现了这个接口 就说明这个控件可以不启动activity直接换肤!

 
1 package com.example.administrator.mainchangethemeapp; 2  3 import android.content.res.Resources; 4 import android.view.View; 5  6 /** 7  * Created by Administrator on 2015/11/14. 8  */ 9 public interface ThemeUIInterface {10 11     public View getView();12     public  void setTheme(Resources.Theme themeId);13 }
 

然后我们来看看2个自定义控件怎么写:

 
1 package com.example.administrator.mainchangethemeapp; 2  3 import android.content.Context; 4 import android.content.res.Resources; 5 import android.util.AttributeSet; 6 import android.util.Log; 7 import android.view.View; 8 import android.widget.RelativeLayout; 9 10 /**11  * Created by Administrator on 2015/11/14.12  */13 public class ThemeRelativeLayout extends RelativeLayout implements ThemeUIInterface{14 15     private int attr_background = -1;16 17     public ThemeRelativeLayout(Context context) {18         super(context);19     }20 21     public ThemeRelativeLayout(Context context, AttributeSet attrs) {22         super(context, attrs);23         this.attr_background =ViewAttributeUtil.getBackgroundAttibute(attrs);24     }25 26     public ThemeRelativeLayout(Context context, AttributeSet attrs, int defStyleAttr) {27         super(context, attrs, defStyleAttr);28         this.attr_background =ViewAttributeUtil.getBackgroundAttibute(attrs);29 30     }31 32 33     @Override34     public View getView() {35         return this;36     }37 38     @Override39     public void setTheme(Resources.Theme themeId) {40         if(attr_background!=-1) {41             ViewAttributeUtil.applyBackgroundDrawable(this, themeId, attr_background);42         }43     }44 }
 
 
1 package com.example.administrator.mainchangethemeapp; 2  3 import android.content.Context; 4 import android.content.res.Resources; 5 import android.util.AttributeSet; 6 import android.view.View; 7 import android.widget.TextView; 8  9 /**10  * Created by Administrator on 2015/11/16.11  */12 public class ThemeTextView extends TextView implements ThemeUIInterface{13 14     private int attr_drawable=-1;15     private int attr_textColor=-1;16 17     public ThemeTextView(Context context) {18         super(context);19     }20 21     public ThemeTextView(Context context, AttributeSet attrs) {22         super(context, attrs);23         this.attr_drawable = ViewAttributeUtil.getBackgroundAttibute(attrs);24         this.attr_textColor = ViewAttributeUtil.getTextColorAttribute(attrs);25     }26 27     public ThemeTextView(Context context, AttributeSet attrs, int defStyleAttr) {28         super(context, attrs, defStyleAttr);29         this.attr_drawable = ViewAttributeUtil.getBackgroundAttibute(attrs);30         this.attr_textColor = ViewAttributeUtil.getTextColorAttribute(attrs);31     }32 33     @Override34     public View getView() {35         return this;36     }37 38     @Override39     public void setTheme(Resources.Theme themeId) {40         if (attr_drawable != -1) {41             ViewAttributeUtil.applyBackgroundDrawable(this, themeId, attr_drawable);42         }43         if (attr_textColor != -1) {44             ViewAttributeUtil.applyTextColor(this, themeId, attr_textColor);45         }46     }47 }
 

看上去 其实也蛮简单的对吧,无非就相比传统控件,他对外暴露了 setTheme这个方法罢了,而这个setTheme方法 其实就做了一件事,调用系统自己的方法重新set那些属性罢了。

 
1 package com.example.administrator.mainchangethemeapp; 2  3  4 import android.content.res.Resources; 5 import android.content.res.TypedArray; 6 import android.graphics.drawable.Drawable; 7 import android.util.AttributeSet; 8 import android.util.Log; 9 import android.widget.ImageView;10 import android.widget.TextView;11 12 public class ViewAttributeUtil {13 14     public static int getAttributeValue(AttributeSet attr, int paramInt) {15         int value = -1;16         int count = attr.getAttributeCount();17         for(int i = 0; i 
 

好,到这里 脉络就逐渐清晰了,我们就看看主activity里怎么写了:

 
1 package com.example.administrator.mainchangethemeapp; 2  3 import android.app.Activity; 4 import android.os.Bundle; 5 import android.view.Menu; 6 import android.view.MenuItem; 7 import android.view.View; 8 import android.widget.TextView; 9 10 public class MainActivity extends Activity {11 12     private TextView tv;13 14     private View view;15 16     @Override17     protected void onCreate(Bundle savedInstanceState) {18         //这行代码千万不能漏掉,否则会报错的。因为你在xml里 的那些值 根本就没有实际的值,如果不在这里强制性的setTheme就直接报错了19         //在实际使用中 我们当然可以把这个方法 放在baseactivity里面。我们这里默认主题就白天吧20         setTheme(R.style.dayTheme);21         super.onCreate(savedInstanceState);22         setContentView(R.layout.activity_main);23         //这个就是跟布局24         view = this.findViewById(R.id.mainview);25         tv = (TextView) this.findViewById(R.id.tv);26         tv.setOnClickListener(new View.OnClickListener() {27 28             @Override29             public void onClick(View v) {30 31                 //这边逻辑没啥好说的 其实可以不需要看啊,就是白天就切换到黑夜 黑夜就切换到白天呗32                 if (SPUtils.get(MainActivity.this, "theme", "dayTheme").equals("dayTheme")) {33                     SPUtils.put(MainActivity.this, "theme", "nightTheme");34                     setTheme(R.style.nightTheme);35                 } else {36                     SPUtils.put(MainActivity.this, "theme", "dayTheme");37                     setTheme(R.style.dayTheme);38                 }39                 //这个方法就是实现不重启页面切换主题的40                 ThemeUiUtil.changeTheme(view, getTheme());41             }42         });43     }44 45     @Override46     public boolean onCreateOptionsMenu(Menu menu) {47         // Inflate the menu; this adds items to the action bar if it is present.48         getMenuInflater().inflate(R.menu.menu_main, menu);49         return true;50     }51 52     @Override53     public boolean onOptionsItemSelected(MenuItem item) {54         // Handle action bar item clicks here. The action bar will55         // automatically handle clicks on the Home/Up button, so long56         // as you specify a parent activity in AndroidManifest.xml.57         int id = item.getItemId();58 59         //noinspection SimplifiableIfStatement60         if (id == R.id.action_settings) {61             return true;62         }63 64         return super.onOptionsItemSelected(item);65     }66 67 }
 

然后看一下切换主题的方法是怎么做的:

 
1 package com.example.administrator.mainchangethemeapp; 2  3 import android.content.res.Resources; 4 import android.util.Log; 5 import android.view.View; 6 import android.view.ViewGroup; 7 import android.widget.AbsListView; 8  9 import java.lang.reflect.Field;10 import java.lang.reflect.InvocationTargetException;11 import java.lang.reflect.Method;12 13 /**14  * Created by Administrator on 2015/11/14.15  */16 public class ThemeUiUtil {17     /**18      * 切换应用主题19      *20      * @param rootView21      */22     public static void changeTheme(View rootView, Resources.Theme theme) {23         //這裡邏輯很簡單 就是递归调用changeTheme-----递归调用setTheme了。24         //注意 你们如果是listview也包含在里面的话 listview自定义实现接口的时候要稍微复杂一些,看你们需要不需要也刷新listview里的item了25         //这里为了简单 我就不写那么复杂了,就这一个逻辑:先set自己的theme 然后遍历自己的子控件 逐一set26         if (rootView instanceof ThemeUIInterface) {27             ((ThemeUIInterface) rootView).setTheme(theme);28             if (rootView instanceof ViewGroup) {29                 int count = ((ViewGroup) rootView).getChildCount();30                 for (int i = 0; i < count; i++) {31                     changeTheme(((ViewGroup) rootView).getChildAt(i), theme);32                 }33             }34         }35     }36 37 38 }
 

 

你看,到这里 我们不重启activity实现换肤的功能就基本实现了,当然要做的完美的话 还需要各位自己扩充一下 其他控件。但是思路都是一样的。

 

 

看到这里 有人仍然会说,你这个虽然没有重启activity,但是还是不好看呀,还是显的突兀了,能否做到知乎 android app那样 切换白天黑夜主题的时候 显的很柔顺呢。

答案是可以的,而且解决方案也比较简单。就是给个动画就完事了,在切换前 先保留一下 之前界面的bitmap 然后切换皮肤的的时候 把这个bitmap 显示出来以后 然后改变他的

alpha值 就可以了,当全部动画显示结束以后 把那些不需要的资源全部释放 就ok了!

来看下代码,这里就放出点击事件的代码了,其他地方与前面的代码一致:

 
1 { 2  3                 //我们先取这个根布局的 bitmap缓存 这个实际上跟截屏是差不多的一个东西。 4                 view.setDrawingCacheEnabled(true); 5                 view.buildDrawingCache(true); 6                 final Bitmap localBitmap = Bitmap.createBitmap(view.getDrawingCache()); 7                 view.setDrawingCacheEnabled(false); 8  9                 //这边逻辑没啥好说的 其实可以不需要看啊,就是白天就切换到黑夜 黑夜就切换到白天呗10                 if (SPUtils.get(MainActivity.this, "theme", "dayTheme").equals("dayTheme")) {11                     SPUtils.put(MainActivity.this, "theme", "nightTheme");12                     setTheme(R.style.nightTheme);13                 } else {14                     SPUtils.put(MainActivity.this, "theme", "dayTheme");15                     setTheme(R.style.dayTheme);16                 }17                 //我们new出来的这个蒙版view --mengbanview 就把他放到跟布局view里面 并且让他充满 同时这个view的background就是截屏前我们的那个截图bitmap18                 final View mengbanView = new View(getApplicationContext());19                 mengbanView.setBackgroundDrawable(new BitmapDrawable(getResources(), localBitmap));20                 ViewGroup.LayoutParams params = new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT);21                 ((ViewGroup) view).addView(mengbanView, params);22                 mengbanView.animate().alpha(0).setDuration(400).setListener(new Animator.AnimatorListener() {23                     @Override24                     public void onAnimationStart(Animator animation) {25                         //这个方法就是实现不重启页面切换主题的26                         ThemeUiUtil.changeTheme(view, getTheme());27                     }28 29                     @Override30                     public void onAnimationEnd(Animator animation) {31                         //动画结束的时候移出这个蒙版view 并释放bitmap32                         ((ViewGroup) view).removeView(mengbanView);33                         localBitmap.recycle();34                     }35 36                     @Override37                     public void onAnimationCancel(Animator animation) {38 39                     }40 41                     @Override42                     public void onAnimationRepeat(Animator animation) {43 44                     }45                 }).start();46 47 48             }
 

最后来看下效果是否和知乎一样:

大功告成,看上去自然多了!

转载于:https://www.cnblogs.com/android-blogs/p/4968941.html

你可能感兴趣的文章
Xcode 工程文件打开不出来, cannot be opened because the project file cannot be parsed.
查看>>
iOS在Xcode6中怎么创建OC category文件
查看>>
5、JavaWeb学习之基础篇—标签(自定义&JSTL)
查看>>
8、JavaWEB学习之基础篇—文件上传&下载
查看>>
reRender属性的使用
查看>>
href="javascript:void(0)"
查看>>
h:panelGrid、h:panelGroup标签学习
查看>>
f:facet标签 的用法
查看>>
<h:panelgroup>相当于span元素
查看>>
java中append()的方法
查看>>
必学高级SQL语句
查看>>
经典SQL语句大全
查看>>
log日志记录是什么
查看>>
<rich:modelPanel>标签的使用
查看>>
<h:commandLink>和<h:inputLink>的区别
查看>>
<a4j:keeyAlive>的英文介绍
查看>>
关于list对象的转化问题
查看>>
VOPO对象介绍
查看>>
suse创建的虚拟机,修改ip地址
查看>>
linux的挂载的问题,重启后就挂载就没有了
查看>>