Bladeren bron

初始化

詹子聪 5 jaren geleden
commit
2428ffc1d3
70 gewijzigde bestanden met toevoegingen van 3431 en 0 verwijderingen
  1. 14 0
      .gitignore
  2. 19 0
      NewMvp.iml
  3. 1 0
      app/.gitignore
  4. 30 0
      app/build.gradle
  5. 21 0
      app/proguard-rules.pro
  6. 27 0
      app/src/androidTest/java/com/miekir/newmvp/ExampleInstrumentedTest.java
  7. 21 0
      app/src/main/AndroidManifest.xml
  8. 14 0
      app/src/main/java/com/miekir/newmvp/MainActivity.java
  9. 34 0
      app/src/main/res/drawable-v24/ic_launcher_foreground.xml
  10. 170 0
      app/src/main/res/drawable/ic_launcher_background.xml
  11. 18 0
      app/src/main/res/layout/activity_main.xml
  12. 5 0
      app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
  13. 5 0
      app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
  14. BIN
      app/src/main/res/mipmap-hdpi/ic_launcher.png
  15. BIN
      app/src/main/res/mipmap-hdpi/ic_launcher_round.png
  16. BIN
      app/src/main/res/mipmap-mdpi/ic_launcher.png
  17. BIN
      app/src/main/res/mipmap-mdpi/ic_launcher_round.png
  18. BIN
      app/src/main/res/mipmap-xhdpi/ic_launcher.png
  19. BIN
      app/src/main/res/mipmap-xhdpi/ic_launcher_round.png
  20. BIN
      app/src/main/res/mipmap-xxhdpi/ic_launcher.png
  21. BIN
      app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png
  22. BIN
      app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
  23. BIN
      app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png
  24. 6 0
      app/src/main/res/values/colors.xml
  25. 3 0
      app/src/main/res/values/strings.xml
  26. 11 0
      app/src/main/res/values/styles.xml
  27. 17 0
      app/src/test/java/com/miekir/newmvp/ExampleUnitTest.java
  28. 37 0
      build.gradle
  29. 67 0
      common/build.gradle
  30. 21 0
      common/proguard-rules.pro
  31. 27 0
      common/src/androidTest/java/com/miekir/common/ExampleInstrumentedTest.java
  32. 10 0
      common/src/main/AndroidManifest.xml
  33. 31 0
      common/src/main/java/com/miekir/common/adapter/TabFragmentAdapter.java
  34. 59 0
      common/src/main/java/com/miekir/common/provider/CommonInstaller.java
  35. 30 0
      common/src/main/java/com/miekir/common/utils/ActivityTool.java
  36. 978 0
      common/src/main/java/com/miekir/common/utils/Base64.java
  37. 39 0
      common/src/main/java/com/miekir/common/utils/ContextManager.java
  38. 16 0
      common/src/main/java/com/miekir/common/utils/SizeTool.java
  39. 72 0
      common/src/main/java/com/miekir/common/utils/ToastTool.java
  40. 55 0
      common/src/main/java/com/miekir/common/utils/ViewTool.java
  41. 141 0
      common/src/main/java/com/miekir/common/view/AlignTextView.java
  42. 237 0
      common/src/main/java/com/miekir/common/view/RoundedCornersTransformation.java
  43. 45 0
      common/src/main/java/com/miekir/common/widget/ScrollableViewPager.java
  44. 3 0
      common/src/main/res/values/strings.xml
  45. 17 0
      common/src/test/java/com/miekir/common/ExampleUnitTest.java
  46. 20 0
      gradle.properties
  47. BIN
      gradle/wrapper/gradle-wrapper.jar
  48. 6 0
      gradle/wrapper/gradle-wrapper.properties
  49. 172 0
      gradlew
  50. 84 0
      gradlew.bat
  51. 10 0
      local.properties
  52. 37 0
      mvp/build.gradle
  53. 21 0
      mvp/proguard-rules.pro
  54. 27 0
      mvp/src/androidTest/java/com/miekir/mvp/ExampleInstrumentedTest.java
  55. 2 0
      mvp/src/main/AndroidManifest.xml
  56. 133 0
      mvp/src/main/java/com/miekir/mvp/base/BaseActivity.java
  57. 107 0
      mvp/src/main/java/com/miekir/mvp/base/BaseFragment.java
  58. 24 0
      mvp/src/main/java/com/miekir/mvp/presenter/BasePresenter.java
  59. 15 0
      mvp/src/main/java/com/miekir/mvp/presenter/InjectPresenter.java
  60. 70 0
      mvp/src/main/java/com/miekir/mvp/view/BaseMVPActivity.java
  61. 102 0
      mvp/src/main/java/com/miekir/mvp/view/BaseMVPFragment.java
  62. 9 0
      mvp/src/main/java/com/miekir/mvp/view/IView.java
  63. 112 0
      mvp/src/main/java/com/miekir/mvp/widget/LVCircularRing.java
  64. 64 0
      mvp/src/main/java/com/miekir/mvp/widget/LoadingDialog.java
  65. 12 0
      mvp/src/main/res/drawable/shape_dialog_bg.xml
  66. 34 0
      mvp/src/main/res/layout/dialog_loading_view.xml
  67. 3 0
      mvp/src/main/res/values/strings.xml
  68. 47 0
      mvp/src/main/res/values/styles.xml
  69. 17 0
      mvp/src/test/java/com/miekir/mvp/ExampleUnitTest.java
  70. 2 0
      settings.gradle

+ 14 - 0
.gitignore

@@ -0,0 +1,14 @@
+*.iml
+.gradle
+/local.properties
+/.idea/caches
+/.idea/libraries
+/.idea/modules.xml
+/.idea/workspace.xml
+/.idea/navEditor.xml
+/.idea/assetWizardSettings.xml
+.DS_Store
+/build
+/captures
+.externalNativeBuild
+.cxx

+ 19 - 0
NewMvp.iml

@@ -0,0 +1,19 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<module external.linked.project.id="NewMvp" external.linked.project.path="$MODULE_DIR$" external.root.project.path="$MODULE_DIR$" external.system.id="GRADLE" type="JAVA_MODULE" version="4">
+  <component name="FacetManager">
+    <facet type="java-gradle" name="Java-Gradle">
+      <configuration>
+        <option name="BUILD_FOLDER_PATH" value="$MODULE_DIR$/build" />
+        <option name="BUILDABLE" value="false" />
+      </configuration>
+    </facet>
+  </component>
+  <component name="NewModuleRootManager" LANGUAGE_LEVEL="JDK_1_7" inherit-compiler-output="true">
+    <exclude-output />
+    <content url="file://$MODULE_DIR$">
+      <excludeFolder url="file://$MODULE_DIR$/.gradle" />
+    </content>
+    <orderEntry type="inheritedJdk" />
+    <orderEntry type="sourceFolder" forTests="false" />
+  </component>
+</module>

+ 1 - 0
app/.gitignore

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

+ 30 - 0
app/build.gradle

@@ -0,0 +1,30 @@
+apply plugin: 'com.android.application'
+
+android {
+    compileSdkVersion 30
+    buildToolsVersion "29.0.3"
+    defaultConfig {
+        applicationId "com.miekir.newmvp"
+        minSdkVersion 21
+        targetSdkVersion 30
+        versionCode 1
+        versionName "1.0"
+        testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
+    }
+    buildTypes {
+        release {
+            minifyEnabled false
+            proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
+        }
+    }
+}
+
+dependencies {
+    implementation fileTree(dir: 'libs', include: ['*.jar'])
+    implementation 'androidx.appcompat:appcompat:1.0.2'
+    implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
+    testImplementation 'junit:junit:4.12'
+    androidTestImplementation 'androidx.test.ext:junit:1.1.0'
+    androidTestImplementation 'androidx.test.espresso:espresso-core:3.1.1'
+    implementation project(path: ':mvp')
+}

+ 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

+ 27 - 0
app/src/androidTest/java/com/miekir/newmvp/ExampleInstrumentedTest.java

@@ -0,0 +1,27 @@
+package com.miekir.newmvp;
+
+import android.content.Context;
+
+import androidx.test.platform.app.InstrumentationRegistry;
+import androidx.test.ext.junit.runners.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() {
+        // Context of the app under test.
+        Context appContext = InstrumentationRegistry.getInstrumentation().getTargetContext();
+
+        assertEquals("com.miekir.newmvp", appContext.getPackageName());
+    }
+}

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

@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?>
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+    package="com.miekir.newmvp">
+
+    <application
+        android:allowBackup="true"
+        android:icon="@mipmap/ic_launcher"
+        android:label="@string/app_name"
+        android:roundIcon="@mipmap/ic_launcher_round"
+        android:supportsRtl="true"
+        android:theme="@style/AppTheme">
+        <activity android:name=".MainActivity">
+            <intent-filter>
+                <action android:name="android.intent.action.MAIN" />
+
+                <category android:name="android.intent.category.LAUNCHER" />
+            </intent-filter>
+        </activity>
+    </application>
+
+</manifest>

+ 14 - 0
app/src/main/java/com/miekir/newmvp/MainActivity.java

@@ -0,0 +1,14 @@
+package com.miekir.newmvp;
+
+import androidx.appcompat.app.AppCompatActivity;
+
+import android.os.Bundle;
+
+public class MainActivity extends AppCompatActivity {
+
+    @Override
+    protected void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+        setContentView(R.layout.activity_main);
+    }
+}

File diff suppressed because it is too large
+ 34 - 0
app/src/main/res/drawable-v24/ic_launcher_foreground.xml


+ 170 - 0
app/src/main/res/drawable/ic_launcher_background.xml

@@ -0,0 +1,170 @@
+<?xml version="1.0" encoding="utf-8"?>
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="108dp"
+    android:height="108dp"
+    android:viewportWidth="108"
+    android:viewportHeight="108">
+    <path
+        android:fillColor="#008577"
+        android:pathData="M0,0h108v108h-108z" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M9,0L9,108"
+        android:strokeWidth="0.8"
+        android:strokeColor="#33FFFFFF" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M19,0L19,108"
+        android:strokeWidth="0.8"
+        android:strokeColor="#33FFFFFF" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M29,0L29,108"
+        android:strokeWidth="0.8"
+        android:strokeColor="#33FFFFFF" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M39,0L39,108"
+        android:strokeWidth="0.8"
+        android:strokeColor="#33FFFFFF" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M49,0L49,108"
+        android:strokeWidth="0.8"
+        android:strokeColor="#33FFFFFF" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M59,0L59,108"
+        android:strokeWidth="0.8"
+        android:strokeColor="#33FFFFFF" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M69,0L69,108"
+        android:strokeWidth="0.8"
+        android:strokeColor="#33FFFFFF" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M79,0L79,108"
+        android:strokeWidth="0.8"
+        android:strokeColor="#33FFFFFF" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M89,0L89,108"
+        android:strokeWidth="0.8"
+        android:strokeColor="#33FFFFFF" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M99,0L99,108"
+        android:strokeWidth="0.8"
+        android:strokeColor="#33FFFFFF" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M0,9L108,9"
+        android:strokeWidth="0.8"
+        android:strokeColor="#33FFFFFF" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M0,19L108,19"
+        android:strokeWidth="0.8"
+        android:strokeColor="#33FFFFFF" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M0,29L108,29"
+        android:strokeWidth="0.8"
+        android:strokeColor="#33FFFFFF" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M0,39L108,39"
+        android:strokeWidth="0.8"
+        android:strokeColor="#33FFFFFF" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M0,49L108,49"
+        android:strokeWidth="0.8"
+        android:strokeColor="#33FFFFFF" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M0,59L108,59"
+        android:strokeWidth="0.8"
+        android:strokeColor="#33FFFFFF" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M0,69L108,69"
+        android:strokeWidth="0.8"
+        android:strokeColor="#33FFFFFF" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M0,79L108,79"
+        android:strokeWidth="0.8"
+        android:strokeColor="#33FFFFFF" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M0,89L108,89"
+        android:strokeWidth="0.8"
+        android:strokeColor="#33FFFFFF" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M0,99L108,99"
+        android:strokeWidth="0.8"
+        android:strokeColor="#33FFFFFF" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M19,29L89,29"
+        android:strokeWidth="0.8"
+        android:strokeColor="#33FFFFFF" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M19,39L89,39"
+        android:strokeWidth="0.8"
+        android:strokeColor="#33FFFFFF" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M19,49L89,49"
+        android:strokeWidth="0.8"
+        android:strokeColor="#33FFFFFF" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M19,59L89,59"
+        android:strokeWidth="0.8"
+        android:strokeColor="#33FFFFFF" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M19,69L89,69"
+        android:strokeWidth="0.8"
+        android:strokeColor="#33FFFFFF" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M19,79L89,79"
+        android:strokeWidth="0.8"
+        android:strokeColor="#33FFFFFF" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M29,19L29,89"
+        android:strokeWidth="0.8"
+        android:strokeColor="#33FFFFFF" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M39,19L39,89"
+        android:strokeWidth="0.8"
+        android:strokeColor="#33FFFFFF" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M49,19L49,89"
+        android:strokeWidth="0.8"
+        android:strokeColor="#33FFFFFF" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M59,19L59,89"
+        android:strokeWidth="0.8"
+        android:strokeColor="#33FFFFFF" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M69,19L69,89"
+        android:strokeWidth="0.8"
+        android:strokeColor="#33FFFFFF" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M79,19L79,89"
+        android:strokeWidth="0.8"
+        android:strokeColor="#33FFFFFF" />
+</vector>

+ 18 - 0
app/src/main/res/layout/activity_main.xml

@@ -0,0 +1,18 @@
+<?xml version="1.0" encoding="utf-8"?>
+<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
+    xmlns:tools="http://schemas.android.com/tools"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    tools:context=".MainActivity">
+
+    <TextView
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:text="Hello World!"
+        app:layout_constraintBottom_toBottomOf="parent"
+        app:layout_constraintLeft_toLeftOf="parent"
+        app:layout_constraintRight_toRightOf="parent"
+        app:layout_constraintTop_toTopOf="parent" />
+
+</androidx.constraintlayout.widget.ConstraintLayout>

