Skip to content

Header 动画

第一章:不同页面 fixed 切换

通过观察页面,Home 与 entire 页面的 header 是固定定位,而 detail 页面不是固定定位。简单实现是给每个页面单独写一个组件,不共用。这里采用共用 header 组件。

为了能动态改变定位模式,把这部分信息存到 redux 中,header 组件在一加载的时候从 store 中取数据,看是否要固定定位。拿到数据后通过 css in js (props) 把信息传到 css 中。

一、数据存到 redux

1. 新建 store 模块

@\store\features\main.js

js
import { createSlice } from "@reduxjs/toolkit"


const mainSlice = createSlice({
  name: "main",
  initialState: {
    headerConfig: {
      isFixed: false,
    }
  },
  reducers: {
    changeHeaderConfigAction(state, { payload }) {
      state.headerConfig = payload
    }
  }
})

export const { changeHeaderConfigAction } = mainSlice.actions
export default mainSlice.reducer

2. 引入 store

@\store\index.js

js
import { configureStore } from "@reduxjs/toolkit"
import mainReducer from "./features/main"

const store = configureStore({
  reducer: {
    // ......
    main: mainReducer,
  },
})

export default store

二、分发配置

在 Home、entire、detail 页面分发 css 配置。

jsx
/* 以 Home 组件为例,其他无非是把 isFixed: true 改为 false */
const dispatch = useDispatch()
useEffect(() => {
  dispatch(changeHeaderConfigAction({ isFixed: true }))
}, [dispatch])

特别注意,@\index.js 中 Suspense 标签与 Provider 标签顺序。一定要 Provider 标签在外,Suspense 标签在里面。为什么?

@\components\app-header\index.jsx 中,使用 useSelector 从 store 中拿到数据,进而取出 isFixed,打印 isFixed 值。

jsx
/* 从redux中获取数据 */
const { headerConfig } = useSelector((state) => ({
  headerConfig: state.main.headerConfig
}), shallowEqual)
const { isFixed } = headerConfig
console.log(isFixed)

当进入 Home、entire、detail 页面,会分发 action,但是 header 组件的 isFixed 打印语句没有打印(store 中的数据改变了)。

因为组件是异步加载的,subscribe 不监听异步组件中 action 的。解决办法跟简单,Suspense 被 Provider 包裹即可。

jsx
<Provider store={store}>
  <Suspense fallback={<div>Loading...</div>}>
    <BrowserRouter>
        <ThemeProvider theme={theme}>
          <App />
        </ThemeProvider>
    </BrowserRouter>
  </Suspense>
</Provider>

说明

今后开发中,必须 <Provider store={store}> 标签置为最外层。

三、header 使用配置

@\components\app-header\index.jsx

jsx
<HeaderWrapper className={classNames({fixed: isFixed})}>
  {/* ...... */}
</HeaderWrapper>

@\components\app-header\style.jsx

jsx
&.fixed {
  position: fixed;
  z-index: 99;
  top: 0;
  left: 0;
  right: 0;
}

第二章:页面切换滚动到顶部

1)编写 hooks

@\hooks\useScrollTop.js

js
import { useEffect } from "react"
import { useLocation } from "react-router-dom"


export default function useScrollTop() {
  const location = useLocation()
  useEffect(() => {
    window.scrollTo(0, 0)
  }, [location.pathname])
}

2)使用 hook,在 @\App.js 文件中

js
import useScrollTop from './hooks/useScrollTop'

useScrollTop()

第三章:搜索状态下的布局

一、留出布局位置

@\components\app-header\index.jsx

jsx
<HeaderWrapper className={classNames({ fixed: isFixed })}>
  <div className='content'>
    <div className='top'>
      <HeaderLeft />
      <HeaderCenter />
      <HeaderRight />
    </div>
    <div className="search-area"></div>
  </div>
  <div className='cover'></div>
</HeaderWrapper>

二、封装选项卡组件

@\components\app-header\c-cpns\header-center\c-cpns 下面封装 SearchSections 与 SearchTabs 组件。

三、使用组件

@\components\app-header\c-cpns\header-center\index.jsx

jsx
const [tabIndex, setTabIndex] = useState(0)
const titles = SearchTitles.map(item => item.title)

<CenterWrapper>
  {/* <div className='search-bar'>
    <div className='text'>搜索房源和体验</div>
    <div className='icon'>
      <IconSearchBar />
    </div>
  </div> */}
  <div className='search-detail'>
    <SearchTabs titles={titles} />
    <div className='infos'>
      <SearchSections searchInfos={SearchTitles[tabIndex].searchInfos} />
    </div>
  </div>
</CenterWrapper>

第四章:搜索状态的切换动画效果

什么时候搜索状态显示或隐藏?单击下面的控件来切换。

一、SearchArea 封装为组件

SearchArea 区域要隐藏或展示,当单击【搜索房源和体验】的搜索框时。还要动画。所以封装为组件。

