詹子聪 hace 5 años
commit
3849d02d35
Se han modificado 100 ficheros con 11793 adiciones y 0 borrados
  1. 9 0
      .gitignore
  2. 1 0
      android-skin-loader-lib/.gitignore
  3. 25 0
      android-skin-loader-lib/build.gradle
  4. BIN
      android-skin-loader-lib/gradle/wrapper/gradle-wrapper.jar
  5. 6 0
      android-skin-loader-lib/gradle/wrapper/gradle-wrapper.properties
  6. 160 0
      android-skin-loader-lib/gradlew
  7. 90 0
      android-skin-loader-lib/gradlew.bat
  8. 12 0
      android-skin-loader-lib/local.properties
  9. 17 0
      android-skin-loader-lib/proguard-rules.pro
  10. 9 0
      android-skin-loader-lib/src/main/AndroidManifest.xml
  11. 83 0
      android-skin-loader-lib/src/main/java/cn/feng/skin/manager/base/BaseActivity.java
  12. 42 0
      android-skin-loader-lib/src/main/java/cn/feng/skin/manager/base/BaseFragment.java
  13. 90 0
      android-skin-loader-lib/src/main/java/cn/feng/skin/manager/base/BaseFragmentActivity.java
  14. 33 0
      android-skin-loader-lib/src/main/java/cn/feng/skin/manager/config/SkinConfig.java
  15. 61 0
      android-skin-loader-lib/src/main/java/cn/feng/skin/manager/entity/AttrFactory.java
  16. 27 0
      android-skin-loader-lib/src/main/java/cn/feng/skin/manager/entity/BackgroundAttr.java
  17. 26 0
      android-skin-loader-lib/src/main/java/cn/feng/skin/manager/entity/DividerAttr.java
  18. 23 0
      android-skin-loader-lib/src/main/java/cn/feng/skin/manager/entity/DynamicAttr.java
  19. 21 0
      android-skin-loader-lib/src/main/java/cn/feng/skin/manager/entity/ListSelectorAttr.java
  20. 45 0
      android-skin-loader-lib/src/main/java/cn/feng/skin/manager/entity/SkinAttr.java
  21. 42 0
      android-skin-loader-lib/src/main/java/cn/feng/skin/manager/entity/SkinItem.java
  22. 24 0
      android-skin-loader-lib/src/main/java/cn/feng/skin/manager/entity/SrcAttr.java
  23. 21 0
      android-skin-loader-lib/src/main/java/cn/feng/skin/manager/entity/TextColorAttr.java
  24. 8 0
      android-skin-loader-lib/src/main/java/cn/feng/skin/manager/listener/IAttrUpdate.java
  25. 10 0
      android-skin-loader-lib/src/main/java/cn/feng/skin/manager/listener/IDynamicNewView.java
  26. 7 0
      android-skin-loader-lib/src/main/java/cn/feng/skin/manager/listener/ILoaderListener.java
  27. 4 0
      android-skin-loader-lib/src/main/java/cn/feng/skin/manager/listener/IResourceParser.java
  28. 14 0
      android-skin-loader-lib/src/main/java/cn/feng/skin/manager/listener/ISkinChecker.java
  29. 8 0
      android-skin-loader-lib/src/main/java/cn/feng/skin/manager/listener/ISkinLoader.java
  30. 12 0
      android-skin-loader-lib/src/main/java/cn/feng/skin/manager/listener/ISkinUpdate.java
  31. 200 0
      android-skin-loader-lib/src/main/java/cn/feng/skin/manager/loader/SkinInflaterFactory.java
  32. 325 0
      android-skin-loader-lib/src/main/java/cn/feng/skin/manager/loader/SkinManager.java
  33. 179 0
      android-skin-loader-lib/src/main/java/cn/feng/skin/manager/util/ArrayUtils.java
  34. 90 0
      android-skin-loader-lib/src/main/java/cn/feng/skin/manager/util/CommonBaseAdapter.java
  35. 127 0
      android-skin-loader-lib/src/main/java/cn/feng/skin/manager/util/CommonViewHolder.java
  36. 75 0
      android-skin-loader-lib/src/main/java/cn/feng/skin/manager/util/L.java
  37. 253 0
      android-skin-loader-lib/src/main/java/cn/feng/skin/manager/util/ListUtils.java
  38. 289 0
      android-skin-loader-lib/src/main/java/cn/feng/skin/manager/util/MapUtils.java
  39. 124 0
      android-skin-loader-lib/src/main/java/cn/feng/skin/manager/util/ObjectUtils.java
  40. 248 0
      android-skin-loader-lib/src/main/java/cn/feng/skin/manager/util/PreferencesUtils.java
  41. 136 0
      android-skin-loader-lib/src/main/java/cn/feng/skin/manager/util/ResourceUtils.java
  42. 293 0
      android-skin-loader-lib/src/main/java/cn/feng/skin/manager/util/StringUtils.java
  43. 5 0
      android-skin-loader-lib/src/main/res/values/attrs.xml
  44. 3 0
      android-skin-loader-lib/src/main/res/values/strings.xml
  45. 1 0
      app/.gitignore
  46. 68 0
      app/build.gradle
  47. BIN
      app/libs/open_sdk_r6019_lite.jar
  48. 21 0
      app/proguard-rules.pro
  49. 26 0
      app/src/androidTest/java/com/itant/shiwushu/ExampleInstrumentedTest.java
  50. 140 0
      app/src/main/AndroidManifest.xml
  51. 4914 0
      app/src/main/assets/data/cities.txt
  52. BIN
      app/src/main/assets/data/city.realm
  53. BIN
      app/src/main/assets/skin/blue.skin
  54. BIN
      app/src/main/assets/skin/red.skin
  55. 1 0
      app/src/main/assets/todo.txt
  56. 34 0
      app/src/main/java/com/itant/shiwushu/LAFApplication.java
  57. 128 0
      app/src/main/java/com/itant/shiwushu/MainActivity.java
  58. 56 0
      app/src/main/java/com/itant/shiwushu/WelcomeActivity.java
  59. 31 0
      app/src/main/java/com/itant/shiwushu/adapter/FragmentPagerItemAdapter.java
  60. 32 0
      app/src/main/java/com/itant/shiwushu/adapter/TabPageAdapter.java
  61. 135 0
      app/src/main/java/com/itant/shiwushu/base/BaseSkinActivity.java
  62. 45 0
      app/src/main/java/com/itant/shiwushu/base/BaseSkinFragment.java
  63. 56 0
      app/src/main/java/com/itant/shiwushu/base/BaseSwipeActivity.java
  64. 9 0
      app/src/main/java/com/itant/shiwushu/base/IBasePresenter.java
  65. 9 0
      app/src/main/java/com/itant/shiwushu/base/IBaseView.java
  66. 10 0
      app/src/main/java/com/itant/shiwushu/base/IPermission.java
  67. 101 0
      app/src/main/java/com/itant/shiwushu/base/LAFBaseActivity.java
  68. 44 0
      app/src/main/java/com/itant/shiwushu/base/LAFBaseFragment.java
  69. 47 0
      app/src/main/java/com/itant/shiwushu/bean/City.java
  70. 22 0
      app/src/main/java/com/itant/shiwushu/bean/FavoriteResult.java
  71. 187 0
      app/src/main/java/com/itant/shiwushu/bean/LostFound.java
  72. 45 0
      app/src/main/java/com/itant/shiwushu/bean/Province.java
  73. 40 0
      app/src/main/java/com/itant/shiwushu/bean/ResponseResult.java
  74. 147 0
      app/src/main/java/com/itant/shiwushu/bean/User.java
  75. 32 0
      app/src/main/java/com/itant/shiwushu/constant/CommonConstant.java
  76. 17 0
      app/src/main/java/com/itant/shiwushu/constant/ResultCode.java
  77. 163 0
      app/src/main/java/com/itant/shiwushu/constant/ServerHost.java
  78. 11 0
      app/src/main/java/com/itant/shiwushu/listener/OnUserUpdateListener.java
  79. 85 0
      app/src/main/java/com/itant/shiwushu/manager/ActivityManager.java
  80. 76 0
      app/src/main/java/com/itant/shiwushu/manager/CityManager.java
  81. 35 0
      app/src/main/java/com/itant/shiwushu/manager/ExecutorManager.java
  82. 135 0
      app/src/main/java/com/itant/shiwushu/manager/RealmManager.java
  83. 71 0
      app/src/main/java/com/itant/shiwushu/manager/SharedPreferenceManager.java
  84. 65 0
      app/src/main/java/com/itant/shiwushu/manager/ThemeManager.java
  85. 56 0
      app/src/main/java/com/itant/shiwushu/manager/ToastManager.java
  86. 81 0
      app/src/main/java/com/itant/shiwushu/manager/UserManager.java
  87. 65 0
      app/src/main/java/com/itant/shiwushu/net/ElasticCallback.java
  88. 142 0
      app/src/main/java/com/itant/shiwushu/net/GlobalPresenter.java
  89. 28 0
      app/src/main/java/com/itant/shiwushu/temp/TempCity.java
  90. 28 0
      app/src/main/java/com/itant/shiwushu/temp/TempProvince.java
  91. 51 0
      app/src/main/java/com/itant/shiwushu/tool/ActivityTool.java
  92. 128 0
      app/src/main/java/com/itant/shiwushu/tool/BitmapTool.java
  93. 35 0
      app/src/main/java/com/itant/shiwushu/tool/BottomNavigationViewHelper.java
  94. 34 0
      app/src/main/java/com/itant/shiwushu/tool/ColorTool.java
  95. 122 0
      app/src/main/java/com/itant/shiwushu/tool/DialogTool.java
  96. 58 0
      app/src/main/java/com/itant/shiwushu/tool/DonateTool.java
  97. 25 0
      app/src/main/java/com/itant/shiwushu/tool/GlideTool.java
  98. 547 0
      app/src/main/java/com/itant/shiwushu/tool/ImageTools.java
  99. 77 0
      app/src/main/java/com/itant/shiwushu/tool/PermissionTool.java
  100. 0 0
      app/src/main/java/com/itant/shiwushu/tool/PhotoTool.java

+ 9 - 0
.gitignore

@@ -0,0 +1,9 @@
+*.iml
+.gradle
+/local.properties
+/.idea/workspace.xml
+/.idea/libraries
+.DS_Store
+/build
+/captures
+.externalNativeBuild

+ 1 - 0
android-skin-loader-lib/.gitignore

@@ -0,0 +1 @@
+/build

+ 25 - 0
android-skin-loader-lib/build.gradle

@@ -0,0 +1,25 @@
+apply plugin: 'com.android.library'
+
+android {
+    compileSdkVersion 27
+    buildToolsVersion '26.0.3'
+
+    defaultConfig {
+        minSdkVersion 15
+        targetSdkVersion 27
+        versionCode 1
+        versionName "1.0"
+    }
+    buildTypes {
+        release {
+            minifyEnabled false
+            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
+        }
+    }
+}
+
+dependencies {
+    compile fileTree(include: ['*.jar'], dir: 'libs')
+    testCompile 'junit:junit:4.12'
+    compile 'com.android.support:support-v4:27.1.1'
+}

BIN
android-skin-loader-lib/gradle/wrapper/gradle-wrapper.jar


+ 6 - 0
android-skin-loader-lib/gradle/wrapper/gradle-wrapper.properties

@@ -0,0 +1,6 @@
+#Sun Sep 16 17:21:39 CST 2018
+distributionBase=GRADLE_USER_HOME
+distributionPath=wrapper/dists
+zipStoreBase=GRADLE_USER_HOME
+zipStorePath=wrapper/dists
+distributionUrl=https\://services.gradle.org/distributions/gradle-4.1-all.zip

+ 160 - 0
android-skin-loader-lib/gradlew

@@ -0,0 +1,160 @@
+#!/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 "$@"

+ 90 - 0
android-skin-loader-lib/gradlew.bat

@@ -0,0 +1,90 @@
+@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

+ 12 - 0
android-skin-loader-lib/local.properties

@@ -0,0 +1,12 @@
+## This file is automatically generated by Android Studio.
+# Do not modify this file -- YOUR CHANGES WILL BE ERASED!
+#
+# This file must *NOT* be checked into Version Control Systems,
+# as it contains information specific to your local configuration.
+#
+# Location of the SDK. This is only used by Gradle.
+# For customization when using a Version Control System, please read the
+# header note.
+#Sun Sep 16 17:21:30 CST 2018
+ndk.dir=D\:\\software\\work\\newassdk\\ndk-bundle
+sdk.dir=D\:\\software\\work\\newassdk

+ 17 - 0
android-skin-loader-lib/proguard-rules.pro

@@ -0,0 +1,17 @@
+# Add project specific ProGuard rules here.
+# By default, the flags in this file are appended to flags specified
+# in /Users/fengjun/Developement/Environment/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 *;
+#}

+ 9 - 0
android-skin-loader-lib/src/main/AndroidManifest.xml

@@ -0,0 +1,9 @@
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+    package="com.example.android_skin_loader_lib">
+
+    <application android:allowBackup="true" android:label="@string/app_name"
+        android:supportsRtl="true">
+
+    </application>
+
+</manifest>

+ 83 - 0
android-skin-loader-lib/src/main/java/cn/feng/skin/manager/base/BaseActivity.java

@@ -0,0 +1,83 @@
+package cn.feng.skin.manager.base;
+
+import android.app.Activity;
+import android.os.Bundle;
+import android.view.View;
+
+import java.util.List;
+
+import cn.feng.skin.manager.entity.DynamicAttr;
+import cn.feng.skin.manager.listener.IDynamicNewView;
+import cn.feng.skin.manager.listener.ISkinUpdate;
+import cn.feng.skin.manager.loader.SkinInflaterFactory;
+import cn.feng.skin.manager.loader.SkinManager;
+
+/**
+ * Base Activity for development
+ * 
+ * <p>NOTICE:<br> 
+ * You should extends from this if you what to do skin change
+ * 
+ * @author fengjun
+ */
+public class BaseActivity extends Activity implements ISkinUpdate, IDynamicNewView{
+	
+	/**
+	 * Whether response to skin changing after create
+	 */
+	private boolean isResponseOnSkinChanging = true;
+	
+	private SkinInflaterFactory mSkinInflaterFactory;
+	
+	@Override
+	protected void onCreate(Bundle savedInstanceState) {
+		super.onCreate(savedInstanceState);
+		mSkinInflaterFactory = new SkinInflaterFactory();
+		getLayoutInflater().setFactory(mSkinInflaterFactory);
+	}
+	
+	@Override
+	protected void onResume() {
+		super.onResume();
+		SkinManager.getInstance().attach(this);
+	}
+	
+	@Override
+	protected void onDestroy() {
+		super.onDestroy();
+		SkinManager.getInstance().detach(this);
+		mSkinInflaterFactory.clean();
+	}
+	
+	/**
+	 * dynamic add a skin view 
+	 * 
+	 * @param view
+	 * @param attrName
+	 * @param attrValueResId
+	 */
+	protected void dynamicAddSkinEnableView(View view, String attrName, int attrValueResId){	
+		mSkinInflaterFactory.dynamicAddSkinEnableView(this, view, attrName, attrValueResId);
+	}
+	
+	protected void dynamicAddSkinEnableView(View view, List<DynamicAttr> pDAttrs){	
+		mSkinInflaterFactory.dynamicAddSkinEnableView(this, view, pDAttrs);
+	}
+	
+	final protected void enableResponseOnSkinChanging(boolean enable){
+		isResponseOnSkinChanging = enable;
+	}
+
+	@Override
+	public void onThemeUpdate() {
+		if(!isResponseOnSkinChanging){
+			return;
+		}
+		mSkinInflaterFactory.applySkin();
+	}
+
+	@Override
+	public void dynamicAddView(View view, List<DynamicAttr> pDAttrs) {
+		mSkinInflaterFactory.dynamicAddSkinEnableView(this, view, pDAttrs);
+	}
+}

+ 42 - 0
android-skin-loader-lib/src/main/java/cn/feng/skin/manager/base/BaseFragment.java

@@ -0,0 +1,42 @@
+package cn.feng.skin.manager.base;
+
+import android.content.Context;
+import android.os.Bundle;
+import android.support.v4.app.Fragment;
+import android.view.LayoutInflater;
+import android.view.View;
+
+import java.util.List;
+
+import cn.feng.skin.manager.entity.DynamicAttr;
+import cn.feng.skin.manager.listener.IDynamicNewView;
+
+public class BaseFragment extends Fragment implements IDynamicNewView{
+	
+	private IDynamicNewView mIDynamicNewView;
+	private LayoutInflater mLayoutInflater;
+
+	@Override
+	public void onAttach(Context context) {
+		super.onAttach(context);
+		try{
+			mIDynamicNewView = (IDynamicNewView)context;
+		}catch(ClassCastException e){
+			mIDynamicNewView = null;
+		}
+	}
+
+	@Override
+	public void dynamicAddView(View view, List<DynamicAttr> pDAttrs) {
+		if(mIDynamicNewView == null){
+			throw new RuntimeException("IDynamicNewView should be implements !");
+		}else{
+			mIDynamicNewView.dynamicAddView(view, pDAttrs);
+		}
+	}
+
+	public LayoutInflater getLayoutInflater(Bundle savedInstanceState) {
+		LayoutInflater result = getActivity().getLayoutInflater();
+		return result;
+	}
+}

+ 90 - 0
android-skin-loader-lib/src/main/java/cn/feng/skin/manager/base/BaseFragmentActivity.java

@@ -0,0 +1,90 @@
+package cn.feng.skin.manager.base;
+
+import android.os.Bundle;
+import android.support.v4.app.FragmentActivity;
+import android.view.LayoutInflater;
+import android.view.View;
+
+import java.lang.reflect.Field;
+import java.util.List;
+
+import cn.feng.skin.manager.entity.DynamicAttr;
+import cn.feng.skin.manager.listener.IDynamicNewView;
+import cn.feng.skin.manager.listener.ISkinUpdate;
+import cn.feng.skin.manager.loader.SkinInflaterFactory;
+import cn.feng.skin.manager.loader.SkinManager;
+
+/**
+ * Base Fragment Activity for development
+ * 
+ * <p>NOTICE:<br> 
+ * You should extends from this if you want to do skin change
+ * 
+ * @author fengjun
+ */
+public class BaseFragmentActivity extends FragmentActivity implements ISkinUpdate, IDynamicNewView{
+	
+	/**
+	 * Whether response to skin changing after create
+	 */
+	private boolean isResponseOnSkinChanging = true;
+	
+	private SkinInflaterFactory mSkinInflaterFactory;
+	
+	@Override
+	protected void onCreate(Bundle savedInstanceState) {
+		super.onCreate(savedInstanceState);
+	
+        try {
+            Field field = LayoutInflater.class.getDeclaredField("mFactorySet");
+            field.setAccessible(true);
+            field.setBoolean(getLayoutInflater(), false);
+            
+    		mSkinInflaterFactory = new SkinInflaterFactory();
+    		getLayoutInflater().setFactory(mSkinInflaterFactory);
+
+        } catch (NoSuchFieldException e) {
+            e.printStackTrace();
+        } catch (IllegalArgumentException e) {
+            e.printStackTrace();
+        } catch (IllegalAccessException e) {
+            e.printStackTrace();
+        } 
+		
+	}
+	
+	@Override
+	protected void onResume() {
+		super.onResume();
+		SkinManager.getInstance().attach(this);
+	}
+	
+	@Override
+	protected void onDestroy() {
+		super.onDestroy();
+		SkinManager.getInstance().detach(this);
+	}
+	
+	protected void dynamicAddSkinEnableView(View view, String attrName, int attrValueResId){	
+		mSkinInflaterFactory.dynamicAddSkinEnableView(this, view, attrName, attrValueResId);
+	}
+	
+	protected void dynamicAddSkinEnableView(View view, List<DynamicAttr> pDAttrs){	
+		mSkinInflaterFactory.dynamicAddSkinEnableView(this, view, pDAttrs);
+	}
+	
+	final protected void enableResponseOnSkinChanging(boolean enable){
+		isResponseOnSkinChanging = enable;
+	}
+
+	@Override
+	public void onThemeUpdate() {
+		if(!isResponseOnSkinChanging) return;
+		mSkinInflaterFactory.applySkin();
+	}
+	
+	@Override
+	public void dynamicAddView(View view, List<DynamicAttr> pDAttrs) {
+		mSkinInflaterFactory.dynamicAddSkinEnableView(this, view, pDAttrs);
+	}
+}

+ 33 - 0
android-skin-loader-lib/src/main/java/cn/feng/skin/manager/config/SkinConfig.java

@@ -0,0 +1,33 @@
+package cn.feng.skin.manager.config;
+
+import android.content.Context;
+import cn.feng.skin.manager.util.PreferencesUtils;
+
+public class SkinConfig {
+	public  static final String     NAMESPACE 				=   "http://schemas.android.com/android/skin";
+	public 	static final String 	SKIN_SUFFIX				= 	".theme";
+	public 	static final String 	SKIN_FOLER_NAME 		= 	"skin";
+	public 	static final String 	PREF_CUSTOM_SKIN_PATH 	= 	"cn_feng_skin_custom_path";
+	public  static final String 	DEFALT_SKIN 			= 	"cn_feng_skin_default";
+	public 	static final String 	SKIN_FROM	 			= 	"cn_feng_skin_from";
+	public 	static final int 		FROM_INTERNAL 			= 	0;
+    public 	static final int 		FROM_EXTERNAL 			= 	1;
+    public 	static final String 	ATTR_SKIN_ENABLE	    =   "enable";
+	
+	/**
+	 * get path of last skin package path
+	 * @param context
+	 * @return path of skin package
+	 */
+	public static String getCustomSkinPath(Context context){
+		return PreferencesUtils.getString(context, PREF_CUSTOM_SKIN_PATH, DEFALT_SKIN);
+	}
+	
+	public static void saveSkinPath(Context context, String path){
+		PreferencesUtils.putString(context, PREF_CUSTOM_SKIN_PATH, path);
+	}
+	
+	public static boolean isDefaultSkin(Context context){
+		return DEFALT_SKIN.equals(getCustomSkinPath(context));
+	}
+}

+ 61 - 0
android-skin-loader-lib/src/main/java/cn/feng/skin/manager/entity/AttrFactory.java

@@ -0,0 +1,61 @@
+package cn.feng.skin.manager.entity;
+
+
+public class AttrFactory {
+	
+	public static final String SRC = "src";
+	public static final String BACKGROUND = "background";
+	public static final String TEXT_COLOR = "textColor";
+	public static final String LIST_SELECTOR = "listSelector";
+	public static final String DIVIDER = "divider";
+	
+	public static SkinAttr get(String attrName, int attrValueRefId, String attrValueRefName, String typeName){
+		
+		SkinAttr mSkinAttr = null;
+
+		if (SRC.equals(attrName)) {
+			mSkinAttr = new SrcAttr();
+		} else if(BACKGROUND.equals(attrName)){
+			mSkinAttr = new BackgroundAttr();
+		}else if(TEXT_COLOR.equals(attrName)){ 
+			mSkinAttr = new TextColorAttr();
+		}else if(LIST_SELECTOR.equals(attrName)){ 
+			mSkinAttr = new ListSelectorAttr();
+		}else if(DIVIDER.equals(attrName)){ 
+			mSkinAttr = new DividerAttr();
+		}else{
+			return null;
+		}
+		
+		mSkinAttr.attrName = attrName;
+		mSkinAttr.attrValueRefId = attrValueRefId;
+		mSkinAttr.attrValueRefName = attrValueRefName;
+		mSkinAttr.attrValueTypeName = typeName;
+		return mSkinAttr;
+	}
+	
+	/**
+	 * Check whether the attribute is supported
+	 * @param attrName
+	 * @return true : supported <br>
+	 * 		   false: not supported
+	 */
+	public static boolean isSupportedAttr(String attrName){
+		if(SRC.equals(attrName)){
+			return true;
+		}
+		if(BACKGROUND.equals(attrName)){ 
+			return true;
+		}
+		if(TEXT_COLOR.equals(attrName)){ 
+			return true;
+		}
+		if(LIST_SELECTOR.equals(attrName)){ 
+			return true;
+		}
+		if(DIVIDER.equals(attrName)){ 
+			return true;
+		}
+		return false;
+	}
+}

+ 27 - 0
android-skin-loader-lib/src/main/java/cn/feng/skin/manager/entity/BackgroundAttr.java

@@ -0,0 +1,27 @@
+package cn.feng.skin.manager.entity;
+
+import android.graphics.drawable.Drawable;
+import android.util.Log;
+import android.view.View;
+import cn.feng.skin.manager.loader.SkinManager;
+
+public class BackgroundAttr extends SkinAttr {
+
+	@Override
+	public void apply(View view) {
+		
+		if(RES_TYPE_NAME_COLOR.equals(attrValueTypeName)){
+			view.setBackgroundColor(SkinManager.getInstance().getColor(attrValueRefId));
+			Log.i("attr", "_________________________________________________________");
+			Log.i("attr", "apply as color");
+		}else if(RES_TYPE_NAME_DRAWABLE.equals(attrValueTypeName)){
+			Drawable bg = SkinManager.getInstance().getDrawable(attrValueRefId);
+			view.setBackground(bg);
+			Log.i("attr", "_________________________________________________________");
+			Log.i("attr", "apply as drawable");
+			Log.i("attr", "bg.toString()  " + bg.toString());
+			
+			Log.i("attr", this.attrValueRefName + " 是否可变换状态? : " + bg.isStateful());
+		}
+	}
+}

+ 26 - 0
android-skin-loader-lib/src/main/java/cn/feng/skin/manager/entity/DividerAttr.java

@@ -0,0 +1,26 @@
+package cn.feng.skin.manager.entity;
+
+import android.graphics.drawable.ColorDrawable;
+import android.view.View;
+import android.widget.ListView;
+import cn.feng.skin.manager.loader.SkinManager;
+
+public class DividerAttr extends SkinAttr {
+
+	public int dividerHeight = 1;
+	
+	@Override
+	public void apply(View view) {
+		if(view instanceof ListView){
+			ListView tv = (ListView)view;
+			if(RES_TYPE_NAME_COLOR.equals(attrValueTypeName)){
+				int color = SkinManager.getInstance().getColor(attrValueRefId);
+				ColorDrawable sage = new ColorDrawable(color);
+				tv.setDivider(sage);
+				tv.setDividerHeight(dividerHeight);
+			}else if(RES_TYPE_NAME_DRAWABLE.equals(attrValueTypeName)){
+				tv.setDivider(SkinManager.getInstance().getDrawable(attrValueRefId));
+			}
+		}
+	}
+}

+ 23 - 0
android-skin-loader-lib/src/main/java/cn/feng/skin/manager/entity/DynamicAttr.java

@@ -0,0 +1,23 @@
+package cn.feng.skin.manager.entity;
+
+public class DynamicAttr {
+	
+	/**
+	 * attr name , defined from {@link AttrFactory} :<br>
+	 * should be
+	 * <li> AttrFactory.BACKGROUND
+	 * <li> AttrFactory.TEXT_COLOR <br>
+	 * ...and so on
+	 */
+	public String attrName;
+	
+	/**
+	 * resource id from default context , eg: "R.drawable.app_bg"
+	 */
+	public int refResId;
+	
+	public DynamicAttr(String attrName, int refResId){
+		this.attrName = attrName;
+		this.refResId = refResId;
+	}
+}

+ 21 - 0
android-skin-loader-lib/src/main/java/cn/feng/skin/manager/entity/ListSelectorAttr.java

@@ -0,0 +1,21 @@
+package cn.feng.skin.manager.entity;
+
+import android.view.View;
+import android.widget.AbsListView;
+import cn.feng.skin.manager.loader.SkinManager;
+
+public class ListSelectorAttr extends SkinAttr {
+
+	@Override
+	public void apply(View view) {
+		if(view instanceof AbsListView){
+			AbsListView tv = (AbsListView)view;
+			
+			if(RES_TYPE_NAME_COLOR.equals(attrValueTypeName)){
+				tv.setSelector(SkinManager.getInstance().getColor(attrValueRefId));
+			}else if(RES_TYPE_NAME_DRAWABLE.equals(attrValueTypeName)){
+				tv.setSelector(SkinManager.getInstance().getDrawable(attrValueRefId));
+			}
+		}
+	}
+}

+ 45 - 0
android-skin-loader-lib/src/main/java/cn/feng/skin/manager/entity/SkinAttr.java

