This commit is contained in:
ocean 2024-07-17 17:09:32 +08:00
parent 7fb44962a8
commit cd287e1228
142 changed files with 9225 additions and 0 deletions

3
.idea/.gitignore generated vendored Normal file
View File

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

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="17" />
</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>

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="externalProjectPath" value="$PROJECT_DIR$" />
<option name="gradleJvm" value="#GRADLE_LOCAL_JAVA_HOME" />
<option name="modules">
<set>
<option value="$PROJECT_DIR$" />
<option value="$PROJECT_DIR$/app" />
</set>
</option>
<option name="resolveExternalAnnotations" value="false" />
</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>

9
.idea/misc.xml generated Normal file
View File

@ -0,0 +1,9 @@
<project version="4">
<component name="ExternalStorageConfigurationManager" enabled="true" />
<component name="ProjectRootManager" version="2" languageLevel="JDK_17" default="true" project-jdk-name="jbr-17" project-jdk-type="JavaSDK">
<output url="file://$PROJECT_DIR$/build/classes" />
</component>
<component name="ProjectType">
<option name="id" value="Android" />
</component>
</project>

Binary file not shown.

17
.safedk/app_sdks.lst Normal file
View File

@ -0,0 +1,17 @@
106f9be0e66f52f36eaaaff4dd231971
75939c4ce23c53ad9534d43be176b3e9
217e8f437c9fc4244d6e74653ac8a8c7
66b774de6608db14a84e972fba1ec954
e1c9ddef73e5621f62c717badf1be3f2
daaea35726ab7cd457ab61d4538fb822
1be9e72506f3307ce6a9e78d26d65bd0
4df96d3bc9afd17b812e65e6c6add1ef
eb3214f29c0a52815b41977d6cc9a46e
becf75b2cc99e82716da2e6697879509
7eec7b9476b99b3ce94533da4f2eb987
974322f19d813702ea048d95288d2b8c
29015bbfcc182d80e7f75bd2c38e4521
86a0d598cde251321e21a0da4ab94065
74616804a7dc29147dfb0afe122a9fd2
35695de726f6044576c830bf197f36f7

Binary file not shown.

Binary file not shown.

2
.safedk/hashes.safedk Normal file
View File

@ -0,0 +1,2 @@
#Wed Jul 17 17:05:37 CST 2024
json=521354371

1
.safedk/list.enc Normal file

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,5 @@
#
#Wed Jul 17 17:05:37 CST 2024
BoZtE6LMU2QaUEowq3SoQFO_HqwztZQdgF3VZGmNRR17TGv0XhXSwlT6LiaRllitI7yAsCkSGo_pfE0yfipADf=wt2KAZMCf_SkV_coMIB7GWtaOQtCd2ZFRK8hFAQo7zbXpIGpG5iI0fZ0sMJr5n_cCO3LEVU66gpxe099OFXXvv
sdk_analysis_plugin_version=5.0.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

85
app/build.gradle Normal file
View File

@ -0,0 +1,85 @@
plugins {
id 'com.android.application'
id 'org.jetbrains.kotlin.android'
id 'applovin-quality-service'
id 'com.google.gms.google-services'
id 'com.google.firebase.crashlytics'
}
applovin {
apiKey = "BoZtE6LMU2QaUEowq3SoQFO_HqwztZQdgF3VZGmNRR17TGv0XhXSwlT6LiaRllitI7yAsCkSGo_pfE0yfipADf"
}
android {
namespace 'com.keyboard.journey'
compileSdk 34
defaultConfig {
applicationId "com.keyboards.journey"
minSdk 23
targetSdk 34
versionCode 5
versionName "1.0.5"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
}
buildTypes {
release {
minifyEnabled true
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
kotlinOptions {
jvmTarget = '1.8'
}
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 'com.squareup.okhttp3:okhttp:4.11.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.applovin:applovin-sdk:12.1.0'
implementation 'com.applovin.mediation:vungle-adapter:6.12.0.0'
implementation 'com.applovin.mediation:mintegral-adapter:16.2.31.0'
implementation 'androidx.recyclerview:recyclerview:1.3.2'
// implementation 'com.applovin.mediation:bytedance-adapter:4.7.0.8.0'
implementation 'com.applovin.mediation:unityads-adapter:4.4.1.0'
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")
}

29
app/google-services.json Normal file
View File

@ -0,0 +1,29 @@
{
"project_info": {
"project_number": "651805746416",
"project_id": "keyboard-journey",
"storage_bucket": "keyboard-journey.appspot.com"
},
"client": [
{
"client_info": {
"mobilesdk_app_id": "1:651805746416:android:51f4a6799c2fc2dac88e35",
"android_client_info": {
"package_name": "com.keyboards.journey"
}
},
"oauth_client": [],
"api_key": [
{
"current_key": "AIzaSyCIX2Rq8AOsnmGDWsoeYFb67rzgauVidpc"
}
],
"services": {
"appinvite_service": {
"other_platform_oauth_client": []
}
}
}
],
"configuration_version": "1"
}

26
app/proguard-rules.pro vendored Normal file
View File

@ -0,0 +1,26 @@
# 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.** { *; }

View File

@ -0,0 +1,24 @@
package com.keyboard.journey
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.journey", appContext.packageName)
}
}

View File

@ -0,0 +1,65 @@
<?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" />
<application
android:name=".JourneyApp"
android:allowBackup="true"
android:dataExtractionRules="@xml/data_extraction_rules"
android:fullBackupContent="@xml/backup_rules"
android:icon="@mipmap/journey_app_logo"
android:label="@string/app_name"
android:roundIcon="@mipmap/journey_app_round_logo"
android:supportsRtl="true"
android:theme="@style/Theme.KeyboardJourney"
tools:targetApi="31">
<meta-data android:name="applovin.sdk.key"
android:value="wt2KAZMCf_SkV_coMIB7GWtaOQtCd2ZFRK8hFAQo7zbXpIGpG5iI0fZ0sMJr5n_cCO3LEVU66gpxe099OFXXvv"/>
<activity
android:name=".JourneyStartActivity"
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=".JourneyMActivity"
android:launchMode="singleTask"
android:screenOrientation="portrait" />
<service
android:name=".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=".JourneyCategoryDetailsActivity"
android:screenOrientation="portrait" />
<activity
android:name=".JourneyDetailsActivity"
android:screenOrientation="portrait" />
<activity
android:name=".PreviewActivity"
android:windowSoftInputMode="adjustResize"
android:screenOrientation="portrait" />
</application>
</manifest>

Binary file not shown.

View File

@ -0,0 +1,18 @@
package com.keyboard.journey
import android.app.Application
import com.applovin.sdk.AppLovinSdk
import com.applovin.sdk.AppLovinSdkConfiguration
class JourneyApp : Application() {
companion object {
lateinit var app: JourneyApp
}
override fun onCreate() {
super.onCreate()
app = this
AppLovinSdk.getInstance(this).mediationProvider = "max"
AppLovinSdk.getInstance(this).initializeSdk()
}
}

View File

@ -0,0 +1,138 @@
package com.keyboard.journey
import android.annotation.SuppressLint
import android.os.Bundle
import android.view.View
import androidx.appcompat.app.AppCompatActivity
import androidx.recyclerview.widget.GridLayoutManager
import com.gyf.immersionbar.ktx.immersionBar
import com.keyboard.journey.ad.MaxAdsInsUtil
import com.keyboard.journey.adapter.CategoryDetailsDataAdapter
import com.keyboard.journey.bean.CategoryDataBean
import com.keyboard.journey.bean.ItemDataBean
import com.keyboard.journey.bean.MainDataBean
import com.keyboard.journey.databinding.JourneyCategoryDetailsActivityBinding
import com.keyboard.journey.util.NetworkCallback
import com.keyboard.journey.util.NetworkUtil
import org.libpag.PAGFile
class JourneyCategoryDetailsActivity : AppCompatActivity() {
companion object {
const val CATEGORY_DETAILS_BEAN_KEY = "category_details_bean_key"
}
private lateinit var binding: JourneyCategoryDetailsActivityBinding
private var offset = 0
private var pageSize = 20
private var bean: MainDataBean? = null
private var adapter: CategoryDetailsDataAdapter? = null
private var contentBeans: MutableList<ItemDataBean> = mutableListOf()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = JourneyCategoryDetailsActivityBinding.inflate(layoutInflater)
setContentView(binding.root)
MaxAdsInsUtil.showAdRandomMode(this)
bean = intent.getSerializableExtra(CATEGORY_DETAILS_BEAN_KEY) as MainDataBean?
if (bean == null) {
finish()
}
initBar()
initView()
initData()
MaxAdsInsUtil.loadAllAdIsNotCached(this)
}
private fun initBar() {
immersionBar {
statusBarDarkFont(true)
statusBarView(binding.view)
}
}
private fun initView() {
binding.titleTv.text = bean?.title
binding.backBtn.setOnClickListener { finish() }
binding.refreshLayout.setEnableRefresh(false)
binding.refreshLayout.setOnLoadMoreListener {
refreshData()
}
adapter = CategoryDetailsDataAdapter(this, contentBeans)
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
val pagFile = PAGFile.Load(assets, "loading.pag")
binding.pagView.composition = pagFile
binding.pagView.setRepeatCount(0)
binding.pagView.play()
}
private fun loadingClose(){
binding.loadingLayout.visibility = View.GONE
if(binding.pagView.isPlaying){
binding.pagView.stop()
}
}
}

View File

