现项目中涉及红色、金色主题,同时需要适配红色暗黑、金色暗黑,本地需要手动维护4套色值,并且切换主题时需要重新销毁创建页面,维护跟用户体验都不是很友好。
通过调研,发现换肤的实现原理比较符合适用当前项目的使用场景,开源项目 Android-Skin-Loader
通过查看源码换肤实现原理其实为 通过下载或者加载本地资源包,这里的资源包其实就是一个只有资源文件的项目通过编译打包生成的.apk文件,点击切换时,通过提前手动绑定view和要改变的资源类型 将资源Resource替换成资源包的Resource资源进行设置替换,从而达到换肤的效果。
由此整理出方案需要自行实现的点
LayoutInflatersetFactory(LayoutInflater.Factoryfactory)和setFactory2(LayoutInflater.Factory2 factory)两个方法可以让你去自定义布局的填充(有点类似于过滤器,我们在填充这个View之前可以手动绑定view和要改变的资源类型),Factory2 是在API 11才添加的。
通过阅读源码可以发现,我们在进入setContentView(R.layout.activity_main)可以看到
@Override
public void setContentView(@LayoutRes int layoutResID) {
getDelegate().setContentView(layoutResID);
}
这里的getDelegate()获取到的是AppCompatDelegateImpl,我们可以看到
@RestrictTo(LIBRARY)
class AppCompatDelegateImpl extends AppCompatDelegate
implements MenuBuilder.Callback, LayoutInflater.Factory2 {
实现了LayoutInflater.Factory2接口,在看 获取到的是AppCompatDelegateImpl的
@Override
public void setContentView(int resId) {
ensureSubDecor();
ViewGroup contentParent = mSubDecor.findViewById(android.R.id.content);
contentParent.removeAllViews();
LayoutInflater.from(mContext).inflate(resId, contentParent);
mAppCompatWindowCallback.getWrapped().onContentChanged();
}
LayoutInflater.from(mContext)获取最终获取的是ontext.getSystemService(Context.LAYOUT_INFLATER_SERVICE)
/**
* Obtains the LayoutInflater from the given context.
*/
public static LayoutInflater from(Context context) {
LayoutInflater LayoutInflater =
(LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
if (LayoutInflater == null) {
throw new AssertionError("LayoutInflater not found.");
}
return LayoutInflater;
}
继续往里看会找到
public View inflate(XmlPullParser parser, @Nullable ViewGroup root, boolean attachToRoot) {
synchronized (mConstructorArgs) {
Trace.traceBegin(Trace.TRACE_TAG_VIEW, "inflate");
final Context inflaterContext = mContext;
final AttributeSet attrs = Xml.asAttributeSet(parser);
Context lastContext = (Context) mConstructorArgs[0];
mConstructorArgs[0] = inflaterContext;
View result = root;
try {
advanceToRootNode(parser);
final String name = parser.getName();
if (DEBUG) {
System.out.println("**************************");
System.out.println("Creating root view: "
+ name);
System.out.println("**************************");
}
if (TAG_MERGE.equals(name)) {
if (root == null || !attachToRoot) {
throw new InflateException("<merge /> can be used only with a valid "
+ "ViewGroup root and attachToRoot=true");
}
rInflate(parser, root, inflaterContext, attrs, false);
} else {
// Temp is the root view that was found in the xml
final View temp = createViewFromTag(root, name, inflaterContext, attrs);
ViewGroup.LayoutParams params = null;
if (root != null) {
if (DEBUG) {
System.out.println("Creating params from root: " +
root);
}
// Create layout params that match root, if supplied
params = root.generateLayoutParams(attrs);
if (!attachToRoot) {
// Set the layout params for temp if we are not
// attaching. (If we are, we use addView, below)
temp.setLayoutParams(params);
}
}
if (DEBUG) {
System.out.println("-----> start inflating children");
}
// Inflate all children under temp against its context.
rInflateChildren(parser, temp, attrs, true);
if (DEBUG) {
System.out.println("-----> done inflating children");
}
// We are supposed to attach all the views we found (int temp)
// to root. Do that now.
if (root != null && attachToRoot) {
root.addView(temp, params);
}
// Decide whether to return the root that was passed in or the
// top view found in xml.
if (root == null || !attachToRoot) {
result = temp;
}
}
} catch (XmlPullParserException e) {
final InflateException ie = new InflateException(e.getMessage(), e);
ie.setStackTrace(EMPTY_STACK_TRACE);
throw ie;
} catch (Exception e) {
final InflateException ie = new InflateException(
getParserStateDescription(inflaterContext, attrs)
+ ": " + e.getMessage(), e);
ie.setStackTrace(EMPTY_STACK_TRACE);
throw ie;
} finally {
// Don't retain static reference on context.
mConstructorArgs[0] = lastContext;
mConstructorArgs[1] = null;
Trace.traceEnd(Trace.TRACE_TAG_VIEW);
}
return result;
}
}
可以看到如果不是merge标签会通过createViewFromTag(root,name,inflaterContext, attrs)创建view,找到方法
@UnsupportedAppUsage
View createViewFromTag(View parent, String name, Context context, AttributeSet attrs,
boolean ignoreThemeAttr) {
if (name.equals("view")) {
name = attrs.getAttributeValue(null, "class");
}
// Apply a theme wrapper, if allowed and one is specified.
if (!ignoreThemeAttr) {
final TypedArray ta = context.obtainStyledAttributes(attrs, ATTRS_THEME);
final int themeResId = ta.getResourceId(0, 0);
if (themeResId != 0) {
context = new ContextThemeWrapper(context, themeResId);
}
ta.recycle();
}
try {
View view = tryCreateView(parent, name, context, attrs);
if (view == null) {
final Object lastContext = mConstructorArgs[0];
mConstructorArgs[0] = context;
try {
if (-1 == name.indexOf('.')) {
view = onCreateView(context, parent, name, attrs);
} else {
view = createView(context, name, null, attrs);
}
} finally {
mConstructorArgs[0] = lastContext;
}
}
return view;
} catch (InflateException e) {
throw e;
} catch (ClassNotFoundException e) {
final InflateException ie = new InflateException(
getParserStateDescription(context, attrs)
+ ": Error inflating class " + name, e);
ie.setStackTrace(EMPTY_STACK_TRACE);
throw ie;
} catch (Exception e) {
final InflateException ie = new InflateException(
getParserStateDescription(context, attrs)
+ ": Error inflating class " + name, e);
ie.setStackTrace(EMPTY_STACK_TRACE);
throw ie;
}
}
最终我们在tryCreateView(@Nullable View parent, @NonNull String name,@NonNull Context context,@NonNull AttributeSet attrs)里看到
@UnsupportedAppUsage(trackingBug = 122360734)
@Nullable
public final View tryCreateView(@Nullable View parent, @NonNull String name,
@NonNull Context context,
@NonNull AttributeSet attrs) {
if (name.equals(TAG_1995)) {
// Let's party like it's 1995!
return new BlinkLayout(context, attrs);
}
View view;
if (mFactory2 != null) {
view = mFactory2.onCreateView(parent, name, context, attrs);
} else if (mFactory != null) {
view = mFactory.onCreateView(name, context, attrs);
} else {
view = null;
}
if (view == null && mPrivateFactory != null) {
view = mPrivateFactory.onCreateView(parent, name, context, attrs);
}
return view;
}
最终是调用的LayoutInflater的setFactory2()方法创建View,当我们不手动调用设置Factory2,我们还记得前面说的AppCompatDelegateImpl实现了LayoutInflater.Factory2接口重写了createView()方法并设置了
@Override
public void installViewFactory() {
LayoutInflater layoutInflater = LayoutInflater.from(mContext);
if (layoutInflater.getFactory() == null) {
LayoutInflaterCompat.setFactory2(layoutInflater, this);
} else {
if (!(layoutInflater.getFactory2() instanceof AppCompatDelegateImpl)) {
Log.i(TAG, "The Activity's LayoutInflater already has a Factory installed"
+ " so we can not install AppCompat's");
}
}
}
@Override
public View createView(View parent, final String name, @NonNull Context context,
@NonNull AttributeSet attrs) {
if (mAppCompatViewInflater == null) {
TypedArray a = mContext.obtainStyledAttributes(R.styleable.AppCompatTheme);
String viewInflaterClassName =
a.getString(R.styleable.AppCompatTheme_viewInflaterClass);
if ((viewInflaterClassName == null)
|| AppCompatViewInflater.class.getName().equals(viewInflaterClassName)) {
// Either default class name or set explicitly to null. In both cases
// create the base inflater (no reflection)
mAppCompatViewInflater = new AppCompatViewInflater();
} else {
try {
Class<?> viewInflaterClass = Class.forName(viewInflaterClassName);
mAppCompatViewInflater =
(AppCompatViewInflater) viewInflaterClass.getDeclaredConstructor()
.newInstance();
} catch (Throwable t) {
Log.i(TAG, "Failed to instantiate custom view inflater "
+ viewInflaterClassName + ". Falling back to default.", t);
mAppCompatViewInflater = new AppCompatViewInflater();
}
}
}
发现具体是通过AppCompatViewInflater的createView()去创建的View具体
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 = createTextView(context, attrs);
verifyNotNull(view, name);
break;
case "ImageView":
view = createImageView(context, attrs);
verifyNotNull(view, name);
break;
case "Button":
view = createButton(context, attrs);
verifyNotNull(view, name);
break;
case "EditText":
view = createEditText(context, attrs);
verifyNotNull(view, name);
break;
case "Spinner":
view = createSpinner(context, attrs);
verifyNotNull(view, name);
break;
case "ImageButton":
view = createImageButton(context, attrs);
verifyNotNull(view, name);
break;
case "CheckBox":
view = createCheckBox(context, attrs);
verifyNotNull(view, name);
break;
case "RadioButton":
view = createRadioButton(context, attrs);
verifyNotNull(view, name);
break;
case "CheckedTextView":
view = createCheckedTextView(context, attrs);
verifyNotNull(view, name);
break;
case "AutoCompleteTextView":
view = createAutoCompleteTextView(context, attrs);
verifyNotNull(view, name);
break;
case "MultiAutoCompleteTextView":
view = createMultiAutoCompleteTextView(context, attrs);
verifyNotNull(view, name);
break;
case "RatingBar":
view = createRatingBar(context, attrs);
verifyNotNull(view, name);
break;
case "SeekBar":
view = createSeekBar(context, attrs);
verifyNotNull(view, name);
break;
case "ToggleButton":
view = createToggleButton(context, attrs);
verifyNotNull(view, name);
break;
default:
// The fallback that allows extending class to take over view inflation
// for other tags. Note that we don't check that the result is not-null.
// That allows the custom inflater path to fall back on the default one
// later in this method.
view = createView(context, name, attrs);
}
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 its android:onClick
checkOnClickListener(view, attrs);
}
return view;
}
通过上面的了解我们可以大概知道可以通过自定义一个ThemeInflaterFactory implements LayoutInflater.Factory2用来解析XML布局创建View(相当于hook主系统创建view的过程)。这里我们可以合理的保存下来需要适配主题的view以及属性
@Override
public View onCreateView(View parent, String name, Context context, AttributeSet attrs) {
boolean isThemeEnable = attrs.getAttributeBooleanValue(ThemeConfig.NAMESPACE, ThemeConfig.ATTR_THEME_ENABLE, false);
//调用系统创建基本控件
AppCompatDelegate delegate = mAppCompatActivity.getDelegate();
View view = delegate.createView(parent, name, context, attrs);
if (isThemeEnable || ThemeConfig.isGlobalSkinApply()) { //控件支持切换模式 或者开启类全局支持开关
if (view == null) {
view = ViewCreate.createViewFromTag(context, name, attrs);
}
if (view == null) {
return null;
}
parseSkinAttr(context, attrs, view);
}
return view;
}
private void parseSkinAttr(Context context, AttributeSet attrs, View view) {
List<BaseAttr> viewAttrs = new ArrayList<>();
for (int i = 0; i < attrs.getAttributeCount(); i++) {
String attrName = attrs.getAttributeName(i);
String attrValue = attrs.getAttributeValue(i);
if ("style".equals(attrName)) { //mxl布局引入style
int[] skinAttrs = new int[]{android.R.attr.textColor, android.R.attr.background};
TypedArray a = context.getTheme().obtainStyledAttributes(attrs, skinAttrs, 0, 0);
int textColorId = a.getResourceId(0, -1);
int backgroundId = a.getResourceId(1, -1);
if (textColorId != -1) {
String entryName = context.getResources().getResourceEntryName(textColorId);
String typeName = context.getResources().getResourceTypeName(textColorId);
BaseAttr skinAttr = AttrFactory.get("textColor", textColorId, entryName, typeName);
if (skinAttr != null) {
viewAttrs.add(skinAttr);
}
}
if (backgroundId != -1) {
String entryName = context.getResources().getResourceEntryName(backgroundId);
String typeName = context.getResources().getResourceTypeName(backgroundId);
BaseAttr skinAttr = AttrFactory.get("background", backgroundId, entryName, typeName);
if (skinAttr != null) {
viewAttrs.add(skinAttr);
}
}
a.recycle();
continue;
}
if (AttrFactory.isSupportedAttr(attrName) && attrValue.startsWith("@")) {
try {
int id = Integer.parseInt(attrValue.substring(1));
if (id == 0) {
continue;
}
boolean isOnlyDark = attrs.getAttributeBooleanValue(ThemeConfig.NAMESPACE, ThemeConfig.ATTR_THEME_ONLY_DARK, false);
String entryName = context.getResources().getResourceEntryName(id);
String typeName = context.getResources().getResourceTypeName(id);
BaseAttr mSkinAttr = AttrFactory.get(attrName, id, entryName, typeName, isOnlyDark);
if (mSkinAttr != null) {
viewAttrs.add(mSkinAttr);
}
} catch (NumberFormatException e) {
}
}
}
if (!viewAttrs.isEmpty()) {
ThemeItem skinItem = new ThemeItem();
skinItem.view = view;
skinItem.attrs = viewAttrs;
mThemeItemMap.put(skinItem.view, skinItem);
if (!AppThemeManager.getInstance().isNormalTheme() || AppThemeManager.getInstance().isInNightTheme()) {
skinItem.changeTheme();
}
}
}
view 支持主题切换或者全局支持开关打开,保存对应的view以及属性AttrFactory获取的BaseAttr的实现类到ThemeItem里,保存于mThemeItemMap集合里
/**
* 运用主题
*/
public void applyTheme() {
if (mThemeItemMap.isEmpty()) {
return;
}
for (View view : mThemeItemMap.keySet()) {
if (view == null) {
continue;
}
mThemeItemMap.get(view).changeTheme();
}
}
对外提供方法修改主题
1.在Application初始化AppThemeManager以及配置ThemeConfig
public class DarkApplication extends Application {
@Override
public void onCreate() {
super.onCreate();
AppThemeManager.getInstance().init(this);
ThemeConfig.setCanChangeStatusColor(true);
ThemeConfig.enableGlobalThemeApply();
ThemeConfig.setDebug(true);
}
}
2.BaseActivity实现IThemeUpdate, IDynamicNewView 接口
public class BaseActivity extends AppCompatActivity implements IThemeUpdate, IDynamicNewView {
/**
* 自定义 InflaterFactory
*/
private ThemeInflaterFactory mThemeInflaterFactory;
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
mThemeInflaterFactory = new ThemeInflaterFactory(this);
LayoutInflaterCompat.setFactory2(getLayoutInflater(), mThemeInflaterFactory);
super.onCreate(savedInstanceState);
AppThemeManager.getInstance().addObserver(this);
changeStatusColor();
}
public void changeStatusColor() {
if (!ThemeConfig.isCanChangeStatusColor()) {
return;
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
int color = ThemeResourceUtil.getColorPrimaryDark();
if (color != -1) {
Window window = getWindow();
window.addFlags(WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS);
window.setStatusBarColor(ThemeResourceUtil.getColorPrimaryDark());
}
}
}
public ThemeInflaterFactory getInflaterFactory() {
return mThemeInflaterFactory;
}
@Override
public void dynamicAddView(View view, List<DynamicAttr> pDAttrs) {
mThemeInflaterFactory.dynamicAddThemeEnableView(this, view, pDAttrs);
}
@Override
public void dynamicAddView(View view, String attrName, int attrValueResId, boolean isOnlyDark) {
mThemeInflaterFactory.dynamicAddThemeEnableView(this, view, attrName, attrValueResId, isOnlyDark);
}
@Override
public void onThemeUpdate() {
mThemeInflaterFactory.applyTheme();
changeStatusColor();
}
@Override
protected void onDestroy() {
super.onDestroy();
AppThemeManager.getInstance().removeObserver(this);
mThemeInflaterFactory.clean();
}
}
注意 setFactory2()一定要在 super.onCreate(savedInstanceState)之前调用即setContentView之前设置Factory2
3.在需要切换主题的根布局上添加 <code>xmlns:theme="http://schemas.android.com/android/theme" </code>,然后在需要切换主题的View上加上 <code>theme:enable="true" </code>,注意<code>theme:onlyDark="true" </code>的使用场景是红色主题 和金色主题都使用的同一资源,不需要区分,只区分暗黑资源。不写默认为false
4.资源文件下创建对应资源区分colors.xml、colors-night.xml、colors_gold.xml、colors_gold_night.xml 或者mipmap_img.png、mipmap_img_night.png、mipmap_img_gold.png、mipmap_img_gold_night.png
具体获取的方法看参考ThemeResourceUtil
默认支持 textColor 和 background、src的主题切换。如果你还需要对其他属性进行主题切换,需要去自定义了
比如 TabLayout它下面会有一个指示器,当我们换主题的时候也希望这个指示器的颜色也跟着更改。
public class TabLayoutIndicatorAttr extends BaseAttr {
@Override
public void applyTheme(View view) {
if (view instanceof TabLayout) {
TabLayout tl = (TabLayout) view;
if (RES_TYPE_NAME_COLOR.equals(attrValueTypeName)) {
int color = ThemeResourceUtil.getColor(attrValueRefId);
tl.setSelectedTabIndicatorColor(color);
}
}
}
}
dynamicAddView:当动态创建的View也需要主题切换的时候,就可以调用dynamicAddView




