HBuilderX

HBuilderX

极客开发工具
uni-app

uni-app

开发一次,多端覆盖
uniCloud

uniCloud

云开发平台
HTML5+

HTML5+

增强HTML5的功能体验
MUI

MUI

上万Star的前端框架

基于属性字符串实现Span的边框效果

问题现象
在HarmonyOS开发中,由于Span组件本身不支持设置margin、padding等布局属性,开发者可以通过属性字符串(StyledString)实现类似效果。

背景知识
ArkUI提供轻量的UI元素复用机制@Builder,其内部UI结构固定,仅与使用方进行数据传递。开发者可将重复使用的UI元素抽象成函数,在build函数中调用。
将StyledString应用到文本组件上,可以采用多种方式修改文本,包括调整字号、添加字体颜色、使文本具备可点击性,以及通过自定义方式绘制文本等。
解决方案
可以通过以下几点达成最终效果:
路径计算:定义calculateArcPoints方法,该方法用于生成圆弧边框的路径坐标点,确保边框形状符合预期。
自定义Span绘制:绘制边框与文本,在onDraw方法中调用canvas.drawPath绘制边框路径,并同步渲染文本内容。
样式绑定:将生成的styledString通过setStyledString方法设置到Text组件。
具体代码实现如下:
import { DrawContext } from '@ohos.arkui.node';
import { drawing } from '@kit.ArkGraphics2D';

interface Point {
x: number;
y: number;
}

function calculateArcPoints(
start: Point,
end: Point,
center: Point,
numPoints: number = 20
): Point[] {
const startVec: Point = {
x: start.x - center.x,
y: start.y - center.y
};
const endVec: Point = {
x: end.x - center.x,
y: end.y - center.y
};
const radius = Math.hypot(startVec.x, startVec.y);
let thetaStart = Math.atan2(startVec.y, startVec.x);
let thetaEnd = Math.atan2(endVec.y, endVec.x);

if (thetaEnd < thetaStart) {
thetaEnd += 2 * Math.PI;
}
const angleDiff = thetaEnd - thetaStart;

const angles = Array.from({ length: numPoints + 1 }, (item: undefined, i) =>
thetaStart + angleDiff (i / numPoints)
);
let arr: Point[] = [];
angles.forEach(item => {
arr.push({
x: center.x + radius
Math.cos(item),
y: center.y + radius * Math.sin(item)
});
});
return arr;
}

class MyCustomSpan extends CustomSpan {
width: number = 300;
word: string = '';
height: number = 300;
strokeWidth: number = 5;
uiContext: UIContext | undefined = undefined;

constructor(uiContext: UIContext, word: string, width: number, height: number, strokeWidth: number) {
super();
this.word = word;
this.width = width;
this.height = height;
this.strokeWidth = strokeWidth;
this.uiContext = uiContext;
}

onMeasure(): CustomSpanMetrics {
return { width: this.width, height: this.height };
}

onDraw(context: DrawContext) {
const canvas = context.canvas;

const pen = new drawing.Pen();  
pen.setStrokeWidth(this.strokeWidth);  
pen.setColor({  
  alpha: 255,  
  red: 255,  
  green: 0,  
  blue: 0  
});  
let path = new drawing.Path();  
path.moveTo(this.uiContext?.vp2px(15), this.uiContext?.vp2px(5));  

let rightTopArr =  
  calculateArcPoints({ x: this.width - 15, y: 5 }, { x: this.width - 10, y: 10 }, { x: this.width - 15, y: 10 });  
rightTopArr.forEach(item => {  
  path.lineTo(this.uiContext?.vp2px(item.x), this.uiContext?.vp2px(item.y));  
});  

let rightBottomArr =  
  calculateArcPoints({ x: this.width - 10, y: 20 }, { x: this.width - 15, y: 25 }, { x: this.width - 15, y: 20 });  
rightBottomArr.forEach(item => {  
  path.lineTo(this.uiContext?.vp2px(item.x), this.uiContext?.vp2px(item.y));  
});  

let leftBottomArr = calculateArcPoints({ x: 15, y: 25 }, { x: 10, y: 20 }, { x: 15, y: 20 });  
leftBottomArr.forEach(item => {  
  path.lineTo(this.uiContext?.vp2px(item.x), this.uiContext?.vp2px(item.y));  
});  

let leftTopArr = calculateArcPoints({ x: 10, y: 10 }, { x: 15, y: 5 }, { x: 15, y: 10 });  
leftTopArr.forEach(item => {  
  path.lineTo(this.uiContext?.vp2px(item.x), this.uiContext?.vp2px(item.y));  
});  
path.close();  
canvas.attachPen(pen);  
canvas.drawPath(path);  
const font = new drawing.Font();  
font.setSize(45);  
const textBlob =  
  drawing.TextBlob.makeFromString(this.word, font, drawing.TextEncoding.TEXT_ENCODING_UTF8);  
canvas.drawTextBlob(textBlob, 50, 65);  
canvas.detachPen();  

}
}

@Entry
@Component
struct StyledStringWithBorder {
message: string = 'TESTING';
strokeWidth: number = 5;
borderW: number = 100;
uiContext: UIContext = this.getUIContext();
customSpan: MyCustomSpan = new MyCustomSpan(this.uiContext, this.message, 100, 30, 5);
style: MutableStyledString = new MutableStyledString(this.customSpan);
textController: TextController = new TextController();
controller: TextInputController = new TextInputController();

@Builder
customInput(title: string, upValue: string, type: number) {
Row() {
Text(title)
.width('25%')
.height(40)
.textAlign(TextAlign.Center)
.margin(10);
TextInput({ text: upValue!!, placeholder: 'input your word...', controller: this.controller })
.placeholderColor(Color.Grey)
.placeholderFont({ size: 14, weight: 400 })
.caretColor(Color.Blue)
.width('60%')
.height(40)
.margin(10)
.fontSize(14)
.fontColor(Color.Black)
.onChange((value) => {
if (type === 1) {
this.message = value;
} else if (type === 2) {
this.strokeWidth = parseInt(value);
} else if (type === 3) {
this.borderW = parseInt(value);
}
})
.inputFilter(type === 1 ? '*' : '[0-9]', (e) => {
console.info(JSON.stringify(e));
});
}
.width('75%');
}

build() {
Column() {
Row() {
Text(undefined, { controller: this.textController })
.copyOption(CopyOptions.InApp);
}
.height('20%');

  this.customInput('输入文本', this.message, 1);  
  this.customInput('画笔线宽', this.strokeWidth + '', 2);  
  this.customInput('标签总长', this.borderW + '', 3);  

  Button('属性字符串生成')  
    .margin(10)  
    .onClick(() => {  
      this.customSpan = new MyCustomSpan(this.uiContext, this.message, this.borderW, 30, this.strokeWidth);  
      this.style = new MutableStyledString(this.customSpan);  
      this.textController.setStyledString(this.style);  
    });  
}  
.height('100%')  
.width('100%');  

}
}
https://coub.com/view/anyj3k1n0d
https://coub.com/view/ueqecjieca
https://coub.com/view/30z6ce2nqk
https://coub.com/view/jrsg7mlp4t
https://coub.com/view/tjpo05ke4l
https://coub.com/view/a6pzys4pf1
https://coub.com/view/zohjpi8shu
https://coub.com/view/t79a0xnoxz
https://coub.com/view/ocqtatjbba
https://coub.com/view/et2aw0jamw
https://coub.com/view/1vmtos94m3
https://coub.com/view/i6ze4kcxjw
https://coub.com/view/fqaj7kuhrj
https://coub.com/view/dxhwhxvufo
https://coub.com/view/reaswq109m
https://coub.com/view/vvycfe4nhr
https://coub.com/view/zdkjzp3hhc
https://coub.com/view/nlx8420jcj
https://coub.com/view/dzafjv7z6a
https://coub.com/view/3iyxy7clag
https://coub.com/view/jl95bzar08
https://coub.com/view/zot4hkyob9
https://coub.com/view/1hqucvaxql
https://coub.com/view/x158t366go
https://coub.com/view/6x5y2uo3b2
https://coub.com/view/x4l2ed45uf
https://coub.com/view/x9k27ioqqo
https://coub.com/view/e8ajx2my7b
https://coub.com/view/4hskibzj11
https://coub.com/view/0zfvj18meb
https://coub.com/view/1uc4f6sqp9
https://coub.com/view/yx1ovmauc6
https://coub.com/view/y0bfbv4dll
https://coub.com/view/0fvou1okpq
https://coub.com/view/3xnk6d21o3
https://coub.com/view/0n3nosoh63
https://coub.com/view/ntiwadpa3a
https://coub.com/view/yltipkmp54
https://coub.com/view/baqe0y7o31
https://coub.com/view/mb25ln1j5p
https://coub.com/view/mt7ijlry2f
https://coub.com/view/52k5fb41cu
https://coub.com/view/1qp1yujqzy
https://coub.com/view/857b82iyb7
https://coub.com/view/y1vsao57t8
https://coub.com/view/q6rczrx2pf
https://coub.com/view/rsub43f9hf
https://coub.com/view/ocsuoz47tx
https://coub.com/view/08ajglkbja
https://coub.com/view/0sz0b91hqn
https://coub.com/view/3tafmuomn4
https://coub.com/view/1aikfiygpo
https://coub.com/view/ric5m4kvkj
https://coub.com/view/55osaj87t3
https://coub.com/view/r9m7424w1f
https://coub.com/view/ctau72loy3
https://coub.com/view/6grbvq84ib
https://coub.com/view/r8i6me7ddj
https://coub.com/view/6zk01ab8vn
https://coub.com/view/pi2r2mx3ei
https://coub.com/view/7h02upq3vg
https://coub.com/view/dcjojrubzn
https://coub.com/view/mzuc0kv42i
https://coub.com/view/7f7k6rpg7t
https://coub.com/view/fux2wq7xxa
https://coub.com/view/yhq5d1knwc
https://coub.com/view/djabfip1y7
https://coub.com/view/0qwu1g87wt
https://coub.com/view/ralbmx0s5w
https://coub.com/view/0xpzbujmw3
https://coub.com/view/irmb97i1wg
https://coub.com/view/tuv5slpcv1
https://coub.com/view/3lkzhc5il9
https://coub.com/view/th0f38t0br
https://coub.com/view/fj0olx3b0d
https://coub.com/view/ves4sxd010
https://coub.com/view/xi7jt10gf9
https://coub.com/view/7dy9yqw1xg
https://coub.com/view/xnx8ot87c8
https://coub.com/view/9lel4jvzm5

继续阅读 »

问题现象
在HarmonyOS开发中,由于Span组件本身不支持设置margin、padding等布局属性,开发者可以通过属性字符串(StyledString)实现类似效果。

背景知识
ArkUI提供轻量的UI元素复用机制@Builder,其内部UI结构固定,仅与使用方进行数据传递。开发者可将重复使用的UI元素抽象成函数,在build函数中调用。
将StyledString应用到文本组件上,可以采用多种方式修改文本,包括调整字号、添加字体颜色、使文本具备可点击性,以及通过自定义方式绘制文本等。
解决方案
可以通过以下几点达成最终效果:
路径计算:定义calculateArcPoints方法,该方法用于生成圆弧边框的路径坐标点,确保边框形状符合预期。
自定义Span绘制:绘制边框与文本,在onDraw方法中调用canvas.drawPath绘制边框路径,并同步渲染文本内容。
样式绑定:将生成的styledString通过setStyledString方法设置到Text组件。
具体代码实现如下:
import { DrawContext } from '@ohos.arkui.node';
import { drawing } from '@kit.ArkGraphics2D';

interface Point {
x: number;
y: number;
}

function calculateArcPoints(
start: Point,
end: Point,
center: Point,
numPoints: number = 20
): Point[] {
const startVec: Point = {
x: start.x - center.x,
y: start.y - center.y
};
const endVec: Point = {
x: end.x - center.x,
y: end.y - center.y
};
const radius = Math.hypot(startVec.x, startVec.y);
let thetaStart = Math.atan2(startVec.y, startVec.x);
let thetaEnd = Math.atan2(endVec.y, endVec.x);

if (thetaEnd < thetaStart) {
thetaEnd += 2 * Math.PI;
}
const angleDiff = thetaEnd - thetaStart;

const angles = Array.from({ length: numPoints + 1 }, (item: undefined, i) =>
thetaStart + angleDiff (i / numPoints)
);
let arr: Point[] = [];
angles.forEach(item => {
arr.push({
x: center.x + radius
Math.cos(item),
y: center.y + radius * Math.sin(item)
});
});
return arr;
}

class MyCustomSpan extends CustomSpan {
width: number = 300;
word: string = '';
height: number = 300;
strokeWidth: number = 5;
uiContext: UIContext | undefined = undefined;

constructor(uiContext: UIContext, word: string, width: number, height: number, strokeWidth: number) {
super();
this.word = word;
this.width = width;
this.height = height;
this.strokeWidth = strokeWidth;
this.uiContext = uiContext;
}

onMeasure(): CustomSpanMetrics {
return { width: this.width, height: this.height };
}

onDraw(context: DrawContext) {
const canvas = context.canvas;

const pen = new drawing.Pen();  
pen.setStrokeWidth(this.strokeWidth);  
pen.setColor({  
  alpha: 255,  
  red: 255,  
  green: 0,  
  blue: 0  
});  
let path = new drawing.Path();  
path.moveTo(this.uiContext?.vp2px(15), this.uiContext?.vp2px(5));  

let rightTopArr =  
  calculateArcPoints({ x: this.width - 15, y: 5 }, { x: this.width - 10, y: 10 }, { x: this.width - 15, y: 10 });  
rightTopArr.forEach(item => {  
  path.lineTo(this.uiContext?.vp2px(item.x), this.uiContext?.vp2px(item.y));  
});  

let rightBottomArr =  
  calculateArcPoints({ x: this.width - 10, y: 20 }, { x: this.width - 15, y: 25 }, { x: this.width - 15, y: 20 });  
rightBottomArr.forEach(item => {  
  path.lineTo(this.uiContext?.vp2px(item.x), this.uiContext?.vp2px(item.y));  
});  

let leftBottomArr = calculateArcPoints({ x: 15, y: 25 }, { x: 10, y: 20 }, { x: 15, y: 20 });  
leftBottomArr.forEach(item => {  
  path.lineTo(this.uiContext?.vp2px(item.x), this.uiContext?.vp2px(item.y));  
});  

let leftTopArr = calculateArcPoints({ x: 10, y: 10 }, { x: 15, y: 5 }, { x: 15, y: 10 });  
leftTopArr.forEach(item => {  
  path.lineTo(this.uiContext?.vp2px(item.x), this.uiContext?.vp2px(item.y));  
});  
path.close();  
canvas.attachPen(pen);  
canvas.drawPath(path);  
const font = new drawing.Font();  
font.setSize(45);  
const textBlob =  
  drawing.TextBlob.makeFromString(this.word, font, drawing.TextEncoding.TEXT_ENCODING_UTF8);  
canvas.drawTextBlob(textBlob, 50, 65);  
canvas.detachPen();  

}
}

@Entry
@Component
struct StyledStringWithBorder {
message: string = 'TESTING';
strokeWidth: number = 5;
borderW: number = 100;
uiContext: UIContext = this.getUIContext();
customSpan: MyCustomSpan = new MyCustomSpan(this.uiContext, this.message, 100, 30, 5);
style: MutableStyledString = new MutableStyledString(this.customSpan);
textController: TextController = new TextController();
controller: TextInputController = new TextInputController();

@Builder
customInput(title: string, upValue: string, type: number) {
Row() {
Text(title)
.width('25%')
.height(40)
.textAlign(TextAlign.Center)
.margin(10);
TextInput({ text: upValue!!, placeholder: 'input your word...', controller: this.controller })
.placeholderColor(Color.Grey)
.placeholderFont({ size: 14, weight: 400 })
.caretColor(Color.Blue)
.width('60%')
.height(40)
.margin(10)
.fontSize(14)
.fontColor(Color.Black)
.onChange((value) => {
if (type === 1) {
this.message = value;
} else if (type === 2) {
this.strokeWidth = parseInt(value);
} else if (type === 3) {
this.borderW = parseInt(value);
}
})
.inputFilter(type === 1 ? '*' : '[0-9]', (e) => {
console.info(JSON.stringify(e));
});
}
.width('75%');
}

build() {
Column() {
Row() {
Text(undefined, { controller: this.textController })
.copyOption(CopyOptions.InApp);
}
.height('20%');

  this.customInput('输入文本', this.message, 1);  
  this.customInput('画笔线宽', this.strokeWidth + '', 2);  
  this.customInput('标签总长', this.borderW + '', 3);  

  Button('属性字符串生成')  
    .margin(10)  
    .onClick(() => {  
      this.customSpan = new MyCustomSpan(this.uiContext, this.message, this.borderW, 30, this.strokeWidth);  
      this.style = new MutableStyledString(this.customSpan);  
      this.textController.setStyledString(this.style);  
    });  
}  
.height('100%')  
.width('100%');  

}
}
https://coub.com/view/anyj3k1n0d
https://coub.com/view/ueqecjieca
https://coub.com/view/30z6ce2nqk
https://coub.com/view/jrsg7mlp4t
https://coub.com/view/tjpo05ke4l
https://coub.com/view/a6pzys4pf1
https://coub.com/view/zohjpi8shu
https://coub.com/view/t79a0xnoxz
https://coub.com/view/ocqtatjbba
https://coub.com/view/et2aw0jamw
https://coub.com/view/1vmtos94m3
https://coub.com/view/i6ze4kcxjw
https://coub.com/view/fqaj7kuhrj
https://coub.com/view/dxhwhxvufo
https://coub.com/view/reaswq109m
https://coub.com/view/vvycfe4nhr
https://coub.com/view/zdkjzp3hhc
https://coub.com/view/nlx8420jcj
https://coub.com/view/dzafjv7z6a
https://coub.com/view/3iyxy7clag
https://coub.com/view/jl95bzar08
https://coub.com/view/zot4hkyob9
https://coub.com/view/1hqucvaxql
https://coub.com/view/x158t366go
https://coub.com/view/6x5y2uo3b2
https://coub.com/view/x4l2ed45uf
https://coub.com/view/x9k27ioqqo
https://coub.com/view/e8ajx2my7b
https://coub.com/view/4hskibzj11
https://coub.com/view/0zfvj18meb
https://coub.com/view/1uc4f6sqp9
https://coub.com/view/yx1ovmauc6
https://coub.com/view/y0bfbv4dll
https://coub.com/view/0fvou1okpq
https://coub.com/view/3xnk6d21o3
https://coub.com/view/0n3nosoh63
https://coub.com/view/ntiwadpa3a
https://coub.com/view/yltipkmp54
https://coub.com/view/baqe0y7o31
https://coub.com/view/mb25ln1j5p
https://coub.com/view/mt7ijlry2f
https://coub.com/view/52k5fb41cu
https://coub.com/view/1qp1yujqzy
https://coub.com/view/857b82iyb7
https://coub.com/view/y1vsao57t8
https://coub.com/view/q6rczrx2pf
https://coub.com/view/rsub43f9hf
https://coub.com/view/ocsuoz47tx
https://coub.com/view/08ajglkbja
https://coub.com/view/0sz0b91hqn
https://coub.com/view/3tafmuomn4
https://coub.com/view/1aikfiygpo
https://coub.com/view/ric5m4kvkj
https://coub.com/view/55osaj87t3
https://coub.com/view/r9m7424w1f
https://coub.com/view/ctau72loy3
https://coub.com/view/6grbvq84ib
https://coub.com/view/r8i6me7ddj
https://coub.com/view/6zk01ab8vn
https://coub.com/view/pi2r2mx3ei
https://coub.com/view/7h02upq3vg
https://coub.com/view/dcjojrubzn
https://coub.com/view/mzuc0kv42i
https://coub.com/view/7f7k6rpg7t
https://coub.com/view/fux2wq7xxa
https://coub.com/view/yhq5d1knwc
https://coub.com/view/djabfip1y7
https://coub.com/view/0qwu1g87wt
https://coub.com/view/ralbmx0s5w
https://coub.com/view/0xpzbujmw3
https://coub.com/view/irmb97i1wg
https://coub.com/view/tuv5slpcv1
https://coub.com/view/3lkzhc5il9
https://coub.com/view/th0f38t0br
https://coub.com/view/fj0olx3b0d
https://coub.com/view/ves4sxd010
https://coub.com/view/xi7jt10gf9
https://coub.com/view/7dy9yqw1xg
https://coub.com/view/xnx8ot87c8
https://coub.com/view/9lel4jvzm5

收起阅读 »

解决不同路由页面存在相同组件id导致组件无法获焦的问题

问题现象
页面一使用requestFocus通过组件id获焦成功,然后路由至页面二,再然后路由至页面一,此时为什么无法使用requestFocus通过组件id获焦?

主页代码示例参考如下:

// 主页
@Entry
@Component
struct NavigationIndex {
@Provide('pathInfos') pathInfos: NavPathStack = new NavPathStack();
private listArray: string[] = ['WLAN', 'Connect & Share'];

build() {
Column() {
Navigation(this.pathInfos) {
TextInput({ placeholder: '输入关键字搜索' })
.width('90%')
.height(40)
.margin({ bottom: 10 });
// 通过List定义导航的一级界面
List({ space: 12, initialIndex: 0 }) {
ForEach(this.listArray, (item: string) => {
ListItem() {
Row() {
Row() {
Text(${item.slice(0, 1)})
.fontColor(Color.White)
.fontSize(14)
.fontWeight(FontWeight.Bold);
}
.width(30)
.height(30)
.backgroundColor('#a8a8a8')
.margin({ right: 20 })
.borderRadius(20)
.justifyContent(FlexAlign.Center);

            Column() {  
              Text(item)  
                .fontSize(16)  
                .margin({ bottom: 5 });  
            }  
            .alignItems(HorizontalAlign.Start);  

            Blank();  
            Row()  
              .width(12)  
              .height(12)  
              .margin({ right: 15 })  
              .border({  
                width: { top: 2, right: 2 },  
                color: 0xcccccc  
              })  
              .rotate({ angle: 45 });  
          }  
          .borderRadius(15)  
          .shadow({ radius: 100, color: '#ededed' })  
          .width('90%')  
          .alignItems(VerticalAlign.Center)  
          .padding({ left: 15, top: 15, bottom: 15 })  
          .backgroundColor(Color.White);  
        }  
        .width('100%')  
        .onClick(() => {  
          this.pathInfos.pushPathByName(`${item}`, '');  
        });  
      }, (item: string): string => item);  
    }  
    .listDirection(Axis.Vertical)  
    .edgeEffect(EdgeEffect.Spring)  
    .sticky(StickyStyle.Header)  
    .chainAnimation(false)  
    .width('100%');  
  }  
  .width('100%')  
  .mode(NavigationMode.Auto)  
  .title('设置'); // 设置标题文字  
}  
.size({ width: '100%', height: '100%' })  
.backgroundColor(0xf4f4f5);  

}
}
页面一代码示例参考如下:

// 页面一
@Builder
export function PageOneBuilder(name: string) {
PageOne({ name: name });
}

@Component
struct PageOne {
pathInfos: NavPathStack = new NavPathStack();
name: string = '';
@State isShow: boolean = false;

@Builder
textBuilder(id: string) {
TextInput()
.width('90%')
.id(id)
.onSubmit(() => {
this.isShow = true;
})
.onAppear(() => {
try {
this.getUIContext().getFocusController().requestFocus(id);
console.info(Succeeded in appearing component. name: ${this.name}, id ${id}.);
} catch (e) {
console.error(Failed to appear component. code: ${e.code}, message: ${e.message});
}
});
}

build() {
NavDestination() {
Column({ space: 24 }) {
this.textBuilder(${this.name}1);
if (this.isShow) {
this.textBuilder(${this.name}2);
}
Button('next')
.width('50%')
.height(40)
.margin({ top: 50 })
.onClick(() => {
// 弹出路由栈栈顶元素,跳转'Connect & Share'页面
this.pathInfos.pushPathByName(Connect & Share, '');
});
}
.size({ width: '100%', height: '100%' });
}
.title(${this.name})
.onReady((ctx: NavDestinationContext) => {
// NavDestinationContext获取当前所在的导航控制器
this.pathInfos = ctx.pathStack;
})
.onShown(() => {
this.isShow = false;
});
}
}
页面二跳转页面一代码示例参考如下:

// 页面二
@Builder
export function PageTwoBuilder(name: string) {
PageTwo({ name: name });
}

@Component
struct PageTwo {
pathInfos: NavPathStack = new NavPathStack();
name: string = '';
@State isShow: boolean = false;

build() {
NavDestination() {
Column({ space: 5 }) {
Button('next')
.width('50%')
.height(40)
.margin({ top: 5 })
.onClick(() => {
// 跳转到WLAN
this.pathInfos.pushPath({ name: 'WLAN', param: '' });
});
}
.size({ width: '100%', height: '100%' });
}
.title(${this.name})
.onReady((ctx: NavDestinationContext) => {
// NavDestinationContext获取当前所在的导航控制器
this.pathInfos = ctx.pathStack;
})
.onShown(() => {
this.isShow = false;
});
}
}
背景知识
Navigation组件是路由导航的根视图容器,一般作为Page页面的根容器使用,其内部默认包含了标题栏、内容区和工具栏。其中NavPathStack导航控制器提供多种跳转方式,具体参考LaunchMode。
组件标识(id)为组件的唯一标识,在整个应用内唯一。
问题定位
当由页面一路由至页面二后,页面二重新路由至页面一,传参保持不变,组件最后id为“WLAN1”和“WLAN2”,与路由栈内存在的页面一组件id一致,因此id赋值失败,requestFocus找不到对应组件。报错信息如下:

Error code: 150003, Error message: The component doesn't exist, is currently invisible, or has been disabled.
分析结论
路由栈内存在已有同id组件,因此新页面组件id赋值失败,requestFocus找不到对应组件。

修改建议
路由逻辑:主页跳转页面一,页面一跳转页面二,页面二跳转页面一。

方案一:消除同id组件。

方式一:页面一跳转页面二时,跳转方法使用replacePathByName,用新页面替换旧组件所在页面。
// 弹出路由栈栈顶元素,跳转'Connect & Share'页面
this.pathInfos.replacePathByName(Connect & Share, '详情页面参数');
方式二:页面二跳转页面一时,跳转模式MOVE_TO_TOP_SINGLETON或者POP_TO_SINGLETON,使用原页面。
// 跳转到WLAN
this.pathInfos.pushPath({ name: 'WLAN', param: '' }, { launchMode: LaunchMode.MOVE_TO_TOP_SINGLETON });
方案二:使用新的组件id。

根据页面传入的不同数据,重新命名id。name参数或者param参数均可,如下例子为name参数。

主页跳转页面一:

this.pathInfos.pushPathByName('WLAN', '');
页面二跳转页面一:

this.pathInfos.pushPathByName('Bluetooth', '');
路由表router_map.json如下:

{
"routerMap": [
{
"name": "WLAN",
"pageSourceFile": "src/main/ets/pages/ProblemPage.ets",
"buildFunction": "PageOneBuilder"
},
{
"name": "Bluetooth",
"pageSourceFile": "src/main/ets/pages/ProblemPage.ets",
"buildFunction": "PageOneBuilder"
},
{
"name": "Connect & Share",
"pageSourceFile": "src/main/ets/pages/ProblemPage.ets",
"buildFunction": "PageTwoBuilder"
}
]
}
常见FAQ
Q:使用HMRouter三方库进行页面跳转是否也有上述问题?

A:有上述问题。HMRouter三方库是基于Navigation实现的。

Q:为什么使用replace没有出现上述问题?

A:本质原因是路由栈中只有一个页面一,因此无重复使用同一id问题。首先页面一跳转页面二使用replace,此情况下栈中只存在页面二,下次跳转页面一后,栈中只有一个页面一。其次页面二跳转页面一使用MOVE_TO_TOP_SINGLETON或者POP_TO_SINGLETON跳转模式,此模式为单例跳转,跳转后栈中只存在一个页面一。

总结
id为组件的唯一标识,在整个应用内唯一。其他组件不可使用已命名的id,同时id命名以最新的id为准。
https://coub.com/view/9tjqrj50va
https://coub.com/view/rb2xb8a4n5
https://coub.com/view/5b79yniyth
https://coub.com/view/wamid4rm21
https://coub.com/view/zaw8d5yz2e
https://coub.com/view/zniccr5gwr
https://coub.com/view/3399xuazi1
https://coub.com/view/lsrnrbztyl
https://coub.com/view/x1m7lryfzq
https://coub.com/view/37xic534xp
https://coub.com/view/e4ot7q9l4a
https://coub.com/view/7xfg675fke
https://coub.com/view/cd5uv7n8di
https://coub.com/view/2yftocq9eo
https://coub.com/view/iz8k9vdeoq
https://coub.com/view/ss942ngulq
https://coub.com/view/0otxz11rt6
https://coub.com/view/t380otx63g
https://coub.com/view/s5jv5wwpp8
https://coub.com/view/uqzz8yd5t0
https://coub.com/view/v5mqk9pi4b
https://coub.com/view/3x24yjjngs
https://coub.com/view/bgr3qxu81y
https://coub.com/view/aczggz0c7l
https://coub.com/view/19x48j87yh
https://coub.com/view/mhj9o7akb8
https://coub.com/view/t52hno1jvx
https://coub.com/view/xd692lkh1w
https://coub.com/view/45f8w8eqt6
https://coub.com/view/7ufkfov20r
https://coub.com/view/aracs9rxr1
https://coub.com/view/usxdnvf69s
https://coub.com/view/xn4zgcqewo
https://coub.com/view/feshuj117i
https://coub.com/view/bo1naj1hg3
https://coub.com/view/0ooh9cdel4
https://coub.com/view/3r5m3lecc0
https://coub.com/view/u0e1fs4r10
https://coub.com/view/im9tfsm0vb
https://coub.com/view/xn9sc0dl48
https://coub.com/view/okd6f44ahj
https://coub.com/view/2ul6cv24s5
https://coub.com/view/2wiiaa2m0v
https://coub.com/view/kdm5dqnfm9
https://coub.com/view/v0mkw3fy9q
https://coub.com/view/xf417qh06w
https://coub.com/view/p7o9bdx3hd
https://coub.com/view/a4c7cs4za4
https://coub.com/view/2m9q5l3tye
https://coub.com/view/l5wijck89c
https://coub.com/view/7uv550mvcv
https://coub.com/view/hinzf3owe6
https://coub.com/view/2xs4q3hodd
https://coub.com/view/z2sz46ny5w
https://coub.com/view/fnxtc5jxkn
https://coub.com/view/idp6ix7wa7
https://coub.com/view/143z08ound
https://coub.com/view/xh81ng97rv
https://coub.com/view/b6idbpkjwm
https://coub.com/view/japbulumr8
https://coub.com/view/lzxbq7ueb8
https://coub.com/view/rykuydlgya
https://coub.com/view/g0bcawp1uo
https://coub.com/view/2dha0vzzq9
https://coub.com/view/nnbmf46ubp
https://coub.com/view/k4bl2myg5o
https://coub.com/view/tw1cs9daax
https://coub.com/view/w6k3awnqf7
https://coub.com/view/ufi6kkw1pn
https://coub.com/view/smcemgyolt
https://coub.com/view/n7zx8jkeqx
https://coub.com/view/zejw3zqzj9
https://coub.com/view/3yb82a6rzb
https://coub.com/view/2yfowuwqug
https://coub.com/view/d71yoe6vki
https://coub.com/view/qtoldx6o14
https://coub.com/view/ww8hn3f0ng
https://coub.com/view/m01ucwuhs9
https://coub.com/view/hjur00uu7a
https://coub.com/view/hxqok76tx8

继续阅读 »

问题现象
页面一使用requestFocus通过组件id获焦成功,然后路由至页面二,再然后路由至页面一,此时为什么无法使用requestFocus通过组件id获焦?

主页代码示例参考如下:

// 主页
@Entry
@Component
struct NavigationIndex {
@Provide('pathInfos') pathInfos: NavPathStack = new NavPathStack();
private listArray: string[] = ['WLAN', 'Connect & Share'];

build() {
Column() {
Navigation(this.pathInfos) {
TextInput({ placeholder: '输入关键字搜索' })
.width('90%')
.height(40)
.margin({ bottom: 10 });
// 通过List定义导航的一级界面
List({ space: 12, initialIndex: 0 }) {
ForEach(this.listArray, (item: string) => {
ListItem() {
Row() {
Row() {
Text(${item.slice(0, 1)})
.fontColor(Color.White)
.fontSize(14)
.fontWeight(FontWeight.Bold);
}
.width(30)
.height(30)
.backgroundColor('#a8a8a8')
.margin({ right: 20 })
.borderRadius(20)
.justifyContent(FlexAlign.Center);

            Column() {  
              Text(item)  
                .fontSize(16)  
                .margin({ bottom: 5 });  
            }  
            .alignItems(HorizontalAlign.Start);  

            Blank();  
            Row()  
              .width(12)  
              .height(12)  
              .margin({ right: 15 })  
              .border({  
                width: { top: 2, right: 2 },  
                color: 0xcccccc  
              })  
              .rotate({ angle: 45 });  
          }  
          .borderRadius(15)  
          .shadow({ radius: 100, color: '#ededed' })  
          .width('90%')  
          .alignItems(VerticalAlign.Center)  
          .padding({ left: 15, top: 15, bottom: 15 })  
          .backgroundColor(Color.White);  
        }  
        .width('100%')  
        .onClick(() => {  
          this.pathInfos.pushPathByName(`${item}`, '');  
        });  
      }, (item: string): string => item);  
    }  
    .listDirection(Axis.Vertical)  
    .edgeEffect(EdgeEffect.Spring)  
    .sticky(StickyStyle.Header)  
    .chainAnimation(false)  
    .width('100%');  
  }  
  .width('100%')  
  .mode(NavigationMode.Auto)  
  .title('设置'); // 设置标题文字  
}  
.size({ width: '100%', height: '100%' })  
.backgroundColor(0xf4f4f5);  

}
}
页面一代码示例参考如下:

// 页面一
@Builder
export function PageOneBuilder(name: string) {
PageOne({ name: name });
}

@Component
struct PageOne {
pathInfos: NavPathStack = new NavPathStack();
name: string = '';
@State isShow: boolean = false;

@Builder
textBuilder(id: string) {
TextInput()
.width('90%')
.id(id)
.onSubmit(() => {
this.isShow = true;
})
.onAppear(() => {
try {
this.getUIContext().getFocusController().requestFocus(id);
console.info(Succeeded in appearing component. name: ${this.name}, id ${id}.);
} catch (e) {
console.error(Failed to appear component. code: ${e.code}, message: ${e.message});
}
});
}

build() {
NavDestination() {
Column({ space: 24 }) {
this.textBuilder(${this.name}1);
if (this.isShow) {
this.textBuilder(${this.name}2);
}
Button('next')
.width('50%')
.height(40)
.margin({ top: 50 })
.onClick(() => {
// 弹出路由栈栈顶元素,跳转'Connect & Share'页面
this.pathInfos.pushPathByName(Connect & Share, '');
});
}
.size({ width: '100%', height: '100%' });
}
.title(${this.name})
.onReady((ctx: NavDestinationContext) => {
// NavDestinationContext获取当前所在的导航控制器
this.pathInfos = ctx.pathStack;
})
.onShown(() => {
this.isShow = false;
});
}
}
页面二跳转页面一代码示例参考如下:

// 页面二
@Builder
export function PageTwoBuilder(name: string) {
PageTwo({ name: name });
}

@Component
struct PageTwo {
pathInfos: NavPathStack = new NavPathStack();
name: string = '';
@State isShow: boolean = false;

build() {
NavDestination() {
Column({ space: 5 }) {
Button('next')
.width('50%')
.height(40)
.margin({ top: 5 })
.onClick(() => {
// 跳转到WLAN
this.pathInfos.pushPath({ name: 'WLAN', param: '' });
});
}
.size({ width: '100%', height: '100%' });
}
.title(${this.name})
.onReady((ctx: NavDestinationContext) => {
// NavDestinationContext获取当前所在的导航控制器
this.pathInfos = ctx.pathStack;
})
.onShown(() => {
this.isShow = false;
});
}
}
背景知识
Navigation组件是路由导航的根视图容器,一般作为Page页面的根容器使用,其内部默认包含了标题栏、内容区和工具栏。其中NavPathStack导航控制器提供多种跳转方式,具体参考LaunchMode。
组件标识(id)为组件的唯一标识,在整个应用内唯一。
问题定位
当由页面一路由至页面二后,页面二重新路由至页面一,传参保持不变,组件最后id为“WLAN1”和“WLAN2”,与路由栈内存在的页面一组件id一致,因此id赋值失败,requestFocus找不到对应组件。报错信息如下:

