fix(地点): 候选改为 bindPopup 浮层,根治输入框跳动与选完复现
旧方案把候选做成内联列表,虽放在输入框上方,但:
1. 候选高度变化(搜索中 / 出结果 / 条数变化)会改变表单高度,输入框随之反复移动,
再靠 scrollBy 事后纠正,表现为输入框来回跳好几次;
2. 选中候选后 applyPlace 程序回写 text 会触发 TextInput.onChange,
把 placePicked 重置为 false 并重新发起搜索,导致候选框消失后又冒出来一次。
改为:
- 候选用 .bindPopup($$placeOpen, { placement: Top }) 挂在输入框上,
不参与布局,出现与消失完全不移动输入框;
- 移除全部 scrollBy 对齐逻辑(keyboardPadVp / ensurePlaceVisible / 键盘监听),
键盘避让交由 KeyboardAvoidMode.RESIZE 的默认光标可见行为;
- 用 lastAppliedText 精确识别「程序回写 text 造成的 onChange」,不再发搜索;
- runSuggest 期间浮层持续展开(搜索中 → 结果),避免收起再弹造成闪烁;
- 抽出 closePlacePopup() 作为统一收起入口。
This commit is contained in:
@@ -1,6 +1,6 @@
|
|||||||
// entry/src/main/ets/pages/EventEditPage.ets
|
// entry/src/main/ets/pages/EventEditPage.ets
|
||||||
// 日程编辑页:新建 / 修改 / 删除日程(一律属于某个 CalDAV 日历本,不再支持"本机"日程)
|
// 日程编辑页:新建 / 修改 / 删除日程(一律属于某个 CalDAV 日历本,不再支持"本机"日程)
|
||||||
import { curves, router, KeyboardAvoidMode, ComponentUtils, window } from '@kit.ArkUI';
|
import { curves, router, KeyboardAvoidMode } from '@kit.ArkUI';
|
||||||
import { common } from '@kit.AbilityKit';
|
import { common } from '@kit.AbilityKit';
|
||||||
import { BusinessError } from '@kit.BasicServicesKit';
|
import { BusinessError } from '@kit.BasicServicesKit';
|
||||||
import { notificationManager } from '@kit.NotificationKit';
|
import { notificationManager } from '@kit.NotificationKit';
|
||||||
@@ -37,11 +37,13 @@ struct EventEditPage {
|
|||||||
@State location: string = '';
|
@State location: string = '';
|
||||||
/**
|
/**
|
||||||
* ⭐ 地点补全(鸿蒙 Map Kit / Site Kit):
|
* ⭐ 地点补全(鸿蒙 Map Kit / Site Kit):
|
||||||
* placeItems —— 输入框下方的候选列表(内联展开,**不新增 bindSheet**:
|
* placeItems —— 候选数据(**不参与页面布局**,由 bindPopup 渲染成浮层)
|
||||||
* 本页只能挂一个弹层,见文件末尾注释)
|
* placeLoading —— 是否正在请求,浮层内显示"搜索中"
|
||||||
* placeLoading —— 是否正在请求,用于显示"搜索中"
|
* placeOpen —— 浮层是否弹出(绑在输入框上,向上弹)
|
||||||
* placeOpen —— 候选列表是否展开
|
* placeHint —— 无结果/地图能力不可用时的提示(不阻塞手输地点)
|
||||||
* placeHint —— 地图能力不可用时的提示(不阻塞用户手输地点)
|
*
|
||||||
|
* ⚠️ 为什么必须是浮层而不是内联列表:内联列表改变表单高度 ⇒ 输入框跟着移动 ⇒
|
||||||
|
* 每次"搜索中/出结果/结果变少"都要滚动纠正 ⇒ 用户看到输入框反复上下跳。
|
||||||
*/
|
*/
|
||||||
@State placeItems: PlaceItem[] = [];
|
@State placeItems: PlaceItem[] = [];
|
||||||
@State placeLoading: boolean = false;
|
@State placeLoading: boolean = false;
|
||||||
@@ -50,27 +52,17 @@ struct EventEditPage {
|
|||||||
/** 用户是否已从候选里选中(选中后不再自动弹候选,避免"选完又弹出") */
|
/** 用户是否已从候选里选中(选中后不再自动弹候选,避免"选完又弹出") */
|
||||||
private placePicked: boolean = false;
|
private placePicked: boolean = false;
|
||||||
/**
|
/**
|
||||||
* 地点输入框当前是否聚焦。
|
* 最后一次**程序写入**输入框的文本(选中候选后回写的地址)。
|
||||||
* 用途:候选条数变化会改变输入框上方的内容高度 → 输入框在屏幕上会挪位。
|
* TextInput 的 text 被程序改写同样会触发 onChange;用它精确识别"这次 onChange
|
||||||
* 只要还在聚焦状态(用户正在打字),就每次重新钉一次,保证它始终贴住键盘上沿。
|
* 不是用户打字造成的",从而不重复发起搜索。
|
||||||
*/
|
*/
|
||||||
|
private lastAppliedText: string = '';
|
||||||
|
/** 地点输入框当前是否聚焦(仅用于日志/状态,不再驱动滚动) */
|
||||||
private placeFocused: boolean = false;
|
private placeFocused: boolean = false;
|
||||||
/** 输入防抖定时器 */
|
/** 输入防抖定时器 */
|
||||||
private placeTimer: number = -1;
|
private placeTimer: number = -1;
|
||||||
/**
|
/** 表单滚动控制器(键盘避让由系统 RESIZE 模式负责,这里不再主动滚) */
|
||||||
* ⭐ 表单滚动控制器。
|
|
||||||
* 地点是表单第 7 个字段(标题/全天/时间/重复/提醒/日历本之后),
|
|
||||||
* 键盘弹起后输入框本身已经在屏幕下半部,候选列表更是被键盘压住。
|
|
||||||
* ArkUI 默认 OFFSET 模式只保证"光标可见",不会为候选列表留空间,
|
|
||||||
* 所以聚焦后要**主动滚动**,把输入框钉到可视区底边(= 键盘上沿)。
|
|
||||||
*/
|
|
||||||
private formScroller: Scroller = new Scroller();
|
private formScroller: Scroller = new Scroller();
|
||||||
/** 键盘高度(px),0 = 键盘未弹起。由 window 的 TYPE_KEYBOARD 避让区取得 */
|
|
||||||
private keyboardHeight: number = 0;
|
|
||||||
/** 键盘避让区变化监听器(页面销毁时注销) */
|
|
||||||
private keyboardWatcher: ((opt: window.AvoidAreaOptions) => void) | undefined = undefined;
|
|
||||||
/** 本页所属窗口(异步取得,拿到后才能挂监听) */
|
|
||||||
private hostWindow: window.Window | undefined = undefined;
|
|
||||||
@State description: string = '';
|
@State description: string = '';
|
||||||
@State allDay: boolean = false;
|
@State allDay: boolean = false;
|
||||||
@State startMs: number = 0;
|
@State startMs: number = 0;
|
||||||
@@ -129,63 +121,29 @@ struct EventEditPage {
|
|||||||
return this.initPage();
|
return this.initPage();
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 页面销毁:清掉地点补全的防抖定时器,并注销键盘监听 */
|
/** 页面销毁:清掉地点补全的防抖定时器 */
|
||||||
aboutToDisappear(): void {
|
aboutToDisappear(): void {
|
||||||
this.cancelSuggestTimer();
|
this.cancelSuggestTimer();
|
||||||
this.releaseKeyboardWatch();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* ⭐ 键盘避让:改成 RESIZE 模式。
|
* ⭐ 键盘避让:改成 RESIZE 模式。
|
||||||
* 默认 OFFSET 是「整页上移」,页面上移后表单底部(地点/备注/按钮)会被顶出屏幕,
|
* 默认 OFFSET 是「整页上移」,页面上移后表单底部(地点/备注/按钮)会被顶出屏幕。
|
||||||
* 且候选列表展开时不会重新计算可见区 → 列表被键盘压住。
|
|
||||||
* RESIZE 是「把页面高度压缩到键盘上沿」,滚动区域随之变矮,
|
* RESIZE 是「把页面高度压缩到键盘上沿」,滚动区域随之变矮,
|
||||||
* 此时 Scroll 的矩形底边就是真正的键盘上沿,据此把输入框钉住即可。
|
* 聚焦的输入框由系统自动滚入可见区,**无需再手动 scrollBy**。
|
||||||
|
*
|
||||||
|
* ⚠️ 曾经写过"手动量矩形 + scrollBy 对齐"的方案,是错的方向:
|
||||||
|
* ① getRectangleById 返回 px、scrollBy 收 vp,单位混用会把输入框推飞;
|
||||||
|
* ② 更根本的是——只要候选参与布局,位置就会反复变化,
|
||||||
|
* 靠"事后纠正"必然表现为抖动。所以候选改成了浮层(见 placePopup)。
|
||||||
*/
|
*/
|
||||||
private async setupKeyboardAvoid(): Promise<void> {
|
private setupKeyboardAvoid(): void {
|
||||||
try {
|
try {
|
||||||
this.getUIContext().setKeyboardAvoidMode(KeyboardAvoidMode.RESIZE);
|
this.getUIContext().setKeyboardAvoidMode(KeyboardAvoidMode.RESIZE);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
const e = err as BusinessError;
|
const e = err as BusinessError;
|
||||||
LogUtil.write(`设置键盘避让模式失败: ${e.message}`);
|
LogUtil.write(`设置键盘避让模式失败: ${e.message}`);
|
||||||
}
|
}
|
||||||
// 记录键盘高度:Scroll 矩形取不到时用它兜底算可视底边
|
|
||||||
try {
|
|
||||||
const c = this.getUIContext().getHostContext();
|
|
||||||
if (c === undefined) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
// ⚠️ UIAbilityContext 上没有 window 属性,必须用 window.getLastWindow 取
|
|
||||||
const win: window.Window = await window.getLastWindow(c);
|
|
||||||
this.hostWindow = win;
|
|
||||||
this.keyboardWatcher = (opt: window.AvoidAreaOptions): void => {
|
|
||||||
if (opt.type !== window.AvoidAreaType.TYPE_KEYBOARD) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
this.keyboardHeight = opt.area.visible ? opt.area.bottomRect.height : 0;
|
|
||||||
// 键盘弹起/收起后重新对齐一次(收起时不滚,保持用户当前浏览位置)
|
|
||||||
if (this.keyboardHeight > 0) {
|
|
||||||
this.ensurePlaceVisible();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
win.on('avoidAreaChange', this.keyboardWatcher);
|
|
||||||
} catch (err) {
|
|
||||||
const e = err as BusinessError;
|
|
||||||
LogUtil.write(`监听键盘高度失败: ${e.message}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private releaseKeyboardWatch(): void {
|
|
||||||
if (this.keyboardWatcher === undefined || this.hostWindow === undefined) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
this.hostWindow.off('avoidAreaChange', this.keyboardWatcher);
|
|
||||||
} catch (err) {
|
|
||||||
// 页面已在销毁,注销失败无所谓
|
|
||||||
}
|
|
||||||
this.keyboardWatcher = undefined;
|
|
||||||
this.hostWindow = undefined;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private async initPage(): Promise<void> {
|
private async initPage(): Promise<void> {
|
||||||
@@ -324,14 +282,7 @@ struct EventEditPage {
|
|||||||
this.cancelSuggestTimer();
|
this.cancelSuggestTimer();
|
||||||
const q: string = v.trim();
|
const q: string = v.trim();
|
||||||
if (q.length < 1) {
|
if (q.length < 1) {
|
||||||
this.placeItems = [];
|
this.closePlacePopup();
|
||||||
this.placeOpen = false;
|
|
||||||
this.placeLoading = false;
|
|
||||||
this.placeHint = '';
|
|
||||||
// 候选清空后上方内容变矮,输入框会往下掉 —— 重新钉一次
|
|
||||||
if (this.placeFocused) {
|
|
||||||
this.ensurePlaceVisible();
|
|
||||||
}
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this.placeTimer = setTimeout(() => {
|
this.placeTimer = setTimeout(() => {
|
||||||
@@ -354,18 +305,15 @@ struct EventEditPage {
|
|||||||
if (PlaceSuggest.isUnavailable()) {
|
if (PlaceSuggest.isUnavailable()) {
|
||||||
// 地图服务没开通:给一次明确提示,然后永久静默(不阻塞手输)
|
// 地图服务没开通:给一次明确提示,然后永久静默(不阻塞手输)
|
||||||
this.placeHint = '地图服务不可用,可直接手动输入地点';
|
this.placeHint = '地图服务不可用,可直接手动输入地点';
|
||||||
this.placeOpen = false;
|
this.placeItems = [];
|
||||||
|
this.placeOpen = true;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// 用户输入时若带空格,PlaceSuggest 内部会 trim
|
// ⭐ 浮层一直保持展开(从"搜索中"过渡到结果),中间不收起、不重弹 ——
|
||||||
// ⭐ 先把旧候选收起再显示"搜索中":候选区高度变化会让输入框上下跳,
|
// 收起再弹会让浮层闪一下,看起来像"消失了又出现"。
|
||||||
// 收起后由 ensurePlaceVisible() 统一重新钉住。
|
|
||||||
this.placeOpen = false;
|
|
||||||
this.placeLoading = true;
|
this.placeLoading = true;
|
||||||
this.placeHint = '';
|
this.placeHint = '';
|
||||||
if (this.placeFocused) {
|
this.placeOpen = true;
|
||||||
this.ensurePlaceVisible();
|
|
||||||
}
|
|
||||||
try {
|
try {
|
||||||
const c = this.getUIContext().getHostContext();
|
const c = this.getUIContext().getHostContext();
|
||||||
const ctx: common.Context | undefined = c;
|
const ctx: common.Context | undefined = c;
|
||||||
@@ -379,94 +327,115 @@ struct EventEditPage {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this.placeItems = items.slice(0, 6);
|
this.placeItems = items.slice(0, 6);
|
||||||
this.placeOpen = this.placeItems.length > 0;
|
|
||||||
// ⭐ 候选条数变化改变了输入框上方的高度 → 重新钉住输入框
|
|
||||||
if (this.placeFocused || this.placeOpen) {
|
|
||||||
this.ensurePlaceVisible();
|
|
||||||
}
|
|
||||||
if (this.placeItems.length === 0) {
|
if (this.placeItems.length === 0) {
|
||||||
this.placeHint = PlaceSuggest.isUnavailable()
|
this.placeHint = PlaceSuggest.isUnavailable()
|
||||||
? '地图服务不可用,可直接手动输入地点'
|
? '地图服务不可用,可直接手动输入地点'
|
||||||
: '没有找到匹配地点,可直接手动输入';
|
: '没有找到匹配地点,可直接手动输入';
|
||||||
|
// 无结果也留在浮层里给提示;用户再输入会被下一次请求覆盖
|
||||||
|
this.placeOpen = true;
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
const e = err as BusinessError;
|
const e = err as BusinessError;
|
||||||
LogUtil.write(`地点补全异常: ${e.message}`);
|
LogUtil.write(`地点补全异常: ${e.message}`);
|
||||||
|
this.placeItems = [];
|
||||||
this.placeHint = '地点搜索失败,可直接手动输入';
|
this.placeHint = '地点搜索失败,可直接手动输入';
|
||||||
this.placeOpen = false;
|
this.placeOpen = true;
|
||||||
} finally {
|
} finally {
|
||||||
this.placeLoading = false;
|
this.placeLoading = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* ⭐ 把「地点输入框」钉在键盘正上方:滚到「输入框底边 = 可视区底边 − 10vp」,
|
* ⭐ 候选浮层(bindPopup)的内容。
|
||||||
* 候选列表在它**上方**向上生长,所以输入框不会被候选顶走,用户可以边看边继续打字。
|
|
||||||
*
|
*
|
||||||
* 两个曾经踩过的坑,这里都规避掉:
|
* 为什么是浮层而不是内联列表:
|
||||||
* ① **不要重复扣键盘高度**。本页已设 KeyboardAvoidMode.RESIZE —— 窗口本身
|
* 内联列表会改变表单高度 ⇒ 输入框位置随之变化 ⇒ 每次"搜索中/出结果/结果变少"
|
||||||
* 已经压缩到键盘上沿之外,`Scroll` 的矩形底边就是真正可视底边。
|
* 都要靠滚动去纠正 ⇒ 用户看到的就是**输入框上下跳好几次**。
|
||||||
* 再减一次 keyboardHeight 会滚过头(输入框被推飞出屏幕)。
|
* 浮层不参与布局,出现与消失**完全不移动输入框**,这是唯一能根治该问题的结构。
|
||||||
* ② **单位必须换算**。`getRectangleById()` 返回 **px**,而 `Scroller.scrollBy()`
|
*
|
||||||
* 收 **vp**。直接把 px 当 vp 传,在高密度屏上等于多滚了 3 倍 ——
|
* 为什么向上弹(Placement.Top):
|
||||||
* 这就是上一版"输入框突然跑到上面去、看不见了"的真正原因。
|
* 输入框在表单下部、键盘又在下方,向下弹必然被键盘盖住;
|
||||||
|
* 向上弹则整块候选都落在输入框与标题之间的空白区。
|
||||||
*/
|
*/
|
||||||
private ensurePlaceVisible(): void {
|
@Builder
|
||||||
// 双延时:等键盘收缩动画 + 候选列表重新布局都稳定后再量尺寸
|
placePopup() {
|
||||||
setTimeout(() => {
|
Column({ space: 0 }) {
|
||||||
setTimeout(() => {
|
if (this.placeLoading) {
|
||||||
try {
|
Row({ space: 8 }) {
|
||||||
const ui = this.getUIContext();
|
LoadingProgress().width(16).height(16).color($r('app.color.text_hint'))
|
||||||
// ⚠️ getRectangleById 在 ComponentUtils 上,不在 UIContext 上
|
Text('搜索中…')
|
||||||
const cu: ComponentUtils = ui.getComponentUtils();
|
.fontSize(13)
|
||||||
const vp2px: number = ui.vp2px(1);
|
.fontColor($r('app.color.text_hint'))
|
||||||
let rowBottomPx: number = 0;
|
|
||||||
let visibleBottomPx: number = 0;
|
|
||||||
try {
|
|
||||||
const row = cu.getRectangleById('placeRow');
|
|
||||||
rowBottomPx = row.windowOffset.y + row.size.height;
|
|
||||||
} catch (e) {
|
|
||||||
return; // 组件还没上树,放弃本次校正
|
|
||||||
}
|
}
|
||||||
// 首选:Scroll 的真实矩形(RESIZE 模式下它已经被裁到键盘上沿)
|
.width('100%')
|
||||||
try {
|
.padding({ left: 14, right: 14, top: 12, bottom: 12 })
|
||||||
const sc = cu.getRectangleById('formScroll');
|
} else if (this.placeItems.length > 0) {
|
||||||
visibleBottomPx = sc.windowOffset.y + sc.size.height;
|
// 限高 + 内部滚动:候选最多 6 条,一次全铺开会顶到屏幕顶部
|
||||||
} catch (e2) {
|
List() {
|
||||||
visibleBottomPx = 0;
|
ForEach(this.placeItems, (it: PlaceItem, idx: number) => {
|
||||||
|
ListItem() {
|
||||||
|
Column({ space: 2 }) {
|
||||||
|
Text(it.title)
|
||||||
|
.fontSize(14)
|
||||||
|
.fontColor($r('app.color.text_primary'))
|
||||||
|
.maxLines(1)
|
||||||
|
.textOverflow({ overflow: TextOverflow.Ellipsis })
|
||||||
|
.width('100%')
|
||||||
|
.textAlign(TextAlign.Start)
|
||||||
|
if (it.subtitle !== '') {
|
||||||
|
Text(it.subtitle)
|
||||||
|
.fontSize(12)
|
||||||
|
.fontColor($r('app.color.text_secondary'))
|
||||||
|
.maxLines(1)
|
||||||
|
.textOverflow({ overflow: TextOverflow.Ellipsis })
|
||||||
|
.width('100%')
|
||||||
|
.textAlign(TextAlign.Start)
|
||||||
}
|
}
|
||||||
// 兜底:拿不到 Scroll 矩形时用「窗口高 − 键盘高」
|
|
||||||
if (visibleBottomPx <= 0 && this.keyboardHeight > 0 && this.hostWindow !== undefined) {
|
|
||||||
visibleBottomPx = this.hostWindow.getWindowProperties().windowRect.height - this.keyboardHeight;
|
|
||||||
}
|
}
|
||||||
if (visibleBottomPx <= 0) {
|
.alignItems(HorizontalAlign.Start)
|
||||||
return;
|
.width('100%')
|
||||||
|
.padding({ left: 14, right: 14, top: 10, bottom: 10 })
|
||||||
}
|
}
|
||||||
const gapPx: number = vp2px * 10;
|
.onClick(() => {
|
||||||
const deltaPx: number = rowBottomPx - (visibleBottomPx - gapPx);
|
this.applyPlace(it);
|
||||||
// ±6vp 内视为已对齐,避免每次按键都抖一下
|
})
|
||||||
if (Math.abs(deltaPx) > vp2px * 6) {
|
}, (it: PlaceItem, idx: number) => `${it.siteId}_${idx}`)
|
||||||
// ⚠️ px → vp 换算,见上面坑②
|
|
||||||
this.formScroller.scrollBy(0, deltaPx / vp2px);
|
|
||||||
LogUtil.write(`地点对齐:滚 ${Math.round(deltaPx / vp2px)}vp(行底 ${Math.round(rowBottomPx / vp2px)}vp / 可视底 ${Math.round(visibleBottomPx / vp2px)}vp)`);
|
|
||||||
}
|
}
|
||||||
} catch (err) {
|
.width('100%')
|
||||||
const e = err as BusinessError;
|
.constraintSize({ maxHeight: 216 })
|
||||||
LogUtil.write(`地点对齐失败: ${e.message}`);
|
.scrollBar(BarState.Auto)
|
||||||
|
.divider({ strokeWidth: 0.5, color: $r('app.color.shadow_color') })
|
||||||
|
} else {
|
||||||
|
Text(this.placeHint !== '' ? this.placeHint : '没有找到匹配地点,可直接手动输入')
|
||||||
|
.fontSize(12)
|
||||||
|
.fontColor($r('app.color.text_hint'))
|
||||||
|
.width('100%')
|
||||||
|
.padding({ left: 14, right: 14, top: 12, bottom: 12 })
|
||||||
}
|
}
|
||||||
}, 60);
|
}
|
||||||
}, 60);
|
.width('100%')
|
||||||
|
.padding({ top: 4, bottom: 4 })
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 选中一条候选:写进地点输入框并收起列表 */
|
/** 选中一条候选:写进地点输入框并收起列表 */
|
||||||
private applyPlace(it: PlaceItem): void {
|
private applyPlace(it: PlaceItem): void {
|
||||||
// 写入完整地址(formatAddress),这样同步到服务器/其他客户端时地址是完整的
|
// ⭐ 顺序很关键:先收起浮层,再写文本。
|
||||||
this.location = it.locationText;
|
// TextInput 的 text 被程序改写会触发 onChange,若此时 placePicked 已被
|
||||||
this.placePicked = true;
|
// onChange 重置为 false,就会**重新发起一次搜索** → 用户看到
|
||||||
this.placeItems = [];
|
// "候选框消失了一下又冒出来一次"。用 lastAppliedText 精确识别这类回写。
|
||||||
this.placeOpen = false;
|
|
||||||
this.placeHint = '';
|
|
||||||
this.cancelSuggestTimer();
|
this.cancelSuggestTimer();
|
||||||
|
this.closePlacePopup();
|
||||||
|
this.placePicked = true;
|
||||||
|
this.lastAppliedText = it.locationText;
|
||||||
|
this.location = it.locationText;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 收起候选浮层(统一入口,保证 placePicked 之外的状态都被清干净) */
|
||||||
|
private closePlacePopup(): void {
|
||||||
|
this.placeOpen = false;
|
||||||
|
this.placeLoading = false;
|
||||||
|
this.placeItems = [];
|
||||||
|
this.placeHint = '';
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 打开系统地图选点页(可搜索、可周边、可拖图钉) */
|
/** 打开系统地图选点页(可搜索、可周边、可拖图钉) */
|
||||||
@@ -1457,68 +1426,9 @@ struct EventEditPage {
|
|||||||
.borderRadius(12)
|
.borderRadius(12)
|
||||||
.backgroundColor($r('app.color.card_bg'))
|
.backgroundColor($r('app.color.card_bg'))
|
||||||
|
|
||||||
// ⭐ 候选列表放在输入框**上方**:这样候选展开时只需把"输入框"贴到键盘上沿,
|
|
||||||
// 候选自然向上占据剩余空间,输入框不会被顶走,用户能一边看候选一边继续打字。
|
|
||||||
if (this.placeLoading) {
|
|
||||||
Row({ space: 6 }) {
|
|
||||||
LoadingProgress().width(16).height(16).color($r('app.color.text_hint'))
|
|
||||||
Text('搜索中…')
|
|
||||||
.fontSize(12)
|
|
||||||
.fontColor($r('app.color.text_hint'))
|
|
||||||
}
|
|
||||||
.width('100%')
|
|
||||||
.padding({ left: 14, top: 2, bottom: 6 })
|
|
||||||
} else if (this.placeOpen && this.placeItems.length > 0) {
|
|
||||||
// ⭐ 用 List 而不是 Column:候选可能 6 条,全展开会占掉大半个屏幕。
|
|
||||||
// 限高 216vp(约 4 条)后内部可滚,既看得全又不会把输入框挤出屏幕。
|
|
||||||
List() {
|
|
||||||
ForEach(this.placeItems, (it: PlaceItem, idx: number) => {
|
|
||||||
ListItem() {
|
|
||||||
Column({ space: 2 }) {
|
|
||||||
Text(it.title)
|
|
||||||
.fontSize(14)
|
|
||||||
.fontColor($r('app.color.text_primary'))
|
|
||||||
.maxLines(1)
|
|
||||||
.textOverflow({ overflow: TextOverflow.Ellipsis })
|
|
||||||
.width('100%')
|
|
||||||
.textAlign(TextAlign.Start)
|
|
||||||
if (it.subtitle !== '') {
|
|
||||||
Text(it.subtitle)
|
|
||||||
.fontSize(12)
|
|
||||||
.fontColor($r('app.color.text_secondary'))
|
|
||||||
.maxLines(1)
|
|
||||||
.textOverflow({ overflow: TextOverflow.Ellipsis })
|
|
||||||
.width('100%')
|
|
||||||
.textAlign(TextAlign.Start)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.alignItems(HorizontalAlign.Start)
|
|
||||||
.width('100%')
|
|
||||||
.padding({ left: 14, right: 14, top: 9, bottom: 9 })
|
|
||||||
}
|
|
||||||
.onClick(() => {
|
|
||||||
this.applyPlace(it);
|
|
||||||
})
|
|
||||||
}, (it: PlaceItem, idx: number) => `${it.siteId}_${idx}`)
|
|
||||||
}
|
|
||||||
.width('100%')
|
|
||||||
.constraintSize({ maxHeight: 216 })
|
|
||||||
.borderRadius(12)
|
|
||||||
.backgroundColor($r('app.color.card_bg'))
|
|
||||||
.padding({ top: 4, bottom: 4 })
|
|
||||||
.scrollBar(BarState.Auto)
|
|
||||||
.divider({ strokeWidth: 0.5, color: $r('app.color.shadow_color') })
|
|
||||||
.margin({ bottom: 8 })
|
|
||||||
.id('placeList')
|
|
||||||
} else if (this.placeHint !== '') {
|
|
||||||
Text(this.placeHint)
|
|
||||||
.fontSize(12)
|
|
||||||
.fontColor($r('app.color.text_hint'))
|
|
||||||
.width('100%')
|
|
||||||
.padding({ left: 14, bottom: 6 })
|
|
||||||
}
|
|
||||||
|
|
||||||
// 地点(输入几个字 → 系统地图服务补全候选)
|
// 地点(输入几个字 → 系统地图服务补全候选)
|
||||||
|
// ⭐ 候选列表**不在布局流里**:它通过 bindPopup 挂在输入框上、向**上方**弹出。
|
||||||
|
// 这样候选的出现/消失完全不改变输入框位置 —— 从根上消除"输入框上下跳"。
|
||||||
Row({ space: 8 }) {
|
Row({ space: 8 }) {
|
||||||
TextInput({ text: this.location, placeholder: '地点(可选,输入几个字可搜索)' })
|
TextInput({ text: this.location, placeholder: '地点(可选,输入几个字可搜索)' })
|
||||||
.height(44)
|
.height(44)
|
||||||
@@ -1530,6 +1440,15 @@ struct EventEditPage {
|
|||||||
.placeholderColor($r('app.color.text_hint'))
|
.placeholderColor($r('app.color.text_hint'))
|
||||||
.caretColor($r('app.color.brand'))
|
.caretColor($r('app.color.brand'))
|
||||||
.onChange((v: string) => {
|
.onChange((v: string) => {
|
||||||
|
// ⭐ 程序改写 text(选中候选后回写地址)也会触发 onChange。
|
||||||
|
// 精确比对最后一次程序写入值:命中就不再发起搜索,
|
||||||
|
// 否则会"选完 → 候选框消失 → 又冒出来一次"。
|
||||||
|
if (v === this.lastAppliedText) {
|
||||||
|
this.lastAppliedText = '';
|
||||||
|
this.location = v;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.lastAppliedText = '';
|
||||||
this.location = v;
|
this.location = v;
|
||||||
// 用户重新输入时解除"已选中"状态,恢复自动补全
|
// 用户重新输入时解除"已选中"状态,恢复自动补全
|
||||||
this.placePicked = false;
|
this.placePicked = false;
|
||||||
@@ -1540,19 +1459,33 @@ struct EventEditPage {
|
|||||||
this.cancelSuggestTimer();
|
this.cancelSuggestTimer();
|
||||||
this.runSuggest(this.location, true);
|
this.runSuggest(this.location, true);
|
||||||
})
|
})
|
||||||
// ⭐ 一聚焦就把输入框钉到键盘上方:键盘弹起是异步的,延时对齐两次
|
|
||||||
// (320ms 时键盘基本已展开;ensurePlaceVisible 内部还会再等 120ms 稳定布局)
|
|
||||||
.onFocus(() => {
|
.onFocus(() => {
|
||||||
|
// 键盘避让交给系统(RESIZE 已保证光标可见);
|
||||||
|
// 候选是浮层,不需要再滚动页面去给它腾位置
|
||||||
this.placeFocused = true;
|
this.placeFocused = true;
|
||||||
setTimeout(() => {
|
|
||||||
this.ensurePlaceVisible();
|
|
||||||
}, 320);
|
|
||||||
})
|
})
|
||||||
.onBlur(() => {
|
.onBlur(() => {
|
||||||
// 失焦(点"地图"按钮、点别处、收起键盘)→ 收起候选,避免列表残留在屏幕上
|
// 失焦(点"地图"按钮、点别处、收起键盘)→ 收起候选浮层
|
||||||
this.placeFocused = false;
|
this.placeFocused = false;
|
||||||
|
this.closePlacePopup();
|
||||||
|
})
|
||||||
|
.bindPopup($$this.placeOpen, {
|
||||||
|
builder: (): void => {
|
||||||
|
this.placePopup()
|
||||||
|
},
|
||||||
|
placement: Placement.Top,
|
||||||
|
popupColor: $r('app.color.card_bg'),
|
||||||
|
enableArrow: false,
|
||||||
|
showInSubWindow: false,
|
||||||
|
focusable: false,
|
||||||
|
autoCancel: true,
|
||||||
|
radius: 12,
|
||||||
|
onStateChange: (e: PopupStateChangeParam) => {
|
||||||
|
// 用户点浮层外部/floating 自动关闭时,同步状态,避免下次不弹
|
||||||
|
if (!e.isVisible && this.placeOpen) {
|
||||||
this.placeOpen = false;
|
this.placeOpen = false;
|
||||||
this.placeLoading = false;
|
}
|
||||||
|
}
|
||||||
})
|
})
|
||||||
.id('placeAnchor')
|
.id('placeAnchor')
|
||||||
// 在地图上选点(系统地图界面,可选可搜可周边)
|
// 在地图上选点(系统地图界面,可选可搜可周边)
|
||||||
@@ -1614,7 +1547,6 @@ struct EventEditPage {
|
|||||||
.layoutWeight(1)
|
.layoutWeight(1)
|
||||||
.scrollBar(BarState.Off)
|
.scrollBar(BarState.Off)
|
||||||
.edgeEffect(EdgeEffect.Spring)
|
.edgeEffect(EdgeEffect.Spring)
|
||||||
.id('formScroll')
|
|
||||||
.align(Alignment.Top)
|
.align(Alignment.Top)
|
||||||
}
|
}
|
||||||
.width('100%')
|
.width('100%')
|
||||||
|
|||||||
Reference in New Issue
Block a user