viewRef : tmpList) {
if (null == viewRef || null == viewRef.get()) {
continue;
}
resultList.add(viewRef.get());
}
return resultList;
}
}
================================================
FILE: QSkinLoaderlib/src/main/java/org/qcode/qskinloader/resourceloader/ILoadResourceCallback.java
================================================
package org.qcode.qskinloader.resourceloader;
import org.qcode.qskinloader.IResourceManager;
/**
* 加载皮肤资源的回调
* qqliu
* 2016/9/25.
*/
public interface ILoadResourceCallback {
/***
* 加载皮肤资源开始
*
* @param identifier
*/
void onLoadStart(String identifier);
/***
* 加载皮肤资源成功
*
* @param identifier
* @param result
*/
void onLoadSuccess(String identifier, IResourceManager result);
/***
* 加载皮肤资源失败
*
* @param identifier
* @param errorCode
*/
void onLoadFail(String identifier, int errorCode);
}
================================================
FILE: QSkinLoaderlib/src/main/java/org/qcode/qskinloader/resourceloader/ResourceManager.java
================================================
package org.qcode.qskinloader.resourceloader;
import android.annotation.SuppressLint;
import android.content.Context;
import android.content.res.ColorStateList;
import android.content.res.Resources;
import android.graphics.drawable.Drawable;
import org.qcode.qskinloader.IResourceManager;
import org.qcode.qskinloader.base.utils.Logging;
/**
* 资源管理类的实现,获取资源的行为会代理到其内部的mBase实现
*
* qqliu
* 2016/9/18.
*/
public class ResourceManager implements IResourceManager {
private static final String TAG = "ResourceManager";
private Context mContext;
private Resources mDefaultResources;
private IResourceManager mBase;
private String mSkinIdentifier;
public ResourceManager(Context context) {
mContext = context;
mDefaultResources = mContext.getResources();
}
@Override
public void setBaseResource(String skinIdentifier,
IResourceManager baseResource) {
mSkinIdentifier = skinIdentifier;
mBase = baseResource;
}
@Override
public boolean isDefault() {
if(null != mBase) {
return mBase.isDefault();
}
return true;
}
@Override
public int getColor(int resId) {
if (null != mBase) {
try {
return mBase.getColor(resId);
} catch (Exception ex) {
Logging.d(TAG, "getColor()| error happened", ex);
}
}
return mDefaultResources.getColor(resId);
}
@Override
public int getColor(int resId, String resName) {
if (null != mBase) {
try {
return mBase.getColor(resId, resName);
} catch (Exception ex) {
Logging.d(TAG, "getColor()| error happened", ex);
}
}
return mDefaultResources.getColor(resId);
}
@Override
public ColorStateList getColorStateList(int resId) {
if (null != mBase) {
try {
return mBase.getColorStateList(resId);
} catch (Exception ex) {
Logging.d(TAG, "getColorStateList()| error happened", ex);
}
}
return convertToColorStateList(resId);
}
@Override
public ColorStateList getColorStateList(int resId, String resName) {
if (null != mBase) {
try {
return mBase.getColorStateList(resId, resName);
} catch (Exception ex) {
Logging.d(TAG, "getColorStateList()| error happened", ex);
}
}
return convertToColorStateList(resId);
}
@Override
public ColorStateList getColorStateList(int resId, String typeName, String resName) {
if (null != mBase) {
try {
return mBase.getColorStateList(resId, typeName, resName);
} catch (Exception ex) {
Logging.d(TAG, "getColorStateList()| error happened", ex);
}
}
return convertToColorStateList(resId);
}
public Drawable getDrawable(int resId) {
if (null != mBase) {
try {
return mBase.getDrawable(resId);
} catch (Exception ex) {
Logging.d(TAG, "getDrawable()| error happened", ex);
}
}
return mDefaultResources.getDrawable(resId);
}
@SuppressLint("NewApi")
public Drawable getDrawable(int resId, String resName) {
if (null != mBase) {
try {
return mBase.getDrawable(resId, resName);
} catch (Exception ex) {
Logging.d(TAG, "getDrawable()| error happened", ex);
}
}
return mDefaultResources.getDrawable(resId);
}
/**
* 加载指定资源颜色drawable,转化为ColorStateList,保证selector类型的Color也能被转换。
* 无皮肤包资源返回默认主题颜色
*
* @param resId
* @return
*/
private ColorStateList convertToColorStateList(int resId) {
ColorStateList colorList = null;
try {
colorList = mDefaultResources.getColorStateList(resId);
} catch (Resources.NotFoundException ex) {
Logging.d(TAG, "convertToColorStateList()| error happened", ex);
}
if (colorList != null) {
return colorList;
}
int[][] states = new int[1][1];
colorList =
new ColorStateList(states, new int[]{
mDefaultResources.getColor(resId)});
return colorList;
}
@Override
public String getSkinIdentifier() {
return mSkinIdentifier;
}
}
================================================
FILE: QSkinLoaderlib/src/main/java/org/qcode/qskinloader/resourceloader/impl/APKResourceLoader.java
================================================
package org.qcode.qskinloader.resourceloader.impl;
import android.content.Context;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.content.res.AssetManager;
import android.content.res.Resources;
import android.os.AsyncTask;
import org.qcode.qskinloader.resourceloader.ILoadResourceCallback;
import org.qcode.qskinloader.IResourceLoader;
import org.qcode.qskinloader.base.utils.Logging;
import org.qcode.qskinloader.base.utils.StringUtils;
import java.io.File;
import java.lang.reflect.Method;
/**
* 基于APK方式的资源加载器
* qqliu
* 2016/9/25.
*/
public class APKResourceLoader implements IResourceLoader {
private static final String TAG = "APKResourceLoader";
private Context mContext;
private String mPackageName;
private Resources mResources;
public APKResourceLoader(Context context) {
this.mContext = context;
}
@Override
public void loadResource(final String skinIdentifier,
final ILoadResourceCallback loadCallBack) {
if (StringUtils.isEmpty(skinIdentifier)) {
return;
}
new AsyncTask() {
@Override
protected void onPreExecute() {
if (loadCallBack != null) {
loadCallBack.onLoadStart(skinIdentifier);
}
}
@Override
protected APkLoadResult doInBackground(String... params) {
if (null == mContext || null == params || params.length <= 0) {
return null;
}
try {
String skinPkgPath = params[0];
File file = new File(skinPkgPath);
if (file == null || !file.exists()) {
return null;
}
PackageManager packageManager = mContext.getPackageManager();
PackageInfo packageInfo = packageManager.getPackageArchiveInfo(
skinPkgPath, PackageManager.GET_ACTIVITIES);
String skinPkgName = packageInfo.packageName;
AssetManager assetManager = AssetManager.class.newInstance();
Method addAssetPath = assetManager.getClass().getMethod("addAssetPath", String.class);
addAssetPath.invoke(assetManager, skinPkgPath);
Resources superResources = mContext.getResources();
Resources skinResource = new Resources(
assetManager, superResources.getDisplayMetrics(),
superResources.getConfiguration());
return new APkLoadResult(skinPkgName, skinResource);
} catch (Exception ex) {
Logging.d(TAG, "doInBackground()| exception happened", ex);
}
return null;
}
@Override
protected void onPostExecute(APkLoadResult result) {
if (null != result) {
if (loadCallBack != null) {
loadCallBack.onLoadSuccess(skinIdentifier,
new APKResourceManager(
mContext, result.pkgName, result.resources));
}
} else {
if (loadCallBack != null) {
loadCallBack.onLoadFail(skinIdentifier, -1);
}
}
}
}.execute(skinIdentifier);
}
private static class APkLoadResult {
String pkgName;
Resources resources;
public APkLoadResult(String pkgName, Resources resources) {
this.pkgName = pkgName;
this.resources = resources;
}
}
}
================================================
FILE: QSkinLoaderlib/src/main/java/org/qcode/qskinloader/resourceloader/impl/APKResourceManager.java
================================================
package org.qcode.qskinloader.resourceloader.impl;
import android.annotation.SuppressLint;
import android.content.Context;
import android.content.res.ColorStateList;
import android.content.res.Resources;
import android.graphics.drawable.Drawable;
import org.qcode.qskinloader.IResourceManager;
import org.qcode.qskinloader.base.utils.HashMapCache;
import org.qcode.qskinloader.entity.SkinConstant;
/**
* 基于APK方式的资源管理类
* qqliu
* 2016/9/18.
*/
public class APKResourceManager implements IResourceManager {
private static final String TAG = "APKResourceManager";
private Context mContext;
private Resources mDefaultResources;
private String mPackageName;
private Resources mResources;
private HashMapCache mColorCache
= new HashMapCache(true);
public APKResourceManager(Context context, String pkgName, Resources resources) {
mContext = context;
mDefaultResources = mContext.getResources();
mPackageName = pkgName;
mResources = resources;
}
@Override
public void setBaseResource(String skinIdentifier, IResourceManager baseResource) {
//do nothing
}
@Override
public String getSkinIdentifier() {
return null;
}
@Override
public boolean isDefault() {
return false;
}
@Override
public int getColor(int resId) {
String resName = mDefaultResources.getResourceEntryName(resId);
return getColor(resId, resName);
}
@Override
public int getColor(int resId, String resName) {
String resKey = getResKey(mPackageName, resName);
Integer color = mColorCache.getCache(resKey);
if (null != color) {
return color;
}
int trueResId = mResources.getIdentifier(
resName, SkinConstant.RES_TYPE_NAME_COLOR, mPackageName);
int trueColor = mResources.getColor(trueResId);
mColorCache.addCache(resKey, trueColor);
return trueColor;
}
public Drawable getDrawable(int resId) {
String resName = mDefaultResources.getResourceEntryName(resId);
return getDrawable(resId, resName);
}
@SuppressLint("NewApi")
public Drawable getDrawable(int resId, String resName) {
int trueResId = mResources.getIdentifier(resName,
SkinConstant.RES_TYPE_NAME_DRAWABLE, mPackageName);
if (0 == trueResId) {
trueResId = mResources.getIdentifier(resName,
SkinConstant.RES_TYPE_NAME_MIPMAP, mPackageName);
if (0 == trueResId) {
throw new Resources.NotFoundException(resName);
}
}
Drawable trueDrawable;
if (android.os.Build.VERSION.SDK_INT < 22) {
trueDrawable = mResources.getDrawable(trueResId);
} else {
trueDrawable = mResources.getDrawable(trueResId, null);
}
return trueDrawable;
}
@Override
public ColorStateList getColorStateList(int resId) {
String resName = mDefaultResources.getResourceEntryName(resId);
return getColorStateList(resId, resName);
}
/**
* 读取ColorStateList
* @param resId
* @return
*/
@Override
public ColorStateList getColorStateList(int resId, String resName) {
return getColorStateList(resId, SkinConstant.RES_TYPE_NAME_COLOR, resName);
}
@Override
public ColorStateList getColorStateList(int resId, String typeName, String resName) {
int trueResId = mResources.getIdentifier(
resName, typeName, mPackageName);
ColorStateList colorList = mResources.getColorStateList(trueResId);
return colorList;
}
private String getResKey(String skinPackageName, String resName) {
return (null == skinPackageName ? "" : skinPackageName) + "_" + resName;
}
}
================================================
FILE: QSkinLoaderlib/src/main/java/org/qcode/qskinloader/resourceloader/impl/ConfigChangeResourceLoader.java
================================================
package org.qcode.qskinloader.resourceloader.impl;
import android.content.Context;
import android.content.res.Configuration;
import android.content.res.Resources;
import android.util.DisplayMetrics;
import org.qcode.qskinloader.IResourceLoader;
import org.qcode.qskinloader.base.utils.StringUtils;
import org.qcode.qskinloader.resourceloader.ILoadResourceCallback;
/**
* 基于Android原生夜间模式的ConfigChange方式的资源加载器
* qqliu
* 2016/10/9.
*/
public class ConfigChangeResourceLoader implements IResourceLoader {
private static final String TAG = "ConfigChangeResourceLoader";
public static final String MODE_DAY = "day";
public static final String MODE_NIGHT = "night";
private Context mContext;
public ConfigChangeResourceLoader(Context context) {
this.mContext = context;
}
@Override
public void loadResource(final String skinIdentifier,
final ILoadResourceCallback loadCallBack) {
if (StringUtils.isEmpty(skinIdentifier)) {
return;
}
if (loadCallBack != null) {
loadCallBack.onLoadStart(skinIdentifier);
}
switchMode(MODE_NIGHT.equals(skinIdentifier));
if (loadCallBack != null) {
loadCallBack.onLoadSuccess(skinIdentifier,
new ConfigChangeResourceManager(mContext, skinIdentifier));
}
}
private void switchMode(boolean isNightMode) {
Resources resources = mContext.getResources();
DisplayMetrics dm = resources.getDisplayMetrics();
Configuration config = resources.getConfiguration();
config.uiMode &= ~Configuration.UI_MODE_NIGHT_MASK;
config.uiMode |= isNightMode ?
Configuration.UI_MODE_NIGHT_YES :
Configuration.UI_MODE_NIGHT_NO;
resources.updateConfiguration(config, dm);
}
}
================================================
FILE: QSkinLoaderlib/src/main/java/org/qcode/qskinloader/resourceloader/impl/ConfigChangeResourceManager.java
================================================
package org.qcode.qskinloader.resourceloader.impl;
import android.annotation.SuppressLint;
import android.content.Context;
import android.content.res.ColorStateList;
import android.content.res.Resources;
import android.graphics.drawable.Drawable;
import org.qcode.qskinloader.IResourceManager;
/**
* 基于资Android原生夜间模式的ConfigChange方式的资源管理类
* qqliu
* 2016/10/9.
*/
public class ConfigChangeResourceManager implements IResourceManager {
private static final String TAG = "ConfigChangeResourceManager";
private Context mContext;
private String mSkinIdentifier;
private Resources mResources;
public ConfigChangeResourceManager(Context context, String skinIdentifier) {
mContext = context;
mResources = mContext.getResources();
mSkinIdentifier = skinIdentifier;
}
@Override
public void setBaseResource(String skinIdentifier, IResourceManager baseResource) {
//do nothing
}
@Override
public String getSkinIdentifier() {
return mSkinIdentifier;
}
@Override
public boolean isDefault() {
return ConfigChangeResourceLoader.MODE_DAY.equals(mSkinIdentifier);
}
@Override
public int getColor(int resId) {
return mResources.getColor(resId);
}
@Override
public int getColor(int resId, String resName) {
return mResources.getColor(resId);
}
public Drawable getDrawable(int resId) {
return mResources.getDrawable(resId);
}
@SuppressLint("NewApi")
public Drawable getDrawable(int resId, String resName) {
return mResources.getDrawable(resId);
}
@Override
public ColorStateList getColorStateList(int resId) {
return mResources.getColorStateList(resId);
}
@Override
public ColorStateList getColorStateList(int resId, String resName) {
return mResources.getColorStateList(resId);
}
@Override
public ColorStateList getColorStateList(int resId, String typeName, String resName) {
return mResources.getColorStateList(resId);
}
}
================================================
FILE: QSkinLoaderlib/src/main/java/org/qcode/qskinloader/resourceloader/impl/SuffixResourceLoader.java
================================================
package org.qcode.qskinloader.resourceloader.impl;
import android.content.Context;
import org.qcode.qskinloader.IResourceLoader;
import org.qcode.qskinloader.base.utils.StringUtils;
import org.qcode.qskinloader.resourceloader.ILoadResourceCallback;
/**
* 基于资源名称后缀拼接方式的资源加载器
* qqliu
* 2016/10/9.
*/
public class SuffixResourceLoader implements IResourceLoader {
private static final String TAG = "SuffixResourceLoader";
private Context mContext;
private String mSkinSuffix;
public SuffixResourceLoader(Context context) {
this.mContext = context;
}
@Override
public void loadResource(final String skinIdentifier,
final ILoadResourceCallback loadCallBack) {
if (StringUtils.isEmpty(skinIdentifier)) {
return;
}
if (loadCallBack != null) {
loadCallBack.onLoadStart(skinIdentifier);
}
mSkinSuffix = skinIdentifier;
if (loadCallBack != null) {
loadCallBack.onLoadSuccess(skinIdentifier,
new SuffixResourceManager(mContext, mSkinSuffix));
}
}
}
================================================
FILE: QSkinLoaderlib/src/main/java/org/qcode/qskinloader/resourceloader/impl/SuffixResourceManager.java
================================================
package org.qcode.qskinloader.resourceloader.impl;
import android.annotation.SuppressLint;
import android.content.Context;
import android.content.res.ColorStateList;
import android.content.res.Resources;
import android.graphics.drawable.Drawable;
import org.qcode.qskinloader.IResourceManager;
import org.qcode.qskinloader.base.utils.HashMapCache;
import org.qcode.qskinloader.entity.SkinConstant;
/**
* 基于资源名称后缀拼接方式的资源管理类
* qqliu
* 2016/10/9.
*/
public class SuffixResourceManager implements IResourceManager {
private static final String TAG = "SuffixResourceManager";
private Context mContext;
private Resources mResources;
private String mSkinSuffix;
private String mPackageName;
private HashMapCache mColorCache
= new HashMapCache(true);
public SuffixResourceManager(Context context, String skinSuffix) {
mContext = context;
mPackageName = mContext.getPackageName();
mResources = mContext.getResources();
mSkinSuffix = skinSuffix;
}
@Override
public void setBaseResource(String skinIdentifier, IResourceManager baseResource) {
//do nothing
}
@Override
public String getSkinIdentifier() {
return mSkinSuffix;
}
@Override
public boolean isDefault() {
return false;
}
@Override
public int getColor(int resId) {
String resName = mResources.getResourceEntryName(resId);
return getColor(resId, resName);
}
@Override
public int getColor(int resId, String resName) {
Integer color = mColorCache.getCache(resName);
if (null != color) {
return color;
}
String trueResName = appendSuffix(resName);
int trueResId = mResources.getIdentifier(
trueResName,
SkinConstant.RES_TYPE_NAME_COLOR,
mPackageName);
int trueColor = mResources.getColor(trueResId);
mColorCache.addCache(trueResName, trueColor);
return trueColor;
}
public Drawable getDrawable(int resId) {
String resName = mResources.getResourceEntryName(resId);
return getDrawable(resId, resName);
}
@SuppressLint("NewApi")
public Drawable getDrawable(int resId, String resName) {
String trueResName = appendSuffix(resName);
int trueResId = mResources.getIdentifier(
trueResName,
SkinConstant.RES_TYPE_NAME_DRAWABLE,
mPackageName);
if (0 == trueResId) {
trueResId = mResources.getIdentifier(
trueResName,
SkinConstant.RES_TYPE_NAME_MIPMAP,
mPackageName);
if (0 == trueResId) {
throw new Resources.NotFoundException(resName);
}
}
Drawable trueDrawable;
if (android.os.Build.VERSION.SDK_INT < 22) {
trueDrawable = mResources.getDrawable(trueResId);
} else {
trueDrawable = mResources.getDrawable(trueResId, null);
}
return trueDrawable;
}
@Override
public ColorStateList getColorStateList(int resId) {
String resName = mResources.getResourceEntryName(resId);
return getColorStateList(resId, resName);
}
/**
* 加载指定资源颜色drawable,转化为ColorStateList,保证selector类型的Color也能被转换。
* 无皮肤包资源返回默认主题颜色
*
* @param resId
* @return
*/
@Override
public ColorStateList getColorStateList(int resId, String resName) {
return getColorStateList(resId, SkinConstant.RES_TYPE_NAME_COLOR, resName);
}
@Override
public ColorStateList getColorStateList(int resId, String typeName, String resName) {
String trueResName = appendSuffix(resName);
int trueResId = mResources.getIdentifier(
trueResName,
typeName,
mPackageName);
ColorStateList colorList = mResources.getColorStateList(trueResId);
return colorList;
}
private String appendSuffix(String resName) {
return resName + mSkinSuffix;
}
}
================================================
FILE: QSkinLoaderlib/src/main/java/org/qcode/qskinloader/view/ShadowImageView.java
================================================
package org.qcode.qskinloader.view;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.PorterDuff;
import android.graphics.PorterDuffColorFilter;
import android.graphics.drawable.Drawable;
import android.util.AttributeSet;
import android.widget.ImageView;
import java.lang.ref.WeakReference;
/**
* 支持蒙层的ImageView,建议使用此类作为蒙层处理的ImageView的替代,
* 给ImageView设置ColorFilter可能会失败,此类为drawable设置蒙层,效果更好点。
*
* qqliu
* 2016/9/28.
*/
public class ShadowImageView extends ImageView {
private PorterDuffColorFilter mColorFilter;
private boolean hasFilterSet = true;
private WeakReference mSetFilteredDrawable = null;
public ShadowImageView(Context context) {
this(context, null);
}
public ShadowImageView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public ShadowImageView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
public void setShadowColor(int color) {
hasFilterSet = false;
mColorFilter = new PorterDuffColorFilter(color, PorterDuff.Mode.MULTIPLY);
invalidate();
}
@Override
protected void onDraw(Canvas canvas) {
Drawable drawable = getDrawable();
if(null != mSetFilteredDrawable && drawable != mSetFilteredDrawable.get()) {
//drawable 发生了变化,重新设置filter
hasFilterSet = false;
}
if(!hasFilterSet) {
if (null != drawable) {
drawable.setColorFilter(mColorFilter);
mSetFilteredDrawable = new WeakReference(drawable);
hasFilterSet = true;
}
}
super.onDraw(canvas);
}
}
================================================
FILE: QSkinLoaderlib/src/main/res/values/skin_attrs.xml
================================================
================================================
FILE: QSkinLoaderlib/src/main/res/values/skin_ids.xml
================================================
================================================
FILE: QSkinLoaderlib/src/main/res/values/strings.xml
================================================
QSkinLoaderLib
================================================
FILE: README.md
================================================
# QSkinLoader换肤框架
**如何在一个成熟的应用内换肤?** 请参见文章:[链接](http://blog.csdn.net/u013478336/article/details/78993969)。
**README分三部分:基本简介、使用方法、框架由来与架构设计。
如果不嫌麻烦,还可以去看文章[夜间模式方案调研](http://blog.csdn.net/u013478336/article/details/52484322)和[QSkinLoader框架介绍](http://blog.csdn.net/u013478336/article/details/53083054)**
#**效果图**

#**基本简介:**
QSkinLoader是一个支持多种场景的Android换肤框架。基本原理是通过代理LayoutInflater的View创建过程解析皮肤相关属性(background/src/textColor等),将皮肤相关属性设置到View的Tag内,在切换皮肤时寻找对应的皮肤来完成实时刷新动作。此方案具有代码及XML侵入性小、功能完善(支持Activity/Dialog/悬浮窗/PopWindow等)、无需重启Activity、支持自定义属性换肤、同时支持资源内换肤和独立资源包(下载后换肤)等优点。
#**使用方法**
## 基本使用
由于可以自定义皮肤资源加载过程,QSkinLoader框架内并未提供当前皮肤的保存逻辑(不能支持loadCurrentSkin之类的接口)。因此建议使用框架时封装两个类:一个负责保存当前皮肤(保存皮肤其实就是SharePreference持久化存储,此处略去),一个负责与框架交互,如下:
```Java
public void init(Context context) {
SkinManager.getInstance().init(context);
}
public void switchSkinMode(OnSkinChangeListener listener) {
mIsSwitching = true;
mIsDefaultMode = !mIsDefaultMode;
refreshSkin(listener);
}
public void refreshSkin(OnSkinChangeListener listener) {
if (mIsDefaultMode) {
//恢复到默认皮肤
SkinManager.getInstance().restoreDefault(SkinConstant.DEFAULT_SKIN, new MyLoadSkinListener(listener));
} else {
changeSkin(listener);
}
}
private void changeSkin(OnSkinChangeListener listener) {
SkinManager.getInstance().loadSkin("_night",
new SuffixResourceLoader(mContext),
new MyLoadSkinListener(listener));
}
```
具体代码此处不完全贴出了,工程内有详细的代码。
###1. 框架初始化
在Application创建过程中执行框架的初始化:
```Java
// 初始化皮肤框架
SkinChangeHelper.getInstance().init(this);
//初始化上次缓存的皮肤
SkinChangeHelper.getInstance().refreshSkin(null);
```
初始化了框架后需要调用refreshSkin来加载上一次的皮肤设置,refreshSkin加载完成皮肤后会通知各Activity界面刷新皮肤设置,由于此处在Application初始化时调用,可能加载完成皮肤设置后界面仍未初始化,这并不无影响,因为Activity初始化时会主动执行一次换肤操作,弥补此过程的缺失。
###2. Activity初始化与各生命周期调用
因为换肤一般是整个应用都需要执行的过程,此处建议实现一个基础类(BaseActivity)来封装换肤相关逻辑,此类建议实现接口ISkinActivity,告知是否支持换肤,以及在换肤操作触发后如果界面不在前台是否立刻换肤:
```Java
@Override
public boolean isSupportSkinChange() {
//告知当前界面是否支持换肤:true支持换肤,false不支持
return true;
}
@Override
public boolean isSwitchSkinImmediately() {
//告知当切换皮肤时,是否立刻刷新当前界面;true立刻刷新,false表示在界面onResume时刷新;
//减轻换肤时性能压力
return false;
}
@Override
public void handleSkinChange() {
//当前界面在换肤时收到的回调,可以在此回调内做一些其他事情;
//比如:通知WebView内的页面切换到夜间模式等
}
```
然后在Activity的onCreate中执行IActivitySkinEventHandler的初始化与配置工作:
```Java
//初始化并配置IActivitySkinEventHandler,应在IActivitySkinEventHandler.onCreate之前执行
mSkinEventHandler = SkinManager.newActivitySkinEventHandler()
.setSwitchSkinImmediately(isSwitchSkinImmediately())
.setSupportSkinChange(isSupportSkinChange())
.setWindowBackgroundResource(getWindowBackgroundResource())
.setNeedDelegateViewCreate(false);
//通知框架onCreate事件
mSkinEventHandler.onCreate(this);
```
其中,**setWindowBackgroundResource**用于设置Activity的背景色,在换肤时会寻找对应的背景色替换之,此处传入的不能是色值,只支持引用,类似R.color.xx。
**setNeedDelegateViewCreate**用于设置是否需要代理View创建,因为LayoutInflater的代理View创建Factory只支持设置一次,如果外部已经设置了Factory,则框架内再次设置会引起崩溃,所以框架使用配置与回调来处理此问题。具体见高级使用部分。
其他生命周期回调基本类似,挑两个做实例,如下:
```Java
@Override
protected void onResume() {
super.onResume();
//皮肤相关,此通知放在此处,尽量让子类的view都添加到view树内
if (mFirstTimeApplySkin) {
mSkinEventHandler.onViewCreated();
mFirstTimeApplySkin = false;
}
//皮肤相关
mSkinEventHandler.onResume();
}
@Override
public void onWindowFocusChanged(boolean hasFocus) {
super.onWindowFocusChanged(hasFocus);
//皮肤相关
mSkinEventHandler.onWindowFocusChanged(hasFocus);
}
```
###3. XML配置
QSkinLoader只支持引用型资源换肤,所有的颜色定义都应定义在colors.xml内,在使用时引用。
对于一个布局,需要定义一个skin的命名空间:
```XML
xmlns:skin="http://schemas.android.com/android/skin"
```
然后对所有需要换肤的View增加属性:
```XML
skin:enable="true"
```
即可完成换肤配置。举例如下:
```XML
```
在这段布局内,框架代理创建Linearlayout时会解析其background属性,代理创建View时不解析任何属性,代理创建TextView时会解析textColor属性。
###4. 图片蒙层
对ImageView/ImageButton可以配置属性:
```XML
skin:drawShadow="@color/night_shadow_color"
```
来支持图片蒙层,night_shadow_color是一个颜色引用,在默认情况下建议使用透明色,同时在皮肤包内定义此值为另一个色值(不必须是半透明色)。
需要注意的是:蒙层的原理是ImageView的ColorFilter,有时候对ImageView设置ColorFilter会失效。但是对Drawable设置ColorFilter基本都会生效,所以如果是对ImageView的Src属性做蒙层,建议使用框架内的ShadowImageView替代ImageView。如下:
```XML
```
###5. 换肤与恢复默认皮肤
**先来看看换肤操作:**
```Java
SkinManager.getInstance().loadSkin("_night",
new SuffixResourceLoader(mContext),
new MyLoadSkinListener(listener));
```
这是基于资源后缀的换肤方式,对于R.color.color_text,切换到夜间模式时,框架会去找R.color.color_text_night作为夜间模式的资源。
QSkinLoader换肤框架还支持另一种默认的换肤方式——APK资源换肤,也就是将资源文件定义在独立的APK文件内,此文件可从服务端下载,从而真正实现动态换肤。此方式对现有工程的影响比较小,非常值得推荐。具体方式如下:
```Java
SkinUtils.copyAssetSkin(mContext);
File skin = new File(SkinUtils.getTotalSkinPath(mContext));
SkinManager.getInstance().loadAPKSkin(
skin.getAbsolutePath(), new MyLoadSkinListener(listener));
```
当然也可以写成:
```Java
SkinManager.getInstance().loadSkin(skin.getAbsolutePath(),
new APKResourceLoader(mContext),
new MyLoadSkinListener(listener));
```
此时对于资源R.color.color_text,框架会去skin路径的APK文件内寻找对于的资源R.color.color_text,找不到就继续使用当前应用的color_text资源。
自定义皮肤加载过程见高级使用部分。
**那么怎么恢复默认皮肤呢?**
```Java
//恢复到默认皮肤
SkinManager.getInstance().restoreDefault(SkinConstant.DEFAULT_SKIN,
new MyLoadSkinListener(listener));
```
DEFAULT_SKIN值对框架而言并无意义,框架只是把此值回调到ILoadSkinListener使外部知道当前加载的是默认皮肤,所以此值是在**框架外**定义的。
###6. 动态创建View的皮肤设置
上文中指出的使用方式是基于XML配置的,如果是在Java代码内如何使用呢?
QSkinLoader框架提供了一个帮助类ISkinViewHelper来添加/删除View的皮肤属性。此类设计为链式编程方式,提供的接口有:
```Java
ISkinViewHelper setViewAttrs(String attrName, int resId);
ISkinViewHelper setViewAttrs(DynamicAttr... dynamicAttrs);
ISkinViewHelper setViewAttrs(SkinAttr... skinAttrs);
ISkinViewHelper setViewAttrs(List dynamicAttrs);
ISkinViewHelper addViewAttrs(String attrName, int resId);
ISkinViewHelper addViewAttrs(DynamicAttr... dynamicAttrs);
ISkinViewHelper addViewAttrs(SkinAttr... skinAttrs);
ISkinViewHelper addViewAttrs(List dynamicAttrs);
ISkinViewHelper cleanAttrs(boolean clearChild);
void applySkin(boolean applyChild);
```
如果View本身已经有了皮肤属性,setViewAttrs接口会替换已有的皮肤属性,而addViewAttrs不会覆盖已有属性,而是在已有的皮肤属性内添加新的属性。
cleanAttrs会清除View的所有皮肤属性,如果传入clearChild为true则遍历所有子元素清除皮肤属性,false只清除自身属性。
applySkin则对当前View应用皮肤设置,如果传入applyChild为true则遍历所有子元素应用皮肤,false只应用自身。
假设对一个TextView,动态设置View的皮肤属性大致如下:
```Java
SkinManager
.with(textview)
.setViewAttrs(SkinAttrName.BACKGROUND, R.color.white)
.addViewAttrs(SkinAttrName.TEXT_COLOR, R.color.black)
.applySkin(false);
```
所有框架支持的属性名称都定义在SkinAttrName内,如果需要扩展属性支持,建议参考自定义View属性处理器部分。
###7. 特定View的换肤管理
上面的换肤过程都是对Activity的View树做遍历换肤操作的,树根是:
```Java
activity.findViewById(android.R.id.content);
```
所有不在这颗树内的View都不能换肤,哪些View不在换肤范围呢?
Dialog的View、popWindow的View、悬浮窗(WindowManager上直接加View),目前这三类View要换肤都应该使用特定View的换肤管理模块。
需要注意的是:Dialog的交互具有排他型,通常在换肤操作时是不展示的,所以一般可以在show接口调用时做换肤,而不使用IWindowViewManager。
**怎么对特定View进行换肤管理呢?**
框架提供了IWindowViewManager接口来提供特定View的管理,支持链式编程,接口如下:
```Java
IWindowViewManager addWindowView(View view);
IWindowViewManager removeWindowView(View view);
IWindowViewManager clear();
void applySkinForViews(boolean applyChild);
List getWindowViewList();
```
接口比较简单,主要是增加/删除/清空全局View列表和应用皮肤的操作。
使用如下:
```Java
View popView = LayoutInflater.from(mContext).inflate(
R.layout.news_list_item_pop, null);
SkinManager.getInstance().applySkin(popView, true);
SkinManager
.getWindowViewManager()
.addWindowView(popView);
popupWindow = new PopupWindow(popView, popWidth, popHeight);
```
通常不建议使用applySkinForViews接口,因为它会遍历所有全局View列表的View做遍历,所以替代方式是先对当前View做属性设置,再添加到框架内管理,从而在下次换肤时接口换肤事件。
IWindowViewManager内的View是弱引用存储的,所以不会发生内存泄露,但建议在View无用的时候从框架内移除特定View。
## 高级使用
###1. 自定义View属性处理器
当项目需要自定义View时,一般都会自定义一些属性,这些属性框架是不支持换肤的,此时需要自定义属性处理器并注册到框架内。自定义属性处理器实现接口ISkinAttrHandler,实现方法:
```Java
void apply(View view,
SkinAttr skinAttr,
IResourceManager resourceManager);
```
下面是一个示例:
若有一个自定义CustomTextView,使用属性defTextColor来定义文字颜色,如下:
```XML
```
则其自定义属性处理器为:
```Java
public class DefTextColorAttrHandler implements ISkinAttrHandler {
public static final String DEF_TEXT_COLOR = "defTextColor";
@Override
public void apply(View view, SkinAttr skinAttr, IResourceManager resourceManager) {
if(!(view instanceof CustomTextView)) {
//防止在错误的View上设置了此属性
return;
}
CustomTextView tv = (CustomTextView) view;
if (RES_TYPE_NAME_COLOR.equals(skinAttr.mAttrValueTypeName)) {
if (SkinConstant.RES_TYPE_NAME_COLOR.equals(
skinAttr.mAttrValueTypeName)) {
try {
//先尝试按照int型颜色解析
int textColor = resourceManager.getColor(
skinAttr.mAttrValueRefId,
skinAttr.mAttrValueRefName);
tv.setTextColor(textColor);
} catch (Resources.NotFoundException ex) {
//不是int型则按照ColorStateList引用来解析
ColorStateList textColor =
resourceManager.getColorStateList(
skinAttr.mAttrValueRefId,
skinAttr.mAttrValueRefName);
tv.setTextColor(textColor);
}
}
}
}
}
```
定义了属性处理器后,再注册到框架内,**注册需要在setContentView之前**:
```Java
SkinManager.getInstance().registerSkinAttrHandler(
DEF_TEXT_COLOR, new DefTextColorAttrHandler());
```
**注意:**自定义属性处理器不一定就是与皮肤相关的属性的处理,也可以是换肤过程中需要对View进行的特定处理。比如RecyclerView换肤的时候要清除内部View缓存(因为其onBindViewHolder不是每次子View显示时都回调),此时,可以定义如下的属性处理器:
```Java
class RecyclerViewClearSubAttrHandler implements ISkinAttrHandler {
@Override
public void apply(View view, SkinAttr skinAttr, IResourceManager resourceManager) {
Field declaredField = view.getDeclaredField("mRecycler");
......
RecyclerView.RecycledViewPool recycledViewPool = recyclerView.getRecycledViewPool();
recycledViewPool.clear();
}
```
此处代码有删减,具体见框架内的RecyclerViewClearSubAttrHandler处理器。使用时如下:
```Java
SkinAttr clearSubAttr = new SkinAttr(SkinAttrName.CLEAR_RECYCLER_VIEW);
SkinManager
.with(view)
.addViewAttrs(clearSubAttr);
```
###2. 自定义皮肤资源加载
框架默认支持**资源名称后缀加载、APK加载、Android UIMode Configuration变化**三种换肤方式。集成方式如下:
```Java
//后缀加载
SkinManager.getInstance().loadSkin("_night",
new SuffixResourceLoader(mContext),
new MyLoadSkinListener(listener));
//APK皮肤包
SkinManager.getInstance().loadAPKSkin(
skin.getAbsolutePath(),
new MyLoadSkinListener(listener));
//Android UI Configuration变化
SkinManager.getInstance().loadSkin(
ConfigChangeResourceLoader.MODE_NIGHT,
new ConfigChangeResourceLoader(mContext),
new MyLoadSkinListener(listener));
```
如果项目准备采用其他的加载方式,可以通过自定义皮肤资源加载过程来实现。自定义皮肤资源加载的核心是实现IResourceLoader接口,接口只有一个方法:
```Java
void loadResource(String skinIdentifier,
ILoadResourceCallback loadCallBack);
```
也就是定义了从皮肤标识符skinIdentifier加载资源,并在加载过程中通过loadCallBack对外通知加载过程:
```Java
public interface ILoadResourceCallback {
void onLoadStart(String identifier);
void onLoadSuccess(String identifier, IResourceManager result);
void onLoadFail(String identifier, int errorCode);
}
```
加载开始和失败没啥可说的,主要是加载完成后,需要返回一个资源管理类IResourceManager。这个类定义了如何从指定的换肤流程中抽取对应的皮肤资源:
```Java
public interface IResourceManager {
String getSkinIdentifier();
Drawable getDrawable(int resId, String resName) throws Resources.NotFoundException;
int getColor(int resId, String resName) throws Resources.NotFoundException;
ColorStateList getColorStateList(int resId, String resName) throws Resources.NotFoundException;
}
```
整个过程比较简单,自定义一个加载过程,再返回一个资源管理类即可。下面以后缀资源加载的方式做个示例(摘录部分代码,具体见工程):
```Java
public class SuffixResourceLoader implements IResourceLoader {
private String mSkinSuffix;
@Override
public void loadResource(final String skinIdentifier,
final ILoadResourceCallback loadCallBack) {
//通知加载开始
loadCallBack.onLoadStart(skinIdentifier);
//后缀存下,加载过程就结束了,不像apk加载,还需要操作AssetManager
mSkinSuffix = skinIdentifier;
//通知加载结束,返回一个资源管理类SuffixResourceManager
loadCallBack.onLoadSuccess(skinIdentifier,
new SuffixResourceManager(mContext, mSkinSuffix));
}
}
```
```Java
public class SuffixResourceManager implements IResourceManager {
private Context mContext;
private Resources mResources;
private String mSkinSuffix;
private String mPackageName;
private HashMapCache mColorCache
= new HashMapCache(true);
public SuffixResourceManager(Context context, String skinSuffix) {
mContext = context;
mPackageName = mContext.getPackageName();
mResources = mContext.getResources();
mSkinSuffix = skinSuffix;
}
@Override
public int getColor(int resId, String resName) {
String trueResName = resName + mSkinSuffix;
//找到名字+后缀的id,读取颜色
int trueResId = mResources.getIdentifier(
trueResName,
SkinConstant.RES_TYPE_NAME_COLOR,
mPackageName);
int trueColor = mResources.getColor(trueResId);
return trueColor;
}
......
}
```
###3.解决与其他代理View创建过程的冲突
上文也简要的提到了此问题,对每个Activity的LayoutInflater的setFactory接口(代理View创建与属性解析)只能调用一次,而换肤框架是依赖此操作来完成皮肤属性解析的,因此我们需要设计一套方案在确保框架外已经代理了LayoutInflater后还能保证换肤功能的可用性。
我们需要保证两点:
- 如果框架外需要代理View创建,则框架应被告知不能代理View创建,并且提供一个帮助类在外部创建View创建时完成属性解析;
- 如果框架外不需要代理View创建,但需要解析属性,则提供接口在View创建前后对外回调;
对于第一点,可以通过IActivitySkinEventHandler.setNeedDelegateViewCreate来告知框架不代理View创建,解析属性的帮助类也可以从IActivitySkinEventHandler内取到,如下:
```Java
LayoutInflater.from(this).setFactory(new LayoutInflater.Factory() {
@Override
public View onCreateView(String name, Context context, AttributeSet attrs) {
View view = createView(name, context, attrs);
//创建View后通知框架解析属性
ISkinAttributeParser parser =
mSkinEventHandler.getSkinAttributeParser();
if (parser.isSupportSkin(name, context, attrs)) {
parser.parseAttribute(view, name, context, attrs);
}
return view;
}
});
```
核心代码就是这段解析属性的逻辑。
对于第二点,我们提供接口IViewCreateListener来监听View创建过程:
```Java
public interface IViewCreateListener {
View beforeCreate(String name, Context context, AttributeSet attrs);
void afterCreated(View view, String name, Context context, AttributeSet attrs);
}
```
beforeCreate在View创建之前执行,可以拦截框架的View创建过程,自己创建View,afterCreated在框架创建View后执行,用于框架外进一步处理。
此接口应通过IActivitySkinEventHandler.setViewCreateListener()设置到框架内使用。
##- 各种View的换肤应用
###1. ViewPager
ViewPager使用时,应在PagerAdapter的instantiateItem回调中对创建的View应用当前的皮肤。
```Java
mViewPager.setAdapter(new PagerAdapter() {
......
@Override
public Object instantiateItem(ViewGroup container, int position) {
View view = onCreatePagerView(position);
container.addView(view);
//每次实例化某个View时都对其应用皮肤设置
SkinManager.with(view).applySkin(true);
return view;
}
});
```
###2. ListView/GridView
ListView/GridView都继承AbsListView,并使用BaseAdapter作为适配器,其换肤方法为:
```Java
listView.setAdapter(new BaseAdapter() {
@Override
public View getView(int position, View convertView, ViewGroup parent) {
if(null == convertView) {
convertView = onCreateContentView(position);
}
//每次某个子元素需要展示时,都应用当前皮肤设置
SkinManager.with(convertView).applySkin(true);
return convertView;
}
});
```
需要注意的是,如果ListView存在HeaderView或FooterView时,只使用上面的方法是不完善的,如果换肤时HeaderView/FooterView不在ListView内展示,则换肤失效,此时应调用ListView.mRecycler.clear()方法清除View缓存,具体见[上一篇文章](http://blog.csdn.net/u013478336/article/details/52484322)。
###3. RecyclerView
上一章也大致讲了RecyclerView换肤的注意事项,由于RecyclerView滑动时,其子元素出现的过程不一定会伴有onBindViewHolder回调,导致我们有时出现两种皮肤并存的问题。因此,使用RecyclerView时换肤一定要清除RecyclerView的缓存。
```Java
recyclerView.setAdapter(new RecyclerView.Adapter() {
......
@Override
public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {
//此回调非必执行,但是执行时还是要应用皮肤设置
SkinManager.with(holder.itemView).applySkin(true);
}
});
```
为了应对RecyclerView清除缓存的问题,框架内定义了一个特殊的属性处理器RecyclerViewClearSubAttrHandler,其作用就是在换肤时,清除RecyclerView内的View缓存。具体使用方式如下:
```Java
SkinManager
.with(recyclerView)
.addViewAttrs(new SkinAttr(SkinAttrName.CLEAR_RECYCLER_VIEW));
```
#附录
此框架脱胎于项目需要实现夜间模式的需求,在[文章](http://blog.csdn.net/u013478336/article/details/52484322)中,我们列举了常见的几种实现夜间模式切换的方案,并大致对比了一下各种方案的优缺点,此处不再一一列举。仅大致摘录夜间模式的需求分析如下:
>夜间模式需要对屏幕上的文字/图片/视频三种表现形式做特殊处理,具体细化如下:
>**1)对界面背景,**白色等浅色背景应该变成黑色/灰色之类的深色背景,以此降低屏幕亮度减少视觉刺激;
>**2)对文字,**因背景色变深,文字颜色需变浅,以形成对比效果;
>**3)对图片,**对图片加蒙层,避免加载浅色图片带来的视觉刺激;
>**4)对视频,**通常在播放界面增加亮度变化功能,由用户来决定屏幕亮度。
具体技术选型与框架设计可见文章:http://blog.csdn.net/u013478336/article/details/53083054
================================================
FILE: SkinProject/.gitignore
================================================
*.iml
.gradle
/local.properties
/.idea/workspace.xml
/.idea/libraries
.DS_Store
/build
/captures
.externalNativeBuild
================================================
FILE: SkinProject/app/.gitignore
================================================
/build
================================================
FILE: SkinProject/app/build.gradle
================================================
apply plugin: 'com.android.application'
android {
compileSdkVersion 23
buildToolsVersion "23.0.1"
defaultConfig {
applicationId "org.qcode.skinproject"
minSdkVersion 14
targetSdkVersion 23
versionCode 1
versionName "1.0"
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
}
}
dependencies {
compile fileTree(dir: 'libs', include: ['*.jar'])
androidTestCompile('com.android.support.test.espresso:espresso-core:2.2.2', {
exclude group: 'com.android.support', module: 'support-annotations'
})
compile 'com.android.support:appcompat-v7:23.2.1'
testCompile 'junit:junit:4.12'
}
================================================
FILE: SkinProject/app/proguard-rules.pro
================================================
# Add project specific ProGuard rules here.
# By default, the flags in this file are appended to flags specified
# in E:\Program Files\Android\android-sdk/tools/proguard/proguard-android.txt
# You can edit the include path and order by changing the proguardFiles
# directive in build.gradle.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
# Add any project specific keep options here:
# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}
================================================
FILE: SkinProject/app/src/main/AndroidManifest.xml
================================================
================================================
FILE: SkinProject/app/src/main/java/org/qcode/skinproject/MainActivity.java
================================================
package org.qcode.skinproject;
import android.app.Activity;
/**
* qqliu
* 2016/11/9.
*/
public class MainActivity extends Activity {
}
================================================
FILE: SkinProject/app/src/main/res/drawable/btn_bg.xml
================================================
================================================
FILE: SkinProject/app/src/main/res/drawable/drawable_float_view.xml
================================================
================================================
FILE: SkinProject/app/src/main/res/drawable/news_item_selector.xml
================================================
================================================
FILE: SkinProject/app/src/main/res/values/colors.xml
================================================
#84878c
#000000
#FFFFFF
#000000
#000000
#333333
#1c1d20
#00FF00
#8e8e8e
================================================
FILE: SkinProject/build.gradle
================================================
// Top-level build file where you can add configuration options common to all sub-projects/modules.
buildscript {
repositories {
jcenter()
}
dependencies {
classpath 'com.android.tools.build:gradle:2.2.0'
// NOTE: Do not place your application dependencies here; they belong
// in the individual module build.gradle files
}
}
allprojects {
repositories {
jcenter()
}
}
task clean(type: Delete) {
delete rootProject.buildDir
}
================================================
FILE: SkinProject/gradle.properties
================================================
## Project-wide Gradle settings.
#
# For more details on how to configure your build environment visit
# http://www.gradle.org/docs/current/userguide/build_environment.html
#
# Specifies the JVM arguments used for the daemon process.
# The setting is particularly useful for tweaking memory settings.
# Default value: -Xmx1024m -XX:MaxPermSize=256m
# org.gradle.jvmargs=-Xmx2048m -XX:MaxPermSize=512m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8
#
# When configured, Gradle will run in incubating parallel mode.
# This option should only be used with decoupled projects. More details, visit
# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
# org.gradle.parallel=true
#Wed Nov 09 09:21:50 CST 2016
systemProp.http.proxyHost=127.0.0.1
org.gradle.jvmargs=-Xmx1536m
systemProp.http.proxyPort=1080
================================================
FILE: SkinProject/gradlew
================================================
#!/usr/bin/env bash
##############################################################################
##
## Gradle start up script for UN*X
##
##############################################################################
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS=""
APP_NAME="Gradle"
APP_BASE_NAME=`basename "$0"`
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD="maximum"
warn ( ) {
echo "$*"
}
die ( ) {
echo
echo "$*"
echo
exit 1
}
# OS specific support (must be 'true' or 'false').
cygwin=false
msys=false
darwin=false
case "`uname`" in
CYGWIN* )
cygwin=true
;;
Darwin* )
darwin=true
;;
MINGW* )
msys=true
;;
esac
# Attempt to set APP_HOME
# Resolve links: $0 may be a link
PRG="$0"
# Need this for relative symlinks.
while [ -h "$PRG" ] ; do
ls=`ls -ld "$PRG"`
link=`expr "$ls" : '.*-> \(.*\)$'`
if expr "$link" : '/.*' > /dev/null; then
PRG="$link"
else
PRG=`dirname "$PRG"`"/$link"
fi
done
SAVED="`pwd`"
cd "`dirname \"$PRG\"`/" >/dev/null
APP_HOME="`pwd -P`"
cd "$SAVED" >/dev/null
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
# Determine the Java command to use to start the JVM.
if [ -n "$JAVA_HOME" ] ; then
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
# IBM's JDK on AIX uses strange locations for the executables
JAVACMD="$JAVA_HOME/jre/sh/java"
else
JAVACMD="$JAVA_HOME/bin/java"
fi
if [ ! -x "$JAVACMD" ] ; then
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
else
JAVACMD="java"
which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
# Increase the maximum file descriptors if we can.
if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then
MAX_FD_LIMIT=`ulimit -H -n`
if [ $? -eq 0 ] ; then
if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
MAX_FD="$MAX_FD_LIMIT"
fi
ulimit -n $MAX_FD
if [ $? -ne 0 ] ; then
warn "Could not set maximum file descriptor limit: $MAX_FD"
fi
else
warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
fi
fi
# For Darwin, add options to specify how the application appears in the dock
if $darwin; then
GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
fi
# For Cygwin, switch paths to Windows format before running java
if $cygwin ; then
APP_HOME=`cygpath --path --mixed "$APP_HOME"`
CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
JAVACMD=`cygpath --unix "$JAVACMD"`
# We build the pattern for arguments to be converted via cygpath
ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
SEP=""
for dir in $ROOTDIRSRAW ; do
ROOTDIRS="$ROOTDIRS$SEP$dir"
SEP="|"
done
OURCYGPATTERN="(^($ROOTDIRS))"
# Add a user-defined pattern to the cygpath arguments
if [ "$GRADLE_CYGPATTERN" != "" ] ; then
OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
fi
# Now convert the arguments - kludge to limit ourselves to /bin/sh
i=0
for arg in "$@" ; do
CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
else
eval `echo args$i`="\"$arg\""
fi
i=$((i+1))
done
case $i in
(0) set -- ;;
(1) set -- "$args0" ;;
(2) set -- "$args0" "$args1" ;;
(3) set -- "$args0" "$args1" "$args2" ;;
(4) set -- "$args0" "$args1" "$args2" "$args3" ;;
(5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
(6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
(7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
(8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
(9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
esac
fi
# Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules
function splitJvmOpts() {
JVM_OPTS=("$@")
}
eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS
JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME"
exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@"
================================================
FILE: SkinProject/gradlew.bat
================================================
@if "%DEBUG%" == "" @echo off
@rem ##########################################################################
@rem
@rem Gradle startup script for Windows
@rem
@rem ##########################################################################
@rem Set local scope for the variables with windows NT shell
if "%OS%"=="Windows_NT" setlocal
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
set DEFAULT_JVM_OPTS=
set DIRNAME=%~dp0
if "%DIRNAME%" == "" set DIRNAME=.
set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME%
@rem Find java.exe
if defined JAVA_HOME goto findJavaFromJavaHome
set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1
if "%ERRORLEVEL%" == "0" goto init
echo.
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
goto fail
:findJavaFromJavaHome
set JAVA_HOME=%JAVA_HOME:"=%
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
if exist "%JAVA_EXE%" goto init
echo.
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
goto fail
:init
@rem Get command-line arguments, handling Windowz variants
if not "%OS%" == "Windows_NT" goto win9xME_args
if "%@eval[2+2]" == "4" goto 4NT_args
:win9xME_args
@rem Slurp the command line arguments.
set CMD_LINE_ARGS=
set _SKIP=2
:win9xME_args_slurp
if "x%~1" == "x" goto execute
set CMD_LINE_ARGS=%*
goto execute
:4NT_args
@rem Get arguments from the 4NT Shell from JP Software
set CMD_LINE_ARGS=%$
:execute
@rem Setup the command line
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
@rem Execute Gradle
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
:end
@rem End local scope for the variables with windows NT shell
if "%ERRORLEVEL%"=="0" goto mainEnd
:fail
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
rem the _cmd.exe /c_ return code!
if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
exit /b 1
:mainEnd
if "%OS%"=="Windows_NT" endlocal
:omega
================================================
FILE: SkinProject/settings.gradle
================================================
include ':app'
================================================
FILE: app/.gitignore
================================================
/build
================================================
FILE: app/build.gradle
================================================
apply plugin: 'com.android.application'
android {
compileSdkVersion 23
buildToolsVersion "23.0.1"
defaultConfig {
applicationId "org.qcode.qskinloader"
minSdkVersion 14
targetSdkVersion 23
versionCode 1
versionName "1.0"
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
}
}
dependencies {
compile fileTree(dir: 'libs', include: ['*.jar'])
androidTestCompile('com.android.support.test.espresso:espresso-core:2.2.2', {
exclude group: 'com.android.support', module: 'support-annotations'
})
compile 'com.android.support:appcompat-v7:23.2.1'
testCompile 'junit:junit:4.12'
compile project(':QSkinLoaderlib')
}
================================================
FILE: app/proguard-rules.pro
================================================
# Add project specific ProGuard rules here.
# By default, the flags in this file are appended to flags specified
# in E:\Program Files\Android\android-sdk/tools/proguard/proguard-android.txt
# You can edit the include path and order by changing the proguardFiles
# directive in build.gradle.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
# Add any project specific keep options here:
# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}
================================================
FILE: app/src/main/AndroidManifest.xml
================================================
================================================
FILE: app/src/main/java/org/qcode/demo/BaseActivity.java
================================================
package org.qcode.demo;
import android.app.Activity;
import android.content.Context;
import android.graphics.PixelFormat;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.view.Window;
import android.widget.FrameLayout;
import android.widget.LinearLayout;
import org.qcode.demo.utils.UIUtil;
import org.qcode.qskinloader.IActivitySkinEventHandler;
import org.qcode.qskinloader.ISkinActivity;
import org.qcode.qskinloader.SkinManager;
import org.qcode.skintestdemo.R;
/**
* 所有Activity的父类;需要实现ISkinActivity接口
*/
public abstract class BaseActivity extends Activity implements ISkinActivity {
private IActivitySkinEventHandler mSkinEventHandler;
private boolean mFirstTimeApplySkin = true;
private FrameLayout mContentContainer;
private SkinChangeSwitchView mSwitchView;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
requestWindowFeature(Window.FEATURE_NO_TITLE);
getWindow().setFormat(PixelFormat.RGBA_8888);
mSkinEventHandler = SkinManager.newActivitySkinEventHandler()
.setSwitchSkinImmediately(isSwitchSkinImmediately())
.setSupportSkinChange(isSupportSkinChange())
.setWindowBackgroundResource(getWindowBackgroundResource())
.setNeedDelegateViewCreate(true);
mSkinEventHandler.onCreate(this);
initView(this);
}
private void initView(Context context) {
LinearLayout root = new LinearLayout(context);
root.setOrientation(LinearLayout.VERTICAL);
super.setContentView(root);
LinearLayout.LayoutParams paramTitle = new LinearLayout.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT);
mSwitchView = new SkinChangeSwitchView(context);
int padding = UIUtil.dip2px(context, 15);
mSwitchView.setPadding(padding, padding, padding, padding);
root.addView(mSwitchView, paramTitle);
LinearLayout.LayoutParams paramContent = new LinearLayout.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT, 0);
paramContent.weight = 1;
mContentContainer = new FrameLayout(context);
root.addView(mContentContainer, paramContent);
}
@Override
public void setContentView(int layoutResID) {
LayoutInflater.from(this).inflate(layoutResID, mContentContainer);
}
@Override
public void setContentView(View view) {
FrameLayout.LayoutParams param = new FrameLayout.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT);
mContentContainer.addView(view, param);
}
@Override
protected void onStart() {
super.onStart();
mSkinEventHandler.onStart();
}
@Override
protected void onResume() {
super.onResume();
//皮肤相关,此通知放在此处,尽量让子类的view都添加到view树内
if (mFirstTimeApplySkin) {
mSkinEventHandler.onViewCreated();
mFirstTimeApplySkin = false;
}
mSwitchView.refreshSwitch();
mSkinEventHandler.onResume();
}
@Override
public void onWindowFocusChanged(boolean hasFocus) {
super.onWindowFocusChanged(hasFocus);
//皮肤相关
mSkinEventHandler.onWindowFocusChanged(hasFocus);
}
@Override
protected void onPause() {
super.onPause();
mSkinEventHandler.onPause();
}
@Override
protected void onStop() {
super.onStop();
mSkinEventHandler.onStop();
}
@Override
protected void onDestroy() {
super.onDestroy();
//皮肤相关
mSkinEventHandler.onDestroy();
}
@Override
public boolean isSupportSkinChange() {
//告知当前界面是否支持换肤:true支持换肤,false不支持
return true;
}
@Override
public boolean isSwitchSkinImmediately() {
//告知当切换皮肤时,是否立刻刷新当前界面;true立刻刷新,false表示在界面onResume时刷新;
//减轻换肤时性能压力
return false;
}
@Override
public void handleSkinChange() {
//当前界面在换肤时收到的回调,可以在此回调内做一些其他事情;
//比如:通知WebView内的页面切换到夜间模式等
}
/**
* 告知当前界面Window的background资源,换肤时会寻找对应的资源替换
*/
protected int getWindowBackgroundResource() {
return R.color.activity_bg_color;
}
}
================================================
FILE: app/src/main/java/org/qcode/demo/MainActivity.java
================================================
package org.qcode.demo;
import android.content.Intent;
import android.os.Bundle;
import android.view.View;
import org.qcode.demo.ui.customattr.CustomAttrViewActivity;
import org.qcode.demo.ui.dynamicaddview.DynamicAddViewActivity;
import org.qcode.demo.ui.gridview.GridViewActivity;
import org.qcode.demo.ui.otherscene.OtherSceneActivity;
import org.qcode.demo.ui.recyclerview.RecyclerViewActivity;
import org.qcode.demo.ui.viewpageandlistview.ViewPagerAndListViewActivity;
import org.qcode.skintestdemo.R;
/**
* qqliu
* 2016/10/10.
*/
public class MainActivity extends BaseActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
}
public void onClick(View view) {
Intent intent = new Intent();
switch (view.getId()) {
case R.id.btnCustomView:
intent.setClass(this, CustomAttrViewActivity.class);
break;
case R.id.btnDynamicAddView:
intent.setClass(this, DynamicAddViewActivity.class);
break;
case R.id.btnRecyclerView:
intent.setClass(this, RecyclerViewActivity.class);
break;
case R.id.btnViewPagerAndListView:
intent.setClass(this, ViewPagerAndListViewActivity.class);
break;
case R.id.btnGridView:
intent.setClass(this, GridViewActivity.class);
break;
case R.id.btnOtherScene:
intent.setClass(this, OtherSceneActivity.class);
break;
}
startActivity(intent);
}
}
================================================
FILE: app/src/main/java/org/qcode/demo/SkinChangeSwitchView.java
================================================
package org.qcode.demo;
import android.content.Context;
import android.view.Gravity;
import android.view.View;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.RelativeLayout;
import android.widget.TextView;
import org.qcode.demo.skin.SkinChangeHelper;
import org.qcode.qskinloader.SkinManager;
import org.qcode.qskinloader.entity.SkinAttrName;
import org.qcode.skintestdemo.R;
public class SkinChangeSwitchView extends LinearLayout {
private static final String TAG = "NightModeSettingView";
private ImageView mImgBtnSwitch;
private TextView mTextViewTitle;
public SkinChangeSwitchView(Context context) {
super(context);
initView(context);
}
protected void initView(final Context context) {
setOrientation(LinearLayout.HORIZONTAL);
setGravity(Gravity.CENTER);
SkinManager.with(this)
.setViewAttrs(SkinAttrName.BACKGROUND, R.color.color_background)
.applySkin(true);
mTextViewTitle = new TextView(context);
SkinManager.with(mTextViewTitle)
.setViewAttrs(SkinAttrName.TEXT_COLOR, R.color.color_text)
.applySkin(false);
mTextViewTitle.setTextSize(16);
LinearLayout.LayoutParams paramTitlePart = new LinearLayout.LayoutParams(
0, RelativeLayout.LayoutParams.WRAP_CONTENT);
paramTitlePart.weight = 1;
addView(mTextViewTitle, paramTitlePart);
mTextViewTitle.setText("夜间模式");
mImgBtnSwitch = new ImageView(context);
LinearLayout.LayoutParams paramEntryFlag = new LinearLayout.LayoutParams(
RelativeLayout.LayoutParams.WRAP_CONTENT, RelativeLayout.LayoutParams.WRAP_CONTENT);
addView(mImgBtnSwitch, paramEntryFlag);
refreshSwitch();
mImgBtnSwitch.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
SkinChangeHelper.getInstance().switchSkinMode(
new SkinChangeHelper.OnSkinChangeListener() {
@Override
public void onSuccess() {
refreshSwitch();
}
@Override
public void onError() {
refreshSwitch();
}
});
refreshSwitch();
}
});
}
public void refreshSwitch() {
boolean isDefaultMode = SkinChangeHelper.getInstance().isDefaultMode();
boolean isSwitching = SkinChangeHelper.getInstance().isSwitching();
int drawableId;
if (isDefaultMode) {
//mImgBtnSwitch.setImageResource(R.drawable.news_switch_setting_off_nor);
drawableId = R.mipmap.news_switch_setting_off_nor;
} else {
//mImgBtnSwitch.setImageResource(R.drawable.news_switch_setting_on_nor);
drawableId = R.mipmap.news_switch_setting_on_nor;
}
SkinManager.with(mImgBtnSwitch)
.setViewAttrs(SkinAttrName.SRC, drawableId)
.applySkin(false);
mImgBtnSwitch.setEnabled(!isSwitching);
}
}
================================================
FILE: app/src/main/java/org/qcode/demo/SkinDemoApp.java
================================================
package org.qcode.demo;
import android.app.Application;
import android.content.Context;
import org.qcode.demo.base.Settings;
import org.qcode.demo.skin.SkinChangeHelper;
/**
* qqliu
* 2016/9/9.
*/
public class SkinDemoApp extends Application {
private static Context mContext;
public void onCreate() {
super.onCreate();
mContext = this;
Settings.createInstance(this);
initSkinLoader();
}
/**
* Must call init first
*/
private void initSkinLoader() {
// 初始化皮肤框架
SkinChangeHelper.getInstance().init(this);
//初始化上次缓存的皮肤
SkinChangeHelper.getInstance().refreshSkin(null);
}
public static Context getAppContext() {
return mContext;
}
}
================================================
FILE: app/src/main/java/org/qcode/demo/base/Settings.java
================================================
package org.qcode.demo.base;
import android.content.Context;
import android.content.SharedPreferences;
import android.content.SharedPreferences.Editor;
import android.text.TextUtils;
import org.qcode.qskinloader.base.utils.Logging;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
public class Settings {
private static final String TAG = "SettingsImpl";
private static final String NAME = "SKinSettings";
private SharedPreferences mSharedPref;
private Settings(Context context) {
int mode = Context.MODE_PRIVATE;
mSharedPref = context.getSharedPreferences(NAME, mode);
}
private static volatile Settings mInstance;
public static Settings createInstance(Context context) {
if(null == mInstance) {
synchronized (Settings.class) {
if(null == mInstance) {
mInstance = new Settings(context);
}
}
}
return mInstance;
}
public static Settings getInstance() {
return mInstance;
}
public boolean isSetted(String key) {
return mSharedPref.contains(key);
}
public void setSetting(String key, boolean value) {
try {
Editor editor = mSharedPref.edit();
editor.putBoolean(key, value);
editor.commit();
} catch (Exception e) {
Logging.e(TAG, "setSetting(" + key + ", " + value + ")", e);
}
}
public void setSetting(String key, int value) {
try {
Editor editor = mSharedPref.edit();
editor.putInt(key, value);
editor.commit();
} catch (Exception e) {
Logging.e(TAG, "setSetting(" + key + ", " + value + ")", e);
}
}
public void setSetting(String key, float value) {
try {
Editor editor = mSharedPref.edit();
editor.putFloat(key, value);
editor.commit();
} catch (Exception e) {
Logging.e(TAG, "setSetting(" + key + ", " + value + ")", e);
}
}
public void setSetting(String key, long value) {
try {
Editor editor = mSharedPref.edit();
editor.putLong(key, value);
editor.commit();
} catch (Exception e) {
Logging.e(TAG, "setSetting(" + key + ", " + value + ")", e);
}
}
public void setSetting(String key, String value) {
if (null != value) {
//要过滤'\0',否则会使XML读取异常
value = value.replace("\0", "");
}
try {
Editor editor = mSharedPref.edit();
editor.putString(key, value);
editor.commit();
} catch (Exception e) {
Logging.e(TAG, "setSetting(" + key + ", " + value + ")", e);
}
}
public boolean getBoolean(String key) {
return getBoolean(key, false);
}
public boolean getBoolean(String key, boolean defaultValue) {
boolean result = defaultValue;
try {
result = mSharedPref.getBoolean(key, result);
} catch (Exception e) {
Logging.e(TAG, "getBoolean()", e);
}
return result;
}
public int getInt(String key) {
return getInt(key, 0);
}
public int getInt(String key, int defaultValue) {
int value = defaultValue;
try {
value = mSharedPref.getInt(key, defaultValue);
} catch (Exception e) {
Logging.e(TAG, "getSetting()", e);
}
return value;
}
public float getFloat(String key) {
return getFloat(key, 0);
}
public float getFloat(String key, float defaultValue) {
float value = defaultValue;
try {
value = mSharedPref.getFloat(key, defaultValue);
} catch (Exception e) {
Logging.e(TAG, "getLongSetting()", e);
}
return value;
}
public long getLong(String key) {
return getLong(key, 0);
}
public long getLong(String key, long defaultValue) {
long value = defaultValue;
try {
value = mSharedPref.getLong(key, defaultValue);
} catch (Exception e) {
Logging.e(TAG, "getLongSetting()", e);
}
return value;
}
public String getString(String key) {
return getString(key, null);
}
public String getString(String key, String defaultValue) {
String value = defaultValue;
try {
value = mSharedPref.getString(key, defaultValue);
} catch (Exception e) {
Logging.e(TAG, "getString()", e);
}
return value;
}
public void saveObject(String fileName, Object object) {
ObjectOutputStream objectOutputStream = null;
try {
File file = new File(fileName);
if (file.exists()) {
file.delete();
}
file.createNewFile();
objectOutputStream = new ObjectOutputStream(new FileOutputStream(file));
objectOutputStream.writeObject(object);
objectOutputStream.flush();
} catch (Exception e) {
Logging.e(TAG, "saveObject()", e);
} finally {
if (objectOutputStream != null) {
try {
objectOutputStream.close();
} catch (IOException e) {
Logging.e(TAG, "saveObject()", e);
}
}
}
}
public Object readObject(String fileName) {
Object object = null;
ObjectInputStream objectInputStream = null;
try {
objectInputStream = new ObjectInputStream(new FileInputStream(fileName));
object = objectInputStream.readObject();
} catch (Exception e) {
Logging.e(TAG, "readObject()" + e);
} finally {
if (objectInputStream != null) {
try {
objectInputStream.close();
} catch (IOException e) {
Logging.e(TAG, "readObject()" + e);
}
}
}
return object;
}
public void clearObject(String fileName) {
try {
File file = new File(fileName);
if (file.exists()) {
file.delete();
Logging.d(TAG, "delete file success");
}
} catch (Exception e) {
Logging.e(TAG, " clearObject()", e);
}
}
public void removeSetting(String key) {
try {
//如果key不为空,把key删掉
if (!TextUtils.isEmpty(key)) {
Editor editor = mSharedPref.edit();
editor.remove(key);
editor.commit();
}
} catch (Exception e) {
Logging.e(TAG, "removeSetting(" + key + ")", e);
}
}
}
================================================
FILE: app/src/main/java/org/qcode/demo/skin/SkinChangeHelper.java
================================================
package org.qcode.demo.skin;
import android.content.Context;
import org.qcode.demo.SkinDemoApp;
import org.qcode.demo.utils.UITaskRunner;
import org.qcode.demo.utils.UIUtil;
import org.qcode.qskinloader.ILoadSkinListener;
import org.qcode.qskinloader.SkinManager;
import org.qcode.qskinloader.resourceloader.impl.ConfigChangeResourceLoader;
import org.qcode.qskinloader.resourceloader.impl.SuffixResourceLoader;
import java.io.File;
/**
* qqliu
* 2016/9/26.
*/
public class SkinChangeHelper {
private static final String TAG = "SkinChangeHelper";
//基于suffix换肤
private static final int TYPE_SUFFIX = 1;
//基于apk换肤
private static final int TYPE_APK = 2;
//基于UIMode换肤
private static final int TYPE_UIMODE = 3;
private static volatile SkinChangeHelper mInstance;
private final Context mContext;
//目前框架支持三种换肤方式,后缀换肤/APK资源包换肤/UIMode换肤
private int mSkinChangeType = TYPE_SUFFIX;
private SkinChangeHelper() {
mContext = SkinDemoApp.getAppContext();
mIsDefaultMode = SkinConfigHelper.isDefaultSkin();
}
public static SkinChangeHelper getInstance() {
if (null == mInstance) {
synchronized (SkinChangeHelper.class) {
if (null == mInstance) {
mInstance = new SkinChangeHelper();
}
}
}
return mInstance;
}
private volatile boolean mIsDefaultMode = false;
private volatile boolean mIsSwitching = false;
public void init(Context context) {
SkinManager.getInstance().init(context);
}
public void switchSkinMode(OnSkinChangeListener listener) {
mIsSwitching = true;
mIsDefaultMode = !mIsDefaultMode;
refreshSkin(listener);
}
public void refreshSkin(OnSkinChangeListener listener) {
if (mIsDefaultMode) {
switch (mSkinChangeType) {
case TYPE_SUFFIX:
case TYPE_APK:
//恢复到默认皮肤
SkinManager.getInstance().restoreDefault(
SkinConstant.DEFAULT_SKIN,
new MyLoadSkinListener(listener));
break;
case TYPE_UIMODE:
//基于UIMode换肤只能通过改回配置才能换肤
changeSkinByConfig(ConfigChangeResourceLoader.MODE_DAY, listener);
break;
}
} else {
switch (mSkinChangeType) {
case TYPE_SUFFIX:
changeSkinBySuffix(listener);
break;
case TYPE_APK:
changeSkinByApk(listener);
break;
case TYPE_UIMODE:
//基于UIMode换肤只能通过改回配置才能换肤
changeSkinByConfig(ConfigChangeResourceLoader.MODE_NIGHT, listener);
break;
}
}
}
public boolean isDefaultMode() {
return mIsDefaultMode;
}
public boolean isSwitching() {
return mIsSwitching;
}
private void changeSkinByApk(OnSkinChangeListener listener) {
SkinUtils.copyAssetSkin(mContext);
File skin = new File(
SkinUtils.getTotalSkinPath(mContext));
if (skin == null || !skin.exists()) {
UIUtil.showToast(mContext, "皮肤初始化失败");
return;
}
SkinManager.getInstance().loadAPKSkin(
skin.getAbsolutePath(), new MyLoadSkinListener(listener));
}
private void changeSkinBySuffix(OnSkinChangeListener listener) {
SkinManager.getInstance().loadSkin("_night",
new SuffixResourceLoader(mContext),
new MyLoadSkinListener(listener));
}
private void changeSkinByConfig(String mode, OnSkinChangeListener listener) {
SkinManager.getInstance().loadSkin(mode,
new ConfigChangeResourceLoader(mContext),
new MyLoadSkinListener(listener));
}
private class MyLoadSkinListener implements ILoadSkinListener {
private final OnSkinChangeListener mListener;
public MyLoadSkinListener(OnSkinChangeListener listener) {
mListener = listener;
}
@Override
public void onLoadStart(String skinIdentifier) {
}
@Override
public void onLoadSuccess(String skinIdentifier) {
mIsSwitching = false;
//存储皮肤标识
SkinConfigHelper.saveSkinIdentifier(skinIdentifier);
UITaskRunner.getHandler().post(new Runnable() {
@Override
public void run() {
if(null != mListener) {
mListener.onSuccess();
}
}
});
}
@Override
public void onLoadFail(String skinIdentifier) {
mIsSwitching = false;
UITaskRunner.getHandler().post(new Runnable() {
@Override
public void run() {
if (null != mListener) {
mListener.onError();
}
}
});
}
};
public interface OnSkinChangeListener {
void onSuccess();
void onError();
}
}
================================================
FILE: app/src/main/java/org/qcode/demo/skin/SkinConfigHelper.java
================================================
package org.qcode.demo.skin;
import org.qcode.demo.base.Settings;
public class SkinConfigHelper {
/***
* 获取当前皮肤包的标识
*/
public static String getSkinIdentifier() {
return Settings.getInstance().getString(
SkinConstant.CUSTOM_SKIN_IDENTIFIER,
SkinConstant.DEFAULT_SKIN);
}
/**
* 保存皮肤包的标识
*/
public static void saveSkinIdentifier(String identifier) {
Settings.getInstance().setSetting(
SkinConstant.CUSTOM_SKIN_IDENTIFIER,
identifier);
}
/**
* 是否默认皮肤
*/
public static boolean isDefaultSkin() {
return SkinConstant.DEFAULT_SKIN.equals(getSkinIdentifier());
}
}
================================================
FILE: app/src/main/java/org/qcode/demo/skin/SkinConstant.java
================================================
package org.qcode.demo.skin;
/**
* qqliu
* 2016/10/8.
*/
public class SkinConstant {
public static final String PACKAGE_NAME = "org.qcode.demo";
/**皮肤标识存放*/
public static final String CUSTOM_SKIN_IDENTIFIER =
PACKAGE_NAME + ".CUSTOM_SKIN_IDENTIFIER";
/**默认皮肤*/
public static final String DEFAULT_SKIN = "default";
}
================================================
FILE: app/src/main/java/org/qcode/demo/skin/SkinUtils.java
================================================
package org.qcode.demo.skin;
import android.content.Context;
import org.qcode.demo.utils.FileUtils;
import org.qcode.qskinloader.base.utils.Logging;
import java.io.File;
/**
* qqliu
* 2016/9/21.
*/
public class SkinUtils {
private static final String TAG = "NightModeUtils";
private static final String SKIN_NAME = "nightMode.skin";
public static String getTotalSkinPath(Context context) {
String SKIN_PATH = context.getCacheDir().getAbsolutePath();
String totalPath = SKIN_PATH + File.separator + SKIN_NAME;
return totalPath;
}
public static boolean copyAssetSkin(Context context) {
String totalPath = getTotalSkinPath(context);
File skin = new File(totalPath);
if (skin == null || !skin.exists() || needUpdateSkin()) {
long currTime = System.currentTimeMillis();
boolean isSuccess = FileUtils.copyAssetFile(context, SKIN_NAME,
context.getCacheDir().getAbsolutePath(),
SKIN_NAME);
long diff = System.currentTimeMillis() - currTime;
Logging.d(TAG, "copyAssetSkin()| copy file time: " + diff);
return isSuccess;
}
return false;
}
private static boolean needUpdateSkin() {
//每次都拷贝皮肤包
return true;
}
}
================================================
FILE: app/src/main/java/org/qcode/demo/ui/customattr/CustomAttrViewActivity.java
================================================
package org.qcode.demo.ui.customattr;
import android.os.Bundle;
import org.qcode.demo.BaseActivity;
import org.qcode.qskinloader.SkinManager;
import org.qcode.skintestdemo.R;
import static org.qcode.demo.ui.customattr.DefBackgroundAttrHandler.DEF_BACKGROUND;
import static org.qcode.demo.ui.customattr.DefTextColorAttrHandler.DEF_TEXT_COLOR;
public class CustomAttrViewActivity extends BaseActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
registerHandler();
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_custom_attr_test);
}
private void registerHandler() {
SkinManager.getInstance().registerSkinAttrHandler(
DEF_TEXT_COLOR, new DefTextColorAttrHandler());
SkinManager.getInstance().registerSkinAttrHandler(
DEF_BACKGROUND, new DefBackgroundAttrHandler());
}
}
================================================
FILE: app/src/main/java/org/qcode/demo/ui/customattr/CustomTextView.java
================================================
package org.qcode.demo.ui.customattr;
import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Color;
import android.util.AttributeSet;
import android.widget.TextView;
import org.qcode.skintestdemo.R;
/**
* qqliu
* 2016/9/11.
*/
public class CustomTextView extends TextView {
public CustomTextView(Context context) {
this(context, null);
}
public CustomTextView(Context context, AttributeSet attrs) {
super(context, attrs);
TypedArray array = context.obtainStyledAttributes(attrs, R.styleable.app);
int textColor = array.getColor(R.styleable.app_defTextColor, Color.BLACK);
int bgDrawableId = array.getResourceId(R.styleable.app_defBackground, 0);
array.recycle();
setTextColor(textColor);
setBackgroundResource(bgDrawableId);
}
}
================================================
FILE: app/src/main/java/org/qcode/demo/ui/customattr/DefBackgroundAttrHandler.java
================================================
package org.qcode.demo.ui.customattr;
import android.graphics.drawable.Drawable;
import android.view.View;
import org.qcode.qskinloader.IResourceManager;
import org.qcode.qskinloader.ISkinAttrHandler;
import org.qcode.qskinloader.attrhandler.SkinAttrUtils;
import org.qcode.qskinloader.entity.SkinAttr;
/**
* qqliu
* 2016/10/9.
*/
public class DefBackgroundAttrHandler implements ISkinAttrHandler {
public static final String DEF_BACKGROUND = "defBackground";
@Override
public void apply(View view, SkinAttr skinAttr, IResourceManager resourceManager) {
if (null == view
|| null == skinAttr
|| !(DEF_BACKGROUND.equals(skinAttr.mAttrName))) {
//自定义属性处理时,需要明确当前处理器能处理的属性,此处是DEF_BACKGROUND
return;
}
if (!(view instanceof CustomTextView)) {
//防止在错误的View上设置了此属性
return;
}
//封装了取ColorDrawable和取普通Drawable的逻辑
Drawable drawable = SkinAttrUtils.getDrawable(
resourceManager, skinAttr.mAttrValueRefId,
skinAttr.mAttrValueTypeName, skinAttr.mAttrValueRefName);
if (null != drawable) {
view.setBackgroundDrawable(drawable);
}
}
}
================================================
FILE: app/src/main/java/org/qcode/demo/ui/customattr/DefTextColorAttrHandler.java
================================================
package org.qcode.demo.ui.customattr;
import android.content.res.ColorStateList;
import android.content.res.Resources;
import android.view.View;
import org.qcode.qskinloader.IResourceManager;
import org.qcode.qskinloader.ISkinAttrHandler;
import org.qcode.qskinloader.entity.SkinAttr;
import org.qcode.qskinloader.entity.SkinConstant;
import static org.qcode.qskinloader.entity.SkinConstant.RES_TYPE_NAME_COLOR;
/**
* qqliu
* 2016/10/9.
*/
public class DefTextColorAttrHandler implements ISkinAttrHandler {
public static final String DEF_TEXT_COLOR = "defTextColor";
@Override
public void apply(View view, SkinAttr skinAttr, IResourceManager resourceManager) {
if(null == view
|| null == skinAttr
|| !(DEF_TEXT_COLOR.equals(skinAttr.mAttrName))) {
//自定义属性处理时,需要明确当前处理器能处理的属性,此处是DEF_TEXT_COLOR
return;
}
if(!(view instanceof CustomTextView)) {
//防止在错误的View上设置了此属性
return;
}
CustomTextView tv = (CustomTextView) view;
if (RES_TYPE_NAME_COLOR.equals(skinAttr.mAttrValueTypeName)) {
if (SkinConstant.RES_TYPE_NAME_COLOR.equals(skinAttr.mAttrValueTypeName)) {
try {
//先尝试按照int型颜色解析
int textColor = resourceManager.getColor(
skinAttr.mAttrValueRefId, skinAttr.mAttrValueRefName);
tv.setTextColor(textColor);
} catch (Resources.NotFoundException ex) {
//不是int型则按照ColorStateList引用来解析
ColorStateList textColor = resourceManager.getColorStateList(
skinAttr.mAttrValueRefId, skinAttr.mAttrValueRefName);
tv.setTextColor(textColor);
}
}
}
}
}
================================================
FILE: app/src/main/java/org/qcode/demo/ui/dynamicaddview/DynamicAddViewActivity.java
================================================
package org.qcode.demo.ui.dynamicaddview;
import android.os.Bundle;
import android.view.ViewGroup;
import android.widget.LinearLayout;
import org.qcode.demo.BaseActivity;
import org.qcode.demo.ui.viewpageandlistview.DataListAdapter;
import org.qcode.demo.utils.UIUtil;
import org.qcode.skintestdemo.R;
public class DynamicAddViewActivity extends BaseActivity {
public static final int SHOW_COUNT = 40;
private LinearLayout mContainer;
private DataListAdapter mAdapter;
private int mCount;
private boolean mIsDestroying;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_dynamic_add_view);
mContainer = (LinearLayout) findViewById(R.id.dynamic_container);
mAdapter = new DataListAdapter(this, "");
mContainer.postDelayed(new Runnable() {
@Override
public void run() {
if(mIsDestroying) {
return;
}
if(mCount >= 25) {
UIUtil.showToast(DynamicAddViewActivity.this, "添加完毕");
return;
}
for (int i = SHOW_COUNT * mCount; i < SHOW_COUNT * (mCount + 1); i++) {
LinearLayout.LayoutParams param = new LinearLayout.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT, UIUtil.dip2px(getApplicationContext(), 50)
);
mContainer.addView(mAdapter.getView(i, null, null), param);
}
mCount++;
mContainer.postDelayed(this, 200);
}
}, 200);
}
@Override
protected void onDestroy() {
mIsDestroying = true;
super.onDestroy();
}
}
================================================
FILE: app/src/main/java/org/qcode/demo/ui/gridview/GridViewActivity.java
================================================
package org.qcode.demo.ui.gridview;
import android.os.Bundle;
import android.widget.GridView;
import org.qcode.demo.BaseActivity;
import org.qcode.demo.ui.viewpageandlistview.DataListAdapter;
import org.qcode.skintestdemo.R;
public class GridViewActivity extends BaseActivity{
private GridView mGridView;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_grid_view);
mGridView = (GridView)findViewById(R.id.grid_view);
mGridView.setAdapter(new DataListAdapter(this, ""));
}
}
================================================
FILE: app/src/main/java/org/qcode/demo/ui/otherscene/CustomDialog.java
================================================
package org.qcode.demo.ui.otherscene;
import android.app.Dialog;
import android.content.Context;
import android.content.DialogInterface;
import android.view.View;
import android.view.Window;
import org.qcode.qskinloader.SkinManager;
import org.qcode.skintestdemo.R;
/**
* qqliu
* 2016/10/14.
*/
public class CustomDialog extends Dialog implements View.OnClickListener {
private WrapperDismissListener mDismissListener;
public CustomDialog(Context context) {
super(context);
getWindow().requestFeature(Window.FEATURE_NO_TITLE);
setContentView(R.layout.layout_dialog_custom);
findViewById(R.id.btn_dialog_confirm).setOnClickListener(this);
findViewById(R.id.btn_dialog_cancel).setOnClickListener(this);
// //方案二:---------------------------------
// mDismissListener = new WrapperDismissListener();
// super.setOnDismissListener(mDismissListener);
// //如果换肤过程中对话框需要展示,则需要将对话框的View
// //添加到框架的WindowViewManager中存储;
// //建议addWindowView与removeWindowView成对使用
// SkinManager
// .getWindowViewManager()
// .addWindowView(findViewById(android.R.id.content))
// .applySkinForViews(true);
}
@Override
public void show() {
super.show();
//方案一:---------------------------------
//如果换肤的过程中对话框不会展示,则在Dialog初始化时
//按照当前皮肤设置应用一次皮肤即可
SkinManager.getInstance().applySkin(
findViewById(android.R.id.content), true);
}
@Override
public void setOnDismissListener(OnDismissListener listener) {
mDismissListener.setDismissListener(listener);
}
public void onClick(View view) {
switch (view.getId()) {
case R.id.btn_dialog_confirm:
case R.id.btn_dialog_cancel:
dismiss();
break;
}
}
//代理外部的OnDismissListener
private static class WrapperDismissListener implements OnDismissListener {
private OnDismissListener mOuterListener;
void setDismissListener(OnDismissListener listener) {
mOuterListener = listener;
}
@Override
public void onDismiss(DialogInterface dialogInterface) {
if(null != mOuterListener) {
mOuterListener.onDismiss(dialogInterface);
}
if(!(dialogInterface instanceof CustomDialog)) {
return;
}
CustomDialog dialog = (CustomDialog) dialogInterface;
//与addWindowView成对使用
SkinManager.getWindowViewManager()
.removeWindowView(dialog.findViewById(android.R.id.content));
}
}
}
================================================
FILE: app/src/main/java/org/qcode/demo/ui/otherscene/FloatView.java
================================================
package org.qcode.demo.ui.otherscene;
import android.content.Context;
import android.widget.ImageView;
import org.qcode.qskinloader.SkinManager;
import org.qcode.qskinloader.entity.SkinAttrName;
import org.qcode.skintestdemo.R;
/**
* qqliu
* 2016/10/14.
*/
public class FloatView extends ImageView {
public FloatView(Context context) {
super(context);
//动态设置皮肤
SkinManager
.with(this)
.setViewAttrs(SkinAttrName.SRC, R.drawable.drawable_float_view)
.applySkin(false);
}
public void dismiss() {
SkinManager
.getWindowViewManager()
.removeWindowView(this);
}
public void show() {
//因为悬浮窗直接加载在WindowManager上,我们需要将View添加到框架内维护
SkinManager
.getWindowViewManager()
.addWindowView(this);
}
}
================================================
FILE: app/src/main/java/org/qcode/demo/ui/otherscene/OtherSceneActivity.java
================================================
package org.qcode.demo.ui.otherscene;
import android.app.Dialog;
import android.content.Context;
import android.graphics.PixelFormat;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.WindowManager;
import android.widget.PopupWindow;
import android.widget.TextView;
import android.widget.Toast;
import org.qcode.demo.BaseActivity;
import org.qcode.demo.ui.customattr.DefBackgroundAttrHandler;
import org.qcode.demo.utils.UIUtil;
import org.qcode.qskinloader.SkinManager;
import org.qcode.qskinloader.entity.DynamicAttr;
import org.qcode.skintestdemo.R;
import static org.qcode.demo.ui.customattr.DefBackgroundAttrHandler.DEF_BACKGROUND;
/**
* qqliu
* 2016/10/13.
*/
public class OtherSceneActivity extends BaseActivity {
private PopupWindow mPopWindow;
private Dialog mDialog;
private FloatView mFloatView;
private boolean mIsShowingFloatView;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
SkinManager.getInstance().registerSkinAttrHandler(
SpannableSkinAttr.HIGHLIGHT_SPANNABLE, new SpannableSkinAttrHandler());
setContentView(R.layout.activity_other_scene);
TextView textView = (TextView) findViewById(R.id.textviewSpannableSkin);
DynamicAttr dynamicAttr = new SpannableSkinAttr(
textView.getText().toString(), R.color.color_red);
SkinManager.with(textView).addViewAttrs(dynamicAttr);
}
public void onClick(View view) {
switch (view.getId()) {
case R.id.btnPopWindow:
View contentView = LayoutInflater.from(this).inflate(R.layout.layout_popwindow, null);
//对所有不在Activity的Content下的View(PopWindow/Dialog/Float View等)都可以应用
//此方法将View添加到SkinManager内维护
//注意,虽然框架内对View采用了弱引用,但是建议此方法配合remove或clear方法一起使用,释放对View的静态引用
SkinManager.getWindowViewManager()
.addWindowView(contentView)
.applySkinForViews(true);
if (null == mPopWindow) {
mPopWindow = new PopupWindow(
contentView,
WindowManager.LayoutParams.WRAP_CONTENT,
WindowManager.LayoutParams.WRAP_CONTENT);
}
if (!mPopWindow.isShowing()) {
mPopWindow.showAsDropDown(view);
} else {
mPopWindow.dismiss();
}
break;
case R.id.btnDialog:
if (null == mDialog) {
mDialog = new CustomDialog(this);
}
if (!mDialog.isShowing()) {
mDialog.show();
} else {
mDialog.dismiss();
}
break;
case R.id.btnFloatWindow:
if (null == mFloatView) {
mFloatView = new FloatView(this);
mFloatView.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
mIsShowingFloatView = false;
hideFloatView();
}
});
}
mIsShowingFloatView = !mIsShowingFloatView;
if (mIsShowingFloatView) {
showFloatView();
} else {
hideFloatView();
}
break;
case R.id.btnPopWindowClick:
findViewById(R.id.btnPopWindow).performClick();
Toast.makeText(this, "popWindow点击", Toast.LENGTH_SHORT).show();
break;
}
}
private void showFloatView() {
WindowManager windowManager = (WindowManager) getApplicationContext()
.getSystemService(Context.WINDOW_SERVICE);
WindowManager.LayoutParams layoutParams = new WindowManager.LayoutParams(
WindowManager.LayoutParams.WRAP_CONTENT,
WindowManager.LayoutParams.WRAP_CONTENT);
int width = UIUtil.dip2px(this, 50);
layoutParams.width = width;
layoutParams.height = width;
layoutParams.flags = WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL;
layoutParams.type = WindowManager.LayoutParams.TYPE_SYSTEM_ERROR;
layoutParams.format = PixelFormat.TRANSLUCENT; // 设置图片格式,效果为背景透明
windowManager.addView(mFloatView, layoutParams);
mFloatView.show();
}
private void hideFloatView() {
WindowManager windowManager = getWindowManager();
windowManager.removeView(mFloatView);
mFloatView.dismiss();
}
@Override
protected int getWindowBackgroundResource() {
return R.color.color_transprent;
}
@Override
public boolean isSwitchSkinImmediately() {
//悬浮窗会导致Activity失去Focus,但是悬浮窗又是半透明的,所以此处建议立刻切换皮肤
return true;
}
}
================================================
FILE: app/src/main/java/org/qcode/demo/ui/otherscene/SpannableSkinAttr.java
================================================
package org.qcode.demo.ui.otherscene;
import org.qcode.qskinloader.entity.DynamicAttr;
/***
* author: qqliu
* created at 2018/1/5
*/
public class SpannableSkinAttr extends DynamicAttr {
public static final String HIGHLIGHT_SPANNABLE = "highlightSpannable";
public final String mText;
public SpannableSkinAttr(String text, int attrValueRefId) {
super(HIGHLIGHT_SPANNABLE, attrValueRefId);
mText = text;
//这里一定要设置,mText就丢弃了
keepInstance = true;
}
}
================================================
FILE: app/src/main/java/org/qcode/demo/ui/otherscene/SpannableSkinAttrHandler.java
================================================
package org.qcode.demo.ui.otherscene;
import android.content.res.ColorStateList;
import android.content.res.Resources;
import android.text.Spannable;
import android.text.SpannableString;
import android.text.Spanned;
import android.text.style.ForegroundColorSpan;
import android.view.View;
import android.widget.TextView;
import org.qcode.demo.ui.customattr.CustomTextView;
import org.qcode.qskinloader.IResourceManager;
import org.qcode.qskinloader.ISkinAttrHandler;
import org.qcode.qskinloader.entity.SkinAttr;
import org.qcode.qskinloader.entity.SkinConstant;
import static org.qcode.qskinloader.entity.SkinConstant.RES_TYPE_NAME_COLOR;
/***
* author: qqliu
* created at 2018/1/5
*/
public class SpannableSkinAttrHandler implements ISkinAttrHandler {
@Override
public void apply(View view, SkinAttr skinAttr, IResourceManager resourceManager) {
if(null == view
|| null == skinAttr
|| !(SpannableSkinAttr.HIGHLIGHT_SPANNABLE.equals(skinAttr.mAttrName))) {
return;
}
if(!(view instanceof TextView)) {
return;
}
SpannableSkinAttr spannableSkinAttr = (SpannableSkinAttr) skinAttr.mDynamicAttr;
if (null == spannableSkinAttr) {
return;
}
TextView tv = (TextView) view;
if (RES_TYPE_NAME_COLOR.equals(skinAttr.mAttrValueTypeName)) {
int textColor = resourceManager.getColor(
skinAttr.mAttrValueRefId, skinAttr.mAttrValueRefName);
SpannableString spannableString = new SpannableString(spannableSkinAttr.mText);
spannableString.setSpan(new ForegroundColorSpan(textColor), 0, 11, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
tv.setText(spannableString);
}
}
}
================================================
FILE: app/src/main/java/org/qcode/demo/ui/recyclerview/DataRecyclerViewAdapter.java
================================================
package org.qcode.demo.ui.recyclerview;
import android.content.Context;
import android.support.v7.widget.RecyclerView;
import android.view.Gravity;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.TextView;
import org.qcode.demo.utils.UIUtil;
import org.qcode.qskinloader.SkinManager;
import org.qcode.qskinloader.entity.SkinAttrName;
import org.qcode.skintestdemo.R;
import java.util.ArrayList;
/**
* qqliu
* 2016/9/11.
*/
public class DataRecyclerViewAdapter extends RecyclerView.Adapter {
private static final int TEXT_ID = 0x5f000031;
private final Context mContext;
private ArrayList mList = new ArrayList();
public DataRecyclerViewAdapter(Context context) {
mContext = context;
for (int i = 0; i < 1000; i++) {
mList.add("测试" + i);
}
}
@Override
public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
View view = newItem();
return new RecyclerView.ViewHolder(view) {
};
}
@Override
public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {
TextView textView = (TextView) holder.itemView.findViewById(TEXT_ID);
textView.setText(mList.get(position));
SkinManager.getInstance().applySkin(holder.itemView, true);
}
@Override
public int getItemCount() {
return mList.size();
}
public View newItem() {
LinearLayout linearLayout = new LinearLayout(mContext);
linearLayout.setOrientation(LinearLayout.HORIZONTAL);
linearLayout.setBackgroundResource(R.color.color_background);
linearLayout.setGravity(Gravity.CENTER_VERTICAL);
ImageView imageView = new ImageView(mContext);
LinearLayout.LayoutParams paramImg =
new LinearLayout.LayoutParams(
UIUtil.dip2px(mContext, 50), UIUtil.dip2px(mContext, 50));
imageView.setImageResource(R.mipmap.ic_launcher);
linearLayout.addView(imageView, paramImg);
TextView textView = new TextView(mContext);
textView.setId(TEXT_ID);
LinearLayout.LayoutParams paramText =
new LinearLayout.LayoutParams(
UIUtil.dip2px(mContext, 100), UIUtil.dip2px(mContext, 50));
textView.setTextColor(mContext.getResources().getColor(R.color.color_text));
linearLayout.addView(textView, paramText);
SkinManager
.with(linearLayout)
.setViewAttrs(SkinAttrName.BACKGROUND, R.color.color_background);
SkinManager
.with(textView)
.setViewAttrs(SkinAttrName.TEXT_COLOR, R.color.color_text);
return linearLayout;
}
}
================================================
FILE: app/src/main/java/org/qcode/demo/ui/recyclerview/RecyclerViewActivity.java
================================================
package org.qcode.demo.ui.recyclerview;
import android.os.Bundle;
import android.support.v7.widget.LinearLayoutManager;
import android.support.v7.widget.RecyclerView;
import org.qcode.demo.BaseActivity;
import org.qcode.skintestdemo.R;
public class RecyclerViewActivity extends BaseActivity {
private RecyclerView mRecyclerView;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_recycler_view);
mRecyclerView = (RecyclerView) findViewById(R.id.recycler_view);
mRecyclerView.setAdapter(new DataRecyclerViewAdapter(this));
LinearLayoutManager layoutManager = new LinearLayoutManager(this);
mRecyclerView.setLayoutManager(layoutManager);
}
}
================================================
FILE: app/src/main/java/org/qcode/demo/ui/viewpageandlistview/DataListAdapter.java
================================================
package org.qcode.demo.ui.viewpageandlistview;
import android.content.Context;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.BaseAdapter;
import android.widget.TextView;
import org.qcode.qskinloader.SkinManager;
import org.qcode.skintestdemo.R;
import java.util.ArrayList;
/**
* qqliu
* 2016/9/9.
*/
public class DataListAdapter extends BaseAdapter {
private static final String TAG = "MyListAdapter";
private final String mOutterIdentifier;
private Context mContext;
private ArrayList mList = new ArrayList();
public DataListAdapter(Context context, String outterIdentifier) {
mContext = context;
mOutterIdentifier = outterIdentifier;
for (int i = 0; i < 1000; i++) {
mList.add("测试" + i);
}
}
public String getIdentifier() {
return mOutterIdentifier;
}
@Override
public int getCount() {
return mList.size();
}
@Override
public Object getItem(int position) {
return mList.get(position);
}
@Override
public long getItemId(int position) {
return position;
}
@Override
public View getView(int position, View convertView, ViewGroup parent) {
if (null == convertView) {
convertView = LayoutInflater.from(mContext).inflate(R.layout.list_item_view, null);
}
//在Adapter getView处应用处理,保证动态添加的View刷新了皮肤
SkinManager.getInstance().applySkin(convertView, true);
TextView txtView = (TextView) convertView.findViewById(R.id.list_item_text_view);
txtView.setText(mOutterIdentifier + " " + mList.get(position));
return convertView;
}
}
================================================
FILE: app/src/main/java/org/qcode/demo/ui/viewpageandlistview/NewsPageAdapter.java
================================================
package org.qcode.demo.ui.viewpageandlistview;
import android.content.Context;
import android.view.View;
import android.widget.ListView;
import org.qcode.qskinloader.base.utils.Logging;
import java.util.ArrayList;
public class NewsPageAdapter extends RecyclablePageAdapter {
private static final String TAG = "NewsPageAdapter";
private Context mContext;
private ArrayList mList = new ArrayList();
public NewsPageAdapter(Context context) {
mContext = context;
for (int i = 0; i < 15; i++) {
mList.add("测试" + i);
}
}
@Override
public int getCount() {
return mList.size();
}
@Override
public boolean isViewFromObject(View view, Object obj) {
if (view instanceof ListView && null != ((ListView) view).getAdapter()) {
return obj.equals(((DataListAdapter) ((ListView) view).getAdapter()).getIdentifier());
}
return false;
}
@Override
public int getItemPosition(Object object) {
super.getItemPosition(object);
for (int i = 0; i < mList.size(); i++) {
if (mList.get(i).equals(object)) {
return i;
}
}
return -1;
}
@Override
protected Object getItemObject(int position) {
return mList.get(position);
}
@Override
protected ListView createItemView(int itemViewType) {
ListView listView = new ListView(mContext);
return listView;
}
@Override
protected void onBindView(ListView itemView, int position, int itemViewType) {
Logging.d(TAG, "onBindView()| position= " + position);
String str = mList.get(position);
DataListAdapter adapter = new DataListAdapter(mContext, str);
itemView.setAdapter(adapter);
}
@Override
protected void destroyItemView(ListView view) {
}
}
================================================
FILE: app/src/main/java/org/qcode/demo/ui/viewpageandlistview/RecyclablePageAdapter.java
================================================
package org.qcode.demo.ui.viewpageandlistview;
import android.os.Parcelable;
import android.support.v4.view.PagerAdapter;
import android.util.SparseArray;
import android.view.View;
import android.view.ViewGroup;
import org.qcode.qskinloader.base.utils.Logging;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
/**
* 可回收利用的PageAdapter
* T代表界面展示的View
* qqliu
* 2016/5/6.
*/
public abstract class RecyclablePageAdapter extends PagerAdapter {
protected static final int TYPE_DEFAULT = 0;
private static final String TAG = "RecyclablePageAdapter";
private SparseArray> mRecycledViews = new SparseArray>();
private HashMap