@@ -0,0 +1,45 @@
+package cn.feng.skin.manager.entity;
+
+import android.view.View;
+
+public abstract class SkinAttr {
+	
+	protected static final String RES_TYPE_NAME_COLOR = "color";
+	protected static final String RES_TYPE_NAME_DRAWABLE = "drawable";
+	
+	/**
+	 * name of the attr, ex: background or textSize or textColor
+	 */
+	public String attrName;
+	
+	/**
+	 * id of the attr value refered to, normally is [2130745655]
+	 */
+	public int attrValueRefId;
+	
+	/**
+	 * entry name of the value , such as [app_exit_btn_background]
+	 */
+	public String attrValueRefName;
+	
+	/**
+	 * type of the value , such as color or drawable
+	 */
+	public String attrValueTypeName;
+	
+	/**
+	 * Use to apply view with new TypedValue
+	 * @param view
+	 * @param tv
+	 */
+	public abstract void apply(View view);
+
+	@Override
+	public String toString() {
+		return "SkinAttr \n[\nattrName=" + attrName + ", \n"
+				+ "attrValueRefId=" + attrValueRefId + ", \n"
+				+ "attrValueRefName=" + attrValueRefName + ", \n"
+				+ "attrValueTypeName=" + attrValueTypeName
+				+ "\n]";
+	}
+}

+ 42 - 0
android-skin-loader-lib/src/main/java/cn/feng/skin/manager/entity/SkinItem.java

@@ -0,0 +1,42 @@
+package cn.feng.skin.manager.entity;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import cn.feng.skin.manager.util.ListUtils;
+import android.content.res.Resources;
+import android.view.View;
+
+public class SkinItem {
+	
+	public View view;
+	
+	public List<SkinAttr> attrs;
+	
+	public SkinItem(){
+		attrs = new ArrayList<SkinAttr>();
+	}
+	
+	public void apply(){
+		if(ListUtils.isEmpty(attrs)){
+			return;
+		}
+		for(SkinAttr at : attrs){
+			at.apply(view);
+		}
+	}
+	
+	public void clean(){
+		if(ListUtils.isEmpty(attrs)){
+			return;
+		}
+		for(SkinAttr at : attrs){
+			at = null;
+		}
+	}
+
+	@Override
+	public String toString() {
+		return "SkinItem [view=" + view.getClass().getSimpleName() + ", attrs=" + attrs + "]";
+	}
+}

+ 24 - 0
android-skin-loader-lib/src/main/java/cn/feng/skin/manager/entity/SrcAttr.java

@@ -0,0 +1,24 @@
+package cn.feng.skin.manager.entity;
+
+import android.view.View;
+import android.widget.ImageView;
+
+import cn.feng.skin.manager.loader.SkinManager;
+import cn.feng.skin.manager.util.L;
+
+/**
+ * Created by Jason on 2018/9/16.
+ */
+
+public class SrcAttr extends SkinAttr {
+    @Override
+    public void apply(View view) {
+        if(view instanceof ImageView){
+            ImageView iv = (ImageView)view;
+            if(RES_TYPE_NAME_DRAWABLE.equals(attrValueTypeName)){
+                L.e("attr1", "SrcAttr");
+                iv.setImageDrawable(SkinManager.getInstance().getDrawable(attrValueRefId));
+            }
+        }
+    }
+}

+ 21 - 0
android-skin-loader-lib/src/main/java/cn/feng/skin/manager/entity/TextColorAttr.java

@@ -0,0 +1,21 @@
+package cn.feng.skin.manager.entity;
+
+import cn.feng.skin.manager.loader.SkinManager;
+import cn.feng.skin.manager.util.L;
+import android.util.Log;
+import android.view.View;
+import android.widget.TextView;
+
+public class TextColorAttr extends SkinAttr {
+
+	@Override
+	public void apply(View view) {
+		if(view instanceof TextView){
+			TextView tv = (TextView)view;
+			if(RES_TYPE_NAME_COLOR.equals(attrValueTypeName)){
+				L.e("attr1", "TextColorAttr");
+				tv.setTextColor(SkinManager.getInstance().convertToColorStateList(attrValueRefId));
+			}
+		}
+	}
+}

+ 8 - 0
android-skin-loader-lib/src/main/java/cn/feng/skin/manager/listener/IAttrUpdate.java

@@ -0,0 +1,8 @@
+package cn.feng.skin.manager.listener;
+
+import android.util.TypedValue;
+import android.view.View;
+
+public interface IAttrUpdate {
+	void apply(View view, TypedValue tv);
+}

+ 10 - 0
android-skin-loader-lib/src/main/java/cn/feng/skin/manager/listener/IDynamicNewView.java

@@ -0,0 +1,10 @@
+package cn.feng.skin.manager.listener;
+
+import java.util.List;
+
+import android.view.View;
+import cn.feng.skin.manager.entity.DynamicAttr;
+
+public interface IDynamicNewView {
+	void dynamicAddView(View view, List<DynamicAttr> pDAttrs);
+}

+ 7 - 0
android-skin-loader-lib/src/main/java/cn/feng/skin/manager/listener/ILoaderListener.java

@@ -0,0 +1,7 @@
+package cn.feng.skin.manager.listener;
+
+public interface ILoaderListener {
+	public void onStart();
+	public void onSuccess();
+	public void onFailed();
+}

+ 4 - 0
android-skin-loader-lib/src/main/java/cn/feng/skin/manager/listener/IResourceParser.java

@@ -0,0 +1,4 @@
+package cn.feng.skin.manager.listener;
+
+public interface IResourceParser {
+}

+ 14 - 0
android-skin-loader-lib/src/main/java/cn/feng/skin/manager/listener/ISkinChecker.java

@@ -0,0 +1,14 @@
+package cn.feng.skin.manager.listener;
+
+import android.content.Context;
+
+public interface ISkinChecker {
+	
+	/**
+	 * Check whether the skin is exits or legal
+	 * @param context
+	 * @param path
+	 * @return 
+	 */
+	boolean isSkinPackageLegality(Context context, String path);
+}

+ 8 - 0
android-skin-loader-lib/src/main/java/cn/feng/skin/manager/listener/ISkinLoader.java

@@ -0,0 +1,8 @@
+package cn.feng.skin.manager.listener;
+
+public interface ISkinLoader {
+	void attach(ISkinUpdate observer);
+	void detach(ISkinUpdate observer);
+	void notifySkinUpdate();
+//	void notifySkinDefault();
+}

+ 12 - 0
android-skin-loader-lib/src/main/java/cn/feng/skin/manager/listener/ISkinUpdate.java

@@ -0,0 +1,12 @@
+package cn.feng.skin.manager.listener;
+
+
+/**
+ * Call back when theme has changed </br>
+ * Normally implements by activity of fragment
+ * 
+ * @author fengjun
+ */
+public interface ISkinUpdate {
+	void onThemeUpdate();	
+}

+ 200 - 0
android-skin-loader-lib/src/main/java/cn/feng/skin/manager/loader/SkinInflaterFactory.java

@@ -0,0 +1,200 @@
+package cn.feng.skin.manager.loader;
+
+import android.content.Context;
+import android.content.res.Resources.NotFoundException;
+import android.util.AttributeSet;
+import android.view.LayoutInflater;
+import android.view.LayoutInflater.Factory;
+import android.view.View;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import cn.feng.skin.manager.config.SkinConfig;
+import cn.feng.skin.manager.entity.AttrFactory;
+import cn.feng.skin.manager.entity.DynamicAttr;
+import cn.feng.skin.manager.entity.SkinAttr;
+import cn.feng.skin.manager.entity.SkinItem;
+import cn.feng.skin.manager.util.L;
+import cn.feng.skin.manager.util.ListUtils;
+
+/**
+ * Supply {@link SkinInflaterFactory} to be called when inflating from a LayoutInflater.
+ * 
+ * <p>Use this to collect the {skin:enable="true|false"} views availabled in our XML layout files.
+ * 
+ * @author fengjun
+ */
+public class SkinInflaterFactory implements Factory {
+	
+	private static final boolean DEBUG = true;
+	
+	/**
+	 * Store the view item that need skin changing in the activity
+	 */
+	private List<SkinItem> mSkinItems = new ArrayList<SkinItem>();
+	
+	@Override
+	public View onCreateView(String name, Context context, AttributeSet attrs) {
+		// if this is NOT enable to be skined , simplly skip it 
+		boolean isSkinEnable = attrs.getAttributeBooleanValue(SkinConfig.NAMESPACE, SkinConfig.ATTR_SKIN_ENABLE, false);
+        if (!isSkinEnable){
+        		return null;
+        }
+		
+		View view = createView(context, name, attrs);
+		
+		if (view == null){
+			return null;
+		}
+		
+		parseSkinAttr(context, attrs, view);
+		
+		return view;
+	}
+	
+	/**
+     * Invoke low-level function for instantiating a view by name. This attempts to
+     * instantiate a view class of the given <var>name</var> found in this
+     * LayoutInflater's ClassLoader.
+     * 
+     * @param context 
+     * @param name The full name of the class to be instantiated.
+     * @param attrs The XML attributes supplied for this instance.
+     * 
+     * @return View The newly instantiated view, or null.
+     */
+	private View createView(Context context, String name, AttributeSet attrs) {
+		View view = null;
+		try {
+			if (-1 == name.indexOf('.')){
+				if ("View".equals(name)) {
+					view = LayoutInflater.from(context).createView(name, "android.view.", attrs);
+				} 
+				if (view == null) {
+					view = LayoutInflater.from(context).createView(name, "android.widget.", attrs);
+				} 
+				if (view == null) {
+					view = LayoutInflater.from(context).createView(name, "android.webkit.", attrs);
+				} 
+			}else {
+	            view = LayoutInflater.from(context).createView(name, null, attrs);
+	        }
+
+			L.i("about to create " + name);
+
+		} catch (Exception e) { 
+			L.e("error while create 【" + name + "】 : " + e.getMessage());
+			view = null;
+		}
+		return view;
+	}
+
+	/**
+	 * Collect skin able tag such as background , textColor and so on
+	 * 
+	 * @param context
+	 * @param attrs
+	 * @param view
+	 */
+	private void parseSkinAttr(Context context, AttributeSet attrs, View view) {
+		List<SkinAttr> viewAttrs = new ArrayList<SkinAttr>();
+		
+		for (int i = 0; i < attrs.getAttributeCount(); i++){
+			String attrName = attrs.getAttributeName(i);
+			String attrValue = attrs.getAttributeValue(i);
+			
+			if(!AttrFactory.isSupportedAttr(attrName)){
+				continue;
+			}
+			
+		    if(attrValue.startsWith("@")){
+				try {
+					int id = Integer.parseInt(attrValue.substring(1));
+					String entryName = context.getResources().getResourceEntryName(id);
+					String typeName = context.getResources().getResourceTypeName(id);
+					SkinAttr mSkinAttr = AttrFactory.get(attrName, id, entryName, typeName);
+					if (mSkinAttr != null) {
+						viewAttrs.add(mSkinAttr);
+					}
+				} catch (NumberFormatException e) {
+					e.printStackTrace();
+				} catch (NotFoundException e) {
+					e.printStackTrace();
+				}
+		    }
+		}
+		
+		if(!ListUtils.isEmpty(viewAttrs)){
+			SkinItem skinItem = new SkinItem();
+			skinItem.view = view;
+			skinItem.attrs = viewAttrs;
+
+			mSkinItems.add(skinItem);
+			
+			if(SkinManager.getInstance().isExternalSkin()){
+				skinItem.apply();
+			}
+		}
+	}
+	
+	public void applySkin(){
+		if(ListUtils.isEmpty(mSkinItems)){
+			return;
+		}
+		
+		for(SkinItem si : mSkinItems){
+			if(si.view == null){
+				continue;
+			}
+			si.apply();
+		}
+	}
+	
+	public void dynamicAddSkinEnableView(Context context, View view, List<DynamicAttr> pDAttrs){	
+		List<SkinAttr> viewAttrs = new ArrayList<SkinAttr>();
+		SkinItem skinItem = new SkinItem();
+		skinItem.view = view;
+		
+		for(DynamicAttr dAttr : pDAttrs){
+			int id = dAttr.refResId;
+			String entryName = context.getResources().getResourceEntryName(id);
+			String typeName = context.getResources().getResourceTypeName(id);
+			SkinAttr mSkinAttr = AttrFactory.get(dAttr.attrName, id, entryName, typeName);
+			viewAttrs.add(mSkinAttr);
+		}
+		
+		skinItem.attrs = viewAttrs;
+		addSkinView(skinItem);
+	}
+	
+	public void dynamicAddSkinEnableView(Context context, View view, String attrName, int attrValueResId){	
+		int id = attrValueResId;
+		String entryName = context.getResources().getResourceEntryName(id);
+		String typeName = context.getResources().getResourceTypeName(id);
+		SkinAttr mSkinAttr = AttrFactory.get(attrName, id, entryName, typeName);
+		SkinItem skinItem = new SkinItem();
+		skinItem.view = view;
+		List<SkinAttr> viewAttrs = new ArrayList<SkinAttr>();
+		viewAttrs.add(mSkinAttr);
+		skinItem.attrs = viewAttrs;
+		addSkinView(skinItem);
+	}
+	
+	public void addSkinView(SkinItem item){
+		mSkinItems.add(item);
+	}
+	
+	public void clean(){
+		if(ListUtils.isEmpty(mSkinItems)){
+			return;
+		}
+		
+		for(SkinItem si : mSkinItems){
+			if(si.view == null){
+				continue;
+			}
+			si.clean();
+		}
+	}
+}

+ 325 - 0
android-skin-loader-lib/src/main/java/cn/feng/skin/manager/loader/SkinManager.java

@@ -0,0 +1,325 @@
+package cn.feng.skin.manager.loader;
+
+import java.io.File;
+import java.lang.reflect.Method;
+import java.util.ArrayList;
+import java.util.List;
+
+import android.annotation.SuppressLint;
+import android.content.Context;
+import android.content.pm.PackageInfo;
+import android.content.pm.PackageManager;
+import android.content.res.AssetManager;
+import android.content.res.ColorStateList;
+import android.content.res.Resources;
+import android.content.res.Resources.NotFoundException;
+import android.graphics.drawable.Drawable;
+import android.os.AsyncTask;
+import cn.feng.skin.manager.config.SkinConfig;
+import cn.feng.skin.manager.listener.ILoaderListener;
+import cn.feng.skin.manager.listener.ISkinLoader;
+import cn.feng.skin.manager.listener.ISkinUpdate;
+import cn.feng.skin.manager.util.L;
+
+/**
+ * Skin Manager Instance
+ *
+ * 
+ * <ul>
+ * <strong>global init skin manager, MUST BE CALLED FIRST ! </strong>
+ * <li> {@link #init()} </li>
+ * </ul>
+ * <ul>
+ * <strong>get single runtime instance</strong>
+ * <li> {@link #getInstance()} </li>
+ * </ul>
+ * <ul>
+ * <strong>attach a listener (Activity or fragment) to SkinManager</strong>
+ * <li> {@link #onAttach(ISkinUpdate observer)} </li>
+ * </ul>
+ * <ul>
+ * <strong>detach a listener (Activity or fragment) to SkinManager</strong>
+ * <li> {@link #detach(ISkinUpdate observer)} </li>
+ * </ul>
+ * <ul>
+ * <strong>load latest theme </strong>
+ * <li> {@link #load()} </li>
+ * <li> {@link #load(ILoaderListener callback)} </li>
+ * </ul>
+ * <ul>
+ * <strong>load new theme with the giving skinPackagePath</strong>
+ * <li> {@link #load(String skinPackagePath,ILoaderListener callback)} </li>
+ * </ul>
+ * 
+ * 
+ * @author fengjun
+ */
+public class SkinManager implements ISkinLoader{
+	
+	private static final String NOT_INIT_ERROR = "SkinManager MUST init with Context first";
+	private static Object synchronizedLock = new Object();
+	private static SkinManager instance;
+
+	private List<ISkinUpdate> skinObservers;
+	private Context context;
+	private String skinPackageName;
+	private Resources mResources;
+	private String skinPath;
+	private boolean isDefaultSkin = false;
+	
+	/**
+	 * whether the skin being used is from external .skin file 
+	 * @return is external skin = true
+	 */
+	public boolean isExternalSkin(){
+		return !isDefaultSkin && mResources != null;
+	}
+	
+	/**
+	 * get current skin path
+	 * @return current skin path
+	 */
+	public String getSkinPath() {
+		return skinPath;
+	}
+
+	/**
+	 * return a global static instance of {@link SkinManager}
+	 * @return
+	 */
+	public static SkinManager getInstance() {
+		if (instance == null) {
+			synchronized (synchronizedLock) {
+				if (instance == null){
+					instance = new SkinManager();
+				}
+			}
+		}
+		return instance;
+	}
+	
+	public String getSkinPackageName() {
+		return skinPackageName;
+	}
+	
+	public Resources getResources(){
+		return mResources;
+	}
+	
+	private SkinManager() { }
+	
+	public void init(Context ctx){
+		context = ctx.getApplicationContext();
+	}
+	
+	public void restoreDefaultTheme(){
+		SkinConfig.saveSkinPath(context, SkinConfig.DEFALT_SKIN);
+		isDefaultSkin = true;
+		mResources = context.getResources();
+		notifySkinUpdate();
+	}
+
+	public void load(){
+		String skin = SkinConfig.getCustomSkinPath(context);
+		load(skin, null);
+	}
+	
+	public void load(ILoaderListener callback){
+		String skin = SkinConfig.getCustomSkinPath(context);
+		if(SkinConfig.isDefaultSkin(context)){ return; }
+		load(skin, callback);
+	}
+	
+	/**
+	 * Load resources from apk in asyc task
+	 * @param skinPackagePath path of skin apk
+	 * @param callback callback to notify user
+	 */
+	public void load(String skinPackagePath, final ILoaderListener callback) {
+		
+		new AsyncTask<String, Void, Resources>() {
+
+			protected void onPreExecute() {
+				if (callback != null) {
+					callback.onStart();
+				}
+			};
+
+			@Override
+			protected Resources doInBackground(String... params) {
+				try {
+					if (params.length == 1) {
+						String skinPkgPath = params[0];
+						
+						File file = new File(skinPkgPath); 
+						if(file == null || !file.exists()){
+							return null;
+						}
+						
+						PackageManager mPm = context.getPackageManager();
+						PackageInfo mInfo = mPm.getPackageArchiveInfo(skinPkgPath, PackageManager.GET_ACTIVITIES);
+						skinPackageName = mInfo.packageName;
+
+						AssetManager assetManager = AssetManager.class.newInstance();
+						Method addAssetPath = assetManager.getClass().getMethod("addAssetPath", String.class);
+						addAssetPath.invoke(assetManager, skinPkgPath);
+
+						Resources superRes = context.getResources();
+						Resources skinResource = new Resources(assetManager,superRes.getDisplayMetrics(),superRes.getConfiguration());
+						
+						SkinConfig.saveSkinPath(context, skinPkgPath);
+						
+						skinPath = skinPkgPath;
+						isDefaultSkin = false;
+						return skinResource;
+					}
+					return null;
+				} catch (Exception e) {
+					e.printStackTrace();
+					return null;
+				}
+			};
+
+			protected void onPostExecute(Resources result) {
+				mResources = result;
+
+				if (mResources != null) {
+					if (callback != null) callback.onSuccess();
+					notifySkinUpdate();
+				}else{
+					isDefaultSkin = true;
+					if (callback != null) callback.onFailed();
+				}
+			};
+
+		}.execute(skinPackagePath);
+	}
+	
+	@Override
+	public void attach(ISkinUpdate observer) {
+		if(skinObservers == null){
+			skinObservers = new ArrayList<ISkinUpdate>();
+		}
+		if(!skinObservers.contains(skinObservers)){
+			skinObservers.add(observer);
+		}
+	}
+
+	@Override
+	public void detach(ISkinUpdate observer) {
+		if(skinObservers == null) return;
+		if(skinObservers.contains(observer)){
+			skinObservers.remove(observer);
+		}
+	}
+
+	@Override
+	public void notifySkinUpdate() {
+		if(skinObservers == null) return;
+		for(ISkinUpdate observer : skinObservers){
+			observer.onThemeUpdate();
+		}
+	}
+	
+	public int getColor(int resId){
+		int originColor = context.getResources().getColor(resId);
+		if(mResources == null || isDefaultSkin){
+			return originColor;
+		}
+		
+		String resName = context.getResources().getResourceEntryName(resId);
+		
+		int trueResId = mResources.getIdentifier(resName, "color", skinPackageName);
+		int trueColor = 0;
+		
+		try{
+			trueColor = mResources.getColor(trueResId);
+		}catch(NotFoundException e){
+			e.printStackTrace();
+			trueColor = originColor;
+		}
+		
+		return trueColor;
+	}
+	
+	@SuppressLint("NewApi")
+	public Drawable getDrawable(int resId){
+		Drawable originDrawable = context.getResources().getDrawable(resId);
+		if(mResources == null || isDefaultSkin){
+			return originDrawable;
+		}
+		String resName = context.getResources().getResourceEntryName(resId);
+		
+		int trueResId = mResources.getIdentifier(resName, "drawable", skinPackageName);
+		
+		Drawable trueDrawable = null;
+		try{
+			if(android.os.Build.VERSION.SDK_INT < 22){
+				trueDrawable = mResources.getDrawable(trueResId);
+			}else{
+				trueDrawable = mResources.getDrawable(trueResId, null);
+			}
+		}catch(NotFoundException e){
+			e.printStackTrace();
+			trueDrawable = originDrawable;
+		}
+		
+		return trueDrawable;
+	}
+	
+	/**
+	 * 加载指定资源颜色drawable,转化为ColorStateList,保证selector类型的Color也能被转换。</br>
+	 * 无皮肤包资源返回默认主题颜色
+	 * @author pinotao
+	 * @param resId
+	 * @return
+	 */
+	public ColorStateList convertToColorStateList(int resId) {
+		L.e("attr1", "convertToColorStateList");
+		
+		boolean isExtendSkin = true;
+		
+		if (mResources == null || isDefaultSkin) {
+			isExtendSkin = false;
+		}
+		
+		String resName = context.getResources().getResourceEntryName(resId);
+		L.e("attr1", "resName = " + resName);
+		if (isExtendSkin) {
+			L.e("attr1", "isExtendSkin");
+			int trueResId = mResources.getIdentifier(resName, "color", skinPackageName);
+			L.e("attr1", "trueResId = " + trueResId);
+			ColorStateList trueColorList = null;
+			if (trueResId == 0) { // 如果皮肤包没有复写该资源,但是需要判断是否是ColorStateList
+				try {
+					ColorStateList originColorList = context.getResources().getColorStateList(resId);
+					return originColorList;
+				} catch (NotFoundException e) {
+					e.printStackTrace();
+					L.e("resName = " + resName + " NotFoundException : "+ e.getMessage());
+				}
+			} else {
+				try {
+					trueColorList = mResources.getColorStateList(trueResId);
+					L.e("attr1", "trueColorList = " + trueColorList);
+					return trueColorList;
+				} catch (NotFoundException e) {
+					e.printStackTrace();
+					L.w("resName = " + resName + " NotFoundException :" + e.getMessage());
+				}
+			}
+		} else {
+			try {
+				ColorStateList originColorList = context.getResources().getColorStateList(resId);
+				return originColorList;
+			} catch (NotFoundException e) {
+				e.printStackTrace();
+				L.w("resName = " + resName + " NotFoundException :" + e.getMessage());
+			}
+
+		}
+
+		int[][] states = new int[1][1];
+		return new ColorStateList(states, new int[] { context.getResources().getColor(resId) });
+	}
+}

+ 179 - 0
android-skin-loader-lib/src/main/java/cn/feng/skin/manager/util/ArrayUtils.java

@@ -0,0 +1,179 @@
+package cn.feng.skin.manager.util;
+
+/**
+ * Array Utils
+ * <ul>
+ * <li>{@link #isEmpty(Object[])} is null or its length is 0</li>
+ * <li>{@link #getLast(Object[], Object, Object, boolean)} get last element of the target element, before the first one
+ * that match the target element front to back</li>
+ * <li>{@link #getNext(Object[], Object, Object, boolean)} get next element of the target element, after the first one
+ * that match the target element front to back</li>
+ * <li>{@link #getLast(Object[], Object, boolean)}</li>
+ * <li>{@link #getLast(int[], int, int, boolean)}</li>
+ * <li>{@link #getLast(long[], long, long, boolean)}</li>
+ * <li>{@link #getNext(Object[], Object, boolean)}</li>
+ * <li>{@link #getNext(int[], int, int, boolean)}</li>
+ * <li>{@link #getNext(long[], long, long, boolean)}</li>
+ * </ul>
+ * 
+ * @author <a href="http://www.trinea.cn" target="_blank">Trinea</a> 2011-10-24
+ */
+public class ArrayUtils {
+
+    private ArrayUtils() {
+        throw new AssertionError();
+    }
+
+    /**
+     * is null or its length is 0
+     * 
+     * @param <V>
+     * @param sourceArray
+     * @return
+     */
+    public static <V> boolean isEmpty(V[] sourceArray) {
+        return (sourceArray == null || sourceArray.length == 0);
+    }
+
+    /**
+     * get last element of the target element, before the first one that match the target element front to back
+     * <ul>
+     * <li>if array is empty, return defaultValue</li>
+     * <li>if target element is not exist in array, return defaultValue</li>
+     * <li>if target element exist in array and its index is not 0, return the last element</li>
+     * <li>if target element exist in array and its index is 0, return the last one in array if isCircle is true, else
+     * return defaultValue</li>
+     * </ul>
+     * 
+     * @param <V>
+     * @param sourceArray
+     * @param value value of target element
+     * @param defaultValue default return value
+     * @param isCircle whether is circle
+     * @return
+     */
+    public static <V> V getLast(V[] sourceArray, V value, V defaultValue, boolean isCircle) {
+        if (isEmpty(sourceArray)) {
+            return defaultValue;
+        }
+
+        int currentPosition = -1;
+        for (int i = 0; i < sourceArray.length; i++) {
+            if (ObjectUtils.isEquals(value, sourceArray[i])) {
+                currentPosition = i;
+                break;
+            }
+        }
+        if (currentPosition == -1) {
+            return defaultValue;
+        }
+
+        if (currentPosition == 0) {
+            return isCircle ? sourceArray[sourceArray.length - 1] : defaultValue;
+        }
+        return sourceArray[currentPosition - 1];
+    }
+
+    /**
+     * get next element of the target element, after the first one that match the target element front to back
+     * <ul>
+     * <li>if array is empty, return defaultValue</li>
+     * <li>if target element is not exist in array, return defaultValue</li>
+     * <li>if target element exist in array and not the last one in array, return the next element</li>
+     * <li>if target element exist in array and the last one in array, return the first one in array if isCircle is
+     * true, else return defaultValue</li>
+     * </ul>
+     * 
+     * @param <V>
+     * @param sourceArray
+     * @param value value of target element
+     * @param defaultValue default return value
+     * @param isCircle whether is circle
+     * @return
+     */
+    public static <V> V getNext(V[] sourceArray, V value, V defaultValue, boolean isCircle) {
+        if (isEmpty(sourceArray)) {
+            return defaultValue;
+        }
+
+        int currentPosition = -1;
+        for (int i = 0; i < sourceArray.length; i++) {
+            if (ObjectUtils.isEquals(value, sourceArray[i])) {
+                currentPosition = i;
+                break;
+            }
+        }
+        if (currentPosition == -1) {
+            return defaultValue;
+        }
+
+        if (currentPosition == sourceArray.length - 1) {
+            return isCircle ? sourceArray[0] : defaultValue;
+        }
+        return sourceArray[currentPosition + 1];
+    }
+
+    /**
+     * @see {@link ArrayUtils#getLast(Object[], Object, Object, boolean)} defaultValue is null
+     */
+    public static <V> V getLast(V[] sourceArray, V value, boolean isCircle) {
+        return getLast(sourceArray, value, null, isCircle);
+    }
+
+    /**
+     * @see {@link ArrayUtils#getNext(Object[], Object, Object, boolean)} defaultValue is null
+     */
+    public static <V> V getNext(V[] sourceArray, V value, boolean isCircle) {
+        return getNext(sourceArray, value, null, isCircle);
+    }
+
+    /**
+     * @see {@link ArrayUtils#getLast(Object[], Object, Object, boolean)} Object is Long
+     */
+    public static long getLast(long[] sourceArray, long value, long defaultValue, boolean isCircle) {
+        if (sourceArray.length == 0) {
+            throw new IllegalArgumentException("The length of source array must be greater than 0.");
+        }
+
+        Long[] array = ObjectUtils.transformLongArray(sourceArray);
+        return getLast(array, value, defaultValue, isCircle);
+
+    }
+
+    /**
+     * @see {@link ArrayUtils#getNext(Object[], Object, Object, boolean)} Object is Long
+     */
+    public static long getNext(long[] sourceArray, long value, long defaultValue, boolean isCircle) {
+        if (sourceArray.length == 0) {
+            throw new IllegalArgumentException("The length of source array must be greater than 0.");
+        }
+
+        Long[] array = ObjectUtils.transformLongArray(sourceArray);
+        return getNext(array, value, defaultValue, isCircle);
+    }
+
+    /**
+     * @see {@link ArrayUtils#getLast(Object[], Object, Object, boolean)} Object is Integer
+     */
+    public static int getLast(int[] sourceArray, int value, int defaultValue, boolean isCircle) {
+        if (sourceArray.length == 0) {
+            throw new IllegalArgumentException("The length of source array must be greater than 0.");
+        }
+
+        Integer[] array = ObjectUtils.transformIntArray(sourceArray);
+        return getLast(array, value, defaultValue, isCircle);
+
+    }
+
+    /**
+     * @see {@link ArrayUtils#getNext(Object[], Object, Object, boolean)} Object is Integer
+     */
+    public static int getNext(int[] sourceArray, int value, int defaultValue, boolean isCircle) {
+        if (sourceArray.length == 0) {
+            throw new IllegalArgumentException("The length of source array must be greater than 0.");
+        }
+
+        Integer[] array = ObjectUtils.transformIntArray(sourceArray);
+        return getNext(array, value, defaultValue, isCircle);
+    }
+}

