一套代码适配四种屏幕——StyleConfiguration 键盘多设备适配方案
文章目录
- 问题在哪?
- StyleConfiguration 的设计思路
- KeyStyle 接口定义
- StyleConfiguration.getInputStyle 完整逻辑
- 资源文件命名规范
- 组件如何使用 StyleConfiguration
- 屏幕旋转适配完整流程
- 这种设计模式的通用价值
- 踩坑记录
- 写在最后
搞输入法开发最头疼的事情之一就是屏幕适配:手机竖屏、手机横屏、平板竖屏、平板横屏,键盘的尺寸、按键大小、字体都不一样。KikaInputMethod 里的StyleConfiguration用了一套挺优雅的方案,值得借鉴。
问题在哪?
不同设备键盘的差异点:
| 场景 | 特点 | 挑战 |
|---|---|---|
| 手机竖屏 | 键盘宽,高度有限 | 按键小,字体要适配 |
| 手机横屏 | 键盘更宽,高度更有限 | 按键要更扁 |
| 平板竖屏 | 分辨率高,空间充足 | 按键可以更大 |
| 平板横屏 | 宽度极大 | 按键宽度要重新分配 |
| RK 开发板 | 分辨率特殊(720×1280) | 完全独立的样式集 |
如果每个场景都用 if-else 直接写死数值,代码会变成噩梦。
StyleConfiguration 的设计思路
核心思路:把"样式选择"和"样式使用"分离。
KeyStyle 接口定义
所有设备共享同一套接口,只是资源值不同:
exportinterfaceKeyStyle{deviceType?:string;// === 按键尺寸 ===basicButtonWidth:Resource;// 普通字母键宽度basicButtonHeight:Resource;// 按键高度switchButtonWidth:Resource;// 切换键(数字/符号)宽度returnButtonWidthType1:Resource;// Enter 键宽度(类型1)returnButtonWidthType2:Resource;// Enter 键宽度(类型2)spaceButtonWidth1:Resource;// 空格键宽度1spaceButtonWidth2:Resource;// 空格键宽度2// === 图标尺寸 ===featurePicSize:Resource;// 功能图标大小returnPicSize:Resource;// 回车图标大小editPicSize:Resource;// 编辑图标大小editImageSize:Resource;// 编辑图片大小// === 间距 ===paddingTop:Resource;// 键盘顶部内边距paddingLeftRight:Resource;// 左右内边距// === 下拉菜单 ===downMenuHeight:Resource;downMenuPicWidth:Resource;downMenuPicHeight:Resource;downMenuWidth:Resource;subMenuWidth:Resource;// === 字体 ===litterNumberFontSize:Resource;// 小号数字字体en_fontSize:Resource;// 英文字体大小symbol_fontSize:Resource;// 符号字体大小switchNumberFontSize:Resource;// 切换按钮字体// === 其他 ===keyboardHeight:Resource;// 键盘总高度editCircleSize:Resource;// 编辑圆圈大小editSmallCircle:Resource;editSmallCircleMargin:Resource;editButtonSize:Resource;editFontSize:Resource;editDriverLeft:Resource;}StyleConfiguration.getInputStyle 完整逻辑
import{deviceInfo}from'@kit.BasicServicesKit';exportclassStyleConfiguration{// 组件初始化时调用,从 AppStorage 读取已计算好的样式// 如果 AppStorage 里还没有(第一次),就重新计算staticgetSavedInputStyle():KeyStyle{letstyle=AppStorage.get<KeyStyle>('inputStyle');if(style){returnstyle;}// AppStorage 里没有,用默认值计算letisLandscape=AppStorage.get<boolean>('isLandscape')??false;letisRkDevice=AppStorage.get<boolean>('isRkDevice')??false;returnStyleConfiguration.getInputStyle(isLandscape,isRkDevice,deviceInfo.deviceType);}// 根据设备信息选择对应样式集staticgetInputStyle(isLandscape:boolean,isRkDevice:boolean,deviceType:string):KeyStyle{// 优先级1:RK 开发板独立适配if(isRkDevice){return{basicButtonWidth:$r('app.float.rk_basic_button_width'),basicButtonHeight:$r('app.float.rk_basic_button_height'),en_fontSize:$r('app.float.rk_en_fontSize'),// ... 其他 rk_* 资源};}// 优先级2:平板设备if(deviceType==='tablet'){if(isLandscape){return{deviceType:'tablet_landSpace',basicButtonWidth:$r('app.float.landSpace_basic_button_width'),basicButtonHeight:$r('app.float.landSpace_basic_button_height'),en_fontSize:$r('app.float.landSpace_en_fontSize'),symbol_fontSize:$r('app.float.landSpace_symbol_fontSize'),// ... 其他 landSpace_* 资源};}else{return{deviceType:'tablet_portrait',basicButtonWidth:$r('app.float.portrait_basic_button_width'),basicButtonHeight:$r('app.float.portrait_basic_button_height'),en_fontSize:$r('app.float.portrait_en_fontSize'),// ... 其他 portrait_* 资源};}}// 优先级3:手机if(!isLandscape){// 手机竖屏return{basicButtonWidth:$r('app.float.s_basic_button_width'),basicButtonHeight:$r('app.float.s_basic_button_height'),en_fontSize:$r('app.float.en_fontSize'),keyboardHeight:$r('app.float.keyboard_height'),// ... 其他 s_* 资源};}else{// 手机横屏return{basicButtonWidth:$r('app.float.h_basic_button_width'),basicButtonHeight:$r('app.float.h_basic_button_height'),en_fontSize:$r('app.float.en_fontSize'),// ... 其他 h_* 资源};}}}资源文件命名规范
每种设备的样式值存在resources/base/element/float.json里,用前缀区分:
rk_* → RK 开发板 s_* → 手机(small/standard)竖屏 h_* → 手机横屏(horizontal) landSpace_* → 平板横屏 portrait_* → 平板竖屏比如按键高度:
{"float":[{"name":"s_basic_button_height","value":"40.0"},{"name":"h_basic_button_height","value":"32.0"},{"name":"portrait_basic_button_height","value":"48.0"},{"name":"landSpace_basic_button_height","value":"36.0"},{"name":"rk_basic_button_height","value":"36.0"}]}组件如何使用 StyleConfiguration
所有键盘组件通过@StorageLink响应式读取样式:
// SpaceItem.ets@Componentexportstruct SpaceItem{@StorageLink('inputStyle')inputStyle:KeyStyle=StyleConfiguration.getSavedInputStyle();build(){Stack(){Text('space').fontSize(this.inputStyle.symbol_fontSize)// 根据设备自动适配字体}.width(this.spaceWith).height('100%').onClick(()=>{InputHandler.getInstance().insertText(' ');})}}// KeyItem.ets@Componentexportstruct KeyItem{@StorageLink('inputStyle')inputStyle:KeyStyle=StyleConfiguration.getSavedInputStyle();build(){Stack(){Text(this.keyValue).fontSize(this.inputStyle.en_fontSize)// 字体自适应}.width(this.inputStyle.basicButtonWidth)// 按键宽度自适应.height(this.inputStyle.basicButtonHeight)// 按键高度自适应}}当屏幕旋转时,KeyboardController.resizePanel()重新计算inputStyle并更新 AppStorage,所有组件自动重新渲染。
屏幕旋转适配完整流程
@startuml title 屏幕旋转适配流程 :用户旋转设备; :display.on('change') 触发; :KeyboardController.resizePanel(); :display.getDefaultDisplaySync()\n获取新的宽高; :重新判断 isLandscape; :StyleConfiguration.getInputStyle(\n newIsLandscape, isRkDevice, deviceType\n); :AppStorage.setOrCreate('inputStyle', newStyle); note right: @StorageLink 自动通知\n所有键盘 UI 组件重新渲染 :panel.resize(newWidth, newHeight); note right: 同步更新面板尺寸\n保证键盘显示正确 @enduml这种设计模式的通用价值
StyleConfiguration的设计不只适合输入法,任何需要多设备适配的应用都可以用类似的模式:
// 通用适配模式classThemeConfig{// 1. 定义统一接口interfaceAppTheme{fontSize:Resource;padding:Resource;buttonHeight:Resource;// ...}// 2. 根据设备类型选择具体值staticgetTheme(deviceType:string,isLandscape:boolean):AppTheme{if(deviceType==='tablet'){returnisLandscape?tabletLandscapeTheme:tabletPortraitTheme;}returnisLandscape?phoneLandscapeTheme:phonePortraitTheme;}}// 3. 应用启动时存入 AppStorageAppStorage.setOrCreate('appTheme',ThemeConfig.getTheme(deviceType,isLandscape));// 4. 组件通过 @StorageLink 使用@StorageLink('appTheme')theme:AppTheme=ThemeConfig.getTheme('phone',false);踩坑记录
坑1:getSavedInputStyle 的兜底逻辑要完整
getSavedInputStyle会在组件初始化时调用,这时KeyboardController.initWindow可能还没执行完(Panel 还没创建好),AppStorage里可能没有inputStyle。所以getSavedInputStyle必须有完整的兜底计算逻辑,不能直接返回undefined。
坑2:resizePanel 要同时更新样式和面板尺寸
旋转时只调panel.resize但不更新inputStyle,键盘 UI 会显示旧样式;只更新inputStyle但不调panel.resize,面板大小不对。两个操作都要做。
坑3:$r() 资源引用在 StyleConfiguration 里不是问题
StyleConfiguration是一个普通类,不是 ArkUI 组件,但$r()在非组件环境里仍然有效,它返回的是Resource类型,组件里用的时候才实际解析。
写在最后
StyleConfiguration + AppStorage + @StorageLink这套组合拳用在输入法适配上效果很好:运行时根据设备信息选一次样式存入 AppStorage,所有组件响应式读取,屏幕旋转时重新计算更新,整个过程不需要手动通知任何组件。这个设计模式值得在其他多设备项目里复用。
