本文手把手教你自定义一个丝滑的 TabBar,让你在面对同类需求时,游刃有余,作用如下:

如安在 React Native App 中,为 PagerView 自定义 TabBar

本文将运用到 React Native Anima动画片少儿小猪佩奇ted API,假如你不太熟悉,可能需求先了解下,这儿有elementary几篇不错的文章:

  • React Native 动画基础

  • 运用 React Native 动画进行插值

  • 动画和 React Native 翻滚视图

根本结构

咱们先来看看页面根本动画片汪汪队结构:

import PagerView from "react-native-pager-view"
const langs = ["JavaScript", "Golang", "Objective-C"]
function TabBarDemo() {
  return (
    <View style={styles.container}>
      <TabBar style={styles.tabbar} />
      <PagerView
        onPageScroll={onPageScroll}
        onPageSelected={onPageSelected}
        onPageScrollStateChanged={onPageScrollStateChanged}>
        {langs.map((lang) => (
          <LangPage key={lang} language={lang} />
        ))}
      </PagerView>
    </View>
  )
}

Pagerelements中文翻译View

PagerView 有三个比较重要的回调:

  • onPageScroll 当以手动或编程方法切换页面时,奉告当时地点页面以及页面偏移量。

  • onPageSelected 一旦 PagerViandroidstudio安装教程ew 完结了对所选页面的导航,会触发此回调,奉告当时地点页面。

  • onPageScrollelementuiStateChanged 奉告 PagerView 的翻滚状况,draggingsettlingandroid是什么系统 idle

以上这三个回调在 iOS 和 Andriod 渠道上的调用机遇略有差异,这动画片汪汪队使得咱们需求做一些适配作业。

LangPage

LangPage 便是一个个可切换的页面,它是非常简略的

// LangPage.tsx
export default function LangPage({ language }: LangPageProps) {
  return (
    <View style={styles.container}>
      <Text style={styles.text}>{language}</Text>
    </View>
  )
}
const styles = StyleSheet.create({
  container: {
    width: '100%',
    height: '100%',
    alignItems: 'center',
  },
})

这儿依据 react-native-pager-view Known IObjective-Cssues 的提示,将页面宽高都设置成 100%,但如同设置成 flex: 1 也没什么问题。

TabBar

TabBar 是本文的主角,它的初始代码如下:

// TabBar.tsx
import TabBarItem from "./TabBarItem"
export default function TabBar() {
  return (
    <View style={[styles.tabbar, style]}>
      {tabs.map((tab: string, index: number) => {
        return <TabBarItem key={tab} title={tab} />
      })}
    </View>
  )
}

它运reactnative停止维护用到的 TabBarItem 代码也很简略

TabBarItem 也称作 Tab,本文运用 Tab 指代 Tabandroid的drawable类BarItem。

// TabBarItem.tsx
export default function TabBarItem() {
  return (
    <Pressable onPress={onPress} style={[styles.tab, style]}>
      <Text style={[styles.label, labelStyle]}>{title}</Text>
    </Pressable>
  )
}

现在页面长下面这个姿态

如安在 React Native App 中,为 PagerView 自定义 TabBar

相关 TabBar 和 PagerView

PagerView 需求回调函数,而 TabBar 需求能够从 PagerView 的回调函数中获得数据。这是一个较为通用的逻辑Objective-C

咱们封装一个可复用的 React Hook,把 PagerView 和 TabBar 相相关的逻辑写在一同,并为它们供给各自所需的东西,譬如React+Native,为 PagerV动画片熊出没iew 供给回调,为 TabBar 供给数据。

现在它看起来比较简略,但随着本文的展开,它会变得完善起来。

// usePagerView.ts
import type { default as PagerView } from "react-native-pager-view"
export default function usePagerView() {
  const pagerRef = useRef<PagerView>(null)
  const setPage = useCallback((page: number, animated = true) => {
    if (animated) {
      pagerRef.current?.setPage(page)
    } else {
      pagerRef.current?.setPageWithoutAnimation(page)
    }
    console.log(time() + " setPage", page)
  }, [])
  return {
    pagerRef,
    setPage,
  }
}