+ 90 - 0
android-skin-loader-lib/src/main/java/cn/feng/skin/manager/util/CommonBaseAdapter.java

@@ -0,0 +1,90 @@
+package cn.feng.skin.manager.util;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import android.content.Context;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.BaseAdapter;
+
+/**
+ * @description  通用listview适配器
+ * @author       fengjun
+ * @version      1.0
+ * @created      2015-5-15
+ */
+public abstract class CommonBaseAdapter<T> extends BaseAdapter {
+
+	protected List<T> mDatas;
+	protected LayoutInflater mInflater;
+	protected Context context;
+	protected int itemLayoutId;
+	
+	/** 同一个列表中的item可能有多种布局文件 */
+	private int[] itemLayoutIds;
+	
+	public CommonBaseAdapter(Context context, List<T> mDatas, int[] itemLayoutIds) {
+		this.context = context;
+		this.mDatas= mDatas;
+		this.itemLayoutIds = itemLayoutIds;
+	}
+	
+	public List<T> getList(){
+		if(mDatas == null){
+			mDatas = new ArrayList<T>();
+		}
+		
+		return mDatas;
+	}
+	
+	public void setList(List<T> mDatas){
+		this.mDatas= mDatas;
+		this.notifyDataSetChanged();
+	}
+	
+	@Override
+	public int getCount() {
+		return (mDatas == null) ? 0 : mDatas.size();
+	}
+
+	@Override
+	public T getItem(int position) {
+		if(mDatas != null && position < mDatas.size()){
+			return mDatas.get(position);
+		}
+		else{ 
+			return null;
+		}
+	}
+
+	@Override
+	public long getItemId(int position) {
+		return position;
+	}
+	
+	/**
+	 * 默认指定第一种item布局
+	 * 如果要实现列表中的不同布局,请复写此方法<br>
+	 * 根据position对应的bean相关属性指定itemLayoutId
+	 * @param position
+	 * @return 对应的itemlayout
+	 */
+	protected int getItemLayout(int position){
+		return itemLayoutIds[0];
+	}
+
+	@Override
+	public View getView(int position, View convertView, ViewGroup parent) {
+		
+		CommonViewHolder holder = CommonViewHolder.get(context, convertView, parent, getItemLayout(position), position);
+		
+		convertItemView(holder, getItem(position), position);
+		
+		return holder.getConvertView();
+	}
+	
+	public abstract void convertItemView(CommonViewHolder holder, T item, int position);
+
+}

+ 127 - 0
android-skin-loader-lib/src/main/java/cn/feng/skin/manager/util/CommonViewHolder.java

@@ -0,0 +1,127 @@
+package cn.feng.skin.manager.util;
+
+import android.content.Context;
+import android.util.SparseArray;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.View.OnClickListener;
+import android.view.ViewGroup;
+import android.widget.ImageView;
+import android.widget.RatingBar;
+import android.widget.TextView;
+
+/**
+ * Common View Holder to establish a absview
+ * 
+ * @author fengjun
+ */
+public class CommonViewHolder {
+	
+	private SparseArray<View> mViews;
+	private int mPosition;
+	private View mConvertView;
+	private Context context;
+	
+	public CommonViewHolder(Context context, ViewGroup parent, int itemLayoutId, int position) {
+		this.context = context;
+		mPosition = position;
+		mViews = new SparseArray<View>();
+		mConvertView = LayoutInflater.from(context).inflate(itemLayoutId, parent, false);
+		mConvertView.setTag(this);
+	}
+	
+	public static CommonViewHolder get(Context context, View convertView, ViewGroup parent, int layoutId, int position){
+		if(convertView == null){
+			return new CommonViewHolder(context, parent, layoutId, position);
+		}else{
+			CommonViewHolder holder = (CommonViewHolder)convertView.getTag();
+			// 每次都对position进行更新
+			holder.mPosition = position;
+			return holder;
+		}
+	}
+	
+	public <T extends View> T getView(int viewId){
+		View view = mViews.get(viewId);
+		if(view == null){
+			view = mConvertView.findViewById(viewId);
+			
+			if(view == null){
+				String resName = context.getResources().getResourceEntryName(viewId);
+				throw new RuntimeException("Can not find "+ resName + " in parent view !");
+			}
+			
+			mViews.put(viewId, view);
+		}
+		return (T)view;
+	}
+
+	public <T extends View> T getView(int viewId, OnClickListener listener){
+		View view = mViews.get(viewId);
+		if(view == null){
+			view = mConvertView.findViewById(viewId);
+			
+			if(view == null){
+				String resName = context.getResources().getResourceEntryName(viewId);
+				throw new RuntimeException("Can not find "+ resName + " in parent view !");
+			}
+			
+			view.setOnClickListener(listener);
+			mViews.put(viewId, view);
+		}
+		return (T)view;
+	}
+	
+	public View getConvertView(){
+		return mConvertView;
+	}
+	
+	public int getPosititon(){
+		return mPosition;
+	}
+	
+	public CommonViewHolder setText(int viewId, String text){
+		TextView textview = getView(viewId);
+		textview.setText(text);
+		return this;
+	}
+	
+	public CommonViewHolder setRating(int viewId, float rate){
+		RatingBar ratingBar = getView(viewId);
+		ratingBar.setRating(rate);
+		return this;
+	}
+	
+	public CommonViewHolder setImageResource(int viewId, int resId){
+		ImageView imageview = getView(viewId);
+		imageview.setImageResource(resId);
+		return this;
+	}
+	
+	public CommonViewHolder setVisibility(int viewId, int visibility){
+		View view = getView(viewId);
+		
+		if(view == null){
+			return this;
+		}
+		
+		view.setVisibility(visibility);
+		
+		return this;
+	}
+	
+	public CommonViewHolder setOnClickListenr(int viewId, OnClickListener listener){
+		View view = mViews.get(viewId);
+		
+		if(view == null){
+			view = mConvertView.findViewById(viewId);
+			view.setOnClickListener(listener);
+			mViews.put(viewId, view);
+		}else if(!view.hasOnClickListeners()){
+			view.setOnClickListener(listener);
+			mViews.put(viewId, view);
+		}
+		
+		return this;
+	}
+}

+ 75 - 0
android-skin-loader-lib/src/main/java/cn/feng/skin/manager/util/L.java

@@ -0,0 +1,75 @@
+package cn.feng.skin.manager.util;
+
+import android.util.Log;
+
+/**
+ * Log Utils for debug
+ * 
+ * @author fengjun
+ */
+public class L {
+
+	private static final boolean DEBUG  = true;
+	private static final String  TAG    = "SkinLoader";
+	private static final String  LINE    = "________________________________________________________";
+	
+    private L() {
+        throw new AssertionError();
+    }
+    
+    public static void i(String msg){
+    	if(DEBUG){
+    		Log.i(TAG, LINE);
+    		Log.i(TAG, msg);
+    	}
+    }
+    
+    public static void d(String msg){
+    	if(DEBUG){
+    		Log.d(TAG, LINE);
+    		Log.d(TAG, msg);
+    	}
+    }
+    
+    public static void w(String msg){
+    	if(DEBUG){
+    		Log.w(TAG, LINE);
+    		Log.w(TAG, msg);
+    	}
+    }
+    
+    public static void e(String msg){
+    	if(DEBUG){
+    		Log.e(TAG, LINE);
+    		Log.e(TAG, msg);
+    	}
+    }
+    
+    public static void i(String tag, String msg){
+    	if(DEBUG){
+    		Log.i(tag, LINE);
+    		Log.i(tag, msg);
+    	}
+    }
+    
+    public static void d(String tag, String msg){
+    	if(DEBUG){
+    		Log.d(tag, LINE);
+    		Log.d(tag, msg);
+    	}
+    }
+    
+    public static void w(String tag, String msg){
+    	if(DEBUG){
+    		Log.w(tag, LINE);
+    		Log.w(tag, msg);
+    	}
+    }
+    
+    public static void e(String tag, String msg){
+    	if(DEBUG){
+    		Log.e(tag, LINE);
+    		Log.e(tag, msg);
+    	}
+    }
+}

+ 253 - 0
android-skin-loader-lib/src/main/java/cn/feng/skin/manager/util/ListUtils.java

@@ -0,0 +1,253 @@
+package cn.feng.skin.manager.util;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import android.text.TextUtils;
+
+/**
+ * List Utils
+ * 
+ * @author fengjun
+ */
+public class ListUtils {
+
+    /** default join separator **/
+    public static final String DEFAULT_JOIN_SEPARATOR = ",";
+
+    private ListUtils() {
+        throw new AssertionError();
+    }
+
+    /**
+     * get size of list
+     * 
+     * <pre>
+     * getSize(null)   =   0;
+     * getSize({})     =   0;
+     * getSize({1})    =   1;
+     * </pre>
+     * 
+     * @param <V>
+     * @param sourceList
+     * @return if list is null or empty, return 0, else return {@link List#size()}.
+     */
+    public static <V> int getSize(List<V> sourceList) {
+        return sourceList == null ? 0 : sourceList.size();
+    }
+
+    /**
+     * is null or its size is 0
+     * 
+     * <pre>
+     * isEmpty(null)   =   true;
+     * isEmpty({})     =   true;
+     * isEmpty({1})    =   false;
+     * </pre>
+     * 
+     * @param <V>
+     * @param sourceList
+     * @return if list is null or its size is 0, return true, else return false.
+     */
+    public static <V> boolean isEmpty(List<V> sourceList) {
+        return (sourceList == null || sourceList.size() == 0);
+    }
+
+    /**
+     * compare two list
+     * 
+     * <pre>
+     * isEquals(null, null) = true;
+     * isEquals(new ArrayList&lt;String&gt;(), null) = false;
+     * isEquals(null, new ArrayList&lt;String&gt;()) = false;
+     * isEquals(new ArrayList&lt;String&gt;(), new ArrayList&lt;String&gt;()) = true;
+     * </pre>
+     * 
+     * @param <V>
+     * @param actual
+     * @param expected
+     * @return
+     */
+    public static <V> boolean isEquals(ArrayList<V> actual, ArrayList<V> expected) {
+        if (actual == null) {
+            return expected == null;
+        }
+        if (expected == null) {
+            return false;
+        }
+        if (actual.size() != expected.size()) {
+            return false;
+        }
+
+        for (int i = 0; i < actual.size(); i++) {
+            if (!ObjectUtils.isEquals(actual.get(i), expected.get(i))) {
+                return false;
+            }
+        }
+        return true;
+    }
+
+    /**
+     * join list to string, separator is ","
+     * 
+     * <pre>
+     * join(null)      =   "";
+     * join({})        =   "";
+     * join({a,b})     =   "a,b";
+     * </pre>
+     * 
+     * @param list
+     * @return join list to string, separator is ",". if list is empty, return ""
+     */
+    public static String join(List<String> list) {
+        return join(list, DEFAULT_JOIN_SEPARATOR);
+    }
+
+    /**
+     * join list to string
+     * 
+     * <pre>
+     * join(null, '#')     =   "";
+     * join({}, '#')       =   "";
+     * join({a,b,c}, ' ')  =   "abc";
+     * join({a,b,c}, '#')  =   "a#b#c";
+     * </pre>
+     * 
+     * @param list
+     * @param separator
+     * @return join list to string. if list is empty, return ""
+     */
+    public static String join(List<String> list, char separator) {
+        return join(list, new String(new char[] {separator}));
+    }
+
+    /**
+     * join list to string. if separator is null, use {@link #DEFAULT_JOIN_SEPARATOR}
+     * 
+     * <pre>
+     * join(null, "#")     =   "";
+     * join({}, "#$")      =   "";
+     * join({a,b,c}, null) =   "a,b,c";
+     * join({a,b,c}, "")   =   "abc";
+     * join({a,b,c}, "#")  =   "a#b#c";
+     * join({a,b,c}, "#$") =   "a#$b#$c";
+     * </pre>
+     * 
+     * @param list
+     * @param separator
+     * @return join list to string with separator. if list is empty, return ""
+     */
+    public static String join(List<String> list, String separator) {
+        return list == null ? "" : TextUtils.join(separator, list);
+    }
+
+    /**
+     * add distinct entry to list
+     * 
+     * @param <V>
+     * @param sourceList
+     * @param entry
+     * @return if entry already exist in sourceList, return false, else add it and return true.
+     */
+    public static <V> boolean addDistinctEntry(List<V> sourceList, V entry) {
+        return (sourceList != null && !sourceList.contains(entry)) ? sourceList.add(entry) : false;
+    }
+
+    /**
+     * add all distinct entry to list1 from list2
+     * 
+     * @param <V>
+     * @param sourceList
+     * @param entryList
+     * @return the count of entries be added
+     */
+    public static <V> int addDistinctList(List<V> sourceList, List<V> entryList) {
+        if (sourceList == null || isEmpty(entryList)) {
+            return 0;
+        }
+
+        int sourceCount = sourceList.size();
+        for (V entry : entryList) {
+            if (!sourceList.contains(entry)) {
+                sourceList.add(entry);
+            }
+        }
+        return sourceList.size() - sourceCount;
+    }
+
+    /**
+     * remove duplicate entries in list
+     * 
+     * @param <V>
+     * @param sourceList
+     * @return the count of entries be removed
+     */
+    public static <V> int distinctList(List<V> sourceList) {
+        if (isEmpty(sourceList)) {
+            return 0;
+        }
+
+        int sourceCount = sourceList.size();
+        int sourceListSize = sourceList.size();
+        for (int i = 0; i < sourceListSize; i++) {
+            for (int j = (i + 1); j < sourceListSize; j++) {
+                if (sourceList.get(i).equals(sourceList.get(j))) {
+                    sourceList.remove(j);
+                    sourceListSize = sourceList.size();
+                    j--;
+                }
+            }
+        }
+        return sourceCount - sourceList.size();
+    }
+
+    /**
+     * add not null entry to list
+     * 
+     * @param sourceList
+     * @param value
+     * @return <ul>
+     *         <li>if sourceList is null, return false</li>
+     *         <li>if value is null, return false</li>
+     *         <li>return {@link List#add(Object)}</li>
+     *         </ul>
+     */
+    public static <V> boolean addListNotNullValue(List<V> sourceList, V value) {
+        return (sourceList != null && value != null) ? sourceList.add(value) : false;
+    }
+
+    /**
+     * @see {@link ArrayUtils#getLast(Object[], Object, Object, boolean)} defaultValue is null, isCircle is true
+     */
+    @SuppressWarnings("unchecked")
+    public static <V> V getLast(List<V> sourceList, V value) {
+        return (sourceList == null) ? null : (V)ArrayUtils.getLast(sourceList.toArray(), value, true);
+    }
+
+    /**
+     * @see {@link ArrayUtils#getNext(Object[], Object, Object, boolean)} defaultValue is null, isCircle is true
+     */
+    @SuppressWarnings("unchecked")
+    public static <V> V getNext(List<V> sourceList, V value) {
+        return (sourceList == null) ? null : (V)ArrayUtils.getNext(sourceList.toArray(), value, true);
+    }
+
+    /**
+     * invert list
+     * 
+     * @param <V>
+     * @param sourceList
+     * @return
+     */
+    public static <V> List<V> invertList(List<V> sourceList) {
+        if (isEmpty(sourceList)) {
+            return sourceList;
+        }
+
+        List<V> invertList = new ArrayList<V>(sourceList.size());
+        for (int i = sourceList.size() - 1; i >= 0; i--) {
+            invertList.add(sourceList.get(i));
+        }
+        return invertList;
+    }
+}

+ 289 - 0
android-skin-loader-lib/src/main/java/cn/feng/skin/manager/util/MapUtils.java

@@ -0,0 +1,289 @@
+package cn.feng.skin.manager.util;
+
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.Map;
+import java.util.Map.Entry;
+
+/**
+ * Map Utils
+ * 
+ * @author <a href="http://www.trinea.cn" target="_blank">Trinea</a> 2011-7-22
+ */
+public class MapUtils {
+
+    /** default separator between key and value **/
+    public static final String DEFAULT_KEY_AND_VALUE_SEPARATOR      = ":";
+    /** default separator between key-value pairs **/
+    public static final String DEFAULT_KEY_AND_VALUE_PAIR_SEPARATOR = ",";
+    
+    private MapUtils() {
+        throw new AssertionError();
+    }
+    /**
+     * is null or its size is 0
+     * 
+     * <pre>
+     * isEmpty(null)   =   true;
+     * isEmpty({})     =   true;
+     * isEmpty({1, 2})    =   false;
+     * </pre>
+     * 
+     * @param sourceMap
+     * @return if map is null or its size is 0, return true, else return false.
+     */
+    public static <K, V> boolean isEmpty(Map<K, V> sourceMap) {
+        return (sourceMap == null || sourceMap.size() == 0);
+    }
+
+    /**
+     * add key-value pair to map, and key need not null or empty
+     * 
+     * @param map
+     * @param key
+     * @param value
+     * @return <ul>
+     *         <li>if map is null, return false</li>
+     *         <li>if key is null or empty, return false</li>
+     *         <li>return {@link Map#put(Object, Object)}</li>
+     *         </ul>
+     */
+    public static boolean putMapNotEmptyKey(Map<String, String> map, String key, String value) {
+        if (map == null || StringUtils.isEmpty(key)) {
+            return false;
+        }
+
+        map.put(key, value);
+        return true;
+    }
+
+    /**
+     * add key-value pair to map, both key and value need not null or empty
+     * 
+     * @param map
+     * @param key
+     * @param value
+     * @return <ul>
+     *         <li>if map is null, return false</li>
+     *         <li>if key is null or empty, return false</li>
+     *         <li>if value is null or empty, return false</li>
+     *         <li>return {@link Map#put(Object, Object)}</li>
+     *         </ul>
+     */
+    public static boolean putMapNotEmptyKeyAndValue(Map<String, String> map, String key, String value) {
+        if (map == null || StringUtils.isEmpty(key) || StringUtils.isEmpty(value)) {
+            return false;
+        }
+
+        map.put(key, value);
+        return true;
+    }
+
+    /**
+     * add key-value pair to map, key need not null or empty
+     * 
+     * @param map
+     * @param key
+     * @param value
+     * @param defaultValue
+     * @return <ul>
+     *         <li>if map is null, return false</li>
+     *         <li>if key is null or empty, return false</li>
+     *         <li>if value is null or empty, put defaultValue, return true</li>
+     *         <li>if value is neither null nor empty,put value, return true</li>
+     *         </ul>
+     */
+    public static boolean putMapNotEmptyKeyAndValue(Map<String, String> map, String key, String value,
+            String defaultValue) {
+        if (map == null || StringUtils.isEmpty(key)) {
+            return false;
+        }
+
+        map.put(key, StringUtils.isEmpty(value) ? defaultValue : value);
+        return true;
+    }
+
+    /**
+     * add key-value pair to map, key need not null
+     * 
+     * @param map
+     * @param key
+     * @param value
+     * @return <ul>
+     *         <li>if map is null, return false</li>
+     *         <li>if key is null, return false</li>
+     *         <li>return {@link Map#put(Object, Object)}</li>
+     *         </ul>
+     */
+    public static <K, V> boolean putMapNotNullKey(Map<K, V> map, K key, V value) {
+        if (map == null || key == null) {
+            return false;
+        }
+
+        map.put(key, value);
+        return true;
+    }
+
+    /**
+     * add key-value pair to map, both key and value need not null
+     * 
+     * @param map
+     * @param key
+     * @param value
+     * @return <ul>
+     *         <li>if map is null, return false</li>
+     *         <li>if key is null, return false</li>
+     *         <li>if value is null, return false</li>
+     *         <li>return {@link Map#put(Object, Object)}</li>
+     *         </ul>
+     */
+    public static <K, V> boolean putMapNotNullKeyAndValue(Map<K, V> map, K key, V value) {
+        if (map == null || key == null || value == null) {
+            return false;
+        }
+
+        map.put(key, value);
+        return true;
+    }
+
+    /**
+     * get key by value, match the first entry front to back
+     * <ul>
+     * <strong>Attentions:</strong>
+     * <li>for HashMap, the order of entry not same to put order, so you may need to use TreeMap</li>
+     * </ul>
+     * 
+     * @param <V>
+     * @param map
+     * @param value
+     * @return <ul>
+     *         <li>if map is null, return null</li>
+     *         <li>if value exist, return key</li>
+     *         <li>return null</li>
+     *         </ul>
+     */
+    public static <K, V> K getKeyByValue(Map<K, V> map, V value) {
+        if (isEmpty(map)) {
+            return null;
+        }
+
+        for (Entry<K, V> entry : map.entrySet()) {
+            if (ObjectUtils.isEquals(entry.getValue(), value)) {
+                return entry.getKey();
+            }
+        }
+        return null;
+    }
+
+    /**
+     * parse key-value pairs to map, ignore empty key
+     * 
+     * <pre>
+     * parseKeyAndValueToMap("","","",true)=null
+     * parseKeyAndValueToMap(null,"","",true)=null
+     * parseKeyAndValueToMap("a:b,:","","",true)={(a,b)}
+     * parseKeyAndValueToMap("a:b,:d","","",true)={(a,b)}
+     * parseKeyAndValueToMap("a:b,c:d","","",true)={(a,b),(c,d)}
+     * parseKeyAndValueToMap("a=b, c = d","=",",",true)={(a,b),(c,d)}
+     * parseKeyAndValueToMap("a=b, c = d","=",",",false)={(a, b),( c , d)}
+     * parseKeyAndValueToMap("a=b, c=d","=", ",", false)={(a,b),( c,d)}
+     * parseKeyAndValueToMap("a=b; c=d","=", ";", false)={(a,b),( c,d)}
+     * parseKeyAndValueToMap("a=b, c=d", ",", ";", false)={(a=b, c=d)}
+     * </pre>
+     * 
+     * @param source key-value pairs
+     * @param keyAndValueSeparator separator between key and value
+     * @param keyAndValuePairSeparator separator between key-value pairs
+     * @param ignoreSpace whether ignore space at the begging or end of key and value
+     * @return
+     */
+    public static Map<String, String> parseKeyAndValueToMap(String source, String keyAndValueSeparator,
+            String keyAndValuePairSeparator, boolean ignoreSpace) {
+        if (StringUtils.isEmpty(source)) {
+            return null;
+        }
+
+        if (StringUtils.isEmpty(keyAndValueSeparator)) {
+            keyAndValueSeparator = DEFAULT_KEY_AND_VALUE_SEPARATOR;
+        }
+        if (StringUtils.isEmpty(keyAndValuePairSeparator)) {
+            keyAndValuePairSeparator = DEFAULT_KEY_AND_VALUE_PAIR_SEPARATOR;
+        }
+        Map<String, String> keyAndValueMap = new HashMap<String, String>();
+        String[] keyAndValueArray = source.split(keyAndValuePairSeparator);
+        if (keyAndValueArray == null) {
+            return null;
+        }
+
+        int seperator;
+        for (String valueEntity : keyAndValueArray) {
+            if (!StringUtils.isEmpty(valueEntity)) {
+                seperator = valueEntity.indexOf(keyAndValueSeparator);
+                if (seperator != -1) {
+                    if (ignoreSpace) {
+                        MapUtils.putMapNotEmptyKey(keyAndValueMap, valueEntity.substring(0, seperator).trim(),
+                                valueEntity.substring(seperator + 1).trim());
+                    } else {
+                        MapUtils.putMapNotEmptyKey(keyAndValueMap, valueEntity.substring(0, seperator),
+                                valueEntity.substring(seperator + 1));
+                    }
+                }
+            }
+        }
+        return keyAndValueMap;
+    }
+
+    /**
+     * parse key-value pairs to map, ignore empty key
+     * 
+     * @param source key-value pairs
+     * @param ignoreSpace whether ignore space at the begging or end of key and value
+     * @return
+     * @see {@link MapUtils#parseKeyAndValueToMap(String, String, String, boolean)}, keyAndValueSeparator is
+     *      {@link #DEFAULT_KEY_AND_VALUE_SEPARATOR}, keyAndValuePairSeparator is
+     *      {@link #DEFAULT_KEY_AND_VALUE_PAIR_SEPARATOR}
+     */
+    public static Map<String, String> parseKeyAndValueToMap(String source, boolean ignoreSpace) {
+        return parseKeyAndValueToMap(source, DEFAULT_KEY_AND_VALUE_SEPARATOR, DEFAULT_KEY_AND_VALUE_PAIR_SEPARATOR,
+                ignoreSpace);
+    }
+
+    /**
+     * parse key-value pairs to map, ignore empty key, ignore space at the begging or end of key and value
+     * 
+     * @param source key-value pairs
+     * @return
+     * @see {@link MapUtils#parseKeyAndValueToMap(String, String, String, boolean)}, keyAndValueSeparator is
+     *      {@link #DEFAULT_KEY_AND_VALUE_SEPARATOR}, keyAndValuePairSeparator is
+     *      {@link #DEFAULT_KEY_AND_VALUE_PAIR_SEPARATOR}, ignoreSpace is true
+     */
+    public static Map<String, String> parseKeyAndValueToMap(String source) {
+        return parseKeyAndValueToMap(source, DEFAULT_KEY_AND_VALUE_SEPARATOR, DEFAULT_KEY_AND_VALUE_PAIR_SEPARATOR,
+                true);
+    }
+
+    /**
+     * join map
+     * 
+     * @param map
+     * @return
+     */
+    public static String toJson(Map<String, String> map) {
+        if (map == null || map.size() == 0) {
+            return null;
+        }
+
+        StringBuilder paras = new StringBuilder();
+        paras.append("{");
+        Iterator<Map.Entry<String, String>> ite = map.entrySet().iterator();
+        while (ite.hasNext()) {
+            Map.Entry<String, String> entry = (Map.Entry<String, String>)ite.next();
+            paras.append("\"").append(entry.getKey()).append("\":\"").append(entry.getValue()).append("\"");
+            if (ite.hasNext()) {
+                paras.append(",");
+            }
+        }
+        paras.append("}");
+        return paras.toString();
+    }
+}

+ 124 - 0
android-skin-loader-lib/src/main/java/cn/feng/skin/manager/util/ObjectUtils.java