+ 5 - 0
app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml

@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
+    <background android:drawable="@drawable/ic_launcher_background" />
+    <foreground android:drawable="@drawable/ic_launcher_foreground" />
+</adaptive-icon>

+ 5 - 0
app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml

@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
+    <background android:drawable="@drawable/ic_launcher_background" />
+    <foreground android:drawable="@drawable/ic_launcher_foreground" />
+</adaptive-icon>

BIN
app/src/main/res/mipmap-hdpi/ic_launcher.png


BIN
app/src/main/res/mipmap-hdpi/ic_launcher_round.png


BIN
app/src/main/res/mipmap-mdpi/ic_launcher.png


BIN
app/src/main/res/mipmap-mdpi/ic_launcher_round.png


BIN
app/src/main/res/mipmap-xhdpi/ic_launcher.png


BIN
app/src/main/res/mipmap-xhdpi/ic_launcher_round.png


BIN
app/src/main/res/mipmap-xxhdpi/ic_launcher.png


BIN
app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png


BIN
app/src/main/res/mipmap-xxxhdpi/ic_launcher.png


BIN
app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png


+ 6 - 0
app/src/main/res/values/colors.xml

@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+    <color name="colorPrimary">#008577</color>
+    <color name="colorPrimaryDark">#00574B</color>
+    <color name="colorAccent">#D81B60</color>
+</resources>

+ 3 - 0
app/src/main/res/values/strings.xml

@@ -0,0 +1,3 @@
+<resources>
+    <string name="app_name">NewMvp</string>
+</resources>

+ 11 - 0
app/src/main/res/values/styles.xml

@@ -0,0 +1,11 @@
+<resources>
+
+    <!-- Base application theme. -->
+    <style name="AppTheme" parent="Theme.AppCompat.Light.DarkActionBar">
+        <!-- Customize your theme here. -->
+        <item name="colorPrimary">@color/colorPrimary</item>
+        <item name="colorPrimaryDark">@color/colorPrimaryDark</item>
+        <item name="colorAccent">@color/colorAccent</item>
+    </style>
+
+</resources>

+ 17 - 0
app/src/test/java/com/miekir/newmvp/ExampleUnitTest.java

@@ -0,0 +1,17 @@
+package com.miekir.newmvp;
+
+import org.junit.Test;
+
+import static org.junit.Assert.*;
+
+/**
+ * Example local unit test, which will execute on the development machine (host).
+ *
+ * @see <a href="http://d.android.com/tools/testing">Testing documentation</a>
+ */
+public class ExampleUnitTest {
+    @Test
+    public void addition_isCorrect() {
+        assertEquals(4, 2 + 2);
+    }
+}

+ 37 - 0
build.gradle

@@ -0,0 +1,37 @@
+// Top-level build file where you can add configuration options common to all sub-projects/modules.
+
+buildscript {
+    repositories {
+        google()
+        jcenter()
+        
+    }
+    dependencies {
+        classpath 'com.android.tools.build:gradle:3.5.2'
+        
+        // NOTE: Do not place your application dependencies here; they belong
+        // in the individual module build.gradle files
+    }
+}
+
+allprojects {
+    repositories {
+        google()
+        jcenter()
+        
+    }
+}
+
+task clean(type: Delete) {
+    delete rootProject.buildDir
+}
+
+
+// 统一编译版本
+ext.versions = [
+        // Project
+        minSdk              : 21,
+        compileSdk          : 29,
+        targetSdk           : 29,
+        buildTools          : '29.0.2',
+]

+ 67 - 0
common/build.gradle

@@ -0,0 +1,67 @@
+apply plugin: 'com.android.library'
+
+android {
+    compileSdkVersion versions.compileSdk
+    buildToolsVersion versions.buildTools
+
+    defaultConfig {
+        minSdkVersion versions.minSdk
+        targetSdkVersion versions.targetSdk
+        versionCode 1
+        versionName "1.0"
+
+        testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
+        consumerProguardFiles 'consumer-rules.pro'
+    }
+
+    buildTypes {
+        debug {
+            minifyEnabled false
+            buildConfigField("boolean", "IS_DEBUG", 'true')
+        }
+
+        release {
+            minifyEnabled false
+            buildConfigField("boolean", "IS_DEBUG", 'false')
+            proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
+        }
+    }
+
+}
+
+dependencies {
+    implementation fileTree(dir: 'libs', include: ['*.jar'])
+
+    testImplementation 'junit:junit:4.12'
+    androidTestImplementation 'androidx.test.ext:junit:1.1.1'
+    androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0'
+
+    api "androidx.core:core:1.4.0-alpha01"
+    api 'androidx.appcompat:appcompat:1.3.0-alpha01'
+    api 'com.google.android.material:material:1.1.0'
+    api 'androidx.vectordrawable:vectordrawable:1.1.0'
+    api 'androidx.navigation:navigation-fragment:2.2.2'
+    api 'androidx.navigation:navigation-ui:2.2.2'
+    api 'androidx.lifecycle:lifecycle-extensions:2.2.0'
+
+    // 带行号的Log
+    api 'com.github.zhaokaiqiang.klog:library:1.6.0'
+
+    // 查看内存泄露
+    debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.1'
+
+    // 防止Retrofit内存泄露
+    api 'com.trello.rxlifecycle3:rxlifecycle:3.1.0'
+    api 'com.trello.rxlifecycle3:rxlifecycle-android:3.1.0'
+    api 'com.trello.rxlifecycle3:rxlifecycle-components:3.1.0'
+
+    // 漂亮的TabLayout
+    api 'com.flyco.tablayout:FlycoTabLayout_Lib:2.1.2@aar'
+
+    // Glide加载图片
+    api 'com.github.bumptech.glide:glide:4.11.0'
+    annotationProcessor 'com.github.bumptech.glide:compiler:4.11.0'
+
+    // 适配器
+    //api 'com.github.CymChad:BaseRecyclerViewAdapterHelper:2.9.50'
+}

+ 21 - 0
common/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

+ 27 - 0
common/src/androidTest/java/com/miekir/common/ExampleInstrumentedTest.java

@@ -0,0 +1,27 @@
+package com.miekir.common;
+
+import android.content.Context;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.platform.app.InstrumentationRegistry;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import static org.junit.Assert.assertEquals;
+
+/**
+ * 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() {
+        // Context of the app under test.
+        Context appContext = InstrumentationRegistry.getInstrumentation().getTargetContext();
+
+        assertEquals("com.itant.common.test", appContext.getPackageName());
+    }
+}

+ 10 - 0
common/src/main/AndroidManifest.xml

@@ -0,0 +1,10 @@
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+    package="com.miekir.common" >
+    <application>
+        <!--authorities需要是唯一的,随着包名变化而变化,否则一个手机只能有一个项目引入这个库-->
+        <provider
+            android:name=".provider.CommonInstaller"
+            android:authorities="${applicationId}.common"
+            android:exported="false"/>
+    </application>
+</manifest>

+ 31 - 0
common/src/main/java/com/miekir/common/adapter/TabFragmentAdapter.java

@@ -0,0 +1,31 @@
+package com.miekir.common.adapter;
+
+import androidx.fragment.app.Fragment;
+import androidx.fragment.app.FragmentManager;
+import androidx.fragment.app.FragmentPagerAdapter;
+
+import java.util.List;
+
+/**
+ * Created by Jason on 2018/8/20.
+ * 在Fragment里使用ViewPager的适配器
+ */
+
+public class TabFragmentAdapter extends FragmentPagerAdapter {
+
+    private List<Fragment> tabFragments;
+    public TabFragmentAdapter(FragmentManager fm, List<Fragment> 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();
+    }
+}

+ 59 - 0
common/src/main/java/com/miekir/common/provider/CommonInstaller.java

@@ -0,0 +1,59 @@
+package com.miekir.common.provider;
+
+import android.content.ContentProvider;
+import android.content.ContentValues;
+import android.database.Cursor;
+import android.net.Uri;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import com.miekir.common.utils.ContextManager;
+import com.miekir.common.utils.SizeTool;
+
+/**
+ *
+ * @author Miekir
+ * @date 2020/7/5 0:17
+ * Description: 免手动初始化
+ */
+public class CommonInstaller extends ContentProvider {
+    @Override
+    public boolean onCreate() {
+        if (getContext() != null) {
+            ContextManager.getInstance().initContext(getContext().getApplicationContext());
+            SizeTool.SCREEN_WIDTH = getContext().getResources().getDisplayMetrics().widthPixels;
+            SizeTool.SCREEN_HEIGHT = getContext().getResources().getDisplayMetrics().heightPixels;
+        }
+
+        return false;
+    }
+
+    @Nullable
+    @Override
+    public Cursor query(@NonNull Uri uri, @Nullable String[] projection, @Nullable String selection, @Nullable String[] selectionArgs, @Nullable String sortOrder) {
+        return null;
+    }
+
+    @Nullable
+    @Override
+    public String getType(@NonNull Uri uri) {
+        return null;
+    }
+
+    @Nullable
+    @Override
+    public Uri insert(@NonNull Uri uri, @Nullable ContentValues values) {
+        return null;
+    }
+
+    @Override
+    public int delete(@NonNull Uri uri, @Nullable String selection, @Nullable String[] selectionArgs) {
+        return 0;
+    }
+
+    @Override
+    public int update(@NonNull Uri uri, @Nullable ContentValues values, @Nullable String selection, @Nullable String[] selectionArgs) {
+        return 0;
+    }
+}

+ 30 - 0
common/src/main/java/com/miekir/common/utils/ActivityTool.java

@@ -0,0 +1,30 @@
+package com.miekir.common.utils;
+
+import android.app.Activity;
+import android.content.Intent;
+import android.net.Uri;
+
+/**
+ *
+ *
+ * @author Miekir
+ * @date 2020/6/21 21:32
+ * Description: 关于Activity的工具
+ */
+public class ActivityTool {
+    private ActivityTool() {}
+
+    public static void openUrl(Activity activity, String webUrl) {
+        Intent intent = new Intent();
+        intent.setAction("android.intent.action.VIEW");
+        intent.addCategory("android.intent.category.DEFAULT");
+        Uri uri = Uri.parse(webUrl);
+        intent.setData(uri);
+        try {
+            activity.startActivity(intent);
+        } catch (Exception e) {
+            ToastTool.showShort("找不到支付通道");
+            e.printStackTrace();
+        }
+    }
+}

+ 978 - 0
common/src/main/java/com/miekir/common/utils/Base64.java