usePagerView 供给了一个 refelementary翻译 来引证 PagerView,一个函数来以编程方法切换页面。

修正 TabBarDemo,将 pagerRef 传递给 PagerView,将 setPage 传递给 TabBar。

function TabBarDemo() {
  const { pagerRef, setPage } = usePagerView()
  return (
    <View style={styles.container}>
      <TabBar tabs={langs} onTabPress={setPage} />
      <PagerView ref={pagerRef}>
        {langs.map((lang) => (
          <LangPage key={lang} language={lang} />
        ))}
      </PagerView>
    </View>
  )
}

TabBar 在调用 setPage 时,动画大放映传入 Tab 的索引。

// TabBar.tsx
export default function TabBar({ onTabPress }) {
  return (
    <View style={[styles.tabbar, style]}>
      {tabs.map((tab: string, index: number) => {
        return (
          <TabBarItem key={tab} title={tab} onPress={() => onTabPress(index)} />
        )
      })}
    </View>
  )
}

现在点击 Tab 就会切换到相应的页面。

指示器

咱们需求一个指示器来表明当时正android下载安装处于哪个页面,哪个方位。它实质是对 PagerView 的 onPageScroll 事情的响应。

指示器的初始代码如下:

// TabBarIndicator.tsx
interface TabBarIndicatorProps {
  style: StyleProp<ViewStyle>
  scrollX: Animated.AnimatedInterpolation
}
export default function TabBarIndicator({ style, scrollX }: TabBarIndicatorProps) {
  return (
    <Animated.View
      key={"indicator"}
      style={[styles.indicator, style, { transform: [{ translateX: scrollX }] }]}
    />
  )
}
const styles = StyleSheet.create({
  indicator: {
    position: "absolute",
    left: 0,
    bottom: 0,
    width: 24,
    height: 4,
    backgroundColor: "#448AFF",
    borderRadius: 2,
  },
})

TabBarIobjective-c和c++的区别ndicator 的布局方法是肯定定位,然后依据 scandroid平板电脑价格rollX 的值来动态调整方位。scrollXandroidstudio安装教程 是个动画值,它映射到 onPageScroll 事情的 positionoandroid是什么系统ffset 特点。

供给动画值

修正 usePagerView动画头像,让动画片猫和老鼠它供给 onPageScroll 回调函数 和动画头像 positionoffset 这两个动画值。

// usePagerView.ts
export default function usePagerView() {
  const position = useRef(new Animated.Value(0)).current
  const offset = useRef(new Animated.Value(0)).current
  const onPageScroll = useMemo(
    () =>
      Animated.event<PagerViewOnPageScrollEventData>(
        [
          {
            nativeEvent: {
              position: position,
              offset: offset,
            },
          },
        ],
        {
          listener: ({ nativeEvent: { position, offset } }) => {
            console.log(time() + " onPageScroll", position, offset)
          },
          useNativeDriver: true,
        }
      ),
    [position, offset]
  )
  return {
    pagerRef,
    setPage,
    position,
    offset,
    onPageScroll,
  }
}

onPageScroll 没有像下面这样运用一般的函数来实现:

const onPageScroll = useCallback(
  (event: NativeSyntheticEvent<PagerViewOnPageScrollEventData>) => {
    position.setValue(event.nativeEvent.position)
    offset.setValue(event.nativeEvent.offset)
  },
  [offset, position]
)

而是运用了 Animated.event() 这个辅助函数,将动画值映射到事情值上。在这儿,会把 positionoffset 这两个动画值映射到 onPageScroll 事情的 positionoffs动画头像et 特点上。

因为设置了 useNati动画大放映veDriandroid平板电脑价格vertrue,这个映射背后并不会调用 positionoffelementaryset 动画值的 setVaObjective-Clue 方法,而是以一种优化的方法,在 UI 主线程动画电影完结绑定。这就使得动画是同步跟随手势的。