@@ -0,0 +1,124 @@
+package cn.feng.skin.manager.util;
+
+/**
+ * Object Utils
+ * 
+ * @author fengjun
+ */
+public class ObjectUtils {
+
+    private ObjectUtils() {
+        throw new AssertionError();
+    }
+
+    /**
+     * compare two object
+     * 
+     * @param actual
+     * @param expected
+     * @return <ul>
+     *         <li>if both are null, return true</li>
+     *         <li>return actual.{@link Object#equals(Object)}</li>
+     *         </ul>
+     */
+    public static boolean isEquals(Object actual, Object expected) {
+        return actual == expected || (actual == null ? expected == null : actual.equals(expected));
+    }
+
+    /**
+     * null Object to empty string
+     * 
+     * <pre>
+     * nullStrToEmpty(null) = &quot;&quot;;
+     * nullStrToEmpty(&quot;&quot;) = &quot;&quot;;
+     * nullStrToEmpty(&quot;aa&quot;) = &quot;aa&quot;;
+     * </pre>
+     * 
+     * @param str
+     * @return
+     */
+    public static String nullStrToEmpty(Object str) {
+        return (str == null ? "" : (str instanceof String ? (String)str : str.toString()));
+    }
+
+    /**
+     * convert long array to Long array
+     * 
+     * @param source
+     * @return
+     */
+    public static Long[] transformLongArray(long[] source) {
+        Long[] destin = new Long[source.length];
+        for (int i = 0; i < source.length; i++) {
+            destin[i] = source[i];
+        }
+        return destin;
+    }
+
+    /**
+     * convert Long array to long array
+     * 
+     * @param source
+     * @return
+     */
+    public static long[] transformLongArray(Long[] source) {
+        long[] destin = new long[source.length];
+        for (int i = 0; i < source.length; i++) {
+            destin[i] = source[i];
+        }
+        return destin;
+    }
+
+    /**
+     * convert int array to Integer array
+     * 
+     * @param source
+     * @return
+     */
+    public static Integer[] transformIntArray(int[] source) {
+        Integer[] destin = new Integer[source.length];
+        for (int i = 0; i < source.length; i++) {
+            destin[i] = source[i];
+        }
+        return destin;
+    }
+
+    /**
+     * convert Integer array to int array
+     * 
+     * @param source
+     * @return
+     */
+    public static int[] transformIntArray(Integer[] source) {
+        int[] destin = new int[source.length];
+        for (int i = 0; i < source.length; i++) {
+            destin[i] = source[i];
+        }
+        return destin;
+    }
+
+    /**
+     * compare two object
+     * <ul>
+     * <strong>About result</strong>
+     * <li>if v1 > v2, return 1</li>
+     * <li>if v1 = v2, return 0</li>
+     * <li>if v1 < v2, return -1</li>
+     * </ul>
+     * <ul>
+     * <strong>About rule</strong>
+     * <li>if v1 is null, v2 is null, then return 0</li>
+     * <li>if v1 is null, v2 is not null, then return -1</li>
+     * <li>if v1 is not null, v2 is null, then return 1</li>
+     * <li>return v1.{@link Comparable#compareTo(Object)}</li>
+     * </ul>
+     * 
+     * @param v1
+     * @param v2
+     * @return
+     */
+    @SuppressWarnings({"unchecked", "rawtypes"})
+    public static <V> int compare(V v1, V v2) {
+        return v1 == null ? (v2 == null ? 0 : -1) : (v2 == null ? 1 : ((Comparable)v1).compareTo(v2));
+    }
+}

+ 248 - 0
android-skin-loader-lib/src/main/java/cn/feng/skin/manager/util/PreferencesUtils.java

@@ -0,0 +1,248 @@
+package cn.feng.skin.manager.util;
+
+import android.content.Context;
+import android.content.SharedPreferences;
+
+/**
+ * PreferencesUtils, easy to get or put data
+ * <ul>
+ * <strong>Preference Name</strong>
+ * <li>you can change preference name by {@link #PREFERENCE_NAME}</li>
+ * </ul>
+ * <ul>
+ * <strong>Put Value</strong>
+ * <li>put string {@link #putString(Context, String, String)}</li>
+ * <li>put int {@link #putInt(Context, String, int)}</li>
+ * <li>put long {@link #putLong(Context, String, long)}</li>
+ * <li>put float {@link #putFloat(Context, String, float)}</li>
+ * <li>put boolean {@link #putBoolean(Context, String, boolean)}</li>
+ * </ul>
+ * <ul>
+ * <strong>Get Value</strong>
+ * <li>get string {@link #getString(Context, String)}, {@link #getString(Context, String, String)}</li>
+ * <li>get int {@link #getInt(Context, String)}, {@link #getInt(Context, String, int)}</li>
+ * <li>get long {@link #getLong(Context, String)}, {@link #getLong(Context, String, long)}</li>
+ * <li>get float {@link #getFloat(Context, String)}, {@link #getFloat(Context, String, float)}</li>
+ * <li>get boolean {@link #getBoolean(Context, String)}, {@link #getBoolean(Context, String, boolean)}</li>
+ * </ul>
+ * 
+ * @author fengjun
+ */
+public class PreferencesUtils {
+
+    public static String PREFERENCE_NAME = "cn_feng_skin_pref";
+
+    private PreferencesUtils() {
+        throw new AssertionError();
+    }
+
+    /**
+     * put string preferences
+     * 
+     * @param context
+     * @param key The name of the preference to modify
+     * @param value The new value for the preference
+     * @return True if the new values were successfully written to persistent storage.
+     */
+    public static boolean putString(Context context, String key, String value) {
+        SharedPreferences settings = context.getSharedPreferences(PREFERENCE_NAME, Context.MODE_PRIVATE);
+        SharedPreferences.Editor editor = settings.edit();
+        editor.putString(key, value);
+        return editor.commit();
+    }
+
+    /**
+     * get string preferences
+     * 
+     * @param context
+     * @param key The name of the preference to retrieve
+     * @return The preference value if it exists, or null. Throws ClassCastException if there is a preference with this
+     *         name that is not a string
+     * @see #getString(Context, String, String)
+     */
+    public static String getString(Context context, String key) {
+        return getString(context, key, null);
+    }
+
+    /**
+     * get string preferences
+     * 
+     * @param context
+     * @param key The name of the preference to retrieve
+     * @param defaultValue Value to return if this preference does not exist
+     * @return The preference value if it exists, or defValue. Throws ClassCastException if there is a preference with
+     *         this name that is not a string
+     */
+    public static String getString(Context context, String key, String defaultValue) {
+        SharedPreferences settings = context.getSharedPreferences(PREFERENCE_NAME, Context.MODE_PRIVATE);
+        return settings.getString(key, defaultValue);
+    }
+
+    /**
+     * put int preferences
+     * 
+     * @param context
+     * @param key The name of the preference to modify
+     * @param value The new value for the preference
+     * @return True if the new values were successfully written to persistent storage.
+     */
+    public static boolean putInt(Context context, String key, int value) {
+        SharedPreferences settings = context.getSharedPreferences(PREFERENCE_NAME, Context.MODE_PRIVATE);
+        SharedPreferences.Editor editor = settings.edit();
+        editor.putInt(key, value);
+        return editor.commit();
+    }
+
+    /**
+     * get int preferences
+     * 
+     * @param context
+     * @param key The name of the preference to retrieve
+     * @return The preference value if it exists, or -1. Throws ClassCastException if there is a preference with this
+     *         name that is not a int
+     * @see #getInt(Context, String, int)
+     */
+    public static int getInt(Context context, String key) {
+        return getInt(context, key, -1);
+    }
+
+    /**
+     * get int preferences
+     * 
+     * @param context
+     * @param key The name of the preference to retrieve
+     * @param defaultValue Value to return if this preference does not exist
+     * @return The preference value if it exists, or defValue. Throws ClassCastException if there is a preference with
+     *         this name that is not a int
+     */
+    public static int getInt(Context context, String key, int defaultValue) {
+        SharedPreferences settings = context.getSharedPreferences(PREFERENCE_NAME, Context.MODE_PRIVATE);
+        return settings.getInt(key, defaultValue);
+    }
+
+    /**
+     * put long preferences
+     * 
+     * @param context
+     * @param key The name of the preference to modify
+     * @param value The new value for the preference
+     * @return True if the new values were successfully written to persistent storage.
+     */
+    public static boolean putLong(Context context, String key, long value) {
+        SharedPreferences settings = context.getSharedPreferences(PREFERENCE_NAME, Context.MODE_PRIVATE);
+        SharedPreferences.Editor editor = settings.edit();
+        editor.putLong(key, value);
+        return editor.commit();
+    }
+
+    /**
+     * get long preferences
+     * 
+     * @param context
+     * @param key The name of the preference to retrieve
+     * @return The preference value if it exists, or -1. Throws ClassCastException if there is a preference with this
+     *         name that is not a long
+     * @see #getLong(Context, String, long)
+     */
+    public static long getLong(Context context, String key) {
+        return getLong(context, key, -1);
+    }
+
+    /**
+     * get long preferences
+     * 
+     * @param context
+     * @param key The name of the preference to retrieve
+     * @param defaultValue Value to return if this preference does not exist
+     * @return The preference value if it exists, or defValue. Throws ClassCastException if there is a preference with
+     *         this name that is not a long
+     */
+    public static long getLong(Context context, String key, long defaultValue) {
+        SharedPreferences settings = context.getSharedPreferences(PREFERENCE_NAME, Context.MODE_PRIVATE);
+        return settings.getLong(key, defaultValue);
+    }
+
+    /**
+     * put float preferences
+     * 
+     * @param context
+     * @param key The name of the preference to modify
+     * @param value The new value for the preference
+     * @return True if the new values were successfully written to persistent storage.
+     */
+    public static boolean putFloat(Context context, String key, float value) {
+        SharedPreferences settings = context.getSharedPreferences(PREFERENCE_NAME, Context.MODE_PRIVATE);
+        SharedPreferences.Editor editor = settings.edit();
+        editor.putFloat(key, value);
+        return editor.commit();
+    }
+
+    /**
+     * get float preferences
+     * 
+     * @param context
+     * @param key The name of the preference to retrieve
+     * @return The preference value if it exists, or -1. Throws ClassCastException if there is a preference with this
+     *         name that is not a float
+     * @see #getFloat(Context, String, float)
+     */
+    public static float getFloat(Context context, String key) {
+        return getFloat(context, key, -1);
+    }
+
+    /**
+     * get float preferences
+     * 
+     * @param context
+     * @param key The name of the preference to retrieve
+     * @param defaultValue Value to return if this preference does not exist
+     * @return The preference value if it exists, or defValue. Throws ClassCastException if there is a preference with
+     *         this name that is not a float
+     */
+    public static float getFloat(Context context, String key, float defaultValue) {
+        SharedPreferences settings = context.getSharedPreferences(PREFERENCE_NAME, Context.MODE_PRIVATE);
+        return settings.getFloat(key, defaultValue);
+    }
+
+    /**
+     * put boolean preferences
+     * 
+     * @param context
+     * @param key The name of the preference to modify
+     * @param value The new value for the preference
+     * @return True if the new values were successfully written to persistent storage.
+     */
+    public static boolean putBoolean(Context context, String key, boolean value) {
+        SharedPreferences settings = context.getSharedPreferences(PREFERENCE_NAME, Context.MODE_PRIVATE);
+        SharedPreferences.Editor editor = settings.edit();
+        editor.putBoolean(key, value);
+        return editor.commit();
+    }
+
+    /**
+     * get boolean preferences, default is false
+     * 
+     * @param context
+     * @param key The name of the preference to retrieve
+     * @return The preference value if it exists, or false. Throws ClassCastException if there is a preference with this
+     *         name that is not a boolean
+     * @see #getBoolean(Context, String, boolean)
+     */
+    public static boolean getBoolean(Context context, String key) {
+        return getBoolean(context, key, false);
+    }
+
+    /**
+     * get boolean preferences
+     * 
+     * @param context
+     * @param key The name of the preference to retrieve
+     * @param defaultValue Value to return if this preference does not exist
+     * @return The preference value if it exists, or defValue. Throws ClassCastException if there is a preference with
+     *         this name that is not a boolean
+     */
+    public static boolean getBoolean(Context context, String key, boolean defaultValue) {
+        SharedPreferences settings = context.getSharedPreferences(PREFERENCE_NAME, Context.MODE_PRIVATE);
+        return settings.getBoolean(key, defaultValue);
+    }
+}

+ 136 - 0
android-skin-loader-lib/src/main/java/cn/feng/skin/manager/util/ResourceUtils.java

@@ -0,0 +1,136 @@
+package cn.feng.skin.manager.util;
+
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.util.ArrayList;
+import java.util.List;
+
+import android.content.Context;
+
+/**
+ * ResourceUtils
+ * 
+ * @author <a href="http://www.trinea.cn" target="_blank">Trinea</a> 2012-5-26
+ */
+public class ResourceUtils {
+
+    private ResourceUtils() {
+        throw new AssertionError();
+    }
+
+    /**
+     * get an asset using ACCESS_STREAMING mode. This provides access to files that have been bundled with an
+     * application as assets -- that is, files placed in to the "assets" directory.
+     * 
+     * @param context
+     * @param fileName The name of the asset to open. This name can be hierarchical.
+     * @return
+     */
+    public static String geFileFromAssets(Context context, String fileName) {
+        if (context == null || StringUtils.isEmpty(fileName)) {
+            return null;
+        }
+
+        StringBuilder s = new StringBuilder("");
+        try {
+            InputStreamReader in = new InputStreamReader(context.getResources().getAssets().open(fileName));
+            BufferedReader br = new BufferedReader(in);
+            String line;
+            while ((line = br.readLine()) != null) {
+                s.append(line);
+                s.append("\n");
+            }
+            return s.toString();
+        } catch (IOException e) {
+            e.printStackTrace();
+            return null;
+        }
+    }
+
+    /**
+     * get content from a raw resource. This can only be used with resources whose value is the name of an asset files
+     * -- that is, it can be used to open drawable, sound, and raw resources; it will fail on string and color
+     * resources.
+     * 
+     * @param context
+     * @param resId The resource identifier to open, as generated by the appt tool.
+     * @return
+     */
+    public static String geFileFromRaw(Context context, int resId) {
+        if (context == null) {
+            return null;
+        }
+
+        StringBuilder s = new StringBuilder();
+        try {
+            InputStreamReader in = new InputStreamReader(context.getResources().openRawResource(resId));
+            BufferedReader br = new BufferedReader(in);
+            String line;
+            while ((line = br.readLine()) != null) {
+                s.append(line);
+            }
+            return s.toString();
+        } catch (IOException e) {
+            e.printStackTrace();
+            return null;
+        }
+    }
+
+    /**
+     * same to {@link ResourceUtils#geFileFromAssets(Context, String)}, but return type is List<String>
+     * 
+     * @param context
+     * @param fileName
+     * @return
+     */
+    public static List<String> geFileToListFromAssets(Context context, String fileName) {
+        if (context == null || StringUtils.isEmpty(fileName)) {
+            return null;
+        }
+
+        List<String> fileContent = new ArrayList<String>();
+        try {
+            InputStreamReader in = new InputStreamReader(context.getResources().getAssets().open(fileName));
+            BufferedReader br = new BufferedReader(in);
+            String line;
+            while ((line = br.readLine()) != null) {
+                fileContent.add(line);
+            }
+            br.close();
+            return fileContent;
+        } catch (IOException e) {
+            e.printStackTrace();
+            return null;
+        }
+    }
+
+    /**
+     * same to {@link ResourceUtils#geFileFromRaw(Context, int)}, but return type is List<String>
+     * 
+     * @param context
+     * @param resId
+     * @return
+     */
+    public static List<String> geFileToListFromRaw(Context context, int resId) {
+        if (context == null) {
+            return null;
+        }
+
+        List<String> fileContent = new ArrayList<String>();
+        BufferedReader reader = null;
+        try {
+            InputStreamReader in = new InputStreamReader(context.getResources().openRawResource(resId));
+            reader = new BufferedReader(in);
+            String line = null;
+            while ((line = reader.readLine()) != null) {
+                fileContent.add(line);
+            }
+            reader.close();
+            return fileContent;
+        } catch (IOException e) {
+            e.printStackTrace();
+            return null;
+        }
+    }
+}

+ 293 - 0
android-skin-loader-lib/src/main/java/cn/feng/skin/manager/util/StringUtils.java

@@ -0,0 +1,293 @@
+package cn.feng.skin.manager.util;
+
+import java.io.UnsupportedEncodingException;
+import java.net.URLEncoder;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * String Utils
+ * 
+ * @author <a href="http://www.trinea.cn" target="_blank">Trinea</a> 2011-7-22
+ */
+public class StringUtils {
+
+    private StringUtils() {
+        throw new AssertionError();
+    }
+
+    /**
+     * is null or its length is 0 or it is made by space
+     * 
+     * <pre>
+     * isBlank(null) = true;
+     * isBlank(&quot;&quot;) = true;
+     * isBlank(&quot;  &quot;) = true;
+     * isBlank(&quot;a&quot;) = false;
+     * isBlank(&quot;a &quot;) = false;
+     * isBlank(&quot; a&quot;) = false;
+     * isBlank(&quot;a b&quot;) = false;
+     * </pre>
+     * 
+     * @param str
+     * @return if string is null or its size is 0 or it is made by space, return true, else return false.
+     */
+    public static boolean isBlank(String str) {
+        return (str == null || str.trim().length() == 0);
+    }
+
+    /**
+     * is null or its length is 0
+     * 
+     * <pre>
+     * isEmpty(null) = true;
+     * isEmpty(&quot;&quot;) = true;
+     * isEmpty(&quot;  &quot;) = false;
+     * </pre>
+     * 
+     * @param str
+     * @return if string is null or its size is 0, return true, else return false.
+     */
+    public static boolean isEmpty(CharSequence str) {
+        return (str == null || str.length() == 0);
+    }
+
+    /**
+     * compare two string
+     * 
+     * @param actual
+     * @param expected
+     * @return
+     * @see ObjectUtils#isEquals(Object, Object)
+     */
+    public static boolean isEquals(String actual, String expected) {
+        return ObjectUtils.isEquals(actual, expected);
+    }
+
+    /**
+     * get length of CharSequence
+     * 
+     * <pre>
+     * length(null) = 0;
+     * length(\"\") = 0;
+     * length(\"abc\") = 3;
+     * </pre>
+     * 
+     * @param str
+     * @return if str is null or empty, return 0, else return {@link CharSequence#length()}.
+     */
+    public static int length(CharSequence str) {
+        return str == null ? 0 : str.length();
+    }
+
+    /**
+     * null Object to empty string
+     * 
+     * <pre>
+     * nullStrToEmpty(null) = &quot;&quot;;
+     * nullStrToEmpty(&quot;&quot;) = &quot;&quot;;
+     * nullStrToEmpty(&quot;aa&quot;) = &quot;aa&quot;;
+     * </pre>
+     * 
+     * @param str
+     * @return
+     */
+    public static String nullStrToEmpty(Object str) {
+        return (str == null ? "" : (str instanceof String ? (String)str : str.toString()));
+    }
+
+    /**
+     * capitalize first letter
+     * 
+     * <pre>
+     * capitalizeFirstLetter(null)     =   null;
+     * capitalizeFirstLetter("")       =   "";
+     * capitalizeFirstLetter("2ab")    =   "2ab"
+     * capitalizeFirstLetter("a")      =   "A"
+     * capitalizeFirstLetter("ab")     =   "Ab"
+     * capitalizeFirstLetter("Abc")    =   "Abc"
+     * </pre>
+     * 
+     * @param str
+     * @return
+     */
+    public static String capitalizeFirstLetter(String str) {
+        if (isEmpty(str)) {
+            return str;
+        }
+
+        char c = str.charAt(0);
+        return (!Character.isLetter(c) || Character.isUpperCase(c)) ? str : new StringBuilder(str.length())
+                .append(Character.toUpperCase(c)).append(str.substring(1)).toString();
+    }
+
+    /**
+     * encoded in utf-8
+     * 
+     * <pre>
+     * utf8Encode(null)        =   null
+     * utf8Encode("")          =   "";
+     * utf8Encode("aa")        =   "aa";
+     * utf8Encode("啊啊啊啊")   = "%E5%95%8A%E5%95%8A%E5%95%8A%E5%95%8A";
+     * </pre>
+     * 
+     * @param str
+     * @return
+     * @throws UnsupportedEncodingException if an error occurs
+     */
+    public static String utf8Encode(String str) {
+        if (!isEmpty(str) && str.getBytes().length != str.length()) {
+            try {
+                return URLEncoder.encode(str, "UTF-8");
+            } catch (UnsupportedEncodingException e) {
+                throw new RuntimeException("UnsupportedEncodingException occurred. ", e);
+            }
+        }
+        return str;
+    }
+
+    /**
+     * encoded in utf-8, if exception, return defultReturn
+     * 
+     * @param str
+     * @param defultReturn
+     * @return
+     */
+    public static String utf8Encode(String str, String defultReturn) {
+        if (!isEmpty(str) && str.getBytes().length != str.length()) {
+            try {
+                return URLEncoder.encode(str, "UTF-8");
+            } catch (UnsupportedEncodingException e) {
+                return defultReturn;
+            }
+        }
+        return str;
+    }
+
+    /**
+     * get innerHtml from href
+     * 
+     * <pre>
+     * getHrefInnerHtml(null)                                  = ""
+     * getHrefInnerHtml("")                                    = ""
+     * getHrefInnerHtml("mp3")                                 = "mp3";
+     * getHrefInnerHtml("&lt;a innerHtml&lt;/a&gt;")                    = "&lt;a innerHtml&lt;/a&gt;";
+     * getHrefInnerHtml("&lt;a&gt;innerHtml&lt;/a&gt;")                    = "innerHtml";
+     * getHrefInnerHtml("&lt;a&lt;a&gt;innerHtml&lt;/a&gt;")                    = "innerHtml";
+     * getHrefInnerHtml("&lt;a href="baidu.com"&gt;innerHtml&lt;/a&gt;")               = "innerHtml";
+     * getHrefInnerHtml("&lt;a href="baidu.com" title="baidu"&gt;innerHtml&lt;/a&gt;") = "innerHtml";
+     * getHrefInnerHtml("   &lt;a&gt;innerHtml&lt;/a&gt;  ")                           = "innerHtml";
+     * getHrefInnerHtml("&lt;a&gt;innerHtml&lt;/a&gt;&lt;/a&gt;")                      = "innerHtml";
+     * getHrefInnerHtml("jack&lt;a&gt;innerHtml&lt;/a&gt;&lt;/a&gt;")                  = "innerHtml";
+     * getHrefInnerHtml("&lt;a&gt;innerHtml1&lt;/a&gt;&lt;a&gt;innerHtml2&lt;/a&gt;")        = "innerHtml2";
+     * </pre>
+     * 
+     * @param href
+     * @return <ul>
+     *         <li>if href is null, return ""</li>
+     *         <li>if not match regx, return source</li>
+     *         <li>return the last string that match regx</li>
+     *         </ul>
+     */
+    public static String getHrefInnerHtml(String href) {
+        if (isEmpty(href)) {
+            return "";
+        }
+
+        String hrefReg = ".*<[\\s]*a[\\s]*.*>(.+?)<[\\s]*/a[\\s]*>.*";
+        Pattern hrefPattern = Pattern.compile(hrefReg, Pattern.CASE_INSENSITIVE);
+        Matcher hrefMatcher = hrefPattern.matcher(href);
+        if (hrefMatcher.matches()) {
+            return hrefMatcher.group(1);
+        }
+        return href;
+    }
+
+/**
+     * process special char in html
+     * 
+     * <pre>
+     * htmlEscapeCharsToString(null) = null;
+     * htmlEscapeCharsToString("") = "";
+     * htmlEscapeCharsToString("mp3") = "mp3";
+     * htmlEscapeCharsToString("mp3&lt;") = "mp3<";
+     * htmlEscapeCharsToString("mp3&gt;") = "mp3\>";
+     * htmlEscapeCharsToString("mp3&amp;mp4") = "mp3&mp4";
+     * htmlEscapeCharsToString("mp3&quot;mp4") = "mp3\"mp4";
+     * htmlEscapeCharsToString("mp3&lt;&gt;&amp;&quot;mp4") = "mp3\<\>&\"mp4";
+     * </pre>
+     * 
+     * @param source
+     * @return
+     */
+    public static String htmlEscapeCharsToString(String source) {
+        return StringUtils.isEmpty(source) ? source : source.replaceAll("&lt;", "<").replaceAll("&gt;", ">")
+                .replaceAll("&amp;", "&").replaceAll("&quot;", "\"");
+    }
+
+    /**
+     * transform half width char to full width char
+     * 
+     * <pre>
+     * fullWidthToHalfWidth(null) = null;
+     * fullWidthToHalfWidth("") = "";
+     * fullWidthToHalfWidth(new String(new char[] {12288})) = " ";
+     * fullWidthToHalfWidth("!"#$%&) = "!\"#$%&";
+     * </pre>
+     * 
+     * @param s
+     * @return
+     */
+    public static String fullWidthToHalfWidth(String s) {
+        if (isEmpty(s)) {
+            return s;
+        }
+
+        char[] source = s.toCharArray();
+        for (int i = 0; i < source.length; i++) {
+            if (source[i] == 12288) {
+                source[i] = ' ';
+                // } else if (source[i] == 12290) {
+                // source[i] = '.';
+            } else if (source[i] >= 65281 && source[i] <= 65374) {
+                source[i] = (char)(source[i] - 65248);
+            } else {
+                source[i] = source[i];
+            }
+        }
+        return new String(source);
+    }
+
+    /**
+     * transform full width char to half width char
+     * 
+     * <pre>
+     * halfWidthToFullWidth(null) = null;
+     * halfWidthToFullWidth("") = "";
+     * halfWidthToFullWidth(" ") = new String(new char[] {12288});
+     * halfWidthToFullWidth("!\"#$%&) = "!"#$%&";
+     * </pre>
+     * 
+     * @param s
+     * @return
+     */
+    public static String halfWidthToFullWidth(String s) {
+        if (isEmpty(s)) {
+            return s;
+        }
+
+        char[] source = s.toCharArray();
+        for (int i = 0; i < source.length; i++) {
+            if (source[i] == ' ') {
+                source[i] = (char)12288;
+                // } else if (source[i] == '.') {
+                // source[i] = (char)12290;
+            } else if (source[i] >= 33 && source[i] <= 126) {
+                source[i] = (char)(source[i] + 65248);
+            } else {
+                source[i] = source[i];
+            }
+        }
+        return new String(source);
+    }
+}

+ 5 - 0
android-skin-loader-lib/src/main/res/values/attrs.xml

@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+    <!-- Used for set whether the view should be skin enable-->
+    <attr name="enable" format="boolean"/>
+</resources>

+ 3 - 0
android-skin-loader-lib/src/main/res/values/strings.xml

@@ -0,0 +1,3 @@
+<resources>
+    <string name="app_name">android-skin-loader-lib</string>
+</resources>

+ 1 - 0
app/.gitignore

@@ -0,0 +1 @@
+/build

+ 68 - 0
app/build.gradle

@@ -0,0 +1,68 @@
+apply plugin: 'com.android.application'
+apply plugin: 'realm-android'
+
+android {
+    compileSdkVersion 27
+    defaultConfig {
+        applicationId "com.itant.shiwushu"
+        minSdkVersion 19
+        targetSdkVersion 27
+        versionCode 7
+        versionName "1.0.7"
+        testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
+    }
+    buildTypes {
+        release {
+            minifyEnabled false
+            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
+        }
+    }
+
+    packagingOptions {
+        exclude "lib/arm64-v8a/librealm-jni.so"
+        exclude "lib/mips/librealm-jni.so"
+        exclude "lib/x86/librealm-jni.so"
+        exclude "lib/x86_64/librealm-jni.so"
+    }
+
+    defaultConfig {
+        ndk {
+            abiFilters "armeabi", 'armeabi-v7a'
+        }
+    }
+}
+
+dependencies {
+    implementation fileTree(include: ['*.jar'], dir: 'libs')
+    implementation 'com.android.support:appcompat-v7:27.1.1'
+    implementation 'com.android.support.constraint:constraint-layout:1.1.3'
+    testImplementation 'junit:junit:4.12'
+    androidTestImplementation 'com.android.support.test:runner:1.0.2'
+    androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2'
+    /*图片选择*/
+    compile 'me.iwf.photopicker:PhotoPicker:0.9.12@aar'
+    compile 'com.android.support:recyclerview-v7:27.1.1'
+    compile 'com.github.bumptech.glide:glide:4.1.1'
+    implementation 'com.squareup.okhttp3:okhttp:3.11.0'
+    compile 'com.alibaba:fastjson:1.2.51'
+    // Rx Java
+    implementation 'io.reactivex.rxjava2:rxandroid:2.1.0'
+    implementation 'io.reactivex.rxjava2:rxjava:2.2.0'
+    implementation 'com.flyco.tablayout:FlycoTabLayout_Lib:2.1.2@aar'
+    implementation 'com.android.support:cardview-v7:27.1.1'
+    implementation project(':library')
+    /*圆形头像*/
+    implementation 'de.hdodenhof:circleimageview:2.2.0'
+    /*字体对齐*/
+    /*compile 'me.codeboy.android:align-text-view:2.3.2'*/
+    /*compile 'me.biubiubiu.justifytext:library:1.1'*/
+    /*换肤*/
+    implementation project(':android-skin-loader-lib')
+
+    // 图片点击可以放大缩小
+    compile 'com.github.chrisbanes:PhotoView:1.2.6'
+    compile 'com.github.liuguangqiang.swipeback:library:1.0.2@aar'
+
+    compile 'com.umeng.sdk:common:1.5.0'
+    compile 'com.umeng.sdk:analytics:7.5.0'
+}

