标签: css 动画

  • 小教程:React 创建支持切换效果的导航组件

    小教程:React 创建支持切换效果的导航组件

    需求

    需求不复杂,如上图所示:

    1. 导航栏,里面有若干个链接按钮
    2. 点击切换的时候,会有一个高亮标记随着选中的按钮移动
    3. 按钮尺寸不确定,高亮标记的尺寸随着选中的按钮变化

    选择技术方案

    我的第一想法是 CSS 纯原生,这样使用起来会非常简单。但是考察之后,发现不太可行,难点在于:

    1. 高亮标记如果跟着按钮走,无法处理按钮间切换
    2. 如果放在父容器里,可以在按钮间切换,但是尺寸没法跟着按钮走

    所以短暂思考之后,我只能退而求其次,选择和框架结合的开发方式:高亮标记使用绝对定位来控制位置;当用户切换导航(点击按钮)的时候,使用 JS 获取目标按钮的位置和尺寸,并且赋给高亮标记;使用 CSS 变量控制动画,以提升代码的复用性,确保 CSS 规则不要太死硬。

    这个过程并不复杂,我们可以利用事件冒泡机制,获取被点击的按钮,然后利用 getBoundingClientRect() 计算坐标。

    CSS

    CSS 部分比较简单,我们用一个伪元素作为高亮标记,然后配合 CSS 变量移动它即可。唯一需要注意的是,伪元素要跟按钮重合,所以必须用到 z-index,按钮也必须手动设置成 position: relative

    (WP 代码高亮插件挂了,大家将就看……)

    .slide-navigator {
    --highlight-x: 0; /* 标记的 x 坐标 */
    --highlight-width: 0; /* 标记的宽度 */
    position: relative;

    &::before {
    content: '';
    position: absolute;
    z-index: 0;
    bottom: 0;
    left: var(--highlight-x);
    width: var(--highlight-width);
    background: #ffd850;
    transition: all 0.2s;
    }

    > * {
    position relative;
    }
    }

    这里暂时没考虑响应式,如果要切换横纵,可以增加一个 Y 坐标。如果需要定制样式,可以将高亮标记的样式抽出来单独处理。

    React JSX 组件

    作为组件,导航里面的按钮要跟着业务逻辑走,最好在业务组件里生成,然后通过 props.children 传进来。点击事件可以直接交由父容器,也就是本组件侦听,然后利用 event.target 来判断选中的是哪个按钮。

    这里还有一个抉择,即怎么确定高亮按钮是哪个。我考虑了一下,觉得将这个逻辑一并放在业务组件里比较好,然后通过 props.currentIndex 传进来使用。这样能确保本组件的复用性。坏处就是每个用到这个组件的地方都要写一个函数来判断,略嫌麻烦。

    最终组件代码如下:

    (WP 代码高亮插件挂了,大家将就看……)

    import {
    CSSProperties,
    FC,
    MouseEventHandler,
    useEffect,
    useRef,
    useState
    } from 'react';

    interface SlideHighlightProps {
    children: React.ReactNode;
    className: string;
    currentIndex: number;
    }

    type SlideNavigatorHighlight = CSSProperties & {
    '--highlight-x'?: string;
    '--highlight-width'?: string;
    };

    const SlideHighlight: FC<SlideHighlightProps> = function (props) {
    const { className, children, currentIndex } = props;
    const root = useRef<HTMLDivElement>(null);
    const [navStyle, setNavStyle] = useState<SlideNavigatorHighlight>();

    /* 处理按钮点击事件,找到被点击的按钮,并将其位置和宽度赋给高亮标记 */
    const onClick: MouseEventHandler<HTMLDivElement> = (event) => {
    if (!root.current) return;

    const target = Array.from(root.current.children).find((v) =>
    v.contains(event.target as Node)
    ) as HTMLElement;
    const { left } = root.current.getBoundingClientRect();
    const { left: l, width } = target.getBoundingClientRect();
    setNavStyle({
    '--highlight-x': `${l - left}px`,
    '--highlight-width': `${width}px`
    });
    };

    /* 组件初始化时,或者导航切换时,改变高亮标记的位置 */
    useEffect(() => {
    if (!root.current) return;

    /* 如果导航不应该高亮,就把标记隐藏起来 */
    if (currentIndex === -1) {
    setNavStyle({
    '--highlight-width': `0px`
    });
    return;
    }

    const { left } = root.current.getBoundingClientRect();
    const target = root.current.children[currentIndex] as HTMLElement;
    const { left: l, width } = target.getBoundingClientRect();
    setNavStyle({
    '--highlight-x': `${l - left}px`,
    '--highlight-width': `${width}px`
    });
    }, [currentIndex]);

    return (
    <div ref={root} className={'slide-navigator ' + className} style={navStyle} onClick={onClick}>
    {children}
    </div>
    );
    };

    export default SlideHighlight;

    总结

    我的 React 经验不太多,不考虑项目结构倒也问题不大,但是抽象组件的经验比较少,这次也是在 ChatGPT 帮助下才写好,所以赶紧趁热发一篇笔记。

    如果你对 React 经验比较丰富,发现我上面的代码有问题,欢迎指出。如果你对 React 开发有问题,对组件开发有疑问,也欢迎留言讨论。

  • 代码分享:翻转小动画

    代码分享:翻转小动画

    朋友对我图省事用的 animate.css flipInX 效果不满意,软磨硬泡非要我改成 wordle 原版那种整个翻过来的。于是我就想办法实现了一把,比想象的稍微复杂一些,难点在于我不知道 transform-style: preserve-3d 这个属性。没有这个属性,正反两个图层就被压在一个平面里,怎么翻转都是上面那个图层显示出来。

    我用来调试的代码实现请点 这里,为了方便调试,我这次没用 codepen,用的 Vue SFC Playground,效果挺好,尤其是适合不熟悉属性时一边试一边写。不知道 codepen 是否支持这种玩法。

    实现的代码与之类似:

    <template lang="pug">
    .game-item
      .face G
      .face G
    </template>
    
    <style lang="stylus">
    .game-item
      aspect-ratio 1
      position relative
      // 这个样式很重要,没有它就没有背面一说
      transform-style: preserve-3d;
      user-select none
    
    .face
      position absolute
      top 0
      left 0
      width 100%
      height 100%
      display flex
      justify-content center
      align-items center
    
      &:first-child
        // 把第一层稍微提高一点点,不能太多,不然旋转效果不好看
        transform translateZ(0.1px)
    
      &:last-child
        // 背面提前翻转 180 度,这样转过来才是对的
        transform rotateX(180deg)
    
    @keyframes flip
      from
        transform: perspective(400px) rotate3d(1, 0, 0, 0)
    
      to
        transform: perspective(400px) rotate3d(1, 0, 0, 180deg)
    
    .flip
      animation-name: flip
    </style>

    这种效果平时还是找时间写上一两次,CSS 属性是知识性内容,没办法凭经验推出来,平时得注意积累。