源码地址

在一切开始之前,我只想用正当的方式,跪求各位的一个star

预览

在写SegmentFault for Android 4.0的过程中,因为原先采用的夜间模式,代码着实不好看,于是我又开始挖坑了。

在几个月前更新的Android Support Library 23.2中,让我们认识到了DayNight Theme。一看源码,原来以前在API 8的时候就已经有了night相关的资源可以设置,只是之前一直不知道怎么使用,后来发现原来还是利用了AssetManager相关的API —— Android在指定条件下加载指定文件夹中的资源。 这正是我想要的! 这样我们只用指定好引用的资源,(比如@color/colorPrimary) 那么我就可以在白天加载values/color.xml中的资源,晚上加载values-night/color.xml中的资源。

v7已经帮我们完成了这里的功能,放置夜晚资源的问题也已经解决了,可是每次切换DayNight模式的时候,需要重启下Activity,这件事情很让人讨厌,原因就是因为重启后,我们的Context就会重新创建,View也会重新创建,根据当前系统(应用)配置的不同,加载不同的资源。 那我们有没有可能做到不重启Activity来实现夜间模式呢?其实实现方案很简单:我们只用记录好系统渲染xml的时候,当时给View的资源id,在特定时刻,重新加载这些资源,然后设置给View即可。接下去我们碰到两个问题:在引入这个库的情况下,让开发者少改已有的xml文件,把所有的布局都换为我们指定的布局。

API要尽量简单,清楚,明白。

上面两个条件说起来很容易,其实想实现并不是很容易的,还好AppCompat给了我一些思路。

来自AppCompat的启发

当我们引入appcompat-v7,有了AppCompatActivity的时候,我们发现我们渲染的TextView/Button等组件分别变成了AppCompatTextView和AppCompatButton, 这些组件都是包含在v7包中的,很早以前觉得很神奇,当看了AppCompatActivity和AppCompatDelegate的源码,知道了LayoutInflator.Factory这些东西的工作原理之后,这一切也就不神奇了 —— 它只是在inflate的过程中,注入了自己的代码进去,比如把TextView解析成AppCompatTextView类,达到对解析结果拦截的目的。

OK,借助这个方法,我们可以在Activity.onCreate中,注入我们自己的LayoutInflatorFactory:

像这样,有兴趣的同学可以看看AppCompatDelegateImplV7这个类的installViewFactory方法的实现。

接下去我们的目的是把TextView、Button等类换成我们自己的实现——SkinnableTextView和SkinnableButton。

可以翻到AppCompatViewInflater这个类的源码,其实很清晰了:public final View createView(View parent, final String name, @NonNull Context context,

@NonNull AttributeSet attrs, boolean inheritContext,

boolean readAndroidTheme, boolean readAppTheme, boolean wrapContext){

final Context originalContext = context;

// We can emulate Lollipop's android:theme attribute propagating down the view hierarchy

// by using the parent's context

if (inheritContext && parent != null) {

context = parent.getContext();

}

if (readAndroidTheme || readAppTheme) {

// We then apply the theme on the context, if specified

context = themifyContext(context, attrs, readAndroidTheme, readAppTheme);

}

if (wrapContext) {

context = TintContextWrapper.wrap(context);

}

View view = null;

// We need to 'inject' our tint aware Views in place of the standard framework versions

switch (name) {

case "TextView":

view = new AppCompatTextView(context, attrs);

break;

case "ImageView":

view = new AppCompatImageView(context, attrs);

break;

case "Button":

view = new AppCompatButton(context, attrs);

break;

case "EditText":

view = new AppCompatEditText(context, attrs);

break;

case "Spinner":

view = new AppCompatSpinner(context, attrs);

break;

case "ImageButton":

view = new AppCompatImageButton(context, attrs);

break;

case "CheckBox":

view = new AppCompatCheckBox(context, attrs);

break;

case "RadioButton":

view = new AppCompatRadioButton(context, attrs);

break;

case "CheckedTextView":

view = new AppCompatCheckedTextView(context, attrs);

break;

case "AutoCompleteTextView":

view = new AppCompatAutoCompleteTextView(context, attrs);

break;

case "MultiAutoCompleteTextView":

view = new AppCompatMultiAutoCompleteTextView(context, attrs);

break;

case "RatingBar":

view = new AppCompatRatingBar(context, attrs);

break;

case "SeekBar":

view = new AppCompatSeekBar(context, attrs);

break;

}

if (view == null && originalContext != context) {

// If the original context does not equal our themed context, then we need to manually

// inflate it using the name so that android:theme takes effect.

view = createViewFromTag(context, name, attrs);

}

if (view != null) {

// If we have created a view, check it's android:onClick

checkOnClickListener(view, attrs);

}

return view;

}

这里完成的工作就是把XML中的一些Tag解析为java的类实例,我们可以依样画葫芦,只不过把其中的AppCompatTextView换成SkinnableTextView//省略代码

switch (name) {

case "TextView":

view = new SkinnableTextView(context, attrs);

break;

}

//省略代码