Error code: 150003, Error message: The component doesn't exist, is currently invisible, or has been disabled.
分析结论
路由栈内存在已有同id组件,因此新页面组件id赋值失败,requestFocus找不到对应组件。

修改建议
路由逻辑:主页跳转页面一,页面一跳转页面二,页面二跳转页面一。

方案一:消除同id组件。

方式一:页面一跳转页面二时,跳转方法使用replacePathByName,用新页面替换旧组件所在页面。
// 弹出路由栈栈顶元素,跳转'Connect & Share'页面
this.pathInfos.replacePathByName(Connect & Share, '详情页面参数');
方式二:页面二跳转页面一时,跳转模式MOVE_TO_TOP_SINGLETON或者POP_TO_SINGLETON,使用原页面。
// 跳转到WLAN
this.pathInfos.pushPath({ name: 'WLAN', param: '' }, { launchMode: LaunchMode.MOVE_TO_TOP_SINGLETON });
方案二:使用新的组件id。

根据页面传入的不同数据,重新命名id。name参数或者param参数均可,如下例子为name参数。

主页跳转页面一:

this.pathInfos.pushPathByName('WLAN', '');
页面二跳转页面一:

this.pathInfos.pushPathByName('Bluetooth', '');
路由表router_map.json如下:

{
"routerMap": [
{
"name": "WLAN",
"pageSourceFile": "src/main/ets/pages/ProblemPage.ets",
"buildFunction": "PageOneBuilder"
},
{
"name": "Bluetooth",
"pageSourceFile": "src/main/ets/pages/ProblemPage.ets",
"buildFunction": "PageOneBuilder"
},
{
"name": "Connect & Share",
"pageSourceFile": "src/main/ets/pages/ProblemPage.ets",
"buildFunction": "PageTwoBuilder"
}
]
}
常见FAQ
Q:使用HMRouter三方库进行页面跳转是否也有上述问题?

A:有上述问题。HMRouter三方库是基于Navigation实现的。

Q:为什么使用replace没有出现上述问题?

A:本质原因是路由栈中只有一个页面一,因此无重复使用同一id问题。首先页面一跳转页面二使用replace,此情况下栈中只存在页面二,下次跳转页面一后,栈中只有一个页面一。其次页面二跳转页面一使用MOVE_TO_TOP_SINGLETON或者POP_TO_SINGLETON跳转模式,此模式为单例跳转,跳转后栈中只存在一个页面一。

总结
id为组件的唯一标识,在整个应用内唯一。其他组件不可使用已命名的id,同时id命名以最新的id为准。
https://coub.com/view/9tjqrj50va
https://coub.com/view/rb2xb8a4n5
https://coub.com/view/5b79yniyth
https://coub.com/view/wamid4rm21
https://coub.com/view/zaw8d5yz2e
https://coub.com/view/zniccr5gwr
https://coub.com/view/3399xuazi1
https://coub.com/view/lsrnrbztyl
https://coub.com/view/x1m7lryfzq
https://coub.com/view/37xic534xp
https://coub.com/view/e4ot7q9l4a
https://coub.com/view/7xfg675fke
https://coub.com/view/cd5uv7n8di
https://coub.com/view/2yftocq9eo
https://coub.com/view/iz8k9vdeoq
https://coub.com/view/ss942ngulq
https://coub.com/view/0otxz11rt6
https://coub.com/view/t380otx63g
https://coub.com/view/s5jv5wwpp8
https://coub.com/view/uqzz8yd5t0
https://coub.com/view/v5mqk9pi4b
https://coub.com/view/3x24yjjngs
https://coub.com/view/bgr3qxu81y
https://coub.com/view/aczggz0c7l
https://coub.com/view/19x48j87yh
https://coub.com/view/mhj9o7akb8
https://coub.com/view/t52hno1jvx
https://coub.com/view/xd692lkh1w
https://coub.com/view/45f8w8eqt6
https://coub.com/view/7ufkfov20r
https://coub.com/view/aracs9rxr1
https://coub.com/view/usxdnvf69s
https://coub.com/view/xn4zgcqewo
https://coub.com/view/feshuj117i
https://coub.com/view/bo1naj1hg3
https://coub.com/view/0ooh9cdel4
https://coub.com/view/3r5m3lecc0
https://coub.com/view/u0e1fs4r10
https://coub.com/view/im9tfsm0vb
https://coub.com/view/xn9sc0dl48
https://coub.com/view/okd6f44ahj
https://coub.com/view/2ul6cv24s5
https://coub.com/view/2wiiaa2m0v
https://coub.com/view/kdm5dqnfm9
https://coub.com/view/v0mkw3fy9q
https://coub.com/view/xf417qh06w
https://coub.com/view/p7o9bdx3hd
https://coub.com/view/a4c7cs4za4
https://coub.com/view/2m9q5l3tye
https://coub.com/view/l5wijck89c
https://coub.com/view/7uv550mvcv
https://coub.com/view/hinzf3owe6
https://coub.com/view/2xs4q3hodd
https://coub.com/view/z2sz46ny5w
https://coub.com/view/fnxtc5jxkn
https://coub.com/view/idp6ix7wa7
https://coub.com/view/143z08ound
https://coub.com/view/xh81ng97rv
https://coub.com/view/b6idbpkjwm
https://coub.com/view/japbulumr8
https://coub.com/view/lzxbq7ueb8
https://coub.com/view/rykuydlgya
https://coub.com/view/g0bcawp1uo
https://coub.com/view/2dha0vzzq9
https://coub.com/view/nnbmf46ubp
https://coub.com/view/k4bl2myg5o
https://coub.com/view/tw1cs9daax
https://coub.com/view/w6k3awnqf7
https://coub.com/view/ufi6kkw1pn
https://coub.com/view/smcemgyolt
https://coub.com/view/n7zx8jkeqx
https://coub.com/view/zejw3zqzj9
https://coub.com/view/3yb82a6rzb
https://coub.com/view/2yfowuwqug
https://coub.com/view/d71yoe6vki
https://coub.com/view/qtoldx6o14
https://coub.com/view/ww8hn3f0ng
https://coub.com/view/m01ucwuhs9
https://coub.com/view/hjur00uu7a
https://coub.com/view/hxqok76tx8

收起阅读 »

如何在Canvas上实现涂抹效果

canvas

问题现象
在移动应用开发中,屏幕涂抹交互(如签名、涂鸦、擦除)是提升用户体验的关键功能。HarmonyOS的Canvas组件结合触摸事件监听与手势识别,为开发者提供了低门槛、高性能的2D图形绘制能力。本文将通过以下常见应用场景,详解如何在Canvas上实现涂抹效果:

场景一:如何结合手势或者事件实现滑动路径绘制?
场景二:如何撤销已绘制的路径?
场景三:如何擦除部分绘制内容?
背景知识
Canvas:提供画布组件,用于自定义绘制图形,开发者使用CanvasRenderingContext2D对象和OffscreenCanvasRenderingContext2D对象在Canvas组件上进行绘制,绘制对象可以是基础形状、文本、图片等。
lineTo:从当前点到指定点进行路径连接。
globalCompositeOperation:设置合成操作的方式,默认值为source-over。
onTouch:手指触摸动作触发该回调。可以获取滑动过的路径坐标点。
PanGesture:滑动手势事件,当滑动的最小距离达到设定的最小值时触发滑动手势事件。
解决方案
场景一:结合手势或者事件实现路径绘制。
Canvas组件可以绑定触摸事件和滑动手势来获取手指按压时的坐标,在事件触发过程中可以根据event对象获取到在屏幕上触摸的点,再结合Canvas的lineTo方法就可以把手指移动过程中的路径给记录下来,达到手指滑动屏幕就绘制的效果。

onTouch实现如下:
@Entry
@Component
struct CanvasTouch {
private settings: RenderingContextSettings = new RenderingContextSettings(true);
private context: CanvasRenderingContext2D = new CanvasRenderingContext2D(this.settings);

build() {
Column() {
Canvas(this.context)
.width('100%')
.height('100%')
.backgroundColor($r('sys.color.comp_background_focus'))
.onReady(() => {
this.context.lineWidth = 10;
this.context.strokeStyle = '#0000ff';
})
.onTouch((event: TouchEvent) => {
// 获取到触摸的坐标点
let x = event.touches[0].x;
let y = event.touches[0].y;
if (event.type == TouchType.Down) {
// 手指按下时画布移动到当前坐标点
this.context.beginPath();
this.context.moveTo(x, y);
}
if (event.type === TouchType.Move) {
// 手指移动时画布用线条连接到当前坐标点
this.context.lineTo(x, y);
this.context.stroke();
}
if (event.type === TouchType.Up) {
// 手指抬起时生成闭合路径
this.context.closePath();
}
})
.expandSafeArea([SafeAreaType.SYSTEM], [SafeAreaEdge.TOP, SafeAreaEdge.BOTTOM]);
};
}
}
PanGesture实现如下:
@Entry
@Component
struct CanvasPanGesture {
private settings: RenderingContextSettings = new RenderingContextSettings(true);
private context: CanvasRenderingContext2D = new CanvasRenderingContext2D(this.settings);

build() {
Column() {
Canvas(this.context)
.width('100%')
.height('100%')
.backgroundColor($r('sys.color.comp_background_focus'))
.onReady(() => {
this.context.lineWidth = 10;
this.context.strokeStyle = '#0000ff';
})
.gesture(
PanGesture({ fingers: 1 })
.onActionStart((event: GestureEvent) => {
let x = event.fingerList[0].localX;
let y = event.fingerList[0].localY;
// 手指按下时画布移动到当前坐标点
this.context.beginPath();
this.context.moveTo(x, y);
})
.onActionUpdate((event: GestureEvent) => {
let x = event.fingerList[0].localX;
let y = event.fingerList[0].localY;
// 手指移动时画布用线条连接到当前坐标点
this.context.lineTo(x, y);
this.context.stroke();
})
.onActionEnd(() => {
// 手指抬起时生成闭合路径
this.context.closePath();
})
)
.expandSafeArea([SafeAreaType.SYSTEM], [SafeAreaEdge.TOP, SafeAreaEdge.BOTTOM]);
};
}
}
实现效果如下:

点击放大

场景二:撤销绘制的路径。
在一些签名场景,如果用户绘制错误需要重新绘制,直接使用clearRect方法清空画布体验不够友好,需要仅撤销最新的绘制路径,这时可以使用数组来存储绘制过程中的路径,然后点击撤销时移除最新路径,最后重绘剩余路径,这样即可实现撤销绘制功能。示例代码参考如下:

interface Point {
x: number;
y: number;
}

export class DrawingPath {
points: Point[] = [];
}

@Entry
@Component
struct CanvasCancelDraw {
private settings: RenderingContextSettings = new RenderingContextSettings(true);
private context: CanvasRenderingContext2D = new CanvasRenderingContext2D(this.settings);
private paths: DrawingPath[] = [];

build() {
Column() {
Canvas(this.context)
.width('100%')
.height('570vp')
.borderRadius(16)
.backgroundColor($r('sys.color.comp_background_focus'))
.onReady(() => {
this.context.lineWidth = 10;
this.context.strokeStyle = '#0000ff';
})
.onTouch((event: TouchEvent) => {
let x = event.touches[0].x;
let y = event.touches[0].y;
if (event.type == TouchType.Down) {
// 按下时新建一条路径
const path = new DrawingPath();
path.points.push({ x: x, y: y });
this.paths.push(path);
this.context.beginPath();
this.context.moveTo(x, y);
}
if (event.type === TouchType.Move) {
// 移动时把当前的坐标点放入最新的路径中
this.paths[this.paths.length - 1].points.push({ x: x, y: y });
this.context.lineTo(x, y);
this.context.stroke();
}
if (event.type === TouchType.Up) {
this.context.closePath();
}
});

  Row({ space: 20 }) {  
    Button('清空')  
      .layoutWeight(1)  
      .onClick(() => {  
        this.paths = [];  
        this.context.clearRect(0, 0, this.context.width, this.context.height);  
      });  
    Button('撤销')  
      .layoutWeight(1)  
      .onClick(() => {  
        if (this.paths.length < 1) {  
          return;  
        }  
        this.paths.pop()!!;  
        this.context.clearRect(0, 0, this.context.width, this.context.height);  
        // 重绘保存的路径  
        this.paths.forEach((path) => {  
          if (path.points.length < 1) {  
            return;  
          }  
          this.context.beginPath();  
          this.context.moveTo(path.points[0].x, path.points[0].y);  
          for (let i = 0; i < path.points.length; i++) {  
            this.context.lineTo(path.points[i].x, path.points[i].y);  
            this.context.stroke();  
          }  
          this.context.closePath();  
        });  
      });  
  }.width('100%')  
  .justifyContent(FlexAlign.Center)  
  .margin(20);  
}.padding(16);  

}
}
https://coub.com/embed/kwki4m8ihy
https://coub.com/embed/1dc59ftpsz
https://coub.com/embed/1eg6k9knxq
https://coub.com/embed/ffcvsoqgy9
https://coub.com/embed/mtf20e2xfs
https://coub.com/embed/vcdyj2hsux
https://coub.com/embed/p8u435h7k0
https://coub.com/embed/53uc97iqzh
https://coub.com/embed/slklk9dip7
https://coub.com/embed/x5r8qua9dn
https://coub.com/embed/suzzf1tabm
https://coub.com/embed/iwlz1k4x5c
https://coub.com/embed/esio81b16u
https://coub.com/embed/08isxtzg6y
https://coub.com/embed/5t2ou1h0u1
https://coub.com/embed/2i826b72cf
https://coub.com/embed/wl4g0a80wd
https://coub.com/embed/noxtktrsld
https://coub.com/embed/4arm33ehej
https://coub.com/embed/4m5uhixzc6
https://coub.com/embed/z4hni578js
https://coub.com/embed/vc1lp4h9wv
https://coub.com/embed/ahq7q7smb9
https://coub.com/embed/s3agn3nkdx
https://coub.com/embed/0tpmvd839f
https://coub.com/embed/a52spj6rs2
https://coub.com/embed/mytvxbwrus
https://coub.com/embed/hto4op5jpb
https://coub.com/embed/5i13w22bho
https://coub.com/embed/ng8whwdnmh
https://coub.com/embed/4tepeaanko
https://coub.com/embed/gq6dpvlatn
https://coub.com/embed/n0xesp3qe7
https://coub.com/embed/i4ciaef3z2
https://coub.com/embed/h6tzz4imsh
https://coub.com/embed/5oh9y0o849
https://coub.com/embed/di1lvxi6rs
https://coub.com/embed/dvr0d1spum
https://coub.com/embed/a0chxn6q37
https://coub.com/embed/9kdpqt8gi3
https://coub.com/embed/wwtpzdcshr
https://coub.com/embed/i3j774nb78
https://coub.com/embed/9vdne44f87
https://coub.com/embed/di14nasb4i
https://coub.com/embed/tjdbi2z10f
https://coub.com/embed/xymgbl98l3
https://coub.com/embed/d46w9vg6y2
https://coub.com/embed/7kjd2r109r
https://coub.com/embed/ewtrtmuecq
https://coub.com/embed/x44fcmuuwa
https://coub.com/embed/ajx3cjloat
https://coub.com/embed/s9xf0u8gl8
https://coub.com/embed/9ogtzmbj73
https://coub.com/embed/2th49b4ms0
https://coub.com/embed/ke6047rz7q
https://coub.com/embed/m2oqeg89q8
https://coub.com/embed/774g3ow4sb
https://coub.com/embed/7548j9l7nl
https://coub.com/embed/zjz1qsyin2
https://coub.com/embed/xik7e7nili
https://coub.com/embed/zp95r4y15i
https://coub.com/embed/adde9a38t1
https://coub.com/embed/1qarvak9lx
https://coub.com/embed/dko4y9u0yf
https://coub.com/embed/ypw9dtvmab
https://coub.com/embed/8ewvlnn2b6
https://coub.com/embed/5s2dev0lra
https://coub.com/embed/c9fihrkf9u
https://coub.com/embed/xzeak1mhkt
https://coub.com/embed/t9dmppwno6
https://coub.com/embed/fz18zmri11
https://coub.com/embed/75espd6kkd
https://coub.com/embed/x2bbs1j2jb
https://coub.com/embed/fe3fwdnu88
https://coub.com/embed/ckzbjutpkj
https://coub.com/embed/qf0w3gxvy2
https://coub.com/embed/ux4gip7l5a
https://coub.com/embed/qavq5q9b1e
https://coub.com/embed/06dq1lpfmv
https://coub.com/embed/h9okejkccd
https://coub.com/embed/z59288b3bc
https://coub.com/embed/jdsgw25pj5
https://coub.com/embed/20ms41ljxh
https://coub.com/embed/cyjima3av9
https://coub.com/embed/xhymuinpm8
https://coub.com/embed/gqqk4335y1
https://coub.com/embed/9uflyvtfzv
https://coub.com/embed/ogd56p24ng
https://coub.com/embed/zydhfymmc3
https://coub.com/embed/pzd0ac0kg5
https://coub.com/embed/obdv3r4cy0
https://coub.com/embed/ka2mdh07zg
https://coub.com/embed/k8417q6t33
https://coub.com/embed/b797hoxnvb
https://coub.com/embed/p3wb9ayf0i
https://coub.com/embed/cylp42xa95
https://coub.com/embed/5vyv5859hb
https://coub.com/embed/uolo157r87
https://coub.com/embed/7bmp0nxmze
https://coub.com/embed/d2q6yzkrli
https://coub.com/embed/zs1hg1bhrv
https://coub.com/embed/yz4tqllp0u
https://coub.com/embed/a2kvibvldp
https://coub.com/embed/09kqx4xcfx
https://coub.com/embed/2c1zyd3kx3
https://coub.com/embed/wep5eg226t
https://coub.com/embed/su365ycwsu
https://coub.com/embed/nzdz5k3kr0
https://coub.com/embed/qg568ebu9j
https://coub.com/embed/md5pqvk50f
https://coub.com/embed/0gt0ay1zk3
https://coub.com/embed/hecylej9t3
https://coub.com/embed/wmxvxv1kkj
https://coub.com/embed/bxxpl0brtv
https://coub.com/embed/pholjf70ut
https://coub.com/embed/m8l2xwwy39
https://coub.com/embed/1c1x2w7t08
https://coub.com/embed/53mubpegym
https://coub.com/embed/on6slxqrt5
https://coub.com/embed/55q881xx6l
https://coub.com/embed/s77w2ok24r
https://coub.com/embed/4e9hy0237n
https://coub.com/embed/3entpjmdbw
https://coub.com/embed/8besg49zbq
https://coub.com/embed/usqat13gtl
https://coub.com/embed/66yxwx6676
https://coub.com/embed/np30bv52rn
https://coub.com/embed/3nk88ivl0b
https://coub.com/embed/83eyn5yj6r
https://coub.com/embed/bymmywq8fr
https://coub.com/embed/9bj0bp5xii
https://coub.com/embed/agvyb0zhme
https://coub.com/embed/8bcvp8upb3
https://coub.com/embed/c8p7zv5zan
https://coub.com/embed/yry7la9tco
https://coub.com/embed/ho2dopbp7j
https://coub.com/embed/amubk2uwuc
https://coub.com/embed/2jwoy1e4u4
https://coub.com/embed/ezz7hv58aq
https://coub.com/embed/sxb8y6f7cm
https://coub.com/embed/y4kvuhob20
https://coub.com/embed/t3iyg4zbk6
https://coub.com/embed/hlu3q922ul
https://coub.com/embed/wr5t1i5hjn
https://coub.com/embed/ig12bx6jem
https://coub.com/embed/elh7b4dm2v
https://coub.com/embed/p3aozbkqis
https://coub.com/embed/1x387gqa9h
https://coub.com/embed/11vq0o0urq
https://coub.com/embed/n95945i2xa
https://coub.com/embed/kwxot0bqpd
https://coub.com/embed/0bzsnthjf8
https://coub.com/embed/l6n7cci0v1
https://coub.com/embed/e33t42b2e8
https://coub.com/embed/uufau92iew
https://coub.com/embed/8nu2gzpi87
https://coub.com/embed/bzvc0eyha5
https://coub.com/embed/m7i31eulsq
https://coub.com/embed/1r27esow61
https://coub.com/embed/v18hqlwcpz
https://coub.com/embed/hltc1g5iav
https://coub.com/embed/eq093ohvb8
https://coub.com/embed/i44t54uooo
https://coub.com/embed/1p2epqlik8
https://coub.com/embed/r7s85h5tgs
https://coub.com/embed/x3a913pgtg
https://coub.com/embed/rm3vt5vimg
https://coub.com/embed/15wu728znl
https://coub.com/embed/2iwy8dlso1
https://coub.com/embed/9ecofudaq2
https://coub.com/embed/odtibqvqqb
https://coub.com/embed/kfit182kgd
https://coub.com/embed/oyuutd8klx
https://coub.com/embed/2ab3tdnsq2
https://coub.com/embed/0lxrs8kij0
https://coub.com/embed/o1b4imc38s
https://coub.com/embed/hmvpibka9g
https://coub.com/embed/pvj08u1amk
https://coub.com/embed/g3fxbal3j0
https://coub.com/embed/hunlqq2jwi
https://coub.com/embed/wsrr0y4x1q
https://coub.com/embed/kg284yqkff
https://coub.com/embed/hbiuts4e7e
https://coub.com/embed/cl8t4qvz2x
https://coub.com/embed/ydk9cyiff7
https://coub.com/embed/ek17k4yytm
https://coub.com/embed/y6t00hom1a
https://coub.com/embed/19q7wp5i00
https://coub.com/embed/4h8bxs9zz7
https://coub.com/embed/jwflpmph2z
https://coub.com/embed/f213jay0fp
https://coub.com/embed/bsf3lctm94
https://coub.com/embed/e83n1arbod
https://coub.com/embed/swembduv5h
https://coub.com/embed/n3fq7zzf5w
https://coub.com/embed/9lio0yr585
https://coub.com/embed/o4fakj61o0
https://coub.com/embed/1hjvwr455z
https://coub.com/embed/xzugpc1qds
https://coub.com/embed/ev7098ev03
https://coub.com/embed/kx4fsmb4b6
https://coub.com/embed/gcorgj2lp8
https://coub.com/embed/gsk38h0x1t
https://coub.com/embed/1gzcnq0drf
https://coub.com/embed/8kk1rohlkq
https://coub.com/embed/95lk677xts
https://coub.com/embed/klzoirlr0i
https://coub.com/embed/ruh9e6e23z
https://coub.com/embed/kaffn57ll3
https://coub.com/embed/wtwakxr59k
https://coub.com/embed/oq7e9iq28m
https://coub.com/embed/xf0mc6ti8q
https://coub.com/embed/ptjtimwt1u
https://coub.com/embed/uu6kt9484i
https://coub.com/embed/1upd83q7xu
https://coub.com/embed/kry12uzw3v
https://coub.com/embed/dtg6xjn98z
https://coub.com/embed/sn0yhm79wv
https://coub.com/embed/9gqnb8624w
https://coub.com/embed/p32o9d4k7v
https://coub.com/embed/e4p0k8n96b
https://coub.com/embed/ordooqx1q4
https://coub.com/embed/w0en3j9kmt
https://coub.com/embed/z925vaal0x
https://coub.com/embed/llsxxio2o3
https://coub.com/embed/xydiujzwro
https://coub.com/embed/2e486z583s
https://coub.com/embed/b850qpvdwf
https://coub.com/embed/uasf7vsd00
https://coub.com/embed/ne9r9ql0ke
https://coub.com/embed/wrb0dawhk3
https://coub.com/embed/e7vo2n3rv0
https://coub.com/embed/sq5lb2quya
https://coub.com/embed/q7er5zdf9c
https://coub.com/embed/hijacyi6zc

继续阅读 »

问题现象
在移动应用开发中,屏幕涂抹交互(如签名、涂鸦、擦除)是提升用户体验的关键功能。HarmonyOS的Canvas组件结合触摸事件监听与手势识别,为开发者提供了低门槛、高性能的2D图形绘制能力。本文将通过以下常见应用场景,详解如何在Canvas上实现涂抹效果:

场景一:如何结合手势或者事件实现滑动路径绘制?
场景二:如何撤销已绘制的路径?
场景三:如何擦除部分绘制内容?
背景知识
Canvas:提供画布组件,用于自定义绘制图形,开发者使用CanvasRenderingContext2D对象和OffscreenCanvasRenderingContext2D对象在Canvas组件上进行绘制,绘制对象可以是基础形状、文本、图片等。
lineTo:从当前点到指定点进行路径连接。
globalCompositeOperation:设置合成操作的方式,默认值为source-over。
onTouch:手指触摸动作触发该回调。可以获取滑动过的路径坐标点。
PanGesture:滑动手势事件,当滑动的最小距离达到设定的最小值时触发滑动手势事件。
解决方案
场景一:结合手势或者事件实现路径绘制。
Canvas组件可以绑定触摸事件和滑动手势来获取手指按压时的坐标,在事件触发过程中可以根据event对象获取到在屏幕上触摸的点,再结合Canvas的lineTo方法就可以把手指移动过程中的路径给记录下来,达到手指滑动屏幕就绘制的效果。

onTouch实现如下:
@Entry
@Component
struct CanvasTouch {
private settings: RenderingContextSettings = new RenderingContextSettings(true);
private context: CanvasRenderingContext2D = new CanvasRenderingContext2D(this.settings);

build() {
Column() {
Canvas(this.context)
.width('100%')
.height('100%')
.backgroundColor($r('sys.color.comp_background_focus'))
.onReady(() => {
this.context.lineWidth = 10;
this.context.strokeStyle = '#0000ff';
})
.onTouch((event: TouchEvent) => {
// 获取到触摸的坐标点
let x = event.touches[0].x;
let y = event.touches[0].y;
if (event.type == TouchType.Down) {
// 手指按下时画布移动到当前坐标点
this.context.beginPath();
this.context.moveTo(x, y);
}
if (event.type === TouchType.Move) {
// 手指移动时画布用线条连接到当前坐标点
this.context.lineTo(x, y);
this.context.stroke();
}
if (event.type === TouchType.Up) {
// 手指抬起时生成闭合路径
this.context.closePath();
}
})
.expandSafeArea([SafeAreaType.SYSTEM], [SafeAreaEdge.TOP, SafeAreaEdge.BOTTOM]);
};
}
}
PanGesture实现如下:
@Entry
@Component
struct CanvasPanGesture {
private settings: RenderingContextSettings = new RenderingContextSettings(true);
private context: CanvasRenderingContext2D = new CanvasRenderingContext2D(this.settings);

build() {
Column() {
Canvas(this.context)
.width('100%')
.height('100%')
.backgroundColor($r('sys.color.comp_background_focus'))
.onReady(() => {
this.context.lineWidth = 10;
this.context.strokeStyle = '#0000ff';
})
.gesture(
PanGesture({ fingers: 1 })
.onActionStart((event: GestureEvent) => {
let x = event.fingerList[0].localX;
let y = event.fingerList[0].localY;
// 手指按下时画布移动到当前坐标点
this.context.beginPath();
this.context.moveTo(x, y);
})
.onActionUpdate((event: GestureEvent) => {
let x = event.fingerList[0].localX;
let y = event.fingerList[0].localY;
// 手指移动时画布用线条连接到当前坐标点
this.context.lineTo(x, y);
this.context.stroke();
})
.onActionEnd(() => {
// 手指抬起时生成闭合路径
this.context.closePath();
})
)
.expandSafeArea([SafeAreaType.SYSTEM], [SafeAreaEdge.TOP, SafeAreaEdge.BOTTOM]);
};
}
}
实现效果如下:

点击放大

场景二:撤销绘制的路径。
在一些签名场景,如果用户绘制错误需要重新绘制,直接使用clearRect方法清空画布体验不够友好,需要仅撤销最新的绘制路径,这时可以使用数组来存储绘制过程中的路径,然后点击撤销时移除最新路径,最后重绘剩余路径,这样即可实现撤销绘制功能。示例代码参考如下:

interface Point {
x: number;
y: number;
}

export class DrawingPath {
points: Point[] = [];
}

@Entry
@Component
struct CanvasCancelDraw {
private settings: RenderingContextSettings = new RenderingContextSettings(true);
private context: CanvasRenderingContext2D = new CanvasRenderingContext2D(this.settings);
private paths: DrawingPath[] = [];

build() {
Column() {
Canvas(this.context)
.width('100%')
.height('570vp')
.borderRadius(16)
.backgroundColor($r('sys.color.comp_background_focus'))
.onReady(() => {
this.context.lineWidth = 10;
this.context.strokeStyle = '#0000ff';
})
.onTouch((event: TouchEvent) => {
let x = event.touches[0].x;
let y = event.touches[0].y;
if (event.type == TouchType.Down) {
// 按下时新建一条路径
const path = new DrawingPath();
path.points.push({ x: x, y: y });
this.paths.push(path);
this.context.beginPath();
this.context.moveTo(x, y);
}
if (event.type === TouchType.Move) {
// 移动时把当前的坐标点放入最新的路径中
this.paths[this.paths.length - 1].points.push({ x: x, y: y });
this.context.lineTo(x, y);
this.context.stroke();
}
if (event.type === TouchType.Up) {
this.context.closePath();
}
});

  Row({ space: 20 }) {  
    Button('清空')  
      .layoutWeight(1)  
      .onClick(() => {  
        this.paths = [];  
        this.context.clearRect(0, 0, this.context.width, this.context.height);  
      });  
    Button('撤销')  
      .layoutWeight(1)  
      .onClick(() => {  
        if (this.paths.length < 1) {  
          return;  
        }  
        this.paths.pop()!!;  
        this.context.clearRect(0, 0, this.context.width, this.context.height);  
        // 重绘保存的路径  
        this.paths.forEach((path) => {  
          if (path.points.length < 1) {  
            return;  
          }  
          this.context.beginPath();  
          this.context.moveTo(path.points[0].x, path.points[0].y);  
          for (let i = 0; i < path.points.length; i++) {  
            this.context.lineTo(path.points[i].x, path.points[i].y);  
            this.context.stroke();  
          }  
          this.context.closePath();  
        });  
      });  
  }.width('100%')  
  .justifyContent(FlexAlign.Center)  
  .margin(20);  
}.padding(16);  

}
}
https://coub.com/embed/kwki4m8ihy
https://coub.com/embed/1dc59ftpsz
https://coub.com/embed/1eg6k9knxq
https://coub.com/embed/ffcvsoqgy9
https://coub.com/embed/mtf20e2xfs
https://coub.com/embed/vcdyj2hsux
https://coub.com/embed/p8u435h7k0
https://coub.com/embed/53uc97iqzh
https://coub.com/embed/slklk9dip7
https://coub.com/embed/x5r8qua9dn
https://coub.com/embed/suzzf1tabm
https://coub.com/embed/iwlz1k4x5c
https://coub.com/embed/esio81b16u
https://coub.com/embed/08isxtzg6y
https://coub.com/embed/5t2ou1h0u1
https://coub.com/embed/2i826b72cf
https://coub.com/embed/wl4g0a80wd
https://coub.com/embed/noxtktrsld
https://coub.com/embed/4arm33ehej
https://coub.com/embed/4m5uhixzc6
https://coub.com/embed/z4hni578js
https://coub.com/embed/vc1lp4h9wv
https://coub.com/embed/ahq7q7smb9
https://coub.com/embed/s3agn3nkdx
https://coub.com/embed/0tpmvd839f
https://coub.com/embed/a52spj6rs2
https://coub.com/embed/mytvxbwrus
https://coub.com/embed/hto4op5jpb
https://coub.com/embed/5i13w22bho
https://coub.com/embed/ng8whwdnmh
https://coub.com/embed/4tepeaanko
https://coub.com/embed/gq6dpvlatn
https://coub.com/embed/n0xesp3qe7
https://coub.com/embed/i4ciaef3z2
https://coub.com/embed/h6tzz4imsh
https://coub.com/embed/5oh9y0o849
https://coub.com/embed/di1lvxi6rs
https://coub.com/embed/dvr0d1spum
https://coub.com/embed/a0chxn6q37
https://coub.com/embed/9kdpqt8gi3
https://coub.com/embed/wwtpzdcshr
https://coub.com/embed/i3j774nb78
https://coub.com/embed/9vdne44f87
https://coub.com/embed/di14nasb4i
https://coub.com/embed/tjdbi2z10f
https://coub.com/embed/xymgbl98l3
https://coub.com/embed/d46w9vg6y2
https://coub.com/embed/7kjd2r109r
https://coub.com/embed/ewtrtmuecq
https://coub.com/embed/x44fcmuuwa
https://coub.com/embed/ajx3cjloat
https://coub.com/embed/s9xf0u8gl8
https://coub.com/embed/9ogtzmbj73
https://coub.com/embed/2th49b4ms0
https://coub.com/embed/ke6047rz7q
https://coub.com/embed/m2oqeg89q8
https://coub.com/embed/774g3ow4sb
https://coub.com/embed/7548j9l7nl
https://coub.com/embed/zjz1qsyin2
https://coub.com/embed/xik7e7nili
https://coub.com/embed/zp95r4y15i
https://coub.com/embed/adde9a38t1
https://coub.com/embed/1qarvak9lx
https://coub.com/embed/dko4y9u0yf
https://coub.com/embed/ypw9dtvmab
https://coub.com/embed/8ewvlnn2b6
https://coub.com/embed/5s2dev0lra
https://coub.com/embed/c9fihrkf9u
https://coub.com/embed/xzeak1mhkt
https://coub.com/embed/t9dmppwno6
https://coub.com/embed/fz18zmri11
https://coub.com/embed/75espd6kkd
https://coub.com/embed/x2bbs1j2jb
https://coub.com/embed/fe3fwdnu88
https://coub.com/embed/ckzbjutpkj
https://coub.com/embed/qf0w3gxvy2
https://coub.com/embed/ux4gip7l5a
https://coub.com/embed/qavq5q9b1e
https://coub.com/embed/06dq1lpfmv
https://coub.com/embed/h9okejkccd
https://coub.com/embed/z59288b3bc
https://coub.com/embed/jdsgw25pj5
https://coub.com/embed/20ms41ljxh
https://coub.com/embed/cyjima3av9
https://coub.com/embed/xhymuinpm8
https://coub.com/embed/gqqk4335y1
https://coub.com/embed/9uflyvtfzv
https://coub.com/embed/ogd56p24ng
https://coub.com/embed/zydhfymmc3
https://coub.com/embed/pzd0ac0kg5
https://coub.com/embed/obdv3r4cy0
https://coub.com/embed/ka2mdh07zg
https://coub.com/embed/k8417q6t33
https://coub.com/embed/b797hoxnvb
https://coub.com/embed/p3wb9ayf0i
https://coub.com/embed/cylp42xa95
https://coub.com/embed/5vyv5859hb
https://coub.com/embed/uolo157r87
https://coub.com/embed/7bmp0nxmze
https://coub.com/embed/d2q6yzkrli
https://coub.com/embed/zs1hg1bhrv
https://coub.com/embed/yz4tqllp0u
https://coub.com/embed/a2kvibvldp
https://coub.com/embed/09kqx4xcfx
https://coub.com/embed/2c1zyd3kx3
https://coub.com/embed/wep5eg226t
https://coub.com/embed/su365ycwsu
https://coub.com/embed/nzdz5k3kr0
https://coub.com/embed/qg568ebu9j
https://coub.com/embed/md5pqvk50f
https://coub.com/embed/0gt0ay1zk3
https://coub.com/embed/hecylej9t3
https://coub.com/embed/wmxvxv1kkj
https://coub.com/embed/bxxpl0brtv
https://coub.com/embed/pholjf70ut
https://coub.com/embed/m8l2xwwy39
https://coub.com/embed/1c1x2w7t08
https://coub.com/embed/53mubpegym
https://coub.com/embed/on6slxqrt5
https://coub.com/embed/55q881xx6l
https://coub.com/embed/s77w2ok24r
https://coub.com/embed/4e9hy0237n
https://coub.com/embed/3entpjmdbw
https://coub.com/embed/8besg49zbq
https://coub.com/embed/usqat13gtl
https://coub.com/embed/66yxwx6676
https://coub.com/embed/np30bv52rn
https://coub.com/embed/3nk88ivl0b
https://coub.com/embed/83eyn5yj6r
https://coub.com/embed/bymmywq8fr
https://coub.com/embed/9bj0bp5xii
https://coub.com/embed/agvyb0zhme
https://coub.com/embed/8bcvp8upb3
https://coub.com/embed/c8p7zv5zan
https://coub.com/embed/yry7la9tco
https://coub.com/embed/ho2dopbp7j
https://coub.com/embed/amubk2uwuc
https://coub.com/embed/2jwoy1e4u4
https://coub.com/embed/ezz7hv58aq
https://coub.com/embed/sxb8y6f7cm
https://coub.com/embed/y4kvuhob20
https://coub.com/embed/t3iyg4zbk6
https://coub.com/embed/hlu3q922ul
https://coub.com/embed/wr5t1i5hjn
https://coub.com/embed/ig12bx6jem
https://coub.com/embed/elh7b4dm2v
https://coub.com/embed/p3aozbkqis
https://coub.com/embed/1x387gqa9h
https://coub.com/embed/11vq0o0urq
https://coub.com/embed/n95945i2xa
https://coub.com/embed/kwxot0bqpd
https://coub.com/embed/0bzsnthjf8
https://coub.com/embed/l6n7cci0v1
https://coub.com/embed/e33t42b2e8
https://coub.com/embed/uufau92iew
https://coub.com/embed/8nu2gzpi87
https://coub.com/embed/bzvc0eyha5
https://coub.com/embed/m7i31eulsq
https://coub.com/embed/1r27esow61
https://coub.com/embed/v18hqlwcpz
https://coub.com/embed/hltc1g5iav
https://coub.com/embed/eq093ohvb8
https://coub.com/embed/i44t54uooo
https://coub.com/embed/1p2epqlik8
https://coub.com/embed/r7s85h5tgs
https://coub.com/embed/x3a913pgtg
https://coub.com/embed/rm3vt5vimg
https://coub.com/embed/15wu728znl
https://coub.com/embed/2iwy8dlso1
https://coub.com/embed/9ecofudaq2
https://coub.com/embed/odtibqvqqb
https://coub.com/embed/kfit182kgd
https://coub.com/embed/oyuutd8klx
https://coub.com/embed/2ab3tdnsq2
https://coub.com/embed/0lxrs8kij0
https://coub.com/embed/o1b4imc38s
https://coub.com/embed/hmvpibka9g
https://coub.com/embed/pvj08u1amk
https://coub.com/embed/g3fxbal3j0
https://coub.com/embed/hunlqq2jwi
https://coub.com/embed/wsrr0y4x1q
https://coub.com/embed/kg284yqkff
https://coub.com/embed/hbiuts4e7e
https://coub.com/embed/cl8t4qvz2x
https://coub.com/embed/ydk9cyiff7
https://coub.com/embed/ek17k4yytm
https://coub.com/embed/y6t00hom1a
https://coub.com/embed/19q7wp5i00
https://coub.com/embed/4h8bxs9zz7
https://coub.com/embed/jwflpmph2z
https://coub.com/embed/f213jay0fp
https://coub.com/embed/bsf3lctm94
https://coub.com/embed/e83n1arbod
https://coub.com/embed/swembduv5h
https://coub.com/embed/n3fq7zzf5w
https://coub.com/embed/9lio0yr585
https://coub.com/embed/o4fakj61o0
https://coub.com/embed/1hjvwr455z
https://coub.com/embed/xzugpc1qds
https://coub.com/embed/ev7098ev03
https://coub.com/embed/kx4fsmb4b6
https://coub.com/embed/gcorgj2lp8
https://coub.com/embed/gsk38h0x1t
https://coub.com/embed/1gzcnq0drf
https://coub.com/embed/8kk1rohlkq
https://coub.com/embed/95lk677xts
https://coub.com/embed/klzoirlr0i
https://coub.com/embed/ruh9e6e23z
https://coub.com/embed/kaffn57ll3
https://coub.com/embed/wtwakxr59k
https://coub.com/embed/oq7e9iq28m
https://coub.com/embed/xf0mc6ti8q
https://coub.com/embed/ptjtimwt1u
https://coub.com/embed/uu6kt9484i
https://coub.com/embed/1upd83q7xu
https://coub.com/embed/kry12uzw3v
https://coub.com/embed/dtg6xjn98z
https://coub.com/embed/sn0yhm79wv
https://coub.com/embed/9gqnb8624w
https://coub.com/embed/p32o9d4k7v
https://coub.com/embed/e4p0k8n96b
https://coub.com/embed/ordooqx1q4
https://coub.com/embed/w0en3j9kmt
https://coub.com/embed/z925vaal0x
https://coub.com/embed/llsxxio2o3
https://coub.com/embed/xydiujzwro
https://coub.com/embed/2e486z583s
https://coub.com/embed/b850qpvdwf
https://coub.com/embed/uasf7vsd00
https://coub.com/embed/ne9r9ql0ke
https://coub.com/embed/wrb0dawhk3
https://coub.com/embed/e7vo2n3rv0
https://coub.com/embed/sq5lb2quya
https://coub.com/embed/q7er5zdf9c
https://coub.com/embed/hijacyi6zc

