設(shè)計(jì)一種自適應(yīng)的React卡片列表組件

本文介紹一種用React實(shí)現(xiàn)的自適應(yīng)的卡片列表組件,該組件根據(jù)卡片的寬度與間隔自動適應(yīng)不同容器的寬度對卡片進(jìn)行排列甩牺。

接口設(shè)計(jì)

type PropertyType = number

/** 支持自適應(yīng)的大小接口 */
interface Size {
  xs?: PropertyType
  sm?: PropertyType
  md?: PropertyType
  lg?: PropertyType
  xl?: PropertyType
  xxl?: PropertyType
}

type SizeType = PropertyType | Size

interface ResponsiveGridProps<T> {
  /** 元素?cái)?shù)據(jù)列表 */
  items: T[]
  /** 卡片寬度 */
  size?: SizeType
  /** 列最小間隔 */
  columnGap?: SizeType
  /** 行間隔 */
  rowGap?: SizeType
  /** 自定義樣式類 */
  className?: string
  /** 
    * 是否支持彈性: 
    * 當(dāng)容器寬度大于所有列最小間隔和列寬時(shí)曹鸠,是否支持列寬同間隙一同放大 
    */
  flexible?: boolean
  /** 元素渲染器 */
  renderItem: (item: T) => React.ReactNode
}

實(shí)現(xiàn)邏輯

  1. 根據(jù)當(dāng)前瀏覽器情況計(jì)算出卡片和卡片間隔大小
function getSize(size: SizeType, screens: ScreenMap) {
  if (isNumber(size)) {
    return size as number
  }

  const currSize = size as Size

  if (!isNull(currSize.xxl)) {
    if (screens.xxl) {
      return currSize.xxl!
    }
  }

  if (!isNull(currSize.xl)) {
    if (screens.xxl || screens.xl) {
      return currSize.xl!
    }
  }

  if (!isNull(currSize.lg)) {
    if (screens.xxl || screens.xl || screens.lg) {
      return currSize.lg!
    }
  }

  if (!isNull(currSize.md)) {
    if (screens.xxl || screens.xl || screens.lg || screens.md) {
      return currSize.md!
    }
  }

  if (!isNull(currSize.sm)) {
    if (screens.xxl || screens.xl || screens.lg || screens.md || screens.sm) {
      return currSize.sm!
    }
  }

  return currSize.xs!
}

function ResponsiveGrid<T>({
  items,
  size = 280,
  columnGap = 24,
  rowGap = 24,
  className,
  renderItem,
  flexible = false
}: ResponsiveGridProps<T>) {
  // ...
  const screens = useBreakpoint()

  const columnSize = getSize(size, screens)
 
    // ....
    const columnGapSize = getSize(columnGap, screens)
    // ...
}
  1. 獲取容器大小
    此處使用的react-resize-detector來監(jiān)控容器的大小煌茬。
const [containerWidth, setContainerWidth] = useState<number>(0)

<ReactResizeDetector
        handleWidth
        onResize={(w: number) => setContainerWidth(w)}
      />
  1. 計(jì)算每行所能容納的卡片數(shù)量,并對卡片進(jìn)行分組
countOfRow = Math.floor(
      (containerWidth + columnGapSize) / (columnSize + columnGapSize)
    )
    rows = countOfRow >= 1 ? group(items, countOfRow) : [items]
  1. 如果支持flexible彻桃,重新計(jì)算寬度
if (flexible && countOfRow > 0) {
      width =
        (columnSize * containerWidth) /
        ((columnSize + columnGapSize) * countOfRow - columnGapSize)
    }
  1. 調(diào)用renderItem渲染卡片
