├── .github └── workflows │ └── build_demo.yml ├── .gitignore ├── README.md ├── app ├── .gitignore ├── build.gradle.kts ├── consumer-rules.pro ├── proguard-rules.pro └── src │ └── main │ ├── AndroidManifest.xml │ ├── java │ └── cn │ │ └── xiaowine │ │ └── app │ │ ├── DemoActivity.kt │ │ └── pages │ │ ├── ButtonPage.kt │ │ ├── CardPage.kt │ │ ├── CustomPage.kt │ │ ├── DialogPage.kt │ │ ├── MainPage.kt │ │ ├── SeeKBarPage.kt │ │ ├── SpinnerPage.kt │ │ ├── SwitchPage.kt │ │ └── TextPage.kt │ └── res │ ├── drawable │ ├── ic_launcher_background.xml │ └── ic_launcher_foreground.xml │ ├── layout │ └── activity_main.xml │ └── values │ ├── colors.xml │ ├── strings.xml │ └── styles.xml ├── build.gradle.kts ├── gradle.properties ├── gradle ├── libs.versions.toml └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── settings.gradle.kts └── ui ├── .gitignore ├── build.gradle.kts ├── consumer-rules.pro ├── proguard-rules.pro └── src └── main ├── AndroidManifest.xml ├── java └── cn │ └── xiaowine │ └── ui │ ├── WineActivity.kt │ ├── WinePage.kt │ ├── annotation │ └── Coroutine.kt │ ├── appcompat │ ├── HyperButton.kt │ ├── HyperEditText.kt │ ├── HyperPopup.kt │ ├── HyperSeekBar.kt │ ├── HyperSwitch.kt │ └── WineRoundImageView.kt │ ├── build │ ├── CardViewBuild.kt │ └── PageBuild.kt │ ├── data │ ├── BackgroundStyle.kt │ ├── PageData.kt │ ├── SpinnerData.kt │ └── TogglePageDate.kt │ ├── dialog │ ├── WineDialog.kt │ ├── WineEditTextDialog.kt │ └── WineWaitDialog.kt │ ├── tools │ ├── ClassScanner.kt │ ├── DrawableTools.kt │ ├── HyperEditTextFocusTools.kt │ └── Tools.kt │ ├── viewmodel │ └── PageViewModel.kt │ └── widget │ ├── BaseWineText.kt │ ├── WineCard.kt │ ├── WineCardLink.kt │ ├── WineCardTitle.kt │ ├── WineLine.kt │ ├── WineSeekBar.kt │ ├── WineSpinner.kt │ ├── WineSwitch.kt │ ├── WineText.kt │ └── WineTitle.kt └── res ├── anim ├── popup_enter.xml └── popup_exit.xml ├── animator ├── dialog_enter.xml ├── dialog_exit.xml ├── dialog_pad_enter.xml ├── dialog_pad_exit.xml ├── slide_left_in.xml ├── slide_left_out.xml ├── slide_right_in.xml └── slide_right_out.xml ├── drawable ├── ic_popup_select.xml ├── ic_progress.xml ├── ic_right_arrow.xml └── ic_spinner.xml ├── layout ├── activity_wine.xml ├── base_text.xml ├── fragment_page.xml ├── wine_dialog.xml ├── wine_dialog_progressbar.xml └── wine_seek.xml ├── values-night ├── colors.xml └── themes.xml └── values ├── colors.xml ├── strings.xml └── themes.xml /.github/workflows/build_demo.yml: -------------------------------------------------------------------------------- 1 | name: Android CI 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | paths-ignore: 7 | - "README.md" 8 | pull_request: 9 | workflow_dispatch: 10 | 11 | jobs: 12 | build: 13 | runs-on: ubuntu-latest 14 | 15 | steps: 16 | - name: Checkout 17 | uses: actions/checkout@v4.1.1 18 | with: 19 | submodules: true 20 | 21 | - name: Write key 22 | if: ${{ github.event_name != 'pull_request' || github.ref_type == 'tag' }} 23 | run: | 24 | if [ ! -z "${{ secrets.SIGNING_KEY }}" ]; then 25 | echo androidStorePassword='${{ secrets.KEY_STORE_PASSWORD }}' >> local.properties 26 | echo androidKeyAlias='xiao_wine' >> local.properties 27 | echo androidKeyPassword='${{ secrets.KEY_STORE_PASSWORD }}' >> local.properties 28 | echo androidStoreFile=`pwd`/key.jks >> local.properties 29 | echo ${{ secrets.SIGNING_KEY }} | base64 --decode > key.jks 30 | fi 31 | 32 | - name: set up JDK 17 33 | uses: actions/setup-java@v4.0.0 34 | with: 35 | java-version: "17" 36 | distribution: "microsoft" 37 | 38 | - name: Setup Gradle 39 | uses: gradle/gradle-build-action@v3.1.0 40 | with: 41 | gradle-home-cache-cleanup: true 42 | cache-read-only: ${{ github.ref != 'refs/heads/master' }} 43 | 44 | - name: Build with Gradle 45 | run: | 46 | ./gradlew assembleRelease 47 | 48 | 49 | - name: Upload Demo APK 50 | uses: actions/upload-artifact@v4.3.1 51 | with: 52 | name: Demo 53 | path: ./app/build/outputs/apk/release 54 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by https://www.toptal.com/developers/gitignore/api/java,linux,macos,gradle,kotlin,android,windows,jetbrains,androidstudio 2 | # Edit at https://www.toptal.com/developers/gitignore?templates=java,linux,macos,gradle,kotlin,android,windows,jetbrains,androidstudio 3 | 4 | ### Android ### 5 | # Gradle files 6 | .gradle/ 7 | build/ 8 | 9 | # Local configuration file (sdk path, etc) 10 | local.properties 11 | 12 | # Log/OS Files 13 | *.log 14 | 15 | # Android Studio generated files and folders 16 | captures/ 17 | .externalNativeBuild/ 18 | .cxx/ 19 | *.apk 20 | output.json 21 | 22 | # IntelliJ 23 | *.iml 24 | .idea/ 25 | misc.xml 26 | deploymentTargetDropDown.xml 27 | render.experimental.xml 28 | 29 | # Keystore files 30 | *.jks 31 | *.keystore 32 | 33 | # Google Services (e.g. APIs or Firebase) 34 | google-services.json 35 | 36 | # Android Profiling 37 | *.hprof 38 | 39 | ### Android Patch ### 40 | gen-external-apklibs 41 | 42 | # Replacement of .externalNativeBuild directories introduced 43 | # with Android Studio 3.5. 44 | 45 | ### Java ### 46 | # Compiled class file 47 | *.class 48 | 49 | # Log file 50 | 51 | # BlueJ files 52 | *.ctxt 53 | 54 | # Mobile Tools for Java (J2ME) 55 | .mtj.tmp/ 56 | 57 | # Package Files # 58 | *.jar 59 | *.war 60 | *.nar 61 | *.ear 62 | *.zip 63 | *.tar.gz 64 | *.rar 65 | 66 | # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml 67 | hs_err_pid* 68 | replay_pid* 69 | 70 | ### JetBrains ### 71 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider 72 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 73 | 74 | # User-specific stuff 75 | .idea/**/workspace.xml 76 | .idea/**/tasks.xml 77 | .idea/**/usage.statistics.xml 78 | .idea/**/dictionaries 79 | .idea/**/shelf 80 | 81 | # AWS User-specific 82 | .idea/**/aws.xml 83 | 84 | # Generated files 85 | .idea/**/contentModel.xml 86 | 87 | # Sensitive or high-churn files 88 | .idea/**/dataSources/ 89 | .idea/**/dataSources.ids 90 | .idea/**/dataSources.local.xml 91 | .idea/**/sqlDataSources.xml 92 | .idea/**/dynamic.xml 93 | .idea/**/uiDesigner.xml 94 | .idea/**/dbnavigator.xml 95 | 96 | # Gradle 97 | .idea/**/gradle.xml 98 | .idea/**/libraries 99 | 100 | # Gradle and Maven with auto-import 101 | # When using Gradle or Maven with auto-import, you should exclude module files, 102 | # since they will be recreated, and may cause churn. Uncomment if using 103 | # auto-import. 104 | # .idea/artifacts 105 | # .idea/compiler.xml 106 | # .idea/jarRepositories.xml 107 | # .idea/modules.xml 108 | # .idea/*.iml 109 | # .idea/modules 110 | # *.iml 111 | # *.ipr 112 | 113 | # CMake 114 | cmake-build-*/ 115 | 116 | # Mongo Explorer plugin 117 | .idea/**/mongoSettings.xml 118 | 119 | # File-based project format 120 | *.iws 121 | 122 | # IntelliJ 123 | out/ 124 | 125 | # mpeltonen/sbt-idea plugin 126 | .idea_modules/ 127 | 128 | # JIRA plugin 129 | atlassian-ide-plugin.xml 130 | 131 | # Cursive Clojure plugin 132 | .idea/replstate.xml 133 | 134 | # SonarLint plugin 135 | .idea/sonarlint/ 136 | 137 | # Crashlytics plugin (for Android Studio and IntelliJ) 138 | com_crashlytics_export_strings.xml 139 | crashlytics.properties 140 | crashlytics-build.properties 141 | fabric.properties 142 | 143 | # Editor-based Rest Client 144 | .idea/httpRequests 145 | 146 | # Android studio 3.1+ serialized cache file 147 | .idea/caches/build_file_checksums.ser 148 | 149 | ### JetBrains Patch ### 150 | # Comment Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-215987721 151 | 152 | # *.iml 153 | # modules.xml 154 | # .idea/misc.xml 155 | # *.ipr 156 | 157 | # Sonarlint plugin 158 | # https://plugins.jetbrains.com/plugin/7973-sonarlint 159 | .idea/**/sonarlint/ 160 | 161 | # SonarQube Plugin 162 | # https://plugins.jetbrains.com/plugin/7238-sonarqube-community-plugin 163 | .idea/**/sonarIssues.xml 164 | 165 | # Markdown Navigator plugin 166 | # https://plugins.jetbrains.com/plugin/7896-markdown-navigator-enhanced 167 | .idea/**/markdown-navigator.xml 168 | .idea/**/markdown-navigator-enh.xml 169 | .idea/**/markdown-navigator/ 170 | 171 | # Cache file creation bug 172 | # See https://youtrack.jetbrains.com/issue/JBR-2257 173 | .idea/$CACHE_FILE$ 174 | 175 | # CodeStream plugin 176 | # https://plugins.jetbrains.com/plugin/12206-codestream 177 | .idea/codestream.xml 178 | 179 | # Azure Toolkit for IntelliJ plugin 180 | # https://plugins.jetbrains.com/plugin/8053-azure-toolkit-for-intellij 181 | .idea/**/azureSettings.xml 182 | 183 | ### Kotlin ### 184 | # Compiled class file 185 | 186 | # Log file 187 | 188 | # BlueJ files 189 | 190 | # Mobile Tools for Java (J2ME) 191 | 192 | # Package Files # 193 | 194 | # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml 195 | 196 | ### Linux ### 197 | *~ 198 | 199 | # temporary files which can be created if a process still has a handle open of a deleted file 200 | .fuse_hidden* 201 | 202 | # KDE directory preferences 203 | .directory 204 | 205 | # Linux trash folder which might appear on any partition or disk 206 | .Trash-* 207 | 208 | # .nfs files are created when an open file is removed but is still being accessed 209 | .nfs* 210 | 211 | ### macOS ### 212 | # General 213 | .DS_Store 214 | .AppleDouble 215 | .LSOverride 216 | 217 | # Icon must end with two \r 218 | Icon 219 | 220 | 221 | # Thumbnails 222 | ._* 223 | 224 | # Files that might appear in the root of a volume 225 | .DocumentRevisions-V100 226 | .fseventsd 227 | .Spotlight-V100 228 | .TemporaryItems 229 | .Trashes 230 | .VolumeIcon.icns 231 | .com.apple.timemachine.donotpresent 232 | 233 | # Directories potentially created on remote AFP share 234 | .AppleDB 235 | .AppleDesktop 236 | Network Trash Folder 237 | Temporary Items 238 | .apdisk 239 | 240 | ### macOS Patch ### 241 | # iCloud generated files 242 | *.icloud 243 | 244 | ### Windows ### 245 | # Windows thumbnail cache files 246 | Thumbs.db 247 | Thumbs.db:encryptable 248 | ehthumbs.db 249 | ehthumbs_vista.db 250 | 251 | # Dump file 252 | *.stackdump 253 | 254 | # Folder config file 255 | [Dd]esktop.ini 256 | 257 | # Recycle Bin used on file shares 258 | $RECYCLE.BIN/ 259 | 260 | # Windows Installer files 261 | *.cab 262 | *.msi 263 | *.msix 264 | *.msm 265 | *.msp 266 | 267 | # Windows shortcuts 268 | *.lnk 269 | 270 | ### Gradle ### 271 | .gradle 272 | **/build/ 273 | !src/**/build/ 274 | 275 | # Ignore Gradle GUI config 276 | gradle-app.setting 277 | 278 | # Avoid ignoring Gradle wrapper jar file (.jar files are usually ignored) 279 | !gradle-wrapper.jar 280 | 281 | # Avoid ignore Gradle wrappper properties 282 | !gradle-wrapper.properties 283 | 284 | # Cache of project 285 | .gradletasknamecache 286 | 287 | # Eclipse Gradle plugin generated files 288 | # Eclipse Core 289 | .project 290 | # JDT-specific (Eclipse Java Development Tools) 291 | .classpath 292 | 293 | ### Gradle Patch ### 294 | # Java heap dump 295 | 296 | ### AndroidStudio ### 297 | # Covers files to be ignored for android development using Android Studio. 298 | 299 | # Built application files 300 | *.ap_ 301 | *.aab 302 | 303 | # Files for the ART/Dalvik VM 304 | *.dex 305 | 306 | # Java class files 307 | 308 | # Generated files 309 | bin/ 310 | gen/ 311 | 312 | # Gradle files 313 | 314 | # Signing files 315 | .signing/ 316 | 317 | # Local configuration file (sdk path, etc) 318 | 319 | # Proguard folder generated by Eclipse 320 | proguard/ 321 | 322 | # Log Files 323 | 324 | # Android Studio 325 | /*/build/ 326 | /*/local.properties 327 | /*/out 328 | /*/*/build 329 | /*/*/production 330 | .navigation/ 331 | *.ipr 332 | *.swp 333 | 334 | # Keystore files 335 | 336 | # Google Services (e.g. APIs or Firebase) 337 | # google-services.json 338 | 339 | # Android Patch 340 | 341 | # External native build folder generated in Android Studio 2.2 and later 342 | .externalNativeBuild 343 | 344 | # NDK 345 | obj/ 346 | 347 | # IntelliJ IDEA 348 | /out/ 349 | 350 | # User-specific configurations 351 | .idea/caches/ 352 | .idea/libraries/ 353 | .idea/shelf/ 354 | .idea/workspace.xml 355 | .idea/tasks.xml 356 | .idea/.name 357 | .idea/compiler.xml 358 | .idea/copyright/profiles_settings.xml 359 | .idea/encodings.xml 360 | .idea/misc.xml 361 | .idea/modules.xml 362 | .idea/scopes/scope_settings.xml 363 | .idea/dictionaries 364 | .idea/vcs.xml 365 | .idea/jsLibraryMappings.xml 366 | .idea/datasources.xml 367 | .idea/dataSources.ids 368 | .idea/sqlDataSources.xml 369 | .idea/dynamic.xml 370 | .idea/uiDesigner.xml 371 | .idea/assetWizardSettings.xml 372 | .idea/gradle.xml 373 | .idea/jarRepositories.xml 374 | .idea/navEditor.xml 375 | 376 | # Legacy Eclipse project files 377 | .cproject 378 | .settings/ 379 | 380 | # Mobile Tools for Java (J2ME) 381 | 382 | # Package Files # 383 | 384 | # virtual machine crash logs (Reference: http://www.java.com/en/download/help/error_hotspot.xml) 385 | 386 | ## Plugin-specific files: 387 | 388 | # mpeltonen/sbt-idea plugin 389 | 390 | # JIRA plugin 391 | 392 | # Mongo Explorer plugin 393 | .idea/mongoSettings.xml 394 | 395 | # Crashlytics plugin (for Android Studio and IntelliJ) 396 | 397 | ### AndroidStudio Patch ### 398 | 399 | !/gradle/wrapper/gradle-wrapper.jar 400 | 401 | # End of https://www.toptal.com/developers/gitignore/api/java,linux,macos,gradle,kotlin,android,windows,jetbrains,androidstudio -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![](https://socialify.git.ci/xiaowine/WineUI/image?description=1&descriptionEditable=一个模仿`HyperOS`风格制作的DSL界面库&language=1&name=1&owner=1&theme=Auto) 2 | 3 | # 这是什么 4 | 5 | ### 这是一个模仿`HyperOS`风格制作的DSL界面库 6 | 适合快速编写模仿`HyperOS`风格的软件/模块 7 | 本库一切行为,与小米无关 8 | 9 | ## 已经实现可快速使用的组件 10 | 11 | - [x] 标题 (title) 12 | - [x] 文本 (text) 13 | - [x] 分割线 (line) 14 | - [x] 按钮 (button) 15 | - [x] 开关 (switch) 16 | - [x] 拖动条 (seekbar) 17 | - [x] 对话框 (dialog) 18 | - [x] 卡片 (card) 19 | - [x] 自定义组件 (custom) 20 | 具体可查看 cn/xiaowine/ui/build/PageBuild.kt 21 | 22 | # 如何使用 23 | 24 | ### 1. 项目 Gradle 添加 JitPack 依赖 25 | 26 | groovy 27 | 28 | ``` 29 | allprojects { 30 | repositories { 31 | // ... 32 | maven { url 'https://jitpack.io' } 33 | } 34 | } 35 | ``` 36 | 37 | kotlin 38 | 39 | ```kotlin 40 | allprojects { 41 | repositories { 42 | // ... 43 | maven("https://jitpack.io") 44 | } 45 | } 46 | ``` 47 | 48 | ### 2. 要使用的模块下添加 DSP 依赖 49 | 50 | 最新版本⬇️⬇️⬇️ 51 | 52 | [![](https://jitpack.io/v/xiaowine/WineUI.svg)](https://jitpack.io/#xiaowine/WineUI/) 53 | --- 54 | groovy 55 | 56 | ```groovy 57 | dependencies { 58 | // ... 59 | implementation 'com.github.xiaowine:WineUI:' 60 | } 61 | ``` 62 | 63 | kotlin 64 | 65 | ```kotlin 66 | dependencies { 67 | // ... 68 | implementation("com.github.xiaowine:WineUI:") 69 | } 70 | ``` 71 | 72 | ### 3. 修改清单文件 73 | 74 | ```xml 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | ``` 83 | 84 | ### 4. 继承WineActivity,并注册Page 85 | 86 | ```kotlin 87 | // 1.继承 WineActivity 88 | // 2.注册页面 registerPage 89 | // 3.注册页面的时候, PageData 应设置 isHome 参数为 true, 表示这个页面是首页 90 | // 4.首页的 TabBar 上不会有返回按钮, 其余页面的 TabBar 上都会有返回按钮 91 | class DemoActivity : WineActivity() { 92 | override fun onCreate(savedInstanceState: Bundle?) { 93 | super.onCreate(savedInstanceState) 94 | // 注册页面,两种方式可以混用,后注册的覆盖先注册的 95 | 96 | // 或通过手动添加注册页面 97 | registerPage( 98 | PageData(MainPage::class.java, isHome = true), 99 | PageData(TextPage::class.java), 100 | PageData(SwitchPage::class.java), 101 | ) 102 | 103 | // 或通过扫描包名注册页面(全部按照默认配置生成界面) 104 | registerPage("cn.xiaowine.app.pages", MainPage::class.java) 105 | } 106 | } 107 | ``` 108 | 109 | ### 5. 创建页面 110 | 111 | ```kotlin 112 | // 1.继承WinePage 113 | // 2.初始化页面initPage 114 | // 并非必须在init中初始化页面,可以在除了onCreateView和onViewCreated任何地方初始化页面,只需要加上一个reloadPage()方法 115 | // 可参考 cn/xiaowine/ui/pages/MainPage.kt 116 | 117 | // 加上@Coroutine注解,初始化页面的时候会采用协程 118 | @Coroutine 119 | class MainPage : WinePage() { 120 | init { 121 | initPage { 122 | title { 123 | text = this@MainPage::class.java.simpleName 124 | } 125 | title { 126 | text = "标题" 127 | } 128 | toPageText(page = TextPage::class.java) 129 | toPageText(page = SwitchPage::class.java) 130 | toPageText(page = SeeKBarPage::class.java) 131 | toPageText(page = DialogPage::class.java) 132 | toPageText(page = CustomPage::class.java) 133 | toPageText(page = CardPage::class.java) 134 | } 135 | } 136 | } 137 | ``` 138 | 139 | ### !!!注意,如果你开启了混淆,请添加以下规则 140 | 141 | ```pro 142 | -keep class cn.xiaowine.ui.annotation.** { *; } 143 | -keep class cn.xiaowine.ui.data.** { *; } 144 | -keep class cn.xiaowine.ui.widget.**{ (...);} 145 | -keep class cn.xiaowine.ui.appcompat.**{ (...);} 146 | ``` 147 | 148 | ## Star History 149 | 150 | [![Star History Chart](https://api.star-history.com/svg?repos=xiaowine/WineUI&type=Timeline)](https://star-history.com/#xiaowine/WineUI&Timeline) 151 | 152 | ## Thanks 153 | [](https://www.jetbrains.com) 154 | -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /app/build.gradle.kts: -------------------------------------------------------------------------------- 1 | import org.jetbrains.kotlin.konan.properties.Properties 2 | 3 | plugins { 4 | alias(libs.plugins.jetbrainsKotlinAndroid) 5 | alias(libs.plugins.androidApplication) 6 | } 7 | val localProperties = Properties() 8 | if (rootProject.file("local.properties").canRead()) 9 | localProperties.load(rootProject.file("local.properties").inputStream()) 10 | 11 | android { 12 | namespace = "cn.xiaowine.app" 13 | compileSdk = 34 14 | 15 | defaultConfig { 16 | minSdk = 21 17 | targetSdk = 34 18 | versionCode = 2 19 | versionName = "1.1" 20 | } 21 | val config = localProperties.getProperty("androidStoreFile")?.let { 22 | signingConfigs.create("config") { 23 | storeFile = file(it) 24 | storePassword = localProperties.getProperty("androidStorePassword") 25 | keyAlias = localProperties.getProperty("androidKeyAlias") 26 | keyPassword = localProperties.getProperty("androidKeyPassword") 27 | enableV3Signing = true 28 | enableV4Signing = true 29 | } 30 | } 31 | buildTypes { 32 | all { 33 | signingConfig = config ?: signingConfigs["debug"] 34 | } 35 | release { 36 | isMinifyEnabled = false 37 | isShrinkResources = false 38 | setProguardFiles( 39 | listOf( 40 | getDefaultProguardFile("proguard-android-optimize.txt"), 41 | "proguard-rules.pro", 42 | "proguard-log.pro" 43 | ) 44 | ) 45 | } 46 | } 47 | compileOptions { 48 | sourceCompatibility = JavaVersion.VERSION_17 49 | targetCompatibility = JavaVersion.VERSION_17 50 | } 51 | kotlinOptions { 52 | jvmTarget = JavaVersion.VERSION_17.majorVersion 53 | } 54 | } 55 | 56 | dependencies { 57 | // implementation(libs.core.ktx) 58 | implementation(libs.androidx.appcompat) 59 | implementation(libs.material) 60 | implementation(project(mapOf("path" to ":ui"))) 61 | } -------------------------------------------------------------------------------- /app/consumer-rules.pro: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xiaowine/WineUI/4ee70acaa175d054b404988cdc057de34e522f00/app/consumer-rules.pro -------------------------------------------------------------------------------- /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 22 | -optimizationpasses 5 23 | -keep class cn.xiaowine.ui.annotation.** { *; } 24 | -keep class cn.xiaowine.ui.data.** { *; } 25 | -keep class cn.xiaowine.ui.widget.**{ (...);} 26 | -keep class cn.xiaowine.ui.appcompat.**{ (...);} -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 9 | 10 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /app/src/main/java/cn/xiaowine/app/DemoActivity.kt: -------------------------------------------------------------------------------- 1 | package cn.xiaowine.app 2 | 3 | import android.os.Bundle 4 | import cn.xiaowine.app.pages.ButtonPage 5 | import cn.xiaowine.app.pages.CardPage 6 | import cn.xiaowine.app.pages.CustomPage 7 | import cn.xiaowine.app.pages.DialogPage 8 | import cn.xiaowine.app.pages.MainPage 9 | import cn.xiaowine.app.pages.SeeKBarPage 10 | import cn.xiaowine.app.pages.SwitchPage 11 | import cn.xiaowine.app.pages.TextPage 12 | import cn.xiaowine.ui.WineActivity 13 | import cn.xiaowine.ui.data.PageData 14 | 15 | // 1.继承 WineActivity 16 | // 2.注册页面 registerPage 17 | // 3.注册页面的时候, PageData 应设置 isHome 参数为 true, 表示这个页面是首页 18 | // 4.首页的 TabBar 上不会有返回按钮, 其余页面的 TabBar 上都会有返回按钮 19 | class DemoActivity : WineActivity() { 20 | override fun onCreate(savedInstanceState: Bundle?) { 21 | super.onCreate(savedInstanceState) 22 | // 注册页面,两种方式可以混用,后注册的覆盖先注册的 23 | 24 | // 或通过手动添加注册页面 25 | registerPage( 26 | PageData(MainPage::class.java, isHome = true), 27 | PageData(TextPage::class.java), 28 | PageData(SwitchPage::class.java), 29 | PageData(ButtonPage::class.java), 30 | PageData(CardPage::class.java), 31 | PageData(CustomPage::class.java), 32 | PageData(DialogPage::class.java), 33 | PageData(SeeKBarPage::class.java), 34 | ) 35 | 36 | // 或通过扫描包名注册页面(全部按照默认配置生成界面) 37 | registerPage("cn.xiaowine.app.pages", MainPage::class.java) 38 | } 39 | } -------------------------------------------------------------------------------- /app/src/main/java/cn/xiaowine/app/pages/ButtonPage.kt: -------------------------------------------------------------------------------- 1 | package cn.xiaowine.app.pages 2 | 3 | import android.annotation.SuppressLint 4 | import android.widget.LinearLayout 5 | import android.widget.Toast 6 | import cn.xiaowine.ui.WinePage 7 | import cn.xiaowine.ui.tools.Tools.dp 8 | 9 | @SuppressLint("SetTextI18n") 10 | class ButtonPage : WinePage() { 11 | 12 | init { 13 | initPage { 14 | toPageText() 15 | title { 16 | text = "Button" 17 | } 18 | button { 19 | text = "Click me" 20 | setOnClickListener { 21 | Toast.makeText(context, "Hello", Toast.LENGTH_SHORT).show() 22 | } 23 | (layoutParams as LinearLayout.LayoutParams).apply { 24 | setMargins(30.dp, 10.dp, 30.dp, 10.dp) 25 | } 26 | } 27 | button { 28 | text = "Click me" 29 | setOnClickListener { 30 | Toast.makeText(context, "Hello", Toast.LENGTH_SHORT).show() 31 | } 32 | (layoutParams as LinearLayout.LayoutParams).apply { 33 | setMargins(30.dp, 10.dp, 30.dp, 10.dp) 34 | } 35 | isEnabled = false 36 | } 37 | button { 38 | text = "Click me" 39 | setOnClickListener { 40 | Toast.makeText(context, "Hello", Toast.LENGTH_SHORT).show() 41 | } 42 | (layoutParams as LinearLayout.LayoutParams).apply { 43 | setMargins(30.dp, 10.dp, 30.dp, 10.dp) 44 | } 45 | setCancel(true) 46 | } 47 | } 48 | } 49 | } -------------------------------------------------------------------------------- /app/src/main/java/cn/xiaowine/app/pages/CardPage.kt: -------------------------------------------------------------------------------- 1 | package cn.xiaowine.app.pages 2 | 3 | import android.annotation.SuppressLint 4 | import android.widget.Toast 5 | import cn.xiaowine.ui.WinePage 6 | 7 | @SuppressLint("SetTextI18n") 8 | class CardPage : WinePage() { 9 | 10 | init { 11 | initPage { 12 | toPageText() 13 | title { text = "Card" } 14 | card { 15 | build { 16 | title { 17 | text = "WineCardTitle" 18 | } 19 | link { 20 | title = "链接" 21 | onClick { 22 | Toast.makeText(context, "onClick", Toast.LENGTH_SHORT).show() 23 | } 24 | } 25 | link { 26 | title = "链接" 27 | } 28 | } 29 | } 30 | card { 31 | build { 32 | title { 33 | text = "WineCardTitle" 34 | } 35 | link { 36 | title = "链接" 37 | onClick { 38 | Toast.makeText(context, "onClick", Toast.LENGTH_SHORT).show() 39 | } 40 | } 41 | } 42 | } 43 | 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /app/src/main/java/cn/xiaowine/app/pages/CustomPage.kt: -------------------------------------------------------------------------------- 1 | package cn.xiaowine.app.pages 2 | 3 | import android.annotation.SuppressLint 4 | import android.webkit.WebView 5 | import android.widget.LinearLayout 6 | import cn.xiaowine.ui.WinePage 7 | import cn.xiaowine.ui.appcompat.HyperEditText 8 | import cn.xiaowine.ui.data.HyperEditBackgroundStyle 9 | import cn.xiaowine.ui.tools.Tools.dp 10 | 11 | @SuppressLint("SetTextI18n") 12 | class CustomPage : WinePage() { 13 | 14 | init { 15 | initPage { 16 | toPageText() 17 | edittext { 18 | hint = "请输入" 19 | (layoutParams as LinearLayout.LayoutParams).apply { 20 | setMargins(30.dp, 10.dp, 30.dp, 10.dp) 21 | } 22 | } 23 | custom(HyperEditText::class.java) { 24 | (it as HyperEditText).apply { 25 | (layoutParams as LinearLayout.LayoutParams).apply { 26 | setMargins(30.dp, 10.dp, 30.dp, 10.dp) 27 | } 28 | hint = "请输入" 29 | } 30 | } 31 | custom(HyperEditText::class.java) { 32 | (it as HyperEditText).apply { 33 | (layoutParams as LinearLayout.LayoutParams).apply { 34 | setMargins(30.dp, 10.dp, 30.dp, 10.dp) 35 | } 36 | isSingleLine = true 37 | hint = "请输入" 38 | } 39 | } 40 | custom(HyperEditText::class.java) { 41 | (it as HyperEditText).apply { 42 | (layoutParams as LinearLayout.LayoutParams).apply { 43 | setMargins(30.dp, 10.dp, 30.dp, 10.dp) 44 | } 45 | setBackgroundStyle(HyperEditBackgroundStyle.NORMAL) 46 | isSingleLine = true 47 | hint = "请输入" 48 | } 49 | } 50 | custom(HyperEditText::class.java) { 51 | (it as HyperEditText).apply { 52 | (layoutParams as LinearLayout.LayoutParams).apply { 53 | setMargins(30.dp, 10.dp, 30.dp, 10.dp) 54 | } 55 | setBackgroundStyle(HyperEditBackgroundStyle.STROKE) 56 | isSingleLine = true 57 | hint = "请输入" 58 | } 59 | } 60 | custom(WebView::class.java) { 61 | (it as WebView).apply { 62 | scrollY = 550 63 | scrollX = 500 64 | layoutParams = LinearLayout.LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT, 400.dp) 65 | loadUrl("https://www.bilibili.com/video/BV1GJ411x7h7/") 66 | getSettings().userAgentString = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.0.0 Safari/537.36 Edg/121.0.0.0" 67 | setWebViewClient(object : android.webkit.WebViewClient() { 68 | override fun shouldOverrideUrlLoading(view: WebView, request: android.webkit.WebResourceRequest): Boolean { 69 | view.loadUrl(request.url.toString()) 70 | return true 71 | } 72 | }) 73 | } 74 | } 75 | } 76 | } 77 | } -------------------------------------------------------------------------------- /app/src/main/java/cn/xiaowine/app/pages/DialogPage.kt: -------------------------------------------------------------------------------- 1 | package cn.xiaowine.app.pages 2 | 3 | import android.annotation.SuppressLint 4 | import android.widget.Toast 5 | import cn.xiaowine.ui.WinePage 6 | import cn.xiaowine.ui.dialog.WineDialog 7 | import cn.xiaowine.ui.dialog.WineEditTextDialog 8 | import cn.xiaowine.ui.dialog.WineWaitDialog 9 | import kotlinx.coroutines.CoroutineScope 10 | import kotlinx.coroutines.Dispatchers 11 | import kotlinx.coroutines.launch 12 | 13 | @SuppressLint("SetTextI18n") 14 | class DialogPage : WinePage() { 15 | 16 | init { 17 | initPage { 18 | toPageText() 19 | title { text = "Dialog" } 20 | text { 21 | title = "Show Dialog" 22 | onClick { 23 | WineDialog(context).apply { 24 | setTitle("标题") 25 | setMessage("这是一个消息") 26 | addButton("确定") { 27 | Toast.makeText(context, "确定", Toast.LENGTH_SHORT).show() 28 | dismiss() 29 | } 30 | addButton("取消") { 31 | Toast.makeText(context, "取消", Toast.LENGTH_SHORT).show() 32 | dismiss() 33 | }.setCancel(true) 34 | show() 35 | } 36 | } 37 | } 38 | text { 39 | title = "Show Dialog" 40 | onClick { 41 | WineDialog(context).apply { 42 | setTitle("标题") 43 | setMessage("这是一个消息") 44 | addButton("确定") { 45 | Toast.makeText(context, "确定", Toast.LENGTH_SHORT).show() 46 | dismiss() 47 | } 48 | addButton("取消") { 49 | Toast.makeText(context, "取消", Toast.LENGTH_SHORT).show() 50 | dismiss() 51 | }.isEnabled = false 52 | show() 53 | } 54 | } 55 | } 56 | text { 57 | title = "Show Dialog" 58 | onClick { 59 | WineDialog(context).apply { 60 | setTitle("标题") 61 | setMessage("这是一个消息") 62 | addButton("确定") { 63 | Toast.makeText(context, "确定", Toast.LENGTH_SHORT).show() 64 | dismiss() 65 | } 66 | addButton("1取消") { 67 | Toast.makeText(context, "1取消", Toast.LENGTH_SHORT).show() 68 | dismiss() 69 | } 70 | addButton("2取消") { 71 | Toast.makeText(context, "2取消", Toast.LENGTH_SHORT).show() 72 | dismiss() 73 | } 74 | show() 75 | } 76 | } 77 | } 78 | text { 79 | title = "Show WineWaitDialog" 80 | onClick { 81 | val wineWaitDialog = WineWaitDialog(requireContext()).apply { 82 | setTitle("标题") 83 | show() 84 | } 85 | Thread { 86 | Thread.sleep(3000) 87 | CoroutineScope(Dispatchers.Main).launch { 88 | wineWaitDialog.dismiss() 89 | } 90 | }.start() 91 | } 92 | } 93 | text { 94 | title = "Show WineEditTextDialog" 95 | onClick { 96 | WineEditTextDialog(context).apply { 97 | setTitle("标题") 98 | setMessage("这是一个消息") 99 | setHint("请输入") 100 | addButton("确定") { 101 | Toast.makeText(context, "确定", Toast.LENGTH_SHORT).show() 102 | dismiss() 103 | } 104 | addButton("取消") { 105 | Toast.makeText(context, "取消", Toast.LENGTH_SHORT).show() 106 | dismiss() 107 | }.setCancel(true) 108 | show() 109 | } 110 | } 111 | } 112 | } 113 | } 114 | } -------------------------------------------------------------------------------- /app/src/main/java/cn/xiaowine/app/pages/MainPage.kt: -------------------------------------------------------------------------------- 1 | package cn.xiaowine.app.pages 2 | 3 | import android.widget.Toast 4 | import cn.xiaowine.ui.WinePage 5 | import cn.xiaowine.ui.data.SpinnerData 6 | import cn.xiaowine.ui.dialog.WineWaitDialog 7 | 8 | // 1.继承 WinePage 9 | // 2.初始化页面 initPage 10 | 11 | class MainPage : WinePage() { 12 | 13 | // 防止重复初始化 14 | private var isPageInitialized = false 15 | 16 | init { 17 | initPage { 18 | title { 19 | text = this@MainPage::class.java.simpleName 20 | } 21 | title { 22 | text = "标题" 23 | } 24 | } 25 | } 26 | 27 | override fun onStart() { 28 | super.onStart() 29 | if (!isPageInitialized) { 30 | val wineWaitDialog = WineWaitDialog(requireContext()).apply { 31 | setTitle("加载中") 32 | show() 33 | } 34 | initPage { 35 | isPageInitialized = true 36 | pageItems.forEach { 37 | if (it.page == this@MainPage::class.java) { 38 | return@forEach 39 | } 40 | toPageText(page = it.page) 41 | } 42 | } 43 | reloadPage() 44 | wineWaitDialog.dismiss() 45 | } 46 | } 47 | } -------------------------------------------------------------------------------- /app/src/main/java/cn/xiaowine/app/pages/SeeKBarPage.kt: -------------------------------------------------------------------------------- 1 | package cn.xiaowine.app.pages 2 | 3 | import android.annotation.SuppressLint 4 | import android.widget.SeekBar 5 | import android.widget.Toast 6 | import cn.xiaowine.ui.WinePage 7 | import cn.xiaowine.ui.widget.WineSeekBar 8 | 9 | @SuppressLint("SetTextI18n") 10 | class SeeKBarPage : WinePage() { 11 | 12 | init { 13 | initPage { 14 | toPageText() 15 | title { 16 | text = "SeekBar" 17 | } 18 | title { 19 | text = "SeekBar的最大值为9999,最小值为-9999,Android 8.0以上才支持设置最小值" 20 | } 21 | seekbar { 22 | minProgress = -200 23 | maxProgress = 200 24 | onProgressChanged(object : WineSeekBar.ProgressChangedListener { 25 | override fun onChanged(seekBar: SeekBar, progress: Int, fromUser: Boolean) { 26 | if (fromUser) Toast.makeText(context, "progress:$progress", Toast.LENGTH_SHORT).show() 27 | } 28 | }) 29 | onLongClick { _, _ -> 30 | Toast.makeText(context, "onLongClick", Toast.LENGTH_SHORT).show() 31 | } 32 | } 33 | seekbar { 34 | nowProgress = 101 35 | minProgress = 0 36 | maxProgress = 1000 37 | onProgressChanged(object : WineSeekBar.ProgressChangedListener { 38 | override fun onChanged(seekBar: SeekBar, progress: Int, fromUser: Boolean) { 39 | if (fromUser) Toast.makeText(context, "progress:$progress", Toast.LENGTH_SHORT).show() 40 | } 41 | }) 42 | onLongClick { _, _ -> 43 | Toast.makeText(context, "onLongClick", Toast.LENGTH_SHORT).show() 44 | } 45 | } 46 | seekbar { 47 | nowProgress = -9999 48 | minProgress = -9999 49 | maxProgress = 9999 50 | onProgressChanged(object : WineSeekBar.ProgressChangedListener { 51 | override fun onChanged(seekBar: SeekBar, progress: Int, fromUser: Boolean) { 52 | if (fromUser) Toast.makeText(context, "progress:$progress", Toast.LENGTH_SHORT).show() 53 | } 54 | }) 55 | onLongClick { _, _ -> 56 | Toast.makeText(context, "onLongClick", Toast.LENGTH_SHORT).show() 57 | } 58 | } 59 | } 60 | } 61 | } -------------------------------------------------------------------------------- /app/src/main/java/cn/xiaowine/app/pages/SpinnerPage.kt: -------------------------------------------------------------------------------- 1 | package cn.xiaowine.app.pages 2 | 3 | import android.widget.Toast 4 | import cn.xiaowine.ui.WinePage 5 | import cn.xiaowine.ui.data.SpinnerData 6 | 7 | class SpinnerPage : WinePage() { 8 | init { 9 | initPage { 10 | toPageText() 11 | title { text = "Spinner" } 12 | spinner { 13 | title = "测试" 14 | setData( 15 | SpinnerData("测试1") { 16 | Toast.makeText(requireContext(), "测试1", Toast.LENGTH_SHORT).show() 17 | }, 18 | SpinnerData("测试2") { 19 | Toast.makeText(requireContext(), "测试2", Toast.LENGTH_SHORT).show() 20 | }, 21 | SpinnerData("测试3") { 22 | Toast.makeText(requireContext(), "测试3", Toast.LENGTH_SHORT).show() 23 | }, 24 | ) 25 | 26 | } 27 | spinner { 28 | title = "测试" 29 | currentValue = "测试2" 30 | for (i in 1..10) { 31 | setData( 32 | SpinnerData("测试$i") { 33 | Toast.makeText(requireContext(), "测试$i", Toast.LENGTH_SHORT).show() 34 | } 35 | ) 36 | } 37 | } 38 | } 39 | } 40 | } -------------------------------------------------------------------------------- /app/src/main/java/cn/xiaowine/app/pages/SwitchPage.kt: -------------------------------------------------------------------------------- 1 | package cn.xiaowine.app.pages 2 | 3 | import android.widget.Toast 4 | import cn.xiaowine.app.R 5 | import cn.xiaowine.ui.WinePage 6 | 7 | class SwitchPage : WinePage() { 8 | 9 | init { 10 | initPage { 11 | toPageText() 12 | title { 13 | text = "开关" 14 | } 15 | switch { 16 | title = "开关" 17 | isChecked = true 18 | onChange { _, isChecked -> 19 | Toast.makeText(context, "onChange value:$isChecked", Toast.LENGTH_SHORT).show() 20 | } 21 | onLongClick { 22 | Toast.makeText(context, "onLongClick value:$it", Toast.LENGTH_SHORT).show() 23 | } 24 | } 25 | switch { 26 | title = "开关" 27 | onChange { _, isChecked -> 28 | Toast.makeText(context, "onChange value:$isChecked", Toast.LENGTH_SHORT).show() 29 | } 30 | setIcon(R.drawable.ic_launcher_background) 31 | } 32 | switch { 33 | title = "带摘要的开关" 34 | summary = "这是一个摘要" 35 | isChecked = true 36 | onChange { _, isChecked -> 37 | Toast.makeText(context, "onChange value:$isChecked", Toast.LENGTH_SHORT).show() 38 | } 39 | } 40 | switch { 41 | title = "带摘要带图标的开关" 42 | summary = "这是一个摘要" 43 | isChecked = false 44 | onChange { _, isChecked -> 45 | Toast.makeText(context, "onChange value:$isChecked", Toast.LENGTH_SHORT).show() 46 | } 47 | setIcon(R.drawable.ic_launcher_background) 48 | } 49 | } 50 | } 51 | } -------------------------------------------------------------------------------- /app/src/main/java/cn/xiaowine/app/pages/TextPage.kt: -------------------------------------------------------------------------------- 1 | package cn.xiaowine.app.pages 2 | 3 | import android.annotation.SuppressLint 4 | import cn.xiaowine.app.R 5 | import cn.xiaowine.ui.WinePage 6 | import cn.xiaowine.ui.annotation.Coroutine 7 | 8 | @SuppressLint("SetTextI18n") 9 | @Coroutine 10 | class TextPage : WinePage() { 11 | 12 | init { 13 | initPage { 14 | toPageText() 15 | text { 16 | title = "这是一个标题" 17 | } 18 | text { 19 | setIcon(R.drawable.ic_launcher_background) 20 | title = "这是一个带图标的标题" 21 | } 22 | text { 23 | title = "这是一个带摘要的标题" 24 | summary = "这是一个摘要" 25 | } 26 | text { 27 | title = "这是一个带图标带摘要的标题" 28 | summary = "这是一个摘要" 29 | setIcon(R.drawable.ic_launcher_background) 30 | } 31 | text { 32 | title = "带摘要的标题" 33 | summary = 34 | "这是摘摘摘摘摘摘摘摘摘摘摘摘摘摘摘摘摘摘摘摘摘摘摘摘摘摘摘摘摘摘摘摘摘摘摘摘摘摘摘摘摘摘摘摘摘摘摘摘摘摘摘摘摘摘摘摘摘摘摘摘摘摘摘摘摘摘摘摘摘摘摘摘要" 35 | } 36 | text { 37 | title = "带摘要带图标的标题" 38 | summary = 39 | "这是摘摘摘摘摘摘摘摘摘摘摘摘摘摘摘摘摘摘摘摘摘摘摘摘摘摘摘摘摘摘摘摘摘摘摘摘摘摘摘摘摘摘摘摘摘摘摘摘摘摘摘摘摘摘摘摘摘摘摘摘摘摘摘摘摘摘摘摘摘摘摘摘要" 40 | setIcon(R.drawable.ic_launcher_background) 41 | } 42 | } 43 | } 44 | } -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_launcher_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 10 | 15 | 20 | 25 | 30 | 35 | 40 | 45 | 50 | 55 | 60 | 65 | 70 | 75 | 80 | 85 | 90 | 95 | 100 | 105 | 110 | 115 | 120 | 125 | 130 | 135 | 140 | 145 | 150 | 155 | 160 | 165 | 170 | 171 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_launcher_foreground.xml: -------------------------------------------------------------------------------- 1 | 7 | 8 | 9 | 15 | 18 | 21 | 22 | 23 | 24 | 30 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_main.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | -------------------------------------------------------------------------------- /app/src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /app/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | WineUI 3 | -------------------------------------------------------------------------------- /app/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /build.gradle.kts: -------------------------------------------------------------------------------- 1 | // Top-level build file where you can add configuration options common to all sub-projects/modules. 2 | @Suppress("DSL_SCOPE_VIOLATION") // TODO: Remove once KTIJ-19369 is fixed 3 | plugins { 4 | alias(libs.plugins.androidApplication) apply false 5 | alias(libs.plugins.jetbrainsKotlinAndroid) apply false 6 | alias(libs.plugins.androidLibrary) apply false 7 | } 8 | true // Needed to make the Suppress annotation work for the plugins block -------------------------------------------------------------------------------- /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=-Xmx2048m -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 | # Kotlin code style for this project: "official" or "obsolete": 19 | kotlin.code.style=official 20 | # Enables namespacing of each library's R class so that its R class includes only the 21 | # resources declared in the library itself and none from the library's dependencies, 22 | # thereby reducing the size of the R class for that library 23 | android.nonTransitiveRClass=true -------------------------------------------------------------------------------- /gradle/libs.versions.toml: -------------------------------------------------------------------------------- 1 | [versions] 2 | agp = "8.2.2" 3 | kotlin = "1.9.20" 4 | appcompat = "1.6.1" 5 | material = "1.10.0" 6 | core-ktx = "1.12.0" 7 | 8 | [libraries] 9 | androidx-appcompat = { group = "androidx.appcompat", name = "appcompat", version.ref = "appcompat" } 10 | material = { group = "com.google.android.material", name = "material", version.ref = "material" } 11 | core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "core-ktx" } 12 | 13 | 14 | [plugins] 15 | androidApplication = { id = "com.android.application", version.ref = "agp" } 16 | jetbrainsKotlinAndroid = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" } 17 | androidLibrary = { id = "com.android.library", version.ref = "agp" } 18 | 19 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xiaowine/WineUI/4ee70acaa175d054b404988cdc057de34e522f00/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Wed Nov 15 19:36:09 CST 2023 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.4-bin.zip 5 | zipStoreBase=GRADLE_USER_HOME 6 | zipStorePath=wrapper/dists 7 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | # 4 | # Copyright 2015 the original author or authors. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # https://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | 19 | ############################################################################## 20 | ## 21 | ## Gradle start up script for UN*X 22 | ## 23 | ############################################################################## 24 | 25 | # Attempt to set APP_HOME 26 | # Resolve links: $0 may be a link 27 | PRG="$0" 28 | # Need this for relative symlinks. 29 | while [ -h "$PRG" ] ; do 30 | ls=`ls -ld "$PRG"` 31 | link=`expr "$ls" : '.*-> \(.*\)$'` 32 | if expr "$link" : '/.*' > /dev/null; then 33 | PRG="$link" 34 | else 35 | PRG=`dirname "$PRG"`"/$link" 36 | fi 37 | done 38 | SAVED="`pwd`" 39 | cd "`dirname \"$PRG\"`/" >/dev/null 40 | APP_HOME="`pwd -P`" 41 | cd "$SAVED" >/dev/null 42 | 43 | APP_NAME="Gradle" 44 | APP_BASE_NAME=`basename "$0"` 45 | 46 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 47 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' 48 | 49 | # Use the maximum available, or set MAX_FD != -1 to use that value. 50 | MAX_FD="maximum" 51 | 52 | warn () { 53 | echo "$*" 54 | } 55 | 56 | die () { 57 | echo 58 | echo "$*" 59 | echo 60 | exit 1 61 | } 62 | 63 | # OS specific support (must be 'true' or 'false'). 64 | cygwin=false 65 | msys=false 66 | darwin=false 67 | nonstop=false 68 | case "`uname`" in 69 | CYGWIN* ) 70 | cygwin=true 71 | ;; 72 | Darwin* ) 73 | darwin=true 74 | ;; 75 | MINGW* ) 76 | msys=true 77 | ;; 78 | NONSTOP* ) 79 | nonstop=true 80 | ;; 81 | esac 82 | 83 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 84 | 85 | 86 | # Determine the Java command to use to start the JVM. 87 | if [ -n "$JAVA_HOME" ] ; then 88 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 89 | # IBM's JDK on AIX uses strange locations for the executables 90 | JAVACMD="$JAVA_HOME/jre/sh/java" 91 | else 92 | JAVACMD="$JAVA_HOME/bin/java" 93 | fi 94 | if [ ! -x "$JAVACMD" ] ; then 95 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 96 | 97 | Please set the JAVA_HOME variable in your environment to match the 98 | location of your Java installation." 99 | fi 100 | else 101 | JAVACMD="java" 102 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 103 | 104 | Please set the JAVA_HOME variable in your environment to match the 105 | location of your Java installation." 106 | fi 107 | 108 | # Increase the maximum file descriptors if we can. 109 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then 110 | MAX_FD_LIMIT=`ulimit -H -n` 111 | if [ $? -eq 0 ] ; then 112 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 113 | MAX_FD="$MAX_FD_LIMIT" 114 | fi 115 | ulimit -n $MAX_FD 116 | if [ $? -ne 0 ] ; then 117 | warn "Could not set maximum file descriptor limit: $MAX_FD" 118 | fi 119 | else 120 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 121 | fi 122 | fi 123 | 124 | # For Darwin, add options to specify how the application appears in the dock 125 | if $darwin; then 126 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 127 | fi 128 | 129 | # For Cygwin or MSYS, switch paths to Windows format before running java 130 | if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then 131 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 132 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 133 | 134 | JAVACMD=`cygpath --unix "$JAVACMD"` 135 | 136 | # We build the pattern for arguments to be converted via cygpath 137 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 138 | SEP="" 139 | for dir in $ROOTDIRSRAW ; do 140 | ROOTDIRS="$ROOTDIRS$SEP$dir" 141 | SEP="|" 142 | done 143 | OURCYGPATTERN="(^($ROOTDIRS))" 144 | # Add a user-defined pattern to the cygpath arguments 145 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 146 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 147 | fi 148 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 149 | i=0 150 | for arg in "$@" ; do 151 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 152 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 153 | 154 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 155 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 156 | else 157 | eval `echo args$i`="\"$arg\"" 158 | fi 159 | i=`expr $i + 1` 160 | done 161 | case $i in 162 | 0) set -- ;; 163 | 1) set -- "$args0" ;; 164 | 2) set -- "$args0" "$args1" ;; 165 | 3) set -- "$args0" "$args1" "$args2" ;; 166 | 4) set -- "$args0" "$args1" "$args2" "$args3" ;; 167 | 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 168 | 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 169 | 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 170 | 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 171 | 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 172 | esac 173 | fi 174 | 175 | # Escape application args 176 | save () { 177 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done 178 | echo " " 179 | } 180 | APP_ARGS=`save "$@"` 181 | 182 | # Collect all arguments for the java command, following the shell quoting and substitution rules 183 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" 184 | 185 | exec "$JAVACMD" "$@" 186 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | 17 | @if "%DEBUG%" == "" @echo off 18 | @rem ########################################################################## 19 | @rem 20 | @rem Gradle startup script for Windows 21 | @rem 22 | @rem ########################################################################## 23 | 24 | @rem Set local scope for the variables with windows NT shell 25 | if "%OS%"=="Windows_NT" setlocal 26 | 27 | set DIRNAME=%~dp0 28 | if "%DIRNAME%" == "" set DIRNAME=. 29 | set APP_BASE_NAME=%~n0 30 | set APP_HOME=%DIRNAME% 31 | 32 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 33 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 34 | 35 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 36 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 37 | 38 | @rem Find java.exe 39 | if defined JAVA_HOME goto findJavaFromJavaHome 40 | 41 | set JAVA_EXE=java.exe 42 | %JAVA_EXE% -version >NUL 2>&1 43 | if "%ERRORLEVEL%" == "0" goto execute 44 | 45 | echo. 46 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 47 | echo. 48 | echo Please set the JAVA_HOME variable in your environment to match the 49 | echo location of your Java installation. 50 | 51 | goto fail 52 | 53 | :findJavaFromJavaHome 54 | set JAVA_HOME=%JAVA_HOME:"=% 55 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 56 | 57 | if exist "%JAVA_EXE%" goto execute 58 | 59 | echo. 60 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 61 | echo. 62 | echo Please set the JAVA_HOME variable in your environment to match the 63 | echo location of your Java installation. 64 | 65 | goto fail 66 | 67 | :execute 68 | @rem Setup the command line 69 | 70 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 71 | 72 | 73 | @rem Execute Gradle 74 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* 75 | 76 | :end 77 | @rem End local scope for the variables with windows NT shell 78 | if "%ERRORLEVEL%"=="0" goto mainEnd 79 | 80 | :fail 81 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 82 | rem the _cmd.exe /c_ return code! 83 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 84 | exit /b 1 85 | 86 | :mainEnd 87 | if "%OS%"=="Windows_NT" endlocal 88 | 89 | :omega 90 | -------------------------------------------------------------------------------- /settings.gradle.kts: -------------------------------------------------------------------------------- 1 | pluginManagement { 2 | repositories { 3 | google { 4 | content { 5 | includeGroupByRegex("com\\.android.*") 6 | includeGroupByRegex("com\\.google.*") 7 | includeGroupByRegex("androidx.*") 8 | } 9 | } 10 | mavenCentral() 11 | gradlePluginPortal() 12 | } 13 | } 14 | dependencyResolutionManagement { 15 | repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) 16 | repositories { 17 | google() 18 | mavenCentral() 19 | } 20 | } 21 | 22 | rootProject.name = "WineUI" 23 | include(":app",":ui") 24 | -------------------------------------------------------------------------------- /ui/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /ui/build.gradle.kts: -------------------------------------------------------------------------------- 1 | @Suppress("DSL_SCOPE_VIOLATION") // TODO: Remove once KTIJ-19369 is fixed 2 | plugins { 3 | alias(libs.plugins.androidLibrary) 4 | alias(libs.plugins.jetbrainsKotlinAndroid) 5 | id("maven-publish") 6 | } 7 | 8 | android { 9 | namespace = "cn.xiaowine.ui" 10 | compileSdk = 34 11 | defaultConfig { 12 | minSdk = 21 13 | } 14 | compileOptions { 15 | sourceCompatibility = JavaVersion.VERSION_17 16 | targetCompatibility = JavaVersion.VERSION_17 17 | } 18 | kotlinOptions { 19 | jvmTarget = JavaVersion.VERSION_17.majorVersion 20 | } 21 | buildFeatures { 22 | viewBinding = true 23 | } 24 | } 25 | 26 | dependencies { 27 | implementation(libs.core.ktx) 28 | implementation(libs.androidx.appcompat) 29 | implementation(libs.material) 30 | } 31 | afterEvaluate { 32 | publishing { 33 | publications { 34 | create("release") { 35 | groupId = "cn.xiaowine.ui" 36 | artifactId = "WineUI" 37 | version = "0.0.3" 38 | from(components["release"]) 39 | } 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /ui/consumer-rules.pro: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xiaowine/WineUI/4ee70acaa175d054b404988cdc057de34e522f00/ui/consumer-rules.pro -------------------------------------------------------------------------------- /ui/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 -------------------------------------------------------------------------------- /ui/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /ui/src/main/java/cn/xiaowine/ui/WineActivity.kt: -------------------------------------------------------------------------------- 1 | package cn.xiaowine.ui 2 | 3 | import android.os.Build 4 | import android.os.Bundle 5 | import android.view.MotionEvent 6 | import androidx.activity.OnBackPressedCallback 7 | import androidx.appcompat.app.AppCompatActivity 8 | import androidx.lifecycle.ViewModelProvider 9 | import cn.xiaowine.ui.appcompat.HyperEditText 10 | import cn.xiaowine.ui.data.PageData 11 | import cn.xiaowine.ui.data.TogglePageDate 12 | import cn.xiaowine.ui.databinding.ActivityWineBinding 13 | import cn.xiaowine.ui.tools.ClassScanner.scanPages 14 | import cn.xiaowine.ui.tools.HyperEditTextFocusTools.hideKeyboardAndClearFocus 15 | import cn.xiaowine.ui.tools.HyperEditTextFocusTools.touchIfNeedHideKeyboard 16 | import cn.xiaowine.ui.viewmodel.PageViewModel 17 | 18 | 19 | open class WineActivity : AppCompatActivity() { 20 | private val pageViewModel: PageViewModel by lazy { ViewModelProvider(this)[PageViewModel::class.java] } 21 | private var pageItems: MutableSet 22 | get() = pageViewModel.pageItems.value!! 23 | set(value) { 24 | pageViewModel.pageItems.postValue(value) 25 | } 26 | 27 | var pageQueue: MutableList> 28 | get() = pageViewModel.pageQueue.value!! 29 | set(value) { 30 | pageViewModel.pageQueue.postValue(value) 31 | } 32 | 33 | private var _binding: ActivityWineBinding? = null 34 | private val binding get() = _binding!! 35 | 36 | private var isHeavyLoad: Boolean = false 37 | 38 | override fun onCreate(savedInstanceState: Bundle?) { 39 | super.onCreate(savedInstanceState) 40 | isHeavyLoad = savedInstanceState != null 41 | _binding = ActivityWineBinding.inflate(layoutInflater) 42 | setContentView(binding.root) 43 | pageViewModel.nowPage.observe(this) { 44 | if (it.now == null) { 45 | pageQueue.remove(it.last) 46 | if (pageQueue.isEmpty()) { 47 | finish() 48 | return@observe 49 | } 50 | performFragmentTransaction(pageQueue.last(), true) 51 | } else { 52 | if (pageItems.none { item -> item.page == it.now }) { 53 | error("Page not found") 54 | } 55 | if (!isHeavyLoad) { 56 | pageQueue.add(it.now) 57 | } 58 | performFragmentTransaction(it.now, false) 59 | } 60 | 61 | } 62 | setupOnBackPressedCallback() 63 | } 64 | 65 | override fun dispatchTouchEvent(ev: MotionEvent): Boolean { 66 | return this.touchIfNeedHideKeyboard(currentFocus, ev, false) { 67 | super.dispatchTouchEvent(ev) 68 | } 69 | } 70 | 71 | override fun onPause() { 72 | super.onPause() 73 | val view = currentFocus 74 | if (view is HyperEditText) { 75 | hideKeyboardAndClearFocus(this, view) 76 | } 77 | } 78 | 79 | private fun setupOnBackPressedCallback() { 80 | val callback = object : OnBackPressedCallback(true) { 81 | override fun handleOnBackPressed() { 82 | if (pageQueue.size == 1) { 83 | finish() 84 | return 85 | } 86 | pageViewModel.nowPage.postValue(TogglePageDate(null, pageQueue.last())) 87 | } 88 | } 89 | onBackPressedDispatcher.addCallback(this, callback) 90 | } 91 | 92 | fun registerPage(vararg allPageData: PageData) { 93 | allPageData.forEach { newData -> 94 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { 95 | pageItems.removeIf { it.page == newData.page } 96 | } else { 97 | val iterator = pageItems.iterator() 98 | while (iterator.hasNext()) { 99 | if (iterator.next().page == newData.page) { 100 | iterator.remove() 101 | } 102 | } 103 | } 104 | } 105 | pageItems.addAll(allPageData) 106 | if (pageViewModel.nowPage.value == null) { 107 | val home = pageItems.singleOrNull { it.isHome } ?: error("No Found single home page") 108 | pageViewModel.nowPage.postValue(TogglePageDate(home.page, null)) 109 | } 110 | } 111 | 112 | @Suppress("UNCHECKED_CAST") 113 | fun registerPage(packages: String, homePage: Class) { 114 | val scanPages = scanPages(this, packages) 115 | scanPages.forEach { 116 | if (it != homePage && pageItems.none { item -> item.page == it }) { 117 | pageItems.add(PageData(it as Class)) 118 | } else if (it == homePage && pageItems.none { item -> item.page == it && item.isHome }) { 119 | pageItems.add(PageData(it, isHome = true)) 120 | if (pageViewModel.nowPage.value == null) { 121 | pageViewModel.nowPage.postValue(TogglePageDate(it, null)) 122 | } 123 | } 124 | } 125 | } 126 | 127 | private fun performFragmentTransaction(page: Class, isExit: Boolean) { 128 | supportFragmentManager.beginTransaction().apply { 129 | if (isExit) { 130 | setCustomAnimations(R.animator.slide_left_in, R.animator.slide_right_out, R.animator.slide_right_in, R.animator.slide_left_out) 131 | } else { 132 | setCustomAnimations(R.animator.slide_right_in, R.animator.slide_left_out, R.animator.slide_left_in, R.animator.slide_right_out) 133 | } 134 | }.replace(R.id.page, page, null).commitNow() 135 | } 136 | 137 | override fun onDestroy() { 138 | super.onDestroy() 139 | _binding = null 140 | } 141 | } -------------------------------------------------------------------------------- /ui/src/main/java/cn/xiaowine/ui/WinePage.kt: -------------------------------------------------------------------------------- 1 | package cn.xiaowine.ui 2 | 3 | import android.content.Context 4 | import android.graphics.Typeface 5 | import android.os.Build 6 | import android.os.Bundle 7 | import android.view.Gravity 8 | import android.view.LayoutInflater 9 | import android.view.View 10 | import android.view.ViewGroup 11 | import android.widget.TextView 12 | import androidx.fragment.app.Fragment 13 | import androidx.lifecycle.ViewModelProvider 14 | import cn.xiaowine.ui.annotation.Coroutine 15 | import cn.xiaowine.ui.build.PageBuild 16 | import cn.xiaowine.ui.data.PageData 17 | import cn.xiaowine.ui.data.TogglePageDate 18 | import cn.xiaowine.ui.databinding.FragmentPageBinding 19 | import cn.xiaowine.ui.tools.Tools.dp 20 | import cn.xiaowine.ui.viewmodel.PageViewModel 21 | import cn.xiaowine.ui.widget.WineCard 22 | import kotlinx.coroutines.CoroutineScope 23 | import kotlinx.coroutines.Dispatchers 24 | import kotlinx.coroutines.launch 25 | 26 | open class WinePage : Fragment() { 27 | private val pageViewModel: PageViewModel by lazy { ViewModelProvider(requireActivity())[PageViewModel::class.java] } 28 | 29 | private var viewList = ArrayList, View.() -> Unit>>() 30 | 31 | private var _binding: FragmentPageBinding? = null 32 | private val binding get() = _binding!! 33 | var pageItems: MutableSet 34 | get() = pageViewModel.pageItems.value!! 35 | set(value) { 36 | pageViewModel.pageItems.postValue(value) 37 | } 38 | 39 | var pageQueue: MutableList> 40 | get() = pageViewModel.pageQueue.value!! 41 | set(value) { 42 | pageViewModel.pageQueue.postValue(value) 43 | } 44 | 45 | override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { 46 | _binding = FragmentPageBinding.inflate(inflater, container, false) 47 | return binding.root 48 | } 49 | 50 | override fun onViewCreated(view: View, savedInstanceState: Bundle?) { 51 | super.onViewCreated(view, savedInstanceState) 52 | init() 53 | } 54 | 55 | fun init() { 56 | if (this::class.java.getAnnotation(Coroutine::class.java) != null) { 57 | CoroutineScope(Dispatchers.Main).launch { 58 | addView2Root() 59 | } 60 | } else { 61 | addView2Root() 62 | } 63 | } 64 | 65 | // override fun onDestroy() { 66 | // Log.e("WinePage", "onDestroy") 67 | // super.onDestroy() 68 | // } 69 | 70 | private fun addView2Root() { 71 | binding.apply { 72 | toolbar.title = this@WinePage::class.java.simpleName 73 | fragmentContainer.apply { 74 | viewList.forEach { 75 | if (!isAdded) return 76 | val view = it.first.getDeclaredConstructor(Context::class.java).newInstance(requireContext()) 77 | addView(view.apply { 78 | it.second.invoke(this) 79 | if (this::class.java.name != WineCard::class.java.name) { 80 | setPadding(28.dp, 0, 28.dp, 0) 81 | } 82 | findViewById(R.id.summary_view)?.let { summaryView -> 83 | if (summaryView.text.isEmpty()) { 84 | summaryView.visibility = View.GONE 85 | } 86 | } 87 | }) 88 | } 89 | setPadding(0, 0, 0, 30.dp) 90 | } 91 | collapsingToolbarLayout.apply { 92 | expandedTitleTextSize = 30.dp.toFloat() 93 | collapsedTitleTextSize = 20.dp.toFloat() 94 | setCollapsedTitleTypeface( 95 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { 96 | Typeface.create(null, 400, false) 97 | } else { 98 | Typeface.defaultFromStyle(Typeface.NORMAL) 99 | } 100 | ) 101 | setExpandedTitleTypeface( 102 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { 103 | Typeface.create(null, 300, false) 104 | } else { 105 | Typeface.defaultFromStyle(Typeface.NORMAL) 106 | } 107 | ) 108 | } 109 | 110 | val findPage = pageItems.find { it.page == pageQueue.last() } ?: return 111 | binding.apply { 112 | collapsingToolbarLayout.apply { 113 | collapsedTitleGravity = if (findPage.isHome) Gravity.CENTER else Gravity.START 114 | title = findPage.title ?: getString(findPage.titleRes) 115 | } 116 | if (!findPage.isHome) { 117 | toolbar.apply { 118 | setNavigationIcon(androidx.appcompat.R.drawable.abc_ic_ab_back_material) 119 | setNavigationOnClickListener { 120 | backPage() 121 | } 122 | } 123 | } 124 | } 125 | } 126 | } 127 | 128 | 129 | fun initPage(init: PageBuild.() -> Unit) { 130 | val list = PageBuild().apply(init).viewList 131 | viewList.addAll(list) 132 | } 133 | 134 | fun toPage(page: Class) { 135 | pageViewModel.nowPage.postValue(TogglePageDate(page, null)) 136 | } 137 | 138 | fun backPage() { 139 | requireActivity().onBackPressedDispatcher.onBackPressed() 140 | } 141 | 142 | fun reloadPage() { 143 | binding.fragmentContainer.removeAllViews() 144 | init() 145 | } 146 | } -------------------------------------------------------------------------------- /ui/src/main/java/cn/xiaowine/ui/annotation/Coroutine.kt: -------------------------------------------------------------------------------- 1 | package cn.xiaowine.ui.annotation 2 | 3 | annotation class Coroutine() 4 | -------------------------------------------------------------------------------- /ui/src/main/java/cn/xiaowine/ui/appcompat/HyperButton.kt: -------------------------------------------------------------------------------- 1 | package cn.xiaowine.ui.appcompat 2 | 3 | import android.content.Context 4 | import android.graphics.Typeface 5 | import android.graphics.drawable.StateListDrawable 6 | import android.util.AttributeSet 7 | import android.util.TypedValue 8 | import android.view.Gravity 9 | import android.widget.LinearLayout 10 | import androidx.appcompat.widget.AppCompatButton 11 | import androidx.core.content.ContextCompat 12 | import cn.xiaowine.ui.R 13 | import cn.xiaowine.ui.tools.DrawableTools.createRoundShape 14 | import cn.xiaowine.ui.tools.Tools.dp 15 | 16 | class HyperButton(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : AppCompatButton(context, attrs, defStyleAttr) { 17 | constructor(context: Context, attrs: AttributeSet?) : this(context, attrs, androidx.appcompat.R.attr.buttonStyle) 18 | constructor(context: Context) : this(context, null) 19 | 20 | private var isCancel: Boolean = false 21 | 22 | init { 23 | gravity = Gravity.CENTER 24 | typeface = Typeface.defaultFromStyle(Typeface.BOLD) 25 | setTextColor(ContextCompat.getColor(context, R.color.button_text_color)) 26 | setTextSize(TypedValue.COMPLEX_UNIT_SP, 17.5f) 27 | stateListAnimator = null 28 | setDefaultBackground() 29 | setPadding(0, 0, 0, 0) 30 | layoutParams = LinearLayout.LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT, 51.dp, 1f) 31 | } 32 | 33 | private fun setDefaultBackground() { 34 | val defaultDrawable = createRoundShape(18f.dp, ContextCompat.getColor(context, R.color.button_default_color)) 35 | val pressedDrawable = createRoundShape(18f.dp, ContextCompat.getColor(context, R.color.button_pressed_color)) 36 | val disabledDrawable = createRoundShape(18f.dp, ContextCompat.getColor(context, R.color.button_disable_color)) 37 | background = StateListDrawable().apply { 38 | addState(intArrayOf(-android.R.attr.state_enabled), disabledDrawable) 39 | addState(intArrayOf(android.R.attr.state_pressed), pressedDrawable) 40 | addState(intArrayOf(), defaultDrawable) 41 | } 42 | } 43 | 44 | override fun setEnabled(enabled: Boolean) { 45 | super.setEnabled(enabled) 46 | if (enabled) { 47 | setTextColor(ContextCompat.getColor(context, R.color.button_text_color)) 48 | } else { 49 | setTextColor(ContextCompat.getColor(context, R.color.button_disable_text_color)) 50 | } 51 | } 52 | 53 | fun setCancel(cancel: Boolean) { 54 | isCancel = cancel 55 | if (cancel) { 56 | setTextColor(ContextCompat.getColor(context, R.color.button_cancel_text_color)) 57 | background = StateListDrawable().apply { 58 | addState(intArrayOf(-android.R.attr.state_enabled), createRoundShape(18f.dp, ContextCompat.getColor(context, R.color.button_cancel_disable_color))) 59 | addState(intArrayOf(android.R.attr.state_pressed), createRoundShape(18f.dp, ContextCompat.getColor(context, R.color.button_cancel_pressed_color))) 60 | addState(intArrayOf(), createRoundShape(18f.dp, ContextCompat.getColor(context, R.color.button_cancel_color))) 61 | } 62 | } else { 63 | setTextColor(ContextCompat.getColor(context, R.color.button_text_color)) 64 | setDefaultBackground() 65 | } 66 | } 67 | 68 | fun getCancel(): Boolean { 69 | return isCancel 70 | } 71 | } -------------------------------------------------------------------------------- /ui/src/main/java/cn/xiaowine/ui/appcompat/HyperEditText.kt: -------------------------------------------------------------------------------- 1 | package cn.xiaowine.ui.appcompat 2 | 3 | import android.animation.ValueAnimator 4 | import android.content.Context 5 | import android.graphics.drawable.GradientDrawable 6 | import android.os.Build 7 | import android.util.AttributeSet 8 | import android.util.TypedValue 9 | import android.view.Gravity 10 | import android.view.inputmethod.EditorInfo 11 | import android.widget.LinearLayout 12 | import androidx.appcompat.widget.AppCompatEditText 13 | import androidx.core.content.ContextCompat 14 | import cn.xiaowine.ui.R 15 | import cn.xiaowine.ui.data.HyperEditBackgroundStyle 16 | import cn.xiaowine.ui.tools.HyperEditTextFocusTools.hideKeyboardAndClearFocus 17 | import cn.xiaowine.ui.tools.Tools.dp 18 | 19 | class HyperEditText(context: Context, attrs: AttributeSet? = null, defStyleAttr: Int) : AppCompatEditText(context, attrs, defStyleAttr) { 20 | constructor(context: Context, attrs: AttributeSet?) : this(context, attrs, androidx.appcompat.R.attr.editTextStyle) 21 | constructor(context: Context) : this(context, null) 22 | 23 | private var isCustomBackgroundStyle: Boolean = false 24 | 25 | private var animator: ValueAnimator? = null 26 | 27 | private val mHeight = 55.dp 28 | 29 | init { 30 | minHeight = mHeight 31 | layoutParams = LinearLayout.LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.WRAP_CONTENT).apply { 32 | setMargins(30.dp, 0, 30.dp, 0) 33 | } 34 | setTextSize(TypedValue.COMPLEX_UNIT_SP, 20f) 35 | setTextColor(ContextCompat.getColor(context, R.color.title_text_color)) 36 | gravity = Gravity.CENTER_VERTICAL 37 | background = createBackgroundDrawable(false) 38 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { 39 | val drawable = GradientDrawable().apply { 40 | shape = GradientDrawable.RECTANGLE 41 | setSize(2.dp, 2.dp) 42 | setColor(ContextCompat.getColor(context, R.color.edittext_background_stroke)) 43 | } 44 | setTextCursorDrawable(drawable) 45 | } 46 | imeOptions = EditorInfo.IME_ACTION_DONE 47 | setPadding(20.dp, 15.dp, 20.dp, 15.dp) 48 | setHintTextColor(ContextCompat.getColor(context, R.color.edittext_hit_color)) 49 | setOnFocusChangeListener { _, hasFocus -> 50 | if (!hasFocus) { 51 | hideKeyboardAndClearFocus(context, this) 52 | 53 | } 54 | if (!isCustomBackgroundStyle) { 55 | background = createBackgroundDrawable(hasFocus) 56 | } 57 | } 58 | } 59 | 60 | fun setBackgroundStyle(backgroundStyle: HyperEditBackgroundStyle?) { 61 | isCustomBackgroundStyle = backgroundStyle != null 62 | background = when (backgroundStyle) { 63 | HyperEditBackgroundStyle.NORMAL -> { 64 | createBackgroundDrawable(false) 65 | } 66 | 67 | HyperEditBackgroundStyle.STROKE -> { 68 | createBackgroundDrawable(true) 69 | } 70 | 71 | null -> { 72 | createBackgroundDrawable(false) 73 | } 74 | } 75 | } 76 | 77 | private fun createBackgroundDrawable(hasFocus: Boolean): GradientDrawable { 78 | return GradientDrawable().apply { 79 | shape = GradientDrawable.RECTANGLE 80 | cornerRadius = 15f.dp 81 | if (hasFocus) { 82 | animateStrokeWidth(0.dp, 2.dp) 83 | } 84 | setColor(ContextCompat.getColor(context, R.color.dialog_edit_background_color)) 85 | } 86 | } 87 | 88 | private fun animateStrokeWidth(from: Int, to: Int) { 89 | animator = ValueAnimator.ofInt(from, to) 90 | animator?.duration = 300 91 | animator?.addUpdateListener { animation -> 92 | val strokeWidth = animation.animatedValue as Int 93 | (background as? GradientDrawable)?.setStroke(strokeWidth, ContextCompat.getColor(context, R.color.edittext_background_stroke)) 94 | } 95 | animator?.start() 96 | } 97 | 98 | fun cancelAnimation() { 99 | animator?.cancel() 100 | } 101 | 102 | override fun setSingleLine(singleLine: Boolean) { 103 | super.setSingleLine(singleLine) 104 | (layoutParams as LinearLayout.LayoutParams).apply { 105 | height = if (singleLine) { 106 | mHeight 107 | } else { 108 | LinearLayout.LayoutParams.WRAP_CONTENT 109 | } 110 | } 111 | } 112 | } -------------------------------------------------------------------------------- /ui/src/main/java/cn/xiaowine/ui/appcompat/HyperPopup.kt: -------------------------------------------------------------------------------- 1 | package cn.xiaowine.ui.appcompat 2 | 3 | import android.content.Context 4 | import android.graphics.drawable.GradientDrawable 5 | import android.util.AttributeSet 6 | import android.view.Gravity 7 | import android.view.ViewGroup 8 | import androidx.appcompat.widget.ListPopupWindow 9 | import androidx.core.content.ContextCompat 10 | import cn.xiaowine.ui.R 11 | import cn.xiaowine.ui.tools.Tools.dp 12 | 13 | class HyperPopup(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : ListPopupWindow(context, attrs, defStyleAttr) { 14 | constructor(context: Context, attrs: AttributeSet?) : this(context, attrs, androidx.appcompat.R.attr.listPopupWindowStyle) 15 | constructor(context: Context) : this(context, null) 16 | 17 | init { 18 | val drawable = GradientDrawable().apply { 19 | setColor(ContextCompat.getColor(context, R.color.popup_background_color)) 20 | cornerRadius = 20.dp.toFloat() 21 | } 22 | setBackgroundDrawable(drawable) 23 | width = 150.dp 24 | height = ViewGroup.LayoutParams.WRAP_CONTENT 25 | isModal = true 26 | animationStyle = R.style.Theme_WinePopupAnim 27 | 28 | } 29 | 30 | 31 | override fun show() { 32 | horizontalOffset = (-24).dp 33 | setDropDownGravity(Gravity.RIGHT) 34 | super.show() 35 | listView?.apply { 36 | isScrollbarFadingEnabled = false 37 | isVerticalScrollBarEnabled = false 38 | isHorizontalScrollBarEnabled = false 39 | } 40 | } 41 | } -------------------------------------------------------------------------------- /ui/src/main/java/cn/xiaowine/ui/appcompat/HyperSeekBar.kt: -------------------------------------------------------------------------------- 1 | package cn.xiaowine.ui.appcompat 2 | 3 | import android.annotation.SuppressLint 4 | import android.content.Context 5 | import android.graphics.drawable.ClipDrawable 6 | import android.graphics.drawable.GradientDrawable 7 | import android.graphics.drawable.LayerDrawable 8 | import android.os.Build 9 | import android.os.Handler 10 | import android.os.Looper 11 | import android.os.Message 12 | import android.util.AttributeSet 13 | import android.view.Gravity 14 | import android.view.MotionEvent 15 | import android.view.ViewGroup 16 | import android.widget.LinearLayout 17 | import android.widget.SeekBar 18 | import androidx.appcompat.widget.AppCompatSeekBar 19 | import androidx.core.content.ContextCompat 20 | import cn.xiaowine.ui.R 21 | import cn.xiaowine.ui.tools.Tools.dp 22 | import kotlin.math.abs 23 | 24 | class HyperSeekBar(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : AppCompatSeekBar(context, attrs, defStyleAttr) { 25 | constructor(context: Context, attrs: AttributeSet?) : this(context, attrs, androidx.appcompat.R.attr.seekBarStyle) 26 | constructor(context: Context) : this(context, null) 27 | 28 | private var downTime: Long = 0 29 | private var longClickListener: LongClickListener? = null 30 | private var lastY = 0f 31 | private var lastX = 0f 32 | private var hasLongTouch = false 33 | private var possibleLongTouch = true 34 | private val delayMillis = 400L 35 | 36 | private val handler = object : Handler(Looper.getMainLooper()) { 37 | override fun handleMessage(msg: Message) { 38 | if (msg.what == 1 && !hasLongTouch) { 39 | hasLongTouch = true 40 | longClickListener?.onLongClick(this@HyperSeekBar, progress) 41 | } 42 | } 43 | } 44 | 45 | 46 | init { 47 | layoutParams = LinearLayout.LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.WRAP_CONTENT) 48 | val backgroundDrawable = GradientDrawable().apply { 49 | shape = GradientDrawable.RECTANGLE 50 | cornerRadius = 50f 51 | orientation = GradientDrawable.Orientation.TOP_BOTTOM 52 | setColor(ContextCompat.getColor(context, R.color.seekbar_bg_color)) 53 | } 54 | val progressDrawable = GradientDrawable().apply { 55 | shape = GradientDrawable.RECTANGLE 56 | cornerRadius = 50f 57 | setColor(ContextCompat.getColor(context, R.color.seekbar_color)) 58 | } 59 | val clipDrawable = ClipDrawable(progressDrawable, Gravity.START, ClipDrawable.HORIZONTAL) 60 | this.progressDrawable = LayerDrawable(arrayOf(backgroundDrawable, clipDrawable)).apply { 61 | setId(0, android.R.id.background) 62 | setId(1, android.R.id.progress) 63 | } 64 | indeterminateDrawable = ContextCompat.getDrawable(context, R.color.seekbar_color) 65 | thumb = null 66 | background = null 67 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { 68 | maxHeight = 31.dp 69 | minHeight = 31.dp 70 | } else { 71 | layoutParams = ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, 31.dp) 72 | } 73 | setPadding(0, 0, 0, 0) 74 | } 75 | 76 | @SuppressLint("ClickableViewAccessibility") 77 | override fun onTouchEvent(event: MotionEvent): Boolean { 78 | val y = event.y 79 | val x = event.x 80 | when (event.action) { 81 | MotionEvent.ACTION_DOWN -> { 82 | hasLongTouch = false 83 | possibleLongTouch = true 84 | lastY = y 85 | lastX = x 86 | downTime = System.currentTimeMillis() 87 | if (!handler.hasMessages(1)) { 88 | handler.sendEmptyMessageDelayed(1, delayMillis) 89 | } 90 | return super.onTouchEvent(event) 91 | } 92 | 93 | MotionEvent.ACTION_MOVE -> { 94 | if (lastX != 0f && lastY != 0f && (abs(y - lastY) > 5 || abs(x - lastX) > 5)) { 95 | possibleLongTouch = false 96 | handler.removeMessages(1) 97 | return super.onTouchEvent(event) 98 | } 99 | lastY = y 100 | lastX = x 101 | } 102 | 103 | MotionEvent.ACTION_UP -> { 104 | lastX = 0f 105 | lastY = 0f 106 | handler.removeMessages(1) 107 | if (System.currentTimeMillis() - downTime > delayMillis && possibleLongTouch) { 108 | if (!hasLongTouch) { 109 | hasLongTouch = true 110 | longClickListener?.onLongClick(this, progress) 111 | } 112 | return true 113 | } 114 | return super.onTouchEvent(event) 115 | } 116 | 117 | MotionEvent.ACTION_CANCEL -> { 118 | handler.removeMessages(1) 119 | lastX = 0f 120 | lastY = 0f 121 | return super.onTouchEvent(event) 122 | } 123 | } 124 | return false 125 | } 126 | 127 | fun setLongClickListener(longClickListener: LongClickListener?) { 128 | this.longClickListener = longClickListener 129 | } 130 | 131 | interface LongClickListener { 132 | fun onLongClick(seekBar: SeekBar, progress: Int) 133 | } 134 | } -------------------------------------------------------------------------------- /ui/src/main/java/cn/xiaowine/ui/appcompat/HyperSwitch.kt: -------------------------------------------------------------------------------- 1 | package cn.xiaowine.ui.appcompat 2 | 3 | import android.annotation.SuppressLint 4 | import android.content.Context 5 | import android.graphics.Color 6 | import android.graphics.drawable.GradientDrawable 7 | import android.graphics.drawable.StateListDrawable 8 | import android.util.AttributeSet 9 | import androidx.appcompat.widget.SwitchCompat 10 | import androidx.core.content.ContextCompat 11 | import cn.xiaowine.ui.R 12 | import cn.xiaowine.ui.tools.Tools.dp 13 | import java.lang.reflect.Field 14 | 15 | class HyperSwitch(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : SwitchCompat(context, attrs, defStyleAttr) { 16 | constructor(context: Context, attrs: AttributeSet?) : this(context, attrs, androidx.appcompat.R.attr.switchStyle) 17 | constructor(context: Context) : this(context, null) 18 | 19 | 20 | init { 21 | background = null 22 | val drawable = GradientDrawable().apply { 23 | shape = GradientDrawable.RECTANGLE 24 | cornerRadius = 25f * resources.displayMetrics.density 25 | setSize(32.dp, 32.dp) 26 | setColor(Color.WHITE) 27 | setStroke(10.dp, Color.TRANSPARENT) 28 | } 29 | setThumbDrawable(drawable) 30 | val switchOnDrawable = GradientDrawable().apply { 31 | shape = GradientDrawable.RECTANGLE 32 | cornerRadius = 25.dp.toFloat() 33 | setSize(26.dp, 22.dp) 34 | setColor(ContextCompat.getColor(context, R.color.switch_on_color)) 35 | } 36 | 37 | val switchOffDrawable = GradientDrawable().apply { 38 | shape = GradientDrawable.RECTANGLE 39 | cornerRadius = 25f.dp 40 | setSize(26.dp, 22.dp) 41 | setColor(ContextCompat.getColor(context, R.color.color_track_color)) 42 | } 43 | 44 | val stateListDrawable = StateListDrawable().apply { 45 | addState(intArrayOf(android.R.attr.state_checked), switchOnDrawable) 46 | addState(intArrayOf(-android.R.attr.state_checked), switchOffDrawable) 47 | } 48 | 49 | trackDrawable = stateListDrawable 50 | showText = false 51 | } 52 | 53 | @SuppressLint("DiscouragedPrivateApi", "DrawAllocation") 54 | override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) { 55 | super.onMeasure(widthMeasureSpec, heightMeasureSpec) 56 | val mSwitchWidth: Field = SwitchCompat::class.java.getDeclaredField("mSwitchWidth") 57 | mSwitchWidth.isAccessible = true 58 | mSwitchWidth.setInt(this, 56.dp) 59 | } 60 | 61 | } -------------------------------------------------------------------------------- /ui/src/main/java/cn/xiaowine/ui/appcompat/WineRoundImageView.kt: -------------------------------------------------------------------------------- 1 | package cn.xiaowine.ui.appcompat 2 | 3 | import android.content.Context 4 | import android.graphics.Bitmap 5 | import android.graphics.Canvas 6 | import android.graphics.Color 7 | import android.graphics.Paint 8 | import android.graphics.Path 9 | import android.graphics.PorterDuff 10 | import android.graphics.PorterDuffXfermode 11 | import android.graphics.RectF 12 | import android.util.AttributeSet 13 | import androidx.appcompat.widget.AppCompatImageView 14 | 15 | class WineRoundImageView(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : AppCompatImageView(context, attrs, defStyleAttr) { 16 | constructor(context: Context, attrs: AttributeSet?) : this(context, attrs, 0) 17 | constructor(context: Context) : this(context, null) 18 | 19 | private var paint: Paint? = null 20 | private var paint2: Paint? = null 21 | val roundWidth: Int = 25 22 | val roundHeight: Int = 25 23 | 24 | init { 25 | paint = Paint() 26 | paint!!.color = Color.WHITE 27 | paint!!.isAntiAlias = true 28 | paint!!.xfermode = PorterDuffXfermode(PorterDuff.Mode.DST_OUT) 29 | paint2 = Paint() 30 | paint2!!.xfermode = null 31 | } 32 | 33 | override fun draw(canvas: Canvas) { 34 | val bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888) 35 | val canvas2 = Canvas(bitmap) 36 | super.draw(canvas2) 37 | drawLiftUp(canvas2) 38 | drawRightUp(canvas2) 39 | drawLiftDown(canvas2) 40 | drawRightDown(canvas2) 41 | canvas.drawBitmap(bitmap, 0f, 0f, paint2) 42 | bitmap.recycle() 43 | } 44 | 45 | private fun drawLiftUp(canvas: Canvas) { 46 | canvas.drawPath(Path().apply { 47 | moveTo(0f, roundHeight.toFloat()) 48 | lineTo(0f, 0f) 49 | lineTo(roundWidth.toFloat(), 0f) 50 | arcTo( 51 | RectF( 52 | 0f, 0f, (roundWidth * 2).toFloat(), (roundHeight * 2).toFloat() 53 | ), -90f, -90f 54 | ) 55 | close() 56 | }, paint!!) 57 | } 58 | 59 | private fun drawLiftDown(canvas: Canvas) { 60 | canvas.drawPath(Path().apply { 61 | moveTo(0f, (height - roundHeight).toFloat()) 62 | lineTo(0f, height.toFloat()) 63 | lineTo(roundWidth.toFloat(), height.toFloat()) 64 | arcTo( 65 | RectF( 66 | 0f, (height - roundHeight * 2).toFloat(), (0 + roundWidth * 2).toFloat(), height.toFloat() 67 | ), 90f, 90f 68 | ) 69 | close() 70 | }, paint!!) 71 | } 72 | 73 | private fun drawRightDown(canvas: Canvas) { 74 | canvas.drawPath(Path().apply { 75 | moveTo((width - roundWidth).toFloat(), height.toFloat()) 76 | lineTo(width.toFloat(), height.toFloat()) 77 | lineTo(width.toFloat(), (height - roundHeight).toFloat()) 78 | arcTo( 79 | RectF( 80 | (width - roundWidth * 2).toFloat(), (height - roundHeight * 2).toFloat(), width.toFloat(), height.toFloat() 81 | ), 0f, 90f 82 | ) 83 | close() 84 | }, paint!!) 85 | } 86 | 87 | private fun drawRightUp(canvas: Canvas) { 88 | canvas.drawPath(Path().apply { 89 | moveTo(width.toFloat(), roundHeight.toFloat()) 90 | lineTo(width.toFloat(), 0f) 91 | lineTo((width - roundWidth).toFloat(), 0f) 92 | arcTo( 93 | RectF( 94 | (width - roundWidth * 2).toFloat(), 0f, width.toFloat(), (0 + roundHeight * 2).toFloat() 95 | ), -90f, 90f 96 | ) 97 | close() 98 | }, paint!!) 99 | } 100 | } -------------------------------------------------------------------------------- /ui/src/main/java/cn/xiaowine/ui/build/CardViewBuild.kt: -------------------------------------------------------------------------------- 1 | package cn.xiaowine.ui.build 2 | 3 | import android.view.View 4 | import cn.xiaowine.ui.widget.WineCardLink 5 | import cn.xiaowine.ui.widget.WineCardTitle 6 | import cn.xiaowine.ui.widget.WineLine 7 | 8 | class CardViewBuild { 9 | val viewList = ArrayList, View.() -> Unit>>() 10 | fun title(init: WineCardTitle.() -> Unit) { 11 | viewList.add(Pair(WineCardTitle::class.java) { init.invoke(this as WineCardTitle) }) 12 | } 13 | 14 | fun link(init: WineCardLink.() -> Unit) { 15 | viewList.add(Pair(WineCardLink::class.java) { init.invoke(this as WineCardLink) }) 16 | } 17 | 18 | // fun line(init: (WineLine.() -> Unit)? = null) { 19 | // viewList.add(Pair(WineLine::class.java) { init?.invoke(this as WineLine) }) 20 | // } 21 | 22 | } -------------------------------------------------------------------------------- /ui/src/main/java/cn/xiaowine/ui/build/PageBuild.kt: -------------------------------------------------------------------------------- 1 | package cn.xiaowine.ui.build 2 | 3 | import android.view.View 4 | import cn.xiaowine.ui.WinePage 5 | import cn.xiaowine.ui.appcompat.HyperButton 6 | import cn.xiaowine.ui.appcompat.HyperEditText 7 | import cn.xiaowine.ui.widget.WineCard 8 | import cn.xiaowine.ui.widget.WineLine 9 | import cn.xiaowine.ui.widget.WineSeekBar 10 | import cn.xiaowine.ui.widget.WineSpinner 11 | import cn.xiaowine.ui.widget.WineSwitch 12 | import cn.xiaowine.ui.widget.WineText 13 | import cn.xiaowine.ui.widget.WineTitle 14 | 15 | class PageBuild { 16 | val viewList = ArrayList, View.() -> Unit>>() 17 | 18 | fun WinePage.toPageText(text: String? = null, page: Class) { 19 | text { 20 | title = text ?: "to ${page.simpleName}" 21 | onClick { 22 | toPage(page) 23 | } 24 | } 25 | } 26 | 27 | fun WinePage.toPageText() { 28 | text { 29 | title = "backPage" 30 | onClick { 31 | backPage() 32 | } 33 | } 34 | line() 35 | } 36 | 37 | fun text(init: WineText.() -> Unit) { 38 | viewList.add(Pair(WineText::class.java) { init.invoke(this as WineText) }) 39 | } 40 | 41 | fun line(init: (WineLine.() -> Unit)? = null) { 42 | viewList.add(Pair(WineLine::class.java) { init?.invoke(this as WineLine) }) 43 | } 44 | 45 | fun switch(init: WineSwitch.() -> Unit) { 46 | viewList.add(Pair(WineSwitch::class.java) { init.invoke(this as WineSwitch) }) 47 | } 48 | 49 | fun title(init: WineTitle.() -> Unit) { 50 | viewList.add(Pair(WineTitle::class.java) { init.invoke(this as WineTitle) }) 51 | } 52 | 53 | fun card(init: WineCard.() -> Unit) { 54 | viewList.add(Pair(WineCard::class.java) { init.invoke(this as WineCard) }) 55 | } 56 | 57 | fun seekbar(init: WineSeekBar.() -> Unit) { 58 | viewList.add(Pair(WineSeekBar::class.java) { init.invoke(this as WineSeekBar) }) 59 | } 60 | 61 | fun button(init: HyperButton.() -> Unit) { 62 | viewList.add(Pair(HyperButton::class.java) { init.invoke(this as HyperButton) }) 63 | } 64 | 65 | fun edittext(init: HyperEditText.() -> Unit) { 66 | viewList.add(Pair(HyperEditText::class.java) { init.invoke(this as HyperEditText) }) 67 | } 68 | 69 | fun spinner(init: WineSpinner.() -> Unit) { 70 | viewList.add(Pair(WineSpinner::class.java) { init.invoke(this as WineSpinner) }) 71 | } 72 | 73 | // 74 | // fun custom(view: View, init: T.() -> Unit) { 75 | // viewList.add(Pair(view::class.java) { init.invoke(this as T) }) 76 | // } 77 | fun custom(customView: Class, init: (View) -> Unit) { 78 | viewList.add(Pair(customView) { init.invoke(this) }) 79 | } 80 | } -------------------------------------------------------------------------------- /ui/src/main/java/cn/xiaowine/ui/data/BackgroundStyle.kt: -------------------------------------------------------------------------------- 1 | package cn.xiaowine.ui.data 2 | 3 | enum class HyperEditBackgroundStyle { 4 | NORMAL, STROKE 5 | } -------------------------------------------------------------------------------- /ui/src/main/java/cn/xiaowine/ui/data/PageData.kt: -------------------------------------------------------------------------------- 1 | package cn.xiaowine.ui.data 2 | 3 | import androidx.annotation.StringRes 4 | import cn.xiaowine.ui.WinePage 5 | 6 | data class PageData( 7 | var page: Class, 8 | var title: String? = null, 9 | @StringRes 10 | var titleRes: Int = -1, 11 | var isHome: Boolean = false, 12 | var showMenu: Boolean = false, 13 | var useCatch: Boolean = false 14 | ) { 15 | init { 16 | this.title = title ?: page.simpleName 17 | } 18 | } -------------------------------------------------------------------------------- /ui/src/main/java/cn/xiaowine/ui/data/SpinnerData.kt: -------------------------------------------------------------------------------- 1 | package cn.xiaowine.ui.data 2 | 3 | data class SpinnerData( 4 | val text: String, 5 | val click: ((Int) -> Unit)? = null 6 | ) 7 | -------------------------------------------------------------------------------- /ui/src/main/java/cn/xiaowine/ui/data/TogglePageDate.kt: -------------------------------------------------------------------------------- 1 | package cn.xiaowine.ui.data 2 | 3 | import cn.xiaowine.ui.WinePage 4 | 5 | data class TogglePageDate( 6 | val now: Class?, 7 | val last: Class? 8 | ) -------------------------------------------------------------------------------- /ui/src/main/java/cn/xiaowine/ui/dialog/WineDialog.kt: -------------------------------------------------------------------------------- 1 | package cn.xiaowine.ui.dialog 2 | 3 | import android.app.Dialog 4 | import android.content.Context 5 | import android.content.res.Configuration 6 | import android.graphics.Typeface 7 | import android.os.Build 8 | import android.os.Bundle 9 | import android.util.TypedValue 10 | import android.view.Gravity 11 | import android.view.View 12 | import android.widget.LinearLayout 13 | import androidx.annotation.StringRes 14 | import androidx.appcompat.widget.AppCompatTextView 15 | import androidx.constraintlayout.widget.ConstraintLayout 16 | import androidx.constraintlayout.widget.ConstraintSet 17 | import androidx.core.content.ContextCompat 18 | import cn.xiaowine.ui.R 19 | import cn.xiaowine.ui.appcompat.HyperButton 20 | import cn.xiaowine.ui.databinding.WineDialogBinding 21 | import cn.xiaowine.ui.tools.DrawableTools.createRoundShape 22 | import cn.xiaowine.ui.tools.Tools.dp 23 | import cn.xiaowine.ui.tools.Tools.getProp 24 | 25 | open class WineDialog(context: Context) : Dialog(context, R.style.Theme_WineDialog) { 26 | private var _binding: WineDialogBinding? = null 27 | private val binding: WineDialogBinding get() = _binding!! 28 | val titleView: AppCompatTextView 29 | get() = binding.titleView 30 | val messageView: AppCompatTextView 31 | get() = binding.messageView 32 | val customizAreaeView: LinearLayout 33 | get() = binding.customizeAreaView 34 | val buttonAreaView: LinearLayout 35 | get() = binding.buttonAreaView 36 | val constraintLayout: ConstraintLayout 37 | get() = binding.constraintLayout 38 | 39 | init { 40 | _binding = WineDialogBinding.inflate(layoutInflater).apply { 41 | setContentView(root) 42 | } 43 | // setContentView(R.layout.wine_dialog) 44 | } 45 | 46 | override fun onCreate(savedInstanceState: Bundle?) { 47 | super.onCreate(savedInstanceState) 48 | ConstraintSet().apply { 49 | clone(constraintLayout) 50 | val dp20 = 20.dp 51 | val dp12 = 12.dp 52 | connect(titleView.id, ConstraintSet.TOP, ConstraintSet.PARENT_ID, ConstraintSet.TOP, dp20) 53 | connect(titleView.id, ConstraintSet.START, ConstraintSet.PARENT_ID, ConstraintSet.START, dp20) 54 | connect(titleView.id, ConstraintSet.END, ConstraintSet.PARENT_ID, ConstraintSet.END, dp20) 55 | connect(messageView.id, ConstraintSet.TOP, titleView.id, ConstraintSet.BOTTOM, dp12) 56 | connect(messageView.id, ConstraintSet.START, ConstraintSet.PARENT_ID, ConstraintSet.START, dp20) 57 | connect(messageView.id, ConstraintSet.END, ConstraintSet.PARENT_ID, ConstraintSet.END, dp20) 58 | connect(customizAreaeView.id, ConstraintSet.TOP, messageView.id, ConstraintSet.BOTTOM, dp12) 59 | connect(buttonAreaView.id, ConstraintSet.START, ConstraintSet.PARENT_ID, ConstraintSet.START, dp20) 60 | connect(buttonAreaView.id, ConstraintSet.END, ConstraintSet.PARENT_ID, ConstraintSet.END, dp20) 61 | connect(buttonAreaView.id, ConstraintSet.TOP, customizAreaeView.id, ConstraintSet.BOTTOM, dp12) 62 | connect(buttonAreaView.id, ConstraintSet.BOTTOM, ConstraintSet.PARENT_ID, ConstraintSet.BOTTOM, dp20) 63 | applyTo(constraintLayout) 64 | } 65 | titleView.apply { 66 | setTextSize(TypedValue.COMPLEX_UNIT_SP, 19f) 67 | typeface = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { 68 | Typeface.create(null, 500, false) 69 | } else { 70 | Typeface.DEFAULT_BOLD 71 | } 72 | } 73 | messageView.apply { 74 | setTextSize(TypedValue.COMPLEX_UNIT_SP, 17f) 75 | } 76 | window?.apply { 77 | if (getProp("ro.build.characteristics") == "tablet" || context.resources.configuration.orientation == Configuration.ORIENTATION_LANDSCAPE) { 78 | setGravity(Gravity.CENTER) 79 | setWindowAnimations(R.style.Theme_WineDialogPadAnim) 80 | } else { 81 | setGravity(Gravity.BOTTOM) 82 | attributes.y = 15.dp 83 | setWindowAnimations(R.style.Theme_WineDialogAnim) 84 | } 85 | setBackgroundDrawable(createRoundShape(30.dp.toFloat(), ContextCompat.getColor(context, R.color.dialog_background_color))) 86 | attributes.apply { 87 | dimAmount = 0.5F 88 | width = 380.dp 89 | } 90 | } 91 | 92 | } 93 | 94 | override fun show() { 95 | super.show() 96 | if (buttonAreaView.childCount > 2) { 97 | buttonAreaView.orientation = LinearLayout.VERTICAL 98 | } else { 99 | buttonAreaView.orientation = LinearLayout.HORIZONTAL 100 | } 101 | } 102 | 103 | override fun dismiss() { 104 | super.dismiss() 105 | _binding = null 106 | } 107 | 108 | fun addButton(string: String, event: ((HyperButton) -> Unit)? = null): HyperButton { 109 | return HyperButton(context).apply { 110 | (layoutParams as LinearLayout.LayoutParams).apply { 111 | setMargins(6.dp, 10.dp, 6.dp, 6.dp) 112 | } 113 | text = string 114 | buttonAreaView.addView(this) 115 | setOnClickListener { 116 | if (!it.isEnabled) return@setOnClickListener 117 | if (event != null) { 118 | event(this) 119 | } else { 120 | dismiss() 121 | } 122 | } 123 | } 124 | } 125 | 126 | fun setTitle(title: String) { 127 | titleView.apply { 128 | text = title 129 | visibility = if (title.isEmpty()) View.GONE else View.VISIBLE 130 | } 131 | } 132 | 133 | override fun setTitle(@StringRes title: Int) { 134 | titleView.apply { 135 | setText(title) 136 | visibility = if (title == -1) View.GONE else View.VISIBLE 137 | } 138 | } 139 | 140 | fun setMessage(message: String) { 141 | messageView.apply { 142 | text = message 143 | visibility = if (message.isEmpty()) View.GONE else View.VISIBLE 144 | } 145 | } 146 | 147 | fun setMessage(@StringRes message: Int) { 148 | messageView.apply { 149 | setText(message) 150 | visibility = if (message == -1) View.GONE else View.VISIBLE 151 | } 152 | } 153 | 154 | fun addCustomizeView(customizeView: View) { 155 | customizAreaeView.addView(customizeView) 156 | } 157 | } -------------------------------------------------------------------------------- /ui/src/main/java/cn/xiaowine/ui/dialog/WineEditTextDialog.kt: -------------------------------------------------------------------------------- 1 | package cn.xiaowine.ui.dialog 2 | 3 | import android.content.Context 4 | import android.os.Bundle 5 | import android.text.Editable 6 | import android.text.SpannableStringBuilder 7 | import android.view.MotionEvent 8 | import android.widget.LinearLayout 9 | import cn.xiaowine.ui.appcompat.HyperEditText 10 | import cn.xiaowine.ui.data.HyperEditBackgroundStyle 11 | import cn.xiaowine.ui.tools.HyperEditTextFocusTools.touchIfNeedHideKeyboard 12 | import cn.xiaowine.ui.tools.Tools.dp 13 | 14 | class WineEditTextDialog(context: Context) : WineDialog(context) { 15 | 16 | val editText: HyperEditText = HyperEditText(context) 17 | 18 | override fun onCreate(savedInstanceState: Bundle?) { 19 | super.onCreate(savedInstanceState) 20 | addCustomizeView(editText.apply { 21 | (layoutParams as LinearLayout.LayoutParams).apply { 22 | setMargins(25.dp, 0, 25.dp, 0) 23 | } 24 | }) 25 | } 26 | 27 | override fun dispatchTouchEvent(event: MotionEvent): Boolean { 28 | return context.touchIfNeedHideKeyboard(currentFocus, event, true) { 29 | super.dispatchTouchEvent(event) 30 | } 31 | } 32 | 33 | fun setBackgroundStyle(backgroundStyle: HyperEditBackgroundStyle?) { 34 | editText.setBackgroundStyle(backgroundStyle) 35 | } 36 | 37 | fun getValue(): Editable { 38 | return editText.text ?: SpannableStringBuilder("") 39 | } 40 | 41 | fun setHint(hint: String) { 42 | editText.hint = hint 43 | } 44 | } -------------------------------------------------------------------------------- /ui/src/main/java/cn/xiaowine/ui/dialog/WineWaitDialog.kt: -------------------------------------------------------------------------------- 1 | package cn.xiaowine.ui.dialog 2 | 3 | import android.app.Dialog 4 | import android.content.Context 5 | import android.content.res.Configuration 6 | import android.graphics.drawable.LayerDrawable 7 | import android.graphics.drawable.RotateDrawable 8 | import android.os.Bundle 9 | import android.util.TypedValue 10 | import android.view.Gravity 11 | import android.widget.ProgressBar 12 | import androidx.annotation.StringRes 13 | import androidx.appcompat.widget.AppCompatTextView 14 | import androidx.core.content.ContextCompat 15 | import cn.xiaowine.ui.R 16 | import cn.xiaowine.ui.databinding.WineDialogProgressbarBinding 17 | import cn.xiaowine.ui.tools.DrawableTools.createRoundShape 18 | import cn.xiaowine.ui.tools.Tools 19 | import cn.xiaowine.ui.tools.Tools.dp 20 | 21 | class WineWaitDialog(context: Context) : Dialog(context, R.style.Theme_WineDialog) { 22 | 23 | private var _binding: WineDialogProgressbarBinding? = null 24 | private val binding: WineDialogProgressbarBinding get() = _binding!! 25 | val titleView: AppCompatTextView 26 | get() = binding.titleView 27 | 28 | val progressBar: ProgressBar 29 | get() = binding.progressBar 30 | 31 | init { 32 | setCancelable(false) 33 | _binding = WineDialogProgressbarBinding.inflate(layoutInflater).apply { 34 | setContentView(root) 35 | } 36 | } 37 | 38 | override fun onCreate(savedInstanceState: Bundle?) { 39 | super.onCreate(savedInstanceState) 40 | window?.apply { 41 | if (Tools.getProp("ro.build.characteristics") == "tablet" || context.resources.configuration.orientation == Configuration.ORIENTATION_LANDSCAPE) { 42 | setGravity(Gravity.CENTER) 43 | setWindowAnimations(R.style.Theme_WineDialogPadAnim) 44 | } else { 45 | setGravity(Gravity.BOTTOM) 46 | attributes.y = 15.dp 47 | setWindowAnimations(R.style.Theme_WineDialogAnim) 48 | } 49 | setBackgroundDrawable(createRoundShape(30f.dp, ContextCompat.getColor(context, R.color.dialog_background_color))) 50 | attributes.apply { 51 | dimAmount = 0.5F 52 | width = 380.dp 53 | height = 78.dp 54 | } 55 | } 56 | titleView.apply { 57 | setTextSize(TypedValue.COMPLEX_UNIT_SP, 17f) 58 | setPadding(10.dp, 0, 0, 0) 59 | } 60 | progressBar.apply { 61 | layoutParams.apply { 62 | width = 25.dp 63 | height = 25.dp 64 | } 65 | val rotateDrawable = RotateDrawable().apply { 66 | drawable = ContextCompat.getDrawable(context, R.drawable.ic_progress) 67 | fromDegrees = 0f 68 | toDegrees = 1080f 69 | pivotX = 0.5f 70 | pivotY = 0.5f 71 | } 72 | indeterminateDrawable = LayerDrawable(arrayOf(rotateDrawable)) 73 | } 74 | binding.root.apply { 75 | setPadding(32.dp, 0, 0, 0) 76 | } 77 | } 78 | 79 | fun setTitle(title: String) { 80 | titleView.text = title 81 | } 82 | 83 | override fun setTitle(@StringRes title: Int) { 84 | titleView.setText(title) 85 | } 86 | } -------------------------------------------------------------------------------- /ui/src/main/java/cn/xiaowine/ui/tools/ClassScanner.kt: -------------------------------------------------------------------------------- 1 | package cn.xiaowine.ui.tools 2 | 3 | import android.content.Context 4 | import cn.xiaowine.ui.WinePage 5 | import java.util.* 6 | 7 | object ClassScanner { 8 | 9 | @Suppress("UNCHECKED_CAST") 10 | fun scanPages(context: Context, packageName: String): List> { 11 | val classLoader = context.classLoader 12 | return runCatching { 13 | val dexPathListField = classLoader.javaClass.superclass?.getDeclaredField("pathList") 14 | dexPathListField?.isAccessible = true 15 | val dexPathList = dexPathListField?.get(classLoader) 16 | val dexElementsField = dexPathList?.javaClass?.getDeclaredField("dexElements") 17 | dexElementsField?.isAccessible = true 18 | val dexElements = dexElementsField?.get(dexPathList) as? Array<*> 19 | 20 | dexElements?.flatMap { element -> 21 | val dexFileField = element?.javaClass?.getDeclaredField("dexFile") 22 | dexFileField?.isAccessible = true 23 | val dexFile = dexFileField?.get(element) 24 | 25 | val entriesMethod = dexFile?.javaClass?.getDeclaredMethod("entries") 26 | val entries = entriesMethod?.invoke(dexFile) as? Enumeration 27 | 28 | entries?.toList()?.filter { it.startsWith(packageName) }?.mapNotNull { entry -> 29 | runCatching { 30 | val entryClass = Class.forName(entry, true, classLoader) 31 | if (entryClass.name.contains("$") || !WinePage::class.java.isAssignableFrom(entryClass)) { 32 | null 33 | } else { 34 | entryClass 35 | } 36 | }.getOrNull() 37 | } ?: emptyList() 38 | }?.distinct() ?: emptyList() 39 | }.getOrElse { 40 | it.printStackTrace() 41 | emptyList() 42 | } 43 | } 44 | } -------------------------------------------------------------------------------- /ui/src/main/java/cn/xiaowine/ui/tools/DrawableTools.kt: -------------------------------------------------------------------------------- 1 | package cn.xiaowine.ui.tools 2 | 3 | import android.graphics.drawable.ShapeDrawable 4 | import android.graphics.drawable.shapes.RoundRectShape 5 | 6 | object DrawableTools { 7 | fun createRoundShape(radius: Float, color: Int): ShapeDrawable { 8 | val radii = floatArrayOf(radius, radius, radius, radius, radius, radius, radius, radius) 9 | val roundRectShape = RoundRectShape(radii, null, null) 10 | return ShapeDrawable(roundRectShape).apply { 11 | paint.color = color 12 | } 13 | } 14 | 15 | fun createRoundShape(radius: FloatArray, color: Int): ShapeDrawable { 16 | val radii = floatArrayOf(radius[0], radius[0], radius[1], radius[1], radius[2], radius[2], radius[3], radius[3]) 17 | val roundRectShape = RoundRectShape(radii, null, null) 18 | return ShapeDrawable(roundRectShape).apply { 19 | shape = roundRectShape 20 | paint.color = color 21 | } 22 | } 23 | } -------------------------------------------------------------------------------- /ui/src/main/java/cn/xiaowine/ui/tools/HyperEditTextFocusTools.kt: -------------------------------------------------------------------------------- 1 | package cn.xiaowine.ui.tools 2 | 3 | import android.content.Context 4 | import android.content.Context.INPUT_METHOD_SERVICE 5 | import android.view.MotionEvent 6 | import android.view.View 7 | import android.view.WindowInsets 8 | import android.view.inputmethod.InputMethodManager 9 | import cn.xiaowine.ui.appcompat.HyperEditText 10 | 11 | object HyperEditTextFocusTools { 12 | 13 | private fun isShouldHideKeyboard(v: View, event: MotionEvent): Boolean { 14 | if (v is HyperEditText) { 15 | val l = intArrayOf(0, 0) 16 | v.getLocationInWindow(l) 17 | val left = l[0] 18 | val top = l[1] 19 | val bottom = top + v.getHeight() 20 | val right = left + v.getWidth() 21 | return (!(event.x > left) || !(event.x < right) || !(event.y > top) || !(event.y < bottom)) 22 | } 23 | return false 24 | } 25 | 26 | fun hideKeyboardAndClearFocus(context: Context, editText: HyperEditText) { 27 | if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.R) { 28 | editText.windowInsetsController?.hide(WindowInsets.Type.ime()) 29 | } else { 30 | val imm = context.getSystemService(INPUT_METHOD_SERVICE) as InputMethodManager 31 | imm.hideSoftInputFromWindow(editText.windowToken, 0) 32 | } 33 | editText.apply { 34 | cancelAnimation() 35 | clearFocus() 36 | } 37 | } 38 | 39 | fun Context.touchIfNeedHideKeyboard(currentFocus: View?, event: MotionEvent, isDialog: Boolean, default: () -> Boolean): Boolean { 40 | if (event.action == MotionEvent.ACTION_UP) { 41 | val v = currentFocus ?: return default() 42 | if (isShouldHideKeyboard(v, event)) { 43 | hideKeyboardAndClearFocus(this, v as HyperEditText) 44 | if (isDialog) return false 45 | } 46 | } 47 | return default() 48 | } 49 | } -------------------------------------------------------------------------------- /ui/src/main/java/cn/xiaowine/ui/tools/Tools.kt: -------------------------------------------------------------------------------- 1 | package cn.xiaowine.ui.tools 2 | 3 | import android.annotation.SuppressLint 4 | import android.content.res.Resources.getSystem 5 | import android.graphics.drawable.ShapeDrawable 6 | import android.graphics.drawable.shapes.RoundRectShape 7 | import android.os.Environment 8 | import android.util.TypedValue 9 | import android.view.View 10 | import android.view.animation.AlphaAnimation 11 | import android.view.animation.Animation 12 | import android.view.animation.AnimationSet 13 | import java.io.BufferedReader 14 | import java.io.File 15 | import java.io.FileInputStream 16 | import java.io.InputStreamReader 17 | import java.util.Properties 18 | 19 | object Tools { 20 | val Int.dp: Int get() = (this.toFloat().dp).toInt() 21 | 22 | val Float.dp: Float get() = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, this, getSystem().displayMetrics) 23 | 24 | private fun getAlphaAnimation(into: Boolean, duration: Long = 300): AnimationSet { 25 | val alphaAnimation = (if (into) AlphaAnimation(0F, 1F) else AlphaAnimation(1F, 0F)).apply { 26 | this.duration = duration 27 | } 28 | return AnimationSet(true).apply { 29 | addAnimation(alphaAnimation) 30 | } 31 | } 32 | 33 | fun View.hideView(duration: Long = 300) { 34 | if (visibility == View.GONE) return 35 | val alphaAnimation = getAlphaAnimation(false, duration).apply { 36 | setAnimationListener(object : Animation.AnimationListener { 37 | override fun onAnimationStart(animation: Animation) {} 38 | override fun onAnimationEnd(animation: Animation) { 39 | visibility = View.GONE 40 | } 41 | 42 | override fun onAnimationRepeat(animation: Animation) {} 43 | }) 44 | } 45 | startAnimation(alphaAnimation) 46 | } 47 | 48 | fun View.showView(duration: Long = 300) { 49 | if (visibility == View.VISIBLE) return 50 | val alphaAnimation = getAlphaAnimation(true, duration).apply { 51 | setAnimationListener(object : Animation.AnimationListener { 52 | override fun onAnimationStart(animation: Animation) {} 53 | override fun onAnimationEnd(animation: Animation) { 54 | visibility = View.VISIBLE 55 | } 56 | 57 | override fun onAnimationRepeat(animation: Animation) {} 58 | }) 59 | } 60 | startAnimation(alphaAnimation) 61 | } 62 | 63 | 64 | fun getProp(name: String): String { 65 | var prop = getPropByShell(name) 66 | if (prop.isEmpty()) prop = getPropByStream(name) 67 | if (prop.isEmpty()) prop = getPropByReflect(name) 68 | return prop 69 | } 70 | 71 | private fun getPropByShell(propName: String): String { 72 | return try { 73 | val p = Runtime.getRuntime().exec("getprop $propName") 74 | BufferedReader(InputStreamReader(p.inputStream), 1024).use { it.readLine() ?: "" } 75 | } catch (_: Exception) { 76 | "" 77 | } 78 | } 79 | 80 | private fun getPropByStream(key: String): String { 81 | return try { 82 | val prop = Properties() 83 | FileInputStream(File(Environment.getRootDirectory(), "build.prop")).use { prop.load(it) } 84 | prop.getProperty(key, "") 85 | } catch (_: Exception) { 86 | "" 87 | } 88 | } 89 | 90 | private fun getPropByReflect(key: String): String { 91 | return try { 92 | @SuppressLint("PrivateApi") val clz = Class.forName("android.os.SystemProperties") 93 | val getMethod = clz.getMethod("get", String::class.java, String::class.java) 94 | getMethod.invoke(clz, key, "") as String 95 | } catch (_: Exception) { 96 | "" 97 | } 98 | } 99 | } -------------------------------------------------------------------------------- /ui/src/main/java/cn/xiaowine/ui/viewmodel/PageViewModel.kt: -------------------------------------------------------------------------------- 1 | package cn.xiaowine.ui.viewmodel 2 | 3 | import androidx.lifecycle.MutableLiveData 4 | import androidx.lifecycle.ViewModel 5 | import cn.xiaowine.ui.data.PageData 6 | import cn.xiaowine.ui.data.TogglePageDate 7 | import cn.xiaowine.ui.WinePage 8 | 9 | class PageViewModel : ViewModel() { 10 | val nowPage: MutableLiveData = MutableLiveData() 11 | val pageItems: MutableLiveData> = MutableLiveData(mutableSetOf()) 12 | val pageQueue: MutableLiveData>> = MutableLiveData(mutableListOf()) 13 | } -------------------------------------------------------------------------------- /ui/src/main/java/cn/xiaowine/ui/widget/BaseWineText.kt: -------------------------------------------------------------------------------- 1 | package cn.xiaowine.ui.widget 2 | 3 | import android.content.Context 4 | import android.graphics.Typeface 5 | import android.graphics.drawable.Drawable 6 | import android.os.Build 7 | import android.util.AttributeSet 8 | import android.util.TypedValue 9 | import android.view.LayoutInflater 10 | import android.view.View 11 | import android.widget.ImageView 12 | import android.widget.LinearLayout 13 | import androidx.appcompat.widget.AppCompatTextView 14 | import androidx.constraintlayout.widget.ConstraintLayout 15 | import androidx.constraintlayout.widget.ConstraintSet 16 | import androidx.core.content.ContextCompat 17 | import cn.xiaowine.ui.databinding.BaseTextBinding 18 | import cn.xiaowine.ui.tools.Tools.dp 19 | 20 | open class BaseWineText(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : ConstraintLayout(context, attrs, defStyleAttr) { 21 | constructor(context: Context, attrs: AttributeSet?) : this(context, attrs, 0) 22 | constructor(context: Context) : this(context, null) 23 | 24 | private var _binding: BaseTextBinding? = null 25 | val binding: BaseTextBinding get() = _binding!! 26 | 27 | 28 | val titleView: AppCompatTextView get() = binding.titleView 29 | val summaryView: AppCompatTextView get() = binding.summaryView 30 | val customizeAreaView: LinearLayout get() = binding.customizeAreaView 31 | val constraintLayout: ConstraintLayout get() = binding.constraintLayout 32 | val linearLayout: LinearLayout get() = binding.linearLayout 33 | val iconView: ImageView get() = binding.imageView 34 | 35 | var title: String 36 | get() { 37 | return titleView.text.toString() 38 | } 39 | set(value) { 40 | titleView.text = value 41 | } 42 | var summary: String 43 | get() { 44 | return summaryView.text.toString() 45 | } 46 | set(value) { 47 | summaryView.text = value 48 | } 49 | 50 | 51 | open fun onClick(onClick: ((BaseWineText) -> Unit)? = null) { 52 | setOnClickListener { 53 | onClick?.invoke(it as BaseWineText) 54 | } 55 | } 56 | 57 | fun onLongClick(onLongClick: ((BaseWineText) -> Unit)? = null) { 58 | setOnLongClickListener { 59 | onLongClick?.invoke(it as BaseWineText) 60 | true 61 | } 62 | } 63 | 64 | fun setIcon(resId: Int) { 65 | setIcon(ContextCompat.getDrawable(context, resId)) 66 | } 67 | 68 | fun setIcon(drawable: Drawable?) { 69 | if (drawable != null) { 70 | ConstraintSet().apply { 71 | clone(constraintLayout) 72 | connect(linearLayout.id, ConstraintSet.START, iconView.id, ConstraintSet.END, 60.dp) 73 | applyTo(constraintLayout) 74 | } 75 | } 76 | iconView.visibility = if (drawable == null) GONE else VISIBLE 77 | iconView.setImageDrawable(drawable) 78 | } 79 | 80 | fun addCustomizeView(customizeView: View) { 81 | customizeAreaView.addView(customizeView) 82 | } 83 | 84 | init { 85 | _binding = BaseTextBinding.inflate(LayoutInflater.from(context), this, true) 86 | titleView.apply { 87 | setTextSize(TypedValue.COMPLEX_UNIT_SP, 18f) 88 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { 89 | paint.typeface = Typeface.create(null, 500, false) 90 | } else { 91 | paint.typeface = Typeface.defaultFromStyle(Typeface.BOLD) 92 | } 93 | } 94 | summaryView.apply { 95 | setTextSize(TypedValue.COMPLEX_UNIT_SP, 14f) 96 | } 97 | iconView.apply { 98 | layoutParams = LayoutParams(45.dp, 45.dp) 99 | } 100 | linearLayout.setPadding(0, 20.dp, 0, 20.dp) 101 | ConstraintSet().apply { 102 | clone(constraintLayout) 103 | connect(linearLayout.id, ConstraintSet.END, ConstraintSet.PARENT_ID, ConstraintSet.END, 20.dp) 104 | connect(iconView.id, ConstraintSet.TOP, ConstraintSet.PARENT_ID, ConstraintSet.TOP, 0) 105 | connect(iconView.id, ConstraintSet.BOTTOM, ConstraintSet.PARENT_ID, ConstraintSet.BOTTOM, 0) 106 | connect(linearLayout.id, ConstraintSet.START, iconView.id, ConstraintSet.END, 0) 107 | applyTo(constraintLayout) 108 | } 109 | } 110 | } -------------------------------------------------------------------------------- /ui/src/main/java/cn/xiaowine/ui/widget/WineCard.kt: -------------------------------------------------------------------------------- 1 | package cn.xiaowine.ui.widget 2 | 3 | import android.content.Context 4 | import android.util.AttributeSet 5 | import android.widget.LinearLayout 6 | import androidx.cardview.widget.CardView 7 | import androidx.core.content.ContextCompat 8 | import cn.xiaowine.ui.R 9 | import cn.xiaowine.ui.build.CardViewBuild 10 | import cn.xiaowine.ui.tools.Tools.dp 11 | 12 | class WineCard(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : CardView(context, attrs, defStyleAttr) { 13 | constructor(context: Context, attrs: AttributeSet?) : this(context, attrs, androidx.cardview.R.attr.cardViewStyle) 14 | constructor(context: Context) : this(context, null) 15 | 16 | private val content: LinearLayout 17 | 18 | init { 19 | radius = 45f 20 | cardElevation = 0f 21 | useCompatPadding = false 22 | setCardBackgroundColor(ContextCompat.getColor(context, R.color.card_bg_color)) 23 | layoutParams = LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT).apply { 24 | setMargins(15.dp, 10.dp, 15.dp, 10.dp) 25 | } 26 | content = LinearLayout(context).apply { 27 | orientation = LinearLayout.VERTICAL 28 | setPadding(0, 10.dp, 0, 16.dp) 29 | layoutParams = LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT) 30 | this@WineCard.addView(this) 31 | } 32 | } 33 | 34 | fun build(init: CardViewBuild.() -> Unit) { 35 | CardViewBuild().apply(init).viewList.forEach { 36 | val view = it.first.getConstructor(Context::class.java).newInstance(context).apply { 37 | setPadding(20.dp, 0, 20.dp, 0) 38 | content.addView(this) 39 | } 40 | it.second.invoke(view) 41 | } 42 | } 43 | } -------------------------------------------------------------------------------- /ui/src/main/java/cn/xiaowine/ui/widget/WineCardLink.kt: -------------------------------------------------------------------------------- 1 | package cn.xiaowine.ui.widget 2 | 3 | import android.content.Context 4 | import android.graphics.Typeface 5 | import android.os.Build 6 | import android.util.AttributeSet 7 | import android.util.TypedValue 8 | import android.widget.FrameLayout 9 | import androidx.appcompat.widget.AppCompatTextView 10 | import androidx.core.content.ContextCompat 11 | import cn.xiaowine.ui.R 12 | import cn.xiaowine.ui.tools.Tools.dp 13 | 14 | class WineCardLink(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : AppCompatTextView(context, attrs, defStyleAttr) { 15 | constructor(context: Context, attrs: AttributeSet?) : this(context, attrs, android.R.attr.textViewStyle) 16 | constructor(context: Context) : this(context, null) 17 | 18 | var title: String 19 | get() { 20 | return text.toString() 21 | } 22 | set(value) { 23 | text = value 24 | } 25 | 26 | init { 27 | layoutParams = FrameLayout.LayoutParams(FrameLayout.LayoutParams.MATCH_PARENT, FrameLayout.LayoutParams.WRAP_CONTENT).apply { 28 | setMargins(0, 10.dp, 0, 10.dp) 29 | } 30 | setTextColor(ContextCompat.getColor(context, R.color.card_link_color)) 31 | setTextSize(TypedValue.COMPLEX_UNIT_SP, 18.5f) 32 | typeface = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { 33 | Typeface.create(null, 500, false) 34 | } else { 35 | Typeface.defaultFromStyle(Typeface.BOLD) 36 | } 37 | } 38 | 39 | fun onClick(onClick: ((WineCardLink) -> Unit)? = null) { 40 | setOnClickListener { 41 | onClick?.invoke(it as WineCardLink) 42 | } 43 | } 44 | 45 | fun onLongClick(onLongClick: ((WineCardLink) -> Unit)? = null) { 46 | setOnLongClickListener { 47 | onLongClick?.invoke(it as WineCardLink) 48 | true 49 | } 50 | } 51 | } -------------------------------------------------------------------------------- /ui/src/main/java/cn/xiaowine/ui/widget/WineCardTitle.kt: -------------------------------------------------------------------------------- 1 | package cn.xiaowine.ui.widget 2 | 3 | import android.content.Context 4 | import android.util.AttributeSet 5 | import android.util.TypedValue 6 | import android.widget.FrameLayout 7 | import androidx.appcompat.widget.AppCompatTextView 8 | import androidx.core.content.ContextCompat 9 | import cn.xiaowine.ui.R 10 | import cn.xiaowine.ui.tools.Tools.dp 11 | 12 | class WineCardTitle(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : AppCompatTextView(context, attrs, defStyleAttr) { 13 | constructor(context: Context, attrs: AttributeSet?) : this(context, attrs, android.R.attr.textViewStyle) 14 | constructor(context: Context) : this(context, null) 15 | 16 | init { 17 | layoutParams = FrameLayout.LayoutParams(FrameLayout.LayoutParams.MATCH_PARENT, FrameLayout.LayoutParams.WRAP_CONTENT).apply { 18 | setMargins(0, 10.dp, 0, 10.dp) 19 | } 20 | setTextColor(ContextCompat.getColor(context, R.color.card_title_color)) 21 | setTextColor(ContextCompat.getColor(context, R.color.card_title_color)) 22 | setTextSize(TypedValue.COMPLEX_UNIT_SP, 18f) 23 | } 24 | } -------------------------------------------------------------------------------- /ui/src/main/java/cn/xiaowine/ui/widget/WineLine.kt: -------------------------------------------------------------------------------- 1 | package cn.xiaowine.ui.widget 2 | 3 | 4 | import android.content.Context 5 | import android.util.AttributeSet 6 | import android.view.View 7 | import android.widget.LinearLayout 8 | import androidx.core.content.ContextCompat 9 | import cn.xiaowine.ui.R 10 | import cn.xiaowine.ui.tools.Tools.dp 11 | 12 | class WineLine(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : View(context, attrs, defStyleAttr) { 13 | constructor(context: Context, attrs: AttributeSet?) : this(context, attrs, 0) 14 | constructor(context: Context) : this(context, null) 15 | 16 | 17 | init { 18 | this.layoutParams = LinearLayout.LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT, 1.dp).apply { 19 | setMargins(28.dp, 15.dp, 28.dp, 15.dp) 20 | } 21 | setBackgroundColor(ContextCompat.getColor(context, R.color.line_color)) 22 | } 23 | } -------------------------------------------------------------------------------- /ui/src/main/java/cn/xiaowine/ui/widget/WineSeekBar.kt: -------------------------------------------------------------------------------- 1 | package cn.xiaowine.ui.widget 2 | 3 | import android.content.Context 4 | import android.graphics.Typeface 5 | import android.os.Build 6 | import android.util.AttributeSet 7 | import android.util.TypedValue 8 | import android.view.Gravity 9 | import android.view.LayoutInflater 10 | import android.widget.FrameLayout 11 | import android.widget.LinearLayout 12 | import android.widget.SeekBar 13 | import androidx.appcompat.widget.AppCompatTextView 14 | import androidx.constraintlayout.widget.ConstraintLayout 15 | import androidx.constraintlayout.widget.ConstraintSet 16 | import cn.xiaowine.ui.appcompat.HyperSeekBar 17 | import cn.xiaowine.ui.databinding.WineSeekBinding 18 | import cn.xiaowine.ui.tools.Tools.dp 19 | import cn.xiaowine.ui.tools.Tools.hideView 20 | import cn.xiaowine.ui.tools.Tools.showView 21 | import kotlin.math.roundToInt 22 | import kotlin.properties.Delegates 23 | 24 | 25 | class WineSeekBar(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : ConstraintLayout(context, attrs, defStyleAttr) { 26 | constructor(context: Context, attrs: AttributeSet?) : this(context, attrs, 0) 27 | constructor(context: Context) : this(context, null) 28 | 29 | private var onProgressChangedListener: ProgressChangedListener? = null 30 | private var _binding: WineSeekBinding? = null 31 | private val binding: WineSeekBinding get() = _binding!! 32 | 33 | val minText: AppCompatTextView get() = binding.minText 34 | val maxText: AppCompatTextView get() = binding.maxText 35 | val nowText: AppCompatTextView get() = binding.nowText 36 | val seekBar: HyperSeekBar get() = binding.seekBar 37 | val textLayout: LinearLayout get() = binding.textLayout 38 | private val constraintLayout: ConstraintLayout get() = binding.constraintLayout 39 | private val fragment: FrameLayout get() = binding.fragment 40 | 41 | 42 | // Android 8.1以上才支持设置最小值 43 | var minProgress by Delegates.observable(0) { _, _, newValue -> 44 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { 45 | if (newValue < -9999) { 46 | error("The minimum value cannot be less than -9999") 47 | } 48 | minText.text = newValue.toString() 49 | seekBar.min = newValue 50 | } 51 | } 52 | 53 | // 最大值9999 54 | var maxProgress by Delegates.observable(0) { _, _, newValue -> 55 | if (newValue > 9999) { 56 | error("The maximum value cannot exceed 9999") 57 | } 58 | maxText.text = newValue.toString() 59 | seekBar.max = newValue 60 | } 61 | 62 | var nowProgress by Delegates.observable(0) { _, _, newValue -> 63 | post { 64 | nowText.text = newValue.toString() 65 | seekBar.progress = newValue 66 | } 67 | } 68 | 69 | override fun onAttachedToWindow() { 70 | super.onAttachedToWindow() 71 | if (nowProgress !in minProgress..maxProgress) { 72 | error("The current value cannot exceed the maximum value or be less than the minimum value") 73 | } 74 | } 75 | 76 | private fun AppCompatTextView.createTextView(text: Int): AppCompatTextView { 77 | return this.apply { 78 | this.text = text.toString() 79 | if (layoutParams is LinearLayout.LayoutParams) { 80 | setTextSize(TypedValue.COMPLEX_UNIT_SP, 14f) 81 | setPadding(20.dp, 0, 20.dp, 0) 82 | layoutParams = LinearLayout.LayoutParams(0, 31.dp, 1f) 83 | } else { 84 | setTextSize(TypedValue.COMPLEX_UNIT_SP, 16f) 85 | setPadding(10.dp, 0, 0, 0) 86 | layoutParams.apply { 87 | width = (55 * resources.configuration.fontScale + 5).dp.roundToInt() 88 | } 89 | gravity = Gravity.CENTER_VERTICAL or Gravity.END 90 | typeface = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { 91 | Typeface.create(Typeface.DEFAULT, 600, false) 92 | } else { 93 | Typeface.create(Typeface.DEFAULT, Typeface.BOLD) 94 | } 95 | } 96 | } 97 | } 98 | 99 | 100 | init { 101 | _binding = WineSeekBinding.inflate(LayoutInflater.from(context), this, true) 102 | ConstraintSet().apply { 103 | clone(constraintLayout) 104 | connect(fragment.id, ConstraintSet.TOP, ConstraintSet.PARENT_ID, ConstraintSet.TOP, 15.dp) 105 | connect(fragment.id, ConstraintSet.BOTTOM, ConstraintSet.PARENT_ID, ConstraintSet.BOTTOM, 15.dp) 106 | applyTo(constraintLayout) 107 | } 108 | minText.createTextView(minProgress) 109 | maxText.createTextView(maxProgress) 110 | nowText.createTextView(nowProgress) 111 | seekBar.apply { 112 | setOnSeekBarChangeListener(object : SeekBar.OnSeekBarChangeListener { 113 | override fun onProgressChanged(seekBar: SeekBar, progress: Int, fromUser: Boolean) { 114 | nowText.text = progress.toString() 115 | onProgressChangedListener?.onChanged(seekBar, progress, fromUser) 116 | } 117 | 118 | 119 | override fun onStartTrackingTouch(seekBar: SeekBar) { 120 | textLayout.showView() 121 | } 122 | 123 | override fun onStopTrackingTouch(seekBar: SeekBar) { 124 | textLayout.hideView() 125 | } 126 | }) 127 | progress = this@WineSeekBar.nowProgress 128 | } 129 | } 130 | 131 | fun onProgressChanged(onProgressChangedListener: ProgressChangedListener?) { 132 | this.onProgressChangedListener = onProgressChangedListener 133 | } 134 | 135 | fun onLongClick(onLongClick: ((seekBar: SeekBar, progress: Int) -> Unit)? = null) { 136 | seekBar.setLongClickListener(object : HyperSeekBar.LongClickListener { 137 | override fun onLongClick(seekBar: SeekBar, progress: Int) { 138 | onLongClick?.invoke(seekBar, progress) 139 | } 140 | }) 141 | nowText.setOnLongClickListener { 142 | onLongClick?.invoke(seekBar, seekBar.progress) 143 | true 144 | } 145 | } 146 | 147 | interface ProgressChangedListener { 148 | fun onChanged(seekBar: SeekBar, progress: Int, fromUser: Boolean) 149 | } 150 | 151 | } -------------------------------------------------------------------------------- /ui/src/main/java/cn/xiaowine/ui/widget/WineSpinner.kt: -------------------------------------------------------------------------------- 1 | package cn.xiaowine.ui.widget 2 | 3 | 4 | import android.annotation.SuppressLint 5 | import android.content.Context 6 | import android.graphics.Typeface 7 | import android.graphics.drawable.Drawable 8 | import android.graphics.drawable.StateListDrawable 9 | import android.os.Build 10 | import android.text.TextUtils 11 | import android.util.AttributeSet 12 | import android.util.TypedValue 13 | import android.view.Gravity 14 | import android.view.MotionEvent 15 | import android.view.View 16 | import android.view.ViewGroup 17 | import android.widget.BaseAdapter 18 | import android.widget.ImageView 19 | import android.widget.LinearLayout 20 | import android.widget.TextView 21 | import androidx.appcompat.widget.AppCompatTextView 22 | import androidx.core.content.ContextCompat 23 | import cn.xiaowine.ui.R 24 | import cn.xiaowine.ui.appcompat.HyperPopup 25 | import cn.xiaowine.ui.data.SpinnerData 26 | import cn.xiaowine.ui.tools.DrawableTools.createRoundShape 27 | import cn.xiaowine.ui.tools.Tools.dp 28 | import kotlin.properties.Delegates 29 | 30 | @SuppressLint("ClickableViewAccessibility") 31 | class WineSpinner(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : BaseWineText(context, attrs, defStyleAttr) { 32 | constructor(context: Context, attrs: AttributeSet?) : this(context, attrs, 0) 33 | constructor(context: Context) : this(context, null) 34 | 35 | private val listPopupWindow = HyperPopup(context) 36 | 37 | val item: MutableList = mutableListOf() 38 | var currentValue: String by Delegates.observable("") { _, _, newValue -> 39 | textView.text = newValue 40 | } 41 | 42 | private val adapter = object : BaseAdapter() { 43 | override fun getCount(): Int { 44 | return item.size 45 | } 46 | 47 | override fun getItem(position: Int): Any { 48 | return item[position].text 49 | } 50 | 51 | override fun getItemId(position: Int): Long { 52 | return position.toLong() 53 | } 54 | 55 | override fun getView(position: Int, convertView: View?, parent: ViewGroup?): View { 56 | val spinnerData = item[position] 57 | return LinearLayout(context).apply { 58 | val itemText = spinnerData.text 59 | val isActive = itemText == currentValue 60 | val radius = getRadius(position, item.size, 20f.dp) 61 | val pressedDrawable = createRoundShape(radius, ContextCompat.getColor(context, if (isActive) R.color.popup_select_background_click_color else R.color.popup_background_click_color)) 62 | val normalDrawable = createRoundShape(radius, ContextCompat.getColor(context, if (isActive) R.color.popup_select_background_color else R.color.popup_background_color)) 63 | background = createStateListDrawable(pressedDrawable, pressedDrawable, normalDrawable) 64 | val textview = (object : AppCompatTextView(context) { 65 | init { 66 | isFocusable = true 67 | } 68 | 69 | override fun isFocused(): Boolean { 70 | return true 71 | } 72 | }).apply { 73 | layoutParams = LayoutParams(100.dp, LayoutParams.WRAP_CONTENT) 74 | descendantFocusability = ViewGroup.FOCUS_BLOCK_DESCENDANTS 75 | setTextSize(TypedValue.COMPLEX_UNIT_SP, 18f) 76 | setPadding(25.dp, 25.dp, 10.dp, 25.dp) 77 | width = 105.dp 78 | isSingleLine = true 79 | text = itemText 80 | ellipsize = TextUtils.TruncateAt.MARQUEE 81 | paint.typeface = Typeface.defaultFromStyle(Typeface.BOLD) 82 | setTextColor(ContextCompat.getColor(context, if (isActive) R.color.popup_select_text_color else R.color.title_text_color)) 83 | } 84 | addView(textview) 85 | if (isActive) { 86 | addView(ImageView(context).apply { 87 | gravity = Gravity.CENTER_VERTICAL 88 | layoutParams = LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT).apply { 89 | setMargins(0, 0, 20.dp, 0) 90 | } 91 | background = ContextCompat.getDrawable(context, R.drawable.ic_popup_select) 92 | }) 93 | } 94 | setOnClickListener { 95 | currentValue = spinnerData.text 96 | spinnerData.click?.invoke(position) 97 | listPopupWindow.dismiss() 98 | } 99 | } 100 | } 101 | } 102 | 103 | private val textView = TextView(context).apply { 104 | setTextSize(TypedValue.COMPLEX_UNIT_SP, 12f) 105 | gravity = Gravity.RIGHT or Gravity.CENTER_VERTICAL 106 | setTextColor(ContextCompat.getColor(context, R.color.title_color)) 107 | width = 150.dp 108 | setPadding(30.dp, 0, 5.dp, 0) 109 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { 110 | paint.typeface = Typeface.create(null, 400, false) 111 | } else { 112 | paint.typeface = Typeface.defaultFromStyle(Typeface.BOLD) 113 | } 114 | setTextSize(TypedValue.COMPLEX_UNIT_SP, 15f) 115 | } 116 | private val imageView = ImageView(context).apply { 117 | background = ContextCompat.getDrawable(context, R.drawable.ic_spinner) 118 | } 119 | 120 | init { 121 | val linearLayout = LinearLayout(context).apply { 122 | gravity = Gravity.CENTER_VERTICAL 123 | addView(textView) 124 | addView(imageView) 125 | } 126 | addCustomizeView(linearLayout) 127 | setOnTouchListener { _, event -> 128 | if (event.action == MotionEvent.ACTION_UP) { 129 | listPopupWindow.show() 130 | performClick() 131 | } 132 | true 133 | } 134 | } 135 | 136 | fun onItemClick(onItemClick: (item: SpinnerData, position: Int) -> Unit) { 137 | listPopupWindow.setOnItemClickListener { _, _, position, _ -> 138 | onItemClick(item[position], position) 139 | } 140 | } 141 | 142 | fun setData(vararg data: SpinnerData) { 143 | item.addAll(data) 144 | adapter.notifyDataSetChanged() 145 | } 146 | 147 | fun setData(vararg data: Pair Unit)?>) { 148 | data.forEach { 149 | item.add(SpinnerData(it.first, it.second)) 150 | } 151 | adapter.notifyDataSetChanged() 152 | } 153 | 154 | override fun onAttachedToWindow() { 155 | listPopupWindow.apply { 156 | anchorView = this@WineSpinner 157 | if (currentValue.isEmpty()) currentValue = item.first().text 158 | setAdapter(adapter) 159 | } 160 | super.onAttachedToWindow() 161 | } 162 | 163 | 164 | fun createStateListDrawable(focusedDrawable: Drawable, pressedDrawable: Drawable, normalDrawable: Drawable): StateListDrawable { 165 | return StateListDrawable().apply { 166 | addState(intArrayOf(android.R.attr.state_focused), focusedDrawable) 167 | addState(intArrayOf(android.R.attr.state_pressed), pressedDrawable) 168 | addState(intArrayOf(), normalDrawable) 169 | } 170 | } 171 | 172 | fun getRadius(position: Int, itemSize: Int, radiusFloat: Float): FloatArray { 173 | return when { 174 | itemSize == 1 -> floatArrayOf(radiusFloat, radiusFloat, radiusFloat, radiusFloat) 175 | position == 0 -> floatArrayOf(radiusFloat, radiusFloat, 0f, 0f) 176 | position == itemSize - 1 -> floatArrayOf(0f, 0f, radiusFloat, radiusFloat) 177 | else -> floatArrayOf(0f, 0f, 0f, 0f) 178 | } 179 | } 180 | 181 | interface OnItemClickListener { 182 | fun onItemClick(item: SpinnerData, position: Int) 183 | } 184 | 185 | } -------------------------------------------------------------------------------- /ui/src/main/java/cn/xiaowine/ui/widget/WineSwitch.kt: -------------------------------------------------------------------------------- 1 | package cn.xiaowine.ui.widget 2 | 3 | import android.content.Context 4 | import android.util.AttributeSet 5 | import android.view.Gravity 6 | import android.view.View 7 | import androidx.constraintlayout.widget.ConstraintSet 8 | import cn.xiaowine.ui.appcompat.HyperSwitch 9 | 10 | class WineSwitch(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : BaseWineText(context, attrs, defStyleAttr) { 11 | constructor(context: Context, attrs: AttributeSet?) : this(context, attrs, 0) 12 | constructor(context: Context) : this(context, null) 13 | 14 | val switchView = HyperSwitch(context).apply { 15 | gravity = Gravity.CENTER_VERTICAL 16 | id = View.generateViewId() 17 | } 18 | 19 | var isChecked: Boolean 20 | get() { 21 | return switchView.isChecked 22 | } 23 | set(value) { 24 | switchView.isChecked = value 25 | } 26 | 27 | fun onChange(onClick: ((HyperSwitch, Boolean) -> Unit)?) { 28 | switchView.setOnCheckedChangeListener { v, b -> 29 | if (v.isPressed) { 30 | onClick?.invoke(v as HyperSwitch, b) 31 | } 32 | } 33 | } 34 | 35 | init { 36 | ConstraintSet().apply { 37 | clone(constraintLayout) 38 | connect(switchView.id, ConstraintSet.END, ConstraintSet.PARENT_ID, ConstraintSet.END, 0) 39 | connect(switchView.id, ConstraintSet.TOP, ConstraintSet.PARENT_ID, ConstraintSet.TOP, 0) 40 | connect(switchView.id, ConstraintSet.BOTTOM, ConstraintSet.PARENT_ID, ConstraintSet.BOTTOM, 0) 41 | applyTo(constraintLayout) 42 | } 43 | addCustomizeView(switchView) 44 | } 45 | } -------------------------------------------------------------------------------- /ui/src/main/java/cn/xiaowine/ui/widget/WineText.kt: -------------------------------------------------------------------------------- 1 | package cn.xiaowine.ui.widget 2 | 3 | import android.content.Context 4 | import android.util.AttributeSet 5 | import android.view.View 6 | import android.widget.ImageView 7 | import androidx.constraintlayout.widget.ConstraintSet 8 | import cn.xiaowine.ui.R 9 | 10 | class WineText(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : BaseWineText(context, attrs, defStyleAttr) { 11 | constructor(context: Context, attrs: AttributeSet?) : this(context, attrs, 0) 12 | constructor(context: Context) : this(context, null) 13 | 14 | 15 | val arrowImage = ImageView(context).apply { 16 | id = View.generateViewId() 17 | setImageResource(R.drawable.ic_right_arrow) 18 | setImageResource(R.drawable.ic_right_arrow) 19 | visibility = View.GONE 20 | } 21 | 22 | override fun onClick(onClick: ((BaseWineText) -> Unit)?) { 23 | haveArrow(onClick != null) 24 | super.onClick(onClick) 25 | } 26 | 27 | fun haveArrow(boolean: Boolean = true) { 28 | arrowImage.visibility = if (boolean) VISIBLE else GONE 29 | } 30 | 31 | init { 32 | ConstraintSet().apply { 33 | clone(constraintLayout) 34 | connect(arrowImage.id, ConstraintSet.END, ConstraintSet.PARENT_ID, ConstraintSet.END, 0) 35 | applyTo(constraintLayout) 36 | } 37 | addCustomizeView(arrowImage) 38 | } 39 | } -------------------------------------------------------------------------------- /ui/src/main/java/cn/xiaowine/ui/widget/WineTitle.kt: -------------------------------------------------------------------------------- 1 | package cn.xiaowine.ui.widget 2 | 3 | import android.content.Context 4 | import android.graphics.Typeface 5 | import android.os.Build 6 | import android.util.AttributeSet 7 | import android.util.TypedValue 8 | import androidx.appcompat.widget.AppCompatTextView 9 | import androidx.core.content.ContextCompat 10 | import cn.xiaowine.ui.R 11 | import cn.xiaowine.ui.tools.Tools.dp 12 | 13 | class WineTitle(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : AppCompatTextView(context, attrs, defStyleAttr) { 14 | constructor(context: Context, attrs: AttributeSet?) : this(context, attrs, android.R.attr.textViewStyle) 15 | constructor(context: Context) : this(context, null) 16 | 17 | init { 18 | setPadding(0, 10.dp, 0, 10.dp) 19 | setTextColor(ContextCompat.getColor(context, R.color.title_color)) 20 | setTextSize(TypedValue.COMPLEX_UNIT_SP, 14f) 21 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { 22 | paint.typeface = Typeface.create(null, 400, false) 23 | } else { 24 | paint.typeface = Typeface.defaultFromStyle(Typeface.BOLD) 25 | } 26 | } 27 | } -------------------------------------------------------------------------------- /ui/src/main/res/anim/popup_enter.xml: -------------------------------------------------------------------------------- 1 | 2 | 9 | -------------------------------------------------------------------------------- /ui/src/main/res/anim/popup_exit.xml: -------------------------------------------------------------------------------- 1 | 2 | 9 | -------------------------------------------------------------------------------- /ui/src/main/res/animator/dialog_enter.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 7 | 11 | -------------------------------------------------------------------------------- /ui/src/main/res/animator/dialog_exit.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 7 | 11 | -------------------------------------------------------------------------------- /ui/src/main/res/animator/dialog_pad_enter.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 7 | -------------------------------------------------------------------------------- /ui/src/main/res/animator/dialog_pad_exit.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 7 | -------------------------------------------------------------------------------- /ui/src/main/res/animator/slide_left_in.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 10 | 16 | -------------------------------------------------------------------------------- /ui/src/main/res/animator/slide_left_out.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 10 | 16 | -------------------------------------------------------------------------------- /ui/src/main/res/animator/slide_right_in.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 10 | 16 | -------------------------------------------------------------------------------- /ui/src/main/res/animator/slide_right_out.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 10 | 16 | -------------------------------------------------------------------------------- /ui/src/main/res/drawable/ic_popup_select.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /ui/src/main/res/drawable/ic_progress.xml: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ui/src/main/res/drawable/ic_right_arrow.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | -------------------------------------------------------------------------------- /ui/src/main/res/drawable/ic_spinner.xml: -------------------------------------------------------------------------------- 1 | 7 | 11 | -------------------------------------------------------------------------------- /ui/src/main/res/layout/activity_wine.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | -------------------------------------------------------------------------------- /ui/src/main/res/layout/base_text.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 17 | 18 | 29 | 30 | 36 | 37 | 44 | 45 | 46 | 47 | 55 | 56 | 57 | -------------------------------------------------------------------------------- /ui/src/main/res/layout/fragment_page.xml: -------------------------------------------------------------------------------- 1 | 2 | 9 | 10 | 11 | 16 | 17 | 25 | 26 | 32 | 33 | 34 | 35 | 36 | 40 | 41 | 46 | 47 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | -------------------------------------------------------------------------------- /ui/src/main/res/layout/wine_dialog.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 17 | 18 | 28 | 29 | 37 | 38 | 46 | -------------------------------------------------------------------------------- /ui/src/main/res/layout/wine_dialog_progressbar.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 13 | 14 | 20 | 21 | -------------------------------------------------------------------------------- /ui/src/main/res/layout/wine_seek.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 17 | 18 | 22 | 23 | 29 | 30 | 36 | 37 | 43 | 44 | 45 | 46 | 47 | 56 | 57 | -------------------------------------------------------------------------------- /ui/src/main/res/values-night/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | @android:color/black 4 | @android:color/white 5 | #333333 6 | #0073dd 7 | #2D2D2D 8 | #222224 9 | @android:color/white 10 | #333333 11 | #677893 12 | #232323 13 | #253E64 14 | #0d7AEC 15 | @android:color/white 16 | #454545 17 | #C8C8C8 18 | #5A5A5A 19 | #FFFFFF 20 | #343434 21 | #727272 22 | #242424 23 | #454545 24 | #1F2F4C 25 | #404E67 26 | -------------------------------------------------------------------------------- /ui/src/main/res/values-night/themes.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 10 | -------------------------------------------------------------------------------- /ui/src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | @android:color/white 4 | @android:color/black 5 | #0d84ff 6 | #e6e6e6 7 | #E6E6E6 8 | #9399b3 9 | #909090 10 | #0d84ff 11 | #f0f0f0 12 | #e6e6e6 13 | #0c84FF 14 | @android:color/white 15 | @android:color/white 16 | #0c84FF 17 | #0c84FF 18 | #C2D9FF 19 | #0d7AEC 20 | @android:color/white 21 | #E8E8E8 22 | #F0F0F0 23 | #F0F0F0 24 | #303030 25 | #000000 26 | #f4f4f4 27 | #a8a8a8 28 | #FFFFFF 29 | #EBEBEB 30 | #E6F2FF 31 | #D4DFEB 32 | #1385FF 33 | -------------------------------------------------------------------------------- /ui/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /ui/src/main/res/values/themes.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 10 | 11 | 17 | 18 | 22 | 23 | 27 | 28 | 32 | --------------------------------------------------------------------------------