最近因为项目需要,需要将Android手机系统自带的某个系统软件反编译并更改里面某个资源,并重新打包,签名生成新的自定义的apk,下面我来介绍一下我的实现过程。APK修改,分为以下几步:反编译解包,修改,重打包,修改签名等步骤。安卓apk修改准备工作1.系统配置好JavaJDK环境变量2.需要root权限的手机(针对系统自带apk,其他软件免root)3.Auto-Sign签名工具4.apktool工具安卓apk修改开始反编译本文拿Android系统里面的Settings.apk做demo,具体如何将apk获取出来在此就不过多介绍了,直接进入主题:按键win+R输入cmd,打开命令窗口,并将路
转自:spring.profiles.active和spring.profiles.include的使用及区别说明下文笔者讲述spring.profiles.active和spring.profiles.include的区别简介说明,如下所示我们都知道,在日常开发中,开发|测试|生产环境都拥有不同的配置信息如:jdbc地址、ip、端口等此时为了避免每次都修改全部信息,我们则可以采用以上的属性处理此类异常spring.profiles.active属性例:配置文件,可使用以下方式定义application-${profile}.properties开发环境配置文件:application-dev
多年来,我在各种网站上遇到过各种问题,用户在字符串和文本字段的开头/结尾放置空格。有时这些会导致格式/布局问题,有时会导致搜索问题(即搜索顺序看起来不对,但实际上并非如此),有时它们实际上会使应用程序崩溃。我认为这会很有用,而不是像我过去所做的那样放入一堆before_save回调,向ActiveRecord添加一些功能以在保存之前自动调用任何字符串/文本字段上的.strip,除非我告诉它不是,例如do_not_strip:field_x,:field_y或类定义顶部的类似内容。在我去弄清楚如何做到这一点之前,有没有人看到更好的解决方案?明确一点,我已经知道我可以做到这一点:befor
我希望从我们的登台服务器发送的所有电子邮件的主题中都带有“[STAGING]”字样。在Rails3.2中使用ActionMailer是否有一种优雅的方式来做到这一点? 最佳答案 这是我使用ActionMailerInterceptor找到的一个优雅的解决方案基于anexistinganswer.#config/initializers/change_staging_email_subject.rbifRails.env.staging?classChangeStagingEmailSubjectdefself.delivering_
一些我找到的选项是ActiveCouchCouchRESTCouchPotatoRelaxDBcouch_foo我更喜欢GitHub上的项目,因为这让我更容易fork和推送修复。所有这些都符合该要求。我习惯了Rails,所以我喜欢像ActiveRecord模型一样工作的东西。另一方面,我也不希望我和Couch之间太多--毕竟我使用它作为我的数据库是有原因的。最后,它们似乎都得到了相当积极的维护(couch_foo可能是个异常(exception))。所以我想这归结为(不可否认和不幸的)主观:有没有人对他们有过好的或坏的经历? 最佳答案
我尝试用Ruby设计一个基于Web的应用程序。我开发了一个简单的核心应用程序,在没有框架和数据库的情况下在六边形架构中实现DCI范例。核心六边形中有小六边形和网络,数据库,日志等适配器。每个六边形都在没有数据库和框架的情况下自行运行。在这种方法中,我如何提供与数据库模型和实体类的关系作为独立于数据库的关系。我想在将来将框架从Rails更改为Sinatra或数据库。事实上,我如何在这个核心Hexagon中实现完全隔离的rails和mongodb的数据库适配器或框架适配器。有什么想法吗? 最佳答案 ROM呢?(Ruby对象映射器)。还有
我正在尝试为ChefRecipe编写一个库,以简化一些常见的搜索。例如,我希望能够在cookbook/libraries/library.rb中执行类似的操作,然后从同一Recipe中的Recipe中使用它:moduleExampledefself.search_attribute(attribute_name)returnsearch(:nodes,node[attribute_name])endend问题是,在Chef库文件中,node对象或search函数都不可用。似乎可以使用Chef::Search::Query.new().search(...)进行搜索,但我找不到任何可以访
我的迁移看起来像这样classCreateQuestionings现在,当我运行$rakedb:migrate:reset时,在我的db/schema.rb中看不到限制:create_table"questionings",force::cascadedo|t|t.text"body",null:falseend我做错了吗还是这是一个错误?顺便说一下,我使用的是rails5.0.0.beta3和ruby2.3.0p0。 最佳答案 t.text在PostgreSQL和textdoesn'tallowforsizelimits中生成
我目前正在subject中创建一个对象,需要测试这是否会引发异常。以下代码说明了我要实现的目标:describeMyClassdodescribe'#initialize'dosubject{MyClass.new}it{is_expected.not_toraise_error(Some::Error)}endend我有一种感觉,我正在以错误的方式处理这件事。将subject设置为新对象而不创建对象两次的首选方法是什么?更新我的问题有两个。首先,这种语法不起作用:it{is_expected.not_toraise_error}但是,在itblock中使用expect确实如此(正如J
iOS适配Unity-2019背景由于2019起,Unity的Xcode工程,更改了项目结构。Unity2018的结构:可以看Targets只有一个Unity-iPhone,Unity-iPhone直接依赖管理三方库。Unity2019以后:Targets多了一个UnityFramework,UnityFramework管理三方库,Unity-iPhone依赖于UnityFramwork。所以升级后,会有若干的问题,以下是对问题的解决方式。问题一错误描述error:exportArchive:Missingsigningidentifierat"/var/folders/fr//T/Xcode