用户3116712
用户3116712
  • 发布:2026-08-12 13:50
  • 更新:2026-08-12 13:50
  • 阅读:25

开发自定义键盘,HarmonyOS中有没有长按按键子项生成气泡效果的API

分类:uni-app

问题现象
按键子项生成气泡效果用什么组件实现好?

效果预览
点击放大

背景知识
使用自定义键盘实现官网示例代码进行自定义键盘调试,然后用LongPressGesture增加长按手势事件,最后使用Popup控制为组件绑定Popup气泡,使用Popup气泡,如果按键处于边缘位置,组件会自动偏移,完整的显示在屏幕中间。

解决方案
自定义键盘没有直接长按生成气泡的接口,需要自定义长按逻辑,手指在长按小键盘组件时,使用Popup生成一个气泡,在气泡生成之后,使用onTouch获取手指位置并且根据气泡组件的位置和横向大小,计算当前手指划过的距离从而标记手指当前选择的内容。

代码实现思路如下:

创建变量控制气泡显隐样式以及气泡在边缘的位置:
@State customPopup: boolean = false;
@State indexNum: number = 0;
// 手指位置1、2、3、4(长按气泡中显示的内容数量)四个参数,分别代指长按后出现的四个位置,参数值与实际业务有关。
@State fingerRect: string = '1';
// 组件宽度
@State widthRect: number = 0;
// 键盘宽度
@State inputWidthRect: number = 0;
// 判断是否在偏右侧位置
@State isRight: boolean = false;
增加LongPressGesture长按控制手势,在onAction事件中增加逻辑,让气泡仅显示在指定按键上方;长按手势默认最短长按时间为500毫秒,可配置duration参数控制最短长按时长:
.gesture(
// 绑定可以重复触发的LongPressGesture
LongPressGesture({ repeat: true })
.onAction(() => {
for (let i = 0; i < EnglishKeyboardData[0].length; i++) {
if (item.text === EnglishKeyboardData[0][i].text) {
this.indexNum = i;
break;
}
}
this.customPopup = true;
})
.onActionEnd(() => {
this.customPopup = !this.customPopup;
})
)
增加bindPopup气泡控制,设置placement: Placement.TopRight,让气泡显示在按钮上方,通过this.popupBuilder(item)给自定义气泡组件传参然后自定义气泡内容:
.bindPopup(this.customPopup && EnglishKeyboardData[0].indexOf(item) === this.indexNum, {
// CustomPopupOptions类型气泡的内容
builder: this.popupBuilder(item),
placement: Placement.TopLeft,
targetSpace: '15vp',
enableArrow: false,
onStateChange: (e) => {
if (!e.isVisible) {
this.customPopup = false;
}
}
})
@Builder
popupBuilder(item: Menu) {
Row() {
ForEach([1, 2, 3, 4], (num: number, index: number) => {
if (this.fingerRect === index + 1 + '') {
Row() {
Text(item.text + '' + num)
.fontColor(Color.White);
}
.globalFancy()
.backgroundColor(Color.Blue)
.justifyContent(FlexAlign.Center);
} else {
Row() {
Text(item.text + '' + num);
}
.globalFancy()
.justifyContent(FlexAlign.Center);
}
}, (num: number) => JSON.stringify(num));
}
.height(50)
.width(150)
.padding(5)
.justifyContent(FlexAlign.SpaceAround)
.onAreaChange((oldVal, newVal) => {
// 如果此时键盘在偏右侧位置,需要重新计算位置
this.isRight = this.inputWidthRect <= Math.ceil(Number(newVal.globalPosition.x) + Number(newVal.width));
});
}
使用onAreaChange监听新生成的气泡组件的宽度,再使用onTouch事件获取当前手指的坐标,然后根据手指的坐标和气泡组件的宽度,获取当前手指在气泡组件中的位置:
.onTouch((event) => {
// 获取当前手指的坐标(相对于父容器,与组件rect的坐标系一致)
const fingerX = event.touches[0].x;
// 靠近右边屏幕时,需要特殊处理。
this.fingerRect =
this.isRight ? Math.ceil(((fingerX + event.touches[0].displayX) - 230) / this.widthRect) + '' :
Math.ceil(fingerX / this.widthRect) + '';
this.fingerRect =
(Number(this.fingerRect) <= 1) ? '1' : Number(this.fingerRect) >= 4 ? '4' : this.fingerRect;
})
.onAreaChange((oldVal, newVal) => {
this.widthRect = Number(newVal.width);
});
参考官网自定义键盘实现,将demo中的NumberKeyboard文件替换为以下示例代码:

// 该场景代码基于自定义键盘实现官网示例代码https://developer.huawei.com/consumer/cn/doc/best-practices/bpta-custom-keyboard#section1680905412256进行调试;
// 修改demo中NumberKeyboard文件如下
@Component
export struct NumberKeyboard {
@State customPopup: boolean = false;
@State indexNum: number = 0;
// 手指位置1、2、3、4(长按气泡中显示的内容数量)四个参数,分别代指长按后出现的四个位置,参数值与实际业务有关。
@State fingerRect: string = '1';
// 组件宽度
@State widthRect: number = 0;
// 键盘宽度
@State inputWidthRect: number = 0;
// 判断是否在偏右侧位置
@State isRight: boolean = false;

build() {
Row() {
Scroll() {
Column() {
Row({ space: Constants.ENGLISH_KEYBOARD_BUTTON_SPACE }) {
ForEach(EnglishKeyboardData[0], (item: Menu) => {
EnglishButton({ item: item })
.gesture(
// 绑定可以重复触发的LongPressGesture
LongPressGesture({ repeat: true })
.onAction(() => {
for (let i = 0; i < EnglishKeyboardData[0].length; i++) {
if (item.text === EnglishKeyboardData[0][i].text) {
this.indexNum = i;
break;
}
}
this.customPopup = true;
})
.onActionEnd(() => {
this.customPopup = !this.customPopup;
})
)
.bindPopup(this.customPopup && EnglishKeyboardData[0].indexOf(item) === this.indexNum, {
// CustomPopupOptions类型气泡的内容
builder: this.popupBuilder(item),
placement: Placement.TopLeft,
targetSpace: '15vp',
enableArrow: false,
onStateChange: (e) => {
if (!e.isVisible) {
this.customPopup = false;
}
}
})
.onTouch((event) => {
// 获取当前手指的坐标(相对于父容器,与组件rect的坐标系一致)
const fingerX = event.touches[0].x;
// 靠近右边屏幕时,需要特殊处理。
this.fingerRect =
this.isRight ? Math.ceil(((fingerX + event.touches[0].displayX) - 230) / this.widthRect) + '' :
Math.ceil(fingerX / this.widthRect) + '';
this.fingerRect =
(Number(this.fingerRect) <= 1) ? '1' : Number(this.fingerRect) >= 4 ? '4' : this.fingerRect;
})
.onAreaChange((oldVal, newVal) => {
this.widthRect = Number(newVal.width);
});
}, (item: Menu) => JSON.stringify(item));
};

      Row({ space: Constants.ENGLISH_KEYBOARD_BUTTON_SPACE }) {  
        ForEach(EnglishKeyboardData[1], (item: Menu) => {  
          EnglishButton({ item: item });  
        }, (item: Menu) => JSON.stringify(item));  
      }  
      .padding({  
        left: 18,  
        right: 18,  
        top: 11  
      });  

      Row({ space: Constants.ENGLISH_KEYBOARD_BUTTON_SPACE }) {  
        ForEach(EnglishKeyboardData[2], (item: Menu) => {  
          EnglishButton({ item: item });  
        }, (item: Menu) => JSON.stringify(item));  
      }  
      .padding({ top: 11 });  

      Row({ space: Constants.ENGLISH_KEYBOARD_BUTTON_SPACE }) {  
        ForEach(EnglishKeyboardData[3], (item: Menu) => {  
          EnglishButton({ item: item });  
        }, (item: Menu) => JSON.stringify(item));  
      }  
      .padding({ top: 11 });  
    }  
    .onAreaChange((oldVal, newVal) => {  
      // 键盘宽度  
      this.inputWidthRect = Math.ceil(Number(newVal.globalPosition.x) + Number(newVal.width));  
    });  
  }  
  .scrollable(ScrollDirection.Horizontal)  
  .scrollBar(BarState.Off);  
};  

}

@Builder
popupBuilder(item: Menu) {
Row() {
ForEach([1, 2, 3, 4], (num: number, index: number) => {
if (this.fingerRect === index + 1 + '') {
Row() {
Text(item.text + '' + num)
.fontColor(Color.White);
}
.globalFancy()
.backgroundColor(Color.Blue)
.justifyContent(FlexAlign.Center);
} else {
Row() {
Text(item.text + '' + num);
}
.globalFancy()
.justifyContent(FlexAlign.Center);
}
}, (num: number) => JSON.stringify(num));
}
.height(50)
.width(150)
.padding(5)
.justifyContent(FlexAlign.SpaceAround)
.onAreaChange((oldVal, newVal) => {
// 如果此时键盘在偏右侧位置,需要重新计算位置
this.isRight = this.inputWidthRect <= Math.ceil(Number(newVal.globalPosition.x) + Number(newVal.width));
});
}

}

@Styles
function globalFancy() {
.borderRadius(10)
.width('25%')
.height('100%');
}

// [Start EnglishButton_start]
@Component
struct EnglishButton {
@Consume inputText: string;
// [StartExclude EnglishButton_start]
@Prop item: Menu;
@Consume keyboardController: KeyboardController;

getEnglishText(item: Menu): string | Resource {
if (this.keyboardController.isUpperCase && item.upperText) {
return item.upperText;
} else {
return item.text;
}
}

// [EndExclude EnglishButton_start]

build() {
Button({ type: ButtonType.Normal }) {
if (typeof this.getEnglishText(this.item) === 'string' ||
(this.getEnglishText(this.item) as Resource).type !== Constants.RESOURCE_TYPE_MEDIA) {
Text(this.getEnglishText(this.item));
} else {
Image(this.getEnglishText(this.item))
.width(Constants.KEYBOARD_BUTTON_FONTSIZE_18)
.height(Constants.KEYBOARD_BUTTON_FONTSIZE_18);
}
}
.fontColor(Color.Black)
.backgroundColor(this.item.backgroundColor)
.borderRadius(Constants.KEYBOARD_BUTTON_RADIUS)
.fontSize(Constants.KEYBOARD_BUTTON_FONTSIZE_18)
.padding(0)
.width(this.item.width)
.height(this.item.height)
.onClick(() => {
this.inputText = this.keyboardController.onInput(this.getEnglishText(this.item));
});
}
}