@ -0,0 +1,154 @@
package com.keyboard.journey
import android.content.Intent
import android.os.Bundle
import android.view.View
import androidx.appcompat.app.AppCompatActivity
import com.bumptech.glide.Glide
import com.gyf.immersionbar.ktx.immersionBar
import com.keyboard.journey.ad.MaxAdsInsUtil
import com.keyboard.journey.bean.DetailsBean
import com.keyboard.journey.bean.ItemDataBean
import com.keyboard.journey.databinding.JourneyDetailsActivityBinding
import com.keyboard.journey.util.AppSharedPreferences
import com.keyboard.journey.util.NetworkCallback
import com.keyboard.journey.util.NetworkUtil
import com.keyboard.journey.util.OnDownloadListener
import com.keyboard.journey.util.ResourceDownloadUtil
import com.keyboard.journey.util.fileIsDownload
import com.keyboard.journey.util.loadRoundedImage
import org.libpag.PAGFile
class JourneyDetailsActivity : AppCompatActivity(), OnDownloadListener {
companion object {
const val KEY_JOURNEY_DETAILS_BEAN = "key_journey_details_bean"
}
private lateinit var binding: JourneyDetailsActivityBinding
private var bean: ItemDataBean? = null
private val appSharedPreferences = lazy { AppSharedPreferences(this) }
private var detailsBean: DetailsBean? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = JourneyDetailsActivityBinding.inflate(layoutInflater)
setContentView(binding.root)
MaxAdsInsUtil.showAdRandomMode(this)
bean = intent.getSerializableExtra(KEY_JOURNEY_DETAILS_BEAN) as ItemDataBean?
initBar()
initView()
initData()
MaxAdsInsUtil.loadAllAdIsNotCached(this)
}
private fun initBar() {
immersionBar {
statusBarDarkFont(true)
statusBarView(binding.view)
}
}
private fun initView() {
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) {
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
val pagFile = PAGFile.Load(assets, "loading.pag")
binding.pagView.composition = pagFile
binding.pagView.setRepeatCount(0)
binding.pagView.play()
}
private fun loadingClose() {
binding.loadingLayout.visibility = View.GONE
if (binding.pagView.isPlaying) {
binding.pagView.stop()
}
}
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!!)
}
}

View File

@ -0,0 +1,321 @@
package com.keyboard.journey
import android.annotation.SuppressLint
import android.content.Intent
import android.graphics.Color
import android.net.Uri
import android.os.Bundle
import android.provider.Settings
import android.view.View
import android.view.inputmethod.InputMethodManager
import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity
import androidx.core.view.GravityCompat
import androidx.drawerlayout.widget.DrawerLayout
import androidx.recyclerview.widget.LinearLayoutManager
import com.gyf.immersionbar.ktx.immersionBar
import com.keyboard.journey.ad.MaxAdsInsUtil
import com.keyboard.journey.adapter.BannerImgAdapter
import com.keyboard.journey.adapter.MainDataAdapter
import com.keyboard.journey.bean.CategoryDataBean
import com.keyboard.journey.bean.MainDataBean
import com.keyboard.journey.databinding.JourneyActivityMainBinding
import com.keyboard.journey.util.NetworkCallback
import com.keyboard.journey.util.NetworkUtil
import com.keyboard.journey.util.getRandomInt
import com.keyboard.journey.util.isMyInputMethodDefault
import com.keyboard.journey.util.isMyInputMethodEnabled
import com.keyboard.journey.util.openPrivacyPolicy
import com.keyboard.journey.util.shareAppInfo
import com.youth.banner.listener.OnPageChangeListener
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.sync.withLock
import kotlinx.coroutines.withContext
import okhttp3.Call
import okhttp3.Callback
import okhttp3.Response
import org.libpag.PAGFile
import java.io.IOException
import kotlin.coroutines.resume
import kotlin.coroutines.resumeWithException
class JourneyMActivity : AppCompatActivity() {
private lateinit var binding: JourneyActivityMainBinding
private var imgList: MutableList<Int> = mutableListOf()
private var dataList: MutableList<MainDataBean> = mutableListOf()
private var mainAdapter: MainDataAdapter? = null
private var backPressedOnce = false
private var currentBannerType = 0
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = JourneyActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
initBar()
initView()
initData()
}
override fun onResume() {
super.onResume()
MaxAdsInsUtil.loadAllAdIsNotCached(this)
}
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()
NetworkUtil().fetchData(object : NetworkCallback<List<MainDataBean>> {
@SuppressLint("NotifyDataSetChanged")
override fun onSuccess(data: List<MainDataBean>) {
GlobalScope.launch(Dispatchers.IO) {
val jobs = data.map { bean ->
if (bean.title == "For You") {
bean.title = "Recommend"
}
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)
mainAdapter?.notifyDataSetChanged()
loadingClose()
}
}
}
override fun onFailure(errorMessage: String) {
LogUtil.logMsgD(errorMessage)
}
})
}
private fun initBar() {
immersionBar {
statusBarDarkFont(true)
statusBarView(binding.view)
}
}
private fun setupDrawerListener() {
binding.drawer.addDrawerListener(object : DrawerLayout.DrawerListener {
override fun onDrawerSlide(drawerView: View, slideOffset: Float) {}
override fun onDrawerOpened(drawerView: View) {
drawerView.isClickable = true
}
override fun onDrawerClosed(drawerView: View) {}
override fun onDrawerStateChanged(newState: Int) {}
})
}
private fun initClick() {
binding.menuBtn.setOnClickListener {
if (binding.drawer.isDrawerOpen(GravityCompat.START)) {
binding.drawer.closeDrawer(GravityCompat.START)
} else {
binding.drawer.openDrawer(GravityCompat.START)
}
}
binding.bannerBtn.setOnClickListener {
val recommend = dataList.filter { it.title == "Recommend" }
val cool = dataList.filter { it.title == "Cool" }
if (currentBannerType == 0) {
val intent = Intent(this, JourneyCategoryDetailsActivity::class.java)
intent.putExtra(JourneyCategoryDetailsActivity.CATEGORY_DETAILS_BEAN_KEY, recommend[0])
startActivity(intent)
} else if (currentBannerType == 1) {
val intent = Intent(this, JourneyCategoryDetailsActivity::class.java)
intent.putExtra(JourneyCategoryDetailsActivity.CATEGORY_DETAILS_BEAN_KEY, cool[0])
startActivity(intent)
}
}
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 = getSystemService(INPUT_METHOD_SERVICE) as InputMethodManager
imeManager.showInputMethodPicker()
}
binding.dialogStepLayout.setOnClickListener {
binding.dialogStepLayout.visibility = View.GONE
}
binding.privacyBtn.setOnClickListener {
val privacyPolicyUrl = "https://sites.google.com/view/privacy-policy-html-app"
val intent = Intent(Intent.ACTION_VIEW)
intent.data = Uri.parse(privacyPolicyUrl)
startActivity(intent)
}
binding.shareBtn.setOnClickListener {
shareAppInfo(this)
}
}
override fun onBackPressed() {
if (binding.drawer.isDrawerOpen(GravityCompat.START)) {
binding.drawer.closeDrawer(GravityCompat.START);
} else {
if (backPressedOnce) {
super.onBackPressed()
return
}
backPressedOnce = true
Toast.makeText(this, "Press again to exit!", Toast.LENGTH_SHORT).show()
// 两秒钟内再次按返回键取消退出操作
val handler = android.os.Handler()
handler.postDelayed({ backPressedOnce = false }, 2000)
}
}
private fun initView() {
binding.rv.layoutManager = LinearLayoutManager(this)
mainAdapter = MainDataAdapter(this, dataList)
binding.rv.adapter = mainAdapter
imgList.add(R.mipmap.recommend_top_banner_img)
imgList.add(R.mipmap.cool_top_banner_img)
val adapter = BannerImgAdapter(imgList)
binding.banner.setAdapter(adapter).addBannerLifecycleObserver(this) //添加生命周期观察者
binding.banner.addOnPageChangeListener(object : OnPageChangeListener {
override fun onPageScrolled(
position: Int,
positionOffset: Float,
positionOffsetPixels: Int
) {
}
override fun onPageSelected(position: Int) {
when (position) {
0 -> {
currentBannerType = 0
binding.indicatorLayout.setBackgroundColor(getColor(R.color.banner_indicator_one_bg))
binding.indicatorImg.setImageResource(R.mipmap.top_banner_one_img)
binding.indicatorTxt.text = getString(R.string.recommend)
binding.indicatorGoBg.setCardBackgroundColor(getColor(R.color.banner_indicator_one_go_bg_color))
binding.indicatorLineOneBg.setBackgroundColor(getColor(R.color.banner_indicator_one_go_bg_color))
binding.indicatorLineTowBg.setBackgroundColor(getColor(R.color.black_10))
}
1 -> {
currentBannerType = 1
binding.indicatorLayout.setBackgroundColor(getColor(R.color.banner_indicator_tow_bg))
binding.indicatorImg.setImageResource(R.mipmap.top_banner_tow_img)
binding.indicatorTxt.text = getString(R.string.cool)
binding.indicatorGoBg.setCardBackgroundColor(getColor(R.color.banner_indicator_tow_go_bg_color))
binding.indicatorLineOneBg.setBackgroundColor(getColor(R.color.black_10))
binding.indicatorLineTowBg.setBackgroundColor(getColor(R.color.banner_indicator_tow_go_bg_color))
}
}
}
override fun onPageScrollStateChanged(state: Int) {
}
})
setupDrawerListener()
initClick()
}
private fun loadingPlay() {
binding.loadingLayout.visibility = View.VISIBLE
binding.indicatorGoBg.visibility = View.GONE
val pagFile = PAGFile.Load(assets, "loading.pag")
binding.pagView.composition = pagFile
binding.pagView.setRepeatCount(0)
binding.pagView.play()
}
private fun loadingClose() {
binding.loadingLayout.visibility = View.GONE
binding.indicatorGoBg.visibility = View.VISIBLE
if (binding.pagView.isPlaying) {
binding.pagView.stop()
}
}
private fun updateSetMyInputMethod() {
val enabled = isMyInputMethodEnabled(this)
val default = isMyInputMethodDefault(this)
if (enabled) {
binding.step1Btn.background = getDrawable(R.drawable.drw_gray_select_bg)
binding.step1Btn.text = "Step 1:Enabled"
binding.step1Btn.setTextColor(Color.parseColor("#999999"))
} else {
binding.step1Btn.background = getDrawable(R.mipmap.activate_btn_bg)
binding.step1Btn.text = "Step 1:Select"
binding.step1Btn.setTextColor(Color.parseColor("#000000"))
}
if (default) {
binding.step2Btn.background = getDrawable(R.drawable.drw_gray_select_bg)
binding.step2Btn.text = "Step 2:Enabled"
binding.step1Btn.setTextColor(Color.parseColor("#999999"))
} else {
binding.step2Btn.background = getDrawable(R.mipmap.activate_btn_bg)
binding.step2Btn.text = "Step 2:Select"
binding.step1Btn.setTextColor(Color.parseColor("#000000"))
}
}
}