修正 TabBarDemo,将 onPageScroll 函数挂载objective-c和c++的区别到 PagerView 的 onPageScroll 事情钩子上,将 positionoffset 这两个动画值传递给 TaAndroidbBar。

因为 onPagelement滑板eScroll 运用了一个 Animated 事情,一般组件无法理解 Animated 相关 API,需求运用 Animated.cr动画片少儿小猪佩奇eateAnimatedComponent() 将 Pagandroidstudio安装教程erView 变成动画组objective-c和c++的区别件。

const AnimatedPagerView = Animated.createAnimatedComponent<any>(PagerView)
function TabBarDemo() {
  const { onPageScroll, position, offset } = usePagerView()
  return (
    <View style={styles.container}>
      <TabBar position={position} offset={offset} />
      <AnimatedPagerView onPageScroll={onPageScroll}>
        {langs.map((lang) => (
          <LangPage key={lang} language={lang} />
        ))}
      </AnimatedPagerView>
    </View>
  )
}

核算 scrollX

接下来便是困难的部分了。咱们需求在 TabBar 中,把 positionoffset 映射成 scrollX

// TabBar.tsx
const scrollX = Animated.add(position, offset).interpolate({
  inputRange,
  outputRange,
})

position 是个整数,表明当elements中文翻译时页面,它的取值规模便是 Tab 索引的调集。

offset 表明页面偏移量,它的取值规模是 [0, 1]

Animated.add(position, offset) 的取值规模是 [0, 最大 Tab 索引]

scrollX 是指示器在 x 轴的偏移量,由 interpolateandroid什么意思 映射得到。

interpolate 用于将指定输入规模的数据依照elementary某种曲线映射到指定输出规模的数据上。

首要确定输入规模 inputRange,其实便是 Tab 索引的调集。

// TabBar.tsx
const inputRange = useMemo(() => tabs.map((_, index) => index), [tabs])

接下来核算输出规模 outputRangandroid/harmonyose,其实便是指示器在 x 轴偏移量的调集,一个 Tab动画制作软件 索引对应一个 x 轴偏移量。

// 将每个 Tab 对应的指示器 x 轴偏移量都初始化为 0
const [outputRange, setOutputRange] = useState(inputRange.map(() => 0))
// 获取指示器的宽度,这个值一般是固定的
const indicatorWidth = getIndicatorWidth(indicatorStyle)
// 寄存每个 Tab 的方位和大小
const layouts = useRef<Layout[]>([]).current
const handleTabLayout = useCallback(
  (index: number, layout: Layout) => {
    layouts[index] = layout
    const length = layouts.filter((layout) => layout.width > 0).length
    if (length !== tabs.length) {
      // 等一切 Tab 都现已测量完结,再更新 `outputRange`
      return
    }
    const range: number[] = []
    for (let index = 0; index < length; index++) {
      const { x, width } = layouts[index]
      // 咱们期望指示器和所选 Tab 垂直居中对齐
      // 那么指示器的 x 轴偏移量便是 Tab 的 center.x - 指示器的 center.x
      const tabCenterX = x + width / 2
      const indicatorCenterX = indicatorWidth / 2
      range.push(tabCenterX - indicatorCenterX)
    }
    console.log("---------------onTabLayout-------------------")
    setOutputRange(range)
  },
  [tabs, layouts, indicatorWidth]
)

注册事情

把上面的 handleTabLayout 函数注册到 Tab 的 onLaandroid下载安装yout 事情钩子上。

export default function TabBar() {
  return (
    <View style={[styles.tabbar, style]}>
      {tabs.map((tab: string, index: number) => {
        return (
          <TabBarItem
            key={tab}
            title={tab}
            onLayout={(event) => handleTabLayout(index, event.nativeEvent.layout)}
          />
        )
      })}
      <TabBarIndicator
        style={[styles.indicator, indicatorStyle]}
        scrollX={scrollX}
      />
    </View>
  )
}

