提交完整项目代码

This commit is contained in:
ocean 2025-05-26 16:14:15 +08:00
parent f02ce4bb3f
commit 664bf9db56
174 changed files with 10817 additions and 0 deletions

15
.gitignore vendored Normal file
View 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
View File

@ -0,0 +1,3 @@
# Default ignored files
/shelf/
/workspace.xml

6
.idea/AndroidProjectSystem.xml generated Normal file
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="" vcs="Git" />
</component>
</project>

Binary file not shown.

Binary file not shown.

26
.safedk/app_sdks.lst Normal file
View 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

Binary file not shown.

Binary file not shown.

2
.safedk/hashes.safedk Normal file
View File

@ -0,0 +1,2 @@
#Mon May 26 15:52:52 CST 2025
json=-1146317101

1
.safedk/list.enc Normal file

File diff suppressed because one or more lines are too long

View 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

View 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
View File

@ -0,0 +1 @@
/build

120
app/build.gradle Normal file
View 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 AdsChartboostDT ExchangcGooglc Ad ManagcrGoogle Bidding and Google AdMobInMobiironSourceLiftoff Monetizc
//Meta Audience NetworkMolocoPanglcUnity 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
View 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
View 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 聚合

View File

@ -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)
}
}

View 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>

Binary file not shown.

After

Width:  |  Height:  |  Size: 184 KiB

View 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));
}
})
}
}

View File

@ -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
}
}

View 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()
}
}
}
}

View 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()
}
}

View 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"
}
}

View File

@ -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()
}
}

View 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();
}

View 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();
}
});
}
}

View 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")
}
}
}

View File

@ -0,0 +1,6 @@
package com.keyboard.craft.ad;
public interface onAdAfterAction {
void onAction();
}

View File

@ -0,0 +1,6 @@
package com.keyboard.craft.ad;
public interface onAdStatusListener {
void onAdStatus(int type);
}

View File

@ -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)
}
}

View File

@ -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)
}
}
}
}

View File

@ -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)
}
}
}

View File

@ -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
}

View 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

View File

@ -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

View File

@ -0,0 +1,7 @@
package com.keyboard.craft.bean
import java.io.Serializable
data class Content(
val imageUrl: String
) : Serializable

View 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

View 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
}

View File

@ -0,0 +1,7 @@
package com.keyboard.craft.bean
import java.io.Serializable
data class LockBean(
val type: Int
) : Serializable

View 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

View File

@ -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

View File

@ -0,0 +1,7 @@
package com.keyboard.craft.bean
import java.io.Serializable
data class ThemeContentBean(
val pushIcon: String
) : Serializable

View File

@ -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

View 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 }
}
}
}
}

View 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
}

View 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?
}

View 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
}
}

View File

@ -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
}
}

View 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
}
}

View File

@ -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()
}
}

View 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()
}
}

View 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)
}
}
}

View 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)
)
}
}

View File

@ -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
}
}
}

View 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)
}
}
}

View 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()
}
}
}

View 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", "")
}
}

View File

@ -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)
}
}

View 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 -> "5GAndroid 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 IDAndroid 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
// }
}

View File

@ -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];
}
};
}
}

View 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
}
}

View 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
}
}
}

View File

@ -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
}
}

View 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>
* &lt;Keyboard
* android:keyWidth="%10p"
* android:keyHeight="50px"
* android:horizontalGap="2px"
* android:verticalGap="2px" &gt;
* &lt;Row android:keyWidth="32px" &gt;
* &lt;Key android:keyLabel="A" /&gt;
* ...
* &lt;/Row&gt;
* ...
* &lt;/Keyboard&gt;
* </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;
}
}

File diff suppressed because it is too large Load Diff

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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