View File

@ -0,0 +1,140 @@
package com.keyboard.journey
import android.content.Intent
import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import com.applovin.mediation.MaxError
import com.gyf.immersionbar.ktx.immersionBar
import com.keyboard.journey.ad.AdLoadListener
import com.keyboard.journey.ad.AdShowFailed
import com.keyboard.journey.ad.AdShowListener
import com.keyboard.journey.ad.MaxAdsInsUtil
import com.keyboard.journey.databinding.JourneyStartActivityBinding
import org.libpag.PAGFile
import java.util.Timer
import java.util.TimerTask
class JourneyStartActivity : AppCompatActivity() {
companion object {
const val SPLASH_TIME_OUT: Long = 15000
}
private lateinit var binding: JourneyStartActivityBinding
private var timer: DelayTimer? = null // 可变变量,初始化为 null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = JourneyStartActivityBinding.inflate(layoutInflater)
setContentView(binding.root)
immersionBar {
fullScreen(true)
statusBarDarkFont(false)
}
timer = DelayTimer(SPLASH_TIME_OUT, callback = {
LogUtil.logMsgD("loaded ing")
if (adCanShow()) {
timer?.stop()
showAd()
}
}, com = {
startMain()
})
loadAd()
if (timer != null) {
timer?.start()
}
loadingPlay()
}
private fun loadAd() {
MaxAdsInsUtil.loadMaxAd(
this,
MaxAdsInsUtil.Placement.SPLASH_AD_ID, object : AdLoadListener {
override fun loaded() {
LogUtil.logMsgD("loaded")
}
override fun loadFailed(error: MaxError?) {
LogUtil.logMsgD(error.toString())
startMain()
timer?.stop()
}
})
MaxAdsInsUtil.loadSplashAllAd(this)
}
private fun adCanShow(): Boolean {
return MaxAdsInsUtil.canShowAd(MaxAdsInsUtil.Placement.SPLASH_AD_ID)
}
private fun showAd() {
MaxAdsInsUtil.showMaxAd(
this,
MaxAdsInsUtil.Placement.SPLASH_AD_ID,
object : AdShowListener {
override fun onAdShown() {
}
override fun onAdShowFailed(error: AdShowFailed?) {
startMain()
}
override fun onAdClosed() {
startMain()
}
})
}
private fun startMain() {
val intent = Intent(this, JourneyMActivity::class.java)
startActivity(intent)
finish()
}
private fun loadingPlay() {
val pagFile = PAGFile.Load(assets, "loading.pag")
binding.pagView.composition = pagFile
binding.pagView.setRepeatCount(0)
binding.pagView.play()
}
private fun loadingClose() {
if (binding.pagView.isPlaying) {
binding.pagView.stop()
}
}
override fun onDestroy() {
super.onDestroy()
loadingClose()
}
}
class DelayTimer(
private val totalDelayInMillis: Long,
private val callback: () -> Unit,
private val com: () -> Unit
) {
private val timer = Timer()
private var elapsedTime: Long = 0
fun start() {
timer.scheduleAtFixedRate(object : TimerTask() {
override fun run() {
if (elapsedTime < totalDelayInMillis) {
callback.invoke()
elapsedTime += 1000 // 每秒钟递增
} else {
com.invoke()
stop()
}
}
}, 0, 1000) // 每秒钟执行一次
}
fun stop() {
timer.cancel()
}
}

View File

