HBuilderX

HBuilderX

极客开发工具
uni-app

uni-app

开发一次,多端覆盖
uniCloud

uniCloud

云开发平台
HTML5+

HTML5+

增强HTML5的功能体验
MUI

MUI

上万Star的前端框架

如何让Canvas绘制内容能超出父组件的限制

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(父组件)宽高设置为150
150
.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(父组件)宽高设置为150
150
.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

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 的三个特性:

  1. 返回一个新函数,不立即执行
  2. 支持参数预置(柯里化)
  3. 硬绑定:绑定后无法再被 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() 做了四件事:

  1. 创建一个新的空对象
  2. 将该对象的 [[Prototype]] 指向 Foo.prototype
  3. 以该对象为 this 执行 Foo
  4. 如果 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

箭头函数也没有 argumentssupernew.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 赢了

这正是 bind polyfill 里必须判断 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

(浏览器非严格模式下顶层 thiswindow,故为 'global';ESM/严格模式下为 undefined 或报错)


九、终极判断流程图

函数是箭头函数?  
├─ 是 → this = 定义时外层作用域的 this(向上查找,不可改变)  
└─ 否 ↓  
   用 new 调用? ──→ 是 → this = 新创建的对象  
   ↓ 否  
   用 call/apply/bind 调用? ──→ 是 → this = 指定的对象  
   ↓ 否                              (非严格下 null/undefined 会替换为 globalThis)  
   通过 obj.fn() 调用? ──→ 是 → this = obj(只看最后一个点)  
   ↓ 否  
   严格模式? ──→ 是 → this = undefined  
              └→ 否 → this = globalThis

十、最佳实践

  1. 优先用箭头函数写回调,避免 this 丢失
  2. 对象方法用简写语法 foo() {},不要用箭头函数(除非确实想要外层 this)
  3. class 中默认用原型方法,需要传递为回调时才用箭头字段或构造器 bind
  4. 全项目开启严格模式 / 使用 ESM,让 this 错误尽早暴露为 undefined 报错
  5. TypeScript 用户:开启 noImplicitThis,并善用显式 this 参数:
    function handler(this: HTMLButtonElement, e: MouseEvent) { /* ... */ }  
  6. 不确定时,打印 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 的三个特性:

  1. 返回一个新函数,不立即执行
  2. 支持参数预置(柯里化)
  3. 硬绑定:绑定后无法再被 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() 做了四件事:

  1. 创建一个新的空对象
  2. 将该对象的 [[Prototype]] 指向 Foo.prototype
  3. 以该对象为 this 执行 Foo
  4. 如果 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

箭头函数也没有 argumentssupernew.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 赢了

这正是 bind polyfill 里必须判断 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

(浏览器非严格模式下顶层 thiswindow,故为 'global';ESM/严格模式下为 undefined 或报错)


九、终极判断流程图

函数是箭头函数?  
├─ 是 → this = 定义时外层作用域的 this(向上查找,不可改变)  
└─ 否 ↓  
   用 new 调用? ──→ 是 → this = 新创建的对象  
   ↓ 否  
   用 call/apply/bind 调用? ──→ 是 → this = 指定的对象  
   ↓ 否                              (非严格下 null/undefined 会替换为 globalThis)  
   通过 obj.fn() 调用? ──→ 是 → this = obj(只看最后一个点)  
   ↓ 否  
   严格模式? ──→ 是 → this = undefined  
              └→ 否 → this = globalThis

十、最佳实践

  1. 优先用箭头函数写回调,避免 this 丢失
  2. 对象方法用简写语法 foo() {},不要用箭头函数(除非确实想要外层 this)
  3. class 中默认用原型方法,需要传递为回调时才用箭头字段或构造器 bind
  4. 全项目开启严格模式 / 使用 ESM,让 this 错误尽早暴露为 undefined 报错
  5. TypeScript 用户:开启 noImplicitThis,并善用显式 this 参数:
    function handler(this: HTMLButtonElement, e: MouseEvent) { /* ... */ }  
  6. 不确定时,打印 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

问题现象
在“我的”页面点击“跳转设置页”按钮跳转到设置页面,底部的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

Appstore上传 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 多端可用

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。

对接真实后端

  1. 修改 src/siteinfo.js 中的 siteroot 为你的后端地址;
  2. src/common/request/api_list.js 中核对接口地址与 {code, msg, data} 协议;
  3. 微信小程序构建需在 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.jsenableMock 改为 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。

对接真实后端

  1. 修改 src/siteinfo.js 中的 siteroot 为你的后端地址;
  2. src/common/request/api_list.js 中核对接口地址与 {code, msg, data} 协议;
  3. 微信小程序构建需在 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.jsenableMock 改为 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(发布前请核对);
收起阅读 »