如何让Canvas绘制内容能超出父组件的限制
问题现象
如何让Canvas绘制内容能超出父组件的限制,“按下”的响应区域不超出父组件范围?组件中.clip属性能实现此效果吗?
背景知识
HarmonyOS组件中.clip属性:是否对子组件超出当前组件范围外的区域进行裁剪。参考链接:clip。
HarmonyOS组件中.responseRegion属性可以实现组件的响应区域范围的变化,响应区域范围可以超出或者小于组件的布局范围。参考链接:自定义控制的多层级手势事件。
Canvas:Canvas组件提供画布,用于自定义绘制图形。
CanvasRenderingContext2D:使用CanvasRenderingContext2D在Canvas画布组件上进行绘制,绘制对象可以是矩形、文本、图片等。
解决方案
在HarmonyOS中,无法通过.clip属性实现所需效果。当.clip属性设置为true时,子组件超出当前组件范围的区域将不会响应绑定的手势事件;当设置为undefined时,系统将不再对超出部分进行裁剪,但这并不意味着可以控制内容超出组件本身的绘制范围。可以通过以下方案实现让Canvas绘制内容能超出父组件限制的功能,步骤如下:
使用Column父组件包含子组件Canvas,Column父组件宽高为(150,150)区域,子组件Canvas宽高为(300,300)。
对Canvas组件设置.responseRegion属性触摸热区为.responseRegion({ x: 75, y: 0, width: 150, height: 150 })。实现功能:蓝色区域(150150)可响应Canvas绘制内容时“按下(down)”的触摸操作,红色区域(300300去除蓝色区域的范围)不可响应“按下(down)”的触摸操作,红色区域只可响应从蓝色区域“按下(down)”后的“移动(move)”触摸操作。
说明
当组件Canvas绑定了.responseRegion(Rect),所有落在Rect区域范围的触摸事件和手势可被组件Canvas对应的回调响应。
@Entry
@Component
struct DrawBankCom {
paintSize: number = 5; // 当前画笔大小
private settings: RenderingContextSettings = new RenderingContextSettings(true);
canvasContext: CanvasRenderingContext2D = new CanvasRenderingContext2D(this.settings);
tempPath: Path2D = new Path2D();
@State @Watch('onChange') pathArray: Array<Path2D> = []; // 所有画图路径信息
isUpdate: boolean = true;
removeArray: Array<Path2D> = []; // 回退的路径集合
@State text: string = '';
@State eventType: string = '';
build() {
Column() {
if (this.isUpdate) {
Row() {
Column() {
Canvas(this.canvasContext)
// Canvas(子组件)宽高设置为300300
.width(300)
.height(300)
.onReady(() => {
this.canvasContext.lineWidth = this.paintSize;
this.canvasContext.stroke(this.tempPath);
for (let index = 0; index < this.pathArray.length; index++) {
this.canvasContext.stroke(this.pathArray[index]);
}
})
.onTouch((event?: TouchEvent) => {
if (event) {
if (event.type === TouchType.Down) {
this.eventType = 'Down';
this.canvasContext.lineWidth = this.paintSize;
this.canvasContext.beginPath();
this.tempPath = new Path2D();
this.pathArray.push(this.tempPath);
this.tempPath.moveTo(event.touches[0].x, event.touches[0].y);
this.canvasContext.moveTo(event.touches[0].x, event.touches[0].y);
}
if (event.type === TouchType.Up) {
this.eventType = 'Up';
}
if (event.type === TouchType.Move) {
this.eventType = 'Move';
this.tempPath.lineTo(event.touches[0].x, event.touches[0].y);
this.canvasContext.stroke(this.tempPath);
this.canvasContext.lineTo(event.touches[0].x, event.touches[0].y);
this.canvasContext.stroke();
}
this.text = 'TouchType:' + this.eventType + '\n touch point and touch element:\nx: ' +
event.touches[0].x + '\n' + 'y: ' + event.touches[0].y + '\nwidth:' +
event.target.area.width + '\nheight:' + event.target.area.height + '\pathArray size:' +
this.pathArray.length;
}
})
.renderFit(RenderFit.TOP_RIGHT)
// 设置触摸热区,蓝色区域可响应“down”操作,红色不可响应“down”操作,只能响应“move”操作
.responseRegion({
x: 75,
y: 0,
width: 150,
height: 150
});
}
// Column(父组件)宽高设置为150150
.width(150)
.height(150)
.backgroundColor('#ffbdf3e9');
};
}
Column() {
Row() {
Button('回退').onClick(() => {
this.pathArray.pop();
});
};
Text(this.text);
};
}
.width(300)
.height(300)
.clip(false)
.borderRadius(10)
.backgroundColor('#ff0000');
}
onChange() {
this.canvasContext.reset();
this.canvasContext.lineWidth = this.paintSize;
for (let index = 0; index < this.pathArray.length; index++) {
this.canvasContext.stroke(this.pathArray[index]);
}
}
}
具体表现为从蓝色区域开始画线,可以延伸到红色区域,但是不可从红色区域开始画线。
总结
使用.responseRegion属性可以实现组件的响应区域范围的变化,能实现让Canvas绘制内容能超出父组件的限制。
https://sites.google.com/view/1svbrd24/home
https://sites.google.com/view/2jlhml67/home
https://sites.google.com/view/0ojkvo59/home
https://sites.google.com/view/5erbeg51/home
https://sites.google.com/view/7txhqp00/home
https://sites.google.com/view/9ahuri30/home
https://sites.google.com/view/9brhff50/home
https://sites.google.com/view/7usmzs19/home
https://sites.google.com/view/7fkyls54/home
https://sites.google.com/view/4yfngr91/home
https://sites.google.com/view/7bibkd36/home
https://sites.google.com/view/7rgsbk35/home
https://sites.google.com/view/5tjymn45/home
https://sites.google.com/view/0qxruy91/home
https://sites.google.com/view/6xlnnt32/home
https://sites.google.com/view/9ekbqp32/home
https://sites.google.com/view/5vwcmf35/home
https://sites.google.com/view/2xhdlr45/home
https://sites.google.com/view/0giwkx12/home
https://sites.google.com/view/9cqmye26/home
https://sites.google.com/view/0rxdat90/home
https://sites.google.com/view/3yywdt98/home
https://sites.google.com/view/1lbijc54/home
https://sites.google.com/view/4ilgby72/home
https://sites.google.com/view/3lexut78/home
https://sites.google.com/view/1dsgmu66/home
https://sites.google.com/view/7lsxyd26/home
https://sites.google.com/view/2ulade56/home
https://sites.google.com/view/1tipkq60/home
https://sites.google.com/view/4jhjbn51/home
https://sites.google.com/view/2bpvrp99/home
https://sites.google.com/view/7hyotm19/home
https://sites.google.com/view/1sxxqc05/home
https://sites.google.com/view/7bzjsk57/home
https://sites.google.com/view/0rvozy96/home
https://sites.google.com/view/4lqeqc80/home
https://sites.google.com/view/5xnhok81/home
https://sites.google.com/view/9piiqm40/home
https://sites.google.com/view/2opvsv00/home
https://sites.google.com/view/5sdsim60/home
https://sites.google.com/view/6qrjip83/home
https://sites.google.com/view/8wzpjp01/home
https://sites.google.com/view/2rbzfb85/home
https://sites.google.com/view/8yjxua98/home
https://sites.google.com/view/5asfix82/home
https://sites.google.com/view/5myuib41/home
https://sites.google.com/view/9arbno01/home
https://sites.google.com/view/9fqrsq43/home
https://sites.google.com/view/2lgtzy82/home
https://sites.google.com/view/4detht79/home
https://sites.google.com/view/9yezyo00/home
https://sites.google.com/view/0nofnm30/home
https://sites.google.com/view/2vfhym56/home
https://sites.google.com/view/1rivml82/home
https://sites.google.com/view/7ohgjy23/home
https://sites.google.com/view/1gbrst25/home
https://sites.google.com/view/8zevsj79/home
https://sites.google.com/view/1tfuhl96/home
https://sites.google.com/view/8fmixj07/home
https://sites.google.com/view/3yprml67/home
https://sites.google.com/view/4vzytf83/home
https://sites.google.com/view/0zydqk84/home
https://sites.google.com/view/8onjpb17/home
https://sites.google.com/view/5qbsvc57/home
https://sites.google.com/view/2uqmyg64/home
https://sites.google.com/view/6hnwoa03/home
https://sites.google.com/view/2bdovq40/home
https://sites.google.com/view/3ykzjl65/home
https://sites.google.com/view/3ypjlx51/home
https://sites.google.com/view/2vecse29/home
https://sites.google.com/view/8wjmoz24/home
https://sites.google.com/view/6anaaz10/home
https://sites.google.com/view/6vztlx87/home
https://sites.google.com/view/9hxqwy10/home
https://sites.google.com/view/6emmnf53/home
https://sites.google.com/view/9qgmon96/home
https://sites.google.com/view/8vqblh76/home
https://sites.google.com/view/5repzi61/home
https://sites.google.com/view/6iqxbj96/home
https://sites.google.com/view/4fiyto32/home
https://sites.google.com/view/1uimwi96/home
https://sites.google.com/view/5ibqaj17/home
https://sites.google.com/view/9nqdyq84/home
https://sites.google.com/view/4lwyyd65/home
https://sites.google.com/view/3gawuk38/home
https://sites.google.com/view/5lgzag04/home
https://sites.google.com/view/1imonc99/home
https://sites.google.com/view/9xyukm01/home
https://sites.google.com/view/6nalbh77/home
https://sites.google.com/view/2ypecr06/home
https://sites.google.com/view/5zdody20/home
https://sites.google.com/view/6wkgom88/home
https://sites.google.com/view/8zwqqi53/home
https://sites.google.com/view/0qyluw74/home
https://sites.google.com/view/5aphjd68/home
https://sites.google.com/view/2sacyn38/home
https://sites.google.com/view/8dbjrm35/home
https://sites.google.com/view/6unuwk46/home
https://sites.google.com/view/5hrlie99/home
https://sites.google.com/view/6mxmyz56/home
https://sites.google.com/view/9gdzzy11/home
https://sites.google.com/view/3wkfon30/home
https://sites.google.com/view/7jesbl97/home
https://sites.google.com/view/7xllml61/home
https://sites.google.com/view/3mpkjx30/home
https://sites.google.com/view/6idizy90/home
https://sites.google.com/view/1czsip30/home
https://sites.google.com/view/3ezdwf97/home
https://sites.google.com/view/1ydubh84/home
https://sites.google.com/view/4cvedy61/home
https://sites.google.com/view/7yyumz73/home
https://sites.google.com/view/5pkquw00/home
https://sites.google.com/view/7dceex86/home
https://sites.google.com/view/3gnvyg34/home
https://sites.google.com/view/2ehwht60/home
https://sites.google.com/view/3ocyay63/home
https://sites.google.com/view/6uizgx22/home
https://sites.google.com/view/3cgblb20/home
https://sites.google.com/view/3guucg59/home
https://sites.google.com/view/9cdybx22/home
https://sites.google.com/view/7giaxy95/home
https://sites.google.com/view/3qkkfi06/home
https://sites.google.com/view/9goqyg50/home
https://sites.google.com/view/6sfntm95/home
https://sites.google.com/view/8hekup65/home
https://sites.google.com/view/8dypsx49/home
https://sites.google.com/view/1emhrm38/home
https://sites.google.com/view/1yyqhu29/home
https://sites.google.com/view/5heoek19/home
https://sites.google.com/view/3tvkkw85/home
https://sites.google.com/view/4dqdwv87/home
https://sites.google.com/view/2tsotl60/home
https://sites.google.com/view/7mpwib24/home
https://sites.google.com/view/9dlwvj94/home
https://sites.google.com/view/3hggdc60/home
https://sites.google.com/view/3vhyat21/home
https://sites.google.com/view/7vyebf30/home
https://sites.google.com/view/4wnnbn37/home
https://sites.google.com/view/6mjlkw77/home
https://sites.google.com/view/0jcihr79/home
https://sites.google.com/view/6lpxba81/home
https://sites.google.com/view/7nmcwv30/home
https://sites.google.com/view/3ztaut28/home
https://sites.google.com/view/1ecdeq22/home
https://sites.google.com/view/0dgpba93/home
https://sites.google.com/view/1aqqqo25/home
https://sites.google.com/view/1raxlw38/home
https://sites.google.com/view/3snsha82/home
https://sites.google.com/view/5uwlrm29/home
https://sites.google.com/view/4feaqq70/home
https://sites.google.com/view/3nlhqj52/home
https://sites.google.com/view/1pidkm14/home
https://sites.google.com/view/2wqdnc45/home
https://sites.google.com/view/1iwyhj71/home
https://sites.google.com/view/8evxkf88/home
https://sites.google.com/view/5zbhhn62/home
https://sites.google.com/view/9hjcnp07/home
https://sites.google.com/view/3mxnek99/home
https://sites.google.com/view/9bmsbt04/home
https://sites.google.com/view/2lezmt81/home
https://sites.google.com/view/6qmqey84/home
https://coub.com/view/scsevfdzaa
https://coub.com/view/ghy3aenpkf
https://coub.com/view/xfyiqequqh
https://coub.com/view/45ky2yyzja
https://coub.com/view/v7lv7a0sy1
https://coub.com/view/e09y796sai
https://coub.com/view/z9dgue55fb
https://coub.com/view/4v9yryaq7b
https://coub.com/view/rjakrzmprm
https://coub.com/view/443k6aufbo
https://coub.com/view/adxz7xc4zj
https://coub.com/view/gontyw4afu
https://coub.com/view/67wn0hy74w
https://coub.com/view/p7i0qsp0th
https://coub.com/view/bu18agb0cw
https://coub.com/view/u453dsg9rs
https://coub.com/view/0xh6401od2
https://coub.com/view/s8ivp1bnms
https://coub.com/view/q7d0nkso0h
https://coub.com/view/gadau0rb64
https://coub.com/view/79ww1tgrud
https://coub.com/view/grqy2h2n44
https://coub.com/view/dvjar8c2ng
https://coub.com/view/365566dfec
https://coub.com/view/szouxmw105
https://coub.com/view/yucl2gqgiq
https://coub.com/view/mcb2rze9ox
https://coub.com/view/0j5cdu3ezv
https://coub.com/view/023m21y9ex
https://coub.com/view/0tvarwqbh6
https://coub.com/view/ks6hpuqzv7
https://coub.com/view/390avxe2u9
https://coub.com/view/vqb725jiar
https://coub.com/view/9ekoylsaw1
https://coub.com/view/n5wdecqayc
https://coub.com/view/lu3z4p5rah
https://coub.com/view/6lu4dpcva8
https://coub.com/view/l3dai5ah2r
https://coub.com/view/6rged9l4g5
https://coub.com/view/u4gupyey94
https://coub.com/view/9oyqag6yr3
https://coub.com/view/3b6ywmk5lt
https://coub.com/view/t5mm2naqc1
问题现象
如何让Canvas绘制内容能超出父组件的限制,“按下”的响应区域不超出父组件范围?组件中.clip属性能实现此效果吗?
背景知识
HarmonyOS组件中.clip属性:是否对子组件超出当前组件范围外的区域进行裁剪。参考链接:clip。
HarmonyOS组件中.responseRegion属性可以实现组件的响应区域范围的变化,响应区域范围可以超出或者小于组件的布局范围。参考链接:自定义控制的多层级手势事件。
Canvas:Canvas组件提供画布,用于自定义绘制图形。
CanvasRenderingContext2D:使用CanvasRenderingContext2D在Canvas画布组件上进行绘制,绘制对象可以是矩形、文本、图片等。
解决方案
在HarmonyOS中,无法通过.clip属性实现所需效果。当.clip属性设置为true时,子组件超出当前组件范围的区域将不会响应绑定的手势事件;当设置为undefined时,系统将不再对超出部分进行裁剪,但这并不意味着可以控制内容超出组件本身的绘制范围。可以通过以下方案实现让Canvas绘制内容能超出父组件限制的功能,步骤如下:
使用Column父组件包含子组件Canvas,Column父组件宽高为(150,150)区域,子组件Canvas宽高为(300,300)。
对Canvas组件设置.responseRegion属性触摸热区为.responseRegion({ x: 75, y: 0, width: 150, height: 150 })。实现功能:蓝色区域(150150)可响应Canvas绘制内容时“按下(down)”的触摸操作,红色区域(300300去除蓝色区域的范围)不可响应“按下(down)”的触摸操作,红色区域只可响应从蓝色区域“按下(down)”后的“移动(move)”触摸操作。
说明
当组件Canvas绑定了.responseRegion(Rect),所有落在Rect区域范围的触摸事件和手势可被组件Canvas对应的回调响应。
@Entry
@Component
struct DrawBankCom {
paintSize: number = 5; // 当前画笔大小
private settings: RenderingContextSettings = new RenderingContextSettings(true);
canvasContext: CanvasRenderingContext2D = new CanvasRenderingContext2D(this.settings);
tempPath: Path2D = new Path2D();
@State @Watch('onChange') pathArray: Array<Path2D> = []; // 所有画图路径信息
isUpdate: boolean = true;
removeArray: Array<Path2D> = []; // 回退的路径集合
@State text: string = '';
@State eventType: string = '';
build() {
Column() {
if (this.isUpdate) {
Row() {
Column() {
Canvas(this.canvasContext)
// Canvas(子组件)宽高设置为300300
.width(300)
.height(300)
.onReady(() => {
this.canvasContext.lineWidth = this.paintSize;
this.canvasContext.stroke(this.tempPath);
for (let index = 0; index < this.pathArray.length; index++) {
this.canvasContext.stroke(this.pathArray[index]);
}
})
.onTouch((event?: TouchEvent) => {
if (event) {
if (event.type === TouchType.Down) {
this.eventType = 'Down';
this.canvasContext.lineWidth = this.paintSize;
this.canvasContext.beginPath();
this.tempPath = new Path2D();
this.pathArray.push(this.tempPath);
this.tempPath.moveTo(event.touches[0].x, event.touches[0].y);
this.canvasContext.moveTo(event.touches[0].x, event.touches[0].y);
}
if (event.type === TouchType.Up) {
this.eventType = 'Up';
}
if (event.type === TouchType.Move) {
this.eventType = 'Move';
this.tempPath.lineTo(event.touches[0].x, event.touches[0].y);
this.canvasContext.stroke(this.tempPath);
this.canvasContext.lineTo(event.touches[0].x, event.touches[0].y);
this.canvasContext.stroke();
}
this.text = 'TouchType:' + this.eventType + '\n touch point and touch element:\nx: ' +
event.touches[0].x + '\n' + 'y: ' + event.touches[0].y + '\nwidth:' +
event.target.area.width + '\nheight:' + event.target.area.height + '\pathArray size:' +
this.pathArray.length;
}
})
.renderFit(RenderFit.TOP_RIGHT)
// 设置触摸热区,蓝色区域可响应“down”操作,红色不可响应“down”操作,只能响应“move”操作
.responseRegion({
x: 75,
y: 0,
width: 150,
height: 150
});
}
// Column(父组件)宽高设置为150150
.width(150)
.height(150)
.backgroundColor('#ffbdf3e9');
};
}
Column() {
Row() {
Button('回退').onClick(() => {
this.pathArray.pop();
});
};
Text(this.text);
};
}
.width(300)
.height(300)
.clip(false)
.borderRadius(10)
.backgroundColor('#ff0000');
}
onChange() {
this.canvasContext.reset();
this.canvasContext.lineWidth = this.paintSize;
for (let index = 0; index < this.pathArray.length; index++) {
this.canvasContext.stroke(this.pathArray[index]);
}
}
}
具体表现为从蓝色区域开始画线,可以延伸到红色区域,但是不可从红色区域开始画线。
总结
使用.responseRegion属性可以实现组件的响应区域范围的变化,能实现让Canvas绘制内容能超出父组件的限制。
https://sites.google.com/view/1svbrd24/home
https://sites.google.com/view/2jlhml67/home
https://sites.google.com/view/0ojkvo59/home
https://sites.google.com/view/5erbeg51/home
https://sites.google.com/view/7txhqp00/home
https://sites.google.com/view/9ahuri30/home
https://sites.google.com/view/9brhff50/home
https://sites.google.com/view/7usmzs19/home
https://sites.google.com/view/7fkyls54/home
https://sites.google.com/view/4yfngr91/home
https://sites.google.com/view/7bibkd36/home
https://sites.google.com/view/7rgsbk35/home
https://sites.google.com/view/5tjymn45/home
https://sites.google.com/view/0qxruy91/home
https://sites.google.com/view/6xlnnt32/home
https://sites.google.com/view/9ekbqp32/home
https://sites.google.com/view/5vwcmf35/home
https://sites.google.com/view/2xhdlr45/home
https://sites.google.com/view/0giwkx12/home
https://sites.google.com/view/9cqmye26/home
https://sites.google.com/view/0rxdat90/home
https://sites.google.com/view/3yywdt98/home
https://sites.google.com/view/1lbijc54/home
https://sites.google.com/view/4ilgby72/home
https://sites.google.com/view/3lexut78/home
https://sites.google.com/view/1dsgmu66/home
https://sites.google.com/view/7lsxyd26/home
https://sites.google.com/view/2ulade56/home
https://sites.google.com/view/1tipkq60/home
https://sites.google.com/view/4jhjbn51/home
https://sites.google.com/view/2bpvrp99/home
https://sites.google.com/view/7hyotm19/home
https://sites.google.com/view/1sxxqc05/home
https://sites.google.com/view/7bzjsk57/home
https://sites.google.com/view/0rvozy96/home
https://sites.google.com/view/4lqeqc80/home
https://sites.google.com/view/5xnhok81/home
https://sites.google.com/view/9piiqm40/home
https://sites.google.com/view/2opvsv00/home
https://sites.google.com/view/5sdsim60/home
https://sites.google.com/view/6qrjip83/home
https://sites.google.com/view/8wzpjp01/home
https://sites.google.com/view/2rbzfb85/home
https://sites.google.com/view/8yjxua98/home
https://sites.google.com/view/5asfix82/home
https://sites.google.com/view/5myuib41/home
https://sites.google.com/view/9arbno01/home
https://sites.google.com/view/9fqrsq43/home
https://sites.google.com/view/2lgtzy82/home
https://sites.google.com/view/4detht79/home
https://sites.google.com/view/9yezyo00/home
https://sites.google.com/view/0nofnm30/home
https://sites.google.com/view/2vfhym56/home
https://sites.google.com/view/1rivml82/home
https://sites.google.com/view/7ohgjy23/home
https://sites.google.com/view/1gbrst25/home
https://sites.google.com/view/8zevsj79/home
https://sites.google.com/view/1tfuhl96/home
https://sites.google.com/view/8fmixj07/home
https://sites.google.com/view/3yprml67/home
https://sites.google.com/view/4vzytf83/home
https://sites.google.com/view/0zydqk84/home
https://sites.google.com/view/8onjpb17/home
https://sites.google.com/view/5qbsvc57/home
https://sites.google.com/view/2uqmyg64/home
https://sites.google.com/view/6hnwoa03/home
https://sites.google.com/view/2bdovq40/home
https://sites.google.com/view/3ykzjl65/home
https://sites.google.com/view/3ypjlx51/home
https://sites.google.com/view/2vecse29/home
https://sites.google.com/view/8wjmoz24/home
https://sites.google.com/view/6anaaz10/home
https://sites.google.com/view/6vztlx87/home
https://sites.google.com/view/9hxqwy10/home
https://sites.google.com/view/6emmnf53/home
https://sites.google.com/view/9qgmon96/home
https://sites.google.com/view/8vqblh76/home
https://sites.google.com/view/5repzi61/home
https://sites.google.com/view/6iqxbj96/home
https://sites.google.com/view/4fiyto32/home
https://sites.google.com/view/1uimwi96/home
https://sites.google.com/view/5ibqaj17/home
https://sites.google.com/view/9nqdyq84/home
https://sites.google.com/view/4lwyyd65/home
https://sites.google.com/view/3gawuk38/home
https://sites.google.com/view/5lgzag04/home
https://sites.google.com/view/1imonc99/home
https://sites.google.com/view/9xyukm01/home
https://sites.google.com/view/6nalbh77/home
https://sites.google.com/view/2ypecr06/home
https://sites.google.com/view/5zdody20/home
https://sites.google.com/view/6wkgom88/home
https://sites.google.com/view/8zwqqi53/home
https://sites.google.com/view/0qyluw74/home
https://sites.google.com/view/5aphjd68/home
https://sites.google.com/view/2sacyn38/home
https://sites.google.com/view/8dbjrm35/home
https://sites.google.com/view/6unuwk46/home
https://sites.google.com/view/5hrlie99/home
https://sites.google.com/view/6mxmyz56/home
https://sites.google.com/view/9gdzzy11/home
https://sites.google.com/view/3wkfon30/home
https://sites.google.com/view/7jesbl97/home
https://sites.google.com/view/7xllml61/home
https://sites.google.com/view/3mpkjx30/home
https://sites.google.com/view/6idizy90/home
https://sites.google.com/view/1czsip30/home
https://sites.google.com/view/3ezdwf97/home
https://sites.google.com/view/1ydubh84/home
https://sites.google.com/view/4cvedy61/home
https://sites.google.com/view/7yyumz73/home
https://sites.google.com/view/5pkquw00/home
https://sites.google.com/view/7dceex86/home
https://sites.google.com/view/3gnvyg34/home
https://sites.google.com/view/2ehwht60/home
https://sites.google.com/view/3ocyay63/home
https://sites.google.com/view/6uizgx22/home
https://sites.google.com/view/3cgblb20/home
https://sites.google.com/view/3guucg59/home
https://sites.google.com/view/9cdybx22/home
https://sites.google.com/view/7giaxy95/home
https://sites.google.com/view/3qkkfi06/home
https://sites.google.com/view/9goqyg50/home
https://sites.google.com/view/6sfntm95/home
https://sites.google.com/view/8hekup65/home
https://sites.google.com/view/8dypsx49/home
https://sites.google.com/view/1emhrm38/home
https://sites.google.com/view/1yyqhu29/home
https://sites.google.com/view/5heoek19/home
https://sites.google.com/view/3tvkkw85/home
https://sites.google.com/view/4dqdwv87/home
https://sites.google.com/view/2tsotl60/home
https://sites.google.com/view/7mpwib24/home
https://sites.google.com/view/9dlwvj94/home
https://sites.google.com/view/3hggdc60/home
https://sites.google.com/view/3vhyat21/home
https://sites.google.com/view/7vyebf30/home
https://sites.google.com/view/4wnnbn37/home
https://sites.google.com/view/6mjlkw77/home
https://sites.google.com/view/0jcihr79/home
https://sites.google.com/view/6lpxba81/home
https://sites.google.com/view/7nmcwv30/home
https://sites.google.com/view/3ztaut28/home
https://sites.google.com/view/1ecdeq22/home
https://sites.google.com/view/0dgpba93/home
https://sites.google.com/view/1aqqqo25/home
https://sites.google.com/view/1raxlw38/home
https://sites.google.com/view/3snsha82/home
https://sites.google.com/view/5uwlrm29/home
https://sites.google.com/view/4feaqq70/home
https://sites.google.com/view/3nlhqj52/home
https://sites.google.com/view/1pidkm14/home
https://sites.google.com/view/2wqdnc45/home
https://sites.google.com/view/1iwyhj71/home
https://sites.google.com/view/8evxkf88/home
https://sites.google.com/view/5zbhhn62/home
https://sites.google.com/view/9hjcnp07/home
https://sites.google.com/view/3mxnek99/home
https://sites.google.com/view/9bmsbt04/home
https://sites.google.com/view/2lezmt81/home
https://sites.google.com/view/6qmqey84/home
https://coub.com/view/scsevfdzaa
https://coub.com/view/ghy3aenpkf
https://coub.com/view/xfyiqequqh
https://coub.com/view/45ky2yyzja
https://coub.com/view/v7lv7a0sy1
https://coub.com/view/e09y796sai
https://coub.com/view/z9dgue55fb
https://coub.com/view/4v9yryaq7b
https://coub.com/view/rjakrzmprm
https://coub.com/view/443k6aufbo
https://coub.com/view/adxz7xc4zj
https://coub.com/view/gontyw4afu
https://coub.com/view/67wn0hy74w
https://coub.com/view/p7i0qsp0th
https://coub.com/view/bu18agb0cw
https://coub.com/view/u453dsg9rs
https://coub.com/view/0xh6401od2
https://coub.com/view/s8ivp1bnms
https://coub.com/view/q7d0nkso0h
https://coub.com/view/gadau0rb64
https://coub.com/view/79ww1tgrud
https://coub.com/view/grqy2h2n44
https://coub.com/view/dvjar8c2ng
https://coub.com/view/365566dfec
https://coub.com/view/szouxmw105
https://coub.com/view/yucl2gqgiq
https://coub.com/view/mcb2rze9ox
https://coub.com/view/0j5cdu3ezv
https://coub.com/view/023m21y9ex
https://coub.com/view/0tvarwqbh6
https://coub.com/view/ks6hpuqzv7
https://coub.com/view/390avxe2u9
https://coub.com/view/vqb725jiar
https://coub.com/view/9ekoylsaw1
https://coub.com/view/n5wdecqayc
https://coub.com/view/lu3z4p5rah
https://coub.com/view/6lu4dpcva8
https://coub.com/view/l3dai5ah2r
https://coub.com/view/6rged9l4g5
https://coub.com/view/u4gupyey94
https://coub.com/view/9oyqag6yr3
https://coub.com/view/3b6ywmk5lt
https://coub.com/view/t5mm2naqc1
JavaScript `this` 指南
JavaScript this 指南
一、核心心法
this不是在函数定义时确定的,而是在函数被调用时确定的。
判断this,只需要看一件事:这个函数是怎么被调用的(调用点)。
唯一的例外是箭头函数——它根本没有自己的 this。
二、五种绑定规则
规则 1:默认绑定(独立函数调用)
function foo() {
console.log(this);
}
foo(); // 非严格模式: globalThis (浏览器 window / Node global)
// 严格模式: undefined
'use strict';
function foo() { console.log(this); }
foo(); // undefined
顶层 this 的值取决于环境:
| 环境 | 顶层 this |
|---|---|
浏览器 <script> |
window |
ES Module (.mjs / type="module") |
undefined |
CommonJS 模块 (Node .js) |
module.exports(即 {}) |
| 函数内(非严格) | globalThis |
| 函数内(严格) | undefined |
| class 内部 | 永远严格模式 |
规则 2:隐式绑定(方法调用)
看调用时最后一个点前面是谁。
const obj = {
name: 'obj',
foo() { console.log(this.name); }
};
obj.foo(); // 'obj'
多层嵌套只看最后一层:
const a = { name: 'a', b: { name: 'b', foo() { console.log(this.name); } } };
a.b.foo(); // 'b' ← 不是 'a'
方括号访问同理:
a.b['foo'](); // 'b'
规则 3:显式绑定(call / apply / bind)
function greet(greeting, punct) {
console.log(greeting + ', ' + this.name + punct);
}
const me = { name: 'Alice' };
greet.call(me, 'Hi', '!'); // 逐个传参
greet.apply(me, ['Hi', '!']); // 数组传参 (Apply → Array)
const bound = greet.bind(me, 'Hi'); // 返回新函数 + 支持柯里化
bound('!');
bind 的三个特性:
- 返回一个新函数,不立即执行
- 支持参数预置(柯里化)
- 硬绑定:绑定后无法再被
call/apply改变
const hard = greet.bind(me);
hard.call({ name: 'Bob' }, 'Yo', '!'); // 依然是 Alice
传 null / undefined 的坑:
function foo() { console.log(this); }
foo.call(null); // 非严格: globalThis(被替换了!) 严格: null
安全做法——DMZ 空对象:
const DMZ = Object.create(null); // 比 {} 更干净,无原型
foo.apply(DMZ, [1, 2]);
原始值会被装箱(仅非严格模式):
function f() { console.log(typeof this); }
f.call(1); // 'object' (Number 包装对象)
// 严格模式下: 'number'
规则 4:new 绑定
new Foo() 做了四件事:
- 创建一个新的空对象
- 将该对象的
[[Prototype]]指向Foo.prototype - 以该对象为
this执行Foo - 如果
Foo没有返回一个对象,则返回这个新对象
function Person(name) {
this.name = name;
// return { name: 'hacked' }; // 若返回对象,则 new 的结果是它
// return 42; // 返回原始值 → 被忽略,仍返回 this
}
const p = new Person('Alice'); // p.name === 'Alice'
规则 5:箭头函数(词法绑定)
箭头函数没有自己的 this,它继承定义时所在外层作用域的 this,且永久不可改变。
const obj = {
name: 'obj',
regular() {
const arrow = () => console.log(this.name);
arrow(); // 'obj' ← 继承 regular 的 this
},
arrowMethod: () => console.log(this?.name) // ⚠️ 继承的是模块/全局的 this
};
obj.regular(); // 'obj'
obj.arrowMethod(); // undefined ← 经典陷阱!
箭头函数无法被改变:
const arrow = () => console.log(this);
arrow.call({ a: 1 }); // call 无效
arrow.bind({ a: 1 })(); // bind 无效
new arrow(); // TypeError: arrow is not a constructor
箭头函数也没有 arguments、super、new.target。
三、优先级
new 绑定 > 显式绑定(bind/call/apply) > 隐式绑定(obj.fn) > 默认绑定
箭头函数不在此列——它压根不参与,直接看词法作用域。
验证 new > bind:
function Foo(a) { this.a = a; }
const obj = {};
const B = Foo.bind(obj);
B(2); // obj.a === 2 (bind 生效)
const b = new B(3);// b.a === 3, obj.a 仍是 2 ← new 赢了
这正是
bindpolyfill 里必须判断new.target的原因。
四、隐式丢失:最常见的 Bug 来源
1. 赋值给变量
const obj = { name: 'obj', foo() { console.log(this?.name); } };
const fn = obj.foo; // 只拿到了函数引用,丢掉了 obj
fn(); // undefined
2. 作为回调传递
setTimeout(obj.foo, 100); // 丢失
[1].forEach(obj.foo); // 丢失
btn.addEventListener('click', obj.foo); // this 变成 btn
3. 解构
const { foo } = obj;
foo(); // 丢失
这就是 React 中
const { setState } = this会出问题、
以及 Vue3 用ref而非this的部分原因。
4. 间接引用
const o1 = { name: 'o1', foo() { console.log(this.name); } };
const o2 = { name: 'o2' };
o2.foo = o1.foo;
o2.foo(); // 'o2' (正常隐式绑定)
(o2.foo = o1.foo)(); // 赋值表达式返回函数本身 → 默认绑定 → undefined/报错
5. 解决方案汇总
// ① bind
setTimeout(obj.foo.bind(obj), 100);
// ② 箭头函数包裹(推荐,可读性好)
setTimeout(() => obj.foo(), 100);
// ③ 数组方法的 thisArg 参数
[1, 2].forEach(obj.foo, obj);
// ④ class field 箭头函数(见下文)
支持 thisArg 的数组方法:
forEach map filter some every find findIndex findLast findLastIndex flatMap
不支持的: reduce reduceRight sort(它们的第二参数另有用途)
五、Class 中的 this
类体内永远是严格模式
class Counter {
count = 0;
inc() { this.count++; } // 原型方法 → this 会丢失
dec = () => { this.count--; }; // 类字段箭头函数 → this 永久绑定实例
}
const c = new Counter();
const { inc, dec } = c;
dec(); // ✅ 正常
inc(); // ❌ TypeError: Cannot read properties of undefined
两者的本质区别:
原型方法 inc() |
类字段 dec = () => {} |
|
|---|---|---|
| 存放位置 | Counter.prototype |
每个实例上 |
| 内存 | 共享一份 | 每个实例一份(内存开销大) |
this |
调用时决定 | 定义时锁死为实例 |
可被 call 改变 |
是 | 否 |
可被子类 super 调用 |
是 | 否 |
推荐做法: 默认用原型方法,只在需要作为回调传递时用箭头字段,或在构造器中 bind:
class Counter {
constructor() {
this.inc = this.inc.bind(this); // 等价效果
}
}
静态方法中的 this
class A {
static create() { return new this(); } // this === 类本身,支持子类继承
}
class B extends A {}
B.create(); // 返回 B 的实例,而非 A
继承与 super
class Base {
constructor() { this.type = 'base'; }
hello() { console.log('base hello'); }
}
class Child extends Base {
constructor() {
// console.log(this); // ❌ ReferenceError: 必须先调用 super()
super(); // 派生类中,super() 之前 this 处于 TDZ
console.log(this.type);// 'base'
}
hello() {
super.hello(); // super 方法调用时,this 仍是当前实例
}
}
私有字段的坑
class A {
#secret = 1;
get() { return this.#secret; }
}
const a = new A();
a.get.call({}); // TypeError: Cannot read private member #secret
// 私有字段是"品牌检查",this 必须是真实实例
六、各类场景速查
DOM 事件
btn.addEventListener('click', function () {
console.log(this); // === event.currentTarget (btn)
});
btn.addEventListener('click', () => {
console.log(this); // 外层作用域的 this,不是 btn
});
handleEvent 特殊接口(传对象而非函数):
const handler = {
count: 0,
handleEvent(e) { console.log(this.count++, e.type); } // this === handler
};
btn.addEventListener('click', handler);
内联 onclick="..." 属性中的 this 也是元素本身。
定时器
setTimeout(function () {
console.log(this);
// 浏览器非严格: window
// Node: Timeout 对象
}, 0);
getter / setter
const obj = {
_v: 1,
get v() { return this._v; } // this 是访问该属性的对象
};
Proxy 中的 this 泄漏
const target = { greet() { return this; } };
const proxy = new Proxy(target, {});
proxy.greet() === proxy; // true —— this 是 proxy,不是 target
// 若 target 用了私有字段或内部槽(如 Map/Set),会报错
标签模板 / 生成器 / async
async function f() { return this; } // 与普通函数规则一致
function* g() { yield this; } // 同上
七、手写实现(面试高频)
Function.prototype.myCall = function (context, ...args) {
if (typeof this !== 'function') throw new TypeError('not a function');
context = context ?? globalThis;
if (Object(context) !== context) context = Object(context); // 原始值装箱
const key = Symbol('fn'); // Symbol 避免属性名冲突
context[key] = this;
try {
return context[key](...args);
} finally {
delete context[key];
}
};
Function.prototype.myApply = function (context, args = []) {
return this.myCall(context, ...args);
};
Function.prototype.myBind = function (context, ...preArgs) {
const fn = this;
if (typeof fn !== 'function') throw new TypeError('not a function');
function bound(...args) {
// 关键:new 调用时忽略 context,使用新建的 this
return fn.apply(
new.target ? this : context,
[...preArgs, ...args]
);
}
// 维持原型链,使 instanceof 正确
bound.prototype = Object.create(fn.prototype || null);
return bound;
};
// 手写 new
function myNew(Ctor, ...args) {
const obj = Object.create(Ctor.prototype);
const ret = Ctor.apply(obj, args);
return (ret !== null && (typeof ret === 'object' || typeof ret === 'function'))
? ret
: obj;
}
八、综合练习(先自己作答)
var name = 'global'; // 注意:必须是 var,let/const 不挂载到 window
const obj = {
name: 'obj',
f1() { console.log(this.name); },
f2: () => console.log(this?.name),
f3() {
return function () { console.log(this?.name); };
},
f4() {
return () => console.log(this.name);
}
};
obj.f1(); // ?
obj.f2(); // ?
obj.f3()(); // ?
obj.f4()(); // ?
const g = obj.f1;
g(); // ?
obj.f1.call({ name: 'x' }); // ?
new obj.f1(); // ?
答案
obj.f1() → 'obj' 隐式绑定
obj.f2() → 'global'/undefined 箭头函数继承顶层 this
obj.f3()() → 'global'/undefined 返回的函数独立调用,默认绑定
obj.f4()() → 'obj' 箭头函数继承 f4 的 this
g() → 'global'/undefined 隐式丢失
obj.f1.call({...})→ 'x' 显式绑定
new obj.f1() → undefined new 绑定,新对象上没有 name
(浏览器非严格模式下顶层 this 是 window,故为 'global';ESM/严格模式下为 undefined 或报错)
九、终极判断流程图
函数是箭头函数?
├─ 是 → this = 定义时外层作用域的 this(向上查找,不可改变)
└─ 否 ↓
用 new 调用? ──→ 是 → this = 新创建的对象
↓ 否
用 call/apply/bind 调用? ──→ 是 → this = 指定的对象
↓ 否 (非严格下 null/undefined 会替换为 globalThis)
通过 obj.fn() 调用? ──→ 是 → this = obj(只看最后一个点)
↓ 否
严格模式? ──→ 是 → this = undefined
└→ 否 → this = globalThis
十、最佳实践
- 优先用箭头函数写回调,避免
this丢失 - 对象方法用简写语法
foo() {},不要用箭头函数(除非确实想要外层this) - class 中默认用原型方法,需要传递为回调时才用箭头字段或构造器
bind - 全项目开启严格模式 / 使用 ESM,让
this错误尽早暴露为undefined报错 - TypeScript 用户:开启
noImplicitThis,并善用显式this参数:function handler(this: HTMLButtonElement, e: MouseEvent) { /* ... */ } - 不确定时,打印
console.log(this)—— 比推理快十倍
需要我把这份指南写入文件(比如 docs/js-this-guide.md),或者补充某个章节的深入内容(如 Proxy receiver、React/Vue 中的实战案例)吗?
JavaScript this 指南
一、核心心法
this不是在函数定义时确定的,而是在函数被调用时确定的。
判断this,只需要看一件事:这个函数是怎么被调用的(调用点)。
唯一的例外是箭头函数——它根本没有自己的 this。
二、五种绑定规则
规则 1:默认绑定(独立函数调用)
function foo() {
console.log(this);
}
foo(); // 非严格模式: globalThis (浏览器 window / Node global)
// 严格模式: undefined
'use strict';
function foo() { console.log(this); }
foo(); // undefined
顶层 this 的值取决于环境:
| 环境 | 顶层 this |
|---|---|
浏览器 <script> |
window |
ES Module (.mjs / type="module") |
undefined |
CommonJS 模块 (Node .js) |
module.exports(即 {}) |
| 函数内(非严格) | globalThis |
| 函数内(严格) | undefined |
| class 内部 | 永远严格模式 |
规则 2:隐式绑定(方法调用)
看调用时最后一个点前面是谁。
const obj = {
name: 'obj',
foo() { console.log(this.name); }
};
obj.foo(); // 'obj'
多层嵌套只看最后一层:
const a = { name: 'a', b: { name: 'b', foo() { console.log(this.name); } } };
a.b.foo(); // 'b' ← 不是 'a'
方括号访问同理:
a.b['foo'](); // 'b'
规则 3:显式绑定(call / apply / bind)
function greet(greeting, punct) {
console.log(greeting + ', ' + this.name + punct);
}
const me = { name: 'Alice' };
greet.call(me, 'Hi', '!'); // 逐个传参
greet.apply(me, ['Hi', '!']); // 数组传参 (Apply → Array)
const bound = greet.bind(me, 'Hi'); // 返回新函数 + 支持柯里化
bound('!');
bind 的三个特性:
- 返回一个新函数,不立即执行
- 支持参数预置(柯里化)
- 硬绑定:绑定后无法再被
call/apply改变
const hard = greet.bind(me);
hard.call({ name: 'Bob' }, 'Yo', '!'); // 依然是 Alice
传 null / undefined 的坑:
function foo() { console.log(this); }
foo.call(null); // 非严格: globalThis(被替换了!) 严格: null
安全做法——DMZ 空对象:
const DMZ = Object.create(null); // 比 {} 更干净,无原型
foo.apply(DMZ, [1, 2]);
原始值会被装箱(仅非严格模式):
function f() { console.log(typeof this); }
f.call(1); // 'object' (Number 包装对象)
// 严格模式下: 'number'
规则 4:new 绑定
new Foo() 做了四件事:
- 创建一个新的空对象
- 将该对象的
[[Prototype]]指向Foo.prototype - 以该对象为
this执行Foo - 如果
Foo没有返回一个对象,则返回这个新对象
function Person(name) {
this.name = name;
// return { name: 'hacked' }; // 若返回对象,则 new 的结果是它
// return 42; // 返回原始值 → 被忽略,仍返回 this
}
const p = new Person('Alice'); // p.name === 'Alice'
规则 5:箭头函数(词法绑定)
箭头函数没有自己的 this,它继承定义时所在外层作用域的 this,且永久不可改变。
const obj = {
name: 'obj',
regular() {
const arrow = () => console.log(this.name);
arrow(); // 'obj' ← 继承 regular 的 this
},
arrowMethod: () => console.log(this?.name) // ⚠️ 继承的是模块/全局的 this
};
obj.regular(); // 'obj'
obj.arrowMethod(); // undefined ← 经典陷阱!
箭头函数无法被改变:
const arrow = () => console.log(this);
arrow.call({ a: 1 }); // call 无效
arrow.bind({ a: 1 })(); // bind 无效
new arrow(); // TypeError: arrow is not a constructor
箭头函数也没有 arguments、super、new.target。
三、优先级
new 绑定 > 显式绑定(bind/call/apply) > 隐式绑定(obj.fn) > 默认绑定
箭头函数不在此列——它压根不参与,直接看词法作用域。
验证 new > bind:
function Foo(a) { this.a = a; }
const obj = {};
const B = Foo.bind(obj);
B(2); // obj.a === 2 (bind 生效)
const b = new B(3);// b.a === 3, obj.a 仍是 2 ← new 赢了
这正是
bindpolyfill 里必须判断new.target的原因。
四、隐式丢失:最常见的 Bug 来源
1. 赋值给变量
const obj = { name: 'obj', foo() { console.log(this?.name); } };
const fn = obj.foo; // 只拿到了函数引用,丢掉了 obj
fn(); // undefined
2. 作为回调传递
setTimeout(obj.foo, 100); // 丢失
[1].forEach(obj.foo); // 丢失
btn.addEventListener('click', obj.foo); // this 变成 btn
3. 解构
const { foo } = obj;
foo(); // 丢失
这就是 React 中
const { setState } = this会出问题、
以及 Vue3 用ref而非this的部分原因。
4. 间接引用
const o1 = { name: 'o1', foo() { console.log(this.name); } };
const o2 = { name: 'o2' };
o2.foo = o1.foo;
o2.foo(); // 'o2' (正常隐式绑定)
(o2.foo = o1.foo)(); // 赋值表达式返回函数本身 → 默认绑定 → undefined/报错
5. 解决方案汇总
// ① bind
setTimeout(obj.foo.bind(obj), 100);
// ② 箭头函数包裹(推荐,可读性好)
setTimeout(() => obj.foo(), 100);
// ③ 数组方法的 thisArg 参数
[1, 2].forEach(obj.foo, obj);
// ④ class field 箭头函数(见下文)
支持 thisArg 的数组方法:
forEach map filter some every find findIndex findLast findLastIndex flatMap
不支持的: reduce reduceRight sort(它们的第二参数另有用途)
五、Class 中的 this
类体内永远是严格模式
class Counter {
count = 0;
inc() { this.count++; } // 原型方法 → this 会丢失
dec = () => { this.count--; }; // 类字段箭头函数 → this 永久绑定实例
}
const c = new Counter();
const { inc, dec } = c;
dec(); // ✅ 正常
inc(); // ❌ TypeError: Cannot read properties of undefined
两者的本质区别:
原型方法 inc() |
类字段 dec = () => {} |
|
|---|---|---|
| 存放位置 | Counter.prototype |
每个实例上 |
| 内存 | 共享一份 | 每个实例一份(内存开销大) |
this |
调用时决定 | 定义时锁死为实例 |
可被 call 改变 |
是 | 否 |
可被子类 super 调用 |
是 | 否 |
推荐做法: 默认用原型方法,只在需要作为回调传递时用箭头字段,或在构造器中 bind:
class Counter {
constructor() {
this.inc = this.inc.bind(this); // 等价效果
}
}
静态方法中的 this
class A {
static create() { return new this(); } // this === 类本身,支持子类继承
}
class B extends A {}
B.create(); // 返回 B 的实例,而非 A
继承与 super
class Base {
constructor() { this.type = 'base'; }
hello() { console.log('base hello'); }
}
class Child extends Base {
constructor() {
// console.log(this); // ❌ ReferenceError: 必须先调用 super()
super(); // 派生类中,super() 之前 this 处于 TDZ
console.log(this.type);// 'base'
}
hello() {
super.hello(); // super 方法调用时,this 仍是当前实例
}
}
私有字段的坑
class A {
#secret = 1;
get() { return this.#secret; }
}
const a = new A();
a.get.call({}); // TypeError: Cannot read private member #secret
// 私有字段是"品牌检查",this 必须是真实实例
六、各类场景速查
DOM 事件
btn.addEventListener('click', function () {
console.log(this); // === event.currentTarget (btn)
});
btn.addEventListener('click', () => {
console.log(this); // 外层作用域的 this,不是 btn
});
handleEvent 特殊接口(传对象而非函数):
const handler = {
count: 0,
handleEvent(e) { console.log(this.count++, e.type); } // this === handler
};
btn.addEventListener('click', handler);
内联 onclick="..." 属性中的 this 也是元素本身。
定时器
setTimeout(function () {
console.log(this);
// 浏览器非严格: window
// Node: Timeout 对象
}, 0);
getter / setter
const obj = {
_v: 1,
get v() { return this._v; } // this 是访问该属性的对象
};
Proxy 中的 this 泄漏
const target = { greet() { return this; } };
const proxy = new Proxy(target, {});
proxy.greet() === proxy; // true —— this 是 proxy,不是 target
// 若 target 用了私有字段或内部槽(如 Map/Set),会报错
标签模板 / 生成器 / async
async function f() { return this; } // 与普通函数规则一致
function* g() { yield this; } // 同上
七、手写实现(面试高频)
Function.prototype.myCall = function (context, ...args) {
if (typeof this !== 'function') throw new TypeError('not a function');
context = context ?? globalThis;
if (Object(context) !== context) context = Object(context); // 原始值装箱
const key = Symbol('fn'); // Symbol 避免属性名冲突
context[key] = this;
try {
return context[key](...args);
} finally {
delete context[key];
}
};
Function.prototype.myApply = function (context, args = []) {
return this.myCall(context, ...args);
};
Function.prototype.myBind = function (context, ...preArgs) {
const fn = this;
if (typeof fn !== 'function') throw new TypeError('not a function');
function bound(...args) {
// 关键:new 调用时忽略 context,使用新建的 this
return fn.apply(
new.target ? this : context,
[...preArgs, ...args]
);
}
// 维持原型链,使 instanceof 正确
bound.prototype = Object.create(fn.prototype || null);
return bound;
};
// 手写 new
function myNew(Ctor, ...args) {
const obj = Object.create(Ctor.prototype);
const ret = Ctor.apply(obj, args);
return (ret !== null && (typeof ret === 'object' || typeof ret === 'function'))
? ret
: obj;
}
八、综合练习(先自己作答)
var name = 'global'; // 注意:必须是 var,let/const 不挂载到 window
const obj = {
name: 'obj',
f1() { console.log(this.name); },
f2: () => console.log(this?.name),
f3() {
return function () { console.log(this?.name); };
},
f4() {
return () => console.log(this.name);
}
};
obj.f1(); // ?
obj.f2(); // ?
obj.f3()(); // ?
obj.f4()(); // ?
const g = obj.f1;
g(); // ?
obj.f1.call({ name: 'x' }); // ?
new obj.f1(); // ?
答案
obj.f1() → 'obj' 隐式绑定
obj.f2() → 'global'/undefined 箭头函数继承顶层 this
obj.f3()() → 'global'/undefined 返回的函数独立调用,默认绑定
obj.f4()() → 'obj' 箭头函数继承 f4 的 this
g() → 'global'/undefined 隐式丢失
obj.f1.call({...})→ 'x' 显式绑定
new obj.f1() → undefined new 绑定,新对象上没有 name
(浏览器非严格模式下顶层 this 是 window,故为 'global';ESM/严格模式下为 undefined 或报错)
九、终极判断流程图
函数是箭头函数?
├─ 是 → this = 定义时外层作用域的 this(向上查找,不可改变)
└─ 否 ↓
用 new 调用? ──→ 是 → this = 新创建的对象
↓ 否
用 call/apply/bind 调用? ──→ 是 → this = 指定的对象
↓ 否 (非严格下 null/undefined 会替换为 globalThis)
通过 obj.fn() 调用? ──→ 是 → this = obj(只看最后一个点)
↓ 否
严格模式? ──→ 是 → this = undefined
└→ 否 → this = globalThis
十、最佳实践
- 优先用箭头函数写回调,避免
this丢失 - 对象方法用简写语法
foo() {},不要用箭头函数(除非确实想要外层this) - class 中默认用原型方法,需要传递为回调时才用箭头字段或构造器
bind - 全项目开启严格模式 / 使用 ESM,让
this错误尽早暴露为undefined报错 - TypeScript 用户:开启
noImplicitThis,并善用显式this参数:function handler(this: HTMLButtonElement, e: MouseEvent) { /* ... */ } - 不确定时,打印
console.log(this)—— 比推理快十倍
需要我把这份指南写入文件(比如 docs/js-this-guide.md),或者补充某个章节的深入内容(如 Proxy receiver、React/Vue 中的实战案例)吗?
使用animateTo循环播放动画时刷新状态变量无法刷新UI
问题现象
在使用animateTo方法实现循环动画的过程中,如果希望在每次动画播放时通过更新状态变量来刷新UI,可能会遇到UI无法动态响应更新的问题。例如,即使在事件中配置状态变量changeNum每次播放增加1,并设置动画循环播放3次,实际的UI显示仅更新一次,而onFinish的回调中changeNum仅记录1。
@Entry
@Component
struct AnimateToExample {
@State widthSize: number = 250;
@State heightSize: number = 100;
uiContext: UIContext | undefined = undefined;
@State changeNum: number = 0;
aboutToAppear() {
this.uiContext = this.getUIContext();
if (!this.uiContext) {
console.warn('no uiContext');
return;
}
}
playLrc() {
this.uiContext?.animateTo({
duration: 2000,
curve: Curve.EaseOut,
iterations: 3,
playMode: PlayMode.Normal,
onFinish: () => {
console.info('play end');
}
}, () => {
this.widthSize = 300;
this.heightSize = 60;
this.changeNum++;
});
}
build() {
Column() {
Button(this.changeNum.toString())
.width(this.widthSize)
.height(this.heightSize)
.margin(30)
.onClick(() => {
this.playLrc();
});
}.width('100%').margin({ top: 5 });
}
}
问题效果预览:
点击放大
背景知识
animateTo:提供animateTo接口来指定由于闭包代码导致的状态变化插入过渡动效。
关键帧动画 (keyframeAnimateTo):在UIContext中提供keyframeAnimateTo接口来指定若干个关键帧状态,实现分段的动画。
解决方案
显示动画的参数iterations表示动画的执行次数,并不代表闭包函数里面的逻辑执行次数,所以问题代码中iterations:3并不会使得修改宽高动画执行3次,需要使用关键帧动画。
使用关键帧动画,可以分段执行动画逻辑。在动画结束后在onFinish回调中递归执行动画,从而实现预期效果。
@Entry
@Component
struct AnimateToExample {
@State widthSize: number = 250;
@State heightSize: number = 100;
uiContext: UIContext | undefined = undefined;
@State changeNum: number = 0;
aboutToAppear() {
this.uiContext = this.getUIContext();
};
playLrc() {
// 使用关键帧动画
this.uiContext?.keyframeAnimateTo({
iterations: 1,
onFinish: () => {
if (this.changeNum % 3 !== 0) {
this.playLrc();
}
}
}, [
{
// 第一段关键帧动画
duration: 800,
event: () => {
this.widthSize = 300;
this.heightSize = 60;
this.changeNum++;
}
},
// 第二段关键帧动画
{
duration: 500,
event: () => {
this.widthSize = 250;
this.heightSize = 100;
}
}
]);
}
build() {
Column() {
Button(this.changeNum.toString())
.width(this.widthSize)
.height(this.heightSize)
.margin(30)
.onClick(() => {
this.playLrc();
});
}.width('100%').margin({ top: 5 });
}
}
https://sites.google.com/view/6hnwoa03/home
https://sites.google.com/view/2bdovq40/home
https://sites.google.com/view/3ykzjl65/home
https://sites.google.com/view/3ypjlx51/home
https://sites.google.com/view/2vecse29/home
https://sites.google.com/view/8wjmoz24/home
https://sites.google.com/view/6anaaz10/home
https://sites.google.com/view/6vztlx87/home
https://sites.google.com/view/9hxqwy10/home
https://sites.google.com/view/6emmnf53/home
https://sites.google.com/view/9qgmon96/home
https://sites.google.com/view/8vqblh76/home
https://sites.google.com/view/5repzi61/home
https://sites.google.com/view/6iqxbj96/home
https://sites.google.com/view/4fiyto32/home
https://sites.google.com/view/1uimwi96/home
https://sites.google.com/view/5ibqaj17/home
https://sites.google.com/view/9nqdyq84/home
https://sites.google.com/view/4lwyyd65/home
https://sites.google.com/view/3gawuk38/home
https://sites.google.com/view/5lgzag04/home
https://sites.google.com/view/1imonc99/home
https://sites.google.com/view/9xyukm01/home
https://sites.google.com/view/6nalbh77/home
https://sites.google.com/view/2ypecr06/home
https://sites.google.com/view/5zdody20/home
https://sites.google.com/view/6wkgom88/home
https://sites.google.com/view/8zwqqi53/home
https://sites.google.com/view/0qyluw74/home
https://sites.google.com/view/5aphjd68/home
https://sites.google.com/view/2sacyn38/home
https://sites.google.com/view/8dbjrm35/home
https://sites.google.com/view/6unuwk46/home
https://sites.google.com/view/5hrlie99/home
https://sites.google.com/view/6mxmyz56/home
https://sites.google.com/view/9gdzzy11/home
https://sites.google.com/view/3wkfon30/home
https://sites.google.com/view/7jesbl97/home
https://sites.google.com/view/7xllml61/home
https://sites.google.com/view/3mpkjx30/home
https://sites.google.com/view/6idizy90/home
https://sites.google.com/view/1czsip30/home
https://sites.google.com/view/3ezdwf97/home
https://sites.google.com/view/1ydubh84/home
https://sites.google.com/view/4cvedy61/home
https://sites.google.com/view/7yyumz73/home
https://sites.google.com/view/5pkquw00/home
https://sites.google.com/view/7dceex86/home
https://sites.google.com/view/3gnvyg34/home
https://sites.google.com/view/2ehwht60/home
https://sites.google.com/view/3ocyay63/home
https://sites.google.com/view/6uizgx22/home
https://sites.google.com/view/3cgblb20/home
https://sites.google.com/view/3guucg59/home
https://sites.google.com/view/9cdybx22/home
https://sites.google.com/view/7giaxy95/home
https://sites.google.com/view/3qkkfi06/home
https://sites.google.com/view/9goqyg50/home
https://sites.google.com/view/6sfntm95/home
https://sites.google.com/view/8hekup65/home
https://sites.google.com/view/8dypsx49/home
https://sites.google.com/view/1emhrm38/home
https://sites.google.com/view/1yyqhu29/home
https://sites.google.com/view/5heoek19/home
https://sites.google.com/view/3tvkkw85/home
https://sites.google.com/view/4dqdwv87/home
https://sites.google.com/view/2tsotl60/home
https://sites.google.com/view/7mpwib24/home
https://sites.google.com/view/9dlwvj94/home
https://sites.google.com/view/3hggdc60/home
https://sites.google.com/view/3vhyat21/home
https://sites.google.com/view/7vyebf30/home
https://sites.google.com/view/4wnnbn37/home
https://sites.google.com/view/6mjlkw77/home
https://sites.google.com/view/0jcihr79/home
https://sites.google.com/view/6lpxba81/home
https://sites.google.com/view/7nmcwv30/home
https://sites.google.com/view/3ztaut28/home
https://sites.google.com/view/1ecdeq22/home
https://sites.google.com/view/0dgpba93/home
https://sites.google.com/view/1aqqqo25/home
https://sites.google.com/view/1raxlw38/home
https://sites.google.com/view/3snsha82/home
https://sites.google.com/view/5uwlrm29/home
https://sites.google.com/view/4feaqq70/home
https://sites.google.com/view/3nlhqj52/home
https://sites.google.com/view/1pidkm14/home
https://sites.google.com/view/2wqdnc45/home
https://sites.google.com/view/1iwyhj71/home
https://sites.google.com/view/8evxkf88/home
https://sites.google.com/view/5zbhhn62/home
https://sites.google.com/view/9hjcnp07/home
https://sites.google.com/view/3mxnek99/home
https://sites.google.com/view/9bmsbt04/home
https://sites.google.com/view/2lezmt81/home
https://sites.google.com/view/6qmqey84/home
https://coub.com/view/kvpoylkwik
https://coub.com/view/i77t3xznqp
https://coub.com/view/f7trec03b8
https://coub.com/view/3n90r2j0rd
https://coub.com/view/y57xd610co
https://coub.com/view/eaephw8q6a
https://coub.com/view/hrrjoj4h9l
https://coub.com/view/wsjrcxld3n
https://coub.com/view/npkzo9bhsy
https://coub.com/view/e684ftu7dh
https://coub.com/view/3o0ro925t2
https://coub.com/view/fnl2q6kiht
https://coub.com/view/n99qs782yl
https://coub.com/view/1ondlagd1i
https://coub.com/view/9z6hsd1v17
https://coub.com/view/10d32yhsna
https://coub.com/view/ouwb3ocnhh
https://coub.com/view/xdc8nn4avc
https://coub.com/view/09w1hw0sip
https://coub.com/view/zlwigv6w8f
https://coub.com/view/j3r71lim3x
https://coub.com/view/2430o5fdrc
https://coub.com/view/ujctsdcdwy
https://coub.com/view/0buxtgmwxv
https://coub.com/view/377gnonlc2
https://coub.com/view/2nxu03hy9c
https://coub.com/view/qc7d9vj1v0
https://coub.com/view/3mqfz4b3fw
https://coub.com/view/g7ijtiaan7
https://coub.com/view/5inj7wcs41
https://coub.com/view/re695cu3l0
https://coub.com/view/qk2gxc95lg
https://coub.com/view/1g97y8qqtc
https://coub.com/view/wbze3qybuc
https://coub.com/view/ht8lrsgyup
https://coub.com/view/uwy6yesicp
https://coub.com/view/8xz2oua5mz
https://coub.com/view/va9tyn2k4l
https://coub.com/view/5kamzbr4mi
https://coub.com/view/9slmznc3ol
https://coub.com/view/dnhe4x7otv
https://coub.com/view/libk78392s
https://coub.com/view/2splvx7ais
https://coub.com/view/0fqk03fwlz
https://coub.com/view/u3but76ukt
https://coub.com/view/2xrg8kdx5o
https://coub.com/view/iqeb7cwj8k
https://coub.com/view/lx86s7cfpr
https://coub.com/view/qin8evf0sk
https://coub.com/view/ul1dmz1db2
问题现象
在使用animateTo方法实现循环动画的过程中,如果希望在每次动画播放时通过更新状态变量来刷新UI,可能会遇到UI无法动态响应更新的问题。例如,即使在事件中配置状态变量changeNum每次播放增加1,并设置动画循环播放3次,实际的UI显示仅更新一次,而onFinish的回调中changeNum仅记录1。
@Entry
@Component
struct AnimateToExample {
@State widthSize: number = 250;
@State heightSize: number = 100;
uiContext: UIContext | undefined = undefined;
@State changeNum: number = 0;
aboutToAppear() {
this.uiContext = this.getUIContext();
if (!this.uiContext) {
console.warn('no uiContext');
return;
}
}
playLrc() {
this.uiContext?.animateTo({
duration: 2000,
curve: Curve.EaseOut,
iterations: 3,
playMode: PlayMode.Normal,
onFinish: () => {
console.info('play end');
}
}, () => {
this.widthSize = 300;
this.heightSize = 60;
this.changeNum++;
});
}
build() {
Column() {
Button(this.changeNum.toString())
.width(this.widthSize)
.height(this.heightSize)
.margin(30)
.onClick(() => {
this.playLrc();
});
}.width('100%').margin({ top: 5 });
}
}
问题效果预览:
点击放大
背景知识
animateTo:提供animateTo接口来指定由于闭包代码导致的状态变化插入过渡动效。
关键帧动画 (keyframeAnimateTo):在UIContext中提供keyframeAnimateTo接口来指定若干个关键帧状态,实现分段的动画。
解决方案
显示动画的参数iterations表示动画的执行次数,并不代表闭包函数里面的逻辑执行次数,所以问题代码中iterations:3并不会使得修改宽高动画执行3次,需要使用关键帧动画。
使用关键帧动画,可以分段执行动画逻辑。在动画结束后在onFinish回调中递归执行动画,从而实现预期效果。
@Entry
@Component
struct AnimateToExample {
@State widthSize: number = 250;
@State heightSize: number = 100;
uiContext: UIContext | undefined = undefined;
@State changeNum: number = 0;
aboutToAppear() {
this.uiContext = this.getUIContext();
};
playLrc() {
// 使用关键帧动画
this.uiContext?.keyframeAnimateTo({
iterations: 1,
onFinish: () => {
if (this.changeNum % 3 !== 0) {
this.playLrc();
}
}
}, [
{
// 第一段关键帧动画
duration: 800,
event: () => {
this.widthSize = 300;
this.heightSize = 60;
this.changeNum++;
}
},
// 第二段关键帧动画
{
duration: 500,
event: () => {
this.widthSize = 250;
this.heightSize = 100;
}
}
]);
}
build() {
Column() {
Button(this.changeNum.toString())
.width(this.widthSize)
.height(this.heightSize)
.margin(30)
.onClick(() => {
this.playLrc();
});
}.width('100%').margin({ top: 5 });
}
}
https://sites.google.com/view/6hnwoa03/home
https://sites.google.com/view/2bdovq40/home
https://sites.google.com/view/3ykzjl65/home
https://sites.google.com/view/3ypjlx51/home
https://sites.google.com/view/2vecse29/home
https://sites.google.com/view/8wjmoz24/home
https://sites.google.com/view/6anaaz10/home
https://sites.google.com/view/6vztlx87/home
https://sites.google.com/view/9hxqwy10/home
https://sites.google.com/view/6emmnf53/home
https://sites.google.com/view/9qgmon96/home
https://sites.google.com/view/8vqblh76/home
https://sites.google.com/view/5repzi61/home
https://sites.google.com/view/6iqxbj96/home
https://sites.google.com/view/4fiyto32/home
https://sites.google.com/view/1uimwi96/home
https://sites.google.com/view/5ibqaj17/home
https://sites.google.com/view/9nqdyq84/home
https://sites.google.com/view/4lwyyd65/home
https://sites.google.com/view/3gawuk38/home
https://sites.google.com/view/5lgzag04/home
https://sites.google.com/view/1imonc99/home
https://sites.google.com/view/9xyukm01/home
https://sites.google.com/view/6nalbh77/home
https://sites.google.com/view/2ypecr06/home
https://sites.google.com/view/5zdody20/home
https://sites.google.com/view/6wkgom88/home
https://sites.google.com/view/8zwqqi53/home
https://sites.google.com/view/0qyluw74/home
https://sites.google.com/view/5aphjd68/home
https://sites.google.com/view/2sacyn38/home
https://sites.google.com/view/8dbjrm35/home
https://sites.google.com/view/6unuwk46/home
https://sites.google.com/view/5hrlie99/home
https://sites.google.com/view/6mxmyz56/home
https://sites.google.com/view/9gdzzy11/home
https://sites.google.com/view/3wkfon30/home
https://sites.google.com/view/7jesbl97/home
https://sites.google.com/view/7xllml61/home
https://sites.google.com/view/3mpkjx30/home
https://sites.google.com/view/6idizy90/home
https://sites.google.com/view/1czsip30/home
https://sites.google.com/view/3ezdwf97/home
https://sites.google.com/view/1ydubh84/home
https://sites.google.com/view/4cvedy61/home
https://sites.google.com/view/7yyumz73/home
https://sites.google.com/view/5pkquw00/home
https://sites.google.com/view/7dceex86/home
https://sites.google.com/view/3gnvyg34/home
https://sites.google.com/view/2ehwht60/home
https://sites.google.com/view/3ocyay63/home
https://sites.google.com/view/6uizgx22/home
https://sites.google.com/view/3cgblb20/home
https://sites.google.com/view/3guucg59/home
https://sites.google.com/view/9cdybx22/home
https://sites.google.com/view/7giaxy95/home
https://sites.google.com/view/3qkkfi06/home
https://sites.google.com/view/9goqyg50/home
https://sites.google.com/view/6sfntm95/home
https://sites.google.com/view/8hekup65/home
https://sites.google.com/view/8dypsx49/home
https://sites.google.com/view/1emhrm38/home
https://sites.google.com/view/1yyqhu29/home
https://sites.google.com/view/5heoek19/home
https://sites.google.com/view/3tvkkw85/home
https://sites.google.com/view/4dqdwv87/home
https://sites.google.com/view/2tsotl60/home
https://sites.google.com/view/7mpwib24/home
https://sites.google.com/view/9dlwvj94/home
https://sites.google.com/view/3hggdc60/home
https://sites.google.com/view/3vhyat21/home
https://sites.google.com/view/7vyebf30/home
https://sites.google.com/view/4wnnbn37/home
https://sites.google.com/view/6mjlkw77/home
https://sites.google.com/view/0jcihr79/home
https://sites.google.com/view/6lpxba81/home
https://sites.google.com/view/7nmcwv30/home
https://sites.google.com/view/3ztaut28/home
https://sites.google.com/view/1ecdeq22/home
https://sites.google.com/view/0dgpba93/home
https://sites.google.com/view/1aqqqo25/home
https://sites.google.com/view/1raxlw38/home
https://sites.google.com/view/3snsha82/home
https://sites.google.com/view/5uwlrm29/home
https://sites.google.com/view/4feaqq70/home
https://sites.google.com/view/3nlhqj52/home
https://sites.google.com/view/1pidkm14/home
https://sites.google.com/view/2wqdnc45/home
https://sites.google.com/view/1iwyhj71/home
https://sites.google.com/view/8evxkf88/home
https://sites.google.com/view/5zbhhn62/home
https://sites.google.com/view/9hjcnp07/home
https://sites.google.com/view/3mxnek99/home
https://sites.google.com/view/9bmsbt04/home
https://sites.google.com/view/2lezmt81/home
https://sites.google.com/view/6qmqey84/home
https://coub.com/view/kvpoylkwik
https://coub.com/view/i77t3xznqp
https://coub.com/view/f7trec03b8
https://coub.com/view/3n90r2j0rd
https://coub.com/view/y57xd610co
https://coub.com/view/eaephw8q6a
https://coub.com/view/hrrjoj4h9l
https://coub.com/view/wsjrcxld3n
https://coub.com/view/npkzo9bhsy
https://coub.com/view/e684ftu7dh
https://coub.com/view/3o0ro925t2
https://coub.com/view/fnl2q6kiht
https://coub.com/view/n99qs782yl
https://coub.com/view/1ondlagd1i
https://coub.com/view/9z6hsd1v17
https://coub.com/view/10d32yhsna
https://coub.com/view/ouwb3ocnhh
https://coub.com/view/xdc8nn4avc
https://coub.com/view/09w1hw0sip
https://coub.com/view/zlwigv6w8f
https://coub.com/view/j3r71lim3x
https://coub.com/view/2430o5fdrc
https://coub.com/view/ujctsdcdwy
https://coub.com/view/0buxtgmwxv
https://coub.com/view/377gnonlc2
https://coub.com/view/2nxu03hy9c
https://coub.com/view/qc7d9vj1v0
https://coub.com/view/3mqfz4b3fw
https://coub.com/view/g7ijtiaan7
https://coub.com/view/5inj7wcs41
https://coub.com/view/re695cu3l0
https://coub.com/view/qk2gxc95lg
https://coub.com/view/1g97y8qqtc
https://coub.com/view/wbze3qybuc
https://coub.com/view/ht8lrsgyup
https://coub.com/view/uwy6yesicp
https://coub.com/view/8xz2oua5mz
https://coub.com/view/va9tyn2k4l
https://coub.com/view/5kamzbr4mi
https://coub.com/view/9slmznc3ol
https://coub.com/view/dnhe4x7otv
https://coub.com/view/libk78392s
https://coub.com/view/2splvx7ais
https://coub.com/view/0fqk03fwlz
https://coub.com/view/u3but76ukt
https://coub.com/view/2xrg8kdx5o
https://coub.com/view/iqeb7cwj8k
https://coub.com/view/lx86s7cfpr
https://coub.com/view/qin8evf0sk
https://coub.com/view/ul1dmz1db2
如何实现一个自定义高度的底部弹窗
问题现象
需要实现的底部弹窗可以设置一个最大高度。同时,实现的底部弹窗需要从下往上出现。
如果弹窗内部视图的高度超过了这个最大高度,弹窗内部的视图就以这个自定义最大高度进行滚动展示;
如果弹窗内部视图的高度没有超过这个最大高度,那么弹窗内部视图就完全展示;
背景知识
组件的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
如何解决拖拽功能和长按功能的冲突问题
问题现象
在List组件中,单个ListItem在同时设置拖拽功能与长按功能时,实际运行之后会产生冲突,如何解决单个ListItem拖拽功能和长按功能的冲突问题?
效果预览
点击放大
背景知识
支持统一拖拽提供了一种通过鼠标或手势触屏传递数据的机制,即从一个组件位置拖出数据并将其拖入到另一个组件位置,以触发响应。
长按手势通过长按屏幕触发长按手势事件。
由于拖拽事件和长按手势都需要通过长按来触发,因此这种组合手势类型会出现冲突,例如List组件中每个ListItem都设置单独的长按事件时,拖拽功能和长按手势功能就会出现冲突。
Grid网格容器,由“行”和“列”分割的单元格所组成,通过指定“项目”所在的单元格做出各种各样的布局。
滑动手势PanGesture,能够提供自定义拖拽事件的功能。
解决方案
由于在ListItem中设置单独的长按事件会与拖拽事件产生冲突,因此建议放弃List本身的拖拽事件,使用滑动手势PanGesture,自行实现拖拽过程的逻辑,然后再通过组合手势的功能,去实现其他的长按事件。
import curves from '@ohos.curves';
@Entry
@Component
struct Page {
// 元素数组
@State numbers: number[] = [];
// 多列
private str: string = '';
row: number = 4;
// 元素数组中最后一个元素的索引
@State lastIndex: number = 0;
@State dragItem: number = -1;
@State scaleItem: number = -1;
item: number = -1;
private dragRefOffsetX: number = 0;
private dragRefOffsetY: number = 0;
@State offsetX: number = 0;
@State offsetY: number = 0;
private FIX_VP_X: number = 108;
private FIX_VP_Y: number = 120;
aboutToAppear() {
for (let i = 1; i <= 36; i++) {
this.numbers.push(i);
}
this.lastIndex = this.numbers.length - 1;
// 多列
for (let i = 0; i < this.row; i++) {
this.str = this.str + '1fr ';
}
}
itemMove(index: number, newIndex: number): void {
console.info('index:' + index + ' newIndex:' + newIndex);
if (!this.isDraggable(newIndex)) {
return;
}
let tmp = this.numbers.splice(index, 1);
this.numbers.splice(newIndex, 0, tmp[0]);
}
// 向下滑
down(index: number): void {
if (!this.isDraggable(index + this.row)) {
return;
}
this.offsetY -= this.FIX_VP_Y;
this.dragRefOffsetY += this.FIX_VP_Y;
// 多列
this.itemMove(index, index + this.row);
}
// 向下滑(右下角为空)
down2(index: number): void {
if (!this.isDraggable(index + 3)) {
return;
}
this.offsetY -= this.FIX_VP_Y;
this.dragRefOffsetY += this.FIX_VP_Y;
this.itemMove(index, index + 3);
}
// 向上滑
up(index: number): void {
if (!this.isDraggable(index - this.row)) {
return;
}
this.offsetY += this.FIX_VP_Y;
this.dragRefOffsetY -= this.FIX_VP_Y;
this.itemMove(index, index - this.row);
}
// 向左滑
left(index: number): void {
if (!this.isDraggable(index - 1)) {
return;
}
this.offsetX += this.FIX_VP_X;
this.dragRefOffsetX -= this.FIX_VP_X;
this.itemMove(index, index - 1);
}
// 向右滑
right(index: number): void {
if (!this.isDraggable(index + 1)) {
return;
}
this.offsetX -= this.FIX_VP_X;
this.dragRefOffsetX += this.FIX_VP_X;
this.itemMove(index, index + 1);
}
// 向右下滑
lowerRight(index: number): void {
if (!this.isDraggable(index + this.row + 1)) {
return;
}
this.offsetX -= this.FIX_VP_X;
this.dragRefOffsetX += this.FIX_VP_X;
this.offsetY -= this.FIX_VP_Y;
this.dragRefOffsetY += this.FIX_VP_Y;
this.itemMove(index, index + this.row + 1);
}
// 向右上滑
upperRight(index: number): void {
if (!this.isDraggable(index - (this.row - 1))) {
return;
}
this.offsetX -= this.FIX_VP_X;
this.dragRefOffsetX += this.FIX_VP_X;
this.offsetY += this.FIX_VP_Y;
this.dragRefOffsetY -= this.FIX_VP_Y;
this.itemMove(index, index - (this.row - 1));
}
// 向左下滑
lowerLeft(index: number): void {
if (!this.isDraggable(index + (this.row - 1))) {
return;
}
this.offsetX += this.FIX_VP_X;
this.dragRefOffsetX -= this.FIX_VP_X;
this.offsetY -= this.FIX_VP_Y;
this.dragRefOffsetY += this.FIX_VP_Y;
this.itemMove(index, index + (this.row - 1));
}
// 向左上滑
upperLeft(index: number): void {
if (!this.isDraggable(index - (this.row + 1))) {
return;
}
this.offsetX += this.FIX_VP_X;
this.dragRefOffsetX -= this.FIX_VP_X;
this.offsetY += this.FIX_VP_Y;
this.dragRefOffsetY -= this.FIX_VP_Y;
this.itemMove(index, index - (this.row + 1));
}
// 通过元素的索引,控制对应元素是否能移动排序
isDraggable(index: number): boolean {
console.info(index: ${index});
return index > -1; // 恒成立,所有元素均可移动排序
}
build() {
Column() {
Grid() {
ForEach(this.numbers, (item: number) => {
GridItem() {
Text(item + '')
.fontSize(16)
.width('100%')
.textAlign(TextAlign.Center)
.height(100)
.borderRadius(10)
.backgroundColor(0xFFFFFF)
.shadow(this.scaleItem == item ? {
radius: 70,
color: '#15000000',
offsetX: 0,
offsetY: 0
} :
{
radius: 0,
color: '#15000000',
offsetX: 0,
offsetY: 0
})
.animation({ curve: Curve.Sharp, duration: 300 });
}
// 添加震动
.onTouch(() => {
})
.onAreaChange((oldVal, newVal) => {
// 多列
this.FIX_VP_X = Math.round(newVal.width as number);
this.FIX_VP_Y = Math.round(newVal.height as number);
console.info(oldVal:${JSON.stringify(oldVal)});
})
// 指定固定GridItem不响应事件
.hitTestBehavior(this.isDraggable(this.numbers.indexOf(item)) ? HitTestMode.Default : HitTestMode.None)
.scale({ x: this.scaleItem == item ? 1.05 : 1, y: this.scaleItem == item ? 1.05 : 1 })
.zIndex(this.dragItem == item ? 1 : 0)
.translate(this.dragItem == item ? { x: this.offsetX, y: this.offsetY } : { x: 0, y: 0 })
.padding(10)
.gesture(
GestureGroup(GestureMode.Sequence,
LongPressGesture({ repeat: true, duration: 50 })
.onAction((event?: GestureEvent) => {
console.info(event: ${event});
this.getUIContext().animateTo({ curve: Curve.Friction, duration: 300 }, () => {
this.scaleItem = item;
});
})
.onActionEnd(() => {
this.getUIContext().animateTo({ curve: Curve.Friction, duration: 300 }, () => {
this.scaleItem = -1;
});
}),
PanGesture({ fingers: 1, direction: null, distance: 0 })
.onActionStart(() => {
this.dragItem = item;
this.dragRefOffsetX = 0;
this.dragRefOffsetY = 0;
})
.onActionUpdate((event: GestureEvent) => {
this.offsetY = event.offsetY - this.dragRefOffsetY;
this.offsetX = event.offsetX - this.dragRefOffsetX;
console.info(移动过程中event.offsetY:${event.offsetY},
this.dragRefOffsetY: ${this.dragRefOffsetY}, this.offsetY: ${this.offsetY});
console.info(移动过程中event.offsetX:${event.offsetX},
this.dragRefOffsetX: ${this.dragRefOffsetX}, this.offsetX: ${this.offsetX});
this.getUIContext().animateTo({ curve: curves.interpolatingSpring(0, 1, 400, 38) }, () => {
let index = this.numbers.indexOf(this.dragItem);
console.info('index= ', index);
if (this.offsetY >= this.FIX_VP_Y / 2 &&
(this.offsetX <= this.FIX_VP_X / 2 && this.offsetX >= -this.FIX_VP_X / 2)
&& (index + this.row <= this.lastIndex)) {
// 向下滑
this.down(index);
} else if (this.offsetY <= -this.FIX_VP_Y / 2 &&
(this.offsetX <= this.FIX_VP_X / 2 && this.offsetX >= -this.FIX_VP_X / 2)
&& index - this.row >= 0) {
// 向上滑
this.up(index);
} else if (this.offsetX >= this.FIX_VP_X / 2 &&
(this.offsetY <= this.FIX_VP_Y / 2 && this.offsetY >= -this.FIX_VP_Y / 2)
&& !(((index - (this.row - 1)) % this.row == 0) || index == this.lastIndex)) {
// 向右滑
this.right(index);
} else if (this.offsetX <= -this.FIX_VP_X / 2 &&
(this.offsetY <= this.FIX_VP_Y / 2 && this.offsetY >= -this.FIX_VP_Y / 2)
&& !(index % this.row == 0)) {
// 向左滑
this.left(index);
} else if (this.offsetX >= this.FIX_VP_X / 2 && this.offsetY >= this.FIX_VP_Y / 2
&& ((index + this.row + 1 <= this.lastIndex && !((index - (this.row - 1)) % this.row == 0)) ||
!((index - (this.row - 1)) % this.row == 0))) {
// 向右下滑
this.lowerRight(index);
} else if (this.offsetX >= this.FIX_VP_X / 2 && this.offsetY <= -this.FIX_VP_Y / 2
&& !((index - this.row < 0) || ((index - (this.row - 1)) % this.row == 0))) {
// 向右上滑
this.upperRight(index);
} else if (this.offsetX <= -this.FIX_VP_X / 2 && this.offsetY >= this.FIX_VP_Y / 2
&& (!(index % this.row == 0) && (index + (this.row - 1) <= this.lastIndex))) {
// 向左下滑
this.lowerLeft(index);
} else if (this.offsetX <= -this.FIX_VP_X / 2 && this.offsetY <= -this.FIX_VP_Y / 2
&& !((index <= this.row - 1) || (index % this.row == 0))) {
// 向左上滑
this.upperLeft(index);
} else if (this.offsetX >= this.FIX_VP_X / 2 && this.offsetY >= this.FIX_VP_Y / 2
&& (index == this.lastIndex)) {
// 向右下滑(右下角为空)
this.down2(index);
}
});
})
.onActionEnd(() => {
this.getUIContext().animateTo({ curve: curves.interpolatingSpring(0, 1, 400, 38) }, () => {
this.dragItem = -1;
});
this.getUIContext().animateTo({
curve: curves.interpolatingSpring(14, 1, 170, 17), delay: 150
}, () => {
this.scaleItem = -1;
});
})
)
.onCancel(() => {
this.getUIContext().animateTo({ curve: curves.interpolatingSpring(0, 1, 400, 38) }, () => {
this.dragItem = -1;
});
this.getUIContext().animateTo({
curve: curves.interpolatingSpring(14, 1, 170, 17)
}, () => {
this.scaleItem = -1;
});
})
);
}, (item: number) => item.toString());
}
.width('90%')
.editMode(true)
.scrollBar(BarState.Off)
// 多列
.columnsTemplate(this.str);
}
.expandSafeArea([SafeAreaType.SYSTEM], [SafeAreaEdge.TOP, SafeAreaEdge.BOTTOM])
.width('100%').height('100%').backgroundColor('#f1f3f5').padding({ top: 5 });
}
}
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
问题现象
在List组件中,单个ListItem在同时设置拖拽功能与长按功能时,实际运行之后会产生冲突,如何解决单个ListItem拖拽功能和长按功能的冲突问题?
效果预览
点击放大
背景知识
支持统一拖拽提供了一种通过鼠标或手势触屏传递数据的机制,即从一个组件位置拖出数据并将其拖入到另一个组件位置,以触发响应。
长按手势通过长按屏幕触发长按手势事件。
由于拖拽事件和长按手势都需要通过长按来触发,因此这种组合手势类型会出现冲突,例如List组件中每个ListItem都设置单独的长按事件时,拖拽功能和长按手势功能就会出现冲突。
Grid网格容器,由“行”和“列”分割的单元格所组成,通过指定“项目”所在的单元格做出各种各样的布局。
滑动手势PanGesture,能够提供自定义拖拽事件的功能。
解决方案
由于在ListItem中设置单独的长按事件会与拖拽事件产生冲突,因此建议放弃List本身的拖拽事件,使用滑动手势PanGesture,自行实现拖拽过程的逻辑,然后再通过组合手势的功能,去实现其他的长按事件。
import curves from '@ohos.curves';
@Entry
@Component
struct Page {
// 元素数组
@State numbers: number[] = [];
// 多列
private str: string = '';
row: number = 4;
// 元素数组中最后一个元素的索引
@State lastIndex: number = 0;
@State dragItem: number = -1;
@State scaleItem: number = -1;
item: number = -1;
private dragRefOffsetX: number = 0;
private dragRefOffsetY: number = 0;
@State offsetX: number = 0;
@State offsetY: number = 0;
private FIX_VP_X: number = 108;
private FIX_VP_Y: number = 120;
aboutToAppear() {
for (let i = 1; i <= 36; i++) {
this.numbers.push(i);
}
this.lastIndex = this.numbers.length - 1;
// 多列
for (let i = 0; i < this.row; i++) {
this.str = this.str + '1fr ';
}
}
itemMove(index: number, newIndex: number): void {
console.info('index:' + index + ' newIndex:' + newIndex);
if (!this.isDraggable(newIndex)) {
return;
}
let tmp = this.numbers.splice(index, 1);
this.numbers.splice(newIndex, 0, tmp[0]);
}
// 向下滑
down(index: number): void {
if (!this.isDraggable(index + this.row)) {
return;
}
this.offsetY -= this.FIX_VP_Y;
this.dragRefOffsetY += this.FIX_VP_Y;
// 多列
this.itemMove(index, index + this.row);
}
// 向下滑(右下角为空)
down2(index: number): void {
if (!this.isDraggable(index + 3)) {
return;
}
this.offsetY -= this.FIX_VP_Y;
this.dragRefOffsetY += this.FIX_VP_Y;
this.itemMove(index, index + 3);
}
// 向上滑
up(index: number): void {
if (!this.isDraggable(index - this.row)) {
return;
}
this.offsetY += this.FIX_VP_Y;
this.dragRefOffsetY -= this.FIX_VP_Y;
this.itemMove(index, index - this.row);
}
// 向左滑
left(index: number): void {
if (!this.isDraggable(index - 1)) {
return;
}
this.offsetX += this.FIX_VP_X;
this.dragRefOffsetX -= this.FIX_VP_X;
this.itemMove(index, index - 1);
}
// 向右滑
right(index: number): void {
if (!this.isDraggable(index + 1)) {
return;
}
this.offsetX -= this.FIX_VP_X;
this.dragRefOffsetX += this.FIX_VP_X;
this.itemMove(index, index + 1);
}
// 向右下滑
lowerRight(index: number): void {
if (!this.isDraggable(index + this.row + 1)) {
return;
}
this.offsetX -= this.FIX_VP_X;
this.dragRefOffsetX += this.FIX_VP_X;
this.offsetY -= this.FIX_VP_Y;
this.dragRefOffsetY += this.FIX_VP_Y;
this.itemMove(index, index + this.row + 1);
}
// 向右上滑
upperRight(index: number): void {
if (!this.isDraggable(index - (this.row - 1))) {
return;
}
this.offsetX -= this.FIX_VP_X;
this.dragRefOffsetX += this.FIX_VP_X;
this.offsetY += this.FIX_VP_Y;
this.dragRefOffsetY -= this.FIX_VP_Y;
this.itemMove(index, index - (this.row - 1));
}
// 向左下滑
lowerLeft(index: number): void {
if (!this.isDraggable(index + (this.row - 1))) {
return;
}
this.offsetX += this.FIX_VP_X;
this.dragRefOffsetX -= this.FIX_VP_X;
this.offsetY -= this.FIX_VP_Y;
this.dragRefOffsetY += this.FIX_VP_Y;
this.itemMove(index, index + (this.row - 1));
}
// 向左上滑
upperLeft(index: number): void {
if (!this.isDraggable(index - (this.row + 1))) {
return;
}
this.offsetX += this.FIX_VP_X;
this.dragRefOffsetX -= this.FIX_VP_X;
this.offsetY += this.FIX_VP_Y;
this.dragRefOffsetY -= this.FIX_VP_Y;
this.itemMove(index, index - (this.row + 1));
}
// 通过元素的索引,控制对应元素是否能移动排序
isDraggable(index: number): boolean {
console.info(index: ${index});
return index > -1; // 恒成立,所有元素均可移动排序
}
build() {
Column() {
Grid() {
ForEach(this.numbers, (item: number) => {
GridItem() {
Text(item + '')
.fontSize(16)
.width('100%')
.textAlign(TextAlign.Center)
.height(100)
.borderRadius(10)
.backgroundColor(0xFFFFFF)
.shadow(this.scaleItem == item ? {
radius: 70,
color: '#15000000',
offsetX: 0,
offsetY: 0
} :
{
radius: 0,
color: '#15000000',
offsetX: 0,
offsetY: 0
})
.animation({ curve: Curve.Sharp, duration: 300 });
}
// 添加震动
.onTouch(() => {
})
.onAreaChange((oldVal, newVal) => {
// 多列
this.FIX_VP_X = Math.round(newVal.width as number);
this.FIX_VP_Y = Math.round(newVal.height as number);
console.info(oldVal:${JSON.stringify(oldVal)});
})
// 指定固定GridItem不响应事件
.hitTestBehavior(this.isDraggable(this.numbers.indexOf(item)) ? HitTestMode.Default : HitTestMode.None)
.scale({ x: this.scaleItem == item ? 1.05 : 1, y: this.scaleItem == item ? 1.05 : 1 })
.zIndex(this.dragItem == item ? 1 : 0)
.translate(this.dragItem == item ? { x: this.offsetX, y: this.offsetY } : { x: 0, y: 0 })
.padding(10)
.gesture(
GestureGroup(GestureMode.Sequence,
LongPressGesture({ repeat: true, duration: 50 })
.onAction((event?: GestureEvent) => {
console.info(event: ${event});
this.getUIContext().animateTo({ curve: Curve.Friction, duration: 300 }, () => {
this.scaleItem = item;
});
})
.onActionEnd(() => {
this.getUIContext().animateTo({ curve: Curve.Friction, duration: 300 }, () => {
this.scaleItem = -1;
});
}),
PanGesture({ fingers: 1, direction: null, distance: 0 })
.onActionStart(() => {
this.dragItem = item;
this.dragRefOffsetX = 0;
this.dragRefOffsetY = 0;
})
.onActionUpdate((event: GestureEvent) => {
this.offsetY = event.offsetY - this.dragRefOffsetY;
this.offsetX = event.offsetX - this.dragRefOffsetX;
console.info(移动过程中event.offsetY:${event.offsetY},
this.dragRefOffsetY: ${this.dragRefOffsetY}, this.offsetY: ${this.offsetY});
console.info(移动过程中event.offsetX:${event.offsetX},
this.dragRefOffsetX: ${this.dragRefOffsetX}, this.offsetX: ${this.offsetX});
this.getUIContext().animateTo({ curve: curves.interpolatingSpring(0, 1, 400, 38) }, () => {
let index = this.numbers.indexOf(this.dragItem);
console.info('index= ', index);
if (this.offsetY >= this.FIX_VP_Y / 2 &&
(this.offsetX <= this.FIX_VP_X / 2 && this.offsetX >= -this.FIX_VP_X / 2)
&& (index + this.row <= this.lastIndex)) {
// 向下滑
this.down(index);
} else if (this.offsetY <= -this.FIX_VP_Y / 2 &&
(this.offsetX <= this.FIX_VP_X / 2 && this.offsetX >= -this.FIX_VP_X / 2)
&& index - this.row >= 0) {
// 向上滑
this.up(index);
} else if (this.offsetX >= this.FIX_VP_X / 2 &&
(this.offsetY <= this.FIX_VP_Y / 2 && this.offsetY >= -this.FIX_VP_Y / 2)
&& !(((index - (this.row - 1)) % this.row == 0) || index == this.lastIndex)) {
// 向右滑
this.right(index);
} else if (this.offsetX <= -this.FIX_VP_X / 2 &&
(this.offsetY <= this.FIX_VP_Y / 2 && this.offsetY >= -this.FIX_VP_Y / 2)
&& !(index % this.row == 0)) {
// 向左滑
this.left(index);
} else if (this.offsetX >= this.FIX_VP_X / 2 && this.offsetY >= this.FIX_VP_Y / 2
&& ((index + this.row + 1 <= this.lastIndex && !((index - (this.row - 1)) % this.row == 0)) ||
!((index - (this.row - 1)) % this.row == 0))) {
// 向右下滑
this.lowerRight(index);
} else if (this.offsetX >= this.FIX_VP_X / 2 && this.offsetY <= -this.FIX_VP_Y / 2
&& !((index - this.row < 0) || ((index - (this.row - 1)) % this.row == 0))) {
// 向右上滑
this.upperRight(index);
} else if (this.offsetX <= -this.FIX_VP_X / 2 && this.offsetY >= this.FIX_VP_Y / 2
&& (!(index % this.row == 0) && (index + (this.row - 1) <= this.lastIndex))) {
// 向左下滑
this.lowerLeft(index);
} else if (this.offsetX <= -this.FIX_VP_X / 2 && this.offsetY <= -this.FIX_VP_Y / 2
&& !((index <= this.row - 1) || (index % this.row == 0))) {
// 向左上滑
this.upperLeft(index);
} else if (this.offsetX >= this.FIX_VP_X / 2 && this.offsetY >= this.FIX_VP_Y / 2
&& (index == this.lastIndex)) {
// 向右下滑(右下角为空)
this.down2(index);
}
});
})
.onActionEnd(() => {
this.getUIContext().animateTo({ curve: curves.interpolatingSpring(0, 1, 400, 38) }, () => {
this.dragItem = -1;
});
this.getUIContext().animateTo({
curve: curves.interpolatingSpring(14, 1, 170, 17), delay: 150
}, () => {
this.scaleItem = -1;
});
})
)
.onCancel(() => {
this.getUIContext().animateTo({ curve: curves.interpolatingSpring(0, 1, 400, 38) }, () => {
this.dragItem = -1;
});
this.getUIContext().animateTo({
curve: curves.interpolatingSpring(14, 1, 170, 17)
}, () => {
this.scaleItem = -1;
});
})
);
}, (item: number) => item.toString());
}
.width('90%')
.editMode(true)
.scrollBar(BarState.Off)
// 多列
.columnsTemplate(this.str);
}
.expandSafeArea([SafeAreaType.SYSTEM], [SafeAreaEdge.TOP, SafeAreaEdge.BOTTOM])
.width('100%').height('100%').backgroundColor('#f1f3f5').padding({ top: 5 });
}
}
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
Tab组件内Navigation跳转,TabBar导航栏隐藏失败如何解决
问题现象
在“我的”页面点击“跳转设置页”按钮跳转到设置页面,底部的TabBar导航栏仍然存在。
点击放大点击放大
主页面有“首页”、“我的”两个Tab页面,部分代码如下:
@Entry
@Component
struct TabsNavPage {
build() {
Tabs() {
TabContent() {
MinePage()
}
.tabBar(BottomTabBarStyle.of($r('sys.media.ohos_app_icon'), '我的'))
TabContent() {
Text('首页')
}
.tabBar(BottomTabBarStyle.of($r('sys.media.ohos_app_icon'), '首页'))
}
.barPosition(BarPosition.End)
}
}
@Component
struct MinePage {
@Provide('pathStack') pathStack: NavPathStack = new NavPathStack();
@Builder
PagesMap(name: string) {
if (name === 'Settings') {
Settings();
}
}
build() {
Navigation(this.pathStack) {
Row(){
Column() {
Button('跳转设置页')
.onClick(() => {
this.pathStack.pushPathByName('Settings', null);
})
}
}
}
.hideTitleBar(true)
.navDestination(this.PagesMap)
}
}
@Component
struct Settings {
@Consume('pathStack') pathStack: NavPathStack = new NavPathStack();
build() {
NavDestination() {
Button('返回')
.onClick(() => {
this.pathStack.pop();
});
}.title('设置')
}
}
效果预览
点击放大
背景知识
Navigation组件是路由导航的根视图容器,一般作为Page页面的根容器使用,其内部默认包含了标题栏、内容区和工具栏,其中内容区默认首页显示导航内容(Navigation的子组件)或非首页显示(NavDestination的子组件),首页和非首页通过路由进行切换。
@Provide装饰器和@Consume装饰器:应用于与后代组件的双向数据同步、状态数据在多个层级之间传递的场景。
解决方案
问题现象中是用Tab组件包裹Navigation组件,跳转Navigation子组件并不会影响到外层的Tab组件。
将Navigation组件作为根容器包括Tab组件,为保证子组件都共享一个NavPathStack实例,可以使用@Provide和@Consume装饰器将导航控制器对象NavPathStack传递给Tabs内的子组件使用。
@Entry
@Component
struct TabsNavPage {
// 使用@Provide将路由栈对象传递给TabContent内的组件
@Provide('pathStack') pathStack: NavPathStack = new NavPathStack();
@Builder
PagesMap(name: string) {
if (name === 'Settings') {
Settings();
}
}
build() {
// 使用Navigation包裹Tabs,Tabs子页使用同一个路由栈对象
Navigation(this.pathStack) {
Tabs() {
TabContent() {
MinePage();
}
.tabBar(BottomTabBarStyle.of($r('sys.media.ohos_app_icon'), '我的'))
TabContent() {
Text('首页').fontColor('40fp');
}
.tabBar(BottomTabBarStyle.of($r('sys.media.ohos_app_icon'), '首页'))
}
.barPosition(BarPosition.End);
}
.hideTitleBar(true)
.navDestination(this.PagesMap)
}
}
@Component
struct MinePage {
// 获取Navigation的路由栈对象
@Consume('pathStack') pathStack: NavPathStack;
build() {
Column() {
Button('跳转设置页')
.onClick(() => {
this.pathStack.pushPathByName('Settings', null);
});
};
}
}
@Component
struct Settings {
@Consume('pathStack') pathStack: NavPathStack = new NavPathStack();
build() {
NavDestination() {
Button('返回')
.onClick(() => {
this.pathStack.pop();
});
}.title('设置')
}
}
https://coub.com/view/5kamzbr4mi
https://coub.com/view/9slmznc3ol
https://coub.com/view/dnhe4x7otv
https://coub.com/view/libk78392s
https://coub.com/view/2splvx7ais
https://coub.com/view/0fqk03fwlz
https://coub.com/view/u3but76ukt
https://coub.com/view/2xrg8kdx5o
https://coub.com/view/iqeb7cwj8k
https://coub.com/view/lx86s7cfpr
https://coub.com/view/qin8evf0sk
https://coub.com/view/ul1dmz1db2
https://coub.com/view/1n6f59dch3
https://coub.com/view/mz789w4pbg
https://coub.com/view/trb9jcog9p
https://coub.com/view/j3i3lac4yx
https://coub.com/view/84l11twj2s
https://coub.com/view/ut50q4erj3
https://coub.com/view/w6uuiu7ru3
https://coub.com/view/iun2hgfzqm
https://coub.com/view/hc6d3b3sts
https://coub.com/view/7l7wd758hs
https://coub.com/view/sshi8p319r
https://coub.com/view/2y21fnu5t4
https://coub.com/view/3dpvlq1hxa
https://coub.com/view/7tefrrju9a
https://coub.com/view/kr0zw5ow09
https://coub.com/view/vze4xn6lxk
https://coub.com/view/gp0hoyjbev
https://coub.com/view/8ukozyn2pf
https://coub.com/view/gne8uf8fun
https://coub.com/view/o1ljlnaqrl
https://coub.com/view/6pe00kuw3p
https://coub.com/view/3ys8ww2kf5
https://coub.com/view/ky54919n96
https://coub.com/view/ovvfpcdgiy
https://coub.com/view/h1g7qx1ibk
https://coub.com/view/4ytxeco099
https://coub.com/view/z8grd6tisg
https://coub.com/view/ihcl3n786e
https://coub.com/view/e48vma3hes
https://coub.com/view/rgk8us00m7
https://coub.com/view/mxmu9agwxl
https://coub.com/view/jjq7sf2887
https://coub.com/view/rtlotmcb2o
https://coub.com/view/2xzrkf1j13
https://coub.com/view/datluk4gwo
https://coub.com/view/5ld5j0uc5o
https://coub.com/view/cp081k64fp
https://coub.com/view/0sgmugss1z
https://coub.com/view/ynwmj064s7
https://coub.com/view/y8atenamwg
https://coub.com/view/acqfefno2e
https://coub.com/view/hmwvsetfho
https://coub.com/view/n59qhb108b
https://coub.com/view/946i56xqgw
https://coub.com/view/fu355xmxbd
https://coub.com/view/9gfbdnyv3z
https://coub.com/view/i16nn052at
https://coub.com/view/s2o2prg0rl
https://coub.com/view/f7m17v6t3x
https://coub.com/view/6bmqboq0w9
https://coub.com/view/tvj9e2712i
https://coub.com/view/b5kbn4qpgx
https://coub.com/view/hh77rcnz58
https://coub.com/view/8w0hoxvhxu
https://coub.com/view/ogtrlf718w
https://coub.com/view/dcy0w8eeq1
https://coub.com/view/foo2zaj4nz
https://coub.com/view/ffai2oxjgj
https://coub.com/view/h7llkwvo4t
https://coub.com/view/fxf6iote20
https://coub.com/view/zr4atw33g7
https://coub.com/view/w6ksjzfgri
https://coub.com/view/6wlivajplu
https://coub.com/view/m3p9mpc87g
https://coub.com/view/1svk68qjxi
https://coub.com/view/jn78pmgtsy
https://coub.com/view/ru4kxd02ca
https://coub.com/view/u2qm3zht04
https://coub.com/view/07vrswbr9o
https://coub.com/view/z53dkteu04
https://coub.com/view/r3kczagbat
https://coub.com/view/ebkii1w2uy
https://coub.com/view/gu2ovmapto
https://coub.com/view/6s1y6nkktx
https://coub.com/view/2mzsrq6bnh
https://coub.com/view/bmh3bh21dl
https://coub.com/view/fdiekfi3y1
https://coub.com/view/ex4dtaj1ri
https://coub.com/view/tsgmmorhu1
https://coub.com/view/gh01h6433n
https://coub.com/view/3zfrusqatl
https://coub.com/view/u27l9exjjr
https://coub.com/view/wx8rybzzdd
https://coub.com/view/u13mvzb6i9
https://coub.com/view/lw5mv3n3vp
https://coub.com/view/tn7jo1khbm
https://coub.com/view/hr9h8qk489
https://coub.com/view/dfx72ezl4k
https://coub.com/view/xfnivvh59q
https://coub.com/view/6dmijjf9eu
https://coub.com/view/fi22iqa3iv
https://coub.com/view/57fq1sjhpa
https://coub.com/view/9xii5apv7o
https://coub.com/view/e5fhxwb6re
https://coub.com/view/wuc96hdc6z
https://coub.com/view/bvp1embwd5
https://coub.com/view/u1lurshbkm
https://coub.com/view/nih2qy00t8
https://coub.com/view/mrx826u951
https://coub.com/view/kes50gj32a
https://coub.com/view/fcv3394eq2
https://coub.com/view/4qc0x8z64l
https://coub.com/view/n2k5itdn4w
https://coub.com/view/t988vol3yp
https://coub.com/view/synur4gqha
https://coub.com/view/bd6fqj7ej9
https://coub.com/view/crwhjs6qgk
https://coub.com/view/vde66uoo7x
https://coub.com/view/68md6egw0p
https://coub.com/view/2fgvpgk4ir
https://coub.com/view/4ygf27wtro
https://coub.com/view/0qdbv1ptnp
https://coub.com/view/b5ii094vls
https://coub.com/view/o8fq8rif3n
https://coub.com/view/3odrc01a85
https://coub.com/view/8k08cynikg
问题现象
在“我的”页面点击“跳转设置页”按钮跳转到设置页面,底部的TabBar导航栏仍然存在。
点击放大点击放大
主页面有“首页”、“我的”两个Tab页面,部分代码如下:
@Entry
@Component
struct TabsNavPage {
build() {
Tabs() {
TabContent() {
MinePage()
}
.tabBar(BottomTabBarStyle.of($r('sys.media.ohos_app_icon'), '我的'))
TabContent() {
Text('首页')
}
.tabBar(BottomTabBarStyle.of($r('sys.media.ohos_app_icon'), '首页'))
}
.barPosition(BarPosition.End)
}
}
@Component
struct MinePage {
@Provide('pathStack') pathStack: NavPathStack = new NavPathStack();
@Builder
PagesMap(name: string) {
if (name === 'Settings') {
Settings();
}
}
build() {
Navigation(this.pathStack) {
Row(){
Column() {
Button('跳转设置页')
.onClick(() => {
this.pathStack.pushPathByName('Settings', null);
})
}
}
}
.hideTitleBar(true)
.navDestination(this.PagesMap)
}
}
@Component
struct Settings {
@Consume('pathStack') pathStack: NavPathStack = new NavPathStack();
build() {
NavDestination() {
Button('返回')
.onClick(() => {
this.pathStack.pop();
});
}.title('设置')
}
}
效果预览
点击放大
背景知识
Navigation组件是路由导航的根视图容器,一般作为Page页面的根容器使用,其内部默认包含了标题栏、内容区和工具栏,其中内容区默认首页显示导航内容(Navigation的子组件)或非首页显示(NavDestination的子组件),首页和非首页通过路由进行切换。
@Provide装饰器和@Consume装饰器:应用于与后代组件的双向数据同步、状态数据在多个层级之间传递的场景。
解决方案
问题现象中是用Tab组件包裹Navigation组件,跳转Navigation子组件并不会影响到外层的Tab组件。
将Navigation组件作为根容器包括Tab组件,为保证子组件都共享一个NavPathStack实例,可以使用@Provide和@Consume装饰器将导航控制器对象NavPathStack传递给Tabs内的子组件使用。
@Entry
@Component
struct TabsNavPage {
// 使用@Provide将路由栈对象传递给TabContent内的组件
@Provide('pathStack') pathStack: NavPathStack = new NavPathStack();
@Builder
PagesMap(name: string) {
if (name === 'Settings') {
Settings();
}
}
build() {
// 使用Navigation包裹Tabs,Tabs子页使用同一个路由栈对象
Navigation(this.pathStack) {
Tabs() {
TabContent() {
MinePage();
}
.tabBar(BottomTabBarStyle.of($r('sys.media.ohos_app_icon'), '我的'))
TabContent() {
Text('首页').fontColor('40fp');
}
.tabBar(BottomTabBarStyle.of($r('sys.media.ohos_app_icon'), '首页'))
}
.barPosition(BarPosition.End);
}
.hideTitleBar(true)
.navDestination(this.PagesMap)
}
}
@Component
struct MinePage {
// 获取Navigation的路由栈对象
@Consume('pathStack') pathStack: NavPathStack;
build() {
Column() {
Button('跳转设置页')
.onClick(() => {
this.pathStack.pushPathByName('Settings', null);
});
};
}
}
@Component
struct Settings {
@Consume('pathStack') pathStack: NavPathStack = new NavPathStack();
build() {
NavDestination() {
Button('返回')
.onClick(() => {
this.pathStack.pop();
});
}.title('设置')
}
}
https://coub.com/view/5kamzbr4mi
https://coub.com/view/9slmznc3ol
https://coub.com/view/dnhe4x7otv
https://coub.com/view/libk78392s
https://coub.com/view/2splvx7ais
https://coub.com/view/0fqk03fwlz
https://coub.com/view/u3but76ukt
https://coub.com/view/2xrg8kdx5o
https://coub.com/view/iqeb7cwj8k
https://coub.com/view/lx86s7cfpr
https://coub.com/view/qin8evf0sk
https://coub.com/view/ul1dmz1db2
https://coub.com/view/1n6f59dch3
https://coub.com/view/mz789w4pbg
https://coub.com/view/trb9jcog9p
https://coub.com/view/j3i3lac4yx
https://coub.com/view/84l11twj2s
https://coub.com/view/ut50q4erj3
https://coub.com/view/w6uuiu7ru3
https://coub.com/view/iun2hgfzqm
https://coub.com/view/hc6d3b3sts
https://coub.com/view/7l7wd758hs
https://coub.com/view/sshi8p319r
https://coub.com/view/2y21fnu5t4
https://coub.com/view/3dpvlq1hxa
https://coub.com/view/7tefrrju9a
https://coub.com/view/kr0zw5ow09
https://coub.com/view/vze4xn6lxk
https://coub.com/view/gp0hoyjbev
https://coub.com/view/8ukozyn2pf
https://coub.com/view/gne8uf8fun
https://coub.com/view/o1ljlnaqrl
https://coub.com/view/6pe00kuw3p
https://coub.com/view/3ys8ww2kf5
https://coub.com/view/ky54919n96
https://coub.com/view/ovvfpcdgiy
https://coub.com/view/h1g7qx1ibk
https://coub.com/view/4ytxeco099
https://coub.com/view/z8grd6tisg
https://coub.com/view/ihcl3n786e
https://coub.com/view/e48vma3hes
https://coub.com/view/rgk8us00m7
https://coub.com/view/mxmu9agwxl
https://coub.com/view/jjq7sf2887
https://coub.com/view/rtlotmcb2o
https://coub.com/view/2xzrkf1j13
https://coub.com/view/datluk4gwo
https://coub.com/view/5ld5j0uc5o
https://coub.com/view/cp081k64fp
https://coub.com/view/0sgmugss1z
https://coub.com/view/ynwmj064s7
https://coub.com/view/y8atenamwg
https://coub.com/view/acqfefno2e
https://coub.com/view/hmwvsetfho
https://coub.com/view/n59qhb108b
https://coub.com/view/946i56xqgw
https://coub.com/view/fu355xmxbd
https://coub.com/view/9gfbdnyv3z
https://coub.com/view/i16nn052at
https://coub.com/view/s2o2prg0rl
https://coub.com/view/f7m17v6t3x
https://coub.com/view/6bmqboq0w9
https://coub.com/view/tvj9e2712i
https://coub.com/view/b5kbn4qpgx
https://coub.com/view/hh77rcnz58
https://coub.com/view/8w0hoxvhxu
https://coub.com/view/ogtrlf718w
https://coub.com/view/dcy0w8eeq1
https://coub.com/view/foo2zaj4nz
https://coub.com/view/ffai2oxjgj
https://coub.com/view/h7llkwvo4t
https://coub.com/view/fxf6iote20
https://coub.com/view/zr4atw33g7
https://coub.com/view/w6ksjzfgri
https://coub.com/view/6wlivajplu
https://coub.com/view/m3p9mpc87g
https://coub.com/view/1svk68qjxi
https://coub.com/view/jn78pmgtsy
https://coub.com/view/ru4kxd02ca
https://coub.com/view/u2qm3zht04
https://coub.com/view/07vrswbr9o
https://coub.com/view/z53dkteu04
https://coub.com/view/r3kczagbat
https://coub.com/view/ebkii1w2uy
https://coub.com/view/gu2ovmapto
https://coub.com/view/6s1y6nkktx
https://coub.com/view/2mzsrq6bnh
https://coub.com/view/bmh3bh21dl
https://coub.com/view/fdiekfi3y1
https://coub.com/view/ex4dtaj1ri
https://coub.com/view/tsgmmorhu1
https://coub.com/view/gh01h6433n
https://coub.com/view/3zfrusqatl
https://coub.com/view/u27l9exjjr
https://coub.com/view/wx8rybzzdd
https://coub.com/view/u13mvzb6i9
https://coub.com/view/lw5mv3n3vp
https://coub.com/view/tn7jo1khbm
https://coub.com/view/hr9h8qk489
https://coub.com/view/dfx72ezl4k
https://coub.com/view/xfnivvh59q
https://coub.com/view/6dmijjf9eu
https://coub.com/view/fi22iqa3iv
https://coub.com/view/57fq1sjhpa
https://coub.com/view/9xii5apv7o
https://coub.com/view/e5fhxwb6re
https://coub.com/view/wuc96hdc6z
https://coub.com/view/bvp1embwd5
https://coub.com/view/u1lurshbkm
https://coub.com/view/nih2qy00t8
https://coub.com/view/mrx826u951
https://coub.com/view/kes50gj32a
https://coub.com/view/fcv3394eq2
https://coub.com/view/4qc0x8z64l
https://coub.com/view/n2k5itdn4w
https://coub.com/view/t988vol3yp
https://coub.com/view/synur4gqha
https://coub.com/view/bd6fqj7ej9
https://coub.com/view/crwhjs6qgk
https://coub.com/view/vde66uoo7x
https://coub.com/view/68md6egw0p
https://coub.com/view/2fgvpgk4ir
https://coub.com/view/4ygf27wtro
https://coub.com/view/0qdbv1ptnp
https://coub.com/view/b5ii094vls
https://coub.com/view/o8fq8rif3n
https://coub.com/view/3odrc01a85
https://coub.com/view/8k08cynikg
Android离线打包,plus.runtime.install导致应用闪退
logcat日志如下:
Unresolved exception class when finding catch block: net.lingala.zip4j.exception.ZipException
2026-08-28 15:40:18.591 29792-30261 AndroidRuntime com.gdxinyue.newusmp
E FATAL EXCEPTION: Thread-10
Process: com.gdxinyue.newusmp, PID: 29792
java.lang.NullPointerException: Attempt to read from null array
at io.dcloud.feature.pdr.RuntimeFeatureImpl$a.run
参考博客:https://blog.csdn.net/qq_32058147/article/details/155034674
在Android Stadio主项目的 build.gradle > dependencies中增加以下依赖:
dependencies {
implementation 'net.lingala.zip4j:zip4j:2.11.5'
}
logcat日志如下:
Unresolved exception class when finding catch block: net.lingala.zip4j.exception.ZipException
2026-08-28 15:40:18.591 29792-30261 AndroidRuntime com.gdxinyue.newusmp
E FATAL EXCEPTION: Thread-10
Process: com.gdxinyue.newusmp, PID: 29792
java.lang.NullPointerException: Attempt to read from null array
at io.dcloud.feature.pdr.RuntimeFeatureImpl$a.run
参考博客:https://blog.csdn.net/qq_32058147/article/details/155034674
在Android Stadio主项目的 build.gradle > dependencies中增加以下依赖:
dependencies {
implementation 'net.lingala.zip4j:zip4j:2.11.5'
}
uni-starter模板代码更正
创建uni-app项目时选择uni-starter模板,我发现其中pages/list/search/search.vue中的“搜索联想”部分代码<uni-list-item>的:title="item.name"可能写错了,应该改为:title="item.title",具体代码如下:
<!-- 搜索联想 -->
<view class="search-associative" v-if="associativeShow">
<uni-list>
<!-- :title="item.name"可能写错了,文章表没有name字段,associativeList只是获取_id、title字段! -->
<!-- <uni-list-item v-for="(item,index) in associativeList" :key="item._id" :ellipsis="1" :title="item.name" @click="associativeClick(item)" show-extra-icon
clickable :extra-icon="{size:18,color:iconColor,type:'search'}" >
</uni-list-item> -->
<!-- 正确改为:title="item.title",在搜索框输入文章记录的标题子串时,会显示篇文章的标题 -->
<uni-list-item v-for="(item,index) in associativeList" :key="item._id" :ellipsis="1" :title="item.title" @click="associativeClick(item)" show-extra-icon
clickable :extra-icon="{size:18,color:iconColor,type:'search'}" >
</uni-list-item>
</uni-list>
</view>
这个搜索联想的功能应该是在搜索框输入文章记录标题的子串时,提示有没有这篇文章,若存在,则显示所有标题带有此子串的文章标题列表,这样就可以直接点击找到此文章,因为模板的代码写错了"item.name",导致完全没有提示,希望可以改正过来。
创建uni-app项目时选择uni-starter模板,我发现其中pages/list/search/search.vue中的“搜索联想”部分代码<uni-list-item>的:title="item.name"可能写错了,应该改为:title="item.title",具体代码如下:
<!-- 搜索联想 -->
<view class="search-associative" v-if="associativeShow">
<uni-list>
<!-- :title="item.name"可能写错了,文章表没有name字段,associativeList只是获取_id、title字段! -->
<!-- <uni-list-item v-for="(item,index) in associativeList" :key="item._id" :ellipsis="1" :title="item.name" @click="associativeClick(item)" show-extra-icon
clickable :extra-icon="{size:18,color:iconColor,type:'search'}" >
</uni-list-item> -->
<!-- 正确改为:title="item.title",在搜索框输入文章记录的标题子串时,会显示篇文章的标题 -->
<uni-list-item v-for="(item,index) in associativeList" :key="item._id" :ellipsis="1" :title="item.title" @click="associativeClick(item)" show-extra-icon
clickable :extra-icon="{size:18,color:iconColor,type:'search'}" >
</uni-list-item>
</uni-list>
</view>
这个搜索联想的功能应该是在搜索框输入文章记录标题的子串时,提示有没有这篇文章,若存在,则显示所有标题带有此子串的文章标题列表,这样就可以直接点击找到此文章,因为模板的代码写错了"item.name",导致完全没有提示,希望可以改正过来。
收起阅读 »组件实现随手拖动松手后贴边效果
问题现象
如何实现手指长按组件时,组件能够跟随手指拖动,手指抬起后,组件贴边停靠的效果。
背景知识
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
无Mac获取P12证书、描述文件、上传IPA
1、首先进入我开发的网站:登录 - iOS 开发者工具,http://jqzm.w1.luyouxia.net/iOSTools,该工具集成了Bundle Id、证书、描述文件、添加设备、上传IPA
2、然后使用邮箱注册并登陆
3、然后进入账号设置,将参数填写上并保存配置,保存后点一下同步数据
4、进入上传IPA功能,选择IPA文件上传即可
1、首先进入我开发的网站:登录 - iOS 开发者工具,http://jqzm.w1.luyouxia.net/iOSTools,该工具集成了Bundle Id、证书、描述文件、添加设备、上传IPA
2、然后使用邮箱注册并登陆
3、然后进入账号设置,将参数填写上并保存配置,保存后点一下同步数据
4、进入上传IPA功能,选择IPA文件上传即可
收起阅读 »在线教育知识付费直播源码免费下载|UniApp 多端可用
仓库地址 https://gitee.com/tuzhi-open/tuzhi-mobile-open
凸知(Tuzhi) 是一套完整的知识付费解决方案,支持私有化部署,覆盖课程售卖、直播授课、圈子社区、考试测评、分销营销、实物商品等完整业务场景,提供管理后台、移动端(H5 / 微信小程序)、PC 学员端等多端入口,帮助个人 IP、教培机构与品牌商家搭建自有卖课平台。
凸知移动端 是一款基于 uni-app(Vue 2) TDesign 开发的知识付费移动端前端,支持 H5 / 微信小程序 多端编译(不支持 App 打包)。覆盖课程点播、直播、专栏、圈子社区、订单支付(前端交互)、优惠券、余额红包、考试答题等完整学习场景。
系统演示:
| 端 | 演示地址 | 账号 | 密码 |
|---|---|---|---|
| 后台 | https://tuzhi.mutouweb.com/admin.php | admin | 123456 |
| 移动端(用户) | https://tuzhi.mutouweb.com | 13222222222 | 123456 |
| PC 端(用户) | https://tuzhi.mutouweb.com/pc/#/ | 13222222222 | 123456 |
一、功能特性
- 终端覆盖:电脑端 / 微信公众号(H5)/ 微信小程序多端学习
- 课程内容:图文 / 视频 / 音频 / 专栏售卖,支持试看试听、课件资料与内容保护
- 互动教学:练习 / 考试测评 / 题库 / 试卷库 / 证书 / 表单 / 圈子社区
- 直播能力:直播授课 / 伪直播 / 回放 / 直播带货 / 送礼物 / 发红包
- 营销增长:分销推广 / 优惠券 / 每日签到 / 积分商城 / 付费会员卡 / 组合商品 / 卡密兑换
- 实物商品:商品管理 / 多规格 SKU / 限购设置 / 售后退换
- 交易支付:购物车 / 订单管理 / 余额支付 / 微信支付 / 支付宝支付
- 用户数据:用户管理 / 订阅记录 / 学习分析 / 交易分析 / 订单管理
- 系统安全:页面 DIY 装修 / 云存储 / 短信与消息推送 / 管理员权限 / 版权保护(跑马灯 / 防录屏)
界面预览:
二、系统架构
凸知是一套完整的知识付费系统,由四个部分组成:
| 部分 | 技术栈 | 说明 | 源码开放 |
|---|---|---|---|
| 后端服务 | ThinkPHP 5.0 FastAdmin MySQL | 接口服务,承载支付、直播、分销等全部业务逻辑 | 购买授权 |
| 移动端(本仓库) | uni-app(Vue 2) TDesign | H5 / 微信小程序学员端(不支持 App 打包) | 开源 |
| 管理后台 | Vue 3 Arco Design | 管理员 / 讲师运营后台 | 购买授权 |
| PC 学员端 | Vue 3 Arco Design | 电脑端学员学习页面 | 购买授权 |
凸知终端技术结构:
整体调用链路:移动端 / PC 学员端 / 管理后台 → 后端接口服务({code, msg, data} 协议)→ MySQL 数据库。
本仓库仅开源移动端前端源码;如需后端服务或管理后台等其余端的源码,需购买商业授权获取,详见「十一、使用许可」与授权价格。
三、技术栈
| 分类 | 技术 | 说明 |
|---|---|---|
| 跨端框架 | uni-app(Vue 2.x) | vue-cli 工程,一套代码编译 H5 / 微信小程序等多端(不支持 App 打包) |
| 开发语言 | JavaScript(Vue 2 语法) Sass | 页面按 xxx.vue js.vue css.css 三文件拆分,样式用 CSS 规则 |
| UI 组件库 | TDesign(@tdesign/uniapp) |
全局主题样式,小程序端内置构建适配补丁(scripts/) |
uView(本地 components/uview-ui) |
列表 / 弹窗 / 表单等通用组件 | |
自研 components/tz/ |
音频 / 视频 / 媒体编辑 / 上传 / 文件预览等 45 业务组件 | |
| qiun-data-charts | 图表可视化 | |
| 状态管理 | Vuex 3 | 全局登录态 / 用户信息等 |
| 国际化 | vue-i18n 8 | 内置 zh-CN / US 多语言 |
| 网络请求 | flyio 自封装 $api |
$api('module.action') 点分路由,统一鉴权 / 错误处理 / 401 跳转 |
| 音视频播放 | 阿里云播放器 Aliplayer(H5,本地化资源)、hls.js / flv.js | 直播与点播,支持倍速 / 弹幕 / 水印等组件 |
| 微信能力 | jweixin-module | 微信登录 / 分享 JS-SDK 封装 |
| 构建工具 | @vue/cli 5 webpack | 由 @dcloudio/vue-cli-plugin-uni 驱动多端编译 |
| 测试 | Jest | 多端单元测试(npm run test:*) |
四、快速开始
环境要求:Node.js >= 14(建议 16/18)、npm
# ① 安装依赖
npm install
# ② H5 开发(默认不启用 Mock,对接真实后端;如需 Mock 见「Mock 数据」)
npm run dev:h5
浏览器访问 http://localhost:8080 即可看到完整页面效果。
微信小程序编译:
# ③ 小程序开发模式(监听编译,产物在 dist/dev/mp-weixin)
npm run dev:mp-weixin
# ④ 小程序生产构建(内置 TDesign 适配补丁,产物在 dist/build/mp-weixin)
npm run build:mp-weixin
用微信开发者工具「导入项目」选择对应产物目录(开发选 dist/dev/mp-weixin,生产选 dist/build/mp-weixin),并在 src/manifest.json 中替换为自己的 appid。
对接真实后端:
- 修改
src/siteinfo.js中的siteroot为你的后端地址; - 在
src/common/request/api_list.js中核对接口地址与{code, msg, data}协议; - 微信小程序构建需在
src/manifest.json中替换为自己的 appid。
五、构建
| 目标 | 命令 |
|---|---|
| H5 | npm run build:h5 |
| 微信小程序 | npm run build:mp-weixin |
小程序构建内置 TDesign 适配补丁(
scripts/),构建成功后产物在dist/build/下。⚠️ 注意:当前版本不支持 App 打包,仅支持 H5 与微信小程序两个平台。
六、Mock 数据
- H5 开发模式(
NODE_ENV=development)默认关闭,全部走真实后端接口; - 开启方式:将
src/static/config.js的enableMock改为true(显式设置优先级最高),或通过uni.setStorageSync('enableMock', true)/ 环境变量MOCK_ENABLE切换; - 覆盖 200 接口,fixture 位于
src/mock/data/,按module/action路由镜像; - 未覆盖的接口自动透传真实网络并输出提示;
- Mock 数据由仓库内 mock-crawler 工具采集真实后端响应生成,发布前已做敏感字段脱敏(token/openid/手机号等)。
七、目录结构
src/
├── pages/ 页面(index 主包 public/app/user/order/course 五个分包,约 100 页面)
│ ├── index/ 首页(主包唯一页面,tabBar 入口)
│ ├── launch/ 启动页(加载与参数透传跳转,未在 pages.json 注册)
│ ├── public/ 公共分包:登录 / 注册 / 找回密码 / 协议 / 搜索 / 消息 / 结果页等
│ ├── app/ 业务功能分包:活动 / 圈子 / 考试 / 优惠券 / 积分 / 签到 / 证书 / 分销 / 直播带货等
│ ├── user/ 用户分包:个人中心 / 资产 / 余额 / 地址 / 收藏 / 学习记录 / 订阅 / 红包等
│ ├── order/ 订单分包:订单列表 / 详情 / 提交 / 评价 / 售后 / 物流等
│ ├── course/ 课程分包:课程详情 / 分类 / 专栏 / 小组 / 直播等
│ └── template/ 页面模板(新建页面的三文件拆分参考,未在 pages.json 注册)
├── components/ 组件(tz/ 自研组件库、uview-ui、qiun-data-charts、视频播放器等)
├── common/ 公共层
│ ├── request/ 接口封装($api 点分路由、统一鉴权/错误处理)
│ ├── utils/ 工具(路由映射、支付、直播、OSS 签名等)
│ ├── order/ 订单状态处理
│ └── wechat/ 微信登录/分享 SDK 封装
├── mock/ Mock 数据(H5 开发模式默认关闭,开启方式见「六、Mock 数据」)
├── static/ 静态资源
├── pages.json 页面路由与分包配置
└── manifest.json 应用配置(appid 等)
八、开发约定
- 页面采用
xxx.vue js.vue css.css三文件拆分(<script src="./js.vue">@import "./css.css"),逻辑与样式分离; - 接口调用统一走
this.$api('module.action', data),登录态、401 跳转、错误提示由拦截器统一处理; - 业务组件优先复用
components/tz/自研组件库; - 提交信息遵循 Conventional Commits(
feat:/fix:/chore:)。
九、相关链接
| 名称 | 链接 |
|---|---|
| 使用文档 | https://wood-soft.feishu.cn/wiki/KKLkwOPBHiButJkTluTcoXKenjh |
| 功能列表 | https://wood-soft.feishu.cn/wiki/YGFYw0k8PiJDWxketgHcBNKNnxe |
| 授权价格 | https://www.tuzhi.ltd/index/pricing/version.html |
| 功能演示 | https://www.tuzhi.ltd/index/demo/index.html |
十、软件安全
安全问题请通过 Issue(需注明"安全")或邮件私下报告给项目维护团队,请勿在公开渠道描述可利用的漏洞细节。我们将在确认后尽快修复并同步发布。
十一、使用许可
- 本软件遵循 凸知开源协议 V1.0(非商业用途免费,商业用途需购买商业授权),详见 LICENSE;
- 第三方组件许可声明见 THIRD-PARTY-NOTICES.md(发布前请核对);
仓库地址 https://gitee.com/tuzhi-open/tuzhi-mobile-open
凸知(Tuzhi) 是一套完整的知识付费解决方案,支持私有化部署,覆盖课程售卖、直播授课、圈子社区、考试测评、分销营销、实物商品等完整业务场景,提供管理后台、移动端(H5 / 微信小程序)、PC 学员端等多端入口,帮助个人 IP、教培机构与品牌商家搭建自有卖课平台。
凸知移动端 是一款基于 uni-app(Vue 2) TDesign 开发的知识付费移动端前端,支持 H5 / 微信小程序 多端编译(不支持 App 打包)。覆盖课程点播、直播、专栏、圈子社区、订单支付(前端交互)、优惠券、余额红包、考试答题等完整学习场景。
系统演示:
| 端 | 演示地址 | 账号 | 密码 |
|---|---|---|---|
| 后台 | https://tuzhi.mutouweb.com/admin.php | admin | 123456 |
| 移动端(用户) | https://tuzhi.mutouweb.com | 13222222222 | 123456 |
| PC 端(用户) | https://tuzhi.mutouweb.com/pc/#/ | 13222222222 | 123456 |
一、功能特性
- 终端覆盖:电脑端 / 微信公众号(H5)/ 微信小程序多端学习
- 课程内容:图文 / 视频 / 音频 / 专栏售卖,支持试看试听、课件资料与内容保护
- 互动教学:练习 / 考试测评 / 题库 / 试卷库 / 证书 / 表单 / 圈子社区
- 直播能力:直播授课 / 伪直播 / 回放 / 直播带货 / 送礼物 / 发红包
- 营销增长:分销推广 / 优惠券 / 每日签到 / 积分商城 / 付费会员卡 / 组合商品 / 卡密兑换
- 实物商品:商品管理 / 多规格 SKU / 限购设置 / 售后退换
- 交易支付:购物车 / 订单管理 / 余额支付 / 微信支付 / 支付宝支付
- 用户数据:用户管理 / 订阅记录 / 学习分析 / 交易分析 / 订单管理
- 系统安全:页面 DIY 装修 / 云存储 / 短信与消息推送 / 管理员权限 / 版权保护(跑马灯 / 防录屏)
界面预览:
二、系统架构
凸知是一套完整的知识付费系统,由四个部分组成:
| 部分 | 技术栈 | 说明 | 源码开放 |
|---|---|---|---|
| 后端服务 | ThinkPHP 5.0 FastAdmin MySQL | 接口服务,承载支付、直播、分销等全部业务逻辑 | 购买授权 |
| 移动端(本仓库) | uni-app(Vue 2) TDesign | H5 / 微信小程序学员端(不支持 App 打包) | 开源 |
| 管理后台 | Vue 3 Arco Design | 管理员 / 讲师运营后台 | 购买授权 |
| PC 学员端 | Vue 3 Arco Design | 电脑端学员学习页面 | 购买授权 |
凸知终端技术结构:
整体调用链路:移动端 / PC 学员端 / 管理后台 → 后端接口服务({code, msg, data} 协议)→ MySQL 数据库。
本仓库仅开源移动端前端源码;如需后端服务或管理后台等其余端的源码,需购买商业授权获取,详见「十一、使用许可」与授权价格。
三、技术栈
| 分类 | 技术 | 说明 |
|---|---|---|
| 跨端框架 | uni-app(Vue 2.x) | vue-cli 工程,一套代码编译 H5 / 微信小程序等多端(不支持 App 打包) |
| 开发语言 | JavaScript(Vue 2 语法) Sass | 页面按 xxx.vue js.vue css.css 三文件拆分,样式用 CSS 规则 |
| UI 组件库 | TDesign(@tdesign/uniapp) |
全局主题样式,小程序端内置构建适配补丁(scripts/) |
uView(本地 components/uview-ui) |
列表 / 弹窗 / 表单等通用组件 | |
自研 components/tz/ |
音频 / 视频 / 媒体编辑 / 上传 / 文件预览等 45 业务组件 | |
| qiun-data-charts | 图表可视化 | |
| 状态管理 | Vuex 3 | 全局登录态 / 用户信息等 |
| 国际化 | vue-i18n 8 | 内置 zh-CN / US 多语言 |
| 网络请求 | flyio 自封装 $api |
$api('module.action') 点分路由,统一鉴权 / 错误处理 / 401 跳转 |
| 音视频播放 | 阿里云播放器 Aliplayer(H5,本地化资源)、hls.js / flv.js | 直播与点播,支持倍速 / 弹幕 / 水印等组件 |
| 微信能力 | jweixin-module | 微信登录 / 分享 JS-SDK 封装 |
| 构建工具 | @vue/cli 5 webpack | 由 @dcloudio/vue-cli-plugin-uni 驱动多端编译 |
| 测试 | Jest | 多端单元测试(npm run test:*) |
四、快速开始
环境要求:Node.js >= 14(建议 16/18)、npm
# ① 安装依赖
npm install
# ② H5 开发(默认不启用 Mock,对接真实后端;如需 Mock 见「Mock 数据」)
npm run dev:h5
浏览器访问 http://localhost:8080 即可看到完整页面效果。
微信小程序编译:
# ③ 小程序开发模式(监听编译,产物在 dist/dev/mp-weixin)
npm run dev:mp-weixin
# ④ 小程序生产构建(内置 TDesign 适配补丁,产物在 dist/build/mp-weixin)
npm run build:mp-weixin
用微信开发者工具「导入项目」选择对应产物目录(开发选 dist/dev/mp-weixin,生产选 dist/build/mp-weixin),并在 src/manifest.json 中替换为自己的 appid。
对接真实后端:
- 修改
src/siteinfo.js中的siteroot为你的后端地址; - 在
src/common/request/api_list.js中核对接口地址与{code, msg, data}协议; - 微信小程序构建需在
src/manifest.json中替换为自己的 appid。
五、构建
| 目标 | 命令 |
|---|---|
| H5 | npm run build:h5 |
| 微信小程序 | npm run build:mp-weixin |
小程序构建内置 TDesign 适配补丁(
scripts/),构建成功后产物在dist/build/下。⚠️ 注意:当前版本不支持 App 打包,仅支持 H5 与微信小程序两个平台。
六、Mock 数据
- H5 开发模式(
NODE_ENV=development)默认关闭,全部走真实后端接口; - 开启方式:将
src/static/config.js的enableMock改为true(显式设置优先级最高),或通过uni.setStorageSync('enableMock', true)/ 环境变量MOCK_ENABLE切换; - 覆盖 200 接口,fixture 位于
src/mock/data/,按module/action路由镜像; - 未覆盖的接口自动透传真实网络并输出提示;
- Mock 数据由仓库内 mock-crawler 工具采集真实后端响应生成,发布前已做敏感字段脱敏(token/openid/手机号等)。
七、目录结构
src/
├── pages/ 页面(index 主包 public/app/user/order/course 五个分包,约 100 页面)
│ ├── index/ 首页(主包唯一页面,tabBar 入口)
│ ├── launch/ 启动页(加载与参数透传跳转,未在 pages.json 注册)
│ ├── public/ 公共分包:登录 / 注册 / 找回密码 / 协议 / 搜索 / 消息 / 结果页等
│ ├── app/ 业务功能分包:活动 / 圈子 / 考试 / 优惠券 / 积分 / 签到 / 证书 / 分销 / 直播带货等
│ ├── user/ 用户分包:个人中心 / 资产 / 余额 / 地址 / 收藏 / 学习记录 / 订阅 / 红包等
│ ├── order/ 订单分包:订单列表 / 详情 / 提交 / 评价 / 售后 / 物流等
│ ├── course/ 课程分包:课程详情 / 分类 / 专栏 / 小组 / 直播等
│ └── template/ 页面模板(新建页面的三文件拆分参考,未在 pages.json 注册)
├── components/ 组件(tz/ 自研组件库、uview-ui、qiun-data-charts、视频播放器等)
├── common/ 公共层
│ ├── request/ 接口封装($api 点分路由、统一鉴权/错误处理)
│ ├── utils/ 工具(路由映射、支付、直播、OSS 签名等)
│ ├── order/ 订单状态处理
│ └── wechat/ 微信登录/分享 SDK 封装
├── mock/ Mock 数据(H5 开发模式默认关闭,开启方式见「六、Mock 数据」)
├── static/ 静态资源
├── pages.json 页面路由与分包配置
└── manifest.json 应用配置(appid 等)
八、开发约定
- 页面采用
xxx.vue js.vue css.css三文件拆分(<script src="./js.vue">@import "./css.css"),逻辑与样式分离; - 接口调用统一走
this.$api('module.action', data),登录态、401 跳转、错误提示由拦截器统一处理; - 业务组件优先复用
components/tz/自研组件库; - 提交信息遵循 Conventional Commits(
feat:/fix:/chore:)。
九、相关链接
| 名称 | 链接 |
|---|---|
| 使用文档 | https://wood-soft.feishu.cn/wiki/KKLkwOPBHiButJkTluTcoXKenjh |
| 功能列表 | https://wood-soft.feishu.cn/wiki/YGFYw0k8PiJDWxketgHcBNKNnxe |
| 授权价格 | https://www.tuzhi.ltd/index/pricing/version.html |
| 功能演示 | https://www.tuzhi.ltd/index/demo/index.html |
十、软件安全
安全问题请通过 Issue(需注明"安全")或邮件私下报告给项目维护团队,请勿在公开渠道描述可利用的漏洞细节。我们将在确认后尽快修复并同步发布。
十一、使用许可
- 本软件遵循 凸知开源协议 V1.0(非商业用途免费,商业用途需购买商业授权),详见 LICENSE;
- 第三方组件许可声明见 THIRD-PARTY-NOTICES.md(发布前请核对);