@@ -0,0 +1,978 @@
+package com.miekir.common.utils;
+
+import java.io.FilterOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.nio.ByteBuffer;
+import java.nio.charset.StandardCharsets;
+import java.util.Arrays;
+import java.util.Objects;
+
+/**
+ * This class consists exclusively of static methods for obtaining
+ * encoders and decoders for the Base64 encoding scheme. The
+ * implementation of this class supports the following types of Base64
+ * as specified in
+ * <a href="http://www.ietf.org/rfc/rfc4648.txt">RFC 4648</a> and
+ * <a href="http://www.ietf.org/rfc/rfc2045.txt">RFC 2045</a>.
+ *
+ * <ul>
+ * <li><a name="basic"><b>Basic</b></a>
+ * <p> Uses "The Base64 Alphabet" as specified in Table 1 of
+ *     RFC 4648 and RFC 2045 for encoding and decoding operation.
+ *     The encoder does not add any line feed (line separator)
+ *     character. The decoder rejects data that contains characters
+ *     outside the base64 alphabet.</p></li>
+ *
+ * <li><a name="url"><b>URL and Filename safe</b></a>
+ * <p> Uses the "URL and Filename safe Base64 Alphabet" as specified
+ *     in Table 2 of RFC 4648 for encoding and decoding. The
+ *     encoder does not add any line feed (line separator) character.
+ *     The decoder rejects data that contains characters outside the
+ *     base64 alphabet.</p></li>
+ *
+ * <li><a name="mime"><b>MIME</b></a>
+ * <p> Uses the "The Base64 Alphabet" as specified in Table 1 of
+ *     RFC 2045 for encoding and decoding operation. The encoded output
+ *     must be represented in lines of no more than 76 characters each
+ *     and uses a carriage return {@code '\r'} followed immediately by
+ *     a linefeed {@code '\n'} as the line separator. No line separator
+ *     is added to the end of the encoded output. All line separators
+ *     or other characters not found in the base64 alphabet table are
+ *     ignored in decoding operation.</p></li>
+ * </ul>
+ *
+ * <p> Unless otherwise noted, passing a {@code null} argument to a
+ * method of this class will cause a {@link java.lang.NullPointerException
+ * NullPointerException} to be thrown.
+ *
+ * @author  Xueming Shen
+ * @since   1.8
+ */
+
+public class Base64 {
+
+    private Base64() {}
+
+    /**
+     * Returns a {@link Encoder} that encodes using the
+     * <a href="#basic">Basic</a> type base64 encoding scheme.
+     *
+     * @return  A Base64 encoder.
+     */
+    public static Encoder getEncoder() {
+         return Encoder.RFC4648;
+    }
+
+    /**
+     * Returns a {@link Encoder} that encodes using the
+     * <a href="#url">URL and Filename safe</a> type base64
+     * encoding scheme.
+     *
+     * @return  A Base64 encoder.
+     */
+    public static Encoder getUrlEncoder() {
+         return Encoder.RFC4648_URLSAFE;
+    }
+
+    /**
+     * Returns a {@link Encoder} that encodes using the
+     * <a href="#mime">MIME</a> type base64 encoding scheme.
+     *
+     * @return  A Base64 encoder.
+     */
+    public static Encoder getMimeEncoder() {
+        return Encoder.RFC2045;
+    }
+
+    /**
+     * Returns a {@link Encoder} that encodes using the
+     * <a href="#mime">MIME</a> type base64 encoding scheme
+     * with specified line length and line separators.
+     *
+     * @param   lineLength
+     *          the length of each output line (rounded down to nearest multiple
+     *          of 4). If {@code lineLength <= 0} the output will not be separated
+     *          in lines
+     * @param   lineSeparator
+     *          the line separator for each output line
+     *
+     * @return  A Base64 encoder.
+     *
+     * @throws  IllegalArgumentException if {@code lineSeparator} includes any
+     *          character of "The Base64 Alphabet" as specified in Table 1 of
+     *          RFC 2045.
+     */
+    public static Encoder getMimeEncoder(int lineLength, byte[] lineSeparator) {
+         Objects.requireNonNull(lineSeparator);
+         int[] base64 = Decoder.fromBase64;
+         for (byte b : lineSeparator) {
+             if (base64[b & 0xff] != -1)
+                 throw new IllegalArgumentException(
+                     "Illegal base64 line separator character 0x" + Integer.toString(b, 16));
+         }
+         if (lineLength <= 0) {
+             return Encoder.RFC4648;
+         }
+         return new Encoder(false, lineSeparator, lineLength >> 2 << 2, true);
+    }
+
+    /**
+     * Returns a {@link Decoder} that decodes using the
+     * <a href="#basic">Basic</a> type base64 encoding scheme.
+     *
+     * @return  A Base64 decoder.
+     */
+    public static Decoder getDecoder() {
+         return Decoder.RFC4648;
+    }
+
+    /**
+     * Returns a {@link Decoder} that decodes using the
+     * <a href="#url">URL and Filename safe</a> type base64
+     * encoding scheme.
+     *
+     * @return  A Base64 decoder.
+     */
+    public static Decoder getUrlDecoder() {
+         return Decoder.RFC4648_URLSAFE;
+    }
+
+    /**
+     * Returns a {@link Decoder} that decodes using the
+     * <a href="#mime">MIME</a> type base64 decoding scheme.
+     *
+     * @return  A Base64 decoder.
+     */
+    public static Decoder getMimeDecoder() {
+         return Decoder.RFC2045;
+    }
+
+    /**
+     * This class implements an encoder for encoding byte data using
+     * the Base64 encoding scheme as specified in RFC 4648 and RFC 2045.
+     *
+     * <p> Instances of {@link Encoder} class are safe for use by
+     * multiple concurrent threads.
+     *
+     * <p> Unless otherwise noted, passing a {@code null} argument to
+     * a method of this class will cause a
+     * {@link java.lang.NullPointerException NullPointerException} to
+     * be thrown.
+     *
+     * @see     Decoder
+     * @since   1.8
+     */
+    public static class Encoder {
+
+        private final byte[] newline;
+        private final int linemax;
+        private final boolean isURL;
+        private final boolean doPadding;
+
+        private Encoder(boolean isURL, byte[] newline, int linemax, boolean doPadding) {
+            this.isURL = isURL;
+            this.newline = newline;
+            this.linemax = linemax;
+            this.doPadding = doPadding;
+        }
+
+        /**
+         * This array is a lookup table that translates 6-bit positive integer
+         * index values into their "Base64 Alphabet" equivalents as specified
+         * in "Table 1: The Base64 Alphabet" of RFC 2045 (and RFC 4648).
+         */
+        private static final char[] toBase64 = {
+            'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M',
+            'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z',
+            'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm',
+            'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z',
+            '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '+', '/'
+        };
+
+        /**
+         * It's the lookup table for "URL and Filename safe Base64" as specified
+         * in Table 2 of the RFC 4648, with the '+' and '/' changed to '-' and
+         * '_'. This table is used when BASE64_URL is specified.
+         */
+        private static final char[] toBase64URL = {
+            'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M',
+            'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z',
+            'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm',
+            'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z',
+            '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '-', '_'
+        };
+
+        private static final int MIMELINEMAX = 76;
+        private static final byte[] CRLF = new byte[] {'\r', '\n'};
+
+        static final Encoder RFC4648 = new Encoder(false, null, -1, true);
+        static final Encoder RFC4648_URLSAFE = new Encoder(true, null, -1, true);
+        static final Encoder RFC2045 = new Encoder(false, CRLF, MIMELINEMAX, true);
+
+        private final int outLength(int srclen) {
+            int len = 0;
+            if (doPadding) {
+                len = 4 * ((srclen + 2) / 3);
+            } else {
+                int n = srclen % 3;
+                len = 4 * (srclen / 3) + (n == 0 ? 0 : n + 1);
+            }
+            if (linemax > 0)                                  // line separators
+                len += (len - 1) / linemax * newline.length;
+            return len;
+        }
+
+        /**
+         * Encodes all bytes from the specified byte array into a newly-allocated
+         * byte array using the {@link Base64} encoding scheme. The returned byte
+         * array is of the length of the resulting bytes.
+         *
+         * @param   src
+         *          the byte array to encode
+         * @return  A newly-allocated byte array containing the resulting
+         *          encoded bytes.
+         */
+        public byte[] encode(byte[] src) {
+            int len = outLength(src.length);          // dst array size
+            byte[] dst = new byte[len];
+            int ret = encode0(src, 0, src.length, dst);
+            if (ret != dst.length)
+                 return Arrays.copyOf(dst, ret);
+            return dst;
+        }
+
+        /**
+         * Encodes all bytes from the specified byte array using the
+         * {@link Base64} encoding scheme, writing the resulting bytes to the
+         * given output byte array, starting at offset 0.
+         *
+         * <p> It is the responsibility of the invoker of this method to make
+         * sure the output byte array {@code dst} has enough space for encoding
+         * all bytes from the input byte array. No bytes will be written to the
+         * output byte array if the output byte array is not big enough.
+         *
+         * @param   src
+         *          the byte array to encode
+         * @param   dst
+         *          the output byte array
+         * @return  The number of bytes written to the output byte array
+         *
+         * @throws  IllegalArgumentException if {@code dst} does not have enough
+         *          space for encoding all input bytes.
+         */
+        public int encode(byte[] src, byte[] dst) {
+            int len = outLength(src.length);         // dst array size
+            if (dst.length < len)
+                throw new IllegalArgumentException(
+                    "Output byte array is too small for encoding all input bytes");
+            return encode0(src, 0, src.length, dst);
+        }
+
+        /**
+         * Encodes the specified byte array into a String using the {@link Base64}
+         * encoding scheme.
+         *
+         * <p> This method first encodes all input bytes into a base64 encoded
+         * byte array and then constructs a new String by using the encoded byte
+         * array and the {@link java.nio.charset.StandardCharsets#ISO_8859_1
+         * ISO-8859-1} charset.
+         *
+         * <p> In other words, an invocation of this method has exactly the same
+         * effect as invoking
+         * {@code new String(encode(src), StandardCharsets.ISO_8859_1)}.
+         *
+         * @param   src
+         *          the byte array to encode
+         * @return  A String containing the resulting Base64 encoded characters
+         */
+        @SuppressWarnings("deprecation")
+        public String encodeToString(byte[] src) {
+            byte[] encoded = encode(src);
+            return new String(encoded, 0, 0, encoded.length);
+        }
+
+        /**
+         * Encodes all remaining bytes from the specified byte buffer into
+         * a newly-allocated ByteBuffer using the {@link Base64} encoding
+         * scheme.
+         *
+         * Upon return, the source buffer's position will be updated to
+         * its limit; its limit will not have been changed. The returned
+         * output buffer's position will be zero and its limit will be the
+         * number of resulting encoded bytes.
+         *
+         * @param   buffer
+         *          the source ByteBuffer to encode
+         * @return  A newly-allocated byte buffer containing the encoded bytes.
+         */
+        public ByteBuffer encode(ByteBuffer buffer) {
+            int len = outLength(buffer.remaining());
+            byte[] dst = new byte[len];
+            int ret = 0;
+            if (buffer.hasArray()) {
+                ret = encode0(buffer.array(),
+                              buffer.arrayOffset() + buffer.position(),
+                              buffer.arrayOffset() + buffer.limit(),
+                              dst);
+                buffer.position(buffer.limit());
+            } else {
+                byte[] src = new byte[buffer.remaining()];
+                buffer.get(src);
+                ret = encode0(src, 0, src.length, dst);
+            }
+            if (ret != dst.length)
+                 dst = Arrays.copyOf(dst, ret);
+            return ByteBuffer.wrap(dst);
+        }
+
+        /**
+         * Wraps an output stream for encoding byte data using the {@link Base64}
+         * encoding scheme.
+         *
+         * <p> It is recommended to promptly close the returned output stream after
+         * use, during which it will flush all possible leftover bytes to the underlying
+         * output stream. Closing the returned output stream will close the underlying
+         * output stream.
+         *
+         * @param   os
+         *          the output stream.
+         * @return  the output stream for encoding the byte data into the
+         *          specified Base64 encoded format
+         */
+        public OutputStream wrap(OutputStream os) {
+            Objects.requireNonNull(os);
+            return new EncOutputStream(os, isURL ? toBase64URL : toBase64,
+                                       newline, linemax, doPadding);
+        }
+
+        /**
+         * Returns an encoder instance that encodes equivalently to this one,
+         * but without adding any padding character at the end of the encoded
+         * byte data.
+         *
+         * <p> The encoding scheme of this encoder instance is unaffected by
+         * this invocation. The returned encoder instance should be used for
+         * non-padding encoding operation.
+         *
+         * @return an equivalent encoder that encodes without adding any
+         *         padding character at the end
+         */
+        public Encoder withoutPadding() {
+            if (!doPadding)
+                return this;
+            return new Encoder(isURL, newline, linemax, false);
+        }
+
+        private int encode0(byte[] src, int off, int end, byte[] dst) {
+            char[] base64 = isURL ? toBase64URL : toBase64;
+            int sp = off;
+            int slen = (end - off) / 3 * 3;
+            int sl = off + slen;
+            if (linemax > 0 && slen  > linemax / 4 * 3)
+                slen = linemax / 4 * 3;
+            int dp = 0;
+            while (sp < sl) {
+                int sl0 = Math.min(sp + slen, sl);
+                for (int sp0 = sp, dp0 = dp ; sp0 < sl0; ) {
+                    int bits = (src[sp0++] & 0xff) << 16 |
+                               (src[sp0++] & 0xff) <<  8 |
+                               (src[sp0++] & 0xff);
+                    dst[dp0++] = (byte)base64[(bits >>> 18) & 0x3f];
+                    dst[dp0++] = (byte)base64[(bits >>> 12) & 0x3f];
+                    dst[dp0++] = (byte)base64[(bits >>> 6)  & 0x3f];
+                    dst[dp0++] = (byte)base64[bits & 0x3f];
+                }
+                int dlen = (sl0 - sp) / 3 * 4;
+                dp += dlen;
+                sp = sl0;
+                if (dlen == linemax && sp < end) {
+                    for (byte b : newline){
+                        dst[dp++] = b;
+                    }
+                }
+            }
+            if (sp < end) {               // 1 or 2 leftover bytes
+                int b0 = src[sp++] & 0xff;
+                dst[dp++] = (byte)base64[b0 >> 2];
+                if (sp == end) {
+                    dst[dp++] = (byte)base64[(b0 << 4) & 0x3f];
+                    if (doPadding) {
+                        dst[dp++] = '=';
+                        dst[dp++] = '=';
+                    }
+                } else {
+                    int b1 = src[sp++] & 0xff;
+                    dst[dp++] = (byte)base64[(b0 << 4) & 0x3f | (b1 >> 4)];
+                    dst[dp++] = (byte)base64[(b1 << 2) & 0x3f];
+                    if (doPadding) {
+                        dst[dp++] = '=';
+                    }
+                }
+            }
+            return dp;
+        }
+    }
+
+    /**
+     * This class implements a decoder for decoding byte data using the
+     * Base64 encoding scheme as specified in RFC 4648 and RFC 2045.
+     *
+     * <p> The Base64 padding character {@code '='} is accepted and
+     * interpreted as the end of the encoded byte data, but is not
+     * required. So if the final unit of the encoded byte data only has
+     * two or three Base64 characters (without the corresponding padding
+     * character(s) padded), they are decoded as if followed by padding
+     * character(s). If there is a padding character present in the
+     * final unit, the correct number of padding character(s) must be
+     * present, otherwise {@code IllegalArgumentException} (
+     * {@code IOException} when reading from a Base64 stream) is thrown
+     * during decoding.
+     *
+     * <p> Instances of {@link Decoder} class are safe for use by
+     * multiple concurrent threads.
+     *
+     * <p> Unless otherwise noted, passing a {@code null} argument to
+     * a method of this class will cause a
+     * {@link java.lang.NullPointerException NullPointerException} to
+     * be thrown.
+     *
+     * @see     Encoder
+     * @since   1.8
+     */
+    public static class Decoder {
+
+        private final boolean isURL;
+        private final boolean isMIME;
+
+        private Decoder(boolean isURL, boolean isMIME) {
+            this.isURL = isURL;
+            this.isMIME = isMIME;
+        }
+
+        /**
+         * Lookup table for decoding unicode characters drawn from the
+         * "Base64 Alphabet" (as specified in Table 1 of RFC 2045) into
+         * their 6-bit positive integer equivalents.  Characters that
+         * are not in the Base64 alphabet but fall within the bounds of
+         * the array are encoded to -1.
+         *
+         */
+        private static final int[] fromBase64 = new int[256];
+        static {
+            Arrays.fill(fromBase64, -1);
+            for (int i = 0; i < Encoder.toBase64.length; i++)
+                fromBase64[Encoder.toBase64[i]] = i;
+            fromBase64['='] = -2;
+        }
+
+        /**
+         * Lookup table for decoding "URL and Filename safe Base64 Alphabet"
+         * as specified in Table2 of the RFC 4648.
+         */
+        private static final int[] fromBase64URL = new int[256];
+
+        static {
+            Arrays.fill(fromBase64URL, -1);
+            for (int i = 0; i < Encoder.toBase64URL.length; i++)
+                fromBase64URL[Encoder.toBase64URL[i]] = i;
+            fromBase64URL['='] = -2;
+        }
+
+        static final Decoder RFC4648         = new Decoder(false, false);
+        static final Decoder RFC4648_URLSAFE = new Decoder(true, false);
+        static final Decoder RFC2045         = new Decoder(false, true);
+
+        /**
+         * Decodes all bytes from the input byte array using the {@link Base64}
+         * encoding scheme, writing the results into a newly-allocated output
+         * byte array. The returned byte array is of the length of the resulting
+         * bytes.
+         *
+         * @param   src
+         *          the byte array to decode
+         *
+         * @return  A newly-allocated byte array containing the decoded bytes.
+         *
+         * @throws  IllegalArgumentException
+         *          if {@code src} is not in valid Base64 scheme
+         */
+        public byte[] decode(byte[] src) {
+            byte[] dst = new byte[outLength(src, 0, src.length)];
+            int ret = decode0(src, 0, src.length, dst);
+            if (ret != dst.length) {
+                dst = Arrays.copyOf(dst, ret);
+            }
+            return dst;
+        }
+
+        /**
+         * Decodes a Base64 encoded String into a newly-allocated byte array
+         * using the {@link Base64} encoding scheme.
+         *
+         * <p> An invocation of this method has exactly the same effect as invoking
+         * {@code decode(src.getBytes(StandardCharsets.ISO_8859_1))}
+         *
+         * @param   src
+         *          the string to decode
+         *
+         * @return  A newly-allocated byte array containing the decoded bytes.
+         *
+         * @throws  IllegalArgumentException
+         *          if {@code src} is not in valid Base64 scheme
+         */
+        public byte[] decode(String src) {
+            return decode(src.getBytes(StandardCharsets.ISO_8859_1));
+        }
+
+        /**
+         * Decodes all bytes from the input byte array using the {@link Base64}
+         * encoding scheme, writing the results into the given output byte array,
+         * starting at offset 0.
+         *
+         * <p> It is the responsibility of the invoker of this method to make
+         * sure the output byte array {@code dst} has enough space for decoding
+         * all bytes from the input byte array. No bytes will be be written to
+         * the output byte array if the output byte array is not big enough.
+         *
+         * <p> If the input byte array is not in valid Base64 encoding scheme
+         * then some bytes may have been written to the output byte array before
+         * IllegalargumentException is thrown.
+         *
+         * @param   src
+         *          the byte array to decode
+         * @param   dst
+         *          the output byte array
+         *
+         * @return  The number of bytes written to the output byte array
+         *
+         * @throws  IllegalArgumentException
+         *          if {@code src} is not in valid Base64 scheme, or {@code dst}
+         *          does not have enough space for decoding all input bytes.
+         */
+        public int decode(byte[] src, byte[] dst) {
+            int len = outLength(src, 0, src.length);
+            if (dst.length < len)
+                throw new IllegalArgumentException(
+                    "Output byte array is too small for decoding all input bytes");
+            return decode0(src, 0, src.length, dst);
+        }
+
+        /**
+         * Decodes all bytes from the input byte buffer using the {@link Base64}
+         * encoding scheme, writing the results into a newly-allocated ByteBuffer.
+         *
+         * <p> Upon return, the source buffer's position will be updated to
+         * its limit; its limit will not have been changed. The returned
+         * output buffer's position will be zero and its limit will be the
+         * number of resulting decoded bytes
+         *
+         * <p> {@code IllegalArgumentException} is thrown if the input buffer
+         * is not in valid Base64 encoding scheme. The position of the input
+         * buffer will not be advanced in this case.
+         *
+         * @param   buffer
+         *          the ByteBuffer to decode
+         *
+         * @return  A newly-allocated byte buffer containing the decoded bytes
+         *
+         * @throws  IllegalArgumentException
+         *          if {@code src} is not in valid Base64 scheme.
+         */
+        public ByteBuffer decode(ByteBuffer buffer) {
+            int pos0 = buffer.position();
+            try {
+                byte[] src;
+                int sp, sl;
+                if (buffer.hasArray()) {
+                    src = buffer.array();
+                    sp = buffer.arrayOffset() + buffer.position();
+                    sl = buffer.arrayOffset() + buffer.limit();
+                    buffer.position(buffer.limit());
+                } else {
+                    src = new byte[buffer.remaining()];
+                    buffer.get(src);
+                    sp = 0;
+                    sl = src.length;
+                }
+                byte[] dst = new byte[outLength(src, sp, sl)];
+                return ByteBuffer.wrap(dst, 0, decode0(src, sp, sl, dst));
+            } catch (IllegalArgumentException iae) {
+                buffer.position(pos0);
+                throw iae;
+            }
+        }
+
+        /**
+         * Returns an input stream for decoding {@link Base64} encoded byte stream.
+         *
+         * <p> The {@code read}  methods of the returned {@code InputStream} will
+         * throw {@code IOException} when reading bytes that cannot be decoded.
+         *
+         * <p> Closing the returned input stream will close the underlying
+         * input stream.
+         *
+         * @param   is
+         *          the input stream
+         *
+         * @return  the input stream for decoding the specified Base64 encoded
+         *          byte stream
+         */
+        public InputStream wrap(InputStream is) {
+            Objects.requireNonNull(is);
+            return new DecInputStream(is, isURL ? fromBase64URL : fromBase64, isMIME);
+        }
+
+        private int outLength(byte[] src, int sp, int sl) {
+            int[] base64 = isURL ? fromBase64URL : fromBase64;
+            int paddings = 0;
+            int len = sl - sp;
+            if (len == 0)
+                return 0;
+            if (len < 2) {
+                if (isMIME && base64[0] == -1)
+                    return 0;
+                throw new IllegalArgumentException(
+                    "Input byte[] should at least have 2 bytes for base64 bytes");
+            }
+            if (isMIME) {
+                // scan all bytes to fill out all non-alphabet. a performance
+                // trade-off of pre-scan or Arrays.copyOf
+                int n = 0;
+                while (sp < sl) {
+                    int b = src[sp++] & 0xff;
+                    if (b == '=') {
+                        len -= (sl - sp + 1);
+                        break;
+                    }
+                    if ((b = base64[b]) == -1)
+                        n++;
+                }
+                len -= n;
+            } else {
+                if (src[sl - 1] == '=') {
+                    paddings++;
+                    if (src[sl - 2] == '=')
+                        paddings++;
+                }
+            }
+            if (paddings == 0 && (len & 0x3) !=  0)
+                paddings = 4 - (len & 0x3);
+            return 3 * ((len + 3) / 4) - paddings;
+        }
+
+        private int decode0(byte[] src, int sp, int sl, byte[] dst) {
+            int[] base64 = isURL ? fromBase64URL : fromBase64;
+            int dp = 0;
+            int bits = 0;
+            int shiftto = 18;       // pos of first byte of 4-byte atom
+            while (sp < sl) {
+                int b = src[sp++] & 0xff;
+                if ((b = base64[b]) < 0) {
+                    if (b == -2) {         // padding byte '='
+                        // =     shiftto==18 unnecessary padding
+                        // x=    shiftto==12 a dangling single x
+                        // x     to be handled together with non-padding case
+                        // xx=   shiftto==6&&sp==sl missing last =
+                        // xx=y  shiftto==6 last is not =
+                        if (shiftto == 6 && (sp == sl || src[sp++] != '=') ||
+                            shiftto == 18) {
+                            throw new IllegalArgumentException(
+                                "Input byte array has wrong 4-byte ending unit");
+                        }
+                        break;
+                    }
+                    if (isMIME)    // skip if for rfc2045
+                        continue;
+                    else
+                        throw new IllegalArgumentException(
+                            "Illegal base64 character " +
+                            Integer.toString(src[sp - 1], 16));
+                }
+                bits |= (b << shiftto);
+                shiftto -= 6;
+                if (shiftto < 0) {
+                    dst[dp++] = (byte)(bits >> 16);
+                    dst[dp++] = (byte)(bits >>  8);
+                    dst[dp++] = (byte)(bits);
+                    shiftto = 18;
+                    bits = 0;
+                }
+            }
+            // reached end of byte array or hit padding '=' characters.
+            if (shiftto == 6) {
+                dst[dp++] = (byte)(bits >> 16);
+            } else if (shiftto == 0) {
+                dst[dp++] = (byte)(bits >> 16);
+                dst[dp++] = (byte)(bits >>  8);
+            } else if (shiftto == 12) {
+                // dangling single "x", incorrectly encoded.
+                throw new IllegalArgumentException(
+                    "Last unit does not have enough valid bits");
+            }
+            // anything left is invalid, if is not MIME.
+            // if MIME, ignore all non-base64 character
+            while (sp < sl) {
+                if (isMIME && base64[src[sp++]] < 0)
+                    continue;
+                throw new IllegalArgumentException(
+                    "Input byte array has incorrect ending byte at " + sp);
+            }
+            return dp;
+        }
+    }
+
+    /*
+     * An output stream for encoding bytes into the Base64.
+     */
+    private static class EncOutputStream extends FilterOutputStream {
+
+        private int leftover = 0;
+        private int b0, b1, b2;
+        private boolean closed = false;
+
+        private final char[] base64;    // byte->base64 mapping
+        private final byte[] newline;   // line separator, if needed
+        private final int linemax;
+        private final boolean doPadding;// whether or not to pad
+        private int linepos = 0;
+
+        EncOutputStream(OutputStream os, char[] base64,
+                        byte[] newline, int linemax, boolean doPadding) {
+            super(os);
+            this.base64 = base64;
+            this.newline = newline;
+            this.linemax = linemax;
+            this.doPadding = doPadding;
+        }
+
+        @Override
+        public void write(int b) throws IOException {
+            byte[] buf = new byte[1];
+            buf[0] = (byte)(b & 0xff);
+            write(buf, 0, 1);
+        }
+
+        private void checkNewline() throws IOException {
+            if (linepos == linemax) {
+                out.write(newline);
+                linepos = 0;
+            }
+        }
+
+        @Override
+        public void write(byte[] b, int off, int len) throws IOException {
+            if (closed)
+                throw new IOException("Stream is closed");
+            // Android-changed: Upstream fix to avoid overflow.
+            // This upstream fix is from beyond OpenJDK8u121-b13. http://b/62368386
+            // if (off < 0 || len < 0 || off + len > b.length)
+            if (off < 0 || len < 0 || len > b.length - off)
+                throw new ArrayIndexOutOfBoundsException();
+            if (len == 0)
+                return;
+            if (leftover != 0) {
+                if (leftover == 1) {
+                    b1 = b[off++] & 0xff;
+                    len--;
+                    if (len == 0) {
+                        leftover++;
+                        return;
+                    }
+                }
+                b2 = b[off++] & 0xff;
+                len--;
+                checkNewline();
+                out.write(base64[b0 >> 2]);
+                out.write(base64[(b0 << 4) & 0x3f | (b1 >> 4)]);
+                out.write(base64[(b1 << 2) & 0x3f | (b2 >> 6)]);
+                out.write(base64[b2 & 0x3f]);
+                linepos += 4;
+            }
+            int nBits24 = len / 3;
+            leftover = len - (nBits24 * 3);
+            while (nBits24-- > 0) {
+                checkNewline();
+                int bits = (b[off++] & 0xff) << 16 |
+                           (b[off++] & 0xff) <<  8 |
+                           (b[off++] & 0xff);
+                out.write(base64[(bits >>> 18) & 0x3f]);
+                out.write(base64[(bits >>> 12) & 0x3f]);
+                out.write(base64[(bits >>> 6)  & 0x3f]);
+                out.write(base64[bits & 0x3f]);
+                linepos += 4;
+           }
+            if (leftover == 1) {
+                b0 = b[off++] & 0xff;
+            } else if (leftover == 2) {
+                b0 = b[off++] & 0xff;
+                b1 = b[off++] & 0xff;
+            }
+        }
+
+        @Override
+        public void close() throws IOException {
+            if (!closed) {
+                closed = true;
+                if (leftover == 1) {
+                    checkNewline();
+                    out.write(base64[b0 >> 2]);
+                    out.write(base64[(b0 << 4) & 0x3f]);
+                    if (doPadding) {
+                        out.write('=');
+                        out.write('=');
+                    }
+                } else if (leftover == 2) {
+                    checkNewline();
+                    out.write(base64[b0 >> 2]);
+                    out.write(base64[(b0 << 4) & 0x3f | (b1 >> 4)]);
+                    out.write(base64[(b1 << 2) & 0x3f]);
+                    if (doPadding) {
+                       out.write('=');
+                    }
+                }
+                leftover = 0;
+                out.close();
+            }
+        }
+    }
+
+    /*
+     * An input stream for decoding Base64 bytes
+     */
+    private static class DecInputStream extends InputStream {
+
+        private final InputStream is;
+        private final boolean isMIME;
+        private final int[] base64;      // base64 -> byte mapping
+        private int bits = 0;            // 24-bit buffer for decoding
+        private int nextin = 18;         // next available "off" in "bits" for input;
+                                         // -> 18, 12, 6, 0
+        private int nextout = -8;        // next available "off" in "bits" for output;
+                                         // -> 8, 0, -8 (no byte for output)
+        private boolean eof = false;
+        private boolean closed = false;
+
+        DecInputStream(InputStream is, int[] base64, boolean isMIME) {
+            this.is = is;
+            this.base64 = base64;
+            this.isMIME = isMIME;
+        }
+
+        private byte[] sbBuf = new byte[1];
+
+        @Override
+        public int read() throws IOException {
+            return read(sbBuf, 0, 1) == -1 ? -1 : sbBuf[0] & 0xff;
+        }
+
+        @Override
+        public int read(byte[] b, int off, int len) throws IOException {
+            if (closed)
+                throw new IOException("Stream is closed");
+            if (eof && nextout < 0)    // eof and no leftover
+                return -1;
+            if (off < 0 || len < 0 || len > b.length - off)
+                throw new IndexOutOfBoundsException();
+            int oldOff = off;
+            if (nextout >= 0) {       // leftover output byte(s) in bits buf
+                do {
+                    if (len == 0)
+                        return off - oldOff;
+                    b[off++] = (byte)(bits >> nextout);
+                    len--;
+                    nextout -= 8;
+                } while (nextout >= 0);
+                bits = 0;
+            }
+            while (len > 0) {
+                int v = is.read();
+                if (v == -1) {
+                    eof = true;
+                    if (nextin != 18) {
+                        if (nextin == 12)
+                            throw new IOException("Base64 stream has one un-decoded dangling byte.");
+                        // treat ending xx/xxx without padding character legal.
+                        // same logic as v == '=' below
+                        b[off++] = (byte)(bits >> (16));
+                        len--;
+                        if (nextin == 0) {           // only one padding byte
+                            if (len == 0) {          // no enough output space
+                                bits >>= 8;          // shift to lowest byte
+                                nextout = 0;
+                            } else {
+                                b[off++] = (byte) (bits >>  8);
+                            }
+                        }
+                    }
+                    if (off == oldOff)
+                        return -1;
+                    else
+                        return off - oldOff;
+                }
+                if (v == '=') {                  // padding byte(s)
+                    // =     shiftto==18 unnecessary padding
+                    // x=    shiftto==12 dangling x, invalid unit
+                    // xx=   shiftto==6 && missing last '='
+                    // xx=y  or last is not '='
+                    if (nextin == 18 || nextin == 12 ||
+                        nextin == 6 && is.read() != '=') {
+                        throw new IOException("Illegal base64 ending sequence:" + nextin);
+                    }
+                    b[off++] = (byte)(bits >> (16));
+                    len--;
+                    if (nextin == 0) {           // only one padding byte
+                        if (len == 0) {          // no enough output space
+                            bits >>= 8;          // shift to lowest byte
+                            nextout = 0;
+                        } else {
+                            b[off++] = (byte) (bits >>  8);
+                        }
+                    }
+                    eof = true;
+                    break;
+                }
+                if ((v = base64[v]) == -1) {
+                    if (isMIME)                 // skip if for rfc2045
+                        continue;
+                    else
+                        throw new IOException("Illegal base64 character " +
+                            Integer.toString(v, 16));
+                }
+                bits |= (v << nextin);
+                if (nextin == 0) {
+                    nextin = 18;    // clear for next
+                    nextout = 16;
+                    while (nextout >= 0) {
+                        b[off++] = (byte)(bits >> nextout);
+                        len--;
+                        nextout -= 8;
+                        if (len == 0 && nextout >= 0) {  // don't clean "bits"
+                            return off - oldOff;
+                        }
+                    }
+                    bits = 0;
+                } else {
+                    nextin -= 6;
+                }
+            }
+            return off - oldOff;
+        }
+
+        @Override
+        public int available() throws IOException {
+            if (closed)
+                throw new IOException("Stream is closed");
+            return is.available();   // TBD:
+        }
+
+        @Override
+        public void close() throws IOException {
+            if (!closed) {
+                closed = true;
+                is.close();
+            }
+        }
+    }
+}

