在鸿蒙这个超有趣的创作魔法天地里,RichEditor
就像是一块能绽放无限创意的魔法画布🖼️,而 bindSelectionMenu
属性呢,宛如给这块画布悄悄安装的 "创意百宝盒开关"🔮。
<br />
当你用手指在 RichEditor
这块绚丽画布上轻轻一抹,选中了一段灵动的文字、一幅俏皮的插画,或是一段炫酷的排版元素时,哇哦😃,bindSelectionMenu
就被瞬间点亮啦!它立马召唤出一群超贴心的 "创意小精灵🧚",为你呈上专属定制的功能菜单。
想象一下,你正在精心绘制一篇旅行游记,选中一段描述绝美日落的文字后,通过 bindSelectionMenu
关联的菜单,一键就能让文字变得闪闪发光✨,或是瞬间切换成浪漫的手写体,给文字披上梦幻的外衣;要是选中的是一张随手拍下的风景小图,还能从菜单里挑出一键添加艺术滤镜、裁剪成超酷形状的功能选项,让图片更吸睛。
在代码的奇妙世界里,用 ArkTS 语言开启这份专属魔法也超轻松:
示例代码如下,宝宝们可以自己试着运行一下哦:
// xxx.ets
import { BusinessError, pasteboard } from '@kit.BasicServicesKit';
export interface SelectionMenuTheme {
imageSize: number;
buttonSize: number;
menuSpacing: number;
editorOptionMargin: number;
expandedOptionPadding: number;
defaultMenuWidth: number;
imageFillColor: Resource;
backGroundColor: Resource;
iconBorderRadius: Resource;
containerBorderRadius: Resource;
cutIcon: Resource;
copyIcon: Resource;
pasteIcon: Resource;
selectAllIcon: Resource;
shareIcon: Resource;
translateIcon: Resource;
searchIcon: Resource;
arrowDownIcon: Resource;
iconPanelShadowStyle: ShadowStyle;
iconFocusBorderColor: Resource;
}
export const defaultTheme: SelectionMenuTheme = {
imageSize: 24,
buttonSize: 48,
menuSpacing: 8,
editorOptionMargin: 1,
expandedOptionPadding: 3,
defaultMenuWidth: 256,
imageFillColor: $r('sys.color.ohos_id_color_primary'),
backGroundColor: $r('sys.color.ohos_id_color_dialog_bg'),
iconBorderRadius: $r('sys.float.ohos_id_corner_radius_default_m'),
containerBorderRadius: $r('sys.float.ohos_id_corner_radius_card'),
cutIcon: $r("sys.media.ohos_ic_public_cut"),
copyIcon: $r("sys.media.ohos_ic_public_copy"),
pasteIcon: $r("sys.media.ohos_ic_public_paste"),
selectAllIcon: $r("sys.media.ohos_ic_public_select_all"),
shareIcon: $r("sys.media.ohos_ic_public_share"),
translateIcon: $r("sys.media.ohos_ic_public_translate_c2e"),
searchIcon: $r("sys.media.ohos_ic_public_search_filled"),
arrowDownIcon: $r("sys.media.ohos_ic_public_arrow_down"),
iconPanelShadowStyle: ShadowStyle.OUTER_DEFAULT_MD,
iconFocusBorderColor: $r('sys.color.ohos_id_color_focused_outline'),
}
@Entry
@Component
struct SelectionMenu {
@State message: string = 'Hello World'
@State textSize: number = 40
@State sliderShow: boolean = false
@State start: number = -1
@State end: number = -1
@State colorTransparent: Color = Color.Transparent
controller: RichEditorController = new RichEditorController();
options: RichEditorOptions = { controller: this.controller }
private iconArr: Array<Resource> =
[$r('app.media.icon'), $r("app.media.icon"), $r('app.media.icon'),
$r("app.media.icon"), $r('app.media.icon')]
@State iconBgColor: ResourceColor[] = new Array(this.iconArr.length).fill(this.colorTransparent)
@State pasteEnable: boolean = false
@State visibilityValue: Visibility = Visibility.Visible
@State textStyle: RichEditorTextStyle = {}
private fontWeightTable: string[] = ["100", "200", "300", "400", "500", "600", "700", "800", "900", "bold", "normal", "bolder", "lighter", "medium", "regular"]
private theme: SelectionMenuTheme = defaultTheme;
aboutToAppear() {
if (this.controller) {
let richEditorSelection = this.controller.getSelection()
if (richEditorSelection) {
let start = richEditorSelection.selection[0]
let end = richEditorSelection.selection[1]
if (start === 0 && this.controller.getSpans({ start: end + 1, end: end + 1 }).length === 0) {
this.visibilityValue = Visibility.None
} else {
this.visibilityValue = Visibility.Visible
}
}
}
let sysBoard = pasteboard.getSystemPasteboard()
if (sysBoard && sysBoard.hasDataSync()) {
this.pasteEnable = true
} else {
this.pasteEnable = false
}
}
build() {
Column() {
Column() {
RichEditor(this.options)
.onReady(() => {
this.controller.addTextSpan(this.message, { style: { fontColor: Color.Orange, fontSize: 30 } })
})
.onSelect((value: RichEditorSelection) => {
if (value.selection[0] == -1 && value.selection[1] == -1) {
return
}
this.start = value.selection[0]
this.end = value.selection[1]
})
.bindSelectionMenu(RichEditorSpanType.TEXT, this.panel, ResponseType.LongPress, { onDisappear: () => {
this.sliderShow = false
}})
.bindSelectionMenu(RichEditorSpanType.TEXT, this.panel, ResponseType.RightClick, { onDisappear: () => {
this.sliderShow = false
}})
.borderWidth(1)
.borderColor(Color.Red)
.width(200)
.height(200)
}.width('100%').backgroundColor(Color.White)
}.height('100%')
}
PushDataToPasteboard(richEditorSelection: RichEditorSelection) {
let sysBoard = pasteboard.getSystemPasteboard()
let pasteData = pasteboard.createData(pasteboard.MIMETYPE_TEXT_PLAIN, '')
if (richEditorSelection.spans && richEditorSelection.spans.length > 0) {
let count = richEditorSelection.spans.length
for (let i = count - 1; i >= 0; i--) {
let item = richEditorSelection.spans[i]
if ((item as RichEditorTextSpanResult)?.textStyle) {
let span = item as RichEditorTextSpanResult
let style = span.textStyle
let data = pasteboard.createRecord(pasteboard.MIMETYPE_TEXT_PLAIN, span.value.substring(span.offsetInSpan[0], span.offsetInSpan[1]))
let prop = pasteData.getProperty()
let temp: Record<string, Object> = {
'color': style.fontColor,
'size': style.fontSize,
'style': style.fontStyle,
'weight': this.fontWeightTable[style.fontWeight],
'fontFamily': style.fontFamily,
'decorationType': style.decoration.type,
'decorationColor': style.decoration.color
}
prop.additions[i] = temp;
pasteData.addRecord(data)
pasteData.setProperty(prop)
}
}
}
sysBoard.clearData()
sysBoard.setData(pasteData).then(() => {
console.info('SelectionMenu copy option, Succeeded in setting PasteData.');
this.pasteEnable = true;
}).catch((err: BusinessError) => {
console.error('SelectionMenu copy option, Failed to set PasteData. Cause:' + err.message);
})
}
PopDataFromPasteboard(richEditorSelection: RichEditorSelection) {
let start = richEditorSelection.selection[0]
let end = richEditorSelection.selection[1]
if (start == end && this.controller) {
start = this.controller.getCaretOffset()
end = this.controller.getCaretOffset()
}
let moveOffset = 0
let sysBoard = pasteboard.getSystemPasteboard()
sysBoard.getData((err, data) => {
if (err) {
return
}
let count = data.getRecordCount()
for (let i = 0; i < count; i++) {
const element = data.getRecord(i);
let tex: RichEditorTextStyle = {
fontSize: 16,
fontColor: Color.Black,
fontWeight: FontWeight.Normal,
fontFamily: "HarmonyOS Sans",
fontStyle: FontStyle.Normal,
decoration: { type: TextDecorationType.None, color: "#FF000000", style: TextDecorationStyle.SOLID }
}
if (data.getProperty() && data.getProperty().additions[i]) {
const tmp = data.getProperty().additions[i] as Record<string, Object | undefined>;
if (tmp.color) {
tex.fontColor = tmp.color as ResourceColor;
}
if (tmp.size) {
tex.fontSize = tmp.size as Length | number;
}
if (tmp.style) {
tex.fontStyle = tmp.style as FontStyle;
}
if (tmp.weight) {
tex.fontWeight = tmp.weight as number | FontWeight | string;
}
if (tmp.fontFamily) {
tex.fontFamily = tmp.fontFamily as ResourceStr;
}
if (tmp.decorationType && tex.decoration) {
tex.decoration.type = tmp.decorationType as TextDecorationType;
}
if (tmp.decorationColor && tex.decoration) {
tex.decoration.color = tmp.decorationColor as ResourceColor;
}
if (tex.decoration) {
tex.decoration = { type: tex.decoration.type, color: tex.decoration.color }
}
}
if (element && element.plainText && element.mimeType === pasteboard.MIMETYPE_TEXT_PLAIN && this.controller) {
this.controller.addTextSpan(element.plainText,
{
style: tex,
offset: start + moveOffset
}
)
moveOffset += element.plainText.length
}
}
if (this.controller) {
this.controller.setCaretOffset(start + moveOffset)
this.controller.closeSelectionMenu()
}
if (start != end && this.controller) {
this.controller.deleteSpans({ start: start + moveOffset, end: end + moveOffset })
}
})
}
@Builder
panel() {
Column() {
this.iconPanel()
if (!this.sliderShow) {
this.SystemMenu()
} else {
this.sliderPanel()
}
}.width(256)
}
@Builder iconPanel() {
Column() {
Row({ space: 2 }) {
ForEach(this.iconArr, (item:Resource, index ?: number) => {
Flex({ justifyContent: FlexAlign.Center, alignItems: ItemAlign.Center }) {
Image(item).fillColor(this.theme.imageFillColor).width(24).height(24).focusable(true).draggable(false)
}
.borderRadius(this.theme.iconBorderRadius)
.width(this.theme.buttonSize)
.height(this.theme.buttonSize)
.onClick(() => {
if (index as number == 0) {
this.sliderShow = false
if (this.controller) {
let selection = this.controller.getSelection();
let spans = selection.spans
spans.forEach((item: RichEditorTextSpanResult | RichEditorImageSpanResult, index) => {
if (typeof (item as RichEditorTextSpanResult)['textStyle'] != 'undefined') {
let span = item as RichEditorTextSpanResult
this.textStyle = span.textStyle
let start = span.offsetInSpan[0]
let end = span.offsetInSpan[1]
let offset = span.spanPosition.spanRange[0]
if (this.textStyle.fontWeight != 11) {
this.textStyle.fontWeight = FontWeight.Bolder
} else {
this.textStyle.fontWeight = FontWeight.Normal
}
this.controller.updateSpanStyle({
start: offset + start,
end: offset + end,
textStyle: this.textStyle
})
}
})
}
} else if (index as number == 1) {
this.sliderShow = false
if (this.controller) {
let selection = this.controller.getSelection();
let spans = selection.spans
spans.forEach((item: RichEditorTextSpanResult | RichEditorImageSpanResult, index) => {
if (typeof (item as RichEditorTextSpanResult)['textStyle'] != 'undefined') {
let span = item as RichEditorTextSpanResult
this.textStyle = span.textStyle
let start = span.offsetInSpan[0]
let end = span.offsetInSpan[1]
let offset = span.spanPosition.spanRange[0]
if (this.textStyle.fontStyle == FontStyle.Italic) {
this.textStyle.fontStyle = FontStyle.Normal
} else {
this.textStyle.fontStyle = FontStyle.Italic
}
this.controller.updateSpanStyle({
start: offset + start,
end: offset + end,
textStyle: this.textStyle
})
}
})
}
} else if (index as number == 2) {
this.sliderShow = false
if (this.controller) {
let selection = this.controller.getSelection();
let spans = selection.spans
spans.forEach((item: RichEditorTextSpanResult | RichEditorImageSpanResult, index) => {
if (typeof (item as RichEditorTextSpanResult)['textStyle'] != 'undefined') {
let span = item as RichEditorTextSpanResult
this.textStyle = span.textStyle
let start = span.offsetInSpan[0]
let end = span.offsetInSpan[1]
let offset = span.spanPosition.spanRange[0]
if (this.textStyle.decoration) {
if (this.textStyle.decoration.type == TextDecorationType.Underline) {
this.textStyle.decoration.type = TextDecorationType.None
} else {
this.textStyle.decoration.type = TextDecorationType.Underline
}
} else {
this.textStyle.decoration = { type: TextDecorationType.Underline, color: Color.Black, style: TextDecorationStyle.SOLID }
}
this.controller.updateSpanStyle({
start: offset + start,
end: offset + end,
textStyle: this.textStyle
})
}
})
}
} else if (index as number == 3) {
this.sliderShow = !this.sliderShow
} else if (index as number == 4) {
this.sliderShow = false
if (this.controller) {
let selection = this.controller.getSelection();
let spans = selection.spans
spans.forEach((item: RichEditorTextSpanResult | RichEditorImageSpanResult, index) => {
if (typeof (item as RichEditorTextSpanResult)['textStyle'] != 'undefined') {
let span = item as RichEditorTextSpanResult
this.textStyle = span.textStyle
let start = span.offsetInSpan[0]
let end = span.offsetInSpan[1]
let offset = span.spanPosition.spanRange[0]
if (this.textStyle.fontColor == Color.Orange || this.textStyle.fontColor == '#FFFFA500') {
this.textStyle.fontColor = Color.Black
} else {
this.textStyle.fontColor = Color.Orange
}
this.controller.updateSpanStyle({
start: offset + start,
end: offset + end,
textStyle: this.textStyle
})
}
})
}
}
})
.onTouch((event?: TouchEvent | undefined) => {
if(event != undefined){
if (event.type === TouchType.Down) {
this.iconBgColor[index as number] = $r('sys.color.ohos_id_color_click_effect')
}
if (event.type === TouchType.Up) {
this.iconBgColor[index as number] = this.colorTransparent
}
}
})
.onHover((isHover?: boolean, event?: HoverEvent) => {
this.iconBgColor.forEach((icon:ResourceColor, index1) => {
this.iconBgColor[index1] = this.colorTransparent
})
if(isHover != undefined) {
this.iconBgColor[index as number] = $r('sys.color.ohos_id_color_hover')
}
})
.backgroundColor(this.iconBgColor[index as number])
})
}
}
.clip(true)
.width(this.theme.defaultMenuWidth)
.padding(this.theme.expandedOptionPadding)
.borderRadius(this.theme.containerBorderRadius)
.margin({ bottom: this.theme.menuSpacing })
.backgroundColor(this.theme.backGroundColor)
.shadow(this.theme.iconPanelShadowStyle)
}
@Builder
SystemMenu() {
Column() {
Menu() {
if (this.controller) {
MenuItemGroup() {
MenuItem({ startIcon: this.theme.cutIcon, content: "剪切", labelInfo: "Ctrl+X" })
.onClick(() => {
if (!this.controller) {
return
}
let richEditorSelection = this.controller.getSelection()
this.PushDataToPasteboard(richEditorSelection);
this.controller.deleteSpans({
start: richEditorSelection.selection[0],
end: richEditorSelection.selection[1]
})
})
MenuItem({ startIcon: this.theme.copyIcon, content: "复制", labelInfo: "Ctrl+C" })
.onClick(() => {
if (!this.controller) {
return
}
let richEditorSelection = this.controller.getSelection()
this.PushDataToPasteboard(richEditorSelection);
this.controller.closeSelectionMenu()
})
MenuItem({ startIcon: this.theme.pasteIcon, content: "粘贴", labelInfo: "Ctrl+V" })
.enabled(this.pasteEnable)
.onClick(() => {
if (!this.controller) {
return
}
let richEditorSelection = this.controller.getSelection()
this.PopDataFromPasteboard(richEditorSelection)
})
MenuItem({ startIcon: this.theme.selectAllIcon, content: "全选", labelInfo: "Ctrl+A" })
.visibility(this.visibilityValue)
.onClick(() => {
if (!this.controller) {
return
}
this.controller.setSelection(-1, -1)
this.visibilityValue = Visibility.None
})
MenuItem({ startIcon: this.theme.shareIcon, content: "分享", labelInfo: "" })
.enabled(false)
MenuItem({ startIcon: this.theme.translateIcon, content: "翻译", labelInfo: "" })
.enabled(false)
MenuItem({ startIcon: this.theme.searchIcon, content: "搜索", labelInfo: "" })
.enabled(false)
}
}
}
.onVisibleAreaChange([0.0, 1.0], () => {
if (!this.controller) {
return
}
let richEditorSelection = this.controller.getSelection()
let start = richEditorSelection.selection[0]
let end = richEditorSelection.selection[1]
if (start === 0 && this.controller.getSpans({ start: end + 1, end: end + 1 }).length === 0) {
this.visibilityValue = Visibility.None
} else {
this.visibilityValue = Visibility.Visible
}
})
.radius(this.theme.containerBorderRadius)
.clip(true)
.backgroundColor(Color.White)
.width(this.theme.defaultMenuWidth)
}
.width(this.theme.defaultMenuWidth)
}
@Builder sliderPanel() {
Column() {
Flex({ justifyContent: FlexAlign.SpaceBetween, alignItems: ItemAlign.Center }) {
Text('A').fontSize(15)
Slider({ value: this.textSize, step: 10, style: SliderStyle.InSet })
.width(210)
.onChange((value: number, mode: SliderChangeMode) => {
if (this.controller) {
let selection = this.controller.getSelection();
if (mode == SliderChangeMode.End) {
if (this.textSize == undefined) {
this.textSize = 0
}
let spans = selection.spans
spans.forEach((item: RichEditorTextSpanResult | RichEditorImageSpanResult, index) => {
if (typeof (item as RichEditorTextSpanResult)['textStyle'] != 'undefined') {
this.textSize = Math.max(this.textSize, (item as RichEditorTextSpanResult).textStyle.fontSize)
}
})
}
if (mode == SliderChangeMode.Moving || mode == SliderChangeMode.Click) {
this.start = selection.selection[0]
this.end = selection.selection[1]
this.textSize = value
this.controller.updateSpanStyle({
start: this.start,
end: this.end,
textStyle: { fontSize: this.textSize }
})
}
}
})
Text('A').fontSize(20).fontWeight(FontWeight.Medium)
}.borderRadius(this.theme.containerBorderRadius)
}
.shadow(ShadowStyle.OUTER_DEFAULT_MD)
.backgroundColor(Color.White)
.borderRadius(this.theme.containerBorderRadius)
.padding(15)
.height(48)
}
}
瞧,靠着 bindSelectionMenu
,RichEditor
里的每一次选择,都能开启一场超欢乐、超有创意的小冒险,让创作过程趣味横生哟!
PS:实际项目中如有出入,请告知博主,博主会第一时间修改得哇~