BIN
app/libs/open_sdk_r6019_lite.jar


+ 21 - 0
app/proguard-rules.pro

@@ -0,0 +1,21 @@
+# Add project specific ProGuard rules here.
+# You can control the set of applied configuration files using the
+# proguardFiles setting in build.gradle.
+#
+# For more details, see
+#   http://developer.android.com/guide/developing/tools/proguard.html
+
+# 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 *;
+#}
+
+# Uncomment this to preserve the line number information for
+# debugging stack traces.
+#-keepattributes SourceFile,LineNumberTable
+
+# If you keep the line number information, uncomment this to
+# hide the original source file name.
+#-renamesourcefileattribute SourceFile

+ 26 - 0
app/src/androidTest/java/com/itant/shiwushu/ExampleInstrumentedTest.java

@@ -0,0 +1,26 @@
+package com.itant.shiwushu;
+
+import android.content.Context;
+import android.support.test.InstrumentationRegistry;
+import android.support.test.runner.AndroidJUnit4;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import static org.junit.Assert.*;
+
+/**
+ * Instrumented test, which will execute on an Android device.
+ *
+ * @see <a href="http://d.android.com/tools/testing">Testing documentation</a>
+ */
+@RunWith(AndroidJUnit4.class)
+public class ExampleInstrumentedTest {
+    @Test
+    public void useAppContext() throws Exception {
+        // Context of the app under test.
+        Context appContext = InstrumentationRegistry.getTargetContext();
+
+        assertEquals("com.itant.shiwushu", appContext.getPackageName());
+    }
+}

+ 140 - 0
app/src/main/AndroidManifest.xml

@@ -0,0 +1,140 @@
+<?xml version="1.0" encoding="utf-8"?>
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+    package="com.itant.shiwushu">
+
+    <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
+    <uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
+    <uses-permission android:name="android.permission.INTERNET" />
+    <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
+    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
+    <uses-permission android:name="android.permission.CALL_PHONE" />
+
+    <application
+        android:allowBackup="true"
+        android:icon="@mipmap/ic_launcher"
+        android:supportsRtl="true"
+        android:label="@string/app_name"
+        android:theme="@style/NoActionBarTheme"
+        android:name=".LAFApplication">
+        <activity android:name=".WelcomeActivity"
+            android:configChanges="keyboardHidden|orientation|screenSize"
+            android:screenOrientation="portrait"
+            android:theme="@style/NoActionBarTheme.Welcome">
+            <intent-filter>
+                <action android:name="android.intent.action.MAIN" />
+                <category android:name="android.intent.category.LAUNCHER" />
+            </intent-filter>
+        </activity>
+
+        <activity android:name=".MainActivity"
+            android:configChanges="keyboardHidden|orientation|screenSize"
+            android:screenOrientation="portrait"
+            android:theme="@style/NoActionBarTheme"
+            android:windowSoftInputMode="adjustPan"/>
+        <!--软键盘不会把底部Tab顶上来-->
+
+        <activity android:name=".ui.login.LoginActivity"
+            android:configChanges="keyboardHidden|orientation|screenSize"
+            android:screenOrientation="portrait"
+            android:theme="@style/NoActionBarTheme"/>
+
+        <activity android:name=".ui.register.RegisterActivity"
+            android:configChanges="keyboardHidden|orientation|screenSize"
+            android:screenOrientation="portrait"
+            android:theme="@style/NoActionBarTheme"/>
+
+        <activity android:name=".ui.forget.ForgetActivity"
+            android:configChanges="keyboardHidden|orientation|screenSize"
+            android:screenOrientation="portrait"
+            android:theme="@style/NoActionBarTheme"/>
+
+        <activity android:name=".ui.menu.me.AboutActivity"
+            android:configChanges="keyboardHidden|orientation|screenSize"
+            android:screenOrientation="portrait"
+            android:theme="@style/NoActionBarTheme"/>
+
+        <activity android:name=".ui.city.ProvinceActivity"
+            android:configChanges="keyboardHidden|orientation|screenSize"
+            android:screenOrientation="portrait"
+            android:theme="@style/NoActionBarTheme"/>
+
+        <activity android:name=".ui.city.CityActivity"
+            android:configChanges="keyboardHidden|orientation|screenSize"
+            android:screenOrientation="portrait"
+            android:theme="@style/NoActionBarTheme"/>
+
+        <activity android:name=".ui.menu.message.MessageActivity"
+            android:configChanges="keyboardHidden|orientation|screenSize"
+            android:screenOrientation="portrait"
+            android:theme="@style/NoActionBarTheme"/>
+
+        <activity android:name=".ui.menu.me.info.UserInfoActivity"
+            android:configChanges="keyboardHidden|orientation|screenSize"
+            android:screenOrientation="portrait"
+            android:theme="@style/NoActionBarTheme"/>
+
+        <activity android:name=".ui.menu.post.publish.PublishPostActivity"
+            android:configChanges="keyboardHidden|orientation|screenSize"
+            android:screenOrientation="portrait"
+            android:theme="@style/NoActionBarTheme"/>
+
+        <activity android:name=".ui.detail.PostDetailActivity"
+            android:configChanges="keyboardHidden|orientation|screenSize"
+            android:screenOrientation="portrait"
+            android:theme="@style/NoActionBarTheme"/>
+
+        <activity android:name=".ui.list.PostListActivity"
+            android:configChanges="keyboardHidden|orientation|screenSize"
+            android:screenOrientation="portrait"
+            android:theme="@style/NoActionBarTheme"/>
+
+        <!--8.0以上的机子,主题设置了<item name="android:windowIsTranslucent">true</item>
+        则不能在这里设置screenOrientation,否则会闪退,但不使用windowIsTranslucent滑动的时候又不透明,所以只能去掉screenOrientation-->
+        <activity android:name=".ui.detail.PhotoActivity"
+            android:configChanges="keyboardHidden|orientation|screenSize"
+            android:theme="@style/NoActionBarTheme.PhotoDetailTheme"/>
+
+        <!--QQ登录开始-->
+        <activity
+            android:name="com.tencent.tauth.AuthActivity"
+            android:noHistory="true"
+            android:launchMode="singleTask" >
+            <intent-filter>
+                <action android:name="android.intent.action.VIEW" />
+                <category android:name="android.intent.category.DEFAULT" />
+                <category android:name="android.intent.category.BROWSABLE" />
+                <data android:scheme="tencent1107887184" />
+            </intent-filter>
+        </activity>
+        <activity
+            android:name="com.tencent.connect.common.AssistActivity"
+            android:configChanges="orientation|keyboardHidden|screenSize"
+            android:theme="@android:style/Theme.Translucent.NoTitleBar" />
+        <!--QQ登录结束-->
+
+        <!--图片选择开始-->
+        <activity
+            android:name="me.iwf.photopicker.PhotoPickerActivity"
+            android:theme="@style/Theme.AppCompat.NoActionBar"
+            android:configChanges="keyboardHidden|orientation|screenSize"
+            android:screenOrientation="portrait"/>
+
+        <activity
+            android:name="me.iwf.photopicker.PhotoPagerActivity"
+            android:theme="@style/Theme.AppCompat.NoActionBar"
+            android:configChanges="keyboardHidden|orientation|screenSize"
+            android:screenOrientation="portrait"/>
+        <!--图片选择结束-->
+
+        <!--设置这个provider是由于谷歌的新特性,sdk版本>=24必须设置这个东西才能正常拍照裁剪图片-->
+        <provider
+            android:name="android.support.v4.content.FileProvider"
+            android:authorities="com.itant.shiwushu.fileprovider"
+            android:grantUriPermissions="true"
+            android:exported="false">
+            <meta-data
+                android:name="android.support.FILE_PROVIDER_PATHS"
+                android:resource="@xml/file_paths" />
+        </provider>
+    </application>
+</manifest>

La diferencia del archivo ha sido suprimido porque es demasiado grande
+ 4914 - 0
app/src/main/assets/data/cities.txt


BIN
app/src/main/assets/data/city.realm


BIN
app/src/main/assets/skin/blue.skin


BIN
app/src/main/assets/skin/red.skin


+ 1 - 0
app/src/main/assets/todo.txt

@@ -0,0 +1 @@
+评论:发言人id,发言人昵称,发言内容、发言时间,评论所属帖子id

+ 34 - 0
app/src/main/java/com/itant/shiwushu/LAFApplication.java

@@ -0,0 +1,34 @@
+package com.itant.shiwushu;
+
+import android.app.Application;
+
+import com.itant.shiwushu.manager.RealmManager;
+import com.itant.shiwushu.tool.file.FileTool;
+import com.umeng.commonsdk.UMConfigure;
+
+import cn.feng.skin.manager.loader.SkinManager;
+
+/**
+ * Created by Jason on 2018/10/3.
+ */
+
+public class LAFApplication extends Application {
+    public static LAFApplication instance;
+
+    @Override
+    public void onCreate() {
+        super.onCreate();
+        UMConfigure.init(this, "5bc3091eb465f5807e000529", "all", UMConfigure.DEVICE_TYPE_PHONE, "");
+        new Thread(new Runnable() {
+            @Override
+            public void run() {
+                FileTool.copyAssetToSDCard(getApplicationContext());
+            }
+        }).start();
+        instance = this;
+        SkinManager.getInstance().init(this);
+        SkinManager.getInstance().load();
+
+        RealmManager.getInstance().initRealm(this);
+    }
+}

+ 128 - 0
app/src/main/java/com/itant/shiwushu/MainActivity.java

@@ -0,0 +1,128 @@
+package com.itant.shiwushu;
+
+import android.content.Intent;
+import android.os.Bundle;
+import android.support.annotation.NonNull;
+import android.support.design.widget.BottomNavigationView;
+import android.support.v4.app.Fragment;
+import android.support.v4.view.ViewPager;
+import android.view.MenuItem;
+
+import com.itant.shiwushu.adapter.FragmentPagerItemAdapter;
+import com.itant.shiwushu.base.BaseSkinActivity;
+import com.itant.shiwushu.bean.User;
+import com.itant.shiwushu.manager.CityManager;
+import com.itant.shiwushu.manager.ToastManager;
+import com.itant.shiwushu.manager.UserManager;
+import com.itant.shiwushu.tool.BottomNavigationViewHelper;
+import com.itant.shiwushu.ui.menu.found.FoundFragment;
+import com.itant.shiwushu.ui.menu.lost.LostFragment;
+import com.itant.shiwushu.ui.menu.me.MeFragment;
+import com.itant.shiwushu.ui.menu.message.MessageFragment;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public class MainActivity extends BaseSkinActivity {
+    public static final int CODE_CITY_LOST = 1;
+    public static final int CODE_CITY_FOUND = 2;
+    public static final int CODE_CITY_PUBLISH = 3;
+    public static final String KEY_CITY_TYPE = "city_type";
+
+    private ViewPager vp_main;
+    private List<Fragment> fragmentList;
+    private LostFragment lostFragment;
+    private FoundFragment foundFragment;
+
+    @Override
+    protected void onCreate(Bundle savedInstanceState) {
+
+        super.onCreate(savedInstanceState);
+        setContentView(R.layout.activity_main);
+
+        // 去除底部动画效果
+        BottomNavigationView navigation = findViewById(R.id.navigation);
+        BottomNavigationViewHelper.disableShiftMode(navigation);
+        navigation.setOnNavigationItemSelectedListener(mOnNavigationItemSelectedListener);
+
+        fragmentList = new ArrayList<>();
+        lostFragment = new LostFragment();
+        foundFragment = new FoundFragment();
+        fragmentList.add(lostFragment);
+        fragmentList.add(foundFragment);
+        fragmentList.add(new MessageFragment());
+        fragmentList.add(new MeFragment());
+        vp_main = findViewById(R.id.vp_main);
+        vp_main.setOffscreenPageLimit(fragmentList.size());
+
+        FragmentPagerItemAdapter adapter = new FragmentPagerItemAdapter(getSupportFragmentManager(), fragmentList);
+        vp_main.setAdapter(adapter);
+        vp_main.setCurrentItem(0);
+
+        /*if (UserManager.getInstance().getUser().getUserType() == User.USER_TYPE_VIP) {
+            ToastManager.getInstance().toastShort("欢迎VIP用户");
+        }*/
+        if (UserManager.getInstance().getUser().getUserType() == User.USER_TYPE_INVALID) {
+            // 非法用户,可能是诈骗号
+            ToastManager.getInstance().toastShort("您的账户有误");
+            finish();
+            return;
+        }
+        CityManager.getInstance().initCity();
+    }
+
+    private BottomNavigationView.OnNavigationItemSelectedListener mOnNavigationItemSelectedListener
+            = new BottomNavigationView.OnNavigationItemSelectedListener() {
+
+        @Override
+        public boolean onNavigationItemSelected(@NonNull MenuItem item) {
+            switch (item.getItemId()) {
+                case R.id.menu_lost:
+                    // 选中失物
+                    vp_main.setCurrentItem(0);
+                    return true;
+                case R.id.menu_found:
+                    // 选中招领
+                    vp_main.setCurrentItem(1);
+                    return true;
+
+                case R.id.menu_message:
+                    // 选中消息
+                    vp_main.setCurrentItem(2);
+                    return true;
+                case R.id.menu_me:
+                    // 选中我的
+                    vp_main.setCurrentItem(3);
+                    return true;
+            }
+            return false;
+        }
+    };
+
+    private long lastBackMillis;
+    @Override
+    public void onBackPressed() {
+        if (System.currentTimeMillis() - lastBackMillis > 2500) {
+            lastBackMillis = System.currentTimeMillis();
+            ToastManager.getInstance().toastShort("再按一次退出");
+        } else {
+            super.onBackPressed();
+        }
+    }
+
+    @Override
+    protected void onActivityResult(int requestCode, int resultCode, Intent data) {
+        super.onActivityResult(requestCode, resultCode, data);
+        if (resultCode != RESULT_OK) {
+            return;
+        }
+
+        if (requestCode == CODE_CITY_LOST) {
+            lostFragment.onCityChoose();
+        }
+
+        if (requestCode == CODE_CITY_FOUND) {
+            foundFragment.onCityChoose();
+        }
+    }
+}

+ 56 - 0
app/src/main/java/com/itant/shiwushu/WelcomeActivity.java

@@ -0,0 +1,56 @@
+package com.itant.shiwushu;
+
+import android.content.Intent;
+import android.os.Bundle;
+import android.os.Handler;
+import android.support.annotation.Nullable;
+import android.support.v7.app.AppCompatActivity;
+import android.support.v7.widget.AppCompatImageView;
+import android.view.WindowManager;
+import android.widget.TextView;
+
+import com.itant.shiwushu.bean.User;
+import com.itant.shiwushu.manager.UserManager;
+import com.itant.shiwushu.tool.ColorTool;
+import com.itant.shiwushu.ui.login.LoginActivity;
+
+/**
+ * Created by Jason on 2018/10/2.
+ */
+
+public class WelcomeActivity extends AppCompatActivity {
+    @Override
+    protected void onCreate(@Nullable Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+        setContentView(R.layout.activity_welcome);
+
+        int color = ColorTool.getColorPrimary(this);
+        AppCompatImageView iv_welcome = findViewById(R.id.iv_welcome);
+        iv_welcome.setColorFilter(color, android.graphics.PorterDuff.Mode.SRC_IN);
+
+        TextView tv_welcome = findViewById(R.id.tv_welcome);
+        tv_welcome.setTextColor(color);
+
+        new Handler().postDelayed(new Runnable() {
+            @Override
+            public void run() {
+                /*if (SafeTool.isDeviceRoot() || SafeTool.isNetworkProxy(WelcomeActivity.this)) {
+                    ToastManager.getInstance().toastShort("为了您的账户安全,请勿使用Root设备或VPN网络");
+                }*/
+
+                // 防止MainActivity启动时statusbar闪烁,闪烁是由于前一个界面为全屏造成的.解决方法就是将前一个界面在退出前设置为非全屏.
+                getWindow().setFlags(WindowManager.LayoutParams.FLAG_FORCE_NOT_FULLSCREEN, WindowManager.LayoutParams.FLAG_FORCE_NOT_FULLSCREEN);
+
+                // 判断是否已登录
+                User user = UserManager.getInstance().getUser();
+                if (user != null) {
+                    startActivity(new Intent(WelcomeActivity.this, MainActivity.class));
+                } else {
+                    startActivity(new Intent(WelcomeActivity.this, LoginActivity.class));
+                }
+                finish();
+
+            }
+        }, 2000);
+    }
+}

+ 31 - 0
app/src/main/java/com/itant/shiwushu/adapter/FragmentPagerItemAdapter.java

@@ -0,0 +1,31 @@
+package com.itant.shiwushu.adapter;
+
+import android.support.v4.app.Fragment;
+import android.support.v4.app.FragmentManager;
+import android.support.v4.app.FragmentPagerAdapter;
+
+import java.util.List;
+
+/**
+ * Created by Jason on 2018/5/19.
+ */
+
+public class FragmentPagerItemAdapter extends FragmentPagerAdapter {
+
+    private List<Fragment> fragmentList;
+
+    public FragmentPagerItemAdapter(FragmentManager fm, List<Fragment> fragmentList) {
+        super(fm);
+        this.fragmentList = fragmentList;
+    }
+
+    @Override
+    public Fragment getItem(int position) {
+        return fragmentList.get(position);
+    }
+
+    @Override
+    public int getCount() {
+        return fragmentList.size();
+    }
+}

+ 32 - 0
app/src/main/java/com/itant/shiwushu/adapter/TabPageAdapter.java

@@ -0,0 +1,32 @@
+package com.itant.shiwushu.adapter;
+
+import android.support.v4.app.Fragment;
+import android.support.v4.app.FragmentManager;
+import android.support.v4.app.FragmentPagerAdapter;
+
+import com.itant.shiwushu.ui.menu.sub.PostFragment;
+
+import java.util.List;
+
+/**
+ * Created by Jason on 2018/8/20.
+ */
+
+public class TabPageAdapter extends FragmentPagerAdapter {
+
+    private List<PostFragment> tabFragments;
+    public TabPageAdapter(FragmentManager fm, List<PostFragment> fragments) {
+        super(fm);
+        this.tabFragments = fragments;
+    }
+
+    @Override
+    public Fragment getItem(int position) {
+        return tabFragments == null ? null : tabFragments.get(position);
+    }
+
+    @Override
+    public int getCount() {
+        return tabFragments == null ? 0 : tabFragments.size();
+    }
+}

+ 135 - 0
app/src/main/java/com/itant/shiwushu/base/BaseSkinActivity.java

@@ -0,0 +1,135 @@
+package com.itant.shiwushu.base;
+
+import android.content.Intent;
+import android.content.res.Resources;
+import android.graphics.Rect;
+import android.os.Bundle;
+import android.support.annotation.Nullable;
+import android.view.View;
+import android.view.ViewTreeObserver;
+
+import com.itant.shiwushu.R;
+import com.itant.shiwushu.manager.ActivityManager;
+import com.itant.shiwushu.manager.SharedPreferenceManager;
+import com.itant.shiwushu.manager.ThemeManager;
+import com.itant.shiwushu.manager.ToastManager;
+import com.itant.shiwushu.tool.ActivityTool;
+import com.itant.shiwushu.tool.ThemeTool;
+import com.itant.shiwushu.ui.login.LoginActivity;
+import com.itant.shiwushu.widget.LoadingDialog;
+import com.umeng.analytics.MobclickAgent;
+
+import cn.feng.skin.manager.base.BaseFragmentActivity;
+
+
+/**
+ * Created by Jason on 2018/8/25.
+ */
+
+public class BaseSkinActivity extends BaseFragmentActivity {
+    private LoadingDialog loadingDialog;
+    protected void onSoftKeyboardStateChanged(boolean isVisible) {}
+    public void setLoading(boolean isLoading) {
+        if (isLoading) {
+            if (loadingDialog == null) {
+                loadingDialog = new LoadingDialog(this);
+            }
+            loadingDialog.show();
+        } else {
+            if (loadingDialog != null) {
+                loadingDialog.hide();
+            }
+        }
+    }
+
+    protected void setLoading(boolean isLoading, boolean isCancelable) {
+        setLoading(isLoading);
+        loadingDialog.setCancelable(isCancelable);
+    }
+
+    protected ViewTreeObserver.OnGlobalLayoutListener getLayoutListener(final View view) {
+        return new ViewTreeObserver.OnGlobalLayoutListener() {
+            @Override
+            public void onGlobalLayout() {
+                Rect r = new Rect();
+                view.getWindowVisibleDisplayFrame(r);
+                if (view.getRootView().getHeight() - (r.bottom - r.top) > 500) { // if more than 100 pixels, its probably a keyboard...
+                    //键盘弹出了
+                    onSoftKeyboardStateChanged(true);
+                } else {
+                    //键盘隐藏了
+                    onSoftKeyboardStateChanged(false);
+                }
+            }
+        };
+    }
+
+    @Override
+    public void onTrimMemory(int level) {
+        super.onTrimMemory(level);
+        // 低内存的时候,自杀,防止空指针
+        /*if (level == TRIM_MEMORY_UI_HIDDEN) {
+            finish();
+        }*/
+    }
+
+    @Override
+    protected void onCreate(@Nullable Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+        int themeMode = ThemeManager.getInstance().getInt(ThemeManager.KEY_THEME_MODE, ThemeManager.THEME_MODE_DEFAULT);
+        ThemeTool.setStatusColor(this, themeMode);
+        ActivityManager.getInstance().addActivity(this);
+    }
+
+    @Override
+    protected void onDestroy() {
+        super.onDestroy();
+        ActivityManager.getInstance().removeActivity(this);
+    }
+
+    public void needReLogin() {
+        // 清除数据,重新登录
+        if (!ActivityManager.getInstance().isLoginExist()) {
+            ActivityManager.getInstance().setLoginExist(true);
+            ToastManager.getInstance().toastShort("登录已过期");
+            SharedPreferenceManager.getInstance().clear();
+            Intent intent = new Intent(this, LoginActivity.class);
+            //intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+            ActivityTool.startActivity(this, intent);
+            ActivityManager.getInstance().clearActivityExcept(LoginActivity.class);
+        }
+    }
+
+    @Override
+    public void finish() {
+        super.finish();
+        overridePendingTransition(R.anim.anim_slide_left_in, R.anim.anim_slide_right_out);
+    }
+
+    @Override
+    public Resources.Theme getTheme() {
+        Resources.Theme theme = super.getTheme();
+
+        int themeMode = ThemeManager.getInstance().getInt(ThemeManager.KEY_THEME_MODE, ThemeManager.THEME_MODE_DEFAULT);
+        if(themeMode == ThemeManager.THEME_MODE_RED){
+            theme.applyStyle(R.style.NoActionBarThemeRed, true);
+        } else if (themeMode == ThemeManager.THEME_MODE_BLUE) {
+            theme.applyStyle(R.style.NoActionBarThemeBlue, true);
+        } else {
+            theme.applyStyle(R.style.NoActionBarTheme, true);
+        }
+        // you could also use a switch if you have many themes that could apply
+        return theme;
+    }
+
+    @Override
+    public void onResume() {
+        super.onResume();
+        MobclickAgent.onResume(this);
+    }
+    @Override
+    public void onPause() {
+        super.onPause();
+        MobclickAgent.onPause(this);
+    }
+}

+ 45 - 0
app/src/main/java/com/itant/shiwushu/base/BaseSkinFragment.java

@@ -0,0 +1,45 @@
+package com.itant.shiwushu.base;
+
+import android.app.Activity;
+import android.content.Context;
+import android.os.Bundle;
+import android.support.annotation.Nullable;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+
+import cn.feng.skin.manager.base.BaseFragment;
+
+public abstract class BaseSkinFragment extends BaseFragment {
+    protected IBasePresenter presenter;
+    protected Activity activity;
+
+
+    private View mView;
+
+    public abstract int getLayoutId();
+
+    public abstract void initViews(View view);
+
+    @Nullable
+    @Override
+    public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
+        if (mView == null) {
+            mView = inflater.inflate(getLayoutId(), container, false);
+            initViews(mView);
+        }
+        return mView;
+    }
+
+    @Override
+    public void onDestroy() {
+        super.onDestroy();
+        presenter = null;
+    }
+
+    @Override
+    public void onAttach(Context context) {
+        super.onAttach(context);
+        this.activity = getActivity();
+    }
+}

+ 56 - 0
app/src/main/java/com/itant/shiwushu/base/BaseSwipeActivity.java

@@ -0,0 +1,56 @@
+package com.itant.shiwushu.base;
+
+import android.support.v7.app.AppCompatActivity;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.widget.ImageView;
+import android.widget.RelativeLayout;
+
+import com.liuguangqiang.swipeback.SwipeBackLayout;
+
+/**
+ * Created by iTant on 2017/3/26.
+ */
+
+public class BaseSwipeActivity extends AppCompatActivity implements SwipeBackLayout.SwipeBackListener {
+
+    private SwipeBackLayout swipeBackLayout;
+    private ImageView ivShadow;
+
+    @Override
+    public void setContentView(int layoutResID) {
+        super.setContentView(getContainer());
+        View view = LayoutInflater.from(this).inflate(layoutResID, null);
+        swipeBackLayout.addView(view);
+    }
+
+    private View getContainer() {
+        RelativeLayout container = new RelativeLayout(this);
+        swipeBackLayout = new SwipeBackLayout(this);
+        swipeBackLayout.setOnSwipeBackListener(this);
+        ivShadow = new ImageView(this);
+        ivShadow.setBackgroundColor(getResources().getColor(com.liuguangqiang.swipeback.R.color.black_p50));
+        RelativeLayout.LayoutParams params = new RelativeLayout.LayoutParams(RelativeLayout.LayoutParams.MATCH_PARENT, RelativeLayout.LayoutParams.MATCH_PARENT);
+        container.addView(ivShadow, params);
+        container.addView(swipeBackLayout);
+        return container;
+    }
+
+    public void setDragEdge(SwipeBackLayout.DragEdge dragEdge) {
+        swipeBackLayout.setDragEdge(dragEdge);
+    }
+
+    public SwipeBackLayout getSwipeBackLayout() {
+        return swipeBackLayout;
+    }
+
+    @Override
+    public void onViewPositionChanged(float fractionAnchor, float fractionScreen) {
+        ivShadow.setAlpha(1 - fractionScreen);
+    }
+
+    @Override
+    public void onPointerCaptureChanged(boolean hasCapture) {
+
+    }
+}

+ 9 - 0
app/src/main/java/com/itant/shiwushu/base/IBasePresenter.java

@@ -0,0 +1,9 @@
+package com.itant.shiwushu.base;
+
+/**
+ * Created by iTant on 2017/3/26.
+ */
+
+public interface IBasePresenter {
+
+}

+ 9 - 0
app/src/main/java/com/itant/shiwushu/base/IBaseView.java

@@ -0,0 +1,9 @@
+package com.itant.shiwushu.base;
+
+/**
+ * Created by iTant on 2017/3/26.
+ */
+
+public interface IBaseView {
+    void onRequestComplete();
+}

+ 10 - 0
app/src/main/java/com/itant/shiwushu/base/IPermission.java

@@ -0,0 +1,10 @@
+package com.itant.shiwushu.base;
+
+/**
+ * Created by iTant on 2017/4/15.
+ */
+
+public interface IPermission {
+    void onPermissionSuccess(int requestCode);
+    void onPermissionFail(int requestCode);
+}

+ 101 - 0
app/src/main/java/com/itant/shiwushu/base/LAFBaseActivity.java

