├── .github └── workflows │ └── android.yml ├── .gitignore ├── .idea ├── .gitignore ├── codeStyles │ ├── Project.xml │ └── codeStyleConfig.xml ├── compiler.xml ├── deploymentTargetDropDown.xml ├── gradle.xml ├── inspectionProfiles │ └── Project_Default.xml ├── jarRepositories.xml ├── ktlint.xml ├── misc.xml ├── sonarlint │ └── issuestore │ │ ├── 0 │ │ ├── 0 │ │ │ └── 00bb5a55bbe67254c2c0f18c038e34feb530225d │ │ └── 5 │ │ │ └── 05efc8b1657769a27696d478ded1e95f38737233 │ │ ├── 1 │ │ └── f │ │ │ └── 1ff699be707da39c21730447e3621d6e9c1c97a0 │ │ ├── 2 │ │ └── 3 │ │ │ └── 23c044a63385dd34c0d6c43e119c336cfa71e078 │ │ ├── 3 │ │ ├── 7 │ │ │ └── 37e0b1be3ca00e70c871059689243c15c34fb44a │ │ ├── b │ │ │ └── 3b68b8031302659b137c3fe69d6becbaf2b9698b │ │ ├── e │ │ │ └── 3eb353299266c592fdca2efa765a13c86f79c544 │ │ └── f │ │ │ ├── 3f3614b660c98ad2ec2ec3066dba56ad2e3556ca │ │ │ └── 3fc4d5b5fd8ab8664c5f8c8311e06787ab8690b3 │ │ ├── 4 │ │ ├── 0 │ │ │ └── 40a8402d1b0b07de9d145da63f2160b6295c3891 │ │ ├── 3 │ │ │ └── 43ff339470fe0f3ddf25ea6f557a45637fdd013a │ │ └── 7 │ │ │ └── 47bbea10edd9b7e1f95973e3bc34e51228d8be69 │ │ ├── 5 │ │ ├── 1 │ │ │ └── 51e1c5d383dfaa35e0e7e5873a0a99355a86880f │ │ ├── 2 │ │ │ └── 5282ed8d7d3284c85a70c413ef67c04b412e32f6 │ │ ├── 6 │ │ │ └── 566d6c7352aef9d8aa71dde9d56660f1ef22e62a │ │ └── 8 │ │ │ └── 5804b5b5d9744a69cf54dab9c9f4c6ad2ddc7f9a │ │ ├── 6 │ │ ├── 1 │ │ │ └── 61d258e93d3569d213e6b23af507edcc557e5e07 │ │ ├── 2 │ │ │ └── 623b11249d525083e5fe898c5cd1762660f51ac9 │ │ ├── 6 │ │ │ ├── 66735bbb2138471309e49d49ac3a370569cc125d │ │ │ └── 66e2568e0d3f2652c945513ba940f8f1e4f6813b │ │ ├── 8 │ │ │ └── 68ffd980bc165482d8ae7b3a067170a25a807e2e │ │ └── e │ │ │ └── 6ecd6000a7b6f4a2884412ff19f74193ed089648 │ │ ├── 7 │ │ └── 3 │ │ │ └── 73fe5d7605274dc2cf5a3a110a86d3ec5c0bd0f3 │ │ ├── 8 │ │ └── e │ │ │ └── 8ec9a00bfd09b3190ac6b22251dbb1aa95a0579d │ │ ├── 9 │ │ └── e │ │ │ └── 9e08934d811afe28fbc77aaa3c0d747b94348db9 │ │ ├── a │ │ ├── 5 │ │ │ └── a52f34ee8999426f9468ca72a1b11663f57ba0c4 │ │ ├── d │ │ │ └── ad4f6a39172f1fe4db70a527df96a9649f3d3a51 │ │ └── f │ │ │ └── af824a66dbada2cce18cd1f25c80c537f9fad500 │ │ ├── b │ │ └── 4 │ │ │ └── b4a4dd424d0575baea7f6bd8cfcb4f9e71b79bc8 │ │ ├── c │ │ └── 3 │ │ │ └── c3085a1c4f578eadc45c5499e731342f9401e3bc │ │ ├── e │ │ ├── a │ │ │ └── eaf9a1dbcdebe8d5b11f84b26a2c1d99b9a4239d │ │ └── c │ │ │ └── ecbd5dff7dda95f168e5b6d66ad952c7b937c260 │ │ ├── f │ │ ├── 0 │ │ │ ├── f07866736216be0ee2aba49e392191aeae700a35 │ │ │ └── f0a9ce0e88c5e949614ef2d596a07dc5b52bf1ee │ │ └── c │ │ │ └── fcead86d2672c263fadb598b07bf805b5822dec7 │ │ └── index.pb └── vcs.xml ├── README.md ├── apk └── xiaomi │ └── 345 ├── app ├── .gitignore ├── build.gradle ├── proguard-rules.pro └── src │ ├── androidTest │ └── java │ │ └── com │ │ └── lvkang │ │ └── example │ │ └── ExampleInstrumentedTest.kt │ └── main │ ├── AndroidManifest.xml │ ├── assets │ ├── skin.skin │ ├── skin1.skin │ ├── skin2.skin │ ├── skin3.skin │ ├── skin4.zip │ ├── skin5.skin │ ├── skin6.skin │ └── skin7.skin │ ├── java │ └── com │ │ └── lvkang │ │ └── example │ │ ├── BaseApplication.kt │ │ ├── MainActivity.kt │ │ ├── Test.kt │ │ ├── TestActivity.kt │ │ └── TestAdapter.kt │ └── res │ ├── drawable │ ├── skin_edit_background.xml │ ├── skin_edit_cursor.xml │ ├── skin_main_background.xml │ └── skin_main_image.jpg │ ├── layout │ ├── activity_main.xml │ ├── activity_test.xml │ ├── layout_include.xml │ └── test_item.xml │ ├── mipmap-hdpi │ ├── ic_launcher.png │ └── ic_launcher_round.png │ ├── mipmap-mdpi │ ├── ic_launcher.png │ └── ic_launcher_round.png │ ├── mipmap-xhdpi │ ├── ic_launcher.png │ └── ic_launcher_round.png │ ├── mipmap-xxhdpi │ ├── ic_launcher.png │ └── ic_launcher_round.png │ ├── mipmap-xxxhdpi │ ├── ic_launcher.png │ └── ic_launcher_round.png │ ├── values-night │ └── themes.xml │ └── values │ ├── colors.xml │ ├── dimens.xml │ ├── strings.xml │ └── themes.xml ├── build.gradle ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── settings.gradle └── skin ├── .gitignore ├── build.gradle ├── consumer-rules.pro ├── libs └── zip4j-2.9.0.jar ├── proguard-rules.pro └── src ├── androidTest └── java │ └── com │ └── lvkang │ └── skin │ └── ExampleInstrumentedTest.kt ├── main ├── AndroidManifest.xml ├── java │ └── com │ │ └── lvkang │ │ └── skin │ │ ├── SkinManager.kt │ │ ├── app │ │ ├── SkinActivityLifecycle.kt │ │ └── SkinCompatActivity.kt │ │ ├── config │ │ ├── SkinKey.kt │ │ └── SkinPreUtils.kt │ │ ├── factory │ │ └── SkinCompateFactory.kt │ │ ├── inflater │ │ ├── SkinAppCompatViewInflater.kt │ │ ├── SkinLayoutInflater.kt │ │ └── SkinViewInflater.kt │ │ ├── ktx │ │ └── SkinHelper.kt │ │ ├── listener │ │ └── SkinLoadListener.kt │ │ ├── obsreve │ │ ├── SkinObserver.kt │ │ └── SkinObserverable.kt │ │ ├── resource │ │ ├── AbstractSkinLoadStrategy.kt │ │ ├── SkinCompatResources.kt │ │ ├── SkinLoadStrategyEnum.kt │ │ └── strategy │ │ │ ├── AbstractSkinLoadAssetsImpl.kt │ │ │ ├── AbstractSkinLoadNoneImpl.kt │ │ │ ├── AbstractSkinLoadStorageImpl.kt │ │ │ └── AbstractSkinLoadZipImpl.kt │ │ ├── util │ │ └── SkinLog.kt │ │ └── wedget │ │ ├── SkinCompatHelper.kt │ │ ├── SkinCompatSupportable.kt │ │ ├── android │ │ ├── FrameLayoutX.kt │ │ ├── RelativeLayoutX.kt │ │ └── ViewX.kt │ │ ├── androidx │ │ ├── ButtonX.kt │ │ ├── ConstraintLayoutX.kt │ │ ├── EditTextX.kt │ │ ├── ImageViewX.kt │ │ ├── LinearLayoutX.kt │ │ ├── NestedScrollViewX.kt │ │ ├── TextViewX.kt │ │ └── cardview │ │ │ ├── CardViewX.kt │ │ │ └── RoundRectDrawable.kt │ │ └── helper │ │ ├── SkinCompatBackgroundHelper.kt │ │ ├── SkinCompatCardHelper.kt │ │ ├── SkinCompatEditTextHelpter.kt │ │ ├── SkinCompatImageHelper.kt │ │ └── SkinCompatTextHelper.kt └── res │ └── values │ └── attrs.xml └── test └── java └── com └── lvkang └── skin └── ExampleUnitTest.kt /.github/workflows/android.yml: -------------------------------------------------------------------------------- 1 | name: Android CI 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | branches: [ master ] 8 | 9 | jobs: 10 | build: 11 | 12 | runs-on: ubuntu-latest 13 | 14 | steps: 15 | - uses: actions/checkout@v2 16 | - name: set up JDK 11 17 | uses: actions/setup-java@v2 18 | with: 19 | java-version: '11' 20 | distribution: 'adopt' 21 | cache: gradle 22 | 23 | - name: Grant execute permission for gradlew 24 | run: chmod +x gradlew 25 | - name: Build with Gradle 26 | run: ./gradlew assembleRelease 27 | 28 | - name: Upload a Build Artifact 29 | uses: actions/upload-artifact@v2.3.1 30 | with: 31 | # Artifact name 32 | name: upload-apk 33 | # A file, directory or wildcard pattern that describes what to upload 34 | path: | 35 | ${{github.workspace}}/apk/*.apk 36 | ${{github.workspace}}/apk/xiaomi/*.apk 37 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .gradle 3 | /local.properties 4 | /.idea/caches 5 | /.idea/libraries 6 | /.idea/modules.xml 7 | /.idea/workspace.xml 8 | /.idea/navEditor.xml 9 | /.idea/assetWizardSettings.xml 10 | .DS_Store 11 | /build 12 | /captures 13 | .externalNativeBuild 14 | .cxx 15 | local.properties 16 | -------------------------------------------------------------------------------- /.idea/.gitignore: -------------------------------------------------------------------------------- 1 | # Default ignored files 2 | /shelf/ 3 | /workspace.xml 4 | -------------------------------------------------------------------------------- /.idea/codeStyles/Project.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 7 | 119 | 120 | 122 | 123 | -------------------------------------------------------------------------------- /.idea/codeStyles/codeStyleConfig.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | -------------------------------------------------------------------------------- /.idea/compiler.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /.idea/deploymentTargetDropDown.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /.idea/gradle.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 20 | 21 | -------------------------------------------------------------------------------- /.idea/inspectionProfiles/Project_Default.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 45 | -------------------------------------------------------------------------------- /.idea/jarRepositories.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 9 | 10 | 14 | 15 | 19 | 20 | 24 | 25 | 29 | 30 | 34 | 35 | -------------------------------------------------------------------------------- /.idea/ktlint.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | false 5 | 6 | -------------------------------------------------------------------------------- /.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 27 | -------------------------------------------------------------------------------- /.idea/sonarlint/issuestore/0/0/00bb5a55bbe67254c2c0f18c038e34feb530225d: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LvKang-insist/AndroidSkin/4eef34397a633fbc412e8bb8353ffd3acf324c56/.idea/sonarlint/issuestore/0/0/00bb5a55bbe67254c2c0f18c038e34feb530225d -------------------------------------------------------------------------------- /.idea/sonarlint/issuestore/0/5/05efc8b1657769a27696d478ded1e95f38737233: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LvKang-insist/AndroidSkin/4eef34397a633fbc412e8bb8353ffd3acf324c56/.idea/sonarlint/issuestore/0/5/05efc8b1657769a27696d478ded1e95f38737233 -------------------------------------------------------------------------------- /.idea/sonarlint/issuestore/1/f/1ff699be707da39c21730447e3621d6e9c1c97a0: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LvKang-insist/AndroidSkin/4eef34397a633fbc412e8bb8353ffd3acf324c56/.idea/sonarlint/issuestore/1/f/1ff699be707da39c21730447e3621d6e9c1c97a0 -------------------------------------------------------------------------------- /.idea/sonarlint/issuestore/2/3/23c044a63385dd34c0d6c43e119c336cfa71e078: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LvKang-insist/AndroidSkin/4eef34397a633fbc412e8bb8353ffd3acf324c56/.idea/sonarlint/issuestore/2/3/23c044a63385dd34c0d6c43e119c336cfa71e078 -------------------------------------------------------------------------------- /.idea/sonarlint/issuestore/3/7/37e0b1be3ca00e70c871059689243c15c34fb44a: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LvKang-insist/AndroidSkin/4eef34397a633fbc412e8bb8353ffd3acf324c56/.idea/sonarlint/issuestore/3/7/37e0b1be3ca00e70c871059689243c15c34fb44a -------------------------------------------------------------------------------- /.idea/sonarlint/issuestore/3/b/3b68b8031302659b137c3fe69d6becbaf2b9698b: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LvKang-insist/AndroidSkin/4eef34397a633fbc412e8bb8353ffd3acf324c56/.idea/sonarlint/issuestore/3/b/3b68b8031302659b137c3fe69d6becbaf2b9698b -------------------------------------------------------------------------------- /.idea/sonarlint/issuestore/3/e/3eb353299266c592fdca2efa765a13c86f79c544: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LvKang-insist/AndroidSkin/4eef34397a633fbc412e8bb8353ffd3acf324c56/.idea/sonarlint/issuestore/3/e/3eb353299266c592fdca2efa765a13c86f79c544 -------------------------------------------------------------------------------- /.idea/sonarlint/issuestore/3/f/3f3614b660c98ad2ec2ec3066dba56ad2e3556ca: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LvKang-insist/AndroidSkin/4eef34397a633fbc412e8bb8353ffd3acf324c56/.idea/sonarlint/issuestore/3/f/3f3614b660c98ad2ec2ec3066dba56ad2e3556ca -------------------------------------------------------------------------------- /.idea/sonarlint/issuestore/3/f/3fc4d5b5fd8ab8664c5f8c8311e06787ab8690b3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LvKang-insist/AndroidSkin/4eef34397a633fbc412e8bb8353ffd3acf324c56/.idea/sonarlint/issuestore/3/f/3fc4d5b5fd8ab8664c5f8c8311e06787ab8690b3 -------------------------------------------------------------------------------- /.idea/sonarlint/issuestore/4/0/40a8402d1b0b07de9d145da63f2160b6295c3891: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LvKang-insist/AndroidSkin/4eef34397a633fbc412e8bb8353ffd3acf324c56/.idea/sonarlint/issuestore/4/0/40a8402d1b0b07de9d145da63f2160b6295c3891 -------------------------------------------------------------------------------- /.idea/sonarlint/issuestore/4/3/43ff339470fe0f3ddf25ea6f557a45637fdd013a: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LvKang-insist/AndroidSkin/4eef34397a633fbc412e8bb8353ffd3acf324c56/.idea/sonarlint/issuestore/4/3/43ff339470fe0f3ddf25ea6f557a45637fdd013a -------------------------------------------------------------------------------- /.idea/sonarlint/issuestore/4/7/47bbea10edd9b7e1f95973e3bc34e51228d8be69: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LvKang-insist/AndroidSkin/4eef34397a633fbc412e8bb8353ffd3acf324c56/.idea/sonarlint/issuestore/4/7/47bbea10edd9b7e1f95973e3bc34e51228d8be69 -------------------------------------------------------------------------------- /.idea/sonarlint/issuestore/5/1/51e1c5d383dfaa35e0e7e5873a0a99355a86880f: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LvKang-insist/AndroidSkin/4eef34397a633fbc412e8bb8353ffd3acf324c56/.idea/sonarlint/issuestore/5/1/51e1c5d383dfaa35e0e7e5873a0a99355a86880f -------------------------------------------------------------------------------- /.idea/sonarlint/issuestore/5/2/5282ed8d7d3284c85a70c413ef67c04b412e32f6: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LvKang-insist/AndroidSkin/4eef34397a633fbc412e8bb8353ffd3acf324c56/.idea/sonarlint/issuestore/5/2/5282ed8d7d3284c85a70c413ef67c04b412e32f6 -------------------------------------------------------------------------------- /.idea/sonarlint/issuestore/5/6/566d6c7352aef9d8aa71dde9d56660f1ef22e62a: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LvKang-insist/AndroidSkin/4eef34397a633fbc412e8bb8353ffd3acf324c56/.idea/sonarlint/issuestore/5/6/566d6c7352aef9d8aa71dde9d56660f1ef22e62a -------------------------------------------------------------------------------- /.idea/sonarlint/issuestore/5/8/5804b5b5d9744a69cf54dab9c9f4c6ad2ddc7f9a: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LvKang-insist/AndroidSkin/4eef34397a633fbc412e8bb8353ffd3acf324c56/.idea/sonarlint/issuestore/5/8/5804b5b5d9744a69cf54dab9c9f4c6ad2ddc7f9a -------------------------------------------------------------------------------- /.idea/sonarlint/issuestore/6/1/61d258e93d3569d213e6b23af507edcc557e5e07: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LvKang-insist/AndroidSkin/4eef34397a633fbc412e8bb8353ffd3acf324c56/.idea/sonarlint/issuestore/6/1/61d258e93d3569d213e6b23af507edcc557e5e07 -------------------------------------------------------------------------------- /.idea/sonarlint/issuestore/6/2/623b11249d525083e5fe898c5cd1762660f51ac9: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LvKang-insist/AndroidSkin/4eef34397a633fbc412e8bb8353ffd3acf324c56/.idea/sonarlint/issuestore/6/2/623b11249d525083e5fe898c5cd1762660f51ac9 -------------------------------------------------------------------------------- /.idea/sonarlint/issuestore/6/6/66735bbb2138471309e49d49ac3a370569cc125d: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LvKang-insist/AndroidSkin/4eef34397a633fbc412e8bb8353ffd3acf324c56/.idea/sonarlint/issuestore/6/6/66735bbb2138471309e49d49ac3a370569cc125d -------------------------------------------------------------------------------- /.idea/sonarlint/issuestore/6/6/66e2568e0d3f2652c945513ba940f8f1e4f6813b: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LvKang-insist/AndroidSkin/4eef34397a633fbc412e8bb8353ffd3acf324c56/.idea/sonarlint/issuestore/6/6/66e2568e0d3f2652c945513ba940f8f1e4f6813b -------------------------------------------------------------------------------- /.idea/sonarlint/issuestore/6/8/68ffd980bc165482d8ae7b3a067170a25a807e2e: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LvKang-insist/AndroidSkin/4eef34397a633fbc412e8bb8353ffd3acf324c56/.idea/sonarlint/issuestore/6/8/68ffd980bc165482d8ae7b3a067170a25a807e2e -------------------------------------------------------------------------------- /.idea/sonarlint/issuestore/6/e/6ecd6000a7b6f4a2884412ff19f74193ed089648: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LvKang-insist/AndroidSkin/4eef34397a633fbc412e8bb8353ffd3acf324c56/.idea/sonarlint/issuestore/6/e/6ecd6000a7b6f4a2884412ff19f74193ed089648 -------------------------------------------------------------------------------- /.idea/sonarlint/issuestore/7/3/73fe5d7605274dc2cf5a3a110a86d3ec5c0bd0f3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LvKang-insist/AndroidSkin/4eef34397a633fbc412e8bb8353ffd3acf324c56/.idea/sonarlint/issuestore/7/3/73fe5d7605274dc2cf5a3a110a86d3ec5c0bd0f3 -------------------------------------------------------------------------------- /.idea/sonarlint/issuestore/8/e/8ec9a00bfd09b3190ac6b22251dbb1aa95a0579d: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LvKang-insist/AndroidSkin/4eef34397a633fbc412e8bb8353ffd3acf324c56/.idea/sonarlint/issuestore/8/e/8ec9a00bfd09b3190ac6b22251dbb1aa95a0579d -------------------------------------------------------------------------------- /.idea/sonarlint/issuestore/9/e/9e08934d811afe28fbc77aaa3c0d747b94348db9: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LvKang-insist/AndroidSkin/4eef34397a633fbc412e8bb8353ffd3acf324c56/.idea/sonarlint/issuestore/9/e/9e08934d811afe28fbc77aaa3c0d747b94348db9 -------------------------------------------------------------------------------- /.idea/sonarlint/issuestore/a/5/a52f34ee8999426f9468ca72a1b11663f57ba0c4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LvKang-insist/AndroidSkin/4eef34397a633fbc412e8bb8353ffd3acf324c56/.idea/sonarlint/issuestore/a/5/a52f34ee8999426f9468ca72a1b11663f57ba0c4 -------------------------------------------------------------------------------- /.idea/sonarlint/issuestore/a/d/ad4f6a39172f1fe4db70a527df96a9649f3d3a51: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LvKang-insist/AndroidSkin/4eef34397a633fbc412e8bb8353ffd3acf324c56/.idea/sonarlint/issuestore/a/d/ad4f6a39172f1fe4db70a527df96a9649f3d3a51 -------------------------------------------------------------------------------- /.idea/sonarlint/issuestore/a/f/af824a66dbada2cce18cd1f25c80c537f9fad500: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LvKang-insist/AndroidSkin/4eef34397a633fbc412e8bb8353ffd3acf324c56/.idea/sonarlint/issuestore/a/f/af824a66dbada2cce18cd1f25c80c537f9fad500 -------------------------------------------------------------------------------- /.idea/sonarlint/issuestore/b/4/b4a4dd424d0575baea7f6bd8cfcb4f9e71b79bc8: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LvKang-insist/AndroidSkin/4eef34397a633fbc412e8bb8353ffd3acf324c56/.idea/sonarlint/issuestore/b/4/b4a4dd424d0575baea7f6bd8cfcb4f9e71b79bc8 -------------------------------------------------------------------------------- /.idea/sonarlint/issuestore/c/3/c3085a1c4f578eadc45c5499e731342f9401e3bc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LvKang-insist/AndroidSkin/4eef34397a633fbc412e8bb8353ffd3acf324c56/.idea/sonarlint/issuestore/c/3/c3085a1c4f578eadc45c5499e731342f9401e3bc -------------------------------------------------------------------------------- /.idea/sonarlint/issuestore/e/a/eaf9a1dbcdebe8d5b11f84b26a2c1d99b9a4239d: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LvKang-insist/AndroidSkin/4eef34397a633fbc412e8bb8353ffd3acf324c56/.idea/sonarlint/issuestore/e/a/eaf9a1dbcdebe8d5b11f84b26a2c1d99b9a4239d -------------------------------------------------------------------------------- /.idea/sonarlint/issuestore/e/c/ecbd5dff7dda95f168e5b6d66ad952c7b937c260: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LvKang-insist/AndroidSkin/4eef34397a633fbc412e8bb8353ffd3acf324c56/.idea/sonarlint/issuestore/e/c/ecbd5dff7dda95f168e5b6d66ad952c7b937c260 -------------------------------------------------------------------------------- /.idea/sonarlint/issuestore/f/0/f07866736216be0ee2aba49e392191aeae700a35: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LvKang-insist/AndroidSkin/4eef34397a633fbc412e8bb8353ffd3acf324c56/.idea/sonarlint/issuestore/f/0/f07866736216be0ee2aba49e392191aeae700a35 -------------------------------------------------------------------------------- /.idea/sonarlint/issuestore/f/0/f0a9ce0e88c5e949614ef2d596a07dc5b52bf1ee: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LvKang-insist/AndroidSkin/4eef34397a633fbc412e8bb8353ffd3acf324c56/.idea/sonarlint/issuestore/f/0/f0a9ce0e88c5e949614ef2d596a07dc5b52bf1ee -------------------------------------------------------------------------------- /.idea/sonarlint/issuestore/f/c/fcead86d2672c263fadb598b07bf805b5822dec7: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LvKang-insist/AndroidSkin/4eef34397a633fbc412e8bb8353ffd3acf324c56/.idea/sonarlint/issuestore/f/c/fcead86d2672c263fadb598b07bf805b5822dec7 -------------------------------------------------------------------------------- /.idea/sonarlint/issuestore/index.pb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LvKang-insist/AndroidSkin/4eef34397a633fbc412e8bb8353ffd3acf324c56/.idea/sonarlint/issuestore/index.pb -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # AndroidSkin 2 | ### 一个使用成本极低的换肤框架 3 | 4 | - 支持插件式换肤 5 | - 支持继承和非继承式换肤,无需对base层进行代码侵入 6 | - 支持自定义View 7 | - 支持多种加载类型,Assets,Storage ,Zip 等 8 | - 内部直接进行换肤,无需重启 9 | 10 | ### 目前支持的内置 View 👨‍🔧‍ 11 | 12 | ___ 13 | 14 | 15 | 16 | | | backaground | Des | 17 | | ------------------ | :----------------------------: | :--: | 18 | | AppCompatTextView | 背景,字体颜色,字体大小 | | 19 | | AppCompatButton | 背景,字体颜色,字体大小 | | 20 | | AppCompatImageView | 背景,image | | 21 | | ConstraintLayout | 背景 | | 22 | | LinearLayoutCompat | 背景 | | 23 | | NestedScrollView | 背景 | | 24 | | FrameLayout | 背景 | | 25 | | RelativeLayout | 背景 | | 26 | | AppCompatEditText | 背景,字体颜色,字体大小,hint | | 27 | | CardView | 背景,圆角,elevation | | 28 | 29 | > 其他的陆续支持中。 30 | > 你也可以克隆代码到本地进行适配然后再提交代码,期待你的提交 31 | 32 | ### 使用方式 33 | 34 | #### 初始化 35 | 36 | ``` 37 | SkinManager.init(this) 38 | .addInflaters(SkinAppCompatViewInflater()) 39 | .setAutoLoadSkin(true) 40 | .build() 41 | ``` 42 | 43 | - SkinAppCompatViewInflater 44 | 45 | 框架内部支持换肤的 View , 如果需要对自定义 View 进行换肤,可实现 SkinLayoutInflater 接口,具体可参考 `SkinAppCompatViewInflater` 内部实现。 46 | 47 | 最后将自定义 Inflater 在初始化时添加即可 48 | 49 | - setAutoLoadSkin 50 | 51 | 是否使用非继承式的方式实现换肤,默认 true 52 | 53 | #### 加载皮肤 54 | 55 | - None 56 | 57 | 加载默认皮肤,即不使用皮肤 58 | 59 | - Assets 60 | 61 | 将皮肤文件放在 assets 目录下,传入即可 62 | 63 | ```kotlin 64 | /** 65 | * 加载资源文件夹下的皮肤 66 | * @param name 资源文件名 67 | * @param skinLoadListener 回调 68 | * @param isRepeat false 表示要加载的 skin 和当前使用的相同时不重复加载 69 | */ 70 | fun loadAssetsSkin( 71 | name: String, 72 | skinLoadListener: SkinLoadListener? = null, 73 | isRepeat: Boolean = false, 74 | ) 75 | ``` 76 | 77 | - Storage 78 | 79 | 将皮肤文件下载到沙箱中,传入绝对路径即可 80 | 81 | ```kotlin 82 | /** 83 | * 加载内部存储下的皮肤,必须是沙箱路径 84 | * @param path skin 绝对路径 85 | * @param skinLoadListener 回调 86 | * @param isRepeat false 表示要加载的 skin 和当前使用的相同时不重复加载 87 | */ 88 | fun loadStorageSkin( 89 | path: String, 90 | skinLoadListener: SkinLoadListener? = null, 91 | isRepeat: Boolean = false 92 | ) 93 | ``` 94 | 95 | - Zip 96 | 97 | 将下载好的 zip 的绝对路径传入即可,如果是加密的Zip,则需要传入密码 98 | 99 | ```kotlin 100 | /** 101 | * 加载内部存储下的 zpi 皮肤文件,必须是沙箱路径 102 | * @param path skin 绝对路径 103 | * @param password zip 文件密码,没有可不传 104 | * @param skinLoadListener 回调 105 | * @param isRepeat false 表示要加载的 skin 和当前使用的相同时不重复加载 106 | */ 107 | fun loadZipSkin( 108 | path: String, 109 | password: String? = null, 110 | skinLoadListener: SkinLoadListener? = null, 111 | isRepeat: Boolean = false 112 | ) 113 | ``` 114 | 115 | 116 | 117 | ### 需要注意的点 118 | 119 | - 创建皮肤 120 | 121 | 新建一个项目,将需要换肤的资源放在此项目中,然后修改资源值即可。 122 | 123 | > 注意,资源名称必须和主项目资源名称一致,后缀必须是 .skin 。压缩文件的原文件也是如此 124 | 125 | - 更新皮肤资源 126 | 127 | 如果当前使用的皮肤 `red.skin` 内部的资源发生了改变,则需要修改皮肤名称,例如修改为 `red_1.skin` 。这样做的原因是本框架中对于从 `Assets` 以及 `Zip` 加载类型的有缓存策略,如果不修改名称,默认就会加载到缓存皮肤中。 128 | 129 | -------------------------------------------------------------------------------- /apk/xiaomi/345: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LvKang-insist/AndroidSkin/4eef34397a633fbc412e8bb8353ffd3acf324c56/apk/xiaomi/345 -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /app/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id 'com.android.application' 3 | id 'kotlin-android' 4 | } 5 | 6 | android { 7 | compileSdkVersion 30 8 | buildToolsVersion "30.0.2" 9 | 10 | defaultConfig { 11 | applicationId "com.lvkang.example" 12 | minSdkVersion 21 13 | targetSdkVersion 30 14 | versionCode 1 15 | versionName "1.0" 16 | 17 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" 18 | } 19 | 20 | buildTypes { 21 | release { 22 | minifyEnabled false 23 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' 24 | } 25 | } 26 | compileOptions { 27 | sourceCompatibility JavaVersion.VERSION_1_8 28 | targetCompatibility JavaVersion.VERSION_1_8 29 | } 30 | kotlinOptions { 31 | jvmTarget = '1.8' 32 | } 33 | } 34 | 35 | dependencies { 36 | implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version" 37 | implementation 'androidx.core:core-ktx:1.6.0' 38 | implementation 'androidx.appcompat:appcompat:1.3.1' 39 | implementation 'com.google.android.material:material:1.4.0' 40 | implementation 'androidx.constraintlayout:constraintlayout:2.0.4' 41 | implementation project(path: ':skin') 42 | testImplementation 'junit:junit:4.13.2' 43 | androidTestImplementation 'androidx.test.ext:junit:1.1.3' 44 | androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0' 45 | } -------------------------------------------------------------------------------- /app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile -------------------------------------------------------------------------------- /app/src/androidTest/java/com/lvkang/example/ExampleInstrumentedTest.kt: -------------------------------------------------------------------------------- 1 | package com.lvkang.example 2 | 3 | import androidx.test.platform.app.InstrumentationRegistry 4 | import androidx.test.ext.junit.runners.AndroidJUnit4 5 | 6 | import org.junit.Test 7 | import org.junit.runner.RunWith 8 | 9 | import org.junit.Assert.* 10 | 11 | /** 12 | * Instrumented test, which will execute on an Android device. 13 | * 14 | * See [testing documentation](http://d.android.com/tools/testing). 15 | */ 16 | @RunWith(AndroidJUnit4::class) 17 | class ExampleInstrumentedTest { 18 | @Test 19 | fun useAppContext() { 20 | // Context of the app under test. 21 | val appContext = InstrumentationRegistry.getInstrumentation().targetContext 22 | assertEquals("com.lvkang.androidskin", appContext.packageName) 23 | } 24 | } -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /app/src/main/assets/skin.skin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LvKang-insist/AndroidSkin/4eef34397a633fbc412e8bb8353ffd3acf324c56/app/src/main/assets/skin.skin -------------------------------------------------------------------------------- /app/src/main/assets/skin1.skin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LvKang-insist/AndroidSkin/4eef34397a633fbc412e8bb8353ffd3acf324c56/app/src/main/assets/skin1.skin -------------------------------------------------------------------------------- /app/src/main/assets/skin2.skin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LvKang-insist/AndroidSkin/4eef34397a633fbc412e8bb8353ffd3acf324c56/app/src/main/assets/skin2.skin -------------------------------------------------------------------------------- /app/src/main/assets/skin3.skin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LvKang-insist/AndroidSkin/4eef34397a633fbc412e8bb8353ffd3acf324c56/app/src/main/assets/skin3.skin -------------------------------------------------------------------------------- /app/src/main/assets/skin4.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LvKang-insist/AndroidSkin/4eef34397a633fbc412e8bb8353ffd3acf324c56/app/src/main/assets/skin4.zip -------------------------------------------------------------------------------- /app/src/main/assets/skin5.skin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LvKang-insist/AndroidSkin/4eef34397a633fbc412e8bb8353ffd3acf324c56/app/src/main/assets/skin5.skin -------------------------------------------------------------------------------- /app/src/main/assets/skin6.skin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LvKang-insist/AndroidSkin/4eef34397a633fbc412e8bb8353ffd3acf324c56/app/src/main/assets/skin6.skin -------------------------------------------------------------------------------- /app/src/main/assets/skin7.skin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LvKang-insist/AndroidSkin/4eef34397a633fbc412e8bb8353ffd3acf324c56/app/src/main/assets/skin7.skin -------------------------------------------------------------------------------- /app/src/main/java/com/lvkang/example/BaseApplication.kt: -------------------------------------------------------------------------------- 1 | package com.lvkang.example 2 | 3 | import android.app.Application 4 | import com.lvkang.skin.SkinManager 5 | import com.lvkang.skin.inflater.SkinAppCompatViewInflater 6 | 7 | /** 8 | * @name BaseActivity 9 | * @package com.lvkang.example 10 | * @author 345 QQ:1831712732 11 | * @time 2020/12/02 22:13 12 | * @description 13 | */ 14 | class BaseApplication : Application() { 15 | 16 | override fun onCreate() { 17 | super.onCreate() 18 | SkinManager.init(this) 19 | .addInflaters(SkinAppCompatViewInflater()) 20 | .setAutoLoadSkin(true) 21 | .build() 22 | } 23 | } -------------------------------------------------------------------------------- /app/src/main/java/com/lvkang/example/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package com.lvkang.example 2 | 3 | import android.content.Intent 4 | import android.os.Bundle 5 | import android.view.View 6 | import androidx.appcompat.app.AppCompatActivity 7 | import androidx.core.content.res.ResourcesCompat 8 | import com.lvkang.skin.SkinManager 9 | import com.lvkang.skin.ktx.isFile 10 | import java.io.File 11 | import java.io.IOException 12 | 13 | class MainActivity : AppCompatActivity() { 14 | 15 | private val TAG = "MainActivity" 16 | 17 | override fun onCreate(savedInstanceState: Bundle?) { 18 | super.onCreate(savedInstanceState) 19 | setContentView(R.layout.activity_main) 20 | 21 | val view = findViewById(R.id.button) 22 | view.setOnClickListener { 23 | SkinManager.loadAssetsSkin("skin.skin") 24 | } 25 | findViewById(R.id.button1).setOnClickListener { 26 | SkinManager.loadAssetsSkin("skin1.skin") 27 | } 28 | 29 | findViewById(R.id.button2).setOnClickListener { 30 | SkinManager.loadAssetsSkin("skin2.skin") 31 | } 32 | 33 | findViewById(R.id.button3).setOnClickListener { 34 | SkinManager.loadAssetsSkin("skin3.skin") 35 | } 36 | 37 | findViewById(R.id.button4).setOnClickListener { 38 | 39 | //先将zip复制到沙箱中 40 | val path = copyCache( 41 | "skin4.zip", 42 | "${SkinManager.getApplication().getExternalFilesDir("file")}${File.separator}" 43 | ) 44 | path?.run { 45 | SkinManager.loadZipSkin(path, password = "3310") 46 | } 47 | } 48 | 49 | findViewById(R.id.button5).setOnClickListener { 50 | SkinManager.loadAssetsSkin("skin5.skin") 51 | } 52 | 53 | findViewById(R.id.none).setOnClickListener { 54 | SkinManager.loadNone() 55 | } 56 | 57 | findViewById(R.id.next).setOnClickListener { 58 | startActivity(Intent(this, TestActivity::class.java)) 59 | // findViewById(R.id.cardview).setBackgroundResource( 60 | // ResourcesCompat.getColor( 61 | // resources, 62 | // R.color.skin_cardview_color, 63 | // null 64 | // ) 65 | // ) 66 | } 67 | findViewById(R.id.cardview).setOnClickListener { 68 | SkinManager.loadAssetsSkin("skin7.skin") 69 | // SkinManager.loadAssetsSkin("skin8.skin") 70 | } 71 | } 72 | 73 | private fun copyCache(skinName: String, cacheDir: String): String? { 74 | return try { 75 | val outFile = File(cacheDir, skinName) 76 | if (isFile(outFile.path)) { 77 | outFile.delete() 78 | } 79 | val input = SkinManager.getContext().resources.assets.open(skinName) 80 | input.copyTo(outFile.outputStream()) 81 | outFile.absolutePath 82 | } catch (e: IOException) { 83 | e.printStackTrace() 84 | null 85 | } 86 | // findViewById(R.id.cardview).setOnClickListener { 87 | // SkinManager.loadAssetsSkin("skin7.skin") 88 | // } 89 | } 90 | 91 | 92 | } 93 | 94 | 95 | 96 | -------------------------------------------------------------------------------- /app/src/main/java/com/lvkang/example/Test.kt: -------------------------------------------------------------------------------- 1 | package com.lvkang.example 2 | 3 | /** 4 | * @name Test 5 | * @package com.lvkang.example 6 | * @author 345 QQ:1831712732 7 | * @time 2020/12/20 00:55 8 | * @description 9 | */ 10 | 11 | // 1000,560 12 | fun test(a: Int, b: Int): Int { 13 | var a1 = a 14 | var b1 = b 15 | while (b1 != 0) { 16 | val temp = a1 % b1 17 | a1 = b1 18 | b1 = temp 19 | } 20 | return b1 21 | } 22 | 23 | 24 | fun test2(a: Int, b: Int): Int { 25 | return if (b != 0) { 26 | test2(b, a % b) 27 | } else { 28 | a 29 | } 30 | } 31 | 32 | fun main() { 33 | println(test2(1000, 560)) 34 | } 35 | 36 | fun test1(a: Int, b: Int): Int { 37 | var a1 = a 38 | var b1 = b 39 | if (a < b) { 40 | val t = a1 41 | a1 = b1 42 | b1 = t 43 | } 44 | while (b1 != 0) { 45 | val yu = a1 % b1 46 | a1 = b1 47 | b1 = yu 48 | } 49 | return a1 50 | } 51 | 52 | sealed class Test2 { 53 | val test1: Test2? = null 54 | abstract val test2: Test2 55 | } -------------------------------------------------------------------------------- /app/src/main/java/com/lvkang/example/TestActivity.kt: -------------------------------------------------------------------------------- 1 | package com.lvkang.example 2 | 3 | import android.os.Bundle 4 | import android.view.View 5 | import androidx.recyclerview.widget.LinearLayoutManager 6 | import androidx.recyclerview.widget.RecyclerView 7 | import com.lvkang.skin.app.SkinCompatActivity 8 | 9 | /** 10 | * @name TextActivity 11 | * @package com.lvkang.example 12 | * @author 345 QQ:1831712732 13 | * @time 2020/12/14 00:12 14 | * @description 15 | */ 16 | class TestActivity : SkinCompatActivity() { 17 | override fun onCreate(savedInstanceState: Bundle?) { 18 | super.onCreate(savedInstanceState) 19 | setContentView(R.layout.activity_test) 20 | 21 | val recycler = findViewById(R.id.recycler) 22 | 23 | recycler.layoutManager = LinearLayoutManager(this) 24 | 25 | val list = arrayListOf() 26 | for (i in 0..1000) 27 | list.add("") 28 | recycler.adapter = TestAdapter(list) 29 | 30 | 31 | findViewById(R.id.test).setOnClickListener { 32 | recycler?.adapter?.notifyDataSetChanged() 33 | print() 34 | } 35 | } 36 | } -------------------------------------------------------------------------------- /app/src/main/java/com/lvkang/example/TestAdapter.kt: -------------------------------------------------------------------------------- 1 | package com.lvkang.example 2 | 3 | import android.view.LayoutInflater 4 | import android.view.View 5 | import android.view.ViewGroup 6 | import androidx.appcompat.widget.AppCompatTextView 7 | import androidx.recyclerview.widget.RecyclerView 8 | 9 | /** 10 | * @name TestAdapter 11 | * @package com.lvkang.example 12 | * @author 345 QQ:1831712732 13 | * @time 2022/07/21 17:33 14 | * @description 15 | */ 16 | class TestAdapter(val data: List) : RecyclerView.Adapter() { 17 | 18 | 19 | class TestHolder(view: View) : RecyclerView.ViewHolder(view) { 20 | var text = view.findViewById(R.id.text) 21 | } 22 | 23 | override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): TestHolder { 24 | var view = LayoutInflater.from(parent.context).inflate(R.layout.test_item, parent, false) 25 | return TestHolder(view) 26 | } 27 | 28 | override fun onBindViewHolder(holder: TestHolder, position: Int) { 29 | holder.text.text = "$position" 30 | } 31 | 32 | override fun getItemCount(): Int { 33 | return data.size 34 | } 35 | 36 | } -------------------------------------------------------------------------------- /app/src/main/res/drawable/skin_edit_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/skin_edit_cursor.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/skin_main_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/skin_main_image.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LvKang-insist/AndroidSkin/4eef34397a633fbc412e8bb8353ffd3acf324c56/app/src/main/res/drawable/skin_main_image.jpg -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_main.xml: -------------------------------------------------------------------------------- 1 | 2 | 9 | 10 | 15 | 16 | 17 | 24 | 25 | 26 | 39 | 40 | 53 | 54 | 67 | 68 | 69 | 82 | 83 | 96 | 97 | 107 | 108 | 109 | 121 | 122 | 131 | 132 | 148 | 149 | 161 | 162 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_test.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 9 | 10 | 14 | 15 | 16 | 17 | 27 | 28 | -------------------------------------------------------------------------------- /app/src/main/res/layout/layout_include.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 11 | 12 | 20 | 21 | 22 | 23 | 24 | 33 | 34 | 43 | 44 | 45 | -------------------------------------------------------------------------------- /app/src/main/res/layout/test_item.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 9 | 19 | 20 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LvKang-insist/AndroidSkin/4eef34397a633fbc412e8bb8353ffd3acf324c56/app/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LvKang-insist/AndroidSkin/4eef34397a633fbc412e8bb8353ffd3acf324c56/app/src/main/res/mipmap-hdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LvKang-insist/AndroidSkin/4eef34397a633fbc412e8bb8353ffd3acf324c56/app/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LvKang-insist/AndroidSkin/4eef34397a633fbc412e8bb8353ffd3acf324c56/app/src/main/res/mipmap-mdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LvKang-insist/AndroidSkin/4eef34397a633fbc412e8bb8353ffd3acf324c56/app/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LvKang-insist/AndroidSkin/4eef34397a633fbc412e8bb8353ffd3acf324c56/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LvKang-insist/AndroidSkin/4eef34397a633fbc412e8bb8353ffd3acf324c56/app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LvKang-insist/AndroidSkin/4eef34397a633fbc412e8bb8353ffd3acf324c56/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LvKang-insist/AndroidSkin/4eef34397a633fbc412e8bb8353ffd3acf324c56/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LvKang-insist/AndroidSkin/4eef34397a633fbc412e8bb8353ffd3acf324c56/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/values-night/themes.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 16 | -------------------------------------------------------------------------------- /app/src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #FFBB86FC 4 | #FF6200EE 5 | #FF3700B3 6 | #FF03DAC5 7 | #FF018786 8 | #FF000000 9 | #FFFFFFFF 10 | 11 | 12 | #000000 13 | #434aad 14 | #ff0000 15 | 16 | -------------------------------------------------------------------------------- /app/src/main/res/values/dimens.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 20sp 4 | 25sp 5 | 10dp 6 | 15dp 7 | -------------------------------------------------------------------------------- /app/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | AndroidSkin 3 | 点击换肤 4 | 跳转 5 | 6 | 请输入 7 | -------------------------------------------------------------------------------- /app/src/main/res/values/themes.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 16 | -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | // Top-level build file where you can add configuration options common to all sub-projects/modules. 2 | //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 3 | buildscript { 4 | ext.kotlin_version = "1.5.10" 5 | repositories { 6 | mavenCentral() 7 | google() 8 | } 9 | dependencies { 10 | classpath "com.android.tools.build:gradle:4.1.1" 11 | classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" 12 | 13 | // NOTE: Do not place your application dependencies here; they belong 14 | // in the individual module build.gradle files 15 | } 16 | } 17 | 18 | allprojects { 19 | repositories { 20 | mavenCentral() 21 | google() 22 | maven { url 'https://jitpack.io' } 23 | } 24 | } 25 | 26 | task clean(type: Delete) { 27 | delete rootProject.buildDir 28 | } -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | # Project-wide Gradle settings. 2 | # IDE (e.g. Android Studio) users: 3 | # Gradle settings configured through the IDE *will override* 4 | # any settings specified in this file. 5 | # For more details on how to configure your build environment visit 6 | # http://www.gradle.org/docs/current/userguide/build_environment.html 7 | # Specifies the JVM arguments used for the daemon process. 8 | # The setting is particularly useful for tweaking memory settings. 9 | org.gradle.jvmargs=-Xmx1024m -XX:MaxPermSize=512m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8 10 | # When configured, Gradle will run in incubating parallel mode. 11 | # This option should only be used with decoupled projects. More details, visit 12 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects 13 | # org.gradle.parallel=true 14 | # AndroidX package structure to make it clearer which packages are bundled with the 15 | # Android operating system, and which are packaged with your app"s APK 16 | # https://developer.android.com/topic/libraries/support-library/androidx-rn 17 | android.useAndroidX=true 18 | # Automatically convert third-party libraries to use AndroidX 19 | android.enableJetifier=true 20 | # Kotlin code style for this project: "official" or "obsolete": 21 | kotlin.code.style=official -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LvKang-insist/AndroidSkin/4eef34397a633fbc412e8bb8353ffd3acf324c56/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Wed Dec 02 23:03:43 CST 2020 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | distributionUrl=https\://services.gradle.org/distributions/gradle-6.5-all.zip 7 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | ############################################################################## 4 | ## 5 | ## Gradle start up script for UN*X 6 | ## 7 | ############################################################################## 8 | 9 | # Attempt to set APP_HOME 10 | # Resolve links: $0 may be a link 11 | PRG="$0" 12 | # Need this for relative symlinks. 13 | while [ -h "$PRG" ] ; do 14 | ls=`ls -ld "$PRG"` 15 | link=`expr "$ls" : '.*-> \(.*\)$'` 16 | if expr "$link" : '/.*' > /dev/null; then 17 | PRG="$link" 18 | else 19 | PRG=`dirname "$PRG"`"/$link" 20 | fi 21 | done 22 | SAVED="`pwd`" 23 | cd "`dirname \"$PRG\"`/" >/dev/null 24 | APP_HOME="`pwd -P`" 25 | cd "$SAVED" >/dev/null 26 | 27 | APP_NAME="Gradle" 28 | APP_BASE_NAME=`basename "$0"` 29 | 30 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 31 | DEFAULT_JVM_OPTS="" 32 | 33 | # Use the maximum available, or set MAX_FD != -1 to use that value. 34 | MAX_FD="maximum" 35 | 36 | warn () { 37 | echo "$*" 38 | } 39 | 40 | die () { 41 | echo 42 | echo "$*" 43 | echo 44 | exit 1 45 | } 46 | 47 | # OS specific support (must be 'true' or 'false'). 48 | cygwin=false 49 | msys=false 50 | darwin=false 51 | nonstop=false 52 | case "`uname`" in 53 | CYGWIN* ) 54 | cygwin=true 55 | ;; 56 | Darwin* ) 57 | darwin=true 58 | ;; 59 | MINGW* ) 60 | msys=true 61 | ;; 62 | NONSTOP* ) 63 | nonstop=true 64 | ;; 65 | esac 66 | 67 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 68 | 69 | # Determine the Java command to use to start the JVM. 70 | if [ -n "$JAVA_HOME" ] ; then 71 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 72 | # IBM's JDK on AIX uses strange locations for the executables 73 | JAVACMD="$JAVA_HOME/jre/sh/java" 74 | else 75 | JAVACMD="$JAVA_HOME/bin/java" 76 | fi 77 | if [ ! -x "$JAVACMD" ] ; then 78 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 79 | 80 | Please set the JAVA_HOME variable in your environment to match the 81 | location of your Java installation." 82 | fi 83 | else 84 | JAVACMD="java" 85 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 86 | 87 | Please set the JAVA_HOME variable in your environment to match the 88 | location of your Java installation." 89 | fi 90 | 91 | # Increase the maximum file descriptors if we can. 92 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then 93 | MAX_FD_LIMIT=`ulimit -H -n` 94 | if [ $? -eq 0 ] ; then 95 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 96 | MAX_FD="$MAX_FD_LIMIT" 97 | fi 98 | ulimit -n $MAX_FD 99 | if [ $? -ne 0 ] ; then 100 | warn "Could not set maximum file descriptor limit: $MAX_FD" 101 | fi 102 | else 103 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 104 | fi 105 | fi 106 | 107 | # For Darwin, add options to specify how the application appears in the dock 108 | if $darwin; then 109 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 110 | fi 111 | 112 | # For Cygwin, switch paths to Windows format before running java 113 | if $cygwin ; then 114 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 115 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 116 | JAVACMD=`cygpath --unix "$JAVACMD"` 117 | 118 | # We build the pattern for arguments to be converted via cygpath 119 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 120 | SEP="" 121 | for dir in $ROOTDIRSRAW ; do 122 | ROOTDIRS="$ROOTDIRS$SEP$dir" 123 | SEP="|" 124 | done 125 | OURCYGPATTERN="(^($ROOTDIRS))" 126 | # Add a user-defined pattern to the cygpath arguments 127 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 128 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 129 | fi 130 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 131 | i=0 132 | for arg in "$@" ; do 133 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 134 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 135 | 136 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 137 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 138 | else 139 | eval `echo args$i`="\"$arg\"" 140 | fi 141 | i=$((i+1)) 142 | done 143 | case $i in 144 | (0) set -- ;; 145 | (1) set -- "$args0" ;; 146 | (2) set -- "$args0" "$args1" ;; 147 | (3) set -- "$args0" "$args1" "$args2" ;; 148 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;; 149 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 150 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 151 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 152 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 153 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 154 | esac 155 | fi 156 | 157 | # Escape application args 158 | save () { 159 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done 160 | echo " " 161 | } 162 | APP_ARGS=$(save "$@") 163 | 164 | # Collect all arguments for the java command, following the shell quoting and substitution rules 165 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" 166 | 167 | # by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong 168 | if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then 169 | cd "$(dirname "$0")" 170 | fi 171 | 172 | exec "$JAVACMD" "$@" 173 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @if "%DEBUG%" == "" @echo off 2 | @rem ########################################################################## 3 | @rem 4 | @rem Gradle startup script for Windows 5 | @rem 6 | @rem ########################################################################## 7 | 8 | @rem Set local scope for the variables with windows NT shell 9 | if "%OS%"=="Windows_NT" setlocal 10 | 11 | set DIRNAME=%~dp0 12 | if "%DIRNAME%" == "" set DIRNAME=. 13 | set APP_BASE_NAME=%~n0 14 | set APP_HOME=%DIRNAME% 15 | 16 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 17 | set DEFAULT_JVM_OPTS= 18 | 19 | @rem Find java.exe 20 | if defined JAVA_HOME goto findJavaFromJavaHome 21 | 22 | set JAVA_EXE=java.exe 23 | %JAVA_EXE% -version >NUL 2>&1 24 | if "%ERRORLEVEL%" == "0" goto init 25 | 26 | echo. 27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 28 | echo. 29 | echo Please set the JAVA_HOME variable in your environment to match the 30 | echo location of your Java installation. 31 | 32 | goto fail 33 | 34 | :findJavaFromJavaHome 35 | set JAVA_HOME=%JAVA_HOME:"=% 36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 37 | 38 | if exist "%JAVA_EXE%" goto init 39 | 40 | echo. 41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 42 | echo. 43 | echo Please set the JAVA_HOME variable in your environment to match the 44 | echo location of your Java installation. 45 | 46 | goto fail 47 | 48 | :init 49 | @rem Get command-line arguments, handling Windows variants 50 | 51 | if not "%OS%" == "Windows_NT" goto win9xME_args 52 | 53 | :win9xME_args 54 | @rem Slurp the command line arguments. 55 | set CMD_LINE_ARGS= 56 | set _SKIP=2 57 | 58 | :win9xME_args_slurp 59 | if "x%~1" == "x" goto execute 60 | 61 | set CMD_LINE_ARGS=%* 62 | 63 | :execute 64 | @rem Setup the command line 65 | 66 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 67 | 68 | @rem Execute Gradle 69 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 70 | 71 | :end 72 | @rem End local scope for the variables with windows NT shell 73 | if "%ERRORLEVEL%"=="0" goto mainEnd 74 | 75 | :fail 76 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 77 | rem the _cmd.exe /c_ return code! 78 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 79 | exit /b 1 80 | 81 | :mainEnd 82 | if "%OS%"=="Windows_NT" endlocal 83 | 84 | :omega 85 | -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | include ':skin' 2 | include ':app' 3 | 4 | rootProject.name = "AndroidSkin" -------------------------------------------------------------------------------- /skin/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /skin/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id 'com.android.library' 3 | id 'kotlin-android' 4 | } 5 | 6 | android { 7 | compileSdkVersion 30 8 | buildToolsVersion "30.0.2" 9 | 10 | defaultConfig { 11 | minSdkVersion 21 12 | targetSdkVersion 30 13 | versionCode 1 14 | versionName "1.0" 15 | 16 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" 17 | consumerProguardFiles "consumer-rules.pro" 18 | } 19 | 20 | buildTypes { 21 | release { 22 | minifyEnabled false 23 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' 24 | } 25 | } 26 | compileOptions { 27 | sourceCompatibility JavaVersion.VERSION_1_8 28 | targetCompatibility JavaVersion.VERSION_1_8 29 | } 30 | kotlinOptions { 31 | jvmTarget = '1.8' 32 | } 33 | } 34 | 35 | dependencies { 36 | implementation fileTree(dir: "libs", include: ["*.jar"]) 37 | //noinspection GradleDependency 38 | implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version" 39 | implementation 'androidx.core:core-ktx:1.6.0' 40 | implementation 'androidx.appcompat:appcompat:1.3.1' 41 | implementation 'com.google.android.material:material:1.4.0' 42 | //协程基础库 43 | implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.4.3" 44 | //协程 Android 库,提供 UI 调度器 45 | implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.4.3" 46 | 47 | implementation 'androidx.constraintlayout:constraintlayout:2.0.4' 48 | 49 | } -------------------------------------------------------------------------------- /skin/consumer-rules.pro: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LvKang-insist/AndroidSkin/4eef34397a633fbc412e8bb8353ffd3acf324c56/skin/consumer-rules.pro -------------------------------------------------------------------------------- /skin/libs/zip4j-2.9.0.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LvKang-insist/AndroidSkin/4eef34397a633fbc412e8bb8353ffd3acf324c56/skin/libs/zip4j-2.9.0.jar -------------------------------------------------------------------------------- /skin/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile -------------------------------------------------------------------------------- /skin/src/androidTest/java/com/lvkang/skin/ExampleInstrumentedTest.kt: -------------------------------------------------------------------------------- 1 | package com.lvkang.skin 2 | 3 | import androidx.test.platform.app.InstrumentationRegistry 4 | import androidx.test.ext.junit.runners.AndroidJUnit4 5 | 6 | import org.junit.Test 7 | import org.junit.runner.RunWith 8 | 9 | import org.junit.Assert.* 10 | 11 | /** 12 | * Instrumented test, which will execute on an Android device. 13 | * 14 | * See [testing documentation](http://d.android.com/tools/testing). 15 | */ 16 | @RunWith(AndroidJUnit4::class) 17 | class ExampleInstrumentedTest { 18 | @Test 19 | fun useAppContext() { 20 | // Context of the app under test. 21 | val appContext = InstrumentationRegistry.getInstrumentation().targetContext 22 | assertEquals("com.lvkang.skin.test", appContext.packageName) 23 | } 24 | } -------------------------------------------------------------------------------- /skin/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | -------------------------------------------------------------------------------- /skin/src/main/java/com/lvkang/skin/SkinManager.kt: -------------------------------------------------------------------------------- 1 | package com.lvkang.skin 2 | 3 | import android.app.Application 4 | import android.content.Context 5 | import com.lvkang.skin.app.SkinActivityLifecycle 6 | import com.lvkang.skin.config.SkinPreUtils 7 | import com.lvkang.skin.inflater.SkinLayoutInflater 8 | import com.lvkang.skin.ktx.pathName 9 | import com.lvkang.skin.listener.SkinLoadListener 10 | import com.lvkang.skin.obsreve.SkinObserverable 11 | import com.lvkang.skin.resource.* 12 | import com.lvkang.skin.resource.SkinLoadStrategyEnum 13 | import com.lvkang.skin.resource.strategy.AbstractSkinLoadAssetsImpl 14 | import com.lvkang.skin.resource.strategy.AbstractSkinLoadStorageImpl 15 | import com.lvkang.skin.resource.strategy.AbstractSkinLoadNoneImpl 16 | import com.lvkang.skin.resource.strategy.AbstractSkinLoadZipImpl 17 | import com.lvkang.skin.util.SkinLog 18 | import kotlinx.coroutines.* 19 | 20 | @Suppress("DEPRECATION") 21 | object SkinManager : SkinObserverable() { 22 | 23 | private lateinit var application: Application 24 | private val inflaters = arrayListOf() 25 | private val startegy = mutableMapOf() 26 | private var isAutoLoadSkin = true 27 | 28 | fun init(context: Application): SkinManager { 29 | application = context 30 | return this 31 | } 32 | 33 | /** 自定义 View 时,可选择添加一个{@link SkinLayoutInflater} */ 34 | fun addInflaters(inflater: SkinLayoutInflater): SkinManager { 35 | inflaters.add(inflater) 36 | return this 37 | } 38 | 39 | 40 | fun getInflaters(): ArrayList { 41 | return inflaters 42 | } 43 | 44 | /** 设置是否使用非继承的方式实现换肤,默认为 false */ 45 | fun setAutoLoadSkin(isAutoLoadSkin: Boolean): SkinManager { 46 | this.isAutoLoadSkin = isAutoLoadSkin 47 | return this 48 | } 49 | 50 | /** 初始化完成后必须调用 */ 51 | fun build() { 52 | startegy[SkinLoadStrategyEnum.SKIN_LOADER_STARTEGY_STORAGE.name] = AbstractSkinLoadStorageImpl() 53 | startegy[SkinLoadStrategyEnum.SKIN_LOADER_STRATEGY_NONE.name] = AbstractSkinLoadNoneImpl() 54 | startegy[SkinLoadStrategyEnum.SKIN_LOADER_STRATEGY_ASSETS.name] = 55 | AbstractSkinLoadAssetsImpl() 56 | startegy[SkinLoadStrategyEnum.SKIN_LOADER_STRATEGY_ZIP.name] = AbstractSkinLoadZipImpl() 57 | 58 | if (isAutoLoadSkin) application.registerActivityLifecycleCallbacks(SkinActivityLifecycle()) 59 | val name = SkinPreUtils.getSkinName() 60 | val path = SkinPreUtils.getSkinPath() 61 | val loadStrategy = SkinPreUtils.getSkinStrategy() 62 | if (name != null && loadStrategy != null) { 63 | val strategy = getStrategyType(loadStrategy) 64 | if (strategy != null) { 65 | when (strategy) { 66 | SkinLoadStrategyEnum.SKIN_LOADER_STRATEGY_NONE -> loadNone() 67 | SkinLoadStrategyEnum.SKIN_LOADER_STRATEGY_ASSETS -> { 68 | loadAssetsSkin(name, isRepeat = true) 69 | } 70 | SkinLoadStrategyEnum.SKIN_LOADER_STARTEGY_STORAGE -> { 71 | loadStorageSkin(path, isRepeat = true) 72 | } 73 | SkinLoadStrategyEnum.SKIN_LOADER_STRATEGY_ZIP -> { 74 | loadZipSkin(path, isRepeat = true) 75 | } 76 | } 77 | return 78 | } 79 | } 80 | loadNone() 81 | } 82 | 83 | 84 | /** 加载默认皮肤 ,即无皮肤 */ 85 | fun loadNone(skinLoadListener: SkinLoadListener? = null) { 86 | val none = "none" 87 | val strategy = SkinLoadStrategyEnum.SKIN_LOADER_STRATEGY_NONE 88 | val skinLoaderStrategy = startegy[strategy.name] 89 | skinLoaderStrategy?.loadSkin(none) 90 | loadSkin(none, none, strategy, skinLoadListener) 91 | } 92 | 93 | /** 94 | * 加载资源文件夹下的皮肤 95 | * @param name 资源文件名 96 | * @param skinLoadListener 回调 97 | * @param isRepeat false 表示要加载的 skin 和当前使用的相同时不重复加载 98 | */ 99 | fun loadAssetsSkin( 100 | name: String, 101 | skinLoadListener: SkinLoadListener? = null, 102 | isRepeat: Boolean = false, 103 | ) { 104 | val strategy = SkinLoadStrategyEnum.SKIN_LOADER_STRATEGY_ASSETS 105 | if (compareSkin(name, strategy) && (!isRepeat)) { 106 | SkinLog.log("Repeat loading") 107 | skinLoadListener?.loadRepeat() 108 | return 109 | } 110 | val skinLoaderStrategy = startegy[strategy.name] 111 | 112 | CoroutineScope(Dispatchers.IO).launch { 113 | val skinPath = skinLoaderStrategy?.loadSkin(name) 114 | launch(Dispatchers.Main) { 115 | skinPath?.run { 116 | loadSkin(skinPath, name, strategy, skinLoadListener) 117 | } ?: kotlin.run { 118 | skinLoadListener?.loadSkinFailure("load failure") 119 | } 120 | } 121 | } 122 | } 123 | 124 | /** 125 | * 加载内部存储下的皮肤,必须是沙箱路径 126 | * @param path skin 绝对路径 127 | * @param skinLoadListener 回调 128 | * @param isRepeat false 表示要加载的 skin 和当前使用的相同时不重复加载 129 | */ 130 | fun loadStorageSkin( 131 | path: String, 132 | skinLoadListener: SkinLoadListener? = null, 133 | isRepeat: Boolean = false 134 | ) { 135 | val strategy = SkinLoadStrategyEnum.SKIN_LOADER_STARTEGY_STORAGE 136 | val name = pathName(path) 137 | if (compareSkin(name, strategy) && (!isRepeat)) { 138 | SkinLog.log("Repeat loading") 139 | skinLoadListener?.loadRepeat() 140 | return 141 | } 142 | val skinLoaderStrategy = startegy[strategy.name] 143 | CoroutineScope(Dispatchers.IO).launch { 144 | val skinPath = skinLoaderStrategy?.loadSkin(path) 145 | launch(Dispatchers.Main) { 146 | skinPath?.run { 147 | loadSkin(skinPath, name, strategy, skinLoadListener) 148 | } ?: kotlin.run { 149 | skinLoadListener?.loadSkinFailure("load failure") 150 | } 151 | } 152 | } 153 | } 154 | 155 | /** 156 | * 加载内部存储下的 zpi 皮肤文件,必须是沙箱路径 157 | * @param path skin 绝对路径 158 | * @param password zip 文件密码,没有可不传 159 | * @param skinLoadListener 回调 160 | * @param isRepeat false 表示要加载的 skin 和当前使用的相同时不重复加载 161 | */ 162 | fun loadZipSkin( 163 | path: String, 164 | password: String? = null, 165 | skinLoadListener: SkinLoadListener? = null, 166 | isRepeat: Boolean = false 167 | ) { 168 | val strategy = SkinLoadStrategyEnum.SKIN_LOADER_STRATEGY_ZIP 169 | val name = pathName(path) 170 | if (compareSkin(name, strategy) && (!isRepeat)) { 171 | SkinLog.log("Repeat loading") 172 | skinLoadListener?.loadRepeat() 173 | return 174 | } 175 | val skinLoaderStrategy = startegy[strategy.name] 176 | CoroutineScope(Dispatchers.IO).launch { 177 | val skinPath = skinLoaderStrategy?.loadSkin(path, password) 178 | launch(Dispatchers.Main) { 179 | skinPath?.run { 180 | loadSkin(skinPath, name, strategy, skinLoadListener) 181 | } ?: kotlin.run { 182 | skinLoadListener?.loadSkinFailure("load failure") 183 | } 184 | } 185 | } 186 | } 187 | 188 | private fun loadSkin( 189 | skinPath: String, 190 | name: String, 191 | strategyEnum: SkinLoadStrategyEnum, 192 | skinLoadListener: SkinLoadListener? 193 | ) { 194 | notifyUpdateSkin() 195 | SkinPreUtils.saveSkinStatus(skinPath, name, strategyEnum.name) 196 | skinLoadListener?.loadSkinSucess() 197 | } 198 | 199 | 200 | private fun compareSkin(skinName: String, strategyEnum: SkinLoadStrategyEnum): Boolean { 201 | val name = SkinPreUtils.getSkinName() 202 | val loadStrategy = SkinPreUtils.getSkinStrategy() 203 | if (name == skinName && strategyEnum.name == loadStrategy) { 204 | return true 205 | } 206 | return false 207 | } 208 | 209 | 210 | private fun notifyUpdateSkin() { 211 | SkinManager.notifyUpDataSkin() 212 | } 213 | 214 | 215 | fun getApplication(): Application { 216 | return application 217 | } 218 | 219 | /** 220 | * @return true 表示当前为默认皮肤 221 | */ 222 | fun getIsDefault(): Boolean { 223 | return SkinCompatResources.isDefaultSkin 224 | } 225 | 226 | /** 227 | * @param strategy 策略名称 228 | * 获取加载策略 229 | */ 230 | private fun getStrategyType(strategy: String): SkinLoadStrategyEnum? { 231 | SkinLoadStrategyEnum.values().forEach { 232 | if (it.name == strategy) { 233 | return it 234 | } 235 | } 236 | return null 237 | } 238 | 239 | 240 | fun getContext(): Context { 241 | return application 242 | } 243 | 244 | } 245 | -------------------------------------------------------------------------------- /skin/src/main/java/com/lvkang/skin/app/SkinActivityLifecycle.kt: -------------------------------------------------------------------------------- 1 | package com.lvkang.skin.app 2 | 3 | import android.app.Activity 4 | import android.app.Application 5 | import android.content.Context 6 | import android.os.Bundle 7 | import android.util.Log 8 | import android.view.LayoutInflater 9 | import androidx.core.view.LayoutInflaterCompat 10 | import com.lvkang.skin.SkinManager 11 | import com.lvkang.skin.factory.SkinCompateFactory 12 | import com.lvkang.skin.obsreve.SkinObserver 13 | import com.lvkang.skin.util.SkinLog 14 | import java.lang.Exception 15 | import java.util.* 16 | 17 | /** 18 | * @name SkinActivityLifecycle 19 | * @package com.lvkang.skin.app 20 | * @author 345 QQ:1831712732 21 | * @time 2020/12/13 21:40 22 | * @description 监听 Activity 的创建,以实现换肤 23 | */ 24 | class SkinActivityLifecycle : Application.ActivityLifecycleCallbacks { 25 | 26 | 27 | private val weekDelegateMap by lazy { WeakHashMap() } 28 | private val weekObserverMap by lazy { WeakHashMap() } 29 | 30 | 31 | override fun onActivityCreated(activity: Activity, savedInstanceState: Bundle?) { 32 | (activity as? SkinCompatActivity)?.run { 33 | } ?: kotlin.run { 34 | installLayoutFactory(activity) 35 | Log.e("---345--->", "111111111111111"); 36 | } 37 | 38 | } 39 | 40 | override fun onActivityStarted(activity: Activity) = Unit 41 | 42 | override fun onActivityResumed(activity: Activity) { 43 | (activity as? SkinCompatActivity)?.run { 44 | } ?: kotlin.run { 45 | val lazyObserver = getLazyObserver(activity) 46 | SkinManager.addSkinObserver(lazyObserver) 47 | } 48 | } 49 | 50 | override fun onActivityPaused(activity: Activity) = Unit 51 | 52 | override fun onActivityStopped(activity: Activity) = Unit 53 | 54 | override fun onActivitySaveInstanceState(activity: Activity, outState: Bundle) = Unit 55 | 56 | override fun onActivityDestroyed(activity: Activity) { 57 | (activity as? SkinCompatActivity)?.run { 58 | } ?: kotlin.run { 59 | val lazyObserver = getLazyObserver(activity) 60 | SkinManager.removeSkinObserver(lazyObserver) 61 | } 62 | 63 | } 64 | 65 | private fun installLayoutFactory(context: Context) { 66 | try { 67 | //这里不能同通过 LayoutInflaterCompat 设置 factory2 68 | //因为此时的 activity 已经在 setContentView 方法这种设置过 fractory2 了 69 | //factory2不允许重复设置,所以这里需要通过反射重新设置 70 | // val skinFactory = getSkinFactory(context) 71 | // LayoutInflaterCompat.setFactory2((context as Activity).layoutInflater, skinFactory) 72 | val layoutInflater = LayoutInflater.from(context) 73 | val inflaterCompat = LayoutInflaterCompat::class.java 74 | val inflater = LayoutInflater::class.java 75 | 76 | val sCheckedFiled = inflaterCompat.getDeclaredField("sCheckedField") 77 | sCheckedFiled.isAccessible = true 78 | sCheckedFiled.set(layoutInflater, false) 79 | 80 | val skinFactory = getSkinFactory(context) 81 | val mFactory = inflater.getDeclaredField("mFactory") 82 | val mFactory2 = inflater.getDeclaredField("mFactory2") 83 | mFactory.isAccessible = true 84 | mFactory2.isAccessible = true 85 | 86 | mFactory.set(layoutInflater, skinFactory) 87 | mFactory2.set(layoutInflater, skinFactory) 88 | 89 | } catch (e: Exception) { 90 | e.printStackTrace() 91 | SkinLog.log("A factory has already been set on this LayoutInflater") 92 | } 93 | } 94 | 95 | private fun getLazyObserver(context: Context): LazySkinObserver { 96 | var lazySkinObserver = weekObserverMap[context] 97 | if (lazySkinObserver == null) { 98 | lazySkinObserver = LazySkinObserver(context) 99 | weekObserverMap[context] = lazySkinObserver 100 | } 101 | return lazySkinObserver 102 | } 103 | 104 | private fun getSkinFactory(context: Context): SkinCompateFactory { 105 | var skinCompateFactory = weekDelegateMap[context] 106 | if (skinCompateFactory == null) { 107 | skinCompateFactory = SkinCompateFactory() 108 | weekDelegateMap[context] = skinCompateFactory 109 | } 110 | return skinCompateFactory 111 | } 112 | 113 | 114 | inner class LazySkinObserver(val context: Context) : SkinObserver { 115 | 116 | override fun applySkin() = updataSkin() 117 | 118 | private fun updataSkin() { 119 | val skinFactory = getSkinFactory(context) 120 | skinFactory.applySkin() 121 | } 122 | } 123 | } -------------------------------------------------------------------------------- /skin/src/main/java/com/lvkang/skin/app/SkinCompatActivity.kt: -------------------------------------------------------------------------------- 1 | package com.lvkang.skin.app 2 | 3 | import android.os.Bundle 4 | import androidx.appcompat.app.AppCompatActivity 5 | import androidx.core.view.LayoutInflaterCompat 6 | import com.lvkang.skin.SkinManager 7 | import com.lvkang.skin.factory.SkinCompateFactory 8 | import com.lvkang.skin.obsreve.SkinObserver 9 | 10 | /** 11 | * @name SkinCompatActivity 12 | * @package com.lvkang.skin.app 13 | * @author 345 QQ:1831712732 14 | * @time 2020/11/27 23:43 15 | * @description 16 | */ 17 | open class SkinCompatActivity : AppCompatActivity(), SkinObserver { 18 | 19 | 20 | private val skinFactory by lazy { SkinCompateFactory() } 21 | 22 | override fun onCreate(savedInstanceState: Bundle?) { 23 | LayoutInflaterCompat.setFactory2(layoutInflater, skinFactory) 24 | super.onCreate(savedInstanceState) 25 | } 26 | 27 | override fun onResume() { 28 | super.onResume() 29 | SkinManager.addSkinObserver(this) 30 | } 31 | 32 | override fun onDestroy() { 33 | super.onDestroy() 34 | SkinManager.removeSkinObserver(this) 35 | } 36 | 37 | override fun applySkin() { 38 | skinFactory.applySkin() 39 | } 40 | 41 | } -------------------------------------------------------------------------------- /skin/src/main/java/com/lvkang/skin/config/SkinKey.kt: -------------------------------------------------------------------------------- 1 | package com.lvkang.skin.config 2 | 3 | /** 4 | * @name SkinConfig 5 | * @package com.lvkang.skin.config 6 | * @author 345 QQ:1831712732 7 | * @time 2020/11/24 23:31 8 | * @description 9 | */ 10 | object SkinKey { 11 | 12 | const val TAG = "tag" 13 | 14 | const val SKIN_INFO_NAME = "skinInfo" 15 | 16 | /** 皮肤缓存 DirPath */ 17 | const val SKIN_DIR_PATH = "skin_dir_path" 18 | 19 | /** 皮肤名字 */ 20 | const val SKIN_NAME = "skin_name" 21 | 22 | /** 皮肤加载策略 */ 23 | const val SKIN_STRATEGY = "skin_strategy" 24 | 25 | } -------------------------------------------------------------------------------- /skin/src/main/java/com/lvkang/skin/config/SkinPreUtils.kt: -------------------------------------------------------------------------------- 1 | package com.lvkang.skin.config 2 | 3 | import android.annotation.SuppressLint 4 | import android.content.Context 5 | import com.lvkang.skin.SkinManager 6 | import java.io.File 7 | 8 | 9 | /** 10 | * @name SkinConfig 11 | * @package com.lvkang.skin.config 12 | * @author 345 QQ:1831712732 13 | * @time 2020/11/24 23:31 14 | * @description 15 | */ 16 | @SuppressLint("StaticFieldLeak") 17 | object SkinPreUtils { 18 | 19 | private val context by lazy { SkinManager.getApplication() } 20 | 21 | private val skinDir by lazy { "${context.getExternalFilesDir("file")}${File.separator}" } 22 | 23 | 24 | /** 25 | * 保存当前皮肤路径 26 | */ 27 | private fun saveSkinPath(skinPath: String?) { 28 | context.getSharedPreferences(SkinKey.SKIN_INFO_NAME, Context.MODE_PRIVATE) 29 | .edit() 30 | .putString(SkinKey.SKIN_DIR_PATH, skinPath) 31 | .apply() 32 | } 33 | 34 | 35 | fun getSkinPath(): String { 36 | return context.getSharedPreferences(SkinKey.SKIN_INFO_NAME, Context.MODE_PRIVATE) 37 | .getString(SkinKey.SKIN_DIR_PATH, skinDir)!! 38 | } 39 | 40 | /** 41 | * @param skinPath 皮肤路径,注意不包括皮肤名称 42 | * @param skinName 皮肤名称 43 | * @param skinStrategy 皮肤加载策略 44 | * 保存当前使用皮肤的状态 45 | */ 46 | fun saveSkinStatus(skinPath: String, skinName: String, skinStrategy: String?) { 47 | saveSkinPath(skinPath) 48 | saveSkinName(skinName) 49 | saveSkinStrategy(skinStrategy) 50 | } 51 | 52 | 53 | /** 54 | * 保存当前皮肤名称 55 | */ 56 | private fun saveSkinName(skinName: String?) { 57 | context.getSharedPreferences(SkinKey.SKIN_INFO_NAME, Context.MODE_PRIVATE) 58 | .edit() 59 | .putString(SkinKey.SKIN_NAME, skinName) 60 | .apply() 61 | } 62 | 63 | /** 64 | * 获取当前皮肤名称 65 | */ 66 | fun getSkinName(): String? { 67 | return context.getSharedPreferences(SkinKey.SKIN_INFO_NAME, Context.MODE_PRIVATE) 68 | .getString(SkinKey.SKIN_NAME, null) 69 | } 70 | 71 | 72 | /** 73 | * 保存当前皮肤加载策略 74 | */ 75 | private fun saveSkinStrategy(skinStrategy: String?) { 76 | context.getSharedPreferences(SkinKey.SKIN_INFO_NAME, Context.MODE_PRIVATE) 77 | .edit() 78 | .putString(SkinKey.SKIN_STRATEGY, skinStrategy) 79 | .apply() 80 | } 81 | 82 | /** 83 | * 获取当前皮肤加载策略 84 | */ 85 | fun getSkinStrategy(): String? { 86 | return context.getSharedPreferences(SkinKey.SKIN_INFO_NAME, Context.MODE_PRIVATE) 87 | .getString(SkinKey.SKIN_STRATEGY, null) 88 | } 89 | 90 | /** 91 | * 清空皮肤路径 92 | */ 93 | fun clearSkinInfo() { 94 | saveSkinPath(null) 95 | saveSkinName(null) 96 | saveSkinStrategy(null) 97 | } 98 | 99 | /** 100 | * 添加一个标记 101 | */ 102 | fun setTag(boolean: Boolean) { 103 | context.getSharedPreferences(SkinKey.TAG, Context.MODE_PRIVATE) 104 | .edit() 105 | .putBoolean(SkinKey.TAG, boolean) 106 | .apply() 107 | } 108 | 109 | /** 110 | * 获取标记 111 | */ 112 | fun getTag(): Boolean { 113 | return context.getSharedPreferences(SkinKey.TAG, Context.MODE_PRIVATE) 114 | .getBoolean(SkinKey.TAG, false) 115 | } 116 | } -------------------------------------------------------------------------------- /skin/src/main/java/com/lvkang/skin/factory/SkinCompateFactory.kt: -------------------------------------------------------------------------------- 1 | package com.lvkang.skin.factory 2 | 3 | import android.content.Context 4 | import android.util.AttributeSet 5 | import android.util.Log 6 | import android.view.LayoutInflater 7 | import android.view.View 8 | import com.lvkang.skin.inflater.SkinViewInflater 9 | import com.lvkang.skin.wedget.SkinCompatSupportable 10 | import java.lang.ref.WeakReference 11 | import java.util.concurrent.CopyOnWriteArrayList 12 | 13 | /** 14 | * @author 345 QQ:1831712732 15 | * @name SkinCompateFactory 16 | * @package com.lvkang.skin.factory 17 | * @time 2020/11/27 23:39 18 | * @description 拦截 View 的创建 19 | */ 20 | class SkinCompateFactory : LayoutInflater.Factory2 { 21 | 22 | private val mSkinHelpers = CopyOnWriteArrayList>() 23 | 24 | private val mSkinCompatViewInflater by lazy { SkinViewInflater() } 25 | 26 | override fun onCreateView( 27 | parent: View?, 28 | name: String, 29 | context: Context, 30 | attrs: AttributeSet 31 | ): View? { 32 | return onCreateView(name, context, attrs) 33 | } 34 | 35 | override fun onCreateView(name: String, context: Context, attrs: AttributeSet): View? { 36 | val view = mSkinCompatViewInflater.createView(name, context, attrs) 37 | if (view is SkinCompatSupportable) { 38 | mSkinHelpers.add(WeakReference(view)) 39 | } 40 | return view 41 | } 42 | 43 | fun applySkin() { 44 | if (mSkinHelpers.isNotEmpty()) { 45 | mSkinHelpers.forEach { 46 | it?.run { 47 | get()?.run { 48 | applySkin() 49 | } 50 | } 51 | } 52 | } 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /skin/src/main/java/com/lvkang/skin/inflater/SkinAppCompatViewInflater.kt: -------------------------------------------------------------------------------- 1 | package com.lvkang.skin.inflater 2 | 3 | import android.content.Context 4 | import android.util.AttributeSet 5 | import android.view.View 6 | import androidx.appcompat.widget.LinearLayoutCompat 7 | import com.lvkang.skin.util.SkinLog 8 | import com.lvkang.skin.wedget.android.FrameLayoutX 9 | import com.lvkang.skin.wedget.android.RelativeLayoutX 10 | import com.lvkang.skin.wedget.android.ViewX 11 | import com.lvkang.skin.wedget.androidx.* 12 | import com.lvkang.skin.wedget.androidx.cardview.CardViewX 13 | 14 | /** 15 | * @name SkinAppCompatViewInflater 16 | * @package com.lvkang.skin.inflater 17 | * @author 345 QQ:1831712732 18 | * @time 2020/12/01 23:29 19 | * @description 20 | */ 21 | class SkinAppCompatViewInflater : SkinLayoutInflater { 22 | @Suppress("PrivatePropertyName") 23 | private val TAG = "SkinAppCompatViewInflat" 24 | override fun createView(context: Context, name: String, attres: AttributeSet): View? { 25 | return createViewFrom(context, name, attres) 26 | } 27 | 28 | private fun createViewFrom(context: Context, name: String, attres: AttributeSet): View? { 29 | when (name) { 30 | "View" -> return ViewX(context, attres) 31 | "ImageView" -> return ImageViewX(context, attres) 32 | "Button" -> return ButtonX(context, attres) 33 | "EditText" -> return EditTextX(context, attres) 34 | "TextView" -> return TextViewX(context, attres) 35 | "FrameLayout" -> return FrameLayoutX(context, attres) 36 | "RelativeLayout" -> return RelativeLayoutX(context, attres) 37 | "ScrollView" -> return NestedScrollViewX(context, attres) 38 | "androidx.appcompat.widget.AppCompatImageView" -> return ImageViewX(context, attres) 39 | "androidx.appcompat.widget.AppCompatButton" -> return ButtonX(context, attres) 40 | "androidx.appcompat.widget.AppCompatTextView" -> return TextViewX(context, attres) 41 | "androidx.appcompat.widget.AppCompatEditText" -> return EditTextX(context, attres) 42 | "androidx.cardview.widget.CardView" -> return CardViewX(context, attres) 43 | "androidx.core.widget.NestedScrollView" -> { 44 | return NestedScrollViewX(context, attres) 45 | } 46 | "androidx.constraintlayout.widget.ConstraintLayout" -> { 47 | return ConstraintLayoutX(context, attres) 48 | } 49 | "androidx.appcompat.widget.LinearLayoutCompat" -> { 50 | return LinearLayoutCompat(context, attres) 51 | } 52 | } 53 | 54 | return null 55 | } 56 | } -------------------------------------------------------------------------------- /skin/src/main/java/com/lvkang/skin/inflater/SkinLayoutInflater.kt: -------------------------------------------------------------------------------- 1 | package com.lvkang.skin.inflater 2 | 3 | import android.content.Context 4 | import android.util.AttributeSet 5 | import android.view.View 6 | import androidx.annotation.NonNull 7 | 8 | /** 9 | * @name SkinLayoutInflater 10 | * @package com.lvkang.skin.inflater 11 | * @author 345 QQ:1831712732 12 | * @time 2020/11/29 14:18 13 | * @description 14 | */ 15 | interface SkinLayoutInflater { 16 | fun createView(@NonNull context: Context, name: String, @NonNull attres: AttributeSet): View? 17 | } -------------------------------------------------------------------------------- /skin/src/main/java/com/lvkang/skin/inflater/SkinViewInflater.kt: -------------------------------------------------------------------------------- 1 | package com.lvkang.skin.inflater 2 | 3 | import android.R 4 | import android.annotation.SuppressLint 5 | import android.content.Context 6 | import android.content.ContextWrapper 7 | import android.os.Build 8 | import android.util.AttributeSet 9 | import android.view.InflateException 10 | import android.view.View 11 | import androidx.core.view.ViewCompat 12 | import com.lvkang.skin.SkinManager 13 | import java.lang.reflect.Constructor 14 | import java.lang.reflect.InvocationTargetException 15 | import java.lang.reflect.Method 16 | 17 | /** 18 | * @name SkinCompatViewInflater 19 | * @package com.lvkang.skin.inflater 20 | * @author 345 QQ:1831712732 21 | * @time 2020/11/29 14:41 22 | * @description 23 | */ 24 | open class SkinViewInflater { 25 | 26 | private val mConstructorArgs = arrayOfNulls(2) 27 | private val sClassPrefixList = arrayOf( 28 | "android.widget.", 29 | "android.view.", 30 | "android.webkit." 31 | ) 32 | private val sConstructorMap: MutableMap> = mutableMapOf() 33 | private val sConstructorSignature: Array> = arrayOf( 34 | Context::class.java, AttributeSet::class.java 35 | ) 36 | private val sOnClickAttrs = intArrayOf(R.attr.onClick) 37 | 38 | 39 | fun createView(name: String, context: Context, attrs: AttributeSet): View? { 40 | //从自定义的 Inflater 中进行加載 41 | var view = createViewFromInflater(context, name, attrs) 42 | 43 | if (view == null) { 44 | view = createViewFromTag(context, name, attrs) 45 | } 46 | 47 | if (view != null) { 48 | // If we have created a view, check its android:onClick 49 | checkOnClickListener(view, attrs) 50 | } 51 | return view 52 | } 53 | 54 | 55 | private fun createViewFromInflater(context: Context, name: String, attrs: AttributeSet): View? { 56 | var view: View? 57 | SkinManager.getInflaters().forEach { 58 | view = it.createView(context, name, attrs) 59 | if (view != null) return view 60 | } 61 | return null 62 | } 63 | 64 | 65 | private fun createViewFromTag(context: Context, n: String, attrs: AttributeSet): View? { 66 | var name = n 67 | if (name == "view") { 68 | name = attrs.getAttributeValue(null, "class") 69 | } 70 | return try { 71 | mConstructorArgs[0] = context 72 | mConstructorArgs[1] = attrs 73 | if (-1 == name.indexOf('.')) { 74 | for (s in sClassPrefixList) { 75 | val view: View? = createViewByPrefix(context, name, s) 76 | if (view != null) { 77 | return view 78 | } 79 | } 80 | null 81 | } else { 82 | createViewByPrefix(context, name, null) 83 | } 84 | } catch (e: Exception) { 85 | // We do not want to catch these, lets return null and let the actual LayoutInflater 86 | // try 87 | null 88 | } finally { 89 | // Don't retain references on context. 90 | mConstructorArgs[0] = null 91 | mConstructorArgs[1] = null 92 | } 93 | } 94 | 95 | @Throws(ClassNotFoundException::class, InflateException::class) 96 | private fun createViewByPrefix(context: Context, name: String, prefix: String?): View? { 97 | var constructor: Constructor? = sConstructorMap[name] 98 | return try { 99 | if (constructor == null) { 100 | // Class not found in the cache, see if it's real, and try to add it 101 | val clazz = Class.forName( 102 | if (prefix != null) prefix + name else name, 103 | false, 104 | context.classLoader 105 | ).asSubclass(View::class.java) 106 | constructor = clazz.getConstructor(*sConstructorSignature) 107 | sConstructorMap[name] = constructor 108 | } 109 | constructor?.isAccessible = true 110 | constructor?.newInstance(*mConstructorArgs) 111 | } catch (e: java.lang.Exception) { 112 | // We do not want to catch these, lets return null and let the actual LayoutInflater 113 | // try 114 | null 115 | } 116 | } 117 | 118 | @SuppressLint("ObsoleteSdkInt") 119 | private fun checkOnClickListener(view: View, attrs: AttributeSet) { 120 | val context = view.context 121 | if (context !is ContextWrapper || 122 | Build.VERSION.SDK_INT >= 15 && !ViewCompat.hasOnClickListeners(view) 123 | ) { 124 | // Skip our compat functionality if: the Context isn't a ContextWrapper, or 125 | // the view doesn't have an OnClickListener (we can only rely on this on API 15+ so 126 | // always use our compat code on older devices) 127 | return 128 | } 129 | 130 | val a = context.obtainStyledAttributes(attrs, sOnClickAttrs) 131 | val handlerName = a.getString(0) 132 | if (handlerName != null) { 133 | view.setOnClickListener(DeclaredOnClickListener(view, handlerName)) 134 | } 135 | a.recycle() 136 | } 137 | 138 | /** 139 | * An implementation of OnClickListener that attempts to lazily load a 140 | * named click handling method from a parent or ancestor context. 141 | */ 142 | private class DeclaredOnClickListener( 143 | private val mHostView: View, 144 | private val mMethodName: String 145 | ) : 146 | View.OnClickListener { 147 | private var mResolvedMethod: Method? = null 148 | private var mResolvedContext: Context? = null 149 | override fun onClick(v: View) { 150 | if (mResolvedMethod == null) { 151 | resolveMethod(mHostView.context, mMethodName) 152 | } 153 | try { 154 | mResolvedMethod!!.invoke(mResolvedContext, v) 155 | } catch (e: IllegalAccessException) { 156 | throw IllegalStateException( 157 | "Could not execute non-public method for android:onClick", e 158 | ) 159 | } catch (e: InvocationTargetException) { 160 | throw IllegalStateException( 161 | "Could not execute method for android:onClick", e 162 | ) 163 | } 164 | } 165 | 166 | private fun resolveMethod(mContext: Context?, name: String) { 167 | var context = mContext 168 | while (context != null) { 169 | try { 170 | if (!context.isRestricted) { 171 | val method = context.javaClass.getMethod( 172 | mMethodName, 173 | View::class.java 174 | ) 175 | mResolvedMethod = method 176 | mResolvedContext = context 177 | return 178 | } 179 | } catch (e: NoSuchMethodException) { 180 | // Failed to find method, keep searching up the hierarchy. 181 | } 182 | context = if (context is ContextWrapper) { 183 | context.baseContext 184 | } else { 185 | // Can't search up the hierarchy, null out and fail. 186 | null 187 | } 188 | } 189 | val id = mHostView.id 190 | val idText = 191 | if (id == View.NO_ID) "" else " with id '" + mHostView.context.resources.getResourceEntryName( 192 | id 193 | ) + "'" 194 | throw IllegalStateException( 195 | "Could not find method " + mMethodName 196 | + "(View) in a parent or ancestor Context for android:onClick " 197 | + "attribute defined on view " + mHostView.javaClass + idText 198 | ) 199 | } 200 | } 201 | } -------------------------------------------------------------------------------- /skin/src/main/java/com/lvkang/skin/ktx/SkinHelper.kt: -------------------------------------------------------------------------------- 1 | package com.lvkang.skin.ktx 2 | 3 | import android.content.res.TypedArray 4 | import android.util.AttributeSet 5 | import android.view.View 6 | import androidx.annotation.AttrRes 7 | import androidx.annotation.StyleRes 8 | import androidx.annotation.StyleableRes 9 | import com.lvkang.skin.SkinManager 10 | import com.lvkang.skin.config.SkinPreUtils 11 | import java.io.File 12 | import java.lang.Exception 13 | 14 | /** 15 | * @name SkinHelper 16 | * @package com.lvkang.skin.ktx 17 | * @author 345 QQ:1831712732 18 | * @time 2020/11/29 16:40 19 | * @description 20 | */ 21 | 22 | inline fun obtainStyledAttributes( 23 | view: View, 24 | set: AttributeSet?, 25 | @StyleableRes attrs: IntArray, 26 | @AttrRes defstyleAttr: Int, 27 | @StyleRes defStyleRes: Int, 28 | crossinline block: (TypedArray) -> Unit 29 | ) { 30 | val a = 31 | view.context.obtainStyledAttributes(set, attrs, defstyleAttr, defStyleRes) 32 | tryCatch { block(a) } 33 | a.recycle() 34 | } 35 | 36 | inline fun tryCatch(block: () -> Unit) { 37 | try { 38 | block() 39 | } catch (e: Exception) { 40 | e.printStackTrace() 41 | } 42 | } 43 | 44 | /** 45 | * 根据手机的分辨率从 px(像素) 的单位 转成为 dp 46 | */ 47 | fun px2dip(pxValue: Float): Float { 48 | val scale: Float = SkinManager.getContext().resources.displayMetrics.density 49 | return (pxValue / scale + 0.5f) 50 | } 51 | 52 | 53 | /** 54 | * 文件是否存在,true 表示存在 55 | */ 56 | fun isFile(filePath: String): Boolean { 57 | if (!File(filePath).exists()) { 58 | return false 59 | } 60 | return true 61 | } 62 | 63 | fun pathName(path: String): String { 64 | val name = path.substring(path.lastIndexOf("/") + 1, path.lastIndexOf(".")) 65 | val suffix = path.substring(path.lastIndexOf(".") + 1, path.length) 66 | return "$name.$suffix" 67 | } -------------------------------------------------------------------------------- /skin/src/main/java/com/lvkang/skin/listener/SkinLoadListener.kt: -------------------------------------------------------------------------------- 1 | package com.lvkang.skin.listener 2 | 3 | /** 4 | * @name SkinLoadListener 5 | * @package com.lvkang.skin 6 | * @author 345 QQ:1831712732 7 | * @time 2020/12/07 22:31 8 | * @description 9 | */ 10 | interface SkinLoadListener { 11 | 12 | /** 换肤成功 */ 13 | fun loadSkinSucess() 14 | 15 | /** 加载失败 */ 16 | fun loadSkinFailure(error: String) 17 | 18 | /** 重复加载 */ 19 | fun loadRepeat() 20 | } -------------------------------------------------------------------------------- /skin/src/main/java/com/lvkang/skin/obsreve/SkinObserver.kt: -------------------------------------------------------------------------------- 1 | package com.lvkang.skin.obsreve 2 | 3 | /** 4 | * @name SkinObserver 5 | * @package com.lvkang.skin.obsreve 6 | * @author 345 QQ:1831712732 7 | * @time 2020/12/01 22:31 8 | * @description 9 | */ 10 | interface SkinObserver { 11 | fun applySkin() 12 | } -------------------------------------------------------------------------------- /skin/src/main/java/com/lvkang/skin/obsreve/SkinObserverable.kt: -------------------------------------------------------------------------------- 1 | package com.lvkang.skin.obsreve 2 | 3 | /** 4 | * @name SkinObserverable 5 | * @package com.lvkang.skin.obsreve 6 | * @author 345 QQ:1831712732 7 | * @time 2020/12/01 22:37 8 | * @description 9 | */ 10 | open class SkinObserverable { 11 | private val observers by lazy { 12 | arrayListOf() 13 | } 14 | 15 | fun addSkinObserver(skinObserver: SkinObserver) { 16 | //如果不包含此观察者,则添加 17 | if (!observers.contains(skinObserver)) 18 | observers.add(skinObserver) 19 | } 20 | 21 | fun removeSkinObserver(skinObserver: SkinObserver) { 22 | observers.remove(skinObserver) 23 | } 24 | 25 | fun clearSkinObserver() { 26 | observers.clear() 27 | } 28 | 29 | fun notifyUpDataSkin() { 30 | observers.forEach { 31 | it.applySkin() 32 | } 33 | } 34 | 35 | fun skinObserveSize(): Int { 36 | return observers.size 37 | } 38 | } -------------------------------------------------------------------------------- /skin/src/main/java/com/lvkang/skin/resource/AbstractSkinLoadStrategy.kt: -------------------------------------------------------------------------------- 1 | package com.lvkang.skin.resource 2 | 3 | import android.content.Context 4 | import android.graphics.drawable.Drawable 5 | import com.lvkang.skin.SkinManager 6 | import java.io.File 7 | 8 | /** 9 | * @name SkinLoaderStrategy 10 | * @package com.lvkang.skin.resource 11 | * @author 345 QQ:1831712732 12 | * @time 2020/12/07 00:24 13 | * @description 皮肤包加载策略 14 | */ 15 | 16 | 17 | abstract class AbstractSkinLoadStrategy { 18 | 19 | val skinFileDir = "${SkinManager.getApplication().getExternalFilesDir("file")}${File.separator}" 20 | 21 | /** 22 | * 加载皮肤 23 | * @return 不等于 null 且 length 大于0 表示皮肤加载成功 24 | * length == 0 表示使用的是 SKIN_LOADER_STRATEGY_NONE,即为没有加载皮肤,使用 app 内部资源 25 | * 等于 null 表示皮肤加载失败 26 | */ 27 | abstract fun loadSkin(vararg any: String?): String? 28 | 29 | /** 30 | * @return 加载策略 31 | */ 32 | abstract fun getType(): SkinLoadStrategyEnum 33 | 34 | /** 35 | *@return 皮肤包中资源的名称 36 | */ 37 | open fun getSkinResName(): String? = null 38 | 39 | /** 40 | * 通过重写此方法可返回自定义颜色 41 | * @return color 42 | */ 43 | open fun getColor(context: Context, skinName: String, resId: Int): Int = 44 | SkinCompatResources.NOT_ID 45 | 46 | /** 47 | * 通过重写此方法可返回自定义drawable 48 | * @return drawable 49 | */ 50 | open fun getDrawable(context: Context, skinName: String, resId: Int): Drawable? = null 51 | 52 | open fun getDimension(context: Context, skinName: String, resId: Int): Float? = null 53 | 54 | open fun getString(context: Context, skinName: String, resId: Int): String? = null 55 | 56 | } -------------------------------------------------------------------------------- /skin/src/main/java/com/lvkang/skin/resource/SkinCompatResources.kt: -------------------------------------------------------------------------------- 1 | package com.lvkang.skin.resource 2 | 3 | import android.annotation.SuppressLint 4 | import android.content.pm.PackageManager 5 | import android.content.res.AssetManager 6 | import android.content.res.ColorStateList 7 | import android.content.res.Resources 8 | import android.graphics.drawable.Drawable 9 | import android.util.Log 10 | import androidx.core.content.res.ResourcesCompat 11 | import com.lvkang.skin.SkinManager 12 | import com.lvkang.skin.ktx.tryCatch 13 | import com.lvkang.skin.util.SkinLog 14 | 15 | /** 16 | * @name SkinResource 17 | * @package com.lvkang.skin 18 | * @author 345 QQ:1831712732 19 | * @time 2020/11/24 23:31 20 | * @description 皮肤的资源管理器 21 | */ 22 | @SuppressLint("DiscouragedPrivateApi") 23 | object SkinCompatResources { 24 | const val NOT_ID = 0 25 | private lateinit var resources: Resources 26 | private lateinit var packageName: String 27 | private lateinit var skinName: String 28 | private lateinit var loadStrategyAbstract: AbstractSkinLoadStrategy 29 | private val context by lazy { SkinManager.getContext() } 30 | var isDefaultSkin = true 31 | 32 | 33 | fun resetSkin( 34 | resources: Resources, 35 | loadStrategyAbstract: AbstractSkinLoadStrategy 36 | ) { 37 | isDefaultSkin = true 38 | this.resources = resources 39 | this.packageName = "" 40 | this.skinName = "" 41 | this.loadStrategyAbstract = loadStrategyAbstract 42 | } 43 | 44 | 45 | fun setupSkin( 46 | resources: Resources, 47 | packageName: String, 48 | skinName: String, 49 | loadStrategyAbstract: AbstractSkinLoadStrategy 50 | ) { 51 | this.resources = resources 52 | this.packageName = packageName 53 | this.skinName = skinName 54 | this.loadStrategyAbstract = loadStrategyAbstract 55 | isDefaultSkin = false 56 | } 57 | 58 | 59 | /** 60 | * 获取 String 61 | */ 62 | fun getString(resId: Int): String? { 63 | tryCatch { 64 | val string = loadStrategyAbstract.getString(context, skinName, resId) 65 | if (string != null) return string 66 | if (!isDefaultSkin) { 67 | val skinResId = getSkinResId(resId) 68 | if (skinResId != NOT_ID) 69 | return resources.getString(skinResId) 70 | } else { 71 | return context.resources.getString(resId) 72 | } 73 | } 74 | return null 75 | } 76 | 77 | /** 78 | * 获取 Dimension 79 | */ 80 | fun getDimension(resId: Int): Float? { 81 | tryCatch { 82 | val float = loadStrategyAbstract.getDimension(context, skinName, resId) 83 | if (float != null) return float 84 | if (!isDefaultSkin) { 85 | val skinResId = getSkinResId(resId) 86 | if (skinResId != NOT_ID) 87 | return resources.getDimension(skinResId) 88 | } else { 89 | return context.resources.getDimension(resId) 90 | } 91 | } 92 | return null 93 | } 94 | 95 | /** 96 | * 获取 Drawable 97 | */ 98 | fun getDrawable(resId: Int): Drawable? { 99 | tryCatch { 100 | val drawable = loadStrategyAbstract.getDrawable(context, skinName, resId) 101 | if (drawable != null) return drawable 102 | if (!isDefaultSkin) { 103 | val skinResId = getSkinResId(resId) 104 | if (skinResId != NOT_ID) 105 | return ResourcesCompat.getDrawable(resources, skinResId, null) 106 | } else { 107 | return ResourcesCompat.getDrawable(context.resources, resId, null) 108 | } 109 | } 110 | return null 111 | } 112 | 113 | /** 获取 Color */ 114 | fun getColor(resId: Int): Int? { 115 | tryCatch { 116 | val color = loadStrategyAbstract.getColor(context, skinName, resId) 117 | if (color != NOT_ID) return color 118 | if (!isDefaultSkin) { 119 | val skinResId = getSkinResId(resId) 120 | if (skinResId != NOT_ID) 121 | return ResourcesCompat.getColor(resources, skinResId, null) 122 | } else { 123 | return ResourcesCompat.getColor(context.resources, resId, null) 124 | } 125 | } 126 | return null 127 | } 128 | 129 | fun getColorStateList(resId: Int): ColorStateList? { 130 | tryCatch { 131 | if (!isDefaultSkin) { 132 | val skinResId = getSkinResId(resId) 133 | if (skinResId != NOT_ID) 134 | return ResourcesCompat.getColorStateList(resources, skinResId, null) 135 | } else { 136 | return ResourcesCompat.getColorStateList(context.resources, resId, null) 137 | } 138 | } 139 | return null 140 | } 141 | 142 | private fun getSkinResId(resId: Int): Int { 143 | return try { 144 | val resName = 145 | loadStrategyAbstract.getSkinResName() ?: context.resources.getResourceEntryName( 146 | resId 147 | ) 148 | val resType = context.resources.getResourceTypeName(resId) 149 | SkinLog.log("$resId $resName $resType") 150 | resources.getIdentifier(resName, resType, packageName) 151 | } catch (e: Exception) { 152 | SkinLog.log(e.message ?: "Not Font resId $resId") 153 | NOT_ID 154 | } 155 | } 156 | 157 | /** 获取皮肤包 resources */ 158 | @SuppressLint("DiscouragedPrivateApi") 159 | fun getSkinResources(skinPath: String): Resources? { 160 | return try { 161 | val superRes = SkinManager.getApplication().resources 162 | val asset = AssetManager::class.java.newInstance() 163 | val method = 164 | AssetManager::class.java.getDeclaredMethod("addAssetPath", String::class.java) 165 | method.invoke(asset, skinPath) 166 | Resources(asset, superRes.displayMetrics, superRes.configuration) 167 | } catch (e: java.lang.Exception) { 168 | e.printStackTrace() 169 | null 170 | } 171 | } 172 | 173 | /** 174 | * 获取皮肤包名 175 | */ 176 | fun getSkinPackageName(skinPath: String): String? { 177 | return SkinManager.getContext().packageManager.getPackageArchiveInfo( 178 | skinPath, 179 | PackageManager.GET_ACTIVITIES 180 | )?.packageName 181 | } 182 | 183 | 184 | } -------------------------------------------------------------------------------- /skin/src/main/java/com/lvkang/skin/resource/SkinLoadStrategyEnum.kt: -------------------------------------------------------------------------------- 1 | package com.lvkang.skin.resource 2 | 3 | /** 4 | * @name SkinLoadType 5 | * @package com.lvkang.skin.resource 6 | * @author 345 QQ:1831712732 7 | * @time 2020/12/07 01:07 8 | * @description 9 | */ 10 | enum class SkinLoadStrategyEnum { 11 | 12 | /** 默认加载策略,即没有换肤效果 */ 13 | SKIN_LOADER_STRATEGY_NONE, 14 | 15 | /** 内部存储加载策略 */ 16 | SKIN_LOADER_STARTEGY_STORAGE, 17 | 18 | /** Assets 加载策略 */ 19 | SKIN_LOADER_STRATEGY_ASSETS, 20 | 21 | /** 压缩包加载策略 */ 22 | SKIN_LOADER_STRATEGY_ZIP, 23 | } -------------------------------------------------------------------------------- /skin/src/main/java/com/lvkang/skin/resource/strategy/AbstractSkinLoadAssetsImpl.kt: -------------------------------------------------------------------------------- 1 | package com.lvkang.skin.resource.strategy 2 | 3 | import com.lvkang.skin.SkinManager 4 | import com.lvkang.skin.config.SkinPreUtils 5 | import com.lvkang.skin.ktx.isFile 6 | import com.lvkang.skin.ktx.pathName 7 | import com.lvkang.skin.resource.AbstractSkinLoadStrategy 8 | import com.lvkang.skin.resource.SkinCompatResources 9 | import com.lvkang.skin.resource.SkinLoadStrategyEnum 10 | import java.io.File 11 | import java.io.IOException 12 | 13 | /** 14 | * @name SkinAssetsLoaderImpl 15 | * @package com.lvkang.skin.resource.strategy 16 | * @author 345 QQ:1831712732 17 | * @time 2020/12/07 22:24 18 | * @description Assets 加载策略,即 Assets 目录下的皮肤包 19 | */ 20 | 21 | class AbstractSkinLoadAssetsImpl : AbstractSkinLoadStrategy() { 22 | 23 | override fun loadSkin(vararg any: String?): String? { 24 | val skinPath = copyCache(any[0]!!, skinFileDir) 25 | if (skinPath.isNullOrBlank()) return null 26 | val resource = SkinCompatResources.getSkinResources(skinPath) 27 | val packageName = SkinCompatResources.getSkinPackageName(skinPath) 28 | if (resource != null && packageName != null) { 29 | SkinCompatResources.setupSkin(resource, packageName, pathName(skinPath), this) 30 | return skinPath 31 | } 32 | return null 33 | } 34 | 35 | override fun getType(): SkinLoadStrategyEnum = SkinLoadStrategyEnum.SKIN_LOADER_STRATEGY_ASSETS 36 | 37 | private fun copyCache(skinName: String, cacheDir: String): String? { 38 | return try { 39 | val outFile = File(cacheDir, skinName) 40 | if (isFile(outFile.path)) { 41 | SkinPreUtils.clearSkinInfo() 42 | return outFile.path 43 | } 44 | val input = SkinManager.getContext().resources.assets.open(skinName) 45 | input.copyTo(outFile.outputStream()) 46 | outFile.absolutePath 47 | } catch (e: IOException) { 48 | e.printStackTrace() 49 | null 50 | } 51 | } 52 | } -------------------------------------------------------------------------------- /skin/src/main/java/com/lvkang/skin/resource/strategy/AbstractSkinLoadNoneImpl.kt: -------------------------------------------------------------------------------- 1 | package com.lvkang.skin.resource.strategy 2 | 3 | import com.lvkang.skin.SkinManager 4 | import com.lvkang.skin.resource.AbstractSkinLoadStrategy 5 | import com.lvkang.skin.resource.SkinCompatResources 6 | import com.lvkang.skin.resource.SkinLoadStrategyEnum 7 | 8 | /** 9 | * @name SkinNoneLoaderImpl 10 | * @package com.lvkang.skin.resource.strategy 11 | * @author 345 QQ:1831712732 12 | * @time 2020/12/07 22:22 13 | * @description 默认加载策略,即不加载任何皮肤 14 | */ 15 | class AbstractSkinLoadNoneImpl : AbstractSkinLoadStrategy() { 16 | override fun loadSkin(vararg any: String?): String? { 17 | SkinCompatResources.resetSkin(SkinManager.getContext().resources, this) 18 | return null 19 | } 20 | 21 | override fun getType(): SkinLoadStrategyEnum = SkinLoadStrategyEnum.SKIN_LOADER_STRATEGY_NONE 22 | } -------------------------------------------------------------------------------- /skin/src/main/java/com/lvkang/skin/resource/strategy/AbstractSkinLoadStorageImpl.kt: -------------------------------------------------------------------------------- 1 | package com.lvkang.skin.resource.strategy 2 | 3 | import com.lvkang.skin.ktx.pathName 4 | import com.lvkang.skin.resource.AbstractSkinLoadStrategy 5 | import com.lvkang.skin.resource.SkinCompatResources 6 | import com.lvkang.skin.resource.SkinLoadStrategyEnum 7 | 8 | /** 9 | * @name SkinLoadImpl 10 | * @package ccom.lvkang.skin.resource.strategy 11 | * @author 345 QQ:1831712732 12 | * @time 2020/12/07 22:27 13 | * @description 内部存储加载策略 14 | */ 15 | class AbstractSkinLoadStorageImpl : AbstractSkinLoadStrategy() { 16 | 17 | override fun loadSkin(vararg any: String?): String? { 18 | val path = any[0]!! 19 | val resource = SkinCompatResources.getSkinResources(path) 20 | val packageName = SkinCompatResources.getSkinPackageName(path) 21 | if (resource != null && packageName != null) { 22 | SkinCompatResources.setupSkin(resource, packageName, pathName(path), this) 23 | return path 24 | } 25 | return null 26 | } 27 | 28 | 29 | override fun getType(): SkinLoadStrategyEnum = SkinLoadStrategyEnum.SKIN_LOADER_STARTEGY_STORAGE 30 | } -------------------------------------------------------------------------------- /skin/src/main/java/com/lvkang/skin/resource/strategy/AbstractSkinLoadZipImpl.kt: -------------------------------------------------------------------------------- 1 | package com.lvkang.skin.resource.strategy 2 | 3 | import com.lvkang.skin.ktx.isFile 4 | import com.lvkang.skin.ktx.pathName 5 | import com.lvkang.skin.resource.AbstractSkinLoadStrategy 6 | import com.lvkang.skin.resource.SkinCompatResources 7 | import com.lvkang.skin.resource.SkinLoadStrategyEnum 8 | import com.lvkang.skin.util.SkinLog 9 | import net.lingala.zip4j.ZipFile 10 | import java.io.File 11 | 12 | 13 | /** 14 | * @name AbstractSkinLoadZipImpl 15 | * @package com.lvkang.skin.resource.strategy 16 | * @author 345 QQ:1831712732 17 | * @time 2021/07/30 14:12 18 | * @description 19 | */ 20 | class AbstractSkinLoadZipImpl : AbstractSkinLoadStrategy() { 21 | 22 | override fun loadSkin(vararg any: String?): String? { 23 | val path = any[0]!! 24 | if (!isFile(path)) { 25 | return null 26 | } 27 | //path包含.skin,说明是app初始化加载的 28 | if (path.contains(".skin")) { 29 | return loadSkin(path) 30 | } 31 | //判断该文件是否已经被解压过 32 | val skin = "${path.substring(0, path.lastIndexOf('.'))}.skin" 33 | if (isFile(skin)) { 34 | return loadSkin(skin) 35 | } 36 | val password = if (any.size > 1) any[1] else null 37 | val list = try { 38 | if (password != null) { 39 | SkinLog.log("$password") 40 | unzip(File(path), skinFileDir, password) 41 | } else { 42 | unzip(File(path), skinFileDir, null) 43 | } 44 | } catch (e: Exception) { 45 | e.printStackTrace() 46 | return null 47 | } 48 | if (list.isNotEmpty()) { 49 | return loadSkin(list[0].absolutePath) 50 | } 51 | return null 52 | } 53 | 54 | private fun loadSkin(skinPath: String): String? { 55 | val resource = SkinCompatResources.getSkinResources(skinPath) 56 | val packageName = SkinCompatResources.getSkinPackageName(skinPath) 57 | if (resource != null && packageName != null) { 58 | SkinCompatResources.setupSkin(resource, packageName, pathName(skinPath), this) 59 | return skinPath 60 | } 61 | return null 62 | } 63 | 64 | 65 | override fun getType(): SkinLoadStrategyEnum = SkinLoadStrategyEnum.SKIN_LOADER_STRATEGY_ZIP 66 | 67 | private fun unzip(zipFile: File?, dest: String, passwd: String?): MutableList { 68 | val zFile = ZipFile(zipFile) 69 | 70 | val destDir = File(dest) 71 | if (destDir.isDirectory && !destDir.exists()) { 72 | destDir.mkdir() 73 | } 74 | if (zFile.isEncrypted && passwd != null) { 75 | zFile.setPassword(passwd.toCharArray()) 76 | } 77 | zFile.extractAll(dest) 78 | val headerList = zFile.fileHeaders 79 | val extractedFileList: MutableList = ArrayList() 80 | for (fileHeader in headerList) { 81 | if (!fileHeader.isDirectory) { 82 | extractedFileList.add(File(destDir, fileHeader.fileName)) 83 | } 84 | } 85 | return extractedFileList.toMutableList() 86 | } 87 | } -------------------------------------------------------------------------------- /skin/src/main/java/com/lvkang/skin/util/SkinLog.kt: -------------------------------------------------------------------------------- 1 | package com.lvkang.skin.util 2 | 3 | import android.util.Log 4 | 5 | /** 6 | * @name SkinLog 7 | * @package com.lvkang.skin.util 8 | * @author 345 QQ:1831712732 9 | * @time 2021/07/28 18:08 10 | * @description 11 | */ 12 | object SkinLog { 13 | 14 | private const val TAG = "SkinLog" 15 | 16 | const val LOG_V = "verbose" 17 | const val LOG_D = "debug" 18 | const val LOG_I = "info" 19 | const val LOG_W = "warn" 20 | const val LOG_E = "error" 21 | 22 | var defaultLongLevel = LOG_D 23 | 24 | fun log(content: String, logLevel: String = defaultLongLevel) { 25 | when (logLevel) { 26 | LOG_V -> Log.v(TAG, content) 27 | LOG_D -> Log.d(TAG, content) 28 | LOG_I -> Log.i(TAG, content) 29 | LOG_W -> Log.w(TAG, content) 30 | LOG_E -> Log.e(TAG, content) 31 | else -> { 32 | Log.v(TAG, content) 33 | } 34 | } 35 | } 36 | } -------------------------------------------------------------------------------- /skin/src/main/java/com/lvkang/skin/wedget/SkinCompatHelper.kt: -------------------------------------------------------------------------------- 1 | package com.lvkang.skin.wedget 2 | 3 | /** 4 | * @name SkinCompatHelper 5 | * @package com.lvkang.skin.wedget 6 | * @author 345 QQ:1831712732 7 | * @time 2020/11/29 16:24 8 | * @description 9 | */ 10 | abstract class SkinCompatHelper { 11 | companion object { 12 | const val INVALID_ID = 0 13 | } 14 | abstract fun applySkin() 15 | } -------------------------------------------------------------------------------- /skin/src/main/java/com/lvkang/skin/wedget/SkinCompatSupportable.kt: -------------------------------------------------------------------------------- 1 | package com.lvkang.skin.wedget 2 | 3 | /** 4 | * @name SkinCompatSupportable 5 | * @package com.lvkang.skin.wedget 6 | * @author 345 QQ:1831712732 7 | * @time 2020/11/29 15:52 8 | * @description 9 | */ 10 | 11 | interface SkinCompatSupportable { 12 | fun applySkin() 13 | } -------------------------------------------------------------------------------- /skin/src/main/java/com/lvkang/skin/wedget/android/FrameLayoutX.kt: -------------------------------------------------------------------------------- 1 | package com.lvkang.skin.wedget.android 2 | 3 | import android.content.Context 4 | import android.util.AttributeSet 5 | import android.widget.FrameLayout 6 | import androidx.appcompat.widget.LinearLayoutCompat 7 | import androidx.constraintlayout.widget.ConstraintLayout 8 | import com.lvkang.skin.wedget.SkinCompatSupportable 9 | import com.lvkang.skin.wedget.helper.SkinCompatBackgroundHelper 10 | 11 | /** 12 | * @name LinearLayoutX 13 | * @package com.lvkang.skin.wedget.androidx 14 | * @author 345 QQ:1831712732 15 | * @time 2021/07/28 17:04 16 | * @description 17 | */ 18 | class FrameLayoutX : FrameLayout, SkinCompatSupportable { 19 | 20 | private val mBackgroundHelper by lazy { 21 | SkinCompatBackgroundHelper(this) 22 | } 23 | 24 | constructor(context: Context) : this(context, null) 25 | constructor(context: Context, attrs: AttributeSet?) : this(context, attrs, 0) 26 | constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super( 27 | context, 28 | attrs, 29 | defStyleAttr 30 | ) { 31 | mBackgroundHelper.loadFromAttributes(attrs, defStyleAttr) 32 | } 33 | 34 | override fun applySkin() { 35 | mBackgroundHelper.applySkin() 36 | } 37 | } -------------------------------------------------------------------------------- /skin/src/main/java/com/lvkang/skin/wedget/android/RelativeLayoutX.kt: -------------------------------------------------------------------------------- 1 | package com.lvkang.skin.wedget.android 2 | 3 | import android.content.Context 4 | import android.util.AttributeSet 5 | import android.widget.FrameLayout 6 | import android.widget.RelativeLayout 7 | import androidx.appcompat.widget.LinearLayoutCompat 8 | import androidx.constraintlayout.widget.ConstraintLayout 9 | import com.lvkang.skin.wedget.SkinCompatSupportable 10 | import com.lvkang.skin.wedget.helper.SkinCompatBackgroundHelper 11 | 12 | /** 13 | * @name LinearLayoutX 14 | * @package com.lvkang.skin.wedget.androidx 15 | * @author 345 QQ:1831712732 16 | * @time 2021/07/28 17:04 17 | * @description 18 | */ 19 | class RelativeLayoutX : RelativeLayout, SkinCompatSupportable { 20 | 21 | private val mBackgroundHelper by lazy { 22 | SkinCompatBackgroundHelper(this) 23 | } 24 | 25 | constructor(context: Context) : this(context, null) 26 | constructor(context: Context, attrs: AttributeSet?) : this(context, attrs, 0) 27 | constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super( 28 | context, 29 | attrs, 30 | defStyleAttr 31 | ) { 32 | mBackgroundHelper.loadFromAttributes(attrs, defStyleAttr) 33 | } 34 | 35 | override fun applySkin() { 36 | mBackgroundHelper.applySkin() 37 | } 38 | } -------------------------------------------------------------------------------- /skin/src/main/java/com/lvkang/skin/wedget/android/ViewX.kt: -------------------------------------------------------------------------------- 1 | package com.lvkang.skin.wedget.android 2 | 3 | import android.content.Context 4 | import android.util.AttributeSet 5 | import android.view.View 6 | import com.lvkang.skin.wedget.helper.SkinCompatBackgroundHelper 7 | import com.lvkang.skin.wedget.SkinCompatSupportable 8 | 9 | /** 10 | * @name SkinSupportView 11 | * @package com.lvkang.skin.wedget.support 12 | * @author 345 QQ:1831712732 13 | * @time 2020/12/01 23:34 14 | * @description 15 | */ 16 | class ViewX : View, SkinCompatSupportable { 17 | 18 | private val mBackgroundHelper by lazy { 19 | SkinCompatBackgroundHelper(this) 20 | } 21 | 22 | constructor(context: Context?) : this(context, null) 23 | constructor(context: Context?, attrs: AttributeSet?) : this(context, attrs, 0) 24 | constructor(context: Context?, attrs: AttributeSet?, defStyleAttr: Int) : super( 25 | context, attrs, defStyleAttr 26 | ) { 27 | mBackgroundHelper.loadFromAttributes(attrs, defStyleAttr) 28 | } 29 | 30 | override fun applySkin() { 31 | mBackgroundHelper.applySkin() 32 | } 33 | } -------------------------------------------------------------------------------- /skin/src/main/java/com/lvkang/skin/wedget/androidx/ButtonX.kt: -------------------------------------------------------------------------------- 1 | package com.lvkang.skin.wedget.androidx 2 | 3 | import android.content.Context 4 | import android.util.AttributeSet 5 | import androidx.appcompat.widget.AppCompatButton 6 | import com.lvkang.skin.wedget.SkinCompatSupportable 7 | import com.lvkang.skin.wedget.helper.SkinCompatBackgroundHelper 8 | import com.lvkang.skin.wedget.helper.SkinCompatTextHelper 9 | 10 | /** 11 | * @name AppxButton 12 | * @package com.lvkang.skin.wedget.androidx 13 | * @author 345 QQ:1831712732 14 | * @time 2020/12/10 23:30 15 | * @description 16 | */ 17 | class ButtonX : AppCompatButton, SkinCompatSupportable { 18 | 19 | private val skinTextHelper by lazy(LazyThreadSafetyMode.NONE) { SkinCompatTextHelper(this) } 20 | private val mBackgroundHelper by lazy { SkinCompatBackgroundHelper(this) } 21 | 22 | constructor(context: Context) : this(context, null) 23 | constructor(context: Context, attrs: AttributeSet?) : this(context, attrs, 0) 24 | constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super( 25 | context, 26 | attrs, 27 | defStyleAttr 28 | ) { 29 | skinTextHelper.loadFromAttributes(attrs, defStyleAttr) 30 | mBackgroundHelper.loadFromAttributes(attrs, defStyleAttr) 31 | } 32 | 33 | override fun applySkin() { 34 | skinTextHelper.applySkin() 35 | mBackgroundHelper.applySkin() 36 | } 37 | } -------------------------------------------------------------------------------- /skin/src/main/java/com/lvkang/skin/wedget/androidx/ConstraintLayoutX.kt: -------------------------------------------------------------------------------- 1 | package com.lvkang.skin.wedget.androidx 2 | 3 | import android.content.Context 4 | import android.util.AttributeSet 5 | import androidx.constraintlayout.widget.ConstraintLayout 6 | import com.lvkang.skin.wedget.SkinCompatSupportable 7 | import com.lvkang.skin.wedget.helper.SkinCompatBackgroundHelper 8 | 9 | /** 10 | * @name ConstraintLayoutX 11 | * @package com.lvkang.skin.wedget.androidx 12 | * @author 345 QQ:1831712732 13 | * @time 2021/07/28 16:59 14 | * @description 15 | */ 16 | class ConstraintLayoutX : ConstraintLayout, SkinCompatSupportable { 17 | 18 | private val mBackgroundHelper by lazy { 19 | SkinCompatBackgroundHelper(this) 20 | } 21 | 22 | constructor(context: Context) : this(context, null) 23 | constructor(context: Context, attrs: AttributeSet?) : this(context, attrs, 0) 24 | constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super( 25 | context, 26 | attrs, 27 | defStyleAttr 28 | ) { 29 | mBackgroundHelper.loadFromAttributes(attrs, defStyleAttr) 30 | } 31 | 32 | override fun applySkin() { 33 | mBackgroundHelper.applySkin() 34 | } 35 | } -------------------------------------------------------------------------------- /skin/src/main/java/com/lvkang/skin/wedget/androidx/EditTextX.kt: -------------------------------------------------------------------------------- 1 | package com.lvkang.skin.wedget.androidx 2 | 3 | import android.content.Context 4 | import android.graphics.drawable.Drawable 5 | import android.util.AttributeSet 6 | import androidx.appcompat.widget.AppCompatEditText 7 | import com.lvkang.skin.wedget.SkinCompatSupportable 8 | import com.lvkang.skin.wedget.helper.SkinCompatBackgroundHelper 9 | import com.lvkang.skin.wedget.helper.SkinCompatEditTextHelpter 10 | 11 | /** 12 | * @name EditText 13 | * @package com.lvkang.skin.wedget.androidx 14 | * @author 345 QQ:1831712732 15 | * @time 2021/08/02 16:47 16 | * @description 17 | */ 18 | class EditTextX : AppCompatEditText, SkinCompatSupportable { 19 | 20 | private val skinTextHelper by lazy(LazyThreadSafetyMode.NONE) { SkinCompatEditTextHelpter(this) } 21 | private val mBackgroundHelper by lazy { SkinCompatBackgroundHelper(this) } 22 | 23 | constructor(context: Context) : this(context, null) 24 | constructor(context: Context, attrs: AttributeSet?) : this(context, attrs, 0) 25 | constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super( 26 | context, 27 | attrs, 28 | defStyleAttr 29 | ) { 30 | skinTextHelper.loadFromAttributes(attrs, defStyleAttr) 31 | mBackgroundHelper.loadFromAttributes(attrs, defStyleAttr) 32 | } 33 | 34 | override fun applySkin() { 35 | skinTextHelper.applySkin() 36 | mBackgroundHelper.applySkin() 37 | } 38 | } -------------------------------------------------------------------------------- /skin/src/main/java/com/lvkang/skin/wedget/androidx/ImageViewX.kt: -------------------------------------------------------------------------------- 1 | package com.lvkang.skin.wedget.androidx 2 | 3 | import android.content.Context 4 | import android.util.AttributeSet 5 | import androidx.appcompat.widget.AppCompatImageView 6 | import com.lvkang.skin.wedget.helper.SkinCompatBackgroundHelper 7 | import com.lvkang.skin.wedget.SkinCompatSupportable 8 | import com.lvkang.skin.wedget.helper.SkinCompatImageHelper 9 | 10 | /** 11 | * @name AppXImageView 12 | * @package com.lvkang.skin.wedget.androidx 13 | * @author 345 QQ:1831712732 14 | * @time 2020/12/10 22:45 15 | * @description 16 | */ 17 | class ImageViewX : AppCompatImageView, SkinCompatSupportable { 18 | 19 | 20 | private val mImageHelper by lazy { SkinCompatImageHelper(this) } 21 | private val backgroundHelper by lazy { SkinCompatBackgroundHelper(this) } 22 | 23 | constructor(context: Context) : this(context, null) 24 | constructor(context: Context, attrs: AttributeSet?) : this(context, attrs, 0) 25 | constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super( 26 | context, 27 | attrs, 28 | defStyleAttr 29 | ) { 30 | mImageHelper.loadFromAttributes(attrs, defStyleAttr) 31 | backgroundHelper.loadFromAttributes(attrs, defStyleAttr) 32 | } 33 | 34 | override fun applySkin() { 35 | mImageHelper.applySkin() 36 | backgroundHelper.applySkin() 37 | } 38 | } -------------------------------------------------------------------------------- /skin/src/main/java/com/lvkang/skin/wedget/androidx/LinearLayoutX.kt: -------------------------------------------------------------------------------- 1 | package com.lvkang.skin.wedget.androidx 2 | 3 | import android.content.Context 4 | import android.util.AttributeSet 5 | import androidx.appcompat.widget.LinearLayoutCompat 6 | import com.lvkang.skin.wedget.SkinCompatSupportable 7 | import com.lvkang.skin.wedget.helper.SkinCompatBackgroundHelper 8 | 9 | /** 10 | * @name LinearLayoutX 11 | * @package com.lvkang.skin.wedget.androidx 12 | * @author 345 QQ:1831712732 13 | * @time 2021/07/28 17:04 14 | * @description 15 | */ 16 | class LinearLayoutX : LinearLayoutCompat, SkinCompatSupportable { 17 | 18 | private val mBackgroundHelper by lazy { 19 | SkinCompatBackgroundHelper(this) 20 | } 21 | 22 | constructor(context: Context) : this(context, null) 23 | constructor(context: Context, attrs: AttributeSet?) : this(context, attrs, 0) 24 | constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super( 25 | context, 26 | attrs, 27 | defStyleAttr 28 | ) { 29 | mBackgroundHelper.loadFromAttributes(attrs, defStyleAttr) 30 | } 31 | 32 | override fun applySkin() { 33 | mBackgroundHelper.applySkin() 34 | } 35 | } -------------------------------------------------------------------------------- /skin/src/main/java/com/lvkang/skin/wedget/androidx/NestedScrollViewX.kt: -------------------------------------------------------------------------------- 1 | package com.lvkang.skin.wedget.androidx 2 | 3 | import android.content.Context 4 | import android.util.AttributeSet 5 | import android.widget.ScrollView 6 | import androidx.core.widget.NestedScrollView 7 | import com.lvkang.skin.wedget.SkinCompatSupportable 8 | import com.lvkang.skin.wedget.helper.SkinCompatBackgroundHelper 9 | 10 | /** 11 | * @name NestScrollViewX 12 | * @package com.lvkang.skin.wedget.androidx 13 | * @author 345 QQ:1831712732 14 | * @time 2021/07/28 17:20 15 | * @description 16 | */ 17 | class NestedScrollViewX : NestedScrollView, SkinCompatSupportable { 18 | 19 | private val mBackgroundHelper by lazy { 20 | SkinCompatBackgroundHelper(this) 21 | } 22 | 23 | constructor(context: Context) : this(context, null) 24 | constructor(context: Context, attrs: AttributeSet?) : this(context, attrs, 0) 25 | constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super( 26 | context, 27 | attrs, 28 | defStyleAttr 29 | ) { 30 | mBackgroundHelper.loadFromAttributes(attrs, defStyleAttr) 31 | } 32 | 33 | override fun applySkin() { 34 | mBackgroundHelper.applySkin() 35 | } 36 | } -------------------------------------------------------------------------------- /skin/src/main/java/com/lvkang/skin/wedget/androidx/TextViewX.kt: -------------------------------------------------------------------------------- 1 | package com.lvkang.skin.wedget.androidx 2 | 3 | import android.content.Context 4 | import android.util.AttributeSet 5 | import androidx.appcompat.widget.AppCompatButton 6 | import androidx.appcompat.widget.AppCompatTextView 7 | import com.lvkang.skin.wedget.SkinCompatSupportable 8 | import com.lvkang.skin.wedget.helper.SkinCompatBackgroundHelper 9 | import com.lvkang.skin.wedget.helper.SkinCompatTextHelper 10 | 11 | /** 12 | * @name AppxButton 13 | * @package com.lvkang.skin.wedget.androidx 14 | * @author 345 QQ:1831712732 15 | * @time 2020/12/10 23:30 16 | * @description 17 | */ 18 | class TextViewX : AppCompatTextView, SkinCompatSupportable { 19 | 20 | private val skinTextHelper by lazy(LazyThreadSafetyMode.NONE) { SkinCompatTextHelper(this) } 21 | private val mBackgroundHelper by lazy { SkinCompatBackgroundHelper(this) } 22 | 23 | constructor(context: Context) : this(context, null) 24 | constructor(context: Context, attrs: AttributeSet?) : this(context, attrs, 0) 25 | constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super( 26 | context, 27 | attrs, 28 | defStyleAttr 29 | ) { 30 | skinTextHelper.loadFromAttributes(attrs, defStyleAttr) 31 | mBackgroundHelper.loadFromAttributes(attrs, defStyleAttr) 32 | } 33 | 34 | override fun applySkin() { 35 | skinTextHelper.applySkin() 36 | mBackgroundHelper.applySkin() 37 | } 38 | } -------------------------------------------------------------------------------- /skin/src/main/java/com/lvkang/skin/wedget/androidx/cardview/CardViewX.kt: -------------------------------------------------------------------------------- 1 | package com.lvkang.skin.wedget.androidx.cardview 2 | 3 | import android.content.Context 4 | import android.util.AttributeSet 5 | import androidx.appcompat.widget.AppCompatButton 6 | import androidx.cardview.widget.CardView 7 | import com.lvkang.skin.wedget.SkinCompatSupportable 8 | import com.lvkang.skin.wedget.helper.SkinCompatBackgroundHelper 9 | import com.lvkang.skin.wedget.helper.SkinCompatCardHelper 10 | import com.lvkang.skin.wedget.helper.SkinCompatTextHelper 11 | 12 | /** 13 | * @name CardViewX 14 | * @package com.lvkang.skin.wedget.androidx 15 | * @author 345 QQ:1831712732 16 | * @time 2021/08/03 21:57 17 | * @description 18 | */ 19 | internal class CardViewX : CardView, SkinCompatSupportable { 20 | 21 | private val skinCompatCardHelper by lazy { SkinCompatCardHelper(this) } 22 | 23 | constructor(context: Context) : this(context, null) 24 | constructor(context: Context, attrs: AttributeSet?) : this(context, attrs, 0) 25 | constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super( 26 | context, 27 | attrs, 28 | defStyleAttr 29 | ) { 30 | skinCompatCardHelper.loadFromAttributes(attrs, defStyleAttr) 31 | } 32 | 33 | override fun applySkin() { 34 | skinCompatCardHelper.applySkin() 35 | } 36 | } -------------------------------------------------------------------------------- /skin/src/main/java/com/lvkang/skin/wedget/androidx/cardview/RoundRectDrawable.kt: -------------------------------------------------------------------------------- 1 | package com.lvkang.skin.wedget.androidx.cardview 2 | 3 | import android.content.res.ColorStateList 4 | import android.graphics.* 5 | import android.graphics.drawable.Drawable 6 | import androidx.annotation.RequiresApi 7 | import kotlin.math.cos 8 | 9 | /** 10 | * @name RoundRectDrawable 11 | * @package com.lvkang.skin.wedget.androidx.cardview 12 | * @author 345 QQ:1831712732 13 | * @time 2021/08/04 11:21 14 | * @description 15 | */ 16 | /** 17 | * Very simple drawable that draws a rounded rectangle background with arbitrary corners and also 18 | * reports proper outline for Lollipop. 19 | * 20 | * 21 | * Simpler and uses less resources compared to GradientDrawable or ShapeDrawable. 22 | */ 23 | @RequiresApi(21) 24 | internal class RoundRectDrawable(backgroundColor: ColorStateList?, private var mRadius: Float) : 25 | Drawable() { 26 | private val mPaint = Paint(Paint.ANTI_ALIAS_FLAG or Paint.DITHER_FLAG) 27 | private val mBoundsF: RectF 28 | private val mBoundsI: Rect 29 | var padding = 0f 30 | private set 31 | private var mInsetForPadding = false 32 | private var mInsetForRadius = true 33 | private var mBackground: ColorStateList? = null 34 | private var mTintFilter: PorterDuffColorFilter? = null 35 | private var mTint: ColorStateList? = null 36 | private var mTintMode: PorterDuff.Mode? = PorterDuff.Mode.SRC_IN 37 | 38 | 39 | companion object { 40 | const val SHADOW_MULTIPLIER = 1.5f 41 | val COS_45 = cos(Math.toRadians(45.0)) 42 | } 43 | 44 | 45 | private fun setBackground(color: ColorStateList?) { 46 | mBackground = color ?: ColorStateList.valueOf(Color.TRANSPARENT) 47 | mPaint.color = mBackground!!.getColorForState(state, mBackground!!.defaultColor) 48 | } 49 | 50 | fun setPadding(padding: Float, insetForPadding: Boolean, insetForRadius: Boolean) { 51 | if (padding == this.padding && mInsetForPadding == insetForPadding && mInsetForRadius == insetForRadius) { 52 | return 53 | } 54 | this.padding = padding 55 | mInsetForPadding = insetForPadding 56 | mInsetForRadius = insetForRadius 57 | updateBounds(null) 58 | invalidateSelf() 59 | } 60 | 61 | override fun draw(canvas: Canvas) { 62 | val paint = mPaint 63 | val clearColorFilter: Boolean 64 | if (mTintFilter != null && paint.colorFilter == null) { 65 | paint.colorFilter = mTintFilter 66 | clearColorFilter = true 67 | } else { 68 | clearColorFilter = false 69 | } 70 | canvas.drawRoundRect(mBoundsF, mRadius, mRadius, paint) 71 | if (clearColorFilter) { 72 | paint.colorFilter = null 73 | } 74 | } 75 | 76 | private fun updateBounds(bounds: Rect?) { 77 | var bounds = bounds 78 | if (bounds == null) { 79 | bounds = getBounds() 80 | } 81 | mBoundsF[bounds.left.toFloat(), bounds.top.toFloat(), bounds.right.toFloat()] = 82 | bounds.bottom.toFloat() 83 | mBoundsI.set(bounds) 84 | if (mInsetForPadding) { 85 | val vInset = calculateVerticalPadding( 86 | padding, 87 | mRadius, 88 | mInsetForRadius 89 | ) 90 | val hInset = calculateHorizontalPadding( 91 | padding, 92 | mRadius, 93 | mInsetForRadius 94 | ) 95 | mBoundsI.inset( 96 | Math.ceil(hInset.toDouble()).toInt(), 97 | Math.ceil(vInset.toDouble()).toInt() 98 | ) 99 | // to make sure they have same bounds. 100 | mBoundsF.set(mBoundsI) 101 | } 102 | } 103 | 104 | override fun onBoundsChange(bounds: Rect) { 105 | super.onBoundsChange(bounds) 106 | updateBounds(bounds) 107 | } 108 | 109 | override fun getOutline(outline: Outline) { 110 | outline.setRoundRect(mBoundsI, mRadius) 111 | } 112 | 113 | override fun setAlpha(alpha: Int) { 114 | mPaint.alpha = alpha 115 | } 116 | 117 | override fun setColorFilter(cf: ColorFilter?) { 118 | mPaint.colorFilter = cf 119 | } 120 | 121 | override fun getOpacity(): Int { 122 | return PixelFormat.TRANSLUCENT 123 | } 124 | 125 | var radius: Float 126 | get() = mRadius 127 | set(radius) { 128 | if (radius == mRadius) { 129 | return 130 | } 131 | mRadius = radius 132 | updateBounds(null) 133 | invalidateSelf() 134 | } 135 | var color: ColorStateList? 136 | get() = mBackground 137 | set(color) { 138 | setBackground(color) 139 | invalidateSelf() 140 | } 141 | 142 | override fun setTintList(tint: ColorStateList?) { 143 | mTint = tint 144 | mTintFilter = createTintFilter(mTint, mTintMode) 145 | invalidateSelf() 146 | } 147 | 148 | override fun setTintMode(tintMode: PorterDuff.Mode?) { 149 | mTintMode = tintMode 150 | mTintFilter = createTintFilter(mTint, mTintMode) 151 | invalidateSelf() 152 | } 153 | 154 | override fun onStateChange(stateSet: IntArray): Boolean { 155 | val newColor = mBackground!!.getColorForState(stateSet, mBackground!!.defaultColor) 156 | val colorChanged = newColor != mPaint.color 157 | if (colorChanged) { 158 | mPaint.color = newColor 159 | } 160 | if (mTint != null && mTintMode != null) { 161 | mTintFilter = createTintFilter(mTint, mTintMode) 162 | return true 163 | } 164 | return colorChanged 165 | } 166 | 167 | override fun isStateful(): Boolean { 168 | return (mTint != null && mTint!!.isStateful 169 | || mBackground != null && mBackground!!.isStateful || super.isStateful()) 170 | } 171 | 172 | /** 173 | * Ensures the tint filter is consistent with the current tint color and 174 | * mode. 175 | */ 176 | private fun createTintFilter( 177 | tint: ColorStateList?, 178 | tintMode: PorterDuff.Mode? 179 | ): PorterDuffColorFilter? { 180 | if (tint == null || tintMode == null) { 181 | return null 182 | } 183 | val color = tint.getColorForState(state, Color.TRANSPARENT) 184 | return PorterDuffColorFilter(color, tintMode) 185 | } 186 | 187 | init { 188 | setBackground(backgroundColor) 189 | mBoundsF = RectF() 190 | mBoundsI = Rect() 191 | } 192 | 193 | 194 | private fun calculateVerticalPadding( 195 | maxShadowSize: Float, cornerRadius: Float, 196 | addPaddingForCorners: Boolean 197 | ): Float { 198 | return if (addPaddingForCorners) { 199 | (maxShadowSize * SHADOW_MULTIPLIER + (1 - COS_45) * cornerRadius).toFloat() 200 | } else { 201 | maxShadowSize * SHADOW_MULTIPLIER 202 | } 203 | } 204 | 205 | private fun calculateHorizontalPadding( 206 | maxShadowSize: Float, cornerRadius: Float, addPaddingForCorners: Boolean 207 | ): Float { 208 | return if (addPaddingForCorners) { 209 | (maxShadowSize + (1 - COS_45) * cornerRadius).toFloat() 210 | } else { 211 | maxShadowSize 212 | } 213 | } 214 | } 215 | -------------------------------------------------------------------------------- /skin/src/main/java/com/lvkang/skin/wedget/helper/SkinCompatBackgroundHelper.kt: -------------------------------------------------------------------------------- 1 | package com.lvkang.skin.wedget.helper 2 | 3 | import android.util.AttributeSet 4 | import android.util.Log 5 | import android.view.View 6 | import androidx.core.view.ViewCompat 7 | import androidx.appcompat.R 8 | import com.lvkang.skin.ktx.obtainStyledAttributes 9 | import com.lvkang.skin.resource.SkinCompatResources 10 | import com.lvkang.skin.wedget.SkinCompatHelper 11 | 12 | /** 13 | * @name SkinCompatBackgroundHelper 14 | * @package com.lvkang.skin.wedget 15 | * @author 345 QQ:1831712732 16 | * @time 2020/11/29 16:30 17 | * @description 更换背景的帮助类 18 | */ 19 | 20 | class SkinCompatBackgroundHelper(val view: View) : SkinCompatHelper() { 21 | 22 | private var backgroundResId = INVALID_ID 23 | 24 | fun loadFromAttributes(attrs: AttributeSet?, defStyleAttr: Int) { 25 | obtainStyledAttributes( 26 | view, attrs, R.styleable.ViewBackgroundHelper, defStyleAttr, 0 27 | ) { 28 | backgroundResId = it.getResourceId( 29 | R.styleable.ViewBackgroundHelper_android_background, INVALID_ID 30 | ) 31 | } 32 | applySkin() 33 | } 34 | 35 | 36 | override fun applySkin() { 37 | setBackground(backgroundResId) 38 | } 39 | 40 | private fun setBackground(res: Int) { 41 | if (res == INVALID_ID) return 42 | val drawable = SkinCompatResources.getDrawable(res) 43 | if (drawable != null) { 44 | val paddingleft = view.paddingLeft 45 | val paddingTop = view.paddingTop 46 | val paddingRight = view.paddingRight 47 | val paddingBottom = view.paddingBottom 48 | ViewCompat.setBackground(view, drawable) 49 | view.setPadding(paddingleft, paddingTop, paddingRight, paddingBottom) 50 | return 51 | } 52 | val color = SkinCompatResources.getColor(res) 53 | color?.run { 54 | view.setBackgroundColor(color) 55 | } 56 | } 57 | } -------------------------------------------------------------------------------- /skin/src/main/java/com/lvkang/skin/wedget/helper/SkinCompatCardHelper.kt: -------------------------------------------------------------------------------- 1 | package com.lvkang.skin.wedget.helper 2 | 3 | import android.util.AttributeSet 4 | import androidx.cardview.widget.CardView 5 | import com.lvkang.skin.ktx.obtainStyledAttributes 6 | import com.lvkang.skin.resource.SkinCompatResources 7 | import com.lvkang.skin.wedget.SkinCompatHelper 8 | 9 | import android.content.res.ColorStateList 10 | import android.content.res.TypedArray 11 | import android.graphics.Color 12 | import androidx.core.content.res.ResourcesCompat 13 | import com.lvkang.skin.R 14 | import com.lvkang.skin.util.SkinLog 15 | import com.lvkang.skin.wedget.androidx.cardview.RoundRectDrawable 16 | 17 | 18 | /** 19 | * @name SkinCompatCardHelper 20 | * @package com.lvkang.skin.wedget.helper 21 | * @author 345 QQ:1831712732 22 | * @time 2021/08/04 10:42 23 | * @description 24 | */ 25 | class SkinCompatCardHelper(private val view: CardView) : SkinCompatHelper() { 26 | 27 | private var appBackgroundResId = INVALID_ID 28 | private var appCardCornerRadiusResId = INVALID_ID 29 | private var appCardElevationResId = INVALID_ID 30 | 31 | 32 | private var radius = 0f 33 | private var colorStateList: ColorStateList? = null 34 | 35 | 36 | fun loadFromAttributes(attrs: AttributeSet?, defStyleAttr: Int) { 37 | obtainStyledAttributes( 38 | view, attrs, R.styleable.CardView, defStyleAttr, R.style.CardView 39 | ) { 40 | appBackgroundResId = it.getResourceId( 41 | R.styleable.CardView_cardBackgroundColor, INVALID_ID 42 | ) 43 | appCardCornerRadiusResId = it.getResourceId( 44 | R.styleable.CardView_cardCornerRadius, INVALID_ID 45 | ) 46 | appCardElevationResId = it.getResourceId( 47 | R.styleable.CardView_cardElevation, INVALID_ID 48 | ) 49 | //缓存默认设置的圆角 50 | colorStateList = 51 | it.getColorStateList(R.styleable.CardView_cardBackgroundColor) 52 | SkinLog.log(colorStateList.toString()) 53 | radius = it.getDimension(R.styleable.CardView_cardElevation, 0f) 54 | } 55 | applySkin() 56 | } 57 | 58 | override fun applySkin() { 59 | setElevation(appCardElevationResId) 60 | setBackgroundAndRadius(appBackgroundResId, appCardCornerRadiusResId) 61 | } 62 | 63 | private fun setElevation(resId: Int) { 64 | if (resId == INVALID_ID) return 65 | val elevation = SkinCompatResources.getDimension(resId) ?: 0f 66 | view.clipToOutline = true 67 | view.elevation = elevation 68 | } 69 | 70 | private fun setBackgroundAndRadius( 71 | bgRes: Int, 72 | radiusRes: Int 73 | ) { 74 | //如果没有使用皮肤 75 | if (bgRes == INVALID_ID) { 76 | //并且没有缓存,则根据主题获取背景等属性 77 | if (colorStateList == null) { 78 | // There isn't one set, so we'll compute one based on the theme 79 | val aa: TypedArray = 80 | view.context.obtainStyledAttributes(intArrayOf(android.R.attr.colorBackground)) 81 | val themeColorBackground = aa.getColor(0, 0) 82 | aa.recycle() 83 | // If the theme colorBackground is light, use our own light color, otherwise dark 84 | val hsv = FloatArray(3) 85 | Color.colorToHSV(themeColorBackground, hsv) 86 | colorStateList = ColorStateList.valueOf( 87 | if (hsv[2] > 0.5f) 88 | ResourcesCompat.getColor( 89 | view.resources, R.color.cardview_light_background, null 90 | ) 91 | else 92 | ResourcesCompat.getColor( 93 | view.resources, R.color.cardview_dark_background, null 94 | ) 95 | ) 96 | } 97 | } else { 98 | SkinCompatResources.getColorStateList(bgRes)?.run { 99 | colorStateList = this 100 | } 101 | } 102 | if (radiusRes != INVALID_ID) 103 | SkinCompatResources.getDimension(radiusRes)?.run { 104 | radius = this 105 | } 106 | if (colorStateList != null) { 107 | val drawable = RoundRectDrawable(colorStateList, radius) 108 | view.background = drawable 109 | } 110 | } 111 | } -------------------------------------------------------------------------------- /skin/src/main/java/com/lvkang/skin/wedget/helper/SkinCompatEditTextHelpter.kt: -------------------------------------------------------------------------------- 1 | package com.lvkang.skin.wedget.helper 2 | 3 | import android.util.AttributeSet 4 | import android.widget.EditText 5 | import com.lvkang.skin.R 6 | import com.lvkang.skin.ktx.obtainStyledAttributes 7 | import com.lvkang.skin.resource.SkinCompatResources 8 | 9 | /** 10 | * @name SkinCompatEditTextHelpter 11 | * @package com.lvkang.skin.wedget.helper 12 | * @author 345 QQ:1831712732 13 | * @time 2021/08/02 16:51 14 | * @description 15 | */ 16 | class SkinCompatEditTextHelpter(private val edit: EditText) : SkinCompatTextHelper(edit) { 17 | private var hintId = INVALID_ID 18 | override fun loadFromAttributes(attrs: AttributeSet?, defStyleAttr: Int) { 19 | obtainStyledAttributes( 20 | edit, attrs, R.styleable.skinTextHelper, defStyleAttr, 0 21 | ) { 22 | sizeId = it.getResourceId(R.styleable.skinTextHelper_android_textSize, INVALID_ID) 23 | textColorId = it.getResourceId(R.styleable.skinTextHelper_android_textColor, INVALID_ID) 24 | textId = it.getResourceId(R.styleable.skinTextHelper_android_text, INVALID_ID) 25 | hintId = it.getResourceId(R.styleable.skinTextHelper_android_hint, INVALID_ID) 26 | } 27 | applySkin() 28 | } 29 | 30 | override fun applySkin() { 31 | super.applySkin() 32 | setHintText(hintId) 33 | } 34 | 35 | private fun setHintText(res: Int) { 36 | if (res == INVALID_ID) return 37 | val string = SkinCompatResources.getString(res) 38 | string?.run { edit.hint = this } 39 | } 40 | 41 | } -------------------------------------------------------------------------------- /skin/src/main/java/com/lvkang/skin/wedget/helper/SkinCompatImageHelper.kt: -------------------------------------------------------------------------------- 1 | package com.lvkang.skin.wedget.helper 2 | 3 | import android.util.AttributeSet 4 | import android.util.Log 5 | import android.widget.ImageView 6 | import androidx.appcompat.content.res.AppCompatResources 7 | import androidx.appcompat.widget.AppCompatImageView 8 | import androidx.core.view.ViewCompat 9 | import androidx.appcompat.R 10 | import com.lvkang.skin.ktx.obtainStyledAttributes 11 | import com.lvkang.skin.resource.SkinCompatResources 12 | import com.lvkang.skin.wedget.SkinCompatHelper 13 | 14 | /** 15 | * @name SkinCompatImageHelper 16 | * @package com.lvkang.skin.wedget.helper 17 | * @author 345 QQ:1831712732 18 | * @time 2020/12/10 22:56 19 | * @description 20 | */ 21 | class SkinCompatImageHelper(val view: AppCompatImageView) : SkinCompatHelper() { 22 | 23 | var src = INVALID_ID 24 | 25 | fun loadFromAttributes(attrs: AttributeSet?, defStyleAttr: Int) { 26 | obtainStyledAttributes( 27 | view, attrs, R.styleable.AppCompatImageView, defStyleAttr, 0 28 | ) { 29 | src = it.getResourceId( 30 | R.styleable.AppCompatImageView_android_src, INVALID_ID 31 | ) 32 | } 33 | applySkin() 34 | } 35 | 36 | override fun applySkin() { 37 | setImage(src) 38 | } 39 | 40 | private fun setImage(res: Int) { 41 | if (res == INVALID_ID) return 42 | val drawable = SkinCompatResources.getDrawable(res) 43 | if (drawable != null) { 44 | val paddingleft = view.paddingLeft 45 | val paddingTop = view.paddingTop 46 | val paddingRight = view.paddingRight 47 | val paddingBottom = view.paddingBottom 48 | view.setImageDrawable(drawable) 49 | view.setPadding(paddingleft, paddingTop, paddingRight, paddingBottom) 50 | return 51 | } 52 | val color = SkinCompatResources.getColor(res) 53 | color?.run { 54 | view.setBackgroundColor(color) 55 | } 56 | } 57 | } -------------------------------------------------------------------------------- /skin/src/main/java/com/lvkang/skin/wedget/helper/SkinCompatTextHelper.kt: -------------------------------------------------------------------------------- 1 | package com.lvkang.skin.wedget.helper 2 | 3 | import android.util.AttributeSet 4 | import android.widget.TextView 5 | import com.lvkang.skin.R 6 | import com.lvkang.skin.ktx.obtainStyledAttributes 7 | import com.lvkang.skin.ktx.px2dip 8 | import com.lvkang.skin.resource.SkinCompatResources 9 | import com.lvkang.skin.util.SkinLog 10 | import com.lvkang.skin.wedget.SkinCompatHelper 11 | 12 | /** 13 | * @name SkinCompatImageHelper 14 | * @package com.lvkang.skin.wedget.helper 15 | * @author 345 QQ:1831712732 16 | * @time 2020/12/10 23:10 17 | * @description 18 | */ 19 | open class SkinCompatTextHelper(private val view: TextView) : SkinCompatHelper() { 20 | 21 | internal var sizeId = INVALID_ID 22 | internal var textColorId = INVALID_ID 23 | internal var textId = INVALID_ID 24 | 25 | open fun loadFromAttributes(attrs: AttributeSet?, defStyleAttr: Int) { 26 | obtainStyledAttributes( 27 | view, attrs, R.styleable.skinTextHelper, defStyleAttr, 0 28 | ) { 29 | sizeId = it.getResourceId(R.styleable.skinTextHelper_android_textSize, INVALID_ID) 30 | textColorId = it.getResourceId(R.styleable.skinTextHelper_android_textColor, INVALID_ID) 31 | textId = it.getResourceId(R.styleable.skinTextHelper_android_text, INVALID_ID) 32 | } 33 | applySkin() 34 | } 35 | 36 | 37 | override fun applySkin() { 38 | setSize(sizeId) 39 | setText(textId) 40 | setTextColor(textColorId) 41 | } 42 | 43 | private fun setText(res: Int) { 44 | if (res == INVALID_ID) return 45 | val string = SkinCompatResources.getString(res) 46 | string?.run { view.text = string } 47 | } 48 | 49 | private fun setTextColor(res: Int) { 50 | if (res == INVALID_ID) return 51 | val color = SkinCompatResources.getColor(res) 52 | color?.run { view.setTextColor(this) } 53 | } 54 | 55 | private fun setSize(res: Int) { 56 | if (res == INVALID_ID) return 57 | val size = SkinCompatResources.getDimension(res) 58 | size?.run { view.textSize = px2dip(this) } 59 | } 60 | 61 | } -------------------------------------------------------------------------------- /skin/src/main/res/values/attrs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /skin/src/test/java/com/lvkang/skin/ExampleUnitTest.kt: -------------------------------------------------------------------------------- 1 | package com.lvkang.skin 2 | 3 | import org.junit.Test 4 | 5 | import org.junit.Assert.* 6 | 7 | /** 8 | * Example local unit test, which will execute on the development machine (host). 9 | * 10 | * See [testing documentation](http://d.android.com/tools/testing). 11 | */ 12 | class ExampleUnitTest { 13 | @Test 14 | fun addition_isCorrect() { 15 | assertEquals(4, 2 + 2) 16 | } 17 | } --------------------------------------------------------------------------------