<div className={classNames('fs-responsive-grid', className)}>
      <ReactResizeDetector
        handleWidth
        onResize={(w: number) => setContainerWidth(w)}
      />
      {rows.map((row, i) => (
        <div
          key={i}
          className="flex justify-between flex-wrap"
          style={{ marginTop: i === 0 ? 0 : rowGapSize }}
        >
          {row.map((item, j) => (
            <div key={j} style={{ width }}>
              {renderItem(item)}
            </div>
          ))}
        </div>
      ))}
    </div>
  1. 補(bǔ)充最后一行的空卡片元素
    由于本方法用flex的justify-content: space-between屬性平均分配卡片間隔坛善。因此對最后一行不足的情況,需要進(jìn)行補(bǔ)充。
{isLastRow(i) &&
            countOfRow > 2 &&
            new Array(countOfRow - 1)
              .fill(1)
              .map((_, k) => <div key={k} style={{ width }} />)}
  1. 樣式
.flex {
  display: flex;
}
.justify-between {
  justify-content: space-between;
}
.flex-wrap {
  flex-wrap: wrap;
}

完整代碼

// ResponsiveGrid.tsx
import React, { useState } from 'react'
import ReactResizeDetector from 'react-resize-detector'
import classNames from 'classnames'
import { group } from '@/utils/utils'
import { ScreenMap } from '@/utils/responsiveObserve'
import { isNumber, isNull } from '@/utils/types'
import useBreakpoint from '@/components/hooks/useBreakpoint'

type PropertyType = number

interface Size {
  xs?: PropertyType
  sm?: PropertyType
  md?: PropertyType
  lg?: PropertyType
  xl?: PropertyType
  xxl?: PropertyType
}

type SizeType = PropertyType | Size

interface ResponsiveGridProps<T> {
  items: T[]
  size?: SizeType
  columnGap?: SizeType
  rowGap?: SizeType
  className?: string
  flexible?: boolean
  renderItem: (item: T) => React.ReactNode
}

function getSize(size: SizeType, screens: ScreenMap) {
  if (isNumber(size)) {
    return size as number
  }

  const currSize = size as Size

  if (!isNull(currSize.xxl)) {
    if (screens.xxl) {
      return currSize.xxl!
    }
  }

  if (!isNull(currSize.xl)) {
    if (screens.xxl || screens.xl) {
      return currSize.xl!
    }
  }

  if (!isNull(currSize.lg)) {
    if (screens.xxl || screens.xl || screens.lg) {
      return currSize.lg!
    }
  }

  if (!isNull(currSize.md)) {
    if (screens.xxl || screens.xl || screens.lg || screens.md) {
      return currSize.md!
    }
  }

  if (!isNull(currSize.sm)) {
    if (screens.xxl || screens.xl || screens.lg || screens.md || screens.sm) {
      return currSize.sm!
    }
  }

  return currSize.xs!
}

function ResponsiveGrid<T>({
  items,
  size = 280,
  columnGap = 24,
  rowGap = 24,
  className,
  renderItem,
  flexible = false
}: ResponsiveGridProps<T>) {
  const [containerWidth, setContainerWidth] = useState<number>(0)
  const screens = useBreakpoint()

  const columnSize = getSize(size, screens)
  let width = columnSize
  let countOfRow = 0
  let rows: T[][] = []
  // console.log('containerWidth ==> ', containerWidth, width)
  if (containerWidth !== 0) {
    const columnGapSize = getSize(columnGap, screens)
    // console.log('columnGapSize, width ==>', columnGapSize, width)
    countOfRow = Math.floor(
      (containerWidth + columnGapSize) / (columnSize + columnGapSize)
    )
    rows = countOfRow > 1 ? group(items, countOfRow) : [items]

    if (flexible && countOfRow > 0) {
      width =
        (columnSize * containerWidth) /
        ((columnSize + columnGapSize) * countOfRow - columnGapSize)
    }
  }

  // console.log('rows ==> ', rows)
  const lastRow = rows.length - 1
  const isLastRow = (index: number) => index === lastRow
  const rowGapSize = getSize(rowGap, screens)
  return (
    <div className={classNames('fs-responsive-grid', className)}>
      <ReactResizeDetector
        handleWidth
        onResize={(w: number) => setContainerWidth(w)}
      />
      {rows.map((row, i) => (
        <div
          key={i}
          className={`flex flex-wrap ${countOfRow === 1 ? 'justify-center' : 'justify-between'}`}
          style={{ marginTop: i === 0 ? 0 : rowGapSize }}
        >
          {row.map((item, j) => (
            <div key={j} style={{ width }}>
              {renderItem(item)}
            </div>
          ))}
          {isLastRow(i) &&
            countOfRow > 2 &&
            new Array(countOfRow - 1)
              .fill(1)
              .map((_, k) => <div key={k} style={{ width }} />)}
        </div>
      ))}
    </div>
  )
}
export default ResponsiveGrid
/* index.less */
.flex {
  display: flex;
}
.justify-between {
  justify-content: space-between;
}
.flex-wrap {
  flex-wrap: wrap;
}
.justify-center {
  justify-content: center;
}
/* utils */
export const group = <T>(arr: T[], countOfPerGroup: number) => {
  const groups = []
  for (let i = 0; i < arr.length; i += countOfPerGroup) {
    groups.push(arr.slice(i, i + countOfPerGroup))
  }

  return groups
}