好了,如果有需要,我们在库中把所有的类都替换成自己的实现,就能达到目的了,使得那些使用原始控件的开发者,不修改一丝一毫的代码,渲染出我们定制的控件。

应用DayNightMode

上一节我们解决了自定义View替换原始View的问题,那么接下去怎么办呢?这里我们同样也参考AppCompat关于BackgroundTint的一些设计方式。首先我们可以看到AppComatTextView的声明:public class AppCompatTextView extends TextView implements TintableBackgroundView{

//...

}

实现了一个TintableBackgroundView的接口,而我们使用ViewCompat.setSupportBackgroundTint的时候,可以找到这么一条:static void setBackgroundTintList(View view, ColorStateList tintList){

if (view instanceof TintableBackgroundView) {

((TintableBackgroundView) view).setSupportBackgroundTintList(tintList);

}

}

利用OO的特性,很轻松的判断这个View是否支持我们想要的特性,这时候我也声明了一个接口Skinnablepublic class SkinnableTextView extends AppCompatTextView implements Skinnable{

//...

}

这样等于给我的类打了一个标记,外部调用的时候,就可以判断这个View是否实现了我们的接口,如果实现了接口,就可以调用相关的函数。

我们在Activity的基类中,可以如此调用private void applyDayNightForView(View view){

if (view instanceof Skinnable) {

Skinnable skinnable = (Skinnable) view;

if (skinnable.isSkinnable()) {

skinnable.applyDayNight();

}

}

if (view instanceof ViewGroup) {

ViewGroup parent = (ViewGroup)view;

int childCount = parent.getChildCount();

for (int i = 0; i < childCount; i++) {

applyDayNightForView(parent.getChildAt(i));

}

}

}

利用递归的方式,把所有实现Skinnable接口的View全部应用了applyDayNight方法。 因此开发者使用的时候,只用把Activity的继承改为SkinnableActivity,然后在恰当的时机调用setDayNightMode即可。

Skinnable在View中具体实现

这节讲的是如何解决我们的痛点 —— 不重启Activity应用DayNight mode。

那我们的View实现Skinnable接口中的方法,到底是如何工作的呢,以SkinnableTextView为例子。

一般我们对TextView应用的样式有background和textColor,额外的情况下带一个backgroundTint都是OK的。

首先我们的大前提是,这些资源在xml中是用引用的方式传进来的,什么意思呢,看下面的表格对错android:textColor="@color/primaryColor"android:textColor="#fff"

android:textColor="?attr/colorPrimary"android:textColor="#000"

总结起来一句话,就是不应该是绝对值,如果是绝对值的话,我们去改它的值也不符合逻辑。

那么如果是资源引用的方式的话,我们使用TypedArray这个对象,是可以获取到我们引用的资源的id的,也就是R.color.primaryColor的具体数值。 我们把这个值保存下来,然后在恰当的时候,利用这个值再去变化后的Context中获取一遍指定的颜色ContextCompat.getColor(context, R.color.primaryColor);

这时候我们获取到的实际值,context就会根据系统的配置去正确的文件夹下找我们想要的资源了。

我们利用TypedArray能获取到资源的id,使用TypedArray.getResourceId方法即可,传入属性的索引值就行。public void storeAttributeResource(TypedArray a, int[] styleable) {

int size = a.getIndexCount();

for (int index = 0; index < size; index ++) {

int resourceId = a.getResourceId(index, -1);

int key = styleable[index];

if (resourceId != -1) {

mResourceMap.put(key, resourceId);

}

}

}

最后,在切换夜间模式的时候,我们调用了applyDayNight方法,具体代码如下:@Override

public void applyDayNight(){

Context context = getContext();

int key;

key = R.styleable.SkinnableView[R.styleable.SkinnableView_android_background];

Integer backgroundResource = mAttrsHelper.getAttributeResource(key);

if (backgroundResource != null) {

Drawable background = ContextCompat.getDrawable(context, backgroundResource);

//这时候获取到的background是符合上下文的

setBackgroundDrawable(background);

}

//省略代码

}

总结以及缺陷

经过以上几点的开发,我们使用日/夜模式切换就变得非常容易了,比如我们如果只处理颜色的修改的话,只用在values/colors.xml和values-night/colors.xml配置好指定颜色在不同模式下的表现形式,再调用setDayNightMode方法,就可以完成一键切换,不需要在xml中添加任何复杂凌乱的东西。

因为在配置上节省了许多代码,那我们的约定就变得比较冗长了,如果想进行自定义View的换肤的话,就需要手动去实现Skinnable接口,实现applyDayNight方法,开发者这时候就需要去做一些缓存资源id的操作。

同时因为它依赖于AppCompat DayNight Mode,它只能作用于日/夜间模式的切换,要想实现换肤功能,是做不到的。

这两点是缺陷,同时也是和市面上其他换肤库最不同的地方。但是我们把肮脏的代码隐藏在顶部实现里,就是为了业务逻辑层代码的干净和整洁。

希望各位会喜欢,然后有问题可以留言或者在github上给我提PR,非常感谢。

