diff --git a/PUBLIC_SOURCE_MANIFEST.json b/PUBLIC_SOURCE_MANIFEST.json index 6c5095d..1326a3e 100644 --- a/PUBLIC_SOURCE_MANIFEST.json +++ b/PUBLIC_SOURCE_MANIFEST.json @@ -1,6 +1,6 @@ { "project": "蟹蟹输入 RTOS / Android手表适配改版", - "version": "1.2.4", + "version": "1.2.5", "license": "MIT", "android": true, "independent_history": true, @@ -9,7 +9,7 @@ ".gitattributes": "8e689518e5810501eb34583fa369475902805d8dd6c3ea78699469de4fa22932", ".gitignore": "cf35fe4d5c1c2ca6ae6bd343642fee3d04cd8d3799510a02ca94df73a2d4daad", "android/.gitignore": "9907f78ab6ad68ac162f15ed5554c0ea9bafecf6eb25f2388ba0c11d40fab35d", - "android/app/build.gradle": "9d0b476141e72df003d3232561fe30890d93c114197e03f614866c45c3d169b5", + "android/app/build.gradle": "8fa1849958f77341196a67609588deba991cc3bf8c17b1933fb50a3aa1fee6aa", "android/app/proguard-rules.pro": "41e72ccc70b31f2da13fc597d5196590ec25e0422c2766e1555f3348d64ac6ed", "android/app/proguard-test.pro": "dc5f87f1ec90eb24823174f80d19e93d3e69919af70891529e4c54a744988aa4", "android/app/src/androidTest/java/com/sydxky/crabinput/ImeDeviceTest.java": "d5a7284b6693ee116a9e5450b4c0e38df2a71812c79664ee04311d8088db33ac", @@ -24,12 +24,12 @@ "android/gradle.properties": "1a62f4c0a2a08044c706c378451342a11b307842b6b2ad434a86d6f970acc201", "android/gradlew": "80a4c62723bb67cbabe92c37606d380089c647184afa1bf2b8c136f0e15d2cd6", "android/gradlew.bat": "0e82fa239f8f4dca4efc83863c16c3b143cb6faab6f50cbefab4ee0bd03e5b5f", - "android/host/build.gradle": "9bede276d4c1b8f7229307d8ba9ae569472f7470a3e0dc743d9c9f0ca4fb3615", + "android/host/build.gradle": "b39919102f52577d390697057cf809f98a599ba6e7eea19a648980a54a56462c", "android/host/src/main/AndroidManifest.xml": "2013ca2761bc091216dca86d6b14987147e169902c4e784a2a047d473a7c6c95", "android/host/src/main/java/com/sydxky/crabinput/host/HostActivity.java": "91619d8681376727fd8459157db5dc1e84d261134fc3c8c9354ec01f52fafe7d", "android/keyboard/build.gradle": "512edae398ac76078488f9d2439e23032029b44b4d1bbbbe266d9e2d11effedc", "android/keyboard/consumer-rules.pro": "74ec127457bb863b30cc41be93b5f6b1eb003783addb2cda36b3fa1cf7e80432", - "android/keyboard/src/androidTest/java/com/sydxky/crabinput/SurfaceRegressionTest.java": "d92ab17b0872ec6bbd6eecee897c1376d42721c86dd380e2b0334b95055370ce", + "android/keyboard/src/androidTest/java/com/sydxky/crabinput/SurfaceRegressionTest.java": "b7fcad2114ce1945f2d26f647fc580a2c32488e83f22f869bd80f0c729eb1e6e", "android/keyboard/src/main/AndroidManifest.xml": "343d5be1ac17cdd45d1a2f2ddc616c509290e655869125a9422a96d88aa66066", "android/keyboard/src/main/assets/dictionary-manifest.json": "aab6bf005ec08396e877163c4edb835299946b99362df97ab32ded80eb3e78a4", "android/keyboard/src/main/assets/licenses/CrabInput-LICENSE": "027e84ad11fd2992040da0aad2ece7a74fdf61540dc2d74cc8a281fd76eb942c", @@ -38,26 +38,27 @@ "android/keyboard/src/main/assets/licenses/Gradle-LICENSE.txt": "cf234c4188d773406f640bf89be680d9288f40ff96901c70d643fcea96ad46df", "android/keyboard/src/main/assets/licenses/jieba-LICENSE.txt": "18ba0984839f85853b29fadaf992f7dba8fd0ca0fbeae34de2b8735222dc7a37", "android/keyboard/src/main/java/com/sydxky/crabinput/Candidate.java": "ce0ed27cb8d69d14595f6e3cbc0ac27bfac3245b8fb7f1562540054a98c81e37", - "android/keyboard/src/main/java/com/sydxky/crabinput/CandidatePager.java": "a5071ddec3cee9899b80181ef1f35c6896cdd1455df37839b600a33c29ea640e", + "android/keyboard/src/main/java/com/sydxky/crabinput/CandidatePager.java": "49b8c528c847ac3f66f84661914d5af749abe88a0dd1b6ece4be0ee6226339af", "android/keyboard/src/main/java/com/sydxky/crabinput/CandidateRanking.java": "dbd1e7cbda98225eb2f9b1361508a8f22f403c6a9a8690cc64dec34ffd0c78fc", "android/keyboard/src/main/java/com/sydxky/crabinput/CrabKeyboard.java": "1f03cda3833ba282d12eaf66ddcbb5f4354bb8b99a1d2c9de1b4f430c9effcac", - "android/keyboard/src/main/java/com/sydxky/crabinput/KeyboardController.java": "8427a7d3dbee68031ee5964ad7f22cf64d94cf70049b7e05ae2ec2aca63fade8", + "android/keyboard/src/main/java/com/sydxky/crabinput/KeyboardController.java": "0cec282f2b2f2b1e545929671b7947d13b6004910979b39b7c20b0e31b6ee25f", "android/keyboard/src/main/java/com/sydxky/crabinput/KeyboardState.java": "e5253607c117d18084ca1167868b3ef7ef0f28529700b70507c70af531d17291", - "android/keyboard/src/main/java/com/sydxky/crabinput/KeyboardSurface.java": "1ac71672ca55856a5dbbcbde4af243274b0c59d71cb74fd677ad6ea63815790e", - "android/keyboard/src/main/java/com/sydxky/crabinput/Lexicon.java": "a0e0909184bdb97b5fd8773d6f32b1ae9dee8c96ce589c37d6cabdbcfbb63cbd", - "android/keyboard/src/test/java/com/sydxky/crabinput/CandidatePagerTest.java": "7c0447f5eebea944df38179aafd789c4f2eae25ba53aff5ba9e92a2d46044c64", + "android/keyboard/src/main/java/com/sydxky/crabinput/KeyboardSurface.java": "d908d33b9e3d33207c056e5528af953b1f8409d0a16fc165407ed423c801c09b", + "android/keyboard/src/main/java/com/sydxky/crabinput/Lexicon.java": "34def8a849218ca024410f390e65c7be4d8cee151a528e7e2a3651ffd36c97fa", + "android/keyboard/src/test/java/com/sydxky/crabinput/CandidatePagerTest.java": "bb08063c90db22aa9faf0712cc3d5d5f2272f4e9a514e4bd1642926ba8e0c2df", "android/keyboard/src/test/java/com/sydxky/crabinput/CandidateRankingTest.java": "3e163c8137829de2f26fa737b4a983f0bb4544c0598f2b635e3ddc55631bcb74", "android/LICENSE": "4f3547e23d7fd227a33aaa6a075185be8299403b03e534627bbb3bc3bdcf97af", "android/NOTICE.md": "bba74020843ad841f979d346d13625e26a2cb1c0e9a2031f1a45e62957f132ab", - "android/README.md": "490bf2b1cd03a2aa11cc806edccd057876c84b3778dd7ddfe3d31831081a9442", + "android/README.md": "9441ad2df37bb239821d64529babf2e194e1c3bdc705a828a4105386ee70f564", "android/scripts/build-dictionary.py": "4643f5422f2383a56d8c0653e47e3fc6b02481f065efd924958569fb48435969", "android/scripts/capture-device.py": "34023ff4fb0ff80a8811b1222aad3b58c70b628333edb56efffa3e6adaf05ace", - "android/scripts/validate-device.py": "149f73cc3b6174f0487d95b2bd03e6792f4897036331bb8e0c1012b07dca6644", + "android/scripts/validate-device.py": "8a9252c9265807531a1e96ceb695a10a1d22fcb6d3c0b8fd18a674a1d75b4aa5", "android/settings.gradle": "73a5f1e8c25c44b0c5f983ed822c4256dea33b14b8d53115ed3be118aeaaee1d", "android/VALIDATION_1.2.0.md": "7ce04837f631f9e491ecfb96e8e056c4b01c4aa2f24a061d540c5280201848e6", "android/VALIDATION_1.2.2.md": "6d3ebe3f107099516d2dbcae025e41d516ba8d4de4a40fc9af3342ba9ac9d345", "android/VALIDATION_1.2.3.md": "603ed3903589f4718892ee4211d59c563c638ea3547c28df27f5eef04651bd33", "android/VALIDATION_1.2.4.md": "6ec3e7e2ea9d19d7e5393a8af17eb5d3e76a2db382b70f788fddac259482cb98", + "android/VALIDATION_1.2.5.md": "564221225a405d12d57b350c4acf8f05d7e034cacddb5cb534f3deda9dbac928", "build-rtos-game.ps1": "a0cf9d6fa5ba1ecc22e768373dcca2f87e1e58fc44f0a612b59c7215fb064df8", "com.sydxky.rtos.imefixture/build.ps1": "37cd2f3af83d9c1cb2f1d855d0d95cf2e822ac9e743c8370237c48c4a4d8c0a0", "com.sydxky.rtos.imefixture/glyphix.config.js": "d53b4b0e8597d9f7fb5fd3d53063d285b245669021527a1bfc3bc054c5122514", @@ -73,10 +74,10 @@ "com.sydxky.rtos.inputmethod/LICENSE": "4f3547e23d7fd227a33aaa6a075185be8299403b03e534627bbb3bc3bdcf97af", "com.sydxky.rtos.inputmethod/NOTICE.md": "eb75c7be9055410bec57688b363be4761c2ee6f2ae4cfba4ab908d0b9ce68999", "com.sydxky.rtos.inputmethod/PRIVACY.md": "19049b0bb30add6fa5071ec16e9494f835e04ee1dad83108bfec34c71e16d679", - "com.sydxky.rtos.inputmethod/README.md": "380ebdad19cc27d9358c802d1c3f5c899dad4e0f387ad8a1d5fcc496c29a51e9", + "com.sydxky.rtos.inputmethod/README.md": "5af5ae16e011b7e0c5c5c9ebcd4480751a0dcff3070910e9813eb3344114e4fa", "com.sydxky.rtos.inputmethod/scripts/build-icons.py": "51f859a9517e7dd2e5e4bf9f688ce41dc79d4bc491fc66066dfe2db4aae4d704", - "com.sydxky.rtos.inputmethod/scripts/convert-dictionary.py": "ad747c27931a683b7132e7273e4913dc2c35e110b2c6990083425a7e42817bd9", - "com.sydxky.rtos.inputmethod/scripts/install.mjs": "ab2a23bfd457e40f56f17bcf8868482b0ed2d626629e52f788f6adcc3b4ab71f", + "com.sydxky.rtos.inputmethod/scripts/convert-dictionary.py": "9dfd89a2691b43a0988f5a8180030e3a4258b86b97d444d53d6539929cb2fd22", + "com.sydxky.rtos.inputmethod/scripts/install.mjs": "6338d47aac16c95ebd4a4cf2a821bcb926689f3fbbe39b59e2e6efd50e9aef3f", "com.sydxky.rtos.inputmethod/src/app.js": "e51c7e0a67563bd64b6bc04aedde22842295824432942bc8f1ab0d4731cc3ed3", "com.sydxky.rtos.inputmethod/src/assets/crab-keyboard-license.txt": "b140337f94481310f35abfd2210fde308ba30411899095fd961a77b94e022775", "com.sydxky.rtos.inputmethod/src/assets/icon.png": "f712ac5f8c500a33dd72cc1f15f9ad467e6043c284fb6ece1f1683e54d40e683", @@ -91,19 +92,20 @@ "com.sydxky.rtos.inputmethod/src/assets/ime-icons/LICENSE.txt": "c71d239df91726fc519c6eb72d318ec65820627232b2f796219e87dcf35d0ab4", "com.sydxky.rtos.inputmethod/src/assets/jieba-license.txt": "18ba0984839f85853b29fadaf992f7dba8fd0ca0fbeae34de2b8735222dc7a37", "com.sydxky.rtos.inputmethod/src/common/association-seeds.json": "181aa6f3bd77cf2f1002e405cbbfd5240f3c75a560f1a79c42cca63b70f05b58", - "com.sydxky.rtos.inputmethod/src/common/ime-engine.js": "7b3892b79241e2fa5bc8914bfc047bf6319194c8db1d8b33ff2a63449f859b1c", + "com.sydxky.rtos.inputmethod/src/common/ime-engine.js": "aec3bc585e887fd84bd544af1090e34aa4a76714fbfaaf39dba8397d783ebe0b", "com.sydxky.rtos.inputmethod/src/common/ime-lifecycle.js": "60937c3a0d51ee3978f12302a31a550bcbe352dfcc2c62b45bcf3b4bab55e7ad", "com.sydxky.rtos.inputmethod/src/common/ime-theme.js": "628a77ca3ef9cf239eab9883497ccfb29652f5f70ff0e584de6cf0c17c005999", - "com.sydxky.rtos.inputmethod/src/components/CrabInput/index.ux": "a445026ae4fa8dbb151d7d776ecd8269dc24f1775acf4b71cec30b5b8429e272", + "com.sydxky.rtos.inputmethod/src/components/CrabInput/index.ux": "ab9f4e95165da6968f848cc71b42e2c7dfd4fd628b1d3a38a57b34df70666c82", "com.sydxky.rtos.inputmethod/src/keyboard/index.ux": "27a14680bff055c4625d7858879df7cde804b6aa399c81a07ce1079cdc60140d", "com.sydxky.rtos.inputmethod/src/main/index.ux": "f763f0dc80b9fd46e00830d6ec0de8683063f98c1edee94ee522c2605b695fca", - "com.sydxky.rtos.inputmethod/src/manifest.json": "c855f0db1d62b2da6e3f494c24134af27ef3665997dab35cc29494122b769a92", - "com.sydxky.rtos.inputmethod/tests/client.test.mjs": "197034a327872629c1662fa3a437f41c9d31a23bf74656dad8d7d31ecfa43401", - "com.sydxky.rtos.inputmethod/tests/console-native.py": "9d708993e1f3071871420a3c02c10bf422e5025c3132e6a029ec8ef036e817a2", + "com.sydxky.rtos.inputmethod/src/manifest.json": "549525d87a38520499f37dc8c7a05e8873bd8e57fe2b81ef7013e57676734b50", + "com.sydxky.rtos.inputmethod/tests/client.test.mjs": "a99efb94856d4a9eaa9b9c7ccbae288fbc15d3363ac0a0e71028a1b53dbd78bd", + "com.sydxky.rtos.inputmethod/tests/console-native.py": "ba3cc062d0206da9d45984a6d9dd07e519a9967f64ac49ad9750b4efc8c7aac9", "com.sydxky.rtos.inputmethod/tests/engine.test.mjs": "984a8cea899b8e0762773d48d56566729a9e720f402fa7b946645d0afa429e1a", "com.sydxky.rtos.inputmethod/tests/layout.py": "9b1fc238cae2f39e5385611cb3d5d636e40941cd4566c51d1407cfb72d6dbcff", "com.sydxky.rtos.inputmethod/tests/native.py": "07bb05ad97fe2ca06f7f4a55e57b914b025dd70c0a6ea0249c1195e05aed5fe5", - "com.sydxky.rtos.inputmethod/tests/sync.test.mjs": "89a65009591eb874f82aeafb425afca99c2d5336850277e1cfa2636e05345fee", + "com.sydxky.rtos.inputmethod/tests/performance.mjs": "12eb80c8303e4a9d7355f04abdecd845f152ea68215ad81e5475b44c0ae09dbe", + "com.sydxky.rtos.inputmethod/tests/sync.test.mjs": "230fd14f91425085dc5d75655844ddfee48ce429d328ef2ff3c83035ca60cc0a", "com.sydxky.rtos.inputmethod/tests/visual.py": "cdd48d7cb4851fbcb6b96af5098405f4d02e93726cf02320faa461592b948477", "com.sydxky.rtos.inputmethod/third_party/crabKeyboard/LICENSE": "b140337f94481310f35abfd2210fde308ba30411899095fd961a77b94e022775", "com.sydxky.rtos.inputmethod/third_party/crabKeyboard/lookupDictV5.original.js": "915605439ebc404d808ccce6cb18abfff3981f1cdf53a78e9a743c78c4a6ddc9", @@ -176,12 +178,20 @@ "com.sydxky.rtos.inputmethod/VALIDATION_1.2.2.md": "21792c32b065505b2b81ebbbcf7127958fbbb0d791fa121b84cd4e384ac05221", "com.sydxky.rtos.inputmethod/VALIDATION_1.2.3.md": "2311037bd6901906318dd67882e5adc5055e45086a69408f66fdf8b050f39be7", "com.sydxky.rtos.inputmethod/VALIDATION_1.2.4.md": "6c0f278ca0ab2625bf6875fd4106e0ffe45b4a15b58a36463f96ee63517a3af9", + "com.sydxky.rtos.inputmethod/VALIDATION_1.2.5.md": "8cfe75e685aa7262c948bd09e57dd00a4a5d9b1e309cca619d70279646662169", "docs/1.2.4/rect-bluewhite-middle.png": "4789f2dc33a26869dc5ce0df74891dec67d08bdeb342f38b8265d73270e96569", "docs/1.2.4/rect-bluewhite-py-lower.png": "b02bc442e2f830cbfb7c5d34f2eb55e72be5b993f61acf0b6c4bf9234211aa29", "docs/1.2.4/round-blackgreen-en-lower.png": "08e3313cd949839105eccfaddbbd2fa6d136fd09808b33e792d6f87d43dc1e06", "docs/1.2.4/round-bluewhite-en-lower.png": "e58737ca2f3cb8c11bf164a729c472344b881a6e96d778cb80601b9d10514df1", "docs/1.2.4/round-bluewhite-en-upper.png": "36373e8d3b8dd95b03ae621bccb607db891bd438a1ee1009678efd7ce4264ebf", "docs/1.2.4/round-bluewhite-middle.png": "79e7f93b52e06ec37621bd2c2ef1ff808e12c37a3c501d0f190b5b7dc1d89819", + "docs/1.2.5/PERFORMANCE.json": "393301811ca2fee19d12472f0735ba7f2068bb7ae7f31d6ab977415d59ada634", + "docs/1.2.5/rect-bluewhite-middle.png": "56ace9bbba7374c7347d84ad8beb860a1465a2860f9203127a1cb0fc3ab90d8c", + "docs/1.2.5/rect-bluewhite-py-lower.png": "ef92c657bf08e8eeffe8f6cb6c5291cbff6951db697faff6396001f96c07ae91", + "docs/1.2.5/round-blackgreen-en-lower.png": "54829d3703b378328874aa80e99d94f92d3e029cce8752198506d85335587040", + "docs/1.2.5/round-bluewhite-en-lower.png": "649c488bceab217cc985794affb9ac36a672df775d3ffe574307ac52da183b39", + "docs/1.2.5/round-bluewhite-en-upper.png": "0613d268f1d5992788be90670aebaddf0202a040508ae5475a03390922a2d19e", + "docs/1.2.5/round-bluewhite-middle.png": "c4ead177abb3c7dc330f0b228640a7a19bf17dc8973c5c4d820322fc6d56f948", "docs/screenshots/android-candidates-16.png": "d244a3fb41300a7e8c478930307cfb3d4e781f016be53a9e93615bb79717ae73", "docs/screenshots/android-english.png": "adbc1ede8f2103a6435aa04efe604737895365ff3f53e75fcb9511b9a1f4f551", "docs/screenshots/android-fourteen.png": "88b06bdd1e6a6dc6f45722e01ce7930c94845062b8bf237aad5c5d086f37641c", @@ -200,11 +210,11 @@ "docs/screenshots/rtos-round-en.png": "b94f17f121d165c33f84b0eaa4c1ca48b2dbf9890bd1ab77147965fc138de6c2", "docs/screenshots/rtos-round-num.png": "d7fcf451229861fb564b45b6ada03501aed57cf9ca5c058833b9b65dbace06eb", "docs/screenshots/rtos-round-phrase-candidates.png": "507d09901681bbeae4da39fa0a700663d89b7149f7fab01dd603630c932b4dc4", - "docs/SCREENSHOTS.md": "347e2eb2cb21aa332dbb20f8e2a8aaeb3561ffdcc7139766e92a78b0a24047a9", - "docs/UI_PREVIEW.png": "e58737ca2f3cb8c11bf164a729c472344b881a6e96d778cb80601b9d10514df1", + "docs/SCREENSHOTS.md": "d5ab2209969317d421e2664cfb2afcd96012d5b2b9d3ca54695087cc6b0d5f53", + "docs/UI_PREVIEW.png": "649c488bceab217cc985794affb9ac36a672df775d3ffe574307ac52da183b39", "LICENSE": "4f3547e23d7fd227a33aaa6a075185be8299403b03e534627bbb3bc3bdcf97af", "NOTICE.md": "eb75c7be9055410bec57688b363be4761c2ee6f2ae4cfba4ab908d0b9ce68999", - "README.md": "a36d44e8229f94915c590c88940bbc717e71aa8efb6f6e13fae7efd023c98924", + "README.md": "f05fa72c8047e6fae5ebee56d840963f04783568f719bdb6081660af9401d7f4", "tests/emulator-validate.py": "2a549e3c7bc177b942826425e548f68db39a32d8a55ade080f0d8ab3e9c00060" } } diff --git a/README.md b/README.md index 994fcb4..8c9b839 100644 --- a/README.md +++ b/README.md @@ -4,20 +4,25 @@ **MIT 开源,允许商用、修改、再分发和闭源集成。** 使用时请保留 [LICENSE](LICENSE) 和 [第三方许可](NOTICE.md)。厂商 SDK、运行时与设备固件不包含在本项目授权中。 -![1.2.4 RTOS原生测试工程界面,验证范围见截图说明](docs/UI_PREVIEW.png) +![1.2.5 RTOS原生测试工程界面,验证范围见截图说明](docs/UI_PREVIEW.png) -## 1.2.4 光标贴合、26键扩大与大小写切换 +## 1.2.5 减少内存、加速查词与等大26键 -- RTOS光标改为独立1像素细线,紧贴前后文字,保留真实空格、emoji与私密遮蔽;长文本仍保持光标可见。Android预览及示例编辑框去掉额外字间距。 -- 扩大26键,标准10/9/7三排;圆屏逐排按安全宽度收放,方屏充分利用宽度。 -- 点“abc”进入英文,再点“大写”切到大写,点“小写”恢复小写;按钮显示下一步动作,键面与实际输入同步。 -- 保留1.2.3未完成拼音补全、上下文续词及稳定刷新:全拼/九宫/14键同步支持;完全离线,密码不参与上下文。 +- RTOS运行索引91,357→2,158 B,缓存3分片/48,000字符;去掉整份词库复制,隐藏释放缓存,过期查询停止后续工作。 +- 候选按需分页,首页只更新3项,展开才创建整数页计划,只生成当前页;RTOS反应式字段40→29。Android取消过期SQLite、后台规划候选页,文字变化复用键盘布局。 +- 26字母统一宽高并放大,标准10/9/7三排,方屏利用全宽,圆屏同步调整留白、候选与底栏。保留光标贴合和大写/小写切换。 -RTOS **59项引擎、49项UX及四配置生产构建通过**。独立原生测试工程中,生产键盘组件源码逐字节相同,圆/方普通及QuickApp四配置共96个状态全部通过,光标额外布局间距0px、26个按键边界合格;各配置均实际点击输入`qQq`。96张原生截图随验收ZIP提供。 +同一词库、24种全拼/九宫/14键编码,5轮共120次查询:总耗时 227.12 → 194.87 ms,P95 6.70 → 5.01 ms,中位数 1.01 → 1.07 ms;GC后保留堆 1,149,800 → 805,848 B。总耗时减少14.2%,P95减少25.2%,保留堆减少29.9%。中位数略增,不能称所有输入都提速。 -Android **7项Java测试及OWW261后台原生组件4项通过**,包含24帧非空绘制/控件复用、补全/续词、密码隔离、实际大小写切换和圆屏边界。Release APK/AAR、生产签名及API27元数据通过。上述是组件回归,未代替最终发布APK的前台端到端验收;本版未重跑原前台交互。RTOS截图来自明确标记的原生测试工程,Android截图为历史资料。 +这是Windows Node/V8的对比,数值随主机负载变化;不是Glyphix MCU/真实手表的内存、帧率或触控延迟。原始报告及执行脚本随源码/验收ZIP提供。 -[本版RTOS验收](com.sydxky.rtos.inputmethod/VALIDATION_1.2.4.md) · [本版Android验收](android/VALIDATION_1.2.4.md) +RTOS **59项引擎、49项UX、同步检查及四配置构建通过**,独立原生组件四配置**96状态/96截图通过**,26键等大、模式和底部控件边界合格,各配置实际输入qQq。24种编码的完整候选及补全与1.2.4一致,12,000候选分页顺序完整。 + +Android **8项Java、7项OWW261后台离屏组件回归通过**:原生绘制、24次文字更新只创建一次布局、补全/续词、密码隔离、大小写点击、圆方/192px尺寸等大键和边界、查询取消/恢复。16个运行时class与发布AAR相同;正式签名APK/AAR、API27和zipalign通过。 + +这是组件回归,不替代最终APK的前台整包验收。最终Android APK未安装到共享手表,原9项前台交互及RTOS原84项整包交互未重跑。RTOS截图来自明确标记的原生测试工程,Android截图为历史资料;RTOS真机、长期性能/功耗及商店审核未验收。 + +[本版RTOS验收](com.sydxky.rtos.inputmethod/VALIDATION_1.2.5.md) · [本版Android验收](android/VALIDATION_1.2.5.md) ## 1.2.0 已有功能 @@ -33,7 +38,7 @@ Android **7项Java测试及OWW261后台原生组件4项通过**,包含24帧非 | 平台 | 接入与产物 | 已完成的验证 | | --- | --- | --- | | Glyphix RTOS | 原生 `.ux` 页面;圆屏 466×466、方屏 390×450 两屏 QuickApp 完整 ZIP | 普通/QuickApp四配置构建;59项引擎和49项UX;独立原生组件四配置96状态/截图通过 | -| Android 8.1 / API 27+ | 原生 AAR;演示 APK;独立 `host/` 示例 | OWW261后台原生组件4项通过;24帧非空绘制及控件复用;前台交互未重跑 | +| Android 8.1 / API 27+ | 原生 AAR;演示 APK;独立 `host/` 示例 | OWW261后台原生组件7项通过;24帧非空绘制、布局复用、等大键及查询取消;前台交互未重跑 | Android 版本作为**应用内公开组件**,由每个应用自行接入;组件不注册系统输入法。RTOS 页面也需宿主接入,系统级注册依赖厂商支持。 @@ -41,7 +46,7 @@ Android 版本作为**应用内公开组件**,由每个应用自行接入; ## 下载、源码与反馈 -- [v1.2.4 发行包与更新说明](https://git2.sydxky.cn/sydxky/crab-input-rtos/releases/tag/v1.2.4):AAR、演示 APK、两屏 RTOS QuickApp、原生组件验收记录/截图及 SHA-256。 +- [v1.2.5 发行包与更新说明](https://git2.sydxky.cn/sydxky/crab-input-rtos/releases/tag/v1.2.5):AAR、演示 APK、两屏 RTOS QuickApp、原生组件验收记录/截图及 SHA-256。 - [Android 接入文档](android/README.md) · [RTOS 接入文档](com.sydxky.rtos.inputmethod/README.md) - [实际界面截图](docs/SCREENSHOTS.md) · [RTOS 验收](com.sydxky.rtos.inputmethod/VALIDATION_1.2.0.md) · [Android 真机验收](android/VALIDATION_1.2.0.md) - [已有问题](https://git2.sydxky.cn/sydxky/crab-input-rtos/issues) · [免注册提交问题与建议](https://git2.sydxky.cn/feedback/sydxky/crab-input-rtos) @@ -95,7 +100,7 @@ python .\com.sydxky.rtos.inputmethod\tests\native.py --work work/native --output python .\com.sydxky.rtos.inputmethod\tests\visual.py --work work/visual --output work/evidence/visual ``` -本版光标和26键回归可在独立原生模拟器中执行,不启用Inspector: +本版光标、等大26键及完整底部按钮回归可在独立原生模拟器中执行,不启用Inspector: ```powershell python .\com.sydxky.rtos.inputmethod\tests\console-native.py --work work/caret --output work/evidence/caret diff --git a/android/README.md b/android/README.md index 9e5400b..71c01c4 100644 --- a/android/README.md +++ b/android/README.md @@ -1,11 +1,13 @@ # 蟹蟹输入 Android应用内键盘组件 -基于CrabKeyboard MIT词库,由herry重写的Android手表原生键盘组件,1.2.4 / 10204。提供可集成的 **AAR**、演示APK和独立接入示例;适用Android 8.1 / API 27及以上。 +基于CrabKeyboard MIT词库,由herry重写的Android手表原生键盘组件,1.2.5 / 10205。提供可集成的 **AAR**、演示APK和独立接入示例;适用Android 8.1 / API 27及以上。 应用接入后,点击自己的输入框自动展开内置键盘。**不注册系统输入法、不要求用户切换输入法、不改变系统默认输入法。** 圆屏按每行所在高度计算安全弦宽;4×4完整候选,单字16个一页,词组扩宽或增加行高,候选不显示拼音。九宫、标准QWERTY全拼/英文、14键、数字、三页符号、Unicode删除、按住连删及光标编辑。 完全离线,无网络或外部存储权限;组件不持久化输入内容,不启用学习。密码输入默认英文并遮蔽预览。页面隐藏/销毁时清除组件会话与候选,宿主输入框中的内容仍由宿主决定如何处理。异步查询按会话序号隔离,旧结果不能回填新的输入框。 +1.2.5 取消过期SQLite查询,后台只创建整数分页计划,当前页才生成候选单元;文字更新复用键盘布局。26字母等宽等高并扩大触控区,圆方屏安全边界通过,保留大小写切换。8项Java和7项后台原生组件测试通过;验证范围见[本版验收](./VALIDATION_1.2.5.md)。 + 1.2.4 去掉预览与示例编辑框的额外字间距,扩大26键;每排继续按实际屏幕弦宽计算安全边界。点“abc”进入英文,随后“大写/小写”按钮显示下一步动作,字母标签与实际输入保持一致。见[本版验收](./VALIDATION_1.2.4.md)。 1.2.3 增加未完成拼音补全、上下文续写及词频排序;复用预览、按键及候选控件,按帧合并刷新。联想仅在内存中使用当前输入框光标前的中文,不持久化全文,密码内容不参与。见[本版验收](./VALIDATION_1.2.3.md)。 @@ -14,10 +16,10 @@ ## 一分钟接入 -将发行版`crab-keyboard-1.2.4.aar`放入宿主`app/libs/`: +将发行版`crab-keyboard-1.2.5.aar`放入宿主`app/libs/`: ```groovy -dependencies { implementation files('libs/crab-keyboard-1.2.4.aar') } +dependencies { implementation files('libs/crab-keyboard-1.2.5.aar') } ``` ```java diff --git a/android/VALIDATION_1.2.5.md b/android/VALIDATION_1.2.5.md new file mode 100644 index 0000000..f889f6a --- /dev/null +++ b/android/VALIDATION_1.2.5.md @@ -0,0 +1,13 @@ +# Android 1.2.5 内存、刷新及等大26键验收 + +2026-11-14,版本1.2.5/10205,最低Android8.1/API27。 + +候选分页只保留整数页起点,后台线程计算页计划,当前页才生成单元对象;不再每次查词创建所有候选页。取消旧Future同时通过CancellationSignal取消SQLite读取。预编译中文上下文正则。键盘结构未变时只更新文字及候选事件,不重建Placements或逐键重设样式;仍按帧合并刷新并复用控件,避免输入闪动。 + +26字母维持标准10/9/7三排,以三排安全弦宽计算统一单元宽度,方屏充分使用宽度,缩小间距并增加高度。所有字母等宽等高(像素取整最多1px),保留明确的大写/小写按钮和qQq实际输入。圆屏同步保留底部操作区。 + +8项Java测试通过,包含12,000候选的按需分页、全量顺序及完整性。Release APK/AAR、宿主、后台组件测试APK构建通过,SQLite无损回读104,837组索引/163,238候选。lint 0错误:演示1项备份声明警告,组件2项测试依赖提示。最终APK为1.2.5/10205,API27、zipalign、原生产证书1名签名者和v2签名通过。APK SHA-256:b61729cb9698be794c2eb406d3eb3c9c770ad727e67d6f2fdf722aa9f220eea2。 + +OWW261 Android11/API30上7项后台离屏原生组件测试通过:24帧实际measure/layout/draw文字非空、控件不重建;相同尺寸连续24次文字/候选更新只创建一次布局;补全/续词只提交一次;密码上下文隔离;实际点击大小写切换输入qQq;模拟圆形466/192与方形466、390×450、192尺寸的26键等大、全部控件矩形及圆屏按钮四角边界;取消词库查询及后续恢复。 + +测试未打开Activity、切换前台或修改系统默认输入法,使用debug组件,16个运行时class与发布AAR逐字节相同。最终APK没有安装到共享手表,原9项前台交互未重跑;这些组件测试不代替发布APK前台端到端视觉验收。没有新的Android前台截图。没有Android进程内存、真实打字延迟、长期功耗测量;另一款实体方表和商店审核未验收。 diff --git a/android/app/build.gradle b/android/app/build.gradle index 2dc3066..6a46f9b 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -12,8 +12,8 @@ android { applicationId 'com.sydxky.crabinput' minSdk 27 targetSdk 35 - versionCode 10204 - versionName '1.2.4' + versionCode 10205 + versionName '1.2.5' testInstrumentationRunner 'androidx.test.runner.AndroidJUnitRunner' } signingConfigs { diff --git a/android/host/build.gradle b/android/host/build.gradle index 50ce907..1273494 100644 --- a/android/host/build.gradle +++ b/android/host/build.gradle @@ -2,7 +2,7 @@ plugins { id 'com.android.application' } android { namespace 'com.sydxky.crabinput.host' compileSdk 37 - defaultConfig { applicationId 'com.sydxky.crabinput.host'; minSdk 27; targetSdk 35; versionCode 10204; versionName '1.2.4' } + defaultConfig { applicationId 'com.sydxky.crabinput.host'; minSdk 27; targetSdk 35; versionCode 10205; versionName '1.2.5' } compileOptions { sourceCompatibility JavaVersion.VERSION_11; targetCompatibility JavaVersion.VERSION_11 } } dependencies { implementation project(':keyboard') } diff --git a/android/keyboard/src/androidTest/java/com/sydxky/crabinput/SurfaceRegressionTest.java b/android/keyboard/src/androidTest/java/com/sydxky/crabinput/SurfaceRegressionTest.java index f56ff44..dcd8513 100644 --- a/android/keyboard/src/androidTest/java/com/sydxky/crabinput/SurfaceRegressionTest.java +++ b/android/keyboard/src/androidTest/java/com/sydxky/crabinput/SurfaceRegressionTest.java @@ -1,10 +1,13 @@ package com.sydxky.crabinput; import android.content.Context; +import android.content.res.Configuration; import android.graphics.Bitmap; import android.graphics.Canvas; import android.graphics.Color; import android.os.SystemClock; +import android.os.CancellationSignal; +import android.os.OperationCanceledException; import android.text.Editable; import android.text.InputType; import android.view.ContextThemeWrapper; @@ -22,6 +25,7 @@ import static org.junit.Assert.*; /** Native offscreen regression: no Activity, foreground switch or system IME. */ public class SurfaceRegressionTest { private Context context(){return new ContextThemeWrapper(InstrumentationRegistry.getInstrumentation().getTargetContext(),android.R.style.Theme_Material_Light_NoActionBar);} + private Context context(boolean round){Context base=context();Configuration configuration=new Configuration(base.getResources().getConfiguration());configuration.screenLayout=(configuration.screenLayout&~Configuration.SCREENLAYOUT_ROUND_MASK)|(round?Configuration.SCREENLAYOUT_ROUND_YES:Configuration.SCREENLAYOUT_ROUND_NO);return new ContextThemeWrapper(base.createConfigurationContext(configuration),android.R.style.Theme_Material_Light_NoActionBar);} private void ui(Runnable action){InstrumentationRegistry.getInstrumentation().runOnMainSync(action);} private View find(View root,String description){ if(description.contentEquals(root.getContentDescription()==null?"":root.getContentDescription()))return root; @@ -98,4 +102,41 @@ public class SurfaceRegressionTest { }finally{controller.close();} }); } + @Test public void enlargedAlphabetKeysHaveEqualSizeOnRoundAndRectangularScreens(){ + ui(()->{ + for(boolean round:new boolean[]{false,true})for(int[] size:round?new int[][]{{466,466},{192,192}}:new int[][]{{466,466},{390,450},{192,192}})for(String mode:new String[]{"py","en"}){ + Context themed=context(round);KeyboardState state=new KeyboardState();state.mode=mode; + KeyboardSurface surface=new KeyboardSurface(themed,state,new KeyboardSurface.Actions(){public void action(String v){}public void choose(Candidate v){}public void repeatDelete(boolean v){}}); + int width=size[0],height=size[1];surface.measure(View.MeasureSpec.makeMeasureSpec(width,View.MeasureSpec.EXACTLY),View.MeasureSpec.makeMeasureSpec(height,View.MeasureSpec.EXACTLY));surface.layout(0,0,width,height); + int minW=Integer.MAX_VALUE,maxW=0,minH=Integer.MAX_VALUE,maxH=0; + for(char letter:"qwertyuiopasdfghjklzxcvbnm".toCharArray()){ + View key=find(surface,String.valueOf(letter));assertNotNull(key);minW=Math.min(minW,key.getWidth());maxW=Math.max(maxW,key.getWidth());minH=Math.min(minH,key.getHeight());maxH=Math.max(maxH,key.getHeight()); + } + assertTrue("All 26 letters have the same width within pixel rounding",maxW-minW<=1);assertTrue("All 26 letters have the same height within pixel rounding",maxH-minH<=1);assertTrue(minW>width*.07);assertTrue(minH>height*.13); + for(int i=0;i0&&child.getHeight()>0&&child.getLeft()>=0&&child.getTop()>=0&&child.getRight()<=width&&child.getBottom()<=height); + if(round&&child instanceof android.widget.Button)for(int x:new int[]{child.getLeft(),child.getRight()})for(int y:new int[]{child.getTop(),child.getBottom()})assertTrue("Round buttons stay inside safe screen",Math.hypot(x-width/2.0,y-height/2.0)<=width/2.0-1); + } + } + }); + } + @Test public void sameSizeEditsDoNotRebuildTheKeyboard(){ + ui(()->{ + KeyboardState state=new KeyboardState();state.mode="py";state.candidates=Arrays.asList(new Candidate("你好",5)); + KeyboardSurface surface=new KeyboardSurface(context(),state,new KeyboardSurface.Actions(){public void action(String v){}public void choose(Candidate v){}public void repeatDelete(boolean v){}}); + for(int frame=0;frame<24;frame++){ + state.preview="输入"+frame;state.composition=frame%2==0?"niha":"nihao";state.busy=frame%2==0;state.candidates=Arrays.asList(new Candidate(frame%2==0?"你好":"您好",5)); + surface.forceLayout();surface.measure(View.MeasureSpec.makeMeasureSpec(466,View.MeasureSpec.EXACTLY),View.MeasureSpec.makeMeasureSpec(466,View.MeasureSpec.EXACTLY));surface.layout(0,0,466,466); + assertEquals("Text-only updates reuse placements and key layout",1,surface.layoutBuilds);assertEquals("输入"+frame,((TextView)find(surface,"编辑光标")).getText().toString());assertNotNull(find(surface,frame%2==0?"你好":"您好")); + } + }); + } + @Test public void cancelledDictionaryQueryDoesNotBreakFollowingInput()throws Exception{ + try(Lexicon lexicon=new Lexicon(context())){ + CancellationSignal signal=new CancellationSignal();signal.cancel(); + try{lexicon.lookup("nihao","py","",signal);fail("Cancelled lookup must stop");}catch(OperationCanceledException expected){} + try{lexicon.predict("你好",signal);fail("Cancelled continuation must stop");}catch(OperationCanceledException expected){} + assertEquals("你好",lexicon.lookup("niha","py").get(0).word);assertEquals("世界",lexicon.predict("你好").get(0).word); + } + } } diff --git a/android/keyboard/src/main/java/com/sydxky/crabinput/CandidatePager.java b/android/keyboard/src/main/java/com/sydxky/crabinput/CandidatePager.java index da99544..9e7fa6c 100644 --- a/android/keyboard/src/main/java/com/sydxky/crabinput/CandidatePager.java +++ b/android/keyboard/src/main/java/com/sydxky/crabinput/CandidatePager.java @@ -2,6 +2,8 @@ package com.sydxky.crabinput; import java.util.ArrayList; import java.util.List; +import java.util.AbstractList; +import java.util.RandomAccess; /** 4 columns x 4 row units; long phrases get full width and extra row units. */ public final class CandidatePager { @@ -16,7 +18,7 @@ public final class CandidatePager { public int slots; Row(int units) { this.units=units; } } - public static List> pages(List candidates) { + private static List> materialize(List candidates) { List> pages=new ArrayList<>();List page=new ArrayList<>(); Row row=null;int used=0; for(Candidate candidate:candidates) { @@ -33,5 +35,27 @@ public final class CandidatePager { if(!page.isEmpty())pages.add(page); return pages; } + private static final class Pages extends AbstractList> implements RandomAccess { + private final List candidates; + private final List starts=new ArrayList<>(); + Pages(List candidates){ + this.candidates=candidates;int used=0,slots=0,rowUnits=0; + for(int i=0;i0&&(slots+span>4||rowUnits!=units)){used+=rowUnits;slots=0;} + if(starts.isEmpty()||used+units>4){starts.add(i);used=0;slots=0;} + if(slots==0)rowUnits=units; + slots+=span;if(slots==4){used+=rowUnits;slots=0;} + } + } + @Override public int size(){return starts.size();} + @Override public List get(int page){ + if(page<0||page>=size())throw new IndexOutOfBoundsException(); + int end=page+1> pages(List candidates){return new Pages(candidates);} private CandidatePager() {} } diff --git a/android/keyboard/src/main/java/com/sydxky/crabinput/KeyboardController.java b/android/keyboard/src/main/java/com/sydxky/crabinput/KeyboardController.java index 8c6af8f..8f82981 100644 --- a/android/keyboard/src/main/java/com/sydxky/crabinput/KeyboardController.java +++ b/android/keyboard/src/main/java/com/sydxky/crabinput/KeyboardController.java @@ -12,6 +12,7 @@ import java.util.List; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.Future; +import android.os.CancellationSignal; /** One transient in-app editing session. It never selects a system IME. */ final class KeyboardController implements KeyboardSurface.Actions,AutoCloseable { @@ -25,6 +26,7 @@ final class KeyboardController implements KeyboardSurface.Actions,AutoCloseable private final Runnable dismiss; private final Context context; private Future pending; + private CancellationSignal cancellation; private int revision; private boolean active=true,repeat,closed; private final Runnable lookup=()->query(revision,state.composition,state.mode,contextText()); @@ -37,7 +39,7 @@ final class KeyboardController implements KeyboardSurface.Actions,AutoCloseable state.mode=kind==InputType.TYPE_CLASS_NUMBER||kind==InputType.TYPE_CLASS_PHONE?"num":state.secret||variation==InputType.TYPE_TEXT_VARIATION_URI||variation==InputType.TYPE_TEXT_VARIATION_EMAIL_ADDRESS?"en":"t9"; surface=new KeyboardSurface(context,state,this);refresh(); } - private void invalidateLookup(){revision++;main.removeCallbacks(lookup);if(pending!=null){pending.cancel(true);pending=null;}state.busy=false;} + private void invalidateLookup(){revision++;main.removeCallbacks(lookup);if(cancellation!=null){cancellation.cancel();cancellation=null;}if(pending!=null){pending.cancel(true);pending=null;}state.busy=false;} private void refresh(){ if(active&&!state.secret){CharSequence text=input.getTextBeforeCursor(24,0);state.preview=text==null?"":text.toString();}else state.preview=""; if(!repeat)surface.refresh(); @@ -54,8 +56,9 @@ final class KeyboardController implements KeyboardSurface.Actions,AutoCloseable state.prediction=prediction;state.busy=true;main.postDelayed(lookup,40);refresh(); } private void query(int owner,String composition,String mode,String context){ + CancellationSignal signal=new CancellationSignal();cancellation=signal; pending=worker.submit(()->{ - try {List result=composition.isEmpty()?lexicon.predict(context):lexicon.lookup(composition,mode,context);main.post(()->{if(!active||revision!=owner)return;state.candidates=result;state.pages=CandidatePager.pages(result);state.page=0;state.busy=false;state.message=result.isEmpty()&&!composition.isEmpty()?"暂无匹配,请退格修改":state.prediction?"联想":"离线输入";refresh();});} + try {List result=composition.isEmpty()?lexicon.predict(context,signal):lexicon.lookup(composition,mode,context,signal);List> pages=CandidatePager.pages(result);main.post(()->{if(!active||revision!=owner)return;state.candidates=result;state.pages=pages;state.page=0;state.busy=false;state.message=result.isEmpty()&&!composition.isEmpty()?"暂无匹配,请退格修改":state.prediction?"联想":"离线输入";refresh();});} catch(Exception ignored){main.post(()->{if(active&&revision==owner){clearCandidates();state.busy=false;state.message="词库暂不可用,可切换英文";refresh();}});} }); } diff --git a/android/keyboard/src/main/java/com/sydxky/crabinput/KeyboardSurface.java b/android/keyboard/src/main/java/com/sydxky/crabinput/KeyboardSurface.java index 288c95e..d7e044a 100644 --- a/android/keyboard/src/main/java/com/sydxky/crabinput/KeyboardSurface.java +++ b/android/keyboard/src/main/java/com/sydxky/crabinput/KeyboardSurface.java @@ -34,7 +34,11 @@ final class KeyboardSurface extends ViewGroup { private final Set used=new HashSet<>(); private boolean refreshPosted; private int builtWidth,builtHeight; - private final Runnable render=()->{refreshPosted=false;if(builtWidth>0)build(builtWidth,builtHeight);requestLayout();invalidate();}; + private String builtKey=""; + private List builtCandidates; + int layoutBuilds; + private final Runnable render=this::renderFrame; + private void renderFrame(){refreshPosted=false;if(builtWidth>0){if(!state.expanded&&!state.tools&&layoutKey().equals(builtKey))updateContent();else{build(builtWidth,builtHeight);requestLayout();}}else requestLayout();invalidate();} private float sx=1,sy=1; private static final class Placement { final View view;final Rect bounds; @@ -95,10 +99,28 @@ final class KeyboardSurface extends ViewGroup { float inset=inset(y,height),width=410-2*inset,cell=width/captions.length; for(int i=0;iactions.choose(candidate)); + } + builtCandidates=state.candidates; + } private void build(int width,int height) { - builtWidth=width;builtHeight=height;sx=width/410f;sy=height/410f;placements.clear();used.clear(); + builtWidth=width;builtHeight=height;builtKey=layoutKey();builtCandidates=state.candidates;layoutBuilds++;sx=width/410f;sy=height/410f;placements.clear();used.clear(); try{ - float top=round?28:7; + float top=round?20:7; float headerInset=inset(top,23); action("⌄","hide",headerInset,top,32,23,23,false); String title=state.expanded?"完整候选":state.tools?"编辑工具":"蟹蟹输入"; @@ -113,9 +135,7 @@ final class KeyboardSurface extends ViewGroup { if(state.expanded) { buildCandidates(previewY+38);return; } if(state.tools) { buildTools(previewY+40);return; } float compositionY=previewY+32; - String composition=state.composition; - if(state.mode.equals("14")) { StringBuilder display=new StringBuilder();for(char ch:composition.substring(Math.max(0,composition.length()-6)).toCharArray())display.append(Lexicon.GROUP14["0123456789abcd".indexOf(ch)].toUpperCase(Locale.ROOT)).append(' ');composition=display.toString(); } - TextView compose=label("composition",composition.isEmpty()?modeName():composition,15,BLUE);compose.setMaxLines(1); + TextView compose=label("composition",compositionText(),15,BLUE);compose.setMaxLines(1); float ci=inset(compositionY,23);place(compose,ci,compositionY,410-2*ci,23); float candidateY=compositionY+26; if(isChinese()) { @@ -138,9 +158,17 @@ final class KeyboardSurface extends ViewGroup { } float keyHeight=captions.length==4?36:49; // Hide an unused suggestion strip in direct entry modes to enlarge the keys. - if(!isChinese()){keyHeight=captions.length==4?43:66;} - else if(state.mode.equals("py")){keyHeight=55;} - for(int i=0;i=7?21:24); + if(!isChinese()){keyHeight=captions.length==4?43:round?68:77;} + else if(state.mode.equals("py")){keyHeight=round?56:61;} + boolean qwerty=state.mode.equals("py")||state.mode.equals("en"); + float common=Float.MAX_VALUE; + if(qwerty)for(int i=0;i=7?21:24);continue;} + float left=(410-common*captions[i].length)/2; + for(int j=0;j predict(String context) throws Exception { + return predict(context,null); + } + public List predict(String context,CancellationSignal signal) throws Exception { + if(signal!=null)signal.throwIfCanceled(); List result=new ArrayList<>(); - java.util.regex.Matcher match=java.util.regex.Pattern.compile("[\\u3400-\\u9fff]{1,4}$").matcher(context==null?"":context); + java.util.regex.Matcher match=CONTEXT.matcher(context==null?"":context); if(!match.find())return result; open();String tail=match.group();Set seen=new HashSet<>(); for(int length=tail.length();length>0&&result.size()<8;length--){ - try(Cursor cursor=db.rawQuery("SELECT word,frequency FROM associations WHERE prefix=? ORDER BY frequency DESC,word LIMIT 8",new String[]{tail.substring(tail.length()-length)})){ + try(Cursor cursor=db.rawQuery("SELECT word,frequency FROM associations WHERE prefix=? ORDER BY frequency DESC,word LIMIT 8",new String[]{tail.substring(tail.length()-length)},signal)){ while(cursor.moveToNext()&&result.size()<8){String word=cursor.getString(0);if(seen.add(word))result.add(new Candidate(word,0,cursor.getInt(1),false));} } } return result; } public List lookup(String input,String mode,String context) throws Exception { + return lookup(input,mode,context,null); + } + public List lookup(String input,String mode,String context,CancellationSignal signal) throws Exception { + if(signal!=null)signal.throwIfCanceled(); List result=new ArrayList<>(); String pattern=mode.equals("14")?"[0-9a-d]{1,32}":mode.equals("t9")?"[2-9]{1,32}":"[a-z]{1,32}"; if(!input.matches(pattern))return result;open(); @@ -63,7 +74,7 @@ public final class Lexicon implements AutoCloseable { for(int i=0;i0)marks.append(',');marks.append('?');prefixes[i]=input.substring(0,i+1);} String order=mode.equals("py")?"length(e.pinyin) DESC,e.pinyin,w.ordinal":"length(e.pinyin) DESC,w.frequency DESC,e.pinyin,w.ordinal"; Set seen=new HashSet<>(); - try(Cursor cursor=db.rawQuery("SELECT w.word,length(e.pinyin),w.frequency FROM entries e JOIN words w ON w.entry=e.id WHERE e."+column+" IN ("+marks+") ORDER BY "+order,prefixes)) { + try(Cursor cursor=db.rawQuery("SELECT w.word,length(e.pinyin),w.frequency FROM entries e JOIN words w ON w.entry=e.id WHERE e."+column+" IN ("+marks+") ORDER BY "+order,prefixes,signal)) { while(cursor.moveToNext()) { if(Thread.currentThread().isInterrupted())return new ArrayList<>(); String word=cursor.getString(0);int consume=cursor.getInt(1); @@ -72,7 +83,7 @@ public final class Lexicon implements AutoCloseable { } if(input.length()>=2){ String sql="SELECT w.word,w.frequency FROM entries e JOIN words w ON w.entry=e.id WHERE e."+column+">? AND e."+column+"(); @@ -81,7 +92,7 @@ public final class Lexicon implements AutoCloseable { } } } - CandidateRanking.sort(result,input.length(),predict(context)); + CandidateRanking.sort(result,input.length(),predict(context,signal)); return result; } @Override public void close() { if(db!=null){db.close();db=null;} } diff --git a/android/keyboard/src/test/java/com/sydxky/crabinput/CandidatePagerTest.java b/android/keyboard/src/test/java/com/sydxky/crabinput/CandidatePagerTest.java index 27344b3..53598d4 100644 --- a/android/keyboard/src/test/java/com/sydxky/crabinput/CandidatePagerTest.java +++ b/android/keyboard/src/test/java/com/sydxky/crabinput/CandidatePagerTest.java @@ -20,6 +20,13 @@ public class CandidatePagerTest { for(List page:pages)assertTrue(page.stream().mapToInt(row->row.units).sum()<=4); } @Test public void emptyListHasNoPhantomButtons(){assertTrue(CandidatePager.pages(new ArrayList<>()).isEmpty());} + @Test public void largeDictionaryMaterializesOnlyTheRequestedPage(){ + List input=new ArrayList<>();for(int i=0;i<12000;i++)input.add(new Candidate(i%13==0?"完整词组":String.valueOf((char)(0x4E00+i%1200)),3)); + List> pages=CandidatePager.pages(input);List restored=new ArrayList<>(); + assertTrue(pages.size()>700);assertNotSame("Pages are created on demand",pages.get(0),pages.get(0)); + for(List page:pages){int cells=0,units=0;for(CandidatePager.Row row:page){units+=row.units;for(CandidatePager.Cell cell:row.cells){restored.add(cell.candidate);cells++;}}assertTrue(cells<=16);assertTrue(units<=4);} + assertEquals("No candidate is omitted or reordered",input,restored); + } @Test public void pairedAndT9MappingsCoverExpectedWords(){ assertEquals("c3754",Lexicon.encode("nihao","14"));assertEquals("64426",Lexicon.encode("nihao","t9"));assertEquals("",Lexicon.encode("a!","14")); } diff --git a/android/scripts/validate-device.py b/android/scripts/validate-device.py index 695a2ca..a517ef6 100644 --- a/android/scripts/validate-device.py +++ b/android/scripts/validate-device.py @@ -31,7 +31,7 @@ def main(): command+=[('com.sydxky.crabinput.keyboard.test'if args.background_regression else'com.sydxky.crabinput.test')+'/androidx.test.runner.AndroidJUnitRunner'] result=subprocess.run(command,stdout=subprocess.PIPE,stderr=subprocess.STDOUT,timeout=240) output=result.stdout.decode('utf-8',errors='replace');(args.output/'instrumentation.txt').write_text(output,encoding='utf-8');print(output,flush=True) - expected=4 if args.background_regression else 9 + expected=7 if args.background_regression else 9 passed=result.returncode==0 and f'OK ({expected} tests)'in output and 'FAILURES!!!'not in output version=re.search(r"versionName\s+'([^']+)'",(APP/'app/build.gradle').read_text(encoding='utf-8')).group(1) report={'model':model,'sdk':sdk,'version':version,'release_apk_sha256':hashlib.sha256(apk.read_bytes()).hexdigest(),'instrumentation_tests':expected,'verification_mode':'native_offscreen_component'if args.background_regression else'interactive','interactive_tests_run':not args.background_regression,'component_variant':'debug'if args.background_regression else'release','embedded_component':True,'system_ime_unchanged':adb('shell','settings','get','secure','default_input_method')==before,'passed':passed} diff --git a/com.sydxky.rtos.inputmethod/README.md b/com.sydxky.rtos.inputmethod/README.md index ac73878..097e6cc 100644 --- a/com.sydxky.rtos.inputmethod/README.md +++ b/com.sydxky.rtos.inputmethod/README.md @@ -2,7 +2,9 @@ **蟹蟹输入 RTOS适配改版**:基于CrabKeyboard的独立改版,RTOS适配代码采用[MIT协议](./LICENSE),允许商用、修改及再分发,须保留版权与许可。上游与第三方署名见[NOTICE.md](./NOTICE.md)。公开源码:;免注册[匿名反馈](https://git2.sydxky.cn/feedback/sydxky/crab-input-rtos)。 -独立应用 `com.sydxky.rtos.inputmethod`,版本 `1.2.4 / 10204`。基于 [CrabKeyboard](https://github.com/crabPangXie/crabKeyboard) 的 MIT 全量词库和拼音前缀消费方式,重写为 Glyphix `.ux` 页面;不依赖 HarmonyOS HML、Android 输入法服务、DOM 或网络。 +独立应用 `com.sydxky.rtos.inputmethod`,版本 `1.2.5 / 10205`。基于 [CrabKeyboard](https://github.com/crabPangXie/crabKeyboard) 的 MIT 全量词库和拼音前缀消费方式,重写为 Glyphix `.ux` 页面;不依赖 HarmonyOS HML、Android 输入法服务、DOM 或网络。 + +1.2.5 降低查词及候选开销:2,158 B运行索引,3个分片/48,000字符LRU,去掉整份词库复制,按需生成当前候选页,隐藏即释放缓存。26字母统一大小并放大,圆屏同步调整留白、候选栏和底部操作。详见[本版验收](./VALIDATION_1.2.5.md)。 1.2.4 将字体字符光标换成独立1像素细线,光标紧贴前后文字,保留真实空格与Unicode边界;扩大26键,逐排适配圆屏安全区域。英文模式按钮明确显示下一步“大写/小写”,切换后实际输入大小写一致。验证范围见[本版验收](./VALIDATION_1.2.4.md)。 @@ -68,7 +70,7 @@ python .\tests\visual.py --work '<另一个临时目录>' --output '<原生证 ## 内存与词库 -`convert-dictionary.py` 将原始 V5 trie 无损转换为 **6779** 个编码分片(九宫/全拼4194个、14键2585个),最大分片 **116,686** 字节。九宫/全拼使用四位前缀,14键使用三位前缀;短编码单独索引。解码缓存限制为四项且 JSON 文本合计不超过 90,000 字符,大分片不缓存。仅当前页最多16个候选进入展开网格,所有候选对象不放进响应式状态。转换器分别回读两种索引,验证与原始索引完全相同;不改写原始字典。 +`convert-dictionary.py` 将原始 V5 trie 无损转换为 **6779** 个编码分片(九宫/全拼4194个、14键2585个),最大分片 **116,686** 字节。九宫/全拼使用四位前缀,14键使用三位前缀;短编码单独索引。解码缓存限制为三项且 JSON 文本合计不超过 48,000 字符,大分片不缓存。仅当前页最多16个候选进入展开网格,所有候选对象不放进响应式状态。转换器分别回读两种索引,验证与原始索引完全相同;不改写原始字典。 初期两位编码分片曾触发原生模拟器内存回收,已改为四位编码分片。验收保持既有 `--memory-profile 8192`,没有通过增大模拟器内存规避问题。模拟器耗时不能替代真机帧率、延迟或功耗结论。 diff --git a/com.sydxky.rtos.inputmethod/VALIDATION_1.2.5.md b/com.sydxky.rtos.inputmethod/VALIDATION_1.2.5.md new file mode 100644 index 0000000..0d063b2 --- /dev/null +++ b/com.sydxky.rtos.inputmethod/VALIDATION_1.2.5.md @@ -0,0 +1,23 @@ +# RTOS 1.2.5 内存、查词及等大26键验收 + +2026-11-14,版本1.2.5/10205,8192 KiB原生模拟器配置。 + +运行索引从91,357 B审计meta.json改为2,158 B的存在位图runtime.json;完整meta、原始词库和第三方声明保留。缓存限制3个分片及48,000个JSON字符,真正LRU,大分片不缓存,页面隐藏释放;清理期间的旧异步读取不能重新填满缓存。宿主同步工具同时复制运行索引。 + +移除全词库entries/frequencies复制,26键直接定位输入前缀,九宫/14键保留相关分片消歧。补全只保留12个最优且未被完整匹配覆盖的候选,保留零词频补全。过期查询在读文件后结束。全部匹配结果仍可分页浏览;只有当前最多3项首页候选进入响应式数据,展开时建立整数页起点,翻页只创建当前最多16个单元;反应式字段40降至29。 + +26字母维持10/9/7三排,三排使用一致的宽高。方屏利用全宽;圆屏调整上下留白、候选栏与模式栏,扩大字母触控区且保留完整底部操作。独立1px光标和大写/小写行为保持。 + +## 算法与构建 + +59项引擎、49项UX、组件同步测试及普通/QuickApp四配置生产构建通过。词库保留104,837组索引、163,238候选,两种索引完整回读一致。24种编码的完整匹配及补全与1.2.4逐项一致;12,000个混合候选恢复为原顺序,808页且当前页最多15项;7,291个运行索引存在位与审计清单完全一致。另验证完整匹配/补全去重、零词频补全、旧清单兼容与清理隔离。 + +同一词库、24种全拼/九宫/14键编码,5轮共120次查询:总耗时 227.12 → 194.87 ms,P95 6.70 → 5.01 ms,中位数 1.01 → 1.07 ms;GC后保留堆 1,149,800 → 805,848 B。总耗时减少14.2%,P95减少25.2%,保留堆减少29.9%。中位数略增,不能称所有输入都提速。 + +这是Windows Node/V8的对比,数值随主机负载变化;不是Glyphix MCU/真实手表的内存、帧率或触控延迟。原始报告及执行脚本随源码/验收ZIP提供。 + +## 原生组件与视觉 + +生产CrabInput组件逐字节复制到独立自动测试入口工程,报告保留组件和路由摘要。圆/方普通和QuickApp四配置共96状态/96张原生截图通过。检查8种文本/光标/私密状态、两主题与全拼/英文26键;26键宽高取整误差最多1px。模式切换与空格/删除/完成按钮同时检查矩形及圆屏四角安全边界;人工核对圆屏底部控件没有被父容器裁切。每配置实际点击字母和大小写按钮提交qQq。未使用或接管Inspector。 + +这些是原生测试工程的组件回归,不是RTOS真实手表或最终发布包全套端到端验收。原84项整包前后台交互未重跑;RTOS真机、长期性能/功耗和商店审核未验收。 diff --git a/com.sydxky.rtos.inputmethod/scripts/convert-dictionary.py b/com.sydxky.rtos.inputmethod/scripts/convert-dictionary.py index 69aa672..2ad6b70 100644 --- a/com.sydxky.rtos.inputmethod/scripts/convert-dictionary.py +++ b/com.sydxky.rtos.inputmethod/scripts/convert-dictionary.py @@ -48,6 +48,19 @@ def from94(value): number = number * 94 + ord(ch) - 33 return number +def presence(names,alphabet,lengths,prefix): + masks=[] + for length in lengths: + mask=bytearray((len(alphabet)**length+7)//8) + for name in names: + code=name[len(prefix):] if name.startswith(prefix) else '' + if len(code)!=length or any(ch not in alphabet for ch in code):continue + index=0 + for ch in code:index=index*len(alphabet)+alphabet.index(ch) + mask[index//8]|=1<<(index%8) + masks.append(mask.hex()) + return masks + def convert(source, target): target.parent.mkdir(parents=True, exist_ok=True) assert target.resolve().is_relative_to((APP/'src/assets').resolve()) @@ -124,6 +137,15 @@ def _convert(source, target): predictive_largest_shard_bytes=max((target/(name+'.json')).stat().st_size for name in [*association_shards,*completion_shards]), source_files={name:hashlib.sha256(data).hexdigest() for name,data in sorted(files.items())}) if frequency_path.exists():report['t9_frequency_source']={'project':'jieba 0.42.1','license':'MIT','sha256':hashlib.sha256(frequency_path.read_bytes()).hexdigest()} + # Runtime needs only presence bits, not thousands of filenames and audit data. + runtime={'version':1,'short':presence(shards,'23456789',range(1,4),'s'), + 'long':presence([name for name in shards if re.fullmatch('[2-9]{4}',name)],'23456789',[4],''), + 'dualShort':presence(shards,'0123456789abcd',range(1,3),'ds'), + 'dualLong':presence([name for name in shards if re.fullmatch('d[0-9a-d]{3}',name)],'0123456789abcd',[3],'d'), + 'associations':presence(association_shards,'0123456789abcdef',[2],'a'), + 'completions':presence(completion_shards,'0123456789abcdef',[2],'c')} + output=target/'runtime.json';data=(json.dumps(runtime,separators=(',',':'))+'\n').encode('utf-8') + if not output.exists() or output.read_bytes()!=data:output.write_bytes(data) output=target/'meta.json';data=(json.dumps(report, ensure_ascii=False, indent=2)+'\n').encode('utf-8') if not output.exists() or output.read_bytes()!=data:output.write_bytes(data) print(json.dumps({**{k:v for k,v in report.items() if k not in ['source_files','shards']},'shards':len(shards)}, ensure_ascii=False)) diff --git a/com.sydxky.rtos.inputmethod/scripts/install.mjs b/com.sydxky.rtos.inputmethod/scripts/install.mjs index 19d1429..6d252f6 100644 --- a/com.sydxky.rtos.inputmethod/scripts/install.mjs +++ b/com.sydxky.rtos.inputmethod/scripts/install.mjs @@ -14,7 +14,7 @@ function dictionaryReady(source){ if(fs.existsSync(path.join(source,'src/assets/.ime-dict.generating')))throw Error('Dictionary generation is running; retry after it completes') const converter=path.join(source,'scripts/convert-dictionary.py'),metaFile=path.join(source,'src/assets/ime-dict/meta.json') if(!fs.existsSync(converter))return fs.existsSync(metaFile) - if(!fs.existsSync(metaFile))return false + if(!fs.existsSync(metaFile)||!fs.existsSync(path.join(source,'src/assets/ime-dict/runtime.json')))return false const meta=readJson(metaFile) const seeds=path.join(source,'src/common/association-seeds.json') if(fs.existsSync(seeds)&&meta.association_seed_sha256!==digest(fs.readFileSync(seeds)))return false @@ -54,6 +54,8 @@ export function snapshot(source=APP,includeRoute=true){ for(const file of fs.readdirSync(path.join(source,'src/common')).filter(name=>/^ime-[a-z0-9-]+\.js$/.test(name)))add('src/common/'+file,path.join(source,'src/common',file)) const meta=readJson(path.join(source,'src/assets/ime-dict/meta.json')) if(!Array.isArray(meta.shards)||!meta.shards.length||new Set(meta.shards).size!==meta.shards.length)throw Error('Dictionary is not generated or has invalid metadata') + const runtime=path.join(source,'src/assets/ime-dict/runtime.json') + if(fs.existsSync(runtime))add('src/assets/ime-dict/runtime.json',runtime) for(const name of ['meta',...meta.shards,...(meta.association_shards||[]),...(meta.completion_shards||[])]){ if(name!=='meta'&&!/^(short|s?[0-9]+|ds?[0-9a-d]+|[ac][0-9a-f]{2})$/.test(name))throw Error('Invalid generated shard name') add('src/assets/ime-dict/'+name+'.json',path.join(source,'src/assets/ime-dict',name+'.json')) diff --git a/com.sydxky.rtos.inputmethod/src/common/ime-engine.js b/com.sydxky.rtos.inputmethod/src/common/ime-engine.js index a0e1c98..1172a22 100644 --- a/com.sydxky.rtos.inputmethod/src/common/ime-engine.js +++ b/com.sydxky.rtos.inputmethod/src/common/ime-engine.js @@ -45,6 +45,23 @@ export function candidatePages(candidates) { } flushPage();return pages } +// A page plan retains integer offsets; only the visible page gets cell objects. +export function candidatePageStarts(candidates){ + const starts=[];let used=0,slots=0,rowUnits=0 + for(let i=0;i4||rowUnits!==units)){used+=rowUnits;slots=0} + if(!starts.length||used+units>4){starts.push(i);used=0;slots=0} + if(!slots)rowUnits=units + slots+=span + if(slots===4){used+=rowUnits;slots=0} + } + return starts +} +export function candidatePageRows(candidates,starts,page){ + if(!starts.length)return [] + return candidatePages(candidates.slice(starts[page],page+1x.name===name) - if(existing)return existing.data - if(pending[name])return pending[name] - pending[name]=(async()=>{ - const text=await read('/assets/ime-dict/'+name+'.json') - const data=typeof text==='string'?JSON.parse(text):text - const size=typeof text==='string'?text.length:0 - cache.push({name,data,size});while(cache.length>4||cache.reduce((sum,x)=>sum+x.size,0)>90000)cache.shift() - return data - })() - try{return await pending[name]}finally{delete pending[name]} + const cache=[];let pending={},manifest=null,metadataTask=null,epoch=0,cacheChars=0,reads=0,readChars=0,hits=0 + function hasBits(hex,code,alphabet){let index=0;for(const ch of code){const digit=alphabet.indexOf(ch);if(digit<0)return false;index=index*alphabet.length+digit}const byte=parseInt(hex.slice((index>>3)*2,(index>>3)*2+2),16)||0;return !!(byte&(1<<(index&7)))} + function exists(name){ + if(manifest.version!==1)return ((name[0]==='a'?manifest.association_shards:name[0]==='c'?manifest.completion_shards:manifest.shards)||[]).indexOf(name)>=0 + if(name[0]==='a'||name[0]==='c')return hasBits(manifest[name[0]==='a'?'associations':'completions'][0],name.slice(1),'0123456789abcdef') + const dual=name[0]==='d',short=name[dual?1:0]==='s',code=name.slice(dual?(short?2:1):(short?1:0)) + const masks=manifest[dual?(short?'dualShort':'dualLong'):(short?'short':'long')] + const mask=masks&&masks[short?code.length-1:0] + return !!mask&&hasBits(mask,code,dual?'0123456789abcd':'23456789') } - async function metadata(){if(!manifest){const meta=await read('/assets/ime-dict/meta.json');manifest=typeof meta==='string'?JSON.parse(meta):meta}return manifest} - async function predict(context){ + async function readJson(name){const text=await read('/assets/ime-dict/'+name+'.json');reads++;if(typeof text==='string')readChars+=text.length;return {data:typeof text==='string'?JSON.parse(text):text,size:typeof text==='string'?text.length:0}} + async function load(name) { + const index=cache.findIndex(x=>x.name===name) + if(index>=0){const existing=cache.splice(index,1)[0];cache.push(existing);hits++;return existing.data} + if(pending[name])return pending[name] + const owner=epoch,task=(async()=>{ + const entry=await readJson(name) + if(owner===epoch&&entry.size<=48000){cache.push({name,data:entry.data,size:entry.size});cacheChars+=entry.size;while(cache.length>3||cacheChars>48000)cacheChars-=cache.shift().size} + return entry.data + })() + pending[name]=task + try{return await task}finally{if(pending[name]===task)delete pending[name]} + } + async function metadata(){ + if(manifest)return manifest + if(!metadataTask){const owner=epoch;metadataTask=(async()=>{let entry;try{entry=await readJson('runtime')}catch(error){entry=await readJson('meta')}if(owner===epoch)manifest=entry.data;return entry.data})()} + return metadataTask + } + async function predict(context,current){ + const owner=epoch,previous=current;current=()=>owner===epoch&&(!previous||previous()) const tail=contextTail(context);if(!tail)return [] const meta=await metadata(),result=[],seen={} + if(current&&!current())return [] for(let length=tail.length;length>0&&result.length<8;length--){ const prefix=tail.slice(-length),name='a'+predictiveBucket(prefix) - if(!meta.association_shards||meta.association_shards.indexOf(name)<0)continue + if(!exists(name))continue const rows=(await load(name))[prefix]||[] + if(current&&!current())return [] for(const row of rows)if(!seen[row[0]]){seen[row[0]]=true;result.push({word:row[0],pinyin:'',consume:0,frequency:row[1]});if(result.length===8)break} } return result } return { predict, - async lookup(raw,t9,preferences,context) { + async lookup(raw,t9,preferences,context,current) { + const owner=epoch,previous=current;current=()=>owner===epoch&&(!previous||previous()) const input=String(raw||'').toLowerCase() const dual=t9==='14',ambiguous=!!t9 if(!(dual?/^[0-9a-d]{1,32}$/:t9?/^[2-9]{1,32}$/:/^[a-z]{1,32}$/).test(input))return [] const digits=ambiguous?input:digitsOf(input) - const merged={},frequencies={} - function merge(data){Object.assign(merged,data.entries);Object.assign(frequencies,data.frequencies)} await metadata() + if(current&&!current())return [] const names=[] const prefixLength=dual?3:4 for(let length=1;length<=Math.min(prefixLength-1,digits.length);length++)names.push((dual?'ds':'s')+digits.slice(0,length)) if(digits.length>=prefixLength)names.push((dual?'d':'')+digits.slice(0,prefixLength)) - for(const name of names)if(manifest.shards.indexOf(name)>=0)try{merge(await load(name))}catch(error){throw new Error('词库读取失败')} - const matching=Object.keys(merged).filter(key=>input.indexOf(dual?code14Of(key):t9?digitsOf(key):key)===0) - matching.sort((a,b)=>b.length-a.length||(ab?1:0)) - const result=[],seen={} - for(const key of matching)for(const word of merged[key]){ - const identity=word+'@'+key.length - if(!seen[identity]){seen[identity]=true;result.push({word,pinyin:key,consume:key.length,frequency:frequencies[word]||0})} + const matching=[],extras=[],result=[],seen={};let completionData=null + function offer(word,key,frequency){ + if(seen[word+'@'+input.length])return + const missing=key.length-input.length,old=extras.findIndex(x=>x.word===word) + if(old>=0){if(extras[old].frequency>frequency||extras[old].frequency===frequency&&extras[old].missing<=missing)return;extras.splice(old,1)} + let at=0;while(atfrequency||extras[at].frequency===frequency&&extras[at].missing<=missing))at++ + if(at<12){extras.splice(at,0,{word,pinyin:key,frequency,missing,consume:input.length,completion:true});if(extras.length>12)extras.pop()} } + for(const name of names)if(exists(name)){ + const data=await load(name) + if(current&&!current())return [] + if(!ambiguous){ + for(let length=1;length<=input.length;length++){const key=input.slice(0,length);if(data.entries[key])matching.push({key,words:data.entries[key],weights:data.frequencies})} + } + if(input.length>=prefixLength)completionData=data + if(ambiguous)for(const key in data.entries){ + const code=dual?code14Of(key):t9?digitsOf(key):key + if(input.indexOf(code)===0)matching.push({key,words:data.entries[key],weights:data.frequencies}) + } + } + matching.sort((a,b)=>b.key.length-a.key.length||(a.keyb.key?1:0)) + for(const entry of matching)for(const word of entry.words){const identity=word+'@'+entry.key.length;if(!seen[identity]){seen[identity]=true;result.push({word,pinyin:entry.key,consume:entry.key.length,frequency:entry.weights[word]||0})}} if(input.length>=2){ - const mode=dual?'14':t9?'t9':'py',prefix=mode+':'+input,extras=[] + const mode=dual?'14':t9?'t9':'py',prefix=mode+':'+input if(input.length=0){ - for(const row of (await load(name))[prefix]||[])extras.push({word:row[0],pinyin:row[1],frequency:row[2],missing:row[1].length-input.length,consume:input.length,completion:true}) + if(exists(name)){ + const rows=(await load(name))[prefix]||[] + if(!current())return [] + for(const row of rows)offer(row[0],row[1],row[2]) } - }else for(const key of Object.keys(merged)){ + }else if(completionData)for(const key in completionData.entries){ const code=dual?code14Of(key):t9?digitsOf(key):key - if(code.length<=input.length||code.length>input.length+4||code.indexOf(input)!==0)continue - for(const word of merged[key])extras.push({word,pinyin:key,frequency:frequencies[word]||0,missing:key.length-input.length,consume:input.length,completion:true}) + if(code.length>input.length&&code.length<=input.length+4&&code.indexOf(input)===0)for(const word of completionData.entries[key])offer(word,key,completionData.frequencies[word]||0) } - extras.sort((a,b)=>b.frequency-a.frequency||a.missing-b.missing) - let added=0 - for(const item of extras){const identity=item.word+'@'+item.consume;if(!seen[identity]){seen[identity]=true;result.push(item);if(++added===12)break}} + for(const item of extras){const identity=item.word+'@'+item.consume;if(!seen[identity]){seen[identity]=true;result.push(item)}} } const preferenceKey=dual?'k14:'+input:input const prefs=preferences&&Array.isArray(preferences[preferenceKey])?preferences[preferenceKey]:[] - const rank=item=>{const i=prefs.findIndex(x=>x.word===item.word&&x.consume===item.consume);return i<0?100:i} - const predictions=await predict(context),contextRank=item=>{const i=predictions.findIndex(x=>x.word===item.word);return i<0?0:8-i} + const ranks={},contexts={};for(let i=0;i{const value=ranks[item.word+'@'+item.consume];return value===undefined?100:value},contextRank=item=>contexts[item.word]||0 const category=item=>item.completion?1:item.consume===input.length?0:2 // Preserve every exact candidate; complete unfinished syllables before // shorter fallbacks, and use word frequency plus the editor's local context. - return result.map((item,index)=>({item,index})).sort((a,b)=>rank(a.item)-rank(b.item)||category(a.item)-category(b.item)||b.item.consume-a.item.consume||contextRank(b.item)-contextRank(a.item)||(b.item.frequency||0)-(a.item.frequency||0)||a.index-b.index).map(x=>x.item) + for(let i=0;irank(a)-rank(b)||category(a)-category(b)||b.consume-a.consume||contextRank(b)-contextRank(a)||(b.frequency||0)-(a.frequency||0)||a.order-b.order) }, - clear(){cache.length=0;manifest=null}, - get cacheSize(){return cache.length} + clear(){epoch++;cache.length=0;cacheChars=0;pending={};manifest=null;metadataTask=null}, + get cacheSize(){return cache.length}, + get stats(){return {reads,readChars,hits,cacheChars,cacheEntries:cache.length}} } } diff --git a/com.sydxky.rtos.inputmethod/src/components/CrabInput/index.ux b/com.sydxky.rtos.inputmethod/src/components/CrabInput/index.ux index 53aee8d..797efa3 100644 --- a/com.sydxky.rtos.inputmethod/src/components/CrabInput/index.ux +++ b/com.sydxky.rtos.inputmethod/src/components/CrabInput/index.ux @@ -7,10 +7,15 @@