/**
 * 判斷值是否為數(shù)值
 * 
 * @param v 值
 */
export const isNumber = (v:any) => typeof v === 'number'

/**
 * 判斷值是否為空
 * 
 * @param v 
 */
export const isNull = (v: any) => v === undefined || v === null
*/

注意:useBreakpoint可以使用antd中的useBreakpoint代替眠屎。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末剔交,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子改衩,更是在濱河造成了極大的恐慌岖常,老刑警劉巖,帶你破解...
    沈念sama閱讀 221,548評論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件葫督,死亡現(xiàn)場離奇詭異竭鞍,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)橄镜,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,497評論 3 399
  • 文/潘曉璐 我一進(jìn)店門偎快,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人洽胶,你說我怎么就攤上這事晒夹。” “怎么了姊氓?”我有些...
    開封第一講書人閱讀 167,990評論 0 360
  • 文/不壞的土叔 我叫張陵丐怯,是天一觀的道長。 經(jīng)常有香客問我翔横,道長读跷,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 59,618評論 1 296
  • 正文 為了忘掉前任棕孙,我火速辦了婚禮舔亭,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘蟀俊。我一直安慰自己钦铺,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 68,618評論 6 397
  • 文/花漫 我一把揭開白布肢预。 她就那樣靜靜地躺著矛洞,像睡著了一般。 火紅的嫁衣襯著肌膚如雪烫映。 梳的紋絲不亂的頭發(fā)上沼本,一...
    開封第一講書人閱讀 52,246評論 1 308
  • 那天,我揣著相機(jī)與錄音锭沟,去河邊找鬼抽兆。 笑死,一個(gè)胖子當(dāng)著我的面吹牛族淮,可吹牛的內(nèi)容都是我干的辫红。 我是一名探鬼主播凭涂,決...
    沈念sama閱讀 40,819評論 3 421
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼贴妻!你這毒婦竟也來了切油?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,725評論 0 276
  • 序言:老撾萬榮一對情侶失蹤名惩,失蹤者是張志新(化名)和其女友劉穎澎胡,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體娩鹉,經(jīng)...
    沈念sama閱讀 46,268評論 1 320
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡攻谁,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,356評論 3 340
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了底循。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片巢株。...
    茶點(diǎn)故事閱讀 40,488評論 1 352
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡槐瑞,死狀恐怖熙涤,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情困檩,我是刑警寧澤祠挫,帶...
    沈念sama閱讀 36,181評論 5 350
  • 正文 年R本政府宣布,位于F島的核電站悼沿,受9級特大地震影響等舔,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜糟趾,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,862評論 3 333
  • 文/蒙蒙 一慌植、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧义郑,春花似錦蝶柿、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,331評論 0 24
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至劫笙,卻和暖如春芙扎,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背填大。 一陣腳步聲響...
    開封第一講書人閱讀 33,445評論 1 272
  • 我被黑心中介騙來泰國打工戒洼, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人允华。 一個(gè)月前我還...
    沈念sama閱讀 48,897評論 3 376
  • 正文 我出身青樓圈浇,卻偏偏與公主長得像敷矫,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個(gè)殘疾皇子汉额,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,500評論 2 359