至此,指示器就能够正常作element是什么意思业了。

如安在 React Native App 中,为 PagerView 自定义 TabBar

Tab 的平滑过渡

假定咱们并不仅仅满足于指示器,咱们期望选中页面和未选中页面的 Tab 在款式上有所区别。

根本实现

首要,咱们需求让 TabBar 知道,动画片汪汪队选中的页面element是什么意思是哪个。

修正 usePagerVAndroidiew,供给 onPageSelected 回调函数和 page 状况变量动画电影

// usePagerView.tsx
export default function usePagerView(initialPage = 0) {
  const [activePage, setActivePage] = useState(initialPage)
  const setPage = useCallback((page: number, animated = true) => {
    if (animated) {
      pagerRef.current?.setPage(page)
    } else {
      pagerRef.current?.setPageWithoutAnimation(page)
    }
    console.log(time() + " setPage", page)
    setActivePage(page)
  }, [])
  const onPageSelected = useCallback((e: PagerViewOnPageSelectedEvent) => {
    console.log(time() + " onPageSelected", e.nativeEvent.position)
    setActivePage(e.nativeEvent.position)
  }, [])
  return {
    setPage,
    page: activePage,
    onPageSelected,
  }
}

上述代码中,为什么要在Element setPa动画制作软件ge 中也android什么意思调用 setActivePage 呢?

这首要是因为 onPageSelected 在两个渠道上的调用机遇不同。在 Android 渠道,elementary是什么意思onPageSelectedsetPage 之后立刻触发。而在 iOS 渠道,调用 setPageReact+Native 之后,要等到动画结束,onPageSelected 才会被调用。

setPage 中调用 setActivePage,一方面能让 TabBar 尽早知道当时选中的页面,另一方面也统了两个渠道的行为。

修正 TabBarDemo,将 onPageSelected 回调函数挂载到 PagerViewandroidstudio安装教程 的 onPageSelected 事情钩子上,将 page 状况变量传递给 TabBar。

function TabBarDemo() {
  const { page, onPageSelected } = usePagerView()
  return (
    <View style={styles.container}>
      <TabBar style={styles.tabbar} tabs={langs} page={page} />
      <AnimatedPagerView style={styles.pager} onPageSelected={onPageSelected}>
        {langs.map((lang) => (
          <LangPage key={lang} language={lang} />
        ))}
      </AnimatedPagerView>
    </View>
  )
}

修正 TabBar 代码,给选中的和未选中的 Tab 设置不同的款式。

export default function TabBar({ tabs, page }) {
  return (
    <View style={[styles.tabbar, style]}>
      {tabs.map((tab: string, index: number) => {
        const isActive = index === page
        const opacity = isActive ? 1 : 0.8
        const scale = isActive ? 1.2 : 1
        return (
          <TabBarItem
            key={tab}
            title={tab}
            style={[tabStyle, { marginLeft: index === 0 ? 0 : spacing }]}
            labelStyle={[labelStyle, { opacity, transform: [{ scale }] }]}
          />
        )
      })}
      <TabBarIndicator scrollX={scrollX} />
    </View>
  )
}

现在 Tab 的过渡作用有些僵硬,这是因为没有过渡动画。

如安在 React Native App 中,为 PagerView 自定义 TabBar

增加过渡动画

首要让 Tab 支持动画,运用 Animated.Text 替换Objective-C Text

export default function TabBarItem() {
  return (
    <Pressable style={[styles.tab, style]} onPress={onPress} onLayout={onLayout}>
      <Animated.Text style={[styles.label, labelStyle]}>{title}</Animated.Text>
    </Pressable>
  )
}

当时选中的 Tab 和前次选中的 Tab 都是需求设置过渡动画的Element