+ 39 - 0
common/src/main/java/com/miekir/common/utils/ContextManager.java

@@ -0,0 +1,39 @@
+package com.miekir.common.utils;
+
+import android.content.Context;
+
+import java.lang.ref.WeakReference;
+
+/**
+ * Copyright (C), 2019-2020, Miekir
+ *
+ * @author Miekir
+ * @date 2020/9/12 17:43
+ * Description:
+ */
+public class ContextManager {
+    private ContextManager(){}
+
+    private static WeakReference<Context> contextWeakReference = new WeakReference<>(null);
+    private static ContextManager instance;
+    public void initContext(Context context) {
+        contextWeakReference = new WeakReference<>(context);
+    }
+
+    public static ContextManager getInstance() {
+        if (instance == null) {
+            init();
+        }
+        return instance;
+    }
+
+    private static synchronized void init() {
+        if (instance == null) {
+            instance = new ContextManager();
+        }
+    }
+
+    public Context getContext() {
+        return contextWeakReference.get();
+    }
+}

+ 16 - 0
common/src/main/java/com/miekir/common/utils/SizeTool.java

@@ -0,0 +1,16 @@
+package com.miekir.common.utils;
+
+/**
+ *
+ *
+ * @author Miekir
+ * @date 2020/7/5 11:35
+ * Description: 尺寸工具
+ */
+public class SizeTool {
+    private SizeTool() {}
+
+    public static int SCREEN_WIDTH;
+    public static int SCREEN_HEIGHT;
+
+}