收起阅读 »

列表选择弹窗ActionSheet的sheets属性无法实时更新

问题现象
动态改变列表选择弹窗ActionSheet的sheets属性,弹窗内容无法实时更新。问题代码示例参考如下:

@Entry
@Component
struct showActionSheetExample {
@State sheets: Array<SheetInfo> = [{
title: 'apples',
action: () => {
}
}];
@State count: number = 0;

build() {
Column() {
Button('showActionSheet')
.margin(30)
.onClick(() => {
setInterval(() => {
this.count++;
this.sheets.push({
title: 'bananas' + this.count,
action: () => {
}
});
}, 1000);

      this.getUIContext().showActionSheet({  
        title: 'ActionSheet title',  
        message: 'message',  
        confirm: {  
          value: 'Confirm button',  
          action: () => {  
            console.info('Get Alert Dialog handled');  
          }  
        },  
        alignment: DialogAlignment.Center,  
        sheets: this.sheets  
      });  
    });  
}.width('100%');  

}
}
背景知识
列表选择弹窗 (ActionSheet)是一个列表选择器弹窗适用于呈现多个操作选项,尤其当界面中仅需展示操作列表而无其他内容时。固定样式,当用户需要关注或确认的信息存在列表选择时使用。
当用户需要自定义弹出框内动态更新弹出框属性和内容时,使用不依赖UI组件的自定义弹出框 (openCustomDialog)。存在两种入参方式创建自定义弹出框:
openCustomDialog(传参为ComponentContent形式):通过ComponentContent封装内容可以与UI界面解耦,调用更加灵活,可以满足开发者的封装诉求。具有较高的灵活性,弹出框样式完全自定义,并且在弹出框打开后可以使用updateCustomDialog方法动态更新弹出框的参数。
openCustomDialog(传builder的形式):相对于ComponentContent,builder必须要与上下文做绑定,与UI存在一定耦合。此方法有默认的弹出框样式,适合于开发者想要实现与系统弹窗默认风格一致的效果。
问题定位
在showActionSheet打开列表选择弹窗后,对sheets属性进行动态修改,弹窗UI未变化。

分析结论
ActionSheet列表选择弹窗不支持动态更新属性,需要通过自定义弹出框实现。

修改建议
下面通过openCustomDialog实现,并且以传builder的形式为例,实现动态更新自定义弹出框的内容。

import { BusinessError } from '@kit.BasicServicesKit';

@Entry
@Component
struct Index {
private customDialogComponentId: number = 0;
@State count: number = 0;
intervalId?: number;

@Builder
customDialogComponent() {
Column() {
Text('充电详情').fontSize(25);
Text(已充电时间:${this.count}秒);
Row({ space: 50 }) {
Button('确认').onClick(() => {
this.getUIContext().getPromptAction().closeCustomDialog(this.customDialogComponentId);
clearInterval(this.intervalId);
});
Button('取消').onClick(() => {
this.getUIContext().getPromptAction().closeCustomDialog(this.customDialogComponentId);
clearInterval(this.intervalId);
});
};
}.height(200).padding(5).justifyContent(FlexAlign.SpaceAround);
}

build() {
Row() {
Column({ space: 20 }) {
Text('组件内弹窗')
.fontSize(30)
.onClick(() => {
this.getUIContext()
.getPromptAction()
.openCustomDialog({
builder: () => {
this.customDialogComponent();
}
})
.then((dialogId: number) => {
this.customDialogComponentId = dialogId;
})
.catch((error: BusinessError) => {
console.error(openCustomDialog error code is ${error.code}, message is ${error.message});
});

        this.intervalId = setInterval(() => {  
          this.count++;  
        }, 1 * 1000);  
      });  
  }  
  .width('100%');  
}  
.height('100%');  

}
}
https://coub.com/view/06dq1lpfmv
https://coub.com/view/h9okejkccd
https://coub.com/view/z59288b3bc
https://coub.com/view/jdsgw25pj5
https://coub.com/view/20ms41ljxh
https://coub.com/view/cyjima3av9
https://coub.com/view/xhymuinpm8
https://coub.com/view/gqqk4335y1
https://coub.com/view/9uflyvtfzv
https://coub.com/view/ogd56p24ng
https://coub.com/view/zydhfymmc3
https://coub.com/view/pzd0ac0kg5
https://coub.com/view/obdv3r4cy0
https://coub.com/view/ka2mdh07zg
https://coub.com/view/k8417q6t33
https://coub.com/view/b797hoxnvb
https://coub.com/view/p3wb9ayf0i
https://coub.com/view/cylp42xa95
https://coub.com/view/5vyv5859hb
https://coub.com/view/uolo157r87
https://coub.com/view/7bmp0nxmze
https://coub.com/view/d2q6yzkrli
https://coub.com/view/zs1hg1bhrv
https://coub.com/view/yz4tqllp0u
https://coub.com/view/a2kvibvldp
https://coub.com/view/09kqx4xcfx
https://coub.com/view/2c1zyd3kx3
https://coub.com/view/wep5eg226t
https://coub.com/view/su365ycwsu
https://coub.com/view/nzdz5k3kr0
https://coub.com/view/qg568ebu9j
https://coub.com/view/md5pqvk50f
https://coub.com/view/0gt0ay1zk3
https://coub.com/view/hecylej9t3
https://coub.com/view/wmxvxv1kkj
https://coub.com/view/bxxpl0brtv
https://coub.com/view/pholjf70ut
https://coub.com/view/m8l2xwwy39
https://coub.com/view/1c1x2w7t08
https://coub.com/view/53mubpegym
https://coub.com/view/on6slxqrt5
https://coub.com/view/55q881xx6l
https://coub.com/view/s77w2ok24r
https://coub.com/view/4e9hy0237n
https://coub.com/view/3entpjmdbw
https://coub.com/view/8besg49zbq
https://coub.com/view/usqat13gtl
https://coub.com/view/66yxwx6676
https://coub.com/view/np30bv52rn
https://coub.com/view/3nk88ivl0b
https://coub.com/view/83eyn5yj6r
https://coub.com/view/bymmywq8fr
https://coub.com/view/9bj0bp5xii
https://coub.com/view/agvyb0zhme
https://coub.com/view/8bcvp8upb3
https://coub.com/view/c8p7zv5zan
https://coub.com/view/yry7la9tco
https://coub.com/view/ho2dopbp7j
https://coub.com/view/amubk2uwuc
https://coub.com/view/2jwoy1e4u4
https://coub.com/view/ezz7hv58aq
https://coub.com/view/sxb8y6f7cm
https://coub.com/view/y4kvuhob20
https://coub.com/view/t3iyg4zbk6
https://coub.com/view/hlu3q922ul
https://coub.com/view/wr5t1i5hjn
https://coub.com/view/ig12bx6jem
https://coub.com/view/elh7b4dm2v
https://coub.com/view/p3aozbkqis
https://coub.com/view/1x387gqa9h
https://coub.com/view/11vq0o0urq
https://coub.com/view/n95945i2xa
https://coub.com/view/kwxot0bqpd
https://coub.com/view/0bzsnthjf8
https://coub.com/view/l6n7cci0v1
https://coub.com/view/e33t42b2e8
https://coub.com/view/uufau92iew
https://coub.com/view/8nu2gzpi87
https://coub.com/view/bzvc0eyha5
https://coub.com/view/m7i31eulsq

继续阅读 »

问题现象
动态改变列表选择弹窗ActionSheet的sheets属性,弹窗内容无法实时更新。问题代码示例参考如下:

@Entry
@Component
struct showActionSheetExample {
@State sheets: Array<SheetInfo> = [{
title: 'apples',
action: () => {
}
}];
@State count: number = 0;

build() {
Column() {
Button('showActionSheet')
.margin(30)
.onClick(() => {
setInterval(() => {
this.count++;
this.sheets.push({
title: 'bananas' + this.count,
action: () => {
}
});
}, 1000);

      this.getUIContext().showActionSheet({  
        title: 'ActionSheet title',  
        message: 'message',  
        confirm: {  
          value: 'Confirm button',  
          action: () => {  
            console.info('Get Alert Dialog handled');  
          }  
        },  
        alignment: DialogAlignment.Center,  
        sheets: this.sheets  
      });  
    });  
}.width('100%');  

}
}
背景知识
列表选择弹窗 (ActionSheet)是一个列表选择器弹窗适用于呈现多个操作选项,尤其当界面中仅需展示操作列表而无其他内容时。固定样式,当用户需要关注或确认的信息存在列表选择时使用。
当用户需要自定义弹出框内动态更新弹出框属性和内容时,使用不依赖UI组件的自定义弹出框 (openCustomDialog)。存在两种入参方式创建自定义弹出框:
openCustomDialog(传参为ComponentContent形式):通过ComponentContent封装内容可以与UI界面解耦,调用更加灵活,可以满足开发者的封装诉求。具有较高的灵活性,弹出框样式完全自定义,并且在弹出框打开后可以使用updateCustomDialog方法动态更新弹出框的参数。
openCustomDialog(传builder的形式):相对于ComponentContent,builder必须要与上下文做绑定,与UI存在一定耦合。此方法有默认的弹出框样式,适合于开发者想要实现与系统弹窗默认风格一致的效果。
问题定位
在showActionSheet打开列表选择弹窗后,对sheets属性进行动态修改,弹窗UI未变化。

分析结论
ActionSheet列表选择弹窗不支持动态更新属性,需要通过自定义弹出框实现。

修改建议
下面通过openCustomDialog实现,并且以传builder的形式为例,实现动态更新自定义弹出框的内容。

import { BusinessError } from '@kit.BasicServicesKit';

@Entry
@Component
struct Index {
private customDialogComponentId: number = 0;
@State count: number = 0;
intervalId?: number;

@Builder
customDialogComponent() {
Column() {
Text('充电详情').fontSize(25);
Text(已充电时间:${this.count}秒);
Row({ space: 50 }) {
Button('确认').onClick(() => {
this.getUIContext().getPromptAction().closeCustomDialog(this.customDialogComponentId);
clearInterval(this.intervalId);
});
Button('取消').onClick(() => {
this.getUIContext().getPromptAction().closeCustomDialog(this.customDialogComponentId);
clearInterval(this.intervalId);
});
};
}.height(200).padding(5).justifyContent(FlexAlign.SpaceAround);
}

build() {
Row() {
Column({ space: 20 }) {
Text('组件内弹窗')
.fontSize(30)
.onClick(() => {
this.getUIContext()
.getPromptAction()
.openCustomDialog({
builder: () => {
this.customDialogComponent();
}
})
.then((dialogId: number) => {
this.customDialogComponentId = dialogId;
})
.catch((error: BusinessError) => {
console.error(openCustomDialog error code is ${error.code}, message is ${error.message});
});

        this.intervalId = setInterval(() => {  
          this.count++;  
        }, 1 * 1000);  
      });  
  }  
  .width('100%');  
}  
.height('100%');  

}
}
https://coub.com/view/06dq1lpfmv
https://coub.com/view/h9okejkccd
https://coub.com/view/z59288b3bc
https://coub.com/view/jdsgw25pj5
https://coub.com/view/20ms41ljxh
https://coub.com/view/cyjima3av9
https://coub.com/view/xhymuinpm8
https://coub.com/view/gqqk4335y1
https://coub.com/view/9uflyvtfzv
https://coub.com/view/ogd56p24ng
https://coub.com/view/zydhfymmc3
https://coub.com/view/pzd0ac0kg5
https://coub.com/view/obdv3r4cy0
https://coub.com/view/ka2mdh07zg
https://coub.com/view/k8417q6t33
https://coub.com/view/b797hoxnvb
https://coub.com/view/p3wb9ayf0i
https://coub.com/view/cylp42xa95
https://coub.com/view/5vyv5859hb
https://coub.com/view/uolo157r87
https://coub.com/view/7bmp0nxmze
https://coub.com/view/d2q6yzkrli
https://coub.com/view/zs1hg1bhrv
https://coub.com/view/yz4tqllp0u
https://coub.com/view/a2kvibvldp
https://coub.com/view/09kqx4xcfx
https://coub.com/view/2c1zyd3kx3
https://coub.com/view/wep5eg226t
https://coub.com/view/su365ycwsu
https://coub.com/view/nzdz5k3kr0
https://coub.com/view/qg568ebu9j
https://coub.com/view/md5pqvk50f
https://coub.com/view/0gt0ay1zk3
https://coub.com/view/hecylej9t3
https://coub.com/view/wmxvxv1kkj
https://coub.com/view/bxxpl0brtv
https://coub.com/view/pholjf70ut
https://coub.com/view/m8l2xwwy39
https://coub.com/view/1c1x2w7t08
https://coub.com/view/53mubpegym
https://coub.com/view/on6slxqrt5
https://coub.com/view/55q881xx6l
https://coub.com/view/s77w2ok24r
https://coub.com/view/4e9hy0237n
https://coub.com/view/3entpjmdbw
https://coub.com/view/8besg49zbq
https://coub.com/view/usqat13gtl
https://coub.com/view/66yxwx6676
https://coub.com/view/np30bv52rn
https://coub.com/view/3nk88ivl0b
https://coub.com/view/83eyn5yj6r
https://coub.com/view/bymmywq8fr
https://coub.com/view/9bj0bp5xii
https://coub.com/view/agvyb0zhme
https://coub.com/view/8bcvp8upb3
https://coub.com/view/c8p7zv5zan
https://coub.com/view/yry7la9tco
https://coub.com/view/ho2dopbp7j
https://coub.com/view/amubk2uwuc
https://coub.com/view/2jwoy1e4u4
https://coub.com/view/ezz7hv58aq
https://coub.com/view/sxb8y6f7cm
https://coub.com/view/y4kvuhob20
https://coub.com/view/t3iyg4zbk6
https://coub.com/view/hlu3q922ul
https://coub.com/view/wr5t1i5hjn
https://coub.com/view/ig12bx6jem
https://coub.com/view/elh7b4dm2v
https://coub.com/view/p3aozbkqis
https://coub.com/view/1x387gqa9h
https://coub.com/view/11vq0o0urq
https://coub.com/view/n95945i2xa
https://coub.com/view/kwxot0bqpd
https://coub.com/view/0bzsnthjf8
https://coub.com/view/l6n7cci0v1
https://coub.com/view/e33t42b2e8
https://coub.com/view/uufau92iew
https://coub.com/view/8nu2gzpi87
https://coub.com/view/bzvc0eyha5
https://coub.com/view/m7i31eulsq

收起阅读 »

如何实现拖拽时列表项占位动画的效果

grid

拖拽Grid时,列表项显示占位动画效果。实现步骤如下:

在Grid组件下设置属性editMode(true),使Grid进入编辑模式。进入编辑模式后,可以拖拽Grid组件内部的GridItem。
在onItemDragStart回调中设置拖拽时显示的组件。
在onItemDrop中获取拖拽起始位置和拖拽插入位置,并完成数组位置交换逻辑。
@Entry
@Component
struct GridExample {
@State numbers: string[] = [];
scroller: Scroller = new Scroller();
@State text: string = 'drag';

@Builder
pixelMapBuilder() {
Column() {
Text(this.text)
.fontSize(16)
.backgroundColor(0xF9CF93)
.width(80)
.height(80)
.textAlign(TextAlign.Center)
}
}

aboutToAppear() {
for (let i = 1; i <= 15; i++) {
this.numbers.push(i + '');
}
}

changeIndex(index1: number, index2: number) {
// Swap array positions
let temp = this.numbers[index1];
this.numbers[index1] = this.numbers[index2];
this.numbers[index2] = temp;
}

build() {
Column({ space: 5 }) {
Grid(this.scroller) {
ForEach(this.numbers, (day: string) => {
GridItem() {
Text(day)
.fontSize(16)
.backgroundColor(0xF9CF93)
.width(80)
.height(80)
.textAlign(TextAlign.Center)
.onTouch((event: TouchEvent) => {
if (event.type === TouchType.Up) {
this.text = day;
}
})
}
})
}
.columnsTemplate('1fr 1fr 1fr')
.columnsGap(10)
.rowsGap(10)
.onScrollIndex((first: number) => {
console.info(first.toString());
})
.width('90%')
.backgroundColor(0xFAEEE0)
.height(300)
.editMode(true) // Set whether the Grid enters editing mode. When entering editing mode, you can drag and drop the GridItem inside the Grid component
.onItemDragStart((event: ItemDragInfo, itemIndex: number) => { // When dragging the component bound to this event for the first time, a callback is triggered.
return this.pixelMapBuilder(); //Set the image displayed during the drag and drop process.
})
// The component bound to this event can be used as a drag and drop release target. When the drag behavior stops within the scope of this component, a callback is triggered.
.onItemDrop((event: ItemDragInfo, itemIndex: number, insertIndex: number, isSuccess: boolean) => {
// Drag the starting position of itemIndex, drag the insertion position of insertIndex
this.changeIndex(itemIndex, insertIndex)
})
}.width('100%').margin({ top: 5 })
}
}
https://sites.google.com/view/7wtipu12/home
https://sites.google.com/view/4duref63/home
https://sites.google.com/view/7wvqsg34/home
https://sites.google.com/view/0xxrqf74/home
https://sites.google.com/view/1ruukx88/home
https://sites.google.com/view/1byjvn60/home
https://sites.google.com/view/4xgvng21/home
https://sites.google.com/view/9pciml46/home
https://sites.google.com/view/2hbbnf70/home
https://sites.google.com/view/9rghya88/home
https://sites.google.com/view/7vwdre23/home
https://sites.google.com/view/7przvh02/home
https://sites.google.com/view/3hktye35/home
https://sites.google.com/view/0xykfj78/home
https://sites.google.com/view/0asapr45/home
https://sites.google.com/view/0rgboz72/home
https://sites.google.com/view/0qrzbw79/home
https://sites.google.com/view/4pszed21/home
https://sites.google.com/view/5rjciu59/home
https://sites.google.com/view/0ssfsw28/home
https://sites.google.com/view/7pmhty48/home
https://sites.google.com/view/2dceln83/home
https://sites.google.com/view/1mmlgm03/home
https://sites.google.com/view/1tpbqp74/home
https://sites.google.com/view/9sfedj48/home
https://sites.google.com/view/3ibsri54/home
https://sites.google.com/view/6kncht37/home
https://sites.google.com/view/1aczwe01/home
https://sites.google.com/view/3sxmeg54/home
https://sites.google.com/view/7eprfq47/home
https://sites.google.com/view/1ebnwt11/home
https://sites.google.com/view/5cywba48/home
https://sites.google.com/view/8vqwer81/home
https://sites.google.com/view/8wetjp74/home
https://sites.google.com/view/8goxua35/home
https://sites.google.com/view/4kslyk33/home
https://sites.google.com/view/4essyn94/home
https://sites.google.com/view/2jvyst79/home
https://sites.google.com/view/1roras95/home
https://sites.google.com/view/1ktawz57/home
https://sites.google.com/view/0rgcul98/home
https://sites.google.com/view/0nycaz65/home
https://sites.google.com/view/1omztt55/home
https://sites.google.com/view/7gdqsw63/home
https://sites.google.com/view/4pxdgz83/home
https://sites.google.com/view/0gwyrr33/home
https://sites.google.com/view/2ijimf20/home
https://sites.google.com/view/8wykzr07/home
https://sites.google.com/view/7nudnf27/home
https://sites.google.com/view/3kcowk62/home
https://sites.google.com/view/5pytzw49/home
https://sites.google.com/view/2slklr71/home
https://sites.google.com/view/2ikmgs30/home
https://sites.google.com/view/5hqxfe84/home
https://sites.google.com/view/0kqdmc12/home
https://sites.google.com/view/5axjec74/home
https://sites.google.com/view/5gixkd52/home
https://sites.google.com/view/6kgtbh91/home
https://sites.google.com/view/1dtvwv05/home
https://sites.google.com/view/3nbvob04/home
https://sites.google.com/view/2fpwug45/home
https://sites.google.com/view/6xjygc54/home
https://sites.google.com/view/4uqced58/home
https://sites.google.com/view/5whcuq51/home
https://sites.google.com/view/2rgfcv68/home
https://sites.google.com/view/5pwouh86/home
https://sites.google.com/view/6dwmpc30/home
https://sites.google.com/view/3wihcp64/home
https://sites.google.com/view/2huocs89/home
https://sites.google.com/view/3mmiei39/home
https://sites.google.com/view/1rvjfn27/home
https://sites.google.com/view/6lcinp92/home
https://sites.google.com/view/6gxkse35/home
https://sites.google.com/view/3wahfy11/home
https://coub.com/view/5rqufxinzg
https://coub.com/view/ropziici0u
https://coub.com/view/7or1trdg1c
https://coub.com/view/vcozxsnzer
https://coub.com/view/7kd7t3oj2c
https://coub.com/view/8aqyygye2e
https://coub.com/view/gllow4joor
https://coub.com/view/39dnitz99r
https://coub.com/view/1t0lqxrzi8
https://coub.com/view/vnj2yxw549
https://coub.com/view/3s1pmq3ilx
https://coub.com/view/a1ytt3pw3a
https://coub.com/view/cawk3s04k4
https://coub.com/view/kmvzuhkhjd
https://coub.com/view/ej9o0adzwx
https://coub.com/view/octx2ubxfv
https://coub.com/view/aa9gmfdu3q
https://coub.com/view/9bgaznk1ck
https://coub.com/view/rchkbwofuc
https://coub.com/view/9s2jppcl9u
https://coub.com/view/5sj5dui5qx
https://coub.com/view/m2ajgr2j7k
https://coub.com/view/0yfa17r86g
https://coub.com/view/gr0kpo0pmh
https://coub.com/view/l3hqw0zqc5
https://coub.com/view/88qknf3fk6
https://coub.com/view/isb8wfeda2
https://coub.com/view/yvsr9c6x01
https://coub.com/view/mb687s5mws
https://coub.com/view/rgb9qqxyvj
https://coub.com/view/03xbg531k6
https://coub.com/view/0p29ulxra3
https://coub.com/view/8815w7t7de
https://coub.com/view/shicmahfum
https://coub.com/view/gpbeot9j3l
https://coub.com/view/2dfpjx3czu
https://coub.com/view/xs3ukwnjf3
https://coub.com/view/g71qbn6jru
https://coub.com/view/nshhnvyttc
https://coub.com/view/jmz1bnvuks
https://coub.com/view/fc80zsrnh1
https://coub.com/view/ppu1e0wlob
https://coub.com/view/o8b8ik13xp
https://coub.com/view/wjc31q3bpo
https://coub.com/view/s3h67xw7jb
https://coub.com/view/zofzxw59k4
https://coub.com/view/kdi3wrrueu
https://coub.com/view/98nrx1y16i
https://coub.com/view/agp7euis6f
https://coub.com/view/alaeuzimeo
https://coub.com/view/6y73g5cr1s
https://coub.com/view/vfnl3o6o5c
https://coub.com/view/de4e7iqa60
https://coub.com/view/c4htjqg88m
https://coub.com/view/kz3ieaosfi
https://coub.com/view/d6msuxbq4a
https://coub.com/view/77tzitriot
https://coub.com/view/lpsk81odp9
https://coub.com/view/2nmdnujl00
https://coub.com/view/l8mq0g4760
https://coub.com/view/2mkc8ze5zx
https://coub.com/view/ig0wfzltc6
https://coub.com/view/1ycu3zf1lh
https://coub.com/view/wzat8knm2z
https://coub.com/view/64fv7jr83g
https://coub.com/view/a3did3vro0
https://coub.com/view/ti645azepy
https://coub.com/view/g1vuwgj26b
https://coub.com/view/ffzr61ask8
https://coub.com/view/p5bbm0hek0
https://coub.com/view/g3cd11h0y6
https://coub.com/view/3sf0ida4ix
https://coub.com/view/7bu9ihff89
https://coub.com/view/xjn62txhsl
https://coub.com/view/nlk1uu6rts
https://coub.com/view/t0d2e48ya0
https://coub.com/view/nseh2ehyd9
https://coub.com/view/5v4pyo0v0k
https://coub.com/view/rierf8fjyo

继续阅读 »

拖拽Grid时,列表项显示占位动画效果。实现步骤如下:

在Grid组件下设置属性editMode(true),使Grid进入编辑模式。进入编辑模式后,可以拖拽Grid组件内部的GridItem。
在onItemDragStart回调中设置拖拽时显示的组件。
在onItemDrop中获取拖拽起始位置和拖拽插入位置,并完成数组位置交换逻辑。
@Entry
@Component
struct GridExample {
@State numbers: string[] = [];
scroller: Scroller = new Scroller();
@State text: string = 'drag';

@Builder
pixelMapBuilder() {
Column() {
Text(this.text)
.fontSize(16)
.backgroundColor(0xF9CF93)
.width(80)
.height(80)
.textAlign(TextAlign.Center)
}
}

aboutToAppear() {
for (let i = 1; i <= 15; i++) {
this.numbers.push(i + '');
}
}

changeIndex(index1: number, index2: number) {
// Swap array positions
let temp = this.numbers[index1];
this.numbers[index1] = this.numbers[index2];
this.numbers[index2] = temp;
}

build() {
Column({ space: 5 }) {
Grid(this.scroller) {
ForEach(this.numbers, (day: string) => {
GridItem() {
Text(day)
.fontSize(16)
.backgroundColor(0xF9CF93)
.width(80)
.height(80)
.textAlign(TextAlign.Center)
.onTouch((event: TouchEvent) => {
if (event.type === TouchType.Up) {
this.text = day;
}
})
}
})
}
.columnsTemplate('1fr 1fr 1fr')
.columnsGap(10)
.rowsGap(10)
.onScrollIndex((first: number) => {
console.info(first.toString());
})
.width('90%')
.backgroundColor(0xFAEEE0)
.height(300)
.editMode(true) // Set whether the Grid enters editing mode. When entering editing mode, you can drag and drop the GridItem inside the Grid component
.onItemDragStart((event: ItemDragInfo, itemIndex: number) => { // When dragging the component bound to this event for the first time, a callback is triggered.
return this.pixelMapBuilder(); //Set the image displayed during the drag and drop process.
})
// The component bound to this event can be used as a drag and drop release target. When the drag behavior stops within the scope of this component, a callback is triggered.
.onItemDrop((event: ItemDragInfo, itemIndex: number, insertIndex: number, isSuccess: boolean) => {
// Drag the starting position of itemIndex, drag the insertion position of insertIndex
this.changeIndex(itemIndex, insertIndex)
})
}.width('100%').margin({ top: 5 })
}
}
https://sites.google.com/view/7wtipu12/home
https://sites.google.com/view/4duref63/home
https://sites.google.com/view/7wvqsg34/home
https://sites.google.com/view/0xxrqf74/home
https://sites.google.com/view/1ruukx88/home
https://sites.google.com/view/1byjvn60/home
https://sites.google.com/view/4xgvng21/home
https://sites.google.com/view/9pciml46/home
https://sites.google.com/view/2hbbnf70/home
https://sites.google.com/view/9rghya88/home
https://sites.google.com/view/7vwdre23/home
https://sites.google.com/view/7przvh02/home
https://sites.google.com/view/3hktye35/home
https://sites.google.com/view/0xykfj78/home
https://sites.google.com/view/0asapr45/home
https://sites.google.com/view/0rgboz72/home
https://sites.google.com/view/0qrzbw79/home
https://sites.google.com/view/4pszed21/home
https://sites.google.com/view/5rjciu59/home
https://sites.google.com/view/0ssfsw28/home
https://sites.google.com/view/7pmhty48/home
https://sites.google.com/view/2dceln83/home
https://sites.google.com/view/1mmlgm03/home
https://sites.google.com/view/1tpbqp74/home
https://sites.google.com/view/9sfedj48/home
https://sites.google.com/view/3ibsri54/home
https://sites.google.com/view/6kncht37/home
https://sites.google.com/view/1aczwe01/home
https://sites.google.com/view/3sxmeg54/home
https://sites.google.com/view/7eprfq47/home
https://sites.google.com/view/1ebnwt11/home
https://sites.google.com/view/5cywba48/home
https://sites.google.com/view/8vqwer81/home
https://sites.google.com/view/8wetjp74/home
https://sites.google.com/view/8goxua35/home
https://sites.google.com/view/4kslyk33/home
https://sites.google.com/view/4essyn94/home
https://sites.google.com/view/2jvyst79/home
https://sites.google.com/view/1roras95/home
https://sites.google.com/view/1ktawz57/home
https://sites.google.com/view/0rgcul98/home
https://sites.google.com/view/0nycaz65/home
https://sites.google.com/view/1omztt55/home
https://sites.google.com/view/7gdqsw63/home
https://sites.google.com/view/4pxdgz83/home
https://sites.google.com/view/0gwyrr33/home
https://sites.google.com/view/2ijimf20/home
https://sites.google.com/view/8wykzr07/home
https://sites.google.com/view/7nudnf27/home
https://sites.google.com/view/3kcowk62/home
https://sites.google.com/view/5pytzw49/home
https://sites.google.com/view/2slklr71/home
https://sites.google.com/view/2ikmgs30/home
https://sites.google.com/view/5hqxfe84/home
https://sites.google.com/view/0kqdmc12/home
https://sites.google.com/view/5axjec74/home
https://sites.google.com/view/5gixkd52/home
https://sites.google.com/view/6kgtbh91/home
https://sites.google.com/view/1dtvwv05/home
https://sites.google.com/view/3nbvob04/home
https://sites.google.com/view/2fpwug45/home
https://sites.google.com/view/6xjygc54/home
https://sites.google.com/view/4uqced58/home
https://sites.google.com/view/5whcuq51/home
https://sites.google.com/view/2rgfcv68/home
https://sites.google.com/view/5pwouh86/home
https://sites.google.com/view/6dwmpc30/home
https://sites.google.com/view/3wihcp64/home
https://sites.google.com/view/2huocs89/home
https://sites.google.com/view/3mmiei39/home
https://sites.google.com/view/1rvjfn27/home
https://sites.google.com/view/6lcinp92/home
https://sites.google.com/view/6gxkse35/home
https://sites.google.com/view/3wahfy11/home
https://coub.com/view/5rqufxinzg
https://coub.com/view/ropziici0u
https://coub.com/view/7or1trdg1c
https://coub.com/view/vcozxsnzer
https://coub.com/view/7kd7t3oj2c
https://coub.com/view/8aqyygye2e
https://coub.com/view/gllow4joor
https://coub.com/view/39dnitz99r
https://coub.com/view/1t0lqxrzi8
https://coub.com/view/vnj2yxw549
https://coub.com/view/3s1pmq3ilx
https://coub.com/view/a1ytt3pw3a
https://coub.com/view/cawk3s04k4
https://coub.com/view/kmvzuhkhjd
https://coub.com/view/ej9o0adzwx
https://coub.com/view/octx2ubxfv
https://coub.com/view/aa9gmfdu3q
https://coub.com/view/9bgaznk1ck
https://coub.com/view/rchkbwofuc
https://coub.com/view/9s2jppcl9u
https://coub.com/view/5sj5dui5qx
https://coub.com/view/m2ajgr2j7k
https://coub.com/view/0yfa17r86g
https://coub.com/view/gr0kpo0pmh
https://coub.com/view/l3hqw0zqc5
https://coub.com/view/88qknf3fk6
https://coub.com/view/isb8wfeda2
https://coub.com/view/yvsr9c6x01
https://coub.com/view/mb687s5mws
https://coub.com/view/rgb9qqxyvj
https://coub.com/view/03xbg531k6
https://coub.com/view/0p29ulxra3
https://coub.com/view/8815w7t7de
https://coub.com/view/shicmahfum
https://coub.com/view/gpbeot9j3l
https://coub.com/view/2dfpjx3czu
https://coub.com/view/xs3ukwnjf3
https://coub.com/view/g71qbn6jru
https://coub.com/view/nshhnvyttc
https://coub.com/view/jmz1bnvuks
https://coub.com/view/fc80zsrnh1
https://coub.com/view/ppu1e0wlob
https://coub.com/view/o8b8ik13xp
https://coub.com/view/wjc31q3bpo
https://coub.com/view/s3h67xw7jb
https://coub.com/view/zofzxw59k4
https://coub.com/view/kdi3wrrueu
https://coub.com/view/98nrx1y16i
https://coub.com/view/agp7euis6f
https://coub.com/view/alaeuzimeo
https://coub.com/view/6y73g5cr1s
https://coub.com/view/vfnl3o6o5c
https://coub.com/view/de4e7iqa60
https://coub.com/view/c4htjqg88m
https://coub.com/view/kz3ieaosfi
https://coub.com/view/d6msuxbq4a
https://coub.com/view/77tzitriot
https://coub.com/view/lpsk81odp9
https://coub.com/view/2nmdnujl00
https://coub.com/view/l8mq0g4760
https://coub.com/view/2mkc8ze5zx
https://coub.com/view/ig0wfzltc6
https://coub.com/view/1ycu3zf1lh
https://coub.com/view/wzat8knm2z
https://coub.com/view/64fv7jr83g
https://coub.com/view/a3did3vro0
https://coub.com/view/ti645azepy
https://coub.com/view/g1vuwgj26b
https://coub.com/view/ffzr61ask8
https://coub.com/view/p5bbm0hek0
https://coub.com/view/g3cd11h0y6
https://coub.com/view/3sf0ida4ix
https://coub.com/view/7bu9ihff89
https://coub.com/view/xjn62txhsl
https://coub.com/view/nlk1uu6rts
https://coub.com/view/t0d2e48ya0
https://coub.com/view/nseh2ehyd9
https://coub.com/view/5v4pyo0v0k
https://coub.com/view/rierf8fjyo

