Repository: qqliu10u/QSkinLoader Branch: master Commit: 983ba3cc32aa Files: 154 Total size: 242.6 KB Directory structure: gitextract_gfdlk8qj/ ├── .gitignore ├── .idea/ │ ├── compiler.xml │ ├── copyright/ │ │ └── profiles_settings.xml │ ├── encodings.xml │ ├── gradle.xml │ ├── misc.xml │ ├── modules.xml │ └── runConfigurations.xml ├── QSkinLoaderlib/ │ ├── .gitignore │ ├── build.gradle │ ├── proguard-rules.pro │ └── src/ │ └── main/ │ ├── AndroidManifest.xml │ ├── java/ │ │ └── org/ │ │ └── qcode/ │ │ └── qskinloader/ │ │ ├── IActivitySkinEventHandler.java │ │ ├── ILoadSkinListener.java │ │ ├── IResourceLoader.java │ │ ├── IResourceManager.java │ │ ├── ISkinActivity.java │ │ ├── ISkinAttrHandler.java │ │ ├── ISkinAttributeParser.java │ │ ├── ISkinManager.java │ │ ├── ISkinViewHelper.java │ │ ├── IViewCreateListener.java │ │ ├── IWindowViewManager.java │ │ ├── SkinManager.java │ │ ├── attrhandler/ │ │ │ ├── BackgroundAttrHandler.java │ │ │ ├── DividerAttrHandler.java │ │ │ ├── DrawableLeftAttrHandler.java │ │ │ ├── ListSelectorAttrHandler.java │ │ │ ├── RecyclerViewClearSubAttrHandler.java │ │ │ ├── ShadowAttrHandler.java │ │ │ ├── SkinAttrFactory.java │ │ │ ├── SkinAttrUtils.java │ │ │ ├── SrcAttrHandler.java │ │ │ ├── TextColorAttrHandler.java │ │ │ └── TextColorHintAttrHandler.java │ │ ├── base/ │ │ │ ├── observable/ │ │ │ │ ├── INotifyUpdate.java │ │ │ │ ├── IObservable.java │ │ │ │ └── Observable.java │ │ │ └── utils/ │ │ │ ├── CollectionUtils.java │ │ │ ├── HashMapCache.java │ │ │ ├── Logging.java │ │ │ ├── ReflectUtils.java │ │ │ ├── StringUtils.java │ │ │ └── WeakReferenceHelper.java │ │ ├── entity/ │ │ │ ├── DynamicAttr.java │ │ │ ├── SkinAttr.java │ │ │ ├── SkinAttrName.java │ │ │ ├── SkinAttrSet.java │ │ │ └── SkinConstant.java │ │ ├── impl/ │ │ │ ├── ActivitySkinEventHandlerImpl.java │ │ │ ├── SkinAttributeParser.java │ │ │ ├── SkinInflaterFactoryImpl.java │ │ │ ├── SkinManagerImpl.java │ │ │ ├── SkinViewHelperImpl.java │ │ │ ├── ViewSkinTagHelper.java │ │ │ └── WindowViewManager.java │ │ ├── resourceloader/ │ │ │ ├── ILoadResourceCallback.java │ │ │ ├── ResourceManager.java │ │ │ └── impl/ │ │ │ ├── APKResourceLoader.java │ │ │ ├── APKResourceManager.java │ │ │ ├── ConfigChangeResourceLoader.java │ │ │ ├── ConfigChangeResourceManager.java │ │ │ ├── SuffixResourceLoader.java │ │ │ └── SuffixResourceManager.java │ │ └── view/ │ │ └── ShadowImageView.java │ └── res/ │ └── values/ │ ├── skin_attrs.xml │ ├── skin_ids.xml │ └── strings.xml ├── README.md ├── SkinProject/ │ ├── .gitignore │ ├── app/ │ │ ├── .gitignore │ │ ├── build.gradle │ │ ├── proguard-rules.pro │ │ └── src/ │ │ └── main/ │ │ ├── AndroidManifest.xml │ │ ├── java/ │ │ │ └── org/ │ │ │ └── qcode/ │ │ │ └── skinproject/ │ │ │ └── MainActivity.java │ │ └── res/ │ │ ├── drawable/ │ │ │ ├── btn_bg.xml │ │ │ ├── drawable_float_view.xml │ │ │ └── news_item_selector.xml │ │ └── values/ │ │ └── colors.xml │ ├── build.gradle │ ├── gradle.properties │ ├── gradlew │ ├── gradlew.bat │ └── settings.gradle ├── app/ │ ├── .gitignore │ ├── build.gradle │ ├── proguard-rules.pro │ └── src/ │ └── main/ │ ├── AndroidManifest.xml │ ├── assets/ │ │ └── nightMode.skin │ ├── java/ │ │ └── org/ │ │ └── qcode/ │ │ └── demo/ │ │ ├── BaseActivity.java │ │ ├── MainActivity.java │ │ ├── SkinChangeSwitchView.java │ │ ├── SkinDemoApp.java │ │ ├── base/ │ │ │ └── Settings.java │ │ ├── skin/ │ │ │ ├── SkinChangeHelper.java │ │ │ ├── SkinConfigHelper.java │ │ │ ├── SkinConstant.java │ │ │ └── SkinUtils.java │ │ ├── ui/ │ │ │ ├── customattr/ │ │ │ │ ├── CustomAttrViewActivity.java │ │ │ │ ├── CustomTextView.java │ │ │ │ ├── DefBackgroundAttrHandler.java │ │ │ │ └── DefTextColorAttrHandler.java │ │ │ ├── dynamicaddview/ │ │ │ │ └── DynamicAddViewActivity.java │ │ │ ├── gridview/ │ │ │ │ └── GridViewActivity.java │ │ │ ├── otherscene/ │ │ │ │ ├── CustomDialog.java │ │ │ │ ├── FloatView.java │ │ │ │ ├── OtherSceneActivity.java │ │ │ │ ├── SpannableSkinAttr.java │ │ │ │ └── SpannableSkinAttrHandler.java │ │ │ ├── recyclerview/ │ │ │ │ ├── DataRecyclerViewAdapter.java │ │ │ │ └── RecyclerViewActivity.java │ │ │ └── viewpageandlistview/ │ │ │ ├── DataListAdapter.java │ │ │ ├── NewsPageAdapter.java │ │ │ ├── RecyclablePageAdapter.java │ │ │ └── ViewPagerAndListViewActivity.java │ │ └── utils/ │ │ ├── FileUtils.java │ │ ├── UITaskRunner.java │ │ └── UIUtil.java │ └── res/ │ ├── drawable/ │ │ ├── btn_bg.xml │ │ ├── btn_bg_night.xml │ │ ├── drawable_float_view.xml │ │ ├── drawable_float_view_night.xml │ │ ├── news_item_selector.xml │ │ └── news_item_selector_night.xml │ ├── drawable-night/ │ │ ├── btn_bg.xml │ │ ├── drawable_float_view.xml │ │ └── news_item_selector.xml │ ├── layout/ │ │ ├── activity_base_activity.xml │ │ ├── activity_custom_attr_test.xml │ │ ├── activity_dynamic_add_view.xml │ │ ├── activity_grid_view.xml │ │ ├── activity_main.xml │ │ ├── activity_other_scene.xml │ │ ├── activity_recycler_view.xml │ │ ├── activity_viewpager_listview.xml │ │ ├── grid_item_view.xml │ │ ├── layout_dialog_custom.xml │ │ ├── layout_popwindow.xml │ │ └── list_item_view.xml │ ├── values/ │ │ ├── colors.xml │ │ ├── colors_night.xml │ │ ├── dimens.xml │ │ ├── news_attr.xml │ │ ├── strings.xml │ │ └── styles.xml │ ├── values-night/ │ │ └── colors_night.xml │ └── values-w820dp/ │ └── dimens.xml ├── build.gradle ├── gradle/ │ └── wrapper/ │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradle.properties ├── gradlew ├── gradlew.bat └── settings.gradle ================================================ FILE CONTENTS ================================================ ================================================ FILE: .gitignore ================================================ *.iml .gradle /local.properties /.idea/workspace.xml /.idea/libraries .DS_Store /build /captures .externalNativeBuild ================================================ FILE: .idea/compiler.xml ================================================ ================================================ FILE: .idea/copyright/profiles_settings.xml ================================================ ================================================ FILE: .idea/encodings.xml ================================================ ================================================ FILE: .idea/gradle.xml ================================================ ================================================ FILE: .idea/misc.xml ================================================ ================================================ FILE: .idea/modules.xml ================================================ ================================================ FILE: .idea/runConfigurations.xml ================================================ ================================================ FILE: QSkinLoaderlib/.gitignore ================================================ /build ================================================ FILE: QSkinLoaderlib/build.gradle ================================================ apply plugin: 'com.android.library' android { compileSdkVersion 23 buildToolsVersion "23.0.1" defaultConfig { minSdkVersion 14 targetSdkVersion 23 } } dependencies { compile fileTree(include: ['*.jar'], dir: 'libs') compile 'com.android.support:recyclerview-v7:23.2.1' } ================================================ FILE: QSkinLoaderlib/proguard-rules.pro ================================================ # Add project specific ProGuard rules here. # By default, the flags in this file are appended to flags specified # 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: QSkinLoaderlib/src/main/AndroidManifest.xml ================================================ ================================================ FILE: QSkinLoaderlib/src/main/java/org/qcode/qskinloader/IActivitySkinEventHandler.java ================================================ package org.qcode.qskinloader; import android.app.Activity; /** * 与Activity相关的皮肤框架逻辑处理抽象接口 * * a interface defines how to implement an activity skin event handler, * which handles activity's life events like onCreate/onResume... * * qqliu * 2016/9/25. */ public interface IActivitySkinEventHandler { /*** * 应该在setContentView之前调用 * * invoked in activity's onCreate method, * and should be invoked before setContentView. */ void onCreate(Activity activity); /*** * 设置View创建的监听器; * 可替代框架的View创建,或在框架创建了View后进行进一步处理; * * set an IViewCreateListener to LayoutInflater factory, * to delegate view-creating or do further work after view created. * @param viewCreateListener */ void setViewCreateListener(IViewCreateListener viewCreateListener); /*** * 在setContentView之后调用 * * should be invoked after setContentView. */ void onViewCreated(); /*** * 在onStart()回调内调用 * invoked in activity's onStart() */ void onStart(); /*** * 在onResume()回调内调用 * invoked in activity's onResume() */ void onResume(); /*** * 在onWindowFocusChanged()回调内调用 * * invoked in activity's onWindowFocusChanged() */ void onWindowFocusChanged(boolean hasFocus); /*** * 在onPause()回调内调用 * * invoked in activity's onPause() */ void onPause(); /*** * 在onStop()回调内调用 * invoked in activity's onStop() */ void onStop(); /*** * 在onDestroy()回调内调用 * invoked in activity's onDestroy() */ void onDestroy(); /*** * 告知当前界面是否在换肤事件发生时立刻刷新皮肤, * false表示Activity获取到focus时才会刷新, * onCreate之前调用 * * notify the handler whether the activity handles * skin-change event immediately. * invoked before onCreate(); * * @param isImmediate * @return */ IActivitySkinEventHandler setSwitchSkinImmediately(boolean isImmediate); /*** * 告知当前界面是否支持换肤; * onCreate之前调用 * * notify the handler whether the activity * supports skin change. * invoked before onCreate(); * * @param supportChange * @return */ IActivitySkinEventHandler setSupportSkinChange(boolean supportChange); /*** * 告知当前界面的Window的背景色,需要传入资源id; * onCreate之前调用 * * tell handler the activity's background color, * refered as resource id. * invoked before onCreate(); * @param resId * @return */ IActivitySkinEventHandler setWindowBackgroundResource(int resId); /*** * 设置是否需要代理View创建过程; * true表示框架创建View, * false表示不需要创建View,由框架外其他模块创建View, * 此时属性解析动作应在IViewCreateListener内完成。 * * set whether framework need delegate view-creating. * true indicates that the framework does view-creating; * false indicates that the framework don't handle view-creating, * otherwise, the view is created by outside, * and the property-parsing process should be done in interface @ref{IViewCreateListener}. * * @param needDelegateViewCreate * @return */ IActivitySkinEventHandler setNeedDelegateViewCreate(boolean needDelegateViewCreate); /*** * 当皮肤发生变化时,此方法会被调用,来完成Activity的皮肤切换工作 * * when skin changes, @ref{handleSkinUpdate} will be * called to refresh the activity's skin. */ void handleSkinUpdate(); /*** * 获取皮肤属性解析帮助类 * * get a skin attributes parser used when view is creating. * @return */ ISkinAttributeParser getSkinAttributeParser(); } ================================================ FILE: QSkinLoaderlib/src/main/java/org/qcode/qskinloader/ILoadSkinListener.java ================================================ package org.qcode.qskinloader; /** * 加载皮肤过程的事件回调 * * a interface defines the skin loading progress. * * qqliu * 2016/9/24. */ public interface ILoadSkinListener { /*** * 加载皮肤开始 * * notify skin-loading begin event * * @param skinIdentifier */ void onLoadStart(String skinIdentifier); /*** * 加载皮肤完成 * * notify skin-loading success event * * @param skinIdentifier */ void onLoadSuccess(String skinIdentifier); /*** * 加载皮肤失败 * * notify skin-loading fail event * * @param skinIdentifier */ void onLoadFail(String skinIdentifier); } ================================================ FILE: QSkinLoaderlib/src/main/java/org/qcode/qskinloader/IResourceLoader.java ================================================ package org.qcode.qskinloader; import org.qcode.qskinloader.resourceloader.ILoadResourceCallback; /** * 皮肤资源加载器接口 * * A interface defines the load resource behaviour. * A resource loader loads resource and notify load behaviour by @ref{ILoadResourceCallback} * qqliu * 2016/9/25. */ public interface IResourceLoader { /*** * 定义资源加载的行为接口,加载的皮肤以skinIdentifier标识, * 加载结果以loadCallBack通知加载资源结果 * * loads the skin identified by skinIdentifier, * notifies load behaviour by loadCallBack * @param skinIdentifier * @param loadCallBack */ void loadResource(String skinIdentifier, ILoadResourceCallback loadCallBack); } ================================================ FILE: QSkinLoaderlib/src/main/java/org/qcode/qskinloader/IResourceManager.java ================================================ package org.qcode.qskinloader; import android.content.res.ColorStateList; import android.content.res.Resources; import android.graphics.drawable.Drawable; /** * 皮肤资源管理器接口 * * the interface defines what a resource manager should do. * qqliu * 2016/9/25. */ public interface IResourceManager { /*** * 为IResourceManager设置一个真正工作的资源管理器,IResourceManager是baseResource的封装 * set a resource manager as a base worker for the IResourceManager. * @param skinIdentifier 皮肤的唯一标识; * the skin identifier * @param baseResource 真正读取资源的ResourceManager; * the real resource manager which defines how to attach resources. */ void setBaseResource( String skinIdentifier, IResourceManager baseResource); /*** * get skin identifier * @return */ String getSkinIdentifier(); /*** * return whether current is default skin * @return */ boolean isDefault(); /*** * get drawable by resource id * @param resId * @return * @throws Resources.NotFoundException */ Drawable getDrawable(int resId) throws Resources.NotFoundException; /*** * get drawable by resource id and name * @param resId * @param resName * @return * @throws Resources.NotFoundException */ Drawable getDrawable(int resId, String resName) throws Resources.NotFoundException; /*** * get color by resource id * @param resId * @return * @throws Resources.NotFoundException */ int getColor(int resId) throws Resources.NotFoundException; /*** * get color by resource id and resource name * @param resId * @param resName * @return * @throws Resources.NotFoundException */ int getColor(int resId, String resName) throws Resources.NotFoundException; /*** * get ColorStateList by resource id * @param resId * @return * @throws Resources.NotFoundException */ ColorStateList getColorStateList(int resId) throws Resources.NotFoundException; /*** * get ColorStateList by resource id and name * @param resId * @param resName * @return * @throws Resources.NotFoundException */ ColorStateList getColorStateList(int resId, String resName) throws Resources.NotFoundException; /*** * get ColorStateList by resource id and name * @param resId * @param typeName * @param resName * @return * @throws Resources.NotFoundException */ ColorStateList getColorStateList(int resId, String typeName, String resName) throws Resources.NotFoundException; } ================================================ FILE: QSkinLoaderlib/src/main/java/org/qcode/qskinloader/ISkinActivity.java ================================================ package org.qcode.qskinloader; /** * 支持换肤的Activity应实现的接口 * * A interface indicates that this activity supports skin change. * * qqliu * 2016/9/25. */ public interface ISkinActivity { /*** * 是否需要立刻刷新皮肤;默认不立刻换肤 * tells whether refresh skin immediately, if return false, the activity will * be refreshed after focus obtained. * * @return */ boolean isSwitchSkinImmediately(); /*** * 确定是否支持换肤 *tells whether the activity support skin change, * if return false, the activity will not refresh when skin change. * NOTICE: SkinManager.getInstance().applySkin() will ignore the setting * * @return */ boolean isSupportSkinChange(); /*** * 刷新皮肤; * 此处刷新的是皮肤框架管理之外的界面 * * when skin changes, this method will be called, * to notify activity doing something beyond the framework's ability. */ void handleSkinChange(); } ================================================ FILE: QSkinLoaderlib/src/main/java/org/qcode/qskinloader/ISkinAttrHandler.java ================================================ package org.qcode.qskinloader; import android.view.View; import org.qcode.qskinloader.entity.SkinAttr; /** * 皮肤属性处理器的接口 * * an interface indicates how to apply special skin attributes for a view. * * qqliu * 2016/9/24. */ public interface ISkinAttrHandler { /*** * 将属性应用到View上 * * apply skin attribute to view * * @param view * @param skinAttr * @param resourceManager */ void apply(View view, SkinAttr skinAttr, IResourceManager resourceManager); } ================================================ FILE: QSkinLoaderlib/src/main/java/org/qcode/qskinloader/ISkinAttributeParser.java ================================================ package org.qcode.qskinloader; import android.content.Context; import android.util.AttributeSet; import android.view.View; /** * 框架解析皮肤属性的解析帮助类 * * the skin attribute parser defines * how to parse attributes when view is created. * * qqliu * 2016/11/8. */ public interface ISkinAttributeParser { /*** * 是否支持换肤 * * return the parse result whether the view supports skin-change * @param name * @param context * @param attrs * @return */ boolean isSupportSkin(String name, Context context, AttributeSet attrs); /*** * 解析View的皮肤属性 * * parse skin attributes from view-creating process * * @param view * @param name * @param context * @param attrs */ void parseAttribute(View view, String name, Context context, AttributeSet attrs); } ================================================ FILE: QSkinLoaderlib/src/main/java/org/qcode/qskinloader/ISkinManager.java ================================================ package org.qcode.qskinloader; import android.content.Context; import android.view.View; import org.qcode.qskinloader.base.observable.IObservable; /** * 皮肤框架管理类接口 * * the skin manager interface, supporting skin operations. * qqliu * 2016/9/24. */ public interface ISkinManager extends IObservable { /*** * 初始化皮肤框架 * initing the skin framework * @param context */ void init(Context context); /*** * 恢复到默认皮肤 * * restore to default skin * @param defaultSkinIdentifier: default skin identifier, a skin identifier * identifies a special skin. * @param loadListener */ void restoreDefault(String defaultSkinIdentifier, ILoadSkinListener loadListener); /*** * 从指定位置加载一个APK皮肤包;APK皮肤包是另一个未安装的APK应用(只包含资源). * 通过APK皮肤包可以支持动态下载换肤等功能; * * load an apk resources package from file(indicated by skinPath); * an apk resources is an apk application, which only contains resources. * APK supports can be applied when dynamically downloading the skin resouces. * * @param skinPath skinPath is a file path, * and is also used as the skin identifier. * @param loadListener the load result listener */ void loadAPKSkin(String skinPath, ILoadSkinListener loadListener); /*** * 加载指定的皮肤包,皮肤包以skinIdentifier标识, * 依靠resourceLoader加载,并通过loadListener告知皮肤切换结果。 * 由外部指定皮肤加载方式,目前支持APK加载(APKResourceLoader)、后缀方式加载(SuffixResourceLoader)等。 * * load skin for views, the skin is identified by skinIdentifier, * loaded by resourceLoader(currently supports APKResourceLoader/SuffixResourceLoader), * and the load result is notified by loadListener. * @param skinIdentifier the skin identifier * @param resourceLoader the resource loader(currently supports APKResourceLoader/SuffixResourceLoader) * @param loadListener the skin load result listener */ void loadSkin(String skinIdentifier, IResourceLoader resourceLoader, ILoadSkinListener loadListener); /*** * 对View应用当前的皮肤设置,applyChild 表示对View的子元素设置皮肤 * * apply current skin for the view * * @param view * @param applyChild: true indicates apply skin for view's children */ void applySkin(View view, boolean applyChild); /*** * 注册指定属性的处理器,可以通过此方法覆盖默认的属性处理器,也可以定义自定义属性的属性处理器 * * register a skin attributes handler for attribute(named as attrName) * * @param attrName : the attribute name * @param skinAttrHandler : the attribute handler */ void registerSkinAttrHandler(String attrName, ISkinAttrHandler skinAttrHandler); /*** * 移除指定属性的处理器 * * remove a skin attribute handler for a attribute * @param attrName */ void unregisterSkinAttrHandler(String attrName); /*** * 设置一个IResourceManager对象, * 可用来替换默认的ResourceManager实现, * 在属性处理器内收到替换的ResourceManager实现。 * * set a IResourceManager object to framework to replace the default ResourceManager. * @param resourceManager */ void setResourceManager(IResourceManager resourceManager); /*** * 获取框架内的资源管理器对象 * * return the resource manager object used in framework. * @return */ IResourceManager getResourceManager(); } ================================================ FILE: QSkinLoaderlib/src/main/java/org/qcode/qskinloader/ISkinViewHelper.java ================================================ package org.qcode.qskinloader; import org.qcode.qskinloader.entity.DynamicAttr; import org.qcode.qskinloader.entity.SkinAttr; import java.util.List; /** * 皮肤框架中View相关皮肤属性的管理器接口抽象 * * The abstract interface for registering skin attributes dynamically. * qqliu * 2016/10/8. */ public interface ISkinViewHelper { /*** * 给指定View注册一个属性名称为attrName,属性值为resId的皮肤属性; * 此View之前注册的属性会被覆盖; * * apply a skin attribute(named as attrName, value is resId) to View, * the skin attributes previously registered is removed. * @param attrName * @param resId * @return */ ISkinViewHelper setViewAttrs(String attrName, int resId); /*** * 给指定View注册多个皮肤属性; * 此View之前注册的属性会被覆盖; * * apply skin attributes( * named as attrName, value is resId, indicated in DynamicAttr) to View, * the skin attributes previously registered is removed. * * @param dynamicAttrs * @return */ ISkinViewHelper setViewAttrs(DynamicAttr... dynamicAttrs); /*** * 给指定View注册多个皮肤属性; * 此View之前注册的属性会被覆盖; * * apply skin attributes( * named as attrName, value is resId, indicated in SkinAttr) to View, * the skin attributes previously registered is removed. * * @param skinAttrs * @return */ ISkinViewHelper setViewAttrs(SkinAttr... skinAttrs); /*** * 给指定View注册多个皮肤属性; * 此View之前注册的属性会被覆盖; * * apply skin attributes( * named as attrName, value is resId, indicated in DynamicAttr) to View, * the skin attributes previously registered is removed. * * @param dynamicAttrs * @return */ ISkinViewHelper setViewAttrs(List dynamicAttrs); /*** * 给指定View添加一个属性名称为attrName,属性值为resId的皮肤属性; * 此View之前注册的属性不会被覆盖; * * add a skin attribute(named as attrName, value is resId) to View, * the skin attributes previously registered is maintained. * @param attrName * @param resId * @return */ ISkinViewHelper addViewAttrs(String attrName, int resId); /*** * 给指定View添加多个皮肤属性; * 此View之前注册的属性不会被覆盖; * * add skin attributes( * named as attrName, value is resId, indicated in DynamicAttr) to View, * the skin attributes previously registered is maintained. * * @param dynamicAttrs * @return */ ISkinViewHelper addViewAttrs(DynamicAttr... dynamicAttrs); /*** * 给指定View添加多个皮肤属性; * 此View之前注册的属性不会被覆盖; * * add skin attributes( * named as attrName, value is resId, indicated in SkinAttr) to View, * the skin attributes previously registered is maintained. * * @param skinAttrs * @return */ ISkinViewHelper addViewAttrs(SkinAttr... skinAttrs); /*** * 给指定View添加多个皮肤属性; * 此View之前注册的属性不会被覆盖; * * add skin attributes( * named as attrName, value is resId, indicated in DynamicAttr) to View, * the skin attributes previously registered is maintained. * * @param dynamicAttrs * @return */ ISkinViewHelper addViewAttrs(List dynamicAttrs); /*** * 移除View内注册的皮肤属性 * * remove all skin attributes for a view * @param clearChild true表示同时移除View的子元素的皮肤属性; * false只移除View的皮肤属性; * true indicates also removing skin attributes for the view's children, * false means only remove skin attributes for the view itself; * @return */ ISkinViewHelper cleanAttrs(boolean clearChild); /*** * 对View应用当前的皮肤设置; * * apply current skin for the view * @param applyChild true表示对View子元素也应用皮肤; * false表示只对View应用皮肤; * true indicates apply skin for the view's children, * false otherwise; */ void applySkin(boolean applyChild); } ================================================ FILE: QSkinLoaderlib/src/main/java/org/qcode/qskinloader/IViewCreateListener.java ================================================ package org.qcode.qskinloader; import android.content.Context; import android.util.AttributeSet; import android.view.View; /** * 创建View的过程中的回调 * qqliu * 2016/10/17. */ public interface IViewCreateListener { /*** * 创建View之前的回调; invoked before view create, * should be used to create view outside the framework if needed * @param name * @param context * @param attrs * @return */ View beforeCreate(String name, Context context, AttributeSet attrs); /*** * 创建View之后的回调; invoked after view create, * should be used to parse view attributes outside the framework if needed * @param view * @param name * @param context * @param attrs */ void afterCreated(View view, String name, Context context, AttributeSet attrs); } ================================================ FILE: QSkinLoaderlib/src/main/java/org/qcode/qskinloader/IWindowViewManager.java ================================================ package org.qcode.qskinloader; import android.view.View; import java.util.List; /** * 皮肤框架对直接加载在WindowManager上的View的管理器; * 包含:悬浮窗、popWindow、Dialog等持有的View; * 一般是将Activity的View树以外的View加入框架内管理; * 建议成对调用{@ref{addWindowView}和{@ref{removeWindowView}; * * The manager of Views * (added to WindowManager, such as PopWindow/Dialog/Float View...); * Basically, the views not add to Activity's View tree * should be added to IWindowViewManager for skin changing purpose. * addWindowView should be used with removeWindowView in pairs. * qqliu * 2016/10/8. */ public interface IWindowViewManager { /*** * 在框架内增加View的引用,刷新皮肤时会刷新此View及其所有子元素; * ,应与{@ref{removeWindowView}成对使用 * * add a view in framework, so that we can refresh * the view(and its children)'s skin immediately. * should be used with {@ref{removeWindowView} in pairs. * @param view * @return */ IWindowViewManager addWindowView(View view); /*** * 从框架内移除View的引用; * 应与{@ref{addWindowView}成对使用; * * remove a view from framework, see {@ref{addWindowView} * @param view * @return */ IWindowViewManager removeWindowView(View view); /*** * 清空框架内持有的所有View的引用; * clear all views maintained in framework * @return */ IWindowViewManager clear(); /*** * 对框架内持有的所有View刷新皮肤; * * refresh skin for views maintained in framework * @param applyChild 表示刷新是否同时刷新View的子元素,一般传入true; * true means we also refresh the views' children, * most time true is needed. */ void applySkinForViews(boolean applyChild); /*** * 获取注册到框架内维护的所有View * * return all the views maintained in the framework. * @return */ List getWindowViewList(); } ================================================ FILE: QSkinLoaderlib/src/main/java/org/qcode/qskinloader/SkinManager.java ================================================ package org.qcode.qskinloader; import android.view.View; import org.qcode.qskinloader.impl.ActivitySkinEventHandlerImpl; import org.qcode.qskinloader.impl.SkinManagerImpl; import org.qcode.qskinloader.impl.SkinViewHelperImpl; import org.qcode.qskinloader.impl.WindowViewManager; /** * 皮肤框架对外接口; * * the base entrance class for the QSkinLoader library * qqliu * 2016/10/8. */ public class SkinManager { /*** * 获取皮肤管理类实例; * * return an ISkinManager object to deal with skin manager events * such as init/skin change/apply skin for view ... * @return */ public static ISkinManager getInstance() { return SkinManagerImpl.getInstance(); } /*** * 获取View的皮肤属性管理类,支持链式编程,可动态操作View的皮肤属性; * * return an ISinViewHelper to add/remove skin attrs dynamically; * ISkinViewHelper supports the chain programming style; * @param view * @return */ public static ISkinViewHelper with(View view) { return new SkinViewHelperImpl(view); } /*** * 获取Window View的管理类; * 框架只能自动支持刷新Activity的ContentView, * 对于PopupWindow/对话框/悬浮窗等View, * 只能通过此方法注册到框架内来保证换肤效果; * * return an IWindowViewManager to add/remove view to the framework; * the framework only supports apply skin for Activity's * content view(by findViewById(android.R.id.content)); * so that other views(PopupWindow/Dialog/View directly added to WindowManager) * should be add to framework for skin changing. * @return */ public static IWindowViewManager getWindowViewManager() { return WindowViewManager.getInstance(); } /*** * 创建一个新的Activity的皮肤事件处理器; * IActivitySkinEventHandler用于代理完成 * Activity内各生命周期与皮肤相关的逻辑; * * return a new IActivitySkinEventHandler object for Activity; * IActivitySkinEventHandler handles event for the skin framework, * and should be notified when activity life state change(onCreate/onResume/onPause...) * @return */ public static IActivitySkinEventHandler newActivitySkinEventHandler() { return new ActivitySkinEventHandlerImpl(); } } ================================================ FILE: QSkinLoaderlib/src/main/java/org/qcode/qskinloader/attrhandler/BackgroundAttrHandler.java ================================================ package org.qcode.qskinloader.attrhandler; import android.graphics.drawable.Drawable; import android.view.View; import org.qcode.qskinloader.ISkinAttrHandler; import org.qcode.qskinloader.entity.SkinAttr; import org.qcode.qskinloader.entity.SkinAttrName; import org.qcode.qskinloader.IResourceManager; /*** * 背景属性的换肤支持(android:background) */ class BackgroundAttrHandler implements ISkinAttrHandler { @Override public void apply(View view, SkinAttr skinAttr, IResourceManager resourceManager) { if(null == view || null == skinAttr || !(SkinAttrName.BACKGROUND.equals(skinAttr.mAttrName))) { return; } Drawable drawable = SkinAttrUtils.getDrawable( resourceManager, skinAttr.mAttrValueRefId, skinAttr.mAttrValueTypeName, skinAttr.mAttrValueRefName); if(null != drawable) { view.setBackgroundDrawable(drawable); } } } ================================================ FILE: QSkinLoaderlib/src/main/java/org/qcode/qskinloader/attrhandler/DividerAttrHandler.java ================================================ package org.qcode.qskinloader.attrhandler; import android.graphics.drawable.Drawable; import android.view.View; import android.widget.ListView; import org.qcode.qskinloader.ISkinAttrHandler; import org.qcode.qskinloader.entity.SkinAttr; import org.qcode.qskinloader.entity.SkinAttrName; import org.qcode.qskinloader.IResourceManager; /*** * ListView divider属性的换肤支持(android:divider) */ class DividerAttrHandler implements ISkinAttrHandler { @Override public void apply(View view, SkinAttr skinAttr, IResourceManager resourceManager) { if (null == view || null == skinAttr || !(SkinAttrName.DIVIDER.equals(skinAttr.mAttrName))) { return; } if (!(view instanceof ListView)) { return; } ListView tv = (ListView) view; Drawable drawable = SkinAttrUtils.getDrawable( resourceManager, skinAttr.mAttrValueRefId, skinAttr.mAttrValueTypeName, skinAttr.mAttrValueRefName); if (null != drawable) { tv.setDivider(drawable); } } } ================================================ FILE: QSkinLoaderlib/src/main/java/org/qcode/qskinloader/attrhandler/DrawableLeftAttrHandler.java ================================================ package org.qcode.qskinloader.attrhandler; import android.graphics.drawable.Drawable; import android.view.View; import android.widget.TextView; import org.qcode.qskinloader.ISkinAttrHandler; import org.qcode.qskinloader.entity.SkinAttr; import org.qcode.qskinloader.entity.SkinAttrName; import org.qcode.qskinloader.IResourceManager; /** * TextView的drawableLeft属性处理 * qqliu * 2016/9/27. */ class DrawableLeftAttrHandler implements ISkinAttrHandler { @Override public void apply(View view, SkinAttr skinAttr, IResourceManager resourceManager) { if(null == view || null == skinAttr || !(SkinAttrName.DRAWABLE_LEFT.equals(skinAttr.mAttrName))) { return; } if (!(view instanceof TextView)) { return; } Drawable drawable = SkinAttrUtils.getDrawable(resourceManager, skinAttr.mAttrValueRefId, skinAttr.mAttrValueTypeName, skinAttr.mAttrValueRefName); if(null != drawable) { ((TextView)view).setCompoundDrawablesWithIntrinsicBounds( drawable, null, null, null); } } } ================================================ FILE: QSkinLoaderlib/src/main/java/org/qcode/qskinloader/attrhandler/ListSelectorAttrHandler.java ================================================ package org.qcode.qskinloader.attrhandler; /*** * ListView selector属性的换肤支持(android:listSelector) */ import android.graphics.drawable.Drawable; import android.view.View; import android.widget.AbsListView; import org.qcode.qskinloader.ISkinAttrHandler; import org.qcode.qskinloader.entity.SkinAttr; import org.qcode.qskinloader.entity.SkinAttrName; import org.qcode.qskinloader.IResourceManager; class ListSelectorAttrHandler implements ISkinAttrHandler { @Override public void apply(View view, SkinAttr skinAttr, IResourceManager resourceManager) { if (null == view || null == skinAttr || !(SkinAttrName.LIST_SELECTOR.equals(skinAttr.mAttrName))) { return; } if (!(view instanceof AbsListView)) { return; } Drawable drawable = SkinAttrUtils.getDrawable( resourceManager, skinAttr.mAttrValueRefId, skinAttr.mAttrValueTypeName, skinAttr.mAttrValueRefName); if (null != drawable) { ((AbsListView) view).setSelector(drawable); } } } ================================================ FILE: QSkinLoaderlib/src/main/java/org/qcode/qskinloader/attrhandler/RecyclerViewClearSubAttrHandler.java ================================================ package org.qcode.qskinloader.attrhandler; import android.support.v7.widget.RecyclerView; import android.view.View; import org.qcode.qskinloader.IResourceManager; import org.qcode.qskinloader.ISkinAttrHandler; import org.qcode.qskinloader.base.utils.Logging; import org.qcode.qskinloader.entity.SkinAttr; import org.qcode.qskinloader.entity.SkinAttrName; import java.lang.reflect.Field; import java.lang.reflect.Method; /** * 清除RecyclerView的缓存View池,保证不出现夜间模式与白天模式共存的问题 * qqliu * 2016/9/27. */ class RecyclerViewClearSubAttrHandler implements ISkinAttrHandler { private static final String TAG = "RecyclerViewClearSubAttr"; @Override public void apply(View view, SkinAttr skinAttr, IResourceManager resourceManager) { if(null == view || null == skinAttr || !(SkinAttrName.CLEAR_RECYCLER_VIEW.equals(skinAttr.mAttrName))) { return; } if(!(view instanceof RecyclerView)) { return; } refreshRecyclerView((RecyclerView) view); } private void refreshRecyclerView(RecyclerView recyclerView) { Logging.d(TAG, "refreshRecyclerView()| clear recycler view"); Class recyclerViewClass = RecyclerView.class; try { Field declaredField = recyclerViewClass.getDeclaredField("mRecycler" ); declaredField.setAccessible(true); Method declaredMethod = Class.forName(RecyclerView.Recycler. class.getName()).getDeclaredMethod("clear", (Class[]) new Class[0]); declaredMethod.setAccessible(true); declaredMethod.invoke(declaredField.get(recyclerView), new Object[0]); RecyclerView.RecycledViewPool recycledViewPool = recyclerView.getRecycledViewPool(); recycledViewPool.clear(); } catch (Exception ex) { Logging.d(TAG, "refreshRecyclerView()| error happened", ex); } } } ================================================ FILE: QSkinLoaderlib/src/main/java/org/qcode/qskinloader/attrhandler/ShadowAttrHandler.java ================================================ package org.qcode.qskinloader.attrhandler; import android.graphics.Color; import android.graphics.PorterDuff; import android.view.View; import android.widget.ImageView; import org.qcode.qskinloader.ISkinAttrHandler; import org.qcode.qskinloader.entity.SkinAttr; import org.qcode.qskinloader.entity.SkinAttrName; import org.qcode.qskinloader.entity.SkinConstant; import org.qcode.qskinloader.IResourceManager; import org.qcode.qskinloader.view.ShadowImageView; /** * 蒙层阴影属性,仅支持ImageView,且蒙层只能是int型颜色 * qqliu * 2016/9/25. */ class ShadowAttrHandler implements ISkinAttrHandler { @Override public void apply(View view, SkinAttr skinAttr, IResourceManager resourceManager) { if (null == view || null == skinAttr || !(SkinAttrName.DRAW_SHADOW.equals(skinAttr.mAttrName))) { return; } if (!(view instanceof ImageView)) { return; } if (resourceManager.isDefault()) { if (view instanceof ShadowImageView) { ShadowImageView imageView = (ShadowImageView) view; imageView.setShadowColor(Color.WHITE); } else { ((ImageView) view).setColorFilter(Color.WHITE, PorterDuff.Mode.MULTIPLY); } } else { if (SkinConstant.RES_TYPE_NAME_COLOR.equals(skinAttr.mAttrValueTypeName)) { int bgColor = resourceManager.getColor( skinAttr.mAttrValueRefId, skinAttr.mAttrValueRefName); if (view instanceof ShadowImageView) { ShadowImageView imageView = (ShadowImageView) view; imageView.setShadowColor(bgColor); } else { ((ImageView) view).setColorFilter(bgColor, PorterDuff.Mode.MULTIPLY); } } } } } ================================================ FILE: QSkinLoaderlib/src/main/java/org/qcode/qskinloader/attrhandler/SkinAttrFactory.java ================================================ package org.qcode.qskinloader.attrhandler; import android.text.TextUtils; import org.qcode.qskinloader.ISkinAttrHandler; import org.qcode.qskinloader.base.utils.StringUtils; import org.qcode.qskinloader.entity.DynamicAttr; import org.qcode.qskinloader.entity.SkinAttr; import org.qcode.qskinloader.entity.SkinAttrName; import java.util.HashMap; import java.util.Map; /*** * 获取支持的属性处理器工厂 */ public class SkinAttrFactory { //存放支持的换肤属性和对应的处理器 private static Map mSupportAttrHandler = new HashMap(); //静态注册支持的属性和处理器 static { registerSkinAttrHandler(SkinAttrName.BACKGROUND, new BackgroundAttrHandler()); registerSkinAttrHandler(SkinAttrName.SRC, new SrcAttrHandler()); registerSkinAttrHandler(SkinAttrName.TEXT_COLOR, new TextColorAttrHandler()); registerSkinAttrHandler(SkinAttrName.TEXT_COLOR_HINT, new TextColorHintAttrHandler()); registerSkinAttrHandler(SkinAttrName.LIST_SELECTOR, new ListSelectorAttrHandler()); registerSkinAttrHandler(SkinAttrName.DIVIDER, new DividerAttrHandler()); registerSkinAttrHandler(SkinAttrName.DRAWABLE_LEFT, new DrawableLeftAttrHandler()); registerSkinAttrHandler(SkinAttrName.DRAW_SHADOW, new ShadowAttrHandler()); registerSkinAttrHandler(SkinAttrName.CLEAR_RECYCLER_VIEW, new RecyclerViewClearSubAttrHandler()); } /*** * 创建一个新的皮肤对象 * @param attrName * @param attrValueRefId * @param attrValueRefName * @param typeName * @return */ public static SkinAttr newSkinAttr( String attrName, int attrValueRefId, String attrValueRefName, String typeName) { if (StringUtils.isEmpty(attrName)) { return null; } SkinAttr skinAttr = new SkinAttr(); skinAttr.mAttrName = attrName; skinAttr.mAttrValueRefId = attrValueRefId; skinAttr.mAttrValueRefName = attrValueRefName; skinAttr.mAttrValueTypeName = typeName; return skinAttr; } /*** * 基于属性名称生成SkinAttr * @param attrName * @return */ public static SkinAttr newSkinAttr(String attrName) { if (StringUtils.isEmpty(attrName)) { return null; } SkinAttr skinAttr = new SkinAttr(); skinAttr.mAttrName = attrName; return skinAttr; } /*** * 获取特定属性的换肤处理器 * * @param attrName * @return */ public static ISkinAttrHandler getSkinAttrHandler(String attrName) { return mSupportAttrHandler.get(attrName); } /*** * 是否支持某属性换肤 * * @param attrName * @return */ public static boolean isSupportedAttr(String attrName) { return null != getSkinAttrHandler(attrName); } /**** * 注册对某个属性的换肤支持 * * @param attrName */ public static void registerSkinAttrHandler(String attrName, ISkinAttrHandler skinAttrHandler) { if (TextUtils.isEmpty(attrName) || null == skinAttrHandler) { return; } mSupportAttrHandler.put(attrName, skinAttrHandler); } /*** * 移除对某个属性的换肤支持 * * @param attrName */ public static void removeSkinAttrHandler(String attrName) { if (TextUtils.isEmpty(attrName)) { return; } mSupportAttrHandler.remove(attrName); } } ================================================ FILE: QSkinLoaderlib/src/main/java/org/qcode/qskinloader/attrhandler/SkinAttrUtils.java ================================================ package org.qcode.qskinloader.attrhandler; import android.graphics.drawable.ColorDrawable; import android.graphics.drawable.Drawable; import android.view.View; import org.qcode.qskinloader.IResourceManager; import org.qcode.qskinloader.ISkinAttrHandler; import org.qcode.qskinloader.base.utils.CollectionUtils; import org.qcode.qskinloader.entity.SkinAttr; import org.qcode.qskinloader.entity.SkinAttrSet; import org.qcode.qskinloader.entity.SkinConstant; import java.util.List; /** * 皮肤属性的辅助帮助类 * qqliu * 2016/9/25. */ public class SkinAttrUtils { /*** * 获取指定资源的drawable,支持的resId为int型颜色和drawable * @param resourceManager * @param resId * @param resTypeName * @param resName * @return */ public static Drawable getDrawable(IResourceManager resourceManager, int resId, String resTypeName, String resName) { if (SkinConstant.RES_TYPE_NAME_COLOR.equals(resTypeName)) { int bgColor = resourceManager.getColor( resId, resName); return new ColorDrawable(bgColor); } else if (SkinConstant.RES_TYPE_NAME_DRAWABLE.equals(resTypeName)) { Drawable drawable = resourceManager.getDrawable( resId, resName); return drawable; } else if (SkinConstant.RES_TYPE_NAME_MIPMAP.equals(resTypeName)) { Drawable drawable = resourceManager.getDrawable( resId, resName); return drawable; } return null; } /*** * 对View应用指定的属性集合 * @param view * @param skinAttrSet * @param resourceManager */ public static void applySkinAttrs(View view, SkinAttrSet skinAttrSet, IResourceManager resourceManager) { if(null == view || null == skinAttrSet) { return; } List attrArrayList = skinAttrSet.getAttrList(); if (CollectionUtils.isEmpty(attrArrayList)) { return; } for (SkinAttr attr : attrArrayList) { ISkinAttrHandler attrHandler = SkinAttrFactory.getSkinAttrHandler(attr.mAttrName); if(null != attrHandler) { attrHandler.apply(view, attr, resourceManager); } } } } ================================================ FILE: QSkinLoaderlib/src/main/java/org/qcode/qskinloader/attrhandler/SrcAttrHandler.java ================================================ package org.qcode.qskinloader.attrhandler; import android.graphics.drawable.AnimationDrawable; import android.graphics.drawable.Drawable; import android.view.View; import android.widget.ImageView; import org.qcode.qskinloader.IResourceManager; import org.qcode.qskinloader.ISkinAttrHandler; import org.qcode.qskinloader.entity.SkinAttr; import org.qcode.qskinloader.entity.SkinAttrName; /*** * src属性的换肤支持(android:src) */ class SrcAttrHandler implements ISkinAttrHandler { @Override public void apply(View view, SkinAttr skinAttr, IResourceManager resourceManager) { if (null == view || null == skinAttr || !(SkinAttrName.SRC.equals(skinAttr.mAttrName))) { return; } if (!(view instanceof ImageView)) { return; } boolean isAnimationDrawable = false; boolean isRunning = false; Drawable originalDrawable = ((ImageView) view).getDrawable(); if (originalDrawable instanceof AnimationDrawable) { AnimationDrawable animationDrawable = (AnimationDrawable) originalDrawable; isAnimationDrawable = true; isRunning = animationDrawable.isRunning(); } Drawable drawable = SkinAttrUtils.getDrawable( resourceManager, skinAttr.mAttrValueRefId, skinAttr.mAttrValueTypeName, skinAttr.mAttrValueRefName); if (null != drawable) { ((ImageView) view).setImageDrawable(drawable); if (isAnimationDrawable && drawable instanceof AnimationDrawable) { AnimationDrawable animationDrawable = ((AnimationDrawable) drawable); if (isRunning) { animationDrawable.start(); } else { animationDrawable.stop(); } } } } } ================================================ FILE: QSkinLoaderlib/src/main/java/org/qcode/qskinloader/attrhandler/TextColorAttrHandler.java ================================================ package org.qcode.qskinloader.attrhandler; import android.content.res.ColorStateList; import android.view.View; import android.widget.TextView; import org.qcode.qskinloader.IResourceManager; import org.qcode.qskinloader.ISkinAttrHandler; import org.qcode.qskinloader.entity.SkinAttr; import org.qcode.qskinloader.entity.SkinAttrName; import org.qcode.qskinloader.entity.SkinConstant; /*** * 文字颜色属性的换肤支持(android:textColor) */ class TextColorAttrHandler implements ISkinAttrHandler { @Override public void apply(View view, SkinAttr skinAttr, IResourceManager resourceManager) { if (null == view || null == skinAttr || !(SkinAttrName.TEXT_COLOR.equals(skinAttr.mAttrName))) { return; } if (!(view instanceof TextView)) { return; } TextView tv = (TextView) view; if (SkinConstant.RES_TYPE_NAME_COLOR.equals(skinAttr.mAttrValueTypeName) || SkinConstant.RES_TYPE_NAME_DRAWABLE.equals(skinAttr.mAttrValueTypeName)) { //按照ColorStateList引用来解析; //context.getResources().getColor()方法可以取纯颜色,也可以取ColorStateList引用内的颜色, //如果取的是ColorStateList,则取其中默认颜色; //同时,context.getResources().getColorStateList()方法也可以取纯颜色生成一个ColorStateList ColorStateList textColor = resourceManager.getColorStateList( skinAttr.mAttrValueRefId, skinAttr.mAttrValueTypeName, skinAttr.mAttrValueRefName); tv.setTextColor(textColor); } } } ================================================ FILE: QSkinLoaderlib/src/main/java/org/qcode/qskinloader/attrhandler/TextColorHintAttrHandler.java ================================================ package org.qcode.qskinloader.attrhandler; import android.content.res.ColorStateList; import android.view.View; import android.widget.TextView; import org.qcode.qskinloader.IResourceManager; import org.qcode.qskinloader.ISkinAttrHandler; import org.qcode.qskinloader.entity.SkinAttr; import org.qcode.qskinloader.entity.SkinAttrName; import org.qcode.qskinloader.entity.SkinConstant; /*** * 文字提示颜色属性的换肤支持(android:textColorHint) */ class TextColorHintAttrHandler implements ISkinAttrHandler { @Override public void apply(View view, SkinAttr skinAttr, IResourceManager resourceManager) { if (null == view || null == skinAttr || !(SkinAttrName.TEXT_COLOR_HINT.equals(skinAttr.mAttrName))) { return; } if (!(view instanceof TextView)) { return; } TextView tv = (TextView) view; if (SkinConstant.RES_TYPE_NAME_COLOR.equals(skinAttr.mAttrValueTypeName)) { //按照ColorStateList引用来解析; //context.getResources().getColor()方法可以取纯颜色,也可以取ColorStateList引用内的颜色, //如果取的是ColorStateList,则取其中默认颜色; //同时,context.getResources().getColorStateList()方法也可以取纯颜色生成一个ColorStateList ColorStateList textHintColor = resourceManager.getColorStateList( skinAttr.mAttrValueRefId, skinAttr.mAttrValueRefName); tv.setHintTextColor(textHintColor); } } } ================================================ FILE: QSkinLoaderlib/src/main/java/org/qcode/qskinloader/base/observable/INotifyUpdate.java ================================================ package org.qcode.qskinloader.base.observable; /** * 通知观察者更新 * qqliu * 2016/9/19. */ public interface INotifyUpdate { /*** * 通知观察者发生了标识为identifier的事件,事件参数是params * @param callback 观察者 * @param identifier 事件标识 * @param params 事件参数 * @return 返回true截断事件传播,false继续事件传播 */ boolean notifyEvent(T callback, String identifier, Object... params); } ================================================ FILE: QSkinLoaderlib/src/main/java/org/qcode/qskinloader/base/observable/IObservable.java ================================================ package org.qcode.qskinloader.base.observable; /** * 可观察对象的抽象接口,T为观察者 * qqliu * 2016/9/19. */ public interface IObservable { /*** * 增加新的观察者 * * @param observer */ void addObserver(T observer); /*** * 删除观察者 * * @param observer */ void removeObserver(T observer); /*** * 告知观察者发生了变化 * @param callback */ void notifyUpdate(INotifyUpdate callback, String identifier, Object... params); } ================================================ FILE: QSkinLoaderlib/src/main/java/org/qcode/qskinloader/base/observable/Observable.java ================================================ package org.qcode.qskinloader.base.observable; import java.util.ArrayList; /** * 观察者通用逻辑 * qqliu * 2016/9/19. */ public class Observable implements IObservable { private ArrayList mObservers; @Override public void addObserver(T observer) { if (mObservers == null) { mObservers = new ArrayList(); } if (!mObservers.contains(observer)) { mObservers.add(observer); } } @Override public void removeObserver(T observer) { if (mObservers == null) { return; } if (mObservers.contains(observer)) { mObservers.remove(observer); } } @Override public void notifyUpdate(INotifyUpdate listener, String identifier, Object... params) { if (mObservers == null || null == listener) { return; } ArrayList tmpListeners = (ArrayList) mObservers.clone(); for (T observer : tmpListeners) { listener.notifyEvent(observer, identifier, params); } } } ================================================ FILE: QSkinLoaderlib/src/main/java/org/qcode/qskinloader/base/utils/CollectionUtils.java ================================================ package org.qcode.qskinloader.base.utils; import java.util.Collection; /** * qqliu * 2016/9/25. */ public class CollectionUtils { public static boolean isEmpty(Collection collection){ return null == collection || collection.size() <= 0; } public static boolean isEmpty(T... array){ return null == array || array.length <= 0; } } ================================================ FILE: QSkinLoaderlib/src/main/java/org/qcode/qskinloader/base/utils/HashMapCache.java ================================================ package org.qcode.qskinloader.base.utils; import java.lang.ref.WeakReference; import java.util.HashMap; /** * 维护一个缓存Map,包括强引用和弱引用两种维护方式; * 两种方式中key都是强引用; * qqliu * 2016/9/19. */ public class HashMapCache { private HashMap mCacheMap = null; private HashMap> mWeakCacheMap = null; /*** * @param isStrongReference true 强引用,false弱引用 */ public HashMapCache(boolean isStrongReference) { if (isStrongReference) { mCacheMap = new HashMap(); } else { mWeakCacheMap = new HashMap>(); } } public V getCache(K key) { if(null == key) { return null; } if(null != mCacheMap) { return mCacheMap.get(key); } else { WeakReference refValue = mWeakCacheMap.get(key); if(null != refValue) { return refValue.get(); } return null; } } public void addCache(K key, V value) { if(null == key) { return; } if(null != mCacheMap) { mCacheMap.put(key, value); } else { WeakReference refValue = new WeakReference(value); mWeakCacheMap.put(key, refValue); } } } ================================================ FILE: QSkinLoaderlib/src/main/java/org/qcode/qskinloader/base/utils/Logging.java ================================================ package org.qcode.qskinloader.base.utils; import android.util.Log; public class Logging { protected static boolean mLoggingEnabled = true; private static final String PRE_TAG = "SkinLoader_"; public static void setDebugLogging(boolean enabled) { mLoggingEnabled = enabled; } public static boolean isDebugLogging() { return mLoggingEnabled; } public static int v(String tag, String msg) { int result = 0; if (mLoggingEnabled) { result = Log.v(PRE_TAG + tag, msg); } return result; } public static int v(String tag, String msg, Throwable tr) { int result = 0; if (mLoggingEnabled) { result = Log.v(PRE_TAG + tag, msg, tr); } return result; } public static int d(String tag, String msg) { int result = 0; if (mLoggingEnabled) { result = Log.d(PRE_TAG + tag, msg); } return result; } public static int d(String tag, String msg, Throwable tr) { int result = 0; if (mLoggingEnabled) { result = Log.d(PRE_TAG + tag, msg, tr); } return result; } public static int i(String tag, String msg) { int result = 0; if (mLoggingEnabled) { result = Log.i(PRE_TAG + tag, msg); } return result; } public static int i(String tag, String msg, Throwable tr) { int result = 0; if (mLoggingEnabled) { result = Log.i(PRE_TAG + tag, msg, tr); } return result; } public static int w(String tag, String msg) { int result = 0; if (mLoggingEnabled) { result = Log.w(PRE_TAG + tag, msg); } return result; } public static int w(String tag, String msg, Throwable tr) { int result = 0; if (mLoggingEnabled) { result = Log.w(PRE_TAG + tag, msg, tr); } return result; } public static int w(String tag, Throwable tr) { int result = 0; if (mLoggingEnabled) { result = Log.w(PRE_TAG + tag, tr); } return result; } public static int e(String tag, String msg) { int result = 0; if (mLoggingEnabled) { result = Log.e(PRE_TAG + tag, msg); } return result; } public static int e(String tag, String msg, Throwable tr) { int result = 0; if (mLoggingEnabled) { result = Log.e(PRE_TAG + tag, msg, tr); } return result; } } ================================================ FILE: QSkinLoaderlib/src/main/java/org/qcode/qskinloader/base/utils/ReflectUtils.java ================================================ package org.qcode.qskinloader.base.utils; import java.lang.reflect.Field; /*** * 反射帮助类 * created at 2017/12/31 */ public class ReflectUtils { private static final String TAG = "ReflectUtils"; private static Field getDeclaredField(Object object, String fieldName) throws NoSuchFieldException { for (Class clz = object.getClass(); Object.class != clz; clz = clz.getSuperclass()) { try { return clz.getDeclaredField(fieldName); } catch (Exception ex) { Logging.d(TAG, "getDeclaredField()| field " + fieldName + " is not in class: " + clz.getSimpleName()); } } throw new NoSuchFieldException("field " + fieldName + " NOT found"); } /*** * 获取指定field的value * * @param object * @param fieldName * @param * @return */ public static T getFieldValue(Object object, String fieldName) throws NoSuchFieldException, IllegalAccessException { if (null == object || StringUtils.isEmpty(fieldName)) { return null; } Field field = getDeclaredField(object, fieldName); field.setAccessible(true); Object value = field.get(object); return (T) value; } /*** * 获取指定field的value,无异常 * * @param obj * @param fieldName * @param * @return */ public static T getFieldValueOpt(Object obj, String fieldName) { try { return getFieldValue(obj, fieldName); } catch (Exception ex) { Logging.d(TAG, "getFieldValueOpt()| error happened", ex); return null; } } /*** * 设置field的值 * * @param object * @param fieldName * @param value * @throws NoSuchFieldException * @throws IllegalAccessException */ public static void setFieldValue(Object object, String fieldName, Object value) throws NoSuchFieldException, IllegalAccessException { if (null == object || StringUtils.isEmpty(fieldName)) { return; } Field field = getDeclaredField(object, fieldName); field.setAccessible(true); field.set(object, value); } /*** * 设置field的值,无异常 * * @param object * @param fieldName * @param value */ public static void setFieldValueOpt(Object object, String fieldName, Object value) { try { setFieldValue(object, fieldName, value); } catch (Exception ex) { Logging.d(TAG, "setFieldValueOpt()| error happened", ex); } } } ================================================ FILE: QSkinLoaderlib/src/main/java/org/qcode/qskinloader/base/utils/StringUtils.java ================================================ package org.qcode.qskinloader.base.utils; /** * qqliu * 2016/9/25. */ public class StringUtils { public static boolean isEmpty(CharSequence sequence) { return null == sequence || sequence.length() <= 0; } } ================================================ FILE: QSkinLoaderlib/src/main/java/org/qcode/qskinloader/base/utils/WeakReferenceHelper.java ================================================ package com.iflytek.skin.manager.base.utils; import java.lang.ref.WeakReference; /** * qqliu * 2016/10/19. */ public class WeakReferenceHelper { private WeakReference mRef; public WeakReferenceHelper(T t) { setData(t); } public T getData() { if(null == mRef) { return null; } return mRef.get(); } public void setData(T t) { this.mRef = new WeakReference(t); } @Override public String toString() { return "WeakReferenceHelper{" + "mData= " + (null == mRef ? "NULL" : mRef.get()) + '}'; } } ================================================ FILE: QSkinLoaderlib/src/main/java/org/qcode/qskinloader/entity/DynamicAttr.java ================================================ package org.qcode.qskinloader.entity; /** * 动态代码设置皮肤属性的实体类 * * qqliu * 2016/9/25. */ public class DynamicAttr { /*** * 对应View的属性 */ public String mAttrName; /*** * 属性值对应的reference id值,类似R.color.XX */ public int mAttrValueRefId = -1; /**是否设置了属性值引用*/ public boolean hasSetValueRef = false; /*** * 是否保留dynamicAttr; * 子类继承DynamicAttr时可以改变此属性来保留自定义的属性值 */ public boolean keepInstance = false; public DynamicAttr(String attrName) { this.mAttrName = attrName; hasSetValueRef = false; keepInstance = false; } public DynamicAttr(String attrName, int attrValueRefId) { this.mAttrName = attrName; this.mAttrValueRefId = attrValueRefId; hasSetValueRef = true; keepInstance = false; } @Override public String toString() { return "DynamicAttr{" + "mAttrName='" + mAttrName + '\'' + ", mAttrValueRefId=" + mAttrValueRefId + ", hasSetValueRef=" + hasSetValueRef + ", keepInstance=" + keepInstance + '}'; } } ================================================ FILE: QSkinLoaderlib/src/main/java/org/qcode/qskinloader/entity/SkinAttr.java ================================================ package org.qcode.qskinloader.entity; /** * 皮肤指定属性及其对应的值/类型的实体类封装 * qqliu * 2016/9/24. */ public class SkinAttr { /*** * 对应View的属性 */ public String mAttrName; /*** * 属性值对应的reference id值,类似R.color.XX */ public int mAttrValueRefId; /*** * 属性值refrence id对应的名称,如R.color.XX,则此值为"XX" */ public String mAttrValueRefName; /*** * 属性值refrence id对应的类型,如R.color.XX,则此值为color */ public String mAttrValueTypeName; /*** * 直接存放自定义的属性 */ public DynamicAttr mDynamicAttr; public SkinAttr() { //empty } public SkinAttr(String attrName) { mAttrName = attrName; //others is empty } @Override public String toString() { return "SkinAttr{" + "mAttrName='" + mAttrName + '\'' + ", mAttrValueRefId=" + mAttrValueRefId + ", mAttrValueRefName='" + mAttrValueRefName + '\'' + ", mAttrValueTypeName='" + mAttrValueTypeName + '\'' + ", mDynamicAttr='" + mDynamicAttr + '\'' + '}'; } } ================================================ FILE: QSkinLoaderlib/src/main/java/org/qcode/qskinloader/entity/SkinAttrName.java ================================================ package org.qcode.qskinloader.entity; /** * 皮肤框架内置支持的属性 * qqliu * 2016/9/27. */ public class SkinAttrName { public static final String BACKGROUND = "background"; public static final String SRC = "src"; public static final String DRAWABLE_LEFT = "drawableLeft"; public static final String TEXT_COLOR = "textColor"; public static final String TEXT_COLOR_HINT = "textColorHint"; public static final String LIST_SELECTOR = "listSelector"; public static final String DIVIDER = "divider"; public static final String DRAW_SHADOW = "drawShadow"; public static final String CLEAR_RECYCLER_VIEW = "clearRecyclerView"; } ================================================ FILE: QSkinLoaderlib/src/main/java/org/qcode/qskinloader/entity/SkinAttrSet.java ================================================ package org.qcode.qskinloader.entity; import org.qcode.qskinloader.base.utils.CollectionUtils; import org.qcode.qskinloader.base.utils.StringUtils; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.HashMap; import java.util.List; /** * 皮肤框架支持的属性集合 * qqliu * 2016/9/24. */ public class SkinAttrSet { private HashMap mAttrMap = new HashMap(); public SkinAttrSet(SkinAttr... skinAttrs) { this(Arrays.asList(skinAttrs)); } public SkinAttrSet(List skinAttrs) { saveAttrs(skinAttrs); } private void saveAttrs(List skinAttrs) { if (CollectionUtils.isEmpty(skinAttrs)) { return; } for (SkinAttr attr : skinAttrs) { if (null == attr || StringUtils.isEmpty(attr.mAttrName)) { continue; } mAttrMap.put(attr.mAttrName, attr); } } public synchronized void addSkinAttrSet(SkinAttrSet skinAttrSet) { if (null == skinAttrSet) { return; } List setAttrList = skinAttrSet.getAttrList(); saveAttrs(setAttrList); } public synchronized void addSkinAttr(SkinAttr skinAttr) { if (null == skinAttr || StringUtils.isEmpty(skinAttr.mAttrName)) { return; } mAttrMap.put(skinAttr.mAttrName, skinAttr); } public synchronized List getAttrList() { ArrayList resultList = new ArrayList(); Collection valueAttrList = mAttrMap.values(); if (!CollectionUtils.isEmpty(valueAttrList)) { resultList.addAll(valueAttrList); } return resultList; } } ================================================ FILE: QSkinLoaderlib/src/main/java/org/qcode/qskinloader/entity/SkinConstant.java ================================================ package org.qcode.qskinloader.entity; /** * 皮肤框架的常量定义 * qqliu * 2016/9/25. */ public class SkinConstant { /*** * 支持的命名空间 */ public static final String XML_NAMESPACE = "http://schemas.android.com/android/skin"; /**属性值对应的类型是color*/ public static final String RES_TYPE_NAME_COLOR = "color"; /**属性值对应的类型是drawable*/ public static final String RES_TYPE_NAME_DRAWABLE = "drawable"; /**属性值对应的类型是mipmap*/ public static final String RES_TYPE_NAME_MIPMAP = "mipmap"; /**界面元素支持换肤的属性*/ public static final String ATTR_SKIN_ENABLE = "enable"; } ================================================ FILE: QSkinLoaderlib/src/main/java/org/qcode/qskinloader/impl/ActivitySkinEventHandlerImpl.java ================================================ package org.qcode.qskinloader.impl; import android.app.Activity; import android.content.res.Resources; import android.graphics.drawable.ColorDrawable; import android.graphics.drawable.Drawable; import android.view.View; import org.qcode.qskinloader.IActivitySkinEventHandler; import org.qcode.qskinloader.ISkinActivity; import org.qcode.qskinloader.ISkinAttributeParser; import org.qcode.qskinloader.IViewCreateListener; import org.qcode.qskinloader.SkinManager; import org.qcode.qskinloader.base.utils.Logging; import java.lang.ref.WeakReference; /*** * 支持换肤界面的帮助类 */ public class ActivitySkinEventHandlerImpl implements IActivitySkinEventHandler { private static final String TAG = "ActivityEventHandler"; private final SkinManagerImpl mSkinManager; private volatile boolean mNeedRefreshSkin = false; //当前界面是否有Focus private boolean mHasFocus; //皮肤发生变化时,当前界面是否需要立刻刷新皮肤 private boolean mSwitchSkinImmediately; //当前界面是否支持皮肤变化 private boolean mIsSupportSkinChange; private WeakReference mActivity = null; private int mWindowBgResId = -1; private SkinInflaterFactoryImpl mSkinInflaterFactoryImpl; private IViewCreateListener mViewCreateListener; private boolean mNeedDelegateViewCreate = true; private SkinAttributeParser mSkinAttributeParser; public ActivitySkinEventHandlerImpl() { mSkinManager = SkinManagerImpl.getInstance(); } @Override public void onCreate(Activity activity) { if (!mIsSupportSkinChange) { return; } mActivity = new WeakReference(activity); if(mNeedDelegateViewCreate) { mSkinInflaterFactoryImpl = new SkinInflaterFactoryImpl(getSkinAttributeParser()); mSkinInflaterFactoryImpl.setViewCreateListener(mViewCreateListener); activity.getLayoutInflater().setFactory(mSkinInflaterFactoryImpl); } mSkinManager.addObserver(this); } @Override public void setViewCreateListener(IViewCreateListener viewCreateListener) { mViewCreateListener = viewCreateListener; if(null != mSkinInflaterFactoryImpl) { mSkinInflaterFactoryImpl.setViewCreateListener(viewCreateListener); } } @Override public void onViewCreated() { if (!mIsSupportSkinChange) { return; } Logging.d(TAG, "onViewCreated()"); if (!mSkinManager.getResourceManager().isDefault()) { View contentView = getContentView(); mSkinManager.applySkin(contentView, true); refreshWindowBg(contentView); } mSkinManager.addObserver(this); } @Override public void onStart() { //do nothing } @Override public void onResume() { //do nothing } @Override public void onWindowFocusChanged(boolean hasFocus) { if (!mIsSupportSkinChange) { return; } mHasFocus = hasFocus; if(mHasFocus) { if (mNeedRefreshSkin) { mNeedRefreshSkin = false; //后台界面展示出来时再刷新 refreshSkin(); } } } @Override public void onPause() { //do nothing } @Override public void onStop() { //do nothing } @Override public void onDestroy() { if (!mIsSupportSkinChange) { return; } mSkinManager.removeObserver(this); SkinManager .with(getContentView()) .cleanAttrs(true); mActivity.clear(); } @Override public IActivitySkinEventHandler setSwitchSkinImmediately(boolean isImmediate) { mSwitchSkinImmediately = isImmediate; return this; } @Override public IActivitySkinEventHandler setSupportSkinChange(boolean supportChange) { mIsSupportSkinChange = supportChange; return this; } @Override public void handleSkinUpdate() { if (!mIsSupportSkinChange) { Logging.d(TAG, "onThemeUpdate()| not support theme change: " + getClass().getSimpleName()); return; } if (mHasFocus || mSwitchSkinImmediately) { mNeedRefreshSkin = false; refreshSkin(); } else { //仅置位,不立刻刷新 mNeedRefreshSkin = true; } } @Override public ISkinAttributeParser getSkinAttributeParser() { if(null == mSkinAttributeParser) { mSkinAttributeParser = new SkinAttributeParser(); } return mSkinAttributeParser; } @Override public IActivitySkinEventHandler setWindowBackgroundResource(int resId) { mWindowBgResId = resId; return this; } @Override public IActivitySkinEventHandler setNeedDelegateViewCreate(boolean needDelegateViewCreate) { mNeedDelegateViewCreate = needDelegateViewCreate; return this; } private void refreshSkin() { if (!mIsSupportSkinChange) { return; } if (null == mActivity) { return; } final Activity activity = mActivity.get(); activity.runOnUiThread(new Runnable() { @Override public void run() { View contentView = getContentView(); mSkinManager.applySkin(contentView, true); mSkinManager.applyWindowViewSkin(); refreshWindowBg(contentView); //通知Activity做其他刷新操作 if (activity instanceof ISkinActivity) { ((ISkinActivity) activity).handleSkinChange(); } } }); } private void refreshWindowBg(View contentView) { if(!mIsSupportSkinChange) { return; } if (mWindowBgResId <= 0) { return; } if (null == mActivity) { return; } Activity activity = mActivity.get(); if (null == activity) { return; } Drawable bgDrawable; try { bgDrawable = new ColorDrawable( mSkinManager.getResourceManager().getColor(mWindowBgResId)); } catch (Resources.NotFoundException ex) { try { bgDrawable = mSkinManager.getResourceManager().getDrawable(mWindowBgResId); } catch (Resources.NotFoundException e) { return; } } // contentView.setBackgroundDrawable(bgDrawable); activity.getWindow().setBackgroundDrawable(bgDrawable); } public View getContentView() { if (null == mActivity) { return null; } Activity activity = mActivity.get(); if (null == activity) { return null; } return activity.findViewById(android.R.id.content); } } ================================================ FILE: QSkinLoaderlib/src/main/java/org/qcode/qskinloader/impl/SkinAttributeParser.java ================================================ package org.qcode.qskinloader.impl; import android.content.Context; import android.content.res.Resources.NotFoundException; import android.support.v7.widget.RecyclerView; import android.util.AttributeSet; import android.view.View; import org.qcode.qskinloader.ISkinAttributeParser; import org.qcode.qskinloader.R; import org.qcode.qskinloader.SkinManager; import org.qcode.qskinloader.attrhandler.SkinAttrFactory; import org.qcode.qskinloader.base.utils.CollectionUtils; import org.qcode.qskinloader.base.utils.Logging; import org.qcode.qskinloader.entity.SkinAttr; import org.qcode.qskinloader.entity.SkinAttrName; import org.qcode.qskinloader.entity.SkinAttrSet; import org.qcode.qskinloader.entity.SkinConstant; import java.util.ArrayList; import java.util.List; /*** * 代理View的创建,解析与换肤相关的属性 */ class SkinAttributeParser implements ISkinAttributeParser { private static final String TAG = "SkinAttributeParser"; public boolean isSupportSkin(String name, Context context, AttributeSet attrs) { //只有View设置了skin:enable,才解析属性 boolean isSkinEnable = attrs.getAttributeBooleanValue( SkinConstant.XML_NAMESPACE, SkinConstant.ATTR_SKIN_ENABLE, false); return isSkinEnable; } public void parseAttribute(View view, String name, Context context, AttributeSet attrs) { if (view == null) { return; } SkinAttrSet skinAttrSet = parseSkinAttr(context, attrs); //recyclerview 增加处理 if(view instanceof RecyclerView) { SkinAttr clearSubAttr = new SkinAttr(SkinAttrName.CLEAR_RECYCLER_VIEW); SkinManager .with(view) .addViewAttrs(clearSubAttr); if(null == skinAttrSet) { skinAttrSet = new SkinAttrSet(clearSubAttr); } else { skinAttrSet.addSkinAttr(clearSubAttr); } } if (null != skinAttrSet) { //将SkinItem存储在View的tag内 view.setTag(R.id.tag_skin_attr, skinAttrSet); //如果有drawShadow属性,则替换ImageView为其他View // ShadowAttr2 skinAttrShadow = skinAttrSet.findSkinAttr(ShadowAttr2.class); // if (null != skinAttrShadow) { // if(view instanceof ImageView) { // // // view = createShadowImageView(context); // //将SkinItem存储在View的tag内 // view.setTag(R.id.tag_skin_attr, skinAttrSet); // } else { // view = createFrameWrapper(view, skinAttrShadow); // } // } } } // private View createFrameWrapper(View view, ShadowAttr2 skinAttrShadow) { // Context context = view.getContext(); // FrameLayout wrapperView = new FrameLayout(context); // FrameLayout.LayoutParams paramView = new FrameLayout.LayoutParams( // ViewGroup.LayoutParams.MATCH_PARENT, // ViewGroup.LayoutParams.MATCH_PARENT); // wrapperView.addView(view, paramView); // // View shadowView = new View(context); // shadowView.setVisibility(View.GONE); // FrameLayout.LayoutParams paramShadow = new FrameLayout.LayoutParams( // ViewGroup.LayoutParams.MATCH_PARENT, // ViewGroup.LayoutParams.MATCH_PARENT); // wrapperView.addView(shadowView, paramShadow); // // skinAttrShadow.setShadowView(shadowView); // return wrapperView; // } // private ShadowImageView createShadowImageView(Context context) { // return new ShadowImageView(context); // } /*** * 收集与换肤相关的属性 * * @param context * @param attrs */ private SkinAttrSet parseSkinAttr(Context context, AttributeSet attrs) { List viewAttrs = new ArrayList(); for (int i = 0; i < attrs.getAttributeCount(); i++) { String attrName = attrs.getAttributeName(i); String attrValue = attrs.getAttributeValue(i); if (!SkinAttrFactory.isSupportedAttr(attrName)) { continue; } if (!attrValue.startsWith("@")) { Logging.d(TAG, "parseSkinAttr()| only support ref id"); continue; } SkinAttr skinAttr = null; try { skinAttr = getSkinAttrFromId(context, attrName, attrValue); } catch (NumberFormatException ex) { // Logging.d(TAG, "parseSkinAttr()| error happened", ex); skinAttr = getSkinAttrBySplit(context, attrName, attrValue); } catch (NotFoundException ex) { Logging.d(TAG, "parseSkinAttr()| error happened", ex); } if (skinAttr != null) { viewAttrs.add(skinAttr); } } if (CollectionUtils.isEmpty(viewAttrs)) { return null; } return new SkinAttrSet(viewAttrs); } private SkinAttr getSkinAttrBySplit(Context context, String attrName, String attrValue) { try { int dividerIndex = attrValue.indexOf("/"); String entryName = attrValue.substring(dividerIndex + 1, attrValue.length()); String typeName = attrValue.substring(1, dividerIndex); int id = context.getResources().getIdentifier(entryName, typeName, context.getPackageName()); return SkinAttrFactory.newSkinAttr(attrName, id, entryName, typeName); } catch (NotFoundException e) { Logging.d(TAG, "parseSkinAttr()| error happened", e); } return null; } private SkinAttr getSkinAttrFromId(Context context, String attrName, String attrValue) { int id = Integer.parseInt(attrValue.substring(1)); String entryName = context.getResources().getResourceEntryName(id); String typeName = context.getResources().getResourceTypeName(id); return SkinAttrFactory.newSkinAttr(attrName, id, entryName, typeName); } } ================================================ FILE: QSkinLoaderlib/src/main/java/org/qcode/qskinloader/impl/SkinInflaterFactoryImpl.java ================================================ package org.qcode.qskinloader.impl; import android.content.Context; import android.util.AttributeSet; import android.view.LayoutInflater; import android.view.View; import org.qcode.qskinloader.ISkinAttributeParser; import org.qcode.qskinloader.IViewCreateListener; import org.qcode.qskinloader.base.utils.Logging; import org.qcode.qskinloader.base.utils.ReflectUtils; /*** * 代理View的创建,解析与换肤相关的属性 */ class SkinInflaterFactoryImpl implements LayoutInflater.Factory { private static final String TAG = "SkinInflaterFactoryImpl"; private IViewCreateListener mViewCreateListener; private ISkinAttributeParser mSkinAttributeParser; public SkinInflaterFactoryImpl(ISkinAttributeParser parser) { mSkinAttributeParser = parser; } public void setViewCreateListener(IViewCreateListener viewCreateListener) { mViewCreateListener = viewCreateListener; } @Override public View onCreateView(String name, Context context, AttributeSet attrs) { View view = null; //给框架外创建View的机会 if(null != mViewCreateListener) { view = mViewCreateListener.beforeCreate(name, context, attrs); } //判断是否支持换肤 if(!mSkinAttributeParser.isSupportSkin(name, context, attrs)) { return null; } if(null == view) { //代理创建View view = createView(context, name, attrs); } if (view == null) { return null; } //解析属性 mSkinAttributeParser.parseAttribute(view, name, context, attrs); //给框架外解析属性的机会 if(null != mViewCreateListener) { mViewCreateListener.afterCreated(view, name, context, attrs); } return view; } /*** * 根据名称创建view * * @param context * @param name * @param attrs * @return */ private View createView(Context context, String name, AttributeSet attrs) { View view = null; try { LayoutInflater inflater = LayoutInflater.from(context); setupInflater(inflater, context); if (-1 == name.indexOf('.')) { if ("View".equals(name) || "ViewStub".equals(name) || "ViewGroup".equals(name)) { view = inflater.createView( name, "android.view.", attrs); } if (view == null) { view = inflater.createView( name, "android.widget.", attrs); } if (view == null) { view = inflater.createView( name, "android.webkit.", attrs); } } else { view = inflater.createView(name, null, attrs); } } catch (Exception ex) { Logging.d(TAG, "createView()| create view failed", ex); view = null; } return view; } private void setupInflater(LayoutInflater inflater, Context context) { //异常,处理context为空,一般不会发生 Context inflaterContext = inflater.getContext(); if (null == inflaterContext) { ReflectUtils.setFieldValueOpt(inflater, "mContext", context); } //设置mConstructorArgs的第一个参数context Object[] constructorArgs = ReflectUtils.getFieldValueOpt(inflater, "mConstructorArgs"); if (null == constructorArgs || constructorArgs.length < 2) { //异常,一般不会发生 constructorArgs = new Object[2]; ReflectUtils.setFieldValueOpt(inflater, "mConstructorArgs", constructorArgs); } //如果mConstructorArgs的第一个参数为空,则设置为mContext if (null == constructorArgs[0]) { constructorArgs[0] = inflater.getContext(); } } } ================================================ FILE: QSkinLoaderlib/src/main/java/org/qcode/qskinloader/impl/SkinManagerImpl.java ================================================ package org.qcode.qskinloader.impl; import android.content.Context; import android.os.AsyncTask; import android.view.View; import android.view.ViewGroup; import org.qcode.qskinloader.IActivitySkinEventHandler; import org.qcode.qskinloader.ILoadSkinListener; import org.qcode.qskinloader.IResourceLoader; import org.qcode.qskinloader.IResourceManager; import org.qcode.qskinloader.ISkinAttrHandler; import org.qcode.qskinloader.ISkinManager; import org.qcode.qskinloader.attrhandler.SkinAttrFactory; import org.qcode.qskinloader.attrhandler.SkinAttrUtils; import org.qcode.qskinloader.base.observable.INotifyUpdate; import org.qcode.qskinloader.base.observable.Observable; import org.qcode.qskinloader.base.utils.CollectionUtils; import org.qcode.qskinloader.base.utils.Logging; import org.qcode.qskinloader.base.utils.StringUtils; import org.qcode.qskinloader.entity.SkinAttrSet; import org.qcode.qskinloader.resourceloader.ILoadResourceCallback; import org.qcode.qskinloader.resourceloader.ResourceManager; import org.qcode.qskinloader.resourceloader.impl.APKResourceLoader; import java.util.List; /** * 皮肤加载管理类对外实现接口 * qqliu * 2016/9/24. */ public class SkinManagerImpl implements ISkinManager { private static final String TAG = "SkinManager"; //单例相关 private static volatile SkinManagerImpl mInstance; private SkinManagerImpl() { } public static SkinManagerImpl getInstance() { if (null == mInstance) { synchronized (SkinManagerImpl.class) { if (null == mInstance) { mInstance = new SkinManagerImpl(); } } } return mInstance; } private Context mContext; private IResourceManager mResourceManager; private Observable mObservable; @Override public void init(Context context) { mContext = context.getApplicationContext(); mResourceManager = new ResourceManager(mContext); mObservable = new Observable(); new AsyncTask() { @Override protected Void doInBackground(String... params) { return null; } }.execute(""); } @Override public void restoreDefault(String defaultSkinIdentifier, ILoadSkinListener loadListener) { if (null != loadListener) { loadListener.onLoadStart(defaultSkinIdentifier); } //恢复ResourceManager的行为 mResourceManager.setBaseResource(null, null); refreshAllSkin(); if (loadListener != null) { loadListener.onLoadSuccess(defaultSkinIdentifier); } } private void refreshAllSkin() { //刷新正常的Activity内View的皮肤 refreshSkin(); //刷新框架内维护的View的皮肤,包括Dialog/popWindow/悬浮窗等应用场景 applyWindowViewSkin(); } @Override public void loadAPKSkin(String skinPath, ILoadSkinListener loadListener) { loadSkin(skinPath, new APKResourceLoader(mContext), loadListener); } @Override public void loadSkin(final String skinIdentifier, final IResourceLoader resourceLoader, final ILoadSkinListener loadListener) { if(StringUtils.isEmpty(skinIdentifier) || null == resourceLoader) { if(null != loadListener) { loadListener.onLoadFail(skinIdentifier); } return; } //当前皮肤就是将要换肤的皮肤,则不执行后续行为 if (skinIdentifier.equals(mResourceManager.getSkinIdentifier())) { Logging.d(TAG, "load()| current skin matches target, do nothing"); if(null != loadListener) { loadListener.onLoadSuccess(skinIdentifier); } return; } resourceLoader.loadResource(skinIdentifier, new ILoadResourceCallback() { @Override public void onLoadStart(String identifier) { if (loadListener != null) { loadListener.onLoadStart(skinIdentifier); } } @Override public void onLoadSuccess(String identifier, IResourceManager result) { Logging.d(TAG, "onLoadSuccess() | identifier= " + identifier); mResourceManager.setBaseResource(identifier, result); refreshAllSkin(); Logging.d(TAG, "onLoadSuccess()| notify update"); if (loadListener != null) { loadListener.onLoadSuccess(skinIdentifier); } } @Override public void onLoadFail(String identifier, int errorCode) { mResourceManager.setBaseResource(null, null); if (loadListener != null) { loadListener.onLoadFail(skinIdentifier); } } }); } @Override public void applySkin(View view, boolean applyChild) { if (null == view) { return; } SkinAttrSet skinAttrSet = ViewSkinTagHelper.getSkinAttrs(view); SkinAttrUtils.applySkinAttrs(view, skinAttrSet, mResourceManager); if (applyChild) { if (view instanceof ViewGroup) { //遍历子元素应用皮肤 ViewGroup viewGroup = (ViewGroup) view; for (int i = 0; i < viewGroup.getChildCount(); i++) { applySkin(viewGroup.getChildAt(i), true); } } } } @Override public void registerSkinAttrHandler(String attrName, ISkinAttrHandler skinAttrHandler) { SkinAttrFactory.registerSkinAttrHandler(attrName, skinAttrHandler); } @Override public void unregisterSkinAttrHandler(String attrName) { SkinAttrFactory.removeSkinAttrHandler(attrName); } @Override public void setResourceManager(IResourceManager resourceManager) { if(null == resourceManager) { return; } mResourceManager = resourceManager; } @Override public IResourceManager getResourceManager() { return mResourceManager; } @Override public void addObserver(IActivitySkinEventHandler observer) { mObservable.addObserver(observer); } @Override public void removeObserver(IActivitySkinEventHandler observer) { mObservable.removeObserver(observer); } @Override public void notifyUpdate(INotifyUpdate callback, String identifier, Object... params) { mObservable.notifyUpdate(callback, identifier, params); } /**刷新Window上添加的View的显示模式*/ void applyWindowViewSkin() { List windowViewList = WindowViewManager.getInstance().getWindowViewList(); if(CollectionUtils.isEmpty(windowViewList)) { return; } for(View view : windowViewList) { applySkin(view, true); } } /*** * 告知外部观察者当前皮肤发生了变化 */ private void refreshSkin() { notifyUpdate(new INotifyUpdate() { @Override public boolean notifyEvent( IActivitySkinEventHandler handler, String identifier, Object... params) { handler.handleSkinUpdate(); return false; } }, null); } } ================================================ FILE: QSkinLoaderlib/src/main/java/org/qcode/qskinloader/impl/SkinViewHelperImpl.java ================================================ package org.qcode.qskinloader.impl; import android.content.res.Resources; import android.support.annotation.NonNull; import android.view.View; import android.view.ViewGroup; import org.qcode.qskinloader.ISkinViewHelper; import org.qcode.qskinloader.attrhandler.SkinAttrFactory; import org.qcode.qskinloader.base.utils.CollectionUtils; import org.qcode.qskinloader.base.utils.Logging; import org.qcode.qskinloader.base.utils.StringUtils; import org.qcode.qskinloader.entity.DynamicAttr; import org.qcode.qskinloader.entity.SkinAttr; import org.qcode.qskinloader.entity.SkinAttrSet; import java.util.Arrays; import java.util.List; import static android.content.ContentValues.TAG; /** * View皮肤设置管理 * qqliu * 2016/10/8. */ public class SkinViewHelperImpl implements ISkinViewHelper { private View mView; public SkinViewHelperImpl(View view) { mView = view; } //=========================interfaces================================// @Override public ISkinViewHelper setViewAttrs(String attrName, int resId) { if (StringUtils.isEmpty(attrName)) { return this; } SkinAttr attr = parseSkinAttr(attrName, resId); return setViewAttrs(attr); } @Override public ISkinViewHelper setViewAttrs(DynamicAttr... dynamicAttrs) { if (CollectionUtils.isEmpty(dynamicAttrs)) { return this; } return setViewAttrs(Arrays.asList(dynamicAttrs)); } @Override public ISkinViewHelper setViewAttrs(SkinAttr... skinAttrs) { if (CollectionUtils.isEmpty(skinAttrs)) { return this; } ViewSkinTagHelper.setSkinAttrs(mView, new SkinAttrSet(skinAttrs)); return this; } @Override public ISkinViewHelper setViewAttrs(List dynamicAttrs) { if (CollectionUtils.isEmpty(dynamicAttrs)) { return this; } SkinAttrSet skinAttrSet = parseSkinAttrSet(dynamicAttrs); ViewSkinTagHelper.setSkinAttrs(mView, skinAttrSet); return this; } @Override public ISkinViewHelper addViewAttrs(String attrName, int resId) { if (StringUtils.isEmpty(attrName)) { return this; } SkinAttr attr = parseSkinAttr(attrName, resId); return addViewAttrs(attr); } @Override public ISkinViewHelper addViewAttrs(DynamicAttr... dynamicAttrs) { if (CollectionUtils.isEmpty(dynamicAttrs)) { return this; } return addViewAttrs(Arrays.asList(dynamicAttrs)); } @Override public ISkinViewHelper addViewAttrs(SkinAttr... skinAttrs) { if (CollectionUtils.isEmpty(skinAttrs)) { return this; } ViewSkinTagHelper.addSkinAttrs(mView, new SkinAttrSet(skinAttrs)); return this; } @Override public ISkinViewHelper addViewAttrs(List dynamicAttrs) { if (CollectionUtils.isEmpty(dynamicAttrs)) { return this; } SkinAttrSet skinAttrSet = parseSkinAttrSet(dynamicAttrs); ViewSkinTagHelper.addSkinAttrs(mView, skinAttrSet); return this; } @Override public ISkinViewHelper cleanAttrs(boolean clearChild) { if (null == mView) { return this; } cleanAttrs(mView, clearChild); return this; } @Override public void applySkin(boolean applyChild) { SkinManagerImpl.getInstance().applySkin(mView, applyChild); } private static void cleanAttrs(View view, boolean clearChild) { if (null == view) { return; } ViewSkinTagHelper.setSkinAttrs(view, null); if (clearChild) { if (view instanceof ViewGroup) { //遍历子元素清除皮肤 ViewGroup viewGroup = (ViewGroup) view; for (int i = 0; i < viewGroup.getChildCount(); i++) { cleanAttrs(viewGroup.getChildAt(i), true); } } } } private SkinAttr parseSkinAttr(String attrName, int resId) { SkinAttr skinAttr = null; Resources resources = mView.getResources(); try { String attrValueName = resources.getResourceEntryName(resId); String attrValueType = resources.getResourceTypeName(resId); skinAttr = SkinAttrFactory.newSkinAttr(attrName, resId, attrValueName, attrValueType); } catch (Exception ex) { Logging.d(TAG, "dynamicAddView()| error happened", ex); } return skinAttr; } @NonNull private SkinAttrSet parseSkinAttrSet(List dynamicAttrs) { SkinAttrSet skinAttrSet = new SkinAttrSet(); for (DynamicAttr dynamicAttr : dynamicAttrs) { if (null == dynamicAttr) { continue; } SkinAttr attr; if (dynamicAttr.hasSetValueRef) { //设置了value,则解析resId的名称和类型 attr = parseSkinAttr(dynamicAttr.mAttrName, dynamicAttr.mAttrValueRefId); } else { //没有value,直接解析名称 attr = SkinAttrFactory.newSkinAttr(dynamicAttr.mAttrName); } if (null == attr) { continue; } if (dynamicAttr.keepInstance) { attr.mDynamicAttr = dynamicAttr; } skinAttrSet.addSkinAttr(attr); } return skinAttrSet; } } ================================================ FILE: QSkinLoaderlib/src/main/java/org/qcode/qskinloader/impl/ViewSkinTagHelper.java ================================================ package org.qcode.qskinloader.impl; import android.view.View; import org.qcode.qskinloader.R; import org.qcode.qskinloader.entity.SkinAttrSet; /** * 存取View的皮肤属性的帮助类 * qqliu * 2016/10/8. */ class ViewSkinTagHelper { /*** * 设置View的皮肤属性 * @param view * @param skinAttrSet */ public static void setSkinAttrs(View view, SkinAttrSet skinAttrSet) { if(null == view) { return; } view.setTag(R.id.tag_skin_attr, skinAttrSet); } /*** * 添加View的皮肤属性 * @param view * @param skinAttrSet */ public static void addSkinAttrs(View view, SkinAttrSet skinAttrSet) { if(null == view) { return; } SkinAttrSet attrSet = getSkinAttrs(view); if (null == attrSet) { view.setTag(R.id.tag_skin_attr, skinAttrSet); } else { attrSet.addSkinAttrSet(skinAttrSet); } } /*** * 获取View的皮肤属性 * @param view * @return skinAttrSet */ public static SkinAttrSet getSkinAttrs(View view) { if(null == view) { return null; } return (SkinAttrSet) view.getTag(R.id.tag_skin_attr); } } ================================================ FILE: QSkinLoaderlib/src/main/java/org/qcode/qskinloader/impl/WindowViewManager.java ================================================ package org.qcode.qskinloader.impl; import android.view.View; import org.qcode.qskinloader.IWindowViewManager; import org.qcode.qskinloader.SkinManager; import org.qcode.qskinloader.base.utils.CollectionUtils; import java.lang.ref.WeakReference; import java.util.ArrayList; import java.util.List; /** * 直接加载在Window上的view的管理器 * qqliu * 2016/10/8. */ public class WindowViewManager implements IWindowViewManager { private ArrayList> mSkinViewList = new ArrayList>(); //==========================Singleton========================// private static volatile WindowViewManager mInstance; private WindowViewManager() {} public static WindowViewManager getInstance() { if(null == mInstance) { synchronized (WindowViewManager.class) { if(null == mInstance) { mInstance = new WindowViewManager(); } } } return mInstance; } //==========================interfaces========================// @Override public IWindowViewManager addWindowView(View view) { ArrayList> tmpList = (ArrayList>) mSkinViewList.clone(); for (WeakReference viewRef : tmpList) { if (null == viewRef) { continue; } //已添加此View,则不需要再添加 if (view == viewRef.get()) { return this; } } mSkinViewList.add(new WeakReference(view)); return this; } @Override public IWindowViewManager removeWindowView(View view) { ArrayList> tmpList = (ArrayList>) mSkinViewList.clone(); for (WeakReference viewRef : tmpList) { if (null == viewRef) { continue; } //找到了指定View if (view == viewRef.get()) { mSkinViewList.remove(viewRef); break; } } return this; } @Override public IWindowViewManager clear() { if(CollectionUtils.isEmpty(mSkinViewList)) { return this; } mSkinViewList.clear(); return this; } @Override public void applySkinForViews(boolean applyChild) { if(CollectionUtils.isEmpty(mSkinViewList)) { return; } for(WeakReference viewRef : mSkinViewList) { if(null == viewRef || null == viewRef.get()) { continue; } SkinManager.getInstance().applySkin(viewRef.get(), applyChild); } } @Override public List getWindowViewList() { ArrayList resultList = new ArrayList(); ArrayList> tmpList = (ArrayList>) mSkinViewList.clone(); for (WeakReference 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实现夜间模式效果图](https://github.com/qqliu10u/QSkinLoader/blob/master/skin-change-demo.gif) #**基本简介:** 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 mUsingViewsMap = new HashMap(); //上一次展示的位置 private int mLastItemPosition = -1; @Override public void startUpdate(ViewGroup container) { // Logging.d(TAG, "startUpdate()"); super.startUpdate(container); } @Override public final Object instantiateItem(ViewGroup container, int position) { Logging.d(TAG, "instantiateItem()| position= " + position); Object dataObject = getItemObject(position); //如果要实例化的元素就是当前位置,则直接绑定数据 // Logging.d(TAG, "instantiateItem()| get view for " + dataObject); ViewBundle currentUsingView = mUsingViewsMap.get(dataObject); if (null != currentUsingView) { onBindView(currentUsingView.view, position, currentUsingView.viewType); return dataObject; } int itemViewType = getItemViewType(position); //获取新的View T itemView = null; List recyclableViewList = mRecycledViews.get(itemViewType); if (null != recyclableViewList && recyclableViewList.size() > 0) { itemView = recyclableViewList.remove(0); } else { itemView = createItemView(itemViewType); } //绑定数据 onBindView(itemView, position, itemViewType); container.addView(itemView); // Logging.d(TAG, "instantiateItem()| position= " + position + " itemView= " + itemView.hashCode() + " dataObject= " + dataObject); mUsingViewsMap.put(dataObject, new ViewBundle(itemView, itemViewType)); return dataObject; } protected int getItemViewType(int position) { return TYPE_DEFAULT; } @Override public final void destroyItem(ViewGroup container, int position, Object object) { Logging.d(TAG, "destroyItem()| position= " + position); // Logging.d(TAG, "destroyItem()| object= " + object); ViewBundle itemViewForDestroy = mUsingViewsMap.get(object); if (null != itemViewForDestroy) { //销毁item view destroyItemView(itemViewForDestroy.view); //移除item view, 并挪到待处理列表内 container.removeView(itemViewForDestroy.view); // Logging.d(TAG, "destroyItem()| remove item for " + position + " object= " + object); mUsingViewsMap.remove(object); ensureListNotEmpty(itemViewForDestroy.viewType); List recyclableViewList = mRecycledViews.get(itemViewForDestroy.viewType); recyclableViewList.add(itemViewForDestroy.view); } else { Logging.d(TAG, "destroyItem()| but item view is null for position: " + position); } } @Override public final void setPrimaryItem(ViewGroup container, int position, Object object) { super.setPrimaryItem(container, position, object); onItemPositionChange(mLastItemPosition, position); mLastItemPosition = position; } @Override public void finishUpdate(ViewGroup container) { // Logging.d(TAG, "finishUpdate()"); super.finishUpdate(container); } @Override public Parcelable saveState() { // Logging.d(TAG, "saveState()"); return super.saveState(); } @Override public void restoreState(Parcelable state, ClassLoader loader) { // Logging.d(TAG, "restoreState()"); super.restoreState(state, loader); } @Override public int getItemPosition(Object object) { // Logging.d(TAG, "getItemPosition() object= " + object); return super.getItemPosition(object); } protected void onItemPositionChange(int oldPosition, int newPosition) { //hook } protected T getCreatedView(int position) { Object dataObject = getItemObject(position); ViewBundle viewBundle = mUsingViewsMap.get(dataObject); if (null == viewBundle) { return null; } return viewBundle.view; } protected List getUsingViews() { List resultList = new ArrayList(); Set> entrySet = mUsingViewsMap.entrySet(); Iterator> iterator = entrySet.iterator(); while (iterator.hasNext()) { Map.Entry entry = iterator.next(); ViewBundle viewBundle = entry.getValue(); resultList.add(viewBundle.view); } return resultList; } protected abstract Object getItemObject(int position); protected abstract T createItemView(int itemViewType); protected abstract void onBindView(T itemView, int position, int itemViewType); protected abstract void destroyItemView(T view); private void ensureListNotEmpty(int viewType) { List viewList = mRecycledViews.get(viewType); if (null == viewList) { mRecycledViews.put(viewType, new ArrayList()); } } private class ViewBundle { T view; int viewType; public ViewBundle(T view, int viewType) { this.view = view; this.viewType = viewType; } } } ================================================ FILE: app/src/main/java/org/qcode/demo/ui/viewpageandlistview/ViewPagerAndListViewActivity.java ================================================ package org.qcode.demo.ui.viewpageandlistview; import android.os.Bundle; import android.support.v4.view.ViewPager; import org.qcode.demo.BaseActivity; import org.qcode.skintestdemo.R; public class ViewPagerAndListViewActivity extends BaseActivity { private ViewPager mViewPager; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_viewpager_listview); mViewPager = (ViewPager) findViewById(R.id.home_news_viewpager); mViewPager.setAdapter(new NewsPageAdapter(this)); } } ================================================ FILE: app/src/main/java/org/qcode/demo/utils/FileUtils.java ================================================ package org.qcode.demo.utils; import android.content.Context; import android.text.TextUtils; import java.io.BufferedOutputStream; import java.io.File; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; /** * qqliu * 2016/9/12. */ public class FileUtils { public static String getDirectory(Context context) { return context.getExternalCacheDir() + File.separator + "NightMode"; } public static String getFileName() { return "resultFile.txt"; } public static OutputStream openFileStream(Context context) { return openFileStream(getDirectory(context), getFileName()); } public static OutputStream openFileStream(String directory, String filePath) { if (TextUtils.isEmpty(directory) || TextUtils.isEmpty(filePath)) { return null; } File directoryFile = new File(directory); if (!directoryFile.exists()) { directoryFile.mkdirs(); } File file = new File(directory + File.separator + filePath); try { if (!file.exists()) { file.createNewFile(); } return new FileOutputStream(file, true); } catch (Exception ex) { ex.printStackTrace(); return null; } } public static void writeData(OutputStream stream, String str) { if (null == stream || TextUtils.isEmpty(str)) { return; } str = str + "\r\n"; try { BufferedOutputStream bufferedOutputStream = new BufferedOutputStream(stream); bufferedOutputStream.write(str.getBytes()); bufferedOutputStream.flush(); } catch (Exception ex) { ex.printStackTrace(); } finally { } } public static void closeStream(OutputStream stream) { if (null != stream) { try { stream.close(); } catch (IOException e) { e.printStackTrace(); } } } public static boolean copyAssetFile(Context context, String originFileName, String destFilePath, String destFileName) { InputStream is = null; BufferedOutputStream bos = null; try { is = context.getAssets().open(originFileName); File destPathFile = new File(destFilePath); if(!destPathFile.exists()) { destPathFile.mkdirs(); } File destFile = new File(destFilePath + File.separator + destFileName); if(!destFile.exists()) { destFile.createNewFile(); } FileOutputStream fos = new FileOutputStream(destFile); bos = new BufferedOutputStream(fos); byte[] buffer = new byte[256]; int length = 0; while ((length = is.read(buffer)) > 0) { bos.write(buffer, 0, length); } bos.flush(); return true; } catch (Exception ex) { ex.printStackTrace(); } finally { if(null != is) { try { is.close(); } catch (IOException e) { e.printStackTrace(); } } if(null != bos) { try { bos.close(); } catch (IOException e) { e.printStackTrace(); } } } return false; } } ================================================ FILE: app/src/main/java/org/qcode/demo/utils/UITaskRunner.java ================================================ package org.qcode.demo.utils; import android.os.Handler; import android.os.Looper; /** * 在主线程中执行runnable */ public class UITaskRunner { private Handler mHandler; private UITaskRunner() { mHandler = new Handler(Looper.getMainLooper()); } private static class SingletonHolder { static UITaskRunner sRunner = new UITaskRunner(); } public static Handler getHandler() { return SingletonHolder.sRunner.mHandler; } } ================================================ FILE: app/src/main/java/org/qcode/demo/utils/UIUtil.java ================================================ package org.qcode.demo.utils; import android.content.Context; import android.os.Looper; import android.widget.Toast; /** * 与UI相关的帮助类 */ public final class UIUtil { private static final String TAG = "UIUtil"; private UIUtil() { } /** * 根据手机的分辨率从 dip 的单位 转成为 px(像素) */ public static int dip2px(Context context, double dpValue) { final float scale = context.getResources().getDisplayMetrics().density; return (int) (dpValue * scale + 0.5); } /** * 显示Toast */ public static void showToast(Context context, String toast) { toast(context, toast, false); } private static Toast mStaticToastImpl = null; public static void toast(final Context context, final String msg, final boolean isLong) { if (null == mStaticToastImpl) { //需要在主线程创建toast实例 if (Looper.myLooper() != Looper.getMainLooper()) { UITaskRunner.getHandler().post(new Runnable() { @Override public void run() { toast(context, msg, isLong); } }); return; } } if(null == mStaticToastImpl) { synchronized (UIUtil.class) { if(null == mStaticToastImpl) { mStaticToastImpl = Toast.makeText( context.getApplicationContext(), "", Toast.LENGTH_SHORT); } } } if(Looper.myLooper() == Looper.getMainLooper()) { mStaticToastImpl.setText(msg); mStaticToastImpl.setDuration(isLong ? Toast.LENGTH_LONG : Toast.LENGTH_SHORT); //当前在主线程 mStaticToastImpl.show(); } else { UITaskRunner.getHandler().post(new Runnable() { @Override public void run() { mStaticToastImpl.setText(msg); mStaticToastImpl.setDuration(isLong ? Toast.LENGTH_LONG : Toast.LENGTH_SHORT); mStaticToastImpl.show(); } }); } } public static void toast(Context context, String msg) { toast(context, msg, true); } } ================================================ FILE: app/src/main/res/drawable/btn_bg.xml ================================================ ================================================ FILE: app/src/main/res/drawable/btn_bg_night.xml ================================================ ================================================ FILE: app/src/main/res/drawable/drawable_float_view.xml ================================================ ================================================ FILE: app/src/main/res/drawable/drawable_float_view_night.xml ================================================ ================================================ FILE: app/src/main/res/drawable/news_item_selector.xml ================================================ ================================================ FILE: app/src/main/res/drawable/news_item_selector_night.xml ================================================ ================================================ FILE: app/src/main/res/drawable-night/btn_bg.xml ================================================ ================================================ FILE: app/src/main/res/drawable-night/drawable_float_view.xml ================================================ ================================================ FILE: app/src/main/res/drawable-night/news_item_selector.xml ================================================ ================================================ FILE: app/src/main/res/layout/activity_base_activity.xml ================================================