@\components\app-header\style.jsx

jsx
export const SearchAreaWrapper = styled.div`
  transition: height 250ms ease;
  height: ${props => props.isSearch ? "100px": "0"};
`

@\components\app-header\index.jsx

jsx
/* 定义组件内部的状态 */
const [isSearch, setIsSearch] = useState(false)

/* 为组件添加事件与传入状态 */
<HeaderCenter isSearch={isSearch} searchBarClick={e => setIsSearch(true)} />
<SearchAreaWrapper isSearch={isSearch}/>
{ isSearch && <div className='cover' onClick={e => setIsSearch(false)}></div> }

二、HeaderCenter 编写逻辑

1)isSearch 控制【搜索房源和体验】和【搜索状态】的显示隐藏。

jsx
const { isSearch } = props

<CenterWrapper>
    {!isSearch ? (
        {/* 搜索房源和体验 */}
      ) : (
        {/* 搜索状态 */}
      )}
</CenterWrapper>

2)给【搜索房源和体验】绑定单击事件,来改变 isSearch 的值。

@\components\app-header\c-cpns\header-center\index.jsx

jsx
const { searchBarClick } = props

function searchBarClickHandle() {
  if (searchBarClick) searchBarClick()
}

<div className='search-bar' onClick={searchBarClickHandle}>
    <div className='text'>搜索房源和体验</div>
    <div className='icon'>
        <IconSearchBar />
    </div>
</div>

三、添加动画效果

使用 CSSTransition 来实现。

第五章:监听滚动滚动效果的消失

1)@\hooks\useScrollPosition.js

js
import { useEffect, useState } from 'react'
import { throttle } from 'underscore'


export default function useScrollPosition () {
  // 状态来记录位置
  const [scrollX, setScrollX] = useState(0)
  const [scrollY, setScrollY] = useState(0)

  // 监听window滚动
  useEffect(() => {
    const handleScroll = throttle(function () {
      setScrollX(window.scrollX)
      setScrollY(window.scrollY)
    }, 100)

    window.addEventListener('scroll', handleScroll)
    return () => {
      window.removeEventListener('scroll', handleScroll)
    }
  }, [])

  // 返回
  return { scrollX, scrollY }
}

2)只要【搜索状态】没激活,就一直记录滚动的位置。当【搜索状态】激活时,判断新的滚动位置与记录的滚动位置是否大于 30 px,如果大于,就隐藏【搜索状态】。

@\components\app-header\index.jsx

jsx
/* 监听滚动的监听 */
const { scrollY } = useScrollPosition()
const prevY = useRef(0)
// 在正常情况的情况下 (搜索框没有弹出来) , 不断记录值
if (!isSearch) prevY.current = scrollY
// 在弹出搜索功能的情况, 滚动的距离大于之前记录的距离的 30
if (isSearch && Math.abs(scrollY - prevY.current) > 30) setIsSearch(false)

第六章:首页的顶部透明度的效果

1)顶部透明这个也要用 store 控制,每个使用 header 的组件在一加载的时候就派发配置。

topAlpha 这个状态在 home、detail、entire 页面都用得到,因为 header 组件是共用的。所以,topAlpha 添加到 store 中。

@\store\features\main.js

js
const mainSlice = createSlice({
  name: "main",
  initialState: {
    headerConfig: {
      isFixed: false,
      topAlpha: false,
    }
  },
  reducers: { /* ...... */ }
})

2)home、entire、detail 页面一加载就派发 action。以 home 页面为例。

jsx
const dispatch = useDispatch()
useEffect(() => {
  dispatch(fetchHomeDataAction())
  dispatch(changeHeaderConfigAction({ isFixed: true, topAlpha: true }))
}, [dispatch])

3)在 header 中,根据派发的配置,判断是否需要透明(只有当页面在顶部且派发的配置是透明才背景透明)。

@\components\app-header\index.jsx

jsx
const { topAlpha } = headerConfig

/* 透明度的逻辑 */
const isAlpha = topAlpha && scrollY === 0

<ThemeProvider theme={{ isAlpha }}>
  <HeaderWrapper className={classNames({ fixed: isFixed })}>
    <div className='content'>
      <div className='top'>
        <HeaderLeft />
        <HeaderCenter
          isSearch={isAlpha || isSearch}
          searchBarClick={e => setIsSearch(true)}
        />
        <HeaderRight />
      </div>
      <SearchAreaWrapper isSearch={isAlpha || isSearch} />
    </div>
    {isSearch && (
      <div className='cover' onClick={e => setIsSearch(false)}></div>
    )}
  </HeaderWrapper>
</ThemeProvider>

为什么要用 ThemeProvider 标签?因为不光 header 子组件需要使用 isAlpha,CSS 中也需要根据 isAlpha 判断字体、背景等颜色。干脆直接了当,使用一个主题,这样在 header 后代组件都可以使用啦。

Released under the MIT License.