收起阅读 »

UI布局默认是多少vp为基准,以达到不同机器自适应

无论屏幕分辨率或密度如何,组件的视觉效果保持一致。

vp具体计算公式为:vp= px/(DPI/160)

px 是屏幕的真实物理像素值,densityDPI 通常指系统屏幕密度,densityPixels是屏幕密度与标准DPI的比率,常见取值有 0.75、1.0、1.5、2.0、3.0 等。在HarmonyOS中,标准DPI为160。以华为Mate 40 Pro为例,densityDPI为 560,densityPixels为3.5。要查看真机的DPI,可以调用屏幕属性中的display接口查询。

import { display } from '@kit.ArkUI';

let displayClass: display.Display | null = null;
try {
displayClass = display.getDefaultDisplaySync();
} catch (exception) {
console.error('Failed to obtain the default display object. Code: ' + JSON.stringify(exception));
}
AdaptiveForDifferentmMachines.ets
如果原型图没有提供vp单位的布局,开发者可以根据densityPixels把px转为vp,HarmonyOS也封装了现成的接口px2vp()和vp2px()供开发者直接调用。
https://coub.com/view/1ssmnmys5l
https://coub.com/view/b8vv1qlbt7
https://coub.com/view/4njv0pb1sf
https://coub.com/view/2osl0d6206
https://coub.com/view/8hcuett3go
https://coub.com/view/wb4oa55cid
https://coub.com/view/3cnyxlt5cu
https://coub.com/view/jyzx0cl5pb
https://coub.com/view/rzh0ovu8wc
https://coub.com/view/601vumd0bk
https://coub.com/view/kryjg96qpm
https://coub.com/view/zxp8nixohu
https://coub.com/view/s3zzuk1pfh
https://coub.com/view/th5gpreu9p
https://coub.com/view/vvp0k5p5g2
https://coub.com/view/ug9i0nx3ry
https://coub.com/view/a6hg663761
https://coub.com/view/r2ybmjgcb0
https://coub.com/view/qyxo2oo0vm
https://coub.com/view/eqrncvrd0k
https://coub.com/view/f60efr44zc
https://coub.com/view/8yqp6z887o
https://coub.com/view/l931hfzcac
https://coub.com/view/tit85csau0
https://coub.com/view/k2qbxk9zw4
https://coub.com/view/ag4fxvk8j8
https://coub.com/view/079frfgd9z
https://coub.com/view/h0uny3c8xm
https://coub.com/view/9xqoqiqfbs
https://coub.com/view/3wfhfva45l
https://coub.com/view/c5jmnpf7bo
https://coub.com/view/qvuopnvg0p
https://coub.com/view/qkdi3a72io
https://coub.com/view/vmk22b54yi
https://coub.com/view/a0f6c8kp4s
https://coub.com/view/wadaaaejro
https://coub.com/view/txg69p365y
https://coub.com/view/sm74dy9gq1
https://coub.com/view/5x3owd7gnj
https://coub.com/view/ahwe5wmhis
https://coub.com/view/q6ulkpx5zl
https://coub.com/view/i3utbril6v
https://coub.com/view/hdtgtno7cv
https://coub.com/view/5zgydpfi1g
https://coub.com/view/81ruiq2gsm
https://coub.com/view/djhntig6et
https://coub.com/view/g6ompv5j9v
https://coub.com/view/ogd7m4t2nv
https://coub.com/view/uvlxpdhlx8
https://coub.com/view/gfpc1o7dy8
https://coub.com/view/6iljnuq8rp
https://coub.com/view/vof5o7imk9
https://coub.com/view/hthbnpqg3u
https://coub.com/view/5ljk3310jb
https://coub.com/view/xqlksk0uwr
https://coub.com/view/us08m31g3o
https://coub.com/view/xt8c9f9ik6
https://coub.com/view/nwwjnlg6u3
https://coub.com/view/r8zxzny4n6
https://coub.com/view/80fbenpftn
https://coub.com/view/69127p3k65
https://coub.com/view/uzi71doc2a
https://coub.com/view/imxc29ism8
https://coub.com/view/c14zdeevut
https://coub.com/view/biphkw6uhd
https://coub.com/view/luqv2os2af
https://coub.com/view/chz2n7wk98
https://coub.com/view/c7f3b1dses
https://coub.com/view/t66yfm5wcq
https://coub.com/view/pn27l55do8
https://coub.com/view/sz40uzshaw
https://coub.com/view/o7j89d8rwn
https://coub.com/view/aa3k89ktsk
https://coub.com/view/lf0d5hsld6
https://coub.com/view/gu5537brfz
https://coub.com/view/6uio4ilcrf
https://coub.com/view/ms6kce573a
https://coub.com/view/0lbw9ytytc
https://coub.com/view/gxlgdqofxz
https://coub.com/view/3pln3aqq2k

继续阅读 »

无论屏幕分辨率或密度如何,组件的视觉效果保持一致。

vp具体计算公式为:vp= px/(DPI/160)

px 是屏幕的真实物理像素值,densityDPI 通常指系统屏幕密度,densityPixels是屏幕密度与标准DPI的比率,常见取值有 0.75、1.0、1.5、2.0、3.0 等。在HarmonyOS中,标准DPI为160。以华为Mate 40 Pro为例,densityDPI为 560,densityPixels为3.5。要查看真机的DPI,可以调用屏幕属性中的display接口查询。

import { display } from '@kit.ArkUI';

let displayClass: display.Display | null = null;
try {
displayClass = display.getDefaultDisplaySync();
} catch (exception) {
console.error('Failed to obtain the default display object. Code: ' + JSON.stringify(exception));
}
AdaptiveForDifferentmMachines.ets
如果原型图没有提供vp单位的布局,开发者可以根据densityPixels把px转为vp,HarmonyOS也封装了现成的接口px2vp()和vp2px()供开发者直接调用。
https://coub.com/view/1ssmnmys5l
https://coub.com/view/b8vv1qlbt7
https://coub.com/view/4njv0pb1sf
https://coub.com/view/2osl0d6206
https://coub.com/view/8hcuett3go
https://coub.com/view/wb4oa55cid
https://coub.com/view/3cnyxlt5cu
https://coub.com/view/jyzx0cl5pb
https://coub.com/view/rzh0ovu8wc
https://coub.com/view/601vumd0bk
https://coub.com/view/kryjg96qpm
https://coub.com/view/zxp8nixohu
https://coub.com/view/s3zzuk1pfh
https://coub.com/view/th5gpreu9p
https://coub.com/view/vvp0k5p5g2
https://coub.com/view/ug9i0nx3ry
https://coub.com/view/a6hg663761
https://coub.com/view/r2ybmjgcb0
https://coub.com/view/qyxo2oo0vm
https://coub.com/view/eqrncvrd0k
https://coub.com/view/f60efr44zc
https://coub.com/view/8yqp6z887o
https://coub.com/view/l931hfzcac
https://coub.com/view/tit85csau0
https://coub.com/view/k2qbxk9zw4
https://coub.com/view/ag4fxvk8j8
https://coub.com/view/079frfgd9z
https://coub.com/view/h0uny3c8xm
https://coub.com/view/9xqoqiqfbs
https://coub.com/view/3wfhfva45l
https://coub.com/view/c5jmnpf7bo
https://coub.com/view/qvuopnvg0p
https://coub.com/view/qkdi3a72io
https://coub.com/view/vmk22b54yi
https://coub.com/view/a0f6c8kp4s
https://coub.com/view/wadaaaejro
https://coub.com/view/txg69p365y
https://coub.com/view/sm74dy9gq1
https://coub.com/view/5x3owd7gnj
https://coub.com/view/ahwe5wmhis
https://coub.com/view/q6ulkpx5zl
https://coub.com/view/i3utbril6v
https://coub.com/view/hdtgtno7cv
https://coub.com/view/5zgydpfi1g
https://coub.com/view/81ruiq2gsm
https://coub.com/view/djhntig6et
https://coub.com/view/g6ompv5j9v
https://coub.com/view/ogd7m4t2nv
https://coub.com/view/uvlxpdhlx8
https://coub.com/view/gfpc1o7dy8
https://coub.com/view/6iljnuq8rp
https://coub.com/view/vof5o7imk9
https://coub.com/view/hthbnpqg3u
https://coub.com/view/5ljk3310jb
https://coub.com/view/xqlksk0uwr
https://coub.com/view/us08m31g3o
https://coub.com/view/xt8c9f9ik6
https://coub.com/view/nwwjnlg6u3
https://coub.com/view/r8zxzny4n6
https://coub.com/view/80fbenpftn
https://coub.com/view/69127p3k65
https://coub.com/view/uzi71doc2a
https://coub.com/view/imxc29ism8
https://coub.com/view/c14zdeevut
https://coub.com/view/biphkw6uhd
https://coub.com/view/luqv2os2af
https://coub.com/view/chz2n7wk98
https://coub.com/view/c7f3b1dses
https://coub.com/view/t66yfm5wcq
https://coub.com/view/pn27l55do8
https://coub.com/view/sz40uzshaw
https://coub.com/view/o7j89d8rwn
https://coub.com/view/aa3k89ktsk
https://coub.com/view/lf0d5hsld6
https://coub.com/view/gu5537brfz
https://coub.com/view/6uio4ilcrf
https://coub.com/view/ms6kce573a
https://coub.com/view/0lbw9ytytc
https://coub.com/view/gxlgdqofxz
https://coub.com/view/3pln3aqq2k

收起阅读 »

滚动与滑动组件实现吸顶效果

问题现象
在HarmonyOS中,如何使用不同的滚动与滑动组件,实现标题吸顶效果?

背景知识
吸顶效果是网页开发中的一种常见交互设计,指当用户滚动页面时,某个元素(如导航栏、标题栏、工具栏等)会固定在浏览器窗口的顶部(或其他指定位置),保持始终可见,不会随着页面滚动而消失。在开发过程中,吸顶效果通常用于需要保持关键元素始终可见的场景,以提升用户体验和操作效率。

在HarmonyOS中有多种实现吸顶效果的方案,以及不同的吸顶效果,不同方案及其需要了解的知识如下:

Tabs:通过页签进行内容视图切换的容器组件,每个页签对应一个内容视图。
nestedScroll:设置前后两个方向的嵌套滚动模式,实现与父组件的滚动联动。
List:列表包含一系列相同宽度的列表项。适合连续、多行呈现同类数据,例如图片和文本。
sticky:配合ListItemGroup组件使用,设置ListItemGroup中header是否要吸顶或footer是否要吸底。sticky属性可以设置为StickyStyle.Header|StickyStyle.Footer以同时支持header吸顶和footer吸底。
解决方案
本文主要介绍实现普通吸顶效果、单标题滚动嵌套吸顶效果和多标题滚动嵌套吸顶效果的方案。

普通吸顶效果
方案一:通过Tabs组件的tabBar属性实现吸顶效果。可以自定义实现Tabs效果。
示例代码如下:
@Entry
@Component
export struct CommonSolutionOne {
subsController: TabsController = new TabsController();
@State arr: string[] = ['1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11', '12', '13', '14', '15'];

build() {
Tabs({ barPosition: BarPosition.Start, controller: this.subsController }) {
TabContent() {
List({ space: 10 }) {
ForEach(this.arr, (item: number) => {
ListItem() {
Row() {
Text('item' + item)
.fontSize(16)
.height(72)
.fontColor(Color.Black);
}
.width('100%')
.height(72)
.justifyContent(FlexAlign.Center);
}
.borderRadius(15)
.backgroundColor('#F1F3F5');
}, (item: string) => item);
}
.expandSafeArea([SafeAreaType.SYSTEM], [SafeAreaEdge.BOTTOM])
.padding({ left: 10, right: 10 })
.width('100%')
.height('100%')
.scrollBar(BarState.Off);
}
.expandSafeArea([SafeAreaType.SYSTEM], [SafeAreaEdge.BOTTOM])
.tabBar('关注')
.backgroundColor('#ffffff');

  TabContent() {  
    Text('推荐');  
  }  
  .expandSafeArea([SafeAreaType.SYSTEM], [SafeAreaEdge.BOTTOM])  
  .tabBar('推荐')  
  .backgroundColor('#ffffff');  
}  
.expandSafeArea([SafeAreaType.SYSTEM], [SafeAreaEdge.BOTTOM])  
.backgroundColor('#ffffff');  

}
}
实现效果如下:

点击放大

方案二:自定义标题的吸顶实现。若要实现Tabs页面切换效果,需要自定义页面切换逻辑。
参考上文中方案一,示例代码如下:
@Entry
@Component
export struct CommonSolutionTwo {
subsController: TabsController = new TabsController();
@State arr: string[] = ['1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11', '12', '13', '14', '15'];

build() {
Column() {
Row() {
// 吸顶标题
Text('自定义占位标题')
.width('100%')
.height(60)
.textAlign(TextAlign.Center)
.backgroundColor('#66666666');
};

  List({ space: 10 }) {  
    ForEach(this.arr, (item: number) => {  
      ListItem() {  
        Row() {  
          Text('item' + item)  
            .fontSize(16)  
            .height(72)  
            .fontColor(Color.Black);  
        }  
        .width('100%')  
        .height(72)  
        .justifyContent(FlexAlign.Center);  
      }  
      .borderRadius(15)  
      .backgroundColor('#F1F3F5');  
    }, (item: string) => item);  
  }  
  .expandSafeArea([SafeAreaType.SYSTEM], [SafeAreaEdge.BOTTOM])  
  .padding({ left: 10, right: 10 })  
  .width('100%')  
  .scrollBar(BarState.Off)  
  .layoutWeight(1); // 更换List组件height属性,高度限制以layoutWeight(1)的形式自动填充父组件高度  
}  
.expandSafeArea([SafeAreaType.SYSTEM], [SafeAreaEdge.BOTTOM])  
.width('100%')  
.height('100%') // 普通吸顶效果时,滚动组件的父组件可以不设置高度,但是滚动嵌套吸顶时一定要设置高度限制为100%  
.backgroundColor('#ffffff');  

}
}
实现效果如下:

点击放大

单标题滚动嵌套吸顶效果
方案一: 通过nestedScroll属性,实现吸顶效果。由于nestedScroll属性是滚动与滑动组件的通用属性,理论上在滚动组件嵌套的过程中,支持该属性的滚动组件都可以实现吸顶效果。以Tabs组件为例,示例代码如下:
@Entry
@Component
struct SingleTitleSolutionOne {
subsController: TabsController = new TabsController();
@State arr: string[] = ['1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11', '12', '13', '14', '15'];

build() {
Scroll() {
Column() {
Column() {
Text('自定义占位标题')
.fontSize(30)
.width('100%')
.height(200)
.backgroundColor('#66666666')
.textAlign(TextAlign.Center);
}
.expandSafeArea([SafeAreaType.SYSTEM], [SafeAreaEdge.BOTTOM])

    Tabs({ barPosition: BarPosition.Start, controller: this.subsController }) {  
      TabContent() {  
        List({ space: 10 }) {  
          ForEach(this.arr, (item: number) => {  
            ListItem() {  
              Row() {  
                Text('item' + item)  
                  .fontSize(16)  
                  .height(72)  
                  .fontColor(Color.Black);  
              }  
              .expandSafeArea([SafeAreaType.SYSTEM], [SafeAreaEdge.BOTTOM])  
              .width('100%')  
              .height(72)  
              .justifyContent(FlexAlign.Center);  
            }  
            .expandSafeArea([SafeAreaType.SYSTEM], [SafeAreaEdge.BOTTOM])  
            .borderRadius(15)  
            .backgroundColor('#F1F3F5');  
          }, (item: string) => item);  
        }  
        .expandSafeArea([SafeAreaType.SYSTEM], [SafeAreaEdge.BOTTOM])  
        .padding({ left: 10, right: 10 })  
        .width('100%')  
        .height('100%')  
        .scrollBar(BarState.Off)  
        .nestedScroll({  
          scrollForward: NestedScrollMode.PARENT_FIRST, // 向上滚动PARENT_FIRST:父组件先滚动,父组件滚动到边缘以后自身滚动。  
          scrollBackward: NestedScrollMode.SELF_FIRST // 向下滚动SELF_FIRST:自身先滚动,自身滚动到边缘以后父组件滚动。  
        });  
      }  
      .clip(false)  
      .expandSafeArea([SafeAreaType.SYSTEM], [SafeAreaEdge.BOTTOM])  
      .tabBar('关注')  
      .backgroundColor('#ffffff');  

      TabContent() {  
        Text('推荐');  
      }  
      .expandSafeArea([SafeAreaType.SYSTEM], [SafeAreaEdge.BOTTOM])  
      .tabBar('推荐')  
      .backgroundColor('#ffffff');  
    }  
    .clip(false)  
    .expandSafeArea([SafeAreaType.SYSTEM], [SafeAreaEdge.BOTTOM])  
    .backgroundColor('#ffffff');  
  }  
  .expandSafeArea([SafeAreaType.SYSTEM], [SafeAreaEdge.BOTTOM])  
  .width('100%');  
}  
.clip(false)  
.expandSafeArea([SafeAreaType.SYSTEM], [SafeAreaEdge.BOTTOM])  
.edgeEffect(EdgeEffect.Spring)  
.friction(0.6)  
.backgroundColor('#ffffff')  
.scrollBar(BarState.Off)  
.width('100%')  
.height('100%');  

}
}
效果预览:

点击放大

说明
自定义标题的实现方式,此处主要讲解滚动组件嵌套实现方式。若是想要以自定义标题实现滚动嵌套吸顶效果,滚动组件的父组件一定要设置高度限制为100%,若不设置高度限制,父组件自适应高度会导致吸顶失效,可详见下文中“多标题滚动嵌套吸顶效果”的方案一,其内第一个标题为自定义标题,及其父组件高度注释。

方案二: 由于Waterflow布局本身不支持吸顶,如需使用Waterflow布局实现吸顶效果,可以在滚动组件上通过Stack叠加一个组件实现,示例代码如下:
// 瀑布流布局项数据模型(用于描述每个子项的布局属性)
export class WaterFlowItemModel {
fullWidth: boolean = false;
stickyTop: number | undefined = undefined;
height: number = 0;
width: number = 0;
bgColor: string = '#f0f0f0';
}

// 瀑布流数据源实现类
export class WaterFlowDataSource implements IDataSource {
private dataArray: WaterFlowItemModel[] = [];
private listeners: DataChangeListener[] = [];

constructor() {
this.dataArray = [];
// 生成10个瀑布流测试数据项
for (let i = 0; i < 10; i++) {
const model = new WaterFlowItemModel();
model.fullWidth = i % 5 === 4;
model.height = 120;
model.bgColor = '#f0f0f0';
this.dataArray.push(model);
}
}

// 获取数据总量
totalCount(): number {
return this.dataArray.length;
}

// 根据索引获取单个数据项
getData(index: number) {
return this.dataArray[index];
}

// 注册数据变更监听器
registerDataChangeListener(listener: DataChangeListener) {
if (this.listeners.indexOf(listener) < 0) {
this.listeners.push(listener);
}
}

// 移除已注册的数据变更监听器
unregisterDataChangeListener(listener: DataChangeListener) {
const pos = this.listeners.indexOf(listener);
if (pos >= 0) {
this.listeners.splice(pos, 1);
}
}
}

@Entry
@Component
struct SingleTitleSolutionTwo {
scroller: Scroller = new Scroller();
datasource: WaterFlowDataSource = new WaterFlowDataSource();
@State scrollOffset: number = 0;

build() {
Stack({ alignContent: Alignment.TopStart }) {
WaterFlow({ scroller: this.scroller }) {
LazyForEach(this.datasource, (item: WaterFlowItemModel, index) => {
FlowItem() {
// 当索引不为1,展示当前索引值
if (index !== 1) {
Text(${index});
}
}
.expandSafeArea([SafeAreaType.SYSTEM], [SafeAreaEdge.BOTTOM])
.margin({ right:20,bottom:12,left:19 })
.borderRadius(15)
.onClick(() => {
console.info('onClick ---- ', index);
})
.width('90%')
.height(item.height)
// .backgroundColor(index === 1 ? '#00000000' : Color.White);
.backgroundColor('#F1F3F5')
}, (key: string) => key);
}
.expandSafeArea([SafeAreaType.SYSTEM], [SafeAreaEdge.BOTTOM])
.edgeEffect(EdgeEffect.Spring)
.onWillScroll((offset: number) => {
this.scrollOffset = this.scroller.currentOffset().yOffset + offset;
})
.width('100%')
.height('100%')
.backgroundColor('#ffffff');

  // 在索引为1的WaterFlowItem上方叠加一个组件  
  Stack() {  
    Text('自定义占位标题')  
      .width('100%')  
      .height(this.datasource.getData(1).height)  
      .fontColor(Color.White)  
      .backgroundColor('#66666666')  
      .fontSize(17)  
      .textAlign(TextAlign.Center);  
  }  
  .expandSafeArea([SafeAreaType.SYSTEM], [SafeAreaEdge.BOTTOM])  
  .hitTestBehavior(HitTestMode.Transparent)  
  .position({ x: 0, y: this.scrollOffset >= 120 ? 0 : 120 - this.scrollOffset });  

}  
.expandSafeArea([SafeAreaType.SYSTEM], [SafeAreaEdge.BOTTOM])  
.width('100%')  
.height('100%')  
.clip(true);  

}
}
实现效果如下:

点击放大

多标题滚动嵌套吸顶效果
方案一:通过nestedScroll属性对滚动组件多次滚动嵌套,参考单标题滚动嵌套吸顶效果,示例代码如下:
@Entry
@Component
struct MultipleTitleSolutionOne {
subsController: TabsController = new TabsController();
@State arr: string[] = ['1', '2', '3'];
@State arr1: string[] = ['1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11', '12', '13', '14', '15'];

build() {
Scroll() {
Column() {
Column() {
Text('自定义占位标题')
.fontSize(30)
.width('100%')
.height(200)
.backgroundColor('#66666666')
.textAlign(TextAlign.Center);
};

    Column() {  
      Row() {  
        Text('自定义占位标题') // 自定义实现标题吸顶,依旧采用Tabs组件也适用  
          .fontSize(16)  
          .height(60)  
          .width('100%')  
          .textAlign(TextAlign.Center);  
      }.backgroundColor('#6dbab8b8')  

      Scroll() {  
        Column() {  
          ForEach(this.arr, (item: number) => {  
            Row() {  
              Text('item' + item)  
                .fontSize(16)  
                .height(72)  
                .fontColor(Color.Black);  
            }  
            .width('100%')  
            .height(72)  
            .borderRadius(15)  
            .margin({  
              bottom: 10  
            })  
            .justifyContent(FlexAlign.Center);  
          }, (item: string) => item);  

          Tabs({ barPosition: BarPosition.Start, controller: this.subsController }) {  
            TabContent() {  
              List({ space: 10 }) {  
                ForEach(this.arr1, (item: number) => {  
                  ListItem() {  
                    Row() {  
                      Text('item' + item)  
                        .fontSize(16)  
                        .height(72)  
                        .fontColor(Color.Black);  
                    }  
                    .width('100%')  
                    .height(72)  
                    .justifyContent(FlexAlign.Center);  
                  }  
                  .expandSafeArea([SafeAreaType.SYSTEM], [SafeAreaEdge.BOTTOM])  
                  .borderRadius(15)  
                  .backgroundColor(Color.White);  
                }, (item: string) => item);  
              }  
              .expandSafeArea([SafeAreaType.SYSTEM], [SafeAreaEdge.BOTTOM])  
              .padding({ left: 10, right: 10 })  
              .width('100%')  
              .height('100%')  
              .scrollBar(BarState.Off)  
              .nestedScroll({  
                scrollForward: NestedScrollMode.PARENT_FIRST, // 向上滚动PARENT_FIRST:父组件先滚动,父组件滚动到边缘以后自身滚动。  
                scrollBackward: NestedScrollMode.SELF_FIRST // 向下滚动SELF_FIRST:自身先滚动,自身滚动到边缘以后父组件滚动。  
              });  
            }  
            .expandSafeArea([SafeAreaType.SYSTEM], [SafeAreaEdge.BOTTOM])  
            .tabBar('关注')  
            .backgroundColor('#F1F3F5');  

            TabContent() {  
              Text('推荐');  
            }  
            .expandSafeArea([SafeAreaType.SYSTEM], [SafeAreaEdge.BOTTOM])  
            .tabBar('推荐')  
            .backgroundColor('#F1F3F5');  
          }  
          .expandSafeArea([SafeAreaType.SYSTEM], [SafeAreaEdge.BOTTOM])  
          .backgroundColor('#6dbab8b8');  
        }  
        .expandSafeArea([SafeAreaType.SYSTEM], [SafeAreaEdge.BOTTOM])  
        .backgroundColor(Color.White)  
        .width('100%');  
      }  
      .expandSafeArea([SafeAreaType.SYSTEM], [SafeAreaEdge.BOTTOM])  
      .width('100%')  
      .layoutWeight(1)  
      .scrollBar(BarState.Off)  
      .nestedScroll({  
        scrollForward: NestedScrollMode.PARENT_FIRST, // 向上滚动PARENT_FIRST:父组件先滚动,父组件滚动到边缘以后自身滚动。  
        scrollBackward: NestedScrollMode.SELF_FIRST // 向下滚动SELF_FIRST:自身先滚动,自身滚动到边缘以后父组件滚动。  
      });  
    }  
    .expandSafeArea([SafeAreaType.SYSTEM], [SafeAreaEdge.BOTTOM])  
    .height('100%') // 由于是自定义的标题,所以容纳标题和列表的父组件一定要设置高度为100%  
    .backgroundColor('#6dbab8b8');  
  }  
  .expandSafeArea([SafeAreaType.SYSTEM], [SafeAreaEdge.BOTTOM])  
  .width('100%');  
}  
.expandSafeArea([SafeAreaType.SYSTEM], [SafeAreaEdge.BOTTOM])  
.edgeEffect(EdgeEffect.Spring)  
.friction(0.6)  
.backgroundColor(Color.White)  
.scrollBar(BarState.Off)  
.width('100%')  
.height('100%');  

}
}
实现效果如下:

点击放大

方案二:通过List组件的sticky属性,实现吸顶效果。
常见场景一:header吸顶和footer吸底,该方案仅适用于List组件,具体实施方案参考官方示例:吸顶/吸底。

常见场景二:多标题多tab菜单吸顶时,实现滑动时自动切换tab栏。通过onDidScroll方法,监听滚动事件,根据滚动偏移量动态计算当前处于哪个区域,再根据当前区块切换标题内容。示例代码如下:

@Entry
@Component
struct MultipleTitleSolutionOnePlus {
@State data1: string[] = ['标题1', '标题2', '标题3', '标题4'];
@State data2: string[] = ['1', '2', '3', '4'];
@State selectIndex1: number = 0;
private listController: ListScroller = new ListScroller();

@Builder
topBuilder1() {
Row() {
ForEach(this.data1, (item: string, index: number) => {
Text(item)
.fontColor(index == this.selectIndex1 ? Color.Blue : '#000')
.fontSize(index == this.selectIndex1 ? 20 : 16)
.fontWeight(index == this.selectIndex1 ? 500 : 400)
.onClick(() => {
this.listController.scrollToItemInGroup(0, index, true);
});
});
}
.backgroundColor(0xAABBCC)
.height(60)
.width('100%')
.justifyContent(FlexAlign.SpaceAround);
}

@Builder
topBuilder2() {
Row() {
Text('第二组吸顶')
.padding(5)
.textAlign(TextAlign.Center);
}
.backgroundColor(0xAABBCC)
.justifyContent(FlexAlign.SpaceAround)
.height(60)
.width('100%');
}

build() {
Column() {
List({ space: 10, scroller: this.listController }) {
ListItemGroup({ header: this.topBuilder1, space: 10 }) {
ForEach(this.data1, (item: string) => {
ListItem() {
Text(item)
.width('90%')
.height(200)
.backgroundColor('#f1f3f5')
.borderRadius(10)
.textAlign(TextAlign.Center);
};
});
};

    ListItemGroup() {  
      ListItem() {  
        Text('其他内容')  
          .width('90%')  
          .height(400)  
          .backgroundColor('#f1f3f5')  
          .borderRadius(10)  
          .fontSize(16)  
          .textAlign(TextAlign.Center);  
      };  
    };  

    ListItemGroup({ header: this.topBuilder2, space: 10 }) {  
      ForEach(this.data2, (item: string) => {  
        ListItem() {  
          Text(item)  
            .width('90%')  
            .height(200)  
            .backgroundColor('#f1f3f5')  
            .borderRadius(10)  
            .textAlign(TextAlign.Center);  
        };  
      });  
    };  
  }  
  .expandSafeArea([SafeAreaType.SYSTEM], [SafeAreaEdge.BOTTOM])  
  .onDidScroll(() => {  
    let currentOffset = this.listController.currentOffset().yOffset;  
    this.selectIndex1 = Math.floor(currentOffset / 200);  
  })  
  .scrollBar(BarState.Off)  
  .sticky(StickyStyle.Header)  
  .alignListItem(ListItemAlign.Center);  
};  

}
}
实现效果如下:

点击放大

二级标题滚动嵌套吸顶效果
第一层吸顶用Scroll嵌套List实现,第二层是利用ListItemGroup的sticky属性实现,示例代码如下:

@Entry
@Component
struct MultipleTitleSolutionTwo {
private timeTable: TimeTable[] = [
{
title: 'Header2-0',
projects: ['内容1']
},
{
title: 'Header2-1',
projects: ['内容2']
},
{
title: 'Header2-2',
projects: ['内容3']
}
];

@Builder
itemHead(text: string, index: number) {
if (index === 0) {
Text().height(0);
} else {
Text(text)
.width('100%')
.textAlign(TextAlign.Center)
.fontColor(Color.Black)
.height(index === 1 ? 100 : 200)
.backgroundColor('#d1d1d6');
}
}

build() {
Scroll() {
Column() {
Text('自定义占位标题')
.width('100%')
.height(200)
.backgroundColor('#66666666')
.textAlign(TextAlign.Center);
Column() {
// 第一层吸顶效果主要是通过设置nestedScroll属性以及父组件设置高度100%实现
Text('Header1')
.width('100%')
.height(100)
.backgroundColor('#4d666666')
.textAlign(TextAlign.Center)
.fontColor(Color.Black);
List() {
ForEach(this.timeTable, (item: TimeTable, index) => {
ListItemGroup({ header: this.itemHead(item.title, index) }) {
ForEach(item.projects, (project: string) => {
ListItem() {
Text(project)
.width('100%')
.height(800)
.fontSize(20)
.textAlign(TextAlign.Center)
.backgroundColor('#FFFFFF');
}
}, (item: string) => item);
}.clip(false);
});
}
.expandSafeArea([SafeAreaType.SYSTEM], [SafeAreaEdge.BOTTOM])
.sticky(StickyStyle.Header)
.scrollBar(BarState.Off)
.width('100%')
.height('calc(100% - 100vp)')
.backgroundColor('#ffffff')
.edgeEffect(EdgeEffect.Spring)
.nestedScroll({
scrollForward: NestedScrollMode.PARENT_FIRST,
scrollBackward: NestedScrollMode.SELF_FIRST
});
}.clip(false).expandSafeArea([SafeAreaType.SYSTEM], [SafeAreaEdge.BOTTOM]).height('100%')
}.expandSafeArea([SafeAreaType.SYSTEM], [SafeAreaEdge.BOTTOM])
}
.expandSafeArea([SafeAreaType.SYSTEM], [SafeAreaEdge.BOTTOM])
.clip(false)
.edgeEffect(EdgeEffect.Spring)
.friction(0.6)
.backgroundColor(Color.White)
.scrollBar(BarState.Off)
.width('100%')
.height('100%');
}
}

interface TimeTable {
title: string;
projects: string[];
}
实现效果如下:

点击放大

动态控制二级标题滚动嵌套吸顶效果:
在List组件onDidScroll方法里判断索引为2的组件滚动偏移量,动态调整Header1的position属性和List组件的contentStartOffset属性,示例代码如下:

@Entry
@Component
struct MultipleTitleSolutionTwoDynamic {
@State header1Position: number = 0;
@State listStartOffset: number = 100;
private scroller = new Scroller();
private timeTable: TimeTableDynamic[] = [
{
title: 'Header2-0',
projects: ['内容1']
},
{
title: 'Header2-1',
projects: ['内容2']
},
{
title: 'Header2-2',
projects: ['内容3']
}
];

@Builder
itemHead(text: string, index: number) {
if (index === 0) {
Text().height(0);
} else {
Text(text)
.width('100%')
.textAlign(TextAlign.Center)
.fontColor(Color.Black)
.height(index === 1 ? 100 : 200)
.backgroundColor('#d1d1d6');
}
}

build() {
Scroll() {
Column() {
Text('二级标题滚动嵌套吸顶')
.width('100%')
.height(200)
.backgroundColor('#66666666')
.textAlign(TextAlign.Center);
Column() {
// 第一层吸顶效果主要是通过设置nestedScroll属性以及父组件设置高度100%实现
Text('Header1')
.width('100%')
.height(100)
.backgroundColor('#4d666666')
.textAlign(TextAlign.Center)
.fontColor(Color.Black)
.position({ y: this.header1Position })
.zIndex(10);
List({ scroller: this.scroller }) {
ForEach(this.timeTable, (item: TimeTableDynamic, index) => {
ListItemGroup({ header: this.itemHead(item.title, index) }) {
ForEach(item.projects, (project: string) => {
ListItem() {
Text(project)
.width('100%')
.height(800)
.fontSize(20)
.textAlign(TextAlign.Center)
.backgroundColor('#ffffff');
}.expandSafeArea([SafeAreaType.SYSTEM], [SafeAreaEdge.BOTTOM]);
}, (item: string) => item);
};
});
}
.expandSafeArea([SafeAreaType.SYSTEM], [SafeAreaEdge.BOTTOM])
.sticky(StickyStyle.Header)
.scrollBar(BarState.Off)
.width('100%')
.height('100%')
.backgroundColor('#ffffff')
.edgeEffect(EdgeEffect.Spring)
.contentStartOffset(this.listStartOffset)
.nestedScroll({
scrollForward: NestedScrollMode.PARENT_FIRST,
scrollBackward: NestedScrollMode.SELF_FIRST
})
.onDidScroll(() => {
let secondRect = this.scroller.getItemRect(2);
if (secondRect.x === 0 && secondRect.y === 0 && secondRect.width === 0 && secondRect.height === 0) {
console.info(索引为${2}的组件不在屏幕上);
return;
}
this.header1Position = Math.min(0, secondRect.y - 200);
this.listStartOffset = Math.max(0, 100 + this.header1Position);
});
}.height('100%');
};
}.expandSafeArea([SafeAreaType.SYSTEM], [SafeAreaEdge.BOTTOM])
.clip(false)
.edgeEffect(EdgeEffect.Spring)
.friction(0.6)
.backgroundColor(Color.White)
.scrollBar(BarState.Off)
.width('100%')
.height('100%');
}
}