@ -0,0 +1,359 @@
package com.keyboard.journey
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.journey.util.AppSharedPreferences
import com.keyboard.journey.util.colorsXmlPullParser
import com.keyboard.journey.util.currentlyThemeUFileString
import com.keyboard.journey.util.getBitmapDrawable
import com.keyboard.journey.util.getPath
import com.keyboard.journey.util.getRawVideoPath
import com.keyboard.journey.util.getStateDrawable
import com.keyboard.journey.view.JourneyKeyboardView
import com.keyboard.journey.view.Keyboard
import com.keyboard.journey.view.KeyboardView
import com.keyboard.journey.view.JourneyKeyboard
import java.io.File
class KeyboardService : InputMethodService() {
private lateinit var keyboardView: JourneyKeyboardView
private lateinit var currentKeyboard: 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 = 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 = JourneyKeyboardView.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 : 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 { Keyboard(this, R.xml.my_custom_keyboard_layout) }
private val keyboardNormalModeChange by lazy { Keyboard(this, R.xml.keyboard_mode_change) }
private val keyboardNormalMore by lazy { Keyboard(this, R.xml.keyboard_more_symbol) }
/**
* 根据 primaryCode去做相应的处理
*/
private fun performKey(ic: InputConnection, primaryCode: Int, keyCodes: IntArray?) {
when (primaryCode) {
JourneyKeyboard.KEYCODE_SHIFT -> keyShift()
JourneyKeyboard.KEYCODE_MODE_CHANGE -> keyModeChange()
JourneyKeyboard.KEYCODE_CANCEL -> keyCancel(primaryCode)
JourneyKeyboard.KEYCODE_DONE -> keyDone(primaryCode)
JourneyKeyboard.KEYCODE_DELETE -> keyDelete(ic)
JourneyKeyboard.KEYCODE_MODE_BACK -> keyBack(false)
JourneyKeyboard.KEYCODE_BACK -> keyBack(true)
JourneyKeyboard.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: 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: 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) {
JourneyKeyboard.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,13 @@
package com.keyboard.journey
import android.util.Log
object LogUtil {
private const val LogTag = "Journey-keyboard"
fun logMsgD(msg: String) {
if (BuildConfig.DEBUG) {
Log.d(LogTag, msg)
}
}
}

View File

@ -0,0 +1,174 @@
package com.keyboard.journey
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.graphics.BitmapFactory
import android.graphics.Color
import android.graphics.drawable.BitmapDrawable
import android.os.Bundle
import android.provider.Settings
import android.view.View
import android.view.inputmethod.InputMethodManager
import androidx.appcompat.app.AppCompatActivity
import com.gyf.immersionbar.ktx.immersionBar
import com.keyboard.journey.ad.MaxAdsInsUtil
import com.keyboard.journey.databinding.JourneyPreviewActivityBinding
import com.keyboard.journey.util.currentlyThemeUFileString
import com.keyboard.journey.util.getBitmapXXDrawable
import com.keyboard.journey.util.isMyInputMethodDefault
import com.keyboard.journey.util.isMyInputMethodEnabled
import com.keyboard.journey.util.loadAndBlurImage
class PreviewActivity : AppCompatActivity() {
companion object {
const val KEY_PREVIEW_URL = "key_preview_url"
}
private lateinit var binding: JourneyPreviewActivityBinding
private var themeUrl = ""
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = JourneyPreviewActivityBinding.inflate(layoutInflater)
setContentView(binding.root)
MaxAdsInsUtil.loadAllAdIsNotCached(this)
initBar()
themeUrl = intent.getStringExtra(KEY_PREVIEW_URL).toString()
registerReceiver()
initView()
}
private fun initBar() {
immersionBar {
statusBarDarkFont(true)
fitsSystemWindows(true)
}
}
override fun onResume() {
super.onResume()
updateSetMyInputMethod()
}
override fun onBackPressed() {
MaxAdsInsUtil.showAdRandomMode(this)
super.onBackPressed()
}
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 updateSetMyInputMethod() {
val enabled = isMyInputMethodEnabled(this)
val default = isMyInputMethodDefault(this)
if (enabled) {
binding.step1Btn.background = getDrawable(R.drawable.drw_gray_select_bg)
binding.step1Btn.text = "Step 1:Enabled"
binding.step1Btn.setTextColor(Color.parseColor("#999999"))
} else {
binding.step1Btn.background = getDrawable(R.mipmap.activate_btn_bg)
binding.step1Btn.text = "Step 1:Select"
binding.step1Btn.setTextColor(Color.parseColor("#000000"))
}
if (default) {
binding.step2Btn.background = getDrawable(R.drawable.drw_gray_select_bg)
binding.step2Btn.text = "Step 2:Enabled"
binding.step1Btn.setTextColor(Color.parseColor("#999999"))
} else {
binding.step2Btn.background = getDrawable(R.mipmap.activate_btn_bg)
binding.step2Btn.text = "Step 2:Select"
binding.step1Btn.setTextColor(Color.parseColor("#000000"))
}
if (enabled && default) {
binding.edit.visibility = View.VISIBLE
binding.tv.visibility = View.GONE
} else {
binding.edit.visibility = View.GONE
binding.tv.visibility = 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()
}
private fun showKeyboard(view: View) {
view.postDelayed({
view.requestFocus()
val imm = getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
imm.showSoftInput(view, InputMethodManager.SHOW_IMPLICIT)
}, 1000) // 设置延迟时间
}
}

View File

@ -0,0 +1,8 @@
package com.keyboard.journey.ad
import com.applovin.mediation.MaxError
interface AdLoadListener {
fun loadFailed(error: MaxError?) {}
fun loaded() {}
}

View File

@ -0,0 +1,5 @@
package com.keyboard.journey.ad
data class AdShowFailed(
val msg: String = "",
)

View File

@ -0,0 +1,7 @@
package com.keyboard.journey.ad
interface AdShowListener {
fun onAdShown() {}
fun onAdShowFailed(error: AdShowFailed?) {}
fun onAdClosed() {}
}

View File

@ -0,0 +1,21 @@
package com.keyboard.journey.ad
import com.applovin.mediation.ads.MaxInterstitialAd
class InstAdCacheManager {
private val mMaxAdCacheDict: MutableMap<String, MaxInterstitialAd> = mutableMapOf()
companion object {
val instance: InstAdCacheManager by lazy(mode = LazyThreadSafetyMode.SYNCHRONIZED) {
InstAdCacheManager()
}
}
fun setAdCache(place: String, adCache: MaxInterstitialAd) {
mMaxAdCacheDict[place] = adCache
}
fun getAdCache(place: String): MaxInterstitialAd? {
return mMaxAdCacheDict[place]
}
}

View File

@ -0,0 +1,59 @@
package com.keyboard.journey.ad
import android.app.Activity
import com.applovin.mediation.MaxAd
import com.applovin.mediation.MaxAdListener
import com.applovin.mediation.MaxError
import com.applovin.mediation.ads.MaxInterstitialAd
class MaxAdInstLoad {
private var mPlace: String
private var adLoadListener: AdLoadListener? = null
private var activity: Activity? = null
constructor(activity: Activity, place: String, listener: AdLoadListener?) {
this.mPlace = place
this.adLoadListener = listener
this.activity = activity
init()
}
constructor(place: String, listener: AdLoadListener?) {
this.mPlace = place
this.adLoadListener = listener
init()
}
private fun init() {
val interstitialAd = MaxInterstitialAd(mPlace, activity)
interstitialAd.setListener(object : MaxAdListener {
override fun onAdLoaded(p0: MaxAd) {
// Handle ad loaded event
InstAdCacheManager.instance.setAdCache(mPlace, interstitialAd)
adLoadListener?.loaded()
}
override fun onAdDisplayed(p0: MaxAd) {
// Handle ad displayed event
}
override fun onAdHidden(p0: MaxAd) {
// Handle ad hidden event
}
override fun onAdClicked(p0: MaxAd) {
// Handle ad clicked event
}
override fun onAdLoadFailed(p0: String, p1: MaxError) {
// Handle ad load failure event
adLoadListener?.loadFailed(p1)
}
override fun onAdDisplayFailed(p0: MaxAd, p1: MaxError) {
// Handle ad display failure event
}
})
interstitialAd.loadAd()
}
}

View File

@ -0,0 +1,58 @@
package com.keyboard.journey.ad
import android.app.Activity
import com.applovin.mediation.MaxAd
import com.applovin.mediation.MaxAdListener
import com.applovin.mediation.MaxError
class MaxAdInstShower {
private var mPlace: String
private var showListener: AdShowListener? = null
private var activity: Activity? = null
constructor(activity: Activity, place: String, showListener: AdShowListener?) {
this.mPlace = place
this.showListener = showListener
this.activity = activity
init()
}
constructor(place: String, showListener: AdShowListener?) {
this.mPlace = place
this.showListener = showListener
init()
}
private fun init() {
val interstitialAd = InstAdCacheManager.instance.getAdCache(mPlace)
interstitialAd?.setListener(object : MaxAdListener {
override fun onAdLoaded(p0: MaxAd) {
// Handle ad loaded event
}
override fun onAdDisplayed(p0: MaxAd) {
// Handle ad displayed event
showListener?.onAdShown()
}
override fun onAdHidden(p0: MaxAd) {
// Handle ad hidden event
showListener?.onAdClosed()
}
override fun onAdClicked(p0: MaxAd) {
// Handle ad clicked event
}
override fun onAdLoadFailed(p0: String, p1: MaxError) {
// Handle ad load failure event
showListener?.onAdShowFailed(AdShowFailed(p1.message))
}
override fun onAdDisplayFailed(p0: MaxAd, p1: MaxError) {
// Handle ad display failure event
}
})
interstitialAd?.showAd()
}
}

View File

@ -0,0 +1,82 @@
package com.keyboard.journey.ad
import android.app.Activity
import java.util.Random
object MaxAdsInsUtil {
object Placement {
const val SPLASH_AD_ID = "f17d90b8ffcd8c43"
const val MAX_AD_ID_ONE = "b940838b4e632b48"
const val MAX_AD_ID_TOW = "872bcabcd258447b"
val adPlaceAllList = listOf(
SPLASH_AD_ID,
MAX_AD_ID_ONE,
MAX_AD_ID_TOW
)
}
fun loadSplashAllAd(act: Activity) {
Placement.adPlaceAllList.drop(1).forEach { placement ->
loadAdIfNotCached(act, placement)
}
}
fun loadAllAdIsNotCached(act: Activity) {
Placement.adPlaceAllList.forEach { placement ->
loadAdIfNotCached(act, placement)
}
}
fun loadAdIfNotCached(act: Activity, placement: String, listener: AdLoadListener? = null) {
if (act.isFinishing || canShowAd(placement)) return
loadMaxAd(act, placement, listener)
}
fun showAdRandomMode(act: Activity) {
val adPlace: MutableList<String> = ArrayList()
Placement.adPlaceAllList.forEach { placement ->
if (canShowAd(placement)) {
adPlace.add(placement)
}
}
if (adPlace.isNotEmpty()) {
val placeId = Random().nextInt(adPlace.size)
val place = adPlace[placeId]
showAdIfCached(act, place)
}
}
fun showAdIfCached(act: Activity, placement: String, listener: AdShowListener? = null) {
if (act.isFinishing || !canShowAd(placement)) {
listener?.onAdShowFailed(AdShowFailed("No cache for ads"))
} else {
showMaxAd(act, placement, listener)
}
}
fun loadMaxAd(
act: Activity,
adID: String,
loadListener: AdLoadListener?
): MaxAdInstLoad {
return MaxAdInstLoad(act, adID, loadListener)
}
fun showMaxAd(
act: Activity,
adID: String,
listener: AdShowListener?
): MaxAdInstShower {
return MaxAdInstShower(act, adID, listener)
}
fun canShowAd(adID: String): Boolean {
InstAdCacheManager.instance.getAdCache(adID)?.let {
return it.isReady
} ?: let {
return false
}
}
}

View File

@ -0,0 +1,29 @@
package com.keyboard.journey.adapter
import android.view.ViewGroup
import android.widget.ImageView
import androidx.recyclerview.widget.RecyclerView
import com.youth.banner.adapter.BannerAdapter
class BannerImgAdapter(data: MutableList<Int>) :
BannerAdapter<Int, BannerImgAdapter.BannerViewHolder>(
data
) {
class BannerViewHolder(var imageView: ImageView) :
RecyclerView.ViewHolder(imageView)
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)
}
override fun onBindView(holder: BannerViewHolder, data: Int, position: Int, size: Int) {
holder.imageView.setImageResource(data)
}
}

View File

@ -0,0 +1,58 @@
package com.keyboard.journey.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.bumptech.glide.Glide
import com.keyboard.journey.JourneyCategoryDetailsActivity
import com.keyboard.journey.JourneyCategoryDetailsActivity.Companion.CATEGORY_DETAILS_BEAN_KEY
import com.keyboard.journey.JourneyDetailsActivity
import com.keyboard.journey.bean.CategoryDataBean
import com.keyboard.journey.bean.ItemDataBean
import com.keyboard.journey.bean.MainDataBean
import com.keyboard.journey.databinding.AdapterCategoryDetailsItemBinding
import com.keyboard.journey.databinding.MainRvItemBinding
class CategoryDetailsDataAdapter(private val context: Context, private val mainDataList: List<ItemDataBean>) :
RecyclerView.Adapter<CategoryDetailsDataAdapter.MainDataViewHolder>() {
inner class MainDataViewHolder(private val binding: AdapterCategoryDetailsItemBinding) :
RecyclerView.ViewHolder(binding.root) {
fun bind(data: ItemDataBean) {
binding.apply {
var url = data.thumbUrlGif
if (url.isEmpty()) {
url = data.thumbUrl
}
Glide.with(context)
.load(url)
.into(imageView)
}
}
}
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 = mainDataList[position]
holder.bind(currentItem)
holder.itemView.setOnClickListener {
val intent = Intent(context, JourneyDetailsActivity::class.java)
intent.putExtra(JourneyDetailsActivity.KEY_JOURNEY_DETAILS_BEAN,currentItem)
context.startActivity(intent)
}
}
override fun getItemCount() = mainDataList.size
}

View File

@ -0,0 +1,58 @@
package com.keyboard.journey.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.journey.JourneyDetailsActivity
import com.keyboard.journey.JourneyDetailsActivity.Companion.KEY_JOURNEY_DETAILS_BEAN
import com.keyboard.journey.LogUtil
import com.keyboard.journey.bean.ItemDataBean
import com.keyboard.journey.databinding.HorizontalScrollingRvItemBinding
class CategoryItemDataAdapter(
private val context: Context,
private val mainDataList: List<ItemDataBean>
) :
RecyclerView.Adapter<CategoryItemDataAdapter.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 {
val intent = Intent(context, JourneyDetailsActivity::class.java)
intent.putExtra(KEY_JOURNEY_DETAILS_BEAN,currentItem)
context.startActivity(intent)
}
}
override fun getItemCount() = mainDataList.size
}

View File