+ 72 - 0
common/src/main/java/com/miekir/common/utils/ToastTool.java

@@ -0,0 +1,72 @@
+package com.miekir.common.utils;
+
+import android.content.Context;
+import android.util.TypedValue;
+import android.view.Gravity;
+import android.widget.Toast;
+
+/**
+ * @author Miekir
+ * @date 2020/7/5 0:08
+ * Description: Toast工具
+ */
+public class ToastTool {
+    private static final long PERIOD_SHORT = 1500L;
+    private static final long PERIOD_LONG = 2500L;
+
+    private ToastTool() { }
+
+    private static long mLastShortToastMillis;
+    private static long mLastLongToastMillis;
+
+    private static int mVerticalMargin = 0;
+
+    /**
+     * @param text 要弹出的语句
+     */
+    public static void showShort(String text) {
+        Context context = ContextManager.getInstance().getContext();
+        if (context == null) {
+            return;
+        }
+
+        initMargin(context);
+
+        if (System.currentTimeMillis() - mLastShortToastMillis > PERIOD_SHORT) {
+            mLastShortToastMillis = System.currentTimeMillis();
+            Toast toast = Toast.makeText(context, text, Toast.LENGTH_SHORT);
+            toast.setGravity(Gravity.BOTTOM|Gravity.CENTER_HORIZONTAL, 0, mVerticalMargin);
+            toast.show();
+        }
+    }
+
+    /**
+     * @param text 要弹出的语句
+     */
+    public static void showLong(String text) {
+        Context context = ContextManager.getInstance().getContext();
+        if (context == null) {
+            return;
+        }
+
+        initMargin(context);
+
+        if (System.currentTimeMillis() - mLastLongToastMillis > PERIOD_LONG) {
+            mLastLongToastMillis = System.currentTimeMillis();
+            Toast toast = Toast.makeText(context, text, Toast.LENGTH_LONG);
+            toast.setGravity(Gravity.BOTTOM|Gravity.CENTER_HORIZONTAL, 0, mVerticalMargin);
+            toast.show();
+        }
+    }
+
+    private static void initMargin(Context context) {
+        if (context == null) {
+            return;
+        }
+        if (mVerticalMargin == 0) {
+            mVerticalMargin = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP,
+                    72,
+                    context.getResources().getDisplayMetrics());
+        }
+    }
+}

+ 55 - 0
common/src/main/java/com/miekir/common/utils/ViewTool.java

@@ -0,0 +1,55 @@
+package com.miekir.common.utils;
+
+import android.app.Activity;
+import android.os.SystemClock;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.ViewTreeObserver;
+import android.view.WindowManager;
+
+/**
+ *
+ *
+ * @author Miekir
+ * @date 2020/7/5 8:21
+ * Description: View相关的工具
+ */
+public class ViewTool {
+    private ViewTool() {}
+
+    /**
+     * 批量设置监听
+     * @param activity 控件所在Activity
+     * @param viewIdArray 需要监听的控件ID列表
+     * @param listener 监听回调
+     */
+    public static void setOnClickListener(Activity activity, View.OnClickListener listener, int[] viewIdArray) {
+        if (activity == null || viewIdArray == null || listener == null || viewIdArray.length == 0) {
+            return;
+        }
+
+        for (int viewId : viewIdArray) {
+            activity.findViewById(viewId).setOnClickListener(listener);
+        }
+    }
+
+    /**
+     * 获取焦点
+     * @param activity
+     * @param view
+     */
+    public static void requestInputFocus(Activity activity, final View view) {
+        // 进入页面弹出软键盘
+        view.requestFocus();
+        activity.getWindow().setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_VISIBLE);
+        // 或使用以下方法(view.post不行)
+        view.getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
+            @Override
+            public void onGlobalLayout() {
+                view.dispatchTouchEvent(MotionEvent.obtain(SystemClock.uptimeMillis(), SystemClock.uptimeMillis(), MotionEvent.ACTION_DOWN , 0, 0, 0));
+                view.dispatchTouchEvent(MotionEvent.obtain(SystemClock.uptimeMillis(), SystemClock.uptimeMillis(), MotionEvent.ACTION_UP , 0, 0, 0));
+                view.getViewTreeObserver().removeOnGlobalLayoutListener(this);
+            }
+        });
+    }
+}

+ 141 - 0
common/src/main/java/com/miekir/common/view/AlignTextView.java

@@ -0,0 +1,141 @@
+package com.miekir.common.view;
+
+import android.content.Context;
+import android.graphics.Canvas;
+import android.graphics.Paint;
+import android.text.Layout;
+import android.text.TextPaint;
+import android.util.AttributeSet;
+
+import androidx.appcompat.widget.AppCompatTextView;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * 两端对齐的textview,可以设置最后一行靠左,靠右,居中对齐
+ *
+ * @author yuyh.
+ * @date 16/4/10.
+ */
+public class AlignTextView extends AppCompatTextView {
+    private float textHeight; // 单行文字高度
+    private int width; // textview宽度
+    private List<String> lines = new ArrayList<String>(); // 分割后的行
+    private List<Integer> tailLines = new ArrayList<Integer>(); // 尾行
+    private Align align = Align.ALIGN_LEFT; // 默认最后一行左对齐
+
+    // 尾行对齐方式,针对段落最后一行
+    public enum Align {
+        ALIGN_LEFT,
+        ALIGN_CENTER,
+        ALIGN_RIGHT,
+    }
+
+    public AlignTextView(Context context, AttributeSet attrs) {
+        super(context, attrs);
+    }
+
+    @Override
+    protected void onDraw(Canvas canvas) {
+        TextPaint paint = getPaint();
+        paint.setColor(getCurrentTextColor());
+        paint.drawableState = getDrawableState();
+        width = getMeasuredWidth();
+        String text = getText().toString();
+
+        Paint.FontMetrics fm = paint.getFontMetrics();
+        // 计算行高
+        Layout layout = getLayout();
+
+        // layout.getLayout()在4.4.3出现NullPointerException
+        if (layout == null) {
+            return;
+        }
+
+        textHeight = fm.descent - fm.ascent;
+        textHeight = textHeight * layout.getSpacingMultiplier() + layout.getSpacingAdd();
+
+        float firstHeight = getTextSize();
+
+        int gravity = getGravity();
+        if ((gravity & 0x1000) == 0) { // 是否垂直居中
+            firstHeight = firstHeight + (textHeight - firstHeight) / 2;
+        }
+
+        int paddingLeft = getPaddingLeft();
+        int paddingRight = getPaddingRight();
+        width = width - paddingLeft - paddingRight;
+
+        lines.clear();
+        tailLines.clear();
+
+        // 文本含有换行符时,分割单独处理
+        String[] items = text.split("\\n");
+        for (String item : items) {
+            calc(paint, item);
+        }
+
+        for (int i = 0; i < lines.size(); i++) {
+            float drawY = i * textHeight + firstHeight;
+            String line = lines.get(i);
+            // 绘画起始x坐标
+            float drawSpacingX = paddingLeft;
+            float gap = (width - paint.measureText(line));
+            float interval = gap / (line.length() - 1);
+
+            // 绘制最后一行
+            if (tailLines.contains(i)) {
+                interval = 0;
+                if (align == Align.ALIGN_CENTER)
+                    drawSpacingX += gap / 2;
+                else if (align == Align.ALIGN_RIGHT)
+                    drawSpacingX += gap;
+            }
+
+            for (int j = 0; j < line.length(); j++) {
+                float drawX = paint.measureText(line.substring(0, j))
+                        + interval * j;
+                canvas.drawText(line.substring(j, j + 1), drawX + drawSpacingX,
+                        drawY, paint);
+            }
+        }
+    }
+
+    /**
+     * 设置尾行对齐方式
+     *
+     * @param align
+     */
+    public void setAlign(Align align) {
+        this.align = align;
+        invalidate();
+    }
+
+    /**
+     * 计算每行应显示的文本数
+     *
+     * @param text
+     * @return
+     */
+    private void calc(Paint paint, String text) {
+        if (text.length() == 0) {
+            lines.add("\n");
+            return;
+        }
+        StringBuffer sb = new StringBuffer("");
+        int startPosition = 0; // 起始位置
+        for (int i = 0; i < text.length(); i++) {
+            if (paint.measureText(text.substring(startPosition, i + 1)) > width) {
+                startPosition = i;
+                lines.add(sb.toString());
+                sb = new StringBuffer();
+            }
+            sb.append(text.charAt(i));
+        }
+        if (sb.length() > 0)
+            lines.add(sb.toString());
+
+        tailLines.add(lines.size() - 1);
+    }
+}

+ 237 - 0
common/src/main/java/com/miekir/common/view/RoundedCornersTransformation.java