interface TimeTableDynamic {
title: string;
projects: string[];
}
https://coub.com/view/gylbvhqrkz
https://coub.com/view/gqrdnsqbsm
https://coub.com/view/0a3thuu2b4
https://coub.com/view/lg6lw08ivp
https://coub.com/view/qzxlw9ogx1
https://coub.com/view/68z9behrh7
https://coub.com/view/4iijkgxtaa
https://coub.com/view/xuelqryeob
https://coub.com/view/037iwxjpf9
https://coub.com/view/ydu5rfse9r
https://coub.com/view/g71sfv35n0
https://coub.com/view/hmkl7icf96
https://coub.com/view/7gf8jpjeqf
https://coub.com/view/6ar7vzg3jt
https://coub.com/view/x6brb1zhst
https://coub.com/view/esc2tpa50i
https://coub.com/view/ij4i54b4su
https://coub.com/view/6csj522lvg
https://coub.com/view/oaspyursw3
https://coub.com/view/gvem0zleaj
https://coub.com/view/xs8g570e03
https://coub.com/view/suwbkm015s
https://coub.com/view/74yubqnb80
https://coub.com/view/6wh2qfuaws
https://coub.com/view/2yv1gta5d2
https://coub.com/view/hopm4eqo5c
https://coub.com/view/q1tm3sdodd
https://coub.com/view/7udyb7fyhi
https://coub.com/view/1z3anr32zq
https://coub.com/view/jhh5mhj3y0
https://coub.com/view/dvq9c4eyfc
https://coub.com/view/wv83dp9qeo
https://coub.com/view/a9i5tkfl9w
https://coub.com/view/v372pqbiis
https://coub.com/view/ig9545no10
https://coub.com/view/w79ltp6ou9
https://coub.com/view/u9s0qk1akf
https://coub.com/view/damsu4njzt
https://coub.com/view/v2vriht1bd
https://coub.com/view/e93br3m9qa
https://coub.com/view/xd56t399ne
https://coub.com/view/dnvfpx6l5b
https://coub.com/view/pehxieghx6
https://coub.com/view/6w14zzs972
https://coub.com/view/46b628kwgi
https://coub.com/view/o5bg5eawhx
https://coub.com/view/rrinp7vw3c
https://coub.com/view/mamo602b5a
https://coub.com/view/hlvm2nw499
https://coub.com/view/jzu48fqzw5
https://coub.com/view/ncc5pejn2y
https://coub.com/view/o61yarijz2
https://coub.com/view/xt572wfwwl
https://coub.com/view/t4z6lnd7n8
https://coub.com/view/cxz5p1rtil
https://coub.com/view/a630gf5urg
https://coub.com/view/7khrln5czp
https://coub.com/view/u52ikkf9j8
https://coub.com/view/6ispqsd85w
https://coub.com/view/g5ndpkf32e
https://coub.com/view/nf11v6hl00
https://coub.com/view/g2va9aj5ue
https://coub.com/view/qe1rsfh7ey
https://coub.com/view/ldvx0v2kr2
https://coub.com/view/w3bct13r9i
https://coub.com/view/3zyw0cjxo1
https://coub.com/view/ny9foponj5
https://coub.com/view/g4q545h3da
https://coub.com/view/kekg8y04v7
https://coub.com/view/ms20cjnzki
https://coub.com/view/6hg6xx4f3u
https://coub.com/view/4gd6mjpi41
https://coub.com/view/91selywfey
https://coub.com/view/d9lltiovbh
https://coub.com/view/j6dhi1cqlu
https://coub.com/view/pupns1qvyy
https://coub.com/view/42eqvna2sj
https://coub.com/view/213yl91c0r
https://coub.com/view/ligxe8niat
https://sites.google.com/view/4npswg73/home
https://sites.google.com/view/1cqggg42/home
https://sites.google.com/view/7hwthv95/home
https://sites.google.com/view/8ftpag65/home
https://sites.google.com/view/9hupnv16/home
https://sites.google.com/view/9rcwcc44/home
https://sites.google.com/view/4ygxfw66/home
https://sites.google.com/view/7yjaoo71/home
https://sites.google.com/view/6euxnz02/home
https://sites.google.com/view/2xrcwv82/home
https://sites.google.com/view/3bruzf22/home
https://sites.google.com/view/1fampb85/home
https://sites.google.com/view/0uqjgm22/home
https://sites.google.com/view/8lytyx96/home
https://sites.google.com/view/2rcvcx10/home
https://sites.google.com/view/0wxard26/home
https://sites.google.com/view/0rhcrm43/home
https://sites.google.com/view/1cilam64/home
https://sites.google.com/view/2dsnqw73/home
https://sites.google.com/view/0vgsrq46/home
https://sites.google.com/view/8nqpzw07/home
https://sites.google.com/view/2zlysr71/home
https://sites.google.com/view/6krwym27/home
https://sites.google.com/view/6xuung21/home
https://sites.google.com/view/5irzjv73/home
https://sites.google.com/view/7lukby92/home
https://sites.google.com/view/3oyfhr95/home
https://sites.google.com/view/2srcwe43/home
https://sites.google.com/view/0setkv89/home
https://sites.google.com/view/5rvxgj88/home
https://sites.google.com/view/7tfahr42/home
https://sites.google.com/view/7yboep62/home
https://sites.google.com/view/4upmsh64/home
https://sites.google.com/view/0zwrrv97/home
https://sites.google.com/view/2gawzu13/home
https://sites.google.com/view/6qeyzl51/home
https://sites.google.com/view/3ievss43/home
https://sites.google.com/view/0ibpqm02/home
https://sites.google.com/view/3awrap71/home
https://sites.google.com/view/0tvqkg18/home
https://sites.google.com/view/5sbyjy90/home
https://sites.google.com/view/6ehwco13/home
https://sites.google.com/view/7tjzdg23/home
https://sites.google.com/view/6rakcq50/home
https://sites.google.com/view/7hexol86/home
https://sites.google.com/view/0cyonp67/home
https://sites.google.com/view/7cpmvf18/home
https://sites.google.com/view/7qroxh49/home
https://sites.google.com/view/2qshnp57/home
https://sites.google.com/view/6dwyim71/home
https://sites.google.com/view/4cegdc43/home
https://sites.google.com/view/9kbwox44/home
https://sites.google.com/view/6sciwd21/home
https://sites.google.com/view/6bjrsb10/home
https://sites.google.com/view/3qxmma28/home
https://sites.google.com/view/7cbftv08/home
https://sites.google.com/view/4kkccj88/home
https://sites.google.com/view/5pjonl99/home
https://sites.google.com/view/4aeacr58/home
https://sites.google.com/view/6fiuxa97/home
https://sites.google.com/view/5brhgy37/home
https://sites.google.com/view/1mzbfo35/home
https://sites.google.com/view/9kbckv34/home
https://sites.google.com/view/9cobir53/home
https://sites.google.com/view/6qgbcm33/home
https://sites.google.com/view/8lgcnx33/home
https://sites.google.com/view/9jzmzc49/home
https://sites.google.com/view/0bujrj34/home
https://sites.google.com/view/1heuxf15/home
https://sites.google.com/view/0przrr24/home
https://sites.google.com/view/8gmodp38/home
https://sites.google.com/view/1qgplx06/home
https://sites.google.com/view/1vbfom31/home
https://sites.google.com/view/4fgfqr84/home
https://sites.google.com/view/6oybbx22/home
https://sites.google.com/view/1knpbg15/home
https://sites.google.com/view/0opgke85/home
https://sites.google.com/view/7kirma83/home
https://sites.google.com/view/1zzbdj74/home
https://sites.google.com/view/8csmsa36/home
https://sites.google.com/view/1edswe69/home
https://sites.google.com/view/8kyxaz85/home
https://sites.google.com/view/8bplrw32/home
https://sites.google.com/view/7pdmfv03/home
https://sites.google.com/view/8qkoih30/home
https://sites.google.com/view/4noxpv38/home
https://sites.google.com/view/4itlbr61/home
https://sites.google.com/view/5rjefu09/home
https://sites.google.com/view/5ujnxe58/home
https://sites.google.com/view/3gqlqd93/home
https://sites.google.com/view/4ijybb89/home
https://sites.google.com/view/6ipevr32/home
https://sites.google.com/view/5mhxux08/home
https://sites.google.com/view/8wojcb88/home
https://sites.google.com/view/0riyrm78/home

继续阅读 »

问题现象
在HarmonyOS中,如何使用不同的滚动与滑动组件,实现标题吸顶效果?

背景知识
吸顶效果是网页开发中的一种常见交互设计,指当用户滚动页面时,某个元素(如导航栏、标题栏、工具栏等)会固定在浏览器窗口的顶部(或其他指定位置),保持始终可见,不会随着页面滚动而消失。在开发过程中,吸顶效果通常用于需要保持关键元素始终可见的场景,以提升用户体验和操作效率。

在HarmonyOS中有多种实现吸顶效果的方案,以及不同的吸顶效果,不同方案及其需要了解的知识如下:

Tabs:通过页签进行内容视图切换的容器组件,每个页签对应一个内容视图。
nestedScroll:设置前后两个方向的嵌套滚动模式,实现与父组件的滚动联动。
List:列表包含一系列相同宽度的列表项。适合连续、多行呈现同类数据,例如图片和文本。
sticky:配合ListItemGroup组件使用,设置ListItemGroup中header是否要吸顶或footer是否要吸底。sticky属性可以设置为StickyStyle.Header|StickyStyle.Footer以同时支持header吸顶和footer吸底。
解决方案
本文主要介绍实现普通吸顶效果、单标题滚动嵌套吸顶效果和多标题滚动嵌套吸顶效果的方案。

普通吸顶效果
方案一:通过Tabs组件的tabBar属性实现吸顶效果。可以自定义实现Tabs效果。
示例代码如下:
@Entry
@Component
export struct CommonSolutionOne {
subsController: TabsController = new TabsController();
@State arr: string[] = ['1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11', '12', '13', '14', '15'];

build() {
Tabs({ barPosition: BarPosition.Start, controller: this.subsController }) {
TabContent() {
List({ space: 10 }) {
ForEach(this.arr, (item: number) => {
ListItem() {
Row() {
Text('item' + item)
.fontSize(16)
.height(72)
.fontColor(Color.Black);
}
.width('100%')
.height(72)
.justifyContent(FlexAlign.Center);
}
.borderRadius(15)
.backgroundColor('#F1F3F5');
}, (item: string) => item);
}
.expandSafeArea([SafeAreaType.SYSTEM], [SafeAreaEdge.BOTTOM])
.padding({ left: 10, right: 10 })
.width('100%')
.height('100%')
.scrollBar(BarState.Off);
}
.expandSafeArea([SafeAreaType.SYSTEM], [SafeAreaEdge.BOTTOM])
.tabBar('关注')
.backgroundColor('#ffffff');

  TabContent() {  
    Text('推荐');  
  }  
  .expandSafeArea([SafeAreaType.SYSTEM], [SafeAreaEdge.BOTTOM])  
  .tabBar('推荐')  
  .backgroundColor('#ffffff');  
}  
.expandSafeArea([SafeAreaType.SYSTEM], [SafeAreaEdge.BOTTOM])  
.backgroundColor('#ffffff');  

}
}
实现效果如下:

点击放大

方案二:自定义标题的吸顶实现。若要实现Tabs页面切换效果,需要自定义页面切换逻辑。
参考上文中方案一,示例代码如下:
@Entry
@Component
export struct CommonSolutionTwo {
subsController: TabsController = new TabsController();
@State arr: string[] = ['1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11', '12', '13', '14', '15'];

build() {
Column() {
Row() {
// 吸顶标题
Text('自定义占位标题')
.width('100%')
.height(60)
.textAlign(TextAlign.Center)
.backgroundColor('#66666666');
};

  List({ space: 10 }) {  
    ForEach(this.arr, (item: number) => {  
      ListItem() {  
        Row() {  
          Text('item' + item)  
            .fontSize(16)  
            .height(72)  
            .fontColor(Color.Black);  
        }  
        .width('100%')  
        .height(72)  
        .justifyContent(FlexAlign.Center);  
      }  
      .borderRadius(15)  
      .backgroundColor('#F1F3F5');  
    }, (item: string) => item);  
  }  
  .expandSafeArea([SafeAreaType.SYSTEM], [SafeAreaEdge.BOTTOM])  
  .padding({ left: 10, right: 10 })  
  .width('100%')  
  .scrollBar(BarState.Off)  
  .layoutWeight(1); // 更换List组件height属性,高度限制以layoutWeight(1)的形式自动填充父组件高度  
}  
.expandSafeArea([SafeAreaType.SYSTEM], [SafeAreaEdge.BOTTOM])  
.width('100%')  
.height('100%') // 普通吸顶效果时,滚动组件的父组件可以不设置高度,但是滚动嵌套吸顶时一定要设置高度限制为100%  
.backgroundColor('#ffffff');  

}
}
实现效果如下:

点击放大

单标题滚动嵌套吸顶效果
方案一: 通过nestedScroll属性,实现吸顶效果。由于nestedScroll属性是滚动与滑动组件的通用属性,理论上在滚动组件嵌套的过程中,支持该属性的滚动组件都可以实现吸顶效果。以Tabs组件为例,示例代码如下:
@Entry
@Component
struct SingleTitleSolutionOne {
subsController: TabsController = new TabsController();
@State arr: string[] = ['1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11', '12', '13', '14', '15'];

build() {
Scroll() {
Column() {
Column() {
Text('自定义占位标题')
.fontSize(30)
.width('100%')
.height(200)
.backgroundColor('#66666666')
.textAlign(TextAlign.Center);
}
.expandSafeArea([SafeAreaType.SYSTEM], [SafeAreaEdge.BOTTOM])

    Tabs({ barPosition: BarPosition.Start, controller: this.subsController }) {  
      TabContent() {  
        List({ space: 10 }) {  
          ForEach(this.arr, (item: number) => {  
            ListItem() {  
              Row() {  
                Text('item' + item)  
                  .fontSize(16)  
                  .height(72)  
                  .fontColor(Color.Black);  
              }  
              .expandSafeArea([SafeAreaType.SYSTEM], [SafeAreaEdge.BOTTOM])  
              .width('100%')  
              .height(72)  
              .justifyContent(FlexAlign.Center);  
            }  
            .expandSafeArea([SafeAreaType.SYSTEM], [SafeAreaEdge.BOTTOM])  
            .borderRadius(15)  
            .backgroundColor('#F1F3F5');  
          }, (item: string) => item);  
        }  
        .expandSafeArea([SafeAreaType.SYSTEM], [SafeAreaEdge.BOTTOM])  
        .padding({ left: 10, right: 10 })  
        .width('100%')  
        .height('100%')  
        .scrollBar(BarState.Off)  
        .nestedScroll({  
          scrollForward: NestedScrollMode.PARENT_FIRST, // 向上滚动PARENT_FIRST:父组件先滚动,父组件滚动到边缘以后自身滚动。  
          scrollBackward: NestedScrollMode.SELF_FIRST // 向下滚动SELF_FIRST:自身先滚动,自身滚动到边缘以后父组件滚动。  
        });  
      }  
      .clip(false)  
      .expandSafeArea([SafeAreaType.SYSTEM], [SafeAreaEdge.BOTTOM])  
      .tabBar('关注')  
      .backgroundColor('#ffffff');  

      TabContent() {  
        Text('推荐');  
      }  
      .expandSafeArea([SafeAreaType.SYSTEM], [SafeAreaEdge.BOTTOM])  
      .tabBar('推荐')  
      .backgroundColor('#ffffff');  
    }  
    .clip(false)  
    .expandSafeArea([SafeAreaType.SYSTEM], [SafeAreaEdge.BOTTOM])  
    .backgroundColor('#ffffff');  
  }  
  .expandSafeArea([SafeAreaType.SYSTEM], [SafeAreaEdge.BOTTOM])  
  .width('100%');  
}  
.clip(false)  
.expandSafeArea([SafeAreaType.SYSTEM], [SafeAreaEdge.BOTTOM])  
.edgeEffect(EdgeEffect.Spring)  
.friction(0.6)  
.backgroundColor('#ffffff')  
.scrollBar(BarState.Off)  
.width('100%')  
.height('100%');  

}
}
效果预览:

点击放大

说明
自定义标题的实现方式,此处主要讲解滚动组件嵌套实现方式。若是想要以自定义标题实现滚动嵌套吸顶效果,滚动组件的父组件一定要设置高度限制为100%,若不设置高度限制,父组件自适应高度会导致吸顶失效,可详见下文中“多标题滚动嵌套吸顶效果”的方案一,其内第一个标题为自定义标题,及其父组件高度注释。

方案二: 由于Waterflow布局本身不支持吸顶,如需使用Waterflow布局实现吸顶效果,可以在滚动组件上通过Stack叠加一个组件实现,示例代码如下:
// 瀑布流布局项数据模型(用于描述每个子项的布局属性)
export class WaterFlowItemModel {
fullWidth: boolean = false;
stickyTop: number | undefined = undefined;
height: number = 0;
width: number = 0;
bgColor: string = '#f0f0f0';
}

// 瀑布流数据源实现类
export class WaterFlowDataSource implements IDataSource {
private dataArray: WaterFlowItemModel[] = [];
private listeners: DataChangeListener[] = [];

constructor() {
this.dataArray = [];
// 生成10个瀑布流测试数据项
for (let i = 0; i < 10; i++) {
const model = new WaterFlowItemModel();
model.fullWidth = i % 5 === 4;
model.height = 120;
model.bgColor = '#f0f0f0';
this.dataArray.push(model);
}
}

// 获取数据总量
totalCount(): number {
return this.dataArray.length;
}

// 根据索引获取单个数据项
getData(index: number) {
return this.dataArray[index];
}

// 注册数据变更监听器
registerDataChangeListener(listener: DataChangeListener) {
if (this.listeners.indexOf(listener) < 0) {
this.listeners.push(listener);
}
}

// 移除已注册的数据变更监听器
unregisterDataChangeListener(listener: DataChangeListener) {
const pos = this.listeners.indexOf(listener);
if (pos >= 0) {
this.listeners.splice(pos, 1);
}
}
}

@Entry
@Component
struct SingleTitleSolutionTwo {
scroller: Scroller = new Scroller();
datasource: WaterFlowDataSource = new WaterFlowDataSource();
@State scrollOffset: number = 0;

build() {
Stack({ alignContent: Alignment.TopStart }) {
WaterFlow({ scroller: this.scroller }) {
LazyForEach(this.datasource, (item: WaterFlowItemModel, index) => {
FlowItem() {
// 当索引不为1,展示当前索引值
if (index !== 1) {
Text(${index});
}
}
.expandSafeArea([SafeAreaType.SYSTEM], [SafeAreaEdge.BOTTOM])
.margin({ right:20,bottom:12,left:19 })
.borderRadius(15)
.onClick(() => {
console.info('onClick ---- ', index);
})
.width('90%')
.height(item.height)
// .backgroundColor(index === 1 ? '#00000000' : Color.White);
.backgroundColor('#F1F3F5')
}, (key: string) => key);
}
.expandSafeArea([SafeAreaType.SYSTEM], [SafeAreaEdge.BOTTOM])
.edgeEffect(EdgeEffect.Spring)
.onWillScroll((offset: number) => {
this.scrollOffset = this.scroller.currentOffset().yOffset + offset;
})
.width('100%')
.height('100%')
.backgroundColor('#ffffff');

  // 在索引为1的WaterFlowItem上方叠加一个组件  
  Stack() {  
    Text('自定义占位标题')  
      .width('100%')  
      .height(this.datasource.getData(1).height)  
      .fontColor(Color.White)  
      .backgroundColor('#66666666')  
      .fontSize(17)  
      .textAlign(TextAlign.Center);  
  }  
  .expandSafeArea([SafeAreaType.SYSTEM], [SafeAreaEdge.BOTTOM])  
  .hitTestBehavior(HitTestMode.Transparent)  
  .position({ x: 0, y: this.scrollOffset >= 120 ? 0 : 120 - this.scrollOffset });  

}  
.expandSafeArea([SafeAreaType.SYSTEM], [SafeAreaEdge.BOTTOM])  
.width('100%')  
.height('100%')  
.clip(true);  

}
}
实现效果如下:

点击放大

多标题滚动嵌套吸顶效果
方案一:通过nestedScroll属性对滚动组件多次滚动嵌套,参考单标题滚动嵌套吸顶效果,示例代码如下:
@Entry
@Component
struct MultipleTitleSolutionOne {
subsController: TabsController = new TabsController();
@State arr: string[] = ['1', '2', '3'];
@State arr1: string[] = ['1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11', '12', '13', '14', '15'];

build() {
Scroll() {
Column() {
Column() {
Text('自定义占位标题')
.fontSize(30)
.width('100%')
.height(200)
.backgroundColor('#66666666')
.textAlign(TextAlign.Center);
};

    Column() {  
      Row() {  
        Text('自定义占位标题') // 自定义实现标题吸顶,依旧采用Tabs组件也适用  
          .fontSize(16)  
          .height(60)  
          .width('100%')  
          .textAlign(TextAlign.Center);  
      }.backgroundColor('#6dbab8b8')  

      Scroll() {  
        Column() {  
          ForEach(this.arr, (item: number) => {  
            Row() {  
              Text('item' + item)  
                .fontSize(16)  
                .height(72)  
                .fontColor(Color.Black);  
            }  
            .width('100%')  
            .height(72)  
            .borderRadius(15)  
            .margin({  
              bottom: 10  
            })  
            .justifyContent(FlexAlign.Center);  
          }, (item: string) => item);  

          Tabs({ barPosition: BarPosition.Start, controller: this.subsController }) {  
            TabContent() {  
              List({ space: 10 }) {  
                ForEach(this.arr1, (item: number) => {  
                  ListItem() {  
                    Row() {  
                      Text('item' + item)  
                        .fontSize(16)  
                        .height(72)  
                        .fontColor(Color.Black);  
                    }  
                    .width('100%')  
                    .height(72)  
                    .justifyContent(FlexAlign.Center);  
                  }  
                  .expandSafeArea([SafeAreaType.SYSTEM], [SafeAreaEdge.BOTTOM])  
                  .borderRadius(15)  
                  .backgroundColor(Color.White);  
                }, (item: string) => item);  
              }  
              .expandSafeArea([SafeAreaType.SYSTEM], [SafeAreaEdge.BOTTOM])  
              .padding({ left: 10, right: 10 })  
              .width('100%')  
              .height('100%')  
              .scrollBar(BarState.Off)  
              .nestedScroll({  
                scrollForward: NestedScrollMode.PARENT_FIRST, // 向上滚动PARENT_FIRST:父组件先滚动,父组件滚动到边缘以后自身滚动。  
                scrollBackward: NestedScrollMode.SELF_FIRST // 向下滚动SELF_FIRST:自身先滚动,自身滚动到边缘以后父组件滚动。  
              });  
            }  
            .expandSafeArea([SafeAreaType.SYSTEM], [SafeAreaEdge.BOTTOM])  
            .tabBar('关注')  
            .backgroundColor('#F1F3F5');  

            TabContent() {  
              Text('推荐');  
            }  
            .expandSafeArea([SafeAreaType.SYSTEM], [SafeAreaEdge.BOTTOM])  
            .tabBar('推荐')  
            .backgroundColor('#F1F3F5');  
          }  
          .expandSafeArea([SafeAreaType.SYSTEM], [SafeAreaEdge.BOTTOM])  
          .backgroundColor('#6dbab8b8');  
        }  
        .expandSafeArea([SafeAreaType.SYSTEM], [SafeAreaEdge.BOTTOM])  
        .backgroundColor(Color.White)  
        .width('100%');  
      }  
      .expandSafeArea([SafeAreaType.SYSTEM], [SafeAreaEdge.BOTTOM])  
      .width('100%')  
      .layoutWeight(1)  
      .scrollBar(BarState.Off)  
      .nestedScroll({  
        scrollForward: NestedScrollMode.PARENT_FIRST, // 向上滚动PARENT_FIRST:父组件先滚动,父组件滚动到边缘以后自身滚动。  
        scrollBackward: NestedScrollMode.SELF_FIRST // 向下滚动SELF_FIRST:自身先滚动,自身滚动到边缘以后父组件滚动。  
      });  
    }  
    .expandSafeArea([SafeAreaType.SYSTEM], [SafeAreaEdge.BOTTOM])  
    .height('100%') // 由于是自定义的标题,所以容纳标题和列表的父组件一定要设置高度为100%  
    .backgroundColor('#6dbab8b8');  
  }  
  .expandSafeArea([SafeAreaType.SYSTEM], [SafeAreaEdge.BOTTOM])  
  .width('100%');  
}  
.expandSafeArea([SafeAreaType.SYSTEM], [SafeAreaEdge.BOTTOM])  
.edgeEffect(EdgeEffect.Spring)  
.friction(0.6)  
.backgroundColor(Color.White)  
.scrollBar(BarState.Off)  
.width('100%')  
.height('100%');  

}
}
实现效果如下:

点击放大

方案二:通过List组件的sticky属性,实现吸顶效果。
常见场景一:header吸顶和footer吸底,该方案仅适用于List组件,具体实施方案参考官方示例:吸顶/吸底。

常见场景二:多标题多tab菜单吸顶时,实现滑动时自动切换tab栏。通过onDidScroll方法,监听滚动事件,根据滚动偏移量动态计算当前处于哪个区域,再根据当前区块切换标题内容。示例代码如下:

@Entry
@Component
struct MultipleTitleSolutionOnePlus {
@State data1: string[] = ['标题1', '标题2', '标题3', '标题4'];
@State data2: string[] = ['1', '2', '3', '4'];
@State selectIndex1: number = 0;
private listController: ListScroller = new ListScroller();

@Builder
topBuilder1() {
Row() {
ForEach(this.data1, (item: string, index: number) => {
Text(item)
.fontColor(index == this.selectIndex1 ? Color.Blue : '#000')
.fontSize(index == this.selectIndex1 ? 20 : 16)
.fontWeight(index == this.selectIndex1 ? 500 : 400)
.onClick(() => {
this.listController.scrollToItemInGroup(0, index, true);
});
});
}
.backgroundColor(0xAABBCC)
.height(60)
.width('100%')
.justifyContent(FlexAlign.SpaceAround);
}

@Builder
topBuilder2() {
Row() {
Text('第二组吸顶')
.padding(5)
.textAlign(TextAlign.Center);
}
.backgroundColor(0xAABBCC)
.justifyContent(FlexAlign.SpaceAround)
.height(60)
.width('100%');
}

build() {
Column() {
List({ space: 10, scroller: this.listController }) {
ListItemGroup({ header: this.topBuilder1, space: 10 }) {
ForEach(this.data1, (item: string) => {
ListItem() {
Text(item)
.width('90%')
.height(200)
.backgroundColor('#f1f3f5')
.borderRadius(10)
.textAlign(TextAlign.Center);
};
});
};

    ListItemGroup() {  
      ListItem() {  
        Text('其他内容')  
          .width('90%')  
          .height(400)  
          .backgroundColor('#f1f3f5')  
          .borderRadius(10)  
          .fontSize(16)  
          .textAlign(TextAlign.Center);  
      };  
    };  

    ListItemGroup({ header: this.topBuilder2, space: 10 }) {  
      ForEach(this.data2, (item: string) => {  
        ListItem() {  
          Text(item)  
            .width('90%')  
            .height(200)  
            .backgroundColor('#f1f3f5')  
            .borderRadius(10)  
            .textAlign(TextAlign.Center);  
        };  
      });  
    };  
  }  
  .expandSafeArea([SafeAreaType.SYSTEM], [SafeAreaEdge.BOTTOM])  
  .onDidScroll(() => {  
    let currentOffset = this.listController.currentOffset().yOffset;  
    this.selectIndex1 = Math.floor(currentOffset / 200);  
  })  
  .scrollBar(BarState.Off)  
  .sticky(StickyStyle.Header)  
  .alignListItem(ListItemAlign.Center);  
};  

}
}
实现效果如下:

点击放大

二级标题滚动嵌套吸顶效果
第一层吸顶用Scroll嵌套List实现,第二层是利用ListItemGroup的sticky属性实现,示例代码如下:

@Entry
@Component
struct MultipleTitleSolutionTwo {
private timeTable: TimeTable[] = [
{
title: 'Header2-0',
projects: ['内容1']
},
{
title: 'Header2-1',
projects: ['内容2']
},
{
title: 'Header2-2',
projects: ['内容3']
}
];

@Builder
itemHead(text: string, index: number) {
if (index === 0) {
Text().height(0);
} else {
Text(text)
.width('100%')
.textAlign(TextAlign.Center)
.fontColor(Color.Black)
.height(index === 1 ? 100 : 200)
.backgroundColor('#d1d1d6');
}
}

build() {
Scroll() {
Column() {
Text('自定义占位标题')
.width('100%')
.height(200)
.backgroundColor('#66666666')
.textAlign(TextAlign.Center);
Column() {
// 第一层吸顶效果主要是通过设置nestedScroll属性以及父组件设置高度100%实现
Text('Header1')
.width('100%')
.height(100)
.backgroundColor('#4d666666')
.textAlign(TextAlign.Center)
.fontColor(Color.Black);
List() {
ForEach(this.timeTable, (item: TimeTable, index) => {
ListItemGroup({ header: this.itemHead(item.title, index) }) {
ForEach(item.projects, (project: string) => {
ListItem() {
Text(project)
.width('100%')
.height(800)
.fontSize(20)
.textAlign(TextAlign.Center)
.backgroundColor('#FFFFFF');
}
}, (item: string) => item);
}.clip(false);
});
}
.expandSafeArea([SafeAreaType.SYSTEM], [SafeAreaEdge.BOTTOM])
.sticky(StickyStyle.Header)
.scrollBar(BarState.Off)
.width('100%')
.height('calc(100% - 100vp)')
.backgroundColor('#ffffff')
.edgeEffect(EdgeEffect.Spring)
.nestedScroll({
scrollForward: NestedScrollMode.PARENT_FIRST,
scrollBackward: NestedScrollMode.SELF_FIRST
});
}.clip(false).expandSafeArea([SafeAreaType.SYSTEM], [SafeAreaEdge.BOTTOM]).height('100%')
}.expandSafeArea([SafeAreaType.SYSTEM], [SafeAreaEdge.BOTTOM])
}
.expandSafeArea([SafeAreaType.SYSTEM], [SafeAreaEdge.BOTTOM])
.clip(false)
.edgeEffect(EdgeEffect.Spring)
.friction(0.6)
.backgroundColor(Color.White)
.scrollBar(BarState.Off)
.width('100%')
.height('100%');
}
}

interface TimeTable {
title: string;
projects: string[];
}
实现效果如下:

点击放大

动态控制二级标题滚动嵌套吸顶效果:
在List组件onDidScroll方法里判断索引为2的组件滚动偏移量,动态调整Header1的position属性和List组件的contentStartOffset属性,示例代码如下:

@Entry
@Component
struct MultipleTitleSolutionTwoDynamic {
@State header1Position: number = 0;
@State listStartOffset: number = 100;
private scroller = new Scroller();
private timeTable: TimeTableDynamic[] = [
{
title: 'Header2-0',
projects: ['内容1']
},
{
title: 'Header2-1',
projects: ['内容2']
},
{
title: 'Header2-2',
projects: ['内容3']
}
];

@Builder
itemHead(text: string, index: number) {
if (index === 0) {
Text().height(0);
} else {
Text(text)
.width('100%')
.textAlign(TextAlign.Center)
.fontColor(Color.Black)
.height(index === 1 ? 100 : 200)
.backgroundColor('#d1d1d6');
}
}

build() {
Scroll() {
Column() {
Text('二级标题滚动嵌套吸顶')
.width('100%')
.height(200)
.backgroundColor('#66666666')
.textAlign(TextAlign.Center);
Column() {
// 第一层吸顶效果主要是通过设置nestedScroll属性以及父组件设置高度100%实现
Text('Header1')
.width('100%')
.height(100)
.backgroundColor('#4d666666')
.textAlign(TextAlign.Center)
.fontColor(Color.Black)
.position({ y: this.header1Position })
.zIndex(10);
List({ scroller: this.scroller }) {
ForEach(this.timeTable, (item: TimeTableDynamic, index) => {
ListItemGroup({ header: this.itemHead(item.title, index) }) {
ForEach(item.projects, (project: string) => {
ListItem() {
Text(project)
.width('100%')
.height(800)
.fontSize(20)
.textAlign(TextAlign.Center)
.backgroundColor('#ffffff');
}.expandSafeArea([SafeAreaType.SYSTEM], [SafeAreaEdge.BOTTOM]);
}, (item: string) => item);
};
});
}
.expandSafeArea([SafeAreaType.SYSTEM], [SafeAreaEdge.BOTTOM])
.sticky(StickyStyle.Header)
.scrollBar(BarState.Off)
.width('100%')
.height('100%')
.backgroundColor('#ffffff')
.edgeEffect(EdgeEffect.Spring)
.contentStartOffset(this.listStartOffset)
.nestedScroll({
scrollForward: NestedScrollMode.PARENT_FIRST,
scrollBackward: NestedScrollMode.SELF_FIRST
})
.onDidScroll(() => {
let secondRect = this.scroller.getItemRect(2);
if (secondRect.x === 0 && secondRect.y === 0 && secondRect.width === 0 && secondRect.height === 0) {
console.info(索引为${2}的组件不在屏幕上);
return;
}
this.header1Position = Math.min(0, secondRect.y - 200);
this.listStartOffset = Math.max(0, 100 + this.header1Position);
});
}.height('100%');
};
}.expandSafeArea([SafeAreaType.SYSTEM], [SafeAreaEdge.BOTTOM])
.clip(false)
.edgeEffect(EdgeEffect.Spring)
.friction(0.6)
.backgroundColor(Color.White)
.scrollBar(BarState.Off)
.width('100%')
.height('100%');
}
}

