十分强大的特效组件:affecter-react
目录
- 一、简介
- 二、Affecter 是什么?
- 三、Affecter 能做什么?
- 四、安装方式
- 五、相关概念
- 六、基本使用方式
- 七、Affecter 的运行模式
- 八、Affecter 的结构
- 九、Affecter 类型表示规则
- 十、给 Affecter 和 AffectedItem 设置样式
- 十一、影响锚点
- 项目锚点
- 十二、影响样式
- 十三、转换器数组
- 转换流
- 转换流的分支
- 转换流的汇总
- 距离坐标对象的其它数据
- 共享数据
- 转换器的生成器
- 转换器规范
- 十四、计算布局item
- computeAffectStyleForItem参数
- 十五、css布局item
- 十六、循环
- 设置循环包的样式
- 循环包间隔
- 注意事项
- 十七、计算循环
- 工作机制
- ItemType
- computeLoopItemTypeCreater(ItemContentType)
- itemDataArr
- itemSize
- 项目的间隔
- 循环单元(单个循环周期)的行列数目
- 代码示例
- 十八、自定义 AffectedItem
- 十九、交互方式
- 示例
- onScroll
- 二十、性能优化
- 渲染更新的最小时间间隔
- 渲染更新的最小滑动步长
- 渲染矩形的扩展半径
- 禁止渲染所有的元素
前言
本组件的封装用了2个星期,但教程和示例代码的撰写却花费了3个多星期,因为教程文档不同于API文档,教程在讲解知识点时需要循序渐进,再加上Affecter的概念繁多,在写教程时经常有种无从下手的感觉,所以才导致了该教程的撰写一拖再拖!文档中如有描述还当,还请指教!
如果您在使用该库的过程中有遇到了问题,或者有好的建议和想法,您都可以通过以下方式联系我,期待与您的交流:
- 邮箱:guobinyong@qq.com
- QQ:guobinyong@qq.com
- 微信:keyanzhe
注意:
由于录屏软件的工作会导致网页渲染贞率降低,所以教程中的 gif 动画中特效延迟较明显,而在实际中是没有这种明显的延迟的!
内容
一、简介
Affecter
中文名字是 影响者
,它用于根据子元素与其与特定1个或者多位置的距离对子元素设置样式;
affecter-react 是 React版本的 Affecter ,将来还会有 Vue、Angular 等各大框架的版本;
在 本项目的Git仓库 中包含了 affecter-react 和 下文的示例代码;点击本项目的Git仓库,即可下载;
本文是教程,如果你要查看接口,通过下面的2个文档可能会使你更快地获取您想要的信息:
如果您在使用该库的过程中有遇到了问题,或者有好的建议和想法,您都可以通过以下方式联系我,期待与您的交流:
- 邮箱:guobinyong@qq.com
- QQ:guobinyong@qq.com
- 微信:keyanzhe
二、Affecter 是什么?
Affecter 的定义是:用于根据子元素与其与特定1个或者多位置的距离对子元素设置样式;
Affecter 被设计为一个可扩展、可变换、较易用的功能强大的效果组件;所以,Affecter 并不是只能实现一个特定的效果,而是能实现一类效果;Affecter 并不是一个等效组件,而是对某类等效的逻辑进行了抽离、封装的等效方案组件;
三、Affecter 能做什么?
在 Affecter 中,你可以指定一个或多个点为 影响锚点
,通过一组变换函数更改 影响锚点
周期的元素的样式;
以下是 Affecter 的部分效果示例:
-
单轴效果
单轴效果
-
多轴放大镜效果
多轴放大镜效果
-
圆筒效果
圆筒效果 -
钻洞效果
钻洞效果 -
磁铁鼠标效果
吸附鼠标效果 -
放置磁铁效果
放置磁铁效果 -
组合效果
组合效果1
组合效果2
四、安装方式
通过 npm 安装:
npm install --save affecter-react
五、相关概念
下面是 Affecter 实现的一个放大镜效果:
在该示例的动画中,有许多展示图片的元素,像这样的元素在 Affecter 中叫做 影响项目 AffectedItem
,简称 项目 Item
; 在滑动这些项目的过程中,当项目经过视口中间位置附近时,项目会被放大,仿佛视口中间有个放大镜,会放大其周期的项目,这个会影响周期项目的点 就叫做 影响锚点 affectAnchor
; 影响锚点 影响的范围 叫做 影响区域 ;
这些概念如下图所示:
概念的标准定义:
- Affecter 影响者 : 用于根据子元素与其与特定1个或者多位置的距离对子元素设置样式的组件;
- AffectedItem 影响项目 : Affecter 所展示的内容的基本单位,也是被影响的对象;
- affectAnchor 影响锚点 : 影响 item 的点;
- itemAnchor 项目锚点 : 项目的锚点;
六、基本使用方式
<Affecter>
<AffectedItem>
<p>item的内容</p>
</AffectedItem>
<AffectedItem>
<p>item的内容</p>
</AffectedItem>
</Affecter>
说明:
Affecter 是容器,AffectedItem 是项目, AffectedItem 标签之间包的是您想导示的项目的内容;
Affecter的基本使用规则:
- 项目的内容作为 AffectedItem 的子组件 由 AffectedItem 标签包裹;
- AffectedItem 必须是 Affecter 的直接子组件;
示例代码:
Base.jsx
import React, { Component } from 'react';
import { Affecter, AffectedItem } from 'affecter-react';
import './Base.css';
import img2 from '../assets/img2.png';
class Base extends Component {
render() {
return (
<Affecter className="affecter" >
<AffectedItem className="affected-item" key="1" >
<img className="img" src={img2} />
</AffectedItem>
<AffectedItem className="affected-item" key="2" >
<img className="img" src={img2} />
</AffectedItem>
<AffectedItem className="affected-item" key="3" >
<img className="img" src={img2} />
</AffectedItem>
</Affecter>
);
}
}
Base.css
.affecter {
background-color: rgb(255, 0, 191);
height: 200px;
}
.affected-item {
float: left;
margin: 10px;
width: 50px;
height: 50px;
}
.img {
width: 100%;
height: 100%;
}
示例效果:
这个效果并没有反应出 Affecter 的功能,仅是展示了 Affecter 的基本使用方法;
七、Affecter 的运行模式
Affecter 有多种运行模式,这些运行模式触发条件如下:
运行模式 和 触发条件:
- 计算循环模式 : Affecter 的prop ItemType 有值;
- 单向循环模式 : Affecter 的prop loopType 的值为 "Hor" 或 "Ver" ;
- 任意方向循环模式 : Affecter 的prop loopType 的值为 "All" ;
- 包裹模式 : Affecter 的prop wrapChildren 的值为 true ;
- 基本模式 : 其他模式的触发条件都不成立时;
其中 单向循环模式
和 任意方向循环模式
统称为 循环模式
;
Affecter 同时只能运行于一种模式,所以,当多个模式的触发条件都具备时,Affecter 会按照以下顺序选择运行模式:
模式触发优先级排序:
- 计算循环模式
- 循环模式
- 包裹模式
- 正常模式
八、Affecter 的结构
Affecter 的可能的元素结构有以下几种:
-
无包裹结构
无包裹结构图
在正常模式
或者计算循环模式
时,Affecter 的结构如下:
-
包裹模式结构
包裹模式结构图
在包裹模式
时,Affecter 的结构如下:
- 单方向循环模式结构
在单方向循环模式
时,Affecter 的结构如下:
单方向循环模式结构图
- 任意方向循环模式结构
在任意方向循环模式
时,Affecter 的结构如下:
任意方向循环模式结构图
九、Affecter 类型表示规则
在介绍 Affecter 的过程中,可能会有很多数据类型,为了简化表述,现在常用的数据类型的定义罗列如下:
ScaleNumber : 表示比例的数字类型;
Coord = {x:number,y:number} : 表示坐标的类型
Size = {width:number,height:number} : 表示尺寸的类型
Rect = {x:number,y:number,width:number,height:number} : 表示矩形的类型
Element : 原生Dom元素
RowCol = {row:number,col:number} : 表示行列号的类型
Style : React样式对象类型
CalssName : React的calssName的类型
十、给 Affecter 和 AffectedItem 设置样式
你可以像给普通 HTML 元素设置 props 一样,给 Affecter
和 AffectedItem
设置普通 HTML 的 props , 如:className
、style
等等;Affecter
和 AffectedItem
会把它接收到的未被定义的 props 设置到它的根元素上;所以,你可以通过 给 Affecter
和 AffectedItem
设置 className
或者 style
定义样式;
示例代码:
<Affecter className="affecter" style={styleObj} >
<AffectedItem className="affected-item" style={styleObj} >
<img className="img" src={img2} />
</AffectedItem>
</Affecter>
十一、影响锚点
通过 Affecter 的prop affectAnchors
可以给 Affecter 设置影响锚点,它接收一个包含影响锚点的数组,之所以是数组,是因为 Affecter 支持多个影响锚点;
影响锚点用 Coord 类型的数据表示, 表示影响锚点在Affecter的视口上的偏移坐标,坐标单位是偏移像素相对于 Affecter 宽度和高度的比例;
示例代码:
<Affecter affectAnchors={[{x:0.2,y:0.2},{x:0.8,y:0.8}]} >
如果你想用像素单位来描述影响锚点的坐标,则可以通过设置prop usePixelCoordInAffecter
为 true
;
如下所示:
<Affecter affectAnchors={[{x:0.2,y:0.2},{x:0.8,y:0.8}]} usePixelCoordInAffecter={true} >
项目锚点
在 Affecter 中,经常需要计算 item 与 影响锚点之间的距离坐标,所以需要在 item 上选定一个点作为 item 的项目锚点 ,用以代表 item 的位置;
通过 Affecter 的prop itemAnchor
可以设置 item 的项目锚点的位置,表示 项目锚点 在 项目视口上的 偏移坐标,用于计算与影响锚点之间的距离;单位是:偏移像素相对于项目宽高的偏移比例;,itemAnchor
接收类型为 {x:ScaleNumber,y:ScaleNumber}
的值,如果不设置,则取默认值 {x:0.5,y:0.5}
;
示例代码:
<Affecter itemAnchor={x:0.3,y:0.3} affectAnchors={[{x:0.2,y:0.2},{x:0.8,y:0.8}]} >
十二、影响样式
通过 Affecter 的prop getItemAffectStyle
可以给 Item 动态地设置样式; getItemAffectStyle
接收一个 (distanceArr : [Coord],itemElement : Element?,containerElement : Element?,itemRowCol:{row:number,col:number,index:number},itemRect:Rect)=>Style
类型的函数,该函数会在 item 被渲染时自动调用;该函数接收一个包含 当前 item 的项目锚点 与 各个影响锚点的距离坐标 的数组 distanceArr,还接收很多其它的参数,详见[API文档][];该函数返回的对象会被作为样式对象设置到 Item 的 style
prop;
示例代码:
// 组件
class Magnifier extends Component {
//影响样式
getItemAffectStyle(distanceArr,itemElement,containerElement,itemRowCol,itemRect){
let scale = distanceArr[0].x;
let translateZ = scale * 150;
return {transform:`perspective(400px) translateZ(${translateZ}px)`};
}
render() {
let itemArr = [];
for (let i = 1; i <= 500; i++) {
let elem = (
<AffectedItem key={i} className="affected-item">
<img className={"img"} src={img2} />
</AffectedItem>
);
itemArr.push(elem);
}
return (
<Affecter className="affecter" affectAnchors={[{x:0.5,y:0.5}]} getItemAffectStyle={this.getItemAffectStyle} >{itemArr}</Affecter>
);
}
}
十三、转换器数组
为了实现根据 Item 与 影响锚点 之间的距离坐标 给 Item 设置样式,你可以在 getItemAffectStyle
函数中实现该逻辑,但是,当转换逻辑较复杂,或者,为了更好的复用,你需要把多种效果的共同的转换逻辑给抽离出来时, getItemAffectStyle
函数就不是好的选择了;当初在设计 Affecter 时,便专门为 转换逻辑 设计了方便的接口:你可以使用 Affecter 的prop transforms
来配置转换函数;
transforms 接收一个包含函数的数组,这些函数都是用于转换 Item 与 影响锚点 之间的距离坐标的,称为 转换函数 或者 转换器 ;这些转换器会在需要时,在调用 getItemAffectStyle
之前依次按照转换器在数组中的索引被调用,每个转换器的返回值会成为下个转换器的输入值;
转换器的类型为 (distance:Coord,index:number,distanceArr:[Coord],itemRect :Rect,itemElement?:Element,containerElement?:Element)=>Coord
,其中各个参数的详情如下:
-
@param distance : {x:number,y:number} 项目锚点与第index个影响锚点之间的距离坐标;
-
@param index : number 当前影响锚点的序号;
-
@param distanceArr : [{x:number,y:number}] 项目锚点与所有影响锚点的距离坐标数组,该数组有个 publicData 属性,publicData 属性中存储的是各个距离坐标对象共享的数据;
-
@param itemRect :{x:number,y:number,width:number,height:number} 项目元素的位置和长宽信息;
-
@param itemElement ?:Element 项目元素的Dom节点;
-
@param containerElement ?:Element 项目元素的容器的Dom节点;
-
@this :Object 用于保存各个距离坐标对象所共享的数据; this 的值会被作为 distanceArr.publicData 的值传给下一个转换器;
-
@returns {x:number,y:number,...other} 转换后的距离坐标对象;转换器中返回的值必须包含
x
和y
属性;
备注:Affecter 中内置了一些常用的转换器,你可以通过 import {转换器} from 'affecter-transforms'
导入,详情可参考《内置Transforms的接口文档》;
示例代码
class Magnifier extends Component {
constructor(props) {
super(props);
this.transforms = [this.abs, this.magnifierTransform];
}
// 绝对值转换器
abs(distance) {
let abs = Math.abs;
let transformedResult = {
x: abs(distance.x),
y: abs(distance.y)
}; //转换的距离坐标
let newDistance = { ...distance, ...transformedResult }; //覆盖原来距离坐标对象中的 x 和 y ,但保留原来坐标对象中的其它数据;
return newDistance; //返回合并后的距离坐标
}
// 放大镜转换器
magnifierTransform(distance) {
let referencerX = 100;
let referencerY = 100;
let rangeX = Number.POSITIVE_INFINITY;
let rangeY = Number.POSITIVE_INFINITY;
let abs = Math.abs;
let resultX = distance.x;
let resultY = distance.y;
let effectResultX = abs(resultX) < rangeX ? resultX : rangeX;
let effectResultY = abs(resultY) < rangeY ? resultY : rangeY;
let magnifierX = 1 - (effectResultX / referencerX);
let magnifiery = 1 - (effectResultY / referencerY);
let transformedResult = { x: magnifierX, y: magnifiery }; //转换的距离坐标
let newDistance = { ...distance, ...transformedResult }; //覆盖原来距离坐标对象中的 x 和 y ,但保留原来坐标对象中的其它数据;
return newDistance; //返回合并后的距离坐标
}
//影响样式
getItemAffectStyle(distanceArr, itemElement, containerElement, itemRowCol, itemRect) {
let scale = distanceArr[0].y;
let translateZ = scale * 150;
return { transform: `perspective(400px) translateZ(${translateZ}px)` };
}
render() {
let itemArr = [];
for (let i = 1; i <= 200; i++) {
let elem = (
<AffectedItem key={i} className="affected-item">
<img className={"img"} src={img2} />
</AffectedItem>
);
itemArr.push(elem);
}
return (
// 给 Affecter 传入转换器数据
<Affecter className="affecter" affectAnchors={[{ x: 0.5, y: 0.5 }]} wrapChildren={true} wrapClass="wrap" getItemAffectStyle={this.getItemAffectStyle} transforms={this.transforms} >{itemArr}</Affecter>
);
}
}
示例效果
转换流
转换器是按照转换器在数组中的索引依次被调用的,每个转换器的返回值会成为下个转换器的输入值,但是转换器可以只对传入的距离坐标对象中感兴趣的数据进行转换,也可以把转换后的数据存储在距离坐标对象的任何属性;下个转换器也可以继续处理上个转换器生成的数据,也可以处理距离坐标对象中的其它属性中保存的数据;这样,转换器对距离坐标对象中各个属性的处理就形成了流,这种流就叫做转换流;
转换流转换流的分支
当有多个转换器需要对某个数据的同一值进行处理时,就需要分支转换流;对转换流进行分支,只需要在转换器中,把转换后的数据存入到一个或多个距离坐标对象中 与 该数据转换前所在的属性 不同的属性中即可;
分支转换流示例代码
// 放大镜转换器
function magnifierTransform(distance) {
let referencerX = 100;
let referencerY = 100;
let rangeX = Number.POSITIVE_INFINITY;
let rangeY = Number.POSITIVE_INFINITY;
let abs = Math.abs;
let resultX = distance.x;
let resultY = distance.y;
let effectResultX = abs(resultX) < rangeX ? resultX : rangeX;
let effectResultY = abs(resultY) < rangeY ? resultY : rangeY;
let magnifierX = 1 - (effectResultX / referencerX);
let magnifierY = 1 - (effectResultY / referencerY);
distance.magnifierX = magnifierX; //把转换后的数据存放在 magnifierX 中
distance.magnifierY = magnifierY; //把转换后的数据存放在 magnifierY 中
return distance; //返回转换后的距离坐标对象
}
转换流的汇总
当需要把多个转换流的转换数据合并成一个数据时,就需要汇总转换流;对转换流进行汇总,只需要在转换器中,把多个转换流转换后的数据经过处理后存入到距离坐标对象中同一属性中即可;
汇总转换流示例代码
// 合并转换器
function uniteTransform(distance) {
distance.a = {...distance.a,...distance.b}; //把多个数据合并存成一个数据
return distance; //返回转换后的距离坐标对象
}
距离坐标对象的其它数据
距离坐标对象中x
、y
是必须的属性,除x
、y
属性外,您也可以在距离坐标对象中保存其它属性用来存放其它的数据;这些数据一般都是每个距离坐标对象所特有的数据、不需要与其它距离坐标对象共享的数据;
示例代码
// 合并转换器
function uniteTransform(distance) {
distance.otherData = "任何类型的数据"; //数据存储在除`x`和`y`以外的属性中
return distance; //返回转换后的距离坐标对象
}
共享数据
由于 Affecter 支持多个影响锚点,难免会有一些数据是所有距离坐标对象所共享的,比如:所有距离坐标中最小的 x 和 y;把这些数据存储在距离坐标对象中是十分不合适的;在转换器中,您可以通过 this
来访问和存储共享数据,this
指向一个所有距离坐标对象共享的数据对象,在转换函数被调用后,该共享数据对象会被存储在 距离坐标数组的 publicData
属性中,所以,在影响样式的回调方法 getItemAffectStyle
中,通过 distanceArr.publicData
来访问共享数据;
示例代码
// 转换器:寻找距离坐标中最小的 x 和 y
function minXYIndex(distance, index, distanceArr, itemRect, itemElement, containerElement) {
if (index === 0) {
let xArr = [];
let yArr = [];
distanceArr.forEach(function (distance, index) {
xArr[index] = distance.x;
yArr[index] = distance.y;
});
let min = Math.min;
//把数据存入共享数据对象中
this.minX = min(...xArr);
this.minY = min(...yArr);
}
return distance;
}
//Affecter的影响样式回调函数
getItemAffectStyle(distanceArr, itemElement, containerElement, itemRowCol, itemRect) {
let scale = distanceArr.publicData.minX;
let translateZ = scale * 150;
return { transform: `perspective(400px) translateZ(${translateZ}px)` };
}
转换器的生成器
有些转换器需要一些额外的数据,但转换器是 Affecter
调用的,转换器的参数都是 Affecter
传进来的;为了能让转换器访问到一些额外的数据,可以给一个函数传入这些数据,并通过该函数生成转换器并返回该转换器,这种生成转换器的函数就称为 转换器生成器;其实,这是利用了闭包的特性;
示例代码
/*
# 放大转换器生成器
@param referencer ?: {x:number,y:number} 参考数据,相对数据
@param effectRange ?: {x:number = Number.POSITIVE_INFINITY,y:number = Number.POSITIVE_INFINITY} 有效范围
*/
function magnifierCreater(referencer = { x: 1, y: 1 }, effectRange = { x: Number.POSITIVE_INFINITY, y: Number.POSITIVE_INFINITY }) {
let referencerX = referencer.x || 1;
let referencerY = referencer.y || 1;
let rangeX = effectRange.x || Number.POSITIVE_INFINITY;
let rangeY = effectRange.y || Number.POSITIVE_INFINITY;
//创建转换器
let magnifierTransform = function (distance) {
let abs = Math.abs;
let resultX = distance.x;
let resultY = distance.y;
let effectResultX = abs(resultX) < rangeX ? resultX : rangeX;
let effectResultY = abs(resultY) < rangeY ? resultY : rangeY;
let magnifierX = 1 - (effectResultX / referencerX);
let magnifiery = 1 - (effectResultY / referencerY);
let transformedResult = { x: magnifierX, y: magnifiery };
let newDistance = { ...distance, ...transformedResult };
return newDistance;
};
return magnifierTransform; //返回转换器
}
//使用
let magnifierTran = magnifierCreater({x:150,y:150}); //调用转换器生成器,用来生成转换器
<Affecter className="affecter" affectAnchors={[{ x: 0.5, y: 0.5 }]} wrapChildren={true} wrapClass="wrap" getItemAffectStyle={this.getItemAffectStyle} transforms={[abs,magnifierTran]} >{itemArr}</Affecter>
转换器规范
为了创建合理高效的转换器,您应该遵守以下规范:
- 转换器会按转换器数组中的顺序调用,上个转换器的返回值,会作为下个转换器的distance;
- 转换器中返回的对象必须包含
x
和y
属性; - 对于距离坐标对象的私有数据应该保存在距离坐标对象中;
- 对于不属于任何距离坐标的数据应该保存在 转换器中的 this 里;this 里的数据会被作为共享数据储存在 distanceArr.publicData 中;
- 通用的转换器应该把转换后的值与传入的 distance 合并,以达到覆盖distance的
x
和y
属性,同时又能保留distance中的其它属性值;function transformeFun(distance) { //转换过程省略... let transformedResult = ...; //转换结果 let newDistance = { ...distance, ...transformedResult }; //合并 原距离坐标对象 和 转换结果 return newDistance; 返回合并后的对象 }
- 如果需要实现转换器的分支,则需要把转换的结果保存在 this中 或者 距离坐标对象中除
x
和y
属性之外的其它属性中;
十四、计算布局item
item 在 Affecter 中的排列、布局样式可以通过 Affecter 的prop getItemInitStyle
来设置,getItemInitStyle
接收一个类型为 (element,index,computeAffectStyleForItem)=>Style
的函数,它会在Affecter的props更新时为每个item调用一次,该函数返回的样式对象会设置给 item 的 style;它适合用来返回影响锚点不影响的 item 样式;
示例代码 (只显示关键代码)
ComputeLayout.css
.affecter {
overflow: scroll;
position: relative;
}
.affected-item {
position: absolute;
width: 50px;
height: 50px;
}
ComputeLayout.jsx
class ComputeLayout extends Component {
// 初始样式
getItemInitStyle(element,index,computeAffectStyleForItem){
let maxRowNum = 5;
let space = 20;
let itemSize = 50;
let rowNum = parseInt(index / maxRowNum );
let colNum = index % maxRowNum;
let leftSpace = space * colNum;
let left = itemSize * colNum + leftSpace;
let topSpace = space * rowNum;
let top = itemSize * rowNum + topSpace;
let itemRect = {x:left,y:top,width:itemSize,height:itemSize};
let itemStyle = {left:`${left}px`,top:`${top}px`}; //通过 left、top 来定位;
return itemStyle;
}
render() {
let itemArr = [];
for (let i = 1; i <= 300; i++) {
let elem = (
<AffectedItem key={i} className="affected-item">
<img className={"img"} src={img2} />
</AffectedItem>
);
itemArr.push(elem);
}
return (
<Affecter className="affecter" affectAnchors={[{x:0.5,y:0.5}]} getItemInitStyle={this.getItemInitStyle} >{itemArr}</Affecter>
);
}
}
示例效果
computeAffectStyleForItem参数
在使用 getItemInitStyle
时,你可能会遇到这样的问题:当 Affecter 第一次显示时, 所有的 item 都没有影响样式;这是因为没有触发 getItemInitStyle
从而导致 item 的影响样式没有被设置;
解决这个问题很简单:Affecter 会给 getItemInitStyle
的第3个参数传递一个 computeAffectStyleForItem
函数,该函数的类型为 (itemElement : Element ?, orItemRect : Rect ?)=>Style
,通过给该函数传递一个当前 item 的 Rect ,可以获得相应的影响样式,然后把影响样式与 getItemInitStyle
原本要返回的样式合并,再返回;
示例代码
getItemInitStyle(element,index,computeAffectStyleForItem){
let maxRowNum = 5;
let space = 20;
let itemSize = 50;
let rowNum = parseInt(index / maxRowNum );
let colNum = index % maxRowNum;
let leftSpace = space * colNum;
let left = itemSize * colNum + leftSpace;
let topSpace = space * rowNum;
let top = itemSize * rowNum + topSpace;
let itemRect = {x:left,y:top,width:itemSize,height:itemSize};
let itemStyle = {left:`${left}px`,top:`${top}px`}; //通过 left、top 来定位;
let affectStyle = computeAffectStyleForItem(null,itemRect); //传入item 的矩形,获取该item的影响样式
let initStyle = {...itemStyle,...affectStyle}; // 合并所有的样式
return initStyle; //返回合并后的样式
}
十五、css布局item
通过 getItemInitStyle
可以使用计算的方式来布局 item ,但这样太麻烦,有些布局通过css来实现会非常简单;如果直接把 Affecter 作为 item 的布局容器,则会有很多限制,也会产生很多问题:所以需要一个单独布局容器来包裹 item ;Affecter 的包裹模式符合这个要求;
包裹模式下的 Affecter 的结构如下图所示:
通过 Affecter 的prop wrapClass
可以给包设置css类,通过 Affecter 的prop wrapStyle
,可以给包设置样式对象;可以利用 wrapClass
和 wrapStyle
把item的布局容器的样式设置到包 wrap 上;
示例代码
通过浮动布局来布局item
Magnifier.css
.affecter {
background-color: rgb(255, 0, 191);
height: 500px;
overflow: scroll;
position: relative;
-webkit-overflow-scrolling:touch;
}
.wrap{
overflow: visible;
width: 250%;
}
.affected-item {
float: left;
margin: 10px;
width: 50px;
height: 50px;
}
.img {
width: 100%;
height: 100%;
}
Magnifier.jsx
class Magnifier extends Component {
constructor(props) {
super(props);
this.transforms = [Transforms.hypotenuse,Transforms.magnifierCreater({x:100,y:100})];
}
//影响样式
getItemAffectStyle(distanceArr,itemElement,containerElement,itemRowCol,itemRect){
let scale = distanceArr[0].x;
let translateZ = scale * 150;
return {transform:`perspective(400px) translateZ(${translateZ}px)`};
}
render() {
let itemArr = [];
for (let i = 1; i <= 500; i++) {
let elem = (
<AffectedItem key={i} className="affected-item">
<img className={"img"} src={img2} />
</AffectedItem>
);
itemArr.push(elem);
}
return (
<Affecter wrapChildren={true} wrapClass="wrap" className="affecter" affectAnchors={[{x:0.5,y:0.5}]} getItemAffectStyle={this.getItemAffectStyle} transforms={this.transforms} >{itemArr}</Affecter>
);
}
}
示例效果
十六、循环
通过 Affecter 的prop loopType
,可以让 Affecter 循环滑动;
loopType
可接受以下几个字符串值:
- "Hor" : 在水平方向上循环;
- "Ver" : 在垂直方向上循环;
- "All" : 在任意方向(水平 和 垂直)上都循环;
- "false" : 不循环;
在循环模式时,Affecter 也会用循环包 LoopWrap 包裹每份 item;循环模式下的 Affecter 的结构如下图所示:
-
单方向循环模式结构图
单方向循环模式结构图 -
任意方向循环模式结构图
任意方向循环模式结构图
示例效果
-
水平循环
水平循环 -
垂直循环
垂直循环 -
任意方向循环
任意方向循环
示例代码
<Affecter loopType={"Hor"} className="affecter" affectAnchors={[{x:0.5,y:0.5}]} getItemAffectStyle={this.getItemAffectStyle} transforms={this.transforms} >
设置循环包的样式
在循环模式下,你可以像在包裹模式下一样,通过 Affecter 的prop wrapClass
可以给循环包设置css类,通过 Affecter 的prop wrapStyle
,可以给包设置样式对象;
循环包间隔
循环包 与 循环包之间的间隔称为 包间隔;如下图所示:
通过 Affecter 的prop wrapSpace
可以设置包间隔;wrapSpace
接受 number 类型的值;
注意事项
当循环包的宽(或高) + wrapSpace <= Affecter 的宽(或高) 时,容易出现时而在水平(或者垂直)方向上不能继续循环滑动的现象;解决方案是:加大 循环包的宽(或高) 或者 wrapSpace,使 循环包的宽(或高) + wrapSpace > Affecter;
十七、计算循环
前面讲的循环是通过用2个或者4个循环包分别包裹一份 item 来实现的,当 item 数量过大时,会时分影响性能; Affecter 的计算循环模式可解决这个问题;
工作机制
计算循环模式下的 Affecter 结构如下图所示:
无包裹结构图
计算循环的运行机制如下图:
渲染矩形
item 按指定的排列进行布局,然后 Affecter 在循环方向上、在渲染矩形(详情见二十、性能优化/渲染矩形的扩展半径
)内平铺这些布局;超出渲染矩形的 item 不会被挂载和绘制,所以,即使是 item 的数目十分庞大,Affecter 也只会渲染最 渲染矩形内的 item ;
ItemType
在计算循环模式下,item 是由 Affecter 创建的,所以,您需要通过 Affecter 的prop ItemType
把 item 的类型传给 Affecter; ItemType
接受一个React的组件类,它是通过 computeLoopItemTypeCreater
工厂函数产生的;
computeLoopItemTypeCreater(ItemContentType)
computeLoopItemTypeCreater 接收一个 Component 类型的React组件类 ItemContentType 作为参数, 该组件类的实例会收到以下 props:
- row 当前 item 的行号
- col 当前 item 的列号
- itemIndex 当前 item 的项目序号
- itemData item 的数据,来源于 Affecter 的prop
itemDataArr
的值;
行号 和 列号统称 行列号,它是 计算循环为 项目设计的网络坐标,每个网格对应一个项目的位置;
项目序号 表示的是项目在计算循环的循环单元(循环周期)内的序号;所以如果2个item的 itemIndex 相同,则它们接收的 itemData 也是相同的;
computeLoopItemTypeCreater 会返回一个 Component 类型的React组件类,该组件类可用作 Affecter 的prop ItemType
的值;
itemDataArr
计算循环是用来循环展示一组 item 的,这些 item 可能需要不同的数据去填充,这些数据可由 Affecter 的prop itemDataArr
来设置,itemDataArr
接受一个数组类型的值; itemDataArr 中的元素会被分发到相应的 item 的prop itemData
中;
itemSize
由于计算循环中 item 的位置 和 尺寸都是由 Affecter 管理的,所以,您需要通过 Affecter 的prop itemSize
来设置 item 的尺寸;itemSize
接收一个 Size 类型的值;
项目的间隔
由于计算循环中 item 的布局也是由 Affecter 管理的,所以,您需要通过 Affecter 的prop horSpace
和 verSpace
来设置 item 的 水平 和 垂直 间隔;它们都是接收一个 nunber 类型的值,单位是 px
;
循环单元(单个循环周期)的行列数目
因为 item 是按指定的排列进行布局,然后 Affecter 在循环方向上、在渲染矩形内平铺这些布局;所以,这些排列布局在渲染矩形内是周期性出现的,每个排列布局都称为一个循环单元,或者一个循环周期;在循环单元内,item 的排列规则是可以通过 Affecter 的prop roundRowCount
和 roundColCount
来设置;它们都接受一个 nunber 类型的值; 它们的意义如下:
-
roundRowCount
指定循环单元内的行数,且在循环单元内,item 会纵向依次排列; -
roundColCount
指定循环单元内的行数,且在循环单元内,item 会纵向依次排列;
只需要设置 roundRowCount
和 roundColCount
中的一个prop即可,当这两个prop都被设置时,会忽略 roundColCount
的设置;
代码示例
import { Affecter, AffectedItem, computeLoopItemTypeCreater } from 'Affecter';
class ItemContent extends Component {
constructor(props){
super(props);
this.itemIndex = props.itemIndex;
}
shouldComponentUpdate(nextProps, nextState){
return this.itemIndex != nextProps.itemIndex ;
}
render() {
let {itemData} = this.props;
return (
<div >
<img className="img" src={img2}/>
<p>{itemData}</p>
</div>
);
}
}
let ComputeLoopItem = computeLoopItemTypeCreater(ItemContent);
// 组件
class ComputeLoop extends Component {
constructor(props) {
super(props);
let itemDataArr = [];
for (let index = 0; index < 50; index++) {
itemDataArr.push(index);
}
this.itemDataArr = itemDataArr;
this.transforms = [Transforms.hypotenuse, Transforms.magnifierCreater({ x: 150, y: 150 })];
}
//影响样式
getItemAffectStyle(distanceArr, itemElement, containerElement, itemRowCol, itemRect) {
let scale = distanceArr[0].x;
let translateZ = scale * 140;
return { transform: `perspective(300px) translateZ(${translateZ}px)` };
}
render() {
return (
<div>
<Affecter ItemType={ComputeLoopItem} itemDataArr={this.itemDataArr} itemSize={{ width: 50, height: 50 }} horSpace={20} verSpace={20} roundRowCount={5} renderExtendRadius={100} loopType="All" className="affecter" affectAnchors={[{ x: 0.5, y: 0.5 }]} getItemAffectStyle={this.getItemAffectStyle} transforms={this.transforms}></Affecter>
</div>
);
}
}
示例效果
十八、自定义 AffectedItem
Affecter 包中自带的 AffectedItem 已经能够满足绝大多数场景,但 Affecter 也允许你自定的 AffectedItem;
自定义 AffectedItem 也非常简单,在自定义 AffectedItem 之前,你需要了解以下信息:
Affecter 会给 AffectedItem 传递以下props:
- computeAffectStyleForItem
类型: (itemElement : Element ?, orItemRect : Rect ?)=>Style
说明: 计算元素被影响的样式 - itemIsInRenderRange
类型: (itemElement : Element)=>boolean
说明: 用于判断该元素是否在渲染矩形内;
这些都是 Affecter 为您自定义 AffectedItem 提供的工具方法;所以,在创建自定义的 AffectedItem 时,您只需要在需要设置样式时通过 this.props.computeAffectStyleForItem 函数获取新的样式即可; 在调用 computeAffectStyleForItem
获取样式时,你需要把当前 item 的 dom 或者 item 的矩形 传给它;
示例代码
class CustomAffectedItem extends Component {
constructor(props) {
super(props);
this.setThisDom = this.setThisDom.bind(this);
}
shouldComponentUpdate(nextProps, nextState) {
return nextProps.itemIsInRenderRange(this.thisDom);
}
setThisDom(element) {
this.thisDom = element;
}
render() {
let { computeAffectStyleForItem, style, itemIsInRenderRange, renderAll, ...otherProps } = this.props;
let affectStyle = computeAffectStyleForItem(this.thisDom);
let newStyle = { ...style, ...affectStyle };
return <div {...otherProps} style={newStyle} ref={this.setThisDom}></div>;
}
}
十九、交互方式
以上的示例中,都是以滑动的方式来与 Affecter 交互的,其实,Affecter 不仅仅可以用滑动的方式来影响 item ,Affecter 支持以任意交互方式来影响 item ,如:click、mouseover、mousedown、mousemove 等等; 并且 Affecter 也支持多种交互方式同时生效,这样便可产生极其丰富的交互效果;
注意:
当你用鼠标事件的坐标来设置 Affecter 的影响锚点的坐标时,可能需要改用像素坐标,而非默认的偏移比例坐标;若要用像素坐标,只需要把 Affecter 的prop usePixelCoordInAffecter
设置为 true
即可;
示例
MouseOver方式
class Interaction extends Component {
constructor(props) {
super(props);
this.eventHandel = this.eventHandel.bind(this);
this.transforms = [Transforms.hypotenuse,Transforms.magnifierCreater({x:100,y:100})];
this.state = {affectAnchor:{x:0,y:0}};
}
//影响样式
getItemAffectStyle(distanceArr,itemElement,containerElement,itemRowCol,itemRect){
let scale = distanceArr[0].x;
let translateZ = scale * 150;
return {transform:`perspective(400px) translateZ(${translateZ}px)`};
}
eventHandel(event){
this.setState({affectAnchor:{x:event.clientX,y:event.clientY}});
}
render() {
let itemArr = [];
for (let i = 1; i <= 100; i++) {
let elem = (
<AffectedItem key={i} className="affected-item">
<img className={"img"} src={img2} />
</AffectedItem>
);
itemArr.push(elem);
}
return (
<Affecter onMouseOver={this.eventHandel} affectAnchors={[this.state.affectAnchor]} usePixelCoordInAffecter={true} className="affecter" wrapChildren={true} wrapClass="wrap" getItemAffectStyle={this.getItemAffectStyle} transforms={this.transforms} >{itemArr}</Affecter>
);
}
}
示例效果
Click方式
<Affecter onClick={this.eventHandel} affectAnchors={[this.state.affectAnchor]} usePixelCoordInAffecter={true} className="affecter" wrapChildren={true} wrapClass="wrap" getItemAffectStyle={this.getItemAffectStyle} transforms={this.transforms} >
示例效果
MouseOver和Scroll的组合效果
MouseOver和Scroll的组合效果
onScroll
你也可以像监听其它事件一样监听 Affecter 的 onScroll 事件,与其它事件不同的是:如果在 onScroll 的事件处理函数中返回 true
,则不会执行 Affecter 的默认操作;设计这种特性的目的是为了给你一个完全自定义 Affecter 的滑动逻辑的机会;
示例代码
//省略其它代码
scrollHandel(event){
return true;
}
//省略其它代码
<Affecter onScroll={this.scrollHandel} affectAnchors={[{x:0.5,y:0.5}]} className="affecter" wrapChildren={true} wrapClass="wrap" getItemAffectStyle={this.getItemAffectStyle} transforms={this.transforms} >
示例效果
二十、性能优化
渲染更新的最小时间间隔
有些事件的触发间隔是相当小的,如:onScroll
事件,只要有滑动,该事件就会在每秒内高频地被调用,从而导致 Affecter 频繁更新 item ,在 item 数量较多时,很影响性能; 通过 Affecter 的prop throttleDelay
,你可以设置渲染更新的最小时间间隔,当距离上次更新的时间间隔小于设置的最小时间间隔时,不会触发更新,它接收一个 number 类型的值,表示的单位是 毫秒;
渲染更新的最小滑动步长
onScroll
事件的触发不但频繁,而且灵敏,只要有稍微的滑动,就会触发;对于要求一般的特效,不需那么高的灵敏度; 通过 Affecter 的prop throttleStep
,你可以设置渲染更新的最小的滑动步长,当距离上次更新的位置的坐标距离小于设置的最小步长时,不会触发更新,它接收一个 number 类型的值,表示的单位是 px
;
渲染矩形的扩展半径
Affecter 中的 item 可能会有很多,但用户任意时刻只能看到 Affecter 视口内的 item ,如果每次更新都要为所有的 item 都进行计算的话,在 item 较多时,又会造成很大的性能浪费;为了充分利用性能, Affecter 允许你通过 Affecter 的渲染矩扩展半径prop renderExtendRadius
来设置 渲染矩形,这样,每次更新时,Affecter 只计算 渲染矩形内的 item ; renderExtendRadius
接收一个 number 类型的值,表示的单位是 px
;
渲染矩形的概念图如下所示:
渲染矩形
禁止渲染所有的元素
AffectedItem 有个prop renderAll
,接收一个布尔类型的值;表示:每次刷新是否渲染所有元素;默认是否 false
,即:只渲染 渲染矩形 内的元素;所以,一般不用设置这个prop;
注意:
renderAll
是 AffectedItem 提供的prop,不是 Affecter 提供的!