@@ -0,0 +1,101 @@
+package com.itant.shiwushu.base;
+
+import android.content.Intent;
+import android.graphics.Rect;
+import android.os.Bundle;
+import android.support.annotation.Nullable;
+import android.support.v7.app.AppCompatActivity;
+import android.view.View;
+import android.view.ViewTreeObserver;
+
+import com.itant.shiwushu.R;
+import com.itant.shiwushu.manager.ActivityManager;
+import com.itant.shiwushu.manager.SharedPreferenceManager;
+import com.itant.shiwushu.manager.ToastManager;
+import com.itant.shiwushu.tool.ActivityTool;
+import com.itant.shiwushu.ui.login.LoginActivity;
+import com.itant.shiwushu.widget.LoadingDialog;
+
+
+/**
+ * Created by Jason on 2018/8/25.
+ */
+
+public class LAFBaseActivity extends AppCompatActivity {
+    private LoadingDialog loadingDialog;
+    protected void onSoftKeyboardStateChanged(boolean isVisible) {}
+    protected void setLoading(boolean isLoading) {
+        if (isLoading) {
+            if (loadingDialog == null) {
+                loadingDialog = new LoadingDialog(this);
+            }
+            loadingDialog.show();
+        } else {
+            if (loadingDialog != null) {
+                loadingDialog.hide();
+            }
+        }
+    }
+
+    protected void setLoading(boolean isLoading, boolean isCancelable) {
+        setLoading(isLoading);
+        loadingDialog.setCancelable(isCancelable);
+    }
+
+    protected ViewTreeObserver.OnGlobalLayoutListener getLayoutListener(final View view) {
+        return new ViewTreeObserver.OnGlobalLayoutListener() {
+            @Override
+            public void onGlobalLayout() {
+                Rect r = new Rect();
+                view.getWindowVisibleDisplayFrame(r);
+                if (view.getRootView().getHeight() - (r.bottom - r.top) > 500) { // if more than 100 pixels, its probably a keyboard...
+                    //键盘弹出了
+                    onSoftKeyboardStateChanged(true);
+                } else {
+                    //键盘隐藏了
+                    onSoftKeyboardStateChanged(false);
+                }
+            }
+        };
+    }
+
+    @Override
+    public void onTrimMemory(int level) {
+        super.onTrimMemory(level);
+        // 低内存的时候,自杀,防止空指针
+        /*if (level == TRIM_MEMORY_UI_HIDDEN) {
+            finish();
+        }*/
+    }
+
+    @Override
+    protected void onCreate(@Nullable Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+        ActivityManager.getInstance().addActivity(this);
+    }
+
+    @Override
+    protected void onDestroy() {
+        super.onDestroy();
+        ActivityManager.getInstance().removeActivity(this);
+    }
+
+    public void needReLogin() {
+        // 清除数据,重新登录
+        if (!ActivityManager.getInstance().isLoginExist()) {
+            ActivityManager.getInstance().setLoginExist(true);
+            ToastManager.getInstance().toastShort("登录已过期");
+            SharedPreferenceManager.getInstance().clear();
+            Intent intent = new Intent(this, LoginActivity.class);
+            //intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+            ActivityTool.startActivity(this, intent);
+            ActivityManager.getInstance().clearActivityExcept(LoginActivity.class);
+        }
+    }
+
+    @Override
+    public void finish() {
+        super.finish();
+        overridePendingTransition(R.anim.anim_slide_left_in, R.anim.anim_slide_right_out);
+    }
+}

+ 44 - 0
app/src/main/java/com/itant/shiwushu/base/LAFBaseFragment.java

@@ -0,0 +1,44 @@
+package com.itant.shiwushu.base;
+
+import android.app.Activity;
+import android.content.Context;
+import android.os.Bundle;
+import android.support.annotation.Nullable;
+import android.support.v4.app.Fragment;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+
+public abstract class LAFBaseFragment extends Fragment {
+    protected IBasePresenter presenter;
+    protected Activity activity;
+
+
+    private View mView;
+
+    public abstract int getLayoutId();
+
+    public abstract void initViews(View view);
+
+    @Nullable
+    @Override
+    public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
+        if (mView == null) {
+            mView = inflater.inflate(getLayoutId(), container, false);
+            initViews(mView);
+        }
+        return mView;
+    }
+
+    @Override
+    public void onDestroy() {
+        super.onDestroy();
+        presenter = null;
+    }
+
+    @Override
+    public void onAttach(Context context) {
+        super.onAttach(context);
+        this.activity = getActivity();
+    }
+}

+ 47 - 0
app/src/main/java/com/itant/shiwushu/bean/City.java

@@ -0,0 +1,47 @@
+package com.itant.shiwushu.bean;
+
+import io.realm.RealmObject;
+import io.realm.annotations.PrimaryKey;
+import io.realm.annotations.Required;
+
+/**
+ * Created by Jason on 2018/10/6.
+ */
+
+public class City extends RealmObject {
+    public static final int CITY_ALL = 0;
+    public static final String KEY_CITY = "city";
+
+    @Required
+    @PrimaryKey
+    private Integer cityId;
+
+    @Required
+    private String name;
+
+    private String provinceName;
+
+    public String getName() {
+        return name;
+    }
+
+    public void setName(String name) {
+        this.name = name;
+    }
+
+    public String getProvinceName() {
+        return provinceName;
+    }
+
+    public void setProvinceName(String provinceName) {
+        this.provinceName = provinceName;
+    }
+
+    public Integer getCityId() {
+        return cityId;
+    }
+
+    public void setCityId(Integer cityId) {
+        this.cityId = cityId;
+    }
+}

+ 22 - 0
app/src/main/java/com/itant/shiwushu/bean/FavoriteResult.java

@@ -0,0 +1,22 @@
+package com.itant.shiwushu.bean;
+
+public class FavoriteResult {
+    private int browseNum;
+    private boolean isFavorite;
+
+    public int getBrowseNum() {
+        return browseNum;
+    }
+
+    public void setBrowseNum(int browseNum) {
+        this.browseNum = browseNum;
+    }
+
+    public boolean isFavorite() {
+        return isFavorite;
+    }
+
+    public void setFavorite(boolean favorite) {
+        isFavorite = favorite;
+    }
+}

+ 187 - 0
app/src/main/java/com/itant/shiwushu/bean/LostFound.java

@@ -0,0 +1,187 @@
+package com.itant.shiwushu.bean;
+
+import java.io.Serializable;
+
+/**
+ * Created by Jason on 2018/10/9.
+ */
+
+public class LostFound implements Serializable {
+    public static final String KEY_POST_TYPE = "post_type";
+    public static final String KEY_LOST_TYPE = "lost_type";
+    public static final int POST_TYPE_LOST = 0;
+    public static final int LOST_TYPE_ALL = 0;
+    public static final int LOST_TYPE_MONEY = 1;
+    public static final int LOST_TYPE_MAN = 2;
+    public static final int LOST_TYPE_GOODS = 3;
+    public static final int LOST_TYPE_PETS = 4;
+    // 我的发布
+    public static final int LOST_TYPE_PUBLISH = -1;
+    // 我的收藏
+    public static final int LOST_TYPE_FAVORITE = -2;
+    public static final int POST_TYPE_FOUND = 1;
+    private int lostFoundId;
+    private String authorId;
+
+    // 发帖人是否VIP,发帖人头像URL,当前帖子是否被当前用户所收藏,都是从数据库查到列表后,遍历过程中查询到最新结果,动态set的
+    private boolean isAuthorVip;
+    private String authorHeadUrl;
+    private boolean isFavorite;
+    private String authorName;
+
+    // 描述
+    private String content;
+    // 图片
+    private String photoUrl;
+    private long publishTimeMillis;
+    // 事发城市
+    private String cityName;
+    // 事发城市id
+    private int cityId;
+    private int commentNum;
+    private int browseNum = 0;
+    // 联系方式
+    private String phoneNumber;
+    // 0失物 1招领
+    private int postType;
+    // 失物才有,0寻人 1找物 2爱宠
+    private int lostType;
+    // 悬赏金额
+    private int money;
+
+    public String getAuthorName() {
+        return authorName;
+    }
+
+    public void setAuthorName(String authorName) {
+        this.authorName = authorName;
+    }
+
+    public int getLostFoundId() {
+        return lostFoundId;
+    }
+
+    public void setLostFoundId(int lostFoundId) {
+        this.lostFoundId = lostFoundId;
+    }
+
+    public String getAuthorId() {
+        return authorId;
+    }
+
+    public void setAuthorId(String authorId) {
+        this.authorId = authorId;
+    }
+
+    public boolean isAuthorVip() {
+        return isAuthorVip;
+    }
+
+    public void setAuthorVip(boolean authorVip) {
+        isAuthorVip = authorVip;
+    }
+
+    public String getAuthorHeadUrl() {
+        return authorHeadUrl;
+    }
+
+    public void setAuthorHeadUrl(String authorHeadUrl) {
+        this.authorHeadUrl = authorHeadUrl;
+    }
+
+    public boolean isFavorite() {
+        return isFavorite;
+    }
+
+    public void setFavorite(boolean favorite) {
+        isFavorite = favorite;
+    }
+
+    public String getContent() {
+        return content;
+    }
+
+    public void setContent(String content) {
+        this.content = content;
+    }
+
+    public String getPhotoUrl() {
+        return photoUrl;
+    }
+
+    public void setPhotoUrl(String photoUrl) {
+        this.photoUrl = photoUrl;
+    }
+
+    public long getPublishTimeMillis() {
+        return publishTimeMillis;
+    }
+
+    public void setPublishTimeMillis(long publishTimeMillis) {
+        this.publishTimeMillis = publishTimeMillis;
+    }
+
+    public String getCityName() {
+        return cityName;
+    }
+
+    public void setCityName(String cityName) {
+        this.cityName = cityName;
+    }
+
+    public int getCommentNum() {
+        return commentNum;
+    }
+
+    public void setCommentNum(int commentNum) {
+        this.commentNum = commentNum;
+    }
+
+    public int getBrowseNum() {
+        return browseNum;
+    }
+
+    public void setBrowseNum(int browseNum) {
+        this.browseNum = browseNum;
+    }
+
+    public String getPhoneNumber() {
+        return phoneNumber;
+    }
+
+    public void setPhoneNumber(String phoneNumber) {
+        this.phoneNumber = phoneNumber;
+    }
+
+    public int getPostType() {
+        return postType;
+    }
+
+    public void setPostType(int postType) {
+        this.postType = postType;
+    }
+
+    public int getLostType() {
+        return lostType;
+    }
+
+    public void setLostType(int lostType) {
+        this.lostType = lostType;
+    }
+
+    public int getMoney() {
+        return money;
+    }
+
+    public void setMoney(int money) {
+        this.money = money;
+    }
+
+    public int getCityId() {
+        return cityId;
+    }
+
+    public void setCityId(int cityId) {
+        this.cityId = cityId;
+    }
+}

+ 45 - 0
app/src/main/java/com/itant/shiwushu/bean/Province.java

@@ -0,0 +1,45 @@
+package com.itant.shiwushu.bean;
+
+import io.realm.RealmList;
+import io.realm.RealmObject;
+import io.realm.annotations.PrimaryKey;
+import io.realm.annotations.Required;
+
+/**
+ * Created by Jason on 2018/10/6.
+ */
+
+public class Province extends RealmObject {
+    @Required
+    @PrimaryKey
+    private Integer provinceId;
+
+    @Required
+    private String name;
+
+    private RealmList<City> city;
+
+    public String getName() {
+        return name;
+    }
+
+    public void setName(String name) {
+        this.name = name;
+    }
+
+    public RealmList<City> getCity() {
+        return city;
+    }
+
+    public void setCity(RealmList<City> city) {
+        this.city = city;
+    }
+
+    public Integer getProvinceId() {
+        return provinceId;
+    }
+
+    public void setProvinceId(Integer provinceId) {
+        this.provinceId = provinceId;
+    }
+}

+ 40 - 0
app/src/main/java/com/itant/shiwushu/bean/ResponseResult.java

@@ -0,0 +1,40 @@
+package com.itant.shiwushu.bean;
+
+public class ResponseResult<T> {
+    private int resultCode = -1;
+    private String message;
+    private String extra;
+    private T resultObj;
+
+    public int getResultCode() {
+        return resultCode;
+    }
+
+    public void setResultCode(int resultCode) {
+        this.resultCode = resultCode;
+    }
+
+    public String getMessage() {
+        return message;
+    }
+
+    public void setMessage(String message) {
+        this.message = message;
+    }
+
+    public T getResultObj() {
+        return resultObj;
+    }
+
+    public void setResultObj(T resultObj) {
+        this.resultObj = resultObj;
+    }
+
+    public String getExtra() {
+        return extra;
+    }
+
+    public void setExtra(String extra) {
+        this.extra = extra;
+    }
+}

+ 147 - 0
app/src/main/java/com/itant/shiwushu/bean/User.java

@@ -0,0 +1,147 @@
+package com.itant.shiwushu.bean;
+
+/**
+ * Created by Jason on 2018/8/26.
+ */
+
+public class User {
+    public static final int USER_TYPE_INVALID = -1;
+    public static final int USER_TYPE_NORMAL = 0;
+    public static final int USER_TYPE_VIP = 1;
+    private String userId;
+    private String email;
+    private String nickName;
+    private String password;
+    private String headIcon;
+    private int sex;
+    private String token;
+    private long registerTimeMillis;
+    private long lastLoginTimeMillis;
+    // 0普通会员,-1是非法用户,1是VIP
+    private int userType;
+    private String deviceId;
+    private String lastIp;
+    private int themeMode;
+    private String phone;
+    private String saying;
+
+    public String getUserId() {
+        return userId;
+    }
+
+    public void setUserId(String userId) {
+        this.userId = userId;
+    }
+
+    public String getEmail() {
+        return email;
+    }
+
+    public void setEmail(String email) {
+        this.email = email;
+    }
+
+    public String getNickName() {
+        return nickName;
+    }
+
+    public void setNickName(String nickName) {
+        this.nickName = nickName;
+    }
+
+    public String getPassword() {
+        return password;
+    }
+
+    public void setPassword(String password) {
+        this.password = password;
+    }
+
+    public String getHeadIcon() {
+        return headIcon;
+    }
+
+    public void setHeadIcon(String headIcon) {
+        this.headIcon = headIcon;
+    }
+
+    public int getSex() {
+        return sex;
+    }
+
+    public void setSex(int sex) {
+        this.sex = sex;
+    }
+
+    public String getToken() {
+        return token;
+    }
+
+    public void setToken(String token) {
+        this.token = token;
+    }
+
+    public long getRegisterTimeMillis() {
+        return registerTimeMillis;
+    }
+
+    public void setRegisterTimeMillis(long registerTimeMillis) {
+        this.registerTimeMillis = registerTimeMillis;
+    }
+
+    public long getLastLoginTimeMillis() {
+        return lastLoginTimeMillis;
+    }
+
+    public void setLastLoginTimeMillis(long lastLoginTimeMillis) {
+        this.lastLoginTimeMillis = lastLoginTimeMillis;
+    }
+
+    public String getDeviceId() {
+        return deviceId;
+    }
+
+    public void setDeviceId(String deviceId) {
+        this.deviceId = deviceId;
+    }
+
+    public String getLastIp() {
+        return lastIp;
+    }
+
+    public void setLastIp(String lastIp) {
+        this.lastIp = lastIp;
+    }
+
+    public int getThemeMode() {
+        return themeMode;
+    }
+
+    public void setThemeMode(int themeMode) {
+        this.themeMode = themeMode;
+    }
+
+    public String getPhone() {
+        return phone;
+    }
+
+    public void setPhone(String phone) {
+        this.phone = phone;
+    }
+
+    public String getSaying() {
+        return saying;
+    }
+
+    public void setSaying(String saying) {
+        this.saying = saying;
+    }
+
+    public int getUserType() {
+        return userType;
+    }
+
+    public void setUserType(int userType) {
+        this.userType = userType;
+    }
+}

+ 32 - 0
app/src/main/java/com/itant/shiwushu/constant/CommonConstant.java

@@ -0,0 +1,32 @@
+package com.itant.shiwushu.constant;
+
+/**
+ * Created by Jason on 2018/10/14.
+ */
+
+public class CommonConstant {
+    /**
+     * 新特性,文件提供者路径
+     */
+    public static final String NAME_PROVIDE = "com.itant.shiwushu.fileprovider";
+
+    /**
+     * 应用外部根目录
+     */
+    private static final String DIRECTORY_ROOT = "/Android/data/com.itant.shiwushu";
+
+    /**
+     * 缓存目录
+     */
+    public static final String DIRECTORY_ROOT_CACHE = DIRECTORY_ROOT + "/cache";
+
+    /**
+     * 图片保存目录
+     */
+    public static final String MEI_ZHI_TEMP = DIRECTORY_ROOT_CACHE + "/";
+
+    /**
+     * 图片保存目录
+     */
+    public static final String PHOTO_SHIWUSHU = "/shiwushu_photo/";
+}

+ 17 - 0
app/src/main/java/com/itant/shiwushu/constant/ResultCode.java

@@ -0,0 +1,17 @@
+package com.itant.shiwushu.constant;
+
+public class ResultCode {
+    public static final int SUCCESS = 0;
+    public static final int FAILED_COMMON = -1;
+
+    /**
+     * 用户已存在
+     */
+    public static final int FAILED_USER_ALREADY_EXIST = -2;
+    /**
+     * 用户不存在
+     */
+    public static final int FAILED_USER_NOT_EXIST = -3;
+
+    public static final int FAILED_NEED_RE_LOGIN = -99;
+}

+ 163 - 0
app/src/main/java/com/itant/shiwushu/constant/ServerHost.java

@@ -0,0 +1,163 @@
+package com.itant.shiwushu.constant;
+
+/**
+ * Created by Jason on 2018/8/31.
+ * 解释一下为什么有两个连着的wuji,由于解压文件夹有一个wuji,下面所有子接口有wuji,是因为服务器的controller有wuji
+ * 我们在项目里debug的时候,部署的端口号可以不和tomcat的同(tomcat默认8080),而且debug的时候,就是按我们在项目部署的端口号来访问,
+ * tomcat webapp下面其他项目还是8080端口访问
+ * 但是发布成war包的时候,应该保持项目部署的和tomcat开放的端口一致,比如,都设为8080,或者都设为8940
+ */
+
+public class ServerHost {
+    // 这个是搬瓦工的
+    public static final String URL_BASE = "http://www.shuiliu520.com/laf";
+
+    // 这个是本地发布的服务器
+    //public static final String URL_BASE = "http://192.168.31.17:8080/laf";
+
+    // 开发调试的服务器,在Intellij直接run的那个
+    //public static final String URL_BASE = "http://192.168.31.17:8080";
+
+
+    /**
+     * 获取加密信息
+     */
+    //public static final String URL_GET_KEY_IV = URL_BASE + "/api/aes";
+
+    /**
+     * 获取验证码
+     */
+    public static final String URL_GET_CODE = URL_BASE + "/api/code";
+
+    /**
+     * 注册
+     */
+    public static final String URL_REGISTER = URL_BASE + "/api/register";
+
+    /**
+     * QQ登录
+     */
+    public static final String URL_QQ_LOGIN = URL_BASE + "/api/qqLogin";
+
+    /**
+     * 邮箱密码登录
+     */
+    public static final String URL_LOGIN = URL_BASE + "/api/login";
+
+
+    /**
+     * 重设密码
+     */
+    public static final String URL_RESET_PASSWORD = URL_BASE + "/api/password";
+
+    /**
+     * 系统通知
+     */
+    public static final String URL_SYSTEM_MESSAGE = URL_BASE + "/api/systemMessage";
+
+    /**
+     * 意见反馈
+     */
+    public static final String URL_FEEDBACK = URL_BASE + "/api/feedback";
+
+
+    /**
+     * 成为VIP
+     */
+    public static final String URL_VIP = URL_BASE + "/api/becomeVip";
+
+    /**
+     * 更新用户资料
+     */
+    public static final String URL_UPDATE_USER_DATA = URL_BASE + "/api/updateUserData";
+
+    /**
+     * 发布帖子
+     */
+    public static final String URL_PUBLISH_POST = URL_BASE + "/api/publishPost";
+
+    /**
+     * 获取帖子
+     */
+    public static final String URL_GET_POST = URL_BASE + "/api/getPost";
+
+    /**
+     * 删除帖子
+     */
+    public static final String URL_DELETE_POST = URL_BASE + "/api/delPost";
+
+    /**
+     * 获取帖子是否被当前用户收藏,返回是否被收藏,和浏览次数
+     */
+    public static final String URL_IS_FAVORITE = URL_BASE + "/api/isFavorite";
+
+    /**
+     * 收藏或者取消收藏
+     */
+    public static final String URL_FAVORITE = URL_BASE + "/api/goFavorite";
+
+
+
+
+
+
+
+
+
+    /**
+     * 增加密码记事
+     */
+    public static final String URL_ADD_PASSWORD = URL_BASE + "/api/addPassword";
+
+    /**
+     * 删除密码记事
+     */
+    public static final String URL_DEL_PASSWORD = URL_BASE + "/api/delPassword";
+
+    /**
+     * 更新某个密码记事
+     */
+    public static final String URL_UPDATE_PASSWORD = URL_BASE + "/api/updatePassword";
+
+    /**
+     * 查询密码记事列表
+     */
+    public static final String URL_GET_PASSWORD = URL_BASE + "/api/getPassword";
+
+
+
+
+    /**
+     * 增加笔记
+     */
+    public static final String URL_ADD_NOTE = URL_BASE + "/api/addNote";
+
+    /**
+     * 删除笔记
+     */
+    public static final String URL_DEL_NOTE = URL_BASE + "/api/delNote";
+
+    /**
+     * 更新笔记
+     */
+    public static final String URL_UPDATE_NOTE = URL_BASE + "/api/updateNote";
+
+    /**
+     * 查询个人所有笔记
+     */
+    public static final String URL_GET_NOTE = URL_BASE + "/api/getNote";
+
+    /**
+     * 保存背景图片
+     */
+    public static final String URL_UPLOAD_BG = URL_BASE + "/api/uploadBg";
+    /**
+     * 获取背景图片
+     */
+    public static final String URL_DOWNLOAD_BG = URL_BASE + "/api/downloadBg";
+
+    /**
+     * 更新主题模式
+     */
+    public static final String URL_UPDATE_THEME_MODE = URL_BASE + "/api/updateTheme";
+}

+ 11 - 0
app/src/main/java/com/itant/shiwushu/listener/OnUserUpdateListener.java

@@ -0,0 +1,11 @@
+package com.itant.shiwushu.listener;
+
+import com.itant.shiwushu.bean.User;
+
+/**
+ * Created by Jason on 2018/10/8.
+ */
+
+public interface OnUserUpdateListener {
+    void onUserDataUpdate(User user);
+}

+ 85 - 0
app/src/main/java/com/itant/shiwushu/manager/ActivityManager.java

@@ -0,0 +1,85 @@
+package com.itant.shiwushu.manager;
+
+import android.app.Activity;
+
+import java.util.HashSet;
+import java.util.Set;
+
+/**
+ * Created by Jason on 2018/8/26.
+ */
+
+public class ActivityManager {
+    private static ActivityManager activityManager;
+    private Set<Activity> activitySet;
+
+    private boolean isLoginExist;
+
+    private ActivityManager() {
+        activitySet = new HashSet<>();
+    }
+
+    public static ActivityManager getInstance() {
+        if (activityManager == null) {
+            initManager();
+        }
+
+        return activityManager;
+    }
+
+    private static synchronized void initManager() {
+        if (activityManager == null) {
+            activityManager = new ActivityManager();
+        }
+    }
+
+    public void addActivity(Activity activity) {
+        if (!activitySet.contains(activity)) {
+            activitySet.add(activity);
+        }
+    }
+
+    public void removeActivity(Activity activity) {
+        activitySet.remove(activity);
+    }
+
+    public void clearActivity() {
+        if (activitySet == null || activitySet.size() == 0) {
+            return;
+        }
+        for (Activity activity : activitySet) {
+            activity.finish();
+        }
+        activitySet.clear();
+    }
+
+    public void clearActivityExcept(Activity except) {
+        if (activitySet == null || activitySet.size() == 0) {
+            return;
+        }
+        for (Activity activity : activitySet) {
+            if (activity != except) {
+                activity.finish();
+            }
+        }
+    }
+
+    public void clearActivityExcept(Class except) {
+        if (activitySet == null || activitySet.size() == 0) {
+            return;
+        }
+        for (Activity activity : activitySet) {
+            if (activity.getClass() != except) {
+                activity.finish();
+            }
+        }
+    }
+
+    public boolean isLoginExist() {
+        return isLoginExist;
+    }
+
+    public void setLoginExist(boolean loginExist) {
+        isLoginExist = loginExist;
+    }
+}

+ 76 - 0
app/src/main/java/com/itant/shiwushu/manager/CityManager.java

@@ -0,0 +1,76 @@
+package com.itant.shiwushu.manager;
+
+import com.itant.shiwushu.bean.City;
+
+import java.util.List;
+
+/**
+ * Created by Jason on 2018/10/7.
+ */
+
+public class CityManager {
+    public static CityManager instance;
+    private City lostCity;
+    private City foundCity;
+    private City publishCity;
+    private List<City> cityList;
+    private CityManager() {}
+    public static CityManager getInstance() {
+        if (instance == null) {
+            init();
+        }
+        return instance;
+    }
+
+    private static synchronized void init() {
+        if (instance == null) {
+            instance = new CityManager();
+        }
+    }
+
+    public void initCity() {
+        lostCity = new City();
+        lostCity.setName("全部");
+        lostCity.setCityId(0);
+
+        foundCity = new City();
+        foundCity.setName("全部");
+        foundCity.setCityId(0);
+    }
+
+    public City getLostCity() {
+        return lostCity;
+    }
+
+    public void setLostCity(City lostCity) {
+        this.lostCity = lostCity;
+    }
+
+    public List<City> getCityList() {
+        return cityList;
+    }
+
+    public void setCityList(List<City> cityList) {
+        this.cityList = cityList;
+    }
+
+    public City getFoundCity() {
+        return foundCity;
+    }
+
+    public void setFoundCity(City foundCity) {
+        this.foundCity = foundCity;
+    }
+
+    public City getPublishCity() {
+        return publishCity;
+    }
+
+    public void setPublishCity(City publishCity) {
+        this.publishCity = publishCity;
+    }
+
+    public void restorePublishCity() {
+        publishCity = new City();
+    }
+}

+ 35 - 0
app/src/main/java/com/itant/shiwushu/manager/ExecutorManager.java

@@ -0,0 +1,35 @@
+package com.itant.shiwushu.manager;
+
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+
+/**
+ * Created by Jason on 2018/10/7.
+ */
+
+public class ExecutorManager {
+    private static final int NUMBER_THREAD = 5;
+    private static ExecutorManager instance;
+    private ExecutorService fixedExecutor;
+
+    private ExecutorManager() {
+        fixedExecutor = Executors.newFixedThreadPool(NUMBER_THREAD);
+    }
+
+    public static ExecutorManager getInstance() {
+        if (instance == null) {
+            init();
+        }
+        return instance;
+    }
+
+    private static synchronized void init() {
+        if (instance == null) {
+            instance = new ExecutorManager();
+        }
+    }
+
+    public ExecutorService getFixedExecutor() {
+        return fixedExecutor;
+    }
+}

+ 135 - 0
app/src/main/java/com/itant/shiwushu/manager/RealmManager.java

@@ -0,0 +1,135 @@
+package com.itant.shiwushu.manager;
+
+import android.content.Context;
+
+import com.alibaba.fastjson.JSON;
+import com.itant.shiwushu.bean.City;
+import com.itant.shiwushu.bean.Province;
+import com.itant.shiwushu.temp.TempCity;
+import com.itant.shiwushu.temp.TempProvince;
+import com.itant.shiwushu.tool.StringTool;
+
+import java.io.IOException;
+import java.util.List;
+
+import io.realm.Realm;
+import io.realm.RealmConfiguration;
+import io.realm.RealmList;
+import io.realm.RealmResults;
+
+/**
+ * Created by Jason on 2018/10/7.
+ */
+
+public class RealmManager {
+    private static RealmManager instance;
+    private List<Province> provinceList;
+
+    private RealmManager() {
+
+    }
+
+    public static RealmManager getInstance() {
+        if (instance == null) {
+            init();
+        }
+        return instance;
+    }
+
+    private static synchronized void init() {
+        if (instance == null) {
+            instance = new RealmManager();
+        }
+    }
+
+    public void initRealm(Context context) {
+        Realm.init(context);
+        RealmConfiguration realmConfig = new RealmConfiguration.Builder()
+                .assetFile("data/city.realm")
+                .schemaVersion(1)
+                //.name("city.realm")
+                //.directory(Environment.getExternalStorageDirectory())
+                .build();
+        Realm.setDefaultConfiguration(realmConfig);
+
+        // 在SD卡制作好数据库文件之后,上传到assets文件夹,以后就直接用assets文件夹里的数据库了
+        //parseCityData(context);
+        loadProvinceListFromDB();
+    }
+
+    /**
+     * 从文件读取JSON字符串并解析成为对象
+     */
+    private void parseCityData(final Context context) {
+        // 从assets里读取城市的json字符串,耗时操作
+        ExecutorManager.getInstance().getFixedExecutor().submit(new Runnable() {
+            @Override
+            public void run() {
+                try {
+                    String json = StringTool.getStringFromAssets(context, "data/cities.txt");
+                    List<TempProvince> provinceList = JSON.parseArray(json, TempProvince.class);
+                    saveCityDataToDB(provinceList);
+                } catch (IOException e) {
+                    e.printStackTrace();
+                }
+            }
+        });
+    }
+
+    /**
+     * 保存数据到数据库,以提高查询效率
+     */
+    private void saveCityDataToDB(List<TempProvince> tempProvinceList) {
+        Realm realm = Realm.getDefaultInstance();
+        realm.beginTransaction();
+
+        int provinceId = 0;
+        int cityId = 0;
+        Province allProvince = realm.createObject(Province.class, provinceId);
+        allProvince.setName("全部");
+        RealmList<City> allCityRealmList = new RealmList<>();
+        City allCity = realm.createObject(City.class, cityId);
+        allCity.setName("全部");
+        allCity.setProvinceName("全部");
+        allCityRealmList.add(allCity);
+        allProvince.setCity(allCityRealmList);
+
+        for (TempProvince tempProvince : tempProvinceList) {
+            provinceId++;
+            Province province = realm.createObject(Province.class, provinceId);
+            province.setName(tempProvince.getName());
+
+            List<TempCity> tempCityList = tempProvince.getCity();
+            RealmList<City> cityRealmList = new RealmList<>();
+            for (TempCity tempCity : tempCityList) {
+                cityId++;
+                City city = realm.createObject(City.class, cityId);
+                city.setName(tempCity.getName());
+                city.setProvinceName(tempProvince.getName());
+                cityRealmList.add(city);
+            }
+            province.setCity(cityRealmList);
+        }
+        realm.commitTransaction();
+    }
+
+    /**
+     * 从数据库加载省市数据到内存,方便使用
+     */
+    private void loadProvinceListFromDB() {
+        ExecutorManager.getInstance().getFixedExecutor().submit(new Runnable() {
+            @Override
+            public void run() {
+                Realm realm = Realm.getDefaultInstance();
+                RealmResults<Province> provinceRealmResults = realm.where(Province.class).findAll();
+                // 及时关闭数据库,回收资源,如果是Activity里使用,则onCreate里getInstance,onDestroy里close,一般都是成对出现
+                provinceList = realm.copyFromRealm(provinceRealmResults);
+                realm.close();
+            }
+        });
+    }
+
+    public List<Province> getProvinceList() {
+        return provinceList;
+    }
+}