interface TimeTableDynamic {
title: string;
projects: string[];
}
https://coub.com/view/gylbvhqrkz
https://coub.com/view/gqrdnsqbsm
https://coub.com/view/0a3thuu2b4
https://coub.com/view/lg6lw08ivp
https://coub.com/view/qzxlw9ogx1
https://coub.com/view/68z9behrh7
https://coub.com/view/4iijkgxtaa
https://coub.com/view/xuelqryeob
https://coub.com/view/037iwxjpf9
https://coub.com/view/ydu5rfse9r
https://coub.com/view/g71sfv35n0
https://coub.com/view/hmkl7icf96
https://coub.com/view/7gf8jpjeqf
https://coub.com/view/6ar7vzg3jt
https://coub.com/view/x6brb1zhst
https://coub.com/view/esc2tpa50i
https://coub.com/view/ij4i54b4su
https://coub.com/view/6csj522lvg
https://coub.com/view/oaspyursw3
https://coub.com/view/gvem0zleaj
https://coub.com/view/xs8g570e03
https://coub.com/view/suwbkm015s
https://coub.com/view/74yubqnb80
https://coub.com/view/6wh2qfuaws
https://coub.com/view/2yv1gta5d2
https://coub.com/view/hopm4eqo5c
https://coub.com/view/q1tm3sdodd
https://coub.com/view/7udyb7fyhi
https://coub.com/view/1z3anr32zq
https://coub.com/view/jhh5mhj3y0
https://coub.com/view/dvq9c4eyfc
https://coub.com/view/wv83dp9qeo
https://coub.com/view/a9i5tkfl9w
https://coub.com/view/v372pqbiis
https://coub.com/view/ig9545no10
https://coub.com/view/w79ltp6ou9
https://coub.com/view/u9s0qk1akf
https://coub.com/view/damsu4njzt
https://coub.com/view/v2vriht1bd
https://coub.com/view/e93br3m9qa
https://coub.com/view/xd56t399ne
https://coub.com/view/dnvfpx6l5b
https://coub.com/view/pehxieghx6
https://coub.com/view/6w14zzs972
https://coub.com/view/46b628kwgi
https://coub.com/view/o5bg5eawhx
https://coub.com/view/rrinp7vw3c
https://coub.com/view/mamo602b5a
https://coub.com/view/hlvm2nw499
https://coub.com/view/jzu48fqzw5
https://coub.com/view/ncc5pejn2y
https://coub.com/view/o61yarijz2
https://coub.com/view/xt572wfwwl
https://coub.com/view/t4z6lnd7n8
https://coub.com/view/cxz5p1rtil
https://coub.com/view/a630gf5urg
https://coub.com/view/7khrln5czp
https://coub.com/view/u52ikkf9j8
https://coub.com/view/6ispqsd85w
https://coub.com/view/g5ndpkf32e
https://coub.com/view/nf11v6hl00
https://coub.com/view/g2va9aj5ue
https://coub.com/view/qe1rsfh7ey
https://coub.com/view/ldvx0v2kr2
https://coub.com/view/w3bct13r9i
https://coub.com/view/3zyw0cjxo1
https://coub.com/view/ny9foponj5
https://coub.com/view/g4q545h3da
https://coub.com/view/kekg8y04v7
https://coub.com/view/ms20cjnzki
https://coub.com/view/6hg6xx4f3u
https://coub.com/view/4gd6mjpi41
https://coub.com/view/91selywfey
https://coub.com/view/d9lltiovbh
https://coub.com/view/j6dhi1cqlu
https://coub.com/view/pupns1qvyy
https://coub.com/view/42eqvna2sj
https://coub.com/view/213yl91c0r
https://coub.com/view/ligxe8niat
https://sites.google.com/view/4npswg73/home
https://sites.google.com/view/1cqggg42/home
https://sites.google.com/view/7hwthv95/home
https://sites.google.com/view/8ftpag65/home
https://sites.google.com/view/9hupnv16/home
https://sites.google.com/view/9rcwcc44/home
https://sites.google.com/view/4ygxfw66/home
https://sites.google.com/view/7yjaoo71/home
https://sites.google.com/view/6euxnz02/home
https://sites.google.com/view/2xrcwv82/home
https://sites.google.com/view/3bruzf22/home
https://sites.google.com/view/1fampb85/home
https://sites.google.com/view/0uqjgm22/home
https://sites.google.com/view/8lytyx96/home
https://sites.google.com/view/2rcvcx10/home
https://sites.google.com/view/0wxard26/home
https://sites.google.com/view/0rhcrm43/home
https://sites.google.com/view/1cilam64/home
https://sites.google.com/view/2dsnqw73/home
https://sites.google.com/view/0vgsrq46/home
https://sites.google.com/view/8nqpzw07/home
https://sites.google.com/view/2zlysr71/home
https://sites.google.com/view/6krwym27/home
https://sites.google.com/view/6xuung21/home
https://sites.google.com/view/5irzjv73/home
https://sites.google.com/view/7lukby92/home
https://sites.google.com/view/3oyfhr95/home
https://sites.google.com/view/2srcwe43/home
https://sites.google.com/view/0setkv89/home
https://sites.google.com/view/5rvxgj88/home
https://sites.google.com/view/7tfahr42/home
https://sites.google.com/view/7yboep62/home
https://sites.google.com/view/4upmsh64/home
https://sites.google.com/view/0zwrrv97/home
https://sites.google.com/view/2gawzu13/home
https://sites.google.com/view/6qeyzl51/home
https://sites.google.com/view/3ievss43/home
https://sites.google.com/view/0ibpqm02/home
https://sites.google.com/view/3awrap71/home
https://sites.google.com/view/0tvqkg18/home
https://sites.google.com/view/5sbyjy90/home
https://sites.google.com/view/6ehwco13/home
https://sites.google.com/view/7tjzdg23/home
https://sites.google.com/view/6rakcq50/home
https://sites.google.com/view/7hexol86/home
https://sites.google.com/view/0cyonp67/home
https://sites.google.com/view/7cpmvf18/home
https://sites.google.com/view/7qroxh49/home
https://sites.google.com/view/2qshnp57/home
https://sites.google.com/view/6dwyim71/home
https://sites.google.com/view/4cegdc43/home
https://sites.google.com/view/9kbwox44/home
https://sites.google.com/view/6sciwd21/home
https://sites.google.com/view/6bjrsb10/home
https://sites.google.com/view/3qxmma28/home
https://sites.google.com/view/7cbftv08/home
https://sites.google.com/view/4kkccj88/home
https://sites.google.com/view/5pjonl99/home
https://sites.google.com/view/4aeacr58/home
https://sites.google.com/view/6fiuxa97/home
https://sites.google.com/view/5brhgy37/home
https://sites.google.com/view/1mzbfo35/home
https://sites.google.com/view/9kbckv34/home
https://sites.google.com/view/9cobir53/home
https://sites.google.com/view/6qgbcm33/home
https://sites.google.com/view/8lgcnx33/home
https://sites.google.com/view/9jzmzc49/home
https://sites.google.com/view/0bujrj34/home
https://sites.google.com/view/1heuxf15/home
https://sites.google.com/view/0przrr24/home
https://sites.google.com/view/8gmodp38/home
https://sites.google.com/view/1qgplx06/home
https://sites.google.com/view/1vbfom31/home
https://sites.google.com/view/4fgfqr84/home
https://sites.google.com/view/6oybbx22/home
https://sites.google.com/view/1knpbg15/home
https://sites.google.com/view/0opgke85/home
https://sites.google.com/view/7kirma83/home
https://sites.google.com/view/1zzbdj74/home
https://sites.google.com/view/8csmsa36/home
https://sites.google.com/view/1edswe69/home
https://sites.google.com/view/8kyxaz85/home
https://sites.google.com/view/8bplrw32/home
https://sites.google.com/view/7pdmfv03/home
https://sites.google.com/view/8qkoih30/home
https://sites.google.com/view/4noxpv38/home
https://sites.google.com/view/4itlbr61/home
https://sites.google.com/view/5rjefu09/home
https://sites.google.com/view/5ujnxe58/home
https://sites.google.com/view/3gqlqd93/home
https://sites.google.com/view/4ijybb89/home
https://sites.google.com/view/6ipevr32/home
https://sites.google.com/view/5mhxux08/home
https://sites.google.com/view/8wojcb88/home
https://sites.google.com/view/0riyrm78/home

收起阅读 »

如何解决LocalStorage存储function报错问题

问题现象
使用LocalStorage传递function报错:

Error message:@Component 'owning @Component UNKNOWN': Illegal variable value error with decorated variable undefined 'clickSend': failed validation: 'undefined, null, number, boolean, string, or Object but not function, not V2 @ObservedV2 / @Trace class, and makeObserved return value either, attempt to assign value type: 'function', value: 'undefined'!
关键问题参考如下:

function openCommentsInput(title: string, hintMsg: string, clickSend: Function = (content: string) => {}) {
let storage: LocalStorage = new LocalStorage();
storage.setOrCreate('title', title)
storage.setOrCreate('hintMsg', hintMsg)
storage.setOrCreate('clickSend', clickSend) // 程序崩溃,报错
}
背景知识
LocalStorage是页面级的UI状态存储,存储的类型有所限制。

问题定位
查看以下报错日志的关键信息可知,clickSend是非法变量值,支持undefined、null、number、boolean、string、Object等类型,但不支持function函数类型变量。

Illegal variable value error with decorated variable undefined 'clickSend': failed validation: 'undefined, null, number, boolean, string, or Object but not function ...'
分析结论
LocalStorage不支持存储function函数类型变量。

修改建议
使用类class将函数function进行封装为Object对象,LocalStorage支持存储Object对象。

let storage: LocalStorage = new LocalStorage();

function openCommentsInput(title: string, hintMsg: string, clickSend: object) {
storage.setOrCreate('title', title);
storage.setOrCreate('hintMsg', hintMsg);
storage.setOrCreate('clickSend', clickSend);
}

// 使用class对function进行一层封装
class MyFunc {
clickSend: Function = () => {
};
}

// 需要保存至LocalStorage的函数
function clickSend(ctx: string) {
console.info(ctx);
}

@Entry(storage)
@Component
export struct LocalStorageDemo {
@LocalStorageLink('clickSend') myFunc: object = []; // 获取LocalStorage中存储的clickSend对象

build() {
Column({ space: 20 }) {
Button('向storage保存数据')
.onClick(() => {
let myFunc: MyFunc = new MyFunc();
myFunc.clickSend = clickSend; // 将要保存的函数封装在class对象中
openCommentsInput('title', 'hintMsg', myFunc); // 将class对象保存至LocalStorage
});
Button('读取storage的数据')
.onClick(() => {
let tmp = this.myFunc as MyFunc; // 将Object对象转为MyFunc类
tmp.clickSend('读取storage的数据成功'); // 调用MyFunc中的clickSend函数
});
};
}
}
运行截图如下:成功调用LocalStorage中存储的类对象的函数来打印数据。

https://coub.com/view/qk692jnkyy
https://coub.com/view/nqy2r5u04j
https://coub.com/view/rxk7ib6w4i
https://coub.com/view/lfok6c5r0k
https://coub.com/view/cps6sv1832
https://coub.com/view/n4rgmqcpj6
https://coub.com/view/8naf4x8mtj
https://coub.com/view/90hd34u6j5
https://coub.com/view/xxmp511mw1
https://coub.com/view/ga20sx80nf
https://coub.com/view/496cje9x00
https://coub.com/view/e16120c698
https://coub.com/view/qpk44ggqgv
https://coub.com/view/9h64hx23cz
https://coub.com/view/fw89xni3km
https://coub.com/view/fmdlgaxtdh
https://coub.com/view/wbfdz7bao0
https://coub.com/view/jbvjml6xnb
https://coub.com/view/v2gvgqu1pn
https://coub.com/view/lukv9msonl
https://coub.com/view/wy1u881k37
https://coub.com/view/i35bvv7bjj
https://coub.com/view/79bfeowo9y
https://coub.com/view/vfn4r5gd5r
https://coub.com/view/faob3nkydm
https://coub.com/view/t20e00f911
https://coub.com/view/l0n0lv31ss
https://coub.com/view/y1gdff8iup
https://coub.com/view/325iw05lzf
https://coub.com/view/zysxnw2pyg
https://coub.com/view/ip600tbh2t
https://coub.com/view/ik4qxfnchj
https://coub.com/view/x9byx4imcd
https://coub.com/view/j7wpgvy2r9
https://coub.com/view/z9j3mk72kk
https://coub.com/view/u5jraprsbp
https://coub.com/view/18icai1qy2
https://coub.com/view/91t8lsg4vr
https://coub.com/view/zcre25zh9n
https://coub.com/view/4b4f4fhr8u
https://coub.com/view/75reunqnh5
https://coub.com/view/bb1obvuqhi
https://coub.com/view/xxwmrq1it0
https://coub.com/view/zpxfp25pwf
https://coub.com/view/w9qwirmzj7
https://coub.com/view/3he63xj1fl
https://coub.com/view/ij87impqwc
https://coub.com/view/9sllwd1dt7
https://coub.com/view/sap509vv8e
https://coub.com/view/9micgamocw
https://coub.com/view/urk86qp1iu
https://coub.com/view/4tc2me72v9
https://coub.com/view/kq6hr3az82
https://coub.com/view/e7v9yib78m
https://coub.com/view/3me1n3nsrc
https://coub.com/view/ajjah56ium
https://coub.com/view/5fmp56nirq
https://coub.com/view/ouzl95vmsy
https://coub.com/view/5joic8mnfe
https://coub.com/view/wa98vp5r4o
https://coub.com/view/mo88n15ddc
https://coub.com/view/ahpssb3udw
https://coub.com/view/bttevp21v6
https://coub.com/view/wb7er1vpik
https://coub.com/view/u95v6au0cg
https://coub.com/view/w7z0nh2aeb
https://coub.com/view/s2sv3xp1rv
https://coub.com/view/8uw0lqh5t9
https://coub.com/view/3779k4n10i
https://coub.com/view/ks34oh2lf1
https://coub.com/view/p9ypedd2wg
https://coub.com/view/jwiqh1gt58
https://coub.com/view/3mj4f1uew0
https://coub.com/view/zu17pmttav
https://coub.com/view/r2r56ewp66
https://coub.com/view/5ptqyepsog
https://coub.com/view/nfbde2nqvw
https://coub.com/view/8eatx8ni77
https://coub.com/view/4angchwxcu
https://sites.google.com/view/0ngmeh79/home
https://sites.google.com/view/3yrngq53/home
https://sites.google.com/view/3hzydd88/home
https://sites.google.com/view/1ctnmt31/home
https://sites.google.com/view/6sxmmf82/home
https://sites.google.com/view/9pmhfl95/home
https://sites.google.com/view/4jfltz88/home
https://sites.google.com/view/5tysvi63/home
https://sites.google.com/view/0zjxdz60/home
https://sites.google.com/view/0bpoqj02/home
https://sites.google.com/view/8vdldp71/home
https://sites.google.com/view/3uwslk83/home
https://sites.google.com/view/5ltzul21/home
https://sites.google.com/view/8bcvze60/home
https://sites.google.com/view/8qgykm38/home
https://sites.google.com/view/6ajgve57/home
https://sites.google.com/view/5ilxvx17/home
https://sites.google.com/view/4qvlfm93/home
https://sites.google.com/view/4mezwk86/home
https://sites.google.com/view/0atgpp88/home
https://sites.google.com/view/6zfnle48/home
https://sites.google.com/view/6wkkab73/home
https://sites.google.com/view/1uteqg45/home
https://sites.google.com/view/0glrrx12/home
https://sites.google.com/view/4zsavi57/home
https://sites.google.com/view/9oovga45/home
https://sites.google.com/view/3hzgfr61/home
https://sites.google.com/view/9vkbkl41/home
https://sites.google.com/view/6wndzs57/home
https://sites.google.com/view/6uwefj26/home
https://sites.google.com/view/0oipjv09/home
https://sites.google.com/view/4pngoo73/home
https://sites.google.com/view/6fycbf68/home
https://sites.google.com/view/5quyop60/home
https://sites.google.com/view/7oupoi39/home
https://sites.google.com/view/8otdza09/home
https://sites.google.com/view/0sqefg16/home
https://sites.google.com/view/4zrmtg15/home
https://sites.google.com/view/3cgjls08/home
https://sites.google.com/view/6oldtl78/home
https://sites.google.com/view/4ykfdd04/home
https://sites.google.com/view/9wpguc90/home
https://sites.google.com/view/6idlof66/home
https://sites.google.com/view/8omphs74/home
https://sites.google.com/view/6xofvc19/home
https://sites.google.com/view/1difbo72/home
https://sites.google.com/view/4knmhu66/home
https://sites.google.com/view/1ssqkm73/home
https://sites.google.com/view/8zavly68/home
https://sites.google.com/view/9zwrzj64/home
https://sites.google.com/view/7wqfid80/home
https://sites.google.com/view/0mzlvh16/home
https://sites.google.com/view/1hozwq52/home
https://sites.google.com/view/3eikgg48/home
https://sites.google.com/view/3skunx74/home
https://sites.google.com/view/7znrmu32/home
https://sites.google.com/view/9xniil98/home
https://sites.google.com/view/1gmoid98/home
https://sites.google.com/view/6gxmmk76/home
https://sites.google.com/view/6xdnsn40/home
https://sites.google.com/view/6qbvjn86/home
https://sites.google.com/view/9veorf12/home
https://sites.google.com/view/4udhos46/home
https://sites.google.com/view/2ynqpm49/home
https://sites.google.com/view/1pauyq10/home
https://sites.google.com/view/4mmhbb67/home
https://sites.google.com/view/5dimfw85/home
https://sites.google.com/view/7qrywi45/home
https://sites.google.com/view/7xmufr77/home
https://sites.google.com/view/0tzmwq99/home
https://sites.google.com/view/2elngz59/home
https://sites.google.com/view/1mopok42/home
https://sites.google.com/view/5galcb39/home
https://sites.google.com/view/3dplbi09/home
https://sites.google.com/view/4hxzuw21/home
https://sites.google.com/view/3ndtxe40/home
https://sites.google.com/view/1wgxgh74/home
https://sites.google.com/view/0ujdvq38/home
https://sites.google.com/view/3qtkgv13/home
https://sites.google.com/view/5jqmpf47/home
https://sites.google.com/view/6ovyem57/home
https://sites.google.com/view/3jpimw15/home
https://sites.google.com/view/1xlfel73/home
https://sites.google.com/view/8jqklu53/home
https://sites.google.com/view/2vrqgs17/home
https://sites.google.com/view/2cmkar10/home
https://sites.google.com/view/5sktrz87/home
https://sites.google.com/view/9qkbxe89/home
https://sites.google.com/view/2zlonk24/home
https://sites.google.com/view/6jnevv87/home
https://sites.google.com/view/1wrgrh89/home
https://sites.google.com/view/5yjmql75/home
https://sites.google.com/view/1eqrct60/home
https://sites.google.com/view/5bgxgx08/home
https://sites.google.com/view/5ikctu17/home
https://sites.google.com/view/8hqypq15/home
https://sites.google.com/view/9nhoum17/home
https://sites.google.com/view/0pvuts20/home
https://sites.google.com/view/9gqmnb23/home
https://sites.google.com/view/6ifnwy59/home
https://sites.google.com/view/2dacaw12/home
https://sites.google.com/view/9vhqej39/home
https://sites.google.com/view/5tmvjt88/home
https://sites.google.com/view/3mnghf05/home
https://sites.google.com/view/6ttioq69/home

继续阅读 »

问题现象
使用LocalStorage传递function报错:

Error message:@Component 'owning @Component UNKNOWN': Illegal variable value error with decorated variable undefined 'clickSend': failed validation: 'undefined, null, number, boolean, string, or Object but not function, not V2 @ObservedV2 / @Trace class, and makeObserved return value either, attempt to assign value type: 'function', value: 'undefined'!
关键问题参考如下:

function openCommentsInput(title: string, hintMsg: string, clickSend: Function = (content: string) => {}) {
let storage: LocalStorage = new LocalStorage();
storage.setOrCreate('title', title)
storage.setOrCreate('hintMsg', hintMsg)
storage.setOrCreate('clickSend', clickSend) // 程序崩溃,报错
}
背景知识
LocalStorage是页面级的UI状态存储,存储的类型有所限制。

问题定位
查看以下报错日志的关键信息可知,clickSend是非法变量值,支持undefined、null、number、boolean、string、Object等类型,但不支持function函数类型变量。

Illegal variable value error with decorated variable undefined 'clickSend': failed validation: 'undefined, null, number, boolean, string, or Object but not function ...'
分析结论
LocalStorage不支持存储function函数类型变量。

修改建议
使用类class将函数function进行封装为Object对象,LocalStorage支持存储Object对象。

let storage: LocalStorage = new LocalStorage();

function openCommentsInput(title: string, hintMsg: string, clickSend: object) {
storage.setOrCreate('title', title);
storage.setOrCreate('hintMsg', hintMsg);
storage.setOrCreate('clickSend', clickSend);
}

// 使用class对function进行一层封装
class MyFunc {
clickSend: Function = () => {
};
}

// 需要保存至LocalStorage的函数
function clickSend(ctx: string) {
console.info(ctx);
}

@Entry(storage)
@Component
export struct LocalStorageDemo {
@LocalStorageLink('clickSend') myFunc: object = []; // 获取LocalStorage中存储的clickSend对象

build() {
Column({ space: 20 }) {
Button('向storage保存数据')
.onClick(() => {
let myFunc: MyFunc = new MyFunc();
myFunc.clickSend = clickSend; // 将要保存的函数封装在class对象中
openCommentsInput('title', 'hintMsg', myFunc); // 将class对象保存至LocalStorage
});
Button('读取storage的数据')
.onClick(() => {
let tmp = this.myFunc as MyFunc; // 将Object对象转为MyFunc类
tmp.clickSend('读取storage的数据成功'); // 调用MyFunc中的clickSend函数
});
};
}
}
运行截图如下:成功调用LocalStorage中存储的类对象的函数来打印数据。

https://coub.com/view/qk692jnkyy
https://coub.com/view/nqy2r5u04j
https://coub.com/view/rxk7ib6w4i
https://coub.com/view/lfok6c5r0k
https://coub.com/view/cps6sv1832
https://coub.com/view/n4rgmqcpj6
https://coub.com/view/8naf4x8mtj
https://coub.com/view/90hd34u6j5
https://coub.com/view/xxmp511mw1
https://coub.com/view/ga20sx80nf
https://coub.com/view/496cje9x00
https://coub.com/view/e16120c698
https://coub.com/view/qpk44ggqgv
https://coub.com/view/9h64hx23cz
https://coub.com/view/fw89xni3km
https://coub.com/view/fmdlgaxtdh
https://coub.com/view/wbfdz7bao0
https://coub.com/view/jbvjml6xnb
https://coub.com/view/v2gvgqu1pn
https://coub.com/view/lukv9msonl
https://coub.com/view/wy1u881k37
https://coub.com/view/i35bvv7bjj
https://coub.com/view/79bfeowo9y
https://coub.com/view/vfn4r5gd5r
https://coub.com/view/faob3nkydm
https://coub.com/view/t20e00f911
https://coub.com/view/l0n0lv31ss
https://coub.com/view/y1gdff8iup
https://coub.com/view/325iw05lzf
https://coub.com/view/zysxnw2pyg
https://coub.com/view/ip600tbh2t
https://coub.com/view/ik4qxfnchj
https://coub.com/view/x9byx4imcd
https://coub.com/view/j7wpgvy2r9
https://coub.com/view/z9j3mk72kk
https://coub.com/view/u5jraprsbp
https://coub.com/view/18icai1qy2
https://coub.com/view/91t8lsg4vr
https://coub.com/view/zcre25zh9n
https://coub.com/view/4b4f4fhr8u
https://coub.com/view/75reunqnh5
https://coub.com/view/bb1obvuqhi
https://coub.com/view/xxwmrq1it0
https://coub.com/view/zpxfp25pwf
https://coub.com/view/w9qwirmzj7
https://coub.com/view/3he63xj1fl
https://coub.com/view/ij87impqwc
https://coub.com/view/9sllwd1dt7
https://coub.com/view/sap509vv8e
https://coub.com/view/9micgamocw
https://coub.com/view/urk86qp1iu
https://coub.com/view/4tc2me72v9
https://coub.com/view/kq6hr3az82
https://coub.com/view/e7v9yib78m
https://coub.com/view/3me1n3nsrc
https://coub.com/view/ajjah56ium
https://coub.com/view/5fmp56nirq
https://coub.com/view/ouzl95vmsy
https://coub.com/view/5joic8mnfe
https://coub.com/view/wa98vp5r4o
https://coub.com/view/mo88n15ddc
https://coub.com/view/ahpssb3udw
https://coub.com/view/bttevp21v6
https://coub.com/view/wb7er1vpik
https://coub.com/view/u95v6au0cg
https://coub.com/view/w7z0nh2aeb
https://coub.com/view/s2sv3xp1rv
https://coub.com/view/8uw0lqh5t9
https://coub.com/view/3779k4n10i
https://coub.com/view/ks34oh2lf1
https://coub.com/view/p9ypedd2wg
https://coub.com/view/jwiqh1gt58
https://coub.com/view/3mj4f1uew0
https://coub.com/view/zu17pmttav
https://coub.com/view/r2r56ewp66
https://coub.com/view/5ptqyepsog
https://coub.com/view/nfbde2nqvw
https://coub.com/view/8eatx8ni77
https://coub.com/view/4angchwxcu
https://sites.google.com/view/0ngmeh79/home
https://sites.google.com/view/3yrngq53/home
https://sites.google.com/view/3hzydd88/home
https://sites.google.com/view/1ctnmt31/home
https://sites.google.com/view/6sxmmf82/home
https://sites.google.com/view/9pmhfl95/home
https://sites.google.com/view/4jfltz88/home
https://sites.google.com/view/5tysvi63/home
https://sites.google.com/view/0zjxdz60/home
https://sites.google.com/view/0bpoqj02/home
https://sites.google.com/view/8vdldp71/home
https://sites.google.com/view/3uwslk83/home
https://sites.google.com/view/5ltzul21/home
https://sites.google.com/view/8bcvze60/home
https://sites.google.com/view/8qgykm38/home
https://sites.google.com/view/6ajgve57/home
https://sites.google.com/view/5ilxvx17/home
https://sites.google.com/view/4qvlfm93/home
https://sites.google.com/view/4mezwk86/home
https://sites.google.com/view/0atgpp88/home
https://sites.google.com/view/6zfnle48/home
https://sites.google.com/view/6wkkab73/home
https://sites.google.com/view/1uteqg45/home
https://sites.google.com/view/0glrrx12/home
https://sites.google.com/view/4zsavi57/home
https://sites.google.com/view/9oovga45/home
https://sites.google.com/view/3hzgfr61/home
https://sites.google.com/view/9vkbkl41/home
https://sites.google.com/view/6wndzs57/home
https://sites.google.com/view/6uwefj26/home
https://sites.google.com/view/0oipjv09/home
https://sites.google.com/view/4pngoo73/home
https://sites.google.com/view/6fycbf68/home
https://sites.google.com/view/5quyop60/home
https://sites.google.com/view/7oupoi39/home
https://sites.google.com/view/8otdza09/home
https://sites.google.com/view/0sqefg16/home
https://sites.google.com/view/4zrmtg15/home
https://sites.google.com/view/3cgjls08/home
https://sites.google.com/view/6oldtl78/home
https://sites.google.com/view/4ykfdd04/home
https://sites.google.com/view/9wpguc90/home
https://sites.google.com/view/6idlof66/home
https://sites.google.com/view/8omphs74/home
https://sites.google.com/view/6xofvc19/home
https://sites.google.com/view/1difbo72/home
https://sites.google.com/view/4knmhu66/home
https://sites.google.com/view/1ssqkm73/home
https://sites.google.com/view/8zavly68/home
https://sites.google.com/view/9zwrzj64/home
https://sites.google.com/view/7wqfid80/home
https://sites.google.com/view/0mzlvh16/home
https://sites.google.com/view/1hozwq52/home
https://sites.google.com/view/3eikgg48/home
https://sites.google.com/view/3skunx74/home
https://sites.google.com/view/7znrmu32/home
https://sites.google.com/view/9xniil98/home
https://sites.google.com/view/1gmoid98/home
https://sites.google.com/view/6gxmmk76/home
https://sites.google.com/view/6xdnsn40/home
https://sites.google.com/view/6qbvjn86/home
https://sites.google.com/view/9veorf12/home
https://sites.google.com/view/4udhos46/home
https://sites.google.com/view/2ynqpm49/home
https://sites.google.com/view/1pauyq10/home
https://sites.google.com/view/4mmhbb67/home
https://sites.google.com/view/5dimfw85/home
https://sites.google.com/view/7qrywi45/home
https://sites.google.com/view/7xmufr77/home
https://sites.google.com/view/0tzmwq99/home
https://sites.google.com/view/2elngz59/home
https://sites.google.com/view/1mopok42/home
https://sites.google.com/view/5galcb39/home
https://sites.google.com/view/3dplbi09/home
https://sites.google.com/view/4hxzuw21/home
https://sites.google.com/view/3ndtxe40/home
https://sites.google.com/view/1wgxgh74/home
https://sites.google.com/view/0ujdvq38/home
https://sites.google.com/view/3qtkgv13/home
https://sites.google.com/view/5jqmpf47/home
https://sites.google.com/view/6ovyem57/home
https://sites.google.com/view/3jpimw15/home
https://sites.google.com/view/1xlfel73/home
https://sites.google.com/view/8jqklu53/home
https://sites.google.com/view/2vrqgs17/home
https://sites.google.com/view/2cmkar10/home
https://sites.google.com/view/5sktrz87/home
https://sites.google.com/view/9qkbxe89/home
https://sites.google.com/view/2zlonk24/home
https://sites.google.com/view/6jnevv87/home
https://sites.google.com/view/1wrgrh89/home
https://sites.google.com/view/5yjmql75/home
https://sites.google.com/view/1eqrct60/home
https://sites.google.com/view/5bgxgx08/home
https://sites.google.com/view/5ikctu17/home
https://sites.google.com/view/8hqypq15/home
https://sites.google.com/view/9nhoum17/home
https://sites.google.com/view/0pvuts20/home
https://sites.google.com/view/9gqmnb23/home
https://sites.google.com/view/6ifnwy59/home
https://sites.google.com/view/2dacaw12/home
https://sites.google.com/view/9vhqej39/home
https://sites.google.com/view/5tmvjt88/home
https://sites.google.com/view/3mnghf05/home
https://sites.google.com/view/6ttioq69/home

收起阅读 »

如何实现一个自定义高度的底部弹窗

问题现象
需要实现的底部弹窗可以设置一个最大高度。同时,实现的底部弹窗需要从下往上出现。

如果弹窗内部视图的高度超过了这个最大高度,弹窗内部的视图就以这个自定义最大高度进行滚动展示;
如果弹窗内部视图的高度没有超过这个最大高度,那么弹窗内部视图就完全展示;
背景知识
组件的constraintSize属性用于设置约束尺寸,组件布局时,进行尺寸范围限制。

对于组件的显示方式,转场主要通过transition属性配置转场参数,在组件插入和删除时显示过渡动效,主要用于容器组件中的子组件插入和删除时,提升用户体验。

解决方案
首先,通过使用容器组件的constraintSize属性对视图进行高度限制,constraintSize的优先级高于Width和Height,constraintSize里面的maxHeight小于Height时,就会滚动展示。接着,实现弹窗从底部向上显示,通过设置组件transition属性的转场参数。

示例代码如下:

@CustomDialog
struct CustomDialogExample {
// 最大可滚动区域高度
maxScrollHeight: number = 70;
// 视图是否显示,用于底部弹窗出现与消失时的动效处理
@Link showFlag: Visibility;
controller: CustomDialogController;
scroller: Scroller = new Scroller;
private arr: number[] = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15];

build() {
Column({ space: 5 }) {
RelativeContainer() {
Text('标题')
.fontSize(17)
.fontColor(Color.Black)
.fontWeight(FontWeight.Bold)
.alignRules({
middle: { anchor: 'container', align: HorizontalAlign.Center },
center: { anchor: 'container', align: VerticalAlign.Center }
});
}
.width('100%')
.height(48);

  Scroll(this.scroller) {  
    Column() {  
      ForEach(this.arr, (item: number) => {  
        Text(item.toString())  
          .width('90%')  
          .height(50)  
          .backgroundColor(0xFFFFFF)  
          .borderColor(Color.Black)  
          .fontSize(16)  
          .textAlign(TextAlign.Center);  
        Divider()  
          .vertical(false)  
          .strokeWidth(0.5)  
          .width('60%');  
      }, (item: string) => item);  
    }.width('100%');  
  }  
  .edgeEffect(EdgeEffect.Spring)  
  .scrollSnap({  
    snapAlign: ScrollSnapAlign.START,  
    snapPagination: 400,  
    enableSnapToStart: true,  
    enableSnapToEnd: true  
  })  
  // 使用constraintSize方法可以设置约束尺寸,组件布局时,进行尺寸范围限制  
  .constraintSize({ maxHeight: this.maxScrollHeight + '%' });  

}  
.justifyContent(FlexAlign.Center)  
.backgroundColor('#f5f5f5')  
.borderRadius({  
  topLeft: '16',  
  topRight: '16'  
})  
.visibility(this.showFlag)  
.transition(TransitionEffect.OPACITY.animation({ duration: 200 })  // 弹窗出现与消失的动效  
  .combine(TransitionEffect.translate({ y: 100 })),  
  (transitionIn: boolean) => {  
    if (!transitionIn) {  
      this.controller.close();  
    }  
  }  
);  

}
}

@Entry
@Component
struct CustomDialogDemo {
// 最大可滚动区域高度
@State maxScrollHeight: number = 70;
@State settingDialogShowFlag: Visibility = Visibility.Visible;
dialogController: CustomDialogController = new CustomDialogController({
builder: CustomDialogExample({
maxScrollHeight: this.maxScrollHeight,
showFlag: this.settingDialogShowFlag
}),
alignment: DialogAlignment.Bottom,
width: '100%',
// customStyle需要设置为true,否则底部弹窗出现的动效会有问题
customStyle: true,
autoCancel: true,
// 修改点击弹窗外部区域和返回操作时弹窗消失的方式,这里的处理会有一个动效。否则弹窗会以默认的方式消失
onWillDismiss: () => {
this.settingDialogShowFlag = Visibility.Hidden;
}
});

build() {
Column() {
Row({ space: 5 }) {
Row() {
Button('减10')
.onClick(() => {
this.maxScrollHeight -= 10;
});
};

    Row() {  
      Text(this.maxScrollHeight + '%')  
        .width('40%')  
        .height(45)  
        .textAlign(TextAlign.Center)  
        .backgroundColor('#f5f5f5');  
    };  

    Row() {  
      Button('加10')  
        .onClick(() => {  
          this.maxScrollHeight += 10;  
        });  
    };  
  };  

  Button('打开底部弹窗')  
    .onClick(() => {  
      this.settingDialogShowFlag = Visibility.Visible;  
      this.dialogController.open();  
    });  
}.justifyContent(FlexAlign.Center).width('100%').height('100%');  

}
}

https://sites.google.com/view/8ojcyz21/home
https://sites.google.com/view/8votdo34/home
https://sites.google.com/view/2glnsx28/home
https://sites.google.com/view/5cljni07/home
https://sites.google.com/view/8prkhh07/home
https://sites.google.com/view/7khplo30/home
https://sites.google.com/view/1iwwoj29/home
https://sites.google.com/view/5lokbt20/home
https://sites.google.com/view/8ucjap35/home
https://sites.google.com/view/3vmwqk28/home
https://sites.google.com/view/4hqmfe46/home
https://sites.google.com/view/8jjhsd05/home
https://sites.google.com/view/2ajqbl31/home
https://sites.google.com/view/8eroul90/home
https://sites.google.com/view/8whpgb50/home
https://sites.google.com/view/7fhwgy79/home
https://sites.google.com/view/8otxzd32/home
https://sites.google.com/view/0skklh79/home
https://sites.google.com/view/7epixg32/home
https://sites.google.com/view/2fdymh76/home
https://sites.google.com/view/1qzbbd10/home
https://sites.google.com/view/3betru91/home
https://sites.google.com/view/0fjymy02/home
https://sites.google.com/view/1tuyht61/home
https://sites.google.com/view/3lqsci93/home
https://sites.google.com/view/4moosi57/home
https://sites.google.com/view/0ebmiw84/home
https://sites.google.com/view/4usaol83/home
https://sites.google.com/view/0hknci80/home
https://sites.google.com/view/7ldcgt68/home
https://sites.google.com/view/8emzek60/home
https://sites.google.com/view/9oaldi75/home
https://sites.google.com/view/1hujlx46/home
https://sites.google.com/view/9krrdx96/home
https://sites.google.com/view/0rdwwl74/home
https://sites.google.com/view/4oalem69/home
https://sites.google.com/view/7tpgsy97/home
https://sites.google.com/view/4sjvkh25/home
https://sites.google.com/view/6ixmrs26/home
https://sites.google.com/view/7jjcws23/home
https://sites.google.com/view/4entvi31/home
https://sites.google.com/view/0hqtsy80/home
https://sites.google.com/view/2vthve29/home
https://sites.google.com/view/2wjwhq06/home
https://sites.google.com/view/7lnlrg38/home
https://sites.google.com/view/3jwoff92/home
https://sites.google.com/view/9exfcx28/home
https://sites.google.com/view/6ayynm56/home
https://sites.google.com/view/2mkert04/home
https://sites.google.com/view/9bfpha34/home
https://sites.google.com/view/7kwinp20/home
https://sites.google.com/view/0kzpsl33/home
https://sites.google.com/view/7vqzug72/home
https://sites.google.com/view/8qybcb91/home
https://sites.google.com/view/1manil34/home
https://sites.google.com/view/9zlzlw68/home
https://sites.google.com/view/7ytpgf83/home
https://sites.google.com/view/0figtg65/home
https://sites.google.com/view/3nphmi02/home
https://sites.google.com/view/0qkeih69/home
https://sites.google.com/view/1qqdjc96/home
https://sites.google.com/view/2ocmzl47/home
https://sites.google.com/view/0rszvm31/home
https://sites.google.com/view/8twpcf39/home
https://sites.google.com/view/1tmhcz01/home
https://sites.google.com/view/1fsimv87/home
https://sites.google.com/view/0krela31/home
https://sites.google.com/view/8jhshd10/home
https://sites.google.com/view/5tseou06/home
https://sites.google.com/view/2msyts76/home
https://sites.google.com/view/6dxuri73/home
https://sites.google.com/view/1pmboa56/home
https://sites.google.com/view/9iueuv34/home
https://sites.google.com/view/8pubxw30/home
https://sites.google.com/view/1tlyco56/home
https://sites.google.com/view/8srvbu36/home
https://sites.google.com/view/4gsnto37/home
https://sites.google.com/view/6akswp56/home
https://sites.google.com/view/2inxol47/home
https://sites.google.com/view/3oojsn55/home
https://sites.google.com/view/5mdqwb17/home
https://sites.google.com/view/7xuice12/home
https://sites.google.com/view/0qzfci43/home
https://sites.google.com/view/6ohonc26/home
https://sites.google.com/view/4tbmuv32/home
https://sites.google.com/view/1rbxss83/home
https://sites.google.com/view/2niebx01/home
https://sites.google.com/view/3qaeqy35/home
https://sites.google.com/view/0cbewx04/home
https://sites.google.com/view/3csyuh21/home
https://sites.google.com/view/8wylyx13/home
https://sites.google.com/view/1kpxse38/home
https://sites.google.com/view/8ovanb09/home
https://sites.google.com/view/6rmoih50/home
https://sites.google.com/view/5cfhmg25/home
https://sites.google.com/view/4ivxrt19/home
https://sites.google.com/view/4ltzqb02/home
https://sites.google.com/view/3vdlsy06/home
https://sites.google.com/view/5vwlbj78/home
https://sites.google.com/view/4naisw60/home