page 便是当elementanimation时选中的 Tab 的索引。那么android手机前次选中的 Tab 的索引便是 page 的上一个值,咱们需求在elementary是什么意思恰当的机遇把这个值记录动画头像下来。而这个机遇便是 PagerView 处于 idleelementui况时。

修正 usePagerView 代码,增加动画片猫和老鼠一个 is动画电影Idle 状况变量。

export default function usePagerView(initialPage = 0) {
  const [activePage, setActivePage] = useState(initialPage)
  const [isIdle, setIdle] = useState(true)
  const setPage = useCallback(
    (page: number, animated = true) => {
      console.log(time() + " setPage", page)
      setActivePage(page)
      if (activePage !== page) {
        setIdle(false)
      }
    },
    [activePage]
  )
  const onPageSelected = useCallback((e: PagerViewOnPageSelectedEvent) => {
    console.log(time() + " onPageSelected", e.nativeEvent.position)
    setActivePage(e.nativeEvent.position)
    if (Platform.OS === "ios") {
      setIdle(true)
    }
  }, [])
  const onPageScrollStateChanged = useCallback(
    ({ nativeEvent: { pageScrollState } }: PageScrollStateChangedNativeEvent) => {
      console.log(time() + " onPageScrollStateChanged", pageScrollState)
      setIdle(pageScrollState === "idle")
    },
    []
  )
  return {
    isIdle,
    onPageScrollStateChanged,
  }
}

当调用 setPage 切换页面时,咱们就认为 PagerView 处于非闲暇状况。

而设置为闲暇状况的机遇,则因为 Pagereactnative停止维护rView 的回调事情在不同渠道的触发机遇不同而不同。

  • 在 Android 渠道,onPagandroid手机eScroll动画片猫和老鼠StateChanged 事情会在页面切换动画完结后触发androidstudio安装教程。明显,这是设置为闲暇状React+Native况的最佳机遇。

  • 在 iOS 渠道,当经过编程方法,也便是调用 setPage 切换页面时,onPageScro动画专业llStelementary是什么意思ateChanged 事情并不会被触发。幸运的是,在页面切换动画完结后,onPageSelected 事情会被触发,因而 iOS 在这儿设置为闲暇状况。

  • 但在 Android 渠道,onPageSelected 事情在调用 setPage 之后,会当即触发,然后播放页面切换动画。明显不是设置为动画专业闲暇状况的机遇。

现在android什么意思咱们知道 PagerViandroid是什么系统ew 是否处于 idle 状况了,让咱们来记录 page 的上一个值。

export default function TabBar({ tabs, page, idle }) {
  const lastPage = useLastPage(page, idle)
}
function useLastPage(page: number, idle: boolean) {
  const lastPageRef = useRef(0)
  useEffect(() => {
    if (idle) {
      lastPageRef.current = page
    }
  }, [idle, page])
  return lastPageRef.current
}

有了 pagelastPage,能够开端编写过elements渡动画了。

// TabBar.tsx
export default function TabBar() {
  const lastPage = useLastPage(page, isIdle)
  return (
    <View style={[styles.tabbar, style]}>
      {tabs.map((tab: string, index: number) => {
        const enhanced = index === page || index === lastPage
        const scale = Animated.add(offset, position).interpolate({
          inputRange: [index - 1, index, index + 1],
          outputRange: [1, enhanced ? 1.2 : 1, 1],
          extrapolate: "clamp",
        })
        const opacity = Animated.add(offset, position).interpolate({
          inputRange: [index - 1, index, index + 1],
          outputRange: [0.8, enhanced ? 1 : 0.8, 0.8],
          extrapolate: "clamp",
        })
        return (
          <TabBarItem
            style={[tabStyle, { marginLeft: index === 0 ? 0 : spacing }]}
            // @ts-ignore
            labelStyle={[labelStyle, { opacity, transform: [{ scale }] }]}
          />
        )
      })}
      <TabBarIndicator scrollX={scrollX} />
    </View>
  )
}

动画原理

咱们讲解下如下要害代码是什么意思