{{secret ? '私密输入' : '选择候选词'}}

-
-
-
-
+
+
+
+
+
+
+
+
+
@@ -50,30 +55,31 @@ .body,.keypad,.full-list{display:flex;flex-direction:column;align-items:stretch;min-height:0}.body{flex:1;justify-content:flex-start}.keypad{flex-shrink:0} .candidates{height:42px;margin:0 4px 4px;background-color:#F8F9FB;border-radius:12px}.candidate{flex:1;min-width:45px;height:36px;background-color:transparent;color:#20232B;font-size:.78rem}.candidate-placeholder{flex:1;text-align:center;color:#A4A9B2;font-size:.45rem}.more{width:46px;height:36px;background-color:transparent;font-size:.5rem;color:#2867D9} .keyrow{align-items:stretch;align-self:center;width:100%}.key{display:flex;flex-direction:column;align-items:center;justify-content:center;flex:1;height:52px;min-width:0;background-color:#FFFFFF;border-radius:12px}.key-label{font-size:.88rem;color:#22252E}.key-sub{font-size:.35rem;color:#9DA2AD;margin-top:1px} - .qwerty-first,.qwerty-first-py{width:100%}.qwerty-middle{width:94%}.qwerty-last,.qwerty-last-py{width:84%} + .qwerty-pad{width:100%}.qwerty-first{width:100%}.qwerty-middle{width:90%}.qwerty-last{width:70%}.qwerty-key{margin:2px;height:58px}.english-pad .qwerty-key{height:76px} .modes{height:36px;margin-top:3px}.modes button{flex:1;height:31px;background-color:transparent;font-size:.53rem;color:#636A77} .bottom{height:41px;margin-top:2px}.bottom button{flex:1;height:37px;font-size:.6rem;background-color:#FFFFFF}.bottom .done{background-color:#2867D9;color:#FFFFFF}.bottom .delete{background-color:#D9DDE4;color:#515866} .page-info{text-align:center;color:#8B919D;font-size:.45rem;margin:5px 0;flex-shrink:0}.full-list{flex:1;justify-content:flex-start}.candidate-grid-row{display:flex;flex-direction:row;align-self:flex-start;align-items:stretch;flex-shrink:0}.full-candidate{display:flex;align-items:center;justify-content:center;flex:1;min-width:0;margin:2px;padding:2px 4px;background-color:#FFFFFF;border-radius:10px}.word{width:100%;text-align:center;font-size:25px;color:#22252E}.empty{margin:15px 8px;color:#9299A5;text-align:center} .pager{height:38px}.pager button{flex:1;height:34px;background-color:transparent;color:#2867D9;font-size:.55rem}.toolrow button{flex:1;height:40px;font-size:.57rem}.tools{justify-content:center}.tools .danger{color:#C85656}.tools .bottom{margin-top:8px} @media (shape:circle){.ime{padding:30px 8px}.top{align-self:center;width:186px;height:24px}.cancel{height:24px}.preview{align-self:center;width:280px;height:32px;margin-top:5px}.composition{align-self:center;width:300px;height:23px}.candidates{align-self:center;width:352px;height:38px}.candidate{height:34px}.more{height:34px}.keypad{align-self:center;width:368px}.key{height:49px}.modes{align-self:center;width:286px;height:33px}.modes button{height:29px}.bottom{align-self:center;width:200px;height:38px}.bottom button{height:34px}.full-list{align-self:center;width:340px}.pager{align-self:center;width:276px}.toolrow{align-self:center;width:300px}.toolrow button{height:34px}.tools .page-info{align-self:center;width:280px}.tools .bottom{margin-top:4px}} - @media (shape:circle){.qwerty-first{width:92%}.qwerty-first-py{width:96%}.qwerty-middle{width:100%}.qwerty-last{width:90%}.qwerty-last-py{width:80%}} + @media (shape:circle){.ime{padding:22px 8px}.candidates{height:32px}.candidate,.more{height:26px}.modes{margin-top:1px}.qwerty-pad{width:98%}.pinyin-pad .qwerty-key{height:54px}.english-pad{width:92%}.english-pad .qwerty-key{height:65px}.bottom{width:176px}}