继续阅读 »

问题现象
需要实现的底部弹窗可以设置一个最大高度。同时,实现的底部弹窗需要从下往上出现。

如果弹窗内部视图的高度超过了这个最大高度,弹窗内部的视图就以这个自定义最大高度进行滚动展示;
如果弹窗内部视图的高度没有超过这个最大高度,那么弹窗内部视图就完全展示;
背景知识
组件的constraintSize属性用于设置约束尺寸,组件布局时,进行尺寸范围限制。

对于组件的显示方式,转场主要通过transition属性配置转场参数,在组件插入和删除时显示过渡动效,主要用于容器组件中的子组件插入和删除时,提升用户体验。

解决方案
首先,通过使用容器组件的constraintSize属性对视图进行高度限制,constraintSize的优先级高于Width和Height,constraintSize里面的maxHeight小于Height时,就会滚动展示。接着,实现弹窗从底部向上显示,通过设置组件transition属性的转场参数。

示例代码如下:

@CustomDialog
struct CustomDialogExample {
// 最大可滚动区域高度
maxScrollHeight: number = 70;
// 视图是否显示,用于底部弹窗出现与消失时的动效处理
@Link showFlag: Visibility;
controller: CustomDialogController;
scroller: Scroller = new Scroller;
private arr: number[] = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15];

build() {
Column({ space: 5 }) {
RelativeContainer() {
Text('标题')
.fontSize(17)
.fontColor(Color.Black)
.fontWeight(FontWeight.Bold)
.alignRules({
middle: { anchor: 'container', align: HorizontalAlign.Center },
center: { anchor: 'container', align: VerticalAlign.Center }
});
}
.width('100%')
.height(48);

  Scroll(this.scroller) {  
    Column() {  
      ForEach(this.arr, (item: number) => {  
        Text(item.toString())  
          .width('90%')  
          .height(50)  
          .backgroundColor(0xFFFFFF)  
          .borderColor(Color.Black)  
          .fontSize(16)  
          .textAlign(TextAlign.Center);  
        Divider()  
          .vertical(false)  
          .strokeWidth(0.5)  
          .width('60%');  
      }, (item: string) => item);  
    }.width('100%');  
  }  
  .edgeEffect(EdgeEffect.Spring)  
  .scrollSnap({  
    snapAlign: ScrollSnapAlign.START,  
    snapPagination: 400,  
    enableSnapToStart: true,  
    enableSnapToEnd: true  
  })  
  // 使用constraintSize方法可以设置约束尺寸,组件布局时,进行尺寸范围限制  
  .constraintSize({ maxHeight: this.maxScrollHeight + '%' });  

}  
.justifyContent(FlexAlign.Center)  
.backgroundColor('#f5f5f5')  
.borderRadius({  
  topLeft: '16',  
  topRight: '16'  
})  
.visibility(this.showFlag)  
.transition(TransitionEffect.OPACITY.animation({ duration: 200 })  // 弹窗出现与消失的动效  
  .combine(TransitionEffect.translate({ y: 100 })),  
  (transitionIn: boolean) => {  
    if (!transitionIn) {  
      this.controller.close();  
    }  
  }  
);  

}
}

@Entry
@Component
struct CustomDialogDemo {
// 最大可滚动区域高度
@State maxScrollHeight: number = 70;
@State settingDialogShowFlag: Visibility = Visibility.Visible;
dialogController: CustomDialogController = new CustomDialogController({
builder: CustomDialogExample({
maxScrollHeight: this.maxScrollHeight,
showFlag: this.settingDialogShowFlag
}),
alignment: DialogAlignment.Bottom,
width: '100%',
// customStyle需要设置为true,否则底部弹窗出现的动效会有问题
customStyle: true,
autoCancel: true,
// 修改点击弹窗外部区域和返回操作时弹窗消失的方式,这里的处理会有一个动效。否则弹窗会以默认的方式消失
onWillDismiss: () => {
this.settingDialogShowFlag = Visibility.Hidden;
}
});

build() {
Column() {
Row({ space: 5 }) {
Row() {
Button('减10')
.onClick(() => {
this.maxScrollHeight -= 10;
});
};

    Row() {  
      Text(this.maxScrollHeight + '%')  
        .width('40%')  
        .height(45)  
        .textAlign(TextAlign.Center)  
        .backgroundColor('#f5f5f5');  
    };  

    Row() {  
      Button('加10')  
        .onClick(() => {  
          this.maxScrollHeight += 10;  
        });  
    };  
  };  

  Button('打开底部弹窗')  
    .onClick(() => {  
      this.settingDialogShowFlag = Visibility.Visible;  
      this.dialogController.open();  
    });  
}.justifyContent(FlexAlign.Center).width('100%').height('100%');  

}
}

https://sites.google.com/view/8ojcyz21/home
https://sites.google.com/view/8votdo34/home
https://sites.google.com/view/2glnsx28/home
https://sites.google.com/view/5cljni07/home
https://sites.google.com/view/8prkhh07/home
https://sites.google.com/view/7khplo30/home
https://sites.google.com/view/1iwwoj29/home
https://sites.google.com/view/5lokbt20/home
https://sites.google.com/view/8ucjap35/home
https://sites.google.com/view/3vmwqk28/home
https://sites.google.com/view/4hqmfe46/home
https://sites.google.com/view/8jjhsd05/home
https://sites.google.com/view/2ajqbl31/home
https://sites.google.com/view/8eroul90/home
https://sites.google.com/view/8whpgb50/home
https://sites.google.com/view/7fhwgy79/home
https://sites.google.com/view/8otxzd32/home
https://sites.google.com/view/0skklh79/home
https://sites.google.com/view/7epixg32/home
https://sites.google.com/view/2fdymh76/home
https://sites.google.com/view/1qzbbd10/home
https://sites.google.com/view/3betru91/home
https://sites.google.com/view/0fjymy02/home
https://sites.google.com/view/1tuyht61/home
https://sites.google.com/view/3lqsci93/home
https://sites.google.com/view/4moosi57/home
https://sites.google.com/view/0ebmiw84/home
https://sites.google.com/view/4usaol83/home
https://sites.google.com/view/0hknci80/home
https://sites.google.com/view/7ldcgt68/home
https://sites.google.com/view/8emzek60/home
https://sites.google.com/view/9oaldi75/home
https://sites.google.com/view/1hujlx46/home
https://sites.google.com/view/9krrdx96/home
https://sites.google.com/view/0rdwwl74/home
https://sites.google.com/view/4oalem69/home
https://sites.google.com/view/7tpgsy97/home
https://sites.google.com/view/4sjvkh25/home
https://sites.google.com/view/6ixmrs26/home
https://sites.google.com/view/7jjcws23/home
https://sites.google.com/view/4entvi31/home
https://sites.google.com/view/0hqtsy80/home
https://sites.google.com/view/2vthve29/home
https://sites.google.com/view/2wjwhq06/home
https://sites.google.com/view/7lnlrg38/home
https://sites.google.com/view/3jwoff92/home
https://sites.google.com/view/9exfcx28/home
https://sites.google.com/view/6ayynm56/home
https://sites.google.com/view/2mkert04/home
https://sites.google.com/view/9bfpha34/home
https://sites.google.com/view/7kwinp20/home
https://sites.google.com/view/0kzpsl33/home
https://sites.google.com/view/7vqzug72/home
https://sites.google.com/view/8qybcb91/home
https://sites.google.com/view/1manil34/home
https://sites.google.com/view/9zlzlw68/home
https://sites.google.com/view/7ytpgf83/home
https://sites.google.com/view/0figtg65/home
https://sites.google.com/view/3nphmi02/home
https://sites.google.com/view/0qkeih69/home
https://sites.google.com/view/1qqdjc96/home
https://sites.google.com/view/2ocmzl47/home
https://sites.google.com/view/0rszvm31/home
https://sites.google.com/view/8twpcf39/home
https://sites.google.com/view/1tmhcz01/home
https://sites.google.com/view/1fsimv87/home
https://sites.google.com/view/0krela31/home
https://sites.google.com/view/8jhshd10/home
https://sites.google.com/view/5tseou06/home
https://sites.google.com/view/2msyts76/home
https://sites.google.com/view/6dxuri73/home
https://sites.google.com/view/1pmboa56/home
https://sites.google.com/view/9iueuv34/home
https://sites.google.com/view/8pubxw30/home
https://sites.google.com/view/1tlyco56/home
https://sites.google.com/view/8srvbu36/home
https://sites.google.com/view/4gsnto37/home
https://sites.google.com/view/6akswp56/home
https://sites.google.com/view/2inxol47/home
https://sites.google.com/view/3oojsn55/home
https://sites.google.com/view/5mdqwb17/home
https://sites.google.com/view/7xuice12/home
https://sites.google.com/view/0qzfci43/home
https://sites.google.com/view/6ohonc26/home
https://sites.google.com/view/4tbmuv32/home
https://sites.google.com/view/1rbxss83/home
https://sites.google.com/view/2niebx01/home
https://sites.google.com/view/3qaeqy35/home
https://sites.google.com/view/0cbewx04/home
https://sites.google.com/view/3csyuh21/home
https://sites.google.com/view/8wylyx13/home
https://sites.google.com/view/1kpxse38/home
https://sites.google.com/view/8ovanb09/home
https://sites.google.com/view/6rmoih50/home
https://sites.google.com/view/5cfhmg25/home
https://sites.google.com/view/4ivxrt19/home
https://sites.google.com/view/4ltzqb02/home
https://sites.google.com/view/3vdlsy06/home
https://sites.google.com/view/5vwlbj78/home
https://sites.google.com/view/4naisw60/home

收起阅读 »

如何解决打断无限循环动画后,动画失效问题

问题现象
第一次触发无限循环动画后,快速连续多次点击打断该无限循环动画,再次点击就无法看到动画了。

背景知识
animateTo:指定由于闭包代码导致的状态变化插入过渡动效。接口参数有两个,分别是value和event,其中value指定AnimateParam对象(包括时长、Curve等)event为动画的闭包函数,闭包内变量改变产生的属性动画将遵循相同的动画参数。

问题定位
对动画的执行机制进行排查,确认在动画打断过程中是否存在多次叠加的情况,从而导致动画未能正常显示。

分析结论
动画并未真正消失,而是在每次打断过程中被持续叠加,由于叠加的动画实例过多,彼此之间相互覆盖或干扰,导致在视觉上表现不明显,从而给人以动画消失的错觉。

修改结论
首先,应设置一个duration为0的动画,用于确保在每次动画被中断时,能够清除前一次的动画实例,避免动画叠加;该动画的属性值需设置为一个与上一次动画最终状态不同的指定值,以确保状态的更新和正确清除。随后,再创建一个用于实现所需初始动画效果的动画实例,以确保动画表现符合预期。

if (this.isRecording) {
// 设置一个duration为0的动画停掉上一次动画
this.getUIContext().animateTo({ duration: 0, iterations: 1, playMode: PlayMode.Normal }, () => {
// 这里的opacityValue不可以和上一次设置的终止0.1相同
this.opacityValue = 0.2;
});
this.isRecording = false;
// 再创建需要的动画
this.getUIContext().animateTo({ duration: 0, iterations: 1, playMode: PlayMode.Normal }, () => {
this.opacityValue = 1;
});
} else {
this.isRecording = true;
this.getUIContext().animateTo({ duration: 1500, iterations: -1, }, () => {
this.opacityValue = 0.1;
});
}
完整示例参考如下:

@Entry
@Component
struct Page {
@State opacityValue: number = 1;
@State isRecording: boolean = false;

build() {
Row() {
Column() {
Text(this.isRecording ? 'Hello World' : 'Welcome')
.fontSize(50)
.fontWeight(FontWeight.Bold)
.opacity(this.opacityValue)
.textAlign(TextAlign.Center)
.fontColor('#000')
.onClick(() => {
if (this.isRecording) {
// 设置一个duration为0的动画停掉上一次动画
this.getUIContext().animateTo({ duration: 0, iterations: 1, playMode: PlayMode.Normal }, () => {
// 这里的opacityValue不可以和上一次设置的终止0.1相同
this.opacityValue = 0.2;
});
this.isRecording = false;
// 再创建需要的动画
this.getUIContext().animateTo({ duration: 0, iterations: 1, playMode: PlayMode.Normal }, () => {
this.opacityValue = 1;
});
} else {
this.isRecording = true;
this.getUIContext().animateTo({ duration: 1500, iterations: -1, }, () => {
this.opacityValue = 0.1;
});
}
});
}
.width('100%');
}
.height('100%');
}
}
https://sites.google.com/view/0kqtje63/home
https://sites.google.com/view/2yuici15/home
https://sites.google.com/view/2xujlh37/home
https://sites.google.com/view/3ahalq09/home
https://sites.google.com/view/3tqjcf61/home
https://sites.google.com/view/6jajrr88/home
https://sites.google.com/view/9ghecz74/home
https://sites.google.com/view/8qlhiy40/home
https://sites.google.com/view/6sknwb20/home
https://sites.google.com/view/2bwlws24/home
https://sites.google.com/view/2blczd15/home
https://sites.google.com/view/1lmiwz57/home
https://sites.google.com/view/7htwsc76/home
https://sites.google.com/view/3chktr81/home
https://sites.google.com/view/1rejbt07/home
https://sites.google.com/view/6zfyyu19/home
https://sites.google.com/view/9pcdic70/home
https://sites.google.com/view/0wcdbm94/home
https://sites.google.com/view/2yddpg19/home
https://sites.google.com/view/7sevuo10/home
https://sites.google.com/view/6incag10/home
https://sites.google.com/view/4bbfak54/home
https://sites.google.com/view/1wioqp25/home
https://sites.google.com/view/8vhssf95/home
https://sites.google.com/view/9eekvo81/home
https://sites.google.com/view/8qprfl23/home
https://sites.google.com/view/4wvrav40/home
https://sites.google.com/view/8legom73/home
https://sites.google.com/view/2kwmgy35/home
https://sites.google.com/view/0nkzdh51/home
https://sites.google.com/view/2kucvr93/home
https://sites.google.com/view/3csonu11/home
https://sites.google.com/view/3cajfi21/home
https://sites.google.com/view/2ysddp77/home
https://sites.google.com/view/5cqzyg62/home
https://sites.google.com/view/6waqdl39/home
https://sites.google.com/view/9qwaeq72/home
https://sites.google.com/view/1yknjw85/home
https://sites.google.com/view/6vaarx10/home
https://sites.google.com/view/7ltamt18/home
https://sites.google.com/view/3nrfrt86/home
https://sites.google.com/view/8rzfds48/home
https://sites.google.com/view/2vrecv27/home
https://sites.google.com/view/8pgryd86/home
https://sites.google.com/view/9ukcxw90/home
https://sites.google.com/view/6szkbf52/home
https://sites.google.com/view/1mvila90/home
https://sites.google.com/view/9eytcv61/home
https://sites.google.com/view/4cnnur37/home
https://sites.google.com/view/5uvpgz70/home
https://sites.google.com/view/9wnykm74/home
https://sites.google.com/view/1maebc71/home
https://sites.google.com/view/7bjvba88/home
https://sites.google.com/view/6yxdgq20/home
https://sites.google.com/view/6oplhm39/home
https://sites.google.com/view/0gslqp42/home
https://sites.google.com/view/2fobzs04/home
https://sites.google.com/view/5geezf47/home
https://sites.google.com/view/6damyk90/home
https://sites.google.com/view/8txifb96/home
https://sites.google.com/view/4uycua91/home
https://sites.google.com/view/4qahte14/home
https://sites.google.com/view/3hmwmu69/home
https://sites.google.com/view/2dqgbe78/home
https://sites.google.com/view/4ogacg47/home
https://sites.google.com/view/0zvppg85/home
https://sites.google.com/view/8bsjqs81/home
https://sites.google.com/view/0yhduz67/home
https://sites.google.com/view/0nvpde36/home
https://sites.google.com/view/4svrfc10/in%C3%ADcio
https://sites.google.com/view/4gjwxj46/in%C3%ADcio
https://sites.google.com/view/0xejpo69/in%C3%ADcio
https://sites.google.com/view/4eizzb39/in%C3%ADcio
https://sites.google.com/view/8cglkg37/in%C3%ADcio
https://sites.google.com/view/2poltf08/in%C3%ADcio
https://sites.google.com/view/6ggdzs74/in%C3%ADcio
https://sites.google.com/view/7rpmpu57/in%C3%ADcio
https://sites.google.com/view/6gkcjo69/in%C3%ADcio
https://sites.google.com/view/5jgxxq89/in%C3%ADcio

继续阅读 »

问题现象
第一次触发无限循环动画后,快速连续多次点击打断该无限循环动画,再次点击就无法看到动画了。

背景知识
animateTo:指定由于闭包代码导致的状态变化插入过渡动效。接口参数有两个,分别是value和event,其中value指定AnimateParam对象(包括时长、Curve等)event为动画的闭包函数,闭包内变量改变产生的属性动画将遵循相同的动画参数。

问题定位
对动画的执行机制进行排查,确认在动画打断过程中是否存在多次叠加的情况,从而导致动画未能正常显示。

分析结论
动画并未真正消失,而是在每次打断过程中被持续叠加,由于叠加的动画实例过多,彼此之间相互覆盖或干扰,导致在视觉上表现不明显,从而给人以动画消失的错觉。

修改结论
首先,应设置一个duration为0的动画,用于确保在每次动画被中断时,能够清除前一次的动画实例,避免动画叠加;该动画的属性值需设置为一个与上一次动画最终状态不同的指定值,以确保状态的更新和正确清除。随后,再创建一个用于实现所需初始动画效果的动画实例,以确保动画表现符合预期。

if (this.isRecording) {
// 设置一个duration为0的动画停掉上一次动画
this.getUIContext().animateTo({ duration: 0, iterations: 1, playMode: PlayMode.Normal }, () => {
// 这里的opacityValue不可以和上一次设置的终止0.1相同
this.opacityValue = 0.2;
});
this.isRecording = false;
// 再创建需要的动画
this.getUIContext().animateTo({ duration: 0, iterations: 1, playMode: PlayMode.Normal }, () => {
this.opacityValue = 1;
});
} else {
this.isRecording = true;
this.getUIContext().animateTo({ duration: 1500, iterations: -1, }, () => {
this.opacityValue = 0.1;
});
}
完整示例参考如下:

@Entry
@Component
struct Page {
@State opacityValue: number = 1;
@State isRecording: boolean = false;

build() {
Row() {
Column() {
Text(this.isRecording ? 'Hello World' : 'Welcome')
.fontSize(50)
.fontWeight(FontWeight.Bold)
.opacity(this.opacityValue)
.textAlign(TextAlign.Center)
.fontColor('#000')
.onClick(() => {
if (this.isRecording) {
// 设置一个duration为0的动画停掉上一次动画
this.getUIContext().animateTo({ duration: 0, iterations: 1, playMode: PlayMode.Normal }, () => {
// 这里的opacityValue不可以和上一次设置的终止0.1相同
this.opacityValue = 0.2;
});
this.isRecording = false;
// 再创建需要的动画
this.getUIContext().animateTo({ duration: 0, iterations: 1, playMode: PlayMode.Normal }, () => {
this.opacityValue = 1;
});
} else {
this.isRecording = true;
this.getUIContext().animateTo({ duration: 1500, iterations: -1, }, () => {
this.opacityValue = 0.1;
});
}
});
}
.width('100%');
}
.height('100%');
}
}
https://sites.google.com/view/0kqtje63/home
https://sites.google.com/view/2yuici15/home
https://sites.google.com/view/2xujlh37/home
https://sites.google.com/view/3ahalq09/home
https://sites.google.com/view/3tqjcf61/home
https://sites.google.com/view/6jajrr88/home
https://sites.google.com/view/9ghecz74/home
https://sites.google.com/view/8qlhiy40/home
https://sites.google.com/view/6sknwb20/home
https://sites.google.com/view/2bwlws24/home
https://sites.google.com/view/2blczd15/home
https://sites.google.com/view/1lmiwz57/home
https://sites.google.com/view/7htwsc76/home
https://sites.google.com/view/3chktr81/home
https://sites.google.com/view/1rejbt07/home
https://sites.google.com/view/6zfyyu19/home
https://sites.google.com/view/9pcdic70/home
https://sites.google.com/view/0wcdbm94/home
https://sites.google.com/view/2yddpg19/home
https://sites.google.com/view/7sevuo10/home
https://sites.google.com/view/6incag10/home
https://sites.google.com/view/4bbfak54/home
https://sites.google.com/view/1wioqp25/home
https://sites.google.com/view/8vhssf95/home
https://sites.google.com/view/9eekvo81/home
https://sites.google.com/view/8qprfl23/home
https://sites.google.com/view/4wvrav40/home
https://sites.google.com/view/8legom73/home
https://sites.google.com/view/2kwmgy35/home
https://sites.google.com/view/0nkzdh51/home
https://sites.google.com/view/2kucvr93/home
https://sites.google.com/view/3csonu11/home
https://sites.google.com/view/3cajfi21/home
https://sites.google.com/view/2ysddp77/home
https://sites.google.com/view/5cqzyg62/home
https://sites.google.com/view/6waqdl39/home
https://sites.google.com/view/9qwaeq72/home
https://sites.google.com/view/1yknjw85/home
https://sites.google.com/view/6vaarx10/home
https://sites.google.com/view/7ltamt18/home
https://sites.google.com/view/3nrfrt86/home
https://sites.google.com/view/8rzfds48/home
https://sites.google.com/view/2vrecv27/home
https://sites.google.com/view/8pgryd86/home
https://sites.google.com/view/9ukcxw90/home
https://sites.google.com/view/6szkbf52/home
https://sites.google.com/view/1mvila90/home
https://sites.google.com/view/9eytcv61/home
https://sites.google.com/view/4cnnur37/home
https://sites.google.com/view/5uvpgz70/home
https://sites.google.com/view/9wnykm74/home
https://sites.google.com/view/1maebc71/home
https://sites.google.com/view/7bjvba88/home
https://sites.google.com/view/6yxdgq20/home
https://sites.google.com/view/6oplhm39/home
https://sites.google.com/view/0gslqp42/home
https://sites.google.com/view/2fobzs04/home
https://sites.google.com/view/5geezf47/home
https://sites.google.com/view/6damyk90/home
https://sites.google.com/view/8txifb96/home
https://sites.google.com/view/4uycua91/home
https://sites.google.com/view/4qahte14/home
https://sites.google.com/view/3hmwmu69/home
https://sites.google.com/view/2dqgbe78/home
https://sites.google.com/view/4ogacg47/home
https://sites.google.com/view/0zvppg85/home
https://sites.google.com/view/8bsjqs81/home
https://sites.google.com/view/0yhduz67/home
https://sites.google.com/view/0nvpde36/home
https://sites.google.com/view/4svrfc10/in%C3%ADcio
https://sites.google.com/view/4gjwxj46/in%C3%ADcio
https://sites.google.com/view/0xejpo69/in%C3%ADcio
https://sites.google.com/view/4eizzb39/in%C3%ADcio
https://sites.google.com/view/8cglkg37/in%C3%ADcio
https://sites.google.com/view/2poltf08/in%C3%ADcio
https://sites.google.com/view/6ggdzs74/in%C3%ADcio
https://sites.google.com/view/7rpmpu57/in%C3%ADcio
https://sites.google.com/view/6gkcjo69/in%C3%ADcio
https://sites.google.com/view/5jgxxq89/in%C3%ADcio

收起阅读 »

组件实现随手拖动松手后贴边效果

问题现象
如何实现手指长按组件时,组件能够跟随手指拖动,手指抬起后,组件贴边停靠的效果。

背景知识
onTouch是用于处理触摸事件的组件属性,支持TouchType.Down(按下)、TouchType.Move(移动)、TouchType.Up(抬起),用于多模交互场景(如拖拽、点击、滑动等)。
animateTo是用于实现动画效果的核心函数,支持平滑过渡、弹性动画、曲线控制等。
解决方案
为实现上述功能,可通过如下步骤实现:

通过组件的触摸事件onTouch,其中使用TouchType.Down记录手指按下时的初始坐标,使用TouchType.Move获取手指移动时的位置信息,通过获取的位置信息修改组件的position属性实现元素的拖拽功能:
// 指针按下事件:记录初始坐标
case TouchType.Down: {
this.windowStartX = event.touches[0].windowX;
this.windowStartY = event.touches[0].windowY;
break;
}
case TouchType.Move: {
const windowX: number = event.touches[0].windowX;
const windowY: number = event.touches[0].windowY;
// 左边距吸附时:更新左边缘位置
if (this.edge.left !== undefined) {
this.edge.left = this.edge.left as number + (windowX - this.windowStartX);
}
// 右边距吸附时:更新右边缘位置
else {
this.edge.right = this.edge.right as number - (windowX - this.windowStartX);
}
// 更新顶部位置
this.edge.top = this.edge.top as number + (windowY - this.windowStartY);
// 更新起始坐标用于下次计算
this.windowStartX = windowX;
this.windowStartY = windowY;
break;
}
再通过TouchType.Up(手指抬起),对比手指抬起时组件所处位置,与容器组件的中心位置,判断出需要向哪边进行贴边操作,或与容器元素宽度进行对比,判断元素是否超出容器,选择将组件position属性的left或right设置为undefined并将其对立方向设置为0,且位置的调整设置于animateTo中,实现将元素从容器内部或容器外部缓慢调整至容器边缘,从而实现组件拖拽后的贴边效果:
case TouchType.Up: {
// 计算悬浮窗中心点在父组件中的水平坐标
let centerX: number;
if (this.edge.left !== undefined) {
centerX = this.edge.left as number + this.floatWindowWidth / 2;
} else {
centerX = this.containerWidth - (this.edge.right as number) - this.floatWindowWidth / 2;
}
// 使用阻尼动画实现吸附效果
this.uiContext.animateTo({ curve: curves.springMotion() }, () => {
// 获取屏幕宽度
let width = this.uiContext.px2vp(display.getDefaultDisplaySync().width);
// 启用吸附功能时
if (this.openAdsorb) {
// 根据中心点位置决定吸附方向
if (centerX > (this.containerWidth / 2)) {
// 右侧吸附
this.edge.right = this.pagePadding;
this.edge.left = undefined;
} else {
// 左侧吸附
this.edge.right = undefined;
this.edge.left = this.pagePadding;
}
} else {
// 检查是否超出左侧屏幕
let overflowLeft = this.edge.right! > width - this.floatWindowWidth;
// 检查是否超出右侧屏幕
let overflowRight = this.edge.right! < 0;
// 处理超出屏幕边界的情况
if (overflowLeft) { // 超出左边屏幕
this.edge.left = undefined;
this.edge.right = width - this.floatWindowWidth;
} else if (overflowRight) { // 超出右边屏幕
this.edge.right = this.pagePadding;
this.edge.left = undefined;
}
}
})
break;
}
完整实现如下:

import { curves, display } from '@kit.ArkUI';

@Component
export struct FloatWindowView {
@State edge: Edges = { top: 200, left: 0 };
@Link containerWidth: number;
@Link containerHeight: number;
private windowStartX: number = 0;
private windowStartY: number = 0;
// 是否启用吸附功能
openAdsorb: boolean = true;
@Prop pagePadding: number = 0; // 页面内容内边距,用于悬浮窗位置计算
@State floatWindowWidth: number = 100; // 悬浮窗宽度
@State floatWindowHeight: number = 50; // 悬浮窗高度
uiContext: UIContext = this.getUIContext();

// 触摸回调,实现悬浮窗跟手拖拽和贴边吸附动画
onTouchEvent(event: TouchEvent): void {
switch (event.type) {
// 指针按下事件:记录初始坐标
case TouchType.Down: {
this.windowStartX = event.touches[0].windowX;
this.windowStartY = event.touches[0].windowY;
break;
}
case TouchType.Move: {
const windowX: number = event.touches[0].windowX;
const windowY: number = event.touches[0].windowY;
// 左边距吸附时:更新左边缘位置
if (this.edge.left !== undefined) {
this.edge.left = this.edge.left as number + (windowX - this.windowStartX);
}
// 右边距吸附时:更新右边缘位置
else {
this.edge.right = this.edge.right as number - (windowX - this.windowStartX);
}
// 更新顶部位置
this.edge.top = this.edge.top as number + (windowY - this.windowStartY);
// 更新起始坐标用于下次计算
this.windowStartX = windowX;
this.windowStartY = windowY;
break;
}
// 指针抬起事件:实现吸附动画和边界限制
case TouchType.Up: {
// 计算悬浮窗中心点在父组件中的水平坐标
let centerX: number;
if (this.edge.left !== undefined) {
centerX = this.edge.left as number + this.floatWindowWidth / 2;
} else {
centerX = this.containerWidth - (this.edge.right as number) - this.floatWindowWidth / 2;
}
// 使用阻尼动画实现吸附效果
this.uiContext.animateTo({ curve: curves.springMotion() }, () => {
// 获取屏幕宽度
let width = this.uiContext.px2vp(display.getDefaultDisplaySync().width);
// 启用吸附功能时
if (this.openAdsorb) {
// 根据中心点位置决定吸附方向
if (centerX > (this.containerWidth / 2)) {
// 右侧吸附
this.edge.right = this.pagePadding;
this.edge.left = undefined;
} else {
// 左侧吸附
this.edge.right = undefined;
this.edge.left = this.pagePadding;
}
} else {
// 检查是否超出左侧屏幕
let overflowLeft = this.edge.right! > width - this.floatWindowWidth;
// 检查是否超出右侧屏幕
let overflowRight = this.edge.right! < 0;
// 处理超出屏幕边界的情况
if (overflowLeft) { // 超出左边屏幕
this.edge.left = undefined;
this.edge.right = width - this.floatWindowWidth;
} else if (overflowRight) { // 超出右边屏幕
this.edge.right = this.pagePadding;
this.edge.left = undefined;
}
}
})
break;
}
default: {
break;
}
}
}

build() {
Column() {
}
.clip(true)
.backgroundColor('#0A59F7')
.width(this.floatWindowWidth)
.height(this.floatWindowHeight)
.position(this.edge)
.onTouch((event: TouchEvent) => {
this.onTouchEvent(event);
})
}
}

@Entry
@Component
struct Index {
// 父组件宽度
@State containerWidth: number = 0;
// 父组件高度
@State containerHeight: number = 0;

build() {
Stack() {
FloatWindowView({
containerWidth: this.containerWidth, // 传递父容器宽度
containerHeight: this.containerHeight, // 传递父容器高度
})
.width('100%')
}
// 设置外层Stack容器的尺寸
.height('100%')
.width('100%')

.onAreaChange((oldValue: Area, newValue: Area) => {  
  // 记录父组件的宽高  
  if (oldValue.width !== newValue.width) {  
    this.containerWidth = newValue.width as number;  
  }  

  if (oldValue.height !== newValue.height) {  
    this.containerHeight = newValue.height as number;  
  }  
})  

}
}
https://sites.google.com/view/8ojcyz21/home
https://sites.google.com/view/8votdo34/home
https://sites.google.com/view/2glnsx28/home
https://sites.google.com/view/5cljni07/home
https://sites.google.com/view/8prkhh07/home
https://sites.google.com/view/7khplo30/home
https://sites.google.com/view/1iwwoj29/home
https://sites.google.com/view/5lokbt20/home
https://sites.google.com/view/8ucjap35/home
https://sites.google.com/view/3vmwqk28/home
https://sites.google.com/view/4hqmfe46/home
https://sites.google.com/view/8jjhsd05/home
https://sites.google.com/view/2ajqbl31/home
https://sites.google.com/view/8eroul90/home
https://sites.google.com/view/8whpgb50/home
https://sites.google.com/view/7fhwgy79/home
https://sites.google.com/view/8otxzd32/home
https://sites.google.com/view/0skklh79/home
https://sites.google.com/view/7epixg32/home
https://sites.google.com/view/2fdymh76/home
https://sites.google.com/view/1qzbbd10/home
https://sites.google.com/view/3betru91/home
https://sites.google.com/view/0fjymy02/home
https://sites.google.com/view/1tuyht61/home
https://sites.google.com/view/3lqsci93/home
https://sites.google.com/view/4moosi57/home
https://sites.google.com/view/0ebmiw84/home
https://sites.google.com/view/4usaol83/home
https://sites.google.com/view/0hknci80/home
https://sites.google.com/view/7ldcgt68/home
https://sites.google.com/view/8emzek60/home
https://sites.google.com/view/9oaldi75/home
https://sites.google.com/view/1hujlx46/home
https://sites.google.com/view/9krrdx96/home
https://sites.google.com/view/0rdwwl74/home
https://sites.google.com/view/4oalem69/home
https://sites.google.com/view/7tpgsy97/home
https://sites.google.com/view/4sjvkh25/home
https://sites.google.com/view/6ixmrs26/home
https://sites.google.com/view/7jjcws23/home
https://sites.google.com/view/4entvi31/home
https://sites.google.com/view/0hqtsy80/home
https://sites.google.com/view/2vthve29/home
https://sites.google.com/view/2wjwhq06/home
https://sites.google.com/view/7lnlrg38/home
https://sites.google.com/view/3jwoff92/home
https://sites.google.com/view/9exfcx28/home
https://sites.google.com/view/6ayynm56/home
https://sites.google.com/view/2mkert04/home
https://sites.google.com/view/9bfpha34/home
https://sites.google.com/view/7kwinp20/home
https://sites.google.com/view/0kzpsl33/home
https://sites.google.com/view/7vqzug72/home
https://sites.google.com/view/8qybcb91/home
https://sites.google.com/view/1manil34/home
https://sites.google.com/view/9zlzlw68/home
https://sites.google.com/view/7ytpgf83/home
https://sites.google.com/view/0figtg65/home
https://sites.google.com/view/3nphmi02/home
https://sites.google.com/view/0qkeih69/home
https://sites.google.com/view/1qqdjc96/home
https://sites.google.com/view/2ocmzl47/home
https://sites.google.com/view/0rszvm31/home
https://sites.google.com/view/8twpcf39/home
https://sites.google.com/view/1tmhcz01/home
https://sites.google.com/view/1fsimv87/home
https://sites.google.com/view/0krela31/home
https://sites.google.com/view/8jhshd10/home
https://sites.google.com/view/5tseou06/home
https://sites.google.com/view/2msyts76/home
https://sites.google.com/view/6dxuri73/home
https://sites.google.com/view/1pmboa56/home
https://sites.google.com/view/9iueuv34/home
https://sites.google.com/view/8pubxw30/home
https://sites.google.com/view/1tlyco56/home
https://sites.google.com/view/8srvbu36/home
https://sites.google.com/view/4gsnto37/home
https://sites.google.com/view/6akswp56/home
https://sites.google.com/view/2inxol47/home
https://sites.google.com/view/3oojsn55/home
https://sites.google.com/view/5mdqwb17/home
https://sites.google.com/view/7xuice12/home
https://sites.google.com/view/0qzfci43/home
https://sites.google.com/view/6ohonc26/home
https://sites.google.com/view/4tbmuv32/home
https://sites.google.com/view/1rbxss83/home
https://sites.google.com/view/2niebx01/home
https://sites.google.com/view/3qaeqy35/home
https://sites.google.com/view/0cbewx04/home
https://sites.google.com/view/3csyuh21/home
https://sites.google.com/view/8wylyx13/home
https://sites.google.com/view/1kpxse38/home
https://sites.google.com/view/8ovanb09/home
https://sites.google.com/view/6rmoih50/home
https://sites.google.com/view/5cfhmg25/home
https://sites.google.com/view/4ivxrt19/home
https://sites.google.com/view/4ltzqb02/home
https://sites.google.com/view/3vdlsy06/home
https://sites.google.com/view/5vwlbj78/home
https://sites.google.com/view/4naisw60/home

继续阅读 »

问题现象
如何实现手指长按组件时,组件能够跟随手指拖动,手指抬起后,组件贴边停靠的效果。