+ 71 - 0
app/src/main/java/com/itant/shiwushu/manager/SharedPreferenceManager.java

@@ -0,0 +1,71 @@
+package com.itant.shiwushu.manager;
+
+import android.content.Context;
+import android.content.SharedPreferences;
+
+import com.itant.shiwushu.LAFApplication;
+
+/**
+ * Created by Jason on 2018/8/31.
+ */
+
+public class SharedPreferenceManager {
+    public static final String NAME_MAIN_PREFERENCE = "temp";
+    public static final String KEY_USER = "user";
+
+    // 反馈
+    public static final String KEY_FEEDBACK = "feedback";
+
+
+    private static SharedPreferenceManager manager;
+    private final SharedPreferences preferences;
+
+    private SharedPreferenceManager() {
+        preferences = LAFApplication.instance.getSharedPreferences(NAME_MAIN_PREFERENCE, Context.MODE_PRIVATE);
+    }
+
+    public static SharedPreferenceManager getInstance() {
+        if (manager == null) {
+            init();
+        }
+        return manager;
+    }
+
+    private static synchronized void init() {
+        if (manager == null) {
+            manager = new SharedPreferenceManager();
+        }
+    }
+
+    public void putString(String key, String value) {
+        preferences.edit().putString(key, value).commit();
+    }
+
+    public void putInt(String key, int value) {
+        preferences.edit().putInt(key, value).commit();
+    }
+
+    public String getString(String key) {
+        return preferences.getString(key, "");
+    }
+
+    public String getString(String key, String defaultValue) {
+        return preferences.getString(key, defaultValue);
+    }
+
+    public int getInt(String key) {
+        return preferences.getInt(key, 0);
+    }
+
+    public int getInt(String key, int defaultValue) {
+        return preferences.getInt(key, defaultValue);
+    }
+
+    public void clear() {
+        SharedPreferences.Editor editor = preferences.edit();
+        editor.clear();
+        editor.commit();
+        // 如果是自定义的皮肤,那么就不支持重新保存了
+        //ThemeManager.getInstance().putInt(ThemeManager.KEY_THEME_MODE, CommonConstant.THEME_DEFAULT);
+    }
+}

+ 65 - 0
app/src/main/java/com/itant/shiwushu/manager/ThemeManager.java

@@ -0,0 +1,65 @@
+package com.itant.shiwushu.manager;
+
+import android.content.Context;
+import android.content.SharedPreferences;
+
+import com.itant.shiwushu.LAFApplication;
+
+/**
+ * Created by Jason on 2018/9/18.
+ */
+
+public class ThemeManager {
+    public static final String NAME_THEME = "file_theme";
+    // 主题
+    public static final String KEY_THEME_MODE = "theme_mode";
+    public static final int THEME_MODE_DEFAULT = 0;
+    public static final int THEME_MODE_RED = 1;
+    public static final int THEME_MODE_BLUE = 2;
+
+    private static ThemeManager manager;
+    private SharedPreferences themePreference;
+    private ThemeManager() {
+        themePreference = LAFApplication.instance.getSharedPreferences(NAME_THEME, Context.MODE_PRIVATE);
+    }
+
+    public static ThemeManager getInstance() {
+        if (manager == null) {
+            init();
+        }
+        return manager;
+    }
+
+    private static synchronized void init() {
+        if (manager == null) {
+            manager = new ThemeManager();
+        }
+    }
+
+    public void putString(String key, String value) {
+        themePreference.edit().putString(key, value).commit();
+    }
+
+    public String getString(String key, String defaultValue) {
+        return themePreference.getString(key, defaultValue);
+    }
+
+    public int getInt(String key, int defaultValue) {
+        if (UserManager.getInstance().getUser() == null) {
+            return defaultValue;
+        }
+        return themePreference.getInt(key + UserManager.getInstance().getUser().getUserId(), defaultValue);
+    }
+
+    public void putInt(String key, int value) {
+        if (UserManager.getInstance().getUser() != null) {
+            themePreference.edit().putInt(key + UserManager.getInstance().getUser().getUserId(), value).commit();
+        }
+    }
+
+    public void clear() {
+        SharedPreferences.Editor editor = themePreference.edit();
+        editor.clear();
+        editor.commit();
+    }
+}

+ 56 - 0
app/src/main/java/com/itant/shiwushu/manager/ToastManager.java

@@ -0,0 +1,56 @@
+package com.itant.shiwushu.manager;
+
+import android.content.Context;
+import android.widget.Toast;
+
+import com.itant.shiwushu.LAFApplication;
+
+
+/**
+ * Created by Jason on 2018/8/31.
+ */
+
+public class ToastManager {
+
+    private static ToastManager manager;
+    private long lastShortToastMillis = 0;
+
+    private ToastManager() {
+
+    }
+
+    public static ToastManager getInstance() {
+        if (manager == null) {
+            init();
+        }
+        return manager;
+    }
+
+    private static synchronized void init() {
+        if (manager == null) {
+            manager = new ToastManager();
+        }
+    }
+
+    /**
+     * 不要频繁Toast
+     * @param message 内容
+     */
+    public void toastShort(String message) {
+        if (System.currentTimeMillis() - lastShortToastMillis > 2000) {
+            Toast.makeText(LAFApplication.instance.getApplicationContext(), message, Toast.LENGTH_SHORT).show();
+            //toast.setText(message);
+            //toast.show();
+        }
+        lastShortToastMillis = System.currentTimeMillis();
+    }
+
+    public void toastShort(String message, Context context) {
+        if (System.currentTimeMillis() - lastShortToastMillis > 2000) {
+            Toast toast = Toast.makeText(context, null, Toast.LENGTH_SHORT);
+            toast.setText(message);
+            toast.show();
+        }
+        lastShortToastMillis = System.currentTimeMillis();
+    }
+}

+ 81 - 0
app/src/main/java/com/itant/shiwushu/manager/UserManager.java

@@ -0,0 +1,81 @@
+package com.itant.shiwushu.manager;
+
+import android.text.TextUtils;
+
+import com.alibaba.fastjson.JSON;
+import com.itant.shiwushu.bean.User;
+import com.itant.shiwushu.constant.ServerHost;
+import com.itant.shiwushu.listener.OnUserUpdateListener;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Created by Jason on 2018/8/31.
+ */
+
+public class UserManager {
+
+    private static UserManager manager;
+    private User user;
+    private List<OnUserUpdateListener> userUpdateListenerList;
+
+    private UserManager() {
+        String userJson = SharedPreferenceManager.getInstance().getString(SharedPreferenceManager.KEY_USER, "");
+        if (!TextUtils.isEmpty(userJson)) {
+            user = JSON.parseObject(userJson, User.class);
+        }
+        userUpdateListenerList = new ArrayList<>();
+    }
+
+    public static UserManager getInstance() {
+        if (manager == null) {
+            init();
+        }
+        return manager;
+    }
+
+    private static synchronized void init() {
+        if (manager == null) {
+            manager = new UserManager();
+        }
+    }
+
+    public User getUser() {
+        return user;
+    }
+
+    public void setUser(User user) {
+        if (user != null) {
+            String headIconUrl = user.getHeadIcon();
+            if (!TextUtils.isEmpty(headIconUrl)) {
+                if (!headIconUrl.startsWith("http")) {
+                    headIconUrl = ServerHost.URL_BASE + headIconUrl;
+                    user.setHeadIcon(headIconUrl);
+                }
+            }
+            SharedPreferenceManager.getInstance().putString(SharedPreferenceManager.KEY_USER, JSON.toJSONString(user));
+        }
+        this.user = user;
+    }
+
+    public void registerUserUpdateListener(OnUserUpdateListener listener) {
+        if (userUpdateListenerList != null) {
+            userUpdateListenerList.add(listener);
+        }
+    }
+
+    public void unRegisterUserUpdateListener(OnUserUpdateListener listener) {
+        if (userUpdateListenerList != null) {
+            userUpdateListenerList.remove(listener);
+        }
+    }
+
+    public void notifyUserDataUpdate(User user) {
+        if (userUpdateListenerList != null) {
+            for (OnUserUpdateListener listener : userUpdateListenerList) {
+                listener.onUserDataUpdate(user);
+            }
+        }
+    }
+}

+ 65 - 0
app/src/main/java/com/itant/shiwushu/net/ElasticCallback.java

@@ -0,0 +1,65 @@
+package com.itant.shiwushu.net;
+
+import android.os.Handler;
+import android.support.annotation.NonNull;
+
+import com.itant.shiwushu.LAFApplication;
+
+import java.io.IOException;
+
+import okhttp3.Call;
+import okhttp3.Callback;
+import okhttp3.Response;
+import okhttp3.ResponseBody;
+
+/**
+ * 转到主线程
+ * Created by Jason on 2018/9/1.
+ */
+
+public abstract class ElasticCallback implements Callback {
+    private Handler mainHandler = new Handler(LAFApplication.instance.getMainLooper());
+    @Override
+    public void onFailure(@NonNull final Call call, @NonNull final IOException e) {
+        mainHandler.post(new Runnable() {
+
+            @Override
+            public void run() {
+                onRequestComplete();
+                onRequestFailure(call, e);
+            }
+        });
+    }
+
+    @Override
+    public void onResponse(@NonNull final Call call, @NonNull final Response response) throws IOException {
+
+        ResponseBody body = response.body();
+        if (body != null) {
+            final String result = body.string();
+            final int code = response.code();
+
+            mainHandler.post(new Runnable() {
+
+                @Override
+                public void run() {
+                    onRequestComplete();
+                    try {
+                        onRequestResponse(code, result);
+                    } catch (IOException e) {
+                        e.printStackTrace();
+                        onRequestFailure(call, e);
+                    }
+                }
+            });
+        } else {
+            onRequestFailure(call, new IOException("body is empty"));
+        }
+
+    }
+
+    public abstract void onRequestFailure(Call call, IOException e);
+    public abstract void onRequestResponse(int code, String result) throws IOException;
+    //public abstract void onRequestResponse(Call call, Response response) throws IOException;
+    public abstract void onRequestComplete();
+}

+ 142 - 0
app/src/main/java/com/itant/shiwushu/net/GlobalPresenter.java

@@ -0,0 +1,142 @@
+package com.itant.shiwushu.net;
+
+import android.text.TextUtils;
+
+import com.alibaba.fastjson.JSON;
+import com.alibaba.fastjson.TypeReference;
+import com.itant.shiwushu.bean.ResponseResult;
+import com.itant.shiwushu.bean.User;
+import com.itant.shiwushu.constant.ResultCode;
+import com.itant.shiwushu.constant.ServerHost;
+import com.itant.shiwushu.manager.SharedPreferenceManager;
+import com.itant.shiwushu.manager.ToastManager;
+import com.itant.shiwushu.manager.UserManager;
+import com.itant.shiwushu.tool.RequestClient;
+
+import java.io.IOException;
+
+import okhttp3.Call;
+import okhttp3.FormBody;
+import okhttp3.OkHttpClient;
+import okhttp3.Request;
+
+/**
+ * Created by Jason on 2018/10/7.
+ */
+
+public class GlobalPresenter {
+    private static boolean isReport = false;
+    public static void submitFeedback(final String feedback, String reporterId, String reporterName, String authorId, String authorName) {
+        UserManager userManager = UserManager.getInstance();
+        String email = userManager.getUser().getEmail();
+        //创建表单请求体
+        FormBody.Builder formBody = new FormBody.Builder();
+        String realFeedBack = feedback;
+        if (!TextUtils.isEmpty(reporterId)) {
+            isReport = true;
+            realFeedBack = reporterName + "【" + reporterId + "】举报" + authorName + "【" + authorId + "】:" + feedback;
+        } else {
+            isReport = false;
+        }
+        //传递键值对参数
+        formBody.add("userId", userManager.getUser().getUserId());
+        formBody.add("email", TextUtils.isEmpty(email) ? "" : email);
+        formBody.add("feedback", realFeedBack);
+        //创建Request 对象
+        Request request = new Request.Builder().url(ServerHost.URL_FEEDBACK)
+                //传递请求体
+                .post(formBody.build()).build();
+        OkHttpClient okHttpClient = RequestClient.getClient().getInterceptOkHttpClient();
+        okHttpClient.newCall(request).enqueue(new ElasticCallback() {
+
+            @Override
+            public void onRequestFailure(Call call, IOException e) {
+                // 反馈失败
+                sendFeedBackResult(false, feedback, isReport);
+            }
+
+            @Override
+            public void onRequestResponse(int code, String result) throws IOException {
+                if (code == 200) {
+                    ResponseResult responseResult = JSON.parseObject(result, ResponseResult.class);
+                    if (responseResult != null) {
+                        if (responseResult.getResultCode() == ResultCode.SUCCESS) {
+                            // 反馈成功
+                            sendFeedBackResult(true, feedback, isReport);
+                            return;
+                        }
+                    }
+                }
+                // 反馈失败
+                sendFeedBackResult(false, feedback, isReport);
+            }
+
+            @Override
+            public void onRequestComplete() {
+
+            }
+        });
+    }
+
+    private static void sendFeedBackResult(boolean success, String message, boolean isReport) {
+        if (success) {
+            if (!isReport) {
+                // 反馈成功,清空临时内容
+                SharedPreferenceManager.getInstance().putString(SharedPreferenceManager.KEY_FEEDBACK, "");
+            }
+
+            ToastManager.getInstance().toastShort("反馈发送成功");
+        } else {
+            if (!isReport) {
+                // 反馈失败,保存发送失败的内容
+                SharedPreferenceManager.getInstance().putString(SharedPreferenceManager.KEY_FEEDBACK, message);
+            }
+            ToastManager.getInstance().toastShort("反馈发送失败");
+        }
+    }
+
+    /**
+     * 成为VIP用户
+     */
+    public static void becomeVIP() {
+        User user = UserManager.getInstance().getUser();
+        //创建表单请求体
+        FormBody.Builder formBody = new FormBody.Builder();
+        //传递键值对参数
+        formBody.add("userId", user.getUserId());
+        formBody.add("token", user.getToken());
+        //创建Request 对象
+        Request request = new Request.Builder().url(ServerHost.URL_VIP)
+                //传递请求体
+                .post(formBody.build()).build();
+        OkHttpClient okHttpClient = RequestClient.getClient().getInterceptOkHttpClient();
+        okHttpClient.newCall(request).enqueue(new ElasticCallback() {
+
+            @Override
+            public void onRequestFailure(Call call, IOException e) {
+                // 成为VIP失败
+                e.printStackTrace();
+            }
+
+            @Override
+            public void onRequestResponse(int code, String result) throws IOException {
+                if (code == 200) {
+                    ResponseResult<User> responseResult = JSON.parseObject(result, new TypeReference<ResponseResult<User>>(){});
+                    if (responseResult != null) {
+                        if (responseResult.getResultCode() == ResultCode.SUCCESS) {
+                            // 成为VIP成功
+                            UserManager.getInstance().notifyUserDataUpdate(responseResult.getResultObj());
+                            return;
+                        }
+                    }
+                }
+                // 成为VIP失败
+            }
+
+            @Override
+            public void onRequestComplete() {
+
+            }
+        });
+    }
+}

+ 28 - 0
app/src/main/java/com/itant/shiwushu/temp/TempCity.java

@@ -0,0 +1,28 @@
+package com.itant.shiwushu.temp;
+
+import java.util.List;
+
+/**
+ * Created by Jason on 2018/10/6.
+ */
+
+public class TempCity {
+    private String name;
+    private List<String> area;
+
+    public String getName() {
+        return name;
+    }
+
+    public void setName(String name) {
+        this.name = name;
+    }
+
+    public List<String> getArea() {
+        return area;
+    }
+
+    public void setArea(List<String> area) {
+        this.area = area;
+    }
+}

+ 28 - 0
app/src/main/java/com/itant/shiwushu/temp/TempProvince.java

@@ -0,0 +1,28 @@
+package com.itant.shiwushu.temp;
+
+import java.util.List;
+
+/**
+ * Created by Jason on 2018/10/6.
+ */
+
+public class TempProvince {
+    private String name;
+    private List<TempCity> city;
+
+    public String getName() {
+        return name;
+    }
+
+    public void setName(String name) {
+        this.name = name;
+    }
+
+    public List<TempCity> getCity() {
+        return city;
+    }
+
+    public void setCity(List<TempCity> city) {
+        this.city = city;
+    }
+}

+ 51 - 0
app/src/main/java/com/itant/shiwushu/tool/ActivityTool.java

@@ -0,0 +1,51 @@
+package com.itant.shiwushu.tool;
+
+import android.annotation.SuppressLint;
+import android.app.Activity;
+import android.content.Intent;
+import android.os.Bundle;
+
+import com.itant.shiwushu.R;
+
+
+/**
+ * Created by iTant on 2017/4/8.
+ */
+
+public class ActivityTool {
+
+    /**
+     * Activity从右边往左边进,配合BaseActivity.finish()方法实现从左边往右边销毁
+     * @param activity
+     * @param intent
+     */
+    public static void startActivity(Activity activity, Intent intent) {
+        activity.startActivity(intent);
+        activity.overridePendingTransition(R.anim.anim_slide_right_in, R.anim.anim_slide_left_out);
+    }
+
+    /**
+     * Activity从右边往左边进,配合BaseActivity.finish()方法实现从左边往右边销毁
+     * @param activity
+     */
+    public static void startActivity(Activity activity, Class clazz) {
+        activity.startActivity(new Intent(activity, clazz));
+        activity.overridePendingTransition(R.anim.anim_slide_right_in, R.anim.anim_slide_left_out);
+    }
+
+    /**
+     * Activity从右边往左边进,配合BaseActivity.finish()方法实现从左边往右边销毁
+     * @param activity
+     * @param intent
+     */
+    @SuppressLint("NewApi")
+    public static void startActivity(Activity activity, Intent intent, Bundle bundle) {
+        activity.startActivity(intent, bundle);
+        activity.overridePendingTransition(R.anim.anim_slide_right_in, R.anim.anim_slide_left_out);
+    }
+
+    public static void startActivityForResult(Activity activity, Intent intent, int requestCode) {
+        activity.startActivityForResult(intent, requestCode);
+        activity.overridePendingTransition(R.anim.anim_slide_right_in, R.anim.anim_slide_left_out);
+    }
+}

+ 128 - 0
app/src/main/java/com/itant/shiwushu/tool/BitmapTool.java

@@ -0,0 +1,128 @@
+package com.itant.shiwushu.tool;
+
+import android.content.Context;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.net.Uri;
+
+import java.io.ByteArrayInputStream;
+import java.io.File;
+import java.io.FileNotFoundException;
+import java.io.FileOutputStream;
+import java.io.InputStream;
+
+public class BitmapTool {
+
+    /**
+     * 根据uri获取bitmap
+     * @param uri
+     * @return
+     */
+    public static Bitmap getBitmapFromUri(Context context, Uri uri) {
+        Bitmap bitmap = null;
+        try {
+            bitmap = getBitmapFromStream(context.getContentResolver().openInputStream(uri));
+        } catch (FileNotFoundException e) {
+            return null;
+        }
+        return bitmap;
+    }
+
+    public static Bitmap getBitmapFromBytes(byte[] bytes) {
+
+        InputStream inputStream = new ByteArrayInputStream(bytes);
+
+        // 为位图设置100K的缓存
+        BitmapFactory.Options options = new BitmapFactory.Options();
+        options.inTempStorage = new byte[100 * 1024];
+        options.inPreferredConfig = Bitmap.Config.RGB_565;
+        // 设置图片可以被回收,创建Bitmap用于存储Pixel的内存空间在系统内存不足时可以被回收
+        options.inPurgeable = true;
+
+        int len = bytes.length;
+        if (len < 1024 * 100) {
+            options.inSampleSize = 1;
+        } else if (len < 1024 * 1024) {
+            options.inSampleSize = 2;
+        } else {
+            //options.inSampleSize = 4;
+            double times = Math.floor(((double)len) / (1024 * 1024));
+            options.inSampleSize = (int)times * 2;
+        }
+
+
+        options.inSampleSize = 4;
+        options.inInputShareable  = true;
+        Bitmap bitmap = BitmapFactory.decodeStream(inputStream, null, options);
+        return bitmap;
+    }
+
+    public static Bitmap getBitmapFromStream(InputStream inputStream) {
+        if (inputStream == null) {
+            return null;
+        }
+
+        // 为位图设置100K的缓存
+        BitmapFactory.Options options = new BitmapFactory.Options();
+        options.inTempStorage = new byte[100 * 1024];
+        options.inPreferredConfig = Bitmap.Config.RGB_565;
+        // 设置图片可以被回收,创建Bitmap用于存储Pixel的内存空间在系统内存不足时可以被回收
+        options.inPurgeable = true;
+
+        int len = -1;
+        try {
+            len = inputStream.available();
+        } catch (Exception e) {
+
+        }
+
+        if (len != -1) {
+            if (len < 1024 * 100) {
+                options.inSampleSize = 1;
+            } else if (len < 1024 * 1024) {
+                options.inSampleSize = 2;
+            } else {
+                options.inSampleSize = 4;
+                double times = Math.floor(((double)len) / (1024 * 1024));
+                options.inSampleSize = (int)times * 2;
+            }
+
+            options.inSampleSize = 4;
+            options.inInputShareable  = true;
+            Bitmap bitmap = BitmapFactory.decodeStream(inputStream, null, options);
+            return bitmap;
+        }
+
+
+        return null;
+    }
+
+    /**
+     * 保存图片到SD卡
+     */
+    public static boolean saveBitmap(String fileName, Bitmap mBitmap) {
+        boolean success = false;
+
+        FileOutputStream fOut = null;
+        try {
+            File f = new File(fileName);
+            f.getParentFile().mkdirs();
+            f.createNewFile();
+            fOut = new FileOutputStream(f);
+        } catch (Exception e) {
+            e.printStackTrace();
+        }
+
+        try {
+            mBitmap.compress(Bitmap.CompressFormat.PNG, 100, fOut);
+            fOut.flush();
+            fOut.close();
+            success = true;
+        } catch (Exception e) {
+            success = false;
+            e.printStackTrace();
+        }
+
+        return success;
+    }
+}

+ 35 - 0
app/src/main/java/com/itant/shiwushu/tool/BottomNavigationViewHelper.java

@@ -0,0 +1,35 @@
+package com.itant.shiwushu.tool;
+
+import android.support.design.internal.BottomNavigationItemView;
+import android.support.design.internal.BottomNavigationMenuView;
+import android.support.design.widget.BottomNavigationView;
+import android.util.Log;
+
+import java.lang.reflect.Field;
+
+public class BottomNavigationViewHelper {
+
+    /**
+     * 当Tab个数大于3时,去掉动画
+     * @param view
+     */
+    public static void disableShiftMode(BottomNavigationView view) {
+        BottomNavigationMenuView menuView = (BottomNavigationMenuView) view.getChildAt(0);
+        try {
+            Field shiftingMode = menuView.getClass().getDeclaredField("mShiftingMode");
+            shiftingMode.setAccessible(true);
+            shiftingMode.setBoolean(menuView, false);
+            shiftingMode.setAccessible(false);
+            for (int i = 0; i < menuView.getChildCount(); i++) {
+                BottomNavigationItemView item = (BottomNavigationItemView) menuView.getChildAt(i);
+                item.setShiftingMode(false);
+                // set once again checked value, so view will be updated
+                item.setChecked(item.getItemData().isChecked());
+            }
+        } catch (NoSuchFieldException e) {
+            Log.e("ERROR NO SUCH FIELD", "Unable to get shift mode field");
+        } catch (IllegalAccessException e) {
+            Log.e("ERROR ILLEGAL ALG", "Unable to change value of shift mode");
+        }
+    }
+}

+ 34 - 0
app/src/main/java/com/itant/shiwushu/tool/ColorTool.java

@@ -0,0 +1,34 @@
+package com.itant.shiwushu.tool;
+
+import android.content.Context;
+import android.graphics.Color;
+
+import com.itant.shiwushu.R;
+import com.itant.shiwushu.manager.ThemeManager;
+
+/**
+ * Created by Jason on 2018/10/6.
+ */
+
+public class ColorTool {
+    /**
+     * 获取资源文件的颜色十六进制字符串
+     * @param context
+     * @param id
+     * @return
+     */
+    public static String getColorHexString(Context context, int id) {
+        return context.getResources().getString(id);
+    }
+
+    public static int getColorPrimary(Context context) {
+        int themeMode = ThemeManager.getInstance().getInt(ThemeManager.KEY_THEME_MODE, ThemeManager.THEME_MODE_DEFAULT);
+        if (themeMode == ThemeManager.THEME_MODE_DEFAULT) {
+            return Color.parseColor(getColorHexString(context, R.color.colorPrimary));
+        } else if (themeMode == ThemeManager.THEME_MODE_RED) {
+            return Color.parseColor(getColorHexString(context, R.color.red_2));
+        } else {
+            return Color.parseColor(getColorHexString(context, R.color.blue_alipay));
+        }
+    }
+}

+ 122 - 0
app/src/main/java/com/itant/shiwushu/tool/DialogTool.java

