提交完整项目代码
This commit is contained in:
parent
f02ce4bb3f
commit
664bf9db56
15
.gitignore
vendored
Normal file
15
.gitignore
vendored
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
*.iml
|
||||||
|
.gradle
|
||||||
|
/local.properties
|
||||||
|
/.idea/caches
|
||||||
|
/.idea/libraries
|
||||||
|
/.idea/modules.xml
|
||||||
|
/.idea/workspace.xml
|
||||||
|
/.idea/navEditor.xml
|
||||||
|
/.idea/assetWizardSettings.xml
|
||||||
|
.DS_Store
|
||||||
|
/build
|
||||||
|
/captures
|
||||||
|
.externalNativeBuild
|
||||||
|
.cxx
|
||||||
|
local.properties
|
||||||
3
.idea/.gitignore
generated
vendored
Normal file
3
.idea/.gitignore
generated
vendored
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
# Default ignored files
|
||||||
|
/shelf/
|
||||||
|
/workspace.xml
|
||||||
6
.idea/AndroidProjectSystem.xml
generated
Normal file
6
.idea/AndroidProjectSystem.xml
generated
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="AndroidProjectSystem">
|
||||||
|
<option name="providerId" value="com.android.tools.idea.GradleProjectSystem" />
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
6
.idea/compiler.xml
generated
Normal file
6
.idea/compiler.xml
generated
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="CompilerConfiguration">
|
||||||
|
<bytecodeTargetLevel target="21" />
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
10
.idea/deploymentTargetDropDown.xml
generated
Normal file
10
.idea/deploymentTargetDropDown.xml
generated
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="deploymentTargetDropDown">
|
||||||
|
<value>
|
||||||
|
<entry key="app">
|
||||||
|
<State />
|
||||||
|
</entry>
|
||||||
|
</value>
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
18
.idea/deploymentTargetSelector.xml
generated
Normal file
18
.idea/deploymentTargetSelector.xml
generated
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="deploymentTargetSelector">
|
||||||
|
<selectionStates>
|
||||||
|
<SelectionState runConfigName="app">
|
||||||
|
<option name="selectionMode" value="DROPDOWN" />
|
||||||
|
<DropdownSelection timestamp="2025-05-12T09:48:45.711798Z">
|
||||||
|
<Target type="DEFAULT_BOOT">
|
||||||
|
<handle>
|
||||||
|
<DeviceId pluginId="PhysicalDevice" identifier="serial=659PX8INFIUKHYXK" />
|
||||||
|
</handle>
|
||||||
|
</Target>
|
||||||
|
</DropdownSelection>
|
||||||
|
<DialogSelection />
|
||||||
|
</SelectionState>
|
||||||
|
</selectionStates>
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
19
.idea/gradle.xml
generated
Normal file
19
.idea/gradle.xml
generated
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="GradleMigrationSettings" migrationVersion="1" />
|
||||||
|
<component name="GradleSettings">
|
||||||
|
<option name="linkedExternalProjectsSettings">
|
||||||
|
<GradleProjectSettings>
|
||||||
|
<option name="testRunner" value="CHOOSE_PER_TEST" />
|
||||||
|
<option name="externalProjectPath" value="$PROJECT_DIR$" />
|
||||||
|
<option name="gradleJvm" value="jbr-17" />
|
||||||
|
<option name="modules">
|
||||||
|
<set>
|
||||||
|
<option value="$PROJECT_DIR$" />
|
||||||
|
<option value="$PROJECT_DIR$/app" />
|
||||||
|
</set>
|
||||||
|
</option>
|
||||||
|
</GradleProjectSettings>
|
||||||
|
</option>
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
6
.idea/kotlinc.xml
generated
Normal file
6
.idea/kotlinc.xml
generated
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="KotlinJpsPluginSettings">
|
||||||
|
<option name="version" value="1.8.20" />
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
10
.idea/migrations.xml
generated
Normal file
10
.idea/migrations.xml
generated
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="ProjectMigrations">
|
||||||
|
<option name="MigrateToGradleLocalJavaHome">
|
||||||
|
<set>
|
||||||
|
<option value="$PROJECT_DIR$" />
|
||||||
|
</set>
|
||||||
|
</option>
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
5
.idea/misc.xml
generated
Normal file
5
.idea/misc.xml
generated
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="ExternalStorageConfigurationManager" enabled="true" />
|
||||||
|
<component name="ProjectRootManager" version="2" languageLevel="JDK_21" default="true" project-jdk-name="jbr-21" project-jdk-type="JavaSDK" />
|
||||||
|
</project>
|
||||||
17
.idea/runConfigurations.xml
generated
Normal file
17
.idea/runConfigurations.xml
generated
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="RunConfigurationProducerService">
|
||||||
|
<option name="ignoredProducers">
|
||||||
|
<set>
|
||||||
|
<option value="com.intellij.execution.junit.AbstractAllInDirectoryConfigurationProducer" />
|
||||||
|
<option value="com.intellij.execution.junit.AllInPackageConfigurationProducer" />
|
||||||
|
<option value="com.intellij.execution.junit.PatternConfigurationProducer" />
|
||||||
|
<option value="com.intellij.execution.junit.TestInClassConfigurationProducer" />
|
||||||
|
<option value="com.intellij.execution.junit.UniqueIdConfigurationProducer" />
|
||||||
|
<option value="com.intellij.execution.junit.testDiscovery.JUnitTestDiscoveryConfigurationProducer" />
|
||||||
|
<option value="org.jetbrains.kotlin.idea.junit.KotlinJUnitRunConfigurationProducer" />
|
||||||
|
<option value="org.jetbrains.kotlin.idea.junit.KotlinPatternConfigurationProducer" />
|
||||||
|
</set>
|
||||||
|
</option>
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
6
.idea/vcs.xml
generated
Normal file
6
.idea/vcs.xml
generated
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="VcsDirectoryMappings">
|
||||||
|
<mapping directory="" vcs="Git" />
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
BIN
.safedk/api/SafeDKAndroid-6.0.7.jar
Normal file
BIN
.safedk/api/SafeDKAndroid-6.0.7.jar
Normal file
Binary file not shown.
BIN
.safedk/api/SafeDKAndroid-6.2.6.jar
Normal file
BIN
.safedk/api/SafeDKAndroid-6.2.6.jar
Normal file
Binary file not shown.
26
.safedk/app_sdks.lst
Normal file
26
.safedk/app_sdks.lst
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
106f9be0e66f52f36eaaaff4dd231971
|
||||||
|
75939c4ce23c53ad9534d43be176b3e9
|
||||||
|
217e8f437c9fc4244d6e74653ac8a8c7
|
||||||
|
66b774de6608db14a84e972fba1ec954
|
||||||
|
e1c9ddef73e5621f62c717badf1be3f2
|
||||||
|
daaea35726ab7cd457ab61d4538fb822
|
||||||
|
b9b88d70c3d018bfbda46cd93ba3ddca
|
||||||
|
946dbe0d5ed7fee91c8ece64d035e70b
|
||||||
|
d41ed920405e4bd14f3a42cd93c43d89
|
||||||
|
7eac188d3286b05ccbba774f63a2c049
|
||||||
|
4df96d3bc9afd17b812e65e6c6add1ef
|
||||||
|
9f5a74f6ccfb81b48969231b39bf937f
|
||||||
|
eb3214f29c0a52815b41977d6cc9a46e
|
||||||
|
becf75b2cc99e82716da2e6697879509
|
||||||
|
7eec7b9476b99b3ce94533da4f2eb987
|
||||||
|
974322f19d813702ea048d95288d2b8c
|
||||||
|
95ff573e4cdf46a05f6c5ac703940db3
|
||||||
|
f281c2ca1b0ba69b5805badd314ef646
|
||||||
|
29015bbfcc182d80e7f75bd2c38e4521
|
||||||
|
ff22dbf67af979b8b3169a242d10f166
|
||||||
|
c4d1f1775f251f03dce94fdf267a7b89
|
||||||
|
dd2971b0681141d57b221687791ad1bd
|
||||||
|
86a0d598cde251321e21a0da4ab94065
|
||||||
|
74616804a7dc29147dfb0afe122a9fd2
|
||||||
|
35695de726f6044576c830bf197f36f7
|
||||||
|
|
||||||
BIN
.safedk/dex/SafeDKAndroid-6.2.6.dex
Normal file
BIN
.safedk/dex/SafeDKAndroid-6.2.6.dex
Normal file
Binary file not shown.
BIN
.safedk/dex/android-support-multidex.dex
Normal file
BIN
.safedk/dex/android-support-multidex.dex
Normal file
Binary file not shown.
2
.safedk/hashes.safedk
Normal file
2
.safedk/hashes.safedk
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
#Mon May 26 15:52:52 CST 2025
|
||||||
|
json=-1146317101
|
||||||
1
.safedk/list.enc
Normal file
1
.safedk/list.enc
Normal file
File diff suppressed because one or more lines are too long
6
.safedk/plugin.properties
Normal file
6
.safedk/plugin.properties
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
#
|
||||||
|
#Mon May 26 15:52:52 CST 2025
|
||||||
|
4yWSuJdlSBRrsgCT2fEzZvNoBH0M1hzyqNP9ZiCTn0an2TBTdxk01Ips4aov__5L4dU8_sQpVw0_GOMLgIfJu_=VVN64VW3_CeQrIZ9sSUMQAWAS4XI14L2etylkN_E2IkJpZrsF6Xt26aMuwBGOboUiFvkTVJ28EbPW53NL_6SPT
|
||||||
|
BoZtE6LMU2QaUEowq3SoQFO_HqwztZQdgF3VZGmNRR17TGv0XhXSwlT6LiaRllitI7yAsCkSGo_pfE0yfipADf=wt2KAZMCf_SkV_coMIB7GWtaOQtCd2ZFRK8hFAQo7zbXpIGpG5iI0fZ0sMJr5n_cCO3LEVU66gpxe099OFXXvv
|
||||||
|
sdk_analysis_plugin_version=5.2.7
|
||||||
|
set_multidex=true
|
||||||
34
.safedk/proguard-safedk.pro
Normal file
34
.safedk/proguard-safedk.pro
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
-keep class androidx.multidex.** { *; }
|
||||||
|
-keep class androidx.browser.customtabs.CustomTabsIntent { *; }
|
||||||
|
-keep class androidx.** {
|
||||||
|
*** startActivityForResult(***);
|
||||||
|
*** startActivity(***);
|
||||||
|
}
|
||||||
|
-keep class android.support.multidex.** { *; }
|
||||||
|
-keep class android.support.v4.app.** { *; }
|
||||||
|
-keep class com.google.android.gms.location.FusedLocationProviderApi { *; }
|
||||||
|
-keep class com.google.android.gms.location.LocationListener { *; }
|
||||||
|
-keep class io.fabric.sdk.android.** { *; }
|
||||||
|
-keep class okio.** { *; }
|
||||||
|
-keep class retrofit2.** { *; }
|
||||||
|
-keep class okhttp3.** { *; }
|
||||||
|
-keep class com.squareup.okhttp.** { *; }
|
||||||
|
-keep class com.android.volley.** { *; }
|
||||||
|
-keep class com.flurry.** { *; }
|
||||||
|
-keep class org.apache.** { *; }
|
||||||
|
-keep class com.applovin.** { *; }
|
||||||
|
-keep class com.google.android.gms.ads.** { *; }
|
||||||
|
-keep class com.ironsource.** { *; }
|
||||||
|
-keep class com.fyber.inneractive.** { *; }
|
||||||
|
-keep class com.vungle.** { *; }
|
||||||
|
-keep class com.unity3d.ads.** { *; }
|
||||||
|
-keep class com.unity3d.services.** { *; }
|
||||||
|
-keep class com.mintegral.msdk.** { *; }
|
||||||
|
-keep class com.mbridge.msdk.** { *; }
|
||||||
|
-keep class com.adcolony.sdk.** { *; }
|
||||||
|
-keep class com.inmobi.** { *; }
|
||||||
|
-keep class com.five_corp.** { *; }
|
||||||
|
-keep class com.bytedance.** { *; }
|
||||||
|
-keep class com.smaato.** { *; }
|
||||||
|
-keep class com.safedk.** { *; }
|
||||||
|
-keep class com.applovin.quality.** { *; }
|
||||||
1
app/.gitignore
vendored
Normal file
1
app/.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
/build
|
||||||
120
app/build.gradle
Normal file
120
app/build.gradle
Normal file
@ -0,0 +1,120 @@
|
|||||||
|
import java.util.Date
|
||||||
|
import java.text.SimpleDateFormat
|
||||||
|
plugins {
|
||||||
|
id 'com.android.application'
|
||||||
|
id 'org.jetbrains.kotlin.android'
|
||||||
|
id 'com.google.gms.google-services'
|
||||||
|
id 'com.google.firebase.crashlytics'
|
||||||
|
id 'kotlin-kapt'
|
||||||
|
id 'applovin-quality-service'
|
||||||
|
}
|
||||||
|
|
||||||
|
applovin {
|
||||||
|
apiKey "4yWSuJdlSBRrsgCT2fEzZvNoBH0M1hzyqNP9ZiCTn0an2TBTdxk01Ips4aov__5L4dU8_sQpVw0_GOMLgIfJu_"
|
||||||
|
}
|
||||||
|
|
||||||
|
String timestamp = new SimpleDateFormat("MMddHHmm").format(new Date())
|
||||||
|
|
||||||
|
android {
|
||||||
|
namespace 'com.keyboard.craft'
|
||||||
|
compileSdk 34
|
||||||
|
|
||||||
|
defaultConfig {
|
||||||
|
applicationId "com.keyboards.craft"
|
||||||
|
minSdk 23
|
||||||
|
targetSdk 34
|
||||||
|
versionCode 2
|
||||||
|
versionName "1.0.2"
|
||||||
|
setProperty("archivesBaseName", "Keyboardcraft-V" + versionName + "C${versionCode}-$timestamp")
|
||||||
|
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||||
|
}
|
||||||
|
|
||||||
|
buildTypes {
|
||||||
|
release {
|
||||||
|
minifyEnabled true
|
||||||
|
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
compileOptions {
|
||||||
|
sourceCompatibility JavaVersion.VERSION_17
|
||||||
|
targetCompatibility JavaVersion.VERSION_17
|
||||||
|
}
|
||||||
|
kotlinOptions {
|
||||||
|
jvmTarget = '17'
|
||||||
|
}
|
||||||
|
buildFeatures {
|
||||||
|
viewBinding true
|
||||||
|
buildConfig = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
|
||||||
|
implementation 'androidx.core:core-ktx:1.8.0'
|
||||||
|
implementation 'androidx.appcompat:appcompat:1.6.1'
|
||||||
|
implementation 'com.google.android.material:material:1.11.0'
|
||||||
|
testImplementation 'junit:junit:4.13.2'
|
||||||
|
androidTestImplementation 'androidx.test.ext:junit:1.1.5'
|
||||||
|
androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1'
|
||||||
|
//banner
|
||||||
|
implementation 'io.github.youth5201314:banner:2.2.2'
|
||||||
|
//沉浸式
|
||||||
|
implementation 'com.geyifeng.immersionbar:immersionbar:3.2.2'
|
||||||
|
implementation 'com.geyifeng.immersionbar:immersionbar-ktx:3.2.2'
|
||||||
|
//图片加载
|
||||||
|
implementation 'com.github.bumptech.glide:glide:4.16.0'
|
||||||
|
implementation 'jp.wasabeef:glide-transformations:4.3.0'
|
||||||
|
//上拉下拉
|
||||||
|
implementation 'io.github.scwang90:refresh-layout-kernel:2.1.0'
|
||||||
|
implementation 'io.github.scwang90:refresh-header-classics:2.1.0'
|
||||||
|
implementation 'io.github.scwang90:refresh-footer-classics:2.1.0'
|
||||||
|
//pag
|
||||||
|
implementation 'com.tencent.tav:libpag:4.0.5.10'
|
||||||
|
//json动画
|
||||||
|
implementation 'com.airbnb.android:lottie:6.0.0'
|
||||||
|
//7z
|
||||||
|
implementation 'com.github.omicronapps:7-Zip-JBinding-4Android:Release-16.02-2.02'
|
||||||
|
|
||||||
|
implementation("com.github.lihangleo2:ShadowLayout:3.4.0")
|
||||||
|
implementation("androidx.room:room-ktx:2.6.1")
|
||||||
|
implementation("androidx.room:room-runtime:2.6.1")
|
||||||
|
//noinspection KaptUsageInsteadOfKsp
|
||||||
|
kapt("androidx.room:room-compiler:2.6.1")
|
||||||
|
implementation(platform("com.google.firebase:firebase-bom:32.2.2"))
|
||||||
|
implementation("com.google.firebase:firebase-analytics-ktx")
|
||||||
|
implementation("com.google.firebase:firebase-crashlytics-ktx")
|
||||||
|
implementation("com.google.firebase:firebase-config-ktx")
|
||||||
|
|
||||||
|
|
||||||
|
//--------------------MAX (BIGO Ads、Chartboost、DT Exchangc、Googlc Ad Managcr、Google Bidding and Google AdMob、InMobi、ironSource、Liftoff Monetizc、
|
||||||
|
//Meta Audience Network、Moloco、Panglc、Unity Ads)
|
||||||
|
implementation("com.applovin:applovin-sdk:+")
|
||||||
|
implementation("com.applovin.mediation:bigoads-adapter:5.3.0.1")
|
||||||
|
implementation("com.applovin.mediation:chartboost-adapter:+")
|
||||||
|
implementation("com.google.android.gms:play-services-base:16.1.0")
|
||||||
|
implementation("com.applovin.mediation:fyber-adapter:+")
|
||||||
|
//Google Ad Manager
|
||||||
|
// implementation("com.applovin.mediation:google-ad-manager-adapter:+")
|
||||||
|
|
||||||
|
//Google Bidding and Google AdMob
|
||||||
|
// implementation("com.applovin.mediation:google-adapter:+")
|
||||||
|
implementation("com.applovin.mediation:inmobi-adapter:+")
|
||||||
|
implementation("com.squareup.picasso:picasso:2.71828")
|
||||||
|
implementation("androidx.recyclerview:recyclerview:1.1.0")
|
||||||
|
implementation("com.applovin.mediation:ironsource-adapter:+")
|
||||||
|
implementation("com.applovin.mediation:vungle-adapter:+")
|
||||||
|
implementation("com.applovin.mediation:facebook-adapter:+")
|
||||||
|
implementation("com.applovin.mediation:moloco-adapter:+")
|
||||||
|
implementation("com.applovin.mediation:bytedance-adapter:+")
|
||||||
|
implementation("com.applovin.mediation:unityads-adapter:+")
|
||||||
|
|
||||||
|
//获取gaid
|
||||||
|
implementation("com.google.android.gms:play-services-ads-identifier:18.0.1")
|
||||||
|
implementation("com.google.android.gms:play-services-appset:16.0.1")
|
||||||
|
//开启协程
|
||||||
|
implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.6.2")
|
||||||
|
|
||||||
|
|
||||||
|
implementation ("com.squareup.okhttp3:okhttp:4.12.0")
|
||||||
|
implementation("com.squareup.okhttp3:logging-interceptor:4.12.0")
|
||||||
|
}
|
||||||
29
app/google-services.json
Normal file
29
app/google-services.json
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
{
|
||||||
|
"project_info": {
|
||||||
|
"project_number": "50335954095",
|
||||||
|
"project_id": "keyboard-craft",
|
||||||
|
"storage_bucket": "keyboard-craft.firebasestorage.app"
|
||||||
|
},
|
||||||
|
"client": [
|
||||||
|
{
|
||||||
|
"client_info": {
|
||||||
|
"mobilesdk_app_id": "1:50335954095:android:7ce96918b71974ba53905a",
|
||||||
|
"android_client_info": {
|
||||||
|
"package_name": "com.keyboards.craft"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"oauth_client": [],
|
||||||
|
"api_key": [
|
||||||
|
{
|
||||||
|
"current_key": "AIzaSyBM4WAxdcLWOu_quhVZ8w7xR3wcCjrXEiY"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"services": {
|
||||||
|
"appinvite_service": {
|
||||||
|
"other_platform_oauth_client": []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"configuration_version": "1"
|
||||||
|
}
|
||||||
103
app/proguard-rules.pro
vendored
Normal file
103
app/proguard-rules.pro
vendored
Normal file
@ -0,0 +1,103 @@
|
|||||||
|
# Add project specific ProGuard rules here.
|
||||||
|
# You can control the set of applied configuration files using the
|
||||||
|
# proguardFiles setting in build.gradle.
|
||||||
|
#
|
||||||
|
# For more details, see
|
||||||
|
# http://developer.android.com/guide/developing/tools/proguard.html
|
||||||
|
|
||||||
|
# If your project uses WebView with JS, uncomment the following
|
||||||
|
# and specify the fully qualified class name to the JavaScript interface
|
||||||
|
# class:
|
||||||
|
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
|
||||||
|
# public *;
|
||||||
|
#}
|
||||||
|
|
||||||
|
# Uncomment this to preserve the line number information for
|
||||||
|
# debugging stack traces.
|
||||||
|
#-keepattributes SourceFile,LineNumberTable
|
||||||
|
|
||||||
|
# If you keep the line number information, uncomment this to
|
||||||
|
# hide the original source file name.
|
||||||
|
#-renamesourcefileattribute SourceFile
|
||||||
|
|
||||||
|
-keep class org.libpag.** {*;}
|
||||||
|
-keep class androidx.exifinterface.** {*;}
|
||||||
|
-keep class com.omicronapplications.** { *; }
|
||||||
|
-keep class net.sf.sevenzipjbinding.** { *; }
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
#---------------------------------TopOn 聚合
|
||||||
|
# Vungle
|
||||||
|
-dontwarn com.vungle.ads.**
|
||||||
|
-keepclassmembers class com.vungle.ads.** {
|
||||||
|
*;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
# Google
|
||||||
|
-keep class com.google.android.gms.** { *; }
|
||||||
|
-dontwarn com.google.android.gms.**
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
# START OkHttp + Okio
|
||||||
|
# JSR 305 annotations are for embedding nullability information.
|
||||||
|
-dontwarn javax.annotation.**
|
||||||
|
|
||||||
|
|
||||||
|
# A resource is loaded with a relative path so the package of this class must be preserved.
|
||||||
|
-adaptresourcefilenames okhttp3/internal/publicsuffix/PublicSuffixDatabase.gz
|
||||||
|
|
||||||
|
|
||||||
|
# Animal Sniffer compileOnly dependency to ensure APIs are compatible with older versions of Java.
|
||||||
|
-dontwarn org.codehaus.mojo.animal_sniffer.*
|
||||||
|
|
||||||
|
|
||||||
|
# OkHttp platform used only on JVM and when Conscrypt and other security providers are available.
|
||||||
|
-dontwarn okhttp3.internal.platform.**
|
||||||
|
-dontwarn org.conscrypt.**
|
||||||
|
-dontwarn org.bouncycastle.**
|
||||||
|
-dontwarn org.openjsse.**
|
||||||
|
|
||||||
|
|
||||||
|
# Animal Sniffer compileOnly dependency to ensure APIs are compatible with older versions of Java.
|
||||||
|
-dontwarn org.codehaus.mojo.animal_sniffer.*
|
||||||
|
|
||||||
|
|
||||||
|
# END OkHttp + Okio
|
||||||
|
|
||||||
|
|
||||||
|
# START Protobuf
|
||||||
|
-dontwarn com.google.protobuf.**
|
||||||
|
-keepclassmembers class com.google.protobuf.** {
|
||||||
|
*;
|
||||||
|
}
|
||||||
|
-keep class * extends com.google.protobuf.GeneratedMessageLite { *; }
|
||||||
|
|
||||||
|
|
||||||
|
# END Protobuf
|
||||||
|
-keepattributes Signature
|
||||||
|
-keepattributes *Annotation*
|
||||||
|
-keep class com.mbridge.** {*; }
|
||||||
|
-keep interface com.mbridge.** {*; }
|
||||||
|
-keep class android.support.v4.** { *; }
|
||||||
|
-dontwarn com.mbridge.**
|
||||||
|
-keep class **.R$* { public static final int mbridge*; }
|
||||||
|
-keep public class com.mbridge.* extends androidx.** { *; }
|
||||||
|
-keep public class androidx.viewpager.widget.PagerAdapter{ *; }
|
||||||
|
-keep public class androidx.viewpager.widget.ViewPager.OnPageChangeListener{ *; }
|
||||||
|
-keep interface androidx.annotation.IntDef{ *; }
|
||||||
|
-keep interface androidx.annotation.Nullable{ *; }
|
||||||
|
-keep interface androidx.annotation.CheckResult{ *; }
|
||||||
|
-keep interface androidx.annotation.NonNull{ *; }
|
||||||
|
-keep public class androidx.fragment.app.Fragment{ *; }
|
||||||
|
-keep public class androidx.core.content.FileProvider{ *; }
|
||||||
|
-keep public class androidx.core.app.NotificationCompat{ *; }
|
||||||
|
-keep public class androidx.appcompat.widget.AppCompatImageView { *; }
|
||||||
|
-keep public class androidx.recyclerview.*{ *; }
|
||||||
|
|
||||||
|
#---------------------------------TopOn 聚合
|
||||||
@ -0,0 +1,24 @@
|
|||||||
|
package com.keyboard.craft
|
||||||
|
|
||||||
|
import androidx.test.platform.app.InstrumentationRegistry
|
||||||
|
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||||
|
|
||||||
|
import org.junit.Test
|
||||||
|
import org.junit.runner.RunWith
|
||||||
|
|
||||||
|
import org.junit.Assert.*
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Instrumented test, which will execute on an Android device.
|
||||||
|
*
|
||||||
|
* See [testing documentation](http://d.android.com/tools/testing).
|
||||||
|
*/
|
||||||
|
@RunWith(AndroidJUnit4::class)
|
||||||
|
class ExampleInstrumentedTest {
|
||||||
|
@Test
|
||||||
|
fun useAppContext() {
|
||||||
|
// Context of the app under test.
|
||||||
|
val appContext = InstrumentationRegistry.getInstrumentation().targetContext
|
||||||
|
assertEquals("com.keyboard.craft", appContext.packageName)
|
||||||
|
}
|
||||||
|
}
|
||||||
62
app/src/main/AndroidManifest.xml
Normal file
62
app/src/main/AndroidManifest.xml
Normal file
@ -0,0 +1,62 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools">
|
||||||
|
|
||||||
|
<uses-permission android:name="android.permission.INTERNET" />
|
||||||
|
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
|
||||||
|
<application
|
||||||
|
android:name=".CraftApp"
|
||||||
|
android:allowBackup="true"
|
||||||
|
android:networkSecurityConfig="@xml/net"
|
||||||
|
android:dataExtractionRules="@xml/data_extraction_rules"
|
||||||
|
android:fullBackupContent="@xml/backup_rules"
|
||||||
|
android:icon="@mipmap/ic_launcher"
|
||||||
|
android:label="@string/app_name"
|
||||||
|
android:roundIcon="@mipmap/ic_launcher_round"
|
||||||
|
android:supportsRtl="true"
|
||||||
|
android:theme="@style/Theme.Keyboard"
|
||||||
|
tools:targetApi="31">
|
||||||
|
|
||||||
|
<activity
|
||||||
|
android:name=".activity.SplashActivity"
|
||||||
|
android:exported="true">
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="android.intent.action.MAIN" />
|
||||||
|
|
||||||
|
<category android:name="android.intent.category.LAUNCHER" />
|
||||||
|
</intent-filter>
|
||||||
|
</activity>
|
||||||
|
|
||||||
|
|
||||||
|
<activity
|
||||||
|
android:name=".activity.MainActivity"
|
||||||
|
android:launchMode="singleTask"
|
||||||
|
android:screenOrientation="portrait" />
|
||||||
|
|
||||||
|
<service
|
||||||
|
android:name=".service.KeyboardService"
|
||||||
|
android:exported="true"
|
||||||
|
android:label="@string/app_name"
|
||||||
|
android:permission="android.permission.BIND_INPUT_METHOD">
|
||||||
|
<meta-data
|
||||||
|
android:name="android.view.im"
|
||||||
|
android:resource="@xml/method" />
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="android.view.InputMethod" />
|
||||||
|
</intent-filter>
|
||||||
|
</service>
|
||||||
|
|
||||||
|
<activity
|
||||||
|
android:name=".activity.CategoryDetailsActivity"
|
||||||
|
android:screenOrientation="portrait" />
|
||||||
|
<activity
|
||||||
|
android:name=".activity.DetailsActivity"
|
||||||
|
android:screenOrientation="portrait" />
|
||||||
|
<activity
|
||||||
|
android:name=".activity.PreviewActivity"
|
||||||
|
android:windowSoftInputMode="adjustResize"
|
||||||
|
android:screenOrientation="portrait" />
|
||||||
|
</application>
|
||||||
|
|
||||||
|
|
||||||
|
</manifest>
|
||||||
BIN
app/src/main/ic_launcher-playstore.png
Normal file
BIN
app/src/main/ic_launcher-playstore.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 184 KiB |
46
app/src/main/java/com/keyboard/craft/CraftApp.kt
Normal file
46
app/src/main/java/com/keyboard/craft/CraftApp.kt
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
package com.keyboard.craft
|
||||||
|
|
||||||
|
import android.app.Application
|
||||||
|
import android.content.Intent
|
||||||
|
import androidx.localbroadcastmanager.content.LocalBroadcastManager
|
||||||
|
import com.applovin.sdk.AppLovinMediationProvider
|
||||||
|
import com.applovin.sdk.AppLovinSdk
|
||||||
|
import com.applovin.sdk.AppLovinSdkConfiguration
|
||||||
|
import com.applovin.sdk.AppLovinSdkInitializationConfiguration
|
||||||
|
import com.keyboard.craft.db.DatabaseManager
|
||||||
|
|
||||||
|
|
||||||
|
class CraftApp : Application() {
|
||||||
|
companion object {
|
||||||
|
lateinit var app: CraftApp
|
||||||
|
const val TAG = "CraftApp"
|
||||||
|
|
||||||
|
lateinit var databaseManager: DatabaseManager
|
||||||
|
private set
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreate() {
|
||||||
|
super.onCreate()
|
||||||
|
app = this
|
||||||
|
databaseManager = DatabaseManager.getInstance(this)
|
||||||
|
initMAxSDk()
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
var initSDkOK: Boolean = false
|
||||||
|
var initAction: String = "ACTION_INIT"
|
||||||
|
private val SDK_KEY = "VVN64VW3_CeQrIZ9sSUMQAWAS4XI14L2etylkN_E2IkJpZrsF6Xt26aMuwBGOboUiFvkTVJ28EbPW53NL_6SPT"
|
||||||
|
private fun initMAxSDk() {
|
||||||
|
val initConfig = AppLovinSdkInitializationConfiguration.builder(SDK_KEY, this)
|
||||||
|
.setMediationProvider(AppLovinMediationProvider.MAX)
|
||||||
|
.build()
|
||||||
|
AppLovinSdk.getInstance(this)
|
||||||
|
.initialize(initConfig, object : AppLovinSdk.SdkInitializationListener {
|
||||||
|
override fun onSdkInitialized(appLovinSdkConfiguration: AppLovinSdkConfiguration?) {
|
||||||
|
initSDkOK = true
|
||||||
|
LocalBroadcastManager.getInstance(this@CraftApp).sendBroadcast(Intent(initAction));
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,126 @@
|
|||||||
|
package com.keyboard.craft.activity
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.view.View
|
||||||
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
|
import androidx.recyclerview.widget.GridLayoutManager
|
||||||
|
import com.keyboard.craft.ad.MaxManager
|
||||||
|
import com.keyboard.craft.adapter.KeyDetailsDataAdapter
|
||||||
|
import com.keyboard.craft.bean.CategoryDataBean
|
||||||
|
import com.keyboard.craft.bean.ItemDataBean
|
||||||
|
import com.keyboard.craft.bean.MainDataBean
|
||||||
|
import com.keyboard.craft.databinding.UiCategoryDetailsActivityBinding
|
||||||
|
import com.keyboard.craft.util.NetworkCallback
|
||||||
|
import com.keyboard.craft.util.NetworkUtil
|
||||||
|
import com.keyboard.craft.util.UIHelper
|
||||||
|
|
||||||
|
class CategoryDetailsActivity : AppCompatActivity() {
|
||||||
|
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
const val CATEGORY_DETAILS_BEAN_KEY = "category_details_bean_key"
|
||||||
|
}
|
||||||
|
|
||||||
|
private lateinit var binding: UiCategoryDetailsActivityBinding
|
||||||
|
private var offset = 0
|
||||||
|
private var pageSize = 20
|
||||||
|
|
||||||
|
private var bean: MainDataBean? = null
|
||||||
|
|
||||||
|
private var adapter: KeyDetailsDataAdapter? = null
|
||||||
|
private var contentBeans: MutableList<ItemDataBean> = mutableListOf()
|
||||||
|
|
||||||
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
super.onCreate(savedInstanceState)
|
||||||
|
binding = UiCategoryDetailsActivityBinding.inflate(layoutInflater)
|
||||||
|
setContentView(binding.root)
|
||||||
|
MaxManager.startShowMaxAd (this){ }
|
||||||
|
bean = intent.getSerializableExtra(CATEGORY_DETAILS_BEAN_KEY) as MainDataBean?
|
||||||
|
if (bean == null) {
|
||||||
|
finish()
|
||||||
|
}
|
||||||
|
initBar()
|
||||||
|
initView()
|
||||||
|
initData()
|
||||||
|
MaxManager.onLoadAd()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun initBar() {
|
||||||
|
UIHelper.setupStatusBar(this, true, binding.view)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun initView() {
|
||||||
|
binding.titleTv.text = bean?.title
|
||||||
|
binding.backBtn.setOnClickListener { finish() }
|
||||||
|
|
||||||
|
binding.refreshLayout.setEnableRefresh(false)
|
||||||
|
binding.refreshLayout.setOnLoadMoreListener {
|
||||||
|
refreshData()
|
||||||
|
}
|
||||||
|
adapter = KeyDetailsDataAdapter(this, contentBeans, "category")
|
||||||
|
binding.rv.layoutManager = GridLayoutManager(this, 2)
|
||||||
|
binding.rv.adapter = adapter
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun initData() {
|
||||||
|
loadingPlay()
|
||||||
|
offset = 0
|
||||||
|
NetworkUtil().fetchCategory(
|
||||||
|
bean?.key!!,
|
||||||
|
offset,
|
||||||
|
pageSize,
|
||||||
|
object : NetworkCallback<List<CategoryDataBean>> {
|
||||||
|
@SuppressLint("NotifyDataSetChanged")
|
||||||
|
override fun onSuccess(data: List<CategoryDataBean>) {
|
||||||
|
contentBeans.clear()
|
||||||
|
contentBeans.addAll(data[0].items)
|
||||||
|
runOnUiThread {
|
||||||
|
if (!isFinishing) {
|
||||||
|
offset += pageSize
|
||||||
|
loadingClose()
|
||||||
|
adapter?.notifyDataSetChanged()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onFailure(errorMessage: String) {
|
||||||
|
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun refreshData() {
|
||||||
|
NetworkUtil().fetchCategory(
|
||||||
|
bean?.key!!,
|
||||||
|
offset,
|
||||||
|
pageSize,
|
||||||
|
object : NetworkCallback<List<CategoryDataBean>> {
|
||||||
|
@SuppressLint("NotifyDataSetChanged")
|
||||||
|
override fun onSuccess(data: List<CategoryDataBean>) {
|
||||||
|
contentBeans.addAll(data[0].items)
|
||||||
|
runOnUiThread {
|
||||||
|
if (!isFinishing) {
|
||||||
|
binding.refreshLayout.finishLoadMore()
|
||||||
|
val startPosition = contentBeans.size
|
||||||
|
contentBeans.addAll(data[0].items)
|
||||||
|
offset += pageSize
|
||||||
|
adapter?.notifyItemRangeInserted(startPosition, data[0].items.size)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onFailure(errorMessage: String) {
|
||||||
|
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun loadingPlay() {
|
||||||
|
binding.loadingLayout.visibility = View.VISIBLE
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun loadingClose() {
|
||||||
|
binding.loadingLayout.visibility = View.GONE
|
||||||
|
}
|
||||||
|
}
|
||||||
194
app/src/main/java/com/keyboard/craft/activity/DetailsActivity.kt
Normal file
194
app/src/main/java/com/keyboard/craft/activity/DetailsActivity.kt
Normal file
@ -0,0 +1,194 @@
|
|||||||
|
package com.keyboard.craft.activity
|
||||||
|
|
||||||
|
import android.content.Intent
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.view.View
|
||||||
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
|
import com.keyboard.craft.CraftApp
|
||||||
|
import com.keyboard.craft.R
|
||||||
|
import com.keyboard.craft.ad.MaxManager
|
||||||
|
import com.keyboard.craft.bean.DetailsBean
|
||||||
|
import com.keyboard.craft.bean.ItemDataBean
|
||||||
|
import com.keyboard.craft.databinding.UiDetailsActivityBinding
|
||||||
|
import com.keyboard.craft.util.AppSharedPreferences
|
||||||
|
import com.keyboard.craft.util.NetworkCallback
|
||||||
|
import com.keyboard.craft.util.NetworkUtil
|
||||||
|
import com.keyboard.craft.util.OnDownloadListener
|
||||||
|
import com.keyboard.craft.util.ResourceDownloadUtil
|
||||||
|
import com.keyboard.craft.util.UIHelper
|
||||||
|
import com.keyboard.craft.util.fileIsDownload
|
||||||
|
import com.keyboard.craft.util.loadRoundedImage
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.GlobalScope
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
|
|
||||||
|
class DetailsActivity : AppCompatActivity(), OnDownloadListener {
|
||||||
|
companion object {
|
||||||
|
const val KEY_CRAFT_DETAILS_BEAN = "key_details_bean"
|
||||||
|
const val KEY_CRAFT_FROM = "key_from"
|
||||||
|
}
|
||||||
|
|
||||||
|
private lateinit var binding: UiDetailsActivityBinding
|
||||||
|
private var bean: ItemDataBean? = null
|
||||||
|
private var detailsBean: DetailsBean? = null
|
||||||
|
|
||||||
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
super.onCreate(savedInstanceState)
|
||||||
|
binding = UiDetailsActivityBinding.inflate(layoutInflater)
|
||||||
|
setContentView(binding.root)
|
||||||
|
bean = intent.getSerializableExtra(KEY_CRAFT_DETAILS_BEAN) as ItemDataBean?
|
||||||
|
val from = intent.getStringExtra(KEY_CRAFT_FROM)
|
||||||
|
if (from == "like") {
|
||||||
|
MaxManager.startShowMaxAd(this){}
|
||||||
|
}
|
||||||
|
initBar()
|
||||||
|
initView()
|
||||||
|
initData()
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun initBar() {
|
||||||
|
UIHelper.setupStatusBar(this, true, binding.view)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun initView() {
|
||||||
|
GlobalScope.launch {
|
||||||
|
val current = CraftApp.databaseManager.getItemDataBeanFileByPath(bean?.key!!)
|
||||||
|
withContext(Dispatchers.Main){
|
||||||
|
if (current?.isLiked == true) {
|
||||||
|
binding.likeImg.setImageResource(R.drawable.like_select_icon)
|
||||||
|
} else {
|
||||||
|
binding.likeImg.setImageResource(R.drawable.like_unselect_icon)
|
||||||
|
}
|
||||||
|
bean?.isLiked = current?.isLiked == true
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
binding.likeBtn.setOnClickListener {
|
||||||
|
bean?.isLiked = bean?.isLiked != true
|
||||||
|
if (bean?.isLiked == true) {
|
||||||
|
binding.likeImg.setImageResource(R.drawable.like_select_icon)
|
||||||
|
} else {
|
||||||
|
binding.likeImg.setImageResource(R.drawable.like_unselect_icon)
|
||||||
|
}
|
||||||
|
GlobalScope.launch {
|
||||||
|
if (bean?.isLiked == true) {
|
||||||
|
CraftApp.databaseManager.insertItemDataBeanFile(bean!!)
|
||||||
|
} else {
|
||||||
|
CraftApp.databaseManager.deleteItemDataBeanFile(bean!!)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
binding.titleTv.text = bean?.title
|
||||||
|
binding.backBtn.setOnClickListener { finish() }
|
||||||
|
|
||||||
|
binding.downBtn.setOnClickListener {
|
||||||
|
detailsBean?.let {
|
||||||
|
updateDownloadBtn(it)
|
||||||
|
val resourceHandler = ResourceDownloadUtil(this) // 传入当前上下文
|
||||||
|
resourceHandler.setOnDownloadListener(this)
|
||||||
|
val imageUrl = it.themeContent.androidRawZipUrl
|
||||||
|
val b = fileIsDownload(this, imageUrl)
|
||||||
|
if (b) {
|
||||||
|
MaxManager.startShowMaxAd(this@DetailsActivity) {
|
||||||
|
AppSharedPreferences(this).setCurrentlyThemeUrl(imageUrl)
|
||||||
|
val intent = Intent(this, PreviewActivity::class.java)
|
||||||
|
intent.putExtra(PreviewActivity.KEY_PREVIEW_URL, imageUrl)
|
||||||
|
startActivity(intent)
|
||||||
|
finish()
|
||||||
|
}
|
||||||
|
|
||||||
|
} else {
|
||||||
|
binding.loadingBar.visibility = View.VISIBLE
|
||||||
|
binding.downIcon.visibility = View.GONE
|
||||||
|
resourceHandler.downloadAndExtractResources(imageUrl)//文件不存在则下载
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun initData() {
|
||||||
|
loadingPlay()
|
||||||
|
NetworkUtil().getResourceRequest(bean?.key!!, object : NetworkCallback<DetailsBean> {
|
||||||
|
override fun onSuccess(data: DetailsBean) {
|
||||||
|
detailsBean = data
|
||||||
|
updateUi(data)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onFailure(errorMessage: String) {
|
||||||
|
runOnUiThread {
|
||||||
|
if (!isFinishing && !isDestroyed) {
|
||||||
|
loadingClose()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun updateUi(data: DetailsBean) {
|
||||||
|
runOnUiThread {
|
||||||
|
if (!isFinishing && !isDestroyed) {
|
||||||
|
loadingClose()
|
||||||
|
|
||||||
|
updateDownloadBtn(data)
|
||||||
|
|
||||||
|
var url = data.themeContent.imgGif
|
||||||
|
if (url.isEmpty()) {
|
||||||
|
url = data.themeContent.img
|
||||||
|
}
|
||||||
|
loadRoundedImage(this, url, binding.imageView, 40)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun loadingPlay() {
|
||||||
|
binding.loadingLayout.visibility = View.VISIBLE
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun loadingClose() {
|
||||||
|
binding.loadingLayout.visibility = View.GONE
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun updateDownloadBtn(data: DetailsBean) {
|
||||||
|
if (fileIsDownload(this, data.themeContent.androidRawZipUrl)) {
|
||||||
|
if (AppSharedPreferences(this).getCurrentlyThemeUrl() == data.themeContent.androidRawZipUrl) {
|
||||||
|
binding.downTv.text = getString(R.string.applied)
|
||||||
|
binding.downBtn.isClickable = false
|
||||||
|
binding.downBtn.isFocusable = false
|
||||||
|
binding.downIcon.visibility = View.GONE
|
||||||
|
} else {
|
||||||
|
binding.downTv.text = getString(R.string.apply_it)
|
||||||
|
binding.downBtn.isClickable = true
|
||||||
|
binding.downBtn.isFocusable = true
|
||||||
|
binding.downIcon.visibility = View.GONE
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
binding.downTv.text = getString(R.string.download)
|
||||||
|
binding.downBtn.isClickable = true
|
||||||
|
binding.downBtn.isFocusable = true
|
||||||
|
binding.downIcon.visibility = View.VISIBLE
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDownloadComplete(isDownloaded: Boolean) {
|
||||||
|
binding.loadingBar.visibility = View.GONE
|
||||||
|
binding.downIcon.visibility = View.GONE
|
||||||
|
|
||||||
|
updateUi(detailsBean!!)
|
||||||
|
val imageUrl = detailsBean?.themeContent?.androidRawZipUrl
|
||||||
|
val b = fileIsDownload(this, imageUrl!!)
|
||||||
|
if (b) {
|
||||||
|
MaxManager.startShowMaxAd(this@DetailsActivity) {
|
||||||
|
AppSharedPreferences(this).setCurrentlyThemeUrl(imageUrl)
|
||||||
|
val intent = Intent(this, PreviewActivity::class.java)
|
||||||
|
intent.putExtra(PreviewActivity.KEY_PREVIEW_URL, imageUrl)
|
||||||
|
startActivity(intent)
|
||||||
|
finish()
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
354
app/src/main/java/com/keyboard/craft/activity/MainActivity.kt
Normal file
354
app/src/main/java/com/keyboard/craft/activity/MainActivity.kt
Normal file
@ -0,0 +1,354 @@
|
|||||||
|
package com.keyboard.craft.activity
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint
|
||||||
|
import android.content.BroadcastReceiver
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import android.content.IntentFilter
|
||||||
|
import android.graphics.Color
|
||||||
|
import android.graphics.drawable.ColorDrawable
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.os.Handler
|
||||||
|
import android.os.Looper
|
||||||
|
import android.provider.Settings
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.View
|
||||||
|
import android.view.inputmethod.InputMethodManager
|
||||||
|
import android.widget.ImageView
|
||||||
|
import android.widget.LinearLayout
|
||||||
|
import android.widget.TextView
|
||||||
|
import android.widget.Toast
|
||||||
|
import androidx.activity.OnBackPressedCallback
|
||||||
|
import androidx.appcompat.app.AlertDialog
|
||||||
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
|
import androidx.core.content.ContextCompat
|
||||||
|
import androidx.fragment.app.Fragment
|
||||||
|
import androidx.fragment.app.FragmentTransaction
|
||||||
|
import com.bumptech.glide.Glide
|
||||||
|
import com.keyboard.craft.fragment.HomeFragment
|
||||||
|
import com.keyboard.craft.fragment.LikeFragment
|
||||||
|
import com.keyboard.craft.R
|
||||||
|
import com.keyboard.craft.ad.MaxManager
|
||||||
|
import com.keyboard.craft.bean.ItemDataBean
|
||||||
|
import com.keyboard.craft.databinding.ActivityMainBinding
|
||||||
|
import com.keyboard.craft.util.UIHelper
|
||||||
|
import com.keyboard.craft.util.isMyInputMethodDefault
|
||||||
|
import com.keyboard.craft.util.isMyInputMethodEnabled
|
||||||
|
import com.keyboard.craft.util.openGooglePlayForReview
|
||||||
|
import com.keyboard.craft.util.shareAppInfo
|
||||||
|
|
||||||
|
|
||||||
|
class MainActivity : AppCompatActivity() {
|
||||||
|
private lateinit var binding: ActivityMainBinding
|
||||||
|
private var backPressedOnce = false
|
||||||
|
private val mFragments: MutableList<Fragment> = ArrayList()
|
||||||
|
private var currentIndex: Int = 0
|
||||||
|
private var mCurrentFragment: Fragment? = null
|
||||||
|
private val homeFragment = HomeFragment()
|
||||||
|
|
||||||
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
super.onCreate(savedInstanceState)
|
||||||
|
binding = ActivityMainBinding.inflate(layoutInflater)
|
||||||
|
setContentView(binding.root)
|
||||||
|
backPressedCallback()
|
||||||
|
initBar()
|
||||||
|
initView()
|
||||||
|
registerReceiver()
|
||||||
|
MaxManager.onLoadAd()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun initFragment() {
|
||||||
|
mFragments.clear()
|
||||||
|
mFragments.add(homeFragment)
|
||||||
|
mFragments.add(LikeFragment())
|
||||||
|
changeFragment(0)
|
||||||
|
updateBtnState(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun changeFragment(index: Int) {
|
||||||
|
currentIndex = index
|
||||||
|
val ft: FragmentTransaction = supportFragmentManager.beginTransaction()
|
||||||
|
if (null != mCurrentFragment) {
|
||||||
|
ft.hide(mCurrentFragment!!)
|
||||||
|
}
|
||||||
|
var fragment = supportFragmentManager.findFragmentByTag(
|
||||||
|
mFragments[currentIndex].javaClass.name
|
||||||
|
)
|
||||||
|
if (null == fragment) {
|
||||||
|
fragment = mFragments[index]
|
||||||
|
}
|
||||||
|
mCurrentFragment = fragment
|
||||||
|
|
||||||
|
if (!fragment.isAdded) {
|
||||||
|
ft.add(R.id.frame_layout, fragment, fragment.javaClass.name)
|
||||||
|
} else {
|
||||||
|
ft.show(fragment)
|
||||||
|
}
|
||||||
|
ft.commit()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun updateBtnState(index: Int) {
|
||||||
|
val icons = listOf(
|
||||||
|
Pair(R.drawable.home_select_icon, R.drawable.home_unselect_icon),
|
||||||
|
Pair(R.drawable.main_like_select_icon, R.drawable.main_like_unselect_icon),
|
||||||
|
Pair(R.drawable.home_settings_select_icon, R.drawable.home_settings_unselect_icon)
|
||||||
|
)
|
||||||
|
|
||||||
|
val imageViews = listOf(binding.homeImg, binding.likeImg)
|
||||||
|
|
||||||
|
imageViews.forEachIndexed { i, imageView ->
|
||||||
|
val (selectedIcon, unselectedIcon) = icons[i]
|
||||||
|
imageView.setImageResource(if (i == index) selectedIcon else unselectedIcon)
|
||||||
|
}
|
||||||
|
if (index == 1) {
|
||||||
|
binding.fgTitleTv.text = getString(R.string.my_favorite)
|
||||||
|
} else {
|
||||||
|
binding.fgTitleTv.text = getString(R.string.app_name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onResume() {
|
||||||
|
super.onResume()
|
||||||
|
updateSetMyInputMethodHome()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun initBar() {
|
||||||
|
UIHelper.setupStatusBar(this, true, binding.view)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private fun initClick() {
|
||||||
|
binding.homeBtn.setOnClickListener {
|
||||||
|
changeFragment(0)
|
||||||
|
updateBtnState(0)
|
||||||
|
}
|
||||||
|
binding.likeBtn.setOnClickListener {
|
||||||
|
changeFragment(1)
|
||||||
|
updateBtnState(1)
|
||||||
|
}
|
||||||
|
binding.step1HomeBtn.setOnClickListener {
|
||||||
|
val intent = Intent(Settings.ACTION_INPUT_METHOD_SETTINGS)
|
||||||
|
startActivity(intent)
|
||||||
|
}
|
||||||
|
binding.step2HomeBtn.setOnClickListener {
|
||||||
|
val imeManager = getSystemService(INPUT_METHOD_SERVICE) as InputMethodManager
|
||||||
|
imeManager.showInputMethodPicker()
|
||||||
|
}
|
||||||
|
binding.dialogHomeStepLayout.setOnClickListener {
|
||||||
|
binding.dialogHomeStepLayout.visibility = View.GONE
|
||||||
|
}
|
||||||
|
binding.drawerBtn.setOnClickListener {
|
||||||
|
binding.drawerLayout.openDrawer(binding.drawerView)
|
||||||
|
}
|
||||||
|
binding.drawerView.setOnClickListener { }
|
||||||
|
binding.applyKeyboardBtn.setOnClickListener {
|
||||||
|
val enabled = isMyInputMethodEnabled(this)
|
||||||
|
val default = isMyInputMethodDefault(this)
|
||||||
|
if (enabled && default) {
|
||||||
|
binding.dialogStepLayout.visibility = View.GONE
|
||||||
|
|
||||||
|
Toast.makeText(
|
||||||
|
this, "The keyboard has been set up successfully!", Toast.LENGTH_SHORT
|
||||||
|
).show()
|
||||||
|
} else {
|
||||||
|
binding.dialogStepLayout.visibility = View.VISIBLE
|
||||||
|
updateSetMyInputMethod()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
binding.step1Btn.setOnClickListener {
|
||||||
|
val intent = Intent(Settings.ACTION_INPUT_METHOD_SETTINGS)
|
||||||
|
startActivity(intent)
|
||||||
|
}
|
||||||
|
binding.step2Btn.setOnClickListener {
|
||||||
|
val imeManager =
|
||||||
|
this.getSystemService(AppCompatActivity.INPUT_METHOD_SERVICE) as InputMethodManager
|
||||||
|
imeManager.showInputMethodPicker()
|
||||||
|
}
|
||||||
|
binding.dialogStepLayout.setOnClickListener {
|
||||||
|
binding.dialogStepLayout.visibility = View.GONE
|
||||||
|
}
|
||||||
|
binding.shareBtn.setOnClickListener {
|
||||||
|
shareAppInfo(this)
|
||||||
|
}
|
||||||
|
binding.rateBtn.setOnClickListener {
|
||||||
|
openGooglePlayForReview(this)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressLint("UseCompatLoadingForDrawables")
|
||||||
|
private fun updateSetMyInputMethod() {
|
||||||
|
val enabled = isMyInputMethodEnabled(this)
|
||||||
|
val default = isMyInputMethodDefault(this)
|
||||||
|
if (enabled) {
|
||||||
|
binding.step1Btn.background = this.getDrawable(R.drawable.drw_gray_select_bg)
|
||||||
|
binding.step1Btn.text = "Step 1:Enabled"
|
||||||
|
binding.step1Btn.setTextColor(Color.parseColor("#000000"))
|
||||||
|
} else {
|
||||||
|
binding.step1Btn.background = this.getDrawable(R.drawable.drw_btn_bg)
|
||||||
|
binding.step1Btn.text = "Step 1:Select"
|
||||||
|
binding.step1Btn.setTextColor(Color.parseColor("#ffffff"))
|
||||||
|
}
|
||||||
|
if (default) {
|
||||||
|
binding.step2Btn.background = this.getDrawable(R.drawable.drw_gray_select_bg)
|
||||||
|
binding.step2Btn.text = "Step 2:Enabled"
|
||||||
|
binding.step1Btn.setTextColor(Color.parseColor("#000000"))
|
||||||
|
} else {
|
||||||
|
binding.step2Btn.background = this.getDrawable(R.drawable.drw_btn_bg)
|
||||||
|
binding.step2Btn.text = "Step 2:Select"
|
||||||
|
binding.step1Btn.setTextColor(Color.parseColor("#ffffff"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun backPressedCallback() {
|
||||||
|
// 注册 OnBackPressedCallback
|
||||||
|
onBackPressedDispatcher.addCallback(this, object : OnBackPressedCallback(true) {
|
||||||
|
override fun handleOnBackPressed() {
|
||||||
|
if (backPressedOnce) {
|
||||||
|
// 如果用户已经按过一次返回键,允许系统处理默认的返回操作
|
||||||
|
isEnabled = false
|
||||||
|
onBackPressedDispatcher.onBackPressed()
|
||||||
|
} else {
|
||||||
|
backPressedOnce = true
|
||||||
|
Toast.makeText(this@MainActivity, "Press again to exit!", Toast.LENGTH_SHORT)
|
||||||
|
.show()
|
||||||
|
|
||||||
|
// 两秒钟内再次按返回键取消退出操作
|
||||||
|
Handler(Looper.getMainLooper()).postDelayed({
|
||||||
|
backPressedOnce = false
|
||||||
|
}, 2000)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun initView() {
|
||||||
|
val enabled = isMyInputMethodEnabled(this)
|
||||||
|
val default = isMyInputMethodDefault(this)
|
||||||
|
if (!enabled || !default) {
|
||||||
|
binding.dialogHomeStepLayout.visibility = View.VISIBLE
|
||||||
|
updateSetMyInputMethodHome()
|
||||||
|
}
|
||||||
|
|
||||||
|
initFragment()
|
||||||
|
initClick()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun updateButtonState(
|
||||||
|
button: TextView, enabled: Boolean, textResId: Int, colorResId: Int, backgroundResId: Int
|
||||||
|
) {
|
||||||
|
button.text = getString(textResId)
|
||||||
|
button.setTextColor(ContextCompat.getColor(this, colorResId))
|
||||||
|
button.background = ContextCompat.getDrawable(this, backgroundResId)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun updateSetMyInputMethodHome() {
|
||||||
|
val enabled = isMyInputMethodEnabled(this)
|
||||||
|
val default = isMyInputMethodDefault(this)
|
||||||
|
|
||||||
|
updateButtonState(
|
||||||
|
binding.step1HomeBtn,
|
||||||
|
enabled,
|
||||||
|
if (enabled) R.string.step1_enabled else R.string.step1_select,
|
||||||
|
if (enabled) R.color.black else R.color.white,
|
||||||
|
if (enabled) R.drawable.drw_gray_select_bg else R.drawable.drw_btn_bg
|
||||||
|
)
|
||||||
|
|
||||||
|
updateButtonState(
|
||||||
|
binding.step2HomeBtn,
|
||||||
|
default,
|
||||||
|
if (default) R.string.step2_enabled else R.string.step2_select,
|
||||||
|
if (default) R.color.black else R.color.white,
|
||||||
|
if (default) R.drawable.drw_gray_select_bg else R.drawable.drw_btn_bg
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private fun showDialogRecommend() {
|
||||||
|
val dataList = homeFragment.dataList
|
||||||
|
if (dataList.isNotEmpty()) {
|
||||||
|
val list = homeFragment.getRandomItemsFromMainDataBeans(dataList, 1)
|
||||||
|
val bean = list?.get(0)
|
||||||
|
val inflater = LayoutInflater.from(this)
|
||||||
|
val dialogView = inflater.inflate(R.layout.dialog_recommend, null)
|
||||||
|
val okBtn = dialogView.findViewById<ImageView>(R.id.go_btn)
|
||||||
|
val cancelBtn = dialogView.findViewById<LinearLayout>(R.id.cha_btn)
|
||||||
|
val img = dialogView.findViewById<ImageView>(R.id.img)
|
||||||
|
Glide.with(this).load(bean?.thumbUrl).into(img)
|
||||||
|
val dialogBuilder = AlertDialog.Builder(this).setView(dialogView)
|
||||||
|
val dialog = dialogBuilder.create()
|
||||||
|
dialog.window?.setBackgroundDrawable(ColorDrawable(Color.TRANSPARENT))
|
||||||
|
dialog.show()
|
||||||
|
okBtn.setOnClickListener {
|
||||||
|
dialog.dismiss()
|
||||||
|
val intent = Intent(this, DetailsActivity::class.java)
|
||||||
|
intent.putExtra(DetailsActivity.KEY_CRAFT_DETAILS_BEAN, bean)
|
||||||
|
startActivity(intent)
|
||||||
|
}
|
||||||
|
cancelBtn.setOnClickListener {
|
||||||
|
dialog.dismiss()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
val bean = ItemDataBean(
|
||||||
|
isLiked = false,
|
||||||
|
key = "225e68e251874193a884d7dd4b718586",
|
||||||
|
title = "AIGC Pretty Heart Girl",
|
||||||
|
type = 6,
|
||||||
|
thumbUrl = "https://cdn.kikakeyboard.com/picture/wallpaper/1705373685197_keyboard_preview_604*444.jpg.webp",
|
||||||
|
thumbUrlGif = ""
|
||||||
|
)
|
||||||
|
|
||||||
|
val inflater = LayoutInflater.from(this)
|
||||||
|
val dialogView = inflater.inflate(R.layout.dialog_recommend, null)
|
||||||
|
val okBtn = dialogView.findViewById<ImageView>(R.id.go_btn)
|
||||||
|
val cancelBtn = dialogView.findViewById<LinearLayout>(R.id.cha_btn)
|
||||||
|
val img = dialogView.findViewById<ImageView>(R.id.img)
|
||||||
|
Glide.with(this).load(bean.thumbUrl).into(img)
|
||||||
|
val dialogBuilder = AlertDialog.Builder(this).setView(dialogView)
|
||||||
|
val dialog = dialogBuilder.create()
|
||||||
|
dialog.window?.setBackgroundDrawable(ColorDrawable(Color.TRANSPARENT))
|
||||||
|
dialog.show()
|
||||||
|
okBtn.setOnClickListener {
|
||||||
|
dialog.dismiss()
|
||||||
|
val intent = Intent(this, DetailsActivity::class.java)
|
||||||
|
intent.putExtra(DetailsActivity.KEY_CRAFT_DETAILS_BEAN, bean)
|
||||||
|
startActivity(intent)
|
||||||
|
}
|
||||||
|
cancelBtn.setOnClickListener {
|
||||||
|
dialog.dismiss()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private var receiver: BroadcastReceiver? = null
|
||||||
|
private fun registerReceiver() {
|
||||||
|
val filter = IntentFilter(Intent.ACTION_INPUT_METHOD_CHANGED)
|
||||||
|
receiver = object : BroadcastReceiver() {
|
||||||
|
override fun onReceive(context: Context, intent: Intent) {
|
||||||
|
updateSetMyInputMethodHome()
|
||||||
|
val enabled = isMyInputMethodEnabled(this@MainActivity)
|
||||||
|
val default = isMyInputMethodDefault(this@MainActivity)
|
||||||
|
if (enabled && default) {
|
||||||
|
binding.dialogHomeStepLayout.visibility = View.GONE
|
||||||
|
if (!isFinishing && !isDestroyed) {
|
||||||
|
showDialogRecommend()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
binding.dialogHomeStepLayout.visibility = View.VISIBLE
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
registerReceiver(receiver, filter)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun unregisterReceiver() {
|
||||||
|
if (receiver != null) {
|
||||||
|
unregisterReceiver(receiver)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDestroy() {
|
||||||
|
super.onDestroy()
|
||||||
|
unregisterReceiver()
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
173
app/src/main/java/com/keyboard/craft/activity/PreviewActivity.kt
Normal file
173
app/src/main/java/com/keyboard/craft/activity/PreviewActivity.kt
Normal file
@ -0,0 +1,173 @@
|
|||||||
|
package com.keyboard.craft.activity
|
||||||
|
|
||||||
|
import android.content.BroadcastReceiver
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import android.content.IntentFilter
|
||||||
|
import android.graphics.BitmapFactory
|
||||||
|
import android.graphics.drawable.BitmapDrawable
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.os.Handler
|
||||||
|
import android.os.Looper
|
||||||
|
import android.provider.Settings
|
||||||
|
import android.view.View
|
||||||
|
import android.view.inputmethod.InputMethodManager
|
||||||
|
import android.widget.TextView
|
||||||
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
|
import androidx.core.content.ContextCompat
|
||||||
|
import com.keyboard.craft.R
|
||||||
|
import com.keyboard.craft.ad.MaxManager
|
||||||
|
import com.keyboard.craft.databinding.UiPreviewActivityBinding
|
||||||
|
import com.keyboard.craft.util.UIHelper
|
||||||
|
import com.keyboard.craft.util.currentlyThemeUFileString
|
||||||
|
import com.keyboard.craft.util.getBitmapXXDrawable
|
||||||
|
import com.keyboard.craft.util.isMyInputMethodDefault
|
||||||
|
import com.keyboard.craft.util.isMyInputMethodEnabled
|
||||||
|
import com.keyboard.craft.util.loadAndBlurImage
|
||||||
|
|
||||||
|
class PreviewActivity : AppCompatActivity() {
|
||||||
|
|
||||||
|
private lateinit var binding: UiPreviewActivityBinding
|
||||||
|
|
||||||
|
private var themeUrl = ""
|
||||||
|
|
||||||
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
super.onCreate(savedInstanceState)
|
||||||
|
binding = UiPreviewActivityBinding.inflate(layoutInflater)
|
||||||
|
setContentView(binding.root)
|
||||||
|
|
||||||
|
initBar()
|
||||||
|
initializeData()
|
||||||
|
registerReceiver()
|
||||||
|
initView()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun initializeData() {
|
||||||
|
themeUrl = intent.getStringExtra(KEY_PREVIEW_URL).orEmpty()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun initBar() {
|
||||||
|
UIHelper.setupStatusBar(this, true, binding.view)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onResume() {
|
||||||
|
super.onResume()
|
||||||
|
updateSetMyInputMethod()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun initView() {
|
||||||
|
val currentlyThemeUFile = currentlyThemeUFileString(this, themeUrl)
|
||||||
|
loadAndBlurImage(
|
||||||
|
this,
|
||||||
|
getBitmapXXDrawable(this, currentlyThemeUFile, "keyboard_preview_screenshot.jpg"),
|
||||||
|
binding.previewIv
|
||||||
|
)
|
||||||
|
|
||||||
|
val imagePath =
|
||||||
|
"$currentlyThemeUFile/res/drawable-xxhdpi-v4/keyboard_preview.jpg"
|
||||||
|
val bitmap = BitmapFactory.decodeFile(imagePath)
|
||||||
|
val background = BitmapDrawable(resources, bitmap)
|
||||||
|
binding.keyboardIv.background = background
|
||||||
|
|
||||||
|
binding.backBtn.setOnClickListener { onBackPressed() }
|
||||||
|
|
||||||
|
binding.activateBtn.setOnClickListener {
|
||||||
|
binding.dialogStepLayout.visibility = View.VISIBLE
|
||||||
|
}
|
||||||
|
|
||||||
|
binding.step1Btn.setOnClickListener {
|
||||||
|
val intent = Intent(Settings.ACTION_INPUT_METHOD_SETTINGS)
|
||||||
|
startActivity(intent)
|
||||||
|
}
|
||||||
|
|
||||||
|
binding.step2Btn.setOnClickListener {
|
||||||
|
val imeManager = getSystemService(INPUT_METHOD_SERVICE) as InputMethodManager
|
||||||
|
imeManager.showInputMethodPicker()
|
||||||
|
}
|
||||||
|
|
||||||
|
binding.dialogStepLayout.setOnClickListener {
|
||||||
|
binding.dialogStepLayout.visibility = View.GONE
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun updateButtonState(button: TextView, enabled: Boolean, textResId: Int, colorResId: Int, backgroundResId: Int) {
|
||||||
|
button.text = getString(textResId)
|
||||||
|
button.setTextColor(ContextCompat.getColor(this, colorResId))
|
||||||
|
button.background = ContextCompat.getDrawable(this, backgroundResId)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun updateSetMyInputMethod() {
|
||||||
|
val enabled = isMyInputMethodEnabled(this)
|
||||||
|
val default = isMyInputMethodDefault(this)
|
||||||
|
|
||||||
|
updateButtonState(
|
||||||
|
binding.step1Btn,
|
||||||
|
enabled,
|
||||||
|
if (enabled) R.string.step1_enabled else R.string.step1_select,
|
||||||
|
if (enabled) R.color.black else R.color.white,
|
||||||
|
if (enabled) R.drawable.drw_gray_select_bg else R.drawable.drw_btn_bg
|
||||||
|
)
|
||||||
|
|
||||||
|
updateButtonState(
|
||||||
|
binding.step2Btn,
|
||||||
|
default,
|
||||||
|
if (default) R.string.step2_enabled else R.string.step2_select,
|
||||||
|
if (default) R.color.black else R.color.white,
|
||||||
|
if (default) R.drawable.drw_gray_select_bg else R.drawable.drw_btn_bg
|
||||||
|
)
|
||||||
|
|
||||||
|
binding.edit.visibility = if (enabled && default) View.VISIBLE else View.GONE
|
||||||
|
binding.tv.visibility = if (enabled && default) View.GONE else View.VISIBLE
|
||||||
|
|
||||||
|
if (enabled && default) {
|
||||||
|
binding.unauthorizedLayout.visibility = View.INVISIBLE
|
||||||
|
showKeyboard(binding.edit)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private var receiver: BroadcastReceiver? = null
|
||||||
|
private fun registerReceiver() {
|
||||||
|
val filter = IntentFilter(Intent.ACTION_INPUT_METHOD_CHANGED)
|
||||||
|
receiver = object : BroadcastReceiver() {
|
||||||
|
override fun onReceive(context: Context, intent: Intent) {
|
||||||
|
updateSetMyInputMethod()
|
||||||
|
val enabled = isMyInputMethodEnabled(this@PreviewActivity)
|
||||||
|
val default = isMyInputMethodDefault(this@PreviewActivity)
|
||||||
|
if (enabled && default) {
|
||||||
|
binding.dialogStepLayout.visibility = View.GONE
|
||||||
|
binding.edit.visibility = View.VISIBLE
|
||||||
|
binding.tv.visibility = View.GONE
|
||||||
|
showKeyboard(binding.edit)
|
||||||
|
} else {
|
||||||
|
binding.dialogStepLayout.visibility = View.VISIBLE
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
registerReceiver(receiver, filter)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun unregisterReceiver() {
|
||||||
|
if (receiver != null) {
|
||||||
|
unregisterReceiver(receiver)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDestroy() {
|
||||||
|
super.onDestroy()
|
||||||
|
unregisterReceiver()
|
||||||
|
MaxManager.onLoadAd()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun showKeyboard(view: View) {
|
||||||
|
Handler(Looper.getMainLooper()).postDelayed({
|
||||||
|
view.requestFocus()
|
||||||
|
val imm = getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
|
||||||
|
imm.showSoftInput(view, InputMethodManager.SHOW_IMPLICIT)
|
||||||
|
}, 1000)
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
const val KEY_PREVIEW_URL = "key_preview_url"
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@ -0,0 +1,62 @@
|
|||||||
|
package com.keyboard.craft.activity
|
||||||
|
|
||||||
|
import android.content.Intent
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.os.CountDownTimer
|
||||||
|
import android.util.Log
|
||||||
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
|
import com.gyf.immersionbar.ktx.immersionBar
|
||||||
|
import com.keyboard.craft.ad.WelComManager
|
||||||
|
import com.keyboard.craft.databinding.UiSplashActivityBinding
|
||||||
|
import com.keyboard.craft.util.upload.Http.makeGetRequest
|
||||||
|
import com.keyboard.craft.util.upload.SaveUtils.isPost
|
||||||
|
|
||||||
|
|
||||||
|
class SplashActivity : AppCompatActivity() {
|
||||||
|
|
||||||
|
private val SPLASH_TIME_OUT: Long = 10000
|
||||||
|
|
||||||
|
private lateinit var binding: UiSplashActivityBinding
|
||||||
|
private lateinit var timer: CountDownTimer
|
||||||
|
|
||||||
|
|
||||||
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
super.onCreate(savedInstanceState)
|
||||||
|
binding = UiSplashActivityBinding.inflate(layoutInflater)
|
||||||
|
setContentView(binding.root)
|
||||||
|
immersionBar {
|
||||||
|
fullScreen(true)
|
||||||
|
statusBarDarkFont(true)
|
||||||
|
}
|
||||||
|
timer = WelComManager.initTimer(
|
||||||
|
activity = this,
|
||||||
|
countTime = SPLASH_TIME_OUT,
|
||||||
|
countAction = { millisUntilFinished ->
|
||||||
|
// 更新UI,比如显示剩余秒数
|
||||||
|
Log.d("WelcomeActivity", "倒计时剩余:${millisUntilFinished / 1000}s")
|
||||||
|
},
|
||||||
|
goMainAction = {
|
||||||
|
// 倒计时或广告关闭后跳转主界面
|
||||||
|
startMain()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
val post = isPost
|
||||||
|
if (!post) {
|
||||||
|
makeGetRequest(this)
|
||||||
|
isPost = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun startMain() {
|
||||||
|
val intent = Intent(this, MainActivity::class.java)
|
||||||
|
startActivity(intent)
|
||||||
|
finish()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDestroy() {
|
||||||
|
super.onDestroy()
|
||||||
|
timer.cancel()
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
11
app/src/main/java/com/keyboard/craft/ad/MaxListener.java
Normal file
11
app/src/main/java/com/keyboard/craft/ad/MaxListener.java
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
package com.keyboard.craft.ad;
|
||||||
|
|
||||||
|
import com.applovin.mediation.MaxAd;
|
||||||
|
|
||||||
|
public interface MaxListener {
|
||||||
|
void onFail(MaxAd ad);
|
||||||
|
|
||||||
|
void onShowSuccess(MaxAd ad);
|
||||||
|
|
||||||
|
void onHidden();
|
||||||
|
}
|
||||||
170
app/src/main/java/com/keyboard/craft/ad/MaxManager.java
Normal file
170
app/src/main/java/com/keyboard/craft/ad/MaxManager.java
Normal file
@ -0,0 +1,170 @@
|
|||||||
|
package com.keyboard.craft.ad;
|
||||||
|
|
||||||
|
import android.app.Activity;
|
||||||
|
import android.util.Log;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
|
||||||
|
import com.applovin.mediation.MaxAd;
|
||||||
|
import com.applovin.mediation.MaxAdListener;
|
||||||
|
import com.applovin.mediation.MaxError;
|
||||||
|
import com.applovin.mediation.ads.MaxInterstitialAd;
|
||||||
|
import com.keyboard.craft.CraftApp;
|
||||||
|
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public class MaxManager {
|
||||||
|
|
||||||
|
private static final String one_AD = "6217952675c49896";
|
||||||
|
private static final String two_Ad = "0e4283e22b286755";
|
||||||
|
private static final String three_ad = "acb1fb42b3034b0a";
|
||||||
|
|
||||||
|
|
||||||
|
public static final int type_no_cache = 0;
|
||||||
|
public static final int type_has_cache = 1;
|
||||||
|
public static final int type_show_success = 2;
|
||||||
|
public static final int type_show_close = 3;
|
||||||
|
public static final int type_show_fail = 4;
|
||||||
|
|
||||||
|
private static List<MaxInterstitialAd> adList = new ArrayList<>();
|
||||||
|
|
||||||
|
public static MaxInterstitialAd getAd(List<MaxInterstitialAd> list) {
|
||||||
|
Collections.shuffle(list);
|
||||||
|
for (MaxInterstitialAd ad : list) {
|
||||||
|
if (ad.isReady()) {
|
||||||
|
return ad;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static List<MaxInterstitialAd> onLoadAd() {
|
||||||
|
if (adList.isEmpty()) {
|
||||||
|
MaxInterstitialAd AdT = new MaxInterstitialAd(two_Ad, CraftApp.app);
|
||||||
|
MaxInterstitialAd AdOne = new MaxInterstitialAd(one_AD, CraftApp.app);
|
||||||
|
MaxInterstitialAd AdThree = new MaxInterstitialAd(three_ad, CraftApp.app);
|
||||||
|
adList.add(AdOne);
|
||||||
|
adList.add(AdT);
|
||||||
|
adList.add(AdThree);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (MaxInterstitialAd ad : adList) {
|
||||||
|
if (!ad.isReady()) {
|
||||||
|
setMyListener(ad, new MaxListener() {
|
||||||
|
@Override
|
||||||
|
public void onFail(MaxAd ad) {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onShowSuccess(MaxAd ad) {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onHidden() {
|
||||||
|
|
||||||
|
}
|
||||||
|
});
|
||||||
|
ad.loadAd();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
return adList;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void setMyListener(MaxInterstitialAd ad, MaxListener maxListener) {
|
||||||
|
ad.setListener(new MaxAdListener() {
|
||||||
|
@Override
|
||||||
|
public void onAdLoaded(@NonNull MaxAd maxAd) {
|
||||||
|
Log.d(CraftApp.TAG, "-------onAdLoaded-----maxAd=" + maxAd.getAdUnitId());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onAdDisplayed(@NonNull MaxAd maxAd) {
|
||||||
|
Log.d(CraftApp.TAG, "-------onAdDisplayed-----maxAd=" + maxAd.getAdUnitId());
|
||||||
|
maxListener.onShowSuccess(maxAd);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onAdHidden(@NonNull MaxAd maxAd) {
|
||||||
|
Log.d(CraftApp.TAG, "-------onAdHidden-----maxAd=" + maxAd.getAdUnitId());
|
||||||
|
maxListener.onHidden();
|
||||||
|
setMyListener(ad, new MaxListener() {
|
||||||
|
@Override
|
||||||
|
public void onFail(MaxAd ad) {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onShowSuccess(MaxAd ad) {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onHidden() {
|
||||||
|
|
||||||
|
}
|
||||||
|
});
|
||||||
|
ad.loadAd();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onAdClicked(@NonNull MaxAd maxAd) {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onAdLoadFailed(@NonNull String s, @NonNull MaxError maxError) {
|
||||||
|
Log.d(CraftApp.TAG, "-------onAdLoadFailed-----s=" + s+"----maxError="+maxError.getMessage());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onAdDisplayFailed(@NonNull MaxAd maxAd, @NonNull MaxError maxError) {
|
||||||
|
maxListener.onFail(maxAd);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
protected static void ShowAd(Activity activity, onAdStatusListener listener) {
|
||||||
|
MaxInterstitialAd ad = MaxManager.getAd(adList);
|
||||||
|
if (ad == null) {
|
||||||
|
listener.onAdStatus(type_no_cache);
|
||||||
|
} else {
|
||||||
|
listener.onAdStatus(type_has_cache);
|
||||||
|
MaxManager.setMyListener(ad, new MaxListener() {
|
||||||
|
@Override
|
||||||
|
public void onFail(MaxAd ad) {
|
||||||
|
listener.onAdStatus(type_show_fail);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onShowSuccess(MaxAd ad) {
|
||||||
|
listener.onAdStatus(type_show_success);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onHidden() {
|
||||||
|
listener.onAdStatus(type_show_close);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
ad.showAd(activity);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public static void startShowMaxAd(Activity activity, onAdAfterAction listener) {
|
||||||
|
MaxManager.ShowAd(activity, type -> {
|
||||||
|
if (type == MaxManager.type_show_close || type == MaxManager.type_show_fail || type == MaxManager.type_no_cache) {
|
||||||
|
if (listener != null)
|
||||||
|
listener.onAction();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
76
app/src/main/java/com/keyboard/craft/ad/WelComManager.kt
Normal file
76
app/src/main/java/com/keyboard/craft/ad/WelComManager.kt
Normal file
@ -0,0 +1,76 @@
|
|||||||
|
package com.keyboard.craft.ad
|
||||||
|
|
||||||
|
import android.app.Activity
|
||||||
|
import android.content.BroadcastReceiver
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import android.content.IntentFilter
|
||||||
|
import android.os.CountDownTimer
|
||||||
|
import android.util.Log
|
||||||
|
import androidx.localbroadcastmanager.content.LocalBroadcastManager
|
||||||
|
import com.applovin.mediation.ads.MaxInterstitialAd
|
||||||
|
import com.keyboard.craft.CraftApp
|
||||||
|
|
||||||
|
|
||||||
|
object WelComManager {
|
||||||
|
|
||||||
|
private lateinit var timer: CountDownTimer
|
||||||
|
|
||||||
|
private var need_Show = true
|
||||||
|
|
||||||
|
private lateinit var lists: List<MaxInterstitialAd>
|
||||||
|
|
||||||
|
@JvmStatic
|
||||||
|
fun initTimer(activity: Activity, countTime: Long,countAction: (Long) -> Unit, goMainAction: () -> Unit): CountDownTimer {
|
||||||
|
need_Show = true
|
||||||
|
timer = object : CountDownTimer(countTime, 100) {
|
||||||
|
override fun onTick(millisUntilFinished: Long) {
|
||||||
|
countAction.invoke(millisUntilFinished)
|
||||||
|
if (need_Show) {
|
||||||
|
MaxManager.ShowAd(activity) {
|
||||||
|
Log.d(CraftApp.TAG, "--onTick----------it=$it")
|
||||||
|
if (it == MaxManager.type_has_cache) {
|
||||||
|
need_Show = false
|
||||||
|
}
|
||||||
|
if (it == MaxManager.type_show_close || it == MaxManager.type_show_fail) {
|
||||||
|
Log.d(CraftApp.TAG, "--onTick---------enter")
|
||||||
|
goMainAction.invoke()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onFinish() {
|
||||||
|
if (need_Show) {
|
||||||
|
MaxManager.ShowAd(activity) {
|
||||||
|
if (it == MaxManager.type_show_close || it == MaxManager.type_show_fail || it == MaxManager.type_no_cache) {
|
||||||
|
Log.d(CraftApp.TAG, "--onFinish---------enter")
|
||||||
|
goMainAction.invoke()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
startAd(activity)
|
||||||
|
return timer
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun startAd(activity: Activity) {
|
||||||
|
if (!CraftApp.app.initSDkOK) {
|
||||||
|
LocalBroadcastManager.getInstance(activity)
|
||||||
|
.registerReceiver(object : BroadcastReceiver() {
|
||||||
|
override fun onReceive(context: Context?, intent: Intent?) {
|
||||||
|
lists = MaxManager.onLoadAd()
|
||||||
|
timer.start()
|
||||||
|
Log.d(CraftApp.TAG, "------------1sucess")
|
||||||
|
}
|
||||||
|
}, IntentFilter(CraftApp.app.initAction))
|
||||||
|
} else {
|
||||||
|
lists = MaxManager.onLoadAd()
|
||||||
|
timer.start()
|
||||||
|
Log.d(CraftApp.TAG, "------------2sucess")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
@ -0,0 +1,6 @@
|
|||||||
|
package com.keyboard.craft.ad;
|
||||||
|
|
||||||
|
public interface onAdAfterAction {
|
||||||
|
|
||||||
|
void onAction();
|
||||||
|
}
|
||||||
@ -0,0 +1,6 @@
|
|||||||
|
package com.keyboard.craft.ad;
|
||||||
|
|
||||||
|
public interface onAdStatusListener {
|
||||||
|
|
||||||
|
void onAdStatus(int type);
|
||||||
|
}
|
||||||
@ -0,0 +1,51 @@
|
|||||||
|
package com.keyboard.craft.adapter
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import android.widget.ImageView
|
||||||
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
|
import com.bumptech.glide.Glide
|
||||||
|
import com.keyboard.craft.R
|
||||||
|
import com.keyboard.craft.bean.ItemDataBean
|
||||||
|
import com.keyboard.craft.activity.DetailsActivity
|
||||||
|
import com.youth.banner.adapter.BannerAdapter
|
||||||
|
|
||||||
|
class HomeBannerImgAdapter(private val context: Context, data: List<ItemDataBean>?) :
|
||||||
|
BannerAdapter<ItemDataBean, HomeBannerImgAdapter.BannerViewHolder>(
|
||||||
|
data
|
||||||
|
) {
|
||||||
|
|
||||||
|
class BannerViewHolder(var imageView: ImageView) :
|
||||||
|
RecyclerView.ViewHolder(imageView)
|
||||||
|
|
||||||
|
override fun onBindView(
|
||||||
|
holder: BannerViewHolder,
|
||||||
|
data: ItemDataBean,
|
||||||
|
position: Int,
|
||||||
|
size: Int
|
||||||
|
) {
|
||||||
|
Glide.with(context)
|
||||||
|
.load(data.thumbUrl)
|
||||||
|
.error(R.drawable.drw_banner_placeholder)
|
||||||
|
.placeholder(R.drawable.drw_banner_placeholder)
|
||||||
|
.into(holder.imageView)
|
||||||
|
|
||||||
|
holder.itemView.setOnClickListener {
|
||||||
|
val intent = Intent(context, DetailsActivity::class.java)
|
||||||
|
intent.putExtra(DetailsActivity.KEY_CRAFT_DETAILS_BEAN, data)
|
||||||
|
context.startActivity(intent)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
override fun onCreateHolder(parent: ViewGroup?, viewType: Int): BannerViewHolder {
|
||||||
|
val imageView = ImageView(parent!!.context)
|
||||||
|
imageView.layoutParams = ViewGroup.LayoutParams(
|
||||||
|
ViewGroup.LayoutParams.MATCH_PARENT,
|
||||||
|
ViewGroup.LayoutParams.MATCH_PARENT
|
||||||
|
)
|
||||||
|
imageView.scaleType = ImageView.ScaleType.CENTER_CROP
|
||||||
|
return BannerViewHolder(imageView)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
@ -0,0 +1,50 @@
|
|||||||
|
package com.keyboard.craft.adapter
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
|
import com.keyboard.craft.activity.CategoryDetailsActivity
|
||||||
|
import com.keyboard.craft.activity.CategoryDetailsActivity.Companion.CATEGORY_DETAILS_BEAN_KEY
|
||||||
|
import com.keyboard.craft.bean.MainDataBean
|
||||||
|
import com.keyboard.craft.databinding.MainRvItemBinding
|
||||||
|
|
||||||
|
class HomeDataAdapter(private val context: Context, private val mainDataList: List<MainDataBean>) :
|
||||||
|
RecyclerView.Adapter<HomeDataAdapter.MainDataViewHolder>() {
|
||||||
|
|
||||||
|
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MainDataViewHolder {
|
||||||
|
val binding = MainRvItemBinding.inflate(
|
||||||
|
LayoutInflater.from(parent.context),
|
||||||
|
parent,
|
||||||
|
false
|
||||||
|
)
|
||||||
|
return MainDataViewHolder(binding)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onBindViewHolder(holder: MainDataViewHolder, position: Int) {
|
||||||
|
val currentItem = mainDataList[position]
|
||||||
|
holder.bind(currentItem)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getItemCount() = mainDataList.size
|
||||||
|
|
||||||
|
inner class MainDataViewHolder(private val binding: MainRvItemBinding) :
|
||||||
|
RecyclerView.ViewHolder(binding.root) {
|
||||||
|
|
||||||
|
fun bind(mainData: MainDataBean) {
|
||||||
|
binding.titleTextView.text = mainData.title
|
||||||
|
val adapter = KeyItemDataAdapter(context, mainData.items!!)
|
||||||
|
binding.itemRv.layoutManager =
|
||||||
|
LinearLayoutManager(context, LinearLayoutManager.HORIZONTAL, false)
|
||||||
|
binding.itemRv.adapter = adapter
|
||||||
|
|
||||||
|
binding.moreData.setOnClickListener {
|
||||||
|
val intent = Intent(context, CategoryDetailsActivity::class.java)
|
||||||
|
intent.putExtra(CATEGORY_DETAILS_BEAN_KEY, mainData)
|
||||||
|
context.startActivity(intent)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,55 @@
|
|||||||
|
package com.keyboard.craft.adapter
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
|
import com.bumptech.glide.Glide
|
||||||
|
import com.keyboard.craft.activity.DetailsActivity
|
||||||
|
import com.keyboard.craft.bean.ItemDataBean
|
||||||
|
import com.keyboard.craft.databinding.AdapterCategoryDetailsItemBinding
|
||||||
|
|
||||||
|
class KeyDetailsDataAdapter(
|
||||||
|
private val context: Context,
|
||||||
|
private val list: List<ItemDataBean>,
|
||||||
|
private val from: String
|
||||||
|
) :
|
||||||
|
RecyclerView.Adapter<KeyDetailsDataAdapter.MainDataViewHolder>() {
|
||||||
|
|
||||||
|
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MainDataViewHolder {
|
||||||
|
val binding = AdapterCategoryDetailsItemBinding.inflate(
|
||||||
|
LayoutInflater.from(parent.context),
|
||||||
|
parent,
|
||||||
|
false
|
||||||
|
)
|
||||||
|
return MainDataViewHolder(binding)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onBindViewHolder(holder: MainDataViewHolder, position: Int) {
|
||||||
|
val currentItem = list[position]
|
||||||
|
holder.bind(currentItem)
|
||||||
|
holder.itemView.setOnClickListener {
|
||||||
|
val intent = Intent(context, DetailsActivity::class.java)
|
||||||
|
intent.putExtra(DetailsActivity.KEY_CRAFT_DETAILS_BEAN, currentItem)
|
||||||
|
intent.putExtra(DetailsActivity.KEY_CRAFT_FROM, from)
|
||||||
|
context.startActivity(intent)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getItemCount() = list.size
|
||||||
|
|
||||||
|
inner class MainDataViewHolder(private val binding: AdapterCategoryDetailsItemBinding) :
|
||||||
|
RecyclerView.ViewHolder(binding.root) {
|
||||||
|
|
||||||
|
fun bind(data: ItemDataBean) {
|
||||||
|
var url = data.thumbUrlGif
|
||||||
|
if (url.isEmpty()) {
|
||||||
|
url = data.thumbUrl
|
||||||
|
}
|
||||||
|
Glide.with(context)
|
||||||
|
.load(url)
|
||||||
|
.into(binding.imageView)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,60 @@
|
|||||||
|
package com.keyboard.craft.adapter
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import android.util.Log
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
|
import com.bumptech.glide.Glide
|
||||||
|
import com.keyboard.craft.activity.DetailsActivity
|
||||||
|
import com.keyboard.craft.activity.DetailsActivity.Companion.KEY_CRAFT_DETAILS_BEAN
|
||||||
|
import com.keyboard.craft.util.LogUtil
|
||||||
|
import com.keyboard.craft.bean.ItemDataBean
|
||||||
|
import com.keyboard.craft.databinding.HorizontalScrollingRvItemBinding
|
||||||
|
|
||||||
|
class KeyItemDataAdapter(
|
||||||
|
private val context: Context,
|
||||||
|
private val mainDataList: List<ItemDataBean>
|
||||||
|
) :
|
||||||
|
RecyclerView.Adapter<KeyItemDataAdapter.MainDataViewHolder>() {
|
||||||
|
|
||||||
|
inner class MainDataViewHolder(private val binding: HorizontalScrollingRvItemBinding) :
|
||||||
|
RecyclerView.ViewHolder(binding.root) {
|
||||||
|
|
||||||
|
fun bind(bean: ItemDataBean) {
|
||||||
|
binding.apply {
|
||||||
|
var url = bean.thumbUrlGif
|
||||||
|
if (url.isEmpty()) {
|
||||||
|
url = bean.thumbUrl
|
||||||
|
}
|
||||||
|
LogUtil.logMsgD("url->${url}")
|
||||||
|
Glide.with(context)
|
||||||
|
.load(url)
|
||||||
|
.into(hsRvImg)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MainDataViewHolder {
|
||||||
|
val binding = HorizontalScrollingRvItemBinding.inflate(
|
||||||
|
LayoutInflater.from(parent.context),
|
||||||
|
parent,
|
||||||
|
false
|
||||||
|
)
|
||||||
|
return MainDataViewHolder(binding)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onBindViewHolder(holder: MainDataViewHolder, position: Int) {
|
||||||
|
val currentItem = mainDataList[position]
|
||||||
|
holder.bind(currentItem)
|
||||||
|
holder.itemView.setOnClickListener {
|
||||||
|
Log.d("ocean","currentItem ->$currentItem")
|
||||||
|
val intent = Intent(context, DetailsActivity::class.java)
|
||||||
|
intent.putExtra(KEY_CRAFT_DETAILS_BEAN,currentItem)
|
||||||
|
context.startActivity(intent)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getItemCount() = mainDataList.size
|
||||||
|
}
|
||||||
10
app/src/main/java/com/keyboard/craft/bean/Author.kt
Normal file
10
app/src/main/java/com/keyboard/craft/bean/Author.kt
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
package com.keyboard.craft.bean
|
||||||
|
|
||||||
|
import java.io.Serializable
|
||||||
|
|
||||||
|
data class Author(
|
||||||
|
val name: String,
|
||||||
|
val key: String,
|
||||||
|
val photoUrl: String,
|
||||||
|
val homeUrl: String
|
||||||
|
):Serializable
|
||||||
@ -0,0 +1,12 @@
|
|||||||
|
package com.keyboard.craft.bean
|
||||||
|
|
||||||
|
import java.io.Serializable
|
||||||
|
|
||||||
|
data class CategoryDataBean(
|
||||||
|
val layout: Int,
|
||||||
|
val grid: Int,
|
||||||
|
val type: Int,
|
||||||
|
val title: String,
|
||||||
|
val key: String,
|
||||||
|
var items: List<ItemDataBean>
|
||||||
|
) : Serializable
|
||||||
7
app/src/main/java/com/keyboard/craft/bean/Content.kt
Normal file
7
app/src/main/java/com/keyboard/craft/bean/Content.kt
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
package com.keyboard.craft.bean
|
||||||
|
|
||||||
|
import java.io.Serializable
|
||||||
|
|
||||||
|
data class Content(
|
||||||
|
val imageUrl: String
|
||||||
|
) : Serializable
|
||||||
16
app/src/main/java/com/keyboard/craft/bean/DetailsBean.kt
Normal file
16
app/src/main/java/com/keyboard/craft/bean/DetailsBean.kt
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
package com.keyboard.craft.bean
|
||||||
|
|
||||||
|
import java.io.Serializable
|
||||||
|
|
||||||
|
data class DetailsBean(
|
||||||
|
val key: String,
|
||||||
|
val title: String,
|
||||||
|
val type: Int,
|
||||||
|
val thumbUrl: String,
|
||||||
|
val pkgName: String,
|
||||||
|
val thumbUrlGif: String,
|
||||||
|
val content: Content,
|
||||||
|
val themeContent: ThemeDetailsContent,
|
||||||
|
val author: Author,
|
||||||
|
val lock: LockBean
|
||||||
|
) : Serializable
|
||||||
22
app/src/main/java/com/keyboard/craft/bean/ItemDataBean.kt
Normal file
22
app/src/main/java/com/keyboard/craft/bean/ItemDataBean.kt
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
package com.keyboard.craft.bean
|
||||||
|
|
||||||
|
import androidx.annotation.Keep
|
||||||
|
import androidx.room.ColumnInfo
|
||||||
|
import androidx.room.Entity
|
||||||
|
import androidx.room.PrimaryKey
|
||||||
|
import java.io.Serializable
|
||||||
|
|
||||||
|
@Keep
|
||||||
|
@Entity
|
||||||
|
data class ItemDataBean(
|
||||||
|
@ColumnInfo(name = "isLiked") var isLiked: Boolean,
|
||||||
|
@ColumnInfo(name = "key") var key: String,
|
||||||
|
@ColumnInfo(name = "title") var title: String,
|
||||||
|
@ColumnInfo(name = "type") var type: Int,
|
||||||
|
@ColumnInfo(name = "thumbUrl") var thumbUrl: String,
|
||||||
|
@ColumnInfo(name = "thumbUrlGif") var thumbUrlGif: String,
|
||||||
|
) : Serializable {
|
||||||
|
@PrimaryKey(autoGenerate = true)
|
||||||
|
var id: Long = 0
|
||||||
|
}
|
||||||
|
|
||||||
7
app/src/main/java/com/keyboard/craft/bean/LockBean.kt
Normal file
7
app/src/main/java/com/keyboard/craft/bean/LockBean.kt
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
package com.keyboard.craft.bean
|
||||||
|
|
||||||
|
import java.io.Serializable
|
||||||
|
|
||||||
|
data class LockBean(
|
||||||
|
val type: Int
|
||||||
|
) : Serializable
|
||||||
13
app/src/main/java/com/keyboard/craft/bean/MainDataBean.kt
Normal file
13
app/src/main/java/com/keyboard/craft/bean/MainDataBean.kt
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
package com.keyboard.craft.bean
|
||||||
|
|
||||||
|
import java.io.Serializable
|
||||||
|
|
||||||
|
data class MainDataBean(
|
||||||
|
val layout: Int,
|
||||||
|
val grid: Int,
|
||||||
|
val type: Int,
|
||||||
|
var title: String,
|
||||||
|
val key: String,
|
||||||
|
val mainItemDataBean: MainItemDataBean,
|
||||||
|
var items: List<ItemDataBean>? = null,
|
||||||
|
) : Serializable
|
||||||
@ -0,0 +1,11 @@
|
|||||||
|
package com.keyboard.craft.bean
|
||||||
|
|
||||||
|
import java.io.Serializable
|
||||||
|
|
||||||
|
data class MainItemDataBean(
|
||||||
|
val key: String,
|
||||||
|
val title: String,
|
||||||
|
val type: Int,
|
||||||
|
val thumbUrl: String
|
||||||
|
) : Serializable
|
||||||
|
|
||||||
@ -0,0 +1,7 @@
|
|||||||
|
package com.keyboard.craft.bean
|
||||||
|
|
||||||
|
import java.io.Serializable
|
||||||
|
|
||||||
|
data class ThemeContentBean(
|
||||||
|
val pushIcon: String
|
||||||
|
) : Serializable
|
||||||
@ -0,0 +1,13 @@
|
|||||||
|
package com.keyboard.craft.bean
|
||||||
|
|
||||||
|
import java.io.Serializable
|
||||||
|
|
||||||
|
data class ThemeDetailsContent(
|
||||||
|
val img: String,
|
||||||
|
val imgBanner: String,
|
||||||
|
val imgPreviewGif: String,
|
||||||
|
val pushIcon: String,
|
||||||
|
val pushBanner: String,
|
||||||
|
val androidRawZipUrl: String,
|
||||||
|
val imgGif: String
|
||||||
|
) : Serializable
|
||||||
80
app/src/main/java/com/keyboard/craft/db/DatabaseManager.kt
Normal file
80
app/src/main/java/com/keyboard/craft/db/DatabaseManager.kt
Normal file
@ -0,0 +1,80 @@
|
|||||||
|
package com.keyboard.craft.db
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import androidx.room.Room
|
||||||
|
import com.keyboard.craft.bean.ItemDataBean
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
|
|
||||||
|
class DatabaseManager private constructor(context: Context) {
|
||||||
|
|
||||||
|
private val database = Room.databaseBuilder(
|
||||||
|
context.applicationContext,
|
||||||
|
LikeDatabase::class.java, "local_kj_like_database"
|
||||||
|
).build()
|
||||||
|
|
||||||
|
private val audioFileDao = database.localLikeDao()
|
||||||
|
|
||||||
|
suspend fun insertItemDataBeanFile(audio: ItemDataBean) {
|
||||||
|
withContext(Dispatchers.IO) {
|
||||||
|
val existingItemDataBeanFile = getItemDataBeanFileByPath(audio.key)
|
||||||
|
if (existingItemDataBeanFile == null) {
|
||||||
|
audioFileDao.insertItemDataBeanFile(audio)
|
||||||
|
} else {
|
||||||
|
audioFileDao.updateItemDataBeanFile(audio)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun insertItemDataBeanFiles(audios: List<ItemDataBean>) {
|
||||||
|
withContext(Dispatchers.IO) {
|
||||||
|
for (audio in audios) {
|
||||||
|
val existingItemDataBeanFile = getItemDataBeanFileByPath(audio.key)
|
||||||
|
if (existingItemDataBeanFile == null) {
|
||||||
|
audioFileDao.insertItemDataBeanFile(audio)
|
||||||
|
} else {
|
||||||
|
audioFileDao.updateItemDataBeanFile(audio)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun getAllItemDataBeanFiles(): List<ItemDataBean> {
|
||||||
|
return withContext(Dispatchers.IO) {
|
||||||
|
audioFileDao.getAllItemDataBeanFile()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun deleteItemDataBeanFile(audioFile: ItemDataBean) {
|
||||||
|
withContext(Dispatchers.IO) {
|
||||||
|
audioFileDao.deleteItemDataBeanFile(audioFile)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun deleteAllItemDataBeanFiles() {
|
||||||
|
withContext(Dispatchers.IO) {
|
||||||
|
audioFileDao.deleteAllItemDataBeanFile()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun updateItemDataBeanFiles(audioFile: ItemDataBean) {
|
||||||
|
withContext(Dispatchers.IO) {
|
||||||
|
audioFileDao.updateItemDataBeanFile(audioFile)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun getItemDataBeanFileByPath(path: String): ItemDataBean? {
|
||||||
|
return audioFileDao.getItemDataBeanFileByPath(path)
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
@Volatile
|
||||||
|
private var instance: DatabaseManager? = null
|
||||||
|
|
||||||
|
fun getInstance(context: Context): DatabaseManager {
|
||||||
|
return instance ?: synchronized(this) {
|
||||||
|
instance ?: DatabaseManager(context).also { instance = it }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
11
app/src/main/java/com/keyboard/craft/db/LikeDatabase.kt
Normal file
11
app/src/main/java/com/keyboard/craft/db/LikeDatabase.kt
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
|
||||||
|
package com.keyboard.craft.db
|
||||||
|
|
||||||
|
import androidx.room.Database
|
||||||
|
import androidx.room.RoomDatabase
|
||||||
|
import com.keyboard.craft.bean.ItemDataBean
|
||||||
|
|
||||||
|
@Database(entities = [ItemDataBean::class], version = 1, exportSchema = false)
|
||||||
|
abstract class LikeDatabase : RoomDatabase() {
|
||||||
|
abstract fun localLikeDao(): LocalLikeDao
|
||||||
|
}
|
||||||
31
app/src/main/java/com/keyboard/craft/db/LocalLikeDao.kt
Normal file
31
app/src/main/java/com/keyboard/craft/db/LocalLikeDao.kt
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
|
||||||
|
|
||||||
|
package com.keyboard.craft.db
|
||||||
|
|
||||||
|
import androidx.room.*
|
||||||
|
import com.keyboard.craft.bean.ItemDataBean
|
||||||
|
|
||||||
|
@Dao
|
||||||
|
interface LocalLikeDao {
|
||||||
|
|
||||||
|
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||||
|
suspend fun insertItemDataBeanFile(barcode: ItemDataBean)
|
||||||
|
|
||||||
|
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||||
|
suspend fun insertItemDataBeanFiles(itemDataBeans: List<ItemDataBean>)
|
||||||
|
|
||||||
|
@Query("SELECT * FROM ItemDataBean")
|
||||||
|
suspend fun getAllItemDataBeanFile(): List<ItemDataBean>
|
||||||
|
|
||||||
|
@Delete
|
||||||
|
suspend fun deleteItemDataBeanFile(barcode: ItemDataBean)
|
||||||
|
|
||||||
|
@Query("DELETE FROM ItemDataBean")
|
||||||
|
suspend fun deleteAllItemDataBeanFile()
|
||||||
|
|
||||||
|
@Update
|
||||||
|
suspend fun updateItemDataBeanFile(itemDataBean: ItemDataBean)
|
||||||
|
|
||||||
|
@Query("SELECT * FROM ItemDataBean WHERE `key` = :path LIMIT 1")
|
||||||
|
suspend fun getItemDataBeanFileByPath(path: String): ItemDataBean?
|
||||||
|
}
|
||||||
167
app/src/main/java/com/keyboard/craft/fragment/HomeFragment.kt
Normal file
167
app/src/main/java/com/keyboard/craft/fragment/HomeFragment.kt
Normal file
@ -0,0 +1,167 @@
|
|||||||
|
package com.keyboard.craft.fragment
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.util.Log
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import androidx.fragment.app.Fragment
|
||||||
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
|
import com.keyboard.craft.CraftApp
|
||||||
|
import com.keyboard.craft.adapter.HomeBannerImgAdapter
|
||||||
|
import com.keyboard.craft.adapter.HomeDataAdapter
|
||||||
|
import com.keyboard.craft.bean.CategoryDataBean
|
||||||
|
import com.keyboard.craft.bean.ItemDataBean
|
||||||
|
import com.keyboard.craft.bean.MainDataBean
|
||||||
|
import com.keyboard.craft.databinding.FragmentHomeBinding
|
||||||
|
import com.keyboard.craft.util.LogUtil
|
||||||
|
import com.keyboard.craft.util.NetworkCallback
|
||||||
|
import com.keyboard.craft.util.NetworkUtil
|
||||||
|
import com.keyboard.craft.util.getRandomInt
|
||||||
|
import com.youth.banner.indicator.CircleIndicator
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.DelicateCoroutinesApi
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.GlobalScope
|
||||||
|
import kotlinx.coroutines.async
|
||||||
|
import kotlinx.coroutines.awaitAll
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.coroutines.suspendCancellableCoroutine
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
|
import kotlin.coroutines.resume
|
||||||
|
|
||||||
|
|
||||||
|
class HomeFragment : Fragment() {
|
||||||
|
private lateinit var binding: FragmentHomeBinding
|
||||||
|
var dataList: MutableList<MainDataBean> = mutableListOf()
|
||||||
|
private var mainAdapter: HomeDataAdapter? = null
|
||||||
|
|
||||||
|
override fun onCreateView(
|
||||||
|
inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?
|
||||||
|
): View {
|
||||||
|
binding = FragmentHomeBinding.inflate(layoutInflater)
|
||||||
|
return binding.root
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
|
super.onViewCreated(view, savedInstanceState)
|
||||||
|
initView()
|
||||||
|
initData()
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun getFetchCategory(
|
||||||
|
key: String,
|
||||||
|
offset: Int,
|
||||||
|
pageSize: Int,
|
||||||
|
): CategoryDataBean? {
|
||||||
|
return try {
|
||||||
|
suspendCancellableCoroutine<CategoryDataBean> { continuation ->
|
||||||
|
NetworkUtil().fetchCategory(key,
|
||||||
|
offset,
|
||||||
|
pageSize,
|
||||||
|
object : NetworkCallback<List<CategoryDataBean>> {
|
||||||
|
override fun onSuccess(data: List<CategoryDataBean>) {
|
||||||
|
continuation.resume(data[0])
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onFailure(errorMessage: String) {
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@OptIn(DelicateCoroutinesApi::class)
|
||||||
|
private fun initData() {
|
||||||
|
loadingPlay()
|
||||||
|
binding.noDataLayout.visibility = View.GONE
|
||||||
|
NetworkUtil().fetchData(object : NetworkCallback<List<MainDataBean>> {
|
||||||
|
@SuppressLint("NotifyDataSetChanged")
|
||||||
|
override fun onSuccess(data: List<MainDataBean>) {
|
||||||
|
GlobalScope.launch(Dispatchers.IO) {
|
||||||
|
val jobs = data.map { bean ->
|
||||||
|
GlobalScope.async(Dispatchers.IO) {
|
||||||
|
val resultCategory = getFetchCategory(
|
||||||
|
bean.key, getRandomInt(1, 15),
|
||||||
|
getRandomInt(5, 16),
|
||||||
|
)
|
||||||
|
bean.items = resultCategory?.items!!
|
||||||
|
bean
|
||||||
|
}
|
||||||
|
}
|
||||||
|
val list = awaitAll(*jobs.toTypedArray())
|
||||||
|
withContext(Dispatchers.Main) {
|
||||||
|
dataList.clear()
|
||||||
|
dataList.addAll(list)
|
||||||
|
Log.d(CraftApp.TAG, "--------------dataList=${dataList.size}")
|
||||||
|
mainAdapter?.notifyDataSetChanged()
|
||||||
|
loadingClose()
|
||||||
|
if (dataList.size > 0) {
|
||||||
|
binding.noDataLayout.visibility = View.GONE
|
||||||
|
} else {
|
||||||
|
binding.noDataLayout.visibility = View.VISIBLE
|
||||||
|
}
|
||||||
|
|
||||||
|
val randomList = getRandomItemsFromMainDataBeans(dataList, 5)
|
||||||
|
if (randomList != null) {
|
||||||
|
binding.banner.addBannerLifecycleObserver(requireActivity())//添加生命周期观察者
|
||||||
|
.setAdapter(HomeBannerImgAdapter(requireActivity(), randomList))
|
||||||
|
.setIndicator(CircleIndicator(requireActivity()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onFailure(errorMessage: String) {
|
||||||
|
CoroutineScope(Dispatchers.Main).launch {
|
||||||
|
LogUtil.logMsgD(errorMessage)
|
||||||
|
binding.noDataLayout.visibility = View.VISIBLE
|
||||||
|
loadingClose()
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// numberOfItems需要的数量
|
||||||
|
fun getRandomItemsFromMainDataBeans(
|
||||||
|
mainDataBeans: List<MainDataBean>,
|
||||||
|
numberOfItems: Int
|
||||||
|
): List<ItemDataBean>? {
|
||||||
|
val randomMainDataBeans = mainDataBeans.shuffled().take(2)
|
||||||
|
val items = randomMainDataBeans.flatMap { it.items ?: emptyList() }
|
||||||
|
return if (items.size >= numberOfItems) {
|
||||||
|
items.shuffled().take(numberOfItems)
|
||||||
|
} else {
|
||||||
|
null // 如果总数不足,返回 null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun initClick() {
|
||||||
|
binding.tryAgain.setOnClickListener {
|
||||||
|
initData()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun initView() {
|
||||||
|
binding.rv.layoutManager = LinearLayoutManager(requireActivity())
|
||||||
|
mainAdapter = HomeDataAdapter(requireActivity(), dataList)
|
||||||
|
binding.rv.adapter = mainAdapter
|
||||||
|
|
||||||
|
|
||||||
|
initClick()
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private fun loadingPlay() {
|
||||||
|
binding.loadingLayout.visibility = View.VISIBLE
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun loadingClose() {
|
||||||
|
binding.loadingLayout.visibility = View.GONE
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,76 @@
|
|||||||
|
package com.keyboard.craft.fragment
|
||||||
|
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import androidx.fragment.app.Fragment
|
||||||
|
import androidx.recyclerview.widget.GridLayoutManager
|
||||||
|
import com.keyboard.craft.CraftApp
|
||||||
|
import com.keyboard.craft.adapter.KeyDetailsDataAdapter
|
||||||
|
import com.keyboard.craft.bean.ItemDataBean
|
||||||
|
import com.keyboard.craft.databinding.FragmentLikeBinding
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.GlobalScope
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
|
|
||||||
|
class LikeFragment : Fragment() {
|
||||||
|
private lateinit var binding: FragmentLikeBinding
|
||||||
|
private var adapter: KeyDetailsDataAdapter? = null
|
||||||
|
private var contentBeans: MutableList<ItemDataBean> = mutableListOf()
|
||||||
|
|
||||||
|
|
||||||
|
override fun onCreateView(
|
||||||
|
inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?
|
||||||
|
): View {
|
||||||
|
binding = FragmentLikeBinding.inflate(layoutInflater)
|
||||||
|
return binding.root
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
|
super.onViewCreated(view, savedInstanceState)
|
||||||
|
initView()
|
||||||
|
initData()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onResume() {
|
||||||
|
super.onResume()
|
||||||
|
initData()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun initView() {
|
||||||
|
adapter = KeyDetailsDataAdapter(requireActivity(), contentBeans, "like")
|
||||||
|
binding.rv.layoutManager = GridLayoutManager(requireActivity(), 2)
|
||||||
|
binding.rv.adapter = adapter
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun initData() {
|
||||||
|
loadingPlay()
|
||||||
|
binding.noDataLayout.visibility = View.GONE
|
||||||
|
GlobalScope.launch {
|
||||||
|
val beans = CraftApp.databaseManager.getAllItemDataBeanFiles()
|
||||||
|
if (beans.isNotEmpty()) {
|
||||||
|
contentBeans.clear()
|
||||||
|
contentBeans.addAll(beans)
|
||||||
|
withContext(Dispatchers.Main) {
|
||||||
|
loadingClose()
|
||||||
|
binding.noDataLayout.visibility = View.GONE
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
withContext(Dispatchers.Main) {
|
||||||
|
loadingClose()
|
||||||
|
binding.noDataLayout.visibility = View.VISIBLE
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun loadingPlay() {
|
||||||
|
binding.loadingLayout.visibility = View.VISIBLE
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun loadingClose() {
|
||||||
|
binding.loadingLayout.visibility = View.GONE
|
||||||
|
}
|
||||||
|
}
|
||||||
373
app/src/main/java/com/keyboard/craft/service/KeyboardService.kt
Normal file
373
app/src/main/java/com/keyboard/craft/service/KeyboardService.kt
Normal file
@ -0,0 +1,373 @@
|
|||||||
|
package com.keyboard.craft.service
|
||||||
|
|
||||||
|
import android.graphics.BitmapFactory
|
||||||
|
import android.graphics.drawable.BitmapDrawable
|
||||||
|
import android.inputmethodservice.InputMethodService
|
||||||
|
import android.net.Uri
|
||||||
|
import android.view.View
|
||||||
|
import android.view.inputmethod.EditorInfo
|
||||||
|
import android.view.inputmethod.InputConnection
|
||||||
|
import android.widget.VideoView
|
||||||
|
import com.keyboard.craft.R
|
||||||
|
import com.keyboard.craft.util.AppSharedPreferences
|
||||||
|
import com.keyboard.craft.util.colorsXmlPullParser
|
||||||
|
import com.keyboard.craft.util.currentlyThemeUFileString
|
||||||
|
import com.keyboard.craft.util.getBitmapDrawable
|
||||||
|
import com.keyboard.craft.util.getPath
|
||||||
|
import com.keyboard.craft.util.getRawVideoPath
|
||||||
|
import com.keyboard.craft.util.getStateDrawable
|
||||||
|
import com.keyboard.craft.view.CraftKeyboardView
|
||||||
|
import com.keyboard.craft.view.CraftKeyBoard
|
||||||
|
import java.io.File
|
||||||
|
|
||||||
|
class KeyboardService : InputMethodService() {
|
||||||
|
|
||||||
|
private lateinit var keyboardView: CraftKeyboardView
|
||||||
|
private lateinit var currentKeyboard: com.keyboard.craft.view.Keyboard
|
||||||
|
private var isAllCaps = false
|
||||||
|
private var isCap = false
|
||||||
|
private lateinit var rootView: View
|
||||||
|
|
||||||
|
override fun onCreateInputView(): View {
|
||||||
|
rootView = layoutInflater.inflate(R.layout.custom_keyboard, null) // 这里使用了一个容器布局
|
||||||
|
keyboardView = rootView.findViewById(R.id.keyboardView) // 在容器布局中找到 KeyboardView 的引用
|
||||||
|
currentKeyboard = com.keyboard.craft.view.Keyboard(this, R.xml.my_custom_keyboard_layout)
|
||||||
|
keyboardView.keyboard = currentKeyboard
|
||||||
|
keyboardView.setOnKeyboardActionListener(onKeyboardActionListener)
|
||||||
|
|
||||||
|
isCap = keyboardView.isCap()
|
||||||
|
isAllCaps = keyboardView.isAllCaps()
|
||||||
|
|
||||||
|
return rootView // 返回包含 KeyboardView 的容器布局
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onWindowShown() {
|
||||||
|
super.onWindowShown()
|
||||||
|
updateConfig()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun updateConfig() {
|
||||||
|
val currentlyUrl = AppSharedPreferences(this).getCurrentlyThemeUrl()
|
||||||
|
if (currentlyUrl.isNotEmpty()) {
|
||||||
|
val currentlyThemeUFile = currentlyThemeUFileString(this, currentlyUrl)
|
||||||
|
val config = CraftKeyboardView.Config(this)
|
||||||
|
config.keyBackground = getStateDrawable(this,
|
||||||
|
getPath(currentlyThemeUFile, "btn_keyboard_key_normal_normal.9.png"),
|
||||||
|
getPath(currentlyThemeUFile, "btn_keyboard_key_normal_pressed.9.png")
|
||||||
|
)
|
||||||
|
config.specialKeyBackground = getStateDrawable(this,
|
||||||
|
getPath(currentlyThemeUFile, "btn_keyboard_spacekey_normal_normal.9.png"),
|
||||||
|
getPath(currentlyThemeUFile, "btn_keyboard_spacekey_normal_pressed.9.png")
|
||||||
|
)
|
||||||
|
config.deleteDrawable = getStateDrawable(this,
|
||||||
|
getPath(currentlyThemeUFile, "sym_keyboard_delete_normal.png"),
|
||||||
|
getPath(currentlyThemeUFile, "sym_keyboard_delete_pressed.png")
|
||||||
|
)
|
||||||
|
config.toggleKeyBackground = getStateDrawable(this,
|
||||||
|
getPath(currentlyThemeUFile, "btn_keyboard_key_toggle_normal_on.9.png"),
|
||||||
|
getPath(currentlyThemeUFile, "btn_keyboard_key_toggle_pressed_on.9.png")
|
||||||
|
)
|
||||||
|
|
||||||
|
val colorMap = colorsXmlPullParser(currentlyThemeUFile)
|
||||||
|
colorMap.forEach { (name, value) ->
|
||||||
|
if (name == "key_text_color_normal") {
|
||||||
|
config.keyTextColor = value
|
||||||
|
config.keySpecialTextColor = value
|
||||||
|
}
|
||||||
|
if (name == "key_text_color_functional") {
|
||||||
|
config.toggleKeyTextColor = value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
config.capitalDrawable =
|
||||||
|
getBitmapDrawable(this,currentlyThemeUFile, "sym_keyboard_shift_locked.png")
|
||||||
|
config.lowerDrawable = getBitmapDrawable(this,currentlyThemeUFile, "sym_keyboard_shift.png")
|
||||||
|
config.capitalLockDrawable =
|
||||||
|
getBitmapDrawable(this,currentlyThemeUFile, "sym_keyboard_shift_locked.png")
|
||||||
|
|
||||||
|
val videoView = rootView.findViewById<VideoView>(R.id.videoView)
|
||||||
|
val videoPath = getRawVideoPath(currentlyThemeUFile)
|
||||||
|
if (File(videoPath).exists()) {
|
||||||
|
// 设置视频路径并启动播放
|
||||||
|
videoView.setVideoURI(Uri.parse(videoPath))
|
||||||
|
videoView.setOnPreparedListener { mp ->
|
||||||
|
mp.isLooping = true // 循环播放
|
||||||
|
videoView.start()
|
||||||
|
}
|
||||||
|
videoView.visibility = View.VISIBLE
|
||||||
|
} else {
|
||||||
|
val imagePath =
|
||||||
|
"$currentlyThemeUFile/res/drawable-xxhdpi-v4/keyboard_background.jpg"
|
||||||
|
val bitmap = BitmapFactory.decodeFile(imagePath)
|
||||||
|
val background = BitmapDrawable(resources, bitmap)
|
||||||
|
rootView.background = background
|
||||||
|
videoView.visibility = View.GONE
|
||||||
|
}
|
||||||
|
keyboardView.setConfig(config)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private val onKeyboardActionListener = object : com.keyboard.craft.view.KeyboardView.OnKeyboardActionListener {
|
||||||
|
override fun onKey(primaryCode: Int, keyCodes: IntArray?) {
|
||||||
|
val ic = currentInputConnection
|
||||||
|
performKey(ic, primaryCode, keyCodes)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onPress(primaryCode: Int) {}
|
||||||
|
|
||||||
|
override fun onRelease(primaryCode: Int) {}
|
||||||
|
|
||||||
|
override fun onText(text: CharSequence?) {}
|
||||||
|
|
||||||
|
override fun swipeLeft() {}
|
||||||
|
|
||||||
|
override fun swipeRight() {}
|
||||||
|
|
||||||
|
override fun swipeDown() {}
|
||||||
|
|
||||||
|
override fun swipeUp() {}
|
||||||
|
}
|
||||||
|
|
||||||
|
private var keyboardType = KeyboardType.NORMAL
|
||||||
|
|
||||||
|
private val keyboardNormal by lazy {
|
||||||
|
com.keyboard.craft.view.Keyboard(
|
||||||
|
this,
|
||||||
|
R.xml.my_custom_keyboard_layout
|
||||||
|
)
|
||||||
|
}
|
||||||
|
private val keyboardNormalModeChange by lazy {
|
||||||
|
com.keyboard.craft.view.Keyboard(
|
||||||
|
this,
|
||||||
|
R.xml.keyboard_mode_change
|
||||||
|
)
|
||||||
|
}
|
||||||
|
private val keyboardNormalMore by lazy {
|
||||||
|
com.keyboard.craft.view.Keyboard(
|
||||||
|
this,
|
||||||
|
R.xml.keyboard_more_symbol
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据 primaryCode去做相应的处理
|
||||||
|
*/
|
||||||
|
private fun performKey(ic: InputConnection, primaryCode: Int, keyCodes: IntArray?) {
|
||||||
|
when (primaryCode) {
|
||||||
|
CraftKeyBoard.KEYCODE_SHIFT -> keyShift()
|
||||||
|
CraftKeyBoard.KEYCODE_MODE_CHANGE -> keyModeChange()
|
||||||
|
CraftKeyBoard.KEYCODE_CANCEL -> keyCancel(primaryCode)
|
||||||
|
CraftKeyBoard.KEYCODE_DONE -> keyDone(primaryCode)
|
||||||
|
CraftKeyBoard.KEYCODE_DELETE -> keyDelete(ic)
|
||||||
|
CraftKeyBoard.KEYCODE_MODE_BACK -> keyBack(false)
|
||||||
|
CraftKeyBoard.KEYCODE_BACK -> keyBack(true)
|
||||||
|
CraftKeyBoard.KEYCODE_MORE -> keyMore()
|
||||||
|
//无效的按键值,打印相关日志
|
||||||
|
else -> commitText(ic, primaryCode)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun commitText(ic: InputConnection, primaryCode: Int) {
|
||||||
|
val code = primaryCode.toChar().toString()
|
||||||
|
ic.commitText(code, 1)
|
||||||
|
|
||||||
|
if (isCap && !isAllCaps) {//如果当前是大写键盘,并且并且没有锁定,则自动变换成小写键盘
|
||||||
|
isCap = false
|
||||||
|
isAllCaps = false
|
||||||
|
toLowerCaseKey(currentKeyboard)
|
||||||
|
|
||||||
|
keyboardView.run {
|
||||||
|
setCap(isCap)
|
||||||
|
setAllCaps(isAllCaps)
|
||||||
|
keyboard = currentKeyboard
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* 触发删除
|
||||||
|
*/
|
||||||
|
private fun keyDelete(ic: InputConnection) {
|
||||||
|
ic.deleteSurroundingText(1, 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 触发Shift,切换大小字母键盘
|
||||||
|
*/
|
||||||
|
private fun keyShift() {
|
||||||
|
//将键盘进行大小写键盘切换
|
||||||
|
if (isAllCaps) {//上次状态为大写锁定时,转换为小写
|
||||||
|
toLowerCaseKey(currentKeyboard)
|
||||||
|
} else {//反之上次状态即为小写时,转换为大写
|
||||||
|
toUpperCaseKey(currentKeyboard)
|
||||||
|
}
|
||||||
|
|
||||||
|
when {
|
||||||
|
isAllCaps -> {//上次状态为锁定时,此次状态将改变为小写,将变量状态改变
|
||||||
|
isAllCaps = false
|
||||||
|
isCap = false
|
||||||
|
}
|
||||||
|
|
||||||
|
isCap -> {//上次状态为非锁定,此次状态改变为锁定
|
||||||
|
isAllCaps = true
|
||||||
|
}
|
||||||
|
|
||||||
|
else -> {//上次状态为小写(默认),此次状态改变为大写
|
||||||
|
isCap = true
|
||||||
|
isAllCaps = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
keyboardView.let {
|
||||||
|
it.setCap(isCap)
|
||||||
|
it.setAllCaps(isAllCaps)
|
||||||
|
it.keyboard = currentKeyboard
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 转换为大写
|
||||||
|
*/
|
||||||
|
private fun toUpperCaseKey(keyboard: com.keyboard.craft.view.Keyboard) {
|
||||||
|
keyboard.run {
|
||||||
|
for (key in keys) {
|
||||||
|
if (key.label?.length == 1) {// 一个字符
|
||||||
|
var c = key.label.toString()[0]
|
||||||
|
if (c.isLowerCase()) { //是小写字母
|
||||||
|
//转换为大写
|
||||||
|
val letter = c.toUpperCase()
|
||||||
|
key.label = letter.toString()
|
||||||
|
key.codes[0] = letter.toInt()
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 转换为小写
|
||||||
|
*/
|
||||||
|
private fun toLowerCaseKey(keyboard: com.keyboard.craft.view.Keyboard) {
|
||||||
|
keyboard.run {
|
||||||
|
for (key in keys) {
|
||||||
|
if (key.label?.length == 1) {// 一个字符
|
||||||
|
var c = key.label.toString()[0]
|
||||||
|
if (c.isUpperCase()) { //是大写字母
|
||||||
|
//转换为小写
|
||||||
|
val letter = c.toLowerCase()
|
||||||
|
key.label = letter.toString()
|
||||||
|
key.codes[0] = letter.toInt()
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 模式改变,切换键盘
|
||||||
|
*/
|
||||||
|
private fun keyModeChange() {
|
||||||
|
when (keyboardType) {
|
||||||
|
CraftKeyBoard.KeyboardType.NORMAL -> {
|
||||||
|
keyboardType = KeyboardType.NORMAL_MODE_CHANGE
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
switchKeyboard()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun switchKeyboard() {
|
||||||
|
when (keyboardType) {
|
||||||
|
KeyboardType.NORMAL -> {
|
||||||
|
currentKeyboard = keyboardNormal
|
||||||
|
}
|
||||||
|
|
||||||
|
KeyboardType.NORMAL_MODE_CHANGE -> {
|
||||||
|
currentKeyboard = keyboardNormalModeChange
|
||||||
|
}
|
||||||
|
|
||||||
|
KeyboardType.NORMAL_MORE -> {
|
||||||
|
currentKeyboard = keyboardNormalMore
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
keyboardView.run {
|
||||||
|
keyboard = currentKeyboard
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 取消,关闭键盘
|
||||||
|
*/
|
||||||
|
private fun keyCancel(primaryCode: Int) {
|
||||||
|
val ic = currentInputConnection
|
||||||
|
ic?.performEditorAction(EditorInfo.IME_ACTION_DONE)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 完成
|
||||||
|
*/
|
||||||
|
private fun keyDone(primaryCode: Int) {
|
||||||
|
val ic = currentInputConnection
|
||||||
|
ic?.performEditorAction(EditorInfo.IME_ACTION_DONE)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 返回
|
||||||
|
*/
|
||||||
|
private fun keyBack(isBack: Boolean) {
|
||||||
|
when (keyboardType) {
|
||||||
|
KeyboardType.NORMAL_MODE_CHANGE -> {
|
||||||
|
keyboardType = KeyboardType.NORMAL
|
||||||
|
}
|
||||||
|
|
||||||
|
KeyboardType.NORMAL_MORE -> {
|
||||||
|
keyboardType = if (isBack) KeyboardType.NORMAL else KeyboardType.NORMAL_MODE_CHANGE
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
switchKeyboard()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更多
|
||||||
|
*/
|
||||||
|
private fun keyMore() {
|
||||||
|
when (keyboardType) {
|
||||||
|
KeyboardType.NORMAL -> {
|
||||||
|
keyboardType = KeyboardType.NORMAL_MORE
|
||||||
|
}
|
||||||
|
|
||||||
|
KeyboardType.NORMAL_MODE_CHANGE -> {
|
||||||
|
keyboardType = KeyboardType.NORMAL_MORE
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
switchKeyboard()
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
object KeyboardType {
|
||||||
|
/**
|
||||||
|
* 默认键盘 - 字母带符号
|
||||||
|
*/
|
||||||
|
const val NORMAL = 0x00000001
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 默认键盘 - 切换键盘
|
||||||
|
*/
|
||||||
|
internal const val NORMAL_MODE_CHANGE = 0x00000002
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 默认键盘 - 更多
|
||||||
|
*/
|
||||||
|
internal const val NORMAL_MORE = 0x00000003
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@ -0,0 +1,65 @@
|
|||||||
|
package com.keyboard.craft.util
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.SharedPreferences
|
||||||
|
|
||||||
|
class AppSharedPreferences(context: Context) {
|
||||||
|
companion object {
|
||||||
|
private const val KEY_CURRENTLY_THEME_URL = "key_craft_currently_theme_url"
|
||||||
|
private const val KEY_CURRENTLY_THEME_GIF_URL = "key_craft_currently_theme_gif_url"
|
||||||
|
}
|
||||||
|
|
||||||
|
private val sharedPreferences: SharedPreferences =
|
||||||
|
context.getSharedPreferences(context.packageName, Context.MODE_PRIVATE)
|
||||||
|
|
||||||
|
fun getCurrentlyThemeUrl(): String {
|
||||||
|
return getString(KEY_CURRENTLY_THEME_URL, "")
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setCurrentlyThemeUrl(string: String) {
|
||||||
|
saveString(KEY_CURRENTLY_THEME_URL, string)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getCurrentlyThemeGifUrl(): String {
|
||||||
|
return getString(KEY_CURRENTLY_THEME_GIF_URL, "")
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setCurrentlyThemeGifUrl(string: String) {
|
||||||
|
saveString(KEY_CURRENTLY_THEME_GIF_URL, string)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun saveString(key: String, value: String) {
|
||||||
|
val editor = sharedPreferences.edit()
|
||||||
|
editor.putString(key, value)
|
||||||
|
editor.apply()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getString(key: String, defaultValue: String = ""): String {
|
||||||
|
return sharedPreferences.getString(key, defaultValue) ?: defaultValue
|
||||||
|
}
|
||||||
|
|
||||||
|
fun saveInt(key: String, value: Int) {
|
||||||
|
val editor = sharedPreferences.edit()
|
||||||
|
editor.putInt(key, value)
|
||||||
|
editor.apply()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getInt(key: String, defaultValue: Int = 0): Int {
|
||||||
|
return sharedPreferences.getInt(key, defaultValue)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// 删除特定键对应的值
|
||||||
|
fun removeKey(key: String) {
|
||||||
|
val editor = sharedPreferences.edit()
|
||||||
|
editor.remove(key)
|
||||||
|
editor.apply()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 清空 SharedPreferences 中的所有数据
|
||||||
|
fun clearAll() {
|
||||||
|
val editor = sharedPreferences.edit()
|
||||||
|
editor.clear()
|
||||||
|
editor.apply()
|
||||||
|
}
|
||||||
|
}
|
||||||
261
app/src/main/java/com/keyboard/craft/util/AppUtil.kt
Normal file
261
app/src/main/java/com/keyboard/craft/util/AppUtil.kt
Normal file
@ -0,0 +1,261 @@
|
|||||||
|
package com.keyboard.craft.util
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import android.content.pm.PackageManager
|
||||||
|
import android.graphics.BitmapFactory
|
||||||
|
import android.graphics.Color
|
||||||
|
import android.graphics.drawable.BitmapDrawable
|
||||||
|
import android.graphics.drawable.Drawable
|
||||||
|
import android.graphics.drawable.StateListDrawable
|
||||||
|
import android.net.Uri
|
||||||
|
import android.provider.Settings
|
||||||
|
import android.util.Xml
|
||||||
|
import android.view.inputmethod.InputMethodManager
|
||||||
|
import android.widget.ImageView
|
||||||
|
import androidx.core.app.ShareCompat
|
||||||
|
import com.bumptech.glide.Glide
|
||||||
|
import com.bumptech.glide.load.resource.bitmap.RoundedCorners
|
||||||
|
import com.bumptech.glide.request.RequestOptions
|
||||||
|
import com.keyboard.craft.bean.MainDataBean
|
||||||
|
import jp.wasabeef.glide.transformations.BlurTransformation
|
||||||
|
import org.xmlpull.v1.XmlPullParser
|
||||||
|
import java.io.File
|
||||||
|
import java.io.StringReader
|
||||||
|
import kotlin.random.Random
|
||||||
|
|
||||||
|
fun getRandomInt(from: Int, until: Int): Int {
|
||||||
|
var num = 0
|
||||||
|
val random = Random.Default
|
||||||
|
num = random.nextInt(from, until)
|
||||||
|
return num
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getRandomThumbUrl(data: List<MainDataBean>): String? {
|
||||||
|
// 首先随机选择一个 MainDataBean
|
||||||
|
val mainDataBean = data.randomOrNull()
|
||||||
|
|
||||||
|
// 确保 mainDataBean 和 items 不为空
|
||||||
|
val items = mainDataBean?.items ?: return null
|
||||||
|
|
||||||
|
// 随机选择一个 ItemDataBean
|
||||||
|
val itemDataBean = items.randomOrNull()
|
||||||
|
|
||||||
|
// 返回对应的 thumbUrl
|
||||||
|
return itemDataBean?.thumbUrl
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
fun getFileNameFromUrl(urlString: String): String {
|
||||||
|
val uri = Uri.parse(urlString)
|
||||||
|
val path = uri.path ?: urlString // 如果没有路径,将使用整个 URL
|
||||||
|
val file = File(path)
|
||||||
|
return file.name // 获取文件名部分
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getSubDirectories(directory: File): List<String> {
|
||||||
|
val subDirectories = mutableListOf<String>()
|
||||||
|
|
||||||
|
if (directory.exists() && directory.isDirectory) {
|
||||||
|
val subDirectoryFiles = directory.listFiles { file -> file.isDirectory }
|
||||||
|
subDirectoryFiles?.forEach { subDir ->
|
||||||
|
subDirectories.add(subDir.name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return subDirectories
|
||||||
|
}
|
||||||
|
|
||||||
|
fun removeFileExtension(fileName: String): String {
|
||||||
|
return fileName.substringBeforeLast(".")
|
||||||
|
}
|
||||||
|
|
||||||
|
fun fileIsDownload(context: Context, zipUrl: String): Boolean {
|
||||||
|
val destinationFolder = context.filesDir.absolutePath
|
||||||
|
val childString = getFileNameFromUrl(zipUrl)
|
||||||
|
val file = File(destinationFolder, removeFileExtension(childString))
|
||||||
|
var getSubDirectories = ""
|
||||||
|
if (file.exists() && file.isDirectory) {
|
||||||
|
getSubDirectories = getSubDirectories(file)[0]
|
||||||
|
}
|
||||||
|
val sub = removeFileExtension(childString) + "/" + getSubDirectories
|
||||||
|
val subFile = File(destinationFolder, sub)
|
||||||
|
return subFile.exists()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun currentlyThemeUFileString(context: Context, zipUrl: String): String {
|
||||||
|
val destinationFolder = context.filesDir.absolutePath
|
||||||
|
val childString = getFileNameFromUrl(zipUrl)
|
||||||
|
val file = File(destinationFolder, removeFileExtension(childString))
|
||||||
|
var getSubDirectories = ""
|
||||||
|
if (file.exists() && file.isDirectory) {
|
||||||
|
getSubDirectories = getSubDirectories(file)[0]
|
||||||
|
}
|
||||||
|
val sub = removeFileExtension(childString) + "/" + getSubDirectories
|
||||||
|
return File(destinationFolder, sub).toString()
|
||||||
|
}
|
||||||
|
|
||||||
|
//输入法是否被启用
|
||||||
|
fun isMyInputMethodEnabled(context: Context): Boolean {
|
||||||
|
val imm = context.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
|
||||||
|
val ids = imm.enabledInputMethodList.map { it.id }
|
||||||
|
return ids.any { it.startsWith(context.packageName) }
|
||||||
|
}
|
||||||
|
|
||||||
|
//输入法是否被设置为默认输入法
|
||||||
|
fun isMyInputMethodDefault(context: Context): Boolean {
|
||||||
|
val imm = context.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
|
||||||
|
val id =
|
||||||
|
Settings.Secure.getString(context.contentResolver, Settings.Secure.DEFAULT_INPUT_METHOD)
|
||||||
|
return id != null && id.startsWith(context.packageName)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun loadRoundedImage(
|
||||||
|
context: Context,
|
||||||
|
imageUrl: String,
|
||||||
|
imageView: ImageView,
|
||||||
|
cornerRadius: Int = 10
|
||||||
|
) {
|
||||||
|
val requestOptions = RequestOptions().transform(RoundedCorners(cornerRadius))
|
||||||
|
|
||||||
|
Glide.with(context)
|
||||||
|
.load(imageUrl)
|
||||||
|
.apply(requestOptions)
|
||||||
|
.into(imageView)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 加载并给图片添加模糊效果的函数
|
||||||
|
fun loadAndBlurImage(
|
||||||
|
context: Context,
|
||||||
|
drawable: Drawable,
|
||||||
|
imageView: ImageView,
|
||||||
|
blurRadius: Int = 25
|
||||||
|
) {
|
||||||
|
Glide.with(context)
|
||||||
|
.load(drawable)
|
||||||
|
.apply(RequestOptions.bitmapTransform(BlurTransformation(blurRadius)))
|
||||||
|
.into(imageView)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getBitmapDrawable(context: Context, currentlyThemeUFile: String, name: String): Drawable {
|
||||||
|
val imagePath = "$currentlyThemeUFile/res/drawable-xhdpi-v4/$name"
|
||||||
|
val bitmap = BitmapFactory.decodeFile(imagePath)
|
||||||
|
return BitmapDrawable(context.resources, bitmap)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getBitmapXXDrawable(context: Context, currentlyThemeUFile: String, name: String): Drawable {
|
||||||
|
val imagePath = "$currentlyThemeUFile/res/drawable-xxhdpi-v4/$name"
|
||||||
|
val bitmap = BitmapFactory.decodeFile(imagePath)
|
||||||
|
return BitmapDrawable(context.resources, bitmap)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getRawVideoPath(currentlyThemeUFile: String): String {
|
||||||
|
return "$currentlyThemeUFile/res/raw/keyboard_background_video.mp4"
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getPath(currentlyThemeUFile: String, name: String): String {
|
||||||
|
return "$currentlyThemeUFile/res/drawable-xhdpi-v4/$name"
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getResColorsPath(currentlyThemeUFile: String): String {
|
||||||
|
return "$currentlyThemeUFile/res/colors.xml"
|
||||||
|
}
|
||||||
|
|
||||||
|
fun colorsXmlPullParser(currentlyThemeUFile: String): Map<String, Int> {
|
||||||
|
val colorsFile = File(getResColorsPath(currentlyThemeUFile)) // 构建文件对象
|
||||||
|
val colorsMap = mutableMapOf<String, Int>()
|
||||||
|
if (colorsFile.exists()) {
|
||||||
|
val xmlContent = colorsFile.readText() // 读取文件内容为字符串
|
||||||
|
val parser: XmlPullParser = Xml.newPullParser()
|
||||||
|
parser.setFeature(XmlPullParser.FEATURE_PROCESS_NAMESPACES, false)
|
||||||
|
parser.setInput(StringReader(xmlContent))
|
||||||
|
|
||||||
|
var eventType = parser.eventType
|
||||||
|
|
||||||
|
|
||||||
|
while (eventType != XmlPullParser.END_DOCUMENT) {
|
||||||
|
if (eventType == XmlPullParser.START_TAG && (parser.name == "color" || parser.name == "item")) {
|
||||||
|
val colorName = parser.getAttributeValue(null, "name")
|
||||||
|
val colorValue = parser.nextText()
|
||||||
|
|
||||||
|
// 只添加你需要的颜色到 map 中
|
||||||
|
if (colorName == "key_text_color_normal" ||
|
||||||
|
colorName == "key_text_color_functional"
|
||||||
|
) {
|
||||||
|
colorsMap[colorName] = Color.parseColor(colorValue)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
eventType = parser.next()
|
||||||
|
}
|
||||||
|
|
||||||
|
} else {
|
||||||
|
}
|
||||||
|
return colorsMap
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getStateDrawable(
|
||||||
|
context: Context,
|
||||||
|
defaultPath: String,
|
||||||
|
pressedPath: String
|
||||||
|
): StateListDrawable {
|
||||||
|
val defaultBitmap = BitmapFactory.decodeFile(defaultPath)
|
||||||
|
val pressedBitmap = BitmapFactory.decodeFile(pressedPath)
|
||||||
|
// 创建 StateListDrawable
|
||||||
|
val stateListDrawable = StateListDrawable()
|
||||||
|
val defaultDrawable = BitmapDrawable(context.resources, defaultBitmap)
|
||||||
|
val pressedDrawable = BitmapDrawable(context.resources, pressedBitmap)
|
||||||
|
// 添加按下状态
|
||||||
|
val pressedState = intArrayOf(android.R.attr.state_pressed)
|
||||||
|
stateListDrawable.addState(pressedState, pressedDrawable)
|
||||||
|
// 添加默认状态
|
||||||
|
val normalState = intArrayOf()
|
||||||
|
stateListDrawable.addState(normalState, defaultDrawable)
|
||||||
|
return stateListDrawable
|
||||||
|
}
|
||||||
|
|
||||||
|
fun openPrivacyPolicy(context: Context) {
|
||||||
|
val privacyPolicyUrl = "https://sites.google.com/view/privacy-policy-html-app"
|
||||||
|
|
||||||
|
val intent = Intent(Intent.ACTION_VIEW)
|
||||||
|
intent.data = Uri.parse(privacyPolicyUrl)
|
||||||
|
|
||||||
|
if (intent.resolveActivity(context.packageManager) != null) {
|
||||||
|
context.startActivity(intent)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
fun shareAppInfo(context: Context) {
|
||||||
|
val appPackageName = context.packageName
|
||||||
|
val appInfo = context.packageManager.getApplicationInfo(appPackageName, PackageManager.GET_META_DATA)
|
||||||
|
val appName = context.getString(appInfo.labelRes)
|
||||||
|
|
||||||
|
val appPlayStoreLink = "https://play.google.com/store/apps/details?id=$appPackageName"
|
||||||
|
|
||||||
|
val shareMessage = "Check out $appName on Google Play: $appPlayStoreLink"
|
||||||
|
|
||||||
|
val shareIntent = ShareCompat.IntentBuilder.from(context as androidx.appcompat.app.AppCompatActivity)
|
||||||
|
.setType("text/plain")
|
||||||
|
.setText(shareMessage)
|
||||||
|
.intent
|
||||||
|
|
||||||
|
// 判断是否有可以处理分享的应用程序
|
||||||
|
if (shareIntent.resolveActivity(context.packageManager) != null) {
|
||||||
|
context.startActivity(shareIntent)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun openGooglePlayForReview(context: Context) {
|
||||||
|
val packageName = context.packageName
|
||||||
|
try {
|
||||||
|
val intent = Intent(
|
||||||
|
Intent.ACTION_VIEW,
|
||||||
|
Uri.parse("https://play.google.com/store/apps/details?id=$packageName")
|
||||||
|
)
|
||||||
|
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||||
|
context.startActivity(intent)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
// 如果设备上没有安装 Google Play 商店应用,则使用浏览器打开
|
||||||
|
e.printStackTrace()
|
||||||
|
}
|
||||||
|
}
|
||||||
14
app/src/main/java/com/keyboard/craft/util/LogUtil.kt
Normal file
14
app/src/main/java/com/keyboard/craft/util/LogUtil.kt
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
package com.keyboard.craft.util
|
||||||
|
|
||||||
|
import android.util.Log
|
||||||
|
import com.keyboard.craft.BuildConfig
|
||||||
|
|
||||||
|
object LogUtil {
|
||||||
|
private const val LogTag = "Craft-keyboard"
|
||||||
|
|
||||||
|
fun logMsgD(msg: String) {
|
||||||
|
if (BuildConfig.DEBUG) {
|
||||||
|
Log.d(LogTag, msg)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
285
app/src/main/java/com/keyboard/craft/util/NetworkUtil.kt
Normal file
285
app/src/main/java/com/keyboard/craft/util/NetworkUtil.kt
Normal file
@ -0,0 +1,285 @@
|
|||||||
|
package com.keyboard.craft.util
|
||||||
|
|
||||||
|
import com.keyboard.craft.bean.Author
|
||||||
|
import com.keyboard.craft.bean.CategoryDataBean
|
||||||
|
import com.keyboard.craft.bean.Content
|
||||||
|
import com.keyboard.craft.bean.DetailsBean
|
||||||
|
import com.keyboard.craft.bean.ItemDataBean
|
||||||
|
import com.keyboard.craft.bean.LockBean
|
||||||
|
import com.keyboard.craft.bean.MainDataBean
|
||||||
|
import com.keyboard.craft.bean.MainItemDataBean
|
||||||
|
import com.keyboard.craft.bean.ThemeContentBean
|
||||||
|
import com.keyboard.craft.bean.ThemeDetailsContent
|
||||||
|
import okhttp3.Call
|
||||||
|
import okhttp3.Callback
|
||||||
|
import okhttp3.OkHttpClient
|
||||||
|
import okhttp3.Request
|
||||||
|
import okhttp3.Response
|
||||||
|
import org.json.JSONArray
|
||||||
|
import org.json.JSONObject
|
||||||
|
import java.io.IOException
|
||||||
|
|
||||||
|
interface NetworkCallback<T> {
|
||||||
|
fun onSuccess(data: T)
|
||||||
|
fun onFailure(errorMessage: String)
|
||||||
|
}
|
||||||
|
|
||||||
|
class NetworkUtil {
|
||||||
|
companion object {
|
||||||
|
private const val HEADER_AGENT = "User-Agent"
|
||||||
|
private const val HEADER_KEY = "User-Key"
|
||||||
|
private const val HEADER_KEY_VALUE =
|
||||||
|
"f_M2HePkT1-XKO1y9MsJzt:APA91bG_JRru9-AuBlcAE7TwyhQf4POchj2nHGH6dqsL4nZd-2HhyMzZe1eIVy9TBCMG3avCVUYxj4dcN5FgbTv4_LGV-mAtb4x-FEMLztx79vySuH5gpBV7SdPCeBsB-4NmS5OElFwe"
|
||||||
|
private const val GET_MAIN_URL = "https://backend-wallpaper.kika-backend.com/v1/api/theme/"
|
||||||
|
private const val GET_CATEGORY_URL =
|
||||||
|
GET_MAIN_URL + "page/kbtheme_main?offset=0&fetch_size=100&sign=a28aadc61c76c754944f6ddb48962c9c"
|
||||||
|
private const val GET_CATEGORY_DETAILS_URL =
|
||||||
|
GET_MAIN_URL + "category/"
|
||||||
|
private const val GET_DETAILS_URL =
|
||||||
|
GET_MAIN_URL + "resource/"
|
||||||
|
|
||||||
|
private val HEADER_AGENT_VALUE =
|
||||||
|
"com.ikeyboard.theme.neon.love/200 (2102cf82e4624c3bb94f30359139b1d4/d7b10839af1ba26bc4b64881501e6df0) Country/US Language/en System/android Version/${33} Screen/${560}"
|
||||||
|
}
|
||||||
|
|
||||||
|
private val client = OkHttpClient()
|
||||||
|
|
||||||
|
fun fetchData(callback: NetworkCallback<List<MainDataBean>>) {
|
||||||
|
val request = Request.Builder()
|
||||||
|
.url(GET_CATEGORY_URL)
|
||||||
|
.get()
|
||||||
|
.addHeader(HEADER_KEY, HEADER_KEY_VALUE)
|
||||||
|
.addHeader(HEADER_AGENT, HEADER_AGENT_VALUE)
|
||||||
|
.build()
|
||||||
|
|
||||||
|
client.newCall(request).enqueue(object : Callback {
|
||||||
|
override fun onResponse(call: Call, response: Response) {
|
||||||
|
if (response.isSuccessful) {
|
||||||
|
val jsonData = response.body?.string()
|
||||||
|
jsonData?.let {
|
||||||
|
val resultObj = JSONObject(it)
|
||||||
|
if (resultObj.optBoolean("success")) {
|
||||||
|
val dataObj = resultObj.optJSONObject("data")
|
||||||
|
dataObj?.let {
|
||||||
|
val sectionsArray = dataObj.optJSONArray("sections")
|
||||||
|
sectionsArray?.let {
|
||||||
|
callback.onSuccess(parseMainDataList(sectionsArray))
|
||||||
|
} ?: callback.onFailure("sections array null")
|
||||||
|
} ?: callback.onFailure("Empty Data ")
|
||||||
|
} else {
|
||||||
|
callback.onFailure("success != true")
|
||||||
|
}
|
||||||
|
} ?: callback.onFailure("Empty response body")
|
||||||
|
} else {
|
||||||
|
callback.onFailure("Error: ${response.code}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onFailure(call: Call, e: IOException) {
|
||||||
|
callback.onFailure("Exception: ${e.message}")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun parseMainDataList(jsonArray: JSONArray): List<MainDataBean> {
|
||||||
|
val list = mutableListOf<MainDataBean>()
|
||||||
|
|
||||||
|
for (i in 0 until jsonArray.length()) {
|
||||||
|
val jsonObject = jsonArray.getJSONObject(i)
|
||||||
|
val mainDataBean = MainDataBean(
|
||||||
|
jsonObject.getInt("layout"),
|
||||||
|
jsonObject.getInt("grid"),
|
||||||
|
jsonObject.getInt("type"),
|
||||||
|
jsonObject.getString("title"),
|
||||||
|
jsonObject.getString("key"),
|
||||||
|
parseMainItemDataBean(jsonObject.optJSONArray("items"))
|
||||||
|
)
|
||||||
|
list.add(mainDataBean)
|
||||||
|
}
|
||||||
|
return list
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun parseMainItemDataBean(jsonArray: JSONArray): MainItemDataBean {
|
||||||
|
val list = mutableListOf<MainItemDataBean>()
|
||||||
|
for (i in 0 until jsonArray.length()) {
|
||||||
|
val jsonObject = jsonArray.getJSONObject(i)
|
||||||
|
list.add(
|
||||||
|
MainItemDataBean(
|
||||||
|
jsonObject.optString("key"),
|
||||||
|
jsonObject.optString("title"),
|
||||||
|
jsonObject.optInt("type"),
|
||||||
|
jsonObject.optString("thumbUrl")
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return list[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
fun fetchCategory(
|
||||||
|
key: String,
|
||||||
|
offset: Int,
|
||||||
|
pageSize: Int,
|
||||||
|
callback: NetworkCallback<List<CategoryDataBean>>
|
||||||
|
) {
|
||||||
|
val request = Request.Builder()
|
||||||
|
.url(GET_CATEGORY_DETAILS_URL + "${key}/resources?offset=${offset}&pageSize=${pageSize}&sign=a28aadc61c76c754944f6ddb48962c9c")
|
||||||
|
.get()
|
||||||
|
.addHeader(HEADER_KEY, HEADER_KEY_VALUE)
|
||||||
|
.addHeader(HEADER_AGENT, HEADER_AGENT_VALUE)
|
||||||
|
.build()
|
||||||
|
|
||||||
|
client.newCall(request).enqueue(object : Callback {
|
||||||
|
override fun onResponse(call: Call, response: Response) {
|
||||||
|
if (response.isSuccessful) {
|
||||||
|
val jsonData = response.body?.string()
|
||||||
|
jsonData?.let {
|
||||||
|
val resultObj = JSONObject(it)
|
||||||
|
if (resultObj.optBoolean("success")) {
|
||||||
|
val dataObj = resultObj.optJSONObject("data")
|
||||||
|
dataObj?.let {
|
||||||
|
val sectionsArray = dataObj.optJSONArray("sections")
|
||||||
|
sectionsArray?.let {
|
||||||
|
callback.onSuccess(parseCategoryDataBeanList(sectionsArray))
|
||||||
|
} ?: callback.onFailure("sections array null")
|
||||||
|
} ?: callback.onFailure("Empty Data ")
|
||||||
|
} else {
|
||||||
|
callback.onFailure("success != true")
|
||||||
|
}
|
||||||
|
} ?: callback.onFailure("Empty response body")
|
||||||
|
} else {
|
||||||
|
callback.onFailure("Error: ${response.code}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onFailure(call: Call, e: IOException) {
|
||||||
|
callback.onFailure("Exception: ${e.message}")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun parseCategoryDataBeanList(jsonArray: JSONArray): List<CategoryDataBean> {
|
||||||
|
val sectionsList = mutableListOf<CategoryDataBean>()
|
||||||
|
|
||||||
|
for (i in 0 until jsonArray.length()) {
|
||||||
|
val sectionObj = jsonArray.getJSONObject(i)
|
||||||
|
val layout = sectionObj.getInt("layout")
|
||||||
|
val grid = sectionObj.getInt("grid")
|
||||||
|
val type = sectionObj.getInt("type")
|
||||||
|
val sectionTitle = sectionObj.getString("title")
|
||||||
|
val sectionKey = sectionObj.getString("key")
|
||||||
|
|
||||||
|
val itemsArray = sectionObj.getJSONArray("items")
|
||||||
|
val itemsList = mutableListOf<ItemDataBean>()
|
||||||
|
|
||||||
|
for (j in 0 until itemsArray.length()) {
|
||||||
|
val itemObj = itemsArray.getJSONObject(j)
|
||||||
|
val itemKey = itemObj.getString("key")
|
||||||
|
val itemTitle = itemObj.getString("title")
|
||||||
|
val itemType = itemObj.getInt("type")
|
||||||
|
val thumbUrl = itemObj.getString("thumbUrl")
|
||||||
|
val thumbUrlGif = itemObj.getString("thumbUrlGif")
|
||||||
|
|
||||||
|
val themeContentObj = itemObj.getJSONObject("themeContent")
|
||||||
|
val pushIcon = themeContentObj.getString("pushIcon")
|
||||||
|
val themeContent = ThemeContentBean(pushIcon)
|
||||||
|
|
||||||
|
val lockObj = itemObj.getJSONObject("lock")
|
||||||
|
val lockType = lockObj.getInt("type")
|
||||||
|
val lock = LockBean(lockType)
|
||||||
|
|
||||||
|
val item = ItemDataBean(
|
||||||
|
false,
|
||||||
|
itemKey,
|
||||||
|
itemTitle,
|
||||||
|
itemType,
|
||||||
|
thumbUrl,
|
||||||
|
thumbUrlGif,
|
||||||
|
)
|
||||||
|
itemsList.add(item)
|
||||||
|
}
|
||||||
|
|
||||||
|
val section = CategoryDataBean(layout, grid, type, sectionTitle, sectionKey, itemsList)
|
||||||
|
sectionsList.add(section)
|
||||||
|
}
|
||||||
|
return sectionsList
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getResourceRequest(
|
||||||
|
key: String,
|
||||||
|
callback: NetworkCallback<DetailsBean>
|
||||||
|
) {
|
||||||
|
|
||||||
|
val request = Request.Builder()
|
||||||
|
.url("$GET_DETAILS_URL$key?sign=a28aadc61c76c754944f6ddb48962c9c")
|
||||||
|
.get()
|
||||||
|
.addHeader(HEADER_KEY, HEADER_KEY_VALUE)
|
||||||
|
.addHeader(HEADER_AGENT, HEADER_AGENT_VALUE)
|
||||||
|
.build()
|
||||||
|
|
||||||
|
client.newCall(request).enqueue(object : Callback {
|
||||||
|
override fun onResponse(call: Call, response: Response) {
|
||||||
|
if (response.isSuccessful) {
|
||||||
|
val jsonData = response.body?.string()
|
||||||
|
jsonData?.let {
|
||||||
|
val resultObj = JSONObject(it)
|
||||||
|
if (resultObj.optBoolean("success")) {
|
||||||
|
val dataObject = resultObj.getJSONObject("data")
|
||||||
|
val itemObject = dataObject.getJSONObject("item")
|
||||||
|
callback.onSuccess(parseDetailsBean(itemObject))
|
||||||
|
} else {
|
||||||
|
callback.onFailure("success != true")
|
||||||
|
}
|
||||||
|
} ?: callback.onFailure("Empty response body")
|
||||||
|
} else {
|
||||||
|
callback.onFailure("Error: ${response.code}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onFailure(call: Call, e: IOException) {
|
||||||
|
callback.onFailure("Exception: ${e.message}")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun parseDetailsBean(itemObject: JSONObject): DetailsBean {
|
||||||
|
val key = itemObject.getString("key")
|
||||||
|
val title = itemObject.getString("title")
|
||||||
|
val type = itemObject.getInt("type")
|
||||||
|
val thumbUrl = itemObject.getString("thumbUrl")
|
||||||
|
val pkgName = itemObject.getString("pkgName")
|
||||||
|
val thumbUrlGif = itemObject.optString("thumbUrlGif")
|
||||||
|
|
||||||
|
val contentObject = itemObject.getJSONObject("content")
|
||||||
|
val imageUrl = contentObject.getString("imageUrl")
|
||||||
|
|
||||||
|
val themeContentObject = itemObject.getJSONObject("themeContent")
|
||||||
|
val img = themeContentObject.getString("img")
|
||||||
|
val imgBanner = themeContentObject.getString("imgBanner")
|
||||||
|
val pushIcon = themeContentObject.getString("pushIcon")
|
||||||
|
val pushBanner = themeContentObject.getString("pushBanner")
|
||||||
|
val androidRawZipUrl = themeContentObject.getString("androidRawZipUrl")
|
||||||
|
|
||||||
|
val authorObject = itemObject.getJSONObject("author")
|
||||||
|
val authorName = authorObject.getString("name")
|
||||||
|
val authorKey = authorObject.getString("key")
|
||||||
|
val photoUrl = authorObject.getString("photoUrl")
|
||||||
|
val homeUrl = authorObject.getString("homeUrl")
|
||||||
|
|
||||||
|
val lockObject = itemObject.getJSONObject("lock")
|
||||||
|
val lockType = lockObject.getInt("type")
|
||||||
|
|
||||||
|
return DetailsBean(
|
||||||
|
key,
|
||||||
|
title,
|
||||||
|
type,
|
||||||
|
thumbUrl,
|
||||||
|
pkgName,
|
||||||
|
thumbUrlGif,
|
||||||
|
Content(imageUrl),
|
||||||
|
ThemeDetailsContent(img, imgBanner, "", pushIcon, pushBanner, androidRawZipUrl, ""),
|
||||||
|
Author(authorName, authorKey, photoUrl, homeUrl),
|
||||||
|
LockBean(lockType)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,194 @@
|
|||||||
|
package com.keyboard.craft.util
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.os.AsyncTask
|
||||||
|
import net.sf.sevenzipjbinding.ExtractAskMode
|
||||||
|
import net.sf.sevenzipjbinding.ExtractOperationResult
|
||||||
|
import net.sf.sevenzipjbinding.IArchiveExtractCallback
|
||||||
|
import net.sf.sevenzipjbinding.IInArchive
|
||||||
|
import net.sf.sevenzipjbinding.ISequentialOutStream
|
||||||
|
import net.sf.sevenzipjbinding.PropID
|
||||||
|
import net.sf.sevenzipjbinding.SevenZip
|
||||||
|
import net.sf.sevenzipjbinding.impl.RandomAccessFileInStream
|
||||||
|
import okio.IOException
|
||||||
|
import java.io.BufferedInputStream
|
||||||
|
import java.io.File
|
||||||
|
import java.io.FileOutputStream
|
||||||
|
import java.io.RandomAccessFile
|
||||||
|
import java.net.URL
|
||||||
|
|
||||||
|
interface OnDownloadListener {
|
||||||
|
fun onDownloadComplete(isDownloaded: Boolean)
|
||||||
|
}
|
||||||
|
class ResourceDownloadUtil(private val context: Context) {
|
||||||
|
private val destinationFolder = context.filesDir.absolutePath
|
||||||
|
|
||||||
|
// 声明回调接口变量
|
||||||
|
private var downloadListener: OnDownloadListener? = null
|
||||||
|
|
||||||
|
// 设置回调监听器
|
||||||
|
fun setOnDownloadListener(listener: OnDownloadListener) {
|
||||||
|
this.downloadListener = listener
|
||||||
|
}
|
||||||
|
|
||||||
|
fun downloadAndExtractResources(zipUrl: String): Boolean {
|
||||||
|
var isDownload = false
|
||||||
|
val childString = getFileNameFromUrl(zipUrl)
|
||||||
|
val file = File(destinationFolder, removeFileExtension(childString))
|
||||||
|
var getSubDirectories = ""
|
||||||
|
if (file.exists() && file.isDirectory) {
|
||||||
|
getSubDirectories = getSubDirectories(file)[0]
|
||||||
|
}
|
||||||
|
val sub = removeFileExtension(childString) + "/" + getSubDirectories
|
||||||
|
val subFile = File(destinationFolder, sub)
|
||||||
|
if (!subFile.exists()) {
|
||||||
|
// 创建 DownloadAndExtractTask 时传递回调接口的实例
|
||||||
|
DownloadAndExtractTask(context, zipUrl, object : OnDownloadListener {
|
||||||
|
override fun onDownloadComplete(isDownloaded: Boolean) {
|
||||||
|
// 下载完成后的处理操作
|
||||||
|
// 例如:更新界面、通知用户下载完成等
|
||||||
|
downloadListener?.onDownloadComplete(isDownload)
|
||||||
|
}
|
||||||
|
}).execute()
|
||||||
|
} else {
|
||||||
|
isDownload = true
|
||||||
|
downloadListener?.onDownloadComplete(isDownload)
|
||||||
|
}
|
||||||
|
return isDownload
|
||||||
|
}
|
||||||
|
|
||||||
|
private inner class DownloadAndExtractTask(
|
||||||
|
private val context: Context,
|
||||||
|
url: String,
|
||||||
|
private val callback: OnDownloadListener
|
||||||
|
) :
|
||||||
|
AsyncTask<Void, Void, Boolean>() {
|
||||||
|
|
||||||
|
private val zipUrl = url
|
||||||
|
|
||||||
|
override fun doInBackground(vararg params: Void?): Boolean {
|
||||||
|
return try {
|
||||||
|
downloadZipFile()
|
||||||
|
extractZipFile()
|
||||||
|
true
|
||||||
|
} catch (e: Exception) {
|
||||||
|
e.printStackTrace()
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onPostExecute(result: Boolean) {
|
||||||
|
// 下载解压完成后,触发回调
|
||||||
|
callback.onDownloadComplete(result)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun downloadZipFile() {
|
||||||
|
val url = URL(zipUrl)
|
||||||
|
val connection = url.openConnection()
|
||||||
|
connection.connect()
|
||||||
|
|
||||||
|
val childString = getFileNameFromUrl(zipUrl)
|
||||||
|
val input = BufferedInputStream(url.openStream())
|
||||||
|
val outputFile = File(destinationFolder, childString)
|
||||||
|
val outputStream = FileOutputStream(outputFile)
|
||||||
|
|
||||||
|
val data = ByteArray(1024)
|
||||||
|
var count: Int
|
||||||
|
while (input.read(data, 0, 1024).also { count = it } != -1) {
|
||||||
|
outputStream.write(data, 0, count)
|
||||||
|
}
|
||||||
|
|
||||||
|
outputStream.flush()
|
||||||
|
outputStream.close()
|
||||||
|
input.close()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun extractZipFile() {
|
||||||
|
val childString = getFileNameFromUrl(zipUrl)
|
||||||
|
val newChildString = removeFileExtension(childString)
|
||||||
|
|
||||||
|
val zipFilePath = "$destinationFolder/$childString"
|
||||||
|
val destDirectory = "$destinationFolder/$newChildString"
|
||||||
|
|
||||||
|
extract7z(zipFilePath, destDirectory)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun extract7z(archivePath: String, outputPath: String) {
|
||||||
|
|
||||||
|
val archiveFile = File(archivePath)
|
||||||
|
|
||||||
|
if (!archiveFile.exists()) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
val raf = RandomAccessFile(archiveFile, "r")
|
||||||
|
val inStream = RandomAccessFileInStream(raf)
|
||||||
|
|
||||||
|
val outputDir = File(outputPath)
|
||||||
|
|
||||||
|
if (!outputDir.exists()) {
|
||||||
|
outputDir.mkdirs()
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
SevenZip.openInArchive(null, inStream).use { inArchive ->
|
||||||
|
val extractCallback = ArchiveExtractCallback(outputDir, inArchive)
|
||||||
|
inArchive.extract(null, false, extractCallback)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private class ArchiveExtractCallback(
|
||||||
|
private val outputDir: File,
|
||||||
|
private val inArchive: IInArchive
|
||||||
|
) : IArchiveExtractCallback {
|
||||||
|
override fun getStream(index: Int, extractAskMode: ExtractAskMode?): ISequentialOutStream {
|
||||||
|
val relativeFilePath = getOriginalFileName(index)
|
||||||
|
val outputFile = File(outputDir, relativeFilePath)
|
||||||
|
|
||||||
|
// Create parent directories if they don't exist
|
||||||
|
outputFile.parentFile?.let { parent ->
|
||||||
|
if (!parent.exists()) {
|
||||||
|
parent.mkdirs()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return SequentialOutStream(outputFile.absolutePath)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private fun getOriginalFileName(index: Int): String {
|
||||||
|
return inArchive.getStringProperty(index, PropID.PATH)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun prepareOperation(extractAskMode: ExtractAskMode?) {
|
||||||
|
// Implement if needed
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun setOperationResult(extractOperationResult: ExtractOperationResult?) {
|
||||||
|
// Implement if needed
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun setTotal(total: Long) {
|
||||||
|
// Implement if needed
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun setCompleted(complete: Long) {
|
||||||
|
// Implement if needed
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private class SequentialOutStream(private val fileName: String) : ISequentialOutStream {
|
||||||
|
override fun write(data: ByteArray): Int {
|
||||||
|
try {
|
||||||
|
val fileOutputStream = File(fileName).outputStream()
|
||||||
|
fileOutputStream.write(data)
|
||||||
|
fileOutputStream.close()
|
||||||
|
return data.size
|
||||||
|
} catch (e: IOException) {
|
||||||
|
e.printStackTrace()
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
14
app/src/main/java/com/keyboard/craft/util/UIHelper.kt
Normal file
14
app/src/main/java/com/keyboard/craft/util/UIHelper.kt
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
package com.keyboard.craft.util
|
||||||
|
|
||||||
|
import android.view.View
|
||||||
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
|
import com.gyf.immersionbar.ktx.immersionBar
|
||||||
|
|
||||||
|
object UIHelper {
|
||||||
|
fun setupStatusBar(activity: AppCompatActivity, darkFont: Boolean, view: View) {
|
||||||
|
activity.immersionBar {
|
||||||
|
statusBarDarkFont(darkFont)
|
||||||
|
statusBarView(view)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
105
app/src/main/java/com/keyboard/craft/util/upload/AESUtils.kt
Normal file
105
app/src/main/java/com/keyboard/craft/util/upload/AESUtils.kt
Normal file
@ -0,0 +1,105 @@
|
|||||||
|
package com.keyboard.craft.util.upload
|
||||||
|
|
||||||
|
import android.app.Activity
|
||||||
|
import android.util.Base64
|
||||||
|
import java.security.SecureRandom
|
||||||
|
import javax.crypto.Cipher
|
||||||
|
import javax.crypto.KeyGenerator
|
||||||
|
import javax.crypto.SecretKey
|
||||||
|
import javax.crypto.spec.IvParameterSpec
|
||||||
|
import javax.crypto.spec.SecretKeySpec
|
||||||
|
|
||||||
|
|
||||||
|
object AESUtils {
|
||||||
|
// private const val AES_MODE = "AES/CBC/PKCS5Padding"
|
||||||
|
private const val AES_MODE = "AES"
|
||||||
|
private const val AES_ALGORITHM = "AES"
|
||||||
|
private const val AES_KEY_SIZE = 256 // 支持 128/192/256
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 生成 AES 密钥
|
||||||
|
*/
|
||||||
|
fun generateAESKey(): String {
|
||||||
|
val keyGenerator = KeyGenerator.getInstance(AES_ALGORITHM)
|
||||||
|
keyGenerator.init(AES_KEY_SIZE, SecureRandom())
|
||||||
|
val secretKey: SecretKey = keyGenerator.generateKey()
|
||||||
|
return Base64.encodeToString(secretKey.encoded, Base64.DEFAULT)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 生成 16 字节 IV(初始化向量)
|
||||||
|
*/
|
||||||
|
fun generateIV(): String {
|
||||||
|
val iv = ByteArray(16)
|
||||||
|
SecureRandom().nextBytes(iv)
|
||||||
|
return Base64.encodeToString(iv, Base64.DEFAULT)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* AES 加密
|
||||||
|
*/
|
||||||
|
fun encrypt(jsonString: String, key: String): String {
|
||||||
|
val keySpec = SecretKeySpec(key.toByteArray(Charsets.UTF_8), AES_ALGORITHM)
|
||||||
|
val ivSpec = IvParameterSpec(key.toByteArray(Charsets.UTF_8))
|
||||||
|
|
||||||
|
val cipher = Cipher.getInstance(AES_MODE)
|
||||||
|
cipher.init(Cipher.ENCRYPT_MODE, keySpec)
|
||||||
|
|
||||||
|
val encryptedBytes = cipher.doFinal(jsonString.toByteArray(Charsets.UTF_8))
|
||||||
|
return Base64.encodeToString(encryptedBytes, Base64.DEFAULT) // 返回 Base64 加密数据
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
fun encryptNew(plainText: String,key: String): String {
|
||||||
|
val secretKey = SecretKeySpec(key.toByteArray(), AES_MODE)
|
||||||
|
val cipher = Cipher.getInstance(AES_MODE)
|
||||||
|
cipher.init(Cipher.ENCRYPT_MODE, secretKey)
|
||||||
|
val encryptedBytes = cipher.doFinal(plainText.toByteArray())
|
||||||
|
return Base64.encodeToString(encryptedBytes, Base64.DEFAULT)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* AES 解密
|
||||||
|
*/
|
||||||
|
fun decrypt(encryptedData: String, key: String): String {
|
||||||
|
val keySpec = SecretKeySpec(key.toByteArray(Charsets.UTF_8), AES_ALGORITHM)
|
||||||
|
val ivSpec = IvParameterSpec(key.toByteArray(Charsets.UTF_8))
|
||||||
|
|
||||||
|
val cipher = Cipher.getInstance(AES_MODE)
|
||||||
|
cipher.init(Cipher.DECRYPT_MODE, keySpec)
|
||||||
|
|
||||||
|
val decryptedBytes = cipher.doFinal(Base64.decode(encryptedData, Base64.DEFAULT))
|
||||||
|
return String(decryptedBytes, Charsets.UTF_8) // 返回解密后的 JSON 字符串
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
fun testAES(context:Activity) {
|
||||||
|
try {
|
||||||
|
// 原始 JSON 字符串
|
||||||
|
// val json = """{"username":"Alice","password":"123456"}"""
|
||||||
|
|
||||||
|
val json = Upload.getData(context)
|
||||||
|
|
||||||
|
// 生成 AES 密钥和 IV
|
||||||
|
val aesKey = "e67cbcee5e573d1b"
|
||||||
|
val aesIV = generateIV()
|
||||||
|
|
||||||
|
println("AES 密钥: $aesKey")
|
||||||
|
// println("AES IV: $aesIV")
|
||||||
|
|
||||||
|
// 加密 JSON
|
||||||
|
val encryptedData = encrypt(json, aesKey)
|
||||||
|
println("加密后: $encryptedData")
|
||||||
|
|
||||||
|
// 解密 JSON
|
||||||
|
val decryptedData = decrypt(encryptedData, aesKey)
|
||||||
|
println("解密后: $decryptedData")
|
||||||
|
|
||||||
|
} catch (e: Exception) {
|
||||||
|
e.printStackTrace()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
72
app/src/main/java/com/keyboard/craft/util/upload/Http.kt
Normal file
72
app/src/main/java/com/keyboard/craft/util/upload/Http.kt
Normal file
@ -0,0 +1,72 @@
|
|||||||
|
package com.keyboard.craft.util.upload
|
||||||
|
|
||||||
|
import android.app.Activity
|
||||||
|
import android.util.Log
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.GlobalScope
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
|
import okhttp3.Call
|
||||||
|
import okhttp3.Callback
|
||||||
|
import okhttp3.MediaType.Companion.toMediaType
|
||||||
|
import okhttp3.OkHttpClient
|
||||||
|
import okhttp3.Request
|
||||||
|
import okhttp3.RequestBody
|
||||||
|
import okhttp3.RequestBody.Companion.toRequestBody
|
||||||
|
import okhttp3.Response
|
||||||
|
import okhttp3.logging.HttpLoggingInterceptor
|
||||||
|
import org.json.JSONObject
|
||||||
|
import java.io.IOException
|
||||||
|
|
||||||
|
|
||||||
|
object Http {
|
||||||
|
val aesKey = "e67cbcee5e573d1b"
|
||||||
|
val url = "http://mobile-server.lux-ad.com:58077/api/mobile/save"
|
||||||
|
|
||||||
|
|
||||||
|
fun makeGetRequest(context: Activity) {
|
||||||
|
val logging = HttpLoggingInterceptor()
|
||||||
|
logging.setLevel(HttpLoggingInterceptor.Level.BODY)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
GlobalScope.launch(Dispatchers.IO) {
|
||||||
|
val data = Upload.getData(context)
|
||||||
|
|
||||||
|
withContext(Dispatchers.Main){
|
||||||
|
val encryptJson = AESUtils.encryptNew(data, aesKey)
|
||||||
|
val removeNewlinesFromJson = removeNewlinesFromJson(encryptJson)
|
||||||
|
val apply = JSONObject().apply {
|
||||||
|
put("encrypted", removeNewlinesFromJson)
|
||||||
|
}
|
||||||
|
val client: OkHttpClient = OkHttpClient.Builder()
|
||||||
|
.addInterceptor(logging)
|
||||||
|
.build()
|
||||||
|
// val client = OkHttpClient()
|
||||||
|
val requestBody: RequestBody =
|
||||||
|
apply.toString().toRequestBody("application/json; charset=utf-8".toMediaType())
|
||||||
|
|
||||||
|
val request: Request = Request.Builder()
|
||||||
|
.url(url)
|
||||||
|
.post(requestBody)
|
||||||
|
.build()
|
||||||
|
client.newCall(request).enqueue(object : Callback {
|
||||||
|
override fun onFailure(call: Call, e: IOException) {
|
||||||
|
Log.e("==================", "onFailure e=${e.message}")
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onResponse(call: Call, response: Response) {
|
||||||
|
Log.e("==================", "response=${response.code} ${response.message}")
|
||||||
|
}
|
||||||
|
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
fun removeNewlinesFromJson(jsonString: String): String {
|
||||||
|
return jsonString.replace("\n", "").replace("\r", "")
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,47 @@
|
|||||||
|
package com.keyboard.craft.util.upload
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.SharedPreferences
|
||||||
|
import com.keyboard.craft.CraftApp
|
||||||
|
|
||||||
|
|
||||||
|
object SaveUtils {
|
||||||
|
|
||||||
|
|
||||||
|
val IS_POST = CraftApp.app.packageName+"is_post"
|
||||||
|
private var shared: SharedPreferences? = null
|
||||||
|
|
||||||
|
var isPost: Boolean
|
||||||
|
get() = queryBoolean(
|
||||||
|
IS_POST,
|
||||||
|
false
|
||||||
|
)
|
||||||
|
set(value) {
|
||||||
|
saveBoolean(IS_POST, value)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
private fun getShared(): SharedPreferences {
|
||||||
|
if (shared == null) {
|
||||||
|
shared = CraftApp.app.getSharedPreferences("Wallpaper", Context.MODE_PRIVATE)
|
||||||
|
}
|
||||||
|
return shared!!
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
fun saveBoolean(key: String, value: Boolean) {
|
||||||
|
getShared().edit()
|
||||||
|
.putBoolean(key, value).apply()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun queryBoolean(key: String, defaultValue: Boolean): Boolean {
|
||||||
|
return getShared()
|
||||||
|
.getBoolean(key, defaultValue)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
353
app/src/main/java/com/keyboard/craft/util/upload/Upload.kt
Normal file
353
app/src/main/java/com/keyboard/craft/util/upload/Upload.kt
Normal file
@ -0,0 +1,353 @@
|
|||||||
|
package com.keyboard.craft.util.upload
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint
|
||||||
|
import android.app.Activity
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.pm.PackageInfo
|
||||||
|
import android.content.pm.PackageManager
|
||||||
|
import android.location.Location
|
||||||
|
import android.net.wifi.WifiInfo
|
||||||
|
import android.net.wifi.WifiManager
|
||||||
|
import android.os.BatteryManager
|
||||||
|
import android.os.Build
|
||||||
|
import android.os.SystemClock
|
||||||
|
import android.provider.Settings
|
||||||
|
import android.telephony.TelephonyManager
|
||||||
|
import android.text.format.Formatter
|
||||||
|
import android.util.Log
|
||||||
|
import android.webkit.WebView
|
||||||
|
import com.google.android.gms.ads.identifier.AdvertisingIdClient
|
||||||
|
import org.json.JSONObject
|
||||||
|
import java.io.File
|
||||||
|
import java.text.SimpleDateFormat
|
||||||
|
import java.util.Date
|
||||||
|
import java.util.Locale
|
||||||
|
import java.util.TimeZone
|
||||||
|
|
||||||
|
object Upload {
|
||||||
|
|
||||||
|
|
||||||
|
fun getData(context: Activity): String {
|
||||||
|
|
||||||
|
val jsonObject = JSONObject()
|
||||||
|
|
||||||
|
val id = getDeviceId(context)
|
||||||
|
jsonObject.put("gaid", id)
|
||||||
|
|
||||||
|
getWebViewPackageInfo(context)?.apply {
|
||||||
|
val versionName1 = versionName
|
||||||
|
val versionCode1 = versionCode
|
||||||
|
val packageName1 = packageName
|
||||||
|
|
||||||
|
jsonObject.put("webVersionName", versionName)
|
||||||
|
jsonObject.put("webVersionCode", versionCode)
|
||||||
|
jsonObject.put("webPackageName", packageName)
|
||||||
|
// Log.d("Info1", "versionName: $versionName, versionCode: $versionCode, packageName: $packageName")
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
jsonObject.put("brand", Build.BRAND)
|
||||||
|
jsonObject.put("manufacturer", Build.MANUFACTURER)
|
||||||
|
jsonObject.put("model", Build.MODEL)
|
||||||
|
jsonObject.put("product", Build.PRODUCT)
|
||||||
|
jsonObject.put("device", Build.DEVICE)
|
||||||
|
jsonObject.put("board", Build.BOARD)
|
||||||
|
jsonObject.put("hardware", Build.HARDWARE)
|
||||||
|
jsonObject.put("fingerPrint", Build.FINGERPRINT)
|
||||||
|
jsonObject.put("buildId", Build.ID)
|
||||||
|
jsonObject.put("display", Build.DISPLAY)
|
||||||
|
jsonObject.put("type", Build.TYPE)
|
||||||
|
jsonObject.put("user", Build.USER)
|
||||||
|
jsonObject.put("host", Build.HOST)
|
||||||
|
jsonObject.put("tags", Build.TAGS)
|
||||||
|
jsonObject.put("serial", Build.SERIAL)
|
||||||
|
jsonObject.put("bootloader", Build.BOOTLOADER)
|
||||||
|
jsonObject.put("sdkInt", Build.VERSION.SDK_INT)
|
||||||
|
jsonObject.put("androidVersion", Build.VERSION.RELEASE)
|
||||||
|
jsonObject.put("baseOs", Build.VERSION.BASE_OS)
|
||||||
|
jsonObject.put("incremental", Build.VERSION.INCREMENTAL)
|
||||||
|
jsonObject.put("codename", Build.VERSION.CODENAME)
|
||||||
|
|
||||||
|
|
||||||
|
val androidID = getAndroidID(context)
|
||||||
|
jsonObject.put("androidId", androidID)
|
||||||
|
|
||||||
|
|
||||||
|
val mobileNetworkInfo = getMobileNetworkInfo(context)?.let {
|
||||||
|
//SIM卡的运营商名称
|
||||||
|
it.networkOperatorName
|
||||||
|
//SIM卡的运营商代码
|
||||||
|
it.simOperator
|
||||||
|
//国家代码
|
||||||
|
it.simCountryIso
|
||||||
|
//SIM 卡状态
|
||||||
|
it.simState
|
||||||
|
jsonObject.put("simOperator", it.simOperator)
|
||||||
|
jsonObject.put("simOperatorName", it.networkOperatorName)
|
||||||
|
jsonObject.put("simCountry", it.simCountryIso)
|
||||||
|
jsonObject.put("simState", it.simState)
|
||||||
|
|
||||||
|
// if (ActivityCompat.checkSelfPermission(
|
||||||
|
// context,
|
||||||
|
// Manifest.permission.READ_PHONE_STATE
|
||||||
|
// ) != PackageManager.PERMISSION_GRANTED
|
||||||
|
// ) {
|
||||||
|
// //没有权限
|
||||||
|
// Log.e("==================", "无法获取phone权限")
|
||||||
|
// return@let
|
||||||
|
// } else {
|
||||||
|
// //网络类型
|
||||||
|
// val networkType = getNet(it.networkType)
|
||||||
|
// jsonObject.put("networkType",networkType)
|
||||||
|
// }
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
getWifiInfo(context).let { wifiInfo ->
|
||||||
|
val ssid = wifiInfo.ssid // WiFi 名称
|
||||||
|
val bssid = wifiInfo.bssid // 路由器 MAC 地址
|
||||||
|
val ip = wifiInfo.ipAddress
|
||||||
|
val ipAddress: String = Formatter.formatIpAddress(ip) // IP 地址
|
||||||
|
// Log.d("WiFi Info", "SSID: $ssid, BSSID: $bssid, IP: $ipAddress")
|
||||||
|
|
||||||
|
jsonObject.put("wifiSSID", ssid)
|
||||||
|
jsonObject.put("wifiBSSID", bssid)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// getLastLocation(context){location->
|
||||||
|
// location?.let {
|
||||||
|
// val latitude: Double = location.latitude
|
||||||
|
// val longitude: Double = location.longitude
|
||||||
|
// val accuracy = location.accuracy // 获取精度(米)
|
||||||
|
// jsonObject.put("longitude",longitude)
|
||||||
|
// jsonObject.put("latitude",latitude)
|
||||||
|
//// jsonObject.put("randomOffset",latitude)
|
||||||
|
// Log.d("Location", "纬度: $latitude, 经度: $longitude")
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
//电池电量
|
||||||
|
val batteryInfo = getBatteryInfo(context)
|
||||||
|
jsonObject.put("batteryLevel", batteryInfo)
|
||||||
|
|
||||||
|
//处理器核心数
|
||||||
|
val coreCount = Runtime.getRuntime().availableProcessors()
|
||||||
|
jsonObject.put("availableProcessors", coreCount)
|
||||||
|
|
||||||
|
//系统启动时长
|
||||||
|
// val convertTimestampToDate =
|
||||||
|
// convertTimestampToDate(System.currentTimeMillis() - SystemClock.elapsedRealtime())
|
||||||
|
// val systemUptime = getSystemUptime()
|
||||||
|
jsonObject.put("systemStarTime", SystemClock.elapsedRealtime())
|
||||||
|
|
||||||
|
//应用程序 APK 文件的最后修改时间
|
||||||
|
val installTime = getInstallTime(context)
|
||||||
|
jsonObject.put("apkLastModified", installTime)
|
||||||
|
|
||||||
|
//安装来源
|
||||||
|
val installSource = getInstallSourceNew(context)
|
||||||
|
jsonObject.put("installerPkg", installSource)
|
||||||
|
|
||||||
|
|
||||||
|
Log.d("===================================", jsonObject.toString())
|
||||||
|
return jsonObject.toString()
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
fun getInstallSource(context: Context): String? {
|
||||||
|
val packageManager = context.packageManager
|
||||||
|
val installer = packageManager.getInstallerPackageName(context.packageName)
|
||||||
|
return installer ?: "未知"
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getInstallSourceNew(context: Context): String? {
|
||||||
|
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { // API 30+
|
||||||
|
try {
|
||||||
|
val packageManager = context.packageManager
|
||||||
|
val installSourceInfo = packageManager.getInstallSourceInfo(context.packageName)
|
||||||
|
installSourceInfo.installingPackageName // 安装来源
|
||||||
|
} catch (e: PackageManager.NameNotFoundException) {
|
||||||
|
"未知"
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
getInstallSource(context) // 兼容 API 30 以下
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getSystemUptime(): String {
|
||||||
|
val uptimeMillis = SystemClock.elapsedRealtime() // 设备启动后的毫秒数
|
||||||
|
val uptimeSeconds = uptimeMillis / 1000
|
||||||
|
val hours = uptimeSeconds / 3600
|
||||||
|
val minutes = uptimeSeconds % 3600 / 60
|
||||||
|
val seconds = uptimeSeconds % 60
|
||||||
|
val uptimeFormatted = "$hours 小时 $minutes 分钟 $seconds 秒"
|
||||||
|
Log.d("DeviceInfo", "系统运行时间: $uptimeFormatted")
|
||||||
|
return uptimeFormatted
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getLastLocation(context: Activity, result: (location: Location?) -> Unit) {
|
||||||
|
// val fusedLocationClient = LocationServices.getFusedLocationProviderClient(context)
|
||||||
|
//
|
||||||
|
// if (ActivityCompat.checkSelfPermission(
|
||||||
|
// context,
|
||||||
|
// Manifest.permission.ACCESS_FINE_LOCATION
|
||||||
|
// ) != PackageManager.PERMISSION_GRANTED || ActivityCompat.checkSelfPermission(
|
||||||
|
// context,
|
||||||
|
// Manifest.permission.ACCESS_COARSE_LOCATION
|
||||||
|
// ) != PackageManager.PERMISSION_GRANTED
|
||||||
|
// ) {
|
||||||
|
// Log.e("==================", "无法获取位置权限")
|
||||||
|
// return
|
||||||
|
// }
|
||||||
|
// fusedLocationClient.lastLocation
|
||||||
|
// .addOnSuccessListener(context, object : OnSuccessListener<Location?> {
|
||||||
|
// override fun onSuccess(location: Location?) {
|
||||||
|
// result.invoke(location)
|
||||||
|
// if (location != null) {
|
||||||
|
// } else {
|
||||||
|
//
|
||||||
|
// Log.e("Location", "无法获取位置")
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// })
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getInstallTime(context: Context): String {
|
||||||
|
val lastModified = File(context.applicationInfo.sourceDir).lastModified()
|
||||||
|
return convertTimestampToDate(lastModified)
|
||||||
|
}
|
||||||
|
|
||||||
|
// fun getInstallSource(context: Context): String? {
|
||||||
|
// return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||||
|
// context.packageManager.getInstallSourceInfo(context.applicationInfo.packageName).installingPackageName
|
||||||
|
// } else {
|
||||||
|
// context.packageManager.getInstallerPackageName(context.applicationInfo.packageName)
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// }
|
||||||
|
|
||||||
|
fun getBatteryInfo(context: Context): Int {
|
||||||
|
val batteryManager = context.getSystemService(Context.BATTERY_SERVICE) as BatteryManager
|
||||||
|
val batteryLevel =
|
||||||
|
batteryManager.getIntProperty(BatteryManager.BATTERY_PROPERTY_CAPACITY) // 获取电池电量(0-100%)
|
||||||
|
val isCharging = batteryManager.isCharging // 是否在充电
|
||||||
|
|
||||||
|
return batteryLevel
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ACCESS_FINE_LOCATION
|
||||||
|
*
|
||||||
|
* ACCESS_WIFI_STATE
|
||||||
|
*/
|
||||||
|
fun getWifiInfo(context: Context): WifiInfo {
|
||||||
|
val wifiManager = context.getSystemService(Context.WIFI_SERVICE) as WifiManager
|
||||||
|
return wifiManager.connectionInfo
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* READ_PHONE_STATE
|
||||||
|
*/
|
||||||
|
fun getMobileNetworkInfo(context: Context): TelephonyManager? {
|
||||||
|
val telephonyManager =
|
||||||
|
context.getSystemService(Context.TELEPHONY_SERVICE) as TelephonyManager
|
||||||
|
val operatorName = telephonyManager.networkOperatorName // 运营商名称
|
||||||
|
|
||||||
|
|
||||||
|
return telephonyManager
|
||||||
|
|
||||||
|
|
||||||
|
// Log.d("Mobile Network", "Operator: $operatorName, Type: $networkType")
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getNet(networkType: Int): String {
|
||||||
|
return when (networkType) {
|
||||||
|
TelephonyManager.NETWORK_TYPE_LTE -> "4G"
|
||||||
|
|
||||||
|
TelephonyManager.NETWORK_TYPE_NR -> "5G(Android 11+)"
|
||||||
|
|
||||||
|
TelephonyManager.NETWORK_TYPE_HSPA -> "3G"
|
||||||
|
|
||||||
|
TelephonyManager.NETWORK_TYPE_GPRS -> "2G"
|
||||||
|
else -> ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getWebViewPackageInfo(context: Activity): PackageInfo? {
|
||||||
|
val packageManager: PackageManager = context.packageManager
|
||||||
|
|
||||||
|
// 如果系统支持直接获取 WebView 包信息 (Android 7.0 及以上)
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||||
|
return WebView.getCurrentWebViewPackage()
|
||||||
|
}
|
||||||
|
// 如果不支持,尝试通过常见的 WebView 包名来获取信息
|
||||||
|
val webviewPackageNames = listOf(
|
||||||
|
"com.google.android.webview",
|
||||||
|
"com.android.webview",
|
||||||
|
"com.android.chrome"
|
||||||
|
)
|
||||||
|
for (packageName in webviewPackageNames) {
|
||||||
|
try {
|
||||||
|
val packageInfo = packageManager.getPackageInfo(packageName, 0)
|
||||||
|
if (packageInfo != null) {
|
||||||
|
return packageInfo
|
||||||
|
}
|
||||||
|
} catch (e: PackageManager.NameNotFoundException) {
|
||||||
|
// 忽略异常,继续尝试下一个包名
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 如果都没有找到,返回 null
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressLint("HardwareIds")
|
||||||
|
fun getAndroidID(context: Context): String? {
|
||||||
|
return Settings.Secure.getString(context.contentResolver, Settings.Secure.ANDROID_ID)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
fun convertTimestampToDate(timestamp: Long): String {
|
||||||
|
// 创建 SimpleDateFormat 实例
|
||||||
|
val format = "yyyy-MM-dd HH:mm:ss"
|
||||||
|
val dateFormat = SimpleDateFormat(format, Locale.getDefault())
|
||||||
|
dateFormat.timeZone = TimeZone.getTimeZone("GMT") // 设置时区为 UTC,或者根据需要选择其他时区
|
||||||
|
|
||||||
|
// 将时间戳转换为 Date 对象
|
||||||
|
val date = Date(timestamp)
|
||||||
|
|
||||||
|
// 格式化 Date 对象为指定格式的字符串
|
||||||
|
return dateFormat.format(date)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
fun getDeviceId(context: Context): String? =
|
||||||
|
try {
|
||||||
|
// 优先尝试获取 GAID
|
||||||
|
val adInfo = AdvertisingIdClient.getAdvertisingIdInfo(context)
|
||||||
|
if (!adInfo.isLimitAdTrackingEnabled && !adInfo.id.isNullOrEmpty()) {
|
||||||
|
Log.d("DeviceIdHelper", "Using GAID: ${adInfo.id}")
|
||||||
|
adInfo.id
|
||||||
|
} else {
|
||||||
|
Log.d("DeviceIdHelper", "GAID not available or user limited it, using AppSet ID")
|
||||||
|
null
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e("DeviceIdHelper", "GAID fetch failed: ${e.message}")
|
||||||
|
null
|
||||||
|
}
|
||||||
|
|
||||||
|
// ✅ 回退获取 App Set ID(Android 12+ 替代方案)
|
||||||
|
// return@withContext try {
|
||||||
|
// val appSetInfo: AppSetIdInfo = AppSet.getClient(context).appSetIdInfo.await()
|
||||||
|
// Log.d("DeviceIdHelper", "Using App Set ID: ${appSetInfo.id}")
|
||||||
|
// appSetInfo.id
|
||||||
|
// } catch (e: Exception) {
|
||||||
|
// Log.e("DeviceIdHelper", "App Set ID fetch failed: ${e.message}")
|
||||||
|
// null
|
||||||
|
// }
|
||||||
|
|
||||||
|
}
|
||||||
@ -0,0 +1,700 @@
|
|||||||
|
package com.keyboard.craft.view;
|
||||||
|
|
||||||
|
import android.animation.Animator;
|
||||||
|
import android.animation.AnimatorSet;
|
||||||
|
import android.animation.TimeInterpolator;
|
||||||
|
import android.animation.ValueAnimator;
|
||||||
|
import android.annotation.SuppressLint;
|
||||||
|
import android.annotation.TargetApi;
|
||||||
|
import android.content.Context;
|
||||||
|
import android.content.res.TypedArray;
|
||||||
|
import android.graphics.Canvas;
|
||||||
|
import android.graphics.Color;
|
||||||
|
import android.graphics.LinearGradient;
|
||||||
|
import android.graphics.Paint;
|
||||||
|
import android.graphics.RectF;
|
||||||
|
import android.graphics.Shader;
|
||||||
|
import android.os.Build;
|
||||||
|
import android.os.Parcel;
|
||||||
|
import android.os.Parcelable;
|
||||||
|
import android.util.AttributeSet;
|
||||||
|
|
||||||
|
import com.keyboard.craft.R;
|
||||||
|
|
||||||
|
import androidx.core.view.animation.PathInterpolatorCompat;
|
||||||
|
|
||||||
|
public class AnimDownloadProgressButton extends androidx.appcompat.widget.AppCompatTextView {
|
||||||
|
|
||||||
|
private Context mContext;
|
||||||
|
|
||||||
|
//背景画笔
|
||||||
|
private Paint mBackgroundPaint;
|
||||||
|
//按钮文字画笔
|
||||||
|
private volatile Paint mTextPaint;
|
||||||
|
//第一个点画笔
|
||||||
|
private Paint mDot1Paint;
|
||||||
|
//第二个点画笔
|
||||||
|
private Paint mDot2Paint;
|
||||||
|
|
||||||
|
|
||||||
|
//背景颜色
|
||||||
|
private int[] mBackgroundColor;
|
||||||
|
private int[] mOriginBackgroundColor;
|
||||||
|
//下载中后半部分后面背景颜色
|
||||||
|
private int mBackgroundSecondColor;
|
||||||
|
//文字颜色
|
||||||
|
private int mTextColor;
|
||||||
|
//覆盖后颜色
|
||||||
|
private int mTextCoverColor;
|
||||||
|
//文字大小
|
||||||
|
private float mAboveTextSize = 50;
|
||||||
|
|
||||||
|
|
||||||
|
private float mProgress = -1;
|
||||||
|
private float mToProgress;
|
||||||
|
private int mMaxProgress;
|
||||||
|
private int mMinProgress;
|
||||||
|
private float mProgressPercent;
|
||||||
|
|
||||||
|
private float mButtonRadius;
|
||||||
|
|
||||||
|
//两个点向右移动距离
|
||||||
|
private float mDot1transX;
|
||||||
|
private float mDot2transX;
|
||||||
|
|
||||||
|
private RectF mBackgroundBounds;
|
||||||
|
private LinearGradient mFillBgGradient;
|
||||||
|
private LinearGradient mProgressBgGradient;
|
||||||
|
private LinearGradient mProgressTextGradient;
|
||||||
|
|
||||||
|
//点运动动画
|
||||||
|
private AnimatorSet mDotAnimationSet;
|
||||||
|
//下载平滑动画
|
||||||
|
private ValueAnimator mProgressAnimation;
|
||||||
|
|
||||||
|
//记录当前文字
|
||||||
|
private CharSequence mCurrentText;
|
||||||
|
|
||||||
|
//普通状态
|
||||||
|
public static final int NORMAL = 0;
|
||||||
|
//下载中
|
||||||
|
public static final int DOWNLOADING = 1;
|
||||||
|
//有点运动状态
|
||||||
|
public static final int INSTALLING = 2;
|
||||||
|
|
||||||
|
private ButtonController mDefaultController;
|
||||||
|
|
||||||
|
private ButtonController mCustomerController;
|
||||||
|
|
||||||
|
|
||||||
|
private int mState;
|
||||||
|
|
||||||
|
public AnimDownloadProgressButton(Context context) {
|
||||||
|
this(context, null);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
public AnimDownloadProgressButton(Context context, AttributeSet attrs) {
|
||||||
|
super(context, attrs);
|
||||||
|
if (!isInEditMode()) {
|
||||||
|
mContext = context;
|
||||||
|
initController();
|
||||||
|
initAttrs(context, attrs);
|
||||||
|
init();
|
||||||
|
setupAnimations();
|
||||||
|
} else {
|
||||||
|
initController();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
private void initController() {
|
||||||
|
mDefaultController = new DefaultButtonController();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void drawableStateChanged() {
|
||||||
|
super.drawableStateChanged();
|
||||||
|
ButtonController buttonController = switchController();
|
||||||
|
if (buttonController.enablePress()) {
|
||||||
|
if (mOriginBackgroundColor == null) {
|
||||||
|
mOriginBackgroundColor = new int[5];
|
||||||
|
mOriginBackgroundColor[0] = mBackgroundColor[0];
|
||||||
|
mOriginBackgroundColor[1] = mBackgroundColor[1];
|
||||||
|
mOriginBackgroundColor[2] = mBackgroundColor[2];
|
||||||
|
mOriginBackgroundColor[3] = mBackgroundColor[3];
|
||||||
|
mOriginBackgroundColor[4] = mBackgroundColor[4];
|
||||||
|
}
|
||||||
|
if (this.isPressed()) {
|
||||||
|
int pressColorleft = buttonController.getPressedColor(mBackgroundColor[0]);
|
||||||
|
int pressColorright = buttonController.getPressedColor(mBackgroundColor[1]);
|
||||||
|
int pressColor2 = buttonController.getPressedColor(mBackgroundColor[2]);
|
||||||
|
int pressColor3 = buttonController.getPressedColor(mBackgroundColor[3]);
|
||||||
|
int pressColor4 = buttonController.getPressedColor(mBackgroundColor[4]);
|
||||||
|
if (buttonController.enableGradient()) {
|
||||||
|
initGradientColor(pressColorleft, pressColorright, pressColor2, pressColor3, pressColor4);
|
||||||
|
} else {
|
||||||
|
initGradientColor(pressColorleft, pressColorleft, pressColor2, pressColor3, pressColor4);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (buttonController.enableGradient()) {
|
||||||
|
initGradientColor(mOriginBackgroundColor[0], mOriginBackgroundColor[1], mOriginBackgroundColor[2], mOriginBackgroundColor[3], mOriginBackgroundColor[4]);
|
||||||
|
} else {
|
||||||
|
initGradientColor(mOriginBackgroundColor[4], mOriginBackgroundColor[3], mOriginBackgroundColor[2], mOriginBackgroundColor[1], mOriginBackgroundColor[0]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
invalidate();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
private void initAttrs(Context context, AttributeSet attrs) {
|
||||||
|
|
||||||
|
TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.AnimDownloadProgressButton);
|
||||||
|
int bgColor = a.getColor(R.styleable.AnimDownloadProgressButton_progressbtn_background_color, Color.parseColor("#6699ff"));
|
||||||
|
//初始化背景颜色数组
|
||||||
|
initGradientColor(Color.parseColor("#CDED88"),
|
||||||
|
Color.parseColor("#E8D99F"),
|
||||||
|
Color.parseColor("#83ECCF"),
|
||||||
|
Color.parseColor("#9BBAFC"),
|
||||||
|
Color.parseColor("#92C2FC"));
|
||||||
|
mBackgroundSecondColor = a.getColor(R.styleable.AnimDownloadProgressButton_progressbtn_background_second_color, Color.LTGRAY);
|
||||||
|
mButtonRadius = a.getFloat(R.styleable.AnimDownloadProgressButton_progressbtn_radius, getMeasuredHeight() / 2);
|
||||||
|
mAboveTextSize = a.getFloat(R.styleable.AnimDownloadProgressButton_progressbtn_text_size, 50);
|
||||||
|
mTextColor = a.getColor(R.styleable.AnimDownloadProgressButton_progressbtn_text_color, bgColor);
|
||||||
|
mTextCoverColor = a.getColor(R.styleable.AnimDownloadProgressButton_progressbtn_text_covercolor, Color.WHITE);
|
||||||
|
boolean enableGradient = a.getBoolean(R.styleable.AnimDownloadProgressButton_progressbtn_enable_gradient, false);
|
||||||
|
boolean enablePress = a.getBoolean(R.styleable.AnimDownloadProgressButton_progressbtn_enable_press, false);
|
||||||
|
((DefaultButtonController) mDefaultController).setEnableGradient(enableGradient).setEnablePress(enablePress);
|
||||||
|
if (enableGradient) {
|
||||||
|
initGradientColor(mDefaultController.getLighterColor(mBackgroundColor[0]), mBackgroundColor[1], mBackgroundColor[2], mBackgroundColor[3], mBackgroundColor[4]);
|
||||||
|
}
|
||||||
|
a.recycle();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void init() {
|
||||||
|
|
||||||
|
mMaxProgress = 100;
|
||||||
|
mMinProgress = 0;
|
||||||
|
mProgress = 0;
|
||||||
|
|
||||||
|
|
||||||
|
//设置背景画笔
|
||||||
|
mBackgroundPaint = new Paint();
|
||||||
|
mBackgroundPaint.setAntiAlias(true);
|
||||||
|
mBackgroundPaint.setStyle(Paint.Style.FILL);
|
||||||
|
|
||||||
|
//设置文字画笔
|
||||||
|
mTextPaint = new Paint();
|
||||||
|
mTextPaint.setAntiAlias(true);
|
||||||
|
mTextPaint.setTextSize(mAboveTextSize);
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {
|
||||||
|
//解决文字有时候画不出问题
|
||||||
|
setLayerType(LAYER_TYPE_SOFTWARE, mTextPaint);
|
||||||
|
}
|
||||||
|
|
||||||
|
//设置第一个点画笔
|
||||||
|
mDot1Paint = new Paint();
|
||||||
|
mDot1Paint.setAntiAlias(true);
|
||||||
|
mDot1Paint.setTextSize(mAboveTextSize);
|
||||||
|
|
||||||
|
//设置第二个点画笔
|
||||||
|
mDot2Paint = new Paint();
|
||||||
|
mDot2Paint.setAntiAlias(true);
|
||||||
|
mDot2Paint.setTextSize(mAboveTextSize);
|
||||||
|
|
||||||
|
|
||||||
|
//初始化状态设为NORMAL
|
||||||
|
mState = NORMAL;
|
||||||
|
invalidate();
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
//初始化渐变色
|
||||||
|
private int[] initGradientColor(int color1, int color2, int color3, int color4, int color5) {
|
||||||
|
mBackgroundColor = new int[5];
|
||||||
|
mBackgroundColor[0] = color1;
|
||||||
|
mBackgroundColor[1] = color2;
|
||||||
|
mBackgroundColor[2] = color3;
|
||||||
|
mBackgroundColor[3] = color4;
|
||||||
|
mBackgroundColor[4] = color5;
|
||||||
|
return mBackgroundColor;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private void setupAnimations() {
|
||||||
|
|
||||||
|
//两个点向右移动动画
|
||||||
|
ValueAnimator dotMoveAnimation = ValueAnimator.ofFloat(0, 20);
|
||||||
|
TimeInterpolator pathInterpolator = PathInterpolatorCompat.create(0.11f, 0f, 0.12f, 1f);
|
||||||
|
dotMoveAnimation.setInterpolator(pathInterpolator);
|
||||||
|
dotMoveAnimation.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
|
||||||
|
@Override
|
||||||
|
public void onAnimationUpdate(ValueAnimator animation) {
|
||||||
|
float transX = (float) animation.getAnimatedValue();
|
||||||
|
mDot1transX = transX;
|
||||||
|
mDot2transX = transX;
|
||||||
|
invalidate();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
dotMoveAnimation.setDuration(1243);
|
||||||
|
dotMoveAnimation.setRepeatMode(ValueAnimator.RESTART);
|
||||||
|
dotMoveAnimation.setRepeatCount(ValueAnimator.INFINITE);
|
||||||
|
|
||||||
|
|
||||||
|
//两个点渐显渐隐动画
|
||||||
|
final ValueAnimator dotAlphaAnim = ValueAnimator.ofInt(0, 1243).setDuration(1243);
|
||||||
|
dotAlphaAnim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
|
||||||
|
@Override
|
||||||
|
public void onAnimationUpdate(ValueAnimator animation) {
|
||||||
|
int time = (int) dotAlphaAnim.getAnimatedValue();
|
||||||
|
int dot1Alpha = calculateDot1AlphaByTime(time);
|
||||||
|
int dot2Alpha = calculateDot2AlphaByTime(time);
|
||||||
|
mDot1Paint.setColor(mTextCoverColor);
|
||||||
|
mDot2Paint.setColor(mTextCoverColor);
|
||||||
|
mDot1Paint.setAlpha(dot1Alpha);
|
||||||
|
mDot2Paint.setAlpha(dot2Alpha);
|
||||||
|
}
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
dotAlphaAnim.addListener(new Animator.AnimatorListener() {
|
||||||
|
@Override
|
||||||
|
public void onAnimationStart(Animator animation) {
|
||||||
|
mDot1Paint.setAlpha(0);
|
||||||
|
mDot2Paint.setAlpha(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onAnimationEnd(Animator animation) {
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onAnimationCancel(Animator animation) {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onAnimationRepeat(Animator animation) {
|
||||||
|
|
||||||
|
}
|
||||||
|
});
|
||||||
|
dotAlphaAnim.setRepeatMode(ValueAnimator.RESTART);
|
||||||
|
dotAlphaAnim.setRepeatCount(ValueAnimator.INFINITE);
|
||||||
|
//两个点的动画集合
|
||||||
|
mDotAnimationSet = new AnimatorSet();
|
||||||
|
mDotAnimationSet.playTogether(dotAlphaAnim, dotMoveAnimation);
|
||||||
|
|
||||||
|
//ProgressBar的动画
|
||||||
|
mProgressAnimation = ValueAnimator.ofFloat(0, 1).setDuration(500);
|
||||||
|
mProgressAnimation.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
|
||||||
|
@Override
|
||||||
|
public void onAnimationUpdate(ValueAnimator animation) {
|
||||||
|
float timepercent = (float) animation.getAnimatedValue();
|
||||||
|
mProgress = ((mToProgress - mProgress) * timepercent + mProgress);
|
||||||
|
invalidate();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
//第一个点透明度计算函数
|
||||||
|
private int calculateDot2AlphaByTime(int time) {
|
||||||
|
int alpha;
|
||||||
|
if (0 <= time && time <= 83) {
|
||||||
|
double DAlpha = 255.0 / 83.0 * time;
|
||||||
|
alpha = (int) DAlpha;
|
||||||
|
} else if (83 < time && time <= 1000) {
|
||||||
|
alpha = 255;
|
||||||
|
} else if (1000 < time && time <= 1083) {
|
||||||
|
double DAlpha = -255.0 / 83.0 * (time - 1083);
|
||||||
|
alpha = (int) DAlpha;
|
||||||
|
} else if (1083 < time && time <= 1243) {
|
||||||
|
alpha = 0;
|
||||||
|
} else {
|
||||||
|
alpha = 255;
|
||||||
|
}
|
||||||
|
return alpha;
|
||||||
|
}
|
||||||
|
|
||||||
|
//第二个点透明度计算函数
|
||||||
|
private int calculateDot1AlphaByTime(int time) {
|
||||||
|
int alpha;
|
||||||
|
if (0 <= time && time <= 160) {
|
||||||
|
alpha = 0;
|
||||||
|
} else if (160 < time && time <= 243) {
|
||||||
|
double DAlpha = 255.0 / 83.0 * (time - 160);
|
||||||
|
alpha = (int) DAlpha;
|
||||||
|
} else if (243 < time && time <= 1160) {
|
||||||
|
alpha = 255;
|
||||||
|
} else if (1160 < time && time <= 1243) {
|
||||||
|
double DAlpha = -255.0 / 83.0 * (time - 1243);
|
||||||
|
alpha = (int) DAlpha;
|
||||||
|
} else {
|
||||||
|
alpha = 255;
|
||||||
|
}
|
||||||
|
return alpha;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private ValueAnimator createDotAlphaAnimation(int i, Paint mDot1Paint, int i1, int i2, int i3) {
|
||||||
|
|
||||||
|
return new ValueAnimator();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onDraw(Canvas canvas) {
|
||||||
|
super.onDraw(canvas);
|
||||||
|
if (!isInEditMode()) {
|
||||||
|
drawing(canvas);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void drawing(Canvas canvas) {
|
||||||
|
drawBackground(canvas);
|
||||||
|
drawTextAbove(canvas);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void drawBackground(Canvas canvas) {
|
||||||
|
mBackgroundBounds = new RectF();
|
||||||
|
if (mButtonRadius == 0) {
|
||||||
|
mButtonRadius = getMeasuredHeight() / 2;
|
||||||
|
}
|
||||||
|
mBackgroundBounds.left = 2;
|
||||||
|
mBackgroundBounds.top = 2;
|
||||||
|
mBackgroundBounds.right = getMeasuredWidth() - 2;
|
||||||
|
mBackgroundBounds.bottom = getMeasuredHeight() - 2;
|
||||||
|
|
||||||
|
ButtonController buttonController = switchController();
|
||||||
|
|
||||||
|
//color
|
||||||
|
switch (mState) {
|
||||||
|
case NORMAL:
|
||||||
|
if (buttonController.enableGradient()) {
|
||||||
|
mFillBgGradient = new LinearGradient(0,
|
||||||
|
getMeasuredHeight() / 2,
|
||||||
|
getMeasuredWidth(),
|
||||||
|
getMeasuredHeight() / 2,
|
||||||
|
mBackgroundColor,
|
||||||
|
null,
|
||||||
|
Shader.TileMode.CLAMP);
|
||||||
|
mBackgroundPaint.setShader(mFillBgGradient);
|
||||||
|
} else {
|
||||||
|
if (mBackgroundPaint.getShader() != null) {
|
||||||
|
mBackgroundPaint.setShader(null);
|
||||||
|
}
|
||||||
|
mBackgroundPaint.setColor(mBackgroundColor[0]);
|
||||||
|
}
|
||||||
|
canvas.drawRoundRect(mBackgroundBounds, mButtonRadius, mButtonRadius, mBackgroundPaint);
|
||||||
|
break;
|
||||||
|
case DOWNLOADING:
|
||||||
|
if (buttonController.enableGradient()) {
|
||||||
|
mProgressPercent = mProgress / (mMaxProgress + 0f);
|
||||||
|
int[] colorList = new int[]{mBackgroundColor[0], mBackgroundColor[1], mBackgroundSecondColor};
|
||||||
|
mProgressBgGradient = new LinearGradient(0, 0, getMeasuredWidth(), 0,
|
||||||
|
colorList,
|
||||||
|
new float[]{0, mProgressPercent, mProgressPercent + 0.001f},
|
||||||
|
Shader.TileMode.CLAMP
|
||||||
|
);
|
||||||
|
mBackgroundPaint.setShader(mProgressBgGradient);
|
||||||
|
} else {
|
||||||
|
mProgressPercent = mProgress / (mMaxProgress + 0f);
|
||||||
|
mProgressBgGradient = new LinearGradient(0, 0, getMeasuredWidth(), 0,
|
||||||
|
new int[]{mBackgroundColor[0], mBackgroundSecondColor},
|
||||||
|
new float[]{mProgressPercent, mProgressPercent + 0.001f},
|
||||||
|
Shader.TileMode.CLAMP
|
||||||
|
);
|
||||||
|
mBackgroundPaint.setColor(mBackgroundColor[0]);
|
||||||
|
mBackgroundPaint.setShader(mProgressBgGradient);
|
||||||
|
}
|
||||||
|
canvas.drawRoundRect(mBackgroundBounds, mButtonRadius, mButtonRadius, mBackgroundPaint);
|
||||||
|
break;
|
||||||
|
case INSTALLING:
|
||||||
|
if (buttonController.enableGradient()) {
|
||||||
|
mFillBgGradient = new LinearGradient(0, getMeasuredHeight() / 2, getMeasuredWidth(), getMeasuredHeight() / 2,
|
||||||
|
mBackgroundColor,
|
||||||
|
null,
|
||||||
|
Shader.TileMode.CLAMP);
|
||||||
|
mBackgroundPaint.setShader(mFillBgGradient);
|
||||||
|
} else {
|
||||||
|
mBackgroundPaint.setShader(null);
|
||||||
|
mBackgroundPaint.setColor(mBackgroundColor[0]);
|
||||||
|
}
|
||||||
|
canvas.drawRoundRect(mBackgroundBounds, mButtonRadius, mButtonRadius, mBackgroundPaint);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void drawTextAbove(Canvas canvas) {
|
||||||
|
final float y = canvas.getHeight() / 2 - (mTextPaint.descent() / 2 + mTextPaint.ascent() / 2);
|
||||||
|
if (mCurrentText == null) {
|
||||||
|
mCurrentText = "";
|
||||||
|
}
|
||||||
|
final float textWidth = mTextPaint.measureText(mCurrentText.toString());
|
||||||
|
//color
|
||||||
|
switch (mState) {
|
||||||
|
case NORMAL:
|
||||||
|
mTextPaint.setShader(null);
|
||||||
|
mTextPaint.setColor(mTextCoverColor);
|
||||||
|
canvas.drawText(mCurrentText.toString(), (getMeasuredWidth() - textWidth) / 2, y, mTextPaint);
|
||||||
|
break;
|
||||||
|
case DOWNLOADING:
|
||||||
|
|
||||||
|
//进度条压过距离
|
||||||
|
float coverlength = getMeasuredWidth() * mProgressPercent;
|
||||||
|
//开始渐变指示器
|
||||||
|
float indicator1 = getMeasuredWidth() / 2 - textWidth / 2;
|
||||||
|
//结束渐变指示器
|
||||||
|
float indicator2 = getMeasuredWidth() / 2 + textWidth / 2;
|
||||||
|
//文字变色部分的距离
|
||||||
|
float coverTextLength = textWidth / 2 - getMeasuredWidth() / 2 + coverlength;
|
||||||
|
float textProgress = coverTextLength / textWidth;
|
||||||
|
if (coverlength <= indicator1) {
|
||||||
|
mTextPaint.setShader(null);
|
||||||
|
mTextPaint.setColor(mTextColor);
|
||||||
|
} else if (indicator1 < coverlength && coverlength <= indicator2) {
|
||||||
|
mProgressTextGradient = new LinearGradient((getMeasuredWidth() - textWidth) / 2, 0, (getMeasuredWidth() + textWidth) / 2, 0,
|
||||||
|
new int[]{mTextCoverColor, mTextColor},
|
||||||
|
new float[]{textProgress, textProgress + 0.001f},
|
||||||
|
Shader.TileMode.CLAMP);
|
||||||
|
mTextPaint.setColor(mTextColor);
|
||||||
|
mTextPaint.setShader(mProgressTextGradient);
|
||||||
|
} else {
|
||||||
|
mTextPaint.setShader(null);
|
||||||
|
mTextPaint.setColor(mTextCoverColor);
|
||||||
|
}
|
||||||
|
canvas.drawText(mCurrentText.toString(), (getMeasuredWidth() - textWidth) / 2, y, mTextPaint);
|
||||||
|
break;
|
||||||
|
case INSTALLING:
|
||||||
|
mTextPaint.setColor(mTextCoverColor);
|
||||||
|
canvas.drawText(mCurrentText.toString(), (getMeasuredWidth() - textWidth) / 2, y, mTextPaint);
|
||||||
|
canvas.drawCircle((getMeasuredWidth() + textWidth) / 2 + 4 + mDot1transX, y, 4, mDot1Paint);
|
||||||
|
canvas.drawCircle((getMeasuredWidth() + textWidth) / 2 + 24 + mDot2transX, y, 4, mDot2Paint);
|
||||||
|
break;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
private ButtonController switchController() {
|
||||||
|
if (mCustomerController != null) {
|
||||||
|
return mCustomerController;
|
||||||
|
} else {
|
||||||
|
return mDefaultController;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getState() {
|
||||||
|
return mState;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setState(int state) {
|
||||||
|
if (mState != state) {//状态确实有改变
|
||||||
|
this.mState = state;
|
||||||
|
invalidate();
|
||||||
|
if (state == AnimDownloadProgressButton.INSTALLING) {
|
||||||
|
//开启两个点动画
|
||||||
|
mDotAnimationSet.start();
|
||||||
|
} else if (state == NORMAL) {
|
||||||
|
mDotAnimationSet.cancel();
|
||||||
|
} else if (state == DOWNLOADING) {
|
||||||
|
mDotAnimationSet.cancel();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置按钮文字
|
||||||
|
*/
|
||||||
|
public void setCurrentText(CharSequence charSequence) {
|
||||||
|
mCurrentText = charSequence;
|
||||||
|
invalidate();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置带下载进度的文字
|
||||||
|
*/
|
||||||
|
@SuppressLint("StringFormatMatches")
|
||||||
|
@TargetApi(Build.VERSION_CODES.KITKAT)
|
||||||
|
public void setProgressText(String text, float progress) {
|
||||||
|
if (progress >= mMinProgress && progress < mMaxProgress) {
|
||||||
|
mCurrentText = text + getResources().getString(R.string.downloaded, (int) progress);
|
||||||
|
mToProgress = progress;
|
||||||
|
if (mProgressAnimation.isRunning()) {
|
||||||
|
mProgressAnimation.start();
|
||||||
|
} else {
|
||||||
|
mProgressAnimation.start();
|
||||||
|
}
|
||||||
|
} else if (progress < mMinProgress) {
|
||||||
|
mProgress = 0;
|
||||||
|
} else if (progress >= mMaxProgress) {
|
||||||
|
mProgress = 100;
|
||||||
|
mCurrentText = text + getResources().getString(R.string.downloaded, (int) mProgress);
|
||||||
|
invalidate();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public float getProgress() {
|
||||||
|
return mProgress;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setProgress(float progress) {
|
||||||
|
this.mProgress = progress;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sometimes you should use the method to avoid memory leak
|
||||||
|
*/
|
||||||
|
public void removeAllAnim() {
|
||||||
|
mDotAnimationSet.cancel();
|
||||||
|
mDotAnimationSet.removeAllListeners();
|
||||||
|
mProgressAnimation.cancel();
|
||||||
|
mProgressAnimation.removeAllListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// public void setProgressBtnBackgroundColor(int color) {
|
||||||
|
// initGradientColor(color, color);
|
||||||
|
// }
|
||||||
|
|
||||||
|
|
||||||
|
public void setProgressBtnBackgroundSecondColor(int color) {
|
||||||
|
|
||||||
|
mBackgroundSecondColor = color;
|
||||||
|
}
|
||||||
|
|
||||||
|
public float getButtonRadius() {
|
||||||
|
return mButtonRadius;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setButtonRadius(float buttonRadius) {
|
||||||
|
mButtonRadius = buttonRadius;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getTextColor() {
|
||||||
|
return mTextColor;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setTextColor(int textColor) {
|
||||||
|
mTextColor = textColor;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getTextCoverColor() {
|
||||||
|
return mTextCoverColor;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setTextCoverColor(int textCoverColor) {
|
||||||
|
mTextCoverColor = textCoverColor;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getMinProgress() {
|
||||||
|
return mMinProgress;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setMinProgress(int minProgress) {
|
||||||
|
mMinProgress = minProgress;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getMaxProgress() {
|
||||||
|
return mMaxProgress;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setMaxProgress(int maxProgress) {
|
||||||
|
mMaxProgress = maxProgress;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void enabelDefaultPress(boolean enable) {
|
||||||
|
if (mDefaultController != null) {
|
||||||
|
((DefaultButtonController) mDefaultController).setEnablePress(enable);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// public void enabelDefaultGradient(boolean enable) {
|
||||||
|
// if (mDefaultController != null) {
|
||||||
|
// ((DefaultButtonController) mDefaultController).setEnableGradient(enable);
|
||||||
|
// initGradientColor(mDefaultController.getLighterColor(mBackgroundColor[0]), mBackgroundColor[0]);
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setTextSize(float size) {
|
||||||
|
mAboveTextSize = size;
|
||||||
|
mTextPaint.setTextSize(size);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public float getTextSize() {
|
||||||
|
return mAboveTextSize;
|
||||||
|
}
|
||||||
|
|
||||||
|
public AnimDownloadProgressButton setCustomerController(ButtonController customerController) {
|
||||||
|
mCustomerController = customerController;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onRestoreInstanceState(Parcelable state) {
|
||||||
|
SavedState ss = (SavedState) state;
|
||||||
|
super.onRestoreInstanceState(ss.getSuperState());
|
||||||
|
mState = ss.state;
|
||||||
|
mProgress = ss.progress;
|
||||||
|
mCurrentText = ss.currentText;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Parcelable onSaveInstanceState() {
|
||||||
|
Parcelable superState = super.onSaveInstanceState();
|
||||||
|
return new SavedState(superState, (int) mProgress, mState, mCurrentText.toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class SavedState extends BaseSavedState {
|
||||||
|
|
||||||
|
private int progress;
|
||||||
|
private int state;
|
||||||
|
private String currentText;
|
||||||
|
|
||||||
|
public SavedState(Parcelable parcel, int progress, int state, String currentText) {
|
||||||
|
super(parcel);
|
||||||
|
this.progress = progress;
|
||||||
|
this.state = state;
|
||||||
|
this.currentText = currentText;
|
||||||
|
}
|
||||||
|
|
||||||
|
private SavedState(Parcel in) {
|
||||||
|
super(in);
|
||||||
|
progress = in.readInt();
|
||||||
|
state = in.readInt();
|
||||||
|
currentText = in.readString();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void writeToParcel(Parcel out, int flags) {
|
||||||
|
super.writeToParcel(out, flags);
|
||||||
|
out.writeInt(progress);
|
||||||
|
out.writeInt(state);
|
||||||
|
out.writeString(currentText);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static final Creator<SavedState> CREATOR = new Creator<SavedState>() {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public SavedState createFromParcel(Parcel in) {
|
||||||
|
return new SavedState(in);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public SavedState[] newArray(int size) {
|
||||||
|
return new SavedState[size];
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
58
app/src/main/java/com/keyboard/craft/view/CraftKeyBoard.kt
Normal file
58
app/src/main/java/com/keyboard/craft/view/CraftKeyBoard.kt
Normal file
@ -0,0 +1,58 @@
|
|||||||
|
package com.keyboard.craft.view
|
||||||
|
|
||||||
|
|
||||||
|
open class CraftKeyBoard {
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
/**
|
||||||
|
* Shift键 -> 一般用来切换键盘大小写字母
|
||||||
|
*/
|
||||||
|
const val KEYCODE_SHIFT = -1
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 模式改变 -> 切换键盘输入法
|
||||||
|
*/
|
||||||
|
const val KEYCODE_MODE_CHANGE = -2
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 取消键 -> 关闭输入法
|
||||||
|
*/
|
||||||
|
const val KEYCODE_CANCEL = -3
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 完成键 -> 长出现在右下角蓝色的完成按钮
|
||||||
|
*/
|
||||||
|
const val KEYCODE_DONE = -4
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 删除键 -> 删除输入框内容
|
||||||
|
*/
|
||||||
|
const val KEYCODE_DELETE = -5
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 键盘按键 -> 返回(返回,适用于切换键盘后界面使用,如:NORMAL_MODE_CHANGE或CUSTOM_MODE_CHANGE键盘)
|
||||||
|
*/
|
||||||
|
const val KEYCODE_MODE_BACK = -101
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 键盘按键 ->返回(直接返回到最初,直接返回到NORMAL或CUSTOM键盘)
|
||||||
|
*/
|
||||||
|
const val KEYCODE_BACK = -102
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 键盘按键 ->更多
|
||||||
|
*/
|
||||||
|
const val KEYCODE_MORE = -103
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 键盘类型
|
||||||
|
*/
|
||||||
|
object KeyboardType {
|
||||||
|
/**
|
||||||
|
* 默认键盘 - 字母带符号
|
||||||
|
*/
|
||||||
|
const val NORMAL = 0x00000001
|
||||||
|
}
|
||||||
|
}
|
||||||
497
app/src/main/java/com/keyboard/craft/view/CraftKeyboardView.kt
Normal file
497
app/src/main/java/com/keyboard/craft/view/CraftKeyboardView.kt
Normal file
@ -0,0 +1,497 @@
|
|||||||
|
package com.keyboard.craft.view
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.graphics.Canvas
|
||||||
|
import android.graphics.Paint
|
||||||
|
import android.graphics.Typeface
|
||||||
|
import android.graphics.drawable.Drawable
|
||||||
|
import android.util.AttributeSet
|
||||||
|
import androidx.core.content.ContextCompat
|
||||||
|
import androidx.core.graphics.drawable.DrawableCompat
|
||||||
|
import com.keyboard.craft.R
|
||||||
|
import com.keyboard.craft.view.CraftKeyboardView.Config.Companion.KEYCODE_BACK
|
||||||
|
import com.keyboard.craft.view.CraftKeyboardView.Config.Companion.KEYCODE_CANCEL
|
||||||
|
import com.keyboard.craft.view.CraftKeyboardView.Config.Companion.KEYCODE_DELETE
|
||||||
|
import com.keyboard.craft.view.CraftKeyboardView.Config.Companion.KEYCODE_DONE
|
||||||
|
import com.keyboard.craft.view.CraftKeyboardView.Config.Companion.KEYCODE_DOT
|
||||||
|
import com.keyboard.craft.view.CraftKeyboardView.Config.Companion.KEYCODE_MODE_BACK
|
||||||
|
import com.keyboard.craft.view.CraftKeyboardView.Config.Companion.KEYCODE_MODE_CHANGE
|
||||||
|
import com.keyboard.craft.view.CraftKeyboardView.Config.Companion.KEYCODE_MORE
|
||||||
|
import com.keyboard.craft.view.CraftKeyboardView.Config.Companion.KEYCODE_NONE
|
||||||
|
import com.keyboard.craft.view.CraftKeyboardView.Config.Companion.KEYCODE_QUESTION_MARK
|
||||||
|
import com.keyboard.craft.view.CraftKeyboardView.Config.Companion.KEYCODE_SPACE
|
||||||
|
|
||||||
|
open class CraftKeyboardView : com.keyboard.craft.view.KeyboardView {
|
||||||
|
|
||||||
|
|
||||||
|
private var isCap = false
|
||||||
|
|
||||||
|
private var isAllCaps = false
|
||||||
|
|
||||||
|
private lateinit var config: Config
|
||||||
|
|
||||||
|
private val paint by lazy { Paint() }
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
const val iconRatio = 0.5f
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
constructor(context: Context, attrs: AttributeSet?) : super(context, attrs) {
|
||||||
|
init(context, attrs)
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super(
|
||||||
|
context,
|
||||||
|
attrs,
|
||||||
|
defStyleAttr
|
||||||
|
) {
|
||||||
|
init(context, attrs)
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
context: Context,
|
||||||
|
attrs: AttributeSet?,
|
||||||
|
defStyleAttr: Int,
|
||||||
|
defStyleRes: Int
|
||||||
|
) : super(context, attrs, defStyleAttr, defStyleRes) {
|
||||||
|
init(context, attrs)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private fun init(context: Context, attrs: AttributeSet?) {
|
||||||
|
config = Config(context)
|
||||||
|
|
||||||
|
var a = context.obtainStyledAttributes(attrs, R.styleable.craftKeyboardView)
|
||||||
|
|
||||||
|
a.indexCount.let {
|
||||||
|
config.run {
|
||||||
|
for (i in 0 until it) {
|
||||||
|
when (val attr = a.getIndex(i)) {
|
||||||
|
R.styleable.craftKeyboardView_kkbDeleteDrawable -> deleteDrawable =
|
||||||
|
a.getDrawable(attr)
|
||||||
|
|
||||||
|
R.styleable.craftKeyboardView_kkbCapitalDrawable -> capitalDrawable =
|
||||||
|
a.getDrawable(attr)
|
||||||
|
|
||||||
|
R.styleable.craftKeyboardView_kkbCapitalLockDrawable -> capitalLockDrawable =
|
||||||
|
a.getDrawable(attr)
|
||||||
|
|
||||||
|
R.styleable.craftKeyboardView_android_labelTextSize -> labelTextSize =
|
||||||
|
a.getDimensionPixelSize(attr, labelTextSize)
|
||||||
|
|
||||||
|
R.styleable.craftKeyboardView_android_keyTextSize -> keyTextSize =
|
||||||
|
a.getDimensionPixelSize(attr, keyTextSize)
|
||||||
|
|
||||||
|
R.styleable.craftKeyboardView_android_keyTextColor -> keyTextColor =
|
||||||
|
a.getColor(attr, keyTextColor)
|
||||||
|
|
||||||
|
R.styleable.craftKeyboardView_kkbKeyIconColor -> keyIconColor = a.getColor(
|
||||||
|
attr,
|
||||||
|
ContextCompat.getColor(context, R.color.craft_keyboard_key_icon_color)
|
||||||
|
)
|
||||||
|
|
||||||
|
R.styleable.craftKeyboardView_kkbKeySpecialTextColor -> keySpecialTextColor =
|
||||||
|
a.getColor(attr, keySpecialTextColor)
|
||||||
|
|
||||||
|
R.styleable.craftKeyboardView_kkbKeyDoneTextColor -> keyDoneTextColor =
|
||||||
|
a.getColor(attr, keyDoneTextColor)
|
||||||
|
|
||||||
|
R.styleable.craftKeyboardView_kkbKeyNoneTextColor -> keyNoneTextColor =
|
||||||
|
a.getColor(attr, keyNoneTextColor)
|
||||||
|
|
||||||
|
R.styleable.craftKeyboardView_android_keyBackground -> keyBackground =
|
||||||
|
a.getDrawable(attr)
|
||||||
|
|
||||||
|
R.styleable.craftKeyboardView_kkbSpecialKeyBackground -> specialKeyBackground =
|
||||||
|
a.getDrawable(attr)
|
||||||
|
|
||||||
|
R.styleable.craftKeyboardView_kkbDoneKeyBackground -> doneKeyBackground =
|
||||||
|
a.getDrawable(attr)
|
||||||
|
|
||||||
|
R.styleable.craftKeyboardView_kkbNoneKeyBackground -> noneKeyBackground =
|
||||||
|
a.getDrawable(attr)
|
||||||
|
|
||||||
|
R.styleable.craftKeyboardView_kkbKeyDoneTextSize -> keyDoneTextSize =
|
||||||
|
a.getDimensionPixelSize(attr, keyDoneTextSize)
|
||||||
|
|
||||||
|
R.styleable.craftKeyboardView_kkbKeyDoneText -> keyDoneText =
|
||||||
|
a.getString(attr)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
a.recycle()
|
||||||
|
}
|
||||||
|
|
||||||
|
paint.textAlign = Paint.Align.CENTER
|
||||||
|
paint.isAntiAlias = true
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getConfig(): Config {
|
||||||
|
return config
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setConfig(config: Config) {
|
||||||
|
this.config = config
|
||||||
|
invalidate()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDraw(canvas: Canvas) {
|
||||||
|
super.onDraw(canvas)
|
||||||
|
drawKeyboard(canvas, keyboard?.keys)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 绘制键盘
|
||||||
|
*/
|
||||||
|
private fun drawKeyboard(canvas: Canvas, keys: List<com.keyboard.craft.view.Keyboard.Key>?) {
|
||||||
|
keys?.let {
|
||||||
|
for (key in it) {
|
||||||
|
drawKey(canvas, key)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 绘制键盘按键
|
||||||
|
*/
|
||||||
|
private fun drawKey(canvas: Canvas, key: com.keyboard.craft.view.Keyboard.Key) {
|
||||||
|
when (key.codes[0]) {
|
||||||
|
com.keyboard.craft.view.Keyboard.KEYCODE_SHIFT -> drawShiftKey(canvas, key)
|
||||||
|
KEYCODE_MODE_CHANGE -> drawKey(
|
||||||
|
canvas,
|
||||||
|
key,
|
||||||
|
config.toggleKeyBackground,
|
||||||
|
config.toggleKeyTextColor
|
||||||
|
)
|
||||||
|
|
||||||
|
KEYCODE_CANCEL -> drawCancelKey(canvas, key)
|
||||||
|
KEYCODE_DONE -> drawDoneKey(canvas, key)
|
||||||
|
KEYCODE_DELETE -> drawDeleteKey(canvas, key)
|
||||||
|
KEYCODE_SPACE ->
|
||||||
|
config.keySpecialText.let {
|
||||||
|
key.label = it
|
||||||
|
drawKey(
|
||||||
|
canvas,
|
||||||
|
key,
|
||||||
|
config.toggleKeyBackground,
|
||||||
|
config.toggleKeyTextColor,
|
||||||
|
null
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
KEYCODE_DOT->{
|
||||||
|
drawKey(
|
||||||
|
canvas,
|
||||||
|
key,
|
||||||
|
config.toggleKeyBackground,
|
||||||
|
config.toggleKeyTextColor,
|
||||||
|
null
|
||||||
|
)
|
||||||
|
}
|
||||||
|
KEYCODE_QUESTION_MARK->{
|
||||||
|
drawKey(
|
||||||
|
canvas,
|
||||||
|
key,
|
||||||
|
config.toggleKeyBackground,
|
||||||
|
config.toggleKeyTextColor,
|
||||||
|
null
|
||||||
|
)
|
||||||
|
}
|
||||||
|
KEYCODE_NONE -> drawNoneKey(canvas, key)
|
||||||
|
KEYCODE_MODE_BACK -> drawKey(
|
||||||
|
canvas,
|
||||||
|
key,
|
||||||
|
config.toggleKeyBackground,
|
||||||
|
config.toggleKeyTextColor
|
||||||
|
)
|
||||||
|
|
||||||
|
KEYCODE_BACK -> drawKey(
|
||||||
|
canvas,
|
||||||
|
key,
|
||||||
|
config.toggleKeyBackground,
|
||||||
|
config.toggleKeyTextColor
|
||||||
|
)
|
||||||
|
|
||||||
|
KEYCODE_MORE -> drawKey(
|
||||||
|
canvas,
|
||||||
|
key,
|
||||||
|
config.toggleKeyBackground,
|
||||||
|
config.toggleKeyTextColor
|
||||||
|
)
|
||||||
|
|
||||||
|
in -399..-300 -> drawKey(
|
||||||
|
canvas,
|
||||||
|
key,
|
||||||
|
config.toggleKeyBackground,
|
||||||
|
config.keyTextColor
|
||||||
|
)
|
||||||
|
|
||||||
|
else -> drawKey(canvas, key, config.keyBackground, config.keyTextColor)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun drawCancelKey(canvas: Canvas, key: com.keyboard.craft.view.Keyboard.Key) {
|
||||||
|
config.keyDoneText?.let {
|
||||||
|
key.label = it
|
||||||
|
}
|
||||||
|
drawKey(canvas, key, config.toggleKeyBackground, config.toggleKeyTextColor, null, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun drawDoneKey(canvas: Canvas, key: com.keyboard.craft.view.Keyboard.Key) {
|
||||||
|
config.keyDoneText?.let {
|
||||||
|
key.label = it
|
||||||
|
}
|
||||||
|
drawKey(canvas, key, config.toggleKeyBackground, config.toggleKeyTextColor, null, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun drawNoneKey(canvas: Canvas, key: com.keyboard.craft.view.Keyboard.Key) {
|
||||||
|
drawKey(canvas, key, config.noneKeyBackground, config.keyNoneTextColor)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun drawDeleteKey(canvas: Canvas, key: com.keyboard.craft.view.Keyboard.Key) {
|
||||||
|
drawKey(
|
||||||
|
canvas,
|
||||||
|
key,
|
||||||
|
config.toggleKeyBackground,
|
||||||
|
config.keySpecialTextColor,
|
||||||
|
config.deleteDrawable
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun drawShiftKey(canvas: Canvas, key: com.keyboard.craft.view.Keyboard.Key) {
|
||||||
|
when {
|
||||||
|
isAllCaps -> drawKey(
|
||||||
|
canvas,
|
||||||
|
key,
|
||||||
|
config.toggleKeyBackground,
|
||||||
|
config.keySpecialTextColor,
|
||||||
|
config.capitalLockDrawable
|
||||||
|
)
|
||||||
|
|
||||||
|
isCap -> drawKey(
|
||||||
|
canvas,
|
||||||
|
key,
|
||||||
|
config.toggleKeyBackground,
|
||||||
|
config.keySpecialTextColor,
|
||||||
|
config.capitalDrawable
|
||||||
|
)
|
||||||
|
|
||||||
|
else -> drawKey(
|
||||||
|
canvas,
|
||||||
|
key,
|
||||||
|
config.toggleKeyBackground,
|
||||||
|
config.keySpecialTextColor,
|
||||||
|
config.lowerDrawable
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 绘制键盘按键
|
||||||
|
*/
|
||||||
|
private fun drawKey(
|
||||||
|
canvas: Canvas,
|
||||||
|
key: com.keyboard.craft.view.Keyboard.Key,
|
||||||
|
keyBackground: Drawable?,
|
||||||
|
textColor: Int,
|
||||||
|
iconDrawable: Drawable? = key.icon,
|
||||||
|
isDone: Boolean = false
|
||||||
|
) {
|
||||||
|
//绘制按键背景
|
||||||
|
keyBackground?.run {
|
||||||
|
if (key.codes[0] != 0) {
|
||||||
|
state = key.currentDrawableState
|
||||||
|
}
|
||||||
|
|
||||||
|
setBounds(
|
||||||
|
key.x.plus(paddingLeft),
|
||||||
|
key.y.plus(paddingTop),
|
||||||
|
key.x.plus(paddingLeft).plus(key.width),
|
||||||
|
key.y.plus(paddingTop).plus(key.height)
|
||||||
|
)
|
||||||
|
draw(canvas)
|
||||||
|
}
|
||||||
|
|
||||||
|
//绘制键盘图标
|
||||||
|
iconDrawable?.run {
|
||||||
|
|
||||||
|
val drawable = DrawableCompat.wrap(this)
|
||||||
|
config.keyIconColor?.takeIf { it != 0 }?.let {
|
||||||
|
drawable.setTint(it)
|
||||||
|
}
|
||||||
|
|
||||||
|
key.icon = drawable
|
||||||
|
|
||||||
|
var iconWidth = key.icon.intrinsicWidth.toFloat()
|
||||||
|
var iconHeight = key.icon.intrinsicHeight.toFloat()
|
||||||
|
|
||||||
|
val widthRatio = iconWidth.div(key.width.toFloat())
|
||||||
|
val heightRatio = iconHeight.div(key.height.toFloat())
|
||||||
|
|
||||||
|
if (widthRatio <= heightRatio) {//当图标的宽占比小于等于高占比时,以高度比例为基准并控制在iconRatio比例范围内,进行同比例缩放
|
||||||
|
|
||||||
|
val ratio = heightRatio.coerceAtMost(iconRatio)
|
||||||
|
iconWidth = iconWidth.div(heightRatio).times(ratio)
|
||||||
|
iconHeight = iconHeight.div(heightRatio).times(ratio)
|
||||||
|
|
||||||
|
} else {//反之,则以宽度比例为基准并控制在iconRatio比例范围内,进行同比例缩放
|
||||||
|
|
||||||
|
val ratio = widthRatio.coerceAtMost(iconRatio)
|
||||||
|
iconWidth = iconWidth.div(widthRatio).times(ratio)
|
||||||
|
iconHeight = iconHeight.div(widthRatio).times(ratio)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
val left = key.x.plus(paddingLeft).plus(key.width.minus(iconWidth).div(2f)).toInt()
|
||||||
|
val top = key.y.plus(paddingTop).plus(key.height.minus(iconHeight).div(2f)).toInt()
|
||||||
|
val right = left.plus(iconWidth).toInt()
|
||||||
|
val bottom = top.plus(iconHeight).toInt()
|
||||||
|
key.icon.setBounds(left, top, right, bottom)
|
||||||
|
key.icon.draw(canvas)
|
||||||
|
|
||||||
|
} ?: key.label?.let {
|
||||||
|
//绘制键盘文字
|
||||||
|
if (isDone) {
|
||||||
|
paint.textSize = config.keyDoneTextSize.toFloat()
|
||||||
|
} else if (it.length > 1 && key.codes.size < 2) {// 键盘key内容多个字符
|
||||||
|
paint.textSize = config.labelTextSize.toFloat()
|
||||||
|
} else {
|
||||||
|
paint.textSize = config.keyTextSize.toFloat()
|
||||||
|
}
|
||||||
|
paint.color = textColor
|
||||||
|
paint.typeface = Typeface.DEFAULT
|
||||||
|
|
||||||
|
canvas.drawText(
|
||||||
|
it.toString(),
|
||||||
|
key.x.plus(paddingLeft).plus(key.width.div(2f)),
|
||||||
|
key.y.plus(paddingTop).plus(key.height.div(2.0f)).plus(
|
||||||
|
paint.textSize.minus(paint.descent()).div(2.0f)
|
||||||
|
),
|
||||||
|
paint
|
||||||
|
)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
fun setCap(isCap: Boolean) {
|
||||||
|
this.isCap = isCap
|
||||||
|
}
|
||||||
|
|
||||||
|
fun isCap(): Boolean {
|
||||||
|
return isCap
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setAllCaps(isAllCaps: Boolean) {
|
||||||
|
this.isAllCaps = isAllCaps
|
||||||
|
}
|
||||||
|
|
||||||
|
fun isAllCaps(): Boolean {
|
||||||
|
return isAllCaps
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Config为craftKeyboard的配置类,方便统一管理配置信息
|
||||||
|
*/
|
||||||
|
open class Config(context: Context) {
|
||||||
|
|
||||||
|
var deleteDrawable = context.getDrawable(R.drawable.delete_key_background)
|
||||||
|
var lowerDrawable = context.getDrawable(R.mipmap.sym_keyboard_shift)
|
||||||
|
var capitalDrawable = context.getDrawable(R.mipmap.sym_keyboard_shift)
|
||||||
|
var capitalLockDrawable = context.getDrawable(R.mipmap.sym_keyboard_shift_locked)
|
||||||
|
|
||||||
|
var labelTextSize =
|
||||||
|
context.resources.getDimensionPixelSize(R.dimen.craft_keyboard_label_text_size)
|
||||||
|
|
||||||
|
var keyTextSize = context.resources.getDimensionPixelSize(R.dimen.craft_keyboard_text_size)
|
||||||
|
|
||||||
|
var keyTextColor = ContextCompat.getColor(context, R.color.purple_200)
|
||||||
|
|
||||||
|
var keyIconColor: Int? = null
|
||||||
|
|
||||||
|
var keySpecialTextColor =
|
||||||
|
ContextCompat.getColor(context, R.color.purple_500)
|
||||||
|
|
||||||
|
var keyDoneTextColor =
|
||||||
|
ContextCompat.getColor(context, R.color.purple_700)
|
||||||
|
|
||||||
|
var keyNoneTextColor =
|
||||||
|
ContextCompat.getColor(context, R.color.teal_200)
|
||||||
|
|
||||||
|
var keyBackground = context.getDrawable(R.drawable.key_background)
|
||||||
|
|
||||||
|
var specialKeyBackground = context.getDrawable(R.drawable.key_background)
|
||||||
|
|
||||||
|
var toggleKeyBackground = context.getDrawable(R.drawable.key_background)
|
||||||
|
|
||||||
|
var toggleKeyTextColor = ContextCompat.getColor(context, R.color.purple_500)
|
||||||
|
|
||||||
|
var doneKeyBackground = context.getDrawable(R.drawable.key_background)
|
||||||
|
var noneKeyBackground = context.getDrawable(R.drawable.key_background)
|
||||||
|
|
||||||
|
var keyDoneTextSize =
|
||||||
|
context.resources.getDimensionPixelSize(R.dimen.craft_keyboard_done_text_size)
|
||||||
|
|
||||||
|
var keyDoneText: CharSequence? = context.getString(R.string.craft_keyboard_key_done_text)
|
||||||
|
|
||||||
|
|
||||||
|
var keySpecialText: CharSequence? = "English"
|
||||||
|
|
||||||
|
companion object{
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Shift键 -> 一般用来切换键盘大小写字母
|
||||||
|
*/
|
||||||
|
const val KEYCODE_SHIFT = -1
|
||||||
|
/**
|
||||||
|
* 模式改变 -> 切换键盘输入法
|
||||||
|
*/
|
||||||
|
const val KEYCODE_MODE_CHANGE = -2
|
||||||
|
/**
|
||||||
|
* 取消键 -> 关闭输入法
|
||||||
|
*/
|
||||||
|
const val KEYCODE_CANCEL = -3
|
||||||
|
/**
|
||||||
|
* 完成键 -> 长出现在右下角蓝色的完成按钮
|
||||||
|
*/
|
||||||
|
const val KEYCODE_DONE = -4
|
||||||
|
/**
|
||||||
|
* 删除键 -> 删除输入框内容
|
||||||
|
*/
|
||||||
|
const val KEYCODE_DELETE = -5
|
||||||
|
/**
|
||||||
|
* 空格键
|
||||||
|
*/
|
||||||
|
const val KEYCODE_SPACE = 32
|
||||||
|
|
||||||
|
const val KEYCODE_DOT = 46
|
||||||
|
|
||||||
|
const val KEYCODE_QUESTION_MARK = 63
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 无作用键 -> 一般用来占位或者禁用按键
|
||||||
|
*/
|
||||||
|
const val KEYCODE_NONE = 0
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 键盘按键 -> 返回(返回,适用于切换键盘后界面使用,如:NORMAL_MODE_CHANGE或CUSTOM_MODE_CHANGE键盘)
|
||||||
|
*/
|
||||||
|
const val KEYCODE_MODE_BACK = -101
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 键盘按键 ->返回(直接返回到最初,直接返回到NORMAL或CUSTOM键盘)
|
||||||
|
*/
|
||||||
|
const val KEYCODE_BACK = -102
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 键盘按键 ->更多
|
||||||
|
*/
|
||||||
|
const val KEYCODE_MORE = -103
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@ -0,0 +1,83 @@
|
|||||||
|
package com.keyboard.craft.view
|
||||||
|
|
||||||
|
import android.graphics.Color
|
||||||
|
|
||||||
|
interface ButtonController {
|
||||||
|
fun getPressedColor(color: Int): Int
|
||||||
|
|
||||||
|
fun getLighterColor(color: Int): Int
|
||||||
|
|
||||||
|
fun getDarkerColor(color: Int): Int
|
||||||
|
|
||||||
|
fun enablePress(): Boolean
|
||||||
|
|
||||||
|
fun enableGradient(): Boolean
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
class DefaultButtonController : ButtonController {
|
||||||
|
private var enablePress = false
|
||||||
|
|
||||||
|
private var enableGradient = false
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获得按下的颜色(明度降低10%)
|
||||||
|
*
|
||||||
|
* @param color
|
||||||
|
* @return int
|
||||||
|
*/
|
||||||
|
override fun getPressedColor(color: Int): Int {
|
||||||
|
val hsv = FloatArray(3)
|
||||||
|
Color.colorToHSV(color, hsv)
|
||||||
|
hsv[2] -= 0.1f
|
||||||
|
return Color.HSVToColor(hsv)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 由右边的颜色算出左边的颜色(左边的颜色比右边的颜色降饱和度30%,亮度增加30%)
|
||||||
|
* +
|
||||||
|
*
|
||||||
|
* @param color
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
override fun getLighterColor(color: Int): Int {
|
||||||
|
val hsv = FloatArray(3)
|
||||||
|
Color.colorToHSV(color, hsv)
|
||||||
|
hsv[1] -= 0.3f
|
||||||
|
hsv[2] += 0.3f
|
||||||
|
return Color.HSVToColor(hsv)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 由左边的颜色生成右边的颜色
|
||||||
|
*
|
||||||
|
* @param color
|
||||||
|
* @return int
|
||||||
|
*/
|
||||||
|
override fun getDarkerColor(color: Int): Int {
|
||||||
|
val hsv = FloatArray(3)
|
||||||
|
Color.colorToHSV(color, hsv)
|
||||||
|
hsv[1] += 0.3f
|
||||||
|
hsv[2] -= 0.3f
|
||||||
|
return Color.HSVToColor(hsv)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun enablePress(): Boolean {
|
||||||
|
return enablePress
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun enableGradient(): Boolean {
|
||||||
|
return enableGradient
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setEnablePress(enablePress: Boolean): DefaultButtonController? {
|
||||||
|
this.enablePress = enablePress
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setEnableGradient(enableGradient: Boolean): DefaultButtonController? {
|
||||||
|
this.enableGradient = enableGradient
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
899
app/src/main/java/com/keyboard/craft/view/Keyboard.java
Normal file
899
app/src/main/java/com/keyboard/craft/view/Keyboard.java
Normal file
@ -0,0 +1,899 @@
|
|||||||
|
package com.keyboard.craft.view;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
import android.content.res.Resources;
|
||||||
|
import android.content.res.TypedArray;
|
||||||
|
import android.content.res.XmlResourceParser;
|
||||||
|
import android.graphics.drawable.Drawable;
|
||||||
|
import android.text.TextUtils;
|
||||||
|
import android.util.DisplayMetrics;
|
||||||
|
import android.util.Log;
|
||||||
|
import android.util.TypedValue;
|
||||||
|
import android.util.Xml;
|
||||||
|
|
||||||
|
import com.keyboard.craft.R;
|
||||||
|
|
||||||
|
import androidx.annotation.XmlRes;
|
||||||
|
|
||||||
|
import org.xmlpull.v1.XmlPullParserException;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.StringTokenizer;
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Loads an XML description of a keyboard and stores the attributes of the keys. A keyboard
|
||||||
|
* consists of rows of keys.
|
||||||
|
* <p>The layout file for a keyboard contains XML that looks like the following snippet:</p>
|
||||||
|
* <pre>
|
||||||
|
* <Keyboard
|
||||||
|
* android:keyWidth="%10p"
|
||||||
|
* android:keyHeight="50px"
|
||||||
|
* android:horizontalGap="2px"
|
||||||
|
* android:verticalGap="2px" >
|
||||||
|
* <Row android:keyWidth="32px" >
|
||||||
|
* <Key android:keyLabel="A" />
|
||||||
|
* ...
|
||||||
|
* </Row>
|
||||||
|
* ...
|
||||||
|
* </Keyboard>
|
||||||
|
* </pre>
|
||||||
|
* @attr ref android.R.styleable#craft_Keyboard_keyWidth
|
||||||
|
* @attr ref android.R.styleable#craft_Keyboard_keyHeight
|
||||||
|
* @attr ref android.R.styleable#craft_Keyboard_horizontalGap
|
||||||
|
* @attr ref android.R.styleable#craft_Keyboard_verticalGap
|
||||||
|
* This class is deprecated because this is just a convenient UI widget class that
|
||||||
|
* application developers can re-implement on top of existing public APIs. If you have
|
||||||
|
* already depended on this class, consider copying the implementation from AOSP into
|
||||||
|
* your project or re-implementing a similar widget by yourselves
|
||||||
|
*/
|
||||||
|
public class Keyboard {
|
||||||
|
|
||||||
|
static final String TAG = "Keyboard";
|
||||||
|
|
||||||
|
// Keyboard XML Tags
|
||||||
|
private static final String TAG_KEYBOARD = "Keyboard";
|
||||||
|
private static final String TAG_ROW = "Row";
|
||||||
|
private static final String TAG_KEY = "Key";
|
||||||
|
|
||||||
|
public static final int EDGE_LEFT = 0x01;
|
||||||
|
public static final int EDGE_RIGHT = 0x02;
|
||||||
|
public static final int EDGE_TOP = 0x04;
|
||||||
|
public static final int EDGE_BOTTOM = 0x08;
|
||||||
|
|
||||||
|
public static final int KEYCODE_SHIFT = -1;
|
||||||
|
public static final int KEYCODE_MODE_CHANGE = -2;
|
||||||
|
public static final int KEYCODE_CANCEL = -3;
|
||||||
|
public static final int KEYCODE_DONE = -4;
|
||||||
|
public static final int KEYCODE_DELETE = -5;
|
||||||
|
public static final int KEYCODE_ALT = -6;
|
||||||
|
|
||||||
|
/** Keyboard label **/
|
||||||
|
private CharSequence mLabel;
|
||||||
|
|
||||||
|
/** Horizontal gap default for all rows */
|
||||||
|
private int mDefaultHorizontalGap;
|
||||||
|
|
||||||
|
/** Default key width */
|
||||||
|
private int mDefaultWidth;
|
||||||
|
|
||||||
|
/** Default key height */
|
||||||
|
private int mDefaultHeight;
|
||||||
|
|
||||||
|
/** Default gap between rows */
|
||||||
|
private int mDefaultVerticalGap;
|
||||||
|
|
||||||
|
/** Is the keyboard in the shifted state */
|
||||||
|
private boolean mShifted;
|
||||||
|
|
||||||
|
/** Key instance for the shift key, if present */
|
||||||
|
private Key[] mShiftKeys = { null, null };
|
||||||
|
|
||||||
|
/** Key index for the shift key, if present */
|
||||||
|
private int[] mShiftKeyIndices = {-1, -1};
|
||||||
|
|
||||||
|
/** Current key width, while loading the keyboard */
|
||||||
|
private int mKeyWidth;
|
||||||
|
|
||||||
|
/** Current key height, while loading the keyboard */
|
||||||
|
private int mKeyHeight;
|
||||||
|
|
||||||
|
/** Total height of the keyboard, including the padding and keys */
|
||||||
|
private int mTotalHeight;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Total width of the keyboard, including left side gaps and keys, but not any gaps on the
|
||||||
|
* right side.
|
||||||
|
*/
|
||||||
|
private int mTotalWidth;
|
||||||
|
|
||||||
|
/** List of keys in this keyboard */
|
||||||
|
private List<Key> mKeys;
|
||||||
|
|
||||||
|
/** List of modifier keys such as Shift & Alt, if any */
|
||||||
|
private List<Key> mModifierKeys;
|
||||||
|
|
||||||
|
/** Width of the screen available to fit the keyboard */
|
||||||
|
private int mDisplayWidth;
|
||||||
|
|
||||||
|
/** Height of the screen */
|
||||||
|
private int mDisplayHeight;
|
||||||
|
|
||||||
|
/** Keyboard mode, or zero, if none. */
|
||||||
|
private int mKeyboardMode;
|
||||||
|
|
||||||
|
// Variables for pre-computing nearest keys.
|
||||||
|
|
||||||
|
private static final int GRID_WIDTH = 10;
|
||||||
|
private static final int GRID_HEIGHT = 5;
|
||||||
|
private static final int GRID_SIZE = GRID_WIDTH * GRID_HEIGHT;
|
||||||
|
private int mCellWidth;
|
||||||
|
private int mCellHeight;
|
||||||
|
private int[][] mGridNeighbors;
|
||||||
|
private int mProximityThreshold;
|
||||||
|
/** Number of key widths from current touch point to search for nearest keys. */
|
||||||
|
private static float SEARCH_DISTANCE = 1.8f;
|
||||||
|
|
||||||
|
private ArrayList<Row> rows = new ArrayList<>();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Container for keys in the keyboard. All keys in a row are at the same Y-coordinate.
|
||||||
|
* Some of the key size defaults can be overridden per row from what the {@link Keyboard}
|
||||||
|
* defines.
|
||||||
|
* @attr ref android.R.styleable#craft_Keyboard_keyWidth
|
||||||
|
* @attr ref android.R.styleable#craft_Keyboard_keyHeight
|
||||||
|
* @attr ref android.R.styleable#craft_Keyboard_horizontalGap
|
||||||
|
* @attr ref android.R.styleable#craft_Keyboard_verticalGap
|
||||||
|
* @attr ref android.R.styleable#craft_Keyboard_Row_rowEdgeFlags
|
||||||
|
* @attr ref android.R.styleable#craft_Keyboard_Row_keyboardMode
|
||||||
|
*/
|
||||||
|
public static class Row {
|
||||||
|
/** Default width of a key in this row. */
|
||||||
|
public int defaultWidth;
|
||||||
|
/** Default height of a key in this row. */
|
||||||
|
public int defaultHeight;
|
||||||
|
/** Default horizontal gap between keys in this row. */
|
||||||
|
public int defaultHorizontalGap;
|
||||||
|
/** Vertical gap following this row. */
|
||||||
|
public int verticalGap;
|
||||||
|
|
||||||
|
ArrayList<Key> mKeys = new ArrayList<>();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Edge flags for this row of keys. Possible values that can be assigned are
|
||||||
|
* {@link Keyboard#EDGE_TOP EDGE_TOP} and {@link Keyboard#EDGE_BOTTOM EDGE_BOTTOM}
|
||||||
|
*/
|
||||||
|
public int rowEdgeFlags;
|
||||||
|
|
||||||
|
/** The keyboard mode for this row */
|
||||||
|
public int mode;
|
||||||
|
|
||||||
|
private Keyboard parent;
|
||||||
|
|
||||||
|
public Row(Keyboard parent) {
|
||||||
|
this.parent = parent;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Row(Resources res, Keyboard parent, XmlResourceParser parser) {
|
||||||
|
this.parent = parent;
|
||||||
|
TypedArray a = res.obtainAttributes(Xml.asAttributeSet(parser),
|
||||||
|
R.styleable.craft_Keyboard);
|
||||||
|
defaultWidth = getDimensionOrFraction(a,
|
||||||
|
R.styleable.craft_Keyboard_android_keyWidth,
|
||||||
|
parent.mDisplayWidth, parent.mDefaultWidth);
|
||||||
|
defaultHeight = getDimensionOrFraction(a,
|
||||||
|
R.styleable.craft_Keyboard_android_keyHeight,
|
||||||
|
parent.mDisplayHeight, parent.mDefaultHeight);
|
||||||
|
defaultHorizontalGap = getDimensionOrFraction(a,
|
||||||
|
R.styleable.craft_Keyboard_android_horizontalGap,
|
||||||
|
parent.mDisplayWidth, parent.mDefaultHorizontalGap);
|
||||||
|
verticalGap = getDimensionOrFraction(a,
|
||||||
|
R.styleable.craft_Keyboard_android_verticalGap,
|
||||||
|
parent.mDisplayHeight, parent.mDefaultVerticalGap);
|
||||||
|
a.recycle();
|
||||||
|
a = res.obtainAttributes(Xml.asAttributeSet(parser),
|
||||||
|
R.styleable.craft_Keyboard_Row);
|
||||||
|
rowEdgeFlags = a.getInt(R.styleable.craft_Keyboard_Row_android_rowEdgeFlags, 0);
|
||||||
|
mode = a.getResourceId(R.styleable.craft_Keyboard_Row_android_keyboardMode,
|
||||||
|
0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Class for describing the position and characteristics of a single key in the keyboard.
|
||||||
|
*
|
||||||
|
* @attr ref android.R.styleable#craft_Keyboard_keyWidth
|
||||||
|
* @attr ref android.R.styleable#craft_Keyboard_keyHeight
|
||||||
|
* @attr ref android.R.styleable#craft_Keyboard_horizontalGap
|
||||||
|
* @attr ref android.R.styleable#craft_Keyboard_Key_codes
|
||||||
|
* @attr ref android.R.styleable#craft_Keyboard_Key_keyIcon
|
||||||
|
* @attr ref android.R.styleable#craft_Keyboard_Key_keyLabel
|
||||||
|
* @attr ref android.R.styleable#craft_Keyboard_Key_iconPreview
|
||||||
|
* @attr ref android.R.styleable#craft_Keyboard_Key_isSticky
|
||||||
|
* @attr ref android.R.styleable#craft_Keyboard_Key_isRepeatable
|
||||||
|
* @attr ref android.R.styleable#craft_Keyboard_Key_isModifier
|
||||||
|
* @attr ref android.R.styleable#craft_Keyboard_Key_popupKeyboard
|
||||||
|
* @attr ref android.R.styleable#craft_Keyboard_Key_popupCharacters
|
||||||
|
* @attr ref android.R.styleable#craft_Keyboard_Key_keyOutputText
|
||||||
|
* @attr ref android.R.styleable#craft_Keyboard_Key_keyEdgeFlags
|
||||||
|
*/
|
||||||
|
public static class Key {
|
||||||
|
/**
|
||||||
|
* All the key codes (unicode or custom code) that this key could generate, zero'th
|
||||||
|
* being the most important.
|
||||||
|
*/
|
||||||
|
public int[] codes;
|
||||||
|
|
||||||
|
/** Label to display */
|
||||||
|
public CharSequence label;
|
||||||
|
|
||||||
|
/** Icon to display instead of a label. Icon takes precedence over a label */
|
||||||
|
public Drawable icon;
|
||||||
|
/** Preview version of the icon, for the preview popup */
|
||||||
|
public Drawable iconPreview;
|
||||||
|
/** Width of the key, not including the gap */
|
||||||
|
public int width;
|
||||||
|
/** Height of the key, not including the gap */
|
||||||
|
public int height;
|
||||||
|
/** The horizontal gap before this key */
|
||||||
|
public int gap;
|
||||||
|
/** Whether this key is sticky, i.e., a toggle key */
|
||||||
|
public boolean sticky;
|
||||||
|
/** X coordinate of the key in the keyboard layout */
|
||||||
|
public int x;
|
||||||
|
/** Y coordinate of the key in the keyboard layout */
|
||||||
|
public int y;
|
||||||
|
/** The current pressed state of this key */
|
||||||
|
public boolean pressed;
|
||||||
|
/** If this is a sticky key, is it on? */
|
||||||
|
public boolean on;
|
||||||
|
/** Text to output when pressed. This can be multiple characters, like ".com" */
|
||||||
|
public CharSequence text;
|
||||||
|
/** Popup characters */
|
||||||
|
public CharSequence popupCharacters;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Flags that specify the anchoring to edges of the keyboard for detecting touch events
|
||||||
|
* that are just out of the boundary of the key. This is a bit mask of
|
||||||
|
* {@link Keyboard#EDGE_LEFT}, {@link Keyboard#EDGE_RIGHT}, {@link Keyboard#EDGE_TOP} and
|
||||||
|
* {@link Keyboard#EDGE_BOTTOM}.
|
||||||
|
*/
|
||||||
|
public int edgeFlags;
|
||||||
|
/** Whether this is a modifier key, such as Shift or Alt */
|
||||||
|
public boolean modifier;
|
||||||
|
/** The keyboard that this key belongs to */
|
||||||
|
private Keyboard keyboard;
|
||||||
|
/**
|
||||||
|
* If this key pops up a mini keyboard, this is the resource id for the XML layout for that
|
||||||
|
* keyboard.
|
||||||
|
*/
|
||||||
|
public int popupResId;
|
||||||
|
/** Whether this key repeats itself when held down */
|
||||||
|
public boolean repeatable;
|
||||||
|
|
||||||
|
|
||||||
|
private final static int[] KEY_STATE_NORMAL_ON = {
|
||||||
|
android.R.attr.state_checkable,
|
||||||
|
android.R.attr.state_checked
|
||||||
|
};
|
||||||
|
|
||||||
|
private final static int[] KEY_STATE_PRESSED_ON = {
|
||||||
|
android.R.attr.state_pressed,
|
||||||
|
android.R.attr.state_checkable,
|
||||||
|
android.R.attr.state_checked
|
||||||
|
};
|
||||||
|
|
||||||
|
private final static int[] KEY_STATE_NORMAL_OFF = {
|
||||||
|
android.R.attr.state_checkable
|
||||||
|
};
|
||||||
|
|
||||||
|
private final static int[] KEY_STATE_PRESSED_OFF = {
|
||||||
|
android.R.attr.state_pressed,
|
||||||
|
android.R.attr.state_checkable
|
||||||
|
};
|
||||||
|
|
||||||
|
private final static int[] KEY_STATE_NORMAL = {
|
||||||
|
};
|
||||||
|
|
||||||
|
private final static int[] KEY_STATE_PRESSED = {
|
||||||
|
android.R.attr.state_pressed
|
||||||
|
};
|
||||||
|
|
||||||
|
/** Create an empty key with no attributes. */
|
||||||
|
public Key(Row parent) {
|
||||||
|
keyboard = parent.parent;
|
||||||
|
height = parent.defaultHeight;
|
||||||
|
width = parent.defaultWidth;
|
||||||
|
gap = parent.defaultHorizontalGap;
|
||||||
|
edgeFlags = parent.rowEdgeFlags;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Create a key with the given top-left coordinate and extract its attributes from
|
||||||
|
* the XML parser.
|
||||||
|
* @param res resources associated with the caller's context
|
||||||
|
* @param parent the row that this key belongs to. The row must already be attached to
|
||||||
|
* a {@link Keyboard}.
|
||||||
|
* @param x the x coordinate of the top-left
|
||||||
|
* @param y the y coordinate of the top-left
|
||||||
|
* @param parser the XML parser containing the attributes for this key
|
||||||
|
*/
|
||||||
|
public Key(Resources res, Row parent, int x, int y, XmlResourceParser parser) {
|
||||||
|
this(parent);
|
||||||
|
|
||||||
|
this.x = x;
|
||||||
|
this.y = y;
|
||||||
|
|
||||||
|
TypedArray a = res.obtainAttributes(Xml.asAttributeSet(parser),
|
||||||
|
R.styleable.craft_Keyboard);
|
||||||
|
|
||||||
|
width = getDimensionOrFraction(a,
|
||||||
|
R.styleable.craft_Keyboard_android_keyWidth,
|
||||||
|
keyboard.mDisplayWidth, parent.defaultWidth);
|
||||||
|
height = getDimensionOrFraction(a,
|
||||||
|
R.styleable.craft_Keyboard_android_keyHeight,
|
||||||
|
keyboard.mDisplayHeight, parent.defaultHeight);
|
||||||
|
gap = getDimensionOrFraction(a,
|
||||||
|
R.styleable.craft_Keyboard_android_horizontalGap,
|
||||||
|
keyboard.mDisplayWidth, parent.defaultHorizontalGap);
|
||||||
|
a.recycle();
|
||||||
|
a = res.obtainAttributes(Xml.asAttributeSet(parser),
|
||||||
|
R.styleable.craft_Keyboard_Key);
|
||||||
|
this.x += gap;
|
||||||
|
TypedValue codesValue = new TypedValue();
|
||||||
|
a.getValue(R.styleable.craft_Keyboard_Key_android_codes,
|
||||||
|
codesValue);
|
||||||
|
if (codesValue.type == TypedValue.TYPE_INT_DEC
|
||||||
|
|| codesValue.type == TypedValue.TYPE_INT_HEX) {
|
||||||
|
codes = new int[] { codesValue.data };
|
||||||
|
} else if (codesValue.type == TypedValue.TYPE_STRING) {
|
||||||
|
codes = parseCSV(codesValue.string.toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
iconPreview = a.getDrawable(R.styleable.craft_Keyboard_Key_android_iconPreview);
|
||||||
|
if (iconPreview != null) {
|
||||||
|
iconPreview.setBounds(0, 0, iconPreview.getIntrinsicWidth(),
|
||||||
|
iconPreview.getIntrinsicHeight());
|
||||||
|
}
|
||||||
|
popupCharacters = a.getText(
|
||||||
|
R.styleable.craft_Keyboard_Key_android_popupCharacters);
|
||||||
|
popupResId = a.getResourceId(
|
||||||
|
R.styleable.craft_Keyboard_Key_android_popupKeyboard, 0);
|
||||||
|
repeatable = a.getBoolean(
|
||||||
|
R.styleable.craft_Keyboard_Key_android_isRepeatable, false);
|
||||||
|
modifier = a.getBoolean(
|
||||||
|
R.styleable.craft_Keyboard_Key_android_isModifier, false);
|
||||||
|
sticky = a.getBoolean(
|
||||||
|
R.styleable.craft_Keyboard_Key_android_isSticky, false);
|
||||||
|
edgeFlags = a.getInt(R.styleable.craft_Keyboard_Key_android_keyEdgeFlags, 0);
|
||||||
|
edgeFlags |= parent.rowEdgeFlags;
|
||||||
|
|
||||||
|
icon = a.getDrawable(
|
||||||
|
R.styleable.craft_Keyboard_Key_android_keyIcon);
|
||||||
|
if (icon != null) {
|
||||||
|
icon.setBounds(0, 0, icon.getIntrinsicWidth(), icon.getIntrinsicHeight());
|
||||||
|
}
|
||||||
|
label = a.getText(R.styleable.craft_Keyboard_Key_android_keyLabel);
|
||||||
|
text = a.getText(R.styleable.craft_Keyboard_Key_android_keyOutputText);
|
||||||
|
|
||||||
|
if (codes == null && !TextUtils.isEmpty(label)) {
|
||||||
|
codes = new int[] { label.charAt(0) };
|
||||||
|
}
|
||||||
|
a.recycle();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Informs the key that it has been pressed, in case it needs to change its appearance or
|
||||||
|
* state.
|
||||||
|
* @see #onReleased(boolean)
|
||||||
|
*/
|
||||||
|
public void onPressed() {
|
||||||
|
pressed = !pressed;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Changes the pressed state of the key.
|
||||||
|
*
|
||||||
|
* <p>Toggled state of the key will be flipped when all the following conditions are
|
||||||
|
* fulfilled:</p>
|
||||||
|
*
|
||||||
|
* <ul>
|
||||||
|
* <li>This is a sticky key, that is, {@link #sticky} is {@code true}.
|
||||||
|
* <li>The parameter {@code inside} is {@code true}.
|
||||||
|
* <li>{@link android.os.Build.VERSION#SDK_INT} is greater than
|
||||||
|
* {@link android.os.Build.VERSION_CODES#LOLLIPOP_MR1}.
|
||||||
|
* </ul>
|
||||||
|
*
|
||||||
|
* @param inside whether the finger was released inside the key. Works only on Android M and
|
||||||
|
* later. See the method document for details.
|
||||||
|
* @see #onPressed()
|
||||||
|
*/
|
||||||
|
public void onReleased(boolean inside) {
|
||||||
|
pressed = !pressed;
|
||||||
|
if (sticky && inside) {
|
||||||
|
on = !on;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
int[] parseCSV(String value) {
|
||||||
|
int count = 0;
|
||||||
|
int lastIndex = 0;
|
||||||
|
if (value.length() > 0) {
|
||||||
|
count++;
|
||||||
|
while ((lastIndex = value.indexOf(",", lastIndex + 1)) > 0) {
|
||||||
|
count++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
int[] values = new int[count];
|
||||||
|
count = 0;
|
||||||
|
StringTokenizer st = new StringTokenizer(value, ",");
|
||||||
|
while (st.hasMoreTokens()) {
|
||||||
|
try {
|
||||||
|
values[count++] = Integer.parseInt(st.nextToken());
|
||||||
|
} catch (NumberFormatException nfe) {
|
||||||
|
Log.e(TAG, "Error parsing keycodes " + value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return values;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Detects if a point falls inside this key.
|
||||||
|
* @param x the x-coordinate of the point
|
||||||
|
* @param y the y-coordinate of the point
|
||||||
|
* @return whether or not the point falls inside the key. If the key is attached to an edge,
|
||||||
|
* it will assume that all points between the key and the edge are considered to be inside
|
||||||
|
* the key.
|
||||||
|
*/
|
||||||
|
public boolean isInside(int x, int y) {
|
||||||
|
boolean leftEdge = (edgeFlags & EDGE_LEFT) > 0;
|
||||||
|
boolean rightEdge = (edgeFlags & EDGE_RIGHT) > 0;
|
||||||
|
boolean topEdge = (edgeFlags & EDGE_TOP) > 0;
|
||||||
|
boolean bottomEdge = (edgeFlags & EDGE_BOTTOM) > 0;
|
||||||
|
if ((x >= this.x || (leftEdge && x <= this.x + this.width))
|
||||||
|
&& (x < this.x + this.width || (rightEdge && x >= this.x))
|
||||||
|
&& (y >= this.y || (topEdge && y <= this.y + this.height))
|
||||||
|
&& (y < this.y + this.height || (bottomEdge && y >= this.y))) {
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the square of the distance between the center of the key and the given point.
|
||||||
|
* @param x the x-coordinate of the point
|
||||||
|
* @param y the y-coordinate of the point
|
||||||
|
* @return the square of the distance of the point from the center of the key
|
||||||
|
*/
|
||||||
|
public int squaredDistanceFrom(int x, int y) {
|
||||||
|
int xDist = this.x + width / 2 - x;
|
||||||
|
int yDist = this.y + height / 2 - y;
|
||||||
|
return xDist * xDist + yDist * yDist;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the drawable state for the key, based on the current state and type of the key.
|
||||||
|
* @return the drawable state of the key.
|
||||||
|
* @see android.graphics.drawable.StateListDrawable#setState(int[])
|
||||||
|
*/
|
||||||
|
public int[] getCurrentDrawableState() {
|
||||||
|
int[] states = KEY_STATE_NORMAL;
|
||||||
|
|
||||||
|
if (on) {
|
||||||
|
if (pressed) {
|
||||||
|
states = KEY_STATE_PRESSED_ON;
|
||||||
|
} else {
|
||||||
|
states = KEY_STATE_NORMAL_ON;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (sticky) {
|
||||||
|
if (pressed) {
|
||||||
|
states = KEY_STATE_PRESSED_OFF;
|
||||||
|
} else {
|
||||||
|
states = KEY_STATE_NORMAL_OFF;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (pressed) {
|
||||||
|
states = KEY_STATE_PRESSED;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return states;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a keyboard from the given xml key layout file.
|
||||||
|
* @param context the application or service context
|
||||||
|
* @param xmlLayoutResId the resource file that contains the keyboard layout and keys.
|
||||||
|
*/
|
||||||
|
public Keyboard(Context context, int xmlLayoutResId) {
|
||||||
|
this(context, xmlLayoutResId, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a keyboard from the given xml key layout file. Weeds out rows
|
||||||
|
* that have a keyboard mode defined but don't match the specified mode.
|
||||||
|
* @param context the application or service context
|
||||||
|
* @param xmlLayoutResId the resource file that contains the keyboard layout and keys.
|
||||||
|
* @param modeId keyboard mode identifier
|
||||||
|
* @param width sets width of keyboard
|
||||||
|
* @param height sets height of keyboard
|
||||||
|
*/
|
||||||
|
public Keyboard(Context context, @XmlRes int xmlLayoutResId, int modeId, int width,
|
||||||
|
int height) {
|
||||||
|
mDisplayWidth = width;
|
||||||
|
mDisplayHeight = height;
|
||||||
|
|
||||||
|
mDefaultHorizontalGap = 0;
|
||||||
|
mDefaultWidth = mDisplayWidth / 10;
|
||||||
|
mDefaultVerticalGap = 0;
|
||||||
|
mDefaultHeight = mDefaultWidth;
|
||||||
|
mKeys = new ArrayList<>();
|
||||||
|
mModifierKeys = new ArrayList<>();
|
||||||
|
mKeyboardMode = modeId;
|
||||||
|
loadKeyboard(context, context.getResources().getXml(xmlLayoutResId));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a keyboard from the given xml key layout file. Weeds out rows
|
||||||
|
* that have a keyboard mode defined but don't match the specified mode.
|
||||||
|
* @param context the application or service context
|
||||||
|
* @param xmlLayoutResId the resource file that contains the keyboard layout and keys.
|
||||||
|
* @param modeId keyboard mode identifier
|
||||||
|
*/
|
||||||
|
public Keyboard(Context context, @XmlRes int xmlLayoutResId, int modeId) {
|
||||||
|
DisplayMetrics dm = context.getResources().getDisplayMetrics();
|
||||||
|
mDisplayWidth = dm.widthPixels;
|
||||||
|
mDisplayHeight = dm.heightPixels;
|
||||||
|
//Log.v(TAG, "keyboard's display metrics:" + dm);
|
||||||
|
|
||||||
|
mDefaultHorizontalGap = 0;
|
||||||
|
mDefaultWidth = mDisplayWidth / 10;
|
||||||
|
mDefaultVerticalGap = 0;
|
||||||
|
mDefaultHeight = mDefaultWidth;
|
||||||
|
mKeys = new ArrayList<>();
|
||||||
|
mModifierKeys = new ArrayList<>();
|
||||||
|
mKeyboardMode = modeId;
|
||||||
|
loadKeyboard(context, context.getResources().getXml(xmlLayoutResId));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* <p>Creates a blank keyboard from the given resource file and populates it with the specified
|
||||||
|
* characters in left-to-right, top-to-bottom fashion, using the specified number of columns.
|
||||||
|
* </p>
|
||||||
|
* <p>If the specified number of columns is -1, then the keyboard will fit as many keys as
|
||||||
|
* possible in each row.</p>
|
||||||
|
* @param context the application or service context
|
||||||
|
* @param layoutTemplateResId the layout template file, containing no keys.
|
||||||
|
* @param characters the list of characters to display on the keyboard. One key will be created
|
||||||
|
* for each character.
|
||||||
|
* @param columns the number of columns of keys to display. If this number is greater than the
|
||||||
|
* number of keys that can fit in a row, it will be ignored. If this number is -1, the
|
||||||
|
* keyboard will fit as many keys as possible in each row.
|
||||||
|
*/
|
||||||
|
public Keyboard(Context context, int layoutTemplateResId,
|
||||||
|
CharSequence characters, int columns, int horizontalPadding) {
|
||||||
|
this(context, layoutTemplateResId);
|
||||||
|
int x = 0;
|
||||||
|
int y = 0;
|
||||||
|
int column = 0;
|
||||||
|
mTotalWidth = 0;
|
||||||
|
|
||||||
|
Row row = new Row(this);
|
||||||
|
row.defaultHeight = mDefaultHeight;
|
||||||
|
row.defaultWidth = mDefaultWidth;
|
||||||
|
row.defaultHorizontalGap = mDefaultHorizontalGap;
|
||||||
|
row.verticalGap = mDefaultVerticalGap;
|
||||||
|
row.rowEdgeFlags = EDGE_TOP | EDGE_BOTTOM;
|
||||||
|
final int maxColumns = columns == -1 ? Integer.MAX_VALUE : columns;
|
||||||
|
for (int i = 0; i < characters.length(); i++) {
|
||||||
|
char c = characters.charAt(i);
|
||||||
|
if (column >= maxColumns
|
||||||
|
|| x + mDefaultWidth + horizontalPadding > mDisplayWidth) {
|
||||||
|
x = 0;
|
||||||
|
y += mDefaultVerticalGap + mDefaultHeight;
|
||||||
|
column = 0;
|
||||||
|
}
|
||||||
|
final Key key = new Key(row);
|
||||||
|
key.x = x;
|
||||||
|
key.y = y;
|
||||||
|
key.label = String.valueOf(c);
|
||||||
|
key.codes = new int[] { c };
|
||||||
|
column++;
|
||||||
|
x += key.width + key.gap;
|
||||||
|
mKeys.add(key);
|
||||||
|
row.mKeys.add(key);
|
||||||
|
if (x > mTotalWidth) {
|
||||||
|
mTotalWidth = x;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
mTotalHeight = y + mDefaultHeight;
|
||||||
|
rows.add(row);
|
||||||
|
}
|
||||||
|
|
||||||
|
final void resize(int newWidth, int newHeight) {
|
||||||
|
int numRows = rows.size();
|
||||||
|
for (int rowIndex = 0; rowIndex < numRows; ++rowIndex) {
|
||||||
|
Row row = rows.get(rowIndex);
|
||||||
|
int numKeys = row.mKeys.size();
|
||||||
|
int totalGap = 0;
|
||||||
|
int totalWidth = 0;
|
||||||
|
for (int keyIndex = 0; keyIndex < numKeys; ++keyIndex) {
|
||||||
|
Key key = row.mKeys.get(keyIndex);
|
||||||
|
if (keyIndex > 0) {
|
||||||
|
totalGap += key.gap;
|
||||||
|
}
|
||||||
|
totalWidth += key.width;
|
||||||
|
}
|
||||||
|
if (totalGap + totalWidth > newWidth) {
|
||||||
|
int x = 0;
|
||||||
|
float scaleFactor = (float)(newWidth - totalGap) / totalWidth;
|
||||||
|
for (int keyIndex = 0; keyIndex < numKeys; ++keyIndex) {
|
||||||
|
Key key = row.mKeys.get(keyIndex);
|
||||||
|
key.width *= scaleFactor;
|
||||||
|
key.x = x;
|
||||||
|
x += key.width + key.gap;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
mTotalWidth = newWidth;
|
||||||
|
// TODO: This does not adjust the vertical placement according to the new size.
|
||||||
|
// The main problem in the previous code was horizontal placement/size, but we should
|
||||||
|
// also recalculate the vertical sizes/positions when we get this resize call.
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<Key> getKeys() {
|
||||||
|
return mKeys;
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<Key> getModifierKeys() {
|
||||||
|
return mModifierKeys;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected int getHorizontalGap() {
|
||||||
|
return mDefaultHorizontalGap;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void setHorizontalGap(int gap) {
|
||||||
|
mDefaultHorizontalGap = gap;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected int getVerticalGap() {
|
||||||
|
return mDefaultVerticalGap;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void setVerticalGap(int gap) {
|
||||||
|
mDefaultVerticalGap = gap;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected int getKeyHeight() {
|
||||||
|
return mDefaultHeight;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void setKeyHeight(int height) {
|
||||||
|
mDefaultHeight = height;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected int getKeyWidth() {
|
||||||
|
return mDefaultWidth;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void setKeyWidth(int width) {
|
||||||
|
mDefaultWidth = width;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the total height of the keyboard
|
||||||
|
* @return the total height of the keyboard
|
||||||
|
*/
|
||||||
|
public int getHeight() {
|
||||||
|
return mTotalHeight;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getMinWidth() {
|
||||||
|
return mTotalWidth;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean setShifted(boolean shiftState) {
|
||||||
|
for (Key shiftKey : mShiftKeys) {
|
||||||
|
if (shiftKey != null) {
|
||||||
|
shiftKey.on = shiftState;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (mShifted != shiftState) {
|
||||||
|
mShifted = shiftState;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isShifted() {
|
||||||
|
return mShifted;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @hide
|
||||||
|
*/
|
||||||
|
public int[] getShiftKeyIndices() {
|
||||||
|
return mShiftKeyIndices;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getShiftKeyIndex() {
|
||||||
|
return mShiftKeyIndices[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
private void computeNearestNeighbors() {
|
||||||
|
// Round-up so we don't have any pixels outside the grid
|
||||||
|
mCellWidth = (getMinWidth() + GRID_WIDTH - 1) / GRID_WIDTH;
|
||||||
|
mCellHeight = (getHeight() + GRID_HEIGHT - 1) / GRID_HEIGHT;
|
||||||
|
mGridNeighbors = new int[GRID_SIZE][];
|
||||||
|
int[] indices = new int[mKeys.size()];
|
||||||
|
final int gridWidth = GRID_WIDTH * mCellWidth;
|
||||||
|
final int gridHeight = GRID_HEIGHT * mCellHeight;
|
||||||
|
for (int x = 0; x < gridWidth; x += mCellWidth) {
|
||||||
|
for (int y = 0; y < gridHeight; y += mCellHeight) {
|
||||||
|
int count = 0;
|
||||||
|
for (int i = 0; i < mKeys.size(); i++) {
|
||||||
|
final Key key = mKeys.get(i);
|
||||||
|
if (key.squaredDistanceFrom(x, y) < mProximityThreshold ||
|
||||||
|
key.squaredDistanceFrom(x + mCellWidth - 1, y) < mProximityThreshold ||
|
||||||
|
key.squaredDistanceFrom(x + mCellWidth - 1, y + mCellHeight - 1)
|
||||||
|
< mProximityThreshold ||
|
||||||
|
key.squaredDistanceFrom(x, y + mCellHeight - 1) < mProximityThreshold) {
|
||||||
|
indices[count++] = i;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
int [] cell = new int[count];
|
||||||
|
System.arraycopy(indices, 0, cell, 0, count);
|
||||||
|
mGridNeighbors[(y / mCellHeight) * GRID_WIDTH + (x / mCellWidth)] = cell;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the indices of the keys that are closest to the given point.
|
||||||
|
* @param x the x-coordinate of the point
|
||||||
|
* @param y the y-coordinate of the point
|
||||||
|
* @return the array of integer indices for the nearest keys to the given point. If the given
|
||||||
|
* point is out of range, then an array of size zero is returned.
|
||||||
|
*/
|
||||||
|
public int[] getNearestKeys(int x, int y) {
|
||||||
|
if (mGridNeighbors == null) computeNearestNeighbors();
|
||||||
|
if (x >= 0 && x < getMinWidth() && y >= 0 && y < getHeight()) {
|
||||||
|
int index = (y / mCellHeight) * GRID_WIDTH + (x / mCellWidth);
|
||||||
|
if (index < GRID_SIZE) {
|
||||||
|
return mGridNeighbors[index];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return new int[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
protected Row createRowFromXml(Resources res, XmlResourceParser parser) {
|
||||||
|
return new Row(res, this, parser);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected Key createKeyFromXml(Resources res, Row parent, int x, int y,
|
||||||
|
XmlResourceParser parser) {
|
||||||
|
return new Key(res, parent, x, y, parser);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void loadKeyboard(Context context, XmlResourceParser parser) {
|
||||||
|
boolean inKey = false;
|
||||||
|
boolean inRow = false;
|
||||||
|
boolean leftMostKey = false;
|
||||||
|
int row = 0;
|
||||||
|
int x = 0;
|
||||||
|
int y = 0;
|
||||||
|
Key key = null;
|
||||||
|
Row currentRow = null;
|
||||||
|
Resources res = context.getResources();
|
||||||
|
boolean skipRow = false;
|
||||||
|
|
||||||
|
try {
|
||||||
|
int event;
|
||||||
|
while ((event = parser.next()) != XmlResourceParser.END_DOCUMENT) {
|
||||||
|
if (event == XmlResourceParser.START_TAG) {
|
||||||
|
String tag = parser.getName();
|
||||||
|
if (TAG_ROW.equals(tag)) {
|
||||||
|
inRow = true;
|
||||||
|
x = 0;
|
||||||
|
currentRow = createRowFromXml(res, parser);
|
||||||
|
rows.add(currentRow);
|
||||||
|
skipRow = currentRow.mode != 0 && currentRow.mode != mKeyboardMode;
|
||||||
|
if (skipRow) {
|
||||||
|
skipToEndOfRow(parser);
|
||||||
|
inRow = false;
|
||||||
|
}
|
||||||
|
} else if (TAG_KEY.equals(tag)) {
|
||||||
|
inKey = true;
|
||||||
|
key = createKeyFromXml(res, currentRow, x, y, parser);
|
||||||
|
mKeys.add(key);
|
||||||
|
if (key.codes[0] == KEYCODE_SHIFT) {
|
||||||
|
// Find available shift key slot and put this shift key in it
|
||||||
|
for (int i = 0; i < mShiftKeys.length; i++) {
|
||||||
|
if (mShiftKeys[i] == null) {
|
||||||
|
mShiftKeys[i] = key;
|
||||||
|
mShiftKeyIndices[i] = mKeys.size()-1;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
mModifierKeys.add(key);
|
||||||
|
} else if (key.codes[0] == KEYCODE_ALT) {
|
||||||
|
mModifierKeys.add(key);
|
||||||
|
}
|
||||||
|
currentRow.mKeys.add(key);
|
||||||
|
} else if (TAG_KEYBOARD.equals(tag)) {
|
||||||
|
parseKeyboardAttributes(res, parser);
|
||||||
|
}
|
||||||
|
} else if (event == XmlResourceParser.END_TAG) {
|
||||||
|
if (inKey) {
|
||||||
|
inKey = false;
|
||||||
|
x += key.gap + key.width;
|
||||||
|
if (x > mTotalWidth) {
|
||||||
|
mTotalWidth = x;
|
||||||
|
}
|
||||||
|
} else if (inRow) {
|
||||||
|
inRow = false;
|
||||||
|
y += currentRow.verticalGap;
|
||||||
|
y += currentRow.defaultHeight;
|
||||||
|
row++;
|
||||||
|
} else {
|
||||||
|
// TODO: error or extend?
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
Log.e(TAG, "Parse error:" + e);
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
mTotalHeight = y - mDefaultVerticalGap;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void skipToEndOfRow(XmlResourceParser parser)
|
||||||
|
throws XmlPullParserException, IOException {
|
||||||
|
int event;
|
||||||
|
while ((event = parser.next()) != XmlResourceParser.END_DOCUMENT) {
|
||||||
|
if (event == XmlResourceParser.END_TAG
|
||||||
|
&& parser.getName().equals(TAG_ROW)) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void parseKeyboardAttributes(Resources res, XmlResourceParser parser) {
|
||||||
|
TypedArray a = res.obtainAttributes(Xml.asAttributeSet(parser),
|
||||||
|
R.styleable.craft_Keyboard);
|
||||||
|
|
||||||
|
mDefaultWidth = getDimensionOrFraction(a,
|
||||||
|
R.styleable.craft_Keyboard_android_keyWidth,
|
||||||
|
mDisplayWidth, mDisplayWidth / 10);
|
||||||
|
mDefaultHeight = getDimensionOrFraction(a,
|
||||||
|
R.styleable.craft_Keyboard_android_keyHeight,
|
||||||
|
mDisplayHeight, 50);
|
||||||
|
mDefaultHorizontalGap = getDimensionOrFraction(a,
|
||||||
|
R.styleable.craft_Keyboard_android_horizontalGap,
|
||||||
|
mDisplayWidth, 0);
|
||||||
|
mDefaultVerticalGap = getDimensionOrFraction(a,
|
||||||
|
R.styleable.craft_Keyboard_android_verticalGap,
|
||||||
|
mDisplayHeight, 0);
|
||||||
|
mProximityThreshold = (int) (mDefaultWidth * SEARCH_DISTANCE);
|
||||||
|
mProximityThreshold = mProximityThreshold * mProximityThreshold; // Square it for comparison
|
||||||
|
a.recycle();
|
||||||
|
}
|
||||||
|
|
||||||
|
static int getDimensionOrFraction(TypedArray a, int index, int base, int defValue) {
|
||||||
|
TypedValue value = a.peekValue(index);
|
||||||
|
if (value == null) return defValue;
|
||||||
|
if (value.type == TypedValue.TYPE_DIMENSION) {
|
||||||
|
return a.getDimensionPixelOffset(index, defValue);
|
||||||
|
} else if (value.type == TypedValue.TYPE_FRACTION) {
|
||||||
|
// Round it to avoid values like 47.9999 from getting truncated
|
||||||
|
return Math.round(a.getFraction(index, base, base, defValue));
|
||||||
|
}
|
||||||
|
return defValue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
1453
app/src/main/java/com/keyboard/craft/view/KeyboardView.java
Normal file
1453
app/src/main/java/com/keyboard/craft/view/KeyboardView.java
Normal file
File diff suppressed because it is too large
Load Diff
30
app/src/main/res/drawable-v24/ic_launcher_foreground.xml
Normal file
30
app/src/main/res/drawable-v24/ic_launcher_foreground.xml
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:aapt="http://schemas.android.com/aapt"
|
||||||
|
android:width="108dp"
|
||||||
|
android:height="108dp"
|
||||||
|
android:viewportWidth="108"
|
||||||
|
android:viewportHeight="108">
|
||||||
|
<path android:pathData="M31,63.928c0,0 6.4,-11 12.1,-13.1c7.2,-2.6 26,-1.4 26,-1.4l38.1,38.1L107,108.928l-32,-1L31,63.928z">
|
||||||
|
<aapt:attr name="android:fillColor">
|
||||||
|
<gradient
|
||||||
|
android:endX="85.84757"
|
||||||
|
android:endY="92.4963"
|
||||||
|
android:startX="42.9492"
|
||||||
|
android:startY="49.59793"
|
||||||
|
android:type="linear">
|
||||||
|
<item
|
||||||
|
android:color="#44000000"
|
||||||
|
android:offset="0.0" />
|
||||||
|
<item
|
||||||
|
android:color="#00000000"
|
||||||
|
android:offset="1.0" />
|
||||||
|
</gradient>
|
||||||
|
</aapt:attr>
|
||||||
|
</path>
|
||||||
|
<path
|
||||||
|
android:fillColor="#FFFFFF"
|
||||||
|
android:fillType="nonZero"
|
||||||
|
android:pathData="M65.3,45.828l3.8,-6.6c0.2,-0.4 0.1,-0.9 -0.3,-1.1c-0.4,-0.2 -0.9,-0.1 -1.1,0.3l-3.9,6.7c-6.3,-2.8 -13.4,-2.8 -19.7,0l-3.9,-6.7c-0.2,-0.4 -0.7,-0.5 -1.1,-0.3C38.8,38.328 38.7,38.828 38.9,39.228l3.8,6.6C36.2,49.428 31.7,56.028 31,63.928h46C76.3,56.028 71.8,49.428 65.3,45.828zM43.4,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2c-0.3,-0.7 -0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C45.3,56.528 44.5,57.328 43.4,57.328L43.4,57.328zM64.6,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2s-0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C66.5,56.528 65.6,57.328 64.6,57.328L64.6,57.328z"
|
||||||
|
android:strokeWidth="1"
|
||||||
|
android:strokeColor="#00000000" />
|
||||||
|
</vector>
|
||||||
20
app/src/main/res/drawable/arrow_right.xml
Normal file
20
app/src/main/res/drawable/arrow_right.xml
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="12dp"
|
||||||
|
android:height="12dp"
|
||||||
|
android:viewportWidth="12"
|
||||||
|
android:viewportHeight="12">
|
||||||
|
<path
|
||||||
|
android:pathData="M6.323,1.448L10.875,6L6.323,10.552"
|
||||||
|
android:strokeLineJoin="round"
|
||||||
|
android:strokeWidth="1.5"
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:strokeColor="#ffffff"
|
||||||
|
android:strokeLineCap="round"/>
|
||||||
|
<path
|
||||||
|
android:pathData="M1.125,6H10.748"
|
||||||
|
android:strokeLineJoin="round"
|
||||||
|
android:strokeWidth="1.5"
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:strokeColor="#ffffff"
|
||||||
|
android:strokeLineCap="round"/>
|
||||||
|
</vector>
|
||||||
10
app/src/main/res/drawable/back_icon.xml
Normal file
10
app/src/main/res/drawable/back_icon.xml
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="24dp"
|
||||||
|
android:height="24dp"
|
||||||
|
android:viewportWidth="24"
|
||||||
|
android:viewportHeight="24">
|
||||||
|
<path
|
||||||
|
android:pathData="M15.207,5.293C15.598,5.683 15.598,6.317 15.207,6.707L9.914,12L15.207,17.293C15.598,17.683 15.598,18.317 15.207,18.707C14.817,19.098 14.183,19.098 13.793,18.707L7.793,12.707C7.402,12.317 7.402,11.683 7.793,11.293L13.793,5.293C14.183,4.902 14.817,4.902 15.207,5.293Z"
|
||||||
|
android:fillColor="#000000"
|
||||||
|
android:fillType="evenOdd"/>
|
||||||
|
</vector>
|
||||||
14
app/src/main/res/drawable/cha_icon.xml
Normal file
14
app/src/main/res/drawable/cha_icon.xml
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="24dp"
|
||||||
|
android:height="24dp"
|
||||||
|
android:viewportWidth="24"
|
||||||
|
android:viewportHeight="24">
|
||||||
|
<path
|
||||||
|
android:strokeWidth="1"
|
||||||
|
android:pathData="M12,0.5L12,0.5A11.5,11.5 0,0 1,23.5 12L23.5,12A11.5,11.5 0,0 1,12 23.5L12,23.5A11.5,11.5 0,0 1,0.5 12L0.5,12A11.5,11.5 0,0 1,12 0.5z"
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:strokeColor="#ffffff"/>
|
||||||
|
<path
|
||||||
|
android:pathData="M16.496,8.511C16.774,8.233 16.774,7.782 16.496,7.504C16.218,7.225 15.767,7.225 15.489,7.504L12,10.993L8.511,7.504C8.233,7.225 7.782,7.225 7.504,7.504C7.225,7.782 7.225,8.233 7.504,8.511L10.993,12L7.504,15.489C7.225,15.767 7.225,16.218 7.504,16.496C7.782,16.774 8.233,16.774 8.511,16.496L12,13.007L15.489,16.496C15.767,16.774 16.218,16.774 16.496,16.496C16.774,16.218 16.774,15.767 16.496,15.489L13.007,12L16.496,8.511Z"
|
||||||
|
android:fillColor="#ffffff"/>
|
||||||
|
</vector>
|
||||||
6
app/src/main/res/drawable/delete_key_background.xml
Normal file
6
app/src/main/res/drawable/delete_key_background.xml
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
<selector xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<!-- 按键正常状态 -->
|
||||||
|
<item android:drawable="@mipmap/sym_keyboard_delete_normal" android:state_pressed="false" />
|
||||||
|
<!-- 按键按下状态 -->
|
||||||
|
<item android:drawable="@mipmap/sym_keyboard_delete_pressed" android:state_pressed="true" />
|
||||||
|
</selector>
|
||||||
41
app/src/main/res/drawable/down_icon.xml
Normal file
41
app/src/main/res/drawable/down_icon.xml
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="25dp"
|
||||||
|
android:height="25dp"
|
||||||
|
android:viewportWidth="25"
|
||||||
|
android:viewportHeight="25">
|
||||||
|
<path
|
||||||
|
android:pathData="M6.339,10.233C4.137,10.757 2.5,12.736 2.5,15.098C2.5,17.859 4.738,20.098 7.5,20.098C7.974,20.098 8.432,20.032 8.866,19.909"
|
||||||
|
android:strokeLineJoin="round"
|
||||||
|
android:strokeWidth="2"
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:strokeColor="#ffffff"
|
||||||
|
android:strokeLineCap="round"/>
|
||||||
|
<path
|
||||||
|
android:pathData="M18.527,10.233C20.729,10.757 22.366,12.736 22.366,15.098C22.366,17.859 20.128,20.098 17.366,20.098C16.892,20.098 16.434,20.032 16,19.909"
|
||||||
|
android:strokeLineJoin="round"
|
||||||
|
android:strokeWidth="2"
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:strokeColor="#ffffff"
|
||||||
|
android:strokeLineCap="round"/>
|
||||||
|
<path
|
||||||
|
android:pathData="M18.5,10.098C18.5,6.784 15.814,4.098 12.5,4.098C9.186,4.098 6.5,6.784 6.5,10.098"
|
||||||
|
android:strokeLineJoin="round"
|
||||||
|
android:strokeWidth="2"
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:strokeColor="#ffffff"
|
||||||
|
android:strokeLineCap="round"/>
|
||||||
|
<path
|
||||||
|
android:pathData="M9.033,15.157L12.5,18.636L16.066,15.098"
|
||||||
|
android:strokeLineJoin="round"
|
||||||
|
android:strokeWidth="2"
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:strokeColor="#ffffff"
|
||||||
|
android:strokeLineCap="round"/>
|
||||||
|
<path
|
||||||
|
android:pathData="M12.5,10.098V16.867"
|
||||||
|
android:strokeLineJoin="round"
|
||||||
|
android:strokeWidth="2"
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:strokeColor="#ffffff"
|
||||||
|
android:strokeLineCap="round"/>
|
||||||
|
</vector>
|
||||||
12
app/src/main/res/drawable/draw_activate_layout_bg.xml
Normal file
12
app/src/main/res/drawable/draw_activate_layout_bg.xml
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:shape="rectangle">
|
||||||
|
<corners
|
||||||
|
android:topLeftRadius="24dp"
|
||||||
|
android:topRightRadius="24dp" />
|
||||||
|
|
||||||
|
<gradient
|
||||||
|
android:angle="270"
|
||||||
|
android:endColor="#E5F6FF"
|
||||||
|
android:startColor="#E7FFF9" />
|
||||||
|
</shape>
|
||||||
6
app/src/main/res/drawable/draw_go_2_bg.xml
Normal file
6
app/src/main/res/drawable/draw_go_2_bg.xml
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:shape="rectangle">
|
||||||
|
<corners android:radius="80dp" />
|
||||||
|
<solid android:color="@color/banner_indicator_tow_go_bg_color" />
|
||||||
|
</shape>
|
||||||
6
app/src/main/res/drawable/draw_go_bg.xml
Normal file
6
app/src/main/res/drawable/draw_go_bg.xml
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:shape="rectangle">
|
||||||
|
<corners android:radius="80dp" />
|
||||||
|
<solid android:color="@color/banner_indicator_one_go_bg_color" />
|
||||||
|
</shape>
|
||||||
7
app/src/main/res/drawable/draw_main_tab_bg.xml
Normal file
7
app/src/main/res/drawable/draw_main_tab_bg.xml
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:shape="rectangle">
|
||||||
|
<corners android:topLeftRadius="24dp"
|
||||||
|
android:topRightRadius="24dp"/>
|
||||||
|
<solid android:color="@color/white"/>
|
||||||
|
</shape>
|
||||||
9
app/src/main/res/drawable/draw_round_gray.xml
Normal file
9
app/src/main/res/drawable/draw_round_gray.xml
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:shape="oval">
|
||||||
|
<solid android:color="#244CDB" />
|
||||||
|
<gradient
|
||||||
|
android:angle="270"
|
||||||
|
android:endColor="#1A91D3"
|
||||||
|
android:startColor="#1FDCAF" />
|
||||||
|
</shape>
|
||||||
19
app/src/main/res/drawable/drw_banner_placeholder.xml
Normal file
19
app/src/main/res/drawable/drw_banner_placeholder.xml
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="32dp"
|
||||||
|
android:height="32dp"
|
||||||
|
android:viewportWidth="64"
|
||||||
|
android:viewportHeight="64">
|
||||||
|
|
||||||
|
|
||||||
|
<group
|
||||||
|
android:translateX="16"
|
||||||
|
android:translateY="16">
|
||||||
|
<path
|
||||||
|
android:fillColor="#505E7A"
|
||||||
|
android:pathData="M24,10.667C24,11.374 23.719,12.052 23.219,12.552C22.719,13.052 22.041,13.333 21.333,13.333C20.626,13.333 19.948,13.052 19.448,12.552C18.948,12.052 18.667,11.374 18.667,10.667C18.667,9.959 18.948,9.281 19.448,8.781C19.948,8.281 20.626,8 21.333,8C22.041,8 22.719,8.281 23.219,8.781C23.719,9.281 24,9.959 24,10.667Z" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#505E7A"
|
||||||
|
android:fillType="evenOdd"
|
||||||
|
android:pathData="M15.924,1.667H16.076C19.155,1.667 21.567,1.667 23.449,1.92C25.375,2.179 26.895,2.72 28.088,3.912C29.281,5.105 29.821,6.625 30.08,8.552C30.333,10.433 30.333,12.845 30.333,15.924V16.041C30.333,18.587 30.333,20.669 30.195,22.365C30.056,24.072 29.772,25.495 29.135,26.679C28.855,27.2 28.508,27.668 28.088,28.088C26.895,29.281 25.375,29.821 23.448,30.08C21.567,30.333 19.155,30.333 16.076,30.333H15.924C12.845,30.333 10.433,30.333 8.551,30.08C6.625,29.821 5.105,29.28 3.912,28.088C2.855,27.031 2.308,25.715 2.019,24.08C1.732,22.476 1.68,20.48 1.669,18.003C1.667,17.372 1.667,16.705 1.667,16.001V15.924C1.667,12.845 1.667,10.433 1.92,8.551C2.179,6.625 2.72,5.105 3.912,3.912C5.105,2.719 6.625,2.179 8.552,1.92C10.433,1.667 12.845,1.667 15.924,1.667ZM8.817,3.901C7.113,4.131 6.085,4.568 5.327,5.327C4.567,6.087 4.131,7.113 3.901,8.819C3.669,10.552 3.667,12.829 3.667,16V17.125L5.001,15.956C5.587,15.444 6.345,15.173 7.123,15.199C7.901,15.225 8.639,15.546 9.189,16.096L14.909,21.816C15.353,22.26 15.939,22.533 16.565,22.587C17.19,22.641 17.815,22.473 18.328,22.112L18.725,21.832C19.466,21.312 20.362,21.058 21.266,21.112C22.17,21.167 23.028,21.526 23.701,22.132L27.475,25.528C27.856,24.731 28.081,23.683 28.201,22.204C28.332,20.597 28.333,18.595 28.333,16C28.333,12.829 28.331,10.552 28.099,8.819C27.869,7.113 27.432,6.085 26.673,5.325C25.913,4.567 24.887,4.131 23.181,3.901C21.448,3.669 19.171,3.667 16,3.667C12.829,3.667 10.551,3.669 8.817,3.901Z" />
|
||||||
|
</group>
|
||||||
|
</vector>
|
||||||
11
app/src/main/res/drawable/drw_btn_bg.xml
Normal file
11
app/src/main/res/drawable/drw_btn_bg.xml
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:shape="rectangle">
|
||||||
|
|
||||||
|
<corners
|
||||||
|
android:radius="32dp" />
|
||||||
|
<gradient
|
||||||
|
android:angle="0"
|
||||||
|
android:endColor="#1A91D3"
|
||||||
|
android:startColor="#1FDCAF" />
|
||||||
|
</shape>
|
||||||
7
app/src/main/res/drawable/drw_gray_select_bg.xml
Normal file
7
app/src/main/res/drawable/drw_gray_select_bg.xml
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:shape="rectangle">
|
||||||
|
|
||||||
|
<corners android:radius="32dp"/>
|
||||||
|
<solid android:color="#eeeeee"/>
|
||||||
|
</shape>
|
||||||
9
app/src/main/res/drawable/drw_main_bg.xml
Normal file
9
app/src/main/res/drawable/drw_main_bg.xml
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:shape="rectangle">
|
||||||
|
|
||||||
|
<gradient
|
||||||
|
android:angle="270"
|
||||||
|
android:endColor="#E5F6FF"
|
||||||
|
android:startColor="#E7FFF9" />
|
||||||
|
</shape>
|
||||||
12
app/src/main/res/drawable/drw_main_tab_bg.xml
Normal file
12
app/src/main/res/drawable/drw_main_tab_bg.xml
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:shape="rectangle">
|
||||||
|
|
||||||
|
<corners
|
||||||
|
android:topLeftRadius="16dp"
|
||||||
|
android:topRightRadius="16dp" />
|
||||||
|
<gradient
|
||||||
|
android:angle="270"
|
||||||
|
android:endColor="#1A91D3"
|
||||||
|
android:startColor="#1FDCAF" />
|
||||||
|
</shape>
|
||||||
8
app/src/main/res/drawable/drw_preview_message_bg.xml
Normal file
8
app/src/main/res/drawable/drw_preview_message_bg.xml
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:shape="rectangle">
|
||||||
|
|
||||||
|
<solid android:color="#F1F7FF"/>
|
||||||
|
|
||||||
|
<corners android:radius="8dp"/>
|
||||||
|
</shape>
|
||||||
7
app/src/main/res/drawable/drw_round_bg.xml
Normal file
7
app/src/main/res/drawable/drw_round_bg.xml
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
|
||||||
|
android:shape="oval">
|
||||||
|
|
||||||
|
<solid android:color="#F6F4FF" />
|
||||||
|
</shape>
|
||||||
11
app/src/main/res/drawable/drw_setting_btn_bg.xml
Normal file
11
app/src/main/res/drawable/drw_setting_btn_bg.xml
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:shape="rectangle">
|
||||||
|
|
||||||
|
<corners android:radius="8dp" />
|
||||||
|
|
||||||
|
<gradient
|
||||||
|
android:angle="270"
|
||||||
|
android:endColor="#1A91D3"
|
||||||
|
android:startColor="#1FDCAF" />
|
||||||
|
</shape>
|
||||||
14
app/src/main/res/drawable/gou_icon.xml
Normal file
14
app/src/main/res/drawable/gou_icon.xml
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="20dp"
|
||||||
|
android:height="20dp"
|
||||||
|
android:viewportWidth="20"
|
||||||
|
android:viewportHeight="20">
|
||||||
|
<path
|
||||||
|
android:pathData="M9.943,16.834C13.777,16.834 16.833,13.75 16.833,10.057C16.833,6.237 13.763,3.167 9.943,3.167C6.25,3.167 3.167,6.224 3.167,10.057C3.167,13.764 6.237,16.834 9.943,16.834ZM18.333,10.057C18.333,14.592 14.592,18.334 9.943,18.334C5.408,18.334 1.667,14.592 1.667,10.057C1.667,5.408 5.408,1.667 9.943,1.667C14.592,1.667 18.333,5.408 18.333,10.057Z"
|
||||||
|
android:fillColor="#ffffff"
|
||||||
|
android:fillType="evenOdd"/>
|
||||||
|
<path
|
||||||
|
android:pathData="M14.576,7.404C14.712,7.539 14.712,7.759 14.576,7.895L8.878,13.593C8.743,13.728 8.523,13.728 8.387,13.593L5.371,10.576C5.235,10.441 5.235,10.221 5.371,10.085L5.862,9.594C5.997,9.458 6.217,9.458 6.353,9.594L8.387,11.629C8.523,11.764 8.743,11.764 8.878,11.629L13.594,6.913C13.73,6.777 13.95,6.777 14.085,6.913L14.576,7.404Z"
|
||||||
|
android:fillColor="#ffffff"
|
||||||
|
android:fillType="evenOdd"/>
|
||||||
|
</vector>
|
||||||
10
app/src/main/res/drawable/home_select_icon.xml
Normal file
10
app/src/main/res/drawable/home_select_icon.xml
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="24dp"
|
||||||
|
android:height="24dp"
|
||||||
|
android:viewportWidth="24"
|
||||||
|
android:viewportHeight="24">
|
||||||
|
<path
|
||||||
|
android:pathData="M12.996,21H11.005C7.903,21 6.352,21 5.295,20.089C4.237,19.178 4.018,17.655 3.579,14.606L3.328,12.862C2.986,10.489 2.815,9.302 3.302,8.288C3.788,7.273 4.824,6.656 6.895,5.423L8.141,4.68C10.02,3.56 10.962,3 12,3C13.039,3 13.979,3.56 15.859,4.68L17.106,5.423C19.176,6.656 20.212,7.273 20.699,8.288C21.185,9.302 21.014,10.489 20.672,12.862L20.421,14.606C19.983,17.655 19.764,19.178 18.706,20.089C17.649,21 16.098,21 12.996,21ZM8.759,15.198C8.865,15.054 9.025,14.958 9.202,14.932C9.379,14.906 9.559,14.951 9.703,15.057C10.358,15.543 11.149,15.825 12,15.825C12.852,15.825 13.643,15.543 14.298,15.057C14.369,15.005 14.45,14.966 14.536,14.945C14.622,14.923 14.712,14.919 14.799,14.932C14.887,14.945 14.971,14.975 15.047,15.02C15.123,15.066 15.19,15.126 15.243,15.197C15.295,15.269 15.334,15.349 15.355,15.435C15.377,15.521 15.381,15.611 15.368,15.698C15.355,15.786 15.325,15.87 15.28,15.947C15.234,16.023 15.174,16.089 15.103,16.142C14.207,16.812 13.119,17.174 12,17.175C10.882,17.174 9.794,16.812 8.898,16.142C8.754,16.035 8.659,15.876 8.633,15.699C8.607,15.522 8.652,15.341 8.759,15.198Z"
|
||||||
|
android:fillColor="#ffffff"
|
||||||
|
android:fillType="evenOdd"/>
|
||||||
|
</vector>
|
||||||
14
app/src/main/res/drawable/home_settings_select_icon.xml
Normal file
14
app/src/main/res/drawable/home_settings_select_icon.xml
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="24dp"
|
||||||
|
android:height="24dp"
|
||||||
|
android:viewportWidth="24"
|
||||||
|
android:viewportHeight="24">
|
||||||
|
<group>
|
||||||
|
<clip-path
|
||||||
|
android:pathData="M0,0h24v24h-24z"/>
|
||||||
|
<path
|
||||||
|
android:pathData="M13.685,3.137C13.335,3 12.89,3 12,3C11.111,3 10.665,3 10.315,3.137C9.848,3.319 9.477,3.669 9.282,4.111C9.193,4.312 9.159,4.547 9.145,4.888C9.138,5.135 9.066,5.377 8.934,5.59C8.802,5.803 8.615,5.981 8.391,6.106C8.162,6.227 7.905,6.291 7.644,6.292C7.382,6.293 7.125,6.232 6.895,6.113C6.575,5.953 6.343,5.865 6.113,5.836C5.612,5.773 5.104,5.901 4.702,6.192C4.4,6.41 4.178,6.774 3.734,7.5C3.288,8.226 3.067,8.589 3.016,8.944C2.95,9.418 3.087,9.897 3.395,10.276C3.536,10.448 3.734,10.593 4.04,10.775C4.491,11.042 4.781,11.498 4.781,12C4.781,12.502 4.491,12.958 4.04,13.224C3.734,13.407 3.536,13.552 3.394,13.724C3.242,13.911 3.13,14.125 3.065,14.354C3,14.583 2.984,14.821 3.016,15.056C3.067,15.41 3.288,15.774 3.734,16.5C4.179,17.226 4.4,17.589 4.702,17.808C5.103,18.098 5.611,18.226 6.113,18.164C6.343,18.135 6.575,18.047 6.895,17.887C7.125,17.768 7.383,17.707 7.644,17.708C7.906,17.709 8.163,17.773 8.392,17.894C8.852,18.146 9.125,18.61 9.145,19.112C9.159,19.454 9.193,19.688 9.282,19.889C9.475,20.33 9.847,20.681 10.315,20.863C10.665,21 11.111,21 12,21C12.89,21 13.335,21 13.685,20.863C14.152,20.681 14.523,20.331 14.718,19.889C14.807,19.688 14.841,19.454 14.855,19.112C14.874,18.61 15.148,18.145 15.609,17.894C15.838,17.773 16.095,17.709 16.356,17.708C16.618,17.707 16.875,17.768 17.105,17.887C17.426,18.047 17.658,18.135 17.887,18.164C18.389,18.227 18.897,18.098 19.298,17.808C19.6,17.59 19.822,17.226 20.267,16.5C20.712,15.774 20.934,15.411 20.984,15.056C21.016,14.821 21,14.582 20.934,14.354C20.869,14.125 20.757,13.911 20.605,13.724C20.465,13.552 20.267,13.407 19.961,13.225C19.509,12.958 19.219,12.502 19.219,12C19.219,11.498 19.509,11.042 19.96,10.776C20.267,10.593 20.465,10.448 20.606,10.276C20.758,10.089 20.87,9.874 20.935,9.646C21,9.418 21.016,9.179 20.984,8.944C20.934,8.59 20.712,8.226 20.267,7.5C19.821,6.774 19.6,6.411 19.298,6.192C18.896,5.901 18.388,5.773 17.887,5.836C17.658,5.865 17.426,5.953 17.105,6.113C16.875,6.232 16.618,6.293 16.356,6.292C16.094,6.291 15.837,6.227 15.608,6.106C15.384,5.98 15.198,5.803 15.066,5.59C14.934,5.377 14.862,5.135 14.855,4.888C14.841,4.546 14.807,4.312 14.718,4.111C14.622,3.893 14.481,3.694 14.304,3.527C14.127,3.359 13.916,3.227 13.685,3.137ZM12,14.7C13.582,14.7 14.864,13.491 14.864,12C14.864,10.509 13.581,9.3 12,9.3C10.418,9.3 9.136,10.509 9.136,12C9.136,13.491 10.419,14.7 12,14.7Z"
|
||||||
|
android:fillColor="#ffffff"
|
||||||
|
android:fillType="evenOdd"/>
|
||||||
|
</group>
|
||||||
|
</vector>
|
||||||
16
app/src/main/res/drawable/home_settings_unselect_icon.xml
Normal file
16
app/src/main/res/drawable/home_settings_unselect_icon.xml
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="24dp"
|
||||||
|
android:height="24dp"
|
||||||
|
android:viewportWidth="24"
|
||||||
|
android:viewportHeight="24">
|
||||||
|
<group>
|
||||||
|
<clip-path
|
||||||
|
android:pathData="M0,0h24v24h-24z"/>
|
||||||
|
<path
|
||||||
|
android:pathData="M13.592,3.629C13.261,3.5 12.84,3.5 12,3.5C11.16,3.5 10.739,3.5 10.408,3.629C9.968,3.801 9.617,4.132 9.433,4.55C9.349,4.739 9.317,4.961 9.303,5.283C9.297,5.517 9.229,5.745 9.104,5.946C8.98,6.147 8.803,6.315 8.591,6.433C8.375,6.547 8.133,6.608 7.886,6.609C7.639,6.61 7.396,6.552 7.178,6.44C6.876,6.289 6.657,6.206 6.44,6.178C5.967,6.119 5.488,6.24 5.107,6.515C4.823,6.721 4.612,7.064 4.193,7.75C3.772,8.436 3.563,8.778 3.515,9.114C3.453,9.561 3.582,10.014 3.873,10.371C4.006,10.535 4.193,10.672 4.482,10.843C4.909,11.096 5.182,11.526 5.182,12C5.182,12.474 4.909,12.904 4.483,13.156C4.193,13.328 4.006,13.465 3.872,13.629C3.729,13.805 3.623,14.007 3.562,14.223C3.5,14.439 3.485,14.664 3.515,14.886C3.563,15.221 3.772,15.564 4.193,16.25C4.613,16.936 4.823,17.278 5.107,17.485C5.487,17.76 5.966,17.88 6.44,17.822C6.657,17.795 6.876,17.711 7.178,17.56C7.396,17.448 7.639,17.39 7.886,17.391C8.133,17.392 8.376,17.452 8.592,17.567C9.027,17.805 9.285,18.242 9.303,18.717C9.317,19.04 9.349,19.261 9.433,19.45C9.616,19.867 9.966,20.198 10.408,20.371C10.739,20.5 11.16,20.5 12,20.5C12.84,20.5 13.261,20.5 13.592,20.371C14.032,20.199 14.383,19.868 14.567,19.45C14.651,19.261 14.683,19.04 14.697,18.717C14.715,18.242 14.973,17.804 15.409,17.567C15.625,17.453 15.867,17.392 16.114,17.391C16.361,17.39 16.605,17.448 16.822,17.56C17.124,17.711 17.343,17.795 17.56,17.822C18.034,17.881 18.514,17.76 18.893,17.485C19.177,17.279 19.388,16.936 19.807,16.25C20.228,15.564 20.437,15.222 20.485,14.886C20.515,14.664 20.5,14.439 20.438,14.223C20.377,14.007 20.271,13.805 20.127,13.629C19.994,13.465 19.807,13.329 19.518,13.157C19.091,12.904 18.818,12.474 18.818,12C18.818,11.526 19.091,11.096 19.517,10.844C19.807,10.672 19.994,10.535 20.128,10.371C20.272,10.195 20.377,9.993 20.438,9.777C20.5,9.561 20.515,9.336 20.485,9.114C20.437,8.779 20.228,8.436 19.807,7.75C19.387,7.064 19.177,6.721 18.893,6.515C18.513,6.24 18.034,6.119 17.56,6.178C17.343,6.206 17.124,6.289 16.822,6.44C16.604,6.552 16.361,6.61 16.114,6.609C15.867,6.608 15.624,6.547 15.408,6.433C15.196,6.315 15.02,6.147 14.895,5.946C14.771,5.745 14.703,5.517 14.697,5.283C14.683,4.96 14.651,4.739 14.567,4.55C14.476,4.343 14.343,4.155 14.176,3.997C14.008,3.839 13.81,3.714 13.592,3.629ZM12,14.55C13.494,14.55 14.705,13.408 14.705,12C14.705,10.592 13.493,9.45 12,9.45C10.506,9.45 9.295,10.592 9.295,12C9.295,13.408 10.507,14.55 12,14.55Z"
|
||||||
|
android:strokeWidth="1.5"
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:fillType="evenOdd"
|
||||||
|
android:strokeColor="#ffffff"/>
|
||||||
|
</group>
|
||||||
|
</vector>
|
||||||
17
app/src/main/res/drawable/home_unselect_icon.xml
Normal file
17
app/src/main/res/drawable/home_unselect_icon.xml
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="24dp"
|
||||||
|
android:height="24dp"
|
||||||
|
android:viewportWidth="24"
|
||||||
|
android:viewportHeight="24">
|
||||||
|
<path
|
||||||
|
android:pathData="M3.81,12.814C3.487,10.573 3.325,9.452 3.785,8.494C4.244,7.536 5.222,6.953 7.178,5.788L8.355,5.087C10.13,4.029 11.019,3.5 12,3.5C12.981,3.5 13.869,4.029 15.645,5.087L16.822,5.788C18.777,6.953 19.755,7.536 20.215,8.494C20.674,9.452 20.513,10.573 20.19,12.814L19.953,14.461C19.539,17.34 19.332,18.779 18.333,19.64C17.334,20.5 15.87,20.5 12.94,20.5H11.06C8.13,20.5 6.665,20.5 5.667,19.64C4.668,18.779 4.461,17.34 4.047,14.461L3.81,12.814Z"
|
||||||
|
android:strokeWidth="1.5"
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:strokeColor="#ffffff"/>
|
||||||
|
<path
|
||||||
|
android:pathData="M9.45,15.4C10.172,15.935 11.052,16.25 12,16.25C12.948,16.25 13.827,15.935 14.55,15.4"
|
||||||
|
android:strokeWidth="1.5"
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:strokeColor="#ffffff"
|
||||||
|
android:strokeLineCap="round"/>
|
||||||
|
</vector>
|
||||||
74
app/src/main/res/drawable/ic_launcher_background.xml
Normal file
74
app/src/main/res/drawable/ic_launcher_background.xml
Normal file
@ -0,0 +1,74 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<vector
|
||||||
|
android:height="108dp"
|
||||||
|
android:width="108dp"
|
||||||
|
android:viewportHeight="108"
|
||||||
|
android:viewportWidth="108"
|
||||||
|
xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<path android:fillColor="#3DDC84"
|
||||||
|
android:pathData="M0,0h108v108h-108z"/>
|
||||||
|
<path android:fillColor="#00000000" android:pathData="M9,0L9,108"
|
||||||
|
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||||
|
<path android:fillColor="#00000000" android:pathData="M19,0L19,108"
|
||||||
|
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||||
|
<path android:fillColor="#00000000" android:pathData="M29,0L29,108"
|
||||||
|
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||||
|
<path android:fillColor="#00000000" android:pathData="M39,0L39,108"
|
||||||
|
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||||
|
<path android:fillColor="#00000000" android:pathData="M49,0L49,108"
|
||||||
|
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||||
|
<path android:fillColor="#00000000" android:pathData="M59,0L59,108"
|
||||||
|
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||||
|
<path android:fillColor="#00000000" android:pathData="M69,0L69,108"
|
||||||
|
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||||
|
<path android:fillColor="#00000000" android:pathData="M79,0L79,108"
|
||||||
|
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||||
|
<path android:fillColor="#00000000" android:pathData="M89,0L89,108"
|
||||||
|
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||||
|
<path android:fillColor="#00000000" android:pathData="M99,0L99,108"
|
||||||
|
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||||
|
<path android:fillColor="#00000000" android:pathData="M0,9L108,9"
|
||||||
|
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||||
|
<path android:fillColor="#00000000" android:pathData="M0,19L108,19"
|
||||||
|
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||||
|
<path android:fillColor="#00000000" android:pathData="M0,29L108,29"
|
||||||
|
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||||
|
<path android:fillColor="#00000000" android:pathData="M0,39L108,39"
|
||||||
|
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||||
|
<path android:fillColor="#00000000" android:pathData="M0,49L108,49"
|
||||||
|
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||||
|
<path android:fillColor="#00000000" android:pathData="M0,59L108,59"
|
||||||
|
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||||
|
<path android:fillColor="#00000000" android:pathData="M0,69L108,69"
|
||||||
|
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||||
|
<path android:fillColor="#00000000" android:pathData="M0,79L108,79"
|
||||||
|
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||||
|
<path android:fillColor="#00000000" android:pathData="M0,89L108,89"
|
||||||
|
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||||
|
<path android:fillColor="#00000000" android:pathData="M0,99L108,99"
|
||||||
|
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||||
|
<path android:fillColor="#00000000" android:pathData="M19,29L89,29"
|
||||||
|
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||||
|
<path android:fillColor="#00000000" android:pathData="M19,39L89,39"
|
||||||
|
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||||
|
<path android:fillColor="#00000000" android:pathData="M19,49L89,49"
|
||||||
|
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||||
|
<path android:fillColor="#00000000" android:pathData="M19,59L89,59"
|
||||||
|
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||||
|
<path android:fillColor="#00000000" android:pathData="M19,69L89,69"
|
||||||
|
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||||
|
<path android:fillColor="#00000000" android:pathData="M19,79L89,79"
|
||||||
|
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||||
|
<path android:fillColor="#00000000" android:pathData="M29,19L29,89"
|
||||||
|
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||||
|
<path android:fillColor="#00000000" android:pathData="M39,19L39,89"
|
||||||
|
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||||
|
<path android:fillColor="#00000000" android:pathData="M49,19L49,89"
|
||||||
|
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||||
|
<path android:fillColor="#00000000" android:pathData="M59,19L59,89"
|
||||||
|
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||||
|
<path android:fillColor="#00000000" android:pathData="M69,19L69,89"
|
||||||
|
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||||
|
<path android:fillColor="#00000000" android:pathData="M79,19L79,89"
|
||||||
|
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||||
|
</vector>
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user