@ -0,0 +1,52 @@
package com.keyboard.journey.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.journey.JourneyCategoryDetailsActivity
import com.keyboard.journey.JourneyCategoryDetailsActivity.Companion.CATEGORY_DETAILS_BEAN_KEY
import com.keyboard.journey.bean.MainDataBean
import com.keyboard.journey.databinding.MainRvItemBinding
class MainDataAdapter(private val context: Context, private val mainDataList: List<MainDataBean>) :
RecyclerView.Adapter<MainDataAdapter.MainDataViewHolder>() {
inner class MainDataViewHolder(private val binding: MainRvItemBinding) :
RecyclerView.ViewHolder(binding.root) {
fun bind(mainData: MainDataBean) {
binding.apply {
titleTextView.text = mainData.title
val adapter = CategoryItemDataAdapter(context, mainData.items!!)
itemRv.layoutManager =
LinearLayoutManager(context, LinearLayoutManager.HORIZONTAL, false)
itemRv.adapter = adapter
moreData.setOnClickListener {
val intent = Intent(context, JourneyCategoryDetailsActivity::class.java)
intent.putExtra(CATEGORY_DETAILS_BEAN_KEY, mainData)
context.startActivity(intent)
}
}
}
}
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
}

View File

@ -0,0 +1,10 @@
package com.keyboard.journey.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.journey.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.journey.bean
import java.io.Serializable
data class Content(
val imageUrl: String
) : Serializable

View File

@ -0,0 +1,16 @@
package com.keyboard.journey.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,14 @@
package com.keyboard.journey.bean
import java.io.Serializable
data class ItemDataBean(
val key: String,
val title: String,
val type: Int,
val thumbUrl: String,
val thumbUrlGif: String,
val themeContentBean: ThemeContentBean,
val lockBean: LockBean
) : Serializable

View File

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

View File

@ -0,0 +1,13 @@
package com.keyboard.journey.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.journey.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.journey.bean
import java.io.Serializable
data class ThemeContentBean(
val pushIcon: String
) : Serializable

View File

@ -0,0 +1,13 @@
package com.keyboard.journey.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,65 @@
package com.keyboard.journey.util
import android.content.Context
import android.content.SharedPreferences
class AppSharedPreferences(context: Context) {
companion object {
private const val KEY_CURRENTLY_THEME_URL = "key_journey_currently_theme_url"
private const val KEY_CURRENTLY_THEME_GIF_URL = "key_journey_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,234 @@
package com.keyboard.journey.util
import android.content.Context
import android.content.Intent
import android.content.pm.PackageManager
import android.graphics.Bitmap
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.engine.bitmap_recycle.BitmapPool
import com.bumptech.glide.load.resource.bitmap.BitmapTransformation
import com.bumptech.glide.load.resource.bitmap.RoundedCorners
import com.bumptech.glide.load.resource.bitmap.TransformationUtils
import com.bumptech.glide.request.RequestOptions
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 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)
}
}

View File