const enhanced = index === page || index === lastPage
const scale = Animated.add(offset, position).interpolate({
  inputRange: [index - 1, index, index + 1],
  outputRange: [1, enhanced ? 1.2 : 1, 1],
  extrapolate: "clamp",
})

首要要明白,这段代码是在一个循动画片少儿小猪佩奇环里elements中文翻译的,每个 Tab 都有它自己的动画。

因而要确保每个 Tab 的动画都不会影响到动画片猫和老鼠其他的 Tab。

position 是个整数,表明当时页面,它的取值规模便是 Tab 索引的调集。

offset 表明页面偏移量,它的取值规模是 [0, 1]

Anandroid是什么手机牌子imated.ad动画d(offset, position) 的取值规模是 [0, 最大 Tab 索引]

inputRange: [index - 1, index, index + 1] 配合 extrapolate: "clamp" 运用,非常有意思。确保了每个 Tab 的动画都不会影响到其他的 Tab。elements

假定当时处于页面 1 lastPage === 1,用androidstudio安装教程户点击 Tab 往页面 2 page ==android什么意思= 2 移动。动画片熊出没

那么 poelementuisition 的值是 1,offsreactnative停止维护et 的值是 0.x,它俩的和是 1.x动画电影

1.x 射中了索引为 1 的 Tab 的 inputRange: [0, 1, 2] ,依据 outputRange: [1, 1.2, 1] 设定, Tab 的 scale 值介于 1.2 和动画制作软件 1 之间,趋向缩小。

1.x 射中了索引为 2 的 Tab 的 inputRange: [1, 2, 3] ,依据 outputRange: [1, 1.2, 1] 设定,Tab动画片熊出没 的 scelementary是什么意思ale 值介于 1 和 1.2 之间,趋向放大。

1.x 没有射中索引为 3 的 Tab 的 inputRange: [2, 3, 4] , 1.x 在 2 的左边,因为设置了 extrapolate: "clamp",Tab 的 scale 值为 outputRange: [1, 1, 1] 最左边的值,也便是 1。

处理 iOS 跳页问题

现在点击 Tab,Android 能够优雅地作业了,可是 iOS 还存在一些问题。

当点击相邻reactnative停止维护的 Tab 时,iOS 也能够正常地作业,可是当点击相邻较远android的drawable类的 Tab 时,前一个 Tab 没有过渡作用,显得比较突兀。如图,在 J动画电影avaScriptelements 和 Golang 之间切换,一切正常,但在 Javaelement是什么意思Script 和 Objective-C 之间切换,就有些问题了。

如安在 React Native App 中,为 PagerView 自定义 TabBar

查看 Android 的日志,从 JavaScript 切换到 Objective-C 时,position 会有比较好的过渡。

如安在 React Native App 中,为 PagerView 自定义 TabBar

可是在 iOS 渠道上,从 JavaScript 切换到 Objective-动画制作软件C 时,直接跳过了 position 为 0 的状况,导致没有了过渡作用。

如安在 React Native App 中,为 PagerView 自定义 TabBar

针对这一状况,把缺失的动画补回来便可。

// TabBar.tsx
const enhanced = index === page || index === lastPage
let scale =  Animated.add(...)
let opacity = Animated.add(...)
if (Platform.OS === "ios" && Math.abs(page - lastPage) > 1 && index === lastPage) {
  scale = Animated.add(offset, position).interpolate({
    inputRange: [page - 1, page, page + 1],
    outputRange: [1.2, 1, 1.2],
    extrapolate: "clamp",
  })
  opacity = Animated.add(offset, position).interpolate({
    inputRange: [page - 1, page, page + 1],
    outputRange: [1, 0.8, 1],
    extrapolate: "clamp",
  })
}

处理手动切换页面的问题

以上都是经过点击 Tandroid下载安装ab,也便是编程方法,来进android/harmonyos行页面切换,但假如是手动element滑板方法,也便是拖拽页面的方法呢?

