Android可更换布局的换肤方案
换肤,顾名思义,就是对应用中的视觉元素进行更新,呈现新的显示效果。一般来说,换肤的时候只是更新UI上使用的资源,如颜色,图片,字体等等。本文介绍一种笔者自己使用的基于布局的Android换肤方案,不仅可以更换所有的UI资源,而且可以更换主题样式(style)和布局样式。代码已托管到github:SkinFramework
换肤当然得有相应的皮肤包,不管是内置在应用内,还是做成可安装的皮肤应用包。但是这两种都有弊端:
1.内置在应用内会增加应用包的体积。
2.皮肤安装包需要安装过程,会占用更多的设备内置存储,用户会介意安装过多应用。而且为了是应用能够访问安装包内的资源,必须与应用使用相同的shareUserId。
鉴于此,本文推荐使用无需安装的外置皮肤包,优点在于:
1.无需安装,也无关乎shareUserId,不会引起用户反感。
2.按需下载使用,用户需要使用时自行下载,下载即可使用。
3.可放置于任何可访问的位置,SD卡或内置存储,可随时删除和添加,不会增加应用体积。
先来看一下效果图:
可以看到,图中有三种皮肤,默认皮肤,plain皮肤和vivid皮肤,都是更换了布局和资源的,其中还使用了AdapterView和Fragment作测试。可以看到,不同的皮肤有不同的布局样式,布局样式的不同也带来了很多可能,如隐藏或移动了功能入口。
所以说这是一个有很多可能的换肤框架,下面介绍一下核心 实现。
一、皮肤包
皮肤包就是一个不包含代码文件的Apk包,无需安装,可以新建工程,删除掉代码文件,复制应用里面需要修改的资源到新工程中修改,打成新包即可作为皮肤包使用,皮肤包后缀名可以改为任意。示例中使用了.skin作为后缀名。
二、皮肤包加载
皮肤包中包含的资源文件,需要加载到AssetManager中并创建Resources才能提供使用,关于Android的资源管理机制书上或网上已经有很多介绍,可以参考:Android中资源管理机制详细分析。所以我们的第一件事也是来加载皮肤包:
AssetManager assetManager = AssetManager.class.newInstance();
Method addAssetPath = assetManager.getClass().getMethod("addAssetPath", String.class);
//Return 0 on failure.
Object ret = addAssetPath.invoke(assetManager, skinPath);
if (Integer.parseInt(ret.toString()) == 0) {
throw new IllegalStateException("Add asset fail");
}
Resources localRes = context.getResources();
return new SkinResource(context, assetManager, localRes.getDisplayMetrics(), localRes.getConfiguration(), packageName);
三、资源管理器
加载了皮肤包,我们就有了两套可共使用的皮肤资源,应用默认资源和皮肤包资源,何时使用默认,何时使用皮肤,需要有一个管理器来决定,所以我们实现一个名为ComposedResources的类来扮演ResourcesManager:
/**
* Created by ARES on 2016/5/20
* This is a resources class consists of App default skin and external skin resources if exists. We will find resource in external skin resources first,then the default.
* Assume all resources ids are original so that we should find corresponding resources ids in skin .
*/ public class ComposedResources extends BaseResources {
static int LAYOUT_TAG_ID = -1;
private Context mContext;
private BaseSkinResources mSkinResources; public ComposedResources(Context context) {
this(context, null);
} public ComposedResources(Context context, BaseSkinResources skinResources) {
super(context.getResources());
mContext = context;
mSkinResources = skinResources;
} public ComposedResources setSkinResources(BaseSkinResources resources) {
mSkinResources = resources;
return this;
} public BaseSkinResources getSkinResources() {
return mSkinResources;
} @NonNull
@Override
public CharSequence getText(@StringRes int id) throws NotFoundException {
int realId = getCorrespondResId(id);
if (realId > 0) {
try {
return mSkinResources.getText(realId);
} catch (Exception e) {
}
}
return super.getText(id);
} @NonNull
@Override
public CharSequence getQuantityText(@PluralsRes int id, int quantity) throws NotFoundException {
int realId = getCorrespondResId(id);
if (realId > 0) {
return mSkinResources.getQuantityText(realId, quantity);
}
return super.getQuantityText(id, quantity);
} @NonNull
@Override
public String getQuantityString(@PluralsRes int id, int quantity, Object... formatArgs) throws NotFoundException {
int realId = getCorrespondResId(id);
if (realId > 0) {
return mSkinResources.getQuantityString(realId, quantity, formatArgs);
}
return super.getQuantityString(id, quantity, formatArgs);
} @NonNull
@Override
public String getQuantityString(@PluralsRes int id, int quantity) throws NotFoundException {
int realId = getCorrespondResId(id);
if (realId > 0) {
return mSkinResources.getQuantityString(realId, quantity);
}
return super.getQuantityString(id, quantity);
} @Override
public CharSequence getText(@StringRes int id, CharSequence def) {
int realId = getCorrespondResId(id);
if (realId > 0) {
return mSkinResources.getText(realId, def);
}
return super.getText(id, def);
} @NonNull
@Override
public CharSequence[] getTextArray(@ArrayRes int id) throws NotFoundException {
int realId = getCorrespondResId(id);
if (realId > 0) {
return mSkinResources.getTextArray(id);
}
return super.getTextArray(id);
} @NonNull
@Override
public String[] getStringArray(@ArrayRes int id) throws NotFoundException {
int realId = getCorrespondResId(id);
if (realId > 0) {
return mSkinResources.getStringArray(realId);
}
return super.getStringArray(id);
} @NonNull
@Override
public int[] getIntArray(@ArrayRes int id) throws NotFoundException {
int realId = getCorrespondResId(id);
if (realId > 0) {
return mSkinResources.getIntArray(realId);
}
return super.getIntArray(id);
} @NonNull
@Override
public TypedArray obtainTypedArray(@ArrayRes int id) throws NotFoundException {
int realId = getCorrespondResId(id);
if (realId > 0) {
return mSkinResources.obtainTypedArray(realId);
}
return super.obtainTypedArray(id);
} @Override
public float getDimension(@DimenRes int id) throws NotFoundException {
int realId = getCorrespondResId(id);
if (realId > 0) {
return mSkinResources.getDimension(realId);
}
return super.getDimension(id);
} @Override
public int getDimensionPixelOffset(@DimenRes int id) throws NotFoundException {
int realId = getCorrespondResId(id);
if (realId > 0) {
return mSkinResources.getDimensionPixelOffset(realId);
}
return super.getDimensionPixelOffset(id);
} @Override
public int getDimensionPixelSize(@DimenRes int id) throws NotFoundException {
int realId = getCorrespondResId(id);
if (realId > 0) {
return mSkinResources.getDimensionPixelSize(realId);
}
return super.getDimensionPixelSize(id);
} @Override
public float getFraction(@FractionRes int id, int base, int pbase) {
int realId = getCorrespondResId(id);
if (realId > 0) {
return mSkinResources.getFraction(id, base, pbase);
}
return super.getFraction(id, base, pbase);
} @Override
public Drawable getDrawable(@DrawableRes int id) throws NotFoundException {
int realId = getCorrespondResId(id);
if (realId > 0) {
return mSkinResources.getDrawable(realId);
}
return super.getDrawable(id);
} @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
@Override
public Drawable getDrawable(@DrawableRes int id, @Nullable Theme theme) throws NotFoundException {
int realId = getCorrespondResId(id);
if (realId > 0) {
return mSkinResources.getDrawable(realId, theme);
}
return super.getDrawable(id, theme);
} @Override
public Drawable getDrawableForDensity(@DrawableRes int id, int density) throws NotFoundException {
int realId = getCorrespondResId(id);
if (realId > 0) {
return mSkinResources.getDrawableForDensity(realId, density);
}
return super.getDrawableForDensity(id, density);
} @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
@Override
public Drawable getDrawableForDensity(@DrawableRes int id, int density, @Nullable Theme theme) {
int realId = getCorrespondResId(id);
if (realId > 0) {
return mSkinResources.getDrawableForDensity(realId, density, theme);
}
return super.getDrawableForDensity(id, density, theme);
} @Override
public Movie getMovie(@RawRes int id) throws NotFoundException {
int realId = getCorrespondResId(id);
if (realId > 0) {
return mSkinResources.getMovie(realId);
}
return super.getMovie(id);
} @Override
public int getColor(@ColorRes int id) throws NotFoundException {
int realId = getCorrespondResId(id);
if (realId > 0) {
return mSkinResources.getColor(realId);
}
return super.getColor(id);
} @RequiresApi(api = Build.VERSION_CODES.M)
@Override
public int getColor(@ColorRes int id, @Nullable Theme theme) throws NotFoundException {
int realId = getCorrespondResId(id);
if (realId > 0) {
return mSkinResources.getColor(realId, theme);
}
return super.getColor(id, theme);
} @Nullable
@Override
public ColorStateList getColorStateList(@ColorRes int id) throws NotFoundException {
int realId = getCorrespondResId(id);
if (realId > 0) {
return mSkinResources.getColorStateList(realId);
}
return super.getColorStateList(id);
} @RequiresApi(api = Build.VERSION_CODES.M)
@Nullable
@Override
public ColorStateList getColorStateList(@ColorRes int id, @Nullable Theme theme) throws NotFoundException {
int realId = getCorrespondResId(id);
if (realId > 0) {
return mSkinResources.getColorStateList(realId, theme);
}
return super.getColorStateList(id, theme);
} @Override
public boolean getBoolean(@BoolRes int id) throws NotFoundException {
int realId = getCorrespondResId(id);
if (realId > 0) {
return mSkinResources.getBoolean(realId);
}
return super.getBoolean(id);
} @Override
public int getInteger(@IntegerRes int id) throws NotFoundException {
int realId = getCorrespondResId(id);
if (realId > 0) {
return mSkinResources.getInteger(realId);
}
return super.getInteger(id);
} @Override
public XmlResourceParser getLayout(@LayoutRes int id) throws NotFoundException {
int realId = getCorrespondResIdStrictly(id);
if (realId > 0) {
return mSkinResources.getLayout(realId);
}
return super.getLayout(id);
} @Override
public XmlResourceParser getAnimation(@AnimRes int id) throws NotFoundException {
int realId = getCorrespondResIdStrictly(id);
if (realId > 0) {
return mSkinResources.getAnimation(realId);
}
return super.getAnimation(id);
} @Override
public XmlResourceParser getXml(@XmlRes int id) throws NotFoundException {
int realId = getCorrespondResIdStrictly(id);
if (realId > 0) {
return mSkinResources.getXml(realId);
}
return super.getXml(id);
} @Override
public InputStream openRawResource(@RawRes int id) throws NotFoundException {
int realId = getCorrespondResId(id);
if (realId > 0) {
return mSkinResources.openRawResource(realId);
}
return super.openRawResource(id);
} @Override
public InputStream openRawResource(@RawRes int id, TypedValue value) throws NotFoundException {
int realId = getCorrespondResId(id);
if (realId > 0) {
return mSkinResources.openRawResource(realId, value);
}
return super.openRawResource(id, value);
} @Override
public AssetFileDescriptor openRawResourceFd(@RawRes int id) throws NotFoundException {
int realId = getCorrespondResId(id);
if (realId > 0) {
return mSkinResources.openRawResourceFd(realId);
}
return super.openRawResourceFd(id);
} @Override
public void getValue(@AnyRes int id, TypedValue outValue, boolean resolveRefs) throws NotFoundException {
int realId = getCorrespondResId(id);
if (realId > 0) {
mSkinResources.getValue(realId, outValue, resolveRefs);
return;
}
super.getValue(id, outValue, resolveRefs);
} @Override
public void getValueForDensity(@AnyRes int id, int density, TypedValue outValue, boolean resolveRefs) throws NotFoundException {
int realId = getCorrespondResId(id);
if (realId > 0) {
mSkinResources.getValueForDensity(realId, density, outValue, resolveRefs);
return;
}
super.getValueForDensity(id, density, outValue, resolveRefs);
} @Override
public void getValue(String name, TypedValue outValue, boolean resolveRefs) throws NotFoundException {
if (mSkinResources != null) {
try {
mSkinResources.getValue(name, outValue, resolveRefs);
return;
} catch (Exception e) {
}
}
super.getValue(name, outValue, resolveRefs);
} @Override
public void updateConfiguration(Configuration config, DisplayMetrics metrics) {
if (mSkinResources != null) {
mSkinResources.updateConfiguration(config, metrics);
}
super.updateConfiguration(config, metrics);
} /**
* Get correspond resources id with app package. See also {@link #getCorrespondResId(int)}
*
* @param resId
* @return 0 if not exist
*/
public int getCorrespondResIdStrictly(int resId) {
if (mSkinResources == null) {
return 0;
}
String resName = getResourceName(resId);
return mSkinResources.getIdentifier(resName, null, null);
} /**
* Get correspond resources id with skin package. See also {@link #getCorrespondResId(int)}
*
* @param resId
* @return
*/
public int getCorrespondResId(int resId) {
if (mSkinResources == null) {
return 0;
}
return mSkinResources.getCorrespondResId(resId);
} @Override
public View getView(Context context, @LayoutRes int resId) {
//Take a resource id as the tag key.
if (LAYOUT_TAG_ID < 1) {
LAYOUT_TAG_ID = resId;
}
View view;
if (mSkinResources != null) {
int realId = getCorrespondResId(resId);
if (realId > 0) {
view = mSkinResources.getView(context, realId);
if (view != null) {
view.setTag(LAYOUT_TAG_ID, mSkinResources.getPackageName());
SkinUtils.showIds(view);
return view;
}
}
}
view = LayoutInflater.from(context).inflate(resId, null);
view.setTag(LAYOUT_TAG_ID, getPackageName());
SkinUtils.showIds(view);
return view;
} @Override
public String getPackageName() {
return mContext.getPackageName();
}
}
可以看到这个ResourceManager本身也是一个Resources,它继承自BaseResources,BaseResources继承自android.content.res.Resources。所以它可以直接作为应用的Resources来使用。其中有几点需要注意:
1.查找资源时,资源管理器应优先查找皮肤包中的资源,若皮肤包中没有相应资源,才使用应用默认资源。
2.每个应用包中的资源id是不同的,查找资源时,我们传入Resources的id都是应用中的id,而非皮肤包中的id,所以我们需要转换为皮肤包中相应的资源id,再获取具体的资源(此代码实现在SkinResources中,ComposedResources调用了此方法):
/**
* Get correspond resource id in skin archive.
* @param resId Resource id in app.
* @return 0 if not exist
*/
public int getCorrespondResId(int resId) {
Resources appResources = getAppResources();
String resName = appResources.getResourceName(resId);
if (!TextUtils.isEmpty(resName)) {
String skinName = resName.replace(mAppPackageName, getPackageName());
int id = getIdentifier(skinName, null, null);
return id;
}
return 0;
}
3.在获取XmlResourceParser时,需要使用应用对于资源的描述,而非皮肤包中的资源描述,所以有了getCorrespondResIdStrictly:
/**
* Get correspond resources id with app package. See also {@link #getCorrespondResId(int)}
*
* @param resId
* @return 0 if not exist
*/
public int getCorrespondResIdStrictly(int resId) {
if (mSkinResources == null) {
return 0;
}
String resName = getResourceName(resId);
return mSkinResources.getIdentifier(resName, null, null);
}
4.我们使用了LAYOUT_TAG_ID来记录了Layout所属的皮肤包,以便可以动态的判断是否需要更换布局(此方法可以用在动态换肤的时候,详情参考Demo):
@Override
public View getView(Context context, @LayoutRes int resId) {
//Take a resource id as the tag key.
if (LAYOUT_TAG_ID < 1) {
LAYOUT_TAG_ID = resId;
}
View view;
if (mSkinResources != null) {
int realId = getCorrespondResId(resId);
if (realId > 0) {
view = mSkinResources.getView(context, realId);
if (view != null) {
view.setTag(LAYOUT_TAG_ID, mSkinResources.getPackageName());
return view;
}
}
}
view = LayoutInflater.from(context).inflate(resId, null);
view.setTag(LAYOUT_TAG_ID, getPackageName());
return view;
}
四、布局更换处理
通过上述的代码,我们就已经能够完成常见资源的换肤了。但是对于布局资源,我们还需要做额外的处理。
1.Context与LayoutInflater
渲染View时,我们需要使用皮肤对应的Context和LayoutInflater,这样才能在View中使用正确的资源,所以我们为外置皮肤包创建相应的Context:
/**
* Context implementation for skin package.
*/
private class SkinThemeContext extends ContextThemeWrapper {
private WeakReference<Context> mContextRef; public SkinThemeContext(Context base) {
super();
if (base instanceof ContextThemeWrapper) {
attachBaseContext(((ContextThemeWrapper) base).getBaseContext());
mContextRef = new WeakReference<Context>(base);
} else {
attachBaseContext(base);
}
int themeRes = getThemeRes();
if (themeRes <= 0) {
themeRes = android.R.style.Theme_Light;
}
setTheme(themeRes);
} /**
* This implementation will support <code>onClick</code> attribute of view in xml.
* @param v
*/
public void onClick(View v) {
Context context = mContextRef == null ? null : mContextRef.get();
if (context == null) {
return;
}
if (context instanceof View.OnClickListener) {
((View.OnClickListener) context).onClick(v);
} else {
Class cls = context.getClass();
try {
Method m = cls.getDeclaredMethod("onClick", View.class);
if (m != null) {
m.invoke(context, v);
}
} catch (NoSuchMethodException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
}
}
} @Override
public AssetManager getAssets() {
return getAssets();
} @Override
public Resources getResources() {
return SkinResource.this;
} private int getThemeRes() {
try {
Method m = Context.class.getMethod("getThemeResId");
return (int) m.invoke(getBaseContext());
} catch (Exception e) {
e.printStackTrace();
}
return -1;
} }
其中我们对xml布局文件中的onClick属性作了支持,同时通过提供了皮肤包对应的资源,然后我们使用这个Context的实例创建LayoutInflater并渲染View:
@Override
public View getView(Context context, @LayoutRes int resId) {
try {
Context skinContext = new SkinThemeContext(context);
View v = LayoutInflater.from(skinContext).inflate(resId, null);
handleView(skinContext, v);
return v;
} catch (Exception e) { }
return null;
}
其中注意到handleView方法,正如前文所说,每个应用包生成的资源id是不一样的,这里View中生成的id是皮肤包中的id,需要转换为应用中的id方可使用:
/**
* Handle view to support used by app.
*
* @param v View resource from skin package.
*/
public void handleView(Context context, View v) {
resetID();
//Id map: Key as skin id and Value as local id.
SparseIntArray array = new SparseIntArray();
buildIdRules(context, v, array);
int size = array.size();
// Map ids to which app can recognize locally.
for (int i = 0; i < size; i++) {
//Map id defined in skin package into real id in app.
v.findViewById(array.keyAt(i)).setId(array.valueAt(i));
}
} /**
* Extract id from view , build id rules and inflate rules if needed.
*
* @param v
* @param array
*/
protected void buildIdRules(Context context, View v, SparseIntArray array) {
if (v.getId() != View.NO_ID) {
//Get mapped id by id name.
String idName = getResourceEntryName(v.getId());
int mappedId = getAppResources().getIdentifier(idName, "id", context.getPackageName());
//Add custom id to avoid id conflict when mapped id not exist.
//Key as skin id and value as mapped id.
array.put(v.getId(), mappedId > 0 ? mappedId : generateId());
}
if (v instanceof ViewGroup) {
ViewGroup vp = (ViewGroup) v;
int childCount = vp.getChildCount();
for (int i = 0; i < childCount; i++) {
buildIdRules(context, vp.getChildAt(i), array);
}
}
buildInflateRules(v, array);
} /**
* Build inflate rules.
*
* @param v
* @param array ID map of which Key as skin id and value as mapped id.
*/
protected void buildInflateRules(View v, SparseIntArray array) {
ViewGroup.LayoutParams lp = v.getLayoutParams();
if (lp == null) {
return;
}
if (lp instanceof RelativeLayout.LayoutParams) {
int[] rules = ((RelativeLayout.LayoutParams) lp).getRules();
if (rules == null) {
return;
}
int size = rules.length;
int mapRule = -1;
for (int i = 0; i < size; i++) {
//Key as skin id and value as mapped id.
if (rules[i] > 0 && (mapRule = array.get(rules[i])) > 0) {
// Log.i(TAG, "Rules[" + i + "]: Mapped from: " + rules[i] + " to " +mapRule);
rules[i] = mapRule;
}
}
}
}
五、使用
下载源码,集成skin module到工程中,然后使用SkinManager提供的接口:
public SkinManager initialize(Context context);//初始化皮肤管理器 /**
* Register an observer to be informed of skin changed for ui interface such as activity,fragment, dialog etc.
* @param observer
*/
public void register(ISkinObserver observer);//注册换肤监听器,用于需要动态换肤的场景。 /**
* Get resources.
* @return
*/
public BaseResources getResources();//获取资源 /**
* Change skin.
* @param skinPath Path of skin archive.
* @param pkgName Package name of skin archive.
* @param cb Callback to be informed of skin-changing event.
*/
public void changeSkin(String skinPath, String pkgName, ISkinCallback cb);//更换皮肤 /**
* Restore skin to app default skin.
*
* @param cb
*/
public void restoreSkin(ISkinCallback cb) ;//恢复应用默认皮肤 /**
* Resume skin.Call it on application started.
*
* @param cb
*/
public void resumeSkin(ISkinCallback cb) ;//恢复当前使用的皮肤,应在应用启动界面调用。
框架支持两种换肤方式:
1.静态换肤(推荐)
换肤完成后,关闭掉所有的Activity,然后重新启动主界面。简单方便。
2.动态换肤
需要换肤的Activity、Fragment、Dialog实现ISkinObserver, 并通过register(ISkinObserver observer)注册到SkinManager,动态更换布局,详情见Sample代码。
这种方式需要重新渲染View,绑定数据,在使用Fragment时,还需要在换肤期间detach/attach fragment,使用起来比较麻烦。优点是换肤后可以停留在原来界面。
两种方式都需要使用SkinManager提供的Resource来获取布局或其他资源。推荐写自己的BaseActivity,重写getResources()返回SkinManager提供的Resources方便使用。小伙伴们根据自己的实际情况来选择具体使用何种方式。
好了,到这里换肤框架就介绍完了,欢迎关注SkinFramework的最新动态,若有任何建议和意见,欢迎指出!
Android可更换布局的换肤方案的更多相关文章
- .NET Web后台动态加载Css、JS 文件,换肤方案
后台动态加载文件代码: //假设css文件:TestCss.css #region 动态加载css文件 public void AddCss() { HtmlGenericControl _CssFi ...
- Android实现apk插件方式换肤
换肤思路: 1.什么时候换肤? xml加载前换肤,如果xml加载后换肤,用户将会看见换肤之前的色彩,用户体验不好. 2.皮肤是什么? 皮肤就是apk,是一个资源包,包含了颜色.图片等. 3.什么样的控 ...
- iOS动画效果合集、飞吧企鹅游戏、换肤方案、画板、文字效果等源码
iOS精选源码 动画知识运用及常见动画效果收集 3D卡片拖拽卡片叠加卡片 iFIERO - FLYING PENGUIN 飞吧企鹅SpriteKit游戏(源码) Swift封装的空数据提醒界面Empt ...
- Android应用换肤总结
换肤,我们都很熟悉,像XP的主题,塞班的主题.看过国外的一些技术博客,就会发现国内和国外对软件的,或者说移动开发的软件的需求的不同.国外用户注重社交.邮件等功能,国内用户则重视音乐.小说.皮肤等功能, ...
- Android主题换肤 无缝切换
2016年7月6日 更新:主题换肤库子项目地址:ThemeSkinning,让app集成换肤更加容易.欢迎star以及使用,提供改进意见. 更新日志: v1.3.0:增加一键切换切换字体(初版)v1. ...
- Android 换肤功能的实现(Apk插件方式)
一.概述 由于Android 没有提供一套统一的换肤机制,我猜可能是因为国外更注重功能和体验的原因 所以国内如果要做一个漂亮的换肤方案,需要自己去实现. 目前换肤的方法大概有三种方案: (1)把皮肤资 ...
- QT自定义精美换肤界面
陆陆续续用QT开发过很多项目,也用QT写过不少私活项目,也写过N个工具,一直梦寐以求能像VC一样可以很方便的有个自定义的界面,QSS的强大让我看到了很好的希望,辗转百度谷歌无数次,一直搜索QT相关的换 ...
- Android App插件式换肤实现方案
背景 目前很多app都具有换肤功能,用户可以根据需要切换不同的皮肤,为使我们的App支持换肤功能,给用户提供更好的体验,在这里对换肤原理进行研究总结,并选择一个合适的换肤解决方案. 换肤介绍 App换 ...
- android 换肤模式总结
由于Android的设置中并没有夜间模式的选项,对于喜欢睡前玩手机的用户,只能简单的调节手机屏幕亮度来改善体验.目前越来越多的应用开始把夜间模式加到自家应用中,没准不久google也会把这项功能添加到 ...
随机推荐
- 初码-Azure系列-如何在控制面板中选择中文版操作系统
之前在文章<初码-Azure系列-记一次从阿里云到Azure的迁移和部署>中说到,默认的Windows Server 2016操作系统是英文版,后来摸索出中文版的方法,如下:
- ArcGIS 网络分析[2.5] VRP(车辆配送)
什么是VRP? VRP就是车辆配送. 大家有没有想象过一个城市的某个快递营业点,是怎么让各个快递员配送快递的? 每个快递员针对那片区域的客户,如何走路线才最省时间? 也许你会说,最短路径分析可以做到— ...
- 【T-SQL】系列文章全文目录(2017-06-02更新)
本系列[T-SQL]主要是针对T-SQL的总结. T-SQL基础 [T-SQL基础]01.单表查询-几道sql查询题 [T-SQL基础]02.联接查询 [T-SQL基础]03.子查询 [T-SQL基础 ...
- MPP 二、Greenplum数据加载
Loading external data into greenplum database table using different ways... Greenplum 有常规的COPY加载方法,有 ...
- iOS地理围栏技术的应用
遇到一个需求,要求监测若干区域,设备进入这些区域则要上传数据,且可以后台监测,甚至app被杀死也要监测.发现oc的地理围栏技术完美匹配这个需求,任务做完了,把遇到的坑记录下来,也许能帮到你呢. 要做这 ...
- python基础操作_文件读写操作
#文件读写# r只能读不能写,且文件必须存在,w只能写不能读,a只能写不能读# w+是写读模式,清空原文件内容# r+是读写模式,没有清空原文件内容,# 只要有r,文件必须存在,只要有w,都会清空原文 ...
- selenium+python定位元素方法
定位元素方法 官网地址:http://selenium-python.readthedocs.org/locating-elements.html 这里有各种策略用于定位网页中的元素(l ...
- TeamViewer——可以实现在手机上随时远程控制你的电脑
小编今天给大家推荐一款强大的远程控制软件——TeamViewer,可以让你的手机连接你自己的电脑,不管你身处何处,只要电脑和手机都能联网,那么你就可以在手机上控制你的电脑了.以下介绍下如何安装和使用方 ...
- JDBC加载数据库驱动的方式
JDBC作为数据库访问的规范接口,其中只是定义一些接口.具体的实现是由各个数据库厂商来完成. 一.重要的接口: 1.public interface Driver 每个驱动程序类必须实现的接口.Jav ...
- asp.net core 教程(七)-异常处理、静态文件
Asp.Net Core-异常处理 Asp.Net Core-异常处理 在这一章,我们将讨论异常和错误处理.当 ASP.NET Core应用程序中发生错误时,您可以以各种不同的方式来处理.让我们来看看 ...