// [End EnglishButton_start]
https://coub.com/view/4b8l5y
https://coub.com/view/4b8l5w
https://coub.com/view/4b8l5v
https://coub.com/view/4b8l5t
https://coub.com/view/4b8l5r
https://coub.com/view/4b8l5p
https://coub.com/view/4b8l5l
https://coub.com/view/4b8l5h
https://coub.com/view/4b8l5e
https://coub.com/view/4b8l5c
https://coub.com/view/4b8l5b
https://coub.com/view/4b8l5a
https://coub.com/view/4b8l58
https://coub.com/view/4b8l57
https://coub.com/view/4b8l54
https://coub.com/view/4b8l51
https://coub.com/view/4b8l50
https://coub.com/view/4b8l4z
https://coub.com/view/4b8l4w
https://coub.com/view/4b8l4t
https://coub.com/view/4b8l4r
https://coub.com/view/4b8l4o
https://coub.com/view/4b8l4l
https://coub.com/view/4b8l4j
https://coub.com/view/4b8l4f
https://coub.com/view/4b8l4d
https://coub.com/view/4b8l4b
https://coub.com/view/4b8l47
https://coub.com/view/4b8l46
https://coub.com/view/4b8l41
https://coub.com/view/4b8l3y
https://coub.com/view/4b8l3v
https://coub.com/view/4b8l3s
https://coub.com/view/4b8l3r
https://coub.com/view/4b8l3q
https://coub.com/view/4b8l3h
https://coub.com/view/4b8l3f
https://coub.com/view/4b8l3c
https://coub.com/view/4b8l39
https://coub.com/view/4b8l36
https://coub.com/view/4b8l30
https://coub.com/view/4b8l2y
https://coub.com/view/4b8l2v
https://coub.com/view/4b8l2t
https://coub.com/view/4b8l2q
https://coub.com/view/4b8l2n
https://coub.com/view/4b8l2j
https://coub.com/view/4b8l2f
https://coub.com/view/4b8l2c
https://coub.com/view/4b8l2b
https://coub.com/view/4b8l28
https://coub.com/view/4b8l27
https://coub.com/view/4b8l23
https://coub.com/view/4b8l20
https://coub.com/view/4b8l1v
https://coub.com/view/4b8l16
https://coub.com/view/4b8l12
https://coub.com/view/4b8l0w
https://coub.com/view/4b8l0o
https://coub.com/view/4b8l0j
https://coub.com/view/4b8l0g
https://coub.com/view/4b8l0f
https://coub.com/view/4b8l0d
https://coub.com/view/4b8l08
https://coub.com/view/4b8l06
https://coub.com/view/4b8l04
https://coub.com/view/4b8l01
https://coub.com/view/4b8kzx
https://coub.com/view/4b8kzv
https://coub.com/view/4b8kzr
https://coub.com/view/4b8kzl
https://coub.com/view/4b8kzi
https://coub.com/view/4b8kzg
https://coub.com/view/4b8kze
https://coub.com/view/4b8kzc
https://coub.com/view/4b8kpg
https://coub.com/view/4b8kpf
https://coub.com/view/4b8kpe
https://coub.com/view/4b8kpc
https://coub.com/view/4b8kpa
https://coub.com/view/4b8kp7
https://coub.com/view/4b8kp5
https://coub.com/view/4b8kp1
https://coub.com/view/4b8kox
https://coub.com/view/4b8kov
https://coub.com/view/4b8kot
https://coub.com/view/4b8kor
https://coub.com/view/4b8kop
https://coub.com/view/4b8kol
https://coub.com/view/4b8koj
https://coub.com/view/4b8kog
https://coub.com/view/4b8koe
https://coub.com/view/4b8koc
https://coub.com/view/4b8knx
https://coub.com/view/4b8knw
https://coub.com/view/4b8knv
https://coub.com/view/4b8knu
https://coub.com/view/4b8knt
https://coub.com/view/4b8kns
https://coub.com/view/4b8knq
https://coub.com/view/4b8knp
https://coub.com/view/4b8kno
https://coub.com/view/4b8knn
https://coub.com/view/4b8knm
https://coub.com/view/4b8knl
https://coub.com/view/4b8knk
https://coub.com/view/4b8knj
https://coub.com/view/4b8kni
https://coub.com/view/4b8knd
https://coub.com/view/4b8knb
https://coub.com/view/4b8kna
https://coub.com/view/4b8kn9
https://coub.com/view/4b8kn8

0 关注 分享

要回复文章请先登录注册