前面我们封装的组件都没图标,这一节我们要解决图标问题。
npm i --save @fortawesome/fortawesome-svg-core// 安装fortawesome核心库
npm i --save @fortawesome/free-solid-svg-icons// 你要安装什么样的icon,其余icon可以去官网查看
npm i --save @fortawesome/react-fortawesome// 安装react-fortawesome组件
import {FortAwesomeIcon} from @fortawesome/react-fortawesome
import {faCoffee} from @fortawesome/free-solid-svg-icons
// FortAwesomeIcon标签还有很多属性,如size设置尺寸,rotation旋转图标以及一些animation的属性(spin、pulse等)
const element = <FortAwesomeIcon icon={faCoffee} size='lg'/>
ReactDom.render(element, document.body)
按上面的使用方法,我们每用一个图标就需要引入一下,太麻烦了,我们可以引入fortawesome核心库中的library:
(1)引入:import {library} from '@fortawesome/fortawesome-svg-core'
(2)引入你具体想用的图标:import {faCheckSquare, faCoffee} from '@fortawesome/free-solid-svg-icons'
,也可以引入所有图标:import {fas} from '@fortawesome/free-solid-svg-icons'
(3)将你要用的图标添加进library:library.add(faCheckSquare, faCoffee)
或library.add(fas)
(4)使用时给icon属性赋值:
将我们组件的主题颜色添加进fort awesome的theme属性中,fort awesome的属性不仅仅局限于它自身的,也可以使用我们的属性
import React from 'react'
import classNames from 'classnames'
import {FortAwesomeIcon, FortAwesomeIconProps} from @fortawesome/react-fortawesome
export type ThemeProps = 'primary' | 'secondary' | 'success' | 'info' | 'warning' | 'danger' | 'light' | 'dark'
export interface IconProps extends FortAwesomeIconProps{
theme?: ThemeProps
}
const Icon: React.FC<IconProps> = (props) => {
const {className, theme, ...restProps} = props;
const classes = classNames('viking-icon', className, {
[`icon-${theme}`]: theme
})
return (
<FontAwesomeIcon className={classes} {...restProps} />
)
}
export default Icon
在app.tsx中使用:
import Icon from ./components/Icon/icon
import {library} from '@fortawesome/fortawesome-svg-core'
<Icon icon='coffee' theme='danger' size=10px' />
新建src/components/Icon/_style.scss,icon用的样式的变量是我们在src/styles/_variables.scss中定义的,但是我们在src/styles/_variables.scss中定义了很多变量,一个个去找,再一个个去用很麻烦,所以我们使用SCSS的each来遍历map,我们先在src/styles/_variables.scss中定义一个map,里面保存着我们要用的变量:
$theme-colors:
(
"primary": $primary,
"secondary": $secondary,
"success": $success,
"info": $info,
"warning": $warning,
"danger": $danger,
"light": $light,
"dark": $dark
);
在src/components/Icon/_style.scss中使用each来遍历上述map
@each $key, $val in $theme-colors {
// 在选择器中要使用变量名,需要用#{变量名}包起来
.icon-#{$key} {
color: $val;
}
}
在app.tsx中使用:
我们先将刚才讲的icon运用到上一节讲的subMenu中,subMenu.tsx中代码如下:
return (
<li key={index} className={classes} {...hoverEvents}>
<div className="submenu-title" {...clickEvents}>
{title}
<Icon icon="angle-down" className="arrow-icon"/>
</div>
{renderChildren()}
</li>
)
src/components/Menu/_style.scss中添加以下代码:
.submenu-item {
position: relative;
.submenu-title {
display: flex;
align-items: center;// 垂直
}
.arrow-icon {
//transition:提供了在更改CSS属性时,控制动画速度的方法,让属性变化持续一段时间,而不是立马生效
//transition运用到.arrow-icon选择器上,当.arrow-icon的transform属性发生变化时,变化持续0.25s,且变化速度为ease-in-out
transition: transform .25s ease-in-out;
margin-left: 3px;
}
&:hover {
.arrow-icon {
transform: rotate(180deg);
}
}
}
在横向菜单栏中,当鼠标悬停在子菜单上,title旁边的图标会变化
但是在纵向菜单栏中,只有点击时,才会打开/关闭title旁边的图标,在hover时不需要做任何处理,所以我们需要给subMenu添加两个特殊的类:
const classes = classNames('menu-item submenu-item', className, {
'is-active': context.index === index,
'is-opened': menuOpen,// subMenu是否打开
'is-vertical': context.mode === 'vertical'// 是不是纵向菜单栏
})
src/components/Menu/_style.scss中为以上两个类添加以下代码:
//如果有is-vertical这个类名,说明是垂直菜单栏,就让他不旋转,且优先级最高
.is-vertical {
.arrow-icon {
transform: rotate(0deg) !important;
}
}
//如果有is-vertical、is-opened两个类名,说明是垂直菜单栏且subMenu是打开的,那么此时让他旋转180°
.is-vertical.is-opened {
.arrow-icon {
transform: rotate(180deg) !important;
}
}
我们想给下拉菜单实现渐行渐远的动画效果,如果用CSS来实现:
.viking-submenu {
opacity: 0;
// 名为viking-submenu的类的opacity属性发生变化时,变化持续0.5s,变化速度为ease-in
transition: opacity .5s ease-in;
display: none;
list-style:none;
padding-left: 0;
white-space: nowrap;
transition: $menu-transition;
.menu-item {
padding: $menu-item-padding-y $menu-item-padding-x;
cursor: pointer;
transition: $menu-transition;
color: $body-color;
&.is-active, &:hover {
color: $menu-item-active-color !important;
}
}
}
.viking-submenu.menu-opened {
display: block;
opacity: 1;
}
但是我们发现效果并未生效,这是因为display从none转化为block时,其他所有的动画效果都会失效,因为display不是一个标准的支持animation的属性,所以transition根本不起作用,并且display: block;
和opacity: 1;
是同时生效的,自然缺少opacity: 1;
的变化,我们可以把display:none;
去掉,就能够成功实现。但这并不是一个好的解决方案,那我们可以使用延时方案,让display和opacity不同时变换,不同时生效,过程图如下所示,第一排是出现的流程图,第二排是隐藏时的流程图
针对react动画实现的库:ReactTransitionGroup
ReactTransitionGroup其实是在你组件从无到有或从有到无这个过程中,给组件添加多个描述组件生命周期的class名称,这些名称由时间顺序排列,ReactTransitionGroup中有三个组件:CSSTransition、SwitchTransition、TransitionGroup
ReactTransitionGroup本身并没有实现动画效果,他只是使用不同的class来区分组件进入和离开DOM的各个阶段,我们可以自行添加样式来实现最终的动画效果
组件出现时:在*-enter
时添加动画开始的效果,在*-enter-active
时添加动画结束的效果,如果你添加了自定义的timeout,会有一个*-enter-done
的类。组件消失时流程同上。
安装ReactTransitionGroup:
npm install react-transition-group --save
npm install @types/react-transition-group --save
在subMenu.tsx中引入:import {CSSTransition} from 'react-transition-group'
使用:下拉菜单栏renderChildren中返回的结果:
in:从无到有这个过程会自动给你添加这个类名
timeout:从active到done这个时间段持续的时间,单位为ms
classNames:我们要自定义的名称
appear:假如menuOpen第一次是true,第一次执行也会运行整个动画过程在垂直菜单栏中,子菜单可能一开始是打开的
return (
<CSSTransition
in={menuOpen}
timeout={300}
classNames='zoom-in-top'
appear
>
<ul className={subMenuClasses}>
{childrenComponent}
</ul>
</CSSTransition>
)
animation.css中集合了各种各样的动画
新建src/styles/_animation.scss,其中代码为:
.zoom-in-top-enter{
opacity:0;
transform:scaleY(0);
}
.zoom-in-top-active{
opacity:1;
transform:scaleY(1);
transition: transform 300ms cubic-bezier(0.23,1,0.23,1) 100ms,opacity 300ms cubic-bezier(0.23,1,0.23,1) 100ms;
transform-origin: center top;// 改变元素变形的原点 让动画开始的原点在中心靠上
}
.zoom-in-top-exit{
opacity:1;
}
.zoom-in-top-exit-active{
opacity:0;
transform:scaleY(0);
transition: transform 300ms cubic-bezier(0.23,1,0.23,1) 100ms,opacity 300ms cubic-bezier(0.23,1,0.23,1) 100ms;
transform-origin: center top;
}
在src/styles/index.scss中导入src/styles/_animation.scss
但是我们只有在下拉菜单出现时,才会出现我们刚才添加的动画效果,下拉菜单消失时不会出现。
一开始我们组件是.viking-submenu,当我们把鼠标hover上去,下拉菜单出现时,触发mouseEnter事件,改变state,添加.menu-opened,但由于CSSTransition的出现,会自动添加.zoom-in-top-enter这个类,然后触发reflow,添加.zoom-in-top-enter-active,由于我们自定义了timeout,所以接下来会添加.zoom-in-top-enter-done
下拉菜单消失:一开始我们组件是.viking-submenu和.menu-opened,当我们把鼠标移开,state发生改变,.menu-opened被移除,.zoom-in-top-exit被添加进来,因为.menu-opened被移除,所以有display:none;
,因为.zoom-in-top-exit被添加,所以有opacity:1;transform:scaleY(1);
,此时元素已经消失了,在后面添加的任何动画效果都没有意义了,所以后面的动画效果都消失了。第一种方法是将display:none;
移到.zoom-in-top-exit-done中。第二种方法是脱离.menu-opened来控制子菜单栏的打开和关闭,我们应该从ReactTransitionGroup包裹的子节点来做文章,如果我们能让它里面包裹的节点一开始是不存在的,我们点击后,in属性值menuOpen切换到true时,节点才被动态添加进去,当鼠标移除时,当in属性值menuOpen切换到false时,我们删除节点,就完全不用display:none;
和display:block;
来控制。针对这一需求,ReactTransitionGroup给我们提供了unmountOnExit,unmountOnExit默认为false,当他为true时,被ReactTransitionGroup包裹的组件到达.zoom-in-top-exit-done时,组件会被卸载。当unmountOnExit为false,里面的组件不存在,in从false转到true时,组件会被动态添加到DOM节点中去。
据上述,我们先在src/components/Menu/_style.scss中:.viking-submenu删除display:none;
,.viking-submenu.menu-opened删除display:block;
然后使用unmountOnExit:
return (
<CSSTransition
in={menuOpen}
timeout={300}
classNames='zoom-in-top'
appear
unmountOnExit
>
<ul className={subMenuClasses}>
{childrenComponent}
</ul>
</CSSTransition>
)
上面的动画效果具有可复用性,这一节我们将上述动画封装成transition组件,希望transition组件省略一些参数,transition组件用以提供组件从无到有的动画效果,所以有些参数不用指定,只用加默认值,transition组件也会提供好几种动画效果,以字符串字面量的形式传入,但依然支持CSSTransition的参数。
我们在transition组件中设置了上下左右四个方向,我们将动画的css样式抽到一个mixin中,src/styles/_mixin.scss中新增代码如下:
@mixin zoom-animation(
$direction: 'top',
$scaleStart: scaleY(0),
$scaleEnd: scaleY(1),
$origin: center top,
) {
.zoom-in-#{$direction}-enter {
opacity: 0;
transform: $scaleStart;
}
.zoom-in-#{$direction}-enter-active {
opacity: 1;
transform: $scaleEnd;
transition: transform 300ms cubic-bezier(0.23, 1, 0.32, 1) 100ms, opacity 300ms cubic-bezier(0.23, 1, 0.32, 1) 100ms;
transform-origin: $origin
}
.zoom-in-#{$direction}-exit {
opacity: 1;
}
.zoom-in-#{$direction}-exit-active {
opacity: 0;
transform: $scaleStart;
transition: transform 300ms cubic-bezier(0.23, 1, 0.32, 1) 100ms, opacity 300ms cubic-bezier(0.23, 1, 0.32, 1) 100ms;
transform-origin: $origin;
}
}
src/styles/_animation.scss中代码改为:
@include zoom-animation('top',scaleY(0),scaleY(1),center top);
@include zoom-animation('left',scale(.45,.45),scale(1,1),center left);
@include zoom-animation('right',scale(.45,.45),scale(1,1),center right);
@include zoom-animation('bottom',scaleY(0),scaleY(1),center bottom);
这样transition组件的样式就设置好了,新建src/components/Transition/transition.tsx,开始封装transition组件
import React from 'react'
import {CSSTransition} from 'react-transition-group'
import {CSSTransitionProps} from 'react-transition-group/CSSTransition'
type AnimationName = 'zoom-in-top' | 'zoom-in-left' | 'zoom-in-right' | 'zoom-in-bottom'
interface TransitionProps extends CSSTransitionProps {
animation?: AnimationName
}
const Transition:React.FC<TransitionProps> = (props) => {
const {classNames, children, animation, ...restProps} = props
return (
<CSSTransition
classNames={classNames ? classNames : animation}
{...restProps}
>
{children}
</CSSTransition>
)
}
Transition.defaultProps = {
unmountOnExit: true,
appear: true
}
export default Transition
在subMenu.tsx中导入transition组件:import Transition from '../Transition/transition'
,下拉菜单栏renderChildren中返回的结果改为:
return (
<Transition
in={menuOpen}
timeout={300}
animation="zoom-in-top"
>
<ul className={subMenuClasses}>
{childrenComponent}
</ul>
</Transition>
)
但是如果下拉菜单栏中有我们自定义的button组件,button组件是没有动画的,这是因为我们给button组件也设置了transition属性,button自身的transition属性覆盖了动画的transition,为了解决这个问题,我们可以给transition组件新增一个wrapper属性,在展示transition组件时,如果有wrapper,则在children外层包裹一个div标签,若没有则直接展示children,因为transition属性不会继承,所以父子的transition属性没有任何关系。src/components/Transition/transition.tsx中代码如下:
import React from 'react'
import { CSSTransition } from 'react-transition-group'
import { CSSTransitionProps } from 'react-transition-group/CSSTransition'
type AnimationName = 'zoom-in-top' | 'zoom-in-left' | 'zoom-in-bottom' | 'zoom-in-right'
interface TransitionProps extends CSSTransitionProps {
animation?: AnimationName,
wrapper? : boolean,
}
const Transition: React.FC<TransitionProps> = (props) => {
const {
children,
classNames,
animation,
wrapper,
...restProps
} = props
return (
<CSSTransition
classNames = { classNames ? classNames : animation}
{...restProps}
>
{wrapper ? <div>{children}</div> : children}
</CSSTransition>
)
}
Transition.defaultProps = {
unmountOnExit: true,
appear: true,
}
export default Transition