@@ -0,0 +1,122 @@
+package com.itant.shiwushu.tool;
+
+import android.app.Activity;
+import android.content.DialogInterface;
+import android.content.Intent;
+import android.support.v7.app.AlertDialog;
+import android.text.TextUtils;
+import android.view.View;
+import android.widget.EditText;
+
+import com.itant.shiwushu.R;
+import com.itant.shiwushu.manager.SharedPreferenceManager;
+import com.itant.shiwushu.manager.ToastManager;
+import com.itant.shiwushu.net.GlobalPresenter;
+import com.itant.shiwushu.ui.login.LoginActivity;
+
+/**
+ * Created by Jason on 2018/10/5.
+ */
+
+public class DialogTool {
+    /**
+     * 是否退出登录
+     */
+    public static void showExitDialog(final Activity activity) {
+        // 退出当前账号
+        AlertDialog dialog = new AlertDialog.Builder(activity)
+                //.setIcon(R.mipmap.icon)//设置标题的图片
+                //.setTitle("我是对话框")//设置对话框的标题
+                .setMessage("是否清除当前登录信息并退出?")//设置对话框的内容
+                //设置对话框的按钮
+                .setNegativeButton("取消", new DialogInterface.OnClickListener() {
+                    @Override
+                    public void onClick(DialogInterface dialog, int which) {
+                        dialog.dismiss();
+                    }
+                })
+                .setPositiveButton("确定", new DialogInterface.OnClickListener() {
+                    @Override
+                    public void onClick(DialogInterface dialog, int which) {
+                        dialog.dismiss();
+                        SystemTool.exit();
+                        ActivityTool.startActivity(activity, new Intent(activity, LoginActivity.class));
+                        activity.finish();
+                    }
+                }).create();
+        dialog.show();
+    }
+
+    /**
+     * 意见反馈|举报对话框
+     */
+    public static void showFeedbackDialog(Activity activity, final String reporterId, final String reporterName, final String authorId, final String authorName) {
+        View view = activity.getLayoutInflater().inflate(R.layout.layout_feedback, null);
+        final EditText et_feedback = view.findViewById(R.id.et_feedback);
+        if (TextUtils.isEmpty(reporterId)) {
+            // 不是举报的才显示之前的反馈信息
+            String failedFeedback = SharedPreferenceManager.getInstance().getString(SharedPreferenceManager.KEY_FEEDBACK);
+            if (!TextUtils.isEmpty(failedFeedback)) {
+                et_feedback.setText(failedFeedback);
+                et_feedback.setSelection(failedFeedback.length());
+            }
+        } else {
+            et_feedback.setHint("请输入举报的原因");
+        }
+
+        // 退出当前账号
+        final AlertDialog dialog = new AlertDialog.Builder(activity)
+                .setView(view)
+                //设置对话框的按钮
+                .setNegativeButton("取消", new DialogInterface.OnClickListener() {
+                    @Override
+                    public void onClick(DialogInterface dialog, int which) {
+                        dialog.dismiss();
+                    }
+                })
+                .setPositiveButton("确定", null).create();
+        dialog.show();
+        dialog.setCancelable(false);
+        dialog.setCanceledOnTouchOutside(false);
+        dialog.getButton(AlertDialog.BUTTON_POSITIVE).setOnClickListener(new View.OnClickListener() {
+            @Override
+            public void onClick(View v) {
+                String feedback = et_feedback.getText().toString();
+                if (TextUtils.isEmpty(feedback)) {
+                    ToastManager.getInstance().toastShort("请输入内容");
+                } else {
+                    dialog.dismiss();
+                    // 发送反馈
+                    GlobalPresenter.submitFeedback(feedback, reporterId, reporterName, authorId, authorName);
+                }
+            }
+        });
+    }
+
+    /**
+     * 是否开通VIP会员
+     */
+    public static void showVIPDialog(final Activity activity) {
+        AlertDialog dialog = new AlertDialog.Builder(activity)
+                //.setIcon(R.mipmap.icon)//设置标题的图片
+                .setTitle("开通会员")//设置对话框的标题
+                .setMessage("VIP认证会员可以编辑个人资料,提高个人信用度,是否开通?")//设置对话框的内容
+                //设置对话框的按钮
+                .setNegativeButton("取消", new DialogInterface.OnClickListener() {
+                    @Override
+                    public void onClick(DialogInterface dialog, int which) {
+                        dialog.dismiss();
+                    }
+                })
+                .setPositiveButton("确定", new DialogInterface.OnClickListener() {
+                    @Override
+                    public void onClick(DialogInterface dialog, int which) {
+                        dialog.dismiss();
+                        // 支付宝转账1.99元
+                        DonateTool.openAlipayPayPage(activity, "https://qr.alipay.com/fkx05937kvc49ayh1mko112");
+                        GlobalPresenter.becomeVIP();
+                    }
+                }).create();
+        dialog.show();
+    }
+}

+ 58 - 0
app/src/main/java/com/itant/shiwushu/tool/DonateTool.java

@@ -0,0 +1,58 @@
+package com.itant.shiwushu.tool;
+
+import android.app.Activity;
+import android.content.Intent;
+import android.net.Uri;
+
+import com.itant.shiwushu.manager.ToastManager;
+
+import java.net.URLEncoder;
+
+/**
+ * Created by Jason on 2018/9/9.
+ */
+
+public class DonateTool {
+    public static boolean openAlipayPayPage(Activity context, String qrcode) {
+        try {
+            qrcode = URLEncoder.encode(qrcode, "utf-8");
+        } catch (Exception e) {
+            e.printStackTrace();
+        }
+        try {
+            final String alipayqr = "alipayqr://platformapi/startapp?saId=10000007&clientVersion=3.7.0.0718&qrcode=" + qrcode;
+            openUri(context, alipayqr + "%3F_s%3Dweb-other&_t=" + System.currentTimeMillis());
+            return true;
+        } catch (Exception e) {
+            ToastManager.getInstance().toastShort("找不到浏览器");
+        }
+        return false;
+    }
+
+    public static void obtainRedPacket(Activity context, String url) {
+        Intent intent = new Intent();
+        intent.setAction("android.intent.action.VIEW");
+        Uri content_url = Uri.parse(url);
+        intent.setData(content_url);
+        try {
+            ActivityTool.startActivity(context, intent);
+        } catch (Exception e) {
+            ToastManager.getInstance().toastShort("找不到浏览器");
+        }
+    }
+
+    /**
+     * 发送一个intent
+     *
+     * @param context
+     * @param s
+     */
+    private static void openUri(Activity context, String s) {
+        Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(s));
+        try {
+            ActivityTool.startActivity(context, intent);
+        } catch (Exception e) {
+            ToastManager.getInstance().toastShort("找不到浏览器");
+        }
+    }
+}

+ 25 - 0
app/src/main/java/com/itant/shiwushu/tool/GlideTool.java

@@ -0,0 +1,25 @@
+package com.itant.shiwushu.tool;
+
+import android.app.Activity;
+import android.widget.ImageView;
+
+import com.bumptech.glide.Glide;
+import com.bumptech.glide.load.engine.DiskCacheStrategy;
+import com.bumptech.glide.request.RequestOptions;
+import com.itant.shiwushu.R;
+import com.itant.shiwushu.widget.GlideCircleTransform;
+
+/**
+ * Created by Jason on 2018/10/3.
+ */
+
+public class GlideTool {
+    public static void loadStrokeRound(Activity activity, int resourceId, float borderWidth, ImageView targetView) {
+        Glide.with(activity).load(resourceId)
+                .apply(new RequestOptions().diskCacheStrategy(DiskCacheStrategy.RESOURCE)
+                        .placeholder(R.mipmap.logo)
+                        .centerCrop()
+                        .transform(new GlideCircleTransform(activity, borderWidth, activity.getResources().getColor(R.color.white))))
+                .into(targetView);
+    }
+}

+ 547 - 0
app/src/main/java/com/itant/shiwushu/tool/ImageTools.java

@@ -0,0 +1,547 @@
+package com.itant.shiwushu.tool;
+
+import android.annotation.TargetApi;
+import android.content.ContentUris;
+import android.content.Context;
+import android.database.Cursor;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.net.Uri;
+import android.os.Build;
+import android.os.Environment;
+import android.provider.DocumentsContract;
+import android.provider.MediaStore;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.lang.ref.WeakReference;
+
+/**
+ * Tools for handler picture
+ *
+ * @author Ryan.Tang
+ *
+ */
+public final class ImageTools {
+
+    /**
+     * Save image to the SD card 
+     * @param photoBitmap
+     * @param photoName
+     * @param path
+     */
+    public static void savePhotoToSDCard(Bitmap photoBitmap,String path,String photoName){
+        if (checkSDCardAvailable()) {
+            File photoFile = new File(path , photoName);
+            FileOutputStream fileOutputStream = null;
+            try {
+                fileOutputStream = new FileOutputStream(photoFile);
+                if (photoBitmap != null) {
+                    if (photoBitmap.compress(Bitmap.CompressFormat.PNG, 100, fileOutputStream)) {
+                        fileOutputStream.flush();
+                    }
+                }
+            } catch (Exception e) {
+                photoFile.delete();
+                e.printStackTrace();
+            }finally{
+                try {
+                    fileOutputStream.close();
+                } catch (Exception e) {
+                    e.printStackTrace();
+                }
+            }
+        }
+    }
+
+    public static void savePhotoToSDCard(Bitmap photoBitmap,String path){
+        if (checkSDCardAvailable()) {
+            File photoFile = new File(path);
+            FileOutputStream fileOutputStream = null;
+            try {
+                fileOutputStream = new FileOutputStream(photoFile);
+                if (photoBitmap != null) {
+                    if (photoBitmap.compress(Bitmap.CompressFormat.PNG, 100, fileOutputStream)) {
+                        fileOutputStream.flush();
+                    }
+                }
+            } catch (Exception e) {
+                photoFile.delete();
+                e.printStackTrace();
+            } finally{
+                try {
+                    fileOutputStream.close();
+                } catch (Exception e) {
+                    e.printStackTrace();
+                }
+            }
+        }
+    }
+
+    /**
+     * Check the SD card 
+     * @return
+     */
+    public static boolean checkSDCardAvailable(){
+        return Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED);
+    }
+
+    /**
+     * 根据路径加载bitmap
+     *
+     * @param path
+     *            路径
+     * @param w
+     *            款
+     * @param h
+     *            长
+     * @return
+     */
+    public static final Bitmap convertToBitmap(String path, int w, int h) {
+        try {
+            BitmapFactory.Options opts = new BitmapFactory.Options();
+            // 设置为ture只获取图片大小
+            opts.inJustDecodeBounds = true;
+            opts.inPreferredConfig = Bitmap.Config.ARGB_8888;
+            // 返回为空
+            BitmapFactory.decodeFile(path, opts);
+            int width = opts.outWidth;
+            int height = opts.outHeight;
+            float scaleWidth = 0.f, scaleHeight = 0.f;
+            if (width > w || height > h) {
+                // 缩放
+                scaleWidth = ((float) width) / w;
+                scaleHeight = ((float) height) / h;
+            }
+            opts.inJustDecodeBounds = false;
+            float scale = Math.max(scaleWidth, scaleHeight);
+            opts.inSampleSize = (int) scale;
+            WeakReference<Bitmap> weak = new WeakReference<Bitmap>(BitmapFactory.decodeFile(path, opts));
+            Bitmap bMapRotate = Bitmap.createBitmap(weak.get(), 0, 0, weak.get().getWidth(), weak.get().getHeight(), null, true);
+            if (bMapRotate != null) {
+                return bMapRotate;
+            }
+            return null;
+        } catch (Exception e) {
+            e.printStackTrace();
+            return null;
+        }
+    }
+
+
+    @TargetApi(Build.VERSION_CODES.KITKAT)
+    public static String getPath(final Context context, final Uri uri) {
+        final boolean isKitKat = Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT;
+        // DocumentProvider
+        if (isKitKat && DocumentsContract.isDocumentUri(context, uri)) {
+            // ExternalStorageProvider
+            if (isExternalStorageDocument(uri)) {
+                final String docId = DocumentsContract.getDocumentId(uri);
+                final String[] split = docId.split(":");
+                final String type = split[0];
+                if ("primary".equalsIgnoreCase(type)) {
+                    return Environment.getExternalStorageDirectory() + "/"
+                            + split[1];
+                }
+            }
+            // DownloadsProvider
+            else if (isDownloadsDocument(uri)) {
+                final String id = DocumentsContract.getDocumentId(uri);
+                final Uri contentUri = ContentUris.withAppendedId(
+                        Uri.parse("content://downloads/public_downloads"),
+                        Long.valueOf(id));
+                return getDataColumn(context, contentUri, null, null);
+            }
+            // MediaProvider
+            else if (isMediaDocument(uri)) {
+                final String docId = DocumentsContract.getDocumentId(uri);
+                final String[] split = docId.split(":");
+                final String type = split[0];
+                Uri contentUri = null;
+                if ("image".equals(type)) {
+                    contentUri = MediaStore.Images.Media.EXTERNAL_CONTENT_URI;
+                } else if ("video".equals(type)) {
+                    contentUri = MediaStore.Video.Media.EXTERNAL_CONTENT_URI;
+                } else if ("audio".equals(type)) {
+                    contentUri = MediaStore.Audio.Media.EXTERNAL_CONTENT_URI;
+                }
+                final String selection = MediaStore.MediaColumns._ID + "=?";
+                final String[] selectionArgs = new String[] { split[1] };
+                return getDataColumn(context, contentUri, selection,
+                        selectionArgs);
+            }
+        }
+        // MediaStore (and general)
+        else if ("content".equalsIgnoreCase(uri.getScheme())) {
+            // Return the remote address
+            if (isGooglePhotosUri(uri))
+                return uri.getLastPathSegment();
+            return getDataColumn(context, uri, null, null);
+        }
+        // File
+        else if ("file".equalsIgnoreCase(uri.getScheme())) {
+            return uri.getPath();
+        }
+        return null;
+    }
+
+    /**
+     * Get the value of the data column for this Uri . This is useful for
+     * MediaStore Uris , and other file - based ContentProviders.
+     *
+     * @param context
+     *            The context.
+     * @param uri
+     *            The Uri to query.
+     * @param selection
+     *            (Optional) Filter used in the query.
+     * @param selectionArgs
+     *            (Optional) Selection arguments used in the query.
+     * @return The value of the _data column, which is typically a file path.
+     */
+    public static String getDataColumn(Context context, Uri uri,
+                                       String selection, String[] selectionArgs) {
+        Cursor cursor = null;
+        final String column = MediaStore.MediaColumns.DATA;
+        final String[] projection = { column };
+        try {
+            cursor = context.getContentResolver().query(uri, projection,
+                    selection, selectionArgs, null);
+            if (cursor != null && cursor.moveToFirst()) {
+                final int index = cursor.getColumnIndexOrThrow(column);
+                return cursor.getString(index);
+            }
+        } finally {
+            if (cursor != null)
+                cursor.close();
+        }
+        return null;
+    }
+
+    /**
+     * @param uri
+     *            The Uri to check.
+     * @return Whether the Uri authority is ExternalStorageProvider.
+     */
+    public static boolean isExternalStorageDocument(Uri uri) {
+        return "com.android.externalstorage.documents".equals(uri
+                .getAuthority());
+    }
+
+    /**
+     * @param uri
+     *            The Uri to check.
+     * @return Whether the Uri authority is DownloadsProvider.
+     */
+    public static boolean isDownloadsDocument(Uri uri) {
+        return "com.android.providers.downloads.documents".equals(uri
+                .getAuthority());
+    }
+
+    /**
+     * @param uri
+     *            The Uri to check.
+     * @return Whether the Uri authority is MediaProvider.
+     */
+    public static boolean isMediaDocument(Uri uri) {
+        return "com.android.providers.media.documents".equals(uri
+                .getAuthority());
+    }
+
+    /**
+     * @param uri
+     *            The Uri to check.
+     * @return Whether the Uri authority is Google Photos.
+     */
+    public static boolean isGooglePhotosUri(Uri uri) {
+        return "com.google.android.apps.photos.content".equals(uri
+                .getAuthority());
+    }
+
+    /**
+     * @param url
+     * @return
+     */
+    public static Bitmap getLoacalBitmap(String url) {
+        try {
+            FileInputStream fis = new FileInputStream(url);
+            return BitmapFactory.decodeStream(fis);
+        } catch (FileNotFoundException e) {
+            e.printStackTrace();
+            return null;
+        }
+    }
+
+
+    //==============================压缩图片开始===========================
+    /**
+     * Get bitmap from specified image path
+     *
+     * @param imgPath
+     * @return
+     */
+    public static Bitmap getBitmap(String imgPath) {
+        // Get bitmap through image path
+        BitmapFactory.Options newOpts = new BitmapFactory.Options();
+        newOpts.inJustDecodeBounds = false;
+        newOpts.inPurgeable = true;
+        newOpts.inInputShareable = true;
+        // Do not compress
+        newOpts.inSampleSize = 1;
+        newOpts.inPreferredConfig = Bitmap.Config.RGB_565;
+        return BitmapFactory.decodeFile(imgPath, newOpts);
+    }
+
+    /**
+     * Store bitmap into specified image path
+     *
+     * @param bitmap
+     * @param outPath
+     * @throws FileNotFoundException
+     */
+    public static void storeImage(Bitmap bitmap, String outPath) throws FileNotFoundException {
+        FileOutputStream os = new FileOutputStream(outPath);
+        bitmap.compress(Bitmap.CompressFormat.JPEG, 100, os);
+    }
+
+    /**
+     * Compress image by pixel, this will modify image width/height.
+     * Used to get thumbnail
+     *
+     * @param imgPath image path
+     * @param pixelW target pixel of width
+     * @param pixelH target pixel of height
+     * @return
+     */
+    public Bitmap ratio(String imgPath, float pixelW, float pixelH) {
+        BitmapFactory.Options newOpts = new BitmapFactory.Options();
+        // 开始读入图片,此时把options.inJustDecodeBounds 设回true,即只读边不读内容
+        newOpts.inJustDecodeBounds = true;
+        newOpts.inPreferredConfig = Bitmap.Config.RGB_565;
+        // Get bitmap info, but notice that bitmap is null now
+        Bitmap bitmap = BitmapFactory.decodeFile(imgPath,newOpts);
+
+        newOpts.inJustDecodeBounds = false;
+        int w = newOpts.outWidth;
+        int h = newOpts.outHeight;
+        // 想要缩放的目标尺寸
+        float hh = pixelH;// 设置高度为240f时,可以明显看到图片缩小了
+        float ww = pixelW;// 设置宽度为120f,可以明显看到图片缩小了
+        // 缩放比。由于是固定比例缩放,只用高或者宽其中一个数据进行计算即可
+        int be = 1;//be=1表示不缩放
+        if (w > h && w > ww) {//如果宽度大的话根据宽度固定大小缩放
+            be = (int) (newOpts.outWidth / ww);
+        } else if (w < h && h > hh) {//如果高度高的话根据宽度固定大小缩放
+            be = (int) (newOpts.outHeight / hh);
+        }
+        if (be <= 0) be = 1;
+        newOpts.inSampleSize = be;//设置缩放比例
+        // 开始压缩图片,注意此时已经把options.inJustDecodeBounds 设回false了
+        bitmap = BitmapFactory.decodeFile(imgPath, newOpts);
+        // 压缩好比例大小后再进行质量压缩
+//        return compress(bitmap, maxSize); // 这里再进行质量压缩的意义不大,反而耗资源,删除
+        return bitmap;
+    }
+
+    /**
+     * Compress image by size, this will modify image width/height.
+     * Used to get thumbnail
+     *
+     * @param image
+     * @param pixelW target pixel of width
+     * @param pixelH target pixel of height
+     * @return
+     */
+    public static Bitmap ratio(Bitmap image, float pixelW, float pixelH) {
+        ByteArrayOutputStream os = new ByteArrayOutputStream();
+        image.compress(Bitmap.CompressFormat.JPEG, 100, os);
+        if( os.toByteArray().length / 1024>1024) {//判断如果图片大于1M,进行压缩避免在生成图片(BitmapFactory.decodeStream)时溢出
+            os.reset();//重置baos即清空baos
+            image.compress(Bitmap.CompressFormat.JPEG, 50, os);//这里压缩50%,把压缩后的数据存放到baos中
+        }
+        ByteArrayInputStream is = new ByteArrayInputStream(os.toByteArray());
+        BitmapFactory.Options newOpts = new BitmapFactory.Options();
+        //开始读入图片,此时把options.inJustDecodeBounds 设回true了
+        newOpts.inJustDecodeBounds = true;
+        newOpts.inPreferredConfig = Bitmap.Config.RGB_565;
+        Bitmap bitmap = BitmapFactory.decodeStream(is, null, newOpts);
+        newOpts.inJustDecodeBounds = false;
+        int w = newOpts.outWidth;
+        int h = newOpts.outHeight;
+        float hh = pixelH;// 设置高度为240f时,可以明显看到图片缩小了
+        float ww = pixelW;// 设置宽度为120f,可以明显看到图片缩小了
+        //缩放比。由于是固定比例缩放,只用高或者宽其中一个数据进行计算即可
+        int be = 1;//be=1表示不缩放
+        if (w > h && w > ww) {//如果宽度大的话根据宽度固定大小缩放
+            be = (int) (newOpts.outWidth / ww);
+        } else if (w < h && h > hh) {//如果高度高的话根据宽度固定大小缩放
+            be = (int) (newOpts.outHeight / hh);
+        }
+        if (be <= 0) be = 1;
+        newOpts.inSampleSize = be;//设置缩放比例
+        //重新读入图片,注意此时已经把options.inJustDecodeBounds 设回false了
+        is = new ByteArrayInputStream(os.toByteArray());
+        bitmap = BitmapFactory.decodeStream(is, null, newOpts);
+        //压缩好比例大小后再进行质量压缩
+//      return compress(bitmap, maxSize); // 这里再进行质量压缩的意义不大,反而耗资源,删除
+        return bitmap;
+    }
+
+    /**
+     * Compress by quality,  and generate image to the path specified
+     *
+     * @param image
+     * @param outPath
+     * @param maxSize target will be compressed to be smaller than this size.(kb)
+     * @throws IOException
+     */
+    public static void compressAndGenImage(Bitmap image, String outPath, int maxSize) throws IOException {
+        ByteArrayOutputStream os = new ByteArrayOutputStream();
+        // scale
+        int options = 100;
+        // Store the bitmap into output stream(no compress)
+        image.compress(Bitmap.CompressFormat.PNG, options, os);
+        // Compress by loop
+        while ( os.toByteArray().length / 1024 > maxSize) {
+            // Clean up os
+            os.reset();
+            // interval 10
+            options -= 10;
+            image.compress(Bitmap.CompressFormat.PNG, options, os);
+        }
+
+        // Generate compressed image file
+        FileOutputStream fos = new FileOutputStream(outPath);
+        fos.write(os.toByteArray());
+        fos.flush();
+        fos.close();
+    }
+
+    /**
+     * Compress by quality,  and generate image to the path specified
+     *
+     * @param imgPath
+     * @param outPath
+     * @param maxSize target will be compressed to be smaller than this size.(kb)
+     * @param needsDelete Whether delete original file after compress
+     * @throws IOException
+     */
+    public void compressAndGenImage(String imgPath, String outPath, int maxSize, boolean needsDelete) throws IOException {
+        compressAndGenImage(getBitmap(imgPath), outPath, maxSize);
+
+        // Delete original file
+        if (needsDelete) {
+            File file = new File (imgPath);
+            if (file.exists()) {
+                file.delete();
+            }
+        }
+    }
+
+    /**
+     * Ratio and generate thumb to the path specified
+     *
+     * @param image
+     * @param outPath
+     * @param pixelW target pixel of width
+     * @param pixelH target pixel of height
+     * @throws FileNotFoundException
+     */
+    public static void ratioAndGenThumb(Bitmap image, String outPath, float pixelW, float pixelH) throws FileNotFoundException {
+        Bitmap bitmap = ratio(image, pixelW, pixelH);
+        storeImage( bitmap, outPath);
+    }
+
+    /**
+     * Ratio and generate thumb to the path specified
+     *
+     * @param outPath
+     * @param pixelW target pixel of width
+     * @param pixelH target pixel of height
+     * @param needsDelete Whether delete original file after compress
+     * @throws FileNotFoundException
+     */
+    public void ratioAndGenThumb(String imgPath, String outPath, float pixelW, float pixelH, boolean needsDelete) throws FileNotFoundException {
+        Bitmap bitmap = ratio(imgPath, pixelW, pixelH);
+        storeImage( bitmap, outPath);
+
+        // Delete original file
+        if (needsDelete) {
+            File file = new File (imgPath);
+            if (file.exists()) {
+                file.delete();
+            }
+        }
+    }
+
+    public static void compressBmpToFile(Bitmap bmp,File file){
+        ByteArrayOutputStream baos = new ByteArrayOutputStream();
+        int options = 80;//个人喜欢从80开始,
+        bmp.compress(Bitmap.CompressFormat.JPEG, options, baos);
+        while (baos.toByteArray().length / 1024 > 100) {
+            baos.reset();
+            options -= 10;
+            bmp.compress(Bitmap.CompressFormat.JPEG, options, baos);
+        }
+        try {
+            FileOutputStream fos = new FileOutputStream(file);
+            fos.write(baos.toByteArray());
+            fos.flush();
+            fos.close();
+        } catch (Exception e) {
+            e.printStackTrace();
+        }
+    }
+
+    private Bitmap compressImageFromFile(String srcPath) {
+        BitmapFactory.Options newOpts = new BitmapFactory.Options();
+        newOpts.inJustDecodeBounds = true;//只读边,不读内容
+        Bitmap bitmap = BitmapFactory.decodeFile(srcPath, newOpts);
+
+        newOpts.inJustDecodeBounds = false;
+        int w = newOpts.outWidth;
+        int h = newOpts.outHeight;
+        float hh = 800f;//
+        float ww = 480f;//
+        int be = 1;
+        if (w > h && w > ww) {
+            be = (int) (newOpts.outWidth / ww);
+        } else if (w < h && h > hh) {
+            be = (int) (newOpts.outHeight / hh);
+        }
+        if (be <= 0)
+            be = 1;
+        newOpts.inSampleSize = be;//设置采样率
+
+        newOpts.inPreferredConfig = Bitmap.Config.ARGB_8888;//该模式是默认的,可不设
+        newOpts.inPurgeable = true;// 同时设置才会有效
+        newOpts.inInputShareable = true;//。当系统内存不够时候图片自动被回收
+
+        bitmap = BitmapFactory.decodeFile(srcPath, newOpts);
+//      return compressBmpFromBmp(bitmap);//原来的方法调用了这个方法企图进行二次压缩
+        //其实是无效的,大家尽管尝试
+        return bitmap;
+    }
+
+    private Bitmap compressBmpFromBmp(Bitmap image) {
+        ByteArrayOutputStream baos = new ByteArrayOutputStream();
+        int options = 100;
+        image.compress(Bitmap.CompressFormat.JPEG, 100, baos);
+        while (baos.toByteArray().length / 1024 > 100) {
+            baos.reset();
+            options -= 10;
+            image.compress(Bitmap.CompressFormat.JPEG, options, baos);
+        }
+        ByteArrayInputStream isBm = new ByteArrayInputStream(baos.toByteArray());
+        Bitmap bitmap = BitmapFactory.decodeStream(isBm, null, null);
+        return bitmap;
+    }
+
+    //=====================================图片压缩结束======================
+
+}

+ 77 - 0
app/src/main/java/com/itant/shiwushu/tool/PermissionTool.java

@@ -0,0 +1,77 @@
+package com.itant.shiwushu.tool;
+
+import android.app.Activity;
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.PackageManager;
+import android.content.pm.ResolveInfo;
+import android.net.Uri;
+import android.os.Build;
+import android.support.annotation.NonNull;
+import android.support.v4.app.ActivityCompat;
+
+import com.itant.shiwushu.base.IPermission;
+
+import java.util.List;
+
+/**
+ * Created by iTant on 2017/4/9.
+ */
+
+public class PermissionTool {
+    /**
+     * 初始化权限
+     */
+    public static void initPermission(IPermission iPermission, Activity activity, String[] permissions, int requestCode) {
+        boolean isGranted = true;
+        for (String permission : permissions) {
+            int result = ActivityCompat.checkSelfPermission(activity, permission);
+            if (result != PackageManager.PERMISSION_GRANTED) {
+                isGranted = false;
+                break;
+            }
+        }
+
+        if (!isGranted) {
+            // 还没有的话,去申请权限
+            ActivityCompat.requestPermissions(activity, permissions, requestCode);
+        } else {
+            iPermission.onPermissionSuccess(requestCode);
+        }
+    }
+
+    public static void onActivityPermissionResult(IPermission permission, int requestCode, @NonNull int[] grantResults) {
+        boolean granted = true;
+        for (int result : grantResults) {
+            granted = result == PackageManager.PERMISSION_GRANTED;
+            if (!granted) {
+                break;
+            }
+        }
+
+        if (granted) {
+            permission.onPermissionSuccess(requestCode);
+        } else {
+            permission.onPermissionFail(requestCode);
+        }
+    }
+
+    /**
+     * 赋予读写URI对应文件的权限
+     * @param intent
+     * @param uri
+     */
+    public static void grantUriPermission(Context context, Intent intent, Uri uri) {
+        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
+            //in pre-N devices, manually grant uri permission.
+            List<ResolveInfo> resInfoList = context.getPackageManager().queryIntentActivities(intent, PackageManager.MATCH_DEFAULT_ONLY);
+            for (ResolveInfo resolveInfo : resInfoList) {
+                String packageName = resolveInfo.activityInfo.packageName;
+                context.grantUriPermission(packageName, uri, Intent.FLAG_GRANT_WRITE_URI_PERMISSION | Intent.FLAG_GRANT_READ_URI_PERMISSION);
+            }
+        } else {
+            intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
+            intent.addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
+        }
+    }
+}

+ 0 - 0
app/src/main/java/com/itant/shiwushu/tool/PhotoTool.java


Algunos archivos no se mostraron porque demasiados archivos cambiaron en este cambio