在一切开始以前,我只想用正当的方式,跪求各位的一个star java
https://github.com/geminiwen/skin-spriteandroid
预览git
在写SegmentFault for Android 4.0
的过程当中,由于原先采用的夜间模式,代码着实很差看,因而我又开始挖坑了。github
在几个月前更新的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便可。接下去咱们碰到两个问题:app
在引入这个库的状况下,让开发者少改已有的xml文件,把全部的布局都换为咱们指定的布局。ide
API要尽可能简单,清楚,明白。函数
上面两个条件提及来很容易,其实想实现并非很容易的,还好AppCompat
给了我一些思路。布局
当咱们引入appcompat-v7
,有了AppCompatActivity
的时候,咱们发现咱们渲染的TextView
/Button
等组件分别变成了AppCompatTextView
和AppCompatButton
, 这些组件都是包含在v7
包中的,很早之前以为很神奇,当看了AppCompatActivity
和AppCompatDelegate
的源码,知道了LayoutInflator.Factory
这些东西的工做原理以后,这一切也就不神奇了 —— 它只是在inflate
的过程当中,注入了本身的代码进去,好比把TextView
解析成AppCompatTextView
类,达到对解析结果拦截的目的。spa
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; } //省略代码
好了,若是有须要,咱们在库中把全部的类都替换成本身的实现,就能达到目的了,使得那些使用原始控件的开发者,不修改一丝一毫的代码,渲染出咱们定制的控件。
上一节咱们解决了自定义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是否支持咱们想要的特性,这时候我也声明了一个接口Skinnable
public 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
便可。
这节讲的是如何解决咱们的痛点 —— 不重启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,很是感谢。
Github Repo 地址:https://github.com/geminiwen/skin-sprite