fix(输入法): 修复光标间隙并扩大26键及明确大小写切换
这个提交包含在:
1 父节点
b95e7caae7
当前提交
100757f684
35 个文件已更改
+675
-243
没有与搜索条件相匹配的文件
+5
-3
@@ -1,21 +1,23 @@
|
||||
# 蟹蟹输入 Android应用内键盘组件
|
||||
|
||||
基于CrabKeyboard MIT词库,由herry重写的Android手表原生键盘组件,1.2.3 / 10203。提供可集成的 **AAR**、演示APK和独立接入示例;适用Android 8.1 / API 27及以上。
|
||||
基于CrabKeyboard MIT词库,由herry重写的Android手表原生键盘组件,1.2.4 / 10204。提供可集成的 **AAR**、演示APK和独立接入示例;适用Android 8.1 / API 27及以上。
|
||||
|
||||
应用接入后,点击自己的输入框自动展开内置键盘。**不注册系统输入法、不要求用户切换输入法、不改变系统默认输入法。** 圆屏按每行所在高度计算安全弦宽;4×4完整候选,单字16个一页,词组扩宽或增加行高,候选不显示拼音。九宫、标准QWERTY全拼/英文、14键、数字、三页符号、Unicode删除、按住连删及光标编辑。
|
||||
|
||||
完全离线,无网络或外部存储权限;组件不持久化输入内容,不启用学习。密码输入默认英文并遮蔽预览。页面隐藏/销毁时清除组件会话与候选,宿主输入框中的内容仍由宿主决定如何处理。异步查询按会话序号隔离,旧结果不能回填新的输入框。
|
||||
|
||||
1.2.4 去掉预览与示例编辑框的额外字间距,扩大26键;每排继续按实际屏幕弦宽计算安全边界。点“abc”进入英文,随后“大写/小写”按钮显示下一步动作,字母标签与实际输入保持一致。见[本版验收](./VALIDATION_1.2.4.md)。
|
||||
|
||||
1.2.3 增加未完成拼音补全、上下文续写及词频排序;复用预览、按键及候选控件,按帧合并刷新。联想仅在内存中使用当前输入框光标前的中文,不持久化全文,密码内容不参与。见[本版验收](./VALIDATION_1.2.3.md)。
|
||||
|
||||
1.2.2 去掉英文、数字、符号模式未使用的候选栏空位,文字与键盘紧密相接。本版真机间距检查通过;完整测试的限制见[验收记录](./VALIDATION_1.2.2.md)。
|
||||
|
||||
## 一分钟接入
|
||||
|
||||
将发行版`crab-keyboard-1.2.3.aar`放入宿主`app/libs/`:
|
||||
将发行版`crab-keyboard-1.2.4.aar`放入宿主`app/libs/`:
|
||||
|
||||
```groovy
|
||||
dependencies { implementation files('libs/crab-keyboard-1.2.3.aar') }
|
||||
dependencies { implementation files('libs/crab-keyboard-1.2.4.aar') }
|
||||
```
|
||||
|
||||
```java
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
# Android 1.2.4 光标、26键及大小写验收
|
||||
|
||||
2026-11-14,版本1.2.4/10204,最低API27。预览及演示/独立宿主编辑框显式关闭额外字间距,原生EditText负责文本与光标定位。26键扩大高度,各排仍按实际圆屏弦宽分配安全区域。英文按钮显示下一步“大写/小写”,键面与实际输入同步。保留1.2.3离线补全、续词、控件复用和合并同帧刷新,密码不提供联想上下文。
|
||||
|
||||
7项Java测试通过。Release APK/AAR、宿主及组件测试APK构建通过;SQLite全量回读104,837组索引、163,238候选和完整性检查通过。lint 0错误:演示应用1项备份声明警告,组件2项测试依赖版本提示。最终APK为1.2.4/10204、最低API27、zipalign通过,原生产证书1名签名者,APK v2签名有效。证书SHA-256为b4632fff23289fdfa2b4a644b4e9d8fc18f7b0a3ed49fae1353ea5690cc3961b;最终APK SHA-256为37a0a80fd61ff7079a5d21ffaf016f7776de5a2df8933f1221ad8beae2ef4f6f。
|
||||
|
||||
OWW261 Android11/API30上4项后台原生组件回归通过:24帧measure/layout/draw持续绘出文字并复用预览和按键;niha补全你好、接续世界且仅提交一次;密码上下文隔离;实际点击大写/小写按钮及字母键输入qQq,并检查屏幕内的所有控件与圆屏按钮四角边界。
|
||||
|
||||
测试在离屏原生View执行,未打开Activity、切换前台或修改默认系统输入法。使用debug组件,15个运行时class成员与发布AAR逐字节相同。这是组件回归,不是发布APK前台端到端视觉验收;最终APK没有安装到共享手表,原9项前台交互未重跑。本版没有Android前台截图,历史1.2.2/1.2.0截图仅作历史资料。另一款方表、长期性能/功耗与商店审核未验收。
|
||||
+2
-2
@@ -12,8 +12,8 @@ android {
|
||||
applicationId 'com.sydxky.crabinput'
|
||||
minSdk 27
|
||||
targetSdk 35
|
||||
versionCode 10203
|
||||
versionName '1.2.3'
|
||||
versionCode 10204
|
||||
versionName '1.2.4'
|
||||
testInstrumentationRunner 'androidx.test.runner.AndroidJUnitRunner'
|
||||
}
|
||||
signingConfigs {
|
||||
|
||||
@@ -46,7 +46,7 @@ public final class MainActivity extends Activity {
|
||||
// Vendor EditText backgrounds add large vertical insets on watches.
|
||||
// Explicit padding keeps both empty hints and entered text fully visible.
|
||||
editor.setPadding(px(12),0,px(12),0);editor.setGravity(Gravity.CENTER_VERTICAL);
|
||||
editor.setIncludeFontPadding(false);editor.setSingleLine(true);
|
||||
editor.setIncludeFontPadding(false);editor.setLetterSpacing(0f);editor.setSingleLine(true);
|
||||
android.graphics.drawable.GradientDrawable background=new android.graphics.drawable.GradientDrawable();background.setColor(Color.WHITE);background.setCornerRadius(px(12));editor.setBackground(background);
|
||||
LinearLayout.LayoutParams layout=new LinearLayout.LayoutParams(-1,px(47));layout.topMargin=px(7);body.addView(editor,layout);bindings.add(CrabKeyboard.attach(this,editor));
|
||||
}
|
||||
|
||||
@@ -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 10203; versionName '1.2.3' }
|
||||
defaultConfig { applicationId 'com.sydxky.crabinput.host'; minSdk 27; targetSdk 35; versionCode 10204; versionName '1.2.4' }
|
||||
compileOptions { sourceCompatibility JavaVersion.VERSION_11; targetCompatibility JavaVersion.VERSION_11 }
|
||||
}
|
||||
dependencies { implementation project(':keyboard') }
|
||||
@@ -13,7 +13,7 @@ public final class HostActivity extends Activity {
|
||||
@Override public void onCreate(Bundle saved){
|
||||
super.onCreate(saved);LinearLayout layout=new LinearLayout(this);layout.setOrientation(LinearLayout.VERTICAL);layout.setGravity(Gravity.CENTER);layout.setPadding(55,45,55,45);
|
||||
TextView title=new TextView(this);title.setText("独立应用接入示例");title.setTextSize(13);layout.addView(title);
|
||||
EditText editor=new EditText(this);editor.setContentDescription("fixture_editor");editor.setHint("点击输入");layout.addView(editor,new LinearLayout.LayoutParams(-1,-2));
|
||||
EditText editor=new EditText(this);editor.setContentDescription("fixture_editor");editor.setHint("点击输入");editor.setLetterSpacing(0f);layout.addView(editor,new LinearLayout.LayoutParams(-1,-2));
|
||||
layout.setFocusableInTouchMode(true);layout.requestFocus();setContentView(layout);binding=CrabKeyboard.attach(this,editor);
|
||||
}
|
||||
@Override protected void onDestroy(){if(binding!=null)binding.close();super.onDestroy();}
|
||||
|
||||
@@ -77,4 +77,25 @@ public class SurfaceRegressionTest {
|
||||
finally{controller.close();}
|
||||
});
|
||||
}
|
||||
@Test public void caseSwitchTypesBothCasesInsideWatchBounds(){
|
||||
ui(()->{
|
||||
EditText editor=new EditText(context());KeyboardController controller=session(editor,InputType.TYPE_CLASS_TEXT);
|
||||
try{
|
||||
controller.action("mode:en");KeyboardSurface surface=controller.surface;
|
||||
int[] widths={466,467,466};String[] keys={"q","Q","q"};String[] switches={"大写","小写"};
|
||||
for(int step=0;step<3;step++){
|
||||
int width=widths[step];surface.measure(View.MeasureSpec.makeMeasureSpec(width,View.MeasureSpec.EXACTLY),View.MeasureSpec.makeMeasureSpec(466,View.MeasureSpec.EXACTLY));surface.layout(0,0,width,466);
|
||||
View key=find(surface,keys[step]);assertNotNull(key);assertTrue(key.performClick());
|
||||
for(int i=0;i<surface.getChildCount();i++){
|
||||
View child=surface.getChildAt(i);assertTrue(child.getLeft()>=0&&child.getTop()>=0&&child.getRight()<=width&&child.getBottom()<=466);
|
||||
if(context().getResources().getConfiguration().isScreenRound()&&child instanceof android.widget.Button){
|
||||
for(int x:new int[]{child.getLeft(),child.getRight()})for(int y:new int[]{child.getTop(),child.getBottom()})assertTrue("Button corners stay inside the round screen",Math.hypot(x-width/2.0,y-233)<=width/2.0-1);
|
||||
}
|
||||
}
|
||||
if(step<2){View toggle=find(surface,switches[step]);assertNotNull(toggle);assertTrue(toggle.performClick());}
|
||||
}
|
||||
assertEquals("qQq",editor.getText().toString());assertFalse(controller.state.upper);
|
||||
}finally{controller.close();}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -107,6 +107,7 @@ final class KeyboardSurface extends ViewGroup {
|
||||
float previewY=top+29;
|
||||
String preview=state.secret?"私密输入":state.preview.isEmpty()?"开始输入…":state.preview;
|
||||
TextView previewView=label("preview",preview,21,INK);previewView.setMaxLines(1);previewView.setEllipsize(android.text.TextUtils.TruncateAt.START);
|
||||
previewView.setLetterSpacing(0f);
|
||||
previewView.setOnClickListener(v->actions.action("tools"));previewView.setContentDescription("编辑光标");
|
||||
float pi=inset(previewY,30);place(previewView,pi,previewY,410-2*pi,30);
|
||||
if(state.expanded) { buildCandidates(previewY+38);return; }
|
||||
@@ -137,10 +138,11 @@ 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:58;}
|
||||
if(!isChinese()){keyHeight=captions.length==4?43:66;}
|
||||
else if(state.mode.equals("py")){keyHeight=55;}
|
||||
for(int i=0;i<captions.length;i++)row(captions[i],values[i],keyY+i*(keyHeight+4),keyHeight,captions[i].length>=7?21:24);
|
||||
float modeY=keyY+captions.length*(keyHeight+4)+3;
|
||||
row(new String[]{"九宫","26键","14键",state.upper?"ABC":"abc","123","符号"},new String[]{"mode:t9","mode:py","mode:14","mode:en","mode:num","mode:sym"},modeY,27,13);
|
||||
row(new String[]{"九宫","26键","14键",state.mode.equals("en")?(state.upper?"小写":"大写"):"abc","123","符号"},new String[]{"mode:t9","mode:py","mode:14","mode:en","mode:num","mode:sym"},modeY,27,13);
|
||||
bottom(modeY+34,false);
|
||||
}finally{
|
||||
java.util.Iterator<Map.Entry<String,View>> iterator=children.entrySet().iterator();
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
"""Install the release and run embedded-component instrumentation on one explicit device."""
|
||||
import argparse,hashlib,json,pathlib,subprocess,time
|
||||
import argparse,hashlib,json,pathlib,re,subprocess,time
|
||||
APP=pathlib.Path(__file__).resolve().parents[1]
|
||||
|
||||
def main():
|
||||
@@ -31,9 +31,10 @@ 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=3 if args.background_regression else 9
|
||||
expected=4 if args.background_regression else 9
|
||||
passed=result.returncode==0 and f'OK ({expected} tests)'in output and 'FAILURES!!!'not in output
|
||||
report={'model':model,'sdk':sdk,'version':'1.2.3','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}
|
||||
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}
|
||||
(args.output/'device-validation.json').write_text(json.dumps(report,indent=2)+'\n',encoding='utf-8')
|
||||
assert passed and report['system_ime_unchanged'],'See instrumentation.txt; failed/unfinished cases are not accepted'
|
||||
if not args.background_regression:
|
||||
|
||||
在新工单中引用
屏蔽一个用户