HarmonyOS鸿蒙Next中Scroll组件和textArea组件嵌套滚动解决方案
HarmonyOS鸿蒙Next中Scroll组件和textArea组件嵌套滚动解决方案 需求:textArea文本内容垂直滚动到顶/底时,需要接着滚动父组件Scroll
背景:目前API版本,scroller控制器无法应用于textArea,且textArea组件中没有nestedScroll属性可以像Scroll等组件一样设置
.nestedScroll({
scrollForward: NestedScrollMode.SELF_FIRST,
scrollBackward:NestedScrollMode.PARENT_FIRST
})
环境要求:compatibleSdkVersion 12+
解决方案:
1)判定textArea组件是否滚动到顶/底
是否滚动到顶,可根据
.onContentScroll((_totalOffsetX: number, totalOffsetY: number) => {})
中的totalOffsetY偏移量 > 0,判定滚动到顶
是否滚动到底,可根据
文本总高度 - totalOffsetY偏移量 <= textArea组件实际渲染高度,判定滚动到底
其中文本总高度可利用
this.getUIContext().getMeasureUtils().measureTextSize({
textContent: textContent,
fontSize: fontSize,
// constraintWidth单位vp
constraintWidth: constraintWidth,
lineHeight: ...,
...
}).height
来计算,constraintWidth参数可从textArea组件实际渲染宽度获取
2)当textArea组件滚动到顶/底时,将touch事件的move动作的移动距离,传递给父组件scoll的scoller控制器,实现scoll与textArea嵌套滚动
代码实现:
import Logger from '../../common/utils/Logger'
@Component
@Entry
struct ScrollPage {
@State prompt: ResourceStr = ''
private scroller: Scroller = new Scroller();
build() {
Scroll(this.scroller) {
Column() {
buildTextArea({
textValue: this.prompt,
parentScroller: this.scroller
})
}
// 其他...
}
}
}
@Component
struct buildTextArea {
@Link textValue: ResourceStr;
// 获取textArea组件内当前text文本总高度,单位vp
@State currentTextHeight: Length | undefined = undefined;
@State textAreaWidth: Length | undefined = undefined;
@State textAreaHeight: Length | undefined = undefined;
@State isScrollTop: boolean = false;
@State isScrollBottom: boolean = false;
// 记录触摸点相对父组件的y坐标
@State touchDownY: number = 0
private uiContext: UIContext = this.getUIContext();
private fontSize: Resource = $r('app.float.small_text_size');
private textPadding: Resource = $r('app.float.line_padding_left')
parentScroller: Scroller = new Scroller();
constructor(textValue: ResourceStr, parentScroller: Scroller) {
super();
this.textValue = textValue
this.parentScroller = parentScroller
}
/**
* 根据文本内容,字体大小,行高,行宽等计算文本总高度
* @param textContent
* @param fontSize
* @param constraintWidth 单位vp
* @returns 单位vp
*/
private getTextHeight(textContent: ResourceStr, fontSize: Resource, constraintWidth: Length): Length | undefined {
let textHeight = this.uiContext.getMeasureUtils().measureTextSize({
textContent: textContent,
fontSize: fontSize,
// constraintWidth单位vp
constraintWidth: constraintWidth
}).height
return textHeight ? this.uiContext.px2vp(parseFloat(textHeight.toString())) : undefined
}
build() {
TextArea({ text: this.textValue })
.fontSize(this.fontSize)
.width('100%')
.height('100%')
.padding(this.textPadding)
.border({ width: 1, color: '#ddd' })
.onAreaChange((_oldVal: Area, newVal: Area) => {
// Area单位vp
if (!this.textAreaHeight) {
// 减去上padding的8vp
this.textAreaHeight = parseFloat(newVal.height.toString()) - 8
}
if (!this.textAreaWidth) {
// 减去左右padding的8vp
this.textAreaWidth = parseFloat(newVal.width.toString()) - 16
if (!this.currentTextHeight) {
this.currentTextHeight = this.getTextHeight(this.textValue, this.fontSize, this.textAreaWidth)
}
}
})
.onChange((value: string) => {
this.textValue = value
if (this.textAreaWidth) {
this.currentTextHeight = this.getTextHeight(this.textValue, this.fontSize, this.textAreaWidth)
}
})
.onContentScroll((_totalOffsetX: number, totalOffsetY: number) => {
// totalOffsetY单位px
// 大于padding-top说明已经滚动到顶
if (totalOffsetY > 8) {
Logger.warn("textArea scroll to top")
this.isScrollTop = true
} else {
this.isScrollTop = false
}
// 文本总高度 - 滚动偏移量 <= textArea组件高度说明已经滚动到底
if (this.currentTextHeight && this.textAreaHeight &&
parseFloat(this.currentTextHeight.toString()) + this.uiContext.px2vp(totalOffsetY) <=
parseFloat(this.textAreaHeight.toString())) {
Logger.warn("textArea scroll to bottom")
this.isScrollBottom = true
} else {
this.isScrollBottom = false
}
})
.onTouch((event: TouchEvent) => {
if (this.isScrollTop) {
if (event.type === TouchType.Down) {
this.touchDownY = event.touches[0].y
} else if (event.type === TouchType.Move) {
if (this.touchDownY - event.touches[0].y < 0) {
this.parentScroller.scrollBy(0, this.touchDownY - event.touches[0].y)
}
this.touchDownY = event.touches[0].y
}
} else if (this.isScrollBottom) {
if (event.type === TouchType.Down) {
this.touchDownY = event.touches[0].y
} else if (event.type === TouchType.Move) {
if (this.touchDownY - event.touches[0].y > 0) {
this.parentScroller.scrollBy(0, this.touchDownY - event.touches[0].y)
}
this.touchDownY = event.touches[0].y
}
}
})
}
}
注:其中textPadding为8vp,计算中的padding可从this.textPadding获取
更多关于HarmonyOS鸿蒙Next中Scroll组件和textArea组件嵌套滚动解决方案的实战教程也可以访问 https://www.itying.com/category-93-b0.html
- 判定 TextArea 是否滚动到顶/底
滚动到顶部
通过 onContentScroll 回调中的 totalOffsetY 偏移量判断:
totalOffsetY > 0 // 滚动到顶部
滚动到底部
通过以下公式判断:
文本总高度 - totalOffsetY偏移量 <= TextArea组件实际渲染高度
文本总高度计算方法:
this.getUIContext().getMeasureUtils().measureTextSize({
textContent: textContent,
fontSize: fontSize,
constraintWidth: constraintWidth, // 单位vp
lineHeight: ...,
...
}).height
- 实现嵌套滚动
当 TextArea 滚动到边界时,通过 onTouch 事件捕获滑动距离,并传递给父组件 Scroll 的 scroller.scrollBy() 方法,实现联动滚动。
完整代码实现
主页面组件
import Logger from '../../common/utils/Logger'
@Component
@Entry
struct ScrollPage {
@State prompt: ResourceStr = ''
private scroller: Scroller = new Scroller();
build() {
Scroll(this.scroller) {
Column() {
buildTextArea({
textValue: this.prompt,
parentScroller: this.scroller
})
}
// 其他组件...
}
}
}
TextArea 嵌套滚动组件
@Component
struct buildTextArea {
@Link textValue: ResourceStr;
// 获取textArea组件内当前text文本总高度,单位vp
@State currentTextHeight: Length | undefined = undefined;
@State textAreaWidth: Length | undefined = undefined;
@State textAreaHeight: Length | undefined = undefined;
@State isScrollTop: boolean = false;
@State isScrollBottom: boolean = false;
// 记录触摸点相对父组件的y坐标
@State touchDownY: number = 0
private uiContext: UIContext = this.getUIContext();
private fontSize: Resource = $r('app.float.small_text_size');
private textPadding: Resource = $r('app.float.line_padding_left') // 8vp
parentScroller: Scroller = new Scroller();
constructor(textValue: ResourceStr, parentScroller: Scroller) {
super();
this.textValue = textValue
this.parentScroller = parentScroller
}
/**
* 根据文本内容、字体大小、行高、行宽等计算文本总高度
* @param textContent 文本内容
* @param fontSize 字体大小
* @param constraintWidth 约束宽度,单位vp
* @returns 文本高度,单位vp
*/
private getTextHeight(textContent: ResourceStr, fontSize: Resource, constraintWidth: Length): Length | undefined
{
let textHeight = this.uiContext.getMeasureUtils().measureTextSize({
textContent: textContent,
fontSize: fontSize,
constraintWidth: constraintWidth // 单位vp
}).height
return textHeight ? this.uiContext.px2vp(parseFloat(textHeight.toString())) : undefined
}
build() {
TextArea({ text: this.textValue })
.fontSize(this.fontSize)
.width('100%')
.height('100%')
.padding(this.textPadding)
.border({ width: 1, color: '#ddd' })
.onAreaChange((_oldVal: Area, newVal: Area) => {
// Area单位vp
if (!this.textAreaHeight) {
// 减去上下padding的8vp
this.textAreaHeight = parseFloat(newVal.height.toString()) - 8
}
if (!this.textAreaWidth) {
// 减去左右padding的16vp (左右各8vp)
this.textAreaWidth = parseFloat(newVal.width.toString()) - 16
if (!this.currentTextHeight) {
this.currentTextHeight = this.getTextHeight(this.textValue, this.fontSize, this.textAreaWidth)
}
}
})
.onChange((value: string) => {
this.textValue = value
if (this.textAreaWidth) {
this.currentTextHeight = this.getTextHeight(this.textValue, this.fontSize, this.textAreaWidth)
}
})
.onContentScroll((_totalOffsetX: number, totalOffsetY: number) => {
// totalOffsetY单位px
// 大于padding-top说明已经滚动到顶
if (totalOffsetY > 8) {
Logger.warn("textArea scroll to top")
this.isScrollTop = true
} else {
this.isScrollTop = false
}
// 文本总高度 - 滚动偏移量 <= textArea组件高度说明已经滚动到底
if (this.currentTextHeight && this.textAreaHeight &&
parseFloat(this.currentTextHeight.toString()) + this.uiContext.px2vp(totalOffsetY) <=
parseFloat(this.textAreaHeight.toString())) {
Logger.warn("textArea scroll to bottom")
this.isScrollBottom = true
} else {
this.isScrollBottom = false
}
})
.onTouch((event: TouchEvent) => {
if (this.isScrollTop) {
// 滚动到顶部,向下滑动时滚动父组件
if (event.type === TouchType.Down) {
this.touchDownY = event.touches[0].y
} else if (event.type === TouchType.Move) {
if (this.touchDownY - event.touches[0].y < 0) {
// 向下滑动(下拉)
this.parentScroller.scrollBy(0, this.touchDownY - event.touches[0].y)
}
this.touchDownY = event.touches[0].y
}
} else if (this.isScrollBottom) {
// 滚动到底部,向上滑动时滚动父组件
if (event.type === TouchType.Down) {
this.touchDownY = event.touches[0].y
} else if (event.type === TouchType.Move) {
if (this.touchDownY - event.touches[0].y > 0) {
// 向上滑动(上拉)
this.parentScroller.scrollBy(0, this.touchDownY - event.touches[0].y)
}
this.touchDownY = event.touches[0].y
}
}
})
}
}
关键技术点
- 文本高度计算
使用 MeasureUtils.measureTextSize() API 计算文本总高度:
this.getUIContext().getMeasureUtils().measureTextSize({
textContent: textContent,
fontSize: fontSize,
constraintWidth: constraintWidth // TextArea实际宽度 - padding
})
- 滚动边界判断
滚动到顶部:
if (totalOffsetY > 8) { // 8为padding-top
this.isScrollTop = true
}
滚动到底部:
if (文本总高度 + px2vp(totalOffsetY) <= TextArea组件高度) {
this.isScrollBottom = true
}
- 触摸事件处理
通过 onTouch 捕获滑动距离,并使用 scrollBy() 方法滚动父组件:
.onTouch((event: TouchEvent) => {
if (this.isScrollTop && touchDownY - currentY < 0) {
// 向下滑动,滚动父组件
this.parentScroller.scrollBy(0, touchDownY - currentY)
} else if (this.isScrollBottom && touchDownY - currentY > 0) {
// 向上滑动,滚动父组件
this.parentScroller.scrollBy(0, touchDownY - currentY)
}
})
注意事项
-
Padding 计算:示例中 textPadding 为 8vp,计算组件实际内容区域时需要减去 padding 值
-
单位转换:
- onContentScroll 的 totalOffsetY 单位为 px
- onAreaChange 的 Area 单位为 vp
- 需要使用 px2vp() 进行单位转换
-
性能优化:
- 文本高度计算会触发多次,建议缓存计算结果
- 仅在 onChange 和初始化时重新计算文本高度
-
边界条件:
- 确保 TextArea 组件尺寸已初始化后再进行滚动判断
- 判断条件中的阈值(如 8vp)需要根据实际 padding 调整
更多关于HarmonyOS鸿蒙Next中Scroll组件和textArea组件嵌套滚动解决方案的实战系列教程也可以访问 https://www.itying.com/category-93-b0.html
加油,
在HarmonyOS Next中,Scroll组件与textArea组件嵌套时会产生滚动冲突。解决方案是使用Scroll组件的nestedScroll属性配置嵌套滚动模式。通过设置前向或双向嵌套滚动,可以协调父子组件的滚动优先级。具体实现时,需在Scroll组件中明确指定nestedScroll的联动方向,并确保textArea组件正确响应滚动事件。该方法能有效解决组件间的滚动冲突问题。
在HarmonyOS Next中实现Scroll与TextArea的嵌套滚动确实需要手动处理,因为TextArea组件目前不支持nestedScroll属性。你的解决方案很完整,核心思路是通过监听TextArea的滚动状态和触摸事件来手动控制父Scroll的滚动。
关键实现点:
- 使用onContentScroll监听TextArea的滚动偏移量,通过totalOffsetY判断是否滚动到顶部或底部
- 利用getMeasureUtils().measureTextSize计算文本总高度,结合组件实际高度判断滚动边界
- 在onTouch事件中,当TextArea到达边界时,通过parentScroller.scrollBy将触摸移动距离传递给父Scroll
需要注意单位转换问题:onContentScroll返回的偏移量是px单位,而measureTextSize和AreaChange使用的是vp单位,需要通过px2vp进行转换。另外,计算时要考虑padding对实际可用区域的影响。
这个方案在API 12+环境下运行良好,有效解决了TextArea与Scroll的嵌套滚动需求。