PagerView 供给了 onPageScrollStateChanged 事情来告诉页面翻滚状况 (draggingsettlingidle),但在 iOS 和 Android 渠道上有所差异。iOS 只要在手动方法下,才会触发该事情,Android 在编程方法下也会触发该事情。幸运的是android平板电脑价格,Android 只要在手动方法下,才会发生 dragging 状况。

修正 usePagerView,露elementuiscrollreactnative停止维护State 状况。

// usePagerView.ts
export default function usePagerView(initialPage = 0) {
  const [scrollState, setScrollState] = useState<PageScrollState>("idle")
  const onPageScrollStateChanged = useCallback(
    ({ nativeEvent: { pageScrollState } }: PageScrollStateChangedNativeEvent) => {
      console.log(time() + " onPageScrollStateChanged", pageScrollState)
      setScrollState(pageScrollState)
      setIdle(pageScrollState === "idle")
    },
    []
  )
}
return {
  scrollState,
  onPageScrollStateChanged,
}

因为渠道差异,咱们先定义什么是手动中

  • 只要经过拖拽页面进入 dragging 状况,就被判定为手动中。

  • 关于 Android 渠道,只要进入 idle 状况,就被判定为非手动中。

  • 关于 iOS 渠道,动画电影在接收到 idle 状况时,只要上一个Objective-C状况是 settling,才答应判定为非手动中。此外,需求将 PageView 的 overdrag 特点设置为 true,以避免只要 settling 没有 idle 的状动画专业android平板电脑价格

// TabBar.tsx
function useInteractive(scrollState: "idle" | "dragging" | "settling") {
  const interactiveRef = useRef(false)
  const scrollStateRef = useRef<"idle" | "dragging" | "settling">(scrollState)
  useEffect(() => {
    scrollStateRef.current = scrollState
  }, [scrollState])
  if (scrollState === "dragging") {
    interactiveRef.current = true
  }
  if (
    scrollState === "idle" &&
    // 避免 overdrag 时,读到过期的 idle 回调信息
    (Platform.OS === "android" || scrollStateRef.current === "settling")
  ) {
    interactiveRef.current = false
  }
  return interactiveRef.current
}

修正 TabBarDemo,设置 PagerView 的 overdrag 特点为 true

function TabBarDemo() {
  return (
    <View style={styles.container}>
      <TabBar style={styles.tabbar} />
      <AnimatedPagerView
        ref={pagerRef}
        style={styles.pager}
        overdrag={true}
        overScrollMode="always">
        {langs.map((lang) => (
          <LangPage key={lang} language={lang} />
        ))}
      </AnimatedPagerView>
    </View>
  )
}

接下来,只需求稍稍改动下 TabBar,就能够在手动切换elementary是什么意思页面时,让 Tab 的款式平滑地过渡了。

export default function TabBar() {
  const lastPage = useLastPage(page, isIdle)
  const interactive = useInteractive(scrollState)
  return (
    <View style={[styles.tabbar, style]}>
      {tabs.map((tab: string, index: number) => {
        const enhanced = interactive || index === page || index === lastPage
        return (<TabBarItem />)
      })}
      <TabBarIndicator scrollX={scrollX} />
    </View>
  )
}

依据前面说到的动画原理,你能分分element是什么意思出,为什么拖拽时,只要相邻的两个 Tab 才会有动画作用吗?

实现翻滚作用

当时咱们的页面较少,Tab 的数量也少,但假如 Tab 的数量比较多呢?

现在去 TabBarDemo 增加多几门言语,结果发现放不android是什么手机牌子下了。

如安在 React Native App 中,为 PagerView 自定义 TabBar

解决办法便是让 Tabelement滑板Bar 翻滚起来,能够在 TabBar 里边放一个 ScrollView,但也能够把 Taandroid是什么手机牌子bBar 放在 ScrollView 里边。

作者的选择是把 TabBar 放在 ScrollView 里边,因为这样复用性、组合性、维护性都比较好。