Android实现白天黑夜动画,android 实现【夜晚模式】的另外一种思路相关推荐

  1. android progressbar 水平动画,Android ProgressBar 自定义样式(三),动画模式

    果: 和之前的一样,在布局文件中: android:id="@+id/progressBar3" android:layout_width="wrap_content&q ...

  2. android局部翻转动画,android 围绕中心旋转动画

    本文主要介绍Android中如何使用rotate实现图片不停旋转的效果.Android 平台提供了两类动画,一类是 Tween 动画,即通过对场景里的对象不断做图像变换(平移.缩放.旋转)产生动画效果 ...

  3. android 实现冒泡动画,android 触摸事件冒泡动画效果

    原图魔法效果:(透明的有些看不清) PS之后加了背景色并放大后的效果 在屏幕中的效果(左上很小的那个,其他都是背景图): 中间很小的那个就是 先看动画实现代码explosion.xml(explosi ...

  4. android 心跳效果动画,Android 心跳动画

    直接上代码  MainActivity public class MainActivity extends AppCompatActivity { private ImageView ivHart; ...

  5. android 实现qq动画,Android项目:简易版QQ的实现

    简易版QQ实现涉及的三个功能模块 引导界面 splash界面(静态) 1.作用:初始化服务器端的一些数据,初始化成功后跳转到主界面 2.页面的延迟跳转: //在主线程中: new Handler(). ...

  6. android 淡入位移动画,Android动画 translate(位移)、scale(缩放)、alpha(淡入淡出)、rotate(旋转)...

    一.Android动画类型 Android的animation由四种类型组成 在xml文件中 alpha 渐变透明度动画效果 scale 渐变尺寸伸缩动画效果 translate 画面转换位置移动动画 ...

  7. android+桌面文件夹动画,Android动画

    1.为什么要说动画? 动画的适用是Android开发常用的知识 种类繁多,适用复杂,很多实现需要自定义动画 2.目前Android中有多少种动画? 视图动画(View 动画) 属性动画 揭露动画(Re ...

  8. android 图片查看动画,Android 共享动画实现点击列表图片跳转查看大图页面

    主要内容使用系统提供的 API 实现共享动画 在实现过程中遇到的问题图片点击和关闭之后会出现短暂的黑屏问题实现的动画效果如下: 共享动画.gif 具体实现这个效果是在两个页面之间的切换动画,既然是两个 ...

  9. Android实现蝴蝶动画,Android中的动画具体解释系列——飞舞的蝴蝶

    这一篇来使用逐帧动画和补间动画来实现一个小样例,首先我们来看看Android中的补间动画. Android中使用Animation代表抽象的动画类,该类包含以下几个子类: AlphaAnimation ...

最新文章

  1. [Core Java® for the Impatient]重载Java2
  2. [原创] Neo.Geo 视频帧浏览器开发日志
  3. 超实用的58个office快捷键汇总,办公室人员必备!
  4. 为私有Kubernetes集群创建LoadBalancer服务
  5. php 使用css乱码,分享CSS字符编码引起乱码快速解决的方法
  6. 关于android开发添加菜单XML文件之后无法在R.java中生成ID的问题
  7. HTML的相关路径与绝对路径的问题---通过网络搜索整理
  8. 日期选择器date、week、time、datetime、datetime-local类型
  9. 如何关闭借呗订阅开通通知_支付宝花呗借呗隐藏规则,芝麻分600以上,花呗3.6万,借呗12万!...
  10. lda 协方差矩阵_数据降维算法总结(LDAamp;PCA)
  11. 四叶草引导linux教程,百科全书之黑苹果四叶草引导配置 boot讲解
  12. 网络操作系统VyOS之NAT实践
  13. Excel中实现跨表数据有效性
  14. Android显示图片崩溃的解决办法
  15. 古代十二时辰,时辰,时辰对照表,十二时辰与时间对照表,12时辰,时辰表
  16. 我的世界无限资源的服务器,我的世界无限资源单机版
  17. android陀螺仪惯导手机gps,推荐基于陀螺仪惯性导航的智能停车定位导航解决方案...
  18. 深析C语言的灵魂 -- 指针
  19. TEMPDB空间已满
  20. python对压缩包简单加密_简单文件压缩加密脚本 python

热门文章

  1. 【云原生之Docker实战】使用Docker部署Mindoc文档管理平台
  2. [笔记]VMware常见问题
  3. 1. OpenCV 可视化(Viz)——相机位置
  4. 搭建PHP直播系统源码的教程,手把手教你手机直播app制作
  5. 跳楼程序员让我们思考:程序员中年危机都有哪些?
  6. 双 JK 触发器 74LS112 逻辑功能。真值表_触发器的工作原理是什么
  7. linux内核和发行版本的关系,简述Linux内核和Linux发行版的区别
  8. 学生个人网页设计作品 HTML+CSS+JavaScript仿小米商城(8页) 学生个人网页模板 简单个人主页成品 个人网页制作 HTML学生个人网站作业设计代做
  9. php网站 更改logo,zblog修改网站logo的方法
  10. 少年,暑期学编程可好?