背景知识
onTouch是用于处理触摸事件的组件属性,支持TouchType.Down(按下)、TouchType.Move(移动)、TouchType.Up(抬起),用于多模交互场景(如拖拽、点击、滑动等)。
animateTo是用于实现动画效果的核心函数,支持平滑过渡、弹性动画、曲线控制等。
解决方案
为实现上述功能,可通过如下步骤实现:

通过组件的触摸事件onTouch,其中使用TouchType.Down记录手指按下时的初始坐标,使用TouchType.Move获取手指移动时的位置信息,通过获取的位置信息修改组件的position属性实现元素的拖拽功能:
// 指针按下事件:记录初始坐标
case TouchType.Down: {
this.windowStartX = event.touches[0].windowX;
this.windowStartY = event.touches[0].windowY;
break;
}
case TouchType.Move: {
const windowX: number = event.touches[0].windowX;
const windowY: number = event.touches[0].windowY;
// 左边距吸附时:更新左边缘位置
if (this.edge.left !== undefined) {
this.edge.left = this.edge.left as number + (windowX - this.windowStartX);
}
// 右边距吸附时:更新右边缘位置
else {
this.edge.right = this.edge.right as number - (windowX - this.windowStartX);
}
// 更新顶部位置
this.edge.top = this.edge.top as number + (windowY - this.windowStartY);
// 更新起始坐标用于下次计算
this.windowStartX = windowX;
this.windowStartY = windowY;
break;
}
再通过TouchType.Up(手指抬起),对比手指抬起时组件所处位置,与容器组件的中心位置,判断出需要向哪边进行贴边操作,或与容器元素宽度进行对比,判断元素是否超出容器,选择将组件position属性的left或right设置为undefined并将其对立方向设置为0,且位置的调整设置于animateTo中,实现将元素从容器内部或容器外部缓慢调整至容器边缘,从而实现组件拖拽后的贴边效果:
case TouchType.Up: {
// 计算悬浮窗中心点在父组件中的水平坐标
let centerX: number;
if (this.edge.left !== undefined) {
centerX = this.edge.left as number + this.floatWindowWidth / 2;
} else {
centerX = this.containerWidth - (this.edge.right as number) - this.floatWindowWidth / 2;
}
// 使用阻尼动画实现吸附效果
this.uiContext.animateTo({ curve: curves.springMotion() }, () => {
// 获取屏幕宽度
let width = this.uiContext.px2vp(display.getDefaultDisplaySync().width);
// 启用吸附功能时
if (this.openAdsorb) {
// 根据中心点位置决定吸附方向
if (centerX > (this.containerWidth / 2)) {
// 右侧吸附
this.edge.right = this.pagePadding;
this.edge.left = undefined;
} else {
// 左侧吸附
this.edge.right = undefined;
this.edge.left = this.pagePadding;
}
} else {
// 检查是否超出左侧屏幕
let overflowLeft = this.edge.right! > width - this.floatWindowWidth;
// 检查是否超出右侧屏幕
let overflowRight = this.edge.right! < 0;
// 处理超出屏幕边界的情况
if (overflowLeft) { // 超出左边屏幕
this.edge.left = undefined;
this.edge.right = width - this.floatWindowWidth;
} else if (overflowRight) { // 超出右边屏幕
this.edge.right = this.pagePadding;
this.edge.left = undefined;
}
}
})
break;
}
完整实现如下:

import { curves, display } from '@kit.ArkUI';

@Component
export struct FloatWindowView {
@State edge: Edges = { top: 200, left: 0 };
@Link containerWidth: number;
@Link containerHeight: number;
private windowStartX: number = 0;
private windowStartY: number = 0;
// 是否启用吸附功能
openAdsorb: boolean = true;
@Prop pagePadding: number = 0; // 页面内容内边距,用于悬浮窗位置计算
@State floatWindowWidth: number = 100; // 悬浮窗宽度
@State floatWindowHeight: number = 50; // 悬浮窗高度
uiContext: UIContext = this.getUIContext();

// 触摸回调,实现悬浮窗跟手拖拽和贴边吸附动画
onTouchEvent(event: TouchEvent): void {
switch (event.type) {
// 指针按下事件:记录初始坐标
case TouchType.Down: {
this.windowStartX = event.touches[0].windowX;
this.windowStartY = event.touches[0].windowY;
break;
}
case TouchType.Move: {
const windowX: number = event.touches[0].windowX;
const windowY: number = event.touches[0].windowY;
// 左边距吸附时:更新左边缘位置
if (this.edge.left !== undefined) {
this.edge.left = this.edge.left as number + (windowX - this.windowStartX);
}
// 右边距吸附时:更新右边缘位置
else {
this.edge.right = this.edge.right as number - (windowX - this.windowStartX);
}
// 更新顶部位置
this.edge.top = this.edge.top as number + (windowY - this.windowStartY);
// 更新起始坐标用于下次计算
this.windowStartX = windowX;
this.windowStartY = windowY;
break;
}
// 指针抬起事件:实现吸附动画和边界限制
case TouchType.Up: {
// 计算悬浮窗中心点在父组件中的水平坐标
let centerX: number;
if (this.edge.left !== undefined) {
centerX = this.edge.left as number + this.floatWindowWidth / 2;
} else {
centerX = this.containerWidth - (this.edge.right as number) - this.floatWindowWidth / 2;
}
// 使用阻尼动画实现吸附效果
this.uiContext.animateTo({ curve: curves.springMotion() }, () => {
// 获取屏幕宽度
let width = this.uiContext.px2vp(display.getDefaultDisplaySync().width);
// 启用吸附功能时
if (this.openAdsorb) {
// 根据中心点位置决定吸附方向
if (centerX > (this.containerWidth / 2)) {
// 右侧吸附
this.edge.right = this.pagePadding;
this.edge.left = undefined;
} else {
// 左侧吸附
this.edge.right = undefined;
this.edge.left = this.pagePadding;
}
} else {
// 检查是否超出左侧屏幕
let overflowLeft = this.edge.right! > width - this.floatWindowWidth;
// 检查是否超出右侧屏幕
let overflowRight = this.edge.right! < 0;
// 处理超出屏幕边界的情况
if (overflowLeft) { // 超出左边屏幕
this.edge.left = undefined;
this.edge.right = width - this.floatWindowWidth;
} else if (overflowRight) { // 超出右边屏幕
this.edge.right = this.pagePadding;
this.edge.left = undefined;
}
}
})
break;
}
default: {
break;
}
}
}

build() {
Column() {
}
.clip(true)
.backgroundColor('#0A59F7')
.width(this.floatWindowWidth)
.height(this.floatWindowHeight)
.position(this.edge)
.onTouch((event: TouchEvent) => {
this.onTouchEvent(event);
})
}
}

@Entry
@Component
struct Index {
// 父组件宽度
@State containerWidth: number = 0;
// 父组件高度
@State containerHeight: number = 0;

build() {
Stack() {
FloatWindowView({
containerWidth: this.containerWidth, // 传递父容器宽度
containerHeight: this.containerHeight, // 传递父容器高度
})
.width('100%')
}
// 设置外层Stack容器的尺寸
.height('100%')
.width('100%')

.onAreaChange((oldValue: Area, newValue: Area) => {  
  // 记录父组件的宽高  
  if (oldValue.width !== newValue.width) {  
    this.containerWidth = newValue.width as number;  
  }  

  if (oldValue.height !== newValue.height) {  
    this.containerHeight = newValue.height as number;  
  }  
})  

}
}
https://sites.google.com/view/8ojcyz21/home
https://sites.google.com/view/8votdo34/home
https://sites.google.com/view/2glnsx28/home
https://sites.google.com/view/5cljni07/home
https://sites.google.com/view/8prkhh07/home
https://sites.google.com/view/7khplo30/home
https://sites.google.com/view/1iwwoj29/home
https://sites.google.com/view/5lokbt20/home
https://sites.google.com/view/8ucjap35/home
https://sites.google.com/view/3vmwqk28/home
https://sites.google.com/view/4hqmfe46/home
https://sites.google.com/view/8jjhsd05/home
https://sites.google.com/view/2ajqbl31/home
https://sites.google.com/view/8eroul90/home
https://sites.google.com/view/8whpgb50/home
https://sites.google.com/view/7fhwgy79/home
https://sites.google.com/view/8otxzd32/home
https://sites.google.com/view/0skklh79/home
https://sites.google.com/view/7epixg32/home
https://sites.google.com/view/2fdymh76/home
https://sites.google.com/view/1qzbbd10/home
https://sites.google.com/view/3betru91/home
https://sites.google.com/view/0fjymy02/home
https://sites.google.com/view/1tuyht61/home
https://sites.google.com/view/3lqsci93/home
https://sites.google.com/view/4moosi57/home
https://sites.google.com/view/0ebmiw84/home
https://sites.google.com/view/4usaol83/home
https://sites.google.com/view/0hknci80/home
https://sites.google.com/view/7ldcgt68/home
https://sites.google.com/view/8emzek60/home
https://sites.google.com/view/9oaldi75/home
https://sites.google.com/view/1hujlx46/home
https://sites.google.com/view/9krrdx96/home
https://sites.google.com/view/0rdwwl74/home
https://sites.google.com/view/4oalem69/home
https://sites.google.com/view/7tpgsy97/home
https://sites.google.com/view/4sjvkh25/home
https://sites.google.com/view/6ixmrs26/home
https://sites.google.com/view/7jjcws23/home
https://sites.google.com/view/4entvi31/home
https://sites.google.com/view/0hqtsy80/home
https://sites.google.com/view/2vthve29/home
https://sites.google.com/view/2wjwhq06/home
https://sites.google.com/view/7lnlrg38/home
https://sites.google.com/view/3jwoff92/home
https://sites.google.com/view/9exfcx28/home
https://sites.google.com/view/6ayynm56/home
https://sites.google.com/view/2mkert04/home
https://sites.google.com/view/9bfpha34/home
https://sites.google.com/view/7kwinp20/home
https://sites.google.com/view/0kzpsl33/home
https://sites.google.com/view/7vqzug72/home
https://sites.google.com/view/8qybcb91/home
https://sites.google.com/view/1manil34/home
https://sites.google.com/view/9zlzlw68/home
https://sites.google.com/view/7ytpgf83/home
https://sites.google.com/view/0figtg65/home
https://sites.google.com/view/3nphmi02/home
https://sites.google.com/view/0qkeih69/home
https://sites.google.com/view/1qqdjc96/home
https://sites.google.com/view/2ocmzl47/home
https://sites.google.com/view/0rszvm31/home
https://sites.google.com/view/8twpcf39/home
https://sites.google.com/view/1tmhcz01/home
https://sites.google.com/view/1fsimv87/home
https://sites.google.com/view/0krela31/home
https://sites.google.com/view/8jhshd10/home
https://sites.google.com/view/5tseou06/home
https://sites.google.com/view/2msyts76/home
https://sites.google.com/view/6dxuri73/home
https://sites.google.com/view/1pmboa56/home
https://sites.google.com/view/9iueuv34/home
https://sites.google.com/view/8pubxw30/home
https://sites.google.com/view/1tlyco56/home
https://sites.google.com/view/8srvbu36/home
https://sites.google.com/view/4gsnto37/home
https://sites.google.com/view/6akswp56/home
https://sites.google.com/view/2inxol47/home
https://sites.google.com/view/3oojsn55/home
https://sites.google.com/view/5mdqwb17/home
https://sites.google.com/view/7xuice12/home
https://sites.google.com/view/0qzfci43/home
https://sites.google.com/view/6ohonc26/home
https://sites.google.com/view/4tbmuv32/home
https://sites.google.com/view/1rbxss83/home
https://sites.google.com/view/2niebx01/home
https://sites.google.com/view/3qaeqy35/home
https://sites.google.com/view/0cbewx04/home
https://sites.google.com/view/3csyuh21/home
https://sites.google.com/view/8wylyx13/home
https://sites.google.com/view/1kpxse38/home
https://sites.google.com/view/8ovanb09/home
https://sites.google.com/view/6rmoih50/home
https://sites.google.com/view/5cfhmg25/home
https://sites.google.com/view/4ivxrt19/home
https://sites.google.com/view/4ltzqb02/home
https://sites.google.com/view/3vdlsy06/home
https://sites.google.com/view/5vwlbj78/home
https://sites.google.com/view/4naisw60/home

收起阅读 »

组件截图(ComponentSnapshot)返回错误码100001,可能原因为截图尺寸过大,文档说明其与具体硬件限制有关,如何查看具体限制是多少

硬件限制因平台而异,可以通过如下命令进行查看:

hdc shell hidumper -s 10 -a 'vktextureLimit'
常见值为“width: 8192 height: 8192”,表示最大绘制纹理尺寸的长宽都需要在8192像素以内。比较待截图组件的尺寸,以便确认截图失败是否为该原因导致,如果是,请调整所截图组件的大小,或实现为滚动截图后自行拼接。实现请参考截取长内容(滚动截图)和长截图。

如需实现离屏组件的长截图,可参考以下实现:

// src/main/ets/utils/Utils.ets
import { image } from '@kit.ImageKit';
import { BusinessError } from '@kit.BasicServicesKit';

export class Utils {
static sleep(ms: number): Promise<void> {
return new Promise(resolve => setTimeout(resolve, ms));
}

// Calculate the valid screenshot area
static async getSnapshotArea(context: UIContext, pixelMap: PixelMap, scrollYOffsets: number[], listWidth: number,
listHeight: number): Promise<image.PositionArea> {
let stride = pixelMap.getBytesNumberPerRow();
let bytesNumber = pixelMap.getPixelBytesNumber();
let buffer: ArrayBuffer = new ArrayBuffer(bytesNumber);
let len = scrollYOffsets.length;

if (scrollYOffsets.length >= 2) {  
  let realScrollHeight = scrollYOffsets[len-1] - scrollYOffsets[len-2];  
  if (listHeight - realScrollHeight > 0) {  
    let cropRegion: image.Region = {  
      x: 0,  
      y: context.vp2px(listHeight - realScrollHeight) || 0,  
      size: {  
        height: context.vp2px(realScrollHeight) || 0,  
        width: context.vp2px(listWidth) || 0  
      }  
    };  
    await pixelMap.crop(cropRegion);  
  }  
}  

let area: image.PositionArea = {  
  pixels: buffer,  
  offset: 0,  
  stride: stride,  
  region: {  
    size: {  
      width: 0,  
      height: 0  
    },  
    x: 0,  
    y: 0  
  }  
}  

try {  
  let imgInfo = pixelMap.getImageInfoSync();  
  area.region.size.width = imgInfo.size.width;  
  area.region.size.height = imgInfo.size.height;  
  pixelMap.readPixelsSync(area);  
} catch (err) {  
  let error = err as BusinessError;  
  console.error(`getSnapshotArea err, code:${error.code}, message: ${error.message}`);  
}  
return area;  

}

// Graphic splicing
static async mergeImage(context: UIContext, areaArray: image.PositionArea[], lastOffsetY: number, listWidth: number,
listHeight: number): Promise<PixelMap> {
let opts: image.InitializationOptions = {
editable: true,
pixelFormat: 4,
size: {
width: context.vp2px(listWidth) || 0,
height: context.vp2px(lastOffsetY + listHeight) || 0
}
};
let longPixelMap = image.createPixelMapSync(opts);
let imgPosition: number = 0;

for (let i = 0; i < areaArray.length; i++) {  
  let readArea = areaArray[i];  
  let area: image.PositionArea = {  
    pixels: readArea.pixels,  
    offset: 0,  
    stride: readArea.stride,  
    region: {  
      size: {  
        width: readArea.region.size.width,  
        height: readArea.region.size.height  
      },  
      x: 0,  
      y: imgPosition  
    }  
  }  
  imgPosition += readArea.region.size.height;  
  try {  
    longPixelMap.writePixelsSync(area);  
  } catch (err) {  
    let error = err as BusinessError;  
    console.error(`writePixelsSync err, code:${error.code}, message: ${error.message}`);  
  }  
}  
return longPixelMap;  

}
}
// src/main/ets/pages/Index.ets
import { image } from '@kit.ImageKit';
import { BusinessError } from '@kit.BasicServicesKit';
import { Utils } from '../utils/Utils';

export class MyDataSource {
private _data: string[] = [];
private _listeners: DataChangeListener[] = [];

pushData(data: string): void {
this._data.push(data);
this._listeners.forEach(listener => {
listener.onDataAdd(this._data.length - 1);
})
}

getAllData(): string[] {
return this._data;
}

totalCount(): number {
return this._data.length;
}

getData(index: number): string {
return this._data[index];
}

registerDataChangeListener(listener: DataChangeListener): void {
this._listeners.push(listener);
}

unregisterDataChangeListener(listener: DataChangeListener): void {
const index = this._listeners.indexOf(listener);
if (index != -1) {
this._listeners.splice(index, 1);
}
}
}

@Entry
@Component
struct SnapshotExample {
private scroller: Scroller = new Scroller();
private listComponentWidth: number = 0;
private listComponentHeight: number = 0;
@State mergedImage: PixelMap | undefined = undefined;
private areaArray: image.PositionArea[] = [];
private scrollYOffsets: number[] = [];
private data: MyDataSource = new MyDataSource();
private listId: string = 'LIST_ID';

aboutToAppear(): void {
for (let i = 0; i < 50; i++) {
this.data.pushData(Hello ${i});
}
}

async onceSnapshot() {
await this.beforeSnapshot();
await this.snapAndMerge();
this.afterGeneratorImage();
}

async snapAndMerge() {
try {
// Record the current scrolling position
this.scrollYOffsets.push(this.scroller.currentOffset().yOffset);
// Take a screenshot of the current display part of the component
const pixelMap = await this.getUIContext().getComponentSnapshot().get(this.listId);
// Calculate the valid screenshot area
let area: image.PositionArea =
await Utils.getSnapshotArea(this.getUIContext(), pixelMap, this.scrollYOffsets, this.listComponentWidth,
this.listComponentHeight);
this.areaArray.push(area);
// Determine whether to scroll to the bottom
if (!this.scroller.isAtEnd()) {
// Not to the bottom: Scroll down by one screen height
this.scroller.scrollTo({
xOffset: 0,
yOffset: (this.scroller.currentOffset().yOffset + this.listComponentHeight),
animation: {
duration: 200
}
});
await Utils.sleep(200);
await this.snapAndMerge();
} else {
this.mergedImage =
await Utils.mergeImage(this.getUIContext(), this.areaArray, this.scrollYOffsets[this.scrollYOffsets.length-1],
this.listComponentWidth, this.listComponentHeight);
}
} catch (err) {
let error = err as BusinessError;
console.error(snapAndMerge err, code:${error.code}, message: ${error.message});
}
}

async beforeSnapshot() {
try {
this.scroller.scrollTo({
xOffset: 0,
yOffset: 0,
animation: {
duration: 200
}
});
await Utils.sleep(200);
} catch (err) {
let error = err as BusinessError;
console.error(beforeSnapshot err, code:${error.code}, message: ${error.message});
}
}

afterGeneratorImage() {
this.scrollYOffsets.length = 0;
this.areaArray.length = 0;
}

build() {
Column({ space: 12 }) {
Button('Click to get the snapshot')
.onClick(() => {
this.onceSnapshot();
})
Stack() {
// Screenshot component
List({ space: 12, scroller: this.scroller }) {
LazyForEach(this.data, (item: string) => {
ListItem() {
Row() {
Text(item)
.fontSize(50)
.height(50)
}
}
}, (item: number) => item.toString())
}
.scrollBar(BarState.Off)
.cachedCount(3)
.width('100%')
.height('100%')
.backgroundColor($r('sys.color.background_secondary'))
.id(this.listId)
.onAreaChange((oldValue, newValue) => {
this.listComponentWidth = newValue.width as number;
this.listComponentHeight = newValue.height as number;
})
// Set the Z-sequence to -1 to ensure that this component is invisible
.zIndex(-1)

    // Use a mask to cover the screenshot area  
    Column()  
      .width('100%').height('100%').backgroundColor(Color.White)  
    // Long screenshot  
    Scroll() {  
      Image(this.mergedImage)  
    }  
  }  
  .width('100%')  
  .layoutWeight(1)  
}  

}
}
https://sites.google.com/view/4qpzqf30/home
https://sites.google.com/view/1ggrec61/home
https://sites.google.com/view/7dqpdy74/home
https://sites.google.com/view/5bcwfb15/home
https://sites.google.com/view/3ccqme12/home
https://sites.google.com/view/9fauxq30/home
https://sites.google.com/view/0cskdu29/home
https://sites.google.com/view/7ndhuv19/home
https://sites.google.com/view/2qfdbs75/home
https://sites.google.com/view/7tbfwn54/home
https://sites.google.com/view/8utwqh99/home
https://sites.google.com/view/8tmfsn03/home
https://sites.google.com/view/7hyniw30/home
https://sites.google.com/view/5vcrza32/home
https://sites.google.com/view/9bicoh33/home
https://sites.google.com/view/8mykyr89/home
https://sites.google.com/view/9dkwex15/home
https://sites.google.com/view/3jdidd23/home
https://sites.google.com/view/4jnbko13/home
https://sites.google.com/view/3hiqwy37/home
https://sites.google.com/view/9sdhji16/home
https://sites.google.com/view/9vjvuk08/home
https://sites.google.com/view/4vtgzg10/home
https://sites.google.com/view/9simwj81/home
https://sites.google.com/view/2fhkhe54/home
https://sites.google.com/view/3ifqol91/home
https://sites.google.com/view/5oaygy48/home
https://sites.google.com/view/9uzuby80/home
https://sites.google.com/view/4mfobg52/home
https://sites.google.com/view/7lmnbx80/home
https://sites.google.com/view/7zieer80/home
https://sites.google.com/view/2wsnxl35/home
https://sites.google.com/view/9upiju57/home
https://sites.google.com/view/0cjgci51/home
https://sites.google.com/view/6bbfhi31/home
https://sites.google.com/view/5kfsgr23/home
https://sites.google.com/view/2yahzy66/home
https://sites.google.com/view/3bjdfe66/home
https://sites.google.com/view/8zysdc97/home
https://sites.google.com/view/4whfwi14/home
https://sites.google.com/view/4oxzdw14/home
https://sites.google.com/view/3xtxxz40/home
https://sites.google.com/view/3zfzrk48/home
https://sites.google.com/view/1vuurx26/home
https://sites.google.com/view/5fqasw51/home
https://sites.google.com/view/4auyde58/home
https://sites.google.com/view/3gztws07/home
https://sites.google.com/view/1unpun49/home
https://sites.google.com/view/3jclpx68/home
https://sites.google.com/view/8vfpdb61/home
https://sites.google.com/view/1jskfv65/home
https://sites.google.com/view/8khlye43/home
https://sites.google.com/view/0jlpsf46/home
https://sites.google.com/view/4rrmuk82/home
https://sites.google.com/view/8pyjpk92/home
https://sites.google.com/view/6skokh74/home
https://sites.google.com/view/6xhhhj64/home
https://sites.google.com/view/3zdgnv39/home
https://sites.google.com/view/3krsoe29/home
https://sites.google.com/view/4qccqa54/home
https://sites.google.com/view/6bmcza90/home
https://sites.google.com/view/9myclp29/home
https://sites.google.com/view/8chakc99/home
https://sites.google.com/view/7naelf98/home
https://sites.google.com/view/9ovzhg81/home
https://sites.google.com/view/3yvfkx78/home
https://sites.google.com/view/5cdvsn19/home
https://sites.google.com/view/1huyew52/home
https://sites.google.com/view/8lgdoh08/home
https://sites.google.com/view/2nqdur07/home
https://sites.google.com/view/5scjnw53/home
https://sites.google.com/view/9xvbba99/home
https://sites.google.com/view/3nqkgc40/home
https://sites.google.com/view/8gvcqc86/home
https://sites.google.com/view/9qpetf93/home
https://sites.google.com/view/7gkigb30/home
https://sites.google.com/view/9ghmex19/home
https://sites.google.com/view/1wcrpp04/home
https://sites.google.com/view/8udpqi35/home
https://sites.google.com/view/3canhn37/home
https://sites.google.com/view/7emxvq67/home
https://sites.google.com/view/1glhsm08/home
https://sites.google.com/view/1rbpie37/home
https://sites.google.com/view/6lknzd58/home
https://sites.google.com/view/0wjrrz98/home
https://sites.google.com/view/1kozsf11/home
https://sites.google.com/view/6hjwzy13/home
https://sites.google.com/view/9hhvpx75/home
https://sites.google.com/view/2wnusu60/home
https://sites.google.com/view/5dwcsn95/home
https://sites.google.com/view/8nwpaq06/home

继续阅读 »

硬件限制因平台而异,可以通过如下命令进行查看:

hdc shell hidumper -s 10 -a 'vktextureLimit'
常见值为“width: 8192 height: 8192”,表示最大绘制纹理尺寸的长宽都需要在8192像素以内。比较待截图组件的尺寸,以便确认截图失败是否为该原因导致,如果是,请调整所截图组件的大小,或实现为滚动截图后自行拼接。实现请参考截取长内容(滚动截图)和长截图。

如需实现离屏组件的长截图,可参考以下实现:

// src/main/ets/utils/Utils.ets
import { image } from '@kit.ImageKit';
import { BusinessError } from '@kit.BasicServicesKit';

export class Utils {
static sleep(ms: number): Promise<void> {
return new Promise(resolve => setTimeout(resolve, ms));
}

// Calculate the valid screenshot area
static async getSnapshotArea(context: UIContext, pixelMap: PixelMap, scrollYOffsets: number[], listWidth: number,
listHeight: number): Promise<image.PositionArea> {
let stride = pixelMap.getBytesNumberPerRow();
let bytesNumber = pixelMap.getPixelBytesNumber();
let buffer: ArrayBuffer = new ArrayBuffer(bytesNumber);
let len = scrollYOffsets.length;

if (scrollYOffsets.length >= 2) {  
  let realScrollHeight = scrollYOffsets[len-1] - scrollYOffsets[len-2];  
  if (listHeight - realScrollHeight > 0) {  
    let cropRegion: image.Region = {  
      x: 0,  
      y: context.vp2px(listHeight - realScrollHeight) || 0,  
      size: {  
        height: context.vp2px(realScrollHeight) || 0,  
        width: context.vp2px(listWidth) || 0  
      }  
    };  
    await pixelMap.crop(cropRegion);  
  }  
}  

let area: image.PositionArea = {  
  pixels: buffer,  
  offset: 0,  
  stride: stride,  
  region: {  
    size: {  
      width: 0,  
      height: 0  
    },  
    x: 0,  
    y: 0  
  }  
}  

try {  
  let imgInfo = pixelMap.getImageInfoSync();  
  area.region.size.width = imgInfo.size.width;  
  area.region.size.height = imgInfo.size.height;  
  pixelMap.readPixelsSync(area);  
} catch (err) {  
  let error = err as BusinessError;  
  console.error(`getSnapshotArea err, code:${error.code}, message: ${error.message}`);  
}  
return area;  

}

// Graphic splicing
static async mergeImage(context: UIContext, areaArray: image.PositionArea[], lastOffsetY: number, listWidth: number,
listHeight: number): Promise<PixelMap> {
let opts: image.InitializationOptions = {
editable: true,
pixelFormat: 4,
size: {
width: context.vp2px(listWidth) || 0,
height: context.vp2px(lastOffsetY + listHeight) || 0
}
};
let longPixelMap = image.createPixelMapSync(opts);
let imgPosition: number = 0;

for (let i = 0; i < areaArray.length; i++) {  
  let readArea = areaArray[i];  
  let area: image.PositionArea = {  
    pixels: readArea.pixels,  
    offset: 0,  
    stride: readArea.stride,  
    region: {  
      size: {  
        width: readArea.region.size.width,  
        height: readArea.region.size.height  
      },  
      x: 0,  
      y: imgPosition  
    }  
  }  
  imgPosition += readArea.region.size.height;  
  try {  
    longPixelMap.writePixelsSync(area);  
  } catch (err) {  
    let error = err as BusinessError;  
    console.error(`writePixelsSync err, code:${error.code}, message: ${error.message}`);  
  }  
}  
return longPixelMap;  

}
}
// src/main/ets/pages/Index.ets
import { image } from '@kit.ImageKit';
import { BusinessError } from '@kit.BasicServicesKit';
import { Utils } from '../utils/Utils';

export class MyDataSource {
private _data: string[] = [];
private _listeners: DataChangeListener[] = [];

pushData(data: string): void {
this._data.push(data);
this._listeners.forEach(listener => {
listener.onDataAdd(this._data.length - 1);
})
}

getAllData(): string[] {
return this._data;
}

totalCount(): number {
return this._data.length;
}

getData(index: number): string {
return this._data[index];
}

registerDataChangeListener(listener: DataChangeListener): void {
this._listeners.push(listener);
}

unregisterDataChangeListener(listener: DataChangeListener): void {
const index = this._listeners.indexOf(listener);
if (index != -1) {
this._listeners.splice(index, 1);
}
}
}

@Entry
@Component
struct SnapshotExample {
private scroller: Scroller = new Scroller();
private listComponentWidth: number = 0;
private listComponentHeight: number = 0;
@State mergedImage: PixelMap | undefined = undefined;
private areaArray: image.PositionArea[] = [];
private scrollYOffsets: number[] = [];
private data: MyDataSource = new MyDataSource();
private listId: string = 'LIST_ID';

aboutToAppear(): void {
for (let i = 0; i < 50; i++) {
this.data.pushData(Hello ${i});
}
}

async onceSnapshot() {
await this.beforeSnapshot();
await this.snapAndMerge();
this.afterGeneratorImage();
}

async snapAndMerge() {
try {
// Record the current scrolling position
this.scrollYOffsets.push(this.scroller.currentOffset().yOffset);
// Take a screenshot of the current display part of the component
const pixelMap = await this.getUIContext().getComponentSnapshot().get(this.listId);
// Calculate the valid screenshot area
let area: image.PositionArea =
await Utils.getSnapshotArea(this.getUIContext(), pixelMap, this.scrollYOffsets, this.listComponentWidth,
this.listComponentHeight);
this.areaArray.push(area);
// Determine whether to scroll to the bottom
if (!this.scroller.isAtEnd()) {
// Not to the bottom: Scroll down by one screen height
this.scroller.scrollTo({
xOffset: 0,
yOffset: (this.scroller.currentOffset().yOffset + this.listComponentHeight),
animation: {
duration: 200
}
});
await Utils.sleep(200);
await this.snapAndMerge();
} else {
this.mergedImage =
await Utils.mergeImage(this.getUIContext(), this.areaArray, this.scrollYOffsets[this.scrollYOffsets.length-1],
this.listComponentWidth, this.listComponentHeight);
}
} catch (err) {
let error = err as BusinessError;
console.error(snapAndMerge err, code:${error.code}, message: ${error.message});
}
}

async beforeSnapshot() {
try {
this.scroller.scrollTo({
xOffset: 0,
yOffset: 0,
animation: {
duration: 200
}
});
await Utils.sleep(200);
} catch (err) {
let error = err as BusinessError;
console.error(beforeSnapshot err, code:${error.code}, message: ${error.message});
}
}

afterGeneratorImage() {
this.scrollYOffsets.length = 0;
this.areaArray.length = 0;
}

build() {
Column({ space: 12 }) {
Button('Click to get the snapshot')
.onClick(() => {
this.onceSnapshot();
})
Stack() {
// Screenshot component
List({ space: 12, scroller: this.scroller }) {
LazyForEach(this.data, (item: string) => {
ListItem() {
Row() {
Text(item)
.fontSize(50)
.height(50)
}
}
}, (item: number) => item.toString())
}
.scrollBar(BarState.Off)
.cachedCount(3)
.width('100%')
.height('100%')
.backgroundColor($r('sys.color.background_secondary'))
.id(this.listId)
.onAreaChange((oldValue, newValue) => {
this.listComponentWidth = newValue.width as number;
this.listComponentHeight = newValue.height as number;
})
// Set the Z-sequence to -1 to ensure that this component is invisible
.zIndex(-1)

    // Use a mask to cover the screenshot area  
    Column()  
      .width('100%').height('100%').backgroundColor(Color.White)  
    // Long screenshot  
    Scroll() {  
      Image(this.mergedImage)  
    }  
  }  
  .width('100%')  
  .layoutWeight(1)  
}  

}
}
https://sites.google.com/view/4qpzqf30/home
https://sites.google.com/view/1ggrec61/home
https://sites.google.com/view/7dqpdy74/home
https://sites.google.com/view/5bcwfb15/home
https://sites.google.com/view/3ccqme12/home
https://sites.google.com/view/9fauxq30/home
https://sites.google.com/view/0cskdu29/home
https://sites.google.com/view/7ndhuv19/home
https://sites.google.com/view/2qfdbs75/home
https://sites.google.com/view/7tbfwn54/home
https://sites.google.com/view/8utwqh99/home
https://sites.google.com/view/8tmfsn03/home
https://sites.google.com/view/7hyniw30/home
https://sites.google.com/view/5vcrza32/home
https://sites.google.com/view/9bicoh33/home
https://sites.google.com/view/8mykyr89/home
https://sites.google.com/view/9dkwex15/home
https://sites.google.com/view/3jdidd23/home
https://sites.google.com/view/4jnbko13/home
https://sites.google.com/view/3hiqwy37/home
https://sites.google.com/view/9sdhji16/home
https://sites.google.com/view/9vjvuk08/home
https://sites.google.com/view/4vtgzg10/home
https://sites.google.com/view/9simwj81/home
https://sites.google.com/view/2fhkhe54/home
https://sites.google.com/view/3ifqol91/home
https://sites.google.com/view/5oaygy48/home
https://sites.google.com/view/9uzuby80/home
https://sites.google.com/view/4mfobg52/home
https://sites.google.com/view/7lmnbx80/home
https://sites.google.com/view/7zieer80/home
https://sites.google.com/view/2wsnxl35/home
https://sites.google.com/view/9upiju57/home
https://sites.google.com/view/0cjgci51/home
https://sites.google.com/view/6bbfhi31/home
https://sites.google.com/view/5kfsgr23/home
https://sites.google.com/view/2yahzy66/home
https://sites.google.com/view/3bjdfe66/home
https://sites.google.com/view/8zysdc97/home
https://sites.google.com/view/4whfwi14/home
https://sites.google.com/view/4oxzdw14/home
https://sites.google.com/view/3xtxxz40/home
https://sites.google.com/view/3zfzrk48/home
https://sites.google.com/view/1vuurx26/home
https://sites.google.com/view/5fqasw51/home
https://sites.google.com/view/4auyde58/home
https://sites.google.com/view/3gztws07/home
https://sites.google.com/view/1unpun49/home
https://sites.google.com/view/3jclpx68/home
https://sites.google.com/view/8vfpdb61/home
https://sites.google.com/view/1jskfv65/home
https://sites.google.com/view/8khlye43/home
https://sites.google.com/view/0jlpsf46/home
https://sites.google.com/view/4rrmuk82/home
https://sites.google.com/view/8pyjpk92/home
https://sites.google.com/view/6skokh74/home
https://sites.google.com/view/6xhhhj64/home
https://sites.google.com/view/3zdgnv39/home
https://sites.google.com/view/3krsoe29/home
https://sites.google.com/view/4qccqa54/home
https://sites.google.com/view/6bmcza90/home
https://sites.google.com/view/9myclp29/home
https://sites.google.com/view/8chakc99/home
https://sites.google.com/view/7naelf98/home
https://sites.google.com/view/9ovzhg81/home
https://sites.google.com/view/3yvfkx78/home
https://sites.google.com/view/5cdvsn19/home
https://sites.google.com/view/1huyew52/home
https://sites.google.com/view/8lgdoh08/home
https://sites.google.com/view/2nqdur07/home
https://sites.google.com/view/5scjnw53/home
https://sites.google.com/view/9xvbba99/home
https://sites.google.com/view/3nqkgc40/home
https://sites.google.com/view/8gvcqc86/home
https://sites.google.com/view/9qpetf93/home
https://sites.google.com/view/7gkigb30/home
https://sites.google.com/view/9ghmex19/home
https://sites.google.com/view/1wcrpp04/home
https://sites.google.com/view/8udpqi35/home
https://sites.google.com/view/3canhn37/home
https://sites.google.com/view/7emxvq67/home
https://sites.google.com/view/1glhsm08/home
https://sites.google.com/view/1rbpie37/home
https://sites.google.com/view/6lknzd58/home
https://sites.google.com/view/0wjrrz98/home
https://sites.google.com/view/1kozsf11/home
https://sites.google.com/view/6hjwzy13/home
https://sites.google.com/view/9hhvpx75/home
https://sites.google.com/view/2wnusu60/home
https://sites.google.com/view/5dwcsn95/home
https://sites.google.com/view/8nwpaq06/home

收起阅读 »