This commit is contained in:
litingting 2024-10-10 10:01:10 +08:00
commit 2c88732787
885 changed files with 63294 additions and 0 deletions

89
.gitignore vendored Normal file
View File

@ -0,0 +1,89 @@
# Built application files
*.apk
*.aar
*.ap_
*.aab
# Files for the ART/Dalvik VM
*.dex
# Java class files
*.class
# Generated files
bin/
gen/
out/
# Uncomment the following line in case you need and you don't have the release build type files in your app
# release/
# Gradle files
.gradle/
build/
# Local configuration file (sdk path, etc)
local.properties
# Proguard folder generated by Eclipse
proguard/
# Log Files
*.log
# Android Studio Navigation editor temp files
.navigation/
# Android Studio captures folder
captures/
# IntelliJ
*.iml
.idea/workspace.xml
.idea/tasks.xml
.idea/gradle.xml
.idea/assetWizardSettings.xml
.idea/dictionaries
.idea/libraries
# Android Studio 3 in .gitignore file.
.idea/caches
.idea/modules.xml
# Comment next line if keeping position of elements in Navigation Editor is relevant for you
.idea/navEditor.xml
# Keystore files
# Uncomment the following lines if you do not want to check your keystore files in.
#*.jks
#*.keystore
# External native build folder generated in Android Studio 2.2 and later
.externalNativeBuild
.cxx/
# Google Services (e.g. APIs or Firebase)
# google-services.json
# Freeline
freeline.py
freeline/
freeline_project_description.json
# fastlane
fastlane/report.xml
fastlane/Preview.html
fastlane/screenshots
fastlane/test_output
fastlane/readme.md
# Version control
vcs.xml
# lint
lint/intermediates/
lint/generated/
lint/outputs/
lint/tmp/
# lint/reports/
# Android Profiling
*.hprof
gradle/wrapper/

36
README.en.md Normal file
View File

@ -0,0 +1,36 @@
# 谷歌输入法项目
#### Description
{**When you're done, you can delete the content in this README and update the file with details for others getting started with your repository**}
#### Software Architecture
Software architecture description
#### Installation
1. xxxx
2. xxxx
3. xxxx
#### Instructions
1. xxxx
2. xxxx
3. xxxx
#### Contribution
1. Fork the repository
2. Create Feat_xxx branch
3. Commit your code
4. Create Pull Request
#### Gitee Feature
1. You can use Readme\_XXX.md to support different languages, such as Readme\_en.md, Readme\_zh.md
2. Gitee blog [blog.gitee.com](https://blog.gitee.com)
3. Explore open source project [https://gitee.com/explore](https://gitee.com/explore)
4. The most valuable open source project [GVP](https://gitee.com/gvp)
5. The manual of Gitee [https://gitee.com/help](https://gitee.com/help)
6. The most popular members [https://gitee.com/gitee-stars/](https://gitee.com/gitee-stars/)

39
README.md Normal file
View File

@ -0,0 +1,39 @@
# 谷歌输入法项目
#### 介绍
{**以下是 Gitee 平台说明,您可以替换此简介**
Gitee 是 OSCHINA 推出的基于 Git 的代码托管平台(同时支持 SVN。专为开发者提供稳定、高效、安全的云端软件开发协作平台
无论是个人、团队、或是企业,都能够用 Gitee 实现代码托管、项目管理、协作开发。企业项目请看 [https://gitee.com/enterprises](https://gitee.com/enterprises)}
#### 软件架构
软件架构说明
#### 安装教程
1. xxxx
2. xxxx
3. xxxx
#### 使用说明
1. xxxx
2. xxxx
3. xxxx
#### 参与贡献
1. Fork 本仓库
2. 新建 Feat_xxx 分支
3. 提交代码
4. 新建 Pull Request
#### 特技
1. 使用 Readme\_XXX.md 来支持不同的语言,例如 Readme\_en.md, Readme\_zh.md
2. Gitee 官方博客 [blog.gitee.com](https://blog.gitee.com)
3. 你可以 [https://gitee.com/explore](https://gitee.com/explore) 这个地址来了解 Gitee 上的优秀开源项目
4. [GVP](https://gitee.com/gvp) 全称是 Gitee 最有价值开源项目,是综合评定出的优秀开源项目
5. Gitee 官方提供的使用手册 [https://gitee.com/help](https://gitee.com/help)
6. Gitee 封面人物是一档用来展示 Gitee 会员风采的栏目 [https://gitee.com/gitee-stars/](https://gitee.com/gitee-stars/)

1
app/.gitignore vendored Normal file
View File

@ -0,0 +1 @@
/build

151
app/build.gradle Normal file
View File

@ -0,0 +1,151 @@
plugins {
id 'com.android.application'
id 'org.jetbrains.kotlin.android'
id 'com.google.gms.google-services'
id 'com.google.firebase.crashlytics'
id 'kotlin-kapt'
}
static def releaseTime() {
return new Date().format("MM.dd.HH.mm", TimeZone.getTimeZone("GMT+8"))
}
android {
namespace 'com.colorful.keyboard.theme'
compileSdk 34
defaultConfig {
applicationId "com.colorful.keyboard.theme"
minSdk 21
targetSdk 34
versionCode 3
versionName "1.0.3"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
multiDexEnabled= true
setProperty("archivesBaseName", "ColorfulKeyboard_V" + defaultConfig.versionName + "(${defaultConfig.versionCode})"+"-"+releaseTime())
}
buildTypes {
release {
minifyEnabled true
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
debug {
minifyEnabled true
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
}
kotlinOptions {
jvmTarget = "17"
}
buildFeatures {
viewBinding true
buildConfig = true
}
}
dependencies {
androidTestImplementation 'androidx.test.ext:junit:1.1.5'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1'
implementation 'androidx.appcompat:appcompat:1.6.1'
implementation 'com.google.android.material:material:1.8.0'
implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
testImplementation 'junit:junit:4.13.2'
implementation"androidx.recyclerview:recyclerview:1.3.0"
implementation "androidx.swiperefreshlayout:swiperefreshlayout:1.1.0"
implementation "de.hdodenhof:circleimageview:3.1.0"
implementation"androidx.lifecycle:lifecycle-process:2.6.1"
implementation "com.android.support:multidex:1.0.3"
implementation "io.github.cymchad:BaseRecyclerViewAdapterHelper4:4.1.2"
implementation ("com.github.bumptech.glide:glide:4.11.0")
annotationProcessor ("com.github.bumptech.glide:compiler:4.11.0")
implementation ("com.android.support:multidex:1.0.3")
implementation ("com.squareup.retrofit2:retrofit:2.6.1")
implementation ("com.squareup.retrofit2:converter-gson:2.5.0")
implementation ("com.squareup.retrofit2:adapter-rxjava:2.1.0")
implementation ("io.reactivex.rxjava2:rxandroid:2.1.1")
implementation ("io.reactivex:rxjava:1.2.7")
implementation ("io.reactivex:rxjava:1.2.7")
implementation ("io.reactivex:rxandroid:1.2.0")
implementation("androidx.room:room-ktx:2.6.1")
implementation("androidx.room:room-runtime:2.6.1")
//noinspection KaptUsageInsteadOfKsp
kapt("androidx.room:room-compiler:2.6.1")
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.8.1"
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.8.1"
implementation ("com.squareup.okhttp3:okhttp:4.9.0")
implementation ("com.squareup.okhttp3:logging-interceptor:3.12.2")
implementation 'jp.wasabeef:glide-transformations:4.3.0'
//7z
implementation 'com.github.omicronapps:7-Zip-JBinding-4Android:Release-16.02-2.02'
//
implementation 'com.geyifeng.immersionbar:immersionbar:3.2.2'
implementation 'com.geyifeng.immersionbar:immersionbar-ktx:3.2.2'
// firebase
implementation(platform("com.google.firebase:firebase-bom:33.1.2"))// Import the Firebase BoM
implementation("com.google.firebase:firebase-analytics")
implementation("com.google.firebase:firebase-crashlytics")
implementation("com.google.firebase:firebase-config")
implementation("com.google.firebase:firebase-analytics-ktx")
implementation("com.google.firebase:firebase-crashlytics-ktx")
//Anythink (Necessary)
implementation("com.anythink.sdk:core-tpn:6.3.68")
implementation("com.anythink.sdk:nativead-tpn:6.3.68")
implementation("com.anythink.sdk:banner-tpn:6.3.68")
implementation("com.anythink.sdk:interstitial-tpn:6.3.68")
implementation("com.anythink.sdk:rewardedvideo-tpn:6.3.68")
implementation("com.anythink.sdk:splash-tpn:6.3.68")
//Androidx (Necessary)
implementation("androidx.appcompat:appcompat:1.1.0")
implementation("androidx.browser:browser:1.4.0")
//Vungle
implementation("com.anythink.sdk:adapter-tpn-vungle:6.3.68")
implementation("com.vungle:vungle-ads:7.3.2")
implementation("com.google.android.gms:play-services-basement:18.1.0")
implementation("com.google.android.gms:play-services-ads-identifier:18.0.1")
//UnityAds
implementation("com.anythink.sdk:adapter-tpn-unityads:6.3.68")
implementation("com.unity3d.ads:unity-ads:4.9.3")
//Ironsource
implementation("com.anythink.sdk:adapter-tpn-ironsource:6.3.68")
implementation("com.ironsource.sdk:mediationsdk:8.1.0")
implementation("com.google.android.gms:play-services-appset:16.0.2")
implementation("com.google.android.gms:play-services-ads-identifier:18.0.1")
implementation("com.google.android.gms:play-services-basement:18.1.0")
//Bigo
implementation("com.anythink.sdk:adapter-tpn-bigo:6.3.68")
implementation("com.bigossp:bigo-ads:4.7.4")
//Pangle
implementation("com.anythink.sdk:adapter-tpn-pangle-nonchina:6.3.68.1")
implementation("com.pangle.global:ads-sdk:6.0.0.3")
implementation("com.google.android.gms:play-services-ads-identifier:18.0.1")
//Mintegral
implementation("com.anythink.sdk:adapter-tpn-mintegral-nonchina:6.3.68")
implementation("com.mbridge.msdk.oversea:reward:16.7.51")
implementation("com.mbridge.msdk.oversea:newinterstitial:16.7.51")
implementation("com.mbridge.msdk.oversea:mbnative:16.7.51")
implementation("com.mbridge.msdk.oversea:mbnativeadvanced:16.7.51")
implementation("com.mbridge.msdk.oversea:mbsplash:16.7.51")
implementation("com.mbridge.msdk.oversea:mbbanner:16.7.51")
implementation("com.mbridge.msdk.oversea:mbbid:16.7.51")
implementation("androidx.recyclerview:recyclerview:1.1.0")
}

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

@ -0,0 +1,29 @@
{
"project_info": {
"project_number": "987938744902",
"project_id": "colorful-keyboard-de3d4",
"storage_bucket": "colorful-keyboard-de3d4.appspot.com"
},
"client": [
{
"client_info": {
"mobilesdk_app_id": "1:987938744902:android:d05e8cf3ac15ff540bb847",
"android_client_info": {
"package_name": "com.colorful.keyboard.theme"
}
},
"oauth_client": [],
"api_key": [
{
"current_key": "AIzaSyAS_P0_s948qN8GMPoXnWeWvlD2G_yChCQ"
}
],
"services": {
"appinvite_service": {
"other_platform_oauth_client": []
}
}
}
],
"configuration_version": "1"
}

9
app/key.properties Normal file
View File

@ -0,0 +1,9 @@
# ?????
storePassword=theme123
# key????
keyPassword=theme1
# key??
keyAlias=theme
# key??
storeFile=..\\themes.jks

BIN
app/private_key.pepk Normal file

Binary file not shown.

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

@ -0,0 +1,137 @@
# 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
#---------------------------------TopOn 聚合
# Vungle
-dontwarn com.vungle.ads.**
-keepclassmembers class com.vungle.ads.** {
*;
}
# Google
-keep class com.google.android.gms.** { *; }
-dontwarn com.google.android.gms.**
# START OkHttp + Okio
# JSR 305 annotations are for embedding nullability information.
-dontwarn javax.annotation.**
# A resource is loaded with a relative path so the package of this class must be preserved.
-adaptresourcefilenames okhttp3/internal/publicsuffix/PublicSuffixDatabase.gz
# Animal Sniffer compileOnly dependency to ensure APIs are compatible with older versions of Java.
-dontwarn org.codehaus.mojo.animal_sniffer.*
# OkHttp platform used only on JVM and when Conscrypt and other security providers are available.
-dontwarn okhttp3.internal.platform.**
-dontwarn org.conscrypt.**
-dontwarn org.bouncycastle.**
-dontwarn org.openjsse.**
# Animal Sniffer compileOnly dependency to ensure APIs are compatible with older versions of Java.
-dontwarn org.codehaus.mojo.animal_sniffer.*
# END OkHttp + Okio
# START Protobuf
-dontwarn com.google.protobuf.**
-keepclassmembers class com.google.protobuf.** {
*;
}
-keep class * extends com.google.protobuf.GeneratedMessageLite { *; }
# END Protobuf
-keepattributes Signature
-keepattributes *Annotation*
-keep class com.mbridge.** {*; }
-keep interface com.mbridge.** {*; }
-keep class android.support.v4.** { *; }
-dontwarn com.mbridge.**
-keep class **.R$* { public static final int mbridge*; }
-keep public class com.mbridge.* extends androidx.** { *; }
-keep public class androidx.viewpager.widget.PagerAdapter{ *; }
-keep public class androidx.viewpager.widget.ViewPager.OnPageChangeListener{ *; }
-keep interface androidx.annotation.IntDef{ *; }
-keep interface androidx.annotation.Nullable{ *; }
-keep interface androidx.annotation.CheckResult{ *; }
-keep interface androidx.annotation.NonNull{ *; }
-keep public class androidx.fragment.app.Fragment{ *; }
-keep public class androidx.core.content.FileProvider{ *; }
-keep public class androidx.core.app.NotificationCompat{ *; }
-keep public class androidx.appcompat.widget.AppCompatImageView { *; }
-keep public class androidx.recyclerview.*{ *; }
#---------------------------------TopOn 聚合
-keep class org.libpag.** {*;}
-keep class androidx.exifinterface.** {*;}
-keep class com.omicronapplications.** { *; }
-keep class net.sf.sevenzipjbinding.** { *; }
# Retrofit
-dontnote retrofit2.Platform
-dontnote retrofit2.Platform$IOS$MainThreadExecutor
-dontwarn retrofit2.Platform$Java8
-keepattributes Signature
-keepattributes Exceptions
# okhttp
-dontwarn okio.**
# 保留RxJava相关的类和方法
-keep class rx.** { *; }
-keepclassmembers class rx.** { *; }
# 保留Retrofit相关的类和方法
-keep class retrofit2.** { *; }
-keep class okhttp3.** { *; }
-keep interface retrofit2.** { *; }
-keepclasseswithmembers class * {
@retrofit2.http.* <methods>;
}
# 保留RxJava的CallAdapter
-keep class retrofit2.adapter.rxjava2.** { *; }
# 保留返回Observable的Retrofit接口方法
-keepclassmembers,allowobfuscation interface * {
@retrofit2.http.* <methods>;
rx.Observable *;
}
-keep class com.colorful.keyboard.theme.net.** { *; }
-keep class com.colorful.keyboard.theme.model.** { *; }

View File

@ -0,0 +1,26 @@
package com.colorful.keyboard.theme;
import android.content.Context;
import androidx.test.platform.app.InstrumentationRegistry;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import org.junit.Test;
import org.junit.runner.RunWith;
import static org.junit.Assert.*;
/**
* Instrumented test, which will execute on an Android device.
*
* @see <a href="http://d.android.com/tools/testing">Testing documentation</a>
*/
@RunWith(AndroidJUnit4.class)
public class ExampleInstrumentedTest {
@Test
public void useAppContext() {
// Context of the app under test.
Context appContext = InstrumentationRegistry.getInstrumentation().getTargetContext();
assertEquals("com.colorful.keyboard.theme", appContext.getPackageName());
}
}

View File

@ -0,0 +1,105 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
tools:ignore="DiscouragedApi,LockedOrientationActivity">
<uses-permission android:name="android.permission.VIBRATE" />
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="com.google.android.gms.permission.AD_ID" />
<application
android:name=".App"
android:allowBackup="true"
android:fullBackupOnly="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/english_ime_name"
android:roundIcon="@mipmap/ic_launcher"
android:resizeableActivity="true"
android:maxAspectRatio="2.4"
android:supportsRtl="true"
android:theme="@style/LaunchTheme"
tools:ignore="UnusedAttribute">
<meta-data
android:name="android.max_aspect"
android:value="2.4" />
<!--适配华为huawei刘海屏-->
<meta-data
android:name="android.notch_support"
android:value="true"/>
<!--适配小米xiaomi刘海屏-->
<meta-data
android:name="notch.config"
android:value="portrait|landscape" />
<uses-library android:name="org.apache.http.legacy" android:required="false"/>
<activity
android:name=".LaunchActivity"
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=".MainActivity"
android:exported="true" />
<activity
android:name=".ThemeSettingsActivity"
android:exported="true" />
<activity
android:name="ColorfulDetailsActivity"
android:screenOrientation="portrait" />
<activity
android:name=".PreviewActivity"
android:screenOrientation="portrait" />
<activity
android:name=".SettingActivity"
android:exported="true" />
<activity
android:name=".LocalPrivacyAct"
android:screenOrientation="portrait" />
<activity android:name=".DownloadsActivity"
android:screenOrientation="portrait"/>
<service
android:name=".KeyboardService"
android:exported="true"
android:label="@string/english_ime_name"
android:permission="android.permission.BIND_INPUT_METHOD">
<meta-data
android:name="android.view.im"
android:resource="@xml/key_method" />
<intent-filter>
<action android:name="android.view.InputMethod" />
</intent-filter>
</service>
<receiver
android:name=".latin.SystemBroadcastReceiver"
android:exported="false">
<intent-filter>
<action android:name="android.intent.action.LOCALE_CHANGED" />
</intent-filter>
</receiver>
<provider
android:name="androidx.core.content.FileProvider"
android:authorities="com.colorful.keyboard.theme"
android:exported="false"
android:grantUriPermissions="true"
tools:replace="android:authorities">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/file_path" />
</provider>
</application>
</manifest>

View File

@ -0,0 +1,111 @@
<!DOCTYPE html>
<!-- saved from url=(0041)https://jeasona.bitbucket.io/privacy.html -->
<html><head><meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<meta name="viewport" content="width=device-width">
<title>Privacy Policy</title>
<style> body { font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; padding:1em; } </style>
</head>
<body>
<strong>Privacy Policy</strong>
<p>
</p> <p>
This page is used to inform visitors regarding our
policies with the collection, use, and disclosure of Personal
Information if anyone decided to use our Service.
</p> <p>
If you choose to use our Service, then you agree to
the collection and use of information in relation to this
policy. The Personal Information that We collect is
used for providing and improving the Service. We will not use or share your information with
anyone except as described in this Privacy Policy.
</p> <p>
The terms used in this Privacy Policy have the same meanings
as in our Terms and Conditions, which are accessible at
unless otherwise defined in this Privacy Policy.
</p> <p><strong>Information Collection and Use</strong></p> <p>
For a better experience, while using our Service, We
may require you to provide us with certain personally
identifiable information add whatever else you collect here, e.g. users name, address, location, pictures The information that
We request will be retained on your device and is not collected in any way retained by us and used as described in this privacy policy.
</p> <div><p>
The app does use third-party services that may collect
information used to identify you.
</p> <p>
Link to the privacy policy of third-party service providers used
by the app
</p> <ul><li><a href="https://www.google.com/policies/privacy/" target="_blank" rel="noopener noreferrer">Google Play Services</a></li><li><a href="https://support.google.com/admob/answer/6128543?hl=en" target="_blank" rel="noopener noreferrer">AdMob</a></li><li><a href="https://firebase.google.com/support/privacy" target="_blank" rel="noopener noreferrer">Google Analytics for Firebase</a></li><li><a href="https://firebase.google.com/support/privacy/" target="_blank" rel="noopener noreferrer">Firebase Crashlytics</a></li><!----><!----><!----><!----><li><a href="https://privacy.oath.com/" target="_blank" rel="noopener noreferrer">Flurry Analytics</a></li><!----><!----><li><a href="https://unity3d.com/legal/privacy-policy" target="_blank" rel="noopener noreferrer">Unity</a></li><!----><!----><!----><!----><!----><li><a href="https://www.applovin.com/privacy/" target="_blank" rel="noopener noreferrer">AppLovin</a></li><li><a href="https://vungle.com/privacy/" target="_blank" rel="noopener noreferrer">Vungle</a></li><!----><!----><!----><!----><!----><!----><!----><!----><!----><!----></ul></div> <p><strong>Log Data</strong></p> <p>
We want to inform you that whenever you
use our Service, in a case of an error in the app
We collect data and information (through third-party
products) on your phone called Log Data. This Log Data may
include information such as your device Internet Protocol
(“IP”) address, device name, operating system version, the
configuration of the app when utilizing our Service,
the time and date of your use of the Service, and other
statistics.
</p> <p><strong>Cookies</strong></p> <p>
Cookies are files with a small amount of data that are
commonly used as anonymous unique identifiers. These are sent
to your browser from the websites that you visit and are
stored on your device's internal memory.
</p> <p>
This Service does not use these “cookies” explicitly. However,
the app may use third-party code and libraries that use
“cookies” to collect information and improve their services.
You have the option to either accept or refuse these cookies
and know when a cookie is being sent to your device. If you
choose to refuse our cookies, you may not be able to use some
portions of this Service.
</p> <p><strong>Service Providers</strong></p> <p>
We may employ third-party companies and
individuals due to the following reasons:
</p> <ul><li>To facilitate our Service;</li> <li>To provide the Service on our behalf;</li> <li>To perform Service-related services; or</li> <li>To assist us in analyzing how our Service is used.</li></ul> <p>
We want to inform users of this Service
that these third parties have access to their Personal
Information. The reason is to perform the tasks assigned to
them on our behalf. However, they are obligated not to
disclose or use the information for any other purpose.
</p> <p><strong>Security</strong></p> <p>
We value your trust in providing us your
Personal Information, thus we are striving to use commercially
acceptable means of protecting it. But remember that no method
of transmission over the internet, or method of electronic
storage is 100% secure and reliable, and We cannot
guarantee its absolute security.
</p> <p><strong>Links to Other Sites</strong></p> <p>
This Service may contain links to other sites. If you click on
a third-party link, you will be directed to that site. Note
that these external sites are not operated by us.
Therefore, We strongly advise you to review the
Privacy Policy of these websites. We have
no control over and assume no responsibility for the content,
privacy policies, or practices of any third-party sites or
services.
</p> <p><strong>Childrens Privacy</strong></p> <div><p>
These Services do not address anyone under the age of 13.
We do not knowingly collect personally
identifiable information from children under 13 years of age. In the case
We discover that a child under 13 has provided
us with personal information, We immediately
delete this from our servers. If you are a parent or guardian
and you are aware that your child has provided us with
personal information, please contact us so that
We will be able to do the necessary actions.
</p></div> <!----> <p><strong>Changes to This Privacy Policy</strong></p> <p>
We may update our Privacy Policy from
time to time. Thus, you are advised to review this page
periodically for any changes. We will
notify you of any changes by posting the new Privacy Policy on
this page.
</p> <p>This policy is effective as of 2024-1-8</p> <p><strong>Contact Us</strong></p> <p>
If you have any questions or suggestions about our
Privacy Policy, do not hesitate to contact us at JaysonNazaireMtfWaOIe@gmail.com.
</p>
</body>
</html>

View File

@ -0,0 +1,31 @@
package com.colorful.keyboard.theme
import android.app.Application
import com.anythink.core.api.ATSDK
import com.colorful.keyboard.theme.firebase.RemoteConfig
import com.colorful.keyboard.theme.myutil.Utils
import com.colorful.keyboard.theme.room.DownloadManager
import java.util.concurrent.atomic.AtomicLong
class App : Application() {
companion object {
lateinit var app: App
private set
lateinit var downloadManager: DownloadManager
private set
}
var lastAdDisplayTime: AtomicLong = AtomicLong(0L)//上次广告展示的时间
override fun onCreate() {
super.onCreate()
app = this
Utils.init(this)
downloadManager = DownloadManager.getInstance(this)
RemoteConfig.instance.init(this)
ATSDK.init(this, "h66b1cef2a9a51", "a90d2376d3e1dd2bd9bb5cb4f8296085f")
// ATSDK.integrationChecking(this)
// ATSDK.setNetworkLogDebug(true)
}
}

View File

@ -0,0 +1,166 @@
package com.colorful.keyboard.theme
import android.content.Intent
import android.os.Bundle
import android.view.View
import androidx.appcompat.app.AppCompatActivity
import com.colorful.keyboard.theme.ad.AdsInsUtil
import com.colorful.keyboard.theme.bean.DetailsBean
import com.colorful.keyboard.theme.bean.DownloadBean
import com.colorful.keyboard.theme.databinding.ColorfulDetailsActivityBinding
import com.colorful.keyboard.theme.myutil.AppSharedPreferences
import com.colorful.keyboard.theme.myutil.NetworkCallback
import com.colorful.keyboard.theme.myutil.NetworkUtil
import com.colorful.keyboard.theme.myutil.OnDownloadListener
import com.colorful.keyboard.theme.myutil.ResourceDownloadUtil
import com.colorful.keyboard.theme.myutil.fileIsDownload
import com.colorful.keyboard.theme.myutil.loadRoundedImage
import com.gyf.immersionbar.ktx.immersionBar
import kotlinx.coroutines.DelicateCoroutinesApi
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
@OptIn(DelicateCoroutinesApi::class)
class ColorfulDetailsActivity : AppCompatActivity(), OnDownloadListener {
private var key: String? = null
private var title: String? = null
private lateinit var binding: ColorfulDetailsActivityBinding
private var detailsBean: DetailsBean? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ColorfulDetailsActivityBinding.inflate(layoutInflater)
setContentView(binding.root)
title = intent.getStringExtra("title")
key = intent.getStringExtra("key")
if (key.isNullOrEmpty()) {
finish()
}
initBar()
initView()
initData()
AdsInsUtil.loadAllAdIsNotCached(this)
}
private fun initBar() {
immersionBar {
statusBarDarkFont(true)
statusBarView(binding.view)
}
}
override fun onBackPressed() {
super.onBackPressed()
AdsInsUtil.showAdRandomModeAndTiming(this)
}
private fun initView() {
binding.titleTv.text = title
binding.backBtn.setOnClickListener { onBackPressed() }
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(key!!, object : NetworkCallback<DetailsBean> {
override fun onSuccess(data: DetailsBean) {
detailsBean = data
updateUi(data)
}
override fun onFailure(errorMessage: String) {
runOnUiThread {
if (!isFinishing && !isDestroyed) {
loadingClose()
}
}
}
})
}
private fun updateUi(data: DetailsBean) {
runOnUiThread {
if (!isFinishing && !isDestroyed) {
loadingClose()
updateDownloadBtn(data)
var url = data.themeContent.imgGif
if (url.isEmpty()) {
url = data.themeContent.img
}
loadRoundedImage(this, url, binding.imageView, 40)
}
}
}
private fun loadingPlay() {
binding.loadingLayout.visibility = View.VISIBLE
}
private fun loadingClose() {
binding.loadingLayout.visibility = View.GONE
}
private fun updateDownloadBtn(data: DetailsBean) {
if (fileIsDownload(this, data.themeContent.androidRawZipUrl)) {
if (AppSharedPreferences(this).getCurrentlyThemeUrl() == data.themeContent.androidRawZipUrl) {
binding.downTv.text = getString(R.string.applied)
binding.downBtn.isClickable = false
binding.downBtn.isFocusable = false
binding.downIcon.visibility = View.GONE
} else {
binding.downTv.text = getString(R.string.apply_it)
binding.downBtn.isClickable = true
binding.downBtn.isFocusable = true
binding.downIcon.visibility = View.GONE
}
} else {
binding.downTv.text = getString(R.string.download)
binding.downBtn.isClickable = true
binding.downBtn.isFocusable = true
binding.downIcon.visibility = View.VISIBLE
}
}
override fun onDownloadComplete(isDownloaded: Boolean) {
binding.loadingBar.visibility = View.GONE
binding.downIcon.visibility = View.GONE
updateUi(detailsBean!!)
GlobalScope.launch(Dispatchers.IO) {
var url = detailsBean?.themeContent?.imgGif
if (url.isNullOrEmpty()) {
url = detailsBean?.themeContent?.img
}
val bean = DownloadBean(
title = detailsBean?.title ?: "", key = detailsBean?.key ?: "", imgUrl = url ?: ""
)
App.downloadManager.insertDownloadBean(bean)
}
}
}

View File

@ -0,0 +1,72 @@
package com.colorful.keyboard.theme
import android.content.Intent
import android.os.Bundle
import android.view.View
import androidx.appcompat.app.AppCompatActivity
import androidx.recyclerview.widget.GridLayoutManager
import com.colorful.keyboard.theme.ad.AdsInsUtil
import com.colorful.keyboard.theme.config_adpter.DownloadAdapter
import com.colorful.keyboard.theme.databinding.ColorfulDownloadActivityBinding
import com.gyf.immersionbar.ktx.immersionBar
import kotlinx.coroutines.DelicateCoroutinesApi
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
@OptIn(DelicateCoroutinesApi::class)
class DownloadsActivity : AppCompatActivity() {
private lateinit var binding: ColorfulDownloadActivityBinding
private var downloadAdapter = DownloadAdapter()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ColorfulDownloadActivityBinding.inflate(layoutInflater)
setContentView(binding.root)
AdsInsUtil.showAdRandomModeAndTiming(this)
initBar()
initView()
initData()
AdsInsUtil.loadAllAdIsNotCached(this)
}
private fun initBar() {
immersionBar {
statusBarDarkFont(true)
statusBarView(binding.view)
}
}
private fun initView() {
binding.backBtn.setOnClickListener {
onBackPressed()
}
}
private fun initData() {
binding.loadingLayout.visibility = View.VISIBLE
GlobalScope.launch(Dispatchers.IO) {
val downloads = App.downloadManager.getAllDownloadBeans()
withContext(Dispatchers.Main) {
binding.loadingLayout.visibility = View.GONE
if (downloads.isNotEmpty()) {
binding.noDataLayout.visibility = View.GONE
downloadAdapter.submitList(downloads)
binding.rvPhotoIst.layoutManager = GridLayoutManager(this@DownloadsActivity, 3)
binding.rvPhotoIst.adapter = downloadAdapter
downloadAdapter.setOnItemClickListener { baseQuickAdapter, _, i ->
startActivity(
Intent(this@DownloadsActivity, ColorfulDetailsActivity::class.java)
.putExtra("title", baseQuickAdapter.getItem(i)!!.title)
.putExtra("key", baseQuickAdapter.getItem(i)!!.key)
.putExtra("url", baseQuickAdapter.getItem(i)!!.imgUrl)
)
}
} else {
binding.noDataLayout.visibility = View.VISIBLE
}
}
}
}
}

View File

@ -0,0 +1,359 @@
package com.colorful.keyboard.theme
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.colorful.keyboard.theme.myutil.AppSharedPreferences
import com.colorful.keyboard.theme.myutil.colorsXmlPullParser
import com.colorful.keyboard.theme.myutil.currentlyThemeUFileString
import com.colorful.keyboard.theme.myutil.getBitmapDrawable
import com.colorful.keyboard.theme.myutil.getPath
import com.colorful.keyboard.theme.myutil.getRawVideoPath
import com.colorful.keyboard.theme.myutil.getStateDrawable
import com.colorful.keyboard.theme.view.JourneyKeyboardView
import com.colorful.keyboard.theme.view.Keyboard
import com.colorful.keyboard.theme.view.KeyboardView
import com.colorful.keyboard.theme.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,116 @@
package com.colorful.keyboard.theme
import android.annotation.SuppressLint
import android.content.Intent
import android.os.Bundle
import android.os.Handler
import android.os.Looper
import android.os.Message
import android.util.Log
import androidx.appcompat.app.AppCompatActivity
import com.anythink.core.api.ATAdInfo
import com.anythink.core.api.ATSDK
import com.colorful.keyboard.theme.ad.AdShowFailed
import com.colorful.keyboard.theme.ad.AdsInsUtil
import com.colorful.keyboard.theme.ad.ShowListener
import com.colorful.keyboard.theme.databinding.ActivitySplashBinding
import com.gyf.immersionbar.ktx.immersionBar
class LaunchActivity : AppCompatActivity() {
companion object {
private const val MSG_PROGRESS = 0
private const val MSG_LOAD_AD_LOADED = 1
private const val MSG_LOAD_AD_FAIL = 2
}
private lateinit var binding: ActivitySplashBinding
private val mMaxLoading = 10000L//最大超时
private var progress = 0f
private val delayMillis = mMaxLoading / 100
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivitySplashBinding.inflate(layoutInflater)
setContentView(binding.root)
loadAd()
handler.sendEmptyMessageDelayed(MSG_PROGRESS, delayMillis)
immersionBar {
fullScreen(true)
statusBarDarkFont(true)
}
}
private val handler = object : Handler(Looper.getMainLooper()) {
override fun handleMessage(msg: Message) {
val progressBar = binding.customProgressBar
if (!isFinishing && !isDestroyed) {
when (msg.what) {
MSG_PROGRESS -> {
if (progress >= 100f) {//超时
toMainActivity()
return
}
val cacheAd = AdsInsUtil.canShowAd(AdsInsUtil.Placement.TOP_ON_AD_ONE)
if (cacheAd) {
sendEmptyMessageDelayed(MSG_LOAD_AD_LOADED, delayMillis)
removeMessages(MSG_PROGRESS)
return
}
progress += 1
progressBar.setProgress(progress)
sendEmptyMessageDelayed(MSG_PROGRESS, delayMillis)
}
MSG_LOAD_AD_LOADED -> {
if (progress >= 100f) {
showAd()
removeMessages(MSG_LOAD_AD_LOADED)
return
}
progress += 1
progressBar.setProgress(progress)
sendEmptyMessageDelayed(MSG_LOAD_AD_LOADED, delayMillis)
}
MSG_LOAD_AD_FAIL -> {
sendEmptyMessageDelayed(MSG_LOAD_AD_LOADED, delayMillis)
removeMessages(MSG_LOAD_AD_FAIL)
removeMessages(MSG_PROGRESS)
}
}
}
}
}
private fun toMainActivity() {
startActivity(Intent(this, MainActivity::class.java))
finish()
}
private fun loadAd() {
AdsInsUtil.loadAllAdIsNotCached(this)
}
private fun showAd() {
AdsInsUtil.showAd(this, AdsInsUtil.Placement.TOP_ON_AD_ONE, object : ShowListener {
override fun onAdClosed() {
toMainActivity()
}
override fun onAdShowFailed(error: AdShowFailed?) {
toMainActivity()
}
override fun onAdShown(ad: ATAdInfo?) {
AdsInsUtil.loadAllAdIsNotCached(this@LaunchActivity)
}
})
}
@SuppressLint("MissingSuperCall")
override fun onBackPressed() {
}
}

View File

@ -0,0 +1,121 @@
package com.colorful.keyboard.theme
import android.annotation.SuppressLint
import android.annotation.TargetApi
import android.graphics.Bitmap
import android.os.Build
import android.os.Bundle
import android.view.View
import android.webkit.*
import androidx.appcompat.app.AppCompatActivity
import com.colorful.keyboard.theme.databinding.ActivityWebPrivacyBinding
import com.gyf.immersionbar.ktx.immersionBar
class LocalPrivacyAct : AppCompatActivity() {
private lateinit var binding: ActivityWebPrivacyBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityWebPrivacyBinding.inflate(layoutInflater)
setContentView(binding.root)
initBar()
initView()
}
private fun initBar() {
immersionBar {
statusBarDarkFont(true)
statusBarView(binding.view)
}
}
private fun initView() {
binding.backBtn.setOnClickListener {
finish()
}
initWebView()
loadUrl()
}
private fun loadUrl() {
binding.webView.loadUrl("file:///android_asset/PrivacyPolicy.html")
}
@SuppressLint("SetJavaScriptEnabled")
private fun initWebView() {
// 设置背景色
binding.webView.setBackgroundColor(0);
// 设置填充透明度
binding.webView.background.alpha = 0
binding.webView.settings.javaScriptEnabled = true
binding.webView.settings.domStorageEnabled = true
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
binding.webView.settings.mixedContentMode = 0
}
binding.webView.settings.apply {
javaScriptEnabled = true
loadWithOverviewMode = true
useWideViewPort = true
setSupportZoom(true)
loadWithOverviewMode = true
javaScriptCanOpenWindowsAutomatically = true
}
binding.webView.webChromeClient = object : WebChromeClient() {
override fun onReceivedTitle(view: WebView?, title: String?) {
}
}
binding.webView.webViewClient = object : WebViewClient() {
private var mLoadError = false
override fun shouldOverrideUrlLoading(
view: WebView, request: WebResourceRequest?
): Boolean {
val url = request?.url?.toString()
return overrideUrlLoading(view, url)
}
@Suppress("OverridingDeprecatedMember")
override fun shouldOverrideUrlLoading(view: WebView, url: String?): Boolean {
return overrideUrlLoading(view, url)
}
override fun onPageFinished(view: WebView?, url: String?) {
super.onPageFinished(view, url)
binding.progressBar.visibility = View.GONE
if (mLoadError) {
}
}
override fun onPageStarted(view: WebView?, url: String?, favicon: Bitmap?) {
binding.progressBar.visibility = View.VISIBLE
mLoadError = false
super.onPageStarted(view, url, favicon)
}
@TargetApi(Build.VERSION_CODES.M)
override fun onReceivedError(
view: WebView?, request: WebResourceRequest?, error: WebResourceError?
) {
error(error?.errorCode, error?.description, request?.url?.toString())
super.onReceivedError(view, request, error)
}
@Suppress("OverridingDeprecatedMember", "DEPRECATION")
override fun onReceivedError(
view: WebView?, errorCode: Int, description: String?, failingUrl: String?
) {
error(errorCode, description, failingUrl)
super.onReceivedError(view, errorCode, description, failingUrl)
}
private fun error(code: Int?, desc: CharSequence?, url: String?) {
}
private fun overrideUrlLoading(view: WebView, url: String?): Boolean {
return false
}
}
}
}

View File

@ -0,0 +1,303 @@
package com.colorful.keyboard.theme;
import static com.colorful.keyboard.theme.myutil.AppUtilKt.isMyInputMethodDefault;
import static com.colorful.keyboard.theme.myutil.AppUtilKt.isMyInputMethodEnabled;
import static com.colorful.keyboard.theme.net.BaseInitApiRequest.getNetData;
import static com.colorful.keyboard.theme.net.BaseInitApiRequest.initHHYApiStores;
import android.annotation.SuppressLint;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.graphics.Color;
import android.os.Bundle;
import android.provider.Settings;
import android.text.SpannableString;
import android.text.style.UnderlineSpan;
import android.view.View;
import android.view.inputmethod.InputMethodManager;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.RelativeLayout;
import android.widget.TextView;
import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.appcompat.app.AppCompatActivity;
import androidx.drawerlayout.widget.DrawerLayout;
import androidx.fragment.app.Fragment;
import androidx.viewpager2.widget.ViewPager2;
import com.anythink.core.api.ATSDK;
import com.colorful.keyboard.theme.ad.AdsInsUtil;
import com.colorful.keyboard.theme.adapter.ViewPager2Adapter;
import com.colorful.keyboard.theme.fragment.KeyboardListFragment;
import com.colorful.keyboard.theme.model.ThemeListBean;
import com.colorful.keyboard.theme.myutil.AppUtilKt;
import com.colorful.keyboard.theme.myutil.ConfigBitmapUtils;
import com.colorful.keyboard.theme.myutil.ImmUtils;
import com.colorful.keyboard.theme.myutil.SPUtils;
import com.colorful.keyboard.theme.net.ApiCallbackCommon;
import com.colorful.keyboard.theme.net.ApiException;
import com.google.android.material.tabs.TabLayout;
import com.google.android.material.tabs.TabLayoutMediator;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
public class MainActivity extends AppCompatActivity {
private TabLayout tabLayout;
private ViewPager2 viewPager;
private List<Fragment> fragmentList = new ArrayList<>();
private LinearLayout loadingLayout;
private DrawerLayout drawerLayout;
private LinearLayout navLayout;
private TextView tvVersion;
private RelativeLayout keyboardSettingLayout;
private TextView keyboardNumber;
private RelativeLayout dialog_step_layout;
private TextView step1Btn;
private TextView step2Btn;
private LinearLayout no_data_layout;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
registerReceiver();
// AppLovinSdk.getInstance( this ).showMediationDebugger();
ImmUtils.setStatusBar(this, false, false);
tabLayout = findViewById(R.id.tabLayout);
viewPager = findViewById(R.id.viewPager);
no_data_layout = findViewById(R.id.no_data_layout);
loadingLayout = findViewById(R.id.loadingLayout);
drawerLayout = findViewById(R.id.drawerLayout);
navLayout = findViewById(R.id.nav_view);
ImageView ivSetting = findViewById(R.id.iv_setting);
backToast = Toast.makeText(getBaseContext(), "Press again to exit", Toast.LENGTH_SHORT);
tvVersion = findViewById(R.id.tv_version);
keyboardSettingLayout = findViewById(R.id.keyboard_setting);
dialog_step_layout = findViewById(R.id.dialog_step_layout);
keyboardSettingLayout.setOnClickListener(v -> {
dialog_step_layout.setVisibility(View.VISIBLE);
});
dialog_step_layout.setOnClickListener(v -> {
dialog_step_layout.setVisibility(View.GONE);
});
step1Btn = findViewById(R.id.step_1_btn);
step2Btn = findViewById(R.id.step_2_btn);
keyboardNumber = findViewById(R.id.keyboardNumber);
try {
PackageManager packageManager = getPackageManager();
String packageName = getPackageName();
PackageInfo packageInfo = packageManager.getPackageInfo(packageName, 0);
tvVersion.setText("v" + packageInfo.versionName);
} catch (PackageManager.NameNotFoundException e) {
throw new RuntimeException(e);
}
if (SPUtils.getInstance().getString("bg").isEmpty()) {
ConfigBitmapUtils.saveConfigImgToLocal(this, "https://cdn.kikakeyboard.com/terminal/online/20200331/Us21R82MAD-1585651198.jpg.webp");
}
findViewById(R.id.nav_view).setOnClickListener(v -> {
});
ivSetting.setOnClickListener(view -> {
if (!drawerLayout.isDrawerOpen(findViewById(R.id.nav_view))) {
drawerLayout.openDrawer(findViewById(R.id.nav_view));
}
});
LinearLayout llYinsi = findViewById(R.id.ll_yinsi);
llYinsi.setOnClickListener(view -> {
startActivity(new Intent(MainActivity.this, LocalPrivacyAct.class));
drawerLayout.close();
});
LinearLayout llDownload = findViewById(R.id.ll_download);
llDownload.setOnClickListener(v -> {
drawerLayout.close();
startActivity(new Intent(MainActivity.this, DownloadsActivity.class));
});
LinearLayout ll_share = findViewById(R.id.ll_share);
ll_share.setOnClickListener(v -> {
drawerLayout.close();
AppUtilKt.shareAppInfo(this);
});
step1Btn.setOnClickListener(v -> {
Intent intent = new Intent(Settings.ACTION_INPUT_METHOD_SETTINGS);
startActivity(intent);
});
step2Btn.setOnClickListener(v -> {
InputMethodManager imeManager = (InputMethodManager) getSystemService(INPUT_METHOD_SERVICE);
imeManager.showInputMethodPicker();
});
TextView textView = findViewById(R.id.try_again); // 你的TextView的ID
String text = "Load error, try again!";
// 创建一个SpannableString并应用下划线
SpannableString spannableString = new SpannableString(text);
spannableString.setSpan(new UnderlineSpan(), 0, text.length(), 0);
// 将带有下划线的文本设置给TextView
textView.setText(spannableString);
textView.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
initData();
}
});
initData();
}
private void initData(){
loadingLayout.setVisibility(View.VISIBLE);
no_data_layout.setVisibility(View.GONE);
HashMap<String, Object> hashMap = new HashMap<>();
hashMap.put("offset", 0);
hashMap.put("fetch_size", 100);
hashMap.put("sign", "a28aadc61c76c754944f6ddb48962c9c");
getNetData(initHHYApiStores().getCategories(hashMap), new ApiCallbackCommon<ThemeListBean>(this, false) {
@Override
public void onSuccess(ThemeListBean model) {
loadingLayout.setVisibility(View.GONE);
no_data_layout.setVisibility(View.GONE);
fragmentList.clear();
for (int i = 0; i < model.getData().getSections().size(); i++) {
tabLayout.addTab(tabLayout.newTab());
KeyboardListFragment photoFragment = new KeyboardListFragment();
Bundle bundle = new Bundle();
bundle.putString("id", model.getData().getSections().get(i).getKey());
photoFragment.setArguments(bundle);
fragmentList.add(photoFragment);
}
ViewPager2Adapter homeTalentViewPager2Adapter = new ViewPager2Adapter(MainActivity.this, fragmentList);
viewPager.setAdapter(homeTalentViewPager2Adapter);
viewPager.setCurrentItem(0);
viewPager.setOffscreenPageLimit(2);
TabLayoutMediator mediator = new TabLayoutMediator(tabLayout, viewPager, new TabLayoutMediator.TabConfigurationStrategy() {
@Override
public void onConfigureTab(@NonNull TabLayout.Tab tab, int position) {
String title = model.getData().getSections().get(position).getTitle().trim().toLowerCase();
tab.setText(title);
}
});
mediator.attach();
}
@Override
public void onFailure(ApiException ex) {
loadingLayout.setVisibility(View.GONE);
no_data_layout.setVisibility(View.VISIBLE);
}
});
}
@Override
protected void onResume() {
super.onResume();
updateKeyboardSettingLayout();
AdsInsUtil.INSTANCE.loadAllAdIsNotCached(this);
}
@SuppressLint("SetTextI18n")
private void updateKeyboardSettingLayout() {
boolean enabledSet = isMyInputMethodEnabled(this);
boolean defaultSet = isMyInputMethodDefault(this);
if (enabledSet) {
step1Btn.setBackground(getDrawable(R.drawable.drw_gray_select_bg));
step1Btn.setText("Step 1:Enabled");
step1Btn.setTextColor(Color.parseColor("#999999"));
} else {
step1Btn.setBackground(getDrawable(R.drawable.drw_80_btn));
step1Btn.setText("Step 1:Select");
step1Btn.setTextColor(Color.parseColor("#000000"));
}
if (defaultSet) {
step2Btn.setBackground(getDrawable(R.drawable.drw_gray_select_bg));
step2Btn.setText("Step 2:Enabled");
step1Btn.setTextColor(Color.parseColor("#999999"));
} else {
step2Btn.setBackground(getDrawable(R.drawable.drw_80_btn));
step2Btn.setText("Step 2:Select");
step1Btn.setTextColor(Color.parseColor("#000000"));
}
int number = 0;
if (!enabledSet) {
number += 1;
}
if (!defaultSet) {
number += 1;
}
keyboardNumber.setText("" + number);
if (enabledSet && defaultSet) {
keyboardSettingLayout.setVisibility(View.GONE);
} else {
keyboardSettingLayout.setVisibility(View.VISIBLE);
}
}
private BroadcastReceiver receiver;
private void registerReceiver() {
IntentFilter filter = new IntentFilter(Intent.ACTION_INPUT_METHOD_CHANGED);
receiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
updateKeyboardSettingLayout();
boolean enabledSet = isMyInputMethodEnabled(MainActivity.this);
boolean defaultSet = isMyInputMethodDefault(MainActivity.this);
if (enabledSet && defaultSet) {
dialog_step_layout.setVisibility(View.GONE);
} else {
dialog_step_layout.setVisibility(View.VISIBLE);
}
}
};
registerReceiver(receiver, filter);
}
private void unregisterReceiver() {
if (receiver != null) {
unregisterReceiver(receiver);
}
}
@Override
protected void onDestroy() {
super.onDestroy();
unregisterReceiver();
}
private long backPressedTime = 0;
private Toast backToast;
@Override
public void onBackPressed() {
if (backPressedTime + 2000 > System.currentTimeMillis()) {
super.onBackPressed();
backToast.cancel();
} else {
backToast.show();
}
backPressedTime = System.currentTimeMillis();
}
}

View File

@ -0,0 +1,174 @@
package com.colorful.keyboard.theme
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.colorful.keyboard.theme.ad.AdsInsUtil
import com.colorful.keyboard.theme.databinding.ColorfulPreviewActivityBinding
import com.colorful.keyboard.theme.myutil.currentlyThemeUFileString
import com.colorful.keyboard.theme.myutil.getBitmapXXDrawable
import com.colorful.keyboard.theme.myutil.isMyInputMethodDefault
import com.colorful.keyboard.theme.myutil.isMyInputMethodEnabled
import com.colorful.keyboard.theme.myutil.loadAndBlurImage
import com.gyf.immersionbar.ktx.immersionBar
class PreviewActivity : AppCompatActivity() {
companion object {
const val KEY_PREVIEW_URL = "key_preview_url"
}
private lateinit var binding: ColorfulPreviewActivityBinding
private var themeUrl = ""
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ColorfulPreviewActivityBinding.inflate(layoutInflater)
setContentView(binding.root)
AdsInsUtil.showAdRandomModeAndTiming(this)
initBar()
themeUrl = intent.getStringExtra(KEY_PREVIEW_URL).toString()
registerReceiver()
initView()
AdsInsUtil.loadAllAdIsNotCached(this)
}
private fun initBar() {
immersionBar {
statusBarDarkFont(true)
statusBarView(binding.view)
}
}
override fun onResume() {
super.onResume()
updateSetMyInputMethod()
}
override fun onBackPressed() {
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.drawable.drw_80_btn)
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.drawable.drw_80_btn)
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,49 @@
package com.colorful.keyboard.theme;
import android.content.Intent;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.os.Bundle;
import android.view.View;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.TextView;
import com.colorful.keyboard.theme.myutil.ImmUtils;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AppCompatActivity;
public class SettingActivity extends AppCompatActivity {
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_setting);
ImmUtils.setStatusBar(this, false, false);
TextView tvVersion = findViewById(R.id.tv_version);
ImageView ivBack = findViewById(R.id.iv_back_setting);
LinearLayout llyinsi = findViewById(R.id.ll_yinsi);
PackageManager packageManager = getPackageManager();
String packageName = getPackageName();
try {
PackageInfo packageInfo = packageManager.getPackageInfo(packageName, 0);
tvVersion.setText("v" + packageInfo.versionName);
} catch (PackageManager.NameNotFoundException e) {
throw new RuntimeException(e);
}
llyinsi.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
startActivity(new Intent(SettingActivity.this, LocalPrivacyAct.class));
}
});
ivBack.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
finish();
}
});
}
}

View File

@ -0,0 +1,7 @@
package com.colorful.keyboard.theme;
import android.preference.PreferenceFragment;
public class TestFragment extends PreferenceFragment {
}

View File

@ -0,0 +1,286 @@
package com.colorful.keyboard.theme;
import static com.colorful.keyboard.theme.net.BaseInitApiRequest.getNetData;
import static com.colorful.keyboard.theme.net.BaseInitApiRequest.initHHYApiStores;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.os.Bundle;
import android.provider.Settings;
import android.util.Log;
import android.view.View;
import android.view.inputmethod.InputMethodInfo;
import android.view.inputmethod.InputMethodManager;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.TextView;
import android.widget.Toast;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AppCompatActivity;
import com.bumptech.glide.Glide;
import com.bumptech.glide.load.DataSource;
import com.bumptech.glide.load.engine.GlideException;
import com.bumptech.glide.request.RequestListener;
import com.bumptech.glide.request.target.Target;
import com.colorful.keyboard.theme.constant.ConfigConstant;
import com.colorful.keyboard.theme.latin.LatinIME;
import com.colorful.keyboard.theme.model.ThemeDetailsBean;
import com.colorful.keyboard.theme.myutil.ConfigFileUtils;
import com.colorful.keyboard.theme.myutil.ImmUtils;
import com.colorful.keyboard.theme.myutil.SPUtils;
import com.colorful.keyboard.theme.net.ApiCallbackCommon;
import java.io.File;
import java.util.HashMap;
public class ThemeSettingsActivity extends AppCompatActivity {
private String key;
private String title;
private TextView tvStepOne;
private TextView tvStepTwo;
private LinearLayout ll_setting;
private LinearLayout ll_step;
private TextView tv_to_setting;
ThemeDetailsBean themeDetailsBean;
boolean isCurrentSetting = false;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_setting_theme);
ImmUtils.setStatusBar(this,false,false);
title = getIntent().getStringExtra("title");
key = getIntent().getStringExtra("key");
String url = getIntent().getStringExtra("url");
ImageView imageView = findViewById(R.id.iv_back_setting);
ll_setting = findViewById(R.id.ll_setting);
ll_step = findViewById(R.id.ll_step);
tv_to_setting = findViewById(R.id.tv_to_setting);
tvStepOne = findViewById(R.id.tv_step_one);
tvStepTwo = findViewById(R.id.tv_step_two);
ImageView iv_theme = findViewById(R.id.iv_theme);
Glide.with(this).load(url).into(iv_theme);
TextView tv_setting_title = findViewById(R.id.tv_setting_title);
tv_setting_title.setText(title);
imageView.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
finish();
}
});
getInfo();
/* new MaxAdInputManager(this, Constant.adIdSee, new AdInputListener() {
@Override
public void adShow() {
}
@Override
public void adClose() {
}
});*/
}
@Override
protected void onResume() {
super.onResume();
if(isInputMethodOfThisImeEnabled()){
tvStepOne.setBackground(getResources().getDrawable(R.drawable.shape_green));
}else {
tvStepOne.setBackground(getResources().getDrawable(R.drawable.shape_grey));
}
String string = Settings.Secure.getString(getContentResolver(), Settings.Secure.DEFAULT_INPUT_METHOD);
boolean isMy = string.contains("com.colorful.keyboard.theme");
if(isMy){
tvStepTwo.setBackground(getResources().getDrawable(R.drawable.shape_green));
}else {
tvStepTwo.setBackground(getResources().getDrawable(R.drawable.shape_grey));
}
Log.e("666",string);
tvStepOne.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
if(!isInputMethodOfThisImeEnabled()){
Intent intent = new Intent(android.provider.Settings.ACTION_INPUT_METHOD_SETTINGS);
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
startActivity(intent);
}
}
});
tvStepTwo.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
if(!isMy){
IntentFilter intentFilter = new IntentFilter();
intentFilter.addAction(INPUT_METHOD_ACTION);
registerReceiver(inputMethodChangeReceiver, intentFilter);
try {
((InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE)).showInputMethodPicker();
} catch (Exception e) {
e.printStackTrace();
}
}
}
});
if(isMy && isInputMethodOfThisImeEnabled()){
ll_step.setVisibility(View.GONE);
ll_setting.setVisibility(View.VISIBLE);
}else{
ll_step.setVisibility(View.VISIBLE);
ll_setting.setVisibility(View.GONE);
}
tv_to_setting.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
if(!isCurrentSetting){
if (null!=themeDetailsBean){
SPUtils.getInstance().put("id",themeDetailsBean.getData().getItem().getKey());
/* new MaxAdInputManager(ThemeSettingsActivity.this, Constant.adIdSetting, new AdInputListener() {
@Override
public void adShow() {
}
@Override
public void adClose() {
saveConfigImgToLocal(ThemeSettingsActivity.this,themeDetailsBean.getData().getItem().getThemeContent().getImgBanner());
}
});*/
}
}
}
});
}
public void saveConfigImgToLocal(Context context, String url) {
Glide.with(context)
.downloadOnly()
.load(url)
.listener(new RequestListener<File>() {
@Override
public boolean onLoadFailed(@Nullable GlideException e, Object model, Target<File> target, boolean isFirstResource) {
return false;
}
@Override
public boolean onResourceReady(File resource, Object model, Target<File> target, DataSource dataSource, boolean isFirstResource) {
configSaveToAlbum(context, resource.getAbsolutePath());
return false;
}
})
.preload();
}
public void configSaveToAlbum(Context context, String srcPath) {
String imageSrc = context.getExternalFilesDir("input").getAbsolutePath() +"image/"+ System.currentTimeMillis() + ".png";
File file = new File(imageSrc);
SPUtils.getInstance().put("bg",imageSrc);
App.Companion.getApp().stopService(new Intent(App.Companion.getApp(), LatinIME.class));
App.Companion.getApp().startService(new Intent(App.Companion.getApp(),LatinIME.class));
ConfigConstant.isNeedSetting = true;
Toast.makeText(ThemeSettingsActivity.this,"Success",Toast.LENGTH_SHORT).show();
if(SPUtils.getInstance().getString("id") .equals(themeDetailsBean.getData().getItem().getKey()) ){
isCurrentSetting = true;
tv_to_setting.setBackground(getResources().getDrawable(R.drawable.shape_green));
}else{
tv_to_setting.setBackground(getResources().getDrawable(R.drawable.shape_grey));
isCurrentSetting = false;
}
boolean isCopySuccess = false;
try {
isCopySuccess = ConfigFileUtils.configCopy(srcPath, file.getAbsolutePath(),false);
} catch (Exception e) {
e.printStackTrace();
}
/*if (isCopySuccess) {
try {
MediaStore.Images.Media.insertImage(context.getContentResolver(),
file.getAbsolutePath(),file.getName() , null);
context.sendBroadcast(new Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE, Uri.parse(file.getAbsolutePath())));
Toast.makeText(context,"successfully",Toast.LENGTH_SHORT).show();
} catch (FileNotFoundException e) {
e.printStackTrace();
}
} else {
}*/
}
private static final String INPUT_METHOD_ACTION = "android.intent.action.INPUT_METHOD_CHANGED";
private BroadcastReceiver inputMethodChangeReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
if (intent.getAction().equals(INPUT_METHOD_ACTION)) {
//监听到输入法发生改变
onResume();
}
}
};
private boolean isInputMethodOfThisImeEnabled() {
final InputMethodManager imm =
(InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE);
final String imePackageName = getPackageName();
for (final InputMethodInfo imi : imm.getEnabledInputMethodList()) {
if (imi.getPackageName().equals(imePackageName)) {
return true;
}
}
return false;
}
private void getInfo(){
HashMap<String,String> hashMap = new HashMap<>();
hashMap.put("sign","a28aadc61c76c754944f6ddb48962c9c");
getNetData(initHHYApiStores().getDetailsTheme(key,hashMap),
new ApiCallbackCommon<ThemeDetailsBean>(this, false) {
@Override
public void onSuccess(ThemeDetailsBean model) {
themeDetailsBean = model;
Log.e("666",SPUtils.getInstance().getString("id"));
Log.e("666",model.getData().getItem().getKey());
Log.e("666",model.getData().getItem().getKey());
Log.e("666",(SPUtils.getInstance().getString("id") == model.getData().getItem().getKey())+"");
if(SPUtils.getInstance().getString("id") .equals(model.getData().getItem().getKey()) ){
isCurrentSetting = true;
tv_to_setting.setBackground(getResources().getDrawable(R.drawable.shape_green));
}else{
tv_to_setting.setBackground(getResources().getDrawable(R.drawable.shape_grey));
isCurrentSetting = false;
}
}
@Override
public void onFailure(com.colorful.keyboard.theme.net.ApiException ex) {
}
});
}
}

View File

@ -0,0 +1,74 @@
package com.colorful.keyboard.theme.ad
import android.app.Activity
import android.util.Log
import com.anythink.core.api.ATAdInfo
import com.anythink.core.api.AdError
import com.anythink.interstitial.api.ATInterstitial
import com.anythink.interstitial.api.ATInterstitialListener
class AdInstLoad {
private var mPlace: String
private var adLoadListener: LoadListener? = null
private var activity: Activity? = null
constructor(activity: Activity, place: String, listener: LoadListener?) {
this.mPlace = place
this.adLoadListener = listener
this.activity = activity
init()
}
constructor(place: String, listener: LoadListener?) {
this.mPlace = place
this.adLoadListener = listener
init()
}
private fun init() {
val atInterstitial = ATInterstitial(activity, mPlace)
atInterstitial.setAdListener(object : ATInterstitialListener {
override fun onInterstitialAdLoaded() {
val info = atInterstitial.checkAdStatus().atTopAdInfo
InstAdCacheManager.instance.setAdCache(mPlace, atInterstitial)
adLoadListener?.loaded(info)
}
override fun onInterstitialAdLoadFail(adError: AdError?) {
adLoadListener?.loadFailed(adError)
Log.d("ocean","onInterstitialAdLoadFail->${adError} ")
Log.d("ocean","AdError.getFullErrorInfo()->${adError?.fullErrorInfo}")
}
override fun onInterstitialAdClicked(p0: ATAdInfo?) {
}
override fun onInterstitialAdShow(p0: ATAdInfo?) {
}
override fun onInterstitialAdClose(p0: ATAdInfo?) {
}
override fun onInterstitialAdVideoStart(p0: ATAdInfo?) {
}
override fun onInterstitialAdVideoEnd(p0: ATAdInfo?) {
}
override fun onInterstitialAdVideoError(p0: AdError?) {
}
})
atInterstitial.load()
}
}

View File

@ -0,0 +1,68 @@
package com.colorful.keyboard.theme.ad
import android.app.Activity
import com.anythink.core.api.ATAdInfo
import com.anythink.core.api.AdError
import com.anythink.interstitial.api.ATInterstitialListener
import com.colorful.keyboard.theme.App
class AdInstShower {
private var mPlace: String
private var showListener: ShowListener? = null
private var activity: Activity? = null
constructor(activity: Activity, place: String, showListener: ShowListener?) {
this.mPlace = place
this.showListener = showListener
this.activity = activity
init()
}
constructor(place: String, showListener: ShowListener?) {
this.mPlace = place
this.showListener = showListener
init()
}
private fun init() {
val atInterstitial = InstAdCacheManager.instance.getAdCache(mPlace)
atInterstitial?.setAdListener(object : ATInterstitialListener {
override fun onInterstitialAdLoaded() {
}
override fun onInterstitialAdLoadFail(p0: AdError?) {
}
override fun onInterstitialAdClicked(p0: ATAdInfo?) {
showListener?.onAdClicked()
}
override fun onInterstitialAdShow(p0: ATAdInfo?) {
showListener?.onAdShown(p0)
App.app.lastAdDisplayTime.set(System.currentTimeMillis())
}
override fun onInterstitialAdClose(p0: ATAdInfo?) {
showListener?.onAdClosed()
}
override fun onInterstitialAdVideoStart(p0: ATAdInfo?) {
}
override fun onInterstitialAdVideoEnd(p0: ATAdInfo?) {
}
override fun onInterstitialAdVideoError(adError: AdError?) {
showListener?.onAdShowFailed(AdShowFailed(adError.toString()))
}
})
if (atInterstitial?.isAdReady == true) {
atInterstitial.show(activity)
}
}
}

View File

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

View File

@ -0,0 +1,90 @@
package com.colorful.keyboard.theme.ad
import android.app.Activity
import com.colorful.keyboard.theme.App
import com.colorful.keyboard.theme.myutil.AppSharedPreferences
import com.unity3d.services.ads.api.Show
import java.util.Random
object AdsInsUtil {
object Placement {
const val TOP_ON_AD_ONE = "n66b1cf2f2c70c"
const val TOP_ON_AD_TOW = "n66b1cf2f04a38"
const val TOP_ON_AD_THREE = "n66b1cf2ecd817"
val adPlaceAllList = listOf(
TOP_ON_AD_ONE,
TOP_ON_AD_TOW,
TOP_ON_AD_THREE
)
}
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: LoadListener? = null) {
if (act.isFinishing || canShowAd(placement)) return
loadAd(act, placement, listener)
}
fun showAdRandomModeAndTiming(act: Activity, listener: ShowListener? = null) {
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]
showAd(act, place, listener)
}
}
fun canShowAd(adID: String): Boolean {
InstAdCacheManager.instance.getAdCache(adID)?.let {
return it.isAdReady
} ?: let {
return false
}
}
fun loadAd(
act: Activity,
adID: String,
loadListener: LoadListener?
): AdInstLoad {
return AdInstLoad(act, adID, loadListener)
}
fun showAd(
act: Activity,
adID: String,
listener: ShowListener?
): AdInstShower {
return AdInstShower(act, adID, listener)
}
fun showAdTiming(act: Activity, placement: String, listener: ShowListener? = null) {
//当前时间减去旧时间,才展示广告满足间隔时间才show广告
val showAdIntervalTime = AppSharedPreferences(App.app).getShowAdIntervalTime()
if (System.currentTimeMillis() - App.app.lastAdDisplayTime.get() >= showAdIntervalTime) {
//判断当前广告位是否可以show不能show则判断补位广告是否可以show。
if (act.isFinishing || !canShowAd(placement)) {
showAd(act, placement, listener)
} else {
showAd(act, placement, listener)
}
}
}
}

View File

@ -0,0 +1,35 @@
package com.colorful.keyboard.theme.ad
import com.anythink.interstitial.api.ATInterstitial
class InstAdCacheManager {
private val mAdCacheDict: MutableMap<String, ATInterstitial> = mutableMapOf()
companion object {
val instance: InstAdCacheManager by lazy(mode = LazyThreadSafetyMode.SYNCHRONIZED) {
InstAdCacheManager()
}
}
fun setAdCache(place: String, adCache: ATInterstitial) {
mAdCacheDict[place] = adCache
}
fun getAdCache(place: String): ATInterstitial? {
return mAdCacheDict[place]
}
fun getLoadedInstCount(): Int {
var count = 0
try {
mAdCacheDict.forEach { (key, value) ->
if (value.isAdReady) {
count += 1
}
}
} catch (_: Exception) {
}
return count
}
}

View File

@ -0,0 +1,10 @@
package com.colorful.keyboard.theme.ad
import com.anythink.core.api.ATAdInfo
import com.anythink.core.api.AdError
interface LoadListener {
fun loadFailed(error: AdError?) {}
fun loaded(ad: ATAdInfo) {}
}

View File

@ -0,0 +1,11 @@
package com.colorful.keyboard.theme.ad
import com.anythink.core.api.ATAdInfo
import com.colorful.keyboard.theme.ad.AdShowFailed
interface ShowListener {
fun onAdShown(ad: ATAdInfo?) {}
fun onAdShowFailed(error: AdShowFailed?) {}
fun onAdClosed() {}
fun onAdClicked() {}
}

View File

@ -0,0 +1,83 @@
package com.colorful.keyboard.theme.ad.async;
import android.os.Handler;
import android.os.Looper;
import java.util.concurrent.Callable;
import java.util.concurrent.Future;
public class Async {
private static ThreadPoolExecutorWrapper sThreadPoolExecutorWrapper;
private static ThreadPoolExecutorWrapper getThreadPoolExecutorWrapper() {
if (sThreadPoolExecutorWrapper == null) {
synchronized (Async.class) {
if (sThreadPoolExecutorWrapper == null) {
sThreadPoolExecutorWrapper = new ThreadPoolExecutorWrapper(12, 12, 10);
// if (BuildConfig.DEBUG)
// LogUtil.d(LogFilterDef.APP_INIT, LogHelper.getFileLineMethod(1));
}
}
}
return sThreadPoolExecutorWrapper;
}
public static void run(Runnable task) {
getThreadPoolExecutorWrapper().executeTask(task);
}
public static <T> Future<T> submit(Callable<T> task) {
return getThreadPoolExecutorWrapper().submitTask(task);
}
public static boolean isMainThread() {
return Thread.currentThread() == Looper.getMainLooper().getThread();
}
public static void schedule(long delay, Runnable task) {
getThreadPoolExecutorWrapper().scheduleTask(delay, task);
}
public static void scheduleTaskAtFixedRateIgnoringTaskRunningTime(long initialDelay, long period, Runnable task) {
getThreadPoolExecutorWrapper().scheduleTaskAtFixedRateIgnoringTaskRunningTime(initialDelay, period, task);
}
public static void scheduleTaskAtFixedRateIncludingTaskRunningTime(long initialDelay, long period, Runnable task) {
getThreadPoolExecutorWrapper().scheduleTaskAtFixedRateIncludingTaskRunningTime(initialDelay, period, task);
}
public static boolean removeScheduledTask(Runnable task) {
return getThreadPoolExecutorWrapper().removeScheduledTask(task);
}
public static void scheduleTaskOnUiThread(long delay, Runnable task) {
getThreadPoolExecutorWrapper().scheduleTaskOnUiThread(delay, task);
}
public static void removeScheduledTaskOnUiThread(Runnable task) {
getThreadPoolExecutorWrapper().removeScheduledTaskOnUiThread(task);
}
public static void runOnUiThread(Runnable task) {
getThreadPoolExecutorWrapper().runTaskOnUiThread(task);
}
public static Handler getMainHandler() {
return getThreadPoolExecutorWrapper().getMainHandler();
}
/*
single background thread to execute Job
*/
public static void scheduleInQueue(Runnable task) {
getThreadPoolExecutorWrapper().scheduleOnQueue(task);
}
public static void shutdown() {
if (sThreadPoolExecutorWrapper != null) {
sThreadPoolExecutorWrapper.shutdown();
sThreadPoolExecutorWrapper = null;
}
}
}

View File

@ -0,0 +1,140 @@
package com.colorful.keyboard.theme.ad.async;
import android.os.Handler;
import android.os.Looper;
import android.os.Message;
import android.os.SystemClock;
import java.util.LinkedList;
import java.util.Queue;
public class HandlerPoster extends Handler {
private final int ASYNC = 1;
private final int SYNC = 2;
private final Queue<Runnable> asyncPool;
private final Queue<SyncPost> syncPool;
private final int maxMillisInsideHandleMessage;
private boolean asyncActive;//执行状态避免重复发送消息导致消息队列过多
private boolean syncActive;//执行状态避免重复发送消息导致消息队列过多
HandlerPoster(Looper looper, int maxMillisInsideHandleMessage) {
super(looper);
this.maxMillisInsideHandleMessage = maxMillisInsideHandleMessage;
asyncPool = new LinkedList<Runnable>();
syncPool = new LinkedList<SyncPost>();
}
void dispose() {
this.removeCallbacksAndMessages(null);
this.asyncPool.clear();
this.syncPool.clear();
}
void async(Runnable runnable) throws Exception {
synchronized (asyncPool) {
asyncPool.offer(runnable);
//判断当前是否有异步任务正在执行
if (!asyncActive) {
asyncActive = true;
if (!sendMessage(obtainMessage(ASYNC))) {
throw new Exception("Could not send handler message");
}
}
}
}
void sync(SyncPost post) throws Exception {
synchronized (syncPool) {
syncPool.offer(post);
//判断当前是否有同步任务正在执行
if (!syncActive) {
syncActive = true;
if (!sendMessage(obtainMessage(SYNC))) {
throw new Exception("Could not send handler message");
}
}
}
}
@Override
public void handleMessage(Message msg) {
if (msg.what == ASYNC) {
boolean rescheduled = false;
try {
//当执行完一个任务后就判断一次是否超过时间限制如果超过那么不管队列中的任务是否执行完成都退出同时发起一个新的消息到Handler循环队列
//在while部分使用poll从队列取出一个任务判断是否为空如果为空进入队列同步块然后再取一次再次判断
//如果恰巧在进入同步队列之前有新的任务来了那么第二次取到的当然就不是 NULL也就会继续执行下去
//反之如果还是为空那么重置当前队列的状态为false,同时跳出循环
long started = SystemClock.uptimeMillis();
while (true) {
Runnable runnable = null;
synchronized (asyncPool){
runnable = asyncPool.size()==0?null:asyncPool.poll();//2018.12.24 add by xw 如果为空就去null
}
if (runnable == null) {
synchronized (asyncPool) {
// Check again, this time in synchronized
runnable = asyncPool.poll();
if (runnable == null) {
asyncActive = false;
return;
}
}
}
runnable.run();
long timeInMethod = SystemClock.uptimeMillis() - started;
if (timeInMethod >= maxMillisInsideHandleMessage) {
if (!sendMessage(obtainMessage(ASYNC))) {
throw new Exception("Could not send handler message");
}
rescheduled = true;
return;
}
}
} catch (Exception e) {
e.printStackTrace();
} finally {
asyncActive = rescheduled;
}
} else if (msg.what == SYNC) {
boolean rescheduled = false;
try {
long started = SystemClock.uptimeMillis();
while (true) {
SyncPost post = syncPool.poll();
if (post == null) {
synchronized (syncPool) {
// Check again, this time in synchronized
post = syncPool.poll();
if (post == null) {
syncActive = false;
return;
}
}
}
post.run();
long timeInMethod = SystemClock.uptimeMillis() - started;
if (timeInMethod >= maxMillisInsideHandleMessage) {
if (!sendMessage(obtainMessage(SYNC))) {
throw new Exception("Could not send handler message");
}
rescheduled = true;
return;
}
}
} catch (Exception e) {
e.printStackTrace();
} finally {
syncActive = rescheduled;
}
} else
super.handleMessage(msg);
}
}

View File

@ -0,0 +1,73 @@
package com.colorful.keyboard.theme.ad.async;
import android.os.Looper;
/**
* 网上找到的关于子线程切换到主线程的代码,实现子线程任意时刻切换到主线程并可选择地阻塞子线程
* 此方案避免handler满天飞的情况
* 参考资料http://c.jinhusns.com/cms/c-884
* Created by DonWZ on 2016-10-18
*/
public class MainThreadSwitcher {
private static HandlerPoster mainPoster = null;
private static HandlerPoster getMainPoster() {
if (mainPoster == null) {
synchronized (MainThreadSwitcher.class) {
if (mainPoster == null) {
mainPoster = new HandlerPoster(Looper.getMainLooper(), 500);//限制主线程单次运行时间
}
}
}
return mainPoster;
}
/**
* Asynchronously.
* The child thread asynchronous run relative to the main thread,
* not blocking the child thread
* @param runnable
* Runnable Interface
*/
public static void runOnMainThreadAsync(Runnable runnable) {
if (Looper.myLooper() == Looper.getMainLooper()) {
runnable.run();
return;
}
try {
getMainPoster().async(runnable);
} catch (Exception e) {
e.printStackTrace();
}
}
/**
*
* Synchronously.
* The child thread relative thread synchronization operation,
* blocking the child thread,
* thread for the main thread to complete
* @param runnable
* Runnable Interface
*/
public static void runOnMainThreadSync(Runnable runnable) {
if (Looper.myLooper() == Looper.getMainLooper()) {
runnable.run();
return;
}
SyncPost poster = new SyncPost(runnable);
try {
getMainPoster().sync(poster);
} catch (Exception e) {
e.printStackTrace();
}
poster.waitRun();
}
public static void dispose() {
if (mainPoster != null) {
mainPoster.dispose();
mainPoster = null;
}
}
}

View File

@ -0,0 +1,44 @@
package com.colorful.keyboard.theme.ad.async;
public class SyncPost {
boolean end = false;
Runnable runnable;
SyncPost(Runnable runnable) {
this.runnable = runnable;
}
public void run() {
//进入同步块然后调用Runnable接口的run方法同时在执行完成后将end重置为true;
//然后调用this.notifyAll();通知等待的部分可以继续了当然有这样的情况假如在进入该同步块的时候子线程还未执行到this.wait();部分呢
//所以我们为此准备了end和try
synchronized (this) {
runnable.run();
end = true;
try {
this.notifyAll();
} catch (Exception e) {
e.printStackTrace();
}
}
}
public void waitRun() {
//首先判断状态如果状态已经变了那么证明子线程执行到此处时主线程已经执行了void_run()
//所以也就不用进入同步块进行等待了反之进入等待直到主线程调用this.notifyAll();
if (!end) {
synchronized (this) {
if (!end) {
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
}

View File

@ -0,0 +1,46 @@
package com.colorful.keyboard.theme.ad.async;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;
public class TaskQueue extends Thread{
private BlockingQueue<Object> mQueue;
public TaskQueue() {
mQueue = new LinkedBlockingQueue<Object>();
}
public TaskQueue(String name) {
this();
setName(name);
}
public void stopTaskQueue() {
// use 'Poison Pill Shutdown' to stop the task queue
// add a non-Runnable object, which will be recognized as the command
// by the thread to break the infinite loop
mQueue.add(new Object());
}
public void scheduleTask(Runnable task) {
mQueue.add(task);
}
@Override
public void run() {
while (true) {
try {
Object obj = mQueue.take();
if (obj instanceof Runnable) {
((Runnable) obj).run();
obj = null;
} else {
break;
}
} catch (InterruptedException e) {
}
}
}
}

View File

@ -0,0 +1,158 @@
package com.colorful.keyboard.theme.ad.async;
import android.os.Handler;
import android.os.Looper;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Future;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.ScheduledThreadPoolExecutor;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
public class ThreadPoolExecutorWrapper {
private static final Long IDLE_THREAD_KEEP_ALIVE_TIME = 60L;
private ExecutorService mThreadPoolExecutor;//normal thread pool
private ScheduledThreadPoolExecutor mScheduledThreadPoolExecutor;//can schedule task thread pool
private Handler mMainHandler;
private TaskQueue mActionQueue;
private Map<Integer, Object> mScheduledJobRecord = new HashMap<>();//ScheduledThreadPoolExecutor will wrap Runnable, so we record this
private Object mScheduledJobRecordMutex = new Object();
/*
maxThreadCount:thread pool max thread count
activeThreadCount:thread pool min thread count even if all thread is idle
IDLE_THREAD_KEEP_ALIVE_TIME:IDLE thread will be shutdown when TIME_OUT if (maxThreadCount - activeThreadCount) > 0
*/
public ThreadPoolExecutorWrapper(int activeThreadCount, int maxThreadCount, int maxScheTaskThread) {
mThreadPoolExecutor = new ThreadPoolExecutor(activeThreadCount, maxThreadCount,
IDLE_THREAD_KEEP_ALIVE_TIME, TimeUnit.SECONDS,
new LinkedBlockingQueue<Runnable>());
if (maxScheTaskThread > 0) {
mScheduledThreadPoolExecutor = new ScheduledThreadPoolExecutor(maxScheTaskThread);
}
mMainHandler = new Handler(Looper.getMainLooper());
mActionQueue = new TaskQueue(Async.class.getName());
mActionQueue.start();
}
public void executeTask(Runnable task) {
if (task == null) {
return;
}
mThreadPoolExecutor.execute(task);
}
public <T> Future<T> submitTask(Callable<T> task) {
return mThreadPoolExecutor.submit(task);
}
public void scheduleTask(long delay, Runnable task) {
if (task == null) {
return;
}
mScheduledThreadPoolExecutor.schedule(task, delay, TimeUnit.MILLISECONDS);
}
public void scheduleTaskAtFixedRateIgnoringTaskRunningTime(long initialDelay, long period, Runnable task) {
if (task == null) {
return;
}
synchronized (mScheduledJobRecordMutex) {
if (mScheduledJobRecord.containsKey(task.hashCode())) {
return;
}
mScheduledJobRecord.put(task.hashCode(), mScheduledThreadPoolExecutor.scheduleAtFixedRate(task, initialDelay, period, TimeUnit.MILLISECONDS));
}
}
public void scheduleTaskAtFixedRateIncludingTaskRunningTime(long initialDelay, long period, Runnable task) {
if (task == null) {
return;
}
synchronized (mScheduledJobRecordMutex) {
if (mScheduledJobRecord.containsKey(task.hashCode())) {
return;
}
mScheduledJobRecord.put(task.hashCode(), mScheduledThreadPoolExecutor.scheduleWithFixedDelay(task, initialDelay, period, TimeUnit.MILLISECONDS));
}
}
public boolean removeScheduledTask(Runnable task) {
if (task == null) {
return false;
}
synchronized (mScheduledJobRecordMutex) {
if (!mScheduledJobRecord.containsKey(task.hashCode())) {
return false;
}
ScheduledFuture<?> internalJob = (ScheduledFuture<?>) mScheduledJobRecord.get(task.hashCode());
internalJob.cancel(true);
mScheduledJobRecord.remove(task.hashCode());
return true;
}
}
public void scheduleTaskOnUiThread(long delay, Runnable task) {
if (task == null) {
return;
}
mMainHandler.postDelayed(task, delay);
}
public void removeScheduledTaskOnUiThread(Runnable task) {
if (task == null) {
return;
}
mMainHandler.removeCallbacks(task);
}
public void runTaskOnUiThread(Runnable task) {
if (task == null) {
return;
}
mMainHandler.post(task);
}
public Handler getMainHandler() {
return mMainHandler;
}
public void scheduleOnQueue(Runnable task) {
if (task == null) {
return;
}
mActionQueue.scheduleTask(task);
}
public void shutdown() {
if (mThreadPoolExecutor != null) {
mThreadPoolExecutor.shutdown();
mThreadPoolExecutor = null;
}
if (mScheduledThreadPoolExecutor != null) {
mScheduledThreadPoolExecutor.shutdown();
mScheduledThreadPoolExecutor = null;
}
if (mActionQueue != null) {
mActionQueue.stopTaskQueue();
}
}
}

View File

@ -0,0 +1,27 @@
package com.colorful.keyboard.theme.adapter;
import androidx.annotation.NonNull;
import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentActivity;
import androidx.viewpager2.adapter.FragmentStateAdapter;
import java.util.List;
public class ViewPager2Adapter extends FragmentStateAdapter {
List<Fragment> fragmentList;
public ViewPager2Adapter(@NonNull FragmentActivity fragmentActivity, List<Fragment> fragmentList) {
super(fragmentActivity);
this.fragmentList = fragmentList;
}
@NonNull
@Override
public Fragment createFragment(int position) {
return fragmentList.get(position);
}
@Override
public int getItemCount() {
return fragmentList.size();
}
}

View File

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

View File

@ -0,0 +1,16 @@
package com.colorful.keyboard.theme.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,18 @@
package com.colorful.keyboard.theme.bean
import androidx.annotation.Keep
import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.PrimaryKey
import java.io.Serializable
@Keep
@Entity
data class DownloadBean(
@ColumnInfo(name = "title") var title: String,
@ColumnInfo(name = "key") var key: String,
@ColumnInfo(name = "imgUrl") var imgUrl: String,
) : Serializable {
@PrimaryKey(autoGenerate = true)
var id: Long = 0
}

View File

@ -0,0 +1,14 @@
package com.colorful.keyboard.theme.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.colorful.keyboard.theme.bean
import java.io.Serializable
data class LockBean(
val type: Int
) : Serializable

View File

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

View File

@ -0,0 +1,13 @@
package com.colorful.keyboard.theme.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 @@
/*
* Copyright (C) 2011 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.colorful.keyboard.theme.compat;
import android.os.Build;
import android.os.LocaleList;
import android.view.inputmethod.EditorInfo;
import java.util.Locale;
public final class EditorInfoCompatUtils {
private EditorInfoCompatUtils() {
// This utility class is not publicly instantiable.
}
public static String imeActionName(final int imeOptions) {
final int actionId = imeOptions & EditorInfo.IME_MASK_ACTION;
switch (actionId) {
case EditorInfo.IME_ACTION_UNSPECIFIED:
return "actionUnspecified";
case EditorInfo.IME_ACTION_NONE:
return "actionNone";
case EditorInfo.IME_ACTION_GO:
return "actionGo";
case EditorInfo.IME_ACTION_SEARCH:
return "actionSearch";
case EditorInfo.IME_ACTION_SEND:
return "actionSend";
case EditorInfo.IME_ACTION_NEXT:
return "actionNext";
case EditorInfo.IME_ACTION_DONE:
return "actionDone";
case EditorInfo.IME_ACTION_PREVIOUS:
return "actionPrevious";
default:
return "actionUnknown(" + actionId + ")";
}
}
public static Locale getPrimaryHintLocale(final EditorInfo editorInfo) {
if (editorInfo == null) {
return null;
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
LocaleList localeList = editorInfo.hintLocales;
if (localeList != null && !localeList.isEmpty())
return localeList.get(0);
}
return null;
}
}

View File

@ -0,0 +1,58 @@
/*
* Copyright (C) 2021 Raimondas Rimkus
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.colorful.keyboard.theme.compat;
import android.app.ActionBar;
import android.graphics.PorterDuff;
import android.graphics.drawable.Drawable;
import android.view.MenuItem;
import android.view.View;
import android.widget.TextView;
import java.util.ArrayList;
public class MenuItemIconColorCompat {
/**
* Set a menu item's icon to matching text color.
* @param menuItem the menu item that should change colors.
* @param actionBar target ActionBar.
*/
public static void matchMenuIconColor(final View view, final MenuItem menuItem, final ActionBar actionBar) {
ArrayList<View> views = new ArrayList<>();
view.getRootView().findViewsWithText(views, actionBar.getTitle(), View.FIND_VIEWS_WITH_TEXT);
if (views.size() == 1 && views.get(0) instanceof TextView) {
int color = ((TextView) views.get(0)).getCurrentTextColor();
setIconColor(menuItem, color);
}
}
/**
* Set a menu item's icon to specific color.
* @param menuItem the menu item that should change colors.
* @param color the color that the icon should be changed to.
*/
private static void setIconColor(final MenuItem menuItem, final int color) {
if (menuItem != null) {
Drawable drawable = menuItem.getIcon();
if (drawable != null) {
drawable.mutate();
drawable.setColorFilter(color, PorterDuff.Mode.SRC_ATOP);
}
}
}
}

View File

@ -0,0 +1,36 @@
/*
* Copyright (C) 2020 Raimondas Rimkus
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.colorful.keyboard.theme.compat;
import android.content.Context;
import android.content.SharedPreferences;
import android.os.Build;
import android.preference.PreferenceManager;
public class PreferenceManagerCompat {
public static Context getDeviceContext(Context context) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
return context.createDeviceProtectedStorageContext();
}
return context;
}
public static SharedPreferences getDeviceSharedPreferences(Context context) {
return PreferenceManager.getDefaultSharedPreferences(getDeviceContext(context));
}
}

View File

@ -0,0 +1,43 @@
/*
* Copyright (C) 2014 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.colorful.keyboard.theme.compat;
import android.inputmethodservice.InputMethodService;
import android.os.Build;
import android.view.View;
public class ViewOutlineProviderCompatUtils {
private ViewOutlineProviderCompatUtils() {
// This utility class is not publicly instantiable.
}
public interface InsetsUpdater {
void setInsets(final InputMethodService.Insets insets);
}
private static final InsetsUpdater EMPTY_INSETS_UPDATER = new InsetsUpdater() {
@Override
public void setInsets(final InputMethodService.Insets insets) {}
};
public static InsetsUpdater setInsetsOutlineProvider(final View view) {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
return EMPTY_INSETS_UPDATER;
}
return ViewOutlineProviderCompatUtilsLXX.setInsetsOutlineProvider(view);
}
}

View File

@ -0,0 +1,71 @@
/*
* Copyright (C) 2014 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.colorful.keyboard.theme.compat;
import android.annotation.TargetApi;
import android.graphics.Outline;
import android.inputmethodservice.InputMethodService;
import android.os.Build;
import android.view.View;
import android.view.ViewOutlineProvider;
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
class ViewOutlineProviderCompatUtilsLXX {
private ViewOutlineProviderCompatUtilsLXX() {
// This utility class is not publicly instantiable.
}
static ViewOutlineProviderCompatUtils.InsetsUpdater setInsetsOutlineProvider(final View view) {
final InsetsOutlineProvider provider = new InsetsOutlineProvider(view);
view.setOutlineProvider(provider);
return provider;
}
private static class InsetsOutlineProvider extends ViewOutlineProvider
implements ViewOutlineProviderCompatUtils.InsetsUpdater {
private final View mView;
private static final int NO_DATA = -1;
private int mLastVisibleTopInsets = NO_DATA;
public InsetsOutlineProvider(final View view) {
mView = view;
view.setOutlineProvider(this);
}
@Override
public void setInsets(final InputMethodService.Insets insets) {
final int visibleTopInsets = insets.visibleTopInsets;
if (mLastVisibleTopInsets != visibleTopInsets) {
mLastVisibleTopInsets = visibleTopInsets;
mView.invalidateOutline();
}
}
@Override
public void getOutline(final View view, final Outline outline) {
if (mLastVisibleTopInsets == NO_DATA) {
// Call default implementation.
ViewOutlineProvider.BACKGROUND.getOutline(view, outline);
return;
}
// TODO: Revisit this when floating/resize keyboard is supported.
outline.setRect(
view.getLeft(), mLastVisibleTopInsets, view.getRight(), view.getBottom());
}
}
}

View File

@ -0,0 +1,36 @@
package com.colorful.keyboard.theme.config_adpter;
import android.content.Context;
import android.view.ViewGroup;
import android.widget.ImageView;
import com.bumptech.glide.Glide;
import com.chad.library.adapter4.BaseQuickAdapter;
import com.chad.library.adapter4.viewholder.QuickViewHolder;
import com.colorful.keyboard.theme.App;
import com.colorful.keyboard.theme.R;
import com.colorful.keyboard.theme.bean.DownloadBean;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
public class DownloadAdapter extends BaseQuickAdapter<DownloadBean, QuickViewHolder> {
@Override
protected void onBindViewHolder(@NonNull QuickViewHolder quickViewHolder, int i, @Nullable DownloadBean itemsBean) {
ImageView imageView = quickViewHolder.getView(R.id.iv_photo);
String url = itemsBean.getImgUrl();
Glide.with(App.Companion.getApp())
.load(url)
.placeholder(R.drawable.placeholder)
.into(imageView);
}
@NonNull
@Override
protected QuickViewHolder onCreateViewHolder(@NonNull Context context, @NonNull ViewGroup viewGroup, int i) {
return new QuickViewHolder(R.layout.item_info, viewGroup);
}
}

View File

@ -0,0 +1,39 @@
package com.colorful.keyboard.theme.config_adpter;
import android.content.Context;
import android.view.ViewGroup;
import android.widget.ImageView;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.bumptech.glide.Glide;
import com.chad.library.adapter4.BaseQuickAdapter;
import com.chad.library.adapter4.viewholder.QuickViewHolder;
import com.colorful.keyboard.theme.App;
import com.colorful.keyboard.theme.R;
import com.colorful.keyboard.theme.model.ThemePhotoListBean;
public class PhotoListAdapter extends BaseQuickAdapter<ThemePhotoListBean.DataBean.SectionsBean.ItemsBean, QuickViewHolder> {
@Override
protected void onBindViewHolder(@NonNull QuickViewHolder quickViewHolder, int i, @Nullable ThemePhotoListBean.DataBean.SectionsBean.ItemsBean itemsBean) {
ImageView imageView = quickViewHolder.getView(R.id.iv_photo);
String url = itemsBean.getThumbUrlGif();
if(url.isEmpty()){
url = itemsBean.getThumbUrl();
}
Glide.with(App.Companion.getApp())
.load(url)
.placeholder(R.drawable.placeholder)
.into(imageView);
}
@NonNull
@Override
protected QuickViewHolder onCreateViewHolder(@NonNull Context context, @NonNull ViewGroup viewGroup, int i) {
return new QuickViewHolder(R.layout.item_info, viewGroup);
}
}

View File

@ -0,0 +1,30 @@
package com.colorful.keyboard.theme.config_adpter;
import android.content.Context;
import android.view.ViewGroup;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.chad.library.adapter4.BaseQuickAdapter;
import com.chad.library.adapter4.viewholder.QuickViewHolder;
import com.colorful.keyboard.theme.R;
import com.colorful.keyboard.theme.model.ThemeListModel;
public class TestAdapter extends BaseQuickAdapter<ThemeListModel, QuickViewHolder> {
@Override
protected void onBindViewHolder(@NonNull QuickViewHolder quickViewHolder, int i, @Nullable ThemeListModel s) {
TextView textView = quickViewHolder.getView(R.id.tv_theme);
textView.setText(s.getName());
}
@NonNull
@Override
protected QuickViewHolder onCreateViewHolder(@NonNull Context context, @NonNull ViewGroup viewGroup, int i) {
return new QuickViewHolder(R.layout.item_photo_info,viewGroup);
}
}

View File

@ -0,0 +1,5 @@
package com.colorful.keyboard.theme.constant;
public class ConfigConstant {
public static boolean isNeedSetting = false;
}

View File

@ -0,0 +1,161 @@
/*
* Copyright (C) 2012 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.colorful.keyboard.theme.event;
import com.colorful.keyboard.theme.latin.common.Constants;
import com.colorful.keyboard.theme.latin.common.StringUtils;
/**
* Class representing a generic input event as handled by Latin IME.
*
* This contains information about the origin of the event, but it is generalized and should
* represent a software keypress, hardware keypress, or d-pad move alike.
* Very importantly, this does not necessarily result in inputting one character, or even anything
* at all - it may be a dead key, it may be a partial input, it may be a special key on the
* keyboard, it may be a cancellation of a keypress (e.g. in a soft keyboard the finger of the
* user has slid out of the key), etc. It may also be a batch input from a gesture or handwriting
* for example.
* The combiner should figure out what to do with this.
*/
public class Event {
// Should the types below be represented by separate classes instead? It would be cleaner
// but probably a bit too much
// An event we don't handle in Latin IME, for example pressing Ctrl on a hardware keyboard.
final public static int EVENT_TYPE_NOT_HANDLED = 0;
// A key press that is part of input, for example pressing an alphabetic character on a
// hardware qwerty keyboard. It may be part of a sequence that will be re-interpreted later
// through combination.
final public static int EVENT_TYPE_INPUT_KEYPRESS = 1;
// A toggle event is triggered by a key that affects the previous character. An example would
// be a numeric key on a 10-key keyboard, which would toggle between 1 - a - b - c with
// repeated presses.
final public static int EVENT_TYPE_TOGGLE = 2;
// A mode event instructs the combiner to change modes. The canonical example would be the
// hankaku/zenkaku key on a Japanese keyboard, or even the caps lock key on a qwerty keyboard
// if handled at the combiner level.
final public static int EVENT_TYPE_MODE_KEY = 3;
// An event corresponding to a string generated by some software process.
final public static int EVENT_TYPE_SOFTWARE_GENERATED_STRING = 6;
// An event corresponding to a cursor move
final public static int EVENT_TYPE_CURSOR_MOVE = 7;
// 0 is a valid code point, so we use -1 here.
final public static int NOT_A_CODE_POINT = -1;
// -1 is a valid key code, so we use 0 here.
final public static int NOT_A_KEY_CODE = 0;
final private static int FLAG_NONE = 0;
// This event is coming from a key repeat, software or hardware.
final private static int FLAG_REPEAT = 0x2;
// This event has already been consumed.
final private static int FLAG_CONSUMED = 0x4;
final private int mEventType; // The type of event - one of the constants above
// The code point associated with the event, if relevant. This is a unicode code point, and
// has nothing to do with other representations of the key. It is only relevant if this event
// is of KEYPRESS type, but for a mode key like hankaku/zenkaku or ctrl, there is no code point
// associated so this should be NOT_A_CODE_POINT to avoid unintentional use of its value when
// it's not relevant.
final public int mCodePoint;
final public CharSequence mText;
// The key code associated with the event, if relevant. This is relevant whenever this event
// has been triggered by a key press, but not for a gesture for example. This has conceptually
// no link to the code point, although keys that enter a straight code point may often set
// this to be equal to mCodePoint for convenience. If this is not a key, this must contain
// NOT_A_KEY_CODE.
final public int mKeyCode;
// Coordinates of the touch event, if relevant. If useful, we may want to replace this with
// a MotionEvent or something in the future. This is only relevant when the keypress is from
// a software keyboard obviously, unless there are touch-sensitive hardware keyboards in the
// future or some other awesome sauce.
final public int mX;
final public int mY;
// Some flags that can't go into the key code. It's a bit field of FLAG_*
final private int mFlags;
// The next event, if any. Null if there is no next event yet.
final public Event mNextEvent;
// This method is private - to create a new event, use one of the create* utility methods.
private Event(final int type, final CharSequence text, final int codePoint, final int keyCode,
final int x, final int y, final int flags,
final Event next) {
mEventType = type;
mText = text;
mCodePoint = codePoint;
mKeyCode = keyCode;
mX = x;
mY = y;
mFlags = flags;
mNextEvent = next;
}
public static Event createSoftwareKeypressEvent(final int codePoint, final int keyCode,
final int x, final int y, final boolean isKeyRepeat) {
return new Event(EVENT_TYPE_INPUT_KEYPRESS, null, codePoint, keyCode, x, y,
isKeyRepeat ? FLAG_REPEAT : FLAG_NONE, null);
}
/**
* Creates an input event with a CharSequence. This is used by some software processes whose
* output is a string, possibly with styling. Examples include press on a multi-character key,
* or combination that outputs a string.
* @param text the CharSequence associated with this event.
* @param keyCode the key code, or NOT_A_KEYCODE if not applicable.
* @return an event for this text.
*/
public static Event createSoftwareTextEvent(final CharSequence text, final int keyCode) {
return new Event(EVENT_TYPE_SOFTWARE_GENERATED_STRING, text, NOT_A_CODE_POINT, keyCode,
Constants.NOT_A_COORDINATE, Constants.NOT_A_COORDINATE,
FLAG_NONE, null /* next */);
}
// Returns whether this is a function key like backspace, ctrl, settings... as opposed to keys
// that result in input like letters or space.
public boolean isFunctionalKeyEvent() {
// This logic may need to be refined in the future
return NOT_A_CODE_POINT == mCodePoint;
}
public boolean isKeyRepeat() {
return 0 != (FLAG_REPEAT & mFlags);
}
public boolean isConsumed() { return 0 != (FLAG_CONSUMED & mFlags); }
public CharSequence getTextToCommit() {
if (isConsumed()) {
return ""; // A consumed event should input no text.
}
switch (mEventType) {
case EVENT_TYPE_MODE_KEY:
case EVENT_TYPE_NOT_HANDLED:
case EVENT_TYPE_TOGGLE:
case EVENT_TYPE_CURSOR_MOVE:
return "";
case EVENT_TYPE_INPUT_KEYPRESS:
return StringUtils.newSingleCodePointString(mCodePoint);
case EVENT_TYPE_SOFTWARE_GENERATED_STRING:
return mText;
}
throw new RuntimeException("Unknown event type: " + mEventType);
}
}

View File

@ -0,0 +1,60 @@
/*
* Copyright (C) 2014 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.colorful.keyboard.theme.event;
import com.colorful.keyboard.theme.latin.settings.SettingsValues;
/**
* An object encapsulating a single transaction for input.
*/
public class InputTransaction {
// UPDATE_LATER is stronger than UPDATE_NOW. The reason for this is, if we have to update later,
// it's because something will change that we can't evaluate now, which means that even if we
// re-evaluate now we'll have to do it again later. The only case where that wouldn't apply
// would be if we needed to update now to find out the new state right away, but then we
// can't do it with this deferred mechanism anyway.
public static final int SHIFT_NO_UPDATE = 0;
public static final int SHIFT_UPDATE_NOW = 1;
public static final int SHIFT_UPDATE_LATER = 2;
// Initial conditions
public final SettingsValues mSettingsValues;
// Outputs
private int mRequiredShiftUpdate = SHIFT_NO_UPDATE;
public InputTransaction(final SettingsValues settingsValues) {
mSettingsValues = settingsValues;
}
/**
* Indicate that this transaction requires some type of shift update.
* @param updateType What type of shift update this requires.
*/
public void requireShiftUpdate(final int updateType) {
mRequiredShiftUpdate = Math.max(mRequiredShiftUpdate, updateType);
}
/**
* Gets what type of shift update this transaction requires.
* @return The shift update type.
*/
public int getRequiredShiftUpdate() {
return mRequiredShiftUpdate;
}
}

View File

@ -0,0 +1,8 @@
package com.colorful.keyboard.theme.firebase
object Constants {
const val KEY_SHOW_AD_INTERVAL_TIME = "key_ad_show_interval"
const val DEFAULT_SHOW_AD_INTERVAL_TIME = 1000 * 30L
}

View File

@ -0,0 +1,134 @@
package com.colorful.keyboard.theme.firebase
import android.annotation.SuppressLint
import android.app.Application
import android.content.Context
import android.os.Handler
import android.os.Message
import android.text.TextUtils
import android.util.Log
import com.colorful.keyboard.theme.App
import com.colorful.keyboard.theme.BuildConfig
import com.colorful.keyboard.theme.myutil.AppSharedPreferences
import com.google.firebase.remoteconfig.ConfigUpdate
import com.google.firebase.remoteconfig.ConfigUpdateListener
import com.google.firebase.remoteconfig.FirebaseRemoteConfig
import com.google.firebase.remoteconfig.FirebaseRemoteConfigException
import com.google.firebase.remoteconfig.FirebaseRemoteConfigSettings
import com.google.firebase.remoteconfig.FirebaseRemoteConfigValue
import java.lang.ref.WeakReference
class RemoteConfig {
private var ctx: Context? = null
private var mFirebaseRemoteConfig: FirebaseRemoteConfig? = null
//配置是否初始化成功
private var isInit = false
//上次获取数据的时间
private var lastFetchTime: Long = 0
private val handler = MHandler(this)
companion object {
const val MSG_REFRESH_CONFIG = 1
val instance: RemoteConfig by lazy(mode = LazyThreadSafetyMode.SYNCHRONIZED) {
RemoteConfig()
}
}
fun init(ctx: Application) {
this.ctx = ctx
initConfig()
fetchConfig()
onConfigUpdate()
}
private fun initConfig() {
var intervalTime = (60 * 10).toLong()
//如果是开发状态,则将提取时间缩短
if (BuildConfig.DEBUG) {
intervalTime = (60 * 5).toLong()
}
mFirebaseRemoteConfig = FirebaseRemoteConfig.getInstance()
val configSettings =
FirebaseRemoteConfigSettings.Builder() //默认值12小时的最短提取间隔如果在间隔内取值则优先取上次的结果
.setMinimumFetchIntervalInSeconds(intervalTime).build()
mFirebaseRemoteConfig!!.setConfigSettingsAsync(configSettings)
}
private fun onConfigUpdate() {
mFirebaseRemoteConfig!!.addOnConfigUpdateListener(object : ConfigUpdateListener {
override fun onUpdate(configUpdate: ConfigUpdate) {
try {
// if (configUpdate.updatedKeys.contains(Constants.KEY_SHOULD_ENTER_MUSIC_JSON)) {
//
// }
mFirebaseRemoteConfig!!.activate().addOnCompleteListener { task ->
if (task.isSuccessful) {
updateData("onConfigUpdate", mFirebaseRemoteConfig!!.all)
}
}
} catch (ignore: Exception) {
}
}
override fun onError(error: FirebaseRemoteConfigException) {
}
})
}
@SuppressLint("LongLogTag")
private fun fetchConfig() {
//这里可能会抛出异常 FirebaseRemoteConfigFetchThrottledException
try {
mFirebaseRemoteConfig!!.fetchAndActivate().addOnCompleteListener { task ->
if (task.isSuccessful) {
isInit = true
lastFetchTime = System.currentTimeMillis()
updateData("fetchAndActivate", mFirebaseRemoteConfig!!.all)
//24小时后重新再去获取
handler.removeMessages(MSG_REFRESH_CONFIG)
handler.sendEmptyMessageDelayed(
MSG_REFRESH_CONFIG, (1000 * 60 * 60 * 24).toLong()
)
} else {
//这里需要重新再去获取
handler.removeMessages(MSG_REFRESH_CONFIG)
handler.sendEmptyMessageDelayed(MSG_REFRESH_CONFIG, (1000 * 60 * 15).toLong())
}
}
} catch (ignore: Exception) {
}
}
private fun updateData(from: String, all: Map<String, FirebaseRemoteConfigValue>) {
val appSharedPreferences = AppSharedPreferences(App.app)
for ((key, value) in all) {
try {
Log.d("colorful-config","from->$from key->$key value->${value.asString()}")
if (TextUtils.equals(Constants.KEY_SHOW_AD_INTERVAL_TIME, key)) {
val t = value.asString()
appSharedPreferences.setShowAdIntervalTime(t.toLong())
}
} catch (ignore: Exception) {
}
}
}
private class MHandler(remoteConfig: RemoteConfig) : Handler() {
private val weakReference: WeakReference<RemoteConfig> = WeakReference(remoteConfig)
override fun handleMessage(msg: Message) {
super.handleMessage(msg)
val remoteConfig = weakReference.get()
if (remoteConfig?.ctx != null) {
if (msg.what == MSG_REFRESH_CONFIG) {
remoteConfig.fetchConfig()
}
}
}
}
}

View File

@ -0,0 +1,91 @@
package com.colorful.keyboard.theme.fragment
import android.content.Intent
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.LinearLayout
import androidx.fragment.app.Fragment
import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.RecyclerView
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
import com.colorful.keyboard.theme.ColorfulDetailsActivity
import com.colorful.keyboard.theme.R
import com.colorful.keyboard.theme.config_adpter.PhotoListAdapter
import com.colorful.keyboard.theme.model.ThemePhotoListBean
import com.colorful.keyboard.theme.net.ApiCallbackCommon
import com.colorful.keyboard.theme.net.ApiException
import com.colorful.keyboard.theme.net.BaseInitApiRequest
class KeyboardListFragment : Fragment() {
private var swipeRefreshLayout: SwipeRefreshLayout? = null
private var recyclerView: RecyclerView? = null
private var catid: String? = ""
private var photoListAdapter: PhotoListAdapter? = null
private var loadingLayout: LinearLayout? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
if (arguments != null) {
val arguments = arguments
catid = arguments!!.getString("id")
}
}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
val inflate = inflater.inflate(R.layout.fragment_photo_list, container, false)
swipeRefreshLayout = inflate.findViewById(R.id.swiper)
recyclerView = inflate.findViewById(R.id.rv_photo_ist)
loadingLayout = inflate.findViewById(R.id.loadingLayout)
return inflate
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
loadingLayout?.visibility = View.VISIBLE
//recyclerView.setAdapter(new PhotoListAdapter());
swipeRefreshLayout!!.isEnabled = false
photoListAdapter = PhotoListAdapter()
recyclerView!!.layoutManager = GridLayoutManager(activity, 3)
recyclerView!!.adapter = photoListAdapter
photoListAdapter!!.setOnItemClickListener { baseQuickAdapter, view, i ->
startActivity(
Intent(activity, ColorfulDetailsActivity::class.java)
.putExtra("title", baseQuickAdapter.getItem(i)!!.title)
.putExtra("key", baseQuickAdapter.getItem(i)!!.key).putExtra(
"url", baseQuickAdapter.getItem(i)!!
.thumbUrl
)
.putExtra("",baseQuickAdapter.getItem(i)?.thumbUrlGif)
)
}
info
}
private val info: Unit
private get() {
val hashMap = HashMap<String, String>()
hashMap["offset"] = "0"
hashMap["fetch_size"] = "100"
hashMap["sign"] = "a28aadc61c76c754944f6ddb48962c9c"
BaseInitApiRequest.getNetData(BaseInitApiRequest.initHHYApiStores()
.getThemeList(catid, hashMap),
object : ApiCallbackCommon<ThemePhotoListBean?>(activity, false) {
override fun onSuccess(model: ThemePhotoListBean?) {
photoListAdapter!!.submitList(model?.data?.sections!![0]?.items)
loadingLayout?.visibility = View.GONE
}
override fun onFailure(ex: ApiException) {
loadingLayout?.visibility = View.GONE
}
})
}
}

View File

@ -0,0 +1,909 @@
/*
* Copyright (C) 2010 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.colorful.keyboard.theme.keyboard;
import static com.colorful.keyboard.theme.latin.common.Constants.CODE_OUTPUT_TEXT;
import static com.colorful.keyboard.theme.latin.common.Constants.CODE_SHIFT;
import static com.colorful.keyboard.theme.latin.common.Constants.CODE_SWITCH_ALPHA_SYMBOL;
import static com.colorful.keyboard.theme.latin.common.Constants.CODE_UNSPECIFIED;
import android.content.res.TypedArray;
import android.graphics.Rect;
import android.graphics.Typeface;
import android.graphics.drawable.Drawable;
import android.text.TextUtils;
import com.colorful.keyboard.theme.R;
import com.colorful.keyboard.theme.keyboard.internal.KeyDrawParams;
import com.colorful.keyboard.theme.keyboard.internal.KeySpecParser;
import com.colorful.keyboard.theme.keyboard.internal.KeyStyle;
import com.colorful.keyboard.theme.keyboard.internal.KeyVisualAttributes;
import com.colorful.keyboard.theme.keyboard.internal.KeyboardIconsSet;
import com.colorful.keyboard.theme.keyboard.internal.KeyboardParams;
import com.colorful.keyboard.theme.keyboard.internal.KeyboardRow;
import com.colorful.keyboard.theme.keyboard.internal.MoreKeySpec;
import com.colorful.keyboard.theme.latin.common.Constants;
import com.colorful.keyboard.theme.latin.common.StringUtils;
import java.util.Arrays;
import java.util.Locale;
/**
* Class for describing the position and characteristics of a single key in the keyboard.
*/
public class Key implements Comparable<Key> {
/**
* The key code (unicode or custom code) that this key generates.
*/
private final int mCode;
/** Label to display */
private final String mLabel;
/** Hint label to display on the key in conjunction with the label */
private final String mHintLabel;
/** Flags of the label */
private final int mLabelFlags;
private static final int LABEL_FLAGS_ALIGN_HINT_LABEL_TO_BOTTOM = 0x02;
private static final int LABEL_FLAGS_ALIGN_ICON_TO_BOTTOM = 0x04;
private static final int LABEL_FLAGS_ALIGN_LABEL_OFF_CENTER = 0x08;
// Font typeface specification.
private static final int LABEL_FLAGS_FONT_MASK = 0x30;
private static final int LABEL_FLAGS_FONT_NORMAL = 0x10;
private static final int LABEL_FLAGS_FONT_MONO_SPACE = 0x20;
private static final int LABEL_FLAGS_FONT_DEFAULT = 0x30;
// Start of key text ratio enum values
private static final int LABEL_FLAGS_FOLLOW_KEY_TEXT_RATIO_MASK = 0x1C0;
private static final int LABEL_FLAGS_FOLLOW_KEY_LARGE_LETTER_RATIO = 0x40;
private static final int LABEL_FLAGS_FOLLOW_KEY_LETTER_RATIO = 0x80;
private static final int LABEL_FLAGS_FOLLOW_KEY_LABEL_RATIO = 0xC0;
private static final int LABEL_FLAGS_FOLLOW_KEY_HINT_LABEL_RATIO = 0x140;
// End of key text ratio mask enum values
private static final int LABEL_FLAGS_HAS_SHIFTED_LETTER_HINT = 0x400;
private static final int LABEL_FLAGS_HAS_HINT_LABEL = 0x800;
// The bit to calculate the ratio of key label width against key width. If autoXScale bit is on
// and autoYScale bit is off, the key label may be shrunk only for X-direction.
// If both autoXScale and autoYScale bits are on, the key label text size may be auto scaled.
private static final int LABEL_FLAGS_AUTO_X_SCALE = 0x4000;
private static final int LABEL_FLAGS_AUTO_Y_SCALE = 0x8000;
private static final int LABEL_FLAGS_AUTO_SCALE = LABEL_FLAGS_AUTO_X_SCALE
| LABEL_FLAGS_AUTO_Y_SCALE;
private static final int LABEL_FLAGS_PRESERVE_CASE = 0x10000;
private static final int LABEL_FLAGS_SHIFTED_LETTER_ACTIVATED = 0x20000;
private static final int LABEL_FLAGS_FROM_CUSTOM_ACTION_LABEL = 0x40000;
private static final int LABEL_FLAGS_FOLLOW_FUNCTIONAL_TEXT_COLOR = 0x80000;
private static final int LABEL_FLAGS_DISABLE_HINT_LABEL = 0x40000000;
private static final int LABEL_FLAGS_DISABLE_ADDITIONAL_MORE_KEYS = 0x80000000;
/** Icon to display instead of a label. Icon takes precedence over a label */
private final int mIconId;
/** Width of the key, excluding the padding */
private final int mWidth;
/** Height of the key, excluding the padding */
private final int mHeight;
/** Exact theoretical width of the key, excluding the padding */
private final float mDefinedWidth;
/** Exact theoretical height of the key, excluding the padding */
private final float mDefinedHeight;
/** X coordinate of the top-left corner of the key in the keyboard layout, excluding the
* padding. */
private final int mX;
/** Y coordinate of the top-left corner of the key in the keyboard layout, excluding the
* padding. */
private final int mY;
/** Hit bounding box of the key */
private final Rect mHitbox = new Rect();
/** More keys. It is guaranteed that this is null or an array of one or more elements */
private final MoreKeySpec[] mMoreKeys;
/** More keys column number and flags */
private final int mMoreKeysColumnAndFlags;
private static final int MORE_KEYS_COLUMN_NUMBER_MASK = 0x000000ff;
// If this flag is specified, more keys keyboard should have the specified number of columns.
// Otherwise more keys keyboard should have less than or equal to the specified maximum number
// of columns.
private static final int MORE_KEYS_FLAGS_FIXED_COLUMN = 0x00000100;
// If this flag is specified, the order of more keys is determined by the order in the more
// keys' specification. Otherwise the order of more keys is automatically determined.
private static final int MORE_KEYS_FLAGS_FIXED_ORDER = 0x00000200;
private static final int MORE_KEYS_MODE_MAX_COLUMN_WITH_AUTO_ORDER = 0;
private static final int MORE_KEYS_MODE_FIXED_COLUMN_WITH_AUTO_ORDER =
MORE_KEYS_FLAGS_FIXED_COLUMN;
private static final int MORE_KEYS_MODE_FIXED_COLUMN_WITH_FIXED_ORDER =
(MORE_KEYS_FLAGS_FIXED_COLUMN | MORE_KEYS_FLAGS_FIXED_ORDER);
private static final int MORE_KEYS_FLAGS_HAS_LABELS = 0x40000000;
private static final int MORE_KEYS_FLAGS_NO_PANEL_AUTO_MORE_KEY = 0x10000000;
// TODO: Rename these specifiers to !autoOrder! and !fixedOrder! respectively.
private static final String MORE_KEYS_AUTO_COLUMN_ORDER = "!autoColumnOrder!";
private static final String MORE_KEYS_FIXED_COLUMN_ORDER = "!fixedColumnOrder!";
private static final String MORE_KEYS_HAS_LABELS = "!hasLabels!";
private static final String MORE_KEYS_NO_PANEL_AUTO_MORE_KEY = "!noPanelAutoMoreKey!";
/** Background type that represents different key background visual than normal one. */
private final int mBackgroundType;
public static final int BACKGROUND_TYPE_EMPTY = 0;
public static final int BACKGROUND_TYPE_NORMAL = 1;
public static final int BACKGROUND_TYPE_FUNCTIONAL = 2;
public static final int BACKGROUND_TYPE_ACTION = 5;
public static final int BACKGROUND_TYPE_SPACEBAR = 6;
private final int mActionFlags;
private static final int ACTION_FLAGS_IS_REPEATABLE = 0x01;
private static final int ACTION_FLAGS_NO_KEY_PREVIEW = 0x02;
private static final int ACTION_FLAGS_ALT_CODE_WHILE_TYPING = 0x04;
private static final int ACTION_FLAGS_ENABLE_LONG_PRESS = 0x08;
private final KeyVisualAttributes mKeyVisualAttributes;
private final OptionalAttributes mOptionalAttributes;
private static final class OptionalAttributes {
/** Text to output when pressed. This can be multiple characters, like ".com" */
public final String mOutputText;
public final int mAltCode;
private OptionalAttributes(final String outputText, final int altCode) {
mOutputText = outputText;
mAltCode = altCode;
}
public static OptionalAttributes newInstance(final String outputText, final int altCode) {
if (outputText == null && altCode == CODE_UNSPECIFIED) {
return null;
}
return new OptionalAttributes(outputText, altCode);
}
}
private final int mHashCode;
/** The current pressed state of this key */
private boolean mPressed;
/**
* Constructor for a key on <code>MoreKeyKeyboard</code>.
*/
public Key(final String label, final int iconId, final int code, final String outputText,
final String hintLabel, final int labelFlags, final int backgroundType,
final float x, final float y, final float width, final float height,
final float leftPadding, final float rightPadding, final float topPadding,
final float bottomPadding) {
mHitbox.set(Math.round(x - leftPadding), Math.round(y - topPadding),
Math.round(x + width + rightPadding), Math.round(y + height + bottomPadding));
mX = Math.round(x);
mY = Math.round(y);
mWidth = Math.round(x + width) - mX;
mHeight = Math.round(y + height) - mY;
mDefinedWidth = width;
mDefinedHeight = height;
mHintLabel = hintLabel;
mLabelFlags = labelFlags;
mBackgroundType = backgroundType;
// TODO: Pass keyActionFlags as an argument.
mActionFlags = ACTION_FLAGS_NO_KEY_PREVIEW;
mMoreKeys = null;
mMoreKeysColumnAndFlags = 0;
mLabel = label;
mOptionalAttributes = OptionalAttributes.newInstance(outputText, CODE_UNSPECIFIED);
mCode = code;
mIconId = iconId;
mKeyVisualAttributes = null;
mHashCode = computeHashCode(this);
}
/**
* Create a key with the given top-left coordinate and extract its attributes from a key
* specification string, Key attribute array, key style, and etc.
*
* @param keySpec the key specification.
* @param keyAttr the Key XML attributes array.
* @param style the {@link KeyStyle} of this key.
* @param params the keyboard building parameters.
* @param row the row that this key belongs to. row's x-coordinate will be the right edge of
* this key.
*/
public Key(final String keySpec, final TypedArray keyAttr,
final KeyStyle style, final KeyboardParams params,
final KeyboardRow row) {
// Update the row to work with the new key
row.setCurrentKey(keyAttr, isSpacer());
mDefinedWidth = row.getKeyWidth();
mDefinedHeight = row.getKeyHeight();
final float keyLeft = row.getKeyX();
final float keyTop = row.getKeyY();
final float keyRight = keyLeft + mDefinedWidth;
final float keyBottom = keyTop + mDefinedHeight;
final float leftPadding = row.getKeyLeftPadding();
final float topPadding = row.getKeyTopPadding();
final float rightPadding = row.getKeyRightPadding();
final float bottomPadding = row.getKeyBottomPadding();
mHitbox.set(Math.round(keyLeft - leftPadding), Math.round(keyTop - topPadding),
Math.round(keyRight + rightPadding), Math.round(keyBottom + bottomPadding));
mX = Math.round(keyLeft);
mY = Math.round(keyTop);
mWidth = Math.round(keyRight) - mX;
mHeight = Math.round(keyBottom) - mY;
mBackgroundType = style.getInt(keyAttr,
R.styleable.Keyboard_Key_backgroundType, row.getDefaultBackgroundType());
mLabelFlags = style.getFlags(keyAttr, R.styleable.Keyboard_Key_keyLabelFlags)
| row.getDefaultKeyLabelFlags();
final boolean needsToUpcase = needsToUpcase(mLabelFlags, params.mId.mElementId);
final Locale localeForUpcasing = params.mId.getLocale();
int actionFlags = style.getFlags(keyAttr, R.styleable.Keyboard_Key_keyActionFlags);
String[] moreKeys = style.getStringArray(keyAttr, R.styleable.Keyboard_Key_moreKeys);
// Get maximum column order number and set a relevant mode value.
int moreKeysColumnAndFlags = MORE_KEYS_MODE_MAX_COLUMN_WITH_AUTO_ORDER
| style.getInt(keyAttr, R.styleable.Keyboard_Key_maxMoreKeysColumn,
params.mMaxMoreKeysKeyboardColumn);
int value;
if ((value = MoreKeySpec.getIntValue(moreKeys, MORE_KEYS_AUTO_COLUMN_ORDER, -1)) > 0) {
// Override with fixed column order number and set a relevant mode value.
moreKeysColumnAndFlags = MORE_KEYS_MODE_FIXED_COLUMN_WITH_AUTO_ORDER
| (value & MORE_KEYS_COLUMN_NUMBER_MASK);
}
if ((value = MoreKeySpec.getIntValue(moreKeys, MORE_KEYS_FIXED_COLUMN_ORDER, -1)) > 0) {
// Override with fixed column order number and set a relevant mode value.
moreKeysColumnAndFlags = MORE_KEYS_MODE_FIXED_COLUMN_WITH_FIXED_ORDER
| (value & MORE_KEYS_COLUMN_NUMBER_MASK);
}
if (MoreKeySpec.getBooleanValue(moreKeys, MORE_KEYS_HAS_LABELS)) {
moreKeysColumnAndFlags |= MORE_KEYS_FLAGS_HAS_LABELS;
}
if (MoreKeySpec.getBooleanValue(moreKeys, MORE_KEYS_NO_PANEL_AUTO_MORE_KEY)) {
moreKeysColumnAndFlags |= MORE_KEYS_FLAGS_NO_PANEL_AUTO_MORE_KEY;
}
mMoreKeysColumnAndFlags = moreKeysColumnAndFlags;
final String[] additionalMoreKeys;
if ((mLabelFlags & LABEL_FLAGS_DISABLE_ADDITIONAL_MORE_KEYS) != 0) {
additionalMoreKeys = null;
} else {
additionalMoreKeys = style.getStringArray(keyAttr,
R.styleable.Keyboard_Key_additionalMoreKeys);
}
moreKeys = MoreKeySpec.insertAdditionalMoreKeys(moreKeys, additionalMoreKeys);
if (moreKeys != null) {
actionFlags |= ACTION_FLAGS_ENABLE_LONG_PRESS;
mMoreKeys = new MoreKeySpec[moreKeys.length];
for (int i = 0; i < moreKeys.length; i++) {
mMoreKeys[i] = new MoreKeySpec(moreKeys[i], needsToUpcase, localeForUpcasing);
}
} else {
mMoreKeys = null;
}
mActionFlags = actionFlags;
mIconId = KeySpecParser.getIconId(keySpec);
final int code = KeySpecParser.getCode(keySpec);
if ((mLabelFlags & LABEL_FLAGS_FROM_CUSTOM_ACTION_LABEL) != 0) {
mLabel = params.mId.mCustomActionLabel;
} else if (code >= Character.MIN_SUPPLEMENTARY_CODE_POINT) {
// This is a workaround to have a key that has a supplementary code point in its label.
// Because we can put a string in resource neither as a XML entity of a supplementary
// code point nor as a surrogate pair.
mLabel = new StringBuilder().appendCodePoint(code).toString();
} else {
final String label = KeySpecParser.getLabel(keySpec);
mLabel = needsToUpcase
? StringUtils.toTitleCaseOfKeyLabel(label, localeForUpcasing)
: label;
}
if ((mLabelFlags & LABEL_FLAGS_DISABLE_HINT_LABEL) != 0) {
mHintLabel = null;
} else {
final String hintLabel = style.getString(
keyAttr, R.styleable.Keyboard_Key_keyHintLabel);
mHintLabel = needsToUpcase
? StringUtils.toTitleCaseOfKeyLabel(hintLabel, localeForUpcasing)
: hintLabel;
}
String outputText = KeySpecParser.getOutputText(keySpec);
if (needsToUpcase) {
outputText = StringUtils.toTitleCaseOfKeyLabel(outputText, localeForUpcasing);
}
// Choose the first letter of the label as primary code if not specified.
if (code == CODE_UNSPECIFIED && TextUtils.isEmpty(outputText)
&& !TextUtils.isEmpty(mLabel)) {
if (StringUtils.codePointCount(mLabel) == 1) {
// Use the first letter of the hint label if shiftedLetterActivated flag is
// specified.
if (hasShiftedLetterHint() && isShiftedLetterActivated()) {
mCode = mHintLabel.codePointAt(0);
} else {
mCode = mLabel.codePointAt(0);
}
} else {
// In some locale and case, the character might be represented by multiple code
// points, such as upper case Eszett of German alphabet.
outputText = mLabel;
mCode = CODE_OUTPUT_TEXT;
}
} else if (code == CODE_UNSPECIFIED && outputText != null) {
if (StringUtils.codePointCount(outputText) == 1) {
mCode = outputText.codePointAt(0);
outputText = null;
} else {
mCode = CODE_OUTPUT_TEXT;
}
} else {
mCode = needsToUpcase ? StringUtils.toTitleCaseOfKeyCode(code, localeForUpcasing)
: code;
}
final int altCodeInAttr = KeySpecParser.parseCode(
style.getString(keyAttr, R.styleable.Keyboard_Key_altCode), CODE_UNSPECIFIED);
final int altCode = needsToUpcase
? StringUtils.toTitleCaseOfKeyCode(altCodeInAttr, localeForUpcasing)
: altCodeInAttr;
mOptionalAttributes = OptionalAttributes.newInstance(outputText, altCode);
mKeyVisualAttributes = KeyVisualAttributes.newInstance(keyAttr);
mHashCode = computeHashCode(this);
}
/**
* Copy constructor for DynamicGridKeyboard.GridKey.
*
* @param key the original key.
*/
protected Key(final Key key) {
this(key, key.mMoreKeys);
}
private Key(final Key key,final MoreKeySpec[] moreKeys) {
// Final attributes.
mCode = key.mCode;
mLabel = key.mLabel;
mHintLabel = key.mHintLabel;
mLabelFlags = key.mLabelFlags;
mIconId = key.mIconId;
mWidth = key.mWidth;
mHeight = key.mHeight;
mDefinedWidth = key.mDefinedWidth;
mDefinedHeight = key.mDefinedHeight;
mX = key.mX;
mY = key.mY;
mHitbox.set(key.mHitbox);
mMoreKeys = moreKeys;
mMoreKeysColumnAndFlags = key.mMoreKeysColumnAndFlags;
mBackgroundType = key.mBackgroundType;
mActionFlags = key.mActionFlags;
mKeyVisualAttributes = key.mKeyVisualAttributes;
mOptionalAttributes = key.mOptionalAttributes;
mHashCode = key.mHashCode;
// Key state.
mPressed = key.mPressed;
}
public static Key removeRedundantMoreKeys(final Key key,
final MoreKeySpec.LettersOnBaseLayout lettersOnBaseLayout) {
final MoreKeySpec[] moreKeys = key.getMoreKeys();
final MoreKeySpec[] filteredMoreKeys = MoreKeySpec.removeRedundantMoreKeys(
moreKeys, lettersOnBaseLayout);
return (filteredMoreKeys == moreKeys) ? key : new Key(key, filteredMoreKeys);
}
private static boolean needsToUpcase(final int labelFlags, final int keyboardElementId) {
if ((labelFlags & LABEL_FLAGS_PRESERVE_CASE) != 0) return false;
switch (keyboardElementId) {
case KeyboardId.ELEMENT_ALPHABET_MANUAL_SHIFTED:
case KeyboardId.ELEMENT_ALPHABET_AUTOMATIC_SHIFTED:
case KeyboardId.ELEMENT_ALPHABET_SHIFT_LOCKED:
return true;
default:
return false;
}
}
private static int computeHashCode(final Key key) {
return Arrays.hashCode(new Object[] {
key.mX,
key.mY,
key.mWidth,
key.mHeight,
key.mCode,
key.mLabel,
key.mHintLabel,
key.mIconId,
key.mBackgroundType,
Arrays.hashCode(key.mMoreKeys),
key.getOutputText(),
key.mActionFlags,
key.mLabelFlags,
// Key can be distinguishable without the following members.
// key.mOptionalAttributes.mAltCode,
// key.mOptionalAttributes.mDisabledIconId,
// key.mOptionalAttributes.mPreviewIconId,
// key.mMaxMoreKeysColumn,
// key.mDefinedHeight,
// key.mDefinedWidth,
});
}
private boolean equalsInternal(final Key o) {
if (this == o) return true;
return o.mX == mX
&& o.mY == mY
&& o.mWidth == mWidth
&& o.mHeight == mHeight
&& o.mCode == mCode
&& TextUtils.equals(o.mLabel, mLabel)
&& TextUtils.equals(o.mHintLabel, mHintLabel)
&& o.mIconId == mIconId
&& o.mBackgroundType == mBackgroundType
&& Arrays.equals(o.mMoreKeys, mMoreKeys)
&& TextUtils.equals(o.getOutputText(), getOutputText())
&& o.mActionFlags == mActionFlags
&& o.mLabelFlags == mLabelFlags;
}
@Override
public int compareTo(Key o) {
if (equalsInternal(o)) return 0;
if (mHashCode > o.mHashCode) return 1;
return -1;
}
@Override
public int hashCode() {
return mHashCode;
}
@Override
public boolean equals(final Object o) {
return o instanceof Key && equalsInternal((Key)o);
}
@Override
public String toString() {
return toShortString() + " " + getX() + "," + getY() + " " + getWidth() + "x" + getHeight();
}
public String toShortString() {
final int code = getCode();
if (code == CODE_OUTPUT_TEXT) {
return getOutputText();
}
return Constants.printableCode(code);
}
public int getCode() {
return mCode;
}
public String getLabel() {
return mLabel;
}
public String getHintLabel() {
return mHintLabel;
}
public MoreKeySpec[] getMoreKeys() {
return mMoreKeys;
}
public void setHitboxRightEdge(final int right) {
mHitbox.right = right;
}
public final boolean isSpacer() {
return this instanceof Spacer;
}
public final boolean isActionKey() {
return mBackgroundType == BACKGROUND_TYPE_ACTION;
}
public final boolean isShift() {
return mCode == CODE_SHIFT;
}
public final boolean isModifier() {
return mCode == CODE_SHIFT || mCode == CODE_SWITCH_ALPHA_SYMBOL;
}
public final boolean isRepeatable() {
return (mActionFlags & ACTION_FLAGS_IS_REPEATABLE) != 0;
}
public final boolean noKeyPreview() {
return (mActionFlags & ACTION_FLAGS_NO_KEY_PREVIEW) != 0;
}
public final boolean altCodeWhileTyping() {
return (mActionFlags & ACTION_FLAGS_ALT_CODE_WHILE_TYPING) != 0;
}
public final boolean isLongPressEnabled() {
// We need not start long press timer on the key which has activated shifted letter.
return (mActionFlags & ACTION_FLAGS_ENABLE_LONG_PRESS) != 0
&& (mLabelFlags & LABEL_FLAGS_SHIFTED_LETTER_ACTIVATED) == 0;
}
public KeyVisualAttributes getVisualAttributes() {
return mKeyVisualAttributes;
}
public final Typeface selectTypeface(final KeyDrawParams params) {
switch (mLabelFlags & LABEL_FLAGS_FONT_MASK) {
case LABEL_FLAGS_FONT_NORMAL:
return Typeface.DEFAULT;
case LABEL_FLAGS_FONT_MONO_SPACE:
return Typeface.MONOSPACE;
case LABEL_FLAGS_FONT_DEFAULT:
default:
// The type-face is specified by keyTypeface attribute.
return params.mTypeface;
}
}
public final int selectTextSize(final KeyDrawParams params) {
switch (mLabelFlags & LABEL_FLAGS_FOLLOW_KEY_TEXT_RATIO_MASK) {
case LABEL_FLAGS_FOLLOW_KEY_LETTER_RATIO:
return params.mLetterSize;
case LABEL_FLAGS_FOLLOW_KEY_LARGE_LETTER_RATIO:
return params.mLargeLetterSize;
case LABEL_FLAGS_FOLLOW_KEY_LABEL_RATIO:
return params.mLabelSize;
case LABEL_FLAGS_FOLLOW_KEY_HINT_LABEL_RATIO:
return params.mHintLabelSize;
default: // No follow key ratio flag specified.
return StringUtils.codePointCount(mLabel) == 1 ? params.mLetterSize : params.mLabelSize;
}
}
public final int selectTextColor(final KeyDrawParams params) {
if ((mLabelFlags & LABEL_FLAGS_FOLLOW_FUNCTIONAL_TEXT_COLOR) != 0) {
return params.mFunctionalTextColor;
}
return isShiftedLetterActivated() ? params.mTextInactivatedColor : params.mTextColor;
}
public final int selectHintTextSize(final KeyDrawParams params) {
if (hasHintLabel()) {
return params.mHintLabelSize;
}
if (hasShiftedLetterHint()) {
return params.mShiftedLetterHintSize;
}
return params.mHintLetterSize;
}
public final int selectHintTextColor(final KeyDrawParams params) {
if (hasHintLabel()) {
return params.mHintLabelColor;
}
if (hasShiftedLetterHint()) {
return isShiftedLetterActivated() ? params.mShiftedLetterHintActivatedColor
: params.mShiftedLetterHintInactivatedColor;
}
return params.mHintLetterColor;
}
public final String getPreviewLabel() {
return isShiftedLetterActivated() ? mHintLabel : mLabel;
}
private boolean previewHasLetterSize() {
return (mLabelFlags & LABEL_FLAGS_FOLLOW_KEY_LETTER_RATIO) != 0
|| StringUtils.codePointCount(getPreviewLabel()) == 1;
}
public final int selectPreviewTextSize(final KeyDrawParams params) {
if (previewHasLetterSize()) {
return params.mPreviewTextSize;
}
return params.mLetterSize;
}
public Typeface selectPreviewTypeface(final KeyDrawParams params) {
if (previewHasLetterSize()) {
return selectTypeface(params);
}
return Typeface.DEFAULT_BOLD;
}
public final boolean isAlignHintLabelToBottom(final int defaultFlags) {
return ((mLabelFlags | defaultFlags) & LABEL_FLAGS_ALIGN_HINT_LABEL_TO_BOTTOM) != 0;
}
public final boolean isAlignIconToBottom() {
return (mLabelFlags & LABEL_FLAGS_ALIGN_ICON_TO_BOTTOM) != 0;
}
public final boolean isAlignLabelOffCenter() {
return (mLabelFlags & LABEL_FLAGS_ALIGN_LABEL_OFF_CENTER) != 0;
}
public final boolean hasShiftedLetterHint() {
return (mLabelFlags & LABEL_FLAGS_HAS_SHIFTED_LETTER_HINT) != 0
&& !TextUtils.isEmpty(mHintLabel);
}
public final boolean hasHintLabel() {
return (mLabelFlags & LABEL_FLAGS_HAS_HINT_LABEL) != 0;
}
public final boolean needsAutoXScale() {
return (mLabelFlags & LABEL_FLAGS_AUTO_X_SCALE) != 0;
}
public final boolean needsAutoScale() {
return (mLabelFlags & LABEL_FLAGS_AUTO_SCALE) == LABEL_FLAGS_AUTO_SCALE;
}
private final boolean isShiftedLetterActivated() {
return (mLabelFlags & LABEL_FLAGS_SHIFTED_LETTER_ACTIVATED) != 0
&& !TextUtils.isEmpty(mHintLabel);
}
public final int getMoreKeysColumnNumber() {
return mMoreKeysColumnAndFlags & MORE_KEYS_COLUMN_NUMBER_MASK;
}
public final boolean isMoreKeysFixedColumn() {
return (mMoreKeysColumnAndFlags & MORE_KEYS_FLAGS_FIXED_COLUMN) != 0;
}
public final boolean isMoreKeysFixedOrder() {
return (mMoreKeysColumnAndFlags & MORE_KEYS_FLAGS_FIXED_ORDER) != 0;
}
public final boolean hasLabelsInMoreKeys() {
return (mMoreKeysColumnAndFlags & MORE_KEYS_FLAGS_HAS_LABELS) != 0;
}
public final int getMoreKeyLabelFlags() {
final int labelSizeFlag = hasLabelsInMoreKeys()
? LABEL_FLAGS_FOLLOW_KEY_LABEL_RATIO
: LABEL_FLAGS_FOLLOW_KEY_LETTER_RATIO;
return labelSizeFlag | LABEL_FLAGS_AUTO_X_SCALE;
}
public final boolean hasNoPanelAutoMoreKey() {
return (mMoreKeysColumnAndFlags & MORE_KEYS_FLAGS_NO_PANEL_AUTO_MORE_KEY) != 0;
}
public final String getOutputText() {
final OptionalAttributes attrs = mOptionalAttributes;
return (attrs != null) ? attrs.mOutputText : null;
}
public final int getAltCode() {
final OptionalAttributes attrs = mOptionalAttributes;
return (attrs != null) ? attrs.mAltCode : CODE_UNSPECIFIED;
}
public int getIconId() {
return mIconId;
}
public Drawable getIcon(final KeyboardIconsSet iconSet, final int alpha) {
final Drawable icon = iconSet.getIconDrawable(getIconId());
if (icon != null) {
icon.setAlpha(alpha);
}
return icon;
}
public Drawable getPreviewIcon(final KeyboardIconsSet iconSet) {
return iconSet.getIconDrawable(getIconId());
}
/**
* Gets the width of the key in pixels, excluding the padding.
* @return The width of the key in pixels, excluding the padding.
*/
public int getWidth() {
return mWidth;
}
/**
* Gets the height of the key in pixels, excluding the padding.
* @return The height of the key in pixels, excluding the padding.
*/
public int getHeight() {
return mHeight;
}
/**
* Gets the theoretical width of the key in pixels, excluding the padding. This is the exact
* width that the key was defined to be, but this will likely differ from the actual drawn width
* because the normal (drawn/functional) width was determined by rounding the left and right
* edge to fit evenly in a pixel.
* @return The defined width of the key in pixels, excluding the padding.
*/
public float getDefinedWidth() {
return mDefinedWidth;
}
/**
* Gets the theoretical height of the key in pixels, excluding the padding. This is the exact
* height that the key was defined to be, but this will likely differ from the actual drawn
* height because the normal (drawn/functional) width was determined by rounding the top and
* bottom edge to fit evenly in a pixel.
* @return The defined width of the key in pixels, excluding the padding.
*/
public float getDefinedHeight() {
return mDefinedHeight;
}
/**
* Gets the x-coordinate of the top-left corner of the key in pixels, excluding the padding.
* @return The x-coordinate of the top-left corner of the key in pixels, excluding the padding.
*/
public int getX() {
return mX;
}
/**
* Gets the y-coordinate of the top-left corner of the key in pixels, excluding the padding.
* @return The y-coordinate of the top-left corner of the key in pixels, excluding the padding.
*/
public int getY() {
return mY;
}
/**
* Gets the amount of padding for the hitbox above the key's visible position.
* @return The hitbox padding above the key.
*/
public int getTopPadding() {
return mY - mHitbox.top;
}
/**
* Gets the amount of padding for the hitbox below the key's visible position.
* @return The hitbox padding below the key.
*/
public int getBottomPadding() {
return mHitbox.bottom - mY - mHeight;
}
/**
* Gets the amount of padding for the hitbox to the left of the key's visible position.
* @return The hitbox padding to the left of the key.
*/
public int getLeftPadding() {
return mX - mHitbox.left;
}
/**
* Gets the amount of padding for the hitbox to the right of the key's visible position.
* @return The hitbox padding to the right of the key.
*/
public int getRightPadding() {
return mHitbox.right - mX - mWidth;
}
/**
* Informs the key that it has been pressed, in case it needs to change its appearance or
* state.
* @see #onReleased()
*/
public void onPressed() {
mPressed = true;
}
/**
* Informs the key that it has been released, in case it needs to change its appearance or
* state.
* @see #onPressed()
*/
public void onReleased() {
mPressed = false;
}
/**
* Detects if a point falls on 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 on the key. This generally includes all points
* between the key and the keyboard edge for keys attached to an edge and all points between
* the key and halfway to adjacent keys.
*/
public boolean isOnKey(final int x, final int y) {
return mHitbox.contains(x, y);
}
/**
* Returns the square of the distance to the nearest clickable edge 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 nearest edge of the key
*/
public int squaredDistanceToHitboxEdge(final int x, final int y) {
final int left = mHitbox.left;
// The hit box right is exclusive
final int right = mHitbox.right - 1;
final int top = mHitbox.top;
// The hit box bottom is exclusive
final int bottom = mHitbox.bottom - 1;
final int edgeX = x < left ? left : Math.min(x, right);
final int edgeY = y < top ? top : Math.min(y, bottom);
final int dx = x - edgeX;
final int dy = y - edgeY;
return dx * dx + dy * dy;
}
static class KeyBackgroundState {
private final int[] mReleasedState;
private final int[] mPressedState;
private KeyBackgroundState(final int ... attrs) {
mReleasedState = attrs;
mPressedState = Arrays.copyOf(attrs, attrs.length + 1);
mPressedState[attrs.length] = android.R.attr.state_pressed;
}
public int[] getState(final boolean pressed) {
return pressed ? mPressedState : mReleasedState;
}
public static final KeyBackgroundState[] STATES = {
// 0: BACKGROUND_TYPE_EMPTY
new KeyBackgroundState(android.R.attr.state_empty),
// 1: BACKGROUND_TYPE_NORMAL
new KeyBackgroundState(),
// 2: BACKGROUND_TYPE_FUNCTIONAL
new KeyBackgroundState(),
// 3: BACKGROUND_TYPE_STICKY_OFF
new KeyBackgroundState(android.R.attr.state_checkable),
// 4: BACKGROUND_TYPE_STICKY_ON
new KeyBackgroundState(android.R.attr.state_checkable, android.R.attr.state_checked),
// 5: BACKGROUND_TYPE_ACTION
new KeyBackgroundState(android.R.attr.state_active),
// 6: BACKGROUND_TYPE_SPACEBAR
new KeyBackgroundState(),
};
}
/**
* Returns the background drawable for the key, based on the current state and type of the key.
* @return the background drawable of the key.
* @see android.graphics.drawable.StateListDrawable#setState(int[])
*/
public final Drawable selectBackgroundDrawable(final Drawable keyBackground,
final Drawable functionalKeyBackground,
final Drawable spacebarBackground) {
final Drawable background;
if (mBackgroundType == BACKGROUND_TYPE_FUNCTIONAL) {
background = functionalKeyBackground;
} else if (mBackgroundType == BACKGROUND_TYPE_SPACEBAR) {
background = spacebarBackground;
} else {
background = keyBackground;
}
final int[] state = KeyBackgroundState.STATES[mBackgroundType].getState(mPressed);
background.setState(state);
return background;
}
public static class Spacer extends Key {
public Spacer(final TypedArray keyAttr, final KeyStyle keyStyle,
final KeyboardParams params, final KeyboardRow row) {
super(null /* keySpec */, keyAttr, keyStyle, params, row);
}
}
}

View File

@ -0,0 +1,102 @@
/*
* Copyright (C) 2010 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.colorful.keyboard.theme.keyboard;
/**
* This class handles key detection.
*/
public class KeyDetector {
private final int mKeyHysteresisDistanceSquared;
private final int mKeyHysteresisDistanceForSlidingModifierSquared;
private Keyboard mKeyboard;
private int mCorrectionX;
private int mCorrectionY;
public KeyDetector() {
this(0.0f /* keyHysteresisDistance */, 0.0f /* keyHysteresisDistanceForSlidingModifier */);
}
/**
* Key detection object constructor with key hysteresis distances.
*
* @param keyHysteresisDistance if the pointer movement distance is smaller than this, the
* movement will not be handled as meaningful movement. The unit is pixel.
* @param keyHysteresisDistanceForSlidingModifier the same parameter for sliding input that
* starts from a modifier key such as shift and symbols key.
*/
public KeyDetector(final float keyHysteresisDistance,
final float keyHysteresisDistanceForSlidingModifier) {
mKeyHysteresisDistanceSquared = (int)(keyHysteresisDistance * keyHysteresisDistance);
mKeyHysteresisDistanceForSlidingModifierSquared = (int)(
keyHysteresisDistanceForSlidingModifier * keyHysteresisDistanceForSlidingModifier);
}
public void setKeyboard(final Keyboard keyboard, final float correctionX,
final float correctionY) {
if (keyboard == null) {
throw new NullPointerException();
}
mCorrectionX = (int)correctionX;
mCorrectionY = (int)correctionY;
mKeyboard = keyboard;
}
public int getKeyHysteresisDistanceSquared(final boolean isSlidingFromModifier) {
return isSlidingFromModifier
? mKeyHysteresisDistanceForSlidingModifierSquared : mKeyHysteresisDistanceSquared;
}
public int getTouchX(final int x) {
return x + mCorrectionX;
}
// TODO: Remove vertical correction.
public int getTouchY(final int y) {
return y + mCorrectionY;
}
public Keyboard getKeyboard() {
return mKeyboard;
}
public boolean alwaysAllowsKeySelectionByDraggingFinger() {
return false;
}
/**
* Detect the key whose hitbox the touch point is in.
*
* @param x The x-coordinate of a touch point
* @param y The y-coordinate of a touch point
* @return the key that the touch point hits.
*/
public Key detectHitKey(final int x, final int y) {
if (mKeyboard == null) {
return null;
}
final int touchX = getTouchX(x);
final int touchY = getTouchY(y);
for (final Key key: mKeyboard.getNearestKeys(touchX, touchY)) {
if (key.isOnKey(touchX, touchY)) {
return key;
}
}
return null;
}
}

View File

@ -0,0 +1,166 @@
/*
* Copyright (C) 2010 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.colorful.keyboard.theme.keyboard;
import android.util.SparseArray;
import com.colorful.keyboard.theme.keyboard.internal.KeyVisualAttributes;
import com.colorful.keyboard.theme.keyboard.internal.KeyboardIconsSet;
import com.colorful.keyboard.theme.keyboard.internal.KeyboardParams;
import com.colorful.keyboard.theme.latin.common.Constants;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
/**
* 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
* latin:keyWidth="10%p"
* latin:rowHeight="50px"
* latin:horizontalGap="2%p"
* latin:verticalGap="2%p" &gt;
* &lt;Row latin:keyWidth="10%p" &gt;
* &lt;Key latin:keyLabel="A" /&gt;
* ...
* &lt;/Row&gt;
* ...
* &lt;/Keyboard&gt;
* </pre>
*/
public class Keyboard {
public final KeyboardId mId;
/** Total height of the keyboard, including the padding and keys */
public final int mOccupiedHeight;
/** Total width of the keyboard, including the padding and keys */
public final int mOccupiedWidth;
/** The padding below the keyboard */
public final float mBottomPadding;
/** Default gap between rows */
public final float mVerticalGap;
/** Default gap between columns */
public final float mHorizontalGap;
/** Per keyboard key visual parameters */
public final KeyVisualAttributes mKeyVisualAttributes;
public final int mMostCommonKeyHeight;
public final int mMostCommonKeyWidth;
/** More keys keyboard template */
public final int mMoreKeysTemplate;
/** List of keys in this keyboard */
private final List<Key> mSortedKeys;
public final List<Key> mShiftKeys;
public final List<Key> mAltCodeKeysWhileTyping;
public final KeyboardIconsSet mIconsSet;
private final SparseArray<Key> mKeyCache = new SparseArray<>();
private final ProximityInfo mProximityInfo;
public Keyboard(final KeyboardParams params) {
mId = params.mId;
mOccupiedHeight = params.mOccupiedHeight;
mOccupiedWidth = params.mOccupiedWidth;
mMostCommonKeyHeight = params.mMostCommonKeyHeight;
mMostCommonKeyWidth = params.mMostCommonKeyWidth;
mMoreKeysTemplate = params.mMoreKeysTemplate;
mKeyVisualAttributes = params.mKeyVisualAttributes;
mBottomPadding = params.mBottomPadding;
mVerticalGap = params.mVerticalGap;
mHorizontalGap = params.mHorizontalGap;
mSortedKeys = Collections.unmodifiableList(new ArrayList<>(params.mSortedKeys));
mShiftKeys = Collections.unmodifiableList(params.mShiftKeys);
mAltCodeKeysWhileTyping = Collections.unmodifiableList(params.mAltCodeKeysWhileTyping);
mIconsSet = params.mIconsSet;
mProximityInfo = new ProximityInfo(params.mGridWidth, params.mGridHeight,
mOccupiedWidth, mOccupiedHeight, mSortedKeys);
}
/**
* Return the sorted list of keys of this keyboard.
* The keys are sorted from top-left to bottom-right order.
* The list may contain {@link Key.Spacer} object as well.
* @return the sorted unmodifiable list of {@link Key}s of this keyboard.
*/
public List<Key> getSortedKeys() {
return mSortedKeys;
}
public Key getKey(final int code) {
if (code == Constants.CODE_UNSPECIFIED) {
return null;
}
synchronized (mKeyCache) {
final int index = mKeyCache.indexOfKey(code);
if (index >= 0) {
return mKeyCache.valueAt(index);
}
for (final Key key : getSortedKeys()) {
if (key.getCode() == code) {
mKeyCache.put(code, key);
return key;
}
}
mKeyCache.put(code, null);
return null;
}
}
public boolean hasKey(final Key aKey) {
if (mKeyCache.indexOfValue(aKey) >= 0) {
return true;
}
for (final Key key : getSortedKeys()) {
if (key == aKey) {
mKeyCache.put(key.getCode(), key);
return true;
}
}
return false;
}
@Override
public String toString() {
return mId.toString();
}
/**
* Returns the array 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 list of the nearest keys to the given point. If the given
* point is out of range, then an array of size zero is returned.
*/
public List<Key> getNearestKeys(final int x, final int y) {
// Avoid dead pixels at edges of the keyboard
final int adjustedX = Math.max(0, Math.min(x, mOccupiedWidth - 1));
final int adjustedY = Math.max(0, Math.min(y, mOccupiedHeight - 1));
return mProximityInfo.getNearestKeys(adjustedX, adjustedY);
}
}

View File

@ -0,0 +1,103 @@
/*
* Copyright (C) 2010 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.colorful.keyboard.theme.keyboard;
public interface KeyboardActionListener {
/**
* Called when the user presses a key. This is sent before the {@link #onCodeInput} is called.
* For keys that repeat, this is only called once.
*
* @param primaryCode the unicode of the key being pressed. If the touch is not on a valid key,
* the value will be zero.
* @param repeatCount how many times the key was repeated. Zero if it is the first press.
* @param isSinglePointer true if pressing has occurred while no other key is being pressed.
*/
void onPressKey(int primaryCode, int repeatCount, boolean isSinglePointer);
/**
* Called when the user releases a key. This is sent after the {@link #onCodeInput} is called.
* For keys that repeat, this is only called once.
*
* @param primaryCode the code of the key that was released
* @param withSliding true if releasing has occurred because the user slid finger from the key
* to other key without releasing the finger.
*/
void onReleaseKey(int primaryCode, boolean withSliding);
/**
* Send a key code to the listener.
*
* @param primaryCode this is the code of the key that was pressed
* @param x x-coordinate pixel of touched event. If {@link #onCodeInput} is not called by
* {@link PointerTracker} or so, the value should be
* {@link Constants#NOT_A_COORDINATE}. If it's called on insertion from the
* suggestion strip, it should be {@link Constants#SUGGESTION_STRIP_COORDINATE}.
* @param y y-coordinate pixel of touched event. If {@link #onCodeInput} is not called by
* {@link PointerTracker} or so, the value should be
* {@link Constants#NOT_A_COORDINATE}.If it's called on insertion from the
* suggestion strip, it should be {@link Constants#SUGGESTION_STRIP_COORDINATE}.
* @param isKeyRepeat true if this is a key repeat, false otherwise
*/
// TODO: change this to send an Event object instead
void onCodeInput(int primaryCode, int x, int y, boolean isKeyRepeat);
/**
* Sends a string of characters to the listener.
*
* @param text the string of characters to be registered.
*/
void onTextInput(final String rawText);
/**
* Called when user finished sliding key input.
*/
void onFinishSlidingInput();
/**
* Send a non-"code input" custom request to the listener.
* @return true if the request has been consumed, false otherwise.
*/
boolean onCustomRequest(int requestCode);
void onMovePointer(int steps);
void onMoveDeletePointer(int steps);
void onUpWithDeletePointerActive();
KeyboardActionListener EMPTY_LISTENER = new Adapter();
class Adapter implements KeyboardActionListener {
@Override
public void onPressKey(int primaryCode, int repeatCount, boolean isSinglePointer) {}
@Override
public void onReleaseKey(int primaryCode, boolean withSliding) {}
@Override
public void onCodeInput(int primaryCode, int x, int y, boolean isKeyRepeat) {}
@Override
public void onTextInput(String text) {}
@Override
public void onFinishSlidingInput() {}
@Override
public boolean onCustomRequest(int requestCode) {
return false;
}
@Override
public void onMovePointer(int steps) {}
@Override
public void onMoveDeletePointer(int steps) {}
@Override
public void onUpWithDeletePointerActive() {}
}
}

View File

@ -0,0 +1,228 @@
/*
* Copyright (C) 2015 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.colorful.keyboard.theme.keyboard;
import android.text.InputType;
import android.text.TextUtils;
import android.view.inputmethod.EditorInfo;
import com.colorful.keyboard.theme.compat.EditorInfoCompatUtils;
import com.colorful.keyboard.theme.latin.Subtype;
import com.colorful.keyboard.theme.latin.utils.InputTypeUtils;
import java.util.Arrays;
import java.util.Locale;
/**
* Unique identifier for each keyboard type.
*/
public final class KeyboardId {
public static final int MODE_TEXT = 0;
public static final int MODE_URL = 1;
public static final int MODE_EMAIL = 2;
public static final int MODE_IM = 3;
public static final int MODE_PHONE = 4;
public static final int MODE_NUMBER = 5;
public static final int MODE_DATE = 6;
public static final int MODE_TIME = 7;
public static final int MODE_DATETIME = 8;
public static final int ELEMENT_ALPHABET = 0;
public static final int ELEMENT_ALPHABET_MANUAL_SHIFTED = 1;
public static final int ELEMENT_ALPHABET_AUTOMATIC_SHIFTED = 2;
public static final int ELEMENT_ALPHABET_SHIFT_LOCKED = 3;
public static final int ELEMENT_SYMBOLS = 5;
public static final int ELEMENT_SYMBOLS_SHIFTED = 6;
public static final int ELEMENT_PHONE = 7;
public static final int ELEMENT_PHONE_SYMBOLS = 8;
public static final int ELEMENT_NUMBER = 9;
public final Subtype mSubtype;
public final int mThemeId;
public final int mWidth;
public final int mHeight;
public final int mMode;
public final int mElementId;
public final EditorInfo mEditorInfo;
public final boolean mLanguageSwitchKeyEnabled;
public final String mCustomActionLabel;
public final boolean mShowMoreKeys;
public final boolean mShowNumberRow;
private final int mHashCode;
public KeyboardId(final int elementId, final KeyboardLayoutSet.Params params) {
mSubtype = params.mSubtype;
mThemeId = params.mKeyboardThemeId;
mWidth = params.mKeyboardWidth;
mHeight = params.mKeyboardHeight;
mMode = params.mMode;
mElementId = elementId;
mEditorInfo = params.mEditorInfo;
mLanguageSwitchKeyEnabled = params.mLanguageSwitchKeyEnabled;
mCustomActionLabel = (mEditorInfo.actionLabel != null)
? mEditorInfo.actionLabel.toString() : null;
mShowMoreKeys = params.mShowMoreKeys;
mShowNumberRow = params.mShowNumberRow;
mHashCode = computeHashCode(this);
}
private static int computeHashCode(final KeyboardId id) {
return Arrays.hashCode(new Object[] {
id.mElementId,
id.mMode,
id.mWidth,
id.mHeight,
id.passwordInput(),
id.mLanguageSwitchKeyEnabled,
id.isMultiLine(),
id.imeAction(),
id.mCustomActionLabel,
id.navigateNext(),
id.navigatePrevious(),
id.mSubtype,
id.mThemeId
});
}
private boolean equals(final KeyboardId other) {
if (other == this)
return true;
return other.mElementId == mElementId
&& other.mMode == mMode
&& other.mWidth == mWidth
&& other.mHeight == mHeight
&& other.passwordInput() == passwordInput()
&& other.mLanguageSwitchKeyEnabled == mLanguageSwitchKeyEnabled
&& other.isMultiLine() == isMultiLine()
&& other.imeAction() == imeAction()
&& TextUtils.equals(other.mCustomActionLabel, mCustomActionLabel)
&& other.navigateNext() == navigateNext()
&& other.navigatePrevious() == navigatePrevious()
&& other.mSubtype.equals(mSubtype)
&& other.mThemeId == mThemeId;
}
private static boolean isAlphabetKeyboard(final int elementId) {
return elementId < ELEMENT_SYMBOLS;
}
public boolean isAlphabetKeyboard() {
return isAlphabetKeyboard(mElementId);
}
public boolean navigateNext() {
return (mEditorInfo.imeOptions & EditorInfo.IME_FLAG_NAVIGATE_NEXT) != 0
|| imeAction() == EditorInfo.IME_ACTION_NEXT;
}
public boolean navigatePrevious() {
return (mEditorInfo.imeOptions & EditorInfo.IME_FLAG_NAVIGATE_PREVIOUS) != 0
|| imeAction() == EditorInfo.IME_ACTION_PREVIOUS;
}
public boolean passwordInput() {
final int inputType = mEditorInfo.inputType;
return InputTypeUtils.isPasswordInputType(inputType)
|| InputTypeUtils.isVisiblePasswordInputType(inputType);
}
public boolean isMultiLine() {
return (mEditorInfo.inputType & InputType.TYPE_TEXT_FLAG_MULTI_LINE) != 0;
}
public int imeAction() {
return InputTypeUtils.getImeOptionsActionIdFromEditorInfo(mEditorInfo);
}
public Locale getLocale() {
return mSubtype.getLocaleObject();
}
@Override
public boolean equals(final Object other) {
return other instanceof KeyboardId && equals((KeyboardId) other);
}
@Override
public int hashCode() {
return mHashCode;
}
@Override
public String toString() {
return String.format(Locale.ROOT, "[%s %s:%s %dx%d %s %s%s%s%s%s%s %s]",
elementIdToName(mElementId),
mSubtype.getLocale(),
mSubtype.getKeyboardLayoutSet(),
mWidth, mHeight,
modeName(mMode),
actionName(imeAction()),
(navigateNext() ? " navigateNext" : ""),
(navigatePrevious() ? " navigatePrevious" : ""),
(passwordInput() ? " passwordInput" : ""),
(mLanguageSwitchKeyEnabled ? " languageSwitchKeyEnabled" : ""),
(isMultiLine() ? " isMultiLine" : ""),
KeyboardTheme.getKeyboardThemeName(mThemeId)
);
}
public static boolean equivalentEditorInfoForKeyboard(final EditorInfo a, final EditorInfo b) {
if (a == null && b == null) return true;
if (a == null || b == null) return false;
return a.inputType == b.inputType
&& a.imeOptions == b.imeOptions
&& TextUtils.equals(a.privateImeOptions, b.privateImeOptions);
}
public static String elementIdToName(final int elementId) {
switch (elementId) {
case ELEMENT_ALPHABET: return "alphabet";
case ELEMENT_ALPHABET_MANUAL_SHIFTED: return "alphabetManualShifted";
case ELEMENT_ALPHABET_AUTOMATIC_SHIFTED: return "alphabetAutomaticShifted";
case ELEMENT_ALPHABET_SHIFT_LOCKED: return "alphabetShiftLocked";
case ELEMENT_SYMBOLS: return "symbols";
case ELEMENT_SYMBOLS_SHIFTED: return "symbolsShifted";
case ELEMENT_PHONE: return "phone";
case ELEMENT_PHONE_SYMBOLS: return "phoneSymbols";
case ELEMENT_NUMBER: return "number";
default: return null;
}
}
public static String modeName(final int mode) {
switch (mode) {
case MODE_TEXT: return "text";
case MODE_URL: return "url";
case MODE_EMAIL: return "email";
case MODE_IM: return "im";
case MODE_PHONE: return "phone";
case MODE_NUMBER: return "number";
case MODE_DATE: return "date";
case MODE_TIME: return "time";
case MODE_DATETIME: return "datetime";
default: return null;
}
}
public static String actionName(final int actionId) {
return (actionId == InputTypeUtils.IME_ACTION_CUSTOM_LABEL) ? "actionCustomLabel"
: EditorInfoCompatUtils.imeActionName(actionId);
}
}

View File

@ -0,0 +1,372 @@
/*
* Copyright (C) 2011 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.colorful.keyboard.theme.keyboard;
import android.content.Context;
import android.content.res.Resources;
import android.content.res.TypedArray;
import android.content.res.XmlResourceParser;
import android.text.InputType;
import android.util.Log;
import android.util.SparseArray;
import android.util.Xml;
import android.view.inputmethod.EditorInfo;
import com.colorful.keyboard.theme.R;
import com.colorful.keyboard.theme.keyboard.internal.KeyboardBuilder;
import com.colorful.keyboard.theme.keyboard.internal.KeyboardParams;
import com.colorful.keyboard.theme.keyboard.internal.UniqueKeysCache;
import com.colorful.keyboard.theme.latin.Subtype;
import com.colorful.keyboard.theme.latin.utils.InputTypeUtils;
import com.colorful.keyboard.theme.latin.utils.XmlParseUtils;
import org.xmlpull.v1.XmlPullParser;
import org.xmlpull.v1.XmlPullParserException;
import java.io.IOException;
import java.lang.ref.SoftReference;
import java.util.HashMap;
/**
* This class represents a set of keyboard layouts. Each of them represents a different keyboard
* specific to a keyboard state, such as alphabet, symbols, and so on. Layouts in the same
* {@link KeyboardLayoutSet} are related to each other.
* A {@link KeyboardLayoutSet} needs to be created for each
* {@link EditorInfo}.
*/
public final class KeyboardLayoutSet {
private static final String TAG = KeyboardLayoutSet.class.getSimpleName();
private static final boolean DEBUG_CACHE = false;
private static final String TAG_KEYBOARD_SET = "KeyboardLayoutSet";
private static final String TAG_ELEMENT = "Element";
private static final String KEYBOARD_LAYOUT_SET_RESOURCE_PREFIX = "keyboard_layout_set_";
private final Context mContext;
private final Params mParams;
// How many layouts we forcibly keep in cache. This only includes ALPHABET (default) and
// ALPHABET_AUTOMATIC_SHIFTED layouts - other layouts may stay in memory in the map of
// soft-references, but we forcibly cache this many alphabetic/auto-shifted layouts.
private static final int FORCIBLE_CACHE_SIZE = 4;
// By construction of soft references, anything that is also referenced somewhere else
// will stay in the cache. So we forcibly keep some references in an array to prevent
// them from disappearing from sKeyboardCache.
private static final Keyboard[] sForcibleKeyboardCache = new Keyboard[FORCIBLE_CACHE_SIZE];
private static final HashMap<KeyboardId, SoftReference<Keyboard>> sKeyboardCache =
new HashMap<>();
private static final UniqueKeysCache sUniqueKeysCache = UniqueKeysCache.newInstance();
@SuppressWarnings("serial")
public static final class KeyboardLayoutSetException extends RuntimeException {
public final KeyboardId mKeyboardId;
public KeyboardLayoutSetException(final Throwable cause, final KeyboardId keyboardId) {
super(cause);
mKeyboardId = keyboardId;
}
}
private static final class ElementParams {
int mKeyboardXmlId;
boolean mAllowRedundantMoreKeys;
public ElementParams() {}
}
public static final class Params {
String mKeyboardLayoutSetName;
int mMode;
// TODO: Use {@link InputAttributes} instead of these variables.
EditorInfo mEditorInfo;
boolean mLanguageSwitchKeyEnabled;
Subtype mSubtype;
int mKeyboardThemeId;
int mKeyboardWidth;
int mKeyboardHeight;
boolean mShowMoreKeys;
boolean mShowNumberRow;
// Sparse array of KeyboardLayoutSet element parameters indexed by element's id.
final SparseArray<ElementParams> mKeyboardLayoutSetElementIdToParamsMap =
new SparseArray<>();
}
public static void onSystemLocaleChanged() {
clearKeyboardCache();
}
public static void onKeyboardThemeChanged() {
clearKeyboardCache();
}
private static void clearKeyboardCache() {
sKeyboardCache.clear();
sUniqueKeysCache.clear();
}
KeyboardLayoutSet(final Context context, final Params params) {
mContext = context;
mParams = params;
}
public Keyboard getKeyboard(final int baseKeyboardLayoutSetElementId) {
final int keyboardLayoutSetElementId;
switch (mParams.mMode) {
case KeyboardId.MODE_PHONE:
if (baseKeyboardLayoutSetElementId == KeyboardId.ELEMENT_SYMBOLS) {
keyboardLayoutSetElementId = KeyboardId.ELEMENT_PHONE_SYMBOLS;
} else {
keyboardLayoutSetElementId = KeyboardId.ELEMENT_PHONE;
}
break;
case KeyboardId.MODE_NUMBER:
case KeyboardId.MODE_DATE:
case KeyboardId.MODE_TIME:
case KeyboardId.MODE_DATETIME:
keyboardLayoutSetElementId = KeyboardId.ELEMENT_NUMBER;
break;
default:
keyboardLayoutSetElementId = baseKeyboardLayoutSetElementId;
break;
}
ElementParams elementParams = mParams.mKeyboardLayoutSetElementIdToParamsMap.get(
keyboardLayoutSetElementId);
if (elementParams == null) {
elementParams = mParams.mKeyboardLayoutSetElementIdToParamsMap.get(
KeyboardId.ELEMENT_ALPHABET);
}
// Note: The keyboard for each shift state, and mode are represented as an elementName
// attribute in a keyboard_layout_set XML file. Also each keyboard layout XML resource is
// specified as an elementKeyboard attribute in the file.
// The KeyboardId is an internal key for a Keyboard object.
final KeyboardId id = new KeyboardId(keyboardLayoutSetElementId, mParams);
return getKeyboard(elementParams, id);
}
private Keyboard getKeyboard(final ElementParams elementParams, final KeyboardId id) {
final SoftReference<Keyboard> ref = sKeyboardCache.get(id);
final Keyboard cachedKeyboard = (ref == null) ? null : ref.get();
if (cachedKeyboard != null) {
if (DEBUG_CACHE) {
Log.d(TAG, "keyboard cache size=" + sKeyboardCache.size() + ": HIT id=" + id);
}
return cachedKeyboard;
}
final KeyboardBuilder<KeyboardParams> builder =
new KeyboardBuilder<>(mContext, new KeyboardParams(sUniqueKeysCache));
sUniqueKeysCache.setEnabled(id.isAlphabetKeyboard());
builder.setAllowRedundantMoreKes(elementParams.mAllowRedundantMoreKeys);
final int keyboardXmlId = elementParams.mKeyboardXmlId;
builder.load(keyboardXmlId, id);
final Keyboard keyboard = builder.build();
sKeyboardCache.put(id, new SoftReference<>(keyboard));
if ((id.mElementId == KeyboardId.ELEMENT_ALPHABET
|| id.mElementId == KeyboardId.ELEMENT_ALPHABET_AUTOMATIC_SHIFTED)) {
// We only forcibly cache the primary, "ALPHABET", layouts.
for (int i = sForcibleKeyboardCache.length - 1; i >= 1; --i) {
sForcibleKeyboardCache[i] = sForcibleKeyboardCache[i - 1];
}
sForcibleKeyboardCache[0] = keyboard;
if (DEBUG_CACHE) {
Log.d(TAG, "forcing caching of keyboard with id=" + id);
}
}
if (DEBUG_CACHE) {
Log.d(TAG, "keyboard cache size=" + sKeyboardCache.size() + ": "
+ ((ref == null) ? "LOAD" : "GCed") + " id=" + id);
}
return keyboard;
}
public static final class Builder {
private final Context mContext;
private final Resources mResources;
private final Params mParams = new Params();
private static final EditorInfo EMPTY_EDITOR_INFO = new EditorInfo();
public Builder(final Context context, final EditorInfo ei) {
mContext = context;
mResources = context.getResources();
final Params params = mParams;
final EditorInfo editorInfo = (ei != null) ? ei : EMPTY_EDITOR_INFO;
params.mMode = getKeyboardMode(editorInfo);
// TODO: Consolidate those with {@link InputAttributes}.
params.mEditorInfo = editorInfo;
}
public Builder setKeyboardTheme(final int themeId) {
mParams.mKeyboardThemeId = themeId;
return this;
}
public Builder setKeyboardGeometry(final int keyboardWidth, final int keyboardHeight) {
mParams.mKeyboardWidth = keyboardWidth;
mParams.mKeyboardHeight = keyboardHeight;
return this;
}
public Builder setSubtype(final Subtype subtype) {
// TODO: Consolidate with {@link InputAttributes}.
mParams.mSubtype = subtype;
mParams.mKeyboardLayoutSetName = KEYBOARD_LAYOUT_SET_RESOURCE_PREFIX
+ subtype.getKeyboardLayoutSet();
return this;
}
public Builder setLanguageSwitchKeyEnabled(final boolean enabled) {
mParams.mLanguageSwitchKeyEnabled = enabled;
return this;
}
public Builder setShowSpecialChars(final boolean enabled) {
mParams.mShowMoreKeys = enabled;
return this;
}
public Builder setShowNumberRow(final boolean enabled) {
mParams.mShowNumberRow = enabled;
return this;
}
public KeyboardLayoutSet build() {
if (mParams.mSubtype == null)
throw new RuntimeException("KeyboardLayoutSet subtype is not specified");
final int xmlId = getXmlId(mResources, mParams.mKeyboardLayoutSetName);
try {
parseKeyboardLayoutSet(mResources, xmlId);
} catch (final IOException | XmlPullParserException e) {
throw new RuntimeException(e.getMessage() + " in " + mParams.mKeyboardLayoutSetName,
e);
}
return new KeyboardLayoutSet(mContext, mParams);
}
private static int getXmlId(final Resources resources, final String keyboardLayoutSetName) {
final String packageName = resources.getResourcePackageName(
R.xml.keyboard_layout_set_qwerty);
return resources.getIdentifier(keyboardLayoutSetName, "xml", packageName);
}
private void parseKeyboardLayoutSet(final Resources res, final int resId)
throws XmlPullParserException, IOException {
final XmlResourceParser parser = res.getXml(resId);
try {
while (parser.getEventType() != XmlPullParser.END_DOCUMENT) {
final int event = parser.next();
if (event == XmlPullParser.START_TAG) {
final String tag = parser.getName();
if (TAG_KEYBOARD_SET.equals(tag)) {
parseKeyboardLayoutSetContent(parser);
} else {
throw new XmlParseUtils.IllegalStartTag(parser, tag, TAG_KEYBOARD_SET);
}
}
}
} finally {
parser.close();
}
}
private void parseKeyboardLayoutSetContent(final XmlPullParser parser)
throws XmlPullParserException, IOException {
while (parser.getEventType() != XmlPullParser.END_DOCUMENT) {
final int event = parser.next();
if (event == XmlPullParser.START_TAG) {
final String tag = parser.getName();
if (TAG_ELEMENT.equals(tag)) {
parseKeyboardLayoutSetElement(parser);
} else {
throw new XmlParseUtils.IllegalStartTag(parser, tag, TAG_KEYBOARD_SET);
}
} else if (event == XmlPullParser.END_TAG) {
final String tag = parser.getName();
if (TAG_KEYBOARD_SET.equals(tag)) {
break;
}
throw new XmlParseUtils.IllegalEndTag(parser, tag, TAG_KEYBOARD_SET);
}
}
}
private void parseKeyboardLayoutSetElement(final XmlPullParser parser)
throws XmlPullParserException, IOException {
final TypedArray a = mResources.obtainAttributes(Xml.asAttributeSet(parser),
R.styleable.KeyboardLayoutSet_Element);
try {
XmlParseUtils.checkAttributeExists(a,
R.styleable.KeyboardLayoutSet_Element_elementName, "elementName",
TAG_ELEMENT, parser);
XmlParseUtils.checkAttributeExists(a,
R.styleable.KeyboardLayoutSet_Element_elementKeyboard, "elementKeyboard",
TAG_ELEMENT, parser);
XmlParseUtils.checkEndTag(TAG_ELEMENT, parser);
final ElementParams elementParams = new ElementParams();
final int elementName = a.getInt(
R.styleable.KeyboardLayoutSet_Element_elementName, 0);
elementParams.mKeyboardXmlId = a.getResourceId(
R.styleable.KeyboardLayoutSet_Element_elementKeyboard, 0);
elementParams.mAllowRedundantMoreKeys = a.getBoolean(
R.styleable.KeyboardLayoutSet_Element_allowRedundantMoreKeys, true);
mParams.mKeyboardLayoutSetElementIdToParamsMap.put(elementName, elementParams);
} finally {
a.recycle();
}
}
private static int getKeyboardMode(final EditorInfo editorInfo) {
final int inputType = editorInfo.inputType;
final int variation = inputType & InputType.TYPE_MASK_VARIATION;
switch (inputType & InputType.TYPE_MASK_CLASS) {
case InputType.TYPE_CLASS_NUMBER:
return KeyboardId.MODE_NUMBER;
case InputType.TYPE_CLASS_DATETIME:
switch (variation) {
case InputType.TYPE_DATETIME_VARIATION_DATE:
return KeyboardId.MODE_DATE;
case InputType.TYPE_DATETIME_VARIATION_TIME:
return KeyboardId.MODE_TIME;
default: // InputType.TYPE_DATETIME_VARIATION_NORMAL
return KeyboardId.MODE_DATETIME;
}
case InputType.TYPE_CLASS_PHONE:
return KeyboardId.MODE_PHONE;
case InputType.TYPE_CLASS_TEXT:
if (InputTypeUtils.isEmailVariation(variation)) {
return KeyboardId.MODE_EMAIL;
} else if (variation == InputType.TYPE_TEXT_VARIATION_URI) {
return KeyboardId.MODE_URL;
} else if (variation == InputType.TYPE_TEXT_VARIATION_SHORT_MESSAGE) {
return KeyboardId.MODE_IM;
} else if (variation == InputType.TYPE_TEXT_VARIATION_FILTER) {
return KeyboardId.MODE_TEXT;
} else {
return KeyboardId.MODE_TEXT;
}
default:
return KeyboardId.MODE_TEXT;
}
}
}
}

View File

@ -0,0 +1,414 @@
/*
* Copyright (C) 2008 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.colorful.keyboard.theme.keyboard;
import android.content.Context;
import android.content.res.Resources;
import android.os.Build;
import android.util.Log;
import android.view.ContextThemeWrapper;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.view.inputmethod.EditorInfo;
import android.widget.ImageView;
import com.bumptech.glide.Glide;
import com.colorful.keyboard.theme.App;
import com.colorful.keyboard.theme.R;
import com.colorful.keyboard.theme.event.Event;
import com.colorful.keyboard.theme.keyboard.internal.KeyboardState;
import com.colorful.keyboard.theme.keyboard.internal.KeyboardTextsSet;
import com.colorful.keyboard.theme.latin.InputView;
import com.colorful.keyboard.theme.latin.LatinIME;
import com.colorful.keyboard.theme.latin.RichInputMethodManager;
import com.colorful.keyboard.theme.latin.settings.Settings;
import com.colorful.keyboard.theme.latin.settings.SettingsValues;
import com.colorful.keyboard.theme.latin.utils.CapsModeUtils;
import com.colorful.keyboard.theme.latin.utils.LanguageOnSpacebarUtils;
import com.colorful.keyboard.theme.latin.utils.RecapitalizeStatus;
import com.colorful.keyboard.theme.latin.utils.ResourceUtils;
import com.colorful.keyboard.theme.myutil.SPUtils;
import java.io.File;
public final class KeyboardSwitcher implements KeyboardState.SwitchActions {
private static final String TAG = KeyboardSwitcher.class.getSimpleName();
private InputView mCurrentInputView;
private int mCurrentUiMode;
private int mCurrentTextColor = 0x0;
private View mMainKeyboardFrame;
private MainKeyboardView mKeyboardView;
private LatinIME mLatinIME;
private RichInputMethodManager mRichImm;
private KeyboardState mState;
private KeyboardLayoutSet mKeyboardLayoutSet;
// TODO: The following {@link KeyboardTextsSet} should be in {@link KeyboardLayoutSet}.
private final KeyboardTextsSet mKeyboardTextsSet = new KeyboardTextsSet();
private KeyboardTheme mKeyboardTheme;
private Context mThemeContext;
private static KeyboardSwitcher sInstance = new KeyboardSwitcher();
public static KeyboardSwitcher getInstance() {
return sInstance;
}
private KeyboardSwitcher() {
// Intentional empty constructor for singleton.
}
public static void init(final LatinIME latinIme) {
sInstance.initInternal(latinIme);
}
private void initInternal(final LatinIME latinIme) {
mLatinIME = latinIme;
mRichImm = RichInputMethodManager.getInstance();
mState = new KeyboardState(this);
}
public void updateKeyboardTheme(final int uiMode) {
final boolean themeUpdated = updateKeyboardThemeAndContextThemeWrapper(
mLatinIME, KeyboardTheme.getKeyboardTheme(mLatinIME), uiMode);
if (themeUpdated && mKeyboardView != null) {
mLatinIME.setInputView(onCreateInputView(uiMode));
}
}
private boolean updateKeyboardThemeAndContextThemeWrapper(final Context context,
final KeyboardTheme keyboardTheme, final int uiMode) {
int newTextColor = 0x0;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
newTextColor = context.getResources().getColor(R.color.key_text_color_lxx_system);
}
if (mThemeContext == null
|| !keyboardTheme.equals(mKeyboardTheme)
|| mCurrentUiMode != uiMode
|| newTextColor != mCurrentTextColor) {
mKeyboardTheme = keyboardTheme;
mCurrentUiMode = uiMode;
mCurrentTextColor = newTextColor;
mThemeContext = new ContextThemeWrapper(context, keyboardTheme.mStyleId);
KeyboardLayoutSet.onKeyboardThemeChanged();
return true;
}
return false;
}
public void loadKeyboard(final EditorInfo editorInfo, final SettingsValues settingsValues,
final int currentAutoCapsState, final int currentRecapitalizeState) {
final KeyboardLayoutSet.Builder builder = new KeyboardLayoutSet.Builder(
mThemeContext, editorInfo);
final Resources res = mThemeContext.getResources();
final int keyboardWidth = mLatinIME.getMaxWidth();
final int keyboardHeight = ResourceUtils.getKeyboardHeight(res, settingsValues);
builder.setKeyboardTheme(mKeyboardTheme.mThemeId);
builder.setKeyboardGeometry(keyboardWidth, keyboardHeight);
builder.setSubtype(mRichImm.getCurrentSubtype());
builder.setLanguageSwitchKeyEnabled(mLatinIME.shouldShowLanguageSwitchKey());
builder.setShowSpecialChars(!settingsValues.mHideSpecialChars);
builder.setShowNumberRow(settingsValues.mShowNumberRow);
mKeyboardLayoutSet = builder.build();
try {
mState.onLoadKeyboard(currentAutoCapsState, currentRecapitalizeState);
mKeyboardTextsSet.setLocale(mRichImm.getCurrentSubtype().getLocaleObject(),
mThemeContext);
} catch (KeyboardLayoutSet.KeyboardLayoutSetException e) {
Log.w(TAG, "loading keyboard failed: " + e.mKeyboardId, e.getCause());
}
}
public void saveKeyboardState() {
if (getKeyboard() != null) {
mState.onSaveKeyboardState();
}
}
public void onHideWindow() {
if (mKeyboardView != null) {
mKeyboardView.onHideWindow();
}
}
private void setKeyboard(
final int keyboardId,
final KeyboardSwitchState toggleState) {
final SettingsValues currentSettingsValues = Settings.getInstance().getCurrent();
setMainKeyboardFrame(currentSettingsValues, toggleState);
// TODO: pass this object to setKeyboard instead of getting the current values.
final MainKeyboardView keyboardView = mKeyboardView;
final Keyboard oldKeyboard = keyboardView.getKeyboard();
final Keyboard newKeyboard = mKeyboardLayoutSet.getKeyboard(keyboardId);
keyboardView.setKeyboard(newKeyboard);
keyboardView.setKeyPreviewPopupEnabled(
currentSettingsValues.mKeyPreviewPopupOn,
currentSettingsValues.mKeyPreviewPopupDismissDelay);
final boolean subtypeChanged = (oldKeyboard == null)
|| !newKeyboard.mId.mSubtype.equals(oldKeyboard.mId.mSubtype);
final int languageOnSpacebarFormatType = LanguageOnSpacebarUtils
.getLanguageOnSpacebarFormatType(newKeyboard.mId.mSubtype);
keyboardView.startDisplayLanguageOnSpacebar(subtypeChanged, languageOnSpacebarFormatType);
}
public Keyboard getKeyboard() {
if (mKeyboardView != null) {
return mKeyboardView.getKeyboard();
}
return null;
}
// TODO: Remove this method. Come up with a more comprehensive way to reset the keyboard layout
// when a keyboard layout set doesn't get reloaded in LatinIME.onStartInputViewInternal().
public void resetKeyboardStateToAlphabet(final int currentAutoCapsState,
final int currentRecapitalizeState) {
mState.onResetKeyboardStateToAlphabet(currentAutoCapsState, currentRecapitalizeState);
}
public void onPressKey(final int code, final boolean isSinglePointer,
final int currentAutoCapsState, final int currentRecapitalizeState) {
mState.onPressKey(code, isSinglePointer, currentAutoCapsState, currentRecapitalizeState);
}
public void onReleaseKey(final int code, final boolean withSliding,
final int currentAutoCapsState, final int currentRecapitalizeState) {
mState.onReleaseKey(code, withSliding, currentAutoCapsState, currentRecapitalizeState);
}
public void onFinishSlidingInput(final int currentAutoCapsState,
final int currentRecapitalizeState) {
mState.onFinishSlidingInput(currentAutoCapsState, currentRecapitalizeState);
}
// Implements {@link KeyboardState.SwitchActions}.
@Override
public void setAlphabetKeyboard() {
if (DEBUG_ACTION) {
Log.d(TAG, "setAlphabetKeyboard");
}
setKeyboard(KeyboardId.ELEMENT_ALPHABET, KeyboardSwitchState.OTHER);
}
// Implements {@link KeyboardState.SwitchActions}.
@Override
public void setAlphabetManualShiftedKeyboard() {
if (DEBUG_ACTION) {
Log.d(TAG, "setAlphabetManualShiftedKeyboard");
}
setKeyboard(KeyboardId.ELEMENT_ALPHABET_MANUAL_SHIFTED, KeyboardSwitchState.OTHER);
}
// Implements {@link KeyboardState.SwitchActions}.
@Override
public void setAlphabetAutomaticShiftedKeyboard() {
if (DEBUG_ACTION) {
Log.d(TAG, "setAlphabetAutomaticShiftedKeyboard");
}
setKeyboard(KeyboardId.ELEMENT_ALPHABET_AUTOMATIC_SHIFTED, KeyboardSwitchState.OTHER);
}
// Implements {@link KeyboardState.SwitchActions}.
@Override
public void setAlphabetShiftLockedKeyboard() {
if (DEBUG_ACTION) {
Log.d(TAG, "setAlphabetShiftLockedKeyboard");
}
setKeyboard(KeyboardId.ELEMENT_ALPHABET_SHIFT_LOCKED, KeyboardSwitchState.OTHER);
}
// Implements {@link KeyboardState.SwitchActions}.
@Override
public void setSymbolsKeyboard() {
if (DEBUG_ACTION) {
Log.d(TAG, "setSymbolsKeyboard");
}
setKeyboard(KeyboardId.ELEMENT_SYMBOLS, KeyboardSwitchState.OTHER);
}
// Implements {@link KeyboardState.SwitchActions}.
@Override
public void setSymbolsShiftedKeyboard() {
if (DEBUG_ACTION) {
Log.d(TAG, "setSymbolsShiftedKeyboard");
}
setKeyboard(KeyboardId.ELEMENT_SYMBOLS_SHIFTED, KeyboardSwitchState.SYMBOLS_SHIFTED);
}
public boolean isImeSuppressedByHardwareKeyboard(
final SettingsValues settingsValues,
final KeyboardSwitchState toggleState) {
return settingsValues.mHasHardwareKeyboard && toggleState == KeyboardSwitchState.HIDDEN;
}
private void setMainKeyboardFrame(
final SettingsValues settingsValues,
final KeyboardSwitchState toggleState) {
final int visibility = isImeSuppressedByHardwareKeyboard(settingsValues, toggleState)
? View.GONE : View.VISIBLE;
mKeyboardView.setVisibility(visibility);
// The visibility of {@link #mKeyboardView} must be aligned with {@link #MainKeyboardFrame}.
// @see #getVisibleKeyboardView() and
// @see LatinIME#onComputeInset(android.inputmethodservice.InputMethodService.Insets)
mMainKeyboardFrame.setVisibility(visibility);
}
public enum KeyboardSwitchState {
HIDDEN(-1),
SYMBOLS_SHIFTED(KeyboardId.ELEMENT_SYMBOLS_SHIFTED),
OTHER(-1);
final int mKeyboardId;
KeyboardSwitchState(int keyboardId) {
mKeyboardId = keyboardId;
}
}
public KeyboardSwitchState getKeyboardSwitchState() {
boolean hidden = mKeyboardLayoutSet == null
|| mKeyboardView == null
|| !mKeyboardView.isShown();
if (hidden) {
return KeyboardSwitchState.HIDDEN;
} else if (isShowingKeyboardId(KeyboardId.ELEMENT_SYMBOLS_SHIFTED)) {
return KeyboardSwitchState.SYMBOLS_SHIFTED;
}
return KeyboardSwitchState.OTHER;
}
// Future method for requesting an updating to the shift state.
@Override
public void requestUpdatingShiftState(final int autoCapsFlags, final int recapitalizeMode) {
if (DEBUG_ACTION) {
Log.d(TAG, "requestUpdatingShiftState: "
+ " autoCapsFlags=" + CapsModeUtils.flagsToString(autoCapsFlags)
+ " recapitalizeMode=" + RecapitalizeStatus.modeToString(recapitalizeMode));
}
mState.onUpdateShiftState(autoCapsFlags, recapitalizeMode);
}
// Implements {@link KeyboardState.SwitchActions}.
@Override
public void startDoubleTapShiftKeyTimer() {
if (DEBUG_TIMER_ACTION) {
Log.d(TAG, "startDoubleTapShiftKeyTimer");
}
final MainKeyboardView keyboardView = getMainKeyboardView();
if (keyboardView != null) {
keyboardView.startDoubleTapShiftKeyTimer();
}
}
// Implements {@link KeyboardState.SwitchActions}.
@Override
public void cancelDoubleTapShiftKeyTimer() {
if (DEBUG_TIMER_ACTION) {
Log.d(TAG, "setAlphabetKeyboard");
}
final MainKeyboardView keyboardView = getMainKeyboardView();
if (keyboardView != null) {
keyboardView.cancelDoubleTapShiftKeyTimer();
}
}
// Implements {@link KeyboardState.SwitchActions}.
@Override
public boolean isInDoubleTapShiftKeyTimeout() {
if (DEBUG_TIMER_ACTION) {
Log.d(TAG, "isInDoubleTapShiftKeyTimeout");
}
final MainKeyboardView keyboardView = getMainKeyboardView();
return keyboardView != null && keyboardView.isInDoubleTapShiftKeyTimeout();
}
/**
* Updates state machine to figure out when to automatically switch back to the previous mode.
*/
public void onEvent(final Event event, final int currentAutoCapsState,
final int currentRecapitalizeState) {
mState.onEvent(event, currentAutoCapsState, currentRecapitalizeState);
}
public boolean isShowingKeyboardId(int... keyboardIds) {
if (mKeyboardView == null || !mKeyboardView.isShown()) {
return false;
}
int activeKeyboardId = mKeyboardView.getKeyboard().mId.mElementId;
for (int keyboardId : keyboardIds) {
if (activeKeyboardId == keyboardId) {
return true;
}
}
return false;
}
public boolean isShowingMoreKeysPanel() {
return mKeyboardView.isShowingMoreKeysPanel();
}
public View getVisibleKeyboardView() {
return mKeyboardView;
}
public MainKeyboardView getMainKeyboardView() {
return mKeyboardView;
}
public void deallocateMemory() {
if (mKeyboardView != null) {
mKeyboardView.cancelAllOngoingEvents();
mKeyboardView.deallocateMemory();
}
}
public View onCreateInputView(final int uiMode) {
if (mKeyboardView != null) {
mKeyboardView.closing();
}
updateKeyboardThemeAndContextThemeWrapper(
mLatinIME, KeyboardTheme.getKeyboardTheme(mLatinIME /* context */), uiMode);
mCurrentInputView = (InputView) LayoutInflater.from(mThemeContext).inflate(
R.layout.input_view, null);
mMainKeyboardFrame = mCurrentInputView.findViewById(R.id.main_keyboard_frame);
ImageView imageView = mCurrentInputView.findViewById(R.id.iv_keyboard_bg);
ViewGroup.LayoutParams layoutParams = imageView.getLayoutParams();
mMainKeyboardFrame.post(new Runnable() {
@Override
public void run() {
layoutParams.height = mMainKeyboardFrame.getHeight();
layoutParams.width = mMainKeyboardFrame.getWidth();
/* if (layoutParams.height > layoutParams.width){
return;
}*/
File file = new File(SPUtils.getInstance().getString("bg"));
Glide.with(App.Companion.getApp()).load(file).into(imageView);
}
});
mKeyboardView = (MainKeyboardView) mCurrentInputView.findViewById(R.id.keyboard_view);
mKeyboardView.setKeyboardActionListener(mLatinIME);
return mCurrentInputView;
}
}

View File

@ -0,0 +1,125 @@
/*
* Copyright (C) 2014 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.colorful.keyboard.theme.keyboard;
import android.content.Context;
import android.content.SharedPreferences;
import android.util.Log;
import com.colorful.keyboard.theme.R;
import com.colorful.keyboard.theme.compat.PreferenceManagerCompat;
import com.colorful.keyboard.theme.latin.settings.Settings;
public final class KeyboardTheme {
private static final String TAG = KeyboardTheme.class.getSimpleName();
static final String KEYBOARD_THEME_KEY = "pref_keyboard_theme_20140509";
// These should be aligned with Keyboard.themeId and Keyboard.Case.keyboardTheme
// attributes' values in attrs.xml.
public static final int THEME_ID_LIGHT_BORDER = 1;
public static final int THEME_ID_DARK_BORDER = 2;
public static final int THEME_ID_LIGHT = 3;
public static final int THEME_ID_DARK = 4;
public static final int THEME_ID_SYSTEM = 5;
public static final int THEME_ID_SYSTEM_BORDER = 6;
public static final int DEFAULT_THEME_ID = THEME_ID_LIGHT;
/* package private for testing */
static final KeyboardTheme[] KEYBOARD_THEMES = {
new KeyboardTheme(THEME_ID_LIGHT, "LXXLight", R.style.KeyboardTheme_LXX_Light),
new KeyboardTheme(THEME_ID_DARK, "LXXDark", R.style.KeyboardTheme_LXX_Dark),
new KeyboardTheme(THEME_ID_LIGHT_BORDER, "LXXLightBorder", R.style.KeyboardTheme_LXX_Light_Border),
new KeyboardTheme(THEME_ID_DARK_BORDER, "LXXDarkBorder", R.style.KeyboardTheme_LXX_Dark_Border),
new KeyboardTheme(THEME_ID_SYSTEM, "LXXSystem", R.style.KeyboardTheme_LXX_System),
new KeyboardTheme(THEME_ID_SYSTEM_BORDER, "LXXSystemBorder", R.style.KeyboardTheme_LXX_System_Border),
};
public final int mThemeId;
public final int mStyleId;
public final String mThemeName;
// Note: The themeId should be aligned with "themeId" attribute of Keyboard style
// in values/themes-<style>.xml.
private KeyboardTheme(final int themeId, final String themeName, final int styleId) {
mThemeId = themeId;
mThemeName = themeName;
mStyleId = styleId;
}
@Override
public boolean equals(final Object o) {
if (o == this) return true;
return (o instanceof KeyboardTheme) && ((KeyboardTheme)o).mThemeId == mThemeId;
}
@Override
public int hashCode() {
return mThemeId;
}
/* package private for testing */
static KeyboardTheme searchKeyboardThemeById(final int themeId) {
// TODO: This search algorithm isn't optimal if there are many themes.
for (final KeyboardTheme theme : KEYBOARD_THEMES) {
if (theme.mThemeId == themeId) {
return theme;
}
}
return null;
}
/* package private for testing */
static KeyboardTheme getDefaultKeyboardTheme() {
return searchKeyboardThemeById(DEFAULT_THEME_ID);
}
public static String getKeyboardThemeName(final int themeId) {
final KeyboardTheme theme = searchKeyboardThemeById(themeId);
Log.i("Getting theme ID", Integer.toString(themeId));
return theme.mThemeName;
}
public static void saveKeyboardThemeId(final int themeId, final SharedPreferences prefs) {
prefs.edit().putString(KEYBOARD_THEME_KEY, Integer.toString(themeId)).apply();
}
public static KeyboardTheme getKeyboardTheme(final Context context) {
final SharedPreferences prefs = PreferenceManagerCompat.getDeviceSharedPreferences(context);
return getKeyboardTheme(prefs);
}
public static KeyboardTheme getKeyboardTheme(final SharedPreferences prefs) {
final String themeIdString = prefs.getString(KEYBOARD_THEME_KEY, null);
if (themeIdString == null) {
return searchKeyboardThemeById(THEME_ID_LIGHT);
}
try {
final int themeId = Integer.parseInt(themeIdString);
final KeyboardTheme theme = searchKeyboardThemeById(themeId);
if (theme != null) {
return theme;
}
Log.w(TAG, "Unknown keyboard theme in preference: " + themeIdString);
} catch (final NumberFormatException e) {
Log.w(TAG, "Illegal keyboard theme in preference: " + themeIdString, e);
}
// Remove preference that contains unknown or illegal theme id.
prefs.edit().remove(KEYBOARD_THEME_KEY).remove(Settings.PREF_KEYBOARD_COLOR).apply();
return getDefaultKeyboardTheme();
}
}

View File

@ -0,0 +1,525 @@
/*
* Copyright (C) 2010 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.colorful.keyboard.theme.keyboard;
import android.content.Context;
import android.content.SharedPreferences;
import android.content.res.TypedArray;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Paint.Align;
import android.graphics.PorterDuff;
import android.graphics.Rect;
import android.graphics.Typeface;
import android.graphics.drawable.Drawable;
import android.graphics.drawable.NinePatchDrawable;
import android.util.AttributeSet;
import android.view.View;
import com.colorful.keyboard.theme.R;
import com.colorful.keyboard.theme.compat.PreferenceManagerCompat;
import com.colorful.keyboard.theme.keyboard.internal.KeyDrawParams;
import com.colorful.keyboard.theme.keyboard.internal.KeyVisualAttributes;
import com.colorful.keyboard.theme.latin.common.Constants;
import com.colorful.keyboard.theme.latin.settings.Settings;
import com.colorful.keyboard.theme.latin.utils.TypefaceUtils;
import java.util.HashSet;
/**
* A view that renders a virtual {@link Keyboard}.
*
* @attr ref R.styleable#KeyboardView_keyBackground
* @attr ref R.styleable#KeyboardView_functionalKeyBackground
* @attr ref R.styleable#KeyboardView_spacebarBackground
* @attr ref R.styleable#KeyboardView_spacebarIconWidthRatio
* @attr ref R.styleable#Keyboard_Key_keyLabelFlags
* @attr ref R.styleable#KeyboardView_keyHintLetterPadding
* @attr ref R.styleable#KeyboardView_keyShiftedLetterHintPadding
* @attr ref R.styleable#KeyboardView_keyTextShadowRadius
* @attr ref R.styleable#KeyboardView_verticalCorrection
* @attr ref R.styleable#Keyboard_Key_keyTypeface
* @attr ref R.styleable#Keyboard_Key_keyLetterSize
* @attr ref R.styleable#Keyboard_Key_keyLabelSize
* @attr ref R.styleable#Keyboard_Key_keyLargeLetterRatio
* @attr ref R.styleable#Keyboard_Key_keyLargeLabelRatio
* @attr ref R.styleable#Keyboard_Key_keyHintLetterRatio
* @attr ref R.styleable#Keyboard_Key_keyShiftedLetterHintRatio
* @attr ref R.styleable#Keyboard_Key_keyHintLabelRatio
* @attr ref R.styleable#Keyboard_Key_keyLabelOffCenterRatio
* @attr ref R.styleable#Keyboard_Key_keyHintLabelOffCenterRatio
* @attr ref R.styleable#Keyboard_Key_keyPreviewTextRatio
* @attr ref R.styleable#Keyboard_Key_keyTextColor
* @attr ref R.styleable#Keyboard_Key_keyTextColorDisabled
* @attr ref R.styleable#Keyboard_Key_keyTextShadowColor
* @attr ref R.styleable#Keyboard_Key_keyHintLetterColor
* @attr ref R.styleable#Keyboard_Key_keyHintLabelColor
* @attr ref R.styleable#Keyboard_Key_keyShiftedLetterHintInactivatedColor
* @attr ref R.styleable#Keyboard_Key_keyShiftedLetterHintActivatedColor
* @attr ref R.styleable#Keyboard_Key_keyPreviewTextColor
*/
public class KeyboardView extends View {
// XML attributes
private final KeyVisualAttributes mKeyVisualAttributes;
// Default keyLabelFlags from {@link KeyboardTheme}.
// Currently only "alignHintLabelToBottom" is supported.
private final int mDefaultKeyLabelFlags;
private final float mKeyHintLetterPadding;
private final float mKeyShiftedLetterHintPadding;
private final float mKeyTextShadowRadius;
private final float mVerticalCorrection;
private final Drawable mKeyBackground;
private final Drawable mFunctionalKeyBackground;
private final Drawable mSpacebarBackground;
private final float mSpacebarIconWidthRatio;
private final Rect mKeyBackgroundPadding = new Rect();
private static final float KET_TEXT_SHADOW_RADIUS_DISABLED = -1.0f;
public int mCustomColor = 0;
// The maximum key label width in the proportion to the key width.
private static final float MAX_LABEL_RATIO = 0.90f;
// Main keyboard
// TODO: Consider having a dummy keyboard object to make this @NonNull
private Keyboard mKeyboard;
private final KeyDrawParams mKeyDrawParams = new KeyDrawParams();
// Drawing
/** True if all keys should be drawn */
private boolean mInvalidateAllKeys;
/** The keys that should be drawn */
private final HashSet<Key> mInvalidatedKeys = new HashSet<>();
/** The working rectangle for clipping */
private final Rect mClipRect = new Rect();
/** The keyboard bitmap buffer for faster updates */
private Bitmap mOffscreenBuffer;
/** The canvas for the above mutable keyboard bitmap */
private final Canvas mOffscreenCanvas = new Canvas();
private final Paint mPaint = new Paint();
private final Paint.FontMetrics mFontMetrics = new Paint.FontMetrics();
public KeyboardView(final Context context, final AttributeSet attrs) {
this(context, attrs, R.attr.keyboardViewStyle);
}
public KeyboardView(final Context context, final AttributeSet attrs, final int defStyle) {
super(context, attrs, defStyle);
final TypedArray keyboardViewAttr = context.obtainStyledAttributes(attrs,
R.styleable.KeyboardView, defStyle, R.style.KeyboardView);
mKeyBackground = keyboardViewAttr.getDrawable(R.styleable.KeyboardView_keyBackground);
mKeyBackground.getPadding(mKeyBackgroundPadding);
final Drawable functionalKeyBackground = keyboardViewAttr.getDrawable(
R.styleable.KeyboardView_functionalKeyBackground);
mFunctionalKeyBackground = (functionalKeyBackground != null) ? functionalKeyBackground
: mKeyBackground;
final Drawable spacebarBackground = keyboardViewAttr.getDrawable(
R.styleable.KeyboardView_spacebarBackground);
mSpacebarBackground = (spacebarBackground != null) ? spacebarBackground : mKeyBackground;
mSpacebarIconWidthRatio = keyboardViewAttr.getFloat(
R.styleable.KeyboardView_spacebarIconWidthRatio, 1.0f);
mKeyHintLetterPadding = keyboardViewAttr.getDimension(
R.styleable.KeyboardView_keyHintLetterPadding, 0.0f);
mKeyShiftedLetterHintPadding = keyboardViewAttr.getDimension(
R.styleable.KeyboardView_keyShiftedLetterHintPadding, 0.0f);
mKeyTextShadowRadius = keyboardViewAttr.getFloat(
R.styleable.KeyboardView_keyTextShadowRadius, KET_TEXT_SHADOW_RADIUS_DISABLED);
mVerticalCorrection = keyboardViewAttr.getDimension(
R.styleable.KeyboardView_verticalCorrection, 0.0f);
keyboardViewAttr.recycle();
final TypedArray keyAttr = context.obtainStyledAttributes(attrs,
R.styleable.Keyboard_Key, defStyle, R.style.KeyboardView);
mDefaultKeyLabelFlags = keyAttr.getInt(R.styleable.Keyboard_Key_keyLabelFlags, 0);
mKeyVisualAttributes = KeyVisualAttributes.newInstance(keyAttr);
keyAttr.recycle();
mPaint.setAntiAlias(true);
}
private static void blendAlpha(final Paint paint, final int alpha) {
final int color = paint.getColor();
paint.setARGB((paint.getAlpha() * alpha) / Constants.Color.ALPHA_OPAQUE,
Color.red(color), Color.green(color), Color.blue(color));
}
/**
* Attaches a keyboard to this view. The keyboard can be switched at any time and the
* view will re-layout itself to accommodate the keyboard.
* @see Keyboard
* @see #getKeyboard()
* @param keyboard the keyboard to display in this view
*/
public void setKeyboard(final Keyboard keyboard) {
mKeyboard = keyboard;
final int keyHeight = keyboard.mMostCommonKeyHeight;
mKeyDrawParams.updateParams(keyHeight, mKeyVisualAttributes);
mKeyDrawParams.updateParams(keyHeight, keyboard.mKeyVisualAttributes);
final SharedPreferences prefs = PreferenceManagerCompat.getDeviceSharedPreferences(getContext());
mCustomColor = Settings.readKeyboardColor(prefs, getContext());
invalidateAllKeys();
requestLayout();
}
/**
* Returns the current keyboard being displayed by this view.
* @return the currently attached keyboard
* @see #setKeyboard(Keyboard)
*/
public Keyboard getKeyboard() {
return mKeyboard;
}
protected float getVerticalCorrection() {
return mVerticalCorrection;
}
protected KeyDrawParams getKeyDrawParams() {
return mKeyDrawParams;
}
@Override
protected void onMeasure(final int widthMeasureSpec, final int heightMeasureSpec) {
final Keyboard keyboard = getKeyboard();
if (keyboard == null) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
return;
}
// The main keyboard expands to the entire this {@link KeyboardView}.
final int width = keyboard.mOccupiedWidth + getPaddingLeft() + getPaddingRight();
final int height = keyboard.mOccupiedHeight + getPaddingTop() + getPaddingBottom();
setMeasuredDimension(width, height);
}
@Override
protected void onDraw(final Canvas canvas) {
super.onDraw(canvas);
if (canvas.isHardwareAccelerated()) {
onDrawKeyboard(canvas);
return;
}
final boolean bufferNeedsUpdates = mInvalidateAllKeys || !mInvalidatedKeys.isEmpty();
if (bufferNeedsUpdates || mOffscreenBuffer == null) {
if (maybeAllocateOffscreenBuffer()) {
mInvalidateAllKeys = true;
// TODO: Stop using the offscreen canvas even when in software rendering
mOffscreenCanvas.setBitmap(mOffscreenBuffer);
}
onDrawKeyboard(mOffscreenCanvas);
}
canvas.drawBitmap(mOffscreenBuffer, 0.0f, 0.0f, null);
}
private boolean maybeAllocateOffscreenBuffer() {
final int width = getWidth();
final int height = getHeight();
if (width == 0 || height == 0) {
return false;
}
if (mOffscreenBuffer != null && mOffscreenBuffer.getWidth() == width
&& mOffscreenBuffer.getHeight() == height) {
return false;
}
freeOffscreenBuffer();
mOffscreenBuffer = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
return true;
}
private void freeOffscreenBuffer() {
mOffscreenCanvas.setBitmap(null);
mOffscreenCanvas.setMatrix(null);
if (mOffscreenBuffer != null) {
mOffscreenBuffer.recycle();
mOffscreenBuffer = null;
}
}
private void onDrawKeyboard(final Canvas canvas) {
final Keyboard keyboard = getKeyboard();
if (keyboard == null) {
return;
}
final Paint paint = mPaint;
final Drawable background = getBackground();
if (Color.alpha(mCustomColor) > 0 && keyboard.getKey(Constants.CODE_SPACE) != null) {
setBackgroundColor(mCustomColor);
}
// Calculate clip region and set.
final boolean drawAllKeys = mInvalidateAllKeys || mInvalidatedKeys.isEmpty();
final boolean isHardwareAccelerated = canvas.isHardwareAccelerated();
// TODO: Confirm if it's really required to draw all keys when hardware acceleration is on.
if (drawAllKeys || isHardwareAccelerated) {
if (!isHardwareAccelerated && background != null) {
// Need to draw keyboard background on {@link #mOffscreenBuffer}.
canvas.drawColor(Color.BLACK, PorterDuff.Mode.CLEAR);
background.draw(canvas);
}
// Draw all keys.
for (final Key key : keyboard.getSortedKeys()) {
onDrawKey(key, canvas, paint);
}
} else {
for (final Key key : mInvalidatedKeys) {
if (!keyboard.hasKey(key)) {
continue;
}
if (background != null) {
// Need to redraw key's background on {@link #mOffscreenBuffer}.
final int x = key.getX() + getPaddingLeft();
final int y = key.getY() + getPaddingTop();
mClipRect.set(x, y, x + key.getWidth(), y + key.getHeight());
canvas.save();
canvas.clipRect(mClipRect);
canvas.drawColor(Color.BLACK, PorterDuff.Mode.CLEAR);
background.draw(canvas);
canvas.restore();
}
onDrawKey(key, canvas, paint);
}
}
mInvalidatedKeys.clear();
mInvalidateAllKeys = false;
}
private void onDrawKey(final Key key, final Canvas canvas,
final Paint paint) {
final int keyDrawX = key.getX() + getPaddingLeft();
final int keyDrawY = key.getY() + getPaddingTop();
canvas.translate(keyDrawX, keyDrawY);
final KeyVisualAttributes attr = key.getVisualAttributes();
final KeyDrawParams params = mKeyDrawParams.mayCloneAndUpdateParams(key.getHeight(), attr);
params.mAnimAlpha = Constants.Color.ALPHA_OPAQUE;
if (!key.isSpacer()) {
final Drawable background = key.selectBackgroundDrawable(
mKeyBackground, mFunctionalKeyBackground, mSpacebarBackground);
if (background != null) {
onDrawKeyBackground(key, canvas, background);
}
}
onDrawKeyTopVisuals(key, canvas, paint, params);
canvas.translate(-keyDrawX, -keyDrawY);
}
// Draw key background.
protected void onDrawKeyBackground(final Key key, final Canvas canvas,
final Drawable background) {
final int keyWidth = key.getWidth();
final int keyHeight = key.getHeight();
final Rect padding = mKeyBackgroundPadding;
final int bgWidth = keyWidth + padding.left + padding.right;
final int bgHeight = keyHeight + padding.top + padding.bottom;
final int bgX = -padding.left;
final int bgY = -padding.top;
final Rect bounds = background.getBounds();
if (bgWidth != bounds.right || bgHeight != bounds.bottom) {
background.setBounds(0, 0, bgWidth, bgHeight);
}
canvas.translate(bgX, bgY);
background.draw(canvas);
canvas.translate(-bgX, -bgY);
}
// Draw key top visuals.
protected void onDrawKeyTopVisuals(final Key key,final Canvas canvas,
final Paint paint, final KeyDrawParams params) {
final int keyWidth = key.getWidth();
final int keyHeight = key.getHeight();
final float centerX = keyWidth * 0.5f;
final float centerY = keyHeight * 0.5f;
// Draw key label.
final Keyboard keyboard = getKeyboard();
final Drawable icon = (keyboard == null) ? null
: key.getIcon(keyboard.mIconsSet, params.mAnimAlpha);
float labelX = centerX;
float labelBaseline = centerY;
final String label = key.getLabel();
if (label != null) {
paint.setTypeface(key.selectTypeface(params));
paint.setTextSize(key.selectTextSize(params));
final float labelCharHeight = TypefaceUtils.getReferenceCharHeight(paint);
final float labelCharWidth = TypefaceUtils.getReferenceCharWidth(paint);
// Vertical label text alignment.
labelBaseline = centerY + labelCharHeight / 2.0f;
// Horizontal label text alignment
if (key.isAlignLabelOffCenter()) {
// The label is placed off center of the key. Used mainly on "phone number" layout.
labelX = centerX + params.mLabelOffCenterRatio * labelCharWidth;
paint.setTextAlign(Align.LEFT);
} else {
labelX = centerX;
paint.setTextAlign(Align.CENTER);
}
if (key.needsAutoXScale()) {
final float ratio = Math.min(1.0f, (keyWidth * MAX_LABEL_RATIO) /
TypefaceUtils.getStringWidth(label, paint));
if (key.needsAutoScale()) {
final float autoSize = paint.getTextSize() * ratio;
paint.setTextSize(autoSize);
} else {
paint.setTextScaleX(ratio);
}
}
paint.setColor(key.selectTextColor(params));
// Set a drop shadow for the text if the shadow radius is positive value.
if (mKeyTextShadowRadius > 0.0f) {
paint.setShadowLayer(mKeyTextShadowRadius, 0.0f, 0.0f, params.mTextShadowColor);
} else {
paint.clearShadowLayer();
}
blendAlpha(paint, params.mAnimAlpha);
canvas.drawText(label, 0, label.length(), labelX, labelBaseline, paint);
// Turn off drop shadow and reset x-scale.
paint.clearShadowLayer();
paint.setTextScaleX(1.0f);
}
// Draw hint label.
final String hintLabel = key.getHintLabel();
if (hintLabel != null) {
paint.setTextSize(key.selectHintTextSize(params));
paint.setColor(key.selectHintTextColor(params));
// TODO: Should add a way to specify type face for hint letters
paint.setTypeface(Typeface.DEFAULT_BOLD);
blendAlpha(paint, params.mAnimAlpha);
final float labelCharHeight = TypefaceUtils.getReferenceCharHeight(paint);
final float labelCharWidth = TypefaceUtils.getReferenceCharWidth(paint);
final float hintX, hintBaseline;
if (key.hasHintLabel()) {
// The hint label is placed just right of the key label. Used mainly on
// "phone number" layout.
hintX = labelX + params.mHintLabelOffCenterRatio * labelCharWidth;
if (key.isAlignHintLabelToBottom(mDefaultKeyLabelFlags)) {
hintBaseline = labelBaseline;
} else {
hintBaseline = centerY + labelCharHeight / 2.0f;
}
paint.setTextAlign(Align.LEFT);
} else if (key.hasShiftedLetterHint()) {
// The hint label is placed at top-right corner of the key. Used mainly on tablet.
hintX = keyWidth - mKeyShiftedLetterHintPadding - labelCharWidth / 2.0f;
paint.getFontMetrics(mFontMetrics);
hintBaseline = -mFontMetrics.top;
paint.setTextAlign(Align.CENTER);
} else { // key.hasHintLetter()
// The hint letter is placed at top-right corner of the key. Used mainly on phone.
final float hintDigitWidth = TypefaceUtils.getReferenceDigitWidth(paint);
final float hintLabelWidth = TypefaceUtils.getStringWidth(hintLabel, paint);
hintX = keyWidth - mKeyHintLetterPadding
- Math.max(hintDigitWidth, hintLabelWidth) / 2.0f;
hintBaseline = -paint.ascent();
paint.setTextAlign(Align.CENTER);
}
final float adjustmentY = params.mHintLabelVerticalAdjustment * labelCharHeight;
canvas.drawText(
hintLabel, 0, hintLabel.length(), hintX, hintBaseline + adjustmentY, paint);
}
// Draw key icon.
if (label == null && icon != null) {
final int iconWidth;
if (key.getCode() == Constants.CODE_SPACE && icon instanceof NinePatchDrawable) {
iconWidth = (int)(keyWidth * mSpacebarIconWidthRatio);
} else {
iconWidth = Math.min(icon.getIntrinsicWidth(), keyWidth);
}
final int iconHeight = icon.getIntrinsicHeight();
final int iconY;
if (key.isAlignIconToBottom()) {
iconY = keyHeight - iconHeight;
} else {
iconY = (keyHeight - iconHeight) / 2; // Align vertically center.
}
final int iconX = (keyWidth - iconWidth) / 2; // Align horizontally center.
drawIcon(canvas, icon, iconX, iconY, iconWidth, iconHeight);
}
}
protected static void drawIcon(final Canvas canvas, final Drawable icon,
final int x, final int y, final int width, final int height) {
canvas.translate(x, y);
icon.setBounds(0, 0, width, height);
icon.draw(canvas);
canvas.translate(-x, -y);
}
public Paint newLabelPaint(final Key key) {
final Paint paint = new Paint();
paint.setAntiAlias(true);
if (key == null) {
paint.setTypeface(mKeyDrawParams.mTypeface);
paint.setTextSize(mKeyDrawParams.mLabelSize);
} else {
paint.setColor(key.selectTextColor(mKeyDrawParams));
paint.setTypeface(key.selectTypeface(mKeyDrawParams));
paint.setTextSize(key.selectTextSize(mKeyDrawParams));
}
return paint;
}
/**
* Requests a redraw of the entire keyboard. Calling {@link #invalidate} is not sufficient
* because the keyboard renders the keys to an off-screen buffer and an invalidate() only
* draws the cached buffer.
* @see #invalidateKey(Key)
*/
public void invalidateAllKeys() {
mInvalidatedKeys.clear();
mInvalidateAllKeys = true;
invalidate();
}
/**
* Invalidates a key so that it will be redrawn on the next repaint. Use this method if only
* one key is changing it's content. Any changes that affect the position or size of the key
* may not be honored.
* @param key key in the attached {@link Keyboard}.
* @see #invalidateAllKeys
*/
public void invalidateKey(final Key key) {
if (mInvalidateAllKeys || key == null) {
return;
}
mInvalidatedKeys.add(key);
final int x = key.getX() + getPaddingLeft();
final int y = key.getY() + getPaddingTop();
invalidate(x, y, x + key.getWidth(), y + key.getHeight());
}
@Override
protected void onDetachedFromWindow() {
super.onDetachedFromWindow();
freeOffscreenBuffer();
}
public void deallocateMemory() {
freeOffscreenBuffer();
}
}

View File

@ -0,0 +1,617 @@
/*
* Copyright (C) 2011 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.colorful.keyboard.theme.keyboard;
import android.animation.AnimatorInflater;
import android.animation.ObjectAnimator;
import android.content.Context;
import android.content.pm.PackageManager;
import android.content.res.TypedArray;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Paint.Align;
import android.graphics.Typeface;
import android.util.AttributeSet;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
import com.colorful.keyboard.theme.R;
import com.colorful.keyboard.theme.keyboard.internal.DrawingPreviewPlacerView;
import com.colorful.keyboard.theme.keyboard.internal.DrawingProxy;
import com.colorful.keyboard.theme.keyboard.internal.KeyDrawParams;
import com.colorful.keyboard.theme.keyboard.internal.KeyPreviewChoreographer;
import com.colorful.keyboard.theme.keyboard.internal.KeyPreviewDrawParams;
import com.colorful.keyboard.theme.keyboard.internal.KeyPreviewView;
import com.colorful.keyboard.theme.keyboard.internal.MoreKeySpec;
import com.colorful.keyboard.theme.keyboard.internal.NonDistinctMultitouchHelper;
import com.colorful.keyboard.theme.keyboard.internal.TimerHandler;
import com.colorful.keyboard.theme.latin.RichInputMethodManager;
import com.colorful.keyboard.theme.latin.Subtype;
import com.colorful.keyboard.theme.latin.common.Constants;
import com.colorful.keyboard.theme.latin.common.CoordinateUtils;
import com.colorful.keyboard.theme.latin.utils.LanguageOnSpacebarUtils;
import com.colorful.keyboard.theme.latin.utils.LocaleResourceUtils;
import com.colorful.keyboard.theme.latin.utils.TypefaceUtils;
import java.util.WeakHashMap;
public final class MainKeyboardView extends KeyboardView implements MoreKeysPanel.Controller, DrawingProxy {
private static final String TAG = MainKeyboardView.class.getSimpleName();
/** Listener for {@link KeyboardActionListener}. */
private KeyboardActionListener mKeyboardActionListener;
/* Space key and its icon and background. */
private Key mSpaceKey;
// Stuff to draw language name on spacebar.
private final int mLanguageOnSpacebarFinalAlpha;
private ObjectAnimator mLanguageOnSpacebarFadeoutAnimator;
private int mLanguageOnSpacebarFormatType;
private int mLanguageOnSpacebarAnimAlpha = Constants.Color.ALPHA_OPAQUE;
private final float mLanguageOnSpacebarTextRatio;
private float mLanguageOnSpacebarTextSize;
private final int mLanguageOnSpacebarTextColor;
// The minimum x-scale to fit the language name on spacebar.
private static final float MINIMUM_XSCALE_OF_LANGUAGE_NAME = 0.8f;
// Stuff to draw altCodeWhileTyping keys.
private final ObjectAnimator mAltCodeKeyWhileTypingFadeoutAnimator;
private final ObjectAnimator mAltCodeKeyWhileTypingFadeinAnimator;
private int mAltCodeKeyWhileTypingAnimAlpha = Constants.Color.ALPHA_OPAQUE;
// Drawing preview placer view
private final DrawingPreviewPlacerView mDrawingPreviewPlacerView;
private final int[] mOriginCoords = CoordinateUtils.newInstance();
// Key preview
private final KeyPreviewDrawParams mKeyPreviewDrawParams;
private final KeyPreviewChoreographer mKeyPreviewChoreographer;
// More keys keyboard
private final Paint mBackgroundDimAlphaPaint = new Paint();
private final View mMoreKeysKeyboardContainer;
private final WeakHashMap<Key, Keyboard> mMoreKeysKeyboardCache = new WeakHashMap<>();
private final boolean mConfigShowMoreKeysKeyboardAtTouchedPoint;
// More keys panel (used by both more keys keyboard and more suggestions view)
// TODO: Consider extending to support multiple more keys panels
private MoreKeysPanel mMoreKeysPanel;
private final KeyDetector mKeyDetector;
private final NonDistinctMultitouchHelper mNonDistinctMultitouchHelper;
private final TimerHandler mTimerHandler;
private final int mLanguageOnSpacebarHorizontalMargin;
public MainKeyboardView(final Context context, final AttributeSet attrs) {
this(context, attrs, R.attr.mainKeyboardViewStyle);
}
public MainKeyboardView(final Context context, final AttributeSet attrs, final int defStyle) {
super(context, attrs, defStyle);
final DrawingPreviewPlacerView drawingPreviewPlacerView =
new DrawingPreviewPlacerView(context, attrs);
final TypedArray mainKeyboardViewAttr = context.obtainStyledAttributes(
attrs, R.styleable.MainKeyboardView, defStyle, R.style.MainKeyboardView);
final int ignoreAltCodeKeyTimeout = mainKeyboardViewAttr.getInt(
R.styleable.MainKeyboardView_ignoreAltCodeKeyTimeout, 0);
mTimerHandler = new TimerHandler(this, ignoreAltCodeKeyTimeout);
final float keyHysteresisDistance = mainKeyboardViewAttr.getDimension(
R.styleable.MainKeyboardView_keyHysteresisDistance, 0.0f);
final float keyHysteresisDistanceForSlidingModifier = mainKeyboardViewAttr.getDimension(
R.styleable.MainKeyboardView_keyHysteresisDistanceForSlidingModifier, 0.0f);
mKeyDetector = new KeyDetector(
keyHysteresisDistance, keyHysteresisDistanceForSlidingModifier);
PointerTracker.init(mainKeyboardViewAttr, mTimerHandler, this /* DrawingProxy */);
final boolean hasDistinctMultitouch = context.getPackageManager()
.hasSystemFeature(PackageManager.FEATURE_TOUCHSCREEN_MULTITOUCH_DISTINCT);
mNonDistinctMultitouchHelper = hasDistinctMultitouch ? null
: new NonDistinctMultitouchHelper();
final int backgroundDimAlpha = mainKeyboardViewAttr.getInt(
R.styleable.MainKeyboardView_backgroundDimAlpha, 0);
mBackgroundDimAlphaPaint.setColor(Color.BLACK);
mBackgroundDimAlphaPaint.setAlpha(backgroundDimAlpha);
mLanguageOnSpacebarTextRatio = mainKeyboardViewAttr.getFraction(
R.styleable.MainKeyboardView_languageOnSpacebarTextRatio, 1, 1, 1.0f);
mLanguageOnSpacebarTextColor = mainKeyboardViewAttr.getColor(
R.styleable.MainKeyboardView_languageOnSpacebarTextColor, 0);
mLanguageOnSpacebarFinalAlpha = mainKeyboardViewAttr.getInt(
R.styleable.MainKeyboardView_languageOnSpacebarFinalAlpha,
Constants.Color.ALPHA_OPAQUE);
final int languageOnSpacebarFadeoutAnimatorResId = mainKeyboardViewAttr.getResourceId(
R.styleable.MainKeyboardView_languageOnSpacebarFadeoutAnimator, 0);
final int altCodeKeyWhileTypingFadeoutAnimatorResId = mainKeyboardViewAttr.getResourceId(
R.styleable.MainKeyboardView_altCodeKeyWhileTypingFadeoutAnimator, 0);
final int altCodeKeyWhileTypingFadeinAnimatorResId = mainKeyboardViewAttr.getResourceId(
R.styleable.MainKeyboardView_altCodeKeyWhileTypingFadeinAnimator, 0);
mKeyPreviewDrawParams = new KeyPreviewDrawParams(mainKeyboardViewAttr);
mKeyPreviewChoreographer = new KeyPreviewChoreographer(mKeyPreviewDrawParams);
final int moreKeysKeyboardLayoutId = mainKeyboardViewAttr.getResourceId(
R.styleable.MainKeyboardView_moreKeysKeyboardLayout, 0);
mConfigShowMoreKeysKeyboardAtTouchedPoint = mainKeyboardViewAttr.getBoolean(
R.styleable.MainKeyboardView_showMoreKeysKeyboardAtTouchedPoint, false);
mainKeyboardViewAttr.recycle();
mDrawingPreviewPlacerView = drawingPreviewPlacerView;
final LayoutInflater inflater = LayoutInflater.from(getContext());
mMoreKeysKeyboardContainer = inflater.inflate(moreKeysKeyboardLayoutId, null);
mLanguageOnSpacebarFadeoutAnimator = loadObjectAnimator(
languageOnSpacebarFadeoutAnimatorResId, this);
mAltCodeKeyWhileTypingFadeoutAnimator = loadObjectAnimator(
altCodeKeyWhileTypingFadeoutAnimatorResId, this);
mAltCodeKeyWhileTypingFadeinAnimator = loadObjectAnimator(
altCodeKeyWhileTypingFadeinAnimatorResId, this);
mKeyboardActionListener = KeyboardActionListener.EMPTY_LISTENER;
mLanguageOnSpacebarHorizontalMargin = (int)getResources().getDimension(
R.dimen.config_language_on_spacebar_horizontal_margin);
}
private ObjectAnimator loadObjectAnimator(final int resId, final Object target) {
if (resId == 0) {
// TODO: Stop returning null.
return null;
}
final ObjectAnimator animator = (ObjectAnimator)AnimatorInflater.loadAnimator(
getContext(), resId);
if (animator != null) {
animator.setTarget(target);
}
return animator;
}
private static void cancelAndStartAnimators(final ObjectAnimator animatorToCancel,
final ObjectAnimator animatorToStart) {
if (animatorToCancel == null || animatorToStart == null) {
// TODO: Stop using null as a no-operation animator.
return;
}
float startFraction = 0.0f;
if (animatorToCancel.isStarted()) {
animatorToCancel.cancel();
startFraction = 1.0f - animatorToCancel.getAnimatedFraction();
}
final long startTime = (long)(animatorToStart.getDuration() * startFraction);
animatorToStart.start();
animatorToStart.setCurrentPlayTime(startTime);
}
// Implements {@link DrawingProxy#startWhileTypingAnimation(int)}.
/**
* Called when a while-typing-animation should be started.
* @param fadeInOrOut {@link DrawingProxy#FADE_IN} starts while-typing-fade-in animation.
* {@link DrawingProxy#FADE_OUT} starts while-typing-fade-out animation.
*/
@Override
public void startWhileTypingAnimation(final int fadeInOrOut) {
switch (fadeInOrOut) {
case DrawingProxy.FADE_IN:
cancelAndStartAnimators(
mAltCodeKeyWhileTypingFadeoutAnimator, mAltCodeKeyWhileTypingFadeinAnimator);
break;
case DrawingProxy.FADE_OUT:
cancelAndStartAnimators(
mAltCodeKeyWhileTypingFadeinAnimator, mAltCodeKeyWhileTypingFadeoutAnimator);
break;
}
}
public void setLanguageOnSpacebarAnimAlpha(final int alpha) {
mLanguageOnSpacebarAnimAlpha = alpha;
invalidateKey(mSpaceKey);
}
public void setKeyboardActionListener(final KeyboardActionListener listener) {
mKeyboardActionListener = listener;
PointerTracker.setKeyboardActionListener(listener);
}
// TODO: We should reconsider which coordinate system should be used to represent keyboard
// event.
public int getKeyX(final int x) {
return Constants.isValidCoordinate(x) ? mKeyDetector.getTouchX(x) : x;
}
// TODO: We should reconsider which coordinate system should be used to represent keyboard
// event.
public int getKeyY(final int y) {
return Constants.isValidCoordinate(y) ? mKeyDetector.getTouchY(y) : y;
}
/**
* Attaches a keyboard to this view. The keyboard can be switched at any time and the
* view will re-layout itself to accommodate the keyboard.
* @see Keyboard
* @see #getKeyboard()
* @param keyboard the keyboard to display in this view
*/
@Override
public void setKeyboard(final Keyboard keyboard) {
// Remove any pending messages, except dismissing preview and key repeat.
mTimerHandler.cancelLongPressTimers();
super.setKeyboard(keyboard);
mKeyDetector.setKeyboard(
keyboard, -getPaddingLeft(), -getPaddingTop() + getVerticalCorrection());
PointerTracker.setKeyDetector(mKeyDetector);
mMoreKeysKeyboardCache.clear();
mSpaceKey = keyboard.getKey(Constants.CODE_SPACE);
final int keyHeight = keyboard.mMostCommonKeyHeight;
mLanguageOnSpacebarTextSize = keyHeight * mLanguageOnSpacebarTextRatio;
}
/**
* Enables or disables the key preview popup. This is a popup that shows a magnified
* version of the depressed key. By default the preview is enabled.
* @param previewEnabled whether or not to enable the key feedback preview
* @param delay the delay after which the preview is dismissed
*/
public void setKeyPreviewPopupEnabled(final boolean previewEnabled, final int delay) {
mKeyPreviewDrawParams.setPopupEnabled(previewEnabled, delay);
}
private void locatePreviewPlacerView() {
getLocationInWindow(mOriginCoords);
mDrawingPreviewPlacerView.setKeyboardViewGeometry(mOriginCoords);
}
private void installPreviewPlacerView() {
final View rootView = getRootView();
if (rootView == null) {
Log.w(TAG, "Cannot find root view");
return;
}
final ViewGroup windowContentView = (ViewGroup)rootView.findViewById(android.R.id.content);
// Note: It'd be very weird if we get null by android.R.id.content.
if (windowContentView == null) {
Log.w(TAG, "Cannot find android.R.id.content view to add DrawingPreviewPlacerView");
return;
}
windowContentView.addView(mDrawingPreviewPlacerView);
}
// Implements {@link DrawingProxy#onKeyPressed(Key,boolean)}.
@Override
public void onKeyPressed(final Key key, final boolean withPreview) {
key.onPressed();
invalidateKey(key);
if (withPreview && !key.noKeyPreview()) {
showKeyPreview(key);
}
}
private void showKeyPreview(final Key key) {
final Keyboard keyboard = getKeyboard();
if (keyboard == null) {
return;
}
final KeyPreviewDrawParams previewParams = mKeyPreviewDrawParams;
if (!previewParams.isPopupEnabled()) {
previewParams.setVisibleOffset(-Math.round(keyboard.mVerticalGap));
return;
}
locatePreviewPlacerView();
getLocationInWindow(mOriginCoords);
mKeyPreviewChoreographer.placeAndShowKeyPreview(key, keyboard.mIconsSet, getKeyDrawParams(),
mOriginCoords, mDrawingPreviewPlacerView, isHardwareAccelerated());
}
private void dismissKeyPreviewWithoutDelay(final Key key) {
mKeyPreviewChoreographer.dismissKeyPreview(key, false /* withAnimation */);
invalidateKey(key);
}
// Implements {@link DrawingProxy#onKeyReleased(Key,boolean)}.
@Override
public void onKeyReleased(final Key key, final boolean withAnimation) {
key.onReleased();
invalidateKey(key);
if (!key.noKeyPreview()) {
if (withAnimation) {
dismissKeyPreview(key);
} else {
dismissKeyPreviewWithoutDelay(key);
}
}
}
private void dismissKeyPreview(final Key key) {
if (isHardwareAccelerated()) {
mKeyPreviewChoreographer.dismissKeyPreview(key, true /* withAnimation */);
return;
}
// TODO: Implement preference option to control key preview method and duration.
mTimerHandler.postDismissKeyPreview(key, mKeyPreviewDrawParams.getLingerTimeout());
}
@Override
protected void onAttachedToWindow() {
super.onAttachedToWindow();
installPreviewPlacerView();
}
@Override
protected void onDetachedFromWindow() {
super.onDetachedFromWindow();
mDrawingPreviewPlacerView.removeAllViews();
}
// Implements {@link DrawingProxy@showMoreKeysKeyboard(Key,PointerTracker)}.
//@Override
public MoreKeysPanel showMoreKeysKeyboard(final Key key,
final PointerTracker tracker) {
final MoreKeySpec[] moreKeys = key.getMoreKeys();
if (moreKeys == null) {
return null;
}
Keyboard moreKeysKeyboard = mMoreKeysKeyboardCache.get(key);
if (moreKeysKeyboard == null) {
// {@link KeyPreviewDrawParams#mPreviewVisibleWidth} should have been set at
// {@link KeyPreviewChoreographer#placeKeyPreview(Key,TextView,KeyboardIconsSet,KeyDrawParams,int,int[]},
// though there may be some chances that the value is zero. <code>width == 0</code>
// will cause zero-division error at
// {@link MoreKeysKeyboardParams#setParameters(int,int,int,int,int,int,boolean,int)}.
final boolean isSingleMoreKeyWithPreview = mKeyPreviewDrawParams.isPopupEnabled()
&& !key.noKeyPreview() && moreKeys.length == 1
&& mKeyPreviewDrawParams.getVisibleWidth() > 0;
final MoreKeysKeyboard.Builder builder = new MoreKeysKeyboard.Builder(
getContext(), key, getKeyboard(), isSingleMoreKeyWithPreview,
mKeyPreviewDrawParams.getVisibleWidth(),
mKeyPreviewDrawParams.getVisibleHeight(), newLabelPaint(key));
moreKeysKeyboard = builder.build();
mMoreKeysKeyboardCache.put(key, moreKeysKeyboard);
}
final MoreKeysKeyboardView moreKeysKeyboardView =
mMoreKeysKeyboardContainer.findViewById(R.id.more_keys_keyboard_view);
moreKeysKeyboardView.setKeyboard(moreKeysKeyboard);
mMoreKeysKeyboardContainer.measure(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
final int[] lastCoords = CoordinateUtils.newInstance();
tracker.getLastCoordinates(lastCoords);
final boolean keyPreviewEnabled = mKeyPreviewDrawParams.isPopupEnabled()
&& !key.noKeyPreview();
// The more keys keyboard is usually horizontally aligned with the center of the parent key.
// If showMoreKeysKeyboardAtTouchedPoint is true and the key preview is disabled, the more
// keys keyboard is placed at the touch point of the parent key.
final int pointX = (mConfigShowMoreKeysKeyboardAtTouchedPoint && !keyPreviewEnabled)
? CoordinateUtils.x(lastCoords)
: key.getX() + key.getWidth() / 2;
// The more keys keyboard is usually vertically aligned with the top edge of the parent key
// (plus vertical gap). If the key preview is enabled, the more keys keyboard is vertically
// aligned with the bottom edge of the visible part of the key preview.
// {@code mPreviewVisibleOffset} has been set appropriately in
// {@link KeyboardView#showKeyPreview(PointerTracker)}.
final int pointY = key.getY() + mKeyPreviewDrawParams.getVisibleOffset()
+ Math.round(moreKeysKeyboard.mBottomPadding);
moreKeysKeyboardView.showMoreKeysPanel(this, this, pointX, pointY, mKeyboardActionListener);
return moreKeysKeyboardView;
}
public boolean isInDraggingFinger() {
if (isShowingMoreKeysPanel()) {
return true;
}
return PointerTracker.isAnyInDraggingFinger();
}
@Override
public void onShowMoreKeysPanel(final MoreKeysPanel panel) {
locatePreviewPlacerView();
// Dismiss another {@link MoreKeysPanel} that may be being showed.
onDismissMoreKeysPanel();
// Dismiss all key previews that may be being showed.
PointerTracker.setReleasedKeyGraphicsToAllKeys();
// Dismiss sliding key input preview that may be being showed.
panel.showInParent(mDrawingPreviewPlacerView);
mMoreKeysPanel = panel;
}
public boolean isShowingMoreKeysPanel() {
return mMoreKeysPanel != null && mMoreKeysPanel.isShowingInParent();
}
@Override
public void onCancelMoreKeysPanel() {
PointerTracker.dismissAllMoreKeysPanels();
}
@Override
public void onDismissMoreKeysPanel() {
if (isShowingMoreKeysPanel()) {
mMoreKeysPanel.removeFromParent();
mMoreKeysPanel = null;
}
}
public void startDoubleTapShiftKeyTimer() {
mTimerHandler.startDoubleTapShiftKeyTimer();
}
public void cancelDoubleTapShiftKeyTimer() {
mTimerHandler.cancelDoubleTapShiftKeyTimer();
}
public boolean isInDoubleTapShiftKeyTimeout() {
return mTimerHandler.isInDoubleTapShiftKeyTimeout();
}
@Override
public boolean onTouchEvent(final MotionEvent event) {
if (getKeyboard() == null) {
return false;
}
if (mNonDistinctMultitouchHelper != null) {
if (event.getPointerCount() > 1 && mTimerHandler.isInKeyRepeat()) {
// Key repeating timer will be canceled if 2 or more keys are in action.
mTimerHandler.cancelKeyRepeatTimers();
}
// Non distinct multitouch screen support
mNonDistinctMultitouchHelper.processMotionEvent(event, mKeyDetector);
return true;
}
return processMotionEvent(event);
}
public boolean processMotionEvent(final MotionEvent event) {
final int index = event.getActionIndex();
final int id = event.getPointerId(index);
final PointerTracker tracker = PointerTracker.getPointerTracker(id);
// When a more keys panel is showing, we should ignore other fingers' single touch events
// other than the finger that is showing the more keys panel.
if (isShowingMoreKeysPanel() && !tracker.isShowingMoreKeysPanel()
&& PointerTracker.getActivePointerTrackerCount() == 1) {
return true;
}
tracker.processMotionEvent(event, mKeyDetector);
return true;
}
public void cancelAllOngoingEvents() {
mTimerHandler.cancelAllMessages();
PointerTracker.setReleasedKeyGraphicsToAllKeys();
PointerTracker.dismissAllMoreKeysPanels();
PointerTracker.cancelAllPointerTrackers();
}
public void closing() {
cancelAllOngoingEvents();
mMoreKeysKeyboardCache.clear();
}
public void onHideWindow() {
onDismissMoreKeysPanel();
}
public void startDisplayLanguageOnSpacebar(final boolean subtypeChanged,
final int languageOnSpacebarFormatType) {
if (subtypeChanged) {
KeyPreviewView.clearTextCache();
}
mLanguageOnSpacebarFormatType = languageOnSpacebarFormatType;
final ObjectAnimator animator = mLanguageOnSpacebarFadeoutAnimator;
if (animator == null) {
mLanguageOnSpacebarFormatType = LanguageOnSpacebarUtils.FORMAT_TYPE_NONE;
} else {
if (subtypeChanged
&& languageOnSpacebarFormatType != LanguageOnSpacebarUtils.FORMAT_TYPE_NONE) {
setLanguageOnSpacebarAnimAlpha(Constants.Color.ALPHA_OPAQUE);
if (animator.isStarted()) {
animator.cancel();
}
animator.start();
} else {
if (!animator.isStarted()) {
mLanguageOnSpacebarAnimAlpha = mLanguageOnSpacebarFinalAlpha;
}
}
}
invalidateKey(mSpaceKey);
}
@Override
protected void onDrawKeyTopVisuals(final Key key, final Canvas canvas, final Paint paint,
final KeyDrawParams params) {
if (key.altCodeWhileTyping()) {
params.mAnimAlpha = mAltCodeKeyWhileTypingAnimAlpha;
}
super.onDrawKeyTopVisuals(key, canvas, paint, params);
final int code = key.getCode();
if (code == Constants.CODE_SPACE) {
// If more than one language is enabled in current input method
final RichInputMethodManager imm = RichInputMethodManager.getInstance();
if (imm.hasMultipleEnabledSubtypes()) {
drawLanguageOnSpacebar(key, canvas, paint);
}
}
}
private boolean fitsTextIntoWidth(final int width, final String text, final Paint paint) {
final int maxTextWidth = width - mLanguageOnSpacebarHorizontalMargin * 2;
paint.setTextScaleX(1.0f);
final float textWidth = TypefaceUtils.getStringWidth(text, paint);
if (textWidth < width) {
return true;
}
final float scaleX = maxTextWidth / textWidth;
if (scaleX < MINIMUM_XSCALE_OF_LANGUAGE_NAME) {
return false;
}
paint.setTextScaleX(scaleX);
return TypefaceUtils.getStringWidth(text, paint) < maxTextWidth;
}
// Layout language name on spacebar.
private String layoutLanguageOnSpacebar(final Paint paint,
final Subtype subtype, final int width) {
// Choose appropriate language name to fit into the width.
if (mLanguageOnSpacebarFormatType == LanguageOnSpacebarUtils.FORMAT_TYPE_FULL_LOCALE) {
final String fullText =
LocaleResourceUtils.getLocaleDisplayNameInLocale(subtype.getLocale());
if (fitsTextIntoWidth(width, fullText, paint)) {
return fullText;
}
}
final String middleText =
LocaleResourceUtils.getLanguageDisplayNameInLocale(subtype.getLocale());
if (fitsTextIntoWidth(width, middleText, paint)) {
return middleText;
}
return "";
}
private void drawLanguageOnSpacebar(final Key key, final Canvas canvas, final Paint paint) {
final Keyboard keyboard = getKeyboard();
if (keyboard == null) {
return;
}
final int width = key.getWidth();
final int height = key.getHeight();
paint.setTextAlign(Align.CENTER);
paint.setTypeface(Typeface.DEFAULT);
paint.setTextSize(mLanguageOnSpacebarTextSize);
final String language = layoutLanguageOnSpacebar(paint, keyboard.mId.mSubtype, width);
// Draw language text with shadow
final float descent = paint.descent();
final float textHeight = -paint.ascent() + descent;
final float baseline = height / 2 + textHeight / 2;
paint.setColor(mLanguageOnSpacebarTextColor);
paint.setAlpha(mLanguageOnSpacebarAnimAlpha);
canvas.drawText(language, width / 2, baseline - descent, paint);
paint.clearShadowLayer();
paint.setTextScaleX(1.0f);
}
}

View File

@ -0,0 +1,55 @@
/*
* Copyright (C) 2011 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.colorful.keyboard.theme.keyboard;
public final class MoreKeysDetector extends KeyDetector {
private final int mSlideAllowanceSquare;
private final int mSlideAllowanceSquareTop;
public MoreKeysDetector(float slideAllowance) {
super();
mSlideAllowanceSquare = (int)(slideAllowance * slideAllowance);
// Top slide allowance is slightly longer (sqrt(2) times) than other edges.
mSlideAllowanceSquareTop = mSlideAllowanceSquare * 2;
}
@Override
public boolean alwaysAllowsKeySelectionByDraggingFinger() {
return true;
}
@Override
public Key detectHitKey(final int x, final int y) {
final Keyboard keyboard = getKeyboard();
if (keyboard == null) {
return null;
}
final int touchX = getTouchX(x);
final int touchY = getTouchY(y);
Key nearestKey = null;
int nearestDist = (y < 0) ? mSlideAllowanceSquareTop : mSlideAllowanceSquare;
for (final Key key : keyboard.getSortedKeys()) {
final int dist = key.squaredDistanceToHitboxEdge(touchX, touchY);
if (dist < nearestDist) {
nearestKey = key;
nearestDist = dist;
}
}
return nearestKey;
}
}

View File

@ -0,0 +1,421 @@
/*
* Copyright (C) 2011 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.colorful.keyboard.theme.keyboard;
import android.content.Context;
import android.graphics.Paint;
import android.util.Log;
import com.colorful.keyboard.theme.R;
import com.colorful.keyboard.theme.keyboard.internal.KeyboardBuilder;
import com.colorful.keyboard.theme.keyboard.internal.KeyboardParams;
import com.colorful.keyboard.theme.keyboard.internal.MoreKeySpec;
import com.colorful.keyboard.theme.latin.common.StringUtils;
import com.colorful.keyboard.theme.latin.utils.TypefaceUtils;
public final class MoreKeysKeyboard extends Keyboard {
private static final String TAG = MoreKeysKeyboard.class.getSimpleName();
private final int mDefaultKeyCoordX;
private static final float FLOAT_THRESHOLD = 0.0001f;
MoreKeysKeyboard(final MoreKeysKeyboardParams params) {
super(params);
mDefaultKeyCoordX = Math.round(params.getDefaultKeyCoordX() + params.mOffsetX
+ (params.mDefaultKeyPaddedWidth - params.mHorizontalGap) / 2);
}
public int getDefaultCoordX() {
return mDefaultKeyCoordX;
}
static class MoreKeysKeyboardParams extends KeyboardParams {
public boolean mIsMoreKeysFixedOrder;
/* package */int mTopRowAdjustment;
public int mNumRows;
public int mNumColumns;
public int mTopKeys;
public int mLeftKeys;
public int mRightKeys; // includes default key.
public float mColumnWidth;
public float mOffsetX;
public MoreKeysKeyboardParams() {
super();
}
/**
* Set keyboard parameters of more keys keyboard.
*
* @param numKeys number of keys in this more keys keyboard.
* @param numColumn number of columns of this more keys keyboard.
* @param keyPaddedWidth more keys keyboard key width in pixel, including horizontal gap.
* @param rowHeight more keys keyboard row height in pixel, including vertical gap.
* @param coordXInParent coordinate x of the key preview in parent keyboard.
* @param parentKeyboardWidth parent keyboard width in pixel.
* @param isMoreKeysFixedColumn true if more keys keyboard should have
* <code>numColumn</code> columns. Otherwise more keys keyboard should have
* <code>numColumn</code> columns at most.
* @param isMoreKeysFixedOrder true if the order of more keys is determined by the order in
* the more keys' specification. Otherwise the order of more keys is automatically
* determined.
*/
public void setParameters(final int numKeys, final int numColumn,
final float keyPaddedWidth, final float rowHeight,
final float coordXInParent, final int parentKeyboardWidth,
final boolean isMoreKeysFixedColumn,
final boolean isMoreKeysFixedOrder) {
// Add the horizontal padding because there is no horizontal gap on the outside edge,
// but it is included in the key width, so this compensates for simple division and
// comparison.
final float availableWidth = parentKeyboardWidth - mLeftPadding - mRightPadding
+ mHorizontalGap;
if (availableWidth < keyPaddedWidth) {
throw new IllegalArgumentException("Keyboard is too small to hold more keys: "
+ availableWidth + " " + keyPaddedWidth);
}
mIsMoreKeysFixedOrder = isMoreKeysFixedOrder;
mDefaultKeyPaddedWidth = keyPaddedWidth;
mDefaultRowHeight = rowHeight;
final int maxColumns = getMaxKeys(availableWidth, keyPaddedWidth);
if (isMoreKeysFixedColumn) {
int requestedNumColumns = Math.min(numKeys, numColumn);
if (maxColumns < requestedNumColumns) {
Log.e(TAG, "Keyboard is too small to hold the requested more keys columns: "
+ availableWidth + " " + keyPaddedWidth + " " + numKeys + " "
+ requestedNumColumns + ". The number of columns was reduced.");
mNumColumns = maxColumns;
} else {
mNumColumns = requestedNumColumns;
}
mNumRows = getNumRows(numKeys, mNumColumns);
} else {
int defaultNumColumns = Math.min(maxColumns, numColumn);
mNumRows = getNumRows(numKeys, defaultNumColumns);
mNumColumns = getOptimizedColumns(numKeys, defaultNumColumns, mNumRows);
}
final int topKeys = numKeys % mNumColumns;
mTopKeys = topKeys == 0 ? mNumColumns : topKeys;
final int numLeftKeys = (mNumColumns - 1) / 2;
final int numRightKeys = mNumColumns - numLeftKeys; // including default key.
// Determine the maximum number of keys we can lay out on both side of the left edge of
// a key centered on the parent key. Also, account for horizontal padding because there
// is no horizontal gap on the outside edge.
final float leftWidth = Math.max(coordXInParent - mLeftPadding - keyPaddedWidth / 2
+ mHorizontalGap / 2, 0);
final float rightWidth = Math.max(parentKeyboardWidth - coordXInParent
+ keyPaddedWidth / 2 - mRightPadding + mHorizontalGap / 2, 0);
int maxLeftKeys = getMaxKeys(leftWidth, keyPaddedWidth);
int maxRightKeys = getMaxKeys(rightWidth, keyPaddedWidth);
// Handle the case where the number of columns fits but doesn't have enough room
// for the default key to be centered on the parent key.
if (numKeys >= mNumColumns && mNumColumns == maxColumns
&& maxLeftKeys + maxRightKeys < maxColumns) {
final float extraLeft = leftWidth - maxLeftKeys * keyPaddedWidth;
final float extraRight = rightWidth - maxRightKeys * keyPaddedWidth;
// Put the extra key on whatever side has more space
if (extraLeft > extraRight) {
maxLeftKeys++;
} else {
maxRightKeys++;
}
}
int leftKeys, rightKeys;
if (numLeftKeys > maxLeftKeys) {
leftKeys = maxLeftKeys;
rightKeys = mNumColumns - leftKeys;
} else if (numRightKeys > maxRightKeys) {
// Make sure the default key is included even if it doesn't exactly fit (the default
// key just won't be completely centered on the parent key)
rightKeys = Math.max(maxRightKeys, 1);
leftKeys = mNumColumns - rightKeys;
} else {
leftKeys = numLeftKeys;
rightKeys = numRightKeys;
}
mLeftKeys = leftKeys;
mRightKeys = rightKeys;
// Adjustment of the top row.
mTopRowAdjustment = getTopRowAdjustment();
mColumnWidth = mDefaultKeyPaddedWidth;
mBaseWidth = mNumColumns * mColumnWidth;
// Need to subtract the right most column's gutter only.
mOccupiedWidth = Math.round(mBaseWidth + mLeftPadding + mRightPadding - mHorizontalGap);
mBaseHeight = mNumRows * mDefaultRowHeight;
// Need to subtract the bottom row's gutter only.
mOccupiedHeight = Math.round(mBaseHeight + mTopPadding + mBottomPadding - mVerticalGap);
// The proximity grid size can be reduced because the more keys keyboard is probably
// smaller and doesn't need extra precision from smaller cells.
mGridWidth = Math.min(mGridWidth, mNumColumns);
mGridHeight = Math.min(mGridHeight, mNumRows);
}
private int getTopRowAdjustment() {
final int numOffCenterKeys = Math.abs(mRightKeys - 1 - mLeftKeys);
// Don't center if there are more keys in the top row than can be centered around the
// default more key or if there is an odd number of keys in the top row (already will
// be centered).
if (mTopKeys > mNumColumns - numOffCenterKeys || mTopKeys % 2 == 1) {
return 0;
}
return -1;
}
// Return key position according to column count (0 is default).
/* package */int getColumnPos(final int n) {
return mIsMoreKeysFixedOrder ? getFixedOrderColumnPos(n) : getAutomaticColumnPos(n);
}
private int getFixedOrderColumnPos(final int n) {
final int col = n % mNumColumns;
final int row = n / mNumColumns;
if (!isTopRow(row)) {
return col - mLeftKeys;
}
final int rightSideKeys = mTopKeys / 2;
final int leftSideKeys = mTopKeys - (rightSideKeys + 1);
final int pos = col - leftSideKeys;
final int numLeftKeys = mLeftKeys + mTopRowAdjustment;
final int numRightKeys = mRightKeys - 1;
if (numRightKeys >= rightSideKeys && numLeftKeys >= leftSideKeys) {
return pos;
} else if (numRightKeys < rightSideKeys) {
return pos - (rightSideKeys - numRightKeys);
} else { // numLeftKeys < leftSideKeys
return pos + (leftSideKeys - numLeftKeys);
}
}
private int getAutomaticColumnPos(final int n) {
final int col = n % mNumColumns;
final int row = n / mNumColumns;
int leftKeys = mLeftKeys;
if (isTopRow(row)) {
leftKeys += mTopRowAdjustment;
}
if (col == 0) {
// default position.
return 0;
}
int pos = 0;
int right = 1; // include default position key.
int left = 0;
int i = 0;
while (true) {
// Assign right key if available.
if (right < mRightKeys) {
pos = right;
right++;
i++;
}
if (i >= col)
break;
// Assign left key if available.
if (left < leftKeys) {
left++;
pos = -left;
i++;
}
if (i >= col)
break;
}
return pos;
}
private static int getTopRowEmptySlots(final int numKeys, final int numColumns) {
final int remainings = numKeys % numColumns;
return remainings == 0 ? 0 : numColumns - remainings;
}
private static int getOptimizedColumns(final int numKeys, final int maxColumns,
final int numRows) {
int numColumns = Math.min(numKeys, maxColumns);
while (getTopRowEmptySlots(numKeys, numColumns) >= numRows) {
numColumns--;
}
return numColumns;
}
private static int getNumRows(final int numKeys, final int numColumn) {
return (numKeys + numColumn - 1) / numColumn;
}
private static int getMaxKeys(final float keyboardWidth, final float keyPaddedWidth) {
// This is effectively the same as returning (int)(keyboardWidth / keyPaddedWidth)
// except this handles floating point errors better since rounding in the wrong
// directing here doesn't cause an issue, but truncating incorrectly from an error
// could be a problem (eg: the keyboard width is an exact multiple of the key width
// could return one less than the expected number).
final int maxKeys = Math.round(keyboardWidth / keyPaddedWidth);
if (maxKeys * keyPaddedWidth > keyboardWidth + FLOAT_THRESHOLD) {
return maxKeys - 1;
}
return maxKeys;
}
public float getDefaultKeyCoordX() {
return mLeftKeys * mColumnWidth + mLeftPadding;
}
public float getX(final int n, final int row) {
final float x = getColumnPos(n) * mColumnWidth + getDefaultKeyCoordX();
if (isTopRow(row)) {
return x + mTopRowAdjustment * (mColumnWidth / 2);
}
return x;
}
public float getY(final int row) {
return (mNumRows - 1 - row) * mDefaultRowHeight + mTopPadding;
}
private boolean isTopRow(final int rowCount) {
return mNumRows > 1 && rowCount == mNumRows - 1;
}
}
public static class Builder extends KeyboardBuilder<MoreKeysKeyboardParams> {
private final Key mParentKey;
private static final float LABEL_PADDING_RATIO = 0.2f;
/**
* The builder of MoreKeysKeyboard.
* @param context the context of {@link MoreKeysKeyboardView}.
* @param key the {@link Key} that invokes more keys keyboard.
* @param keyboard the {@link Keyboard} that contains the parentKey.
* @param isSingleMoreKeyWithPreview true if the <code>key</code> has just a single
* "more key" and its key popup preview is enabled.
* @param keyPreviewVisibleWidth the width of visible part of key popup preview.
* @param keyPreviewVisibleHeight the height of visible part of key popup preview
* @param paintToMeasure the {@link Paint} object to measure a "more key" width
*/
public Builder(final Context context, final Key key, final Keyboard keyboard,
final boolean isSingleMoreKeyWithPreview, final int keyPreviewVisibleWidth,
final int keyPreviewVisibleHeight, final Paint paintToMeasure) {
super(context, new MoreKeysKeyboardParams());
load(keyboard.mMoreKeysTemplate, keyboard.mId);
// TODO: More keys keyboard's vertical gap is currently calculated heuristically.
// Should revise the algorithm.
mParams.mVerticalGap = keyboard.mVerticalGap / 2;
// This {@link MoreKeysKeyboard} is invoked from the <code>key</code>.
mParentKey = key;
final float keyPaddedWidth, rowHeight;
if (isSingleMoreKeyWithPreview) {
// Use pre-computed width and height if this more keys keyboard has only one key to
// mitigate visual flicker between key preview and more keys keyboard.
// The bottom paddings don't need to be considered because the vertical positions
// of both backgrounds and the keyboard were already adjusted with their bottom
// paddings deducted. The keyboard's left/right/top paddings do need to be deducted
// so the key including the paddings matches the key preview.
final float keyboardHorizontalPadding = mParams.mLeftPadding
+ mParams.mRightPadding;
final float baseKeyPaddedWidth = keyPreviewVisibleWidth + mParams.mHorizontalGap;
if (keyboardHorizontalPadding > baseKeyPaddedWidth - FLOAT_THRESHOLD) {
// If the padding doesn't fit we'll just add it outside of the key preview.
keyPaddedWidth = baseKeyPaddedWidth;
} else {
keyPaddedWidth = baseKeyPaddedWidth - keyboardHorizontalPadding;
// Keep the more keys keyboard with uneven padding lined up with the key
// preview rather than centering the more keys keyboard's key with the parent
// key.
mParams.mOffsetX = (mParams.mRightPadding - mParams.mLeftPadding) / 2;
}
final float baseKeyPaddedHeight = keyPreviewVisibleHeight + mParams.mVerticalGap;
if (mParams.mTopPadding > baseKeyPaddedHeight - FLOAT_THRESHOLD) {
// If the padding doesn't fit we'll just add it outside of the key preview.
rowHeight = baseKeyPaddedHeight;
} else {
rowHeight = baseKeyPaddedHeight - mParams.mTopPadding;
}
} else {
final float defaultKeyWidth = mParams.mDefaultKeyPaddedWidth
- mParams.mHorizontalGap;
final float padding = context.getResources().getDimension(
R.dimen.config_more_keys_keyboard_key_horizontal_padding)
+ (key.hasLabelsInMoreKeys()
? defaultKeyWidth * LABEL_PADDING_RATIO : 0.0f);
keyPaddedWidth = getMaxKeyWidth(key, defaultKeyWidth, padding, paintToMeasure)
+ mParams.mHorizontalGap;
rowHeight = keyboard.mMostCommonKeyHeight + keyboard.mVerticalGap;
}
final MoreKeySpec[] moreKeys = key.getMoreKeys();
mParams.setParameters(moreKeys.length, key.getMoreKeysColumnNumber(), keyPaddedWidth,
rowHeight, key.getX() + key.getWidth() / 2f, keyboard.mId.mWidth,
key.isMoreKeysFixedColumn(), key.isMoreKeysFixedOrder());
}
private static float getMaxKeyWidth(final Key parentKey, final float minKeyWidth,
final float padding, final Paint paint) {
float maxWidth = minKeyWidth;
for (final MoreKeySpec spec : parentKey.getMoreKeys()) {
final String label = spec.mLabel;
// If the label is single letter, minKeyWidth is enough to hold the label.
if (label != null && StringUtils.codePointCount(label) > 1) {
maxWidth = Math.max(maxWidth,
TypefaceUtils.getStringWidth(label, paint) + padding);
}
}
return maxWidth;
}
@Override
public MoreKeysKeyboard build() {
final MoreKeysKeyboardParams params = mParams;
final int moreKeyFlags = mParentKey.getMoreKeyLabelFlags();
final MoreKeySpec[] moreKeys = mParentKey.getMoreKeys();
for (int n = 0; n < moreKeys.length; n++) {
final MoreKeySpec moreKeySpec = moreKeys[n];
final int row = n / params.mNumColumns;
final float width = params.mDefaultKeyPaddedWidth - params.mHorizontalGap;
final float height = params.mDefaultRowHeight - params.mVerticalGap;
final float keyLeftEdge = params.getX(n, row);
final float keyTopEdge = params.getY(row);
final float keyRightEdge = keyLeftEdge + width;
final float keyBottomEdge = keyTopEdge + height;
final float keyboardLeftEdge = params.mLeftPadding;
final float keyboardRightEdge = params.mOccupiedWidth - params.mRightPadding;
final float keyboardTopEdge = params.mTopPadding;
final float keyboardBottomEdge = params.mOccupiedHeight - params.mBottomPadding;
final float keyLeftPadding = keyLeftEdge < keyboardLeftEdge + FLOAT_THRESHOLD
? params.mLeftPadding : params.mHorizontalGap / 2;
final float keyRightPadding = keyRightEdge > keyboardRightEdge - FLOAT_THRESHOLD
? params.mRightPadding : params.mHorizontalGap / 2;
final float keyTopPadding = keyTopEdge < keyboardTopEdge + FLOAT_THRESHOLD
? params.mTopPadding : params.mVerticalGap / 2;
final float keyBottomPadding = keyBottomEdge > keyboardBottomEdge - FLOAT_THRESHOLD
? params.mBottomPadding : params.mVerticalGap / 2;
final Key key = moreKeySpec.buildKey(keyLeftEdge, keyTopEdge, width, height,
keyLeftPadding, keyRightPadding, keyTopPadding, keyBottomPadding,
moreKeyFlags);
params.onAddKey(key);
}
return new MoreKeysKeyboard(params);
}
}
}

View File

@ -0,0 +1,249 @@
/*
* Copyright (C) 2011 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.colorful.keyboard.theme.keyboard;
import android.content.Context;
import android.content.res.TypedArray;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
import com.colorful.keyboard.theme.R;
import com.colorful.keyboard.theme.latin.common.Constants;
import com.colorful.keyboard.theme.latin.common.CoordinateUtils;
/**
* A view that renders a virtual {@link MoreKeysKeyboard}. It handles rendering of keys and
* detecting key presses and touch movements.
*/
public class MoreKeysKeyboardView extends KeyboardView implements MoreKeysPanel {
private final int[] mCoordinates = CoordinateUtils.newInstance();
protected final KeyDetector mKeyDetector;
private Controller mController = EMPTY_CONTROLLER;
protected KeyboardActionListener mListener;
private int mOriginX;
private int mOriginY;
private Key mCurrentKey;
private int mActivePointerId;
public MoreKeysKeyboardView(final Context context, final AttributeSet attrs) {
this(context, attrs, R.attr.moreKeysKeyboardViewStyle);
}
public MoreKeysKeyboardView(final Context context, final AttributeSet attrs,
final int defStyle) {
super(context, attrs, defStyle);
final TypedArray moreKeysKeyboardViewAttr = context.obtainStyledAttributes(attrs,
R.styleable.MoreKeysKeyboardView, defStyle, R.style.MoreKeysKeyboardView);
moreKeysKeyboardViewAttr.recycle();
mKeyDetector = new MoreKeysDetector(getResources().getDimension(
R.dimen.config_more_keys_keyboard_slide_allowance));
}
@Override
protected void onMeasure(final int widthMeasureSpec, final int heightMeasureSpec) {
final Keyboard keyboard = getKeyboard();
if (keyboard != null) {
final int width = keyboard.mOccupiedWidth + getPaddingLeft() + getPaddingRight();
final int height = keyboard.mOccupiedHeight + getPaddingTop() + getPaddingBottom();
setMeasuredDimension(width, height);
} else {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
}
}
@Override
public void setKeyboard(final Keyboard keyboard) {
super.setKeyboard(keyboard);
mKeyDetector.setKeyboard(
keyboard, -getPaddingLeft(), -getPaddingTop() + getVerticalCorrection());
}
@Override
public void showMoreKeysPanel(final View parentView, final Controller controller,
final int pointX, final int pointY, final KeyboardActionListener listener) {
mController = controller;
mListener = listener;
final View container = getContainerView();
// The coordinates of panel's left-top corner in parentView's coordinate system.
// We need to consider background drawable paddings.
final int x = pointX - getDefaultCoordX() - container.getPaddingLeft() - getPaddingLeft();
final int y = pointY - container.getMeasuredHeight() + container.getPaddingBottom()
+ getPaddingBottom();
parentView.getLocationInWindow(mCoordinates);
// Ensure the horizontal position of the panel does not extend past the parentView edges.
final int maxX = parentView.getMeasuredWidth() - container.getMeasuredWidth();
final int panelX = Math.max(0, Math.min(maxX, x)) + CoordinateUtils.x(mCoordinates);
final int panelY = y + CoordinateUtils.y(mCoordinates);
container.setX(panelX);
container.setY(panelY);
mOriginX = x + container.getPaddingLeft();
mOriginY = y + container.getPaddingTop();
controller.onShowMoreKeysPanel(this);
}
/**
* Returns the default x coordinate for showing this panel.
*/
protected int getDefaultCoordX() {
return ((MoreKeysKeyboard)getKeyboard()).getDefaultCoordX();
}
@Override
public void onDownEvent(final int x, final int y, final int pointerId) {
mActivePointerId = pointerId;
mCurrentKey = detectKey(x, y);
}
@Override
public void onMoveEvent(final int x, final int y, final int pointerId) {
if (mActivePointerId != pointerId) {
return;
}
final boolean hasOldKey = (mCurrentKey != null);
mCurrentKey = detectKey(x, y);
if (hasOldKey && mCurrentKey == null) {
// A more keys keyboard is canceled when detecting no key.
mController.onCancelMoreKeysPanel();
}
}
@Override
public void onUpEvent(final int x, final int y, final int pointerId) {
if (mActivePointerId != pointerId) {
return;
}
// Calling {@link #detectKey(int,int,int)} here is harmless because the last move event and
// the following up event share the same coordinates.
mCurrentKey = detectKey(x, y);
if (mCurrentKey != null) {
updateReleaseKeyGraphics(mCurrentKey);
onKeyInput(mCurrentKey);
mCurrentKey = null;
}
}
/**
* Performs the specific action for this panel when the user presses a key on the panel.
*/
protected void onKeyInput(final Key key) {
final int code = key.getCode();
if (code == Constants.CODE_OUTPUT_TEXT) {
mListener.onTextInput(mCurrentKey.getOutputText());
} else if (code != Constants.CODE_UNSPECIFIED) {
mListener.onCodeInput(code, Constants.NOT_A_COORDINATE, Constants.NOT_A_COORDINATE, false /* isKeyRepeat */);
}
}
private Key detectKey(int x, int y) {
final Key oldKey = mCurrentKey;
final Key newKey = mKeyDetector.detectHitKey(x, y);
if (newKey == oldKey) {
return newKey;
}
// A new key is detected.
if (oldKey != null) {
updateReleaseKeyGraphics(oldKey);
invalidateKey(oldKey);
}
if (newKey != null) {
updatePressKeyGraphics(newKey);
invalidateKey(newKey);
}
return newKey;
}
private void updateReleaseKeyGraphics(final Key key) {
key.onReleased();
invalidateKey(key);
}
private void updatePressKeyGraphics(final Key key) {
key.onPressed();
invalidateKey(key);
}
@Override
public void dismissMoreKeysPanel() {
if (!isShowingInParent()) {
return;
}
mController.onDismissMoreKeysPanel();
}
@Override
public int translateX(final int x) {
return x - mOriginX;
}
@Override
public int translateY(final int y) {
return y - mOriginY;
}
@Override
public boolean onTouchEvent(final MotionEvent me) {
final int action = me.getActionMasked();
final int index = me.getActionIndex();
final int x = (int)me.getX(index);
final int y = (int)me.getY(index);
final int pointerId = me.getPointerId(index);
switch (action) {
case MotionEvent.ACTION_DOWN:
case MotionEvent.ACTION_POINTER_DOWN:
onDownEvent(x, y, pointerId);
break;
case MotionEvent.ACTION_UP:
case MotionEvent.ACTION_POINTER_UP:
onUpEvent(x, y, pointerId);
break;
case MotionEvent.ACTION_MOVE:
onMoveEvent(x, y, pointerId);
break;
}
return true;
}
private View getContainerView() {
return (View)getParent();
}
@Override
public void showInParent(final ViewGroup parentView) {
removeFromParent();
parentView.addView(getContainerView());
}
@Override
public void removeFromParent() {
final View containerView = getContainerView();
final ViewGroup currentParent = (ViewGroup)containerView.getParent();
if (currentParent != null) {
currentParent.removeView(containerView);
}
}
@Override
public boolean isShowingInParent() {
return (getContainerView().getParent() != null);
}
}

View File

@ -0,0 +1,133 @@
/*
* Copyright (C) 2011 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.colorful.keyboard.theme.keyboard;
import android.view.View;
import android.view.ViewGroup;
public interface MoreKeysPanel {
interface Controller {
/**
* Add the {@link MoreKeysPanel} to the target view.
* @param panel the panel to be shown.
*/
void onShowMoreKeysPanel(final MoreKeysPanel panel);
/**
* Remove the current {@link MoreKeysPanel} from the target view.
*/
void onDismissMoreKeysPanel();
/**
* Instructs the parent to cancel the panel (e.g., when entering a different input mode).
*/
void onCancelMoreKeysPanel();
}
Controller EMPTY_CONTROLLER = new Controller() {
@Override
public void onShowMoreKeysPanel(final MoreKeysPanel panel) {}
@Override
public void onDismissMoreKeysPanel() {}
@Override
public void onCancelMoreKeysPanel() {}
};
/**
* Initializes the layout and event handling of this {@link MoreKeysPanel} and calls the
* controller's onShowMoreKeysPanel to add the panel's container view.
*
* @param parentView the parent view of this {@link MoreKeysPanel}
* @param controller the controller that can dismiss this {@link MoreKeysPanel}
* @param pointX x coordinate of this {@link MoreKeysPanel}
* @param pointY y coordinate of this {@link MoreKeysPanel}
* @param listener the listener that will receive keyboard action from this
* {@link MoreKeysPanel}.
*/
// TODO: Currently the MoreKeysPanel is inside a container view that is added to the parent.
// Consider the simpler approach of placing the MoreKeysPanel itself into the parent view.
void showMoreKeysPanel(View parentView, Controller controller, int pointX,
int pointY, KeyboardActionListener listener);
/**
* Dismisses the more keys panel and calls the controller's onDismissMoreKeysPanel to remove
* the panel's container view.
*/
void dismissMoreKeysPanel();
/**
* Process a move event on the more keys panel.
*
* @param x translated x coordinate of the touch point
* @param y translated y coordinate of the touch point
* @param pointerId pointer id touch point
*/
void onMoveEvent(final int x, final int y, final int pointerId);
/**
* Process a down event on the more keys panel.
*
* @param x translated x coordinate of the touch point
* @param y translated y coordinate of the touch point
* @param pointerId pointer id touch point
*/
void onDownEvent(final int x, final int y, final int pointerId);
/**
* Process an up event on the more keys panel.
*
* @param x translated x coordinate of the touch point
* @param y translated y coordinate of the touch point
* @param pointerId pointer id touch point
*/
void onUpEvent(final int x, final int y, final int pointerId);
/**
* Translate X-coordinate of touch event to the local X-coordinate of this
* {@link MoreKeysPanel}.
*
* @param x the global X-coordinate
* @return the local X-coordinate to this {@link MoreKeysPanel}
*/
int translateX(int x);
/**
* Translate Y-coordinate of touch event to the local Y-coordinate of this
* {@link MoreKeysPanel}.
*
* @param y the global Y-coordinate
* @return the local Y-coordinate to this {@link MoreKeysPanel}
*/
int translateY(int y);
/**
* Show this {@link MoreKeysPanel} in the parent view.
*
* @param parentView the {@link ViewGroup} that hosts this {@link MoreKeysPanel}.
*/
void showInParent(ViewGroup parentView);
/**
* Remove this {@link MoreKeysPanel} from the parent view.
*/
void removeFromParent();
/**
* Return whether the panel is currently being shown.
*/
boolean isShowingInParent();
}

View File

@ -0,0 +1,910 @@
/*
* Copyright (C) 2010 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.colorful.keyboard.theme.keyboard;
import android.content.res.Resources;
import android.content.res.TypedArray;
import android.util.Log;
import android.view.MotionEvent;
import com.colorful.keyboard.theme.R;
import com.colorful.keyboard.theme.keyboard.internal.BogusMoveEventDetector;
import com.colorful.keyboard.theme.keyboard.internal.DrawingProxy;
import com.colorful.keyboard.theme.keyboard.internal.PointerTrackerQueue;
import com.colorful.keyboard.theme.keyboard.internal.TimerProxy;
import com.colorful.keyboard.theme.latin.common.Constants;
import com.colorful.keyboard.theme.latin.common.CoordinateUtils;
import com.colorful.keyboard.theme.latin.define.DebugFlags;
import com.colorful.keyboard.theme.latin.settings.Settings;
import java.util.ArrayList;
public final class PointerTracker implements PointerTrackerQueue.Element {
private static final String TAG = PointerTracker.class.getSimpleName();
private static final boolean DEBUG_EVENT = false;
private static final boolean DEBUG_MOVE_EVENT = false;
private static final boolean DEBUG_LISTENER = false;
private static boolean DEBUG_MODE = DebugFlags.DEBUG_ENABLED || DEBUG_EVENT;
static final class PointerTrackerParams {
public final boolean mKeySelectionByDraggingFinger;
public final int mTouchNoiseThresholdTime;
public final int mTouchNoiseThresholdDistance;
public final int mKeyRepeatStartTimeout;
public final int mKeyRepeatInterval;
public final int mLongPressShiftLockTimeout;
public PointerTrackerParams(final TypedArray mainKeyboardViewAttr) {
mKeySelectionByDraggingFinger = mainKeyboardViewAttr.getBoolean(
R.styleable.MainKeyboardView_keySelectionByDraggingFinger, false);
mTouchNoiseThresholdTime = mainKeyboardViewAttr.getInt(
R.styleable.MainKeyboardView_touchNoiseThresholdTime, 0);
mTouchNoiseThresholdDistance = mainKeyboardViewAttr.getDimensionPixelSize(
R.styleable.MainKeyboardView_touchNoiseThresholdDistance, 0);
mKeyRepeatStartTimeout = mainKeyboardViewAttr.getInt(
R.styleable.MainKeyboardView_keyRepeatStartTimeout, 0);
mKeyRepeatInterval = mainKeyboardViewAttr.getInt(
R.styleable.MainKeyboardView_keyRepeatInterval, 0);
mLongPressShiftLockTimeout = mainKeyboardViewAttr.getInt(
R.styleable.MainKeyboardView_longPressShiftLockTimeout, 0);
}
}
// Parameters for pointer handling.
private static PointerTrackerParams sParams;
private static int sPointerStep = (int)(10.0 * Resources.getSystem().getDisplayMetrics().density);
private static final ArrayList<PointerTracker> sTrackers = new ArrayList<>();
private static final PointerTrackerQueue sPointerTrackerQueue = new PointerTrackerQueue();
public final int mPointerId;
private static DrawingProxy sDrawingProxy;
private static TimerProxy sTimerProxy;
private static KeyboardActionListener sListener = KeyboardActionListener.EMPTY_LISTENER;
// The {@link KeyDetector} is set whenever the down event is processed. Also this is updated
// when new {@link Keyboard} is set by {@link #setKeyDetector(KeyDetector)}.
private KeyDetector mKeyDetector = new KeyDetector();
private Keyboard mKeyboard;
private final BogusMoveEventDetector mBogusMoveEventDetector = new BogusMoveEventDetector();
// The position and time at which first down event occurred.
private int[] mDownCoordinates = CoordinateUtils.newInstance();
// The current key where this pointer is.
private Key mCurrentKey = null;
// The position where the current key was recognized for the first time.
private int mKeyX;
private int mKeyY;
// Last pointer position.
private int mLastX;
private int mLastY;
private int mStartX;
//private int mStartY;
private long mStartTime;
private boolean mCursorMoved = false;
// true if keyboard layout has been changed.
private boolean mKeyboardLayoutHasBeenChanged;
// true if this pointer is no longer triggering any action because it has been canceled.
private boolean mIsTrackingForActionDisabled;
// the more keys panel currently being shown. equals null if no panel is active.
private MoreKeysPanel mMoreKeysPanel;
private static final int MULTIPLIER_FOR_LONG_PRESS_TIMEOUT_IN_SLIDING_INPUT = 3;
// true if this pointer is in the dragging finger mode.
boolean mIsInDraggingFinger;
// true if this pointer is sliding from a modifier key and in the sliding key input mode,
// so that further modifier keys should be ignored.
boolean mIsInSlidingKeyInput;
// if not a NOT_A_CODE, the key of this code is repeating
private int mCurrentRepeatingKeyCode = Constants.NOT_A_CODE;
// true if dragging finger is allowed.
private boolean mIsAllowedDraggingFinger;
// TODO: Add PointerTrackerFactory singleton and move some class static methods into it.
public static void init(final TypedArray mainKeyboardViewAttr, final TimerProxy timerProxy,
final DrawingProxy drawingProxy) {
sParams = new PointerTrackerParams(mainKeyboardViewAttr);
final Resources res = mainKeyboardViewAttr.getResources();
BogusMoveEventDetector.init(res);
sTimerProxy = timerProxy;
sDrawingProxy = drawingProxy;
}
public static PointerTracker getPointerTracker(final int id) {
final ArrayList<PointerTracker> trackers = sTrackers;
// Create pointer trackers until we can get 'id+1'-th tracker, if needed.
for (int i = trackers.size(); i <= id; i++) {
final PointerTracker tracker = new PointerTracker(i);
trackers.add(tracker);
}
return trackers.get(id);
}
public static boolean isAnyInDraggingFinger() {
return sPointerTrackerQueue.isAnyInDraggingFinger();
}
public static void cancelAllPointerTrackers() {
sPointerTrackerQueue.cancelAllPointerTrackers();
}
public static void setKeyboardActionListener(final KeyboardActionListener listener) {
sListener = listener;
}
public static void setKeyDetector(final KeyDetector keyDetector) {
final Keyboard keyboard = keyDetector.getKeyboard();
if (keyboard == null) {
return;
}
final int trackersSize = sTrackers.size();
for (int i = 0; i < trackersSize; ++i) {
final PointerTracker tracker = sTrackers.get(i);
tracker.setKeyDetectorInner(keyDetector);
}
}
public static void setReleasedKeyGraphicsToAllKeys() {
final int trackersSize = sTrackers.size();
for (int i = 0; i < trackersSize; ++i) {
final PointerTracker tracker = sTrackers.get(i);
tracker.setReleasedKeyGraphics(tracker.getKey(), true /* withAnimation */);
}
}
public static void dismissAllMoreKeysPanels() {
final int trackersSize = sTrackers.size();
for (int i = 0; i < trackersSize; ++i) {
final PointerTracker tracker = sTrackers.get(i);
tracker.dismissMoreKeysPanel();
}
}
private PointerTracker(final int id) {
mPointerId = id;
}
// Returns true if keyboard has been changed by this callback.
private boolean callListenerOnPressAndCheckKeyboardLayoutChange(final Key key,
final int repeatCount) {
// While gesture input is going on, this method should be a no-operation. But when gesture
// input has been canceled, <code>sInGesture</code> and <code>mIsDetectingGesture</code>
// are set to false. To keep this method is a no-operation,
// <code>mIsTrackingForActionDisabled</code> should also be taken account of.
final boolean ignoreModifierKey = mIsInDraggingFinger && key.isModifier();
if (DEBUG_LISTENER) {
Log.d(TAG, String.format("[%d] onPress : %s%s%s", mPointerId,
(key == null ? "none" : Constants.printableCode(key.getCode())),
ignoreModifierKey ? " ignoreModifier" : "",
repeatCount > 0 ? " repeatCount=" + repeatCount : ""));
}
if (ignoreModifierKey) {
return false;
}
sListener.onPressKey(key.getCode(), repeatCount, getActivePointerTrackerCount() == 1);
final boolean keyboardLayoutHasBeenChanged = mKeyboardLayoutHasBeenChanged;
mKeyboardLayoutHasBeenChanged = false;
sTimerProxy.startTypingStateTimer(key);
return keyboardLayoutHasBeenChanged;
}
// Note that we need primaryCode argument because the keyboard may in shifted state and the
// primaryCode is different from {@link Key#mKeyCode}.
private void callListenerOnCodeInput(final Key key, final int primaryCode, final int x,
final int y, final boolean isKeyRepeat) {
final boolean ignoreModifierKey = mIsInDraggingFinger && key.isModifier();
final boolean altersCode = key.altCodeWhileTyping() && sTimerProxy.isTypingState();
final int code = altersCode ? key.getAltCode() : primaryCode;
if (DEBUG_LISTENER) {
final String output = code == Constants.CODE_OUTPUT_TEXT
? key.getOutputText() : Constants.printableCode(code);
Log.d(TAG, String.format("[%d] onCodeInput: %4d %4d %s%s%s", mPointerId, x, y,
output, ignoreModifierKey ? " ignoreModifier" : "",
altersCode ? " altersCode" : ""));
}
if (ignoreModifierKey) {
return;
}
if (code == Constants.CODE_OUTPUT_TEXT) {
sListener.onTextInput(key.getOutputText());
} else if (code != Constants.CODE_UNSPECIFIED) {
sListener.onCodeInput(code,
Constants.NOT_A_COORDINATE, Constants.NOT_A_COORDINATE, isKeyRepeat);
}
}
// Note that we need primaryCode argument because the keyboard may be in shifted state and the
// primaryCode is different from {@link Key#mKeyCode}.
private void callListenerOnRelease(final Key key, final int primaryCode,
final boolean withSliding) {
// See the comment at {@link #callListenerOnPressAndCheckKeyboardLayoutChange(Key}}.
final boolean ignoreModifierKey = mIsInDraggingFinger && key.isModifier();
if (DEBUG_LISTENER) {
Log.d(TAG, String.format("[%d] onRelease : %s%s%s", mPointerId,
Constants.printableCode(primaryCode),
withSliding ? " sliding" : "", ignoreModifierKey ? " ignoreModifier" : ""));
}
if (ignoreModifierKey) {
return;
}
sListener.onReleaseKey(primaryCode, withSliding);
}
private void callListenerOnFinishSlidingInput() {
if (DEBUG_LISTENER) {
Log.d(TAG, String.format("[%d] onFinishSlidingInput", mPointerId));
}
sListener.onFinishSlidingInput();
}
private void setKeyDetectorInner(final KeyDetector keyDetector) {
final Keyboard keyboard = keyDetector.getKeyboard();
if (keyboard == null) {
return;
}
if (keyDetector == mKeyDetector && keyboard == mKeyboard) {
return;
}
mKeyDetector = keyDetector;
mKeyboard = keyboard;
// Mark that keyboard layout has been changed.
mKeyboardLayoutHasBeenChanged = true;
final int keyPaddedWidth = mKeyboard.mMostCommonKeyWidth
+ Math.round(mKeyboard.mHorizontalGap);
final int keyPaddedHeight = mKeyboard.mMostCommonKeyHeight
+ Math.round(mKeyboard.mVerticalGap);
// Keep {@link #mCurrentKey} that comes from previous keyboard. The key preview of
// {@link #mCurrentKey} will be dismissed by {@setReleasedKeyGraphics(Key)} via
// {@link onMoveEventInternal(int,int,long)} or {@link #onUpEventInternal(int,int,long)}.
mBogusMoveEventDetector.setKeyboardGeometry(keyPaddedWidth, keyPaddedHeight);
}
@Override
public boolean isInDraggingFinger() {
return mIsInDraggingFinger;
}
public Key getKey() {
return mCurrentKey;
}
@Override
public boolean isModifier() {
return mCurrentKey != null && mCurrentKey.isModifier();
}
public Key getKeyOn(final int x, final int y) {
return mKeyDetector.detectHitKey(x, y);
}
private void setReleasedKeyGraphics(final Key key, final boolean withAnimation) {
if (key == null) {
return;
}
sDrawingProxy.onKeyReleased(key, withAnimation);
if (key.isShift()) {
for (final Key shiftKey : mKeyboard.mShiftKeys) {
if (shiftKey != key) {
sDrawingProxy.onKeyReleased(shiftKey, false /* withAnimation */);
}
}
}
if (key.altCodeWhileTyping()) {
final int altCode = key.getAltCode();
final Key altKey = mKeyboard.getKey(altCode);
if (altKey != null) {
sDrawingProxy.onKeyReleased(altKey, false /* withAnimation */);
}
for (final Key k : mKeyboard.mAltCodeKeysWhileTyping) {
if (k != key && k.getAltCode() == altCode) {
sDrawingProxy.onKeyReleased(k, false /* withAnimation */);
}
}
}
}
private void setPressedKeyGraphics(final Key key) {
if (key == null) {
return;
}
// Even if the key is disabled, it should respond if it is in the altCodeWhileTyping state.
final boolean altersCode = key.altCodeWhileTyping() && sTimerProxy.isTypingState();
sDrawingProxy.onKeyPressed(key, true);
if (key.isShift()) {
for (final Key shiftKey : mKeyboard.mShiftKeys) {
if (shiftKey != key) {
sDrawingProxy.onKeyPressed(shiftKey, false /* withPreview */);
}
}
}
if (altersCode) {
final int altCode = key.getAltCode();
final Key altKey = mKeyboard.getKey(altCode);
if (altKey != null) {
sDrawingProxy.onKeyPressed(altKey, false /* withPreview */);
}
for (final Key k : mKeyboard.mAltCodeKeysWhileTyping) {
if (k != key && k.getAltCode() == altCode) {
sDrawingProxy.onKeyPressed(k, false /* withPreview */);
}
}
}
}
public void getLastCoordinates(final int[] outCoords) {
CoordinateUtils.set(outCoords, mLastX, mLastY);
}
private Key onDownKey(final int x, final int y) {
CoordinateUtils.set(mDownCoordinates, x, y);
mBogusMoveEventDetector.onDownKey();
return onMoveToNewKey(onMoveKeyInternal(x, y), x, y);
}
private static int getDistance(final int x1, final int y1, final int x2, final int y2) {
return (int)Math.hypot(x1 - x2, y1 - y2);
}
private Key onMoveKeyInternal(final int x, final int y) {
mBogusMoveEventDetector.onMoveKey(getDistance(x, y, mLastX, mLastY));
mLastX = x;
mLastY = y;
return mKeyDetector.detectHitKey(x, y);
}
private Key onMoveKey(final int x, final int y) {
return onMoveKeyInternal(x, y);
}
private Key onMoveToNewKey(final Key newKey, final int x, final int y) {
mCurrentKey = newKey;
mKeyX = x;
mKeyY = y;
return newKey;
}
/* package */ static int getActivePointerTrackerCount() {
return sPointerTrackerQueue.size();
}
public void processMotionEvent(final MotionEvent me, final KeyDetector keyDetector) {
final int action = me.getActionMasked();
final long eventTime = me.getEventTime();
if (action == MotionEvent.ACTION_MOVE) {
// When this pointer is the only active pointer and is showing a more keys panel,
// we should ignore other pointers' motion event.
final boolean shouldIgnoreOtherPointers =
isShowingMoreKeysPanel() && getActivePointerTrackerCount() == 1;
final int pointerCount = me.getPointerCount();
for (int index = 0; index < pointerCount; index++) {
final int id = me.getPointerId(index);
if (shouldIgnoreOtherPointers && id != mPointerId) {
continue;
}
final int x = (int)me.getX(index);
final int y = (int)me.getY(index);
final PointerTracker tracker = getPointerTracker(id);
tracker.onMoveEvent(x, y, eventTime);
}
return;
}
final int index = me.getActionIndex();
final int x = (int)me.getX(index);
final int y = (int)me.getY(index);
switch (action) {
case MotionEvent.ACTION_DOWN:
case MotionEvent.ACTION_POINTER_DOWN:
onDownEvent(x, y, eventTime, keyDetector);
break;
case MotionEvent.ACTION_UP:
case MotionEvent.ACTION_POINTER_UP:
onUpEvent(x, y, eventTime);
break;
case MotionEvent.ACTION_CANCEL:
onCancelEvent(x, y, eventTime);
break;
}
}
private void onDownEvent(final int x, final int y, final long eventTime,
final KeyDetector keyDetector) {
setKeyDetectorInner(keyDetector);
if (DEBUG_EVENT) {
printTouchEvent("onDownEvent:", x, y, eventTime);
}
// Naive up-to-down noise filter.
final long deltaT = eventTime;
if (deltaT < sParams.mTouchNoiseThresholdTime) {
final int distance = getDistance(x, y, mLastX, mLastY);
if (distance < sParams.mTouchNoiseThresholdDistance) {
if (DEBUG_MODE)
Log.w(TAG, String.format("[%d] onDownEvent:"
+ " ignore potential noise: time=%d distance=%d",
mPointerId, deltaT, distance));
cancelTrackingForAction();
return;
}
}
final Key key = getKeyOn(x, y);
mBogusMoveEventDetector.onActualDownEvent(x, y);
if (key != null && key.isModifier()) {
// Before processing a down event of modifier key, all pointers already being
// tracked should be released.
sPointerTrackerQueue.releaseAllPointers(eventTime);
}
sPointerTrackerQueue.add(this);
onDownEventInternal(x, y);
}
/* package */ boolean isShowingMoreKeysPanel() {
return (mMoreKeysPanel != null);
}
private void dismissMoreKeysPanel() {
if (isShowingMoreKeysPanel()) {
mMoreKeysPanel.dismissMoreKeysPanel();
mMoreKeysPanel = null;
}
}
private void onDownEventInternal(final int x, final int y) {
Key key = onDownKey(x, y);
// Key selection by dragging finger is allowed when 1) key selection by dragging finger is
// enabled by configuration, 2) this pointer starts dragging from modifier key, or 3) this
// pointer's KeyDetector always allows key selection by dragging finger, such as
// {@link MoreKeysKeyboard}.
mIsAllowedDraggingFinger = sParams.mKeySelectionByDraggingFinger
|| (key != null && key.isModifier())
|| mKeyDetector.alwaysAllowsKeySelectionByDraggingFinger();
mKeyboardLayoutHasBeenChanged = false;
mIsTrackingForActionDisabled = false;
resetKeySelectionByDraggingFinger();
if (key != null) {
// This onPress call may have changed keyboard layout. Those cases are detected at
// {@link #setKeyboard}. In those cases, we should update key according to the new
// keyboard layout.
if (callListenerOnPressAndCheckKeyboardLayoutChange(key, 0 /* repeatCount */)) {
key = onDownKey(x, y);
}
startRepeatKey(key);
startLongPressTimer(key);
setPressedKeyGraphics(key);
mStartX = x;
//mStartY = y;
mStartTime = System.currentTimeMillis();
}
}
private void startKeySelectionByDraggingFinger(final Key key) {
if (!mIsInDraggingFinger) {
mIsInSlidingKeyInput = key.isModifier();
}
mIsInDraggingFinger = true;
}
private void resetKeySelectionByDraggingFinger() {
mIsInDraggingFinger = false;
mIsInSlidingKeyInput = false;
}
private void onMoveEvent(final int x, final int y, final long eventTime) {
if (DEBUG_MOVE_EVENT) {
printTouchEvent("onMoveEvent:", x, y, eventTime);
}
if (mIsTrackingForActionDisabled) {
return;
}
if (isShowingMoreKeysPanel()) {
final int translatedX = mMoreKeysPanel.translateX(x);
final int translatedY = mMoreKeysPanel.translateY(y);
mMoreKeysPanel.onMoveEvent(translatedX, translatedY, mPointerId);
onMoveKey(x, y);
return;
}
onMoveEventInternal(x, y, eventTime);
}
private void processDraggingFingerInToNewKey(final Key newKey, final int x, final int y) {
// This onPress call may have changed keyboard layout. Those cases are detected
// at {@link #setKeyboard}. In those cases, we should update key according
// to the new keyboard layout.
Key key = newKey;
if (callListenerOnPressAndCheckKeyboardLayoutChange(key, 0 /* repeatCount */)) {
key = onMoveKey(x, y);
}
onMoveToNewKey(key, x, y);
if (mIsTrackingForActionDisabled) {
return;
}
startLongPressTimer(key);
setPressedKeyGraphics(key);
}
private void processDraggingFingerOutFromOldKey(final Key oldKey) {
setReleasedKeyGraphics(oldKey, true /* withAnimation */);
callListenerOnRelease(oldKey, oldKey.getCode(), true /* withSliding */);
startKeySelectionByDraggingFinger(oldKey);
sTimerProxy.cancelKeyTimersOf(this);
}
private void dragFingerFromOldKeyToNewKey(final Key key, final int x, final int y,
final long eventTime, final Key oldKey) {
// The pointer has been slid in to the new key from the previous key, we must call
// onRelease() first to notify that the previous key has been released, then call
// onPress() to notify that the new key is being pressed.
processDraggingFingerOutFromOldKey(oldKey);
startRepeatKey(key);
if (mIsAllowedDraggingFinger) {
processDraggingFingerInToNewKey(key, x, y);
}
// HACK: If there are currently multiple touches, register the key even if the finger
// slides off the key. This defends against noise from some touch panels when there are
// close multiple touches.
// Caveat: When in chording input mode with a modifier key, we don't use this hack.
else if (getActivePointerTrackerCount() > 1
&& !sPointerTrackerQueue.hasModifierKeyOlderThan(this)) {
if (DEBUG_MODE) {
Log.w(TAG, String.format("[%d] onMoveEvent:"
+ " detected sliding finger while multi touching", mPointerId));
}
onUpEvent(x, y, eventTime);
cancelTrackingForAction();
setReleasedKeyGraphics(oldKey, true /* withAnimation */);
} else {
cancelTrackingForAction();
setReleasedKeyGraphics(oldKey, true /* withAnimation */);
}
}
private void dragFingerOutFromOldKey(final Key oldKey, final int x, final int y) {
// The pointer has been slid out from the previous key, we must call onRelease() to
// notify that the previous key has been released.
processDraggingFingerOutFromOldKey(oldKey);
if (mIsAllowedDraggingFinger) {
onMoveToNewKey(null, x, y);
} else {
cancelTrackingForAction();
}
}
private void onMoveEventInternal(final int x, final int y, final long eventTime) {
final Key oldKey = mCurrentKey;
if (oldKey != null && oldKey.getCode() == Constants.CODE_SPACE && Settings.getInstance().getCurrent().mSpaceSwipeEnabled) {
//Pointer slider
int steps = (x - mStartX) / sPointerStep;
final int swipeIgnoreTime = Settings.getInstance().getCurrent().mKeyLongpressTimeout / MULTIPLIER_FOR_LONG_PRESS_TIMEOUT_IN_SLIDING_INPUT;
if (steps != 0 && mStartTime + swipeIgnoreTime < System.currentTimeMillis()) {
mCursorMoved = true;
mStartX += steps * sPointerStep;
sListener.onMovePointer(steps);
}
return;
}
if (oldKey != null && oldKey.getCode() == Constants.CODE_DELETE && Settings.getInstance().getCurrent().mDeleteSwipeEnabled) {
//Delete slider
int steps = (x - mStartX) / sPointerStep;
if (steps != 0) {
sTimerProxy.cancelKeyTimersOf(this);
mCursorMoved = true;
mStartX += steps * sPointerStep;
sListener.onMoveDeletePointer(steps);
}
return;
}
final Key newKey = onMoveKey(x, y);
if (newKey != null) {
if (oldKey != null && isMajorEnoughMoveToBeOnNewKey(x, y, newKey)) {
dragFingerFromOldKeyToNewKey(newKey, x, y, eventTime, oldKey);
} else if (oldKey == null) {
// The pointer has been slid in to the new key, but the finger was not on any keys.
// In this case, we must call onPress() to notify that the new key is being pressed.
processDraggingFingerInToNewKey(newKey, x, y);
}
} else { // newKey == null
if (oldKey != null && isMajorEnoughMoveToBeOnNewKey(x, y, newKey)) {
dragFingerOutFromOldKey(oldKey, x, y);
}
}
}
private void onUpEvent(final int x, final int y, final long eventTime) {
if (DEBUG_EVENT) {
printTouchEvent("onUpEvent :", x, y, eventTime);
}
sTimerProxy.cancelUpdateBatchInputTimer(this);
if (mCurrentKey != null && mCurrentKey.isModifier()) {
// Before processing an up event of modifier key, all pointers already being
// tracked should be released.
sPointerTrackerQueue.releaseAllPointersExcept(this, eventTime);
} else {
sPointerTrackerQueue.releaseAllPointersOlderThan(this, eventTime);
}
onUpEventInternal(x, y);
sPointerTrackerQueue.remove(this);
}
// Let this pointer tracker know that one of newer-than-this pointer trackers got an up event.
// This pointer tracker needs to keep the key top graphics "pressed", but needs to get a
// "virtual" up event.
@Override
public void onPhantomUpEvent(final long eventTime) {
if (DEBUG_EVENT) {
printTouchEvent("onPhntEvent:", mLastX, mLastY, eventTime);
}
onUpEventInternal(mLastX, mLastY);
cancelTrackingForAction();
}
private void onUpEventInternal(final int x, final int y) {
sTimerProxy.cancelKeyTimersOf(this);
final boolean isInDraggingFinger = mIsInDraggingFinger;
final boolean isInSlidingKeyInput = mIsInSlidingKeyInput;
resetKeySelectionByDraggingFinger();
final Key currentKey = mCurrentKey;
mCurrentKey = null;
final int currentRepeatingKeyCode = mCurrentRepeatingKeyCode;
mCurrentRepeatingKeyCode = Constants.NOT_A_CODE;
// Release the last pressed key.
setReleasedKeyGraphics(currentKey, true /* withAnimation */);
if(mCursorMoved && currentKey.getCode() == Constants.CODE_DELETE) {
sListener.onUpWithDeletePointerActive();
}
if (isShowingMoreKeysPanel()) {
if (!mIsTrackingForActionDisabled) {
final int translatedX = mMoreKeysPanel.translateX(x);
final int translatedY = mMoreKeysPanel.translateY(y);
mMoreKeysPanel.onUpEvent(translatedX, translatedY, mPointerId);
}
dismissMoreKeysPanel();
return;
}
if (mCursorMoved) {
mCursorMoved = false;
return;
}
if (mIsTrackingForActionDisabled) {
return;
}
if (currentKey != null && currentKey.isRepeatable()
&& (currentKey.getCode() == currentRepeatingKeyCode) && !isInDraggingFinger) {
return;
}
detectAndSendKey(currentKey, mKeyX, mKeyY);
if (isInSlidingKeyInput) {
callListenerOnFinishSlidingInput();
}
}
@Override
public void cancelTrackingForAction() {
if (isShowingMoreKeysPanel()) {
return;
}
mIsTrackingForActionDisabled = true;
}
public void onLongPressed() {
sTimerProxy.cancelLongPressTimersOf(this);
if (isShowingMoreKeysPanel()) {
return;
}
if (mCursorMoved) {
return;
}
final Key key = getKey();
if (key == null) {
return;
}
if (key.hasNoPanelAutoMoreKey()) {
cancelKeyTracking();
final int moreKeyCode = key.getMoreKeys()[0].mCode;
sListener.onPressKey(moreKeyCode, 0 /* repeatCont */, true /* isSinglePointer */);
sListener.onCodeInput(moreKeyCode, Constants.NOT_A_COORDINATE,
Constants.NOT_A_COORDINATE, false /* isKeyRepeat */);
sListener.onReleaseKey(moreKeyCode, false /* withSliding */);
return;
}
final int code = key.getCode();
if (code == Constants.CODE_SPACE || code == Constants.CODE_LANGUAGE_SWITCH) {
// Long pressing the space key invokes IME switcher dialog.
if (sListener.onCustomRequest(Constants.CUSTOM_CODE_SHOW_INPUT_METHOD_PICKER)) {
cancelKeyTracking();
sListener.onReleaseKey(code, false /* withSliding */);
return;
}
}
setReleasedKeyGraphics(key, false /* withAnimation */);
final MoreKeysPanel moreKeysPanel = sDrawingProxy.showMoreKeysKeyboard(key, this);
if (moreKeysPanel == null) {
return;
}
final int translatedX = moreKeysPanel.translateX(mLastX);
final int translatedY = moreKeysPanel.translateY(mLastY);
moreKeysPanel.onDownEvent(translatedX, translatedY, mPointerId);
mMoreKeysPanel = moreKeysPanel;
}
private void cancelKeyTracking() {
resetKeySelectionByDraggingFinger();
cancelTrackingForAction();
setReleasedKeyGraphics(mCurrentKey, true /* withAnimation */);
sPointerTrackerQueue.remove(this);
}
private void onCancelEvent(final int x, final int y, final long eventTime) {
if (DEBUG_EVENT) {
printTouchEvent("onCancelEvt:", x, y, eventTime);
}
cancelAllPointerTrackers();
sPointerTrackerQueue.releaseAllPointers(eventTime);
onCancelEventInternal();
}
private void onCancelEventInternal() {
sTimerProxy.cancelKeyTimersOf(this);
setReleasedKeyGraphics(mCurrentKey, true /* withAnimation */);
resetKeySelectionByDraggingFinger();
dismissMoreKeysPanel();
}
private boolean isMajorEnoughMoveToBeOnNewKey(final int x, final int y, final Key newKey) {
final Key curKey = mCurrentKey;
if (newKey == curKey) {
return false;
}
if (curKey == null /* && newKey != null */) {
return true;
}
// Here curKey points to the different key from newKey.
final int keyHysteresisDistanceSquared = mKeyDetector.getKeyHysteresisDistanceSquared(
mIsInSlidingKeyInput);
final int distanceFromKeyEdgeSquared = curKey.squaredDistanceToHitboxEdge(x, y);
if (distanceFromKeyEdgeSquared >= keyHysteresisDistanceSquared) {
if (DEBUG_MODE) {
final float distanceToEdgeRatio = (float)Math.sqrt(distanceFromKeyEdgeSquared)
/ (mKeyboard.mMostCommonKeyWidth + mKeyboard.mHorizontalGap);
Log.d(TAG, String.format("[%d] isMajorEnoughMoveToBeOnNewKey:"
+" %.2f key width from key edge", mPointerId, distanceToEdgeRatio));
}
return true;
}
if (!mIsAllowedDraggingFinger && mBogusMoveEventDetector.hasTraveledLongDistance(x, y)) {
if (DEBUG_MODE) {
final float keyDiagonal = (float)Math.hypot(
mKeyboard.mMostCommonKeyWidth + mKeyboard.mHorizontalGap,
mKeyboard.mMostCommonKeyHeight + mKeyboard.mVerticalGap);
final float lengthFromDownRatio =
mBogusMoveEventDetector.getAccumulatedDistanceFromDownKey() / keyDiagonal;
Log.d(TAG, String.format("[%d] isMajorEnoughMoveToBeOnNewKey:"
+ " %.2f key diagonal from virtual down point",
mPointerId, lengthFromDownRatio));
}
return true;
}
return false;
}
private void startLongPressTimer(final Key key) {
// Note that we need to cancel all active long press shift key timers if any whenever we
// start a new long press timer for both non-shift and shift keys.
sTimerProxy.cancelLongPressShiftKeyTimer();
if (key == null) return;
if (!key.isLongPressEnabled()) return;
// Caveat: Please note that isLongPressEnabled() can be true even if the current key
// doesn't have its more keys. (e.g. spacebar, globe key) If we are in the dragging finger
// mode, we will disable long press timer of such key.
// We always need to start the long press timer if the key has its more keys regardless of
// whether or not we are in the dragging finger mode.
if (mIsInDraggingFinger && key.getMoreKeys() == null) return;
final int delay = getLongPressTimeout(key.getCode());
if (delay <= 0) return;
sTimerProxy.startLongPressTimerOf(this, delay);
}
private int getLongPressTimeout(final int code) {
if (code == Constants.CODE_SHIFT) {
return sParams.mLongPressShiftLockTimeout;
}
final int longpressTimeout = Settings.getInstance().getCurrent().mKeyLongpressTimeout;
if (mIsInSlidingKeyInput) {
// We use longer timeout for sliding finger input started from the modifier key.
return longpressTimeout * MULTIPLIER_FOR_LONG_PRESS_TIMEOUT_IN_SLIDING_INPUT;
}
if (code == Constants.CODE_SPACE) {
// Cursor can be moved in space
return longpressTimeout * MULTIPLIER_FOR_LONG_PRESS_TIMEOUT_IN_SLIDING_INPUT;
}
return longpressTimeout;
}
private void detectAndSendKey(final Key key, final int x, final int y) {
if (key == null) return;
final int code = key.getCode();
callListenerOnCodeInput(key, code, x, y, false /* isKeyRepeat */);
callListenerOnRelease(key, code, false /* withSliding */);
}
private void startRepeatKey(final Key key) {
if (key == null) return;
if (!key.isRepeatable()) return;
// Don't start key repeat when we are in the dragging finger mode.
if (mIsInDraggingFinger) return;
final int startRepeatCount = 1;
startKeyRepeatTimer(startRepeatCount);
}
public void onKeyRepeat(final int code, final int repeatCount) {
final Key key = getKey();
if (key == null || key.getCode() != code) {
mCurrentRepeatingKeyCode = Constants.NOT_A_CODE;
return;
}
mCurrentRepeatingKeyCode = code;
final int nextRepeatCount = repeatCount + 1;
startKeyRepeatTimer(nextRepeatCount);
callListenerOnPressAndCheckKeyboardLayoutChange(key, repeatCount);
callListenerOnCodeInput(key, code, mKeyX, mKeyY, true /* isKeyRepeat */);
}
private void startKeyRepeatTimer(final int repeatCount) {
final int delay =
(repeatCount == 1) ? sParams.mKeyRepeatStartTimeout : sParams.mKeyRepeatInterval;
sTimerProxy.startKeyRepeatTimerOf(this, repeatCount, delay);
}
private void printTouchEvent(final String title, final int x, final int y,
final long eventTime) {
final Key key = mKeyDetector.detectHitKey(x, y);
final String code = (key == null ? "none" : Constants.printableCode(key.getCode()));
Log.d(TAG, String.format("[%d]%s%s %4d %4d %5d %s", mPointerId,
(mIsTrackingForActionDisabled ? "-" : " "), title, x, y, eventTime, code));
}
}

View File

@ -0,0 +1,120 @@
/*
* Copyright (C) 2011 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.colorful.keyboard.theme.keyboard;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
public class ProximityInfo {
private static final List<Key> EMPTY_KEY_LIST = Collections.emptyList();
private final int mGridWidth;
private final int mGridHeight;
private final int mGridSize;
private final int mCellWidth;
private final int mCellHeight;
// TODO: Find a proper name for mKeyboardMinWidth
private final int mKeyboardMinWidth;
private final int mKeyboardHeight;
private final List<Key> mSortedKeys;
private final List<Key>[] mGridNeighbors;
@SuppressWarnings("unchecked")
ProximityInfo(final int gridWidth, final int gridHeight, final int minWidth, final int height,
final List<Key> sortedKeys) {
mGridWidth = gridWidth;
mGridHeight = gridHeight;
mGridSize = mGridWidth * mGridHeight;
mCellWidth = (minWidth + mGridWidth - 1) / mGridWidth;
mCellHeight = (height + mGridHeight - 1) / mGridHeight;
mKeyboardMinWidth = minWidth;
mKeyboardHeight = height;
mSortedKeys = sortedKeys;
mGridNeighbors = new List[mGridSize];
if (minWidth == 0 || height == 0) {
// No proximity required. Keyboard might be more keys keyboard.
return;
}
computeNearestNeighbors();
}
private void computeNearestNeighbors() {
final int keyCount = mSortedKeys.size();
final int gridSize = mGridNeighbors.length;
final int maxKeyRight = mGridWidth * mCellWidth;
final int maxKeyBottom = mGridHeight * mCellHeight;
// For large layouts, 'neighborsFlatBuffer' is about 80k of memory: gridSize is usually 512,
// keycount is about 40 and a pointer to a Key is 4 bytes. This contains, for each cell,
// enough space for as many keys as there are on the keyboard. Hence, every
// keycount'th element is the start of a new cell, and each of these virtual subarrays
// start empty with keycount spaces available. This fills up gradually in the loop below.
// Since in the practice each cell does not have a lot of neighbors, most of this space is
// actually just empty padding in this fixed-size buffer.
final Key[] neighborsFlatBuffer = new Key[gridSize * keyCount];
final int[] neighborCountPerCell = new int[gridSize];
for (final Key key : mSortedKeys) {
if (key.isSpacer()) continue;
// Iterate through all of the cells that overlap with the clickable region of the
// current key and add the key as a neighbor.
final int keyX = key.getX();
final int keyY = key.getY();
final int keyTop = keyY - key.getTopPadding();
final int keyBottom = Math.min(keyY + key.getHeight() + key.getBottomPadding(),
maxKeyBottom);
final int keyLeft = keyX - key.getLeftPadding();
final int keyRight = Math.min(keyX + key.getWidth() + key.getRightPadding(),
maxKeyRight);
final int yDeltaToGrid = keyTop % mCellHeight;
final int xDeltaToGrid = keyLeft % mCellWidth;
final int yStart = keyTop - yDeltaToGrid;
final int xStart = keyLeft - xDeltaToGrid;
int baseIndexOfCurrentRow = (yStart / mCellHeight) * mGridWidth + (xStart / mCellWidth);
for (int cellTop = yStart; cellTop < keyBottom; cellTop += mCellHeight) {
int index = baseIndexOfCurrentRow;
for (int cellLeft = xStart; cellLeft < keyRight; cellLeft += mCellWidth) {
neighborsFlatBuffer[index * keyCount + neighborCountPerCell[index]] = key;
++neighborCountPerCell[index];
++index;
}
baseIndexOfCurrentRow += mGridWidth;
}
}
for (int i = 0; i < gridSize; ++i) {
final int indexStart = i * keyCount;
final int indexEnd = indexStart + neighborCountPerCell[i];
final ArrayList<Key> neighbors = new ArrayList<>(indexEnd - indexStart);
for (int index = indexStart; index < indexEnd; index++) {
neighbors.add(neighborsFlatBuffer[index]);
}
mGridNeighbors[i] = Collections.unmodifiableList(neighbors);
}
}
public List<Key> getNearestKeys(final int x, final int y) {
if (x >= 0 && x < mKeyboardMinWidth && y >= 0 && y < mKeyboardHeight) {
int index = (y / mCellHeight) * mGridWidth + (x / mCellWidth);
if (index < mGridSize) {
return mGridNeighbors[index];
}
}
return EMPTY_KEY_LIST;
}
}

View File

@ -0,0 +1,116 @@
/*
* Copyright (C) 2010 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.colorful.keyboard.theme.keyboard.internal;
import android.util.Log;
public final class AlphabetShiftState {
private static final String TAG = AlphabetShiftState.class.getSimpleName();
private static final boolean DEBUG = false;
private static final int UNSHIFTED = 0;
private static final int MANUAL_SHIFTED = 1;
private static final int AUTOMATIC_SHIFTED = 2;
private static final int SHIFT_LOCKED = 3;
private static final int SHIFT_LOCK_SHIFTED = 4;
private int mState = UNSHIFTED;
public void setShifted(boolean newShiftState) {
final int oldState = mState;
if (newShiftState) {
switch (oldState) {
case UNSHIFTED:
mState = MANUAL_SHIFTED;
break;
case SHIFT_LOCKED:
mState = SHIFT_LOCK_SHIFTED;
break;
}
} else {
switch (oldState) {
case MANUAL_SHIFTED:
case AUTOMATIC_SHIFTED:
mState = UNSHIFTED;
break;
case SHIFT_LOCK_SHIFTED:
mState = SHIFT_LOCKED;
break;
}
}
if (DEBUG)
Log.d(TAG, "setShifted(" + newShiftState + "): " + toString(oldState) + " > " + this);
}
public void setShiftLocked(boolean newShiftLockState) {
final int oldState = mState;
if (newShiftLockState) {
switch (oldState) {
case UNSHIFTED:
case MANUAL_SHIFTED:
case AUTOMATIC_SHIFTED:
mState = SHIFT_LOCKED;
break;
}
} else {
mState = UNSHIFTED;
}
if (DEBUG)
Log.d(TAG, "setShiftLocked(" + newShiftLockState + "): " + toString(oldState)
+ " > " + this);
}
public void setAutomaticShifted() {
mState = AUTOMATIC_SHIFTED;
}
public boolean isShiftedOrShiftLocked() {
return mState != UNSHIFTED;
}
public boolean isShiftLocked() {
return mState == SHIFT_LOCKED || mState == SHIFT_LOCK_SHIFTED;
}
public boolean isShiftLockShifted() {
return mState == SHIFT_LOCK_SHIFTED;
}
public boolean isAutomaticShifted() {
return mState == AUTOMATIC_SHIFTED;
}
public boolean isManualShifted() {
return mState == MANUAL_SHIFTED || mState == SHIFT_LOCK_SHIFTED;
}
@Override
public String toString() {
return toString(mState);
}
private static String toString(int state) {
switch (state) {
case UNSHIFTED: return "UNSHIFTED";
case MANUAL_SHIFTED: return "MANUAL_SHIFTED";
case AUTOMATIC_SHIFTED: return "AUTOMATIC_SHIFTED";
case SHIFT_LOCKED: return "SHIFT_LOCKED";
case SHIFT_LOCK_SHIFTED: return "SHIFT_LOCK_SHIFTED";
default: return "UNKNOWN";
}
}
}

View File

@ -0,0 +1,100 @@
/*
* Copyright (C) 2013 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.colorful.keyboard.theme.keyboard.internal;
import android.content.res.Resources;
import android.util.DisplayMetrics;
import android.util.Log;
import com.colorful.keyboard.theme.R;
import com.colorful.keyboard.theme.latin.common.Constants;
import com.colorful.keyboard.theme.latin.define.DebugFlags;
// This hack is applied to certain classes of tablets.
public final class BogusMoveEventDetector {
private static final String TAG = BogusMoveEventDetector.class.getSimpleName();
private static final boolean DEBUG_MODE = DebugFlags.DEBUG_ENABLED;
// Move these thresholds to resource.
// These thresholds' unit is a diagonal length of a key.
private static final float BOGUS_MOVE_ACCUMULATED_DISTANCE_THRESHOLD = 0.53f;
private static boolean sNeedsProximateBogusDownMoveUpEventHack;
public static void init(final Resources res) {
// The proximate bogus down move up event hack is needed for a device such like,
// 1) is large tablet, or 2) is small tablet and the screen density is less than hdpi.
// Though it seems odd to use screen density as criteria of the quality of the touch
// screen, the small table that has a less density screen than hdpi most likely has been
// made with the touch screen that needs the hack.
final int screenMetrics = res.getInteger(R.integer.config_screen_metrics);
final boolean isLargeTablet = (screenMetrics == Constants.SCREEN_METRICS_LARGE_TABLET);
final boolean isSmallTablet = (screenMetrics == Constants.SCREEN_METRICS_SMALL_TABLET);
final int densityDpi = res.getDisplayMetrics().densityDpi;
final boolean hasLowDensityScreen = (densityDpi < DisplayMetrics.DENSITY_HIGH);
final boolean needsTheHack = isLargeTablet || (isSmallTablet && hasLowDensityScreen);
if (DEBUG_MODE) {
final int sw = res.getConfiguration().smallestScreenWidthDp;
Log.d(TAG, "needsProximateBogusDownMoveUpEventHack=" + needsTheHack
+ " smallestScreenWidthDp=" + sw + " densityDpi=" + densityDpi
+ " screenMetrics=" + screenMetrics);
}
sNeedsProximateBogusDownMoveUpEventHack = needsTheHack;
}
private int mAccumulatedDistanceThreshold;
// Accumulated distance from actual and artificial down keys.
/* package */ int mAccumulatedDistanceFromDownKey;
private int mActualDownX;
private int mActualDownY;
public void setKeyboardGeometry(final int keyPaddedWidth, final int keyPaddedHeight) {
final float keyDiagonal = (float)Math.hypot(keyPaddedWidth, keyPaddedHeight);
mAccumulatedDistanceThreshold = (int)(
keyDiagonal * BOGUS_MOVE_ACCUMULATED_DISTANCE_THRESHOLD);
}
public void onActualDownEvent(final int x, final int y) {
mActualDownX = x;
mActualDownY = y;
}
public void onDownKey() {
mAccumulatedDistanceFromDownKey = 0;
}
public void onMoveKey(final int distance) {
mAccumulatedDistanceFromDownKey += distance;
}
public boolean hasTraveledLongDistance(final int x, final int y) {
if (!sNeedsProximateBogusDownMoveUpEventHack) {
return false;
}
final int dx = Math.abs(x - mActualDownX);
final int dy = Math.abs(y - mActualDownY);
// A bogus move event should be a horizontal movement. A vertical movement might be
// a sloppy typing and should be ignored.
return dx >= dy && mAccumulatedDistanceFromDownKey >= mAccumulatedDistanceThreshold;
}
public int getAccumulatedDistanceFromDownKey() {
return mAccumulatedDistanceFromDownKey;
}
}

View File

@ -0,0 +1,47 @@
/*
* Copyright (C) 2012 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.colorful.keyboard.theme.keyboard.internal;
import android.content.Context;
import android.graphics.Canvas;
import android.util.AttributeSet;
import android.widget.RelativeLayout;
import com.colorful.keyboard.theme.latin.common.CoordinateUtils;
public final class DrawingPreviewPlacerView extends RelativeLayout {
private final int[] mKeyboardViewOrigin = CoordinateUtils.newInstance();
public DrawingPreviewPlacerView(final Context context, final AttributeSet attrs) {
super(context, attrs);
setWillNotDraw(false);
}
public void setKeyboardViewGeometry(final int[] originCoords) {
CoordinateUtils.copy(mKeyboardViewOrigin, originCoords);
}
@Override
public void onDraw(final Canvas canvas) {
super.onDraw(canvas);
final int originX = CoordinateUtils.x(mKeyboardViewOrigin);
final int originY = CoordinateUtils.y(mKeyboardViewOrigin);
canvas.translate(originX, originY);
canvas.translate(-originX, -originY);
}
}

View File

@ -0,0 +1,56 @@
/*
* Copyright (C) 2014 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.colorful.keyboard.theme.keyboard.internal;
import com.colorful.keyboard.theme.keyboard.Key;
import com.colorful.keyboard.theme.keyboard.MoreKeysPanel;
import com.colorful.keyboard.theme.keyboard.PointerTracker;
public interface DrawingProxy {
/**
* Called when a key is being pressed.
* @param key the {@link Key} that is being pressed.
* @param withPreview true if key popup preview should be displayed.
*/
void onKeyPressed(Key key, boolean withPreview);
/**
* Called when a key is being released.
* @param key the {@link Key} that is being released.
* @param withAnimation when true, key popup preview should be dismissed with animation.
*/
void onKeyReleased(Key key, boolean withAnimation);
/**
* Start showing more keys keyboard of a key that is being long pressed.
* @param key the {@link Key} that is being long pressed and showing more keys keyboard.
* @param tracker the {@link PointerTracker} that detects this long pressing.
* @return {@link MoreKeysPanel} that is being shown. null if there is no need to show more keys
* keyboard.
*/
MoreKeysPanel showMoreKeysKeyboard(Key key, PointerTracker tracker);
/**
* Start a while-typing-animation.
* @param fadeInOrOut {@link #FADE_IN} starts while-typing-fade-in animation.
* {@link #FADE_OUT} starts while-typing-fade-out animation.
*/
void startWhileTypingAnimation(int fadeInOrOut);
int FADE_IN = 0;
int FADE_OUT = 1;
}

View File

@ -0,0 +1,162 @@
/*
* Copyright (C) 2012 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.colorful.keyboard.theme.keyboard.internal;
import android.graphics.Typeface;
import com.colorful.keyboard.theme.latin.utils.ResourceUtils;
public final class KeyDrawParams {
public Typeface mTypeface = Typeface.DEFAULT;
public int mLetterSize;
public int mLabelSize;
public int mLargeLetterSize;
public int mHintLetterSize;
public int mShiftedLetterHintSize;
public int mHintLabelSize;
public int mPreviewTextSize;
public int mTextColor;
public int mTextInactivatedColor;
public int mTextShadowColor;
public int mFunctionalTextColor;
public int mHintLetterColor;
public int mHintLabelColor;
public int mShiftedLetterHintInactivatedColor;
public int mShiftedLetterHintActivatedColor;
public int mPreviewTextColor;
public float mHintLabelVerticalAdjustment;
public float mLabelOffCenterRatio;
public float mHintLabelOffCenterRatio;
public int mAnimAlpha;
public KeyDrawParams() {}
private KeyDrawParams(final KeyDrawParams copyFrom) {
mTypeface = copyFrom.mTypeface;
mLetterSize = copyFrom.mLetterSize;
mLabelSize = copyFrom.mLabelSize;
mLargeLetterSize = copyFrom.mLargeLetterSize;
mHintLetterSize = copyFrom.mHintLetterSize;
mShiftedLetterHintSize = copyFrom.mShiftedLetterHintSize;
mHintLabelSize = copyFrom.mHintLabelSize;
mPreviewTextSize = copyFrom.mPreviewTextSize;
mTextColor = copyFrom.mTextColor;
mTextInactivatedColor = copyFrom.mTextInactivatedColor;
mTextShadowColor = copyFrom.mTextShadowColor;
mFunctionalTextColor = copyFrom.mFunctionalTextColor;
mHintLetterColor = copyFrom.mHintLetterColor;
mHintLabelColor = copyFrom.mHintLabelColor;
mShiftedLetterHintInactivatedColor = copyFrom.mShiftedLetterHintInactivatedColor;
mShiftedLetterHintActivatedColor = copyFrom.mShiftedLetterHintActivatedColor;
mPreviewTextColor = copyFrom.mPreviewTextColor;
mHintLabelVerticalAdjustment = copyFrom.mHintLabelVerticalAdjustment;
mLabelOffCenterRatio = copyFrom.mLabelOffCenterRatio;
mHintLabelOffCenterRatio = copyFrom.mHintLabelOffCenterRatio;
mAnimAlpha = copyFrom.mAnimAlpha;
}
public void updateParams(final int keyHeight, final KeyVisualAttributes attr) {
if (attr == null) {
return;
}
if (attr.mTypeface != null) {
mTypeface = attr.mTypeface;
}
mLetterSize = selectTextSizeFromDimensionOrRatio(keyHeight,
attr.mLetterSize, attr.mLetterRatio, mLetterSize);
mLabelSize = selectTextSizeFromDimensionOrRatio(keyHeight,
attr.mLabelSize, attr.mLabelRatio, mLabelSize);
mLargeLetterSize = selectTextSize(keyHeight, attr.mLargeLetterRatio, mLargeLetterSize);
mHintLetterSize = selectTextSize(keyHeight, attr.mHintLetterRatio, mHintLetterSize);
mShiftedLetterHintSize = selectTextSize(keyHeight,
attr.mShiftedLetterHintRatio, mShiftedLetterHintSize);
mHintLabelSize = selectTextSize(keyHeight, attr.mHintLabelRatio, mHintLabelSize);
mPreviewTextSize = selectTextSize(keyHeight, attr.mPreviewTextRatio, mPreviewTextSize);
mTextColor = selectColor(attr.mTextColor, mTextColor);
mTextInactivatedColor = selectColor(attr.mTextInactivatedColor, mTextInactivatedColor);
mTextShadowColor = selectColor(attr.mTextShadowColor, mTextShadowColor);
mFunctionalTextColor = selectColor(attr.mFunctionalTextColor, mFunctionalTextColor);
mHintLetterColor = selectColor(attr.mHintLetterColor, mHintLetterColor);
mHintLabelColor = selectColor(attr.mHintLabelColor, mHintLabelColor);
mShiftedLetterHintInactivatedColor = selectColor(
attr.mShiftedLetterHintInactivatedColor, mShiftedLetterHintInactivatedColor);
mShiftedLetterHintActivatedColor = selectColor(
attr.mShiftedLetterHintActivatedColor, mShiftedLetterHintActivatedColor);
mPreviewTextColor = selectColor(attr.mPreviewTextColor, mPreviewTextColor);
mHintLabelVerticalAdjustment = selectFloatIfNonZero(
attr.mHintLabelVerticalAdjustment, mHintLabelVerticalAdjustment);
mLabelOffCenterRatio = selectFloatIfNonZero(
attr.mLabelOffCenterRatio, mLabelOffCenterRatio);
mHintLabelOffCenterRatio = selectFloatIfNonZero(
attr.mHintLabelOffCenterRatio, mHintLabelOffCenterRatio);
}
public KeyDrawParams mayCloneAndUpdateParams(final int keyHeight,
final KeyVisualAttributes attr) {
if (attr == null) {
return this;
}
final KeyDrawParams newParams = new KeyDrawParams(this);
newParams.updateParams(keyHeight, attr);
return newParams;
}
private static int selectTextSizeFromDimensionOrRatio(final int keyHeight,
final int dimens, final float ratio, final int defaultDimens) {
if (ResourceUtils.isValidDimensionPixelSize(dimens)) {
return dimens;
}
if (ResourceUtils.isValidFraction(ratio)) {
return (int)(keyHeight * ratio);
}
return defaultDimens;
}
private static int selectTextSize(final int keyHeight, final float ratio,
final int defaultSize) {
if (ResourceUtils.isValidFraction(ratio)) {
return (int)(keyHeight * ratio);
}
return defaultSize;
}
private static int selectColor(final int attrColor, final int defaultColor) {
if (attrColor != 0) {
return attrColor;
}
return defaultColor;
}
private static float selectFloatIfNonZero(final float attrFloat, final float defaultFloat) {
if (attrFloat != 0) {
return attrFloat;
}
return defaultFloat;
}
}

View File

@ -0,0 +1,153 @@
package com.colorful.keyboard.theme.keyboard.internal;
import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.content.Context;
import android.view.View;
import android.view.ViewGroup;
import com.colorful.keyboard.theme.keyboard.Key;
import com.colorful.keyboard.theme.latin.common.CoordinateUtils;
import com.colorful.keyboard.theme.latin.utils.ViewLayoutUtils;
import java.util.ArrayDeque;
import java.util.HashMap;
;
/**
* This class controls pop up key previews. This class decides:
* - what kind of key previews should be shown.
* - where key previews should be placed.
* - how key previews should be shown and dismissed.
*/
public final class KeyPreviewChoreographer {
// Free {@link KeyPreviewView} pool that can be used for key preview.
private final ArrayDeque<KeyPreviewView> mFreeKeyPreviewViews = new ArrayDeque<>();
// Map from {@link Key} to {@link KeyPreviewView} that is currently being displayed as key
// preview.
private final HashMap<Key,KeyPreviewView> mShowingKeyPreviewViews = new HashMap<>();
private final KeyPreviewDrawParams mParams;
public KeyPreviewChoreographer(final KeyPreviewDrawParams params) {
mParams = params;
}
public KeyPreviewView getKeyPreviewView(final Key key, final ViewGroup placerView) {
KeyPreviewView keyPreviewView = mShowingKeyPreviewViews.remove(key);
if (keyPreviewView != null) {
keyPreviewView.setScaleX(1);
keyPreviewView.setScaleY(1);
return keyPreviewView;
}
keyPreviewView = mFreeKeyPreviewViews.poll();
if (keyPreviewView != null) {
keyPreviewView.setScaleX(1);
keyPreviewView.setScaleY(1);
return keyPreviewView;
}
final Context context = placerView.getContext();
keyPreviewView = new KeyPreviewView(context, null /* attrs */);
keyPreviewView.setBackgroundResource(mParams.mPreviewBackgroundResId);
placerView.addView(keyPreviewView, ViewLayoutUtils.newLayoutParam(placerView, 0, 0));
return keyPreviewView;
}
public void dismissKeyPreview(final Key key, final boolean withAnimation) {
if (key == null) {
return;
}
final KeyPreviewView keyPreviewView = mShowingKeyPreviewViews.get(key);
if (keyPreviewView == null) {
return;
}
final Object tag = keyPreviewView.getTag();
if (withAnimation) {
if (tag instanceof KeyPreviewAnimators) {
final KeyPreviewAnimators animators = (KeyPreviewAnimators)tag;
animators.startDismiss();
return;
}
}
// Dismiss preview without animation.
mShowingKeyPreviewViews.remove(key);
if (tag instanceof Animator) {
((Animator)tag).cancel();
}
keyPreviewView.setTag(null);
keyPreviewView.setVisibility(View.INVISIBLE);
mFreeKeyPreviewViews.add(keyPreviewView);
}
public void placeAndShowKeyPreview(final Key key, final KeyboardIconsSet iconsSet,
final KeyDrawParams drawParams, final int[] keyboardOrigin,
final ViewGroup placerView, final boolean withAnimation) {
final KeyPreviewView keyPreviewView = getKeyPreviewView(key, placerView);
placeKeyPreview(
key, keyPreviewView, iconsSet, drawParams, keyboardOrigin);
showKeyPreview(key, keyPreviewView, withAnimation);
}
private void placeKeyPreview(final Key key, final KeyPreviewView keyPreviewView,
final KeyboardIconsSet iconsSet, final KeyDrawParams drawParams,
final int[] originCoords) {
keyPreviewView.setPreviewVisual(key, iconsSet, drawParams);
keyPreviewView.measure(
ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
mParams.setGeometry(keyPreviewView);
final int previewWidth = Math.max(keyPreviewView.getMeasuredWidth(), mParams.mMinPreviewWidth);
final int previewHeight = mParams.mPreviewHeight;
final int keyWidth = key.getWidth();
// The key preview is horizontally aligned with the center of the visible part of the
// parent key. If it doesn't fit in this {@link KeyboardView}, it is moved inward to fit and
// the left/right background is used if such background is specified.
int previewX = key.getX() - (previewWidth - keyWidth) / 2
+ CoordinateUtils.x(originCoords);
// The key preview is placed vertically above the top edge of the parent key with an
// arbitrary offset.
final int previewY = key.getY() - previewHeight + mParams.mPreviewOffset
+ CoordinateUtils.y(originCoords);
ViewLayoutUtils.placeViewAt(
keyPreviewView, previewX, previewY, previewWidth, previewHeight);
//keyPreviewView.setPivotX(previewWidth / 2.0f);
//keyPreviewView.setPivotY(previewHeight);
}
void showKeyPreview(final Key key, final KeyPreviewView keyPreviewView,
final boolean withAnimation) {
if (!withAnimation) {
keyPreviewView.setVisibility(View.VISIBLE);
mShowingKeyPreviewViews.put(key, keyPreviewView);
return;
}
// Show preview with animation.
final Animator dismissAnimator = createDismissAnimator(key, keyPreviewView);
final KeyPreviewAnimators animators = new KeyPreviewAnimators(dismissAnimator);
keyPreviewView.setTag(animators);
showKeyPreview(key, keyPreviewView, false /* withAnimation */);
}
private Animator createDismissAnimator(final Key key, final KeyPreviewView keyPreviewView) {
final Animator dismissAnimator = mParams.createDismissAnimator(keyPreviewView);
dismissAnimator.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(final Animator animator) {
dismissKeyPreview(key, false /* withAnimation */);
}
});
return dismissAnimator;
}
private static class KeyPreviewAnimators extends AnimatorListenerAdapter {
private final Animator mDismissAnimator;
public KeyPreviewAnimators(final Animator dismissAnimator) {
mDismissAnimator = dismissAnimator;
}
public void startDismiss() {
mDismissAnimator.start();
}
}
}

View File

@ -0,0 +1,130 @@
/*
* Copyright (C) 2012 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.colorful.keyboard.theme.keyboard.internal;
import android.animation.Animator;
import android.animation.AnimatorInflater;
import android.content.res.TypedArray;
import android.view.View;
import android.view.animation.AccelerateInterpolator;
import com.colorful.keyboard.theme.R;
public final class KeyPreviewDrawParams {
// XML attributes of {@link MainKeyboardView}.
public final int mPreviewOffset;
public final int mPreviewHeight;
public final int mMinPreviewWidth;
public final int mPreviewBackgroundResId;
private final int mDismissAnimatorResId;
private int mLingerTimeout;
private boolean mShowPopup = true;
// The graphical geometry of the key preview.
// <-width->
// +-------+ ^
// | | |
// |preview| height (visible)
// | | |
// + + ^ v
// \ / |offset
// +-\ /-+ v
// | +-+ |
// |parent |
// | key|
// +-------+
// The background of a {@link TextView} being used for a key preview may have invisible
// paddings. To align the more keys keyboard panel's visible part with the visible part of
// the background, we need to record the width and height of key preview that don't include
// invisible paddings.
private int mVisibleWidth;
private int mVisibleHeight;
// The key preview may have an arbitrary offset and its background that may have a bottom
// padding. To align the more keys keyboard and the key preview we also need to record the
// offset between the top edge of parent key and the bottom of the visible part of key
// preview background.
private int mVisibleOffset;
public KeyPreviewDrawParams(final TypedArray mainKeyboardViewAttr) {
mPreviewOffset = mainKeyboardViewAttr.getDimensionPixelOffset(
R.styleable.MainKeyboardView_keyPreviewOffset, 0);
mPreviewHeight = mainKeyboardViewAttr.getDimensionPixelSize(
R.styleable.MainKeyboardView_keyPreviewHeight, 0);
mMinPreviewWidth = mainKeyboardViewAttr.getDimensionPixelSize(
R.styleable.MainKeyboardView_keyPreviewWidth, 0);
mPreviewBackgroundResId = mainKeyboardViewAttr.getResourceId(
R.styleable.MainKeyboardView_keyPreviewBackground, 0);
mLingerTimeout = mainKeyboardViewAttr.getInt(
R.styleable.MainKeyboardView_keyPreviewLingerTimeout, 0);
mDismissAnimatorResId = mainKeyboardViewAttr.getResourceId(
R.styleable.MainKeyboardView_keyPreviewDismissAnimator, 0);
}
public void setVisibleOffset(final int previewVisibleOffset) {
mVisibleOffset = previewVisibleOffset;
}
public int getVisibleOffset() {
return mVisibleOffset;
}
public void setGeometry(final View previewTextView) {
final int previewWidth = Math.max(previewTextView.getMeasuredWidth(), mMinPreviewWidth);
// The width and height of visible part of the key preview background. The content marker
// of the background 9-patch have to cover the visible part of the background.
mVisibleWidth = previewWidth - previewTextView.getPaddingLeft()
- previewTextView.getPaddingRight();
mVisibleHeight = mPreviewHeight - previewTextView.getPaddingTop()
- previewTextView.getPaddingBottom();
// The distance between the top edge of the parent key and the bottom of the visible part
// of the key preview background.
setVisibleOffset(mPreviewOffset - previewTextView.getPaddingBottom());
}
public int getVisibleWidth() {
return mVisibleWidth;
}
public int getVisibleHeight() {
return mVisibleHeight;
}
public void setPopupEnabled(final boolean enabled, final int lingerTimeout) {
mShowPopup = enabled;
mLingerTimeout = lingerTimeout;
}
public boolean isPopupEnabled() {
return mShowPopup;
}
public int getLingerTimeout() {
return mLingerTimeout;
}
private static final AccelerateInterpolator ACCELERATE_INTERPOLATOR =
new AccelerateInterpolator();
public Animator createDismissAnimator(final View target) {
final Animator animator = AnimatorInflater.loadAnimator(
target.getContext(), mDismissAnimatorResId);
animator.setTarget(target);
animator.setInterpolator(ACCELERATE_INTERPOLATOR);
return animator;
}
}

View File

@ -0,0 +1,112 @@
/*
* Copyright (C) 2014 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.colorful.keyboard.theme.keyboard.internal;
import android.content.Context;
import android.graphics.Rect;
import android.graphics.drawable.Drawable;
import android.text.TextPaint;
import android.text.TextUtils;
import android.util.AttributeSet;
import android.util.TypedValue;
import android.view.Gravity;
import com.colorful.keyboard.theme.keyboard.Key;
import java.util.HashSet;
/**
* The pop up key preview view.
*/
public class KeyPreviewView extends androidx.appcompat.widget.AppCompatTextView {
private final Rect mBackgroundPadding = new Rect();
private static final HashSet<String> sNoScaleXTextSet = new HashSet<>();
public KeyPreviewView(final Context context, final AttributeSet attrs) {
this(context, attrs, 0);
}
public KeyPreviewView(final Context context, final AttributeSet attrs, final int defStyleAttr) {
super(context, attrs, defStyleAttr);
setGravity(Gravity.CENTER);
}
public void setPreviewVisual(final Key key, final KeyboardIconsSet iconsSet,
final KeyDrawParams drawParams) {
// What we show as preview should match what we show on a key top in onDraw().
final int iconId = key.getIconId();
if (iconId != KeyboardIconsSet.ICON_UNDEFINED) {
setCompoundDrawables(null, null, null, key.getPreviewIcon(iconsSet));
setText(null);
return;
}
setCompoundDrawables(null, null, null, null);
setTextColor(drawParams.mPreviewTextColor);
setTextSize(TypedValue.COMPLEX_UNIT_PX, key.selectPreviewTextSize(drawParams));
setTypeface(key.selectPreviewTypeface(drawParams));
// TODO Should take care of temporaryShiftLabel here.
setTextAndScaleX(key.getPreviewLabel());
}
private void setTextAndScaleX(final String text) {
setTextScaleX(1.0f);
setText(text);
if (sNoScaleXTextSet.contains(text)) {
return;
}
// TODO: Override {@link #setBackground(Drawable)} that is supported from API 16 and
// calculate maximum text width.
final Drawable background = getBackground();
if (background == null) {
return;
}
background.getPadding(mBackgroundPadding);
final int maxWidth = background.getIntrinsicWidth() - mBackgroundPadding.left
- mBackgroundPadding.right;
final float width = getTextWidth(text, getPaint());
if (width <= maxWidth) {
sNoScaleXTextSet.add(text);
return;
}
setTextScaleX(maxWidth / width);
}
public static void clearTextCache() {
sNoScaleXTextSet.clear();
}
private static float getTextWidth(final String text, final TextPaint paint) {
if (TextUtils.isEmpty(text)) {
return 0.0f;
}
final int len = text.length();
final float[] widths = new float[len];
final int count = paint.getTextWidths(text, 0, len, widths);
float width = 0;
for (int i = 0; i < count; i++) {
width += widths[i];
}
return width;
}
/*public void setPreviewBackground(boolean customColorEnabled, int customColor) {
final Drawable background = getBackground();
if (customColorEnabled)
background.setColorFilter(customColor, PorterDuff.Mode.OVERLAY);
}*/
}

View File

@ -0,0 +1,227 @@
/*
* Copyright (C) 2010 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.colorful.keyboard.theme.keyboard.internal;
import static com.colorful.keyboard.theme.latin.common.Constants.CODE_OUTPUT_TEXT;
import static com.colorful.keyboard.theme.latin.common.Constants.CODE_UNSPECIFIED;
import com.colorful.keyboard.theme.latin.common.Constants;
import com.colorful.keyboard.theme.latin.common.StringUtils;
// TODO: Rename to KeySpec and make this class to the key specification object.
public final class KeySpecParser {
// Constants for parsing.
private static final char BACKSLASH = Constants.CODE_BACKSLASH;
private static final char VERTICAL_BAR = Constants.CODE_VERTICAL_BAR;
private static final String PREFIX_HEX = "0x";
private KeySpecParser() {
// Intentional empty constructor for utility class.
}
private static boolean hasIcon(final String keySpec) {
return keySpec.startsWith(KeyboardIconsSet.PREFIX_ICON);
}
private static boolean hasCode(final String keySpec, final int labelEnd) {
if (labelEnd <= 0 || labelEnd + 1 >= keySpec.length()) {
return false;
}
if (keySpec.startsWith(KeyboardCodesSet.PREFIX_CODE, labelEnd + 1)) {
return true;
}
// This is a workaround to have a key that has a supplementary code point. We can't put a
// string in resource as a XML entity of a supplementary code point or a surrogate pair.
return keySpec.startsWith(PREFIX_HEX, labelEnd + 1);
}
private static String parseEscape(final String text) {
if (text.indexOf(BACKSLASH) < 0) {
return text;
}
final int length = text.length();
final StringBuilder sb = new StringBuilder();
for (int pos = 0; pos < length; pos++) {
final char c = text.charAt(pos);
if (c == BACKSLASH && pos + 1 < length) {
// Skip escape char
pos++;
sb.append(text.charAt(pos));
} else {
sb.append(c);
}
}
return sb.toString();
}
private static int indexOfLabelEnd(final String keySpec) {
final int length = keySpec.length();
if (keySpec.indexOf(BACKSLASH) < 0) {
final int labelEnd = keySpec.indexOf(VERTICAL_BAR);
if (labelEnd == 0) {
if (length == 1) {
// Treat a sole vertical bar as a special case of key label.
return -1;
}
throw new KeySpecParserError("Empty label");
}
return labelEnd;
}
for (int pos = 0; pos < length; pos++) {
final char c = keySpec.charAt(pos);
if (c == BACKSLASH && pos + 1 < length) {
// Skip escape char
pos++;
} else if (c == VERTICAL_BAR) {
return pos;
}
}
return -1;
}
private static String getBeforeLabelEnd(final String keySpec, final int labelEnd) {
return (labelEnd < 0) ? keySpec : keySpec.substring(0, labelEnd);
}
private static String getAfterLabelEnd(final String keySpec, final int labelEnd) {
return keySpec.substring(labelEnd + /* VERTICAL_BAR */1);
}
private static void checkDoubleLabelEnd(final String keySpec, final int labelEnd) {
if (indexOfLabelEnd(getAfterLabelEnd(keySpec, labelEnd)) < 0) {
return;
}
throw new KeySpecParserError("Multiple " + VERTICAL_BAR + ": " + keySpec);
}
public static String getLabel(final String keySpec) {
if (keySpec == null) {
// TODO: Throw {@link KeySpecParserError} once Key.keyLabel attribute becomes mandatory.
return null;
}
if (hasIcon(keySpec)) {
return null;
}
final int labelEnd = indexOfLabelEnd(keySpec);
final String label = parseEscape(getBeforeLabelEnd(keySpec, labelEnd));
if (label.isEmpty()) {
throw new KeySpecParserError("Empty label: " + keySpec);
}
return label;
}
private static String getOutputTextInternal(final String keySpec, final int labelEnd) {
if (labelEnd <= 0) {
return null;
}
checkDoubleLabelEnd(keySpec, labelEnd);
return parseEscape(getAfterLabelEnd(keySpec, labelEnd));
}
public static String getOutputText(final String keySpec) {
if (keySpec == null) {
// TODO: Throw {@link KeySpecParserError} once Key.keyLabel attribute becomes mandatory.
return null;
}
final int labelEnd = indexOfLabelEnd(keySpec);
if (hasCode(keySpec, labelEnd)) {
return null;
}
final String outputText = getOutputTextInternal(keySpec, labelEnd);
if (outputText != null) {
if (StringUtils.codePointCount(outputText) == 1) {
// If output text is one code point, it should be treated as a code.
// See {@link #getCode(Resources, String)}.
return null;
}
if (outputText.isEmpty()) {
throw new KeySpecParserError("Empty outputText: " + keySpec);
}
return outputText;
}
final String label = getLabel(keySpec);
if (label == null) {
throw new KeySpecParserError("Empty label: " + keySpec);
}
// Code is automatically generated for one letter label. See {@link getCode()}.
return (StringUtils.codePointCount(label) == 1) ? null : label;
}
public static int getCode(final String keySpec) {
if (keySpec == null) {
// TODO: Throw {@link KeySpecParserError} once Key.keyLabel attribute becomes mandatory.
return CODE_UNSPECIFIED;
}
final int labelEnd = indexOfLabelEnd(keySpec);
if (hasCode(keySpec, labelEnd)) {
checkDoubleLabelEnd(keySpec, labelEnd);
return parseCode(getAfterLabelEnd(keySpec, labelEnd), CODE_UNSPECIFIED);
}
final String outputText = getOutputTextInternal(keySpec, labelEnd);
if (outputText != null) {
// If output text is one code point, it should be treated as a code.
// See {@link #getOutputText(String)}.
if (StringUtils.codePointCount(outputText) == 1) {
return outputText.codePointAt(0);
}
return CODE_OUTPUT_TEXT;
}
final String label = getLabel(keySpec);
if (label == null) {
throw new KeySpecParserError("Empty label: " + keySpec);
}
// Code is automatically generated for one letter label.
return (StringUtils.codePointCount(label) == 1) ? label.codePointAt(0) : CODE_OUTPUT_TEXT;
}
public static int parseCode(final String text, final int defaultCode) {
if (text == null) {
return defaultCode;
}
if (text.startsWith(KeyboardCodesSet.PREFIX_CODE)) {
return KeyboardCodesSet.getCode(text.substring(KeyboardCodesSet.PREFIX_CODE.length()));
}
// This is a workaround to have a key that has a supplementary code point. We can't put a
// string in resource as a XML entity of a supplementary code point or a surrogate pair.
if (text.startsWith(PREFIX_HEX)) {
return Integer.parseInt(text.substring(PREFIX_HEX.length()), 16);
}
return defaultCode;
}
public static int getIconId(final String keySpec) {
if (keySpec == null) {
// TODO: Throw {@link KeySpecParserError} once Key.keyLabel attribute becomes mandatory.
return KeyboardIconsSet.ICON_UNDEFINED;
}
if (!hasIcon(keySpec)) {
return KeyboardIconsSet.ICON_UNDEFINED;
}
final int labelEnd = indexOfLabelEnd(keySpec);
final String iconName = getBeforeLabelEnd(keySpec, labelEnd)
.substring(KeyboardIconsSet.PREFIX_ICON.length());
return KeyboardIconsSet.getIconId(iconName);
}
@SuppressWarnings("serial")
public static final class KeySpecParserError extends RuntimeException {
public KeySpecParserError(final String message) {
super(message);
}
}
}

View File

@ -0,0 +1,47 @@
/*
* Copyright (C) 2012 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.colorful.keyboard.theme.keyboard.internal;
import android.content.res.TypedArray;
public abstract class KeyStyle {
private final KeyboardTextsSet mTextsSet;
public abstract String[] getStringArray(TypedArray a, int index);
public abstract String getString(TypedArray a, int index);
public abstract int getInt(TypedArray a, int index, int defaultValue);
public abstract int getFlags(TypedArray a, int index);
protected KeyStyle(final KeyboardTextsSet textsSet) {
mTextsSet = textsSet;
}
protected String parseString(final TypedArray a, final int index) {
if (a.hasValue(index)) {
return mTextsSet.resolveTextReference(a.getString(index));
}
return null;
}
protected String[] parseStringArray(final TypedArray a, final int index) {
if (a.hasValue(index)) {
final String text = mTextsSet.resolveTextReference(a.getString(index));
return MoreKeySpec.splitKeySpecs(text);
}
return null;
}
}

View File

@ -0,0 +1,217 @@
/*
* Copyright (C) 2010 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.colorful.keyboard.theme.keyboard.internal;
import android.content.res.TypedArray;
import android.util.Log;
import android.util.SparseArray;
import com.colorful.keyboard.theme.R;
import com.colorful.keyboard.theme.latin.utils.XmlParseUtils;
import org.xmlpull.v1.XmlPullParser;
import org.xmlpull.v1.XmlPullParserException;
import java.util.Arrays;
import java.util.HashMap;
public final class KeyStylesSet {
private static final String TAG = KeyStylesSet.class.getSimpleName();
private static final boolean DEBUG = false;
private final HashMap<String, KeyStyle> mStyles = new HashMap<>();
private final KeyboardTextsSet mTextsSet;
private final KeyStyle mEmptyKeyStyle;
private static final String EMPTY_STYLE_NAME = "<empty>";
public KeyStylesSet(final KeyboardTextsSet textsSet) {
mTextsSet = textsSet;
mEmptyKeyStyle = new EmptyKeyStyle(textsSet);
mStyles.put(EMPTY_STYLE_NAME, mEmptyKeyStyle);
}
private static final class EmptyKeyStyle extends KeyStyle {
EmptyKeyStyle(final KeyboardTextsSet textsSet) {
super(textsSet);
}
@Override
public String[] getStringArray(final TypedArray a, final int index) {
return parseStringArray(a, index);
}
@Override
public String getString(final TypedArray a, final int index) {
return parseString(a, index);
}
@Override
public int getInt(final TypedArray a, final int index, final int defaultValue) {
return a.getInt(index, defaultValue);
}
@Override
public int getFlags(final TypedArray a, final int index) {
return a.getInt(index, 0);
}
}
private static final class DeclaredKeyStyle extends KeyStyle {
private final HashMap<String, KeyStyle> mStyles;
private final String mParentStyleName;
private final SparseArray<Object> mStyleAttributes = new SparseArray<>();
public DeclaredKeyStyle(final String parentStyleName,
final KeyboardTextsSet textsSet,
final HashMap<String, KeyStyle> styles) {
super(textsSet);
mParentStyleName = parentStyleName;
mStyles = styles;
}
@Override
public String[] getStringArray(final TypedArray a, final int index) {
if (a.hasValue(index)) {
return parseStringArray(a, index);
}
final Object value = mStyleAttributes.get(index);
if (value != null) {
final String[] array = (String[])value;
return Arrays.copyOf(array, array.length);
}
final KeyStyle parentStyle = mStyles.get(mParentStyleName);
return parentStyle.getStringArray(a, index);
}
@Override
public String getString(final TypedArray a, final int index) {
if (a.hasValue(index)) {
return parseString(a, index);
}
final Object value = mStyleAttributes.get(index);
if (value != null) {
return (String)value;
}
final KeyStyle parentStyle = mStyles.get(mParentStyleName);
return parentStyle.getString(a, index);
}
@Override
public int getInt(final TypedArray a, final int index, final int defaultValue) {
if (a.hasValue(index)) {
return a.getInt(index, defaultValue);
}
final Object value = mStyleAttributes.get(index);
if (value != null) {
return (Integer)value;
}
final KeyStyle parentStyle = mStyles.get(mParentStyleName);
return parentStyle.getInt(a, index, defaultValue);
}
@Override
public int getFlags(final TypedArray a, final int index) {
final int parentFlags = mStyles.get(mParentStyleName).getFlags(a, index);
final Integer value = (Integer)mStyleAttributes.get(index);
final int styleFlags = (value != null) ? value : 0;
final int flags = a.getInt(index, 0);
return flags | styleFlags | parentFlags;
}
public void readKeyAttributes(final TypedArray keyAttr) {
// TODO: Currently not all Key attributes can be declared as style.
readString(keyAttr, R.styleable.Keyboard_Key_altCode);
readString(keyAttr, R.styleable.Keyboard_Key_keySpec);
readString(keyAttr, R.styleable.Keyboard_Key_keyHintLabel);
readStringArray(keyAttr, R.styleable.Keyboard_Key_moreKeys);
readStringArray(keyAttr, R.styleable.Keyboard_Key_additionalMoreKeys);
readFlags(keyAttr, R.styleable.Keyboard_Key_keyLabelFlags);
readInt(keyAttr, R.styleable.Keyboard_Key_maxMoreKeysColumn);
readInt(keyAttr, R.styleable.Keyboard_Key_backgroundType);
readFlags(keyAttr, R.styleable.Keyboard_Key_keyActionFlags);
}
private void readString(final TypedArray a, final int index) {
if (a.hasValue(index)) {
mStyleAttributes.put(index, parseString(a, index));
}
}
private void readInt(final TypedArray a, final int index) {
if (a.hasValue(index)) {
mStyleAttributes.put(index, a.getInt(index, 0));
}
}
private void readFlags(final TypedArray a, final int index) {
if (a.hasValue(index)) {
final Integer value = (Integer)mStyleAttributes.get(index);
final int styleFlags = value != null ? value : 0;
mStyleAttributes.put(index, a.getInt(index, 0) | styleFlags);
}
}
private void readStringArray(final TypedArray a, final int index) {
if (a.hasValue(index)) {
mStyleAttributes.put(index, parseStringArray(a, index));
}
}
}
public void parseKeyStyleAttributes(final TypedArray keyStyleAttr, final TypedArray keyAttrs,
final XmlPullParser parser) throws XmlPullParserException {
final String styleName = keyStyleAttr.getString(R.styleable.Keyboard_KeyStyle_styleName);
if (styleName == null) {
throw new XmlParseUtils.ParseException(
KeyboardBuilder.TAG_KEY_STYLE + " has no styleName attribute", parser);
}
if (DEBUG) {
Log.d(TAG, String.format("<%s styleName=%s />",
KeyboardBuilder.TAG_KEY_STYLE, styleName));
if (mStyles.containsKey(styleName)) {
Log.d(TAG, KeyboardBuilder.TAG_KEY_STYLE + " " + styleName + " is overridden at "
+ parser.getPositionDescription());
}
}
final String parentStyleInAttr = keyStyleAttr.getString(
R.styleable.Keyboard_KeyStyle_parentStyle);
if (parentStyleInAttr != null && !mStyles.containsKey(parentStyleInAttr)) {
throw new XmlParseUtils.ParseException(
"Unknown parentStyle " + parentStyleInAttr, parser);
}
final String parentStyleName = (parentStyleInAttr == null) ? EMPTY_STYLE_NAME
: parentStyleInAttr;
final DeclaredKeyStyle style = new DeclaredKeyStyle(parentStyleName, mTextsSet, mStyles);
style.readKeyAttributes(keyAttrs);
mStyles.put(styleName, style);
}
public KeyStyle getKeyStyle(final TypedArray keyAttr, final XmlPullParser parser)
throws XmlParseUtils.ParseException {
final String styleName = keyAttr.getString(R.styleable.Keyboard_Key_keyStyle);
if (styleName == null) {
return mEmptyKeyStyle;
}
final KeyStyle style = mStyles.get(styleName);
if (style == null) {
throw new XmlParseUtils.ParseException("Unknown key style: " + styleName, parser);
}
return style;
}
}

View File

@ -0,0 +1,143 @@
/*
* Copyright (C) 2012 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.colorful.keyboard.theme.keyboard.internal;
import android.content.res.TypedArray;
import android.graphics.Typeface;
import android.util.SparseIntArray;
import com.colorful.keyboard.theme.R;
import com.colorful.keyboard.theme.latin.utils.ResourceUtils;
public final class KeyVisualAttributes {
public final Typeface mTypeface;
public final float mLetterRatio;
public final int mLetterSize;
public final float mLabelRatio;
public final int mLabelSize;
public final float mLargeLetterRatio;
public final float mHintLetterRatio;
public final float mShiftedLetterHintRatio;
public final float mHintLabelRatio;
public final float mPreviewTextRatio;
public final int mTextColor;
public final int mTextInactivatedColor;
public final int mTextShadowColor;
public final int mFunctionalTextColor;
public final int mHintLetterColor;
public final int mHintLabelColor;
public final int mShiftedLetterHintInactivatedColor;
public final int mShiftedLetterHintActivatedColor;
public final int mPreviewTextColor;
public final float mHintLabelVerticalAdjustment;
public final float mLabelOffCenterRatio;
public final float mHintLabelOffCenterRatio;
private static final int[] VISUAL_ATTRIBUTE_IDS = {
R.styleable.Keyboard_Key_keyTypeface,
R.styleable.Keyboard_Key_keyLetterSize,
R.styleable.Keyboard_Key_keyLabelSize,
R.styleable.Keyboard_Key_keyLargeLetterRatio,
R.styleable.Keyboard_Key_keyHintLetterRatio,
R.styleable.Keyboard_Key_keyShiftedLetterHintRatio,
R.styleable.Keyboard_Key_keyHintLabelRatio,
R.styleable.Keyboard_Key_keyPreviewTextRatio,
R.styleable.Keyboard_Key_keyTextColor,
R.styleable.Keyboard_Key_keyTextInactivatedColor,
R.styleable.Keyboard_Key_keyTextShadowColor,
R.styleable.Keyboard_Key_functionalTextColor,
R.styleable.Keyboard_Key_keyHintLetterColor,
R.styleable.Keyboard_Key_keyHintLabelColor,
R.styleable.Keyboard_Key_keyShiftedLetterHintInactivatedColor,
R.styleable.Keyboard_Key_keyShiftedLetterHintActivatedColor,
R.styleable.Keyboard_Key_keyPreviewTextColor,
R.styleable.Keyboard_Key_keyHintLabelVerticalAdjustment,
R.styleable.Keyboard_Key_keyLabelOffCenterRatio,
R.styleable.Keyboard_Key_keyHintLabelOffCenterRatio
};
private static final SparseIntArray sVisualAttributeIds = new SparseIntArray();
private static final int ATTR_DEFINED = 1;
private static final int ATTR_NOT_FOUND = 0;
static {
for (final int attrId : VISUAL_ATTRIBUTE_IDS) {
sVisualAttributeIds.put(attrId, ATTR_DEFINED);
}
}
public static KeyVisualAttributes newInstance(final TypedArray keyAttr) {
final int indexCount = keyAttr.getIndexCount();
for (int i = 0; i < indexCount; i++) {
final int attrId = keyAttr.getIndex(i);
if (sVisualAttributeIds.get(attrId, ATTR_NOT_FOUND) == ATTR_NOT_FOUND) {
continue;
}
return new KeyVisualAttributes(keyAttr);
}
return null;
}
private KeyVisualAttributes(final TypedArray keyAttr) {
if (keyAttr.hasValue(R.styleable.Keyboard_Key_keyTypeface)) {
mTypeface = Typeface.defaultFromStyle(
keyAttr.getInt(R.styleable.Keyboard_Key_keyTypeface, Typeface.NORMAL));
} else {
mTypeface = null;
}
mLetterRatio = ResourceUtils.getFraction(keyAttr,
R.styleable.Keyboard_Key_keyLetterSize);
mLetterSize = ResourceUtils.getDimensionPixelSize(keyAttr,
R.styleable.Keyboard_Key_keyLetterSize);
mLabelRatio = ResourceUtils.getFraction(keyAttr,
R.styleable.Keyboard_Key_keyLabelSize);
mLabelSize = ResourceUtils.getDimensionPixelSize(keyAttr,
R.styleable.Keyboard_Key_keyLabelSize);
mLargeLetterRatio = ResourceUtils.getFraction(keyAttr,
R.styleable.Keyboard_Key_keyLargeLetterRatio);
mHintLetterRatio = ResourceUtils.getFraction(keyAttr,
R.styleable.Keyboard_Key_keyHintLetterRatio);
mShiftedLetterHintRatio = ResourceUtils.getFraction(keyAttr,
R.styleable.Keyboard_Key_keyShiftedLetterHintRatio);
mHintLabelRatio = ResourceUtils.getFraction(keyAttr,
R.styleable.Keyboard_Key_keyHintLabelRatio);
mPreviewTextRatio = ResourceUtils.getFraction(keyAttr,
R.styleable.Keyboard_Key_keyPreviewTextRatio);
mTextColor = keyAttr.getColor(R.styleable.Keyboard_Key_keyTextColor, 0);
mTextInactivatedColor = keyAttr.getColor(
R.styleable.Keyboard_Key_keyTextInactivatedColor, 0);
mTextShadowColor = keyAttr.getColor(R.styleable.Keyboard_Key_keyTextShadowColor, 0);
mFunctionalTextColor = keyAttr.getColor(R.styleable.Keyboard_Key_functionalTextColor, 0);
mHintLetterColor = keyAttr.getColor(R.styleable.Keyboard_Key_keyHintLetterColor, 0);
mHintLabelColor = keyAttr.getColor(R.styleable.Keyboard_Key_keyHintLabelColor, 0);
mShiftedLetterHintInactivatedColor = keyAttr.getColor(
R.styleable.Keyboard_Key_keyShiftedLetterHintInactivatedColor, 0);
mShiftedLetterHintActivatedColor = keyAttr.getColor(
R.styleable.Keyboard_Key_keyShiftedLetterHintActivatedColor, 0);
mPreviewTextColor = keyAttr.getColor(R.styleable.Keyboard_Key_keyPreviewTextColor, 0);
mHintLabelVerticalAdjustment = ResourceUtils.getFraction(keyAttr,
R.styleable.Keyboard_Key_keyHintLabelVerticalAdjustment, 0.0f);
mLabelOffCenterRatio = ResourceUtils.getFraction(keyAttr,
R.styleable.Keyboard_Key_keyLabelOffCenterRatio, 0.0f);
mHintLabelOffCenterRatio = ResourceUtils.getFraction(keyAttr,
R.styleable.Keyboard_Key_keyHintLabelOffCenterRatio, 0.0f);
}
}

View File

@ -0,0 +1,762 @@
/*
* Copyright (C) 2012 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.colorful.keyboard.theme.keyboard.internal;
import android.content.Context;
import android.content.res.Resources;
import android.content.res.TypedArray;
import android.content.res.XmlResourceParser;
import android.text.TextUtils;
import android.util.AttributeSet;
import android.util.Log;
import android.util.TypedValue;
import android.util.Xml;
import com.colorful.keyboard.theme.R;
import com.colorful.keyboard.theme.keyboard.Key;
import com.colorful.keyboard.theme.keyboard.Keyboard;
import com.colorful.keyboard.theme.keyboard.KeyboardId;
import com.colorful.keyboard.theme.keyboard.KeyboardTheme;
import com.colorful.keyboard.theme.latin.common.StringUtils;
import com.colorful.keyboard.theme.latin.utils.ResourceUtils;
import com.colorful.keyboard.theme.latin.utils.XmlParseUtils;
import org.xmlpull.v1.XmlPullParser;
import org.xmlpull.v1.XmlPullParserException;
import java.io.IOException;
import java.util.Arrays;
import java.util.Locale;
/**
* Keyboard Building helper.
*
* This class parses Keyboard XML file and eventually build a Keyboard.
* The Keyboard XML file looks like:
* <pre>
* &lt;!-- xml/keyboard.xml --&gt;
* &lt;Keyboard keyboard_attributes*&gt;
* &lt;!-- Keyboard Content --&gt;
* &lt;Row row_attributes*&gt;
* &lt;!-- Row Content --&gt;
* &lt;Key key_attributes* /&gt;
* &lt;Spacer horizontalGap="32.0dp" /&gt;
* &lt;include keyboardLayout="@xml/other_keys"&gt;
* ...
* &lt;/Row&gt;
* &lt;include keyboardLayout="@xml/other_rows"&gt;
* ...
* &lt;/Keyboard&gt;
* </pre>
* The XML file which is included in other file must have &lt;merge&gt; as root element,
* such as:
* <pre>
* &lt;!-- xml/other_keys.xml --&gt;
* &lt;merge&gt;
* &lt;Key key_attributes* /&gt;
* ...
* &lt;/merge&gt;
* </pre>
* and
* <pre>
* &lt;!-- xml/other_rows.xml --&gt;
* &lt;merge&gt;
* &lt;Row row_attributes*&gt;
* &lt;Key key_attributes* /&gt;
* &lt;/Row&gt;
* ...
* &lt;/merge&gt;
* </pre>
* You can also use switch-case-default tags to select Rows and Keys.
* <pre>
* &lt;switch&gt;
* &lt;case case_attribute*&gt;
* &lt;!-- Any valid tags at switch position --&gt;
* &lt;/case&gt;
* ...
* &lt;default&gt;
* &lt;!-- Any valid tags at switch position --&gt;
* &lt;/default&gt;
* &lt;/switch&gt;
* </pre>
* You can declare Key style and specify styles within Key tags.
* <pre>
* &lt;switch&gt;
* &lt;case mode="email"&gt;
* &lt;key-style styleName="f1-key" parentStyle="modifier-key"
* keyLabel=".com"
* /&gt;
* &lt;/case&gt;
* &lt;case mode="url"&gt;
* &lt;key-style styleName="f1-key" parentStyle="modifier-key"
* keyLabel="http://"
* /&gt;
* &lt;/case&gt;
* &lt;/switch&gt;
* ...
* &lt;Key keyStyle="shift-key" ... /&gt;
* </pre>
*/
// TODO: Write unit tests for this class.
public class KeyboardBuilder<KP extends KeyboardParams> {
private static final String BUILDER_TAG = "Keyboard.Builder";
private static final boolean DEBUG = false;
// Keyboard XML Tags
private static final String TAG_KEYBOARD = "Keyboard";
private static final String TAG_ROW = "Row";
private static final String TAG_KEY = "Key";
private static final String TAG_SPACER = "Spacer";
private static final String TAG_INCLUDE = "include";
private static final String TAG_MERGE = "merge";
private static final String TAG_SWITCH = "switch";
private static final String TAG_CASE = "case";
private static final String TAG_DEFAULT = "default";
public static final String TAG_KEY_STYLE = "key-style";
private static final int DEFAULT_KEYBOARD_COLUMNS = 10;
private static final int DEFAULT_KEYBOARD_ROWS = 4;
protected final KP mParams;
protected final Context mContext;
protected final Resources mResources;
private float mCurrentY = 0;
private KeyboardRow mCurrentRow = null;
private Key mPreviousKeyInRow = null;
private boolean mKeyboardDefined = false;
public KeyboardBuilder(final Context context, final KP params) {
mContext = context;
final Resources res = context.getResources();
mResources = res;
mParams = params;
params.mGridWidth = res.getInteger(R.integer.config_keyboard_grid_width);
params.mGridHeight = res.getInteger(R.integer.config_keyboard_grid_height);
}
public void setAllowRedundantMoreKes(final boolean enabled) {
mParams.mAllowRedundantMoreKeys = enabled;
}
public KeyboardBuilder<KP> load(final int xmlId, final KeyboardId id) {
mParams.mId = id;
final XmlResourceParser parser = mResources.getXml(xmlId);
try {
parseKeyboard(parser, false);
if (!mKeyboardDefined) {
throw new XmlParseUtils.ParseException("No " + TAG_KEYBOARD + " tag was found");
}
} catch (XmlPullParserException e) {
Log.w(BUILDER_TAG, "keyboard XML parse error", e);
throw new IllegalArgumentException(e.getMessage(), e);
} catch (IOException e) {
Log.w(BUILDER_TAG, "keyboard XML parse error", e);
throw new RuntimeException(e.getMessage(), e);
} finally {
parser.close();
}
return this;
}
public Keyboard build() {
return new Keyboard(mParams);
}
private int mIndent;
private static final String SPACES = " ";
private static String spaces(final int count) {
return (count < SPACES.length()) ? SPACES.substring(0, count) : SPACES;
}
private void startTag(final String format, final Object ... args) {
Log.d(BUILDER_TAG, String.format(spaces(++mIndent * 2) + format, args));
}
private void endTag(final String format, final Object ... args) {
Log.d(BUILDER_TAG, String.format(spaces(mIndent-- * 2) + format, args));
}
private void startEndTag(final String format, final Object ... args) {
Log.d(BUILDER_TAG, String.format(spaces(++mIndent * 2) + format, args));
mIndent--;
}
private void parseKeyboard(final XmlPullParser parser, final boolean skip)
throws XmlPullParserException, IOException {
while (parser.getEventType() != XmlPullParser.END_DOCUMENT) {
final int event = parser.next();
if (event == XmlPullParser.START_TAG) {
final String tag = parser.getName();
if (TAG_KEYBOARD.equals(tag)) {
if (DEBUG) startTag("<%s> %s%s", TAG_KEYBOARD, mParams.mId,
skip ? " skipped" : "");
if (!skip) {
if (mKeyboardDefined) {
throw new XmlParseUtils.ParseException("Only one " + TAG_KEYBOARD
+ " tag can be defined", parser);
}
mKeyboardDefined = true;
parseKeyboardAttributes(parser);
startKeyboard();
}
parseKeyboardContent(parser, skip);
} else if (TAG_SWITCH.equals(tag)) {
parseSwitchKeyboard(parser, skip);
} else {
throw new XmlParseUtils.IllegalStartTag(parser, tag, TAG_KEYBOARD);
}
} else if (event == XmlPullParser.END_TAG) {
final String tag = parser.getName();
if (DEBUG) endTag("</%s>", tag);
if (TAG_CASE.equals(tag) || TAG_DEFAULT.equals(tag)) {
return;
}
throw new XmlParseUtils.IllegalEndTag(parser, tag, TAG_ROW);
}
}
}
private void parseKeyboardAttributes(final XmlPullParser parser) {
final AttributeSet attr = Xml.asAttributeSet(parser);
final TypedArray keyboardAttr = mContext.obtainStyledAttributes(
attr, R.styleable.Keyboard, R.attr.keyboardStyle, R.style.Keyboard);
final TypedArray keyAttr = mResources.obtainAttributes(attr, R.styleable.Keyboard_Key);
try {
final KeyboardParams params = mParams;
final int height = params.mId.mHeight;
final int width = params.mId.mWidth;
// The bonus height isn't used to determine the other dimensions (gap/padding) to allow
// those to stay consistent between layouts with and without the bonus height added.
final int bonusHeight = (int)keyboardAttr.getFraction(R.styleable.Keyboard_bonusHeight,
height, height, 0);
params.mOccupiedHeight = height + bonusHeight;
params.mOccupiedWidth = width;
params.mTopPadding = ResourceUtils.getDimensionOrFraction(keyboardAttr,
R.styleable.Keyboard_keyboardTopPadding, height, 0);
params.mBottomPadding = ResourceUtils.getDimensionOrFraction(keyboardAttr,
R.styleable.Keyboard_keyboardBottomPadding, height, 0);
params.mLeftPadding = ResourceUtils.getDimensionOrFraction(keyboardAttr,
R.styleable.Keyboard_keyboardLeftPadding, width, 0);
params.mRightPadding = ResourceUtils.getDimensionOrFraction(keyboardAttr,
R.styleable.Keyboard_keyboardRightPadding, width, 0);
params.mHorizontalGap = keyboardAttr.getFraction(
R.styleable.Keyboard_horizontalGap, width, width, 0);
final float baseWidth = params.mOccupiedWidth - params.mLeftPadding
- params.mRightPadding + params.mHorizontalGap;
params.mBaseWidth = baseWidth;
params.mDefaultKeyPaddedWidth = ResourceUtils.getFraction(keyAttr,
R.styleable.Keyboard_Key_keyWidth, baseWidth,
baseWidth / DEFAULT_KEYBOARD_COLUMNS);
// TODO: Fix keyboard geometry calculation clearer. Historically vertical gap between
// rows are determined based on the entire keyboard height including top and bottom
// paddings.
params.mVerticalGap = keyboardAttr.getFraction(
R.styleable.Keyboard_verticalGap, height, height, 0);
final float baseHeight = params.mOccupiedHeight - params.mTopPadding
- params.mBottomPadding + params.mVerticalGap;
params.mBaseHeight = baseHeight;
params.mDefaultRowHeight = ResourceUtils.getDimensionOrFraction(keyboardAttr,
R.styleable.Keyboard_rowHeight, baseHeight, baseHeight / DEFAULT_KEYBOARD_ROWS);
params.mKeyVisualAttributes = KeyVisualAttributes.newInstance(keyAttr);
params.mMoreKeysTemplate = keyboardAttr.getResourceId(
R.styleable.Keyboard_moreKeysTemplate, 0);
params.mMaxMoreKeysKeyboardColumn = keyAttr.getInt(
R.styleable.Keyboard_Key_maxMoreKeysColumn, 5);
params.mIconsSet.loadIcons(keyboardAttr);
params.mTextsSet.setLocale(params.mId.getLocale(), mContext);
} finally {
keyAttr.recycle();
keyboardAttr.recycle();
}
}
private void parseKeyboardContent(final XmlPullParser parser, final boolean skip)
throws XmlPullParserException, IOException {
while (parser.getEventType() != XmlPullParser.END_DOCUMENT) {
final int event = parser.next();
if (event == XmlPullParser.START_TAG) {
final String tag = parser.getName();
if (TAG_ROW.equals(tag)) {
final KeyboardRow row = parseRowAttributes(parser);
if (DEBUG) startTag("<%s>%s", TAG_ROW, skip ? " skipped" : "");
if (!skip) {
startRow(row);
}
parseRowContent(parser, row, skip);
} else if (TAG_INCLUDE.equals(tag)) {
parseIncludeKeyboardContent(parser, skip);
} else if (TAG_SWITCH.equals(tag)) {
parseSwitchKeyboardContent(parser, skip);
} else if (TAG_KEY_STYLE.equals(tag)) {
parseKeyStyle(parser, skip);
} else {
throw new XmlParseUtils.IllegalStartTag(parser, tag, TAG_ROW);
}
} else if (event == XmlPullParser.END_TAG) {
final String tag = parser.getName();
if (DEBUG) endTag("</%s>", tag);
if (TAG_KEYBOARD.equals(tag)) {
endKeyboard();
return;
}
if (TAG_CASE.equals(tag) || TAG_DEFAULT.equals(tag) || TAG_MERGE.equals(tag)) {
return;
}
throw new XmlParseUtils.IllegalEndTag(parser, tag, TAG_ROW);
}
}
}
private KeyboardRow parseRowAttributes(final XmlPullParser parser)
throws XmlPullParserException {
final AttributeSet attr = Xml.asAttributeSet(parser);
final TypedArray keyboardAttr = mResources.obtainAttributes(attr, R.styleable.Keyboard);
try {
if (keyboardAttr.hasValue(R.styleable.Keyboard_horizontalGap)) {
throw new XmlParseUtils.IllegalAttribute(parser, TAG_ROW, "horizontalGap");
}
if (keyboardAttr.hasValue(R.styleable.Keyboard_verticalGap)) {
throw new XmlParseUtils.IllegalAttribute(parser, TAG_ROW, "verticalGap");
}
return new KeyboardRow(mResources, mParams, parser, mCurrentY);
} finally {
keyboardAttr.recycle();
}
}
private void parseRowContent(final XmlPullParser parser, final KeyboardRow row,
final boolean skip) throws XmlPullParserException, IOException {
while (parser.getEventType() != XmlPullParser.END_DOCUMENT) {
final int event = parser.next();
if (event == XmlPullParser.START_TAG) {
final String tag = parser.getName();
if (TAG_KEY.equals(tag)) {
parseKey(parser, row, skip);
} else if (TAG_SPACER.equals(tag)) {
parseSpacer(parser, row, skip);
} else if (TAG_INCLUDE.equals(tag)) {
parseIncludeRowContent(parser, row, skip);
} else if (TAG_SWITCH.equals(tag)) {
parseSwitchRowContent(parser, row, skip);
} else if (TAG_KEY_STYLE.equals(tag)) {
parseKeyStyle(parser, skip);
} else {
throw new XmlParseUtils.IllegalStartTag(parser, tag, TAG_ROW);
}
} else if (event == XmlPullParser.END_TAG) {
final String tag = parser.getName();
if (DEBUG) endTag("</%s>", tag);
if (TAG_ROW.equals(tag)) {
if (!skip) {
endRow(row);
}
return;
}
if (TAG_CASE.equals(tag) || TAG_DEFAULT.equals(tag) || TAG_MERGE.equals(tag)) {
return;
}
throw new XmlParseUtils.IllegalEndTag(parser, tag, TAG_ROW);
}
}
}
private void parseKey(final XmlPullParser parser, final KeyboardRow row, final boolean skip)
throws XmlPullParserException, IOException {
if (skip) {
XmlParseUtils.checkEndTag(TAG_KEY, parser);
if (DEBUG) startEndTag("<%s /> skipped", TAG_KEY);
return;
}
final TypedArray keyAttr = mResources.obtainAttributes(
Xml.asAttributeSet(parser), R.styleable.Keyboard_Key);
final KeyStyle keyStyle = mParams.mKeyStyles.getKeyStyle(keyAttr, parser);
final String keySpec = keyStyle.getString(keyAttr, R.styleable.Keyboard_Key_keySpec);
if (TextUtils.isEmpty(keySpec)) {
throw new XmlParseUtils.ParseException("Empty keySpec", parser);
}
final Key key = new Key(keySpec, keyAttr, keyStyle, mParams, row);
keyAttr.recycle();
if (DEBUG) {
startEndTag("<%s %s moreKeys=%s />", TAG_KEY, key, Arrays.toString(key.getMoreKeys()));
}
XmlParseUtils.checkEndTag(TAG_KEY, parser);
endKey(key, row);
}
private void parseSpacer(final XmlPullParser parser, final KeyboardRow row, final boolean skip)
throws XmlPullParserException, IOException {
if (skip) {
XmlParseUtils.checkEndTag(TAG_SPACER, parser);
if (DEBUG) startEndTag("<%s /> skipped", TAG_SPACER);
return;
}
final TypedArray keyAttr = mResources.obtainAttributes(
Xml.asAttributeSet(parser), R.styleable.Keyboard_Key);
final KeyStyle keyStyle = mParams.mKeyStyles.getKeyStyle(keyAttr, parser);
final Key spacer = new Key.Spacer(keyAttr, keyStyle, mParams, row);
keyAttr.recycle();
if (DEBUG) startEndTag("<%s />", TAG_SPACER);
XmlParseUtils.checkEndTag(TAG_SPACER, parser);
endKey(spacer, row);
}
private void parseIncludeKeyboardContent(final XmlPullParser parser, final boolean skip)
throws XmlPullParserException, IOException {
parseIncludeInternal(parser, null, skip);
}
private void parseIncludeRowContent(final XmlPullParser parser, final KeyboardRow row,
final boolean skip) throws XmlPullParserException, IOException {
parseIncludeInternal(parser, row, skip);
}
private void parseIncludeInternal(final XmlPullParser parser, final KeyboardRow row,
final boolean skip) throws XmlPullParserException, IOException {
if (skip) {
XmlParseUtils.checkEndTag(TAG_INCLUDE, parser);
if (DEBUG) startEndTag("</%s> skipped", TAG_INCLUDE);
return;
}
final AttributeSet attr = Xml.asAttributeSet(parser);
final TypedArray keyboardAttr = mResources.obtainAttributes(
attr, R.styleable.Keyboard_Include);
final TypedArray includeAttr = mResources.obtainAttributes(
attr, R.styleable.Keyboard);
mParams.mDefaultRowHeight = ResourceUtils.getDimensionOrFraction(includeAttr,
R.styleable.Keyboard_rowHeight, mParams.mBaseHeight, mParams.mDefaultRowHeight);
final TypedArray keyAttr = mResources.obtainAttributes(attr, R.styleable.Keyboard_Key);
int keyboardLayout = 0;
try {
XmlParseUtils.checkAttributeExists(
keyboardAttr, R.styleable.Keyboard_Include_keyboardLayout, "keyboardLayout",
TAG_INCLUDE, parser);
keyboardLayout = keyboardAttr.getResourceId(
R.styleable.Keyboard_Include_keyboardLayout, 0);
if (row != null) {
// Override current x coordinate.
row.updateXPos(keyAttr);
// Push current Row attributes and update with new attributes.
row.pushRowAttributes(keyAttr);
}
} finally {
keyboardAttr.recycle();
keyAttr.recycle();
includeAttr.recycle();
}
XmlParseUtils.checkEndTag(TAG_INCLUDE, parser);
if (DEBUG) {
startEndTag("<%s keyboardLayout=%s />",TAG_INCLUDE,
mResources.getResourceEntryName(keyboardLayout));
}
final XmlResourceParser parserForInclude = mResources.getXml(keyboardLayout);
try {
parseMerge(parserForInclude, row, skip);
} finally {
if (row != null) {
// Restore Row attributes.
row.popRowAttributes();
}
parserForInclude.close();
}
}
private void parseMerge(final XmlPullParser parser, final KeyboardRow row, final boolean skip)
throws XmlPullParserException, IOException {
if (DEBUG) startTag("<%s>", TAG_MERGE);
while (parser.getEventType() != XmlPullParser.END_DOCUMENT) {
final int event = parser.next();
if (event == XmlPullParser.START_TAG) {
final String tag = parser.getName();
if (TAG_MERGE.equals(tag)) {
if (row == null) {
parseKeyboardContent(parser, skip);
} else {
parseRowContent(parser, row, skip);
}
return;
}
throw new XmlParseUtils.ParseException(
"Included keyboard layout must have <merge> root element", parser);
}
}
}
private void parseSwitchKeyboard(final XmlPullParser parser, final boolean skip)
throws XmlPullParserException, IOException {
parseSwitchInternal(parser, true, null, skip);
}
private void parseSwitchKeyboardContent(final XmlPullParser parser, final boolean skip)
throws XmlPullParserException, IOException {
parseSwitchInternal(parser, false, null, skip);
}
private void parseSwitchRowContent(final XmlPullParser parser, final KeyboardRow row,
final boolean skip) throws XmlPullParserException, IOException {
parseSwitchInternal(parser, false, row, skip);
}
private void parseSwitchInternal(final XmlPullParser parser, final boolean parseKeyboard,
final KeyboardRow row, final boolean skip) throws XmlPullParserException, IOException {
if (DEBUG) startTag("<%s> %s", TAG_SWITCH, mParams.mId);
boolean selected = false;
while (parser.getEventType() != XmlPullParser.END_DOCUMENT) {
final int event = parser.next();
if (event == XmlPullParser.START_TAG) {
final String tag = parser.getName();
if (TAG_CASE.equals(tag)) {
selected |= parseCase(parser, parseKeyboard, row, selected || skip);
} else if (TAG_DEFAULT.equals(tag)) {
selected |= parseDefault(parser, parseKeyboard, row, selected || skip);
} else {
throw new XmlParseUtils.IllegalStartTag(parser, tag, TAG_SWITCH);
}
} else if (event == XmlPullParser.END_TAG) {
final String tag = parser.getName();
if (TAG_SWITCH.equals(tag)) {
if (DEBUG) endTag("</%s>", TAG_SWITCH);
return;
}
throw new XmlParseUtils.IllegalEndTag(parser, tag, TAG_SWITCH);
}
}
}
private boolean parseCase(final XmlPullParser parser, final boolean parseKeyboard,
final KeyboardRow row, final boolean skip) throws XmlPullParserException, IOException {
final boolean selected = parseCaseCondition(parser);
if (parseKeyboard) {
// Processing Keyboard root.
parseKeyboard(parser, !selected || skip);
} else if (row == null) {
// Processing Rows.
parseKeyboardContent(parser, !selected || skip);
} else {
// Processing Keys.
parseRowContent(parser, row, !selected || skip);
}
return selected;
}
private boolean parseCaseCondition(final XmlPullParser parser) {
final KeyboardId id = mParams.mId;
if (id == null) {
return true;
}
final AttributeSet attr = Xml.asAttributeSet(parser);
final TypedArray caseAttr = mResources.obtainAttributes(attr, R.styleable.Keyboard_Case);
if (DEBUG) startTag("<%s>", TAG_CASE);
try {
final boolean keyboardLayoutSetMatched = matchString(caseAttr,
R.styleable.Keyboard_Case_keyboardLayoutSet,
id.mSubtype.getKeyboardLayoutSet());
final boolean keyboardLayoutSetElementMatched = matchTypedValue(caseAttr,
R.styleable.Keyboard_Case_keyboardLayoutSetElement, id.mElementId,
KeyboardId.elementIdToName(id.mElementId));
final boolean keyboardThemeMatched = matchTypedValue(caseAttr,
R.styleable.Keyboard_Case_keyboardTheme, id.mThemeId,
KeyboardTheme.getKeyboardThemeName(id.mThemeId));
final boolean modeMatched = matchTypedValue(caseAttr,
R.styleable.Keyboard_Case_mode, id.mMode, KeyboardId.modeName(id.mMode));
final boolean navigateNextMatched = matchBoolean(caseAttr,
R.styleable.Keyboard_Case_navigateNext, id.navigateNext());
final boolean navigatePreviousMatched = matchBoolean(caseAttr,
R.styleable.Keyboard_Case_navigatePrevious, id.navigatePrevious());
final boolean passwordInputMatched = matchBoolean(caseAttr,
R.styleable.Keyboard_Case_passwordInput, id.passwordInput());
final boolean languageSwitchKeyEnabledMatched = matchBoolean(caseAttr,
R.styleable.Keyboard_Case_languageSwitchKeyEnabled,
id.mLanguageSwitchKeyEnabled);
final boolean isMultiLineMatched = matchBoolean(caseAttr,
R.styleable.Keyboard_Case_isMultiLine, id.isMultiLine());
final boolean imeActionMatched = matchInteger(caseAttr,
R.styleable.Keyboard_Case_imeAction, id.imeAction());
final boolean isIconDefinedMatched = isIconDefined(caseAttr,
R.styleable.Keyboard_Case_isIconDefined, mParams.mIconsSet);
final Locale locale = id.getLocale();
final boolean localeCodeMatched = matchLocaleCodes(caseAttr, locale);
final boolean languageCodeMatched = matchLanguageCodes(caseAttr, locale);
final boolean countryCodeMatched = matchCountryCodes(caseAttr, locale);
final boolean showMoreKeysMatched = matchBoolean(caseAttr,
R.styleable.Keyboard_Case_showExtraChars, id.mShowMoreKeys);
final boolean showNumberRowMatched = matchBoolean(caseAttr,
R.styleable.Keyboard_Case_showNumberRow, id.mShowNumberRow);
final boolean selected = keyboardLayoutSetMatched && keyboardLayoutSetElementMatched
&& keyboardThemeMatched && modeMatched && navigateNextMatched
&& navigatePreviousMatched && passwordInputMatched
&& languageSwitchKeyEnabledMatched
&& isMultiLineMatched && imeActionMatched && isIconDefinedMatched
&& localeCodeMatched && languageCodeMatched && countryCodeMatched
&& showMoreKeysMatched && showNumberRowMatched;
return selected;
} finally {
caseAttr.recycle();
}
}
private static boolean matchLocaleCodes(TypedArray caseAttr, final Locale locale) {
return matchString(caseAttr, R.styleable.Keyboard_Case_localeCode, locale.toString());
}
private static boolean matchLanguageCodes(TypedArray caseAttr, Locale locale) {
return matchString(caseAttr, R.styleable.Keyboard_Case_languageCode, locale.getLanguage());
}
private static boolean matchCountryCodes(TypedArray caseAttr, Locale locale) {
return matchString(caseAttr, R.styleable.Keyboard_Case_countryCode, locale.getCountry());
}
private static boolean matchInteger(final TypedArray a, final int index, final int value) {
// If <case> does not have "index" attribute, that means this <case> is wild-card for
// the attribute.
return !a.hasValue(index) || a.getInt(index, 0) == value;
}
private static boolean matchBoolean(final TypedArray a, final int index, final boolean value) {
// If <case> does not have "index" attribute, that means this <case> is wild-card for
// the attribute.
return !a.hasValue(index) || a.getBoolean(index, false) == value;
}
private static boolean matchString(final TypedArray a, final int index, final String value) {
// If <case> does not have "index" attribute, that means this <case> is wild-card for
// the attribute.
return !a.hasValue(index)
|| StringUtils.containsInArray(value, a.getString(index).split("\\|"));
}
private static boolean matchTypedValue(final TypedArray a, final int index, final int intValue,
final String strValue) {
// If <case> does not have "index" attribute, that means this <case> is wild-card for
// the attribute.
final TypedValue v = a.peekValue(index);
if (v == null) {
return true;
}
if (ResourceUtils.isIntegerValue(v)) {
return intValue == a.getInt(index, 0);
}
if (ResourceUtils.isStringValue(v)) {
return StringUtils.containsInArray(strValue, a.getString(index).split("\\|"));
}
return false;
}
private static boolean isIconDefined(final TypedArray a, final int index,
final KeyboardIconsSet iconsSet) {
if (!a.hasValue(index)) {
return true;
}
final String iconName = a.getString(index);
final int iconId = KeyboardIconsSet.getIconId(iconName);
return iconsSet.getIconDrawable(iconId) != null;
}
private boolean parseDefault(final XmlPullParser parser, final boolean parseKeyboard,
final KeyboardRow row, final boolean skip) throws XmlPullParserException, IOException {
if (DEBUG) startTag("<%s>", TAG_DEFAULT);
if (parseKeyboard) {
parseKeyboard(parser, skip);
} else if (row == null) {
parseKeyboardContent(parser, skip);
} else {
parseRowContent(parser, row, skip);
}
return true;
}
private void parseKeyStyle(final XmlPullParser parser, final boolean skip)
throws XmlPullParserException, IOException {
final AttributeSet attr = Xml.asAttributeSet(parser);
final TypedArray keyStyleAttr = mResources.obtainAttributes(
attr, R.styleable.Keyboard_KeyStyle);
final TypedArray keyAttrs = mResources.obtainAttributes(attr, R.styleable.Keyboard_Key);
try {
if (!keyStyleAttr.hasValue(R.styleable.Keyboard_KeyStyle_styleName)) {
throw new XmlParseUtils.ParseException("<" + TAG_KEY_STYLE
+ "/> needs styleName attribute", parser);
}
if (DEBUG) {
startEndTag("<%s styleName=%s />%s", TAG_KEY_STYLE,
keyStyleAttr.getString(R.styleable.Keyboard_KeyStyle_styleName),
skip ? " skipped" : "");
}
if (!skip) {
mParams.mKeyStyles.parseKeyStyleAttributes(keyStyleAttr, keyAttrs, parser);
}
} finally {
keyStyleAttr.recycle();
keyAttrs.recycle();
}
XmlParseUtils.checkEndTag(TAG_KEY_STYLE, parser);
}
private void startKeyboard() {
}
private void startRow(final KeyboardRow row) {
mCurrentRow = row;
mPreviousKeyInRow = null;
}
private void endRow(final KeyboardRow row) {
if (mCurrentRow == null) {
throw new RuntimeException("orphan end row tag");
}
if (mPreviousKeyInRow != null && !mPreviousKeyInRow.isSpacer()) {
setKeyHitboxRightEdge(mPreviousKeyInRow, mParams.mOccupiedWidth);
mPreviousKeyInRow = null;
}
mCurrentY += row.getRowHeight();
mCurrentRow = null;
}
private void endKey(final Key key, final KeyboardRow row) {
mParams.onAddKey(key);
if (mPreviousKeyInRow != null && !mPreviousKeyInRow.isSpacer()) {
// Make the last key span the gap so there isn't un-clickable space. The current key's
// hitbox left edge is based on the previous key, so this will make the gap between
// them split evenly.
setKeyHitboxRightEdge(mPreviousKeyInRow, row.getKeyX() - row.getKeyLeftPadding());
}
mPreviousKeyInRow = key;
}
private void setKeyHitboxRightEdge(final Key key, final float xPos) {
final int keyRight = key.getX() + key.getWidth();
final float padding = xPos - keyRight;
key.setHitboxRightEdge(Math.round(padding) + keyRight);
}
private void endKeyboard() {
mParams.removeRedundantMoreKeys();
}
}

View File

@ -0,0 +1,80 @@
/*
* Copyright (C) 2012 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.colorful.keyboard.theme.keyboard.internal;
import com.colorful.keyboard.theme.latin.common.Constants;
import java.util.HashMap;
public final class KeyboardCodesSet {
public static final String PREFIX_CODE = "!code/";
private static final HashMap<String, Integer> sNameToIdMap = new HashMap<>();
private KeyboardCodesSet() {
// This utility class is not publicly instantiable.
}
public static int getCode(final String name) {
Integer id = sNameToIdMap.get(name);
if (id == null) throw new RuntimeException("Unknown key code: " + name);
return DEFAULT[id];
}
private static final String[] ID_TO_NAME = {
"key_tab",
"key_enter",
"key_space",
"key_shift",
"key_capslock",
"key_switch_alpha_symbol",
"key_output_text",
"key_delete",
"key_settings",
"key_action_next",
"key_action_previous",
"key_shift_enter",
"key_language_switch",
"key_left",
"key_right",
"key_unspecified",
};
private static final int[] DEFAULT = {
Constants.CODE_TAB,
Constants.CODE_ENTER,
Constants.CODE_SPACE,
Constants.CODE_SHIFT,
Constants.CODE_CAPSLOCK,
Constants.CODE_SWITCH_ALPHA_SYMBOL,
Constants.CODE_OUTPUT_TEXT,
Constants.CODE_DELETE,
Constants.CODE_SETTINGS,
Constants.CODE_ACTION_NEXT,
Constants.CODE_ACTION_PREVIOUS,
Constants.CODE_SHIFT_ENTER,
Constants.CODE_LANGUAGE_SWITCH,
Constants.CODE_UNSPECIFIED,
};
static {
for (int i = 0; i < ID_TO_NAME.length; i++) {
sNameToIdMap.put(ID_TO_NAME[i], i);
}
}
}

View File

@ -0,0 +1,147 @@
/*
* Copyright (C) 2011 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.colorful.keyboard.theme.keyboard.internal;
import android.content.res.Resources;
import android.content.res.TypedArray;
import android.graphics.drawable.Drawable;
import android.util.Log;
import android.util.SparseIntArray;
import com.colorful.keyboard.theme.R;
import java.util.HashMap;
public final class KeyboardIconsSet {
private static final String TAG = KeyboardIconsSet.class.getSimpleName();
public static final String PREFIX_ICON = "!icon/";
public static final int ICON_UNDEFINED = 0;
private static final int ATTR_UNDEFINED = 0;
private static final String NAME_UNDEFINED = "undefined";
public static final String NAME_SHIFT_KEY = "shift_key";
public static final String NAME_SHIFT_KEY_SHIFTED = "shift_key_shifted";
public static final String NAME_DELETE_KEY = "delete_key";
public static final String NAME_SETTINGS_KEY = "settings_key";
public static final String NAME_SPACE_KEY = "space_key";
public static final String NAME_SPACE_KEY_FOR_NUMBER_LAYOUT = "space_key_for_number_layout";
public static final String NAME_ENTER_KEY = "enter_key";
public static final String NAME_GO_KEY = "go_key";
public static final String NAME_SEARCH_KEY = "search_key";
public static final String NAME_SEND_KEY = "send_key";
public static final String NAME_NEXT_KEY = "next_key";
public static final String NAME_DONE_KEY = "done_key";
public static final String NAME_PREVIOUS_KEY = "previous_key";
public static final String NAME_TAB_KEY = "tab_key";
public static final String NAME_LANGUAGE_SWITCH_KEY = "language_switch_key";
public static final String NAME_ZWNJ_KEY = "zwnj_key";
public static final String NAME_ZWJ_KEY = "zwj_key";
private static final SparseIntArray ATTR_ID_TO_ICON_ID = new SparseIntArray();
// Icon name to icon id map.
private static final HashMap<String, Integer> sNameToIdsMap = new HashMap<>();
private static final Object[] NAMES_AND_ATTR_IDS = {
NAME_UNDEFINED, ATTR_UNDEFINED,
NAME_SHIFT_KEY, R.styleable.Keyboard_iconShiftKey,
NAME_DELETE_KEY, R.styleable.Keyboard_iconDeleteKey,
NAME_SETTINGS_KEY, R.styleable.Keyboard_iconSettingsKey,
NAME_SPACE_KEY, R.styleable.Keyboard_iconSpaceKey,
NAME_ENTER_KEY, R.styleable.Keyboard_iconEnterKey,
NAME_GO_KEY, R.styleable.Keyboard_iconGoKey,
NAME_SEARCH_KEY, R.styleable.Keyboard_iconSearchKey,
NAME_SEND_KEY, R.styleable.Keyboard_iconSendKey,
NAME_NEXT_KEY, R.styleable.Keyboard_iconNextKey,
NAME_DONE_KEY, R.styleable.Keyboard_iconDoneKey,
NAME_PREVIOUS_KEY, R.styleable.Keyboard_iconPreviousKey,
NAME_TAB_KEY, R.styleable.Keyboard_iconTabKey,
NAME_SPACE_KEY_FOR_NUMBER_LAYOUT, R.styleable.Keyboard_iconSpaceKeyForNumberLayout,
NAME_SHIFT_KEY_SHIFTED, R.styleable.Keyboard_iconShiftKeyShifted,
NAME_LANGUAGE_SWITCH_KEY, R.styleable.Keyboard_iconLanguageSwitchKey,
NAME_ZWNJ_KEY, R.styleable.Keyboard_iconZwnjKey,
NAME_ZWJ_KEY, R.styleable.Keyboard_iconZwjKey,
};
private static int NUM_ICONS = NAMES_AND_ATTR_IDS.length / 2;
private static final String[] ICON_NAMES = new String[NUM_ICONS];
private final Drawable[] mIcons = new Drawable[NUM_ICONS];
private final int[] mIconResourceIds = new int[NUM_ICONS];
static {
int iconId = ICON_UNDEFINED;
for (int i = 0; i < NAMES_AND_ATTR_IDS.length; i += 2) {
final String name = (String)NAMES_AND_ATTR_IDS[i];
final Integer attrId = (Integer)NAMES_AND_ATTR_IDS[i + 1];
if (attrId != ATTR_UNDEFINED) {
ATTR_ID_TO_ICON_ID.put(attrId, iconId);
}
sNameToIdsMap.put(name, iconId);
ICON_NAMES[iconId] = name;
iconId++;
}
}
public void loadIcons(final TypedArray keyboardAttrs) {
final int size = ATTR_ID_TO_ICON_ID.size();
for (int index = 0; index < size; index++) {
final int attrId = ATTR_ID_TO_ICON_ID.keyAt(index);
try {
final Drawable icon = keyboardAttrs.getDrawable(attrId);
setDefaultBounds(icon);
final Integer iconId = ATTR_ID_TO_ICON_ID.get(attrId);
mIcons[iconId] = icon;
mIconResourceIds[iconId] = keyboardAttrs.getResourceId(attrId, 0);
} catch (Resources.NotFoundException e) {
Log.w(TAG, "Drawable resource for icon #"
+ keyboardAttrs.getResources().getResourceEntryName(attrId)
+ " not found");
}
}
}
private static boolean isValidIconId(final int iconId) {
return iconId >= 0 && iconId < ICON_NAMES.length;
}
public static String getIconName(final int iconId) {
return isValidIconId(iconId) ? ICON_NAMES[iconId] : "unknown<" + iconId + ">";
}
public static int getIconId(final String name) {
Integer iconId = sNameToIdsMap.get(name);
if (iconId != null) {
return iconId;
}
throw new RuntimeException("unknown icon name: " + name);
}
public Drawable getIconDrawable(final int iconId) {
if (isValidIconId(iconId)) {
return mIcons[iconId];
}
throw new RuntimeException("unknown icon id: " + getIconName(iconId));
}
private static void setDefaultBounds(final Drawable icon) {
if (icon != null) {
icon.setBounds(0, 0, icon.getIntrinsicWidth(), icon.getIntrinsicHeight());
}
}
}

View File

@ -0,0 +1,160 @@
/*
* Copyright (C) 2012 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.colorful.keyboard.theme.keyboard.internal;
import android.util.SparseIntArray;
import com.colorful.keyboard.theme.keyboard.Key;
import com.colorful.keyboard.theme.keyboard.KeyboardId;
import com.colorful.keyboard.theme.latin.common.Constants;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.SortedSet;
import java.util.TreeSet;
public class KeyboardParams {
public KeyboardId mId;
/** Total height and width of the keyboard, including the paddings and keys */
public int mOccupiedHeight;
public int mOccupiedWidth;
/** Base height and width of the keyboard used to calculate rows' or keys' heights and
* widths
*/
public float mBaseHeight;
public float mBaseWidth;
public float mTopPadding;
public float mBottomPadding;
public float mLeftPadding;
public float mRightPadding;
public KeyVisualAttributes mKeyVisualAttributes;
public float mDefaultRowHeight;
public float mDefaultKeyPaddedWidth;
public float mHorizontalGap;
public float mVerticalGap;
public int mMoreKeysTemplate;
public int mMaxMoreKeysKeyboardColumn;
public int mGridWidth;
public int mGridHeight;
// Keys are sorted from top-left to bottom-right order.
public final SortedSet<Key> mSortedKeys = new TreeSet<>(ROW_COLUMN_COMPARATOR);
public final ArrayList<Key> mShiftKeys = new ArrayList<>();
public final ArrayList<Key> mAltCodeKeysWhileTyping = new ArrayList<>();
public final KeyboardIconsSet mIconsSet = new KeyboardIconsSet();
public final KeyboardTextsSet mTextsSet = new KeyboardTextsSet();
public final KeyStylesSet mKeyStyles = new KeyStylesSet(mTextsSet);
private final UniqueKeysCache mUniqueKeysCache;
public boolean mAllowRedundantMoreKeys;
public int mMostCommonKeyHeight = 0;
public int mMostCommonKeyWidth = 0;
// Comparator to sort {@link Key}s from top-left to bottom-right order.
private static final Comparator<Key> ROW_COLUMN_COMPARATOR = new Comparator<Key>() {
@Override
public int compare(final Key lhs, final Key rhs) {
if (lhs.getY() < rhs.getY()) return -1;
if (lhs.getY() > rhs.getY()) return 1;
if (lhs.getX() < rhs.getX()) return -1;
if (lhs.getX() > rhs.getX()) return 1;
return 0;
}
};
public KeyboardParams() {
this(UniqueKeysCache.NO_CACHE);
}
public KeyboardParams(final UniqueKeysCache keysCache) {
mUniqueKeysCache = keysCache;
}
public void onAddKey(final Key newKey) {
final Key key = mUniqueKeysCache.getUniqueKey(newKey);
final boolean isSpacer = key.isSpacer();
if (isSpacer && key.getWidth() == 0) {
// Ignore zero width {@link Spacer}.
return;
}
mSortedKeys.add(key);
if (isSpacer) {
return;
}
updateHistogram(key);
if (key.getCode() == Constants.CODE_SHIFT) {
mShiftKeys.add(key);
}
if (key.altCodeWhileTyping()) {
mAltCodeKeysWhileTyping.add(key);
}
}
public void removeRedundantMoreKeys() {
if (mAllowRedundantMoreKeys) {
return;
}
final MoreKeySpec.LettersOnBaseLayout lettersOnBaseLayout =
new MoreKeySpec.LettersOnBaseLayout();
for (final Key key : mSortedKeys) {
lettersOnBaseLayout.addLetter(key);
}
final ArrayList<Key> allKeys = new ArrayList<>(mSortedKeys);
mSortedKeys.clear();
for (final Key key : allKeys) {
final Key filteredKey = Key.removeRedundantMoreKeys(key, lettersOnBaseLayout);
mSortedKeys.add(mUniqueKeysCache.getUniqueKey(filteredKey));
}
}
private int mMaxHeightCount = 0;
private int mMaxWidthCount = 0;
private final SparseIntArray mHeightHistogram = new SparseIntArray();
private final SparseIntArray mWidthHistogram = new SparseIntArray();
private static int updateHistogramCounter(final SparseIntArray histogram, final int key) {
final int index = histogram.indexOfKey(key);
final int count = (index >= 0 ? histogram.get(key) : 0) + 1;
histogram.put(key, count);
return count;
}
private void updateHistogram(final Key key) {
final int height = Math.round(key.getDefinedHeight());
final int heightCount = updateHistogramCounter(mHeightHistogram, height);
if (heightCount > mMaxHeightCount) {
mMaxHeightCount = heightCount;
mMostCommonKeyHeight = height;
}
final int width = Math.round(key.getDefinedWidth());
final int widthCount = updateHistogramCounter(mWidthHistogram, width);
if (widthCount > mMaxWidthCount) {
mMaxWidthCount = widthCount;
mMostCommonKeyWidth = width;
}
}
}

View File

@ -0,0 +1,326 @@
/*
* Copyright (C) 2012 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.colorful.keyboard.theme.keyboard.internal;
import android.content.res.Resources;
import android.content.res.TypedArray;
import android.util.Log;
import android.util.Xml;
import com.colorful.keyboard.theme.R;
import com.colorful.keyboard.theme.keyboard.Key;
import com.colorful.keyboard.theme.latin.utils.ResourceUtils;
import org.xmlpull.v1.XmlPullParser;
import java.util.ArrayDeque;
public final class KeyboardRow {
private static final String TAG = KeyboardRow.class.getSimpleName();
private static final float FLOAT_THRESHOLD = 0.0001f;
// keyWidth enum constants
private static final int KEYWIDTH_NOT_ENUM = 0;
private static final int KEYWIDTH_FILL_RIGHT = -1;
private final KeyboardParams mParams;
/** The y coordinate of the top edge of all keys in the row, excluding the top padding. */
private final float mY;
/** The height of this row and all keys in it, including the top and bottom padding. */
private final float mRowHeight;
/** The top padding of all of the keys in the row. */
private final float mKeyTopPadding;
/** The bottom padding of all of the keys in the row. */
private final float mKeyBottomPadding;
/** A tracker for where the next key should start, excluding padding. */
private float mNextKeyXPos;
/** The x coordinate of the left edge of the current key, excluding the left padding. */
private float mCurrentX;
/** The width of the current key excluding the left and right padding. */
private float mCurrentKeyWidth;
/** The left padding of the current key. */
private float mCurrentKeyLeftPadding;
/** The right padding of the current key. */
private float mCurrentKeyRightPadding;
/** Flag indicating whether the previous key in the row was a spacer. */
private boolean mLastKeyWasSpacer = false;
/** The x coordinate of the right edge of the previous key, excluding the right padding. */
private float mLastKeyRightEdge = 0;
private final ArrayDeque<RowAttributes> mRowAttributesStack = new ArrayDeque<>();
// TODO: Add keyActionFlags.
private static class RowAttributes {
/** Default padded width of a key in this row. */
public final float mDefaultKeyPaddedWidth;
/** Default keyLabelFlags in this row. */
public final int mDefaultKeyLabelFlags;
/** Default backgroundType for this row */
public final int mDefaultBackgroundType;
/**
* Parse and create key attributes. This constructor is used to parse Row tag.
*
* @param keyAttr an attributes array of Row tag.
* @param defaultKeyPaddedWidth a default padded key width.
* @param keyboardWidth the keyboard width that is required to calculate keyWidth attribute.
*/
public RowAttributes(final TypedArray keyAttr, final float defaultKeyPaddedWidth,
final float keyboardWidth) {
mDefaultKeyPaddedWidth = ResourceUtils.getFraction(keyAttr,
R.styleable.Keyboard_Key_keyWidth, keyboardWidth, defaultKeyPaddedWidth);
mDefaultKeyLabelFlags = keyAttr.getInt(R.styleable.Keyboard_Key_keyLabelFlags, 0);
mDefaultBackgroundType = keyAttr.getInt(R.styleable.Keyboard_Key_backgroundType,
Key.BACKGROUND_TYPE_NORMAL);
}
/**
* Parse and update key attributes using default attributes. This constructor is used
* to parse include tag.
*
* @param keyAttr an attributes array of include tag.
* @param defaultRowAttr default Row attributes.
* @param keyboardWidth the keyboard width that is required to calculate keyWidth attribute.
*/
public RowAttributes(final TypedArray keyAttr, final RowAttributes defaultRowAttr,
final float keyboardWidth) {
mDefaultKeyPaddedWidth = ResourceUtils.getFraction(keyAttr,
R.styleable.Keyboard_Key_keyWidth, keyboardWidth,
defaultRowAttr.mDefaultKeyPaddedWidth);
mDefaultKeyLabelFlags = keyAttr.getInt(R.styleable.Keyboard_Key_keyLabelFlags, 0)
| defaultRowAttr.mDefaultKeyLabelFlags;
mDefaultBackgroundType = keyAttr.getInt(R.styleable.Keyboard_Key_backgroundType,
defaultRowAttr.mDefaultBackgroundType);
}
}
public KeyboardRow(final Resources res, final KeyboardParams params,
final XmlPullParser parser, final float y) {
mParams = params;
final TypedArray keyboardAttr = res.obtainAttributes(Xml.asAttributeSet(parser),
R.styleable.Keyboard);
if (y < FLOAT_THRESHOLD) {
// The top row should use the keyboard's top padding instead of the vertical gap
mKeyTopPadding = params.mTopPadding;
} else {
// All of the vertical gap will be used as bottom padding rather than split between the
// top and bottom because it is probably more likely for users to click below a key
mKeyTopPadding = 0;
}
final float baseRowHeight = ResourceUtils.getDimensionOrFraction(keyboardAttr,
R.styleable.Keyboard_rowHeight, params.mBaseHeight, params.mDefaultRowHeight);
float keyHeight = baseRowHeight - params.mVerticalGap;
final float rowEndY = y + mKeyTopPadding + keyHeight + params.mVerticalGap;
final float keyboardBottomEdge = params.mOccupiedHeight - params.mBottomPadding;
if (rowEndY > keyboardBottomEdge - FLOAT_THRESHOLD) {
// The bottom row's padding should go to the bottom of the keyboard (this might be
// slightly more than the keyboard's bottom padding if the rows don't add up to 100%).
// We'll consider it the bottom row as long as the row's normal bottom padding overlaps
// with the keyboard's bottom padding any amount.
final float keyEndY = y + mKeyTopPadding + keyHeight;
final float keyOverflow = keyEndY - keyboardBottomEdge;
if (keyOverflow > FLOAT_THRESHOLD) {
if (Math.round(keyOverflow) > 0) {
// Only bother logging an error when expected rounding wouldn't resolve this
Log.e(TAG, "The row is too tall to fit in the keyboard (" + keyOverflow
+ " px). The height was reduced to fit.");
}
keyHeight = Math.max(keyboardBottomEdge - y - mKeyTopPadding, 0);
}
mKeyBottomPadding = Math.max(params.mOccupiedHeight - keyEndY, 0);
} else {
mKeyBottomPadding = params.mVerticalGap;
}
mRowHeight = mKeyTopPadding + keyHeight + mKeyBottomPadding;
keyboardAttr.recycle();
final TypedArray keyAttr = res.obtainAttributes(Xml.asAttributeSet(parser),
R.styleable.Keyboard_Key);
mRowAttributesStack.push(new RowAttributes(
keyAttr, params.mDefaultKeyPaddedWidth, params.mBaseWidth));
keyAttr.recycle();
mY = y + mKeyTopPadding;
mLastKeyRightEdge = 0;
mNextKeyXPos = params.mLeftPadding;
}
public void pushRowAttributes(final TypedArray keyAttr) {
final RowAttributes newAttributes = new RowAttributes(
keyAttr, mRowAttributesStack.peek(), mParams.mBaseWidth);
mRowAttributesStack.push(newAttributes);
}
public void popRowAttributes() {
mRowAttributesStack.pop();
}
private float getDefaultKeyPaddedWidth() {
return mRowAttributesStack.peek().mDefaultKeyPaddedWidth;
}
public int getDefaultKeyLabelFlags() {
return mRowAttributesStack.peek().mDefaultKeyLabelFlags;
}
public int getDefaultBackgroundType() {
return mRowAttributesStack.peek().mDefaultBackgroundType;
}
/**
* Update the x position for the next key based on what is set in the keyXPos attribute.
* @param keyAttr the Key XML attributes array.
*/
public void updateXPos(final TypedArray keyAttr) {
if (keyAttr == null || !keyAttr.hasValue(R.styleable.Keyboard_Key_keyXPos)) {
return;
}
// keyXPos is based on the base width, but we need to add in the keyboard padding to
// determine the actual position in the keyboard.
final float keyXPos = ResourceUtils.getFraction(keyAttr, R.styleable.Keyboard_Key_keyXPos,
mParams.mBaseWidth, 0) + mParams.mLeftPadding;
// keyXPos shouldn't be less than mLastKeyRightEdge or this key will overlap the adjacent
// key on its left hand side.
if (keyXPos + FLOAT_THRESHOLD < mLastKeyRightEdge) {
Log.e(TAG, "The specified keyXPos (" + keyXPos
+ ") is smaller than the next available x position (" + mLastKeyRightEdge
+ "). The x position was increased to avoid overlapping keys.");
mNextKeyXPos = mLastKeyRightEdge;
} else {
mNextKeyXPos = keyXPos;
}
}
/**
* Determine the next key's dimensions so they can be retrieved using {@link #getKeyX()},
* {@link #getKeyWidth()}, etc.
* @param keyAttr the Key XML attributes array.
* @param isSpacer flag indicating if the key is a spacer.
*/
public void setCurrentKey(final TypedArray keyAttr, final boolean isSpacer) {
// Split gap on both sides of key
final float defaultGap = mParams.mHorizontalGap / 2;
updateXPos(keyAttr);
final float keyboardRightEdge = mParams.mOccupiedWidth - mParams.mRightPadding;
float keyWidth;
if (isSpacer) {
final float leftGap = Math.min(mNextKeyXPos - mLastKeyRightEdge - defaultGap,
defaultGap);
// Spacers don't have horizontal gaps but should include that space in its width
mCurrentX = mNextKeyXPos - leftGap;
keyWidth = getKeyWidth(keyAttr) + leftGap;
if (mCurrentX + keyWidth + FLOAT_THRESHOLD < keyboardRightEdge) {
// Add what is normally the default right gap for non-edge spacers
keyWidth += defaultGap;
}
mCurrentKeyLeftPadding = 0;
mCurrentKeyRightPadding = 0;
} else {
mCurrentX = mNextKeyXPos;
if (mLastKeyRightEdge < FLOAT_THRESHOLD || mLastKeyWasSpacer) {
// The first key in the row and a key next to a spacer should have a left padding
// that spans the available distance
mCurrentKeyLeftPadding = mCurrentX - mLastKeyRightEdge;
} else {
// Split the gap between the adjacent keys
mCurrentKeyLeftPadding = (mCurrentX - mLastKeyRightEdge) / 2;
}
keyWidth = getKeyWidth(keyAttr);
// We can't know this before seeing the next key, so just use the default. The key can
// be updated later.
mCurrentKeyRightPadding = defaultGap;
}
final float keyOverflow = mCurrentX + keyWidth - keyboardRightEdge;
if (keyOverflow > FLOAT_THRESHOLD) {
if (Math.round(keyOverflow) > 0) {
// Only bother logging an error when expected rounding wouldn't resolve this
Log.e(TAG, "The " + (isSpacer ? "spacer" : "key")
+ " is too wide to fit in the keyboard (" + keyOverflow
+ " px). The width was reduced to fit.");
}
keyWidth = Math.max(keyboardRightEdge - mCurrentX, 0);
}
mCurrentKeyWidth = keyWidth;
// Calculations for the current key are done. Prep for the next key.
mLastKeyRightEdge = mCurrentX + keyWidth;
mLastKeyWasSpacer = isSpacer;
// Set the next key's default position. Spacers only add half because their width includes
// what is normally the horizontal gap.
mNextKeyXPos = mLastKeyRightEdge + (isSpacer ? defaultGap : mParams.mHorizontalGap);
}
private float getKeyWidth(final TypedArray keyAttr) {
if (keyAttr == null) {
return getDefaultKeyPaddedWidth() - mParams.mHorizontalGap;
}
final int widthType = ResourceUtils.getEnumValue(keyAttr,
R.styleable.Keyboard_Key_keyWidth, KEYWIDTH_NOT_ENUM);
switch (widthType) {
case KEYWIDTH_FILL_RIGHT:
// If keyWidth is fillRight, the actual key width will be determined to fill
// out the area up to the right edge of the keyboard.
final float keyboardRightEdge = mParams.mOccupiedWidth - mParams.mRightPadding;
return keyboardRightEdge - mCurrentX;
default: // KEYWIDTH_NOT_ENUM
return ResourceUtils.getFraction(keyAttr, R.styleable.Keyboard_Key_keyWidth,
mParams.mBaseWidth, getDefaultKeyPaddedWidth()) - mParams.mHorizontalGap;
}
}
public float getRowHeight() {
return mRowHeight;
}
public float getKeyY() {
return mY;
}
public float getKeyX() {
return mCurrentX;
}
public float getKeyWidth() {
return mCurrentKeyWidth;
}
public float getKeyHeight() {
return mRowHeight - mKeyTopPadding - mKeyBottomPadding;
}
public float getKeyTopPadding() {
return mKeyTopPadding;
}
public float getKeyBottomPadding() {
return mKeyBottomPadding;
}
public float getKeyLeftPadding() {
return mCurrentKeyLeftPadding;
}
public float getKeyRightPadding() {
return mCurrentKeyRightPadding;
}
}

View File

@ -0,0 +1,649 @@
/*
* Copyright (C) 2011 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.colorful.keyboard.theme.keyboard.internal;
import android.text.TextUtils;
import android.util.Log;
import com.colorful.keyboard.theme.event.Event;
import com.colorful.keyboard.theme.latin.common.Constants;
import com.colorful.keyboard.theme.latin.utils.CapsModeUtils;
import com.colorful.keyboard.theme.latin.utils.RecapitalizeStatus;
public final class KeyboardState {
private static final String TAG = KeyboardState.class.getSimpleName();
private static final boolean DEBUG_EVENT = false;
private static final boolean DEBUG_INTERNAL_ACTION = false;
public interface SwitchActions {
boolean DEBUG_ACTION = false;
void setAlphabetKeyboard();
void setAlphabetManualShiftedKeyboard();
void setAlphabetAutomaticShiftedKeyboard();
void setAlphabetShiftLockedKeyboard();
void setSymbolsKeyboard();
void setSymbolsShiftedKeyboard();
/**
* Request to call back {@link KeyboardState#onUpdateShiftState(int, int)}.
*/
void requestUpdatingShiftState(final int autoCapsFlags, final int recapitalizeMode);
boolean DEBUG_TIMER_ACTION = false;
void startDoubleTapShiftKeyTimer();
boolean isInDoubleTapShiftKeyTimeout();
void cancelDoubleTapShiftKeyTimer();
}
private final SwitchActions mSwitchActions;
private ShiftKeyState mShiftKeyState = new ShiftKeyState("Shift");
private ModifierKeyState mSymbolKeyState = new ModifierKeyState("Symbol");
// TODO: Merge {@link #mSwitchState}, {@link #mIsAlphabetMode}, {@link #mAlphabetShiftState},
// {@link #mIsSymbolShifted}, {@link #mPrevMainKeyboardWasShiftLocked}, and
// {@link #mPrevSymbolsKeyboardWasShifted} into single state variable.
private static final int SWITCH_STATE_ALPHA = 0;
private static final int SWITCH_STATE_SYMBOL_BEGIN = 1;
private static final int SWITCH_STATE_SYMBOL = 2;
private static final int SWITCH_STATE_MOMENTARY_ALPHA_AND_SYMBOL = 3;
private static final int SWITCH_STATE_MOMENTARY_SYMBOL_AND_MORE = 4;
private int mSwitchState = SWITCH_STATE_ALPHA;
private boolean mIsAlphabetMode;
private AlphabetShiftState mAlphabetShiftState = new AlphabetShiftState();
private boolean mIsSymbolShifted;
private boolean mPrevMainKeyboardWasShiftLocked;
private boolean mPrevSymbolsKeyboardWasShifted;
private int mRecapitalizeMode;
// For handling double tap.
private boolean mIsInAlphabetUnshiftedFromShifted;
private boolean mIsInDoubleTapShiftKey;
private final SavedKeyboardState mSavedKeyboardState = new SavedKeyboardState();
static final class SavedKeyboardState {
public boolean mIsValid;
public boolean mIsAlphabetMode;
public boolean mIsAlphabetShiftLocked;
public int mShiftMode;
@Override
public String toString() {
if (!mIsValid) {
return "INVALID";
}
if (mIsAlphabetMode) {
return mIsAlphabetShiftLocked ? "ALPHABET_SHIFT_LOCKED"
: "ALPHABET_" + shiftModeToString(mShiftMode);
}
return "SYMBOLS_" + shiftModeToString(mShiftMode);
}
}
public KeyboardState(final SwitchActions switchActions) {
mSwitchActions = switchActions;
mRecapitalizeMode = RecapitalizeStatus.NOT_A_RECAPITALIZE_MODE;
}
public void onLoadKeyboard(final int autoCapsFlags, final int recapitalizeMode) {
if (DEBUG_EVENT) {
Log.d(TAG, "onLoadKeyboard: " + stateToString(autoCapsFlags, recapitalizeMode));
}
// Reset alphabet shift state.
mAlphabetShiftState.setShiftLocked(false);
mPrevMainKeyboardWasShiftLocked = false;
mPrevSymbolsKeyboardWasShifted = false;
mShiftKeyState.onRelease();
mSymbolKeyState.onRelease();
if (mSavedKeyboardState.mIsValid) {
onRestoreKeyboardState(autoCapsFlags, recapitalizeMode);
mSavedKeyboardState.mIsValid = false;
} else {
// Reset keyboard to alphabet mode.
setAlphabetKeyboard(autoCapsFlags, recapitalizeMode);
}
}
// Constants for {@link SavedKeyboardState#mShiftMode} and {@link #setShifted(int)}.
private static final int UNSHIFT = 0;
private static final int MANUAL_SHIFT = 1;
private static final int AUTOMATIC_SHIFT = 2;
private static final int SHIFT_LOCK_SHIFTED = 3;
public void onSaveKeyboardState() {
final SavedKeyboardState state = mSavedKeyboardState;
state.mIsAlphabetMode = mIsAlphabetMode;
if (mIsAlphabetMode) {
state.mIsAlphabetShiftLocked = mAlphabetShiftState.isShiftLocked();
state.mShiftMode = mAlphabetShiftState.isAutomaticShifted() ? AUTOMATIC_SHIFT
: (mAlphabetShiftState.isShiftedOrShiftLocked() ? MANUAL_SHIFT : UNSHIFT);
} else {
state.mIsAlphabetShiftLocked = mPrevMainKeyboardWasShiftLocked;
state.mShiftMode = mIsSymbolShifted ? MANUAL_SHIFT : UNSHIFT;
}
state.mIsValid = true;
if (DEBUG_EVENT) {
Log.d(TAG, "onSaveKeyboardState: saved=" + state + " " + this);
}
}
private void onRestoreKeyboardState(final int autoCapsFlags, final int recapitalizeMode) {
final SavedKeyboardState state = mSavedKeyboardState;
if (DEBUG_EVENT) {
Log.d(TAG, "onRestoreKeyboardState: saved=" + state
+ " " + stateToString(autoCapsFlags, recapitalizeMode));
}
mPrevMainKeyboardWasShiftLocked = state.mIsAlphabetShiftLocked;
if (state.mIsAlphabetMode) {
setAlphabetKeyboard(autoCapsFlags, recapitalizeMode);
setShiftLocked(state.mIsAlphabetShiftLocked);
if (!state.mIsAlphabetShiftLocked) {
setShifted(state.mShiftMode);
}
return;
}
// Symbol mode
if (state.mShiftMode == MANUAL_SHIFT) {
setSymbolsShiftedKeyboard();
} else {
setSymbolsKeyboard();
}
}
private void setShifted(final int shiftMode) {
if (DEBUG_INTERNAL_ACTION) {
Log.d(TAG, "setShifted: shiftMode=" + shiftModeToString(shiftMode) + " " + this);
}
if (!mIsAlphabetMode) return;
final int prevShiftMode;
if (mAlphabetShiftState.isAutomaticShifted()) {
prevShiftMode = AUTOMATIC_SHIFT;
} else if (mAlphabetShiftState.isManualShifted()) {
prevShiftMode = MANUAL_SHIFT;
} else {
prevShiftMode = UNSHIFT;
}
switch (shiftMode) {
case AUTOMATIC_SHIFT:
mAlphabetShiftState.setAutomaticShifted();
if (shiftMode != prevShiftMode) {
mSwitchActions.setAlphabetAutomaticShiftedKeyboard();
}
break;
case MANUAL_SHIFT:
mAlphabetShiftState.setShifted(true);
if (shiftMode != prevShiftMode) {
mSwitchActions.setAlphabetManualShiftedKeyboard();
}
break;
case UNSHIFT:
mAlphabetShiftState.setShifted(false);
if (shiftMode != prevShiftMode) {
mSwitchActions.setAlphabetKeyboard();
}
break;
case SHIFT_LOCK_SHIFTED:
mAlphabetShiftState.setShifted(true);
break;
}
}
private void setShiftLocked(final boolean shiftLocked) {
if (DEBUG_INTERNAL_ACTION) {
Log.d(TAG, "setShiftLocked: shiftLocked=" + shiftLocked + " " + this);
}
if (!mIsAlphabetMode) return;
if (shiftLocked && (!mAlphabetShiftState.isShiftLocked()
|| mAlphabetShiftState.isShiftLockShifted())) {
mSwitchActions.setAlphabetShiftLockedKeyboard();
}
if (!shiftLocked && mAlphabetShiftState.isShiftLocked()) {
mSwitchActions.setAlphabetKeyboard();
}
mAlphabetShiftState.setShiftLocked(shiftLocked);
}
private void toggleAlphabetAndSymbols(final int autoCapsFlags, final int recapitalizeMode) {
if (DEBUG_INTERNAL_ACTION) {
Log.d(TAG, "toggleAlphabetAndSymbols: "
+ stateToString(autoCapsFlags, recapitalizeMode));
}
if (mIsAlphabetMode) {
mPrevMainKeyboardWasShiftLocked = mAlphabetShiftState.isShiftLocked();
if (mPrevSymbolsKeyboardWasShifted) {
setSymbolsShiftedKeyboard();
} else {
setSymbolsKeyboard();
}
mPrevSymbolsKeyboardWasShifted = false;
} else {
mPrevSymbolsKeyboardWasShifted = mIsSymbolShifted;
setAlphabetKeyboard(autoCapsFlags, recapitalizeMode);
if (mPrevMainKeyboardWasShiftLocked) {
setShiftLocked(true);
}
mPrevMainKeyboardWasShiftLocked = false;
}
}
// TODO: Remove this method. Come up with a more comprehensive way to reset the keyboard layout
// when a keyboard layout set doesn't get reloaded in LatinIME.onStartInputViewInternal().
private void resetKeyboardStateToAlphabet(final int autoCapsFlags, final int recapitalizeMode) {
if (DEBUG_INTERNAL_ACTION) {
Log.d(TAG, "resetKeyboardStateToAlphabet: "
+ stateToString(autoCapsFlags, recapitalizeMode));
}
if (mIsAlphabetMode) return;
mPrevSymbolsKeyboardWasShifted = mIsSymbolShifted;
setAlphabetKeyboard(autoCapsFlags, recapitalizeMode);
if (mPrevMainKeyboardWasShiftLocked) {
setShiftLocked(true);
}
mPrevMainKeyboardWasShiftLocked = false;
}
private void toggleShiftInSymbols() {
if (mIsSymbolShifted) {
setSymbolsKeyboard();
} else {
setSymbolsShiftedKeyboard();
}
}
private void setAlphabetKeyboard(final int autoCapsFlags, final int recapitalizeMode) {
if (DEBUG_INTERNAL_ACTION) {
Log.d(TAG, "setAlphabetKeyboard: " + stateToString(autoCapsFlags, recapitalizeMode));
}
mSwitchActions.setAlphabetKeyboard();
mIsAlphabetMode = true;
mIsSymbolShifted = false;
mRecapitalizeMode = RecapitalizeStatus.NOT_A_RECAPITALIZE_MODE;
mSwitchState = SWITCH_STATE_ALPHA;
mSwitchActions.requestUpdatingShiftState(autoCapsFlags, recapitalizeMode);
}
private void setSymbolsKeyboard() {
if (DEBUG_INTERNAL_ACTION) {
Log.d(TAG, "setSymbolsKeyboard");
}
mSwitchActions.setSymbolsKeyboard();
mIsAlphabetMode = false;
mIsSymbolShifted = false;
mRecapitalizeMode = RecapitalizeStatus.NOT_A_RECAPITALIZE_MODE;
// Reset alphabet shift state.
mAlphabetShiftState.setShiftLocked(false);
mSwitchState = SWITCH_STATE_SYMBOL_BEGIN;
}
private void setSymbolsShiftedKeyboard() {
if (DEBUG_INTERNAL_ACTION) {
Log.d(TAG, "setSymbolsShiftedKeyboard");
}
mSwitchActions.setSymbolsShiftedKeyboard();
mIsAlphabetMode = false;
mIsSymbolShifted = true;
mRecapitalizeMode = RecapitalizeStatus.NOT_A_RECAPITALIZE_MODE;
// Reset alphabet shift state.
mAlphabetShiftState.setShiftLocked(false);
mSwitchState = SWITCH_STATE_SYMBOL_BEGIN;
}
public void onPressKey(final int code, final boolean isSinglePointer, final int autoCapsFlags,
final int recapitalizeMode) {
if (DEBUG_EVENT) {
Log.d(TAG, "onPressKey: code=" + Constants.printableCode(code)
+ " single=" + isSinglePointer
+ " " + stateToString(autoCapsFlags, recapitalizeMode));
}
if (code != Constants.CODE_SHIFT) {
// Because the double tap shift key timer is to detect two consecutive shift key press,
// it should be canceled when a non-shift key is pressed.
mSwitchActions.cancelDoubleTapShiftKeyTimer();
}
if (code == Constants.CODE_SHIFT) {
onPressShift();
} else if (code == Constants.CODE_CAPSLOCK) {
// Nothing to do here. See {@link #onReleaseKey(int,boolean)}.
} else if (code == Constants.CODE_SWITCH_ALPHA_SYMBOL) {
onPressSymbol(autoCapsFlags, recapitalizeMode);
} else {
mShiftKeyState.onOtherKeyPressed();
mSymbolKeyState.onOtherKeyPressed();
// It is required to reset the auto caps state when all of the following conditions
// are met:
// 1) two or more fingers are in action
// 2) in alphabet layout
// 3) not in all characters caps mode
// As for #3, please note that it's required to check even when the auto caps mode is
// off because, for example, we may be in the #1 state within the manual temporary
// shifted mode.
if (!isSinglePointer && mIsAlphabetMode
&& autoCapsFlags != TextUtils.CAP_MODE_CHARACTERS) {
final boolean needsToResetAutoCaps =
(mAlphabetShiftState.isAutomaticShifted() && !mShiftKeyState.isChording())
|| (mAlphabetShiftState.isManualShifted() && mShiftKeyState.isReleasing());
if (needsToResetAutoCaps) {
mSwitchActions.setAlphabetKeyboard();
}
}
}
}
public void onReleaseKey(final int code, final boolean withSliding, final int autoCapsFlags,
final int recapitalizeMode) {
if (DEBUG_EVENT) {
Log.d(TAG, "onReleaseKey: code=" + Constants.printableCode(code)
+ " sliding=" + withSliding
+ " " + stateToString(autoCapsFlags, recapitalizeMode));
}
if (code == Constants.CODE_SHIFT) {
onReleaseShift(withSliding, autoCapsFlags, recapitalizeMode);
} else if (code == Constants.CODE_CAPSLOCK) {
setShiftLocked(!mAlphabetShiftState.isShiftLocked());
} else if (code == Constants.CODE_SWITCH_ALPHA_SYMBOL) {
onReleaseSymbol(withSliding, autoCapsFlags, recapitalizeMode);
}
}
private void onPressSymbol(final int autoCapsFlags,
final int recapitalizeMode) {
toggleAlphabetAndSymbols(autoCapsFlags, recapitalizeMode);
mSymbolKeyState.onPress();
mSwitchState = SWITCH_STATE_MOMENTARY_ALPHA_AND_SYMBOL;
}
private void onReleaseSymbol(final boolean withSliding, final int autoCapsFlags,
final int recapitalizeMode) {
if (mSymbolKeyState.isChording()) {
// Switch back to the previous keyboard mode if the user chords the mode change key and
// another key, then releases the mode change key.
toggleAlphabetAndSymbols(autoCapsFlags, recapitalizeMode);
} else if (!withSliding) {
// If the mode change key is being released without sliding, we should forget the
// previous symbols keyboard shift state and simply switch back to symbols layout
// (never symbols shifted) next time the mode gets changed to symbols layout.
mPrevSymbolsKeyboardWasShifted = false;
}
mSymbolKeyState.onRelease();
}
public void onUpdateShiftState(final int autoCapsFlags, final int recapitalizeMode) {
if (DEBUG_EVENT) {
Log.d(TAG, "onUpdateShiftState: " + stateToString(autoCapsFlags, recapitalizeMode));
}
mRecapitalizeMode = recapitalizeMode;
updateAlphabetShiftState(autoCapsFlags, recapitalizeMode);
}
// TODO: Remove this method. Come up with a more comprehensive way to reset the keyboard layout
// when a keyboard layout set doesn't get reloaded in LatinIME.onStartInputViewInternal().
public void onResetKeyboardStateToAlphabet(final int autoCapsFlags,
final int recapitalizeMode) {
if (DEBUG_EVENT) {
Log.d(TAG, "onResetKeyboardStateToAlphabet: "
+ stateToString(autoCapsFlags, recapitalizeMode));
}
resetKeyboardStateToAlphabet(autoCapsFlags, recapitalizeMode);
}
private void updateShiftStateForRecapitalize(final int recapitalizeMode) {
switch (recapitalizeMode) {
case RecapitalizeStatus.CAPS_MODE_ALL_UPPER:
setShifted(SHIFT_LOCK_SHIFTED);
break;
case RecapitalizeStatus.CAPS_MODE_FIRST_WORD_UPPER:
setShifted(AUTOMATIC_SHIFT);
break;
case RecapitalizeStatus.CAPS_MODE_ALL_LOWER:
case RecapitalizeStatus.CAPS_MODE_ORIGINAL_MIXED_CASE:
default:
setShifted(UNSHIFT);
}
}
private void updateAlphabetShiftState(final int autoCapsFlags, final int recapitalizeMode) {
if (!mIsAlphabetMode) return;
if (RecapitalizeStatus.NOT_A_RECAPITALIZE_MODE != recapitalizeMode) {
// We are recapitalizing. Match the keyboard to the current recapitalize state.
updateShiftStateForRecapitalize(recapitalizeMode);
return;
}
if (!mShiftKeyState.isReleasing()) {
// Ignore update shift state event while the shift key is being pressed (including
// chording).
return;
}
if (!mAlphabetShiftState.isShiftLocked() && !mShiftKeyState.isIgnoring()) {
if (mShiftKeyState.isReleasing() && autoCapsFlags != Constants.TextUtils.CAP_MODE_OFF) {
// Only when shift key is releasing, automatic temporary upper case will be set.
setShifted(AUTOMATIC_SHIFT);
} else {
setShifted(mShiftKeyState.isChording() ? MANUAL_SHIFT : UNSHIFT);
}
}
}
private void onPressShift() {
// If we are recapitalizing, we don't do any of the normal processing, including
// importantly the double tap timer.
if (RecapitalizeStatus.NOT_A_RECAPITALIZE_MODE != mRecapitalizeMode) {
return;
}
if (mIsAlphabetMode) {
mIsInDoubleTapShiftKey = mSwitchActions.isInDoubleTapShiftKeyTimeout();
if (!mIsInDoubleTapShiftKey) {
// This is first tap.
mSwitchActions.startDoubleTapShiftKeyTimer();
}
if (mIsInDoubleTapShiftKey) {
if (mAlphabetShiftState.isManualShifted() || mIsInAlphabetUnshiftedFromShifted) {
// Shift key has been double tapped while in manual shifted or automatic
// shifted state.
setShiftLocked(true);
} else {
// Shift key has been double tapped while in normal state. This is the second
// tap to disable shift locked state, so just ignore this.
}
} else {
if (mAlphabetShiftState.isShiftLocked()) {
// Shift key is pressed while shift locked state, we will treat this state as
// shift lock shifted state and mark as if shift key pressed while normal
// state.
setShifted(SHIFT_LOCK_SHIFTED);
mShiftKeyState.onPress();
} else if (mAlphabetShiftState.isAutomaticShifted()) {
// Shift key pressed while automatic shifted isn't considered a manual shift
// since it doesn't change the keyboard into a shifted state.
mShiftKeyState.onPress();
} else if (mAlphabetShiftState.isShiftedOrShiftLocked()) {
// In manual shifted state, we just record shift key has been pressing while
// shifted state.
mShiftKeyState.onPressOnShifted();
} else {
// In base layout, chording or manual shifted mode is started.
setShifted(MANUAL_SHIFT);
mShiftKeyState.onPress();
}
}
} else {
// In symbol mode, just toggle symbol and symbol more keyboard.
toggleShiftInSymbols();
mSwitchState = SWITCH_STATE_MOMENTARY_SYMBOL_AND_MORE;
mShiftKeyState.onPress();
}
}
private void onReleaseShift(final boolean withSliding, final int autoCapsFlags,
final int recapitalizeMode) {
if (RecapitalizeStatus.NOT_A_RECAPITALIZE_MODE != mRecapitalizeMode) {
// We are recapitalizing. We should match the keyboard state to the recapitalize
// state in priority.
updateShiftStateForRecapitalize(mRecapitalizeMode);
} else if (mIsAlphabetMode) {
final boolean isShiftLocked = mAlphabetShiftState.isShiftLocked();
mIsInAlphabetUnshiftedFromShifted = false;
if (mIsInDoubleTapShiftKey) {
// Double tap shift key has been handled in {@link #onPressShift}, so that just
// ignore this release shift key here.
mIsInDoubleTapShiftKey = false;
} else if (mShiftKeyState.isChording()) {
if (mAlphabetShiftState.isShiftLockShifted()) {
// After chording input while shift locked state.
setShiftLocked(true);
} else {
// After chording input while normal state.
setShifted(UNSHIFT);
}
// After chording input, automatic shift state may have been changed depending on
// what characters were input.
mShiftKeyState.onRelease();
mSwitchActions.requestUpdatingShiftState(autoCapsFlags, recapitalizeMode);
return;
} else if (isShiftLocked && !mAlphabetShiftState.isShiftLockShifted()
&& (mShiftKeyState.isPressing() || mShiftKeyState.isPressingOnShifted())
&& !withSliding) {
// Shift has been long pressed, ignore this release.
} else if (isShiftLocked && !mShiftKeyState.isIgnoring() && !withSliding) {
// Shift has been pressed without chording while shift locked state.
setShiftLocked(false);
} else if (mAlphabetShiftState.isShiftedOrShiftLocked()
&& mShiftKeyState.isPressingOnShifted() && !withSliding) {
// Shift has been pressed without chording while shifted state.
setShifted(UNSHIFT);
mIsInAlphabetUnshiftedFromShifted = true;
} else if (mAlphabetShiftState.isAutomaticShifted() && mShiftKeyState.isPressing()
&& !withSliding) {
// Shift has been pressed without chording while automatic shifted
setShifted(UNSHIFT);
mIsInAlphabetUnshiftedFromShifted = true;
}
} else {
// In symbol mode, switch back to the previous keyboard mode if the user chords the
// shift key and another key, then releases the shift key.
if (mShiftKeyState.isChording()) {
toggleShiftInSymbols();
}
}
mShiftKeyState.onRelease();
}
public void onFinishSlidingInput(final int autoCapsFlags, final int recapitalizeMode) {
if (DEBUG_EVENT) {
Log.d(TAG, "onFinishSlidingInput: " + stateToString(autoCapsFlags, recapitalizeMode));
}
// Switch back to the previous keyboard mode if the user cancels sliding input.
switch (mSwitchState) {
case SWITCH_STATE_MOMENTARY_ALPHA_AND_SYMBOL:
toggleAlphabetAndSymbols(autoCapsFlags, recapitalizeMode);
break;
case SWITCH_STATE_MOMENTARY_SYMBOL_AND_MORE:
toggleShiftInSymbols();
break;
}
}
private static boolean isSpaceOrEnter(final int c) {
return c == Constants.CODE_SPACE || c == Constants.CODE_ENTER;
}
public void onEvent(final Event event, final int autoCapsFlags, final int recapitalizeMode) {
final int code = event.isFunctionalKeyEvent() ? event.mKeyCode : event.mCodePoint;
if (DEBUG_EVENT) {
Log.d(TAG, "onEvent: code=" + Constants.printableCode(code)
+ " " + stateToString(autoCapsFlags, recapitalizeMode));
}
switch (mSwitchState) {
case SWITCH_STATE_MOMENTARY_ALPHA_AND_SYMBOL:
if (code == Constants.CODE_SWITCH_ALPHA_SYMBOL) {
// Detected only the mode change key has been pressed, and then released.
if (mIsAlphabetMode) {
mSwitchState = SWITCH_STATE_ALPHA;
} else {
mSwitchState = SWITCH_STATE_SYMBOL_BEGIN;
}
}
break;
case SWITCH_STATE_MOMENTARY_SYMBOL_AND_MORE:
if (code == Constants.CODE_SHIFT) {
// Detected only the shift key has been pressed on symbol layout, and then
// released.
mSwitchState = SWITCH_STATE_SYMBOL_BEGIN;
}
break;
case SWITCH_STATE_SYMBOL_BEGIN:
if (!isSpaceOrEnter(code) && (Constants.isLetterCode(code)
|| code == Constants.CODE_OUTPUT_TEXT)) {
mSwitchState = SWITCH_STATE_SYMBOL;
}
break;
case SWITCH_STATE_SYMBOL:
// Switch back to alpha keyboard mode if user types one or more non-space/enter
// characters followed by a space/enter.
if (isSpaceOrEnter(code)) {
toggleAlphabetAndSymbols(autoCapsFlags, recapitalizeMode);
mPrevSymbolsKeyboardWasShifted = false;
}
break;
}
// If the code is a letter, update keyboard shift state.
if (Constants.isLetterCode(code)) {
updateAlphabetShiftState(autoCapsFlags, recapitalizeMode);
}
}
static String shiftModeToString(final int shiftMode) {
switch (shiftMode) {
case UNSHIFT: return "UNSHIFT";
case MANUAL_SHIFT: return "MANUAL";
case AUTOMATIC_SHIFT: return "AUTOMATIC";
default: return null;
}
}
private static String switchStateToString(final int switchState) {
switch (switchState) {
case SWITCH_STATE_ALPHA: return "ALPHA";
case SWITCH_STATE_SYMBOL_BEGIN: return "SYMBOL-BEGIN";
case SWITCH_STATE_SYMBOL: return "SYMBOL";
case SWITCH_STATE_MOMENTARY_ALPHA_AND_SYMBOL: return "MOMENTARY-ALPHA-SYMBOL";
case SWITCH_STATE_MOMENTARY_SYMBOL_AND_MORE: return "MOMENTARY-SYMBOL-MORE";
default: return null;
}
}
@Override
public String toString() {
return "[keyboard=" + (mIsAlphabetMode ? mAlphabetShiftState.toString()
: (mIsSymbolShifted ? "SYMBOLS_SHIFTED" : "SYMBOLS"))
+ " shift=" + mShiftKeyState
+ " symbol=" + mSymbolKeyState
+ " switch=" + switchStateToString(mSwitchState) + "]";
}
private String stateToString(final int autoCapsFlags, final int recapitalizeMode) {
return this + " autoCapsFlags=" + CapsModeUtils.flagsToString(autoCapsFlags)
+ " recapitalizeMode=" + RecapitalizeStatus.modeToString(recapitalizeMode);
}
}

View File

@ -0,0 +1,138 @@
/*
* Copyright (C) 2012 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.colorful.keyboard.theme.keyboard.internal;
import android.content.Context;
import android.content.res.Resources;
import android.text.TextUtils;
import com.colorful.keyboard.theme.latin.common.Constants;
import java.util.Locale;
// TODO: Make this an immutable class.
public final class KeyboardTextsSet {
public static final String PREFIX_TEXT = "!text/";
private static final String PREFIX_RESOURCE = "!string/";
private static final char BACKSLASH = Constants.CODE_BACKSLASH;
private static final int MAX_REFERENCE_INDIRECTION = 10;
private Resources mResources;
private String mResourcePackageName;
private String[] mTextsTable;
public void setLocale(final Locale locale, final Context context) {
final Resources res = context.getResources();
// Null means the current system locale.
final String resourcePackageName = res.getResourcePackageName(
context.getApplicationInfo().labelRes);
setLocale(locale, res, resourcePackageName);
}
public void setLocale(final Locale locale, final Resources res,
final String resourcePackageName) {
mResources = res;
// Null means the current system locale.
mResourcePackageName = resourcePackageName;
mTextsTable = KeyboardTextsTable.getTextsTable(locale);
}
public String getText(final String name) {
return KeyboardTextsTable.getText(name, mTextsTable);
}
private static int searchTextNameEnd(final String text, final int start) {
final int size = text.length();
for (int pos = start; pos < size; pos++) {
final char c = text.charAt(pos);
// Label name should be consisted of [a-zA-Z_0-9].
if ((c >= 'a' && c <= 'z') || c == '_' || (c >= '0' && c <= '9')) {
continue;
}
return pos;
}
return size;
}
// TODO: Resolve text reference when creating {@link KeyboardTextsTable} class.
public String resolveTextReference(final String rawText) {
if (TextUtils.isEmpty(rawText)) {
return null;
}
int level = 0;
String text = rawText;
StringBuilder sb;
do {
level++;
if (level >= MAX_REFERENCE_INDIRECTION) {
throw new RuntimeException("Too many " + PREFIX_TEXT + " or " + PREFIX_RESOURCE +
" reference indirection: " + text);
}
final int prefixLength = PREFIX_TEXT.length();
final int size = text.length();
if (size < prefixLength) {
break;
}
sb = null;
for (int pos = 0; pos < size; pos++) {
final char c = text.charAt(pos);
if (text.startsWith(PREFIX_TEXT, pos)) {
if (sb == null) {
sb = new StringBuilder(text.substring(0, pos));
}
pos = expandReference(text, pos, PREFIX_TEXT, sb);
} else if (text.startsWith(PREFIX_RESOURCE, pos)) {
if (sb == null) {
sb = new StringBuilder(text.substring(0, pos));
}
pos = expandReference(text, pos, PREFIX_RESOURCE, sb);
} else if (c == BACKSLASH) {
if (sb != null) {
// Append both escape character and escaped character.
sb.append(text.substring(pos, Math.min(pos + 2, size)));
}
pos++;
} else if (sb != null) {
sb.append(c);
}
}
if (sb != null) {
text = sb.toString();
}
} while (sb != null);
return TextUtils.isEmpty(text) ? null : text;
}
private int expandReference(final String text, final int pos, final String prefix,
final StringBuilder sb) {
final int prefixLength = prefix.length();
final int end = searchTextNameEnd(text, pos + prefixLength);
final String name = text.substring(pos + prefixLength, end);
if (prefix.equals(PREFIX_TEXT)) {
sb.append(getText(name));
} else { // PREFIX_RESOURCE
final int resId = mResources.getIdentifier(name, "string", mResourcePackageName);
sb.append(mResources.getString(resId));
}
return end - 1;
}
}

View File

@ -0,0 +1,77 @@
/*
* Copyright (C) 2010 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.colorful.keyboard.theme.keyboard.internal;
import android.util.Log;
/* package */ class ModifierKeyState {
protected static final String TAG = ModifierKeyState.class.getSimpleName();
protected static final boolean DEBUG = false;
protected static final int RELEASING = 0;
protected static final int PRESSING = 1;
protected static final int CHORDING = 2;
protected final String mName;
protected int mState = RELEASING;
public ModifierKeyState(String name) {
mName = name;
}
public void onPress() {
mState = PRESSING;
}
public void onRelease() {
mState = RELEASING;
}
public void onOtherKeyPressed() {
final int oldState = mState;
if (oldState == PRESSING)
mState = CHORDING;
if (DEBUG)
Log.d(TAG, mName + ".onOtherKeyPressed: " + toString(oldState) + " > " + this);
}
public boolean isPressing() {
return mState == PRESSING;
}
public boolean isReleasing() {
return mState == RELEASING;
}
public boolean isChording() {
return mState == CHORDING;
}
@Override
public String toString() {
return toString(mState);
}
protected String toString(int state) {
switch (state) {
case RELEASING: return "RELEASING";
case PRESSING: return "PRESSING";
case CHORDING: return "CHORDING";
default: return "UNKNOWN";
}
}
}

View File

@ -0,0 +1,344 @@
/*
* Copyright (C) 2012 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.colorful.keyboard.theme.keyboard.internal;
import android.text.TextUtils;
import android.util.SparseIntArray;
import com.colorful.keyboard.theme.keyboard.Key;
import com.colorful.keyboard.theme.latin.common.CollectionUtils;
import com.colorful.keyboard.theme.latin.common.Constants;
import com.colorful.keyboard.theme.latin.common.StringUtils;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.Locale;
/**
* The more key specification object. The more keys are an array of {@link MoreKeySpec}.
*
* The more keys specification is comma separated "key specification" each of which represents one
* "more key".
* The key specification might have label or string resource reference in it. These references are
* expanded before parsing comma.
* Special character, comma ',' backslash '\' can be escaped by '\' character.
* Note that the '\' is also parsed by XML parser and {@link MoreKeySpec#splitKeySpecs(String)}
* as well.
*/
// TODO: Should extend the key specification object.
public final class MoreKeySpec {
public final int mCode;
public final String mLabel;
public final String mOutputText;
public final int mIconId;
public MoreKeySpec(final String moreKeySpec, boolean needsToUpperCase,
final Locale locale) {
if (moreKeySpec.isEmpty()) {
throw new KeySpecParser.KeySpecParserError("Empty more key spec");
}
final String label = KeySpecParser.getLabel(moreKeySpec);
mLabel = needsToUpperCase ? StringUtils.toTitleCaseOfKeyLabel(label, locale) : label;
final int codeInSpec = KeySpecParser.getCode(moreKeySpec);
final int code = needsToUpperCase ? StringUtils.toTitleCaseOfKeyCode(codeInSpec, locale)
: codeInSpec;
if (code == Constants.CODE_UNSPECIFIED) {
// Some letter, for example German Eszett (U+00DF: "ß"), has multiple characters
// upper case representation ("SS").
mCode = Constants.CODE_OUTPUT_TEXT;
mOutputText = mLabel;
} else {
mCode = code;
final String outputText = KeySpecParser.getOutputText(moreKeySpec);
mOutputText = needsToUpperCase
? StringUtils.toTitleCaseOfKeyLabel(outputText, locale) : outputText;
}
mIconId = KeySpecParser.getIconId(moreKeySpec);
}
public Key buildKey(final float x, final float y, final float width, final float height,
final float leftPadding, final float rightPadding, final float topPadding,
final float bottomPadding, final int labelFlags) {
return new Key(mLabel, mIconId, mCode, mOutputText, null /* hintLabel */, labelFlags,
Key.BACKGROUND_TYPE_NORMAL, x, y, width, height, leftPadding, rightPadding,
topPadding, bottomPadding);
}
@Override
public int hashCode() {
int hashCode = 31 + mCode;
hashCode = hashCode * 31 + mIconId;
final String label = mLabel;
hashCode = hashCode * 31 + (label == null ? 0 : label.hashCode());
final String outputText = mOutputText;
hashCode = hashCode * 31 + (outputText == null ? 0 : outputText.hashCode());
return hashCode;
}
@Override
public boolean equals(final Object o) {
if (this == o) {
return true;
}
if (o instanceof MoreKeySpec) {
final MoreKeySpec other = (MoreKeySpec)o;
return mCode == other.mCode
&& mIconId == other.mIconId
&& TextUtils.equals(mLabel, other.mLabel)
&& TextUtils.equals(mOutputText, other.mOutputText);
}
return false;
}
@Override
public String toString() {
final String label = (mIconId == KeyboardIconsSet.ICON_UNDEFINED ? mLabel
: KeyboardIconsSet.PREFIX_ICON + KeyboardIconsSet.getIconName(mIconId));
final String output = (mCode == Constants.CODE_OUTPUT_TEXT ? mOutputText
: Constants.printableCode(mCode));
if (StringUtils.codePointCount(label) == 1 && label.codePointAt(0) == mCode) {
return output;
}
return label + "|" + output;
}
public static class LettersOnBaseLayout {
private final SparseIntArray mCodes = new SparseIntArray();
private final HashSet<String> mTexts = new HashSet<>();
public void addLetter(final Key key) {
final int code = key.getCode();
if (Character.isAlphabetic(code)) {
mCodes.put(code, 0);
} else if (code == Constants.CODE_OUTPUT_TEXT) {
mTexts.add(key.getOutputText());
}
}
public boolean contains(final MoreKeySpec moreKey) {
final int code = moreKey.mCode;
if (Character.isAlphabetic(code) && mCodes.indexOfKey(code) >= 0) {
return true;
} else if (code == Constants.CODE_OUTPUT_TEXT && mTexts.contains(moreKey.mOutputText)) {
return true;
}
return false;
}
}
public static MoreKeySpec[] removeRedundantMoreKeys(final MoreKeySpec[] moreKeys,
final LettersOnBaseLayout lettersOnBaseLayout) {
if (moreKeys == null) {
return null;
}
final ArrayList<MoreKeySpec> filteredMoreKeys = new ArrayList<>();
for (final MoreKeySpec moreKey : moreKeys) {
if (!lettersOnBaseLayout.contains(moreKey)) {
filteredMoreKeys.add(moreKey);
}
}
final int size = filteredMoreKeys.size();
if (size == moreKeys.length) {
return moreKeys;
}
if (size == 0) {
return null;
}
return filteredMoreKeys.toArray(new MoreKeySpec[size]);
}
// Constants for parsing.
private static final char COMMA = Constants.CODE_COMMA;
private static final char BACKSLASH = Constants.CODE_BACKSLASH;
private static final String ADDITIONAL_MORE_KEY_MARKER =
StringUtils.newSingleCodePointString(Constants.CODE_PERCENT);
/**
* Split the text containing multiple key specifications separated by commas into an array of
* key specifications.
* A key specification can contain a character escaped by the backslash character, including a
* comma character.
* Note that an empty key specification will be eliminated from the result array.
*
* @param text the text containing multiple key specifications.
* @return an array of key specification text. Null if the specified <code>text</code> is empty
* or has no key specifications.
*/
public static String[] splitKeySpecs(final String text) {
if (TextUtils.isEmpty(text)) {
return null;
}
final int size = text.length();
// Optimization for one-letter key specification.
if (size == 1) {
return text.charAt(0) == COMMA ? null : new String[] { text };
}
ArrayList<String> list = null;
int start = 0;
// The characters in question in this loop are COMMA and BACKSLASH. These characters never
// match any high or low surrogate character. So it is OK to iterate through with char
// index.
for (int pos = 0; pos < size; pos++) {
final char c = text.charAt(pos);
if (c == COMMA) {
// Skip empty entry.
if (pos - start > 0) {
if (list == null) {
list = new ArrayList<>();
}
list.add(text.substring(start, pos));
}
// Skip comma
start = pos + 1;
} else if (c == BACKSLASH) {
// Skip escape character and escaped character.
pos++;
}
}
final String remain = (size - start > 0) ? text.substring(start) : null;
if (list == null) {
return remain != null ? new String[] { remain } : null;
}
if (remain != null) {
list.add(remain);
}
return list.toArray(new String[list.size()]);
}
private static final String[] EMPTY_STRING_ARRAY = new String[0];
private static String[] filterOutEmptyString(final String[] array) {
if (array == null) {
return EMPTY_STRING_ARRAY;
}
ArrayList<String> out = null;
for (int i = 0; i < array.length; i++) {
final String entry = array[i];
if (TextUtils.isEmpty(entry)) {
if (out == null) {
out = CollectionUtils.arrayAsList(array, 0, i);
}
} else if (out != null) {
out.add(entry);
}
}
if (out == null) {
return array;
}
return out.toArray(new String[out.size()]);
}
public static String[] insertAdditionalMoreKeys(final String[] moreKeySpecs,
final String[] additionalMoreKeySpecs) {
final String[] moreKeys = filterOutEmptyString(moreKeySpecs);
final String[] additionalMoreKeys = filterOutEmptyString(additionalMoreKeySpecs);
final int moreKeysCount = moreKeys.length;
final int additionalCount = additionalMoreKeys.length;
ArrayList<String> out = null;
int additionalIndex = 0;
for (int moreKeyIndex = 0; moreKeyIndex < moreKeysCount; moreKeyIndex++) {
final String moreKeySpec = moreKeys[moreKeyIndex];
if (moreKeySpec.equals(ADDITIONAL_MORE_KEY_MARKER)) {
if (additionalIndex < additionalCount) {
// Replace '%' marker with additional more key specification.
final String additionalMoreKey = additionalMoreKeys[additionalIndex];
if (out != null) {
out.add(additionalMoreKey);
} else {
moreKeys[moreKeyIndex] = additionalMoreKey;
}
additionalIndex++;
} else {
// Filter out excessive '%' marker.
if (out == null) {
out = CollectionUtils.arrayAsList(moreKeys, 0, moreKeyIndex);
}
}
} else {
if (out != null) {
out.add(moreKeySpec);
}
}
}
if (additionalCount > 0 && additionalIndex == 0) {
// No '%' marker is found in more keys.
// Insert all additional more keys to the head of more keys.
out = CollectionUtils.arrayAsList(additionalMoreKeys, additionalIndex, additionalCount);
for (int i = 0; i < moreKeysCount; i++) {
out.add(moreKeys[i]);
}
} else if (additionalIndex < additionalCount) {
// The number of '%' markers are less than additional more keys.
// Append remained additional more keys to the tail of more keys.
out = CollectionUtils.arrayAsList(moreKeys, 0, moreKeysCount);
for (int i = additionalIndex; i < additionalCount; i++) {
out.add(additionalMoreKeys[additionalIndex]);
}
}
if (out == null && moreKeysCount > 0) {
return moreKeys;
} else if (out != null && out.size() > 0) {
return out.toArray(new String[out.size()]);
} else {
return null;
}
}
public static int getIntValue(final String[] moreKeys, final String key,
final int defaultValue) {
if (moreKeys == null) {
return defaultValue;
}
final int keyLen = key.length();
boolean foundValue = false;
int value = defaultValue;
for (int i = 0; i < moreKeys.length; i++) {
final String moreKeySpec = moreKeys[i];
if (moreKeySpec == null || !moreKeySpec.startsWith(key)) {
continue;
}
moreKeys[i] = null;
try {
if (!foundValue) {
value = Integer.parseInt(moreKeySpec.substring(keyLen));
foundValue = true;
}
} catch (NumberFormatException e) {
throw new RuntimeException(
"integer should follow after " + key + ": " + moreKeySpec);
}
}
return value;
}
public static boolean getBooleanValue(final String[] moreKeys, final String key) {
if (moreKeys == null) {
return false;
}
boolean value = false;
for (int i = 0; i < moreKeys.length; i++) {
final String moreKeySpec = moreKeys[i];
if (moreKeySpec == null || !moreKeySpec.equals(key)) {
continue;
}
moreKeys[i] = null;
value = true;
}
return value;
}
}

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