语言:JavaScript、TypeScript、ES6语法;
框架:React;
技术:Canvas画布、React条件渲染、可视化操控溢出控制;
先上图,看看效果:
再来谈谈出发点及该组件的功能。
目的:
1、为了练手;
2、为了学习;
3、为了替代Antd的TabPane(高度算不准,宽度不到位,占用间距太大);
4、更是个人框架的一个重要组成部分;
组件功能特性:
1、组件成员:TabControl、TabPage
2、由于采用Canvas做页签头,样式属性化;
3、页签支持图标、关闭控制、默认、选中、悬浮、激活状态下的背景色、前景色;
4、增加溢出按钮,可以快速浏览溢出的页签。避免页签数量过多时,左右来回切换,却始终找不到用户想要的页签;
核心技术点:
1、Canvas画布,绘制半闭合,或者全闭合的圆角矩形(也可以在四周,补充扇形。作者在此取消,补了几个模糊的点,肉眼看上去是圆形的);
2、页签溢出、左右偏移算法;
3、页签选中、关闭的state状态控制(重要知识点:React.children、React.cloneElement() 的使用 )
上部分核心代码:
1、Canvas绘制顶部圆角的边框
private DrawItemBackgroundRound(poBounds: Rectangle, poColor: any, plSelected: boolean) {
let loPoints = new RayningArray<Point>();
if (this.TabControl.TabStyle === 'Round') {
// 顶部圆角
loPoints.Add(new Point(poBounds.X, poBounds.Bottom));
loPoints.Add(new Point(poBounds.X, poBounds.Y + 4));
loPoints.Add(new Point(poBounds.X + 1, poBounds.Y + 1.5));
loPoints.Add(new Point(poBounds.X + 1, poBounds.Y + 1));
loPoints.Add(new Point(poBounds.X + 3, poBounds.Y + 0.5));
loPoints.Add(new Point(poBounds.X + 4, poBounds.Y));
loPoints.Add(new Point(poBounds.Right - 4, poBounds.Y));
loPoints.Add(new Point(poBounds.Right - 3, poBounds.Y + 0.5));
loPoints.Add(new Point(poBounds.Right - 1, poBounds.Y + 1));
loPoints.Add(new Point(poBounds.Right - 1, poBounds.Y + 1.5));
loPoints.Add(new Point(poBounds.Right, poBounds.Y + 4));
loPoints.Add(new Point(poBounds.Right, poBounds.Bottom));
} else {
// 矩形
loPoints.Add(new Point(poBounds.X, poBounds.Bottom));
loPoints.Add(new Point(poBounds.X, poBounds.Y));
loPoints.Add(new Point(poBounds.Right, poBounds.Y));
loPoints.Add(new Point(poBounds.Right, poBounds.Bottom));
}
this.Graphics.FillPath(poColor, loPoints);
this.Graphics.DrawPath(new Pen(this.TabControl.TabBorderColor, 1), loPoints);
}
2、计算页签溢出、偏移量
/**
* 滚动选中页签至可视范围内。
* @param piOffset
*/
private ScrollPageItems(piOffset: number) {
let loSelectedPage: TabPageItem | null = null;
for (let liIndex = 0; liIndex < this._PageItems.Count; liIndex++) {
let loPage = this._PageItems[liIndex];
if (loPage.Key === this.TabControl.SelectedKey) {
loSelectedPage = loPage;
break;
}
}
if (!loSelectedPage) {
return;
}
if (this._PageItems[0].Bounds.X + piOffset > 5.5) {
// 第一个页签,不能偏移越界
piOffset = this._PageItems[0].Bounds.X - 5.5;
this._ScrollOffset = piOffset;
}
loSelectedPage.Bounds.Offset(piOffset, 0);
loSelectedPage.ImageBounds.Offset(piOffset, 0);
loSelectedPage.CloseBounds.Offset(piOffset, 0);
loSelectedPage.Displayed = true;
if (this._TabPageItemMore.Displayed) {
// 定位溢出按钮
for (let liIndex = this._PageItems.Count - 1; liIndex >= 0; liIndex--) {
let loTempPage = this._PageItems[liIndex];
if (loTempPage.Bounds.Right < this.Width) {
this._TabPageItemMore.Bounds.X = loTempPage.Bounds.Right + 3;
break;
}
}
}
for (let liIndex = 0; liIndex < this._PageItems.Count; liIndex++) {
let loTempPage = this._PageItems[liIndex];
if (loTempPage.Key === loSelectedPage.Key) {
loTempPage.Displayed = true;
continue;
}
// 执行偏移
loTempPage.Bounds.Offset(piOffset, 0);
loTempPage.ImageBounds.Offset(piOffset, 0);
loTempPage.CloseBounds.Offset(piOffset, 0);
loTempPage.Displayed = false;
if (
loTempPage.Bounds.Right > 5.5 &&
loTempPage.Bounds.Right <= this._TabPageItemMore.Bounds.X
) {
loTempPage.Displayed = true;
}
}
}
/**
* 计算页签偏移量。
*/
private GetScrollOffset() {
let liOffset = 0;
let loSelectedPage: TabPageItem | null = null;
for (let liIndex = 0; liIndex < this._PageItems.Count; liIndex++) {
let loPage = this._PageItems[liIndex];
if (loPage.Key === this.TabControl.SelectedKey) {
loSelectedPage = loPage;
break;
}
}
if (!loSelectedPage) {
return liOffset;
}
// 以选中页签做参照物,测量页签偏移量
if (loSelectedPage.Bounds.X < 5.5) {
// 向左偏移
liOffset = 5.5 - loSelectedPage.Bounds.X;
} else {
// 向右偏移
if (this._TabPageItemMore.Displayed && loSelectedPage.Bounds.Right + 40 > this.Width) {
liOffset = this.Width - loSelectedPage.Bounds.Right;
liOffset -= 40;
} else if (loSelectedPage.Bounds.Right > this.Width) {
liOffset = this.Width - loSelectedPage.Bounds.Right;
}
}
if (liOffset !== 0) {
this._ScrollOffset = liOffset;
}
return liOffset;
}
3、React渲染子组件。渲染规则,如果是选中的页签,就可见;否则,不可见。
this.SelectedKey,选中的页签Key;
this.TabPages,基于state状态控制的Page页签集;
Displayed,传递个TabPage页签,由页签自身校验可见性。
TabControl渲染规则:
render() {
const loResult = (
<div style={this.GetStyle()}>
<TabHeader TabControl={this}></TabHeader>
<div style={{ padding: '2px', width: '100%', height: `calc(100% - 30px)` }}>
{React.Children.map(this.Pages, (loTempPage: any) => {
if (loTempPage.props.Key === this.SelectedKey) {
return React.cloneElement(loTempPage, { Displayed: true });
} else {
return React.cloneElement(loTempPage, { Displayed: false });
}
})}
</div>
</div>
);
return loResult;
}
TabPage页签渲染;
render() {
return (
<div style={{ width: '100%', height: '100%', display: `${this.Displayed}` }}>
{this.props.children}
</div>
);
}
本人对GDI+、Canvas相当痴迷,并小有成就。欢迎咨询、洽谈,相互学习、互相进步。