现在封装一个叫 ScrollBar 的组件,其结构如下:

export default function ScrollBar({ style, page, children }) {
  return (
    <ScrollView
      ref={scrollRef}
      style={[styles.scrollbar, style]}
      horizontal
      onContentSizeChange={onContentSizeChange}
      onLayout={onLayout}
      bounces={true}
      showsHorizontalScrollIndicator={false}
      {...props}>
      {React.cloneElement(children as any, { onTabsLayout })}
    </ScrollView>
  )
}

获取布局信息

上面代码,有三个回调,经过这三个回调,咱们获得了所需求的布局信息,然后就能够核算出 ScrollView 应该翻滚到的方位。

  • 经过 onContentS动画制作软件izeChange 回调获得 TabBar 的宽度。

  • 经过 onelement是什么意思Layout 回调elementui获得 ScrollBar 的宽度,通常和屏幕宽度共同。

  • 上面的 chilandroid什么意思dren 便是 TabBar,onTabsandroid的drawable类Layout 用来获取 Tab 的方位和大小,咱们在前面的指示器的实现中现已获element翻译得过了。

略微修正下 TabBar,让它承受动画片少儿小猪佩奇 onTabsLa动画片少儿小猪佩奇yout 参数

export default function TabBar({ onTabsLayout }: TabBarProps) {
  const layouts = useRef<Layout[]>([]).current
  const handleTabLayout = useCallback(
    (index: number, layout: Layout) => {
      layouts[index] = layout
      console.log("---------------onTabLayout-------------------")
      setOutputRange(range)
      onTabsLayout?.(layouts)
    },
    [onTabsLayout, tabs, layouts, indicatorWidth]
  )
}

改动不大,对吧。

核算翻滚距离

咱们尽可能让选中的 Tab 居中显现,为了满足这个需求,需求核算出 ScrollView 应该翻滚到哪。

以下是核算步骤:

useEffect(() => {
  if (tabLayouts.length - 1 < page || contentWidth === 0 || scrollBarWidth === 0) {
    return
  }
  // 获得选中的 Tab 布局数据
  const tabLayout = tabLayouts[page]
  // 核算 Tab 中心到 ScrollBar 中心的 x 轴距离
  const dx = tabLayout.x + tabLayout.width / 2 - scrollBarWidth / 2
  // 核算出 ScrollView 的最大可翻滚距离,ScrollView 的可翻滚规模是 [0, maxScrollX]
  const maxScrollX = contentWidth - scrollBarWidth
  // 核算出 ScrollView 应该翻滚到的 x 坐标,它有必要大于等于 0 而且小于等于 maxScrollX
  const x = Math.min(Math.max(0, dx), maxScrollX)
  scrollRef.current?.scrollTo({ x })
}, [page, tabLayouts, contentWidth, scrollBarWidth])

组合

运用方法也比较简略,把 TabBar 嵌套在 ScrollBar 里边即可,别忘了调整 TabBaobjective-c和c++的区别r 的款式特点,让它的高度等同于 ScrollBaandroid手机r 的高度。在给 ScrollBar 指定高度时,需求设置 flexGrow: 0,不然它会尽可能占满屏幕,这是因为 ScrollAndroidView 有一个androidstudio安装教程默许的 flex: 1 款式特点。

function TabBarDemo() {
  return (
    <View style={styles.container}>
      <ScrollBar style={styles.scrollbar} page={page}>
        <TabBar style={styles.tabbar} />
      </ScrollBar>
      <AnimatedPagerView ref={pagerRef} style={styles.pager}>
        {langs.map((lang) => (
          <LangPage key={lang} language={lang} />
        ))}
      </AnimatedPagerView>
    </View>
  )
}
const styles = StyleSheet.create({
  scrollbar: {
    height: 48,
    flexGrow: 0,
  },
  tabbar: {
    height: "100%",
  },
})

示例

这儿有一个示例,供你参阅。