@ -0,0 +1,286 @@
package com.keyboard.journey.util
import com.keyboard.journey.bean.Author
import com.keyboard.journey.bean.CategoryDataBean
import com.keyboard.journey.bean.Content
import com.keyboard.journey.bean.DetailsBean
import com.keyboard.journey.bean.ItemDataBean
import com.keyboard.journey.bean.LockBean
import com.keyboard.journey.bean.MainDataBean
import com.keyboard.journey.bean.MainItemDataBean
import com.keyboard.journey.bean.ThemeContentBean
import com.keyboard.journey.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(
itemKey,
itemTitle,
itemType,
thumbUrl,
thumbUrlGif,
themeContent,
lock
)
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.journey.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,701 @@
package com.keyboard.journey.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 android.widget.TextView;
import com.keyboard.journey.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,83 @@
package com.keyboard.journey.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,58 @@
package com.keyboard.journey.view
open class JourneyKeyboard {
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.journey.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.journey.R
import com.keyboard.journey.view.JourneyKeyboardView.Config.Companion.KEYCODE_BACK
import com.keyboard.journey.view.JourneyKeyboardView.Config.Companion.KEYCODE_CANCEL
import com.keyboard.journey.view.JourneyKeyboardView.Config.Companion.KEYCODE_DELETE
import com.keyboard.journey.view.JourneyKeyboardView.Config.Companion.KEYCODE_DONE
import com.keyboard.journey.view.JourneyKeyboardView.Config.Companion.KEYCODE_DOT
import com.keyboard.journey.view.JourneyKeyboardView.Config.Companion.KEYCODE_MODE_BACK
import com.keyboard.journey.view.JourneyKeyboardView.Config.Companion.KEYCODE_MODE_CHANGE
import com.keyboard.journey.view.JourneyKeyboardView.Config.Companion.KEYCODE_MORE
import com.keyboard.journey.view.JourneyKeyboardView.Config.Companion.KEYCODE_NONE
import com.keyboard.journey.view.JourneyKeyboardView.Config.Companion.KEYCODE_QUESTION_MARK
import com.keyboard.journey.view.JourneyKeyboardView.Config.Companion.KEYCODE_SPACE
open class JourneyKeyboardView : 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.journeyKeyboardView)
a.indexCount.let {
config.run {
for (i in 0 until it) {
when (val attr = a.getIndex(i)) {
R.styleable.journeyKeyboardView_kkbDeleteDrawable -> deleteDrawable =
a.getDrawable(attr)
R.styleable.journeyKeyboardView_kkbCapitalDrawable -> capitalDrawable =
a.getDrawable(attr)
R.styleable.journeyKeyboardView_kkbCapitalLockDrawable -> capitalLockDrawable =
a.getDrawable(attr)
R.styleable.journeyKeyboardView_android_labelTextSize -> labelTextSize =
a.getDimensionPixelSize(attr, labelTextSize)
R.styleable.journeyKeyboardView_android_keyTextSize -> keyTextSize =
a.getDimensionPixelSize(attr, keyTextSize)
R.styleable.journeyKeyboardView_android_keyTextColor -> keyTextColor =
a.getColor(attr, keyTextColor)
R.styleable.journeyKeyboardView_kkbKeyIconColor -> keyIconColor = a.getColor(
attr,
ContextCompat.getColor(context, R.color.journey_keyboard_key_icon_color)
)
R.styleable.journeyKeyboardView_kkbKeySpecialTextColor -> keySpecialTextColor =
a.getColor(attr, keySpecialTextColor)
R.styleable.journeyKeyboardView_kkbKeyDoneTextColor -> keyDoneTextColor =
a.getColor(attr, keyDoneTextColor)
R.styleable.journeyKeyboardView_kkbKeyNoneTextColor -> keyNoneTextColor =
a.getColor(attr, keyNoneTextColor)
R.styleable.journeyKeyboardView_android_keyBackground -> keyBackground =
a.getDrawable(attr)
R.styleable.journeyKeyboardView_kkbSpecialKeyBackground -> specialKeyBackground =
a.getDrawable(attr)
R.styleable.journeyKeyboardView_kkbDoneKeyBackground -> doneKeyBackground =
a.getDrawable(attr)
R.styleable.journeyKeyboardView_kkbNoneKeyBackground -> noneKeyBackground =
a.getDrawable(attr)
R.styleable.journeyKeyboardView_kkbKeyDoneTextSize -> keyDoneTextSize =
a.getDimensionPixelSize(attr, keyDoneTextSize)
R.styleable.journeyKeyboardView_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<Keyboard.Key>?) {
keys?.let {
for (key in it) {
drawKey(canvas, key)
}
}
}
/**
* 绘制键盘按键
*/
private fun drawKey(canvas: Canvas, key: Keyboard.Key) {
when (key.codes[0]) {
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: Keyboard.Key) {
config.keyDoneText?.let {
key.label = it
}
drawKey(canvas, key, config.toggleKeyBackground, config.toggleKeyTextColor, null, true)
}
private fun drawDoneKey(canvas: Canvas, key: Keyboard.Key) {
config.keyDoneText?.let {
key.label = it
}
drawKey(canvas, key, config.toggleKeyBackground, config.toggleKeyTextColor, null, true)
}
private fun drawNoneKey(canvas: Canvas, key: Keyboard.Key) {
drawKey(canvas, key, config.noneKeyBackground, config.keyNoneTextColor)
}
private fun drawDeleteKey(canvas: Canvas, key: Keyboard.Key) {
drawKey(
canvas,
key,
config.toggleKeyBackground,
config.keySpecialTextColor,
config.deleteDrawable
)
}
private fun drawShiftKey(canvas: Canvas, key: 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: 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为journeyKeyboard的配置类方便统一管理配置信息
*/
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.journey_keyboard_label_text_size)
var keyTextSize = context.resources.getDimensionPixelSize(R.dimen.journey_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.journey_keyboard_done_text_size)
var keyDoneText: CharSequence? = context.getString(R.string.journey_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,899 @@
package com.keyboard.journey.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.journey.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#journey_Keyboard_keyWidth
* @attr ref android.R.styleable#journey_Keyboard_keyHeight
* @attr ref android.R.styleable#journey_Keyboard_horizontalGap
* @attr ref android.R.styleable#journey_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#journey_Keyboard_keyWidth
* @attr ref android.R.styleable#journey_Keyboard_keyHeight
* @attr ref android.R.styleable#journey_Keyboard_horizontalGap
* @attr ref android.R.styleable#journey_Keyboard_verticalGap
* @attr ref android.R.styleable#journey_Keyboard_Row_rowEdgeFlags
* @attr ref android.R.styleable#journey_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.journey_Keyboard);
defaultWidth = getDimensionOrFraction(a,
R.styleable.journey_Keyboard_android_keyWidth,
parent.mDisplayWidth, parent.mDefaultWidth);
defaultHeight = getDimensionOrFraction(a,
R.styleable.journey_Keyboard_android_keyHeight,
parent.mDisplayHeight, parent.mDefaultHeight);
defaultHorizontalGap = getDimensionOrFraction(a,
R.styleable.journey_Keyboard_android_horizontalGap,
parent.mDisplayWidth, parent.mDefaultHorizontalGap);
verticalGap = getDimensionOrFraction(a,
R.styleable.journey_Keyboard_android_verticalGap,
parent.mDisplayHeight, parent.mDefaultVerticalGap);
a.recycle();
a = res.obtainAttributes(Xml.asAttributeSet(parser),
R.styleable.journey_Keyboard_Row);
rowEdgeFlags = a.getInt(R.styleable.journey_Keyboard_Row_android_rowEdgeFlags, 0);
mode = a.getResourceId(R.styleable.journey_Keyboard_Row_android_keyboardMode,
0);
}
}
/**
* Class for describing the position and characteristics of a single key in the keyboard.
*
* @attr ref android.R.styleable#journey_Keyboard_keyWidth
* @attr ref android.R.styleable#journey_Keyboard_keyHeight
* @attr ref android.R.styleable#journey_Keyboard_horizontalGap
* @attr ref android.R.styleable#journey_Keyboard_Key_codes
* @attr ref android.R.styleable#journey_Keyboard_Key_keyIcon
* @attr ref android.R.styleable#journey_Keyboard_Key_keyLabel
* @attr ref android.R.styleable#journey_Keyboard_Key_iconPreview
* @attr ref android.R.styleable#journey_Keyboard_Key_isSticky
* @attr ref android.R.styleable#journey_Keyboard_Key_isRepeatable
* @attr ref android.R.styleable#journey_Keyboard_Key_isModifier
* @attr ref android.R.styleable#journey_Keyboard_Key_popupKeyboard
* @attr ref android.R.styleable#journey_Keyboard_Key_popupCharacters
* @attr ref android.R.styleable#journey_Keyboard_Key_keyOutputText
* @attr ref android.R.styleable#journey_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.journey_Keyboard);
width = getDimensionOrFraction(a,
R.styleable.journey_Keyboard_android_keyWidth,
keyboard.mDisplayWidth, parent.defaultWidth);
height = getDimensionOrFraction(a,
R.styleable.journey_Keyboard_android_keyHeight,
keyboard.mDisplayHeight, parent.defaultHeight);
gap = getDimensionOrFraction(a,
R.styleable.journey_Keyboard_android_horizontalGap,
keyboard.mDisplayWidth, parent.defaultHorizontalGap);
a.recycle();
a = res.obtainAttributes(Xml.asAttributeSet(parser),
R.styleable.journey_Keyboard_Key);
this.x += gap;
TypedValue codesValue = new TypedValue();
a.getValue(R.styleable.journey_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.journey_Keyboard_Key_android_iconPreview);
if (iconPreview != null) {
iconPreview.setBounds(0, 0, iconPreview.getIntrinsicWidth(),
iconPreview.getIntrinsicHeight());
}
popupCharacters = a.getText(
R.styleable.journey_Keyboard_Key_android_popupCharacters);
popupResId = a.getResourceId(
R.styleable.journey_Keyboard_Key_android_popupKeyboard, 0);
repeatable = a.getBoolean(
R.styleable.journey_Keyboard_Key_android_isRepeatable, false);
modifier = a.getBoolean(
R.styleable.journey_Keyboard_Key_android_isModifier, false);
sticky = a.getBoolean(
R.styleable.journey_Keyboard_Key_android_isSticky, false);
edgeFlags = a.getInt(R.styleable.journey_Keyboard_Key_android_keyEdgeFlags, 0);
edgeFlags |= parent.rowEdgeFlags;
icon = a.getDrawable(
R.styleable.journey_Keyboard_Key_android_keyIcon);
if (icon != null) {
icon.setBounds(0, 0, icon.getIntrinsicWidth(), icon.getIntrinsicHeight());
}
label = a.getText(R.styleable.journey_Keyboard_Key_android_keyLabel);
text = a.getText(R.styleable.journey_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.journey_Keyboard);
mDefaultWidth = getDimensionOrFraction(a,
R.styleable.journey_Keyboard_android_keyWidth,
mDisplayWidth, mDisplayWidth / 10);
mDefaultHeight = getDimensionOrFraction(a,
R.styleable.journey_Keyboard_android_keyHeight,
mDisplayHeight, 50);
mDefaultHorizontalGap = getDimensionOrFraction(a,
R.styleable.journey_Keyboard_android_horizontalGap,
mDisplayWidth, 0);
mDefaultVerticalGap = getDimensionOrFraction(a,
R.styleable.journey_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="#000000"
android:strokeLineCap="round"/>
<path
android:pathData="M1.125,6H10.748"
android:strokeLineJoin="round"
android:strokeWidth="1.5"
android:fillColor="#00000000"
android:strokeColor="#000000"
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,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="#000000"
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="#000000"
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="#000000"
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="#000000"
android:strokeLineCap="round"/>
<path
android:pathData="M12.5,10.098V16.867"
android:strokeLineJoin="round"
android:strokeWidth="2"
android:fillColor="#00000000"
android:strokeColor="#000000"
android:strokeLineCap="round"/>
</vector>

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="@color/white" />
<corners
android:topLeftRadius="24dp"
android:topRightRadius="24dp" />
</shape>

View File

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="oval">
<solid android:color="#EEEEEE"/>
</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="80dp"/>
<solid android:color="#eeeeee"/>
</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,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="#000000"
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="#000000"
android:fillType="evenOdd"/>
</vector>

View File

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

View File

@ -0,0 +1,6 @@
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<!-- 按键正常状态 -->
<item android:drawable="@drawable/key_normal" android:state_pressed="false" />
<!-- 按键按下状态 -->
<item android:drawable="@drawable/key_pressed" android:state_pressed="true" />
</selector>

View File

@ -0,0 +1,6 @@
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<!-- 定义按键正常状态的背景颜色和圆角 -->
<solid android:color="#CCCCCC" />
<corners android:radius="8dp" />
</shape>

View File

@ -0,0 +1,6 @@
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<!-- 定义按键按下状态的背景颜色和圆角 -->
<solid android:color="#999999" />
<corners android:radius="8dp" />
</shape>

View File

@ -0,0 +1,24 @@
<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="M6,6H18"
android:strokeWidth="2.5"
android:fillColor="#00000000"
android:strokeColor="#000000"
android:strokeLineCap="round"/>
<path
android:pathData="M6,12H18"
android:strokeWidth="2.5"
android:fillColor="#00000000"
android:strokeColor="#000000"
android:strokeLineCap="round"/>
<path
android:pathData="M6,18H18"
android:strokeWidth="2.5"
android:fillColor="#00000000"
android:strokeColor="#000000"
android:strokeLineCap="round"/>
</vector>

View File

@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="32dp"
android:height="32dp"
android:viewportWidth="32"
android:viewportHeight="32">
<path
android:pathData="M15.999,7.244H2.594C4.049,5.018 6.037,3.19 8.378,1.925C10.718,0.66 13.338,-0.002 15.999,0C18.66,-0.002 21.279,0.66 23.62,1.925C25.961,3.19 27.949,5.018 29.404,7.244H15.999ZM15.999,14.468C15.839,14.789 15.664,15.103 15.475,15.409C13.963,17.859 11.367,19.961 7.297,19.961C4.122,19.961 1.812,18.677 0.224,16.899C0.148,16.813 0.073,16.725 0,16.636C0.164,20.766 1.921,24.672 4.903,27.536C7.886,30.4 11.862,32 15.999,32C20.136,32 24.112,30.401 27.095,27.538C30.078,24.674 31.836,20.768 32,16.639C31.927,16.726 31.852,16.812 31.776,16.897C30.188,18.677 27.878,19.961 24.703,19.961C20.631,19.961 18.037,17.859 16.523,15.409C16.334,15.103 16.159,14.789 15.999,14.468ZM21.688,22.381C21.574,22.283 21.441,22.209 21.299,22.162C21.156,22.115 21.005,22.097 20.855,22.108C20.705,22.12 20.559,22.161 20.424,22.229C20.29,22.297 20.171,22.391 20.073,22.505C19.603,23.043 19.008,23.457 18.339,23.709C18.124,23.766 17.904,23.801 17.685,23.816C17.382,23.838 17.1,23.978 16.901,24.207C16.702,24.436 16.602,24.734 16.623,25.037C16.645,25.339 16.785,25.621 17.014,25.82C17.243,26.019 17.542,26.118 17.845,26.097C18.264,26.067 18.678,25.992 19.08,25.873C20.135,25.489 21.074,24.842 21.809,23.995C21.907,23.881 21.982,23.749 22.029,23.606C22.076,23.463 22.095,23.313 22.084,23.163C22.072,23.013 22.032,22.867 21.964,22.733C21.896,22.599 21.802,22.479 21.688,22.381ZM0.35,10.311C0.308,10.351 0.278,10.403 0.262,10.459C0.246,10.515 0.246,10.574 0.261,10.631C0.581,12.096 1.238,13.743 2.358,14.996C3.44,16.207 4.989,17.105 7.297,17.105C10.191,17.105 11.945,15.688 13.043,13.91C13.743,12.739 14.215,11.446 14.436,10.101H0.881C0.783,10.098 0.685,10.115 0.594,10.151C0.503,10.188 0.42,10.242 0.35,10.311ZM18.957,13.91C18.256,12.739 17.783,11.447 17.561,10.101H31.117C31.364,10.101 31.552,10.201 31.65,10.311C31.692,10.351 31.722,10.403 31.738,10.459C31.754,10.515 31.754,10.574 31.739,10.631C31.419,12.096 30.763,13.743 29.642,14.996C28.56,16.207 27.009,17.105 24.701,17.105C21.809,17.105 20.055,15.688 18.957,13.91Z"
android:fillColor="#DDDDDD"
android:fillType="evenOdd"/>
</vector>

View File

@ -0,0 +1,33 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<androidx.cardview.widget.CardView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="6dp"
app:cardBackgroundColor="#eeeeee"
app:cardCornerRadius="8dp">
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:minHeight="120dp">
<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerInParent="true"
android:src="@drawable/placeholder_icon" />
<ImageView
android:id="@+id/image_view"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
</RelativeLayout>
</androidx.cardview.widget.CardView>
</LinearLayout>

View File

@ -0,0 +1,32 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent">
<VideoView
android:id="@+id/videoView"
android:layout_width="match_parent"
android:layout_height="0dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<com.keyboard.journey.view.JourneyKeyboardView
android:id="@+id/keyboardView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:animateLayoutChanges="true"
android:focusable="true"
android:focusableInTouchMode="true"
android:keyBackground="@drawable/key_background"
android:keyTextColor="#ff333333"
android:keyTextSize="20sp"
android:labelTextSize="16sp"
android:paddingTop="6dp"
android:paddingBottom="6dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -0,0 +1,31 @@
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_margin="4dp">
<androidx.cardview.widget.CardView
android:layout_width="wrap_content"
app:cardBackgroundColor="#eeeeee"
app:cardCornerRadius="8dp"
android:layout_height="wrap_content">
<RelativeLayout
android:layout_width="136dp"
android:layout_height="100dp">
<ImageView
android:layout_centerInParent="true"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="@drawable/placeholder_icon"/>
<ImageView
android:id="@+id/hs_rv_img"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</RelativeLayout>
</androidx.cardview.widget.CardView>
</RelativeLayout>

View File

@ -0,0 +1,417 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.drawerlayout.widget.DrawerLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/drawer"
android:layout_width="match_parent"
android:layout_height="match_parent">
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<View
android:id="@+id/view"
android:layout_width="wrap_content"
android:layout_height="0dp" />
<RelativeLayout
android:id="@+id/banner_layout"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<com.youth.banner.Banner
android:id="@+id/banner"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@mipmap/recommend_top_banner_img" />
<RelativeLayout
android:id="@+id/banner_btn"
android:layout_width="match_parent"
android:layout_height="48dp"
android:layout_below="@+id/banner"
android:layout_marginTop="-24dp">
<LinearLayout
android:id="@+id/indicator_layout"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/banner_indicator_one_bg"
android:gravity="center_vertical"
android:orientation="horizontal">
<ImageView
android:id="@+id/indicator_img"
android:layout_width="24dp"
android:layout_height="24dp"
android:layout_marginStart="16dp"
android:src="@mipmap/top_banner_one_img" />
<TextView
android:id="@+id/indicator_txt"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_marginStart="8dp"
android:layout_weight="1"
android:gravity="center_vertical"
android:text="@string/recommend"
android:textColor="@color/black"
android:textSize="16dp"
android:textStyle="bold" />
<androidx.cardview.widget.CardView
android:id="@+id/indicator_go_bg"
android:layout_width="wrap_content"
android:layout_height="26dp"
android:layout_marginEnd="16dp"
app:cardBackgroundColor="@color/banner_indicator_one_go_bg_color"
app:cardCornerRadius="80dp">
<TextView
android:id="@+id/indicator_go_txt"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:paddingStart="12dp"
android:paddingEnd="12dp"
android:text="@string/go"
android:textColor="@color/white"
android:textSize="14sp"
android:textStyle="bold" />
</androidx.cardview.widget.CardView>
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="4dp"
android:layout_alignParentBottom="true"
android:orientation="horizontal">
<TextView
android:id="@+id/indicator_line_one_bg"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_marginEnd="2dp"
android:layout_weight="1"
android:background="#7863F9" />
<TextView
android:id="@+id/indicator_line_tow_bg"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_marginStart="2dp"
android:layout_weight="1"
android:background="#1A000000" />
</LinearLayout>
</RelativeLayout>
</RelativeLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="48dp"
android:layout_below="@+id/view"
android:gravity="center_vertical"
android:orientation="horizontal">
<androidx.cardview.widget.CardView
android:id="@+id/menu_btn"
android:layout_width="32dp"
android:layout_height="32dp"
android:layout_marginStart="16dp"
app:cardBackgroundColor="#F6F4FF"
app:cardCornerRadius="32dp"
app:cardElevation="6dp">
<ImageView
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:layout_gravity="center"
android:src="@drawable/menu" />
</androidx.cardview.widget.CardView>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginEnd="48dp"
android:gravity="center_horizontal">
<androidx.cardview.widget.CardView
android:layout_width="wrap_content"
android:layout_height="32dp"
android:layout_marginStart="16dp"
app:cardBackgroundColor="#F6F4FF"
app:cardCornerRadius="32dp"
app:cardElevation="6dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:layout_gravity="center"
android:gravity="center"
android:paddingStart="16dp"
android:paddingEnd="16dp"
android:text="@string/app_name"
android:textColor="@color/black"
android:textSize="14dp"
android:textStyle="bold" />
</androidx.cardview.widget.CardView>
</LinearLayout>
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_below="@+id/banner_layout"
android:orientation="vertical">
<LinearLayout
android:id="@+id/loading_layout"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center"
android:orientation="vertical"
android:visibility="gone">
<org.libpag.PAGView
android:id="@+id/pag_view"
android:layout_width="88dp"
android:layout_height="100dp" />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center"
android:text="loading"
android:textColor="#999999"
android:textSize="14sp" />
</LinearLayout>
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/rv"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:listitem="@layout/main_rv_item" />
</LinearLayout>
</RelativeLayout>
<!--android:layout_gravity="start"-->
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_gravity="start"
android:background="#F6F4FF"
android:orientation="vertical">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:paddingStart="16dp"
android:paddingTop="48dp"
android:paddingEnd="16dp"
android:paddingBottom="10dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center_vertical"
android:orientation="horizontal"
android:paddingStart="16dp"
android:paddingTop="10dp"
android:paddingEnd="16dp"
android:paddingBottom="10dp">
<androidx.cardview.widget.CardView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:cardCornerRadius="6dp">
<ImageView
android:layout_width="48dp"
android:layout_height="48dp"
android:src="@mipmap/journey_app_logo" />
</androidx.cardview.widget.CardView>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:text="@string/app_name"
android:textColor="@color/black"
android:textSize="16dp"
android:textStyle="bold" />
</LinearLayout>
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<LinearLayout
android:id="@+id/apply_keyboard_btn"
android:layout_width="match_parent"
android:layout_height="56dp"
android:layout_marginTop="16dp"
android:gravity="center_vertical"
android:orientation="horizontal">
<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:src="@mipmap/keyboard_img" />
<TextView
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_marginStart="16dp"
android:layout_weight="1"
android:gravity="center_vertical"
android:text="Keyboard settings"
android:textColor="@color/black"
android:textStyle="bold" />
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<LinearLayout
android:id="@+id/share_btn"
android:layout_width="match_parent"
android:layout_height="56dp"
android:gravity="center_vertical"
android:orientation="horizontal">
<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:src="@mipmap/shate_img" />
<TextView
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_marginStart="16dp"
android:layout_weight="1"
android:gravity="center_vertical"
android:text="Share"
android:textColor="@color/black"
android:textStyle="bold" />
</LinearLayout>
<LinearLayout
android:id="@+id/privacy_btn"
android:layout_width="match_parent"
android:layout_height="56dp"
android:gravity="center_vertical"
android:orientation="horizontal">
<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:src="@mipmap/privacy_img" />
<TextView
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_marginStart="16dp"
android:layout_weight="1"
android:gravity="center_vertical"
android:text="Privacy"
android:textColor="@color/black"
android:textStyle="bold" />
</LinearLayout>
</LinearLayout>
</LinearLayout>
</RelativeLayout>
</LinearLayout>
<RelativeLayout
android:id="@+id/dialog_step_layout"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#99000000"
android:visibility="gone">
<LinearLayout
android:id="@+id/activate_layout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_alignParentBottom="true"
android:background="@drawable/draw_activate_layout_bg"
android:orientation="vertical"
android:visibility="visible">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="16dp"
android:gravity="center"
android:text="Activate Keyboard Journey"
android:textColor="@color/black"
android:textSize="14dp"
android:textStyle="bold" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="48dp"
android:layout_margin="16dp">
<TextView
android:id="@+id/step_1_btn"
android:layout_width="0dp"
android:layout_height="46dp"
android:layout_marginEnd="4dp"
android:layout_weight="1"
android:background="@drawable/drw_gray_select_bg"
android:gravity="center"
android:text="Step 1:Select"
android:textColor="#999999"
android:textSize="14sp"
android:textStyle="bold" />
<TextView
android:id="@+id/step_2_btn"
android:layout_width="0dp"
android:layout_height="46dp"
android:layout_marginStart="4dp"
android:layout_weight="1"
android:background="@mipmap/activate_btn_bg"
android:gravity="center"
android:text="Step 2:Enabled"
android:textColor="@color/black"
android:textSize="14sp"
android:textStyle="bold" />
</LinearLayout>
</LinearLayout>
</RelativeLayout>
</RelativeLayout>
</androidx.drawerlayout.widget.DrawerLayout>

View File

@ -0,0 +1,97 @@
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<ImageView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:scaleType="centerCrop"
android:src="@mipmap/details_act_bg" />
<View
android:id="@+id/view"
android:layout_width="wrap_content"
android:layout_height="0dp" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_below="@+id/view"
android:orientation="vertical">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="42dp"
android:gravity="center_vertical"
android:orientation="horizontal">
<LinearLayout
android:id="@+id/back_btn"
android:layout_width="42dp"
android:layout_height="match_parent"
android:layout_marginStart="8dp"
android:gravity="center">
<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="@drawable/back_black_icon" />
</LinearLayout>
<TextView
android:id="@+id/title_tv"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:text="@string/app_name"
android:textColor="@color/black"
android:textSize="16dp"
android:textStyle="bold" />
</LinearLayout>
<LinearLayout
android:id="@+id/loading_layout"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center"
android:orientation="vertical"
android:visibility="gone">
<org.libpag.PAGView
android:id="@+id/pag_view"
android:layout_width="88dp"
android:layout_height="100dp" />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center"
android:textSize="14sp"
android:textColor="#999999"
android:text="loading"/>
</LinearLayout>
<com.scwang.smart.refresh.layout.SmartRefreshLayout
android:id="@+id/refreshLayout"
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/rv"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_margin="6dp"
android:overScrollMode="never"
android:scrollbars="none" />
<com.scwang.smart.refresh.footer.ClassicsFooter
android:layout_width="match_parent"
android:layout_height="wrap_content" />
</com.scwang.smart.refresh.layout.SmartRefreshLayout>
</LinearLayout>
</RelativeLayout>

View File

@ -0,0 +1,149 @@
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<ImageView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:scaleType="centerCrop"
android:src="@mipmap/details_act_bg" />
<View
android:id="@+id/view"
android:layout_width="wrap_content"
android:layout_height="0dp" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_below="@+id/view"
android:orientation="vertical">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="42dp"
android:gravity="center_vertical"
android:orientation="horizontal">
<LinearLayout
android:id="@+id/back_btn"
android:layout_width="42dp"
android:layout_height="match_parent"
android:layout_marginStart="8dp"
android:gravity="center">
<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="@drawable/back_black_icon" />
</LinearLayout>
<TextView
android:id="@+id/title_tv"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:text="@string/app_name"
android:textColor="@color/black"
android:textSize="16dp"
android:textStyle="bold" />
</LinearLayout>
<LinearLayout
android:id="@+id/loading_layout"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center"
android:orientation="vertical"
android:visibility="gone">
<org.libpag.PAGView
android:id="@+id/pag_view"
android:layout_width="88dp"
android:layout_height="100dp" />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center"
android:textSize="14sp"
android:textColor="#999999"
android:text="loading"/>
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:orientation="vertical"
android:layout_height="match_parent">
<LinearLayout
android:layout_width="match_parent"
android:layout_marginStart="16dp"
android:layout_marginEnd="16dp"
android:layout_marginBottom="24dp"
android:layout_height="wrap_content"
android:orientation="vertical">
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="280dp">
<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerInParent="true"
android:src="@drawable/placeholder_icon" />
<ImageView
android:id="@+id/image_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_centerInParent="true" />
</RelativeLayout>
<LinearLayout
android:id="@+id/down_btn"
android:layout_width="match_parent"
android:layout_height="48dp"
android:background="@mipmap/down_btn_bg"
android:gravity="center"
android:orientation="horizontal">
<RelativeLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content">
<ProgressBar
android:id="@+id/loading_bar"
android:layout_width="25dp"
android:layout_height="25dp"
android:visibility="gone" />
<ImageView
android:id="@+id/down_icon"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="@drawable/down_icon" />
</RelativeLayout>
<TextView
android:id="@+id/down_tv"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="12dp"
android:text="@string/download"
android:textColor="@color/black"
android:textSize="16dp"
android:textStyle="bold" />
</LinearLayout>
</LinearLayout>
</LinearLayout>
</LinearLayout>
</RelativeLayout>

View File

@ -0,0 +1,230 @@
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#F8F8F8">
<LinearLayout
android:id="@+id/title_layout"
android:layout_width="match_parent"
android:layout_height="46dp"
android:layout_alignParentTop="true"
android:background="@color/white"
android:gravity="center_vertical"
android:orientation="horizontal">
<LinearLayout
android:id="@+id/back_btn"
android:layout_width="42dp"
android:layout_height="match_parent"
android:layout_marginStart="8dp"
android:gravity="center">
<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="@drawable/back_black_icon" />
</LinearLayout>
<TextView
android:id="@+id/title_tv"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:gravity="center"
android:text="@string/preview"
android:textColor="@color/black"
android:textSize="16dp"
android:textStyle="bold" />
<LinearLayout
android:layout_width="42dp"
android:layout_height="match_parent"
android:layout_marginEnd="8dp"
android:gravity="center"
android:visibility="invisible">
<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="@drawable/back_black_icon" />
</LinearLayout>
</LinearLayout>
<ImageView
android:id="@+id/preview_iv"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_below="@+id/title_layout"
android:scaleType="centerCrop" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="32dp"
android:layout_marginTop="88dp"
android:layout_marginEnd="88dp"
android:background="@drawable/drw_preview_message_bg">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:padding="12dp"
android:text="Wow! This keyboard background is so cool!"
android:textSize="14sp" />
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_alignParentBottom="true"
android:gravity="bottom"
android:orientation="vertical">
<RelativeLayout
android:id="@+id/edit_layout"
android:layout_width="match_parent"
android:layout_height="64dp"
android:layout_marginBottom="16dp"
android:background="@color/white"
android:orientation="vertical">
<TextView
android:id="@+id/tv"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginStart="16dp"
android:layout_marginTop="8dp"
android:layout_marginEnd="16dp"
android:layout_marginBottom="8dp"
android:gravity="center_vertical"
android:hint="Activate to enter text..."
android:textSize="14sp" />
<EditText
android:id="@+id/edit"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginStart="16dp"
android:layout_marginTop="8dp"
android:layout_marginEnd="16dp"
android:layout_marginBottom="8dp"
android:background="@null"
android:gravity="center_vertical"
android:hint="Activate to enter text..."
android:textSize="14sp" />
</RelativeLayout>
<RelativeLayout
android:id="@+id/unauthorized_layout"
android:layout_width="match_parent"
android:layout_height="200dp">
<View
android:id="@+id/keyboard_iv"
android:layout_width="match_parent"
android:layout_height="match_parent" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#99000000"
android:gravity="center">
<LinearLayout
android:id="@+id/activate_btn"
android:layout_width="wrap_content"
android:layout_height="46dp"
android:background="@mipmap/activate_btn_bg"
android:gravity="center"
android:orientation="horizontal">
<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="@drawable/gou_icon" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:gravity="center"
android:text="Activate"
android:textColor="@color/black"
android:textSize="16dp"
android:textStyle="bold" />
</LinearLayout>
</LinearLayout>
</RelativeLayout>
</LinearLayout>
<RelativeLayout
android:id="@+id/dialog_step_layout"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_below="@+id/title_layout"
android:background="#99000000"
android:visibility="gone">
<LinearLayout
android:id="@+id/activate_layout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_alignParentBottom="true"
android:background="@drawable/draw_activate_layout_bg"
android:orientation="vertical"
android:visibility="visible">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="16dp"
android:gravity="center"
android:text="Activate Keyboard Journey"
android:textColor="@color/black"
android:textSize="14dp"
android:textStyle="bold" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="48dp"
android:layout_margin="16dp">
<TextView
android:id="@+id/step_1_btn"
android:layout_width="0dp"
android:layout_height="46dp"
android:layout_marginEnd="4dp"
android:layout_weight="1"
android:background="@drawable/drw_gray_select_bg"
android:gravity="center"
android:text="Step 1:Select"
android:textColor="#999999"
android:textSize="14sp"
android:textStyle="bold" />
<TextView
android:id="@+id/step_2_btn"
android:layout_width="0dp"
android:layout_height="46dp"
android:layout_marginStart="4dp"
android:layout_weight="1"
android:background="@mipmap/activate_btn_bg"
android:gravity="center"
android:text="Step 2:Enabled"
android:textColor="@color/black"
android:textSize="14sp"
android:textStyle="bold" />
</LinearLayout>
</LinearLayout>
</RelativeLayout>
</RelativeLayout>

View File

@ -0,0 +1,47 @@
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<ImageView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:scaleType="centerCrop"
android:src="@mipmap/start_act_bg" />
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerInParent="true"
android:gravity="center"
android:orientation="vertical">
<ImageView
android:layout_width="72dp"
android:layout_height="72dp"
android:src="@mipmap/journey_app_round_logo" />
<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="20dp"
android:src="@mipmap/app_name_icon" />
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_alignParentBottom="true"
android:layout_marginBottom="32dp"
android:gravity="center">
<org.libpag.PAGView
android:id="@+id/pag_view"
android:layout_width="wrap_content"
android:layout_height="68dp"
android:layout_marginEnd="12dp" />
</LinearLayout>
</RelativeLayout>

View File

@ -0,0 +1,85 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<View
android:layout_width="match_parent"
android:layout_height="1dp"
android:background="@color/black_10" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="28dp"
android:layout_marginStart="16dp"
android:layout_marginTop="16dp"
android:layout_marginEnd="16dp"
android:orientation="horizontal">
<TextView
android:id="@+id/titleTextView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/app_name"
android:textColor="@color/black"
android:textSize="16sp"
android:textStyle="bold" />
<View
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1" />
<LinearLayout
android:id="@+id/moreData"
android:layout_width="28dp"
android:layout_height="28dp"
android:background="@drawable/draw_round_gray"
android:gravity="center">
<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="@drawable/arrow_right" />
</LinearLayout>
</LinearLayout>
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<TextView
android:id="@+id/loading_tv"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerInParent="true"
android:text="loading"
android:textColor="#999999"
android:textSize="14sp"
android:visibility="gone" />
<!-- <ImageView-->
<!-- android:id="@+id/img"-->
<!-- android:layout_width="match_parent"-->
<!-- android:layout_margin="12dp"-->
<!-- android:layout_height="100dp"-->
<!-- android:scaleType="centerCrop" />-->
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/item_rv"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="12dp"
android:layout_marginTop="12dp"
android:layout_marginBottom="12dp"
android:orientation="horizontal"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
tools:itemCount="4"
tools:listitem="@layout/horizontal_scrolling_rv_item" />
</RelativeLayout>
</LinearLayout>

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 982 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 56 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 67 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 105 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.8 KiB

Some files were not shown because too many files have changed in this diff Show More