@@ -0,0 +1,237 @@
+package com.miekir.common.view;
+
+import android.content.Context;
+import android.graphics.Bitmap;
+import android.graphics.BitmapShader;
+import android.graphics.Canvas;
+import android.graphics.Paint;
+import android.graphics.RectF;
+import android.graphics.Shader;
+import com.bumptech.glide.load.engine.bitmap_recycle.BitmapPool;
+import com.bumptech.glide.load.resource.bitmap.BitmapTransformation;
+
+import java.security.MessageDigest;
+
+public class RoundedCornersTransformation extends BitmapTransformation {
+
+  private static final int VERSION = 1;
+  private static final String ID = "jp.wasabeef.glide.transformations.RoundedCornersTransformation." + VERSION;
+
+  public enum CornerType {
+    ALL,
+    TOP_LEFT, TOP_RIGHT, BOTTOM_LEFT, BOTTOM_RIGHT,
+    TOP, BOTTOM, LEFT, RIGHT,
+    OTHER_TOP_LEFT, OTHER_TOP_RIGHT, OTHER_BOTTOM_LEFT, OTHER_BOTTOM_RIGHT,
+    DIAGONAL_FROM_TOP_LEFT, DIAGONAL_FROM_TOP_RIGHT
+  }
+
+  private int radius;
+  private int diameter;
+  private int margin;
+  private CornerType cornerType;
+
+  public RoundedCornersTransformation(int radius, int margin) {
+    this(radius, margin, CornerType.ALL);
+  }
+
+  public RoundedCornersTransformation(int radius, int margin, CornerType cornerType) {
+    this.radius = radius;
+    this.diameter = this.radius * 2;
+    this.margin = margin;
+    this.cornerType = cornerType;
+  }
+
+  @Override protected Bitmap transform(BitmapPool pool, Bitmap toTransform, int outWidth, int outHeight) {
+    int width = toTransform.getWidth();
+    int height = toTransform.getHeight();
+
+    Bitmap bitmap = pool.get(width, height, Bitmap.Config.ARGB_8888);
+    bitmap.setHasAlpha(true);
+
+    Canvas canvas = new Canvas(bitmap);
+    Paint paint = new Paint();
+    paint.setAntiAlias(true);
+    paint.setShader(new BitmapShader(toTransform, Shader.TileMode.CLAMP, Shader.TileMode.CLAMP));
+    drawRoundRect(canvas, paint, width, height);
+    return bitmap;
+  }
+
+  private void drawRoundRect(Canvas canvas, Paint paint, float width, float height) {
+    float right = width - margin;
+    float bottom = height - margin;
+
+    switch (cornerType) {
+      case ALL:
+        canvas.drawRoundRect(new RectF(margin, margin, right, bottom), radius, radius, paint);
+        break;
+      case TOP_LEFT:
+        drawTopLeftRoundRect(canvas, paint, right, bottom);
+        break;
+      case TOP_RIGHT:
+        drawTopRightRoundRect(canvas, paint, right, bottom);
+        break;
+      case BOTTOM_LEFT:
+        drawBottomLeftRoundRect(canvas, paint, right, bottom);
+        break;
+      case BOTTOM_RIGHT:
+        drawBottomRightRoundRect(canvas, paint, right, bottom);
+        break;
+      case TOP:
+        drawTopRoundRect(canvas, paint, right, bottom);
+        break;
+      case BOTTOM:
+        drawBottomRoundRect(canvas, paint, right, bottom);
+        break;
+      case LEFT:
+        drawLeftRoundRect(canvas, paint, right, bottom);
+        break;
+      case RIGHT:
+        drawRightRoundRect(canvas, paint, right, bottom);
+        break;
+      case OTHER_TOP_LEFT:
+        drawOtherTopLeftRoundRect(canvas, paint, right, bottom);
+        break;
+      case OTHER_TOP_RIGHT:
+        drawOtherTopRightRoundRect(canvas, paint, right, bottom);
+        break;
+      case OTHER_BOTTOM_LEFT:
+        drawOtherBottomLeftRoundRect(canvas, paint, right, bottom);
+        break;
+      case OTHER_BOTTOM_RIGHT:
+        drawOtherBottomRightRoundRect(canvas, paint, right, bottom);
+        break;
+      case DIAGONAL_FROM_TOP_LEFT:
+        drawDiagonalFromTopLeftRoundRect(canvas, paint, right, bottom);
+        break;
+      case DIAGONAL_FROM_TOP_RIGHT:
+        drawDiagonalFromTopRightRoundRect(canvas, paint, right, bottom);
+        break;
+      default:
+        canvas.drawRoundRect(new RectF(margin, margin, right, bottom), radius, radius, paint);
+        break;
+    }
+  }
+
+  private void drawTopLeftRoundRect(Canvas canvas, Paint paint, float right, float bottom) {
+    canvas.drawRoundRect(new RectF(margin, margin, margin + diameter, margin + diameter), radius,
+        radius, paint);
+    canvas.drawRect(new RectF(margin, margin + radius, margin + radius, bottom), paint);
+    canvas.drawRect(new RectF(margin + radius, margin, right, bottom), paint);
+  }
+
+  private void drawTopRightRoundRect(Canvas canvas, Paint paint, float right, float bottom) {
+    canvas.drawRoundRect(new RectF(right - diameter, margin, right, margin + diameter), radius,
+        radius, paint);
+    canvas.drawRect(new RectF(margin, margin, right - radius, bottom), paint);
+    canvas.drawRect(new RectF(right - radius, margin + radius, right, bottom), paint);
+  }
+
+  private void drawBottomLeftRoundRect(Canvas canvas, Paint paint, float right, float bottom) {
+    canvas.drawRoundRect(new RectF(margin, bottom - diameter, margin + diameter, bottom), radius,
+        radius, paint);
+    canvas.drawRect(new RectF(margin, margin, margin + diameter, bottom - radius), paint);
+    canvas.drawRect(new RectF(margin + radius, margin, right, bottom), paint);
+  }
+
+  private void drawBottomRightRoundRect(Canvas canvas, Paint paint, float right, float bottom) {
+    canvas.drawRoundRect(new RectF(right - diameter, bottom - diameter, right, bottom), radius,
+        radius, paint);
+    canvas.drawRect(new RectF(margin, margin, right - radius, bottom), paint);
+    canvas.drawRect(new RectF(right - radius, margin, right, bottom - radius), paint);
+  }
+
+  private void drawTopRoundRect(Canvas canvas, Paint paint, float right, float bottom) {
+    canvas.drawRoundRect(new RectF(margin, margin, right, margin + diameter), radius, radius,
+        paint);
+    canvas.drawRect(new RectF(margin, margin + radius, right, bottom), paint);
+  }
+
+  private void drawBottomRoundRect(Canvas canvas, Paint paint, float right, float bottom) {
+    canvas.drawRoundRect(new RectF(margin, bottom - diameter, right, bottom), radius, radius,
+        paint);
+    canvas.drawRect(new RectF(margin, margin, right, bottom - radius), paint);
+  }
+
+  private void drawLeftRoundRect(Canvas canvas, Paint paint, float right, float bottom) {
+    canvas.drawRoundRect(new RectF(margin, margin, margin + diameter, bottom), radius, radius,
+        paint);
+    canvas.drawRect(new RectF(margin + radius, margin, right, bottom), paint);
+  }
+
+  private void drawRightRoundRect(Canvas canvas, Paint paint, float right, float bottom) {
+    canvas.drawRoundRect(new RectF(right - diameter, margin, right, bottom), radius, radius, paint);
+    canvas.drawRect(new RectF(margin, margin, right - radius, bottom), paint);
+  }
+
+  private void drawOtherTopLeftRoundRect(Canvas canvas, Paint paint, float right, float bottom) {
+    canvas.drawRoundRect(new RectF(margin, bottom - diameter, right, bottom), radius, radius,
+        paint);
+    canvas.drawRoundRect(new RectF(right - diameter, margin, right, bottom), radius, radius, paint);
+    canvas.drawRect(new RectF(margin, margin, right - radius, bottom - radius), paint);
+  }
+
+  private void drawOtherTopRightRoundRect(Canvas canvas, Paint paint, float right, float bottom) {
+    canvas.drawRoundRect(new RectF(margin, margin, margin + diameter, bottom), radius, radius,
+        paint);
+    canvas.drawRoundRect(new RectF(margin, bottom - diameter, right, bottom), radius, radius,
+        paint);
+    canvas.drawRect(new RectF(margin + radius, margin, right, bottom - radius), paint);
+  }
+
+  private void drawOtherBottomLeftRoundRect(Canvas canvas, Paint paint, float right, float bottom) {
+    canvas.drawRoundRect(new RectF(margin, margin, right, margin + diameter), radius, radius,
+        paint);
+    canvas.drawRoundRect(new RectF(right - diameter, margin, right, bottom), radius, radius, paint);
+    canvas.drawRect(new RectF(margin, margin + radius, right - radius, bottom), paint);
+  }
+
+  private void drawOtherBottomRightRoundRect(Canvas canvas, Paint paint, float right,
+      float bottom) {
+    canvas.drawRoundRect(new RectF(margin, margin, right, margin + diameter), radius, radius,
+        paint);
+    canvas.drawRoundRect(new RectF(margin, margin, margin + diameter, bottom), radius, radius,
+        paint);
+    canvas.drawRect(new RectF(margin + radius, margin + radius, right, bottom), paint);
+  }
+
+  private void drawDiagonalFromTopLeftRoundRect(Canvas canvas, Paint paint, float right,
+      float bottom) {
+    canvas.drawRoundRect(new RectF(margin, margin, margin + diameter, margin + diameter), radius,
+        radius, paint);
+    canvas.drawRoundRect(new RectF(right - diameter, bottom - diameter, right, bottom), radius,
+        radius, paint);
+    canvas.drawRect(new RectF(margin, margin + radius, right - diameter, bottom), paint);
+    canvas.drawRect(new RectF(margin + diameter, margin, right, bottom - radius), paint);
+  }
+
+  private void drawDiagonalFromTopRightRoundRect(Canvas canvas, Paint paint, float right,
+      float bottom) {
+    canvas.drawRoundRect(new RectF(right - diameter, margin, right, margin + diameter), radius,
+        radius, paint);
+    canvas.drawRoundRect(new RectF(margin, bottom - diameter, margin + diameter, bottom), radius,
+        radius, paint);
+    canvas.drawRect(new RectF(margin, margin, right - radius, bottom - radius), paint);
+    canvas.drawRect(new RectF(margin + radius, margin + radius, right, bottom), paint);
+  }
+
+  @Override public String toString() {
+    return "RoundedTransformation(radius=" + radius + ", margin=" + margin + ", diameter="
+        + diameter + ", cornerType=" + cornerType.name() + ")";
+  }
+
+  @Override public boolean equals(Object o) {
+    return o instanceof RoundedCornersTransformation &&
+        ((RoundedCornersTransformation) o).radius == radius &&
+        ((RoundedCornersTransformation) o).diameter == diameter &&
+        ((RoundedCornersTransformation) o).margin == margin &&
+        ((RoundedCornersTransformation) o).cornerType == cornerType;
+  }
+
+  @Override public int hashCode() {
+    return ID.hashCode() + radius * 10000 + diameter * 1000 + margin * 100 + cornerType.ordinal() * 10;
+  }
+
+  @Override public void updateDiskCacheKey(MessageDigest messageDigest) {
+    messageDigest.update((ID + radius + diameter + margin + cornerType).getBytes(CHARSET));
+  }
+}

+ 45 - 0
common/src/main/java/com/miekir/common/widget/ScrollableViewPager.java

@@ -0,0 +1,45 @@
+package com.miekir.common.widget;
+
+import android.content.Context;
+import android.util.AttributeSet;
+import android.view.MotionEvent;
+
+import androidx.viewpager.widget.ViewPager;
+
+public class ScrollableViewPager extends ViewPager {
+
+    private boolean scrollable = false;
+
+
+    public ScrollableViewPager(Context context) {
+        super(context);
+    }
+
+    public ScrollableViewPager(Context context, AttributeSet attrs) {
+        super(context, attrs);
+    }
+
+    @Override
+    public boolean onTouchEvent(MotionEvent ev) {
+        if (!scrollable) {
+            return false;
+        }
+        return super.onTouchEvent(ev);
+    }
+
+    @Override
+    public boolean onInterceptTouchEvent(MotionEvent ev) {
+        if (!scrollable) {
+            return false;
+        }
+        return super.onInterceptTouchEvent(ev);
+    }
+
+    public boolean isCanScrollble() {
+        return scrollable;
+    }
+
+    public void setCanScrollble(boolean scrollble) {
+        this.scrollable = scrollble;
+    }
+}  

+ 3 - 0
common/src/main/res/values/strings.xml

@@ -0,0 +1,3 @@
+<resources>
+    <string name="app_name">common</string>
+</resources>

+ 17 - 0
common/src/test/java/com/miekir/common/ExampleUnitTest.java

@@ -0,0 +1,17 @@
+package com.miekir.common;
+
+import org.junit.Test;
+
+import static org.junit.Assert.*;
+
+/**
+ * Example local unit test, which will execute on the development machine (host).
+ *
+ * @see <a href="http://d.android.com/tools/testing">Testing documentation</a>
+ */
+public class ExampleUnitTest {
+    @Test
+    public void addition_isCorrect() {
+        assertEquals(4, 2 + 2);
+    }
+}

+ 20 - 0
gradle.properties

@@ -0,0 +1,20 @@
+# Project-wide Gradle settings.
+# IDE (e.g. Android Studio) users:
+# Gradle settings configured through the IDE *will override*
+# any settings specified in this file.
+# For more details on how to configure your build environment visit
+# http://www.gradle.org/docs/current/userguide/build_environment.html
+# Specifies the JVM arguments used for the daemon process.
+# The setting is particularly useful for tweaking memory settings.
+org.gradle.jvmargs=-Xmx1536m
+# When configured, Gradle will run in incubating parallel mode.
+# This option should only be used with decoupled projects. More details, visit
+# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
+# org.gradle.parallel=true
+# AndroidX package structure to make it clearer which packages are bundled with the
+# Android operating system, and which are packaged with your app's APK
+# https://developer.android.com/topic/libraries/support-library/androidx-rn
+android.useAndroidX=true
+# Automatically convert third-party libraries to use AndroidX
+android.enableJetifier=true
+

BIN
gradle/wrapper/gradle-wrapper.jar


+ 6 - 0
gradle/wrapper/gradle-wrapper.properties

@@ -0,0 +1,6 @@
+#Tue Oct 06 20:00:33 CST 2020
+distributionBase=GRADLE_USER_HOME
+distributionPath=wrapper/dists
+zipStoreBase=GRADLE_USER_HOME
+zipStorePath=wrapper/dists
+distributionUrl=https\://services.gradle.org/distributions/gradle-5.4.1-all.zip

+ 172 - 0
gradlew

@@ -0,0 +1,172 @@
+#!/usr/bin/env sh
+
+##############################################################################
+##
+##  Gradle start up script for UN*X
+##
+##############################################################################
+
+# 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
+
+APP_NAME="Gradle"
+APP_BASE_NAME=`basename "$0"`
+
+# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+DEFAULT_JVM_OPTS=""
+
+# 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
+nonstop=false
+case "`uname`" in
+  CYGWIN* )
+    cygwin=true
+    ;;
+  Darwin* )
+    darwin=true
+    ;;
+  MINGW* )
+    msys=true
+    ;;
+  NONSTOP* )
+    nonstop=true
+    ;;
+esac
+
+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" -a "$nonstop" = "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
+
+# Escape application args
+save () {
+    for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
+    echo " "
+}
+APP_ARGS=$(save "$@")
+
+# Collect all arguments for the java command, following the shell quoting and substitution rules
+eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
+
+# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong
+if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then
+  cd "$(dirname "$0")"
+fi
+
+exec "$JAVACMD" "$@"

+ 84 - 0
gradlew.bat

@@ -0,0 +1,84 @@
+@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
+
+set DIRNAME=%~dp0
+if "%DIRNAME%" == "" set DIRNAME=.
+set APP_BASE_NAME=%~n0
+set APP_HOME=%DIRNAME%
+
+@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=
+
+@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 Windows variants
+
+if not "%OS%" == "Windows_NT" goto win9xME_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=%*
+
+: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

+ 10 - 0
local.properties

@@ -0,0 +1,10 @@
+## This file is automatically generated by Android Studio.
+# Do not modify this file -- YOUR CHANGES WILL BE ERASED!
+#
+# This file should *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.
+sdk.dir=E\:\\software\\Android\\newsdk

+ 37 - 0
mvp/build.gradle

@@ -0,0 +1,37 @@
+apply plugin: 'com.android.library'
+
+android {
+    compileSdkVersion versions.compileSdk
+    buildToolsVersion versions.buildTools
+
+    defaultConfig {
+        minSdkVersion versions.minSdk
+        targetSdkVersion versions.targetSdk
+        versionCode 1
+        versionName "1.0"
+
+        testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
+        consumerProguardFiles 'consumer-rules.pro'
+    }
+
+    buildTypes {
+        release {
+            minifyEnabled false
+            proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
+        }
+    }
+
+}
+
+dependencies {
+    implementation fileTree(dir: 'libs', include: ['*.jar'])
+
+    implementation 'androidx.appcompat:appcompat:1.1.0'
+    testImplementation 'junit:junit:4.12'
+    androidTestImplementation 'androidx.test.ext:junit:1.1.1'
+    androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0'
+
+    def lifecycle_version = "2.2.0"
+    implementation "androidx.lifecycle:lifecycle-livedata-ktx:$lifecycle_version"
+    implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:$lifecycle_version"
+}

+ 21 - 0
mvp/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

+ 27 - 0
mvp/src/androidTest/java/com/miekir/mvp/ExampleInstrumentedTest.java

@@ -0,0 +1,27 @@
+package com.miekir.mvp;
+
+import android.content.Context;
+
+import androidx.test.platform.app.InstrumentationRegistry;
+import androidx.test.ext.junit.runners.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() {
+        // Context of the app under test.
+        Context appContext = InstrumentationRegistry.getInstrumentation().getTargetContext();
+
+        assertEquals("com.miekir.mvp.test", appContext.getPackageName());
+    }
+}

+ 2 - 0
mvp/src/main/AndroidManifest.xml

@@ -0,0 +1,2 @@
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+    package="com.miekir.mvp" />

+ 133 - 0
mvp/src/main/java/com/miekir/mvp/base/BaseActivity.java

@@ -0,0 +1,133 @@
+package com.miekir.mvp.base;
+
+import android.content.Context;
+import android.os.Bundle;
+import android.os.Looper;
+import android.view.LayoutInflater;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.WindowManager;
+import android.view.inputmethod.InputMethodManager;
+
+import androidx.appcompat.app.AppCompatActivity;
+
+import com.miekir.mvp.widget.LoadingDialog;
+
+/**
+ * 适配器模式,这个类会适配子类{@link com.miekir.mvp.view.BaseMVPActivity}的功能,帮子类实现具体的弹出加载框、弹出提示等基本操作
+ */
+public abstract class BaseActivity extends AppCompatActivity {
+    private LoadingDialog mLoadingDialog;
+    private View rootView;
+    //private Unbinder mBinder;
+
+    @Override
+    protected void onCreate(Bundle savedInstanceState) {
+        // 状态栏深色模式,改变状态栏文字颜色
+//        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
+//            getWindow().getDecorView().setSystemUiVisibility( View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN|View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR);
+//        }
+
+        super.onCreate(savedInstanceState);
+        rootView = LayoutInflater.from(this).inflate(getLayoutID(), null);
+        setContentView(rootView);
+
+        View.OnTouchListener rootTouchListener = new View.OnTouchListener() {
+            @Override
+            public boolean onTouch(View v, MotionEvent event) {
+                // 点击空白处隐藏输入法(要配合Activity根布局增加以下标志使用
+                // android:focusable="true"
+                // android:focusableInTouchMode="true")
+                hideInputMethod();
+                return false;
+            }
+        };
+        rootView.setOnTouchListener(rootTouchListener);
+
+        //进入页面隐藏输入框
+        getWindow().setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_HIDDEN);
+        //mBinder = ButterKnife.bind(this);
+        mLoadingDialog = new LoadingDialog(this);
+        initViews(savedInstanceState);
+    }
+
+    protected void hideInputMethod() {
+        if (rootView != null) {
+            rootView.requestFocus();
+        }
+
+        final InputMethodManager imm = (InputMethodManager) getApplicationContext().getSystemService(Context.INPUT_METHOD_SERVICE);
+        if (imm == null) {
+            return;
+        }
+
+        final View currentFocusView = getCurrentFocus();
+        if (currentFocusView != null) {
+            imm.hideSoftInputFromWindow(currentFocusView.getWindowToken(), InputMethodManager.HIDE_NOT_ALWAYS);
+        }
+    }
+
+    @Override
+    protected void onPause() {
+        // 必须要在onPause隐藏键盘,在onDestroy就太晚了
+        hideInputMethod();
+        super.onPause();
+    }
+
+    @Override
+    protected void onDestroy() {
+        super.onDestroy();
+        //mBinder.unbind();
+        dismissLoading();
+        getWindow().setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_HIDDEN);
+    }
+
+    public abstract int getLayoutID();
+
+    public abstract void initViews(Bundle savedInstanceState);
+
+    private boolean isMainThread() {
+        return Looper.myLooper() == Looper.getMainLooper();
+    }
+
+    public void showLoading() {
+        if (isMainThread()) {
+            mLoadingDialog.show();
+        } else {
+            runOnUiThread(new Runnable() {
+                @Override
+                public void run() {
+                    mLoadingDialog.show();
+                }
+            });
+        }
+    }
+
+    public void showLoading(final String msg) {
+        if (isMainThread()) {
+            mLoadingDialog.show(msg);
+        } else {
+            runOnUiThread(new Runnable() {
+                @Override
+                public void run() {
+                    mLoadingDialog.show(msg);
+                }
+            });
+        }
+    }
+
+    public void dismissLoading() {
+        if (mLoadingDialog != null) {
+            if (isMainThread()) {
+                mLoadingDialog.close();
+            } else {
+                runOnUiThread(new Runnable() {
+                    @Override
+                    public void run() {
+                        mLoadingDialog.close();
+                    }
+                });
+            }
+        }
+    }
+}

+ 107 - 0
mvp/src/main/java/com/miekir/mvp/base/BaseFragment.java

@@ -0,0 +1,107 @@
+package com.miekir.mvp.base;
+
+import android.os.Bundle;
+import android.os.Looper;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+
+import androidx.annotation.LayoutRes;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.fragment.app.Fragment;
+import androidx.fragment.app.FragmentActivity;
+
+import com.miekir.mvp.view.IView;
+import com.miekir.mvp.widget.LoadingDialog;
+
+
+public abstract class BaseFragment extends Fragment implements IView {
+    public FragmentActivity activity;
+    //private Unbinder mBinder;
+    private LoadingDialog mLoadingDialog;
+    protected View rootView;
+
+    /**
+     * 设置布局layout
+     *
+     * @return 布局文件id
+     */
+    public abstract @LayoutRes
+    int getLayoutResId();
+
+    @Nullable
+    @Override
+    public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
+        rootView = inflater.inflate(getLayoutResId(), container, false);
+        activity = getActivity();
+        return rootView;
+    }
+
+    @Override
+    public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
+        super.onViewCreated(view, savedInstanceState);
+        //mBinder = ButterKnife.bind(this, view);
+        mLoadingDialog = new LoadingDialog(getActivity());
+        onCreateViewFinished(savedInstanceState);
+    }
+
+    public abstract void onCreateViewFinished(Bundle state);
+
+
+    private boolean isMainThread() {
+        return Looper.myLooper() == Looper.getMainLooper();
+    }
+
+    @Override
+    public void showLoading() {
+        if (isMainThread()) {
+            mLoadingDialog.show();
+        } else {
+            activity.runOnUiThread(new Runnable() {
+                @Override
+                public void run() {
+                    mLoadingDialog.show();
+                }
+            });
+        }
+    }
+
+    @Override
+    public void showLoading(final String msg) {
+        if (isMainThread()) {
+            mLoadingDialog.show(msg);
+        } else {
+            activity.runOnUiThread(new Runnable() {
+                @Override
+                public void run() {
+                    mLoadingDialog.show(msg);
+                }
+            });
+        }
+    }
+
+    @Override
+    public void dismissLoading() {
+        if (mLoadingDialog != null) {
+            if (isMainThread()) {
+                mLoadingDialog.close();
+            } else {
+                activity.runOnUiThread(new Runnable() {
+                    @Override
+                    public void run() {
+                        mLoadingDialog.close();
+                    }
+                });
+            }
+        }
+    }
+
+    @Override
+    public void onDestroyView() {
+        super.onDestroyView();
+        //mBinder.unbind();
+        dismissLoading();
+        activity = null;
+    }
+}

+ 24 - 0
mvp/src/main/java/com/miekir/mvp/presenter/BasePresenter.java

@@ -0,0 +1,24 @@
+package com.miekir.mvp.presenter;
+
+import com.miekir.mvp.view.IView;
+
+import java.lang.ref.WeakReference;
+
+public abstract class BasePresenter<V extends IView> {
+    private WeakReference<V> wrf;
+
+    public void attachView(V view) {
+        wrf = new WeakReference<V>(view);
+    }
+
+    public V getView() {
+        return wrf == null ? null : wrf.get();
+    }
+
+    public void detachView() {
+        if (wrf != null) {
+            wrf.clear();
+            wrf = null;
+        }
+    }
+}

+ 15 - 0
mvp/src/main/java/com/miekir/mvp/presenter/InjectPresenter.java

@@ -0,0 +1,15 @@
+package com.miekir.mvp.presenter;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * @description Presenter注入标记
+ * @date 2019/8/19
+ */
+@Retention(RetentionPolicy.RUNTIME)
+@Target(ElementType.FIELD)
+public @interface InjectPresenter {
+}

+ 70 - 0
mvp/src/main/java/com/miekir/mvp/view/BaseMVPActivity.java

@@ -0,0 +1,70 @@
+package com.miekir.mvp.view;
+
+import android.os.Bundle;
+
+import com.miekir.mvp.base.BaseActivity;
+import com.miekir.mvp.presenter.BasePresenter;
+import com.miekir.mvp.presenter.InjectPresenter;
+
+import java.lang.reflect.Field;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * 利用反射,BaseMVPActivity将会查找子类中有{@link InjectPresenter}注解的成员变量,
+ * 筛选出父类是{@link BasePresenter}的成员变量,对它们进行实例化,此时BaseMVPActivity的子类即拥有了
+ * 具体Presenter的引用,如拥有了LoginPresenter的引用,可以调用具体Presenter的具体方法;
+ * 然后BaseMVPActivity调用{@link BasePresenter}的一个方法把自身的{@link IView}类型引用
+ * 传递给{@link BasePresenter},这样{@link BasePresenter}的子类也就拥有了{@link IView}类型引用;
+ * 注意:因为注解是在BaseMVPActivity的子类声明的,子类实现了{@link IView}的子类如ILoginView,
+ * 这个子类是一个拥有特殊回调接口的,如拥有登录成功、登录失败等接口,
+ * 也就是说把具体的子类传this引用的时候,把具体的ILoginView引用传给了具体的Presenter,并通过{@link BasePresenter}
+ * 的泛型让getView方法返回具体的ILoginView,所以{@link BasePresenter}的子类还可以访问到ILoginView的特殊方法
+ */
+public abstract class BaseMVPActivity extends BaseActivity implements IView {
+
+    private List<BasePresenter> mInjectPresenters;
+
+    @Override
+    protected void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+        mInjectPresenters = new ArrayList<>();
+
+        // 这里可以获取到子类的成员变量
+        Field[] fields = this.getClass().getDeclaredFields();
+        for (Field field : fields) {
+            //获取变量上面的注解类型
+            InjectPresenter injectPresenter = field.getAnnotation(InjectPresenter.class);
+            if (injectPresenter != null) {
+                try {
+                    Class<? extends BasePresenter> type = (Class<? extends BasePresenter>) field.getType();
+                    BasePresenter mInjectPresenter = type.newInstance();
+                    mInjectPresenter.attachView(this);
+                    field.setAccessible(true);
+                    field.set(this, mInjectPresenter);
+                    mInjectPresenters.add(mInjectPresenter);
+                } catch (IllegalAccessException e) {
+                    e.printStackTrace();
+                } catch (InstantiationException e) {
+                    e.printStackTrace();
+                }catch (ClassCastException e){
+                    e.printStackTrace();
+                    throw new RuntimeException("SubClass must extends Class:BasePresenter");
+                }
+            }
+        }
+    }
+
+    @Override
+    protected void onDestroy() {
+        super.onDestroy();
+        for (BasePresenter presenter : mInjectPresenters) {
+            presenter.detachView();
+        }
+        mInjectPresenters.clear();
+        mInjectPresenters = null;
+    }
+
+
+
+}

+ 102 - 0
mvp/src/main/java/com/miekir/mvp/view/BaseMVPFragment.java

@@ -0,0 +1,102 @@
+package com.miekir.mvp.view;
+
+import android.os.Bundle;
+
+import androidx.annotation.Nullable;
+
+import com.miekir.mvp.base.BaseFragment;
+import com.miekir.mvp.presenter.BasePresenter;
+import com.miekir.mvp.presenter.InjectPresenter;
+
+import java.lang.reflect.Field;
+import java.util.ArrayList;
+import java.util.List;
+
+
+public abstract class BaseMVPFragment extends BaseFragment implements IView {
+    /**
+     * 是否被创建了
+     */
+    protected boolean isViewCreated;
+    /**
+     * 当前是否可见
+     */
+    protected boolean isUIVisible;
+
+    private List<BasePresenter> mInjectPresenters;
+
+    @Override
+    public  void onCreateViewFinished(@Nullable Bundle savedInstanceState) {
+        onViewInit();
+
+        mInjectPresenters = new ArrayList<>();
+        Field[] fields = this.getClass().getDeclaredFields();
+        for (Field field : fields) {
+            //获取变量上面的注解类型
+            InjectPresenter injectPresenter = field.getAnnotation(InjectPresenter.class);
+            if (injectPresenter != null) {
+                try {
+                    Class<? extends BasePresenter> type = (Class<? extends BasePresenter>) field.getType();
+                    BasePresenter mInjectPresenter = type.newInstance();
+                    mInjectPresenter.attachView(this);
+                    field.setAccessible(true);
+                    field.set(this, mInjectPresenter);
+                    mInjectPresenters.add(mInjectPresenter);
+                } catch (IllegalAccessException e) {
+                    e.printStackTrace();
+                } catch (java.lang.InstantiationException e) {
+                    e.printStackTrace();
+                }catch (ClassCastException e){
+                    e.printStackTrace();
+                    throw new RuntimeException("SubClass must extends Class:BasePresenter");
+                }
+            }
+        }
+
+        isViewCreated = true;
+        loadData();
+    }
+
+    /**
+     * View初始化
+     */
+    protected abstract void onViewInit();
+
+    @Override
+    public void setUserVisibleHint(boolean isVisibleToUser) {
+        super.setUserVisibleHint(isVisibleToUser);
+        isUIVisible = isVisibleToUser;
+        if (isVisibleToUser) {
+            loadData();
+        }
+    }
+
+    /**
+     * 懒加载,当Fragment可见的时候,再去加载数据
+     * 应用初始化会先调用完所有的setUserVisibleHint再调用onViewCreated,然后切换的时候,就只调用setUserVisibleHint了
+     */
+    private void loadData() {
+        if (isViewCreated && isUIVisible) {
+            isViewCreated = false;
+            isUIVisible = false;
+            onLazyLoad();
+        }
+    }
+
+    /**
+     * 懒加载,初始化加载数据
+     */
+    protected abstract void onLazyLoad();
+
+    @Override
+    public void onDestroyView() {
+        super.onDestroyView();
+        if (mInjectPresenters != null && mInjectPresenters.size() > 0) {
+            for (BasePresenter presenter : mInjectPresenters) {
+                presenter.detachView();
+            }
+            mInjectPresenters.clear();
+            mInjectPresenters = null;
+        }
+    }
+}

+ 9 - 0
mvp/src/main/java/com/miekir/mvp/view/IView.java

@@ -0,0 +1,9 @@
+package com.miekir.mvp.view;
+
+public interface IView {
+    void showLoading();
+
+    void showLoading(String msg);
+
+    void dismissLoading();
+}

+ 112 - 0
mvp/src/main/java/com/miekir/mvp/widget/LVCircularRing.java

@@ -0,0 +1,112 @@
+package com.miekir.mvp.widget;
+
+import android.animation.Animator;
+import android.animation.AnimatorListenerAdapter;
+import android.animation.ValueAnimator;
+import android.content.Context;
+import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.Paint;
+import android.graphics.RectF;
+import android.util.AttributeSet;
+import android.view.View;
+import android.view.animation.LinearInterpolator;
+
+public class LVCircularRing extends View {
+
+    private float mWidth = 0f;
+    private float mPadding = 0f;
+    private float startAngle = 0f;
+    private Paint mPaint;
+
+    public LVCircularRing(Context context) {
+        this(context, null);
+    }
+
+    public LVCircularRing(Context context, AttributeSet attrs) {
+        this(context, attrs, 0);
+    }
+
+    public LVCircularRing(Context context, AttributeSet attrs, int defStyleAttr) {
+        super(context, attrs, defStyleAttr);
+        initPaint();
+    }
+
+    @Override
+    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
+        if (getMeasuredWidth() > getHeight()){
+            mWidth = getMeasuredHeight();
+        }else{
+            mWidth = getMeasuredWidth();
+        }
+        mPadding = 5;
+    }
+
+    @Override
+    protected void onDraw(Canvas canvas) {
+        super.onDraw(canvas);
+        mPaint.setColor(Color.argb(100, 255, 255, 255));
+        canvas.drawCircle(mWidth / 2, mWidth / 2, mWidth / 2 - mPadding, mPaint);
+        mPaint.setColor(Color.WHITE);
+        RectF rectF = new RectF(mPadding, mPadding, mWidth - mPadding, mWidth - mPadding);
+        canvas.drawArc(rectF, startAngle, 100
+                , false, mPaint);//第四个参数是否显示半径
+
+    }
+
+    private void initPaint() {
+        mPaint = new Paint();
+        mPaint.setAntiAlias(true);
+        mPaint.setStyle(Paint.Style.STROKE);
+        mPaint.setColor(Color.WHITE);
+        mPaint.setStrokeWidth(8);
+    }
+
+    public void startAnim() {
+        stopAnim();
+        startViewAnim(0f, 1f, 1000);
+    }
+
+    public void stopAnim() {
+        if (valueAnimator != null) {
+            clearAnimation();
+            valueAnimator.setRepeatCount(1);
+            valueAnimator.cancel();
+            valueAnimator.end();
+        }
+    }
+
+    ValueAnimator valueAnimator;
+
+    private ValueAnimator startViewAnim(float startF, final float endF, long time) {
+        valueAnimator = ValueAnimator.ofFloat(startF, endF);
+        valueAnimator.setDuration(time);
+        valueAnimator.setInterpolator(new LinearInterpolator());
+        valueAnimator.setRepeatCount(ValueAnimator.INFINITE);//无限循环
+        valueAnimator.setRepeatMode(ValueAnimator.RESTART);//
+
+        valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
+            @Override
+            public void onAnimationUpdate(ValueAnimator valueAnimator) {
+                float value = (float) valueAnimator.getAnimatedValue();
+                startAngle = 360 * value;
+                invalidate();
+            }
+        });
+
+        valueAnimator.addListener(new AnimatorListenerAdapter(){
+            @Override
+            public void onAnimationEnd(Animator animation) {
+                super.onAnimationEnd(animation);
+            }
+        });
+
+        if (!valueAnimator.isRunning()) {
+            valueAnimator.start();
+        }
+
+        return valueAnimator;
+    }
+}
+

+ 64 - 0
mvp/src/main/java/com/miekir/mvp/widget/LoadingDialog.java

@@ -0,0 +1,64 @@
+package com.miekir.mvp.widget;
+
+import android.app.Dialog;
+import android.content.Context;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.widget.LinearLayout;
+import android.widget.TextView;
+
+import com.miekir.mvp.R;
+
+
+public class LoadingDialog {
+    LVCircularRing mLoadingView;
+    private TextView loadingText;
+    Dialog mLoadingDialog;
+    Context mContext;
+    private String msg = "加载中...";
+
+    public LoadingDialog(Context context) {
+        mContext = context;
+        // 首先得到整个View
+        View view = LayoutInflater.from(context).inflate(
+                R.layout.dialog_loading_view, null);
+        // 获取整个布局
+        LinearLayout dialogLayout = (LinearLayout) view.findViewById(R.id.dialog_view);
+        // 页面中的LoadingView
+        mLoadingView = (LVCircularRing) view.findViewById(R.id.lv_circularring);
+        // 页面中显示文本
+        loadingText = (TextView) view.findViewById(R.id.loading_text);
+        // 显示文本
+        loadingText.setText(msg);
+        // 创建自定义样式的Dialog
+        mLoadingDialog = new Dialog(context, R.style.loading_dialog);
+        // 设置返回键无效
+        mLoadingDialog.setCancelable(false);
+        mLoadingDialog.setContentView(dialogLayout, new LinearLayout.LayoutParams(
+                LinearLayout.LayoutParams.MATCH_PARENT,
+                LinearLayout.LayoutParams.MATCH_PARENT));
+    }
+
+
+    public void show() {
+        if (mLoadingDialog != null && !mLoadingDialog.isShowing()) {
+            mLoadingDialog.show();
+            mLoadingView.startAnim();
+        }
+    }
+
+    public void show(String message) {
+        if (mLoadingDialog != null && !mLoadingDialog.isShowing()) {
+            loadingText.setText(message);
+            mLoadingDialog.show();
+            mLoadingView.startAnim();
+        }
+    }
+
+    public void close() {
+        if (mLoadingDialog != null) {
+            mLoadingView.stopAnim();
+            mLoadingDialog.dismiss();
+        }
+    }
+}

+ 12 - 0
mvp/src/main/res/drawable/shape_dialog_bg.xml

@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="utf-8"?>
+<shape xmlns:android="http://schemas.android.com/apk/res/android">
+    <!-- 内部颜色 -->
+    <solid android:color="#A61B1B1B" />
+
+    <!-- 圆角的幅度 -->
+    <corners
+        android:bottomLeftRadius="3dp"
+        android:bottomRightRadius="3dp"
+        android:topLeftRadius="3dp"
+        android:topRightRadius="3dp" />
+</shape>

+ 34 - 0
mvp/src/main/res/layout/dialog_loading_view.xml

@@ -0,0 +1,34 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:id="@+id/dialog_view"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    android:gravity="center"
+    android:background="@drawable/shape_dialog_bg"
+    android:padding="16dp"
+    android:orientation="vertical">
+
+    <FrameLayout
+        android:layout_width="55dp"
+        android:layout_height="35dp">
+        <com.miekir.mvp.widget.LVCircularRing
+            android:id="@+id/lv_circularring"
+            android:layout_width="35dp"
+            android:layout_height="35dp"
+            android:layout_gravity="center"/>
+    </FrameLayout>
+
+
+    <TextView
+        android:id="@+id/loading_text"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:singleLine="true"
+        android:ellipsize="end"
+        android:textColor="#ffffff"
+        android:layout_marginTop="6dp"
+        android:textSize="11sp"/>
+
+</LinearLayout>
+

+ 3 - 0
mvp/src/main/res/values/strings.xml

@@ -0,0 +1,3 @@
+<resources>
+    <string name="app_name">mvp</string>
+</resources>

+ 47 - 0
mvp/src/main/res/values/styles.xml

@@ -0,0 +1,47 @@
+<resources>
+
+    <!-- Base application theme. -->
+    <!--<style name="AppTheme" parent="Theme.AppCompat.Light">
+        <item name="windowNoTitle">true</item>
+        <item name="windowActionBar">false</item>
+        <item name="android:windowFullscreen">true</item>
+        <item name="android:windowContentOverlay">@null</item>
+    </style>-->
+
+    <style name="DialogActivity" parent="android:Theme.Dialog">
+        <!-- 下面这两个是最重要的 -->
+        <item name="android:windowIsFloating">true</item> <!-- 是否浮现在activity之上 (内容会居中显示并靠左右有距离)-->
+        <item name="android:windowNoTitle">true</item> <!-- 无标题 -->
+        <!-- 设置透明下面两个一定要同时设置 -->
+        <item name="android:windowBackground">@android:color/transparent</item> <!-- 背景透明 -->
+        <item name="android:windowIsTranslucent">true</item> <!-- 半透明 -->
+        <item name="android:backgroundDimEnabled">true</item> <!-- 模糊 -->
+        <item name="android:windowFrame">@null</item>
+        <item name="android:windowCloseOnTouchOutside">false</item>
+    </style>
+
+    <!--BaseDialog样式-->
+    <style name="BaseDialog" parent="@android:style/Theme.Dialog">
+        <item name="android:windowNoTitle">true</item>
+        <item name="android:windowBackground">@android:color/transparent</item>
+        <item name="android:colorBackgroundCacheHint">@null</item>
+        <item name="android:windowSoftInputMode">adjustPan</item>
+        <item name="android:windowIsFloating">true</item>
+    </style>
+
+    <style name="common_FullScreenDialogStyle">
+        <item name="android:windowFullscreen">false</item>
+        <item name="android:windowNoTitle">true</item>
+    </style>
+
+    <style name="loading_dialog" parent="android:style/Theme.Dialog">
+        <item name="android:windowFrame">@null</item>
+        <item name="android:windowNoTitle">true</item>
+        <item name="android:windowBackground">@android:color/transparent</item>
+        <item name="android:windowIsFloating">true</item>
+        <item name="android:backgroundDimEnabled">false</item>
+        <item name="android:windowContentOverlay">@null</item>
+    </style>
+
+
+</resources>

+ 17 - 0
mvp/src/test/java/com/miekir/mvp/ExampleUnitTest.java

@@ -0,0 +1,17 @@
+package com.miekir.mvp;
+
+import org.junit.Test;
+
+import static org.junit.Assert.*;
+
+/**
+ * Example local unit test, which will execute on the development machine (host).
+ *
+ * @see <a href="http://d.android.com/tools/testing">Testing documentation</a>
+ */
+public class ExampleUnitTest {
+    @Test
+    public void addition_isCorrect() {
+        assertEquals(4, 2 + 2);
+    }
+}

+ 2 - 0
settings.gradle

@@ -0,0 +1,2 @@
+include ':app', ':mvp', ':common'
+rootProject.name='NewMvp'