最近公司開發方向偏向移動端, 因而就被調去作RN(react-native),體驗還不錯,當前有個需求是首頁中間吸頂的效果,雖然已經好久沒寫樣式了,不過這種常見樣式應該是so-easy,沒成想翻車了,網上搜索換了幾個方案都不行,最後去github上覆制封裝好的庫來實現,如今把翻車過程記錄下來。javascript
失敗
一開始的思路是這樣的,大衆思路,咱們須要監聽頁面的滾動狀態,當頁面滾動到要吸頂元素所處的位置的時候,咱們設置它爲固定定位,不過很遺憾, RN對於position屬性只提供了兩種佈局方式:absolute和relative,既沒有fixed也沒有仍處於試驗的api:sticky。尷尬了😅html
失敗
不過也不慌,看網上有第二種方案,把圖上第二 三塊地方做爲ScrollView
,而後ScrollView
滑動監聽距離,把第一塊的marginTop設爲負值,可是這樣第一部分不能滑動,不符合需求,pass前端
徹底失敗
從網上找到第三種方案,就是一二三部分做爲ScrollView
,java
第一部分position
設爲absolute
,剩下的不設置,默認是relative 第二部分(吸頂部分)marginTop設置(setState)爲第一部分高度的state, 添加滑動onScroll事件=》滑動距離y等於第二部分marginTop的state,可是當滑動超過第一部分高度的時候把第二部分(吸頂部分)position
設爲absolute
,並把其marginTop設爲0,看起來不錯,實際用ios模擬器一跑就無語了😅,效果很奇葩,手指滑動時不吸頂直接劃上去隱藏掉大半,一鬆忽然吸頂了。。。react
ios的系統,手指在屏幕上滾動時,onScroll一直在觸發,若是裏面有setState方法,也會不停執行並計算state
,可是改變react的state是異步的,只要手指不離開屏幕,改變的state就沒法生效(觸發界面渲染)ios
我最終意識到因爲ios的機制,react的state機制不能知足需求,RN裏面確定有藉助原生渲染的方式,因而github找了現成的代碼實現以後,反過來進行研究,你們有RN豐富經驗的也能夠直接看最下面代碼👇git
RN的Animator動畫庫旨在解決動畫問題,因爲js橋接過程,動畫一般不能很好展示,最好是把動畫的 數據 和 變化方法 一次性發給原生,由原生進行處理,這就是Animator庫的核心做用。github
記得原來RN的動畫一直被吐槽,不過如今效果還挺不錯的,可能與近年來手機硬件提高也愈來愈大也有關係吧。react-native
因爲Animator內部封裝了這四個組件,因此默承認以導出<Animator.View/>,<Animator.Text/>,<Animator.Image/>,<Animator.ScrollView/>api
在這幾個組件裏面想作一些動畫處理,數據方面也是react的state,可是賦值要給Animated.Value,以下👇
this.state = {
scrollY: new Animated.Value(0)
}
複製代碼
這裏雖然使用的仍是原生state,可是通過Animated處理,渲染機制徹底不同了
通過Animator包裝後的組件,會遍歷傳入的props和自身的state,查找是否有Animated.Value的實例,並綁定進相應的原生操做。 props和自身的state變化時,將Animated.Value值逐個轉化爲普通數值,再交給原生進行渲染,可是值得注意的是,這裏並不會觸發react 的 render,更不會有什麼domdiff ,是一種特殊處理,相似於Animated.Value改變時每次的shouldUpdateComponent返回都是false(毫秒級的渲染react性能扛不住),shouldUpdateComponent函數裏面判斷Animated.Value,而後會把數據變化發給原生組件
既然用了Animator組件了,渲染的問題解決了,下面思路是動態設置吸頂組件的translateY屬性。style:{ transform: [{ translateY:translateY }] }
下面利用插值來實現
const translateY = ScrollY.interpolate({
inputRange: [-1, 0, headerHeight, headerHeight + 1],
outputRange: [0, 0, 0, 1],
});
複製代碼
插值interpolate
略難理解,須要一點基礎,這裏再細提及來這篇文章就太長了 官網介紹, 若是還不懂能夠去網上找找這方面的資料
實現的圖中第二部分吸頂功能的核心代碼 下面重構成function hooks的模式
import * as React from 'react';
import { StyleSheet, Animated } from "react-native";
/** * 滑動吸頂效果組件 * @export * @class StickyHeader */
export default class StickyHeader extends React.Component{
static defaultProps = {
stickyHeaderY: -1,
stickyScrollY: new Animated.Value(0)
}
constructor(props) {
super(props);
this.state = {
stickyLayoutY: 0,
};
}
// 兼容代碼,防止沒有傳頭部高度
_onLayout = (event) => {
this.setState({
stickyLayoutY: event.nativeEvent.layout.y,
});
}
render() {
const { stickyHeaderY, stickyScrollY, children, style } = this.props
const { stickyLayoutY } = this.state
let y = stickyHeaderY != -1 ? stickyHeaderY : stickyLayoutY;
const translateY = stickyScrollY.interpolate({
inputRange: [-1, 0, y, y + 1],
outputRange: [0, 0, 0, 1],
});
return (
<Animated.View onLayout= { this._onLayout } style = { [ style, styles.container, { transform: [{ translateY }] } ]} > { children } </Animated.View> ) } } const styles = StyleSheet.create({ container: { zIndex: 100 }, }); 複製代碼
export default function StickyHeader(props: IStickyHeaderProps){
const [stickyLayoutY, setStickyLayoutY] = useState(0);
// 函數能夠提出去
const _onLayout = (event) => {
setStickyLayoutY(
event.nativeEvent.layout.y,
);
}
const { stickyHeaderY = -1, stickyScrollY = new Animated.Value(0), children, style } = props;
const y = stickyHeaderY != -1 ? stickyHeaderY : stickyLayoutY;
const translateY = stickyScrollY.interpolate({
inputRange: [-1, 0, y, y + 1],
outputRange: [0, 0, 0, 1],
});
return (
<Animated.View
onLayout= { _onLayout }
style = {
[
style,
{ zIndex: 100,transform: [{ translateY }] }
]}
>
{ children }
</Animated.View>
)
}
複製代碼
頁面裏實際用法以下
// 在頁面constructor裏聲明state
this.state = {
scrollY: new Animated.Value(0),
headHeight:-1
};
複製代碼
<Animated.ScrollView style={{ flex: 1 }} onScroll={ Animated.event( [{ nativeEvent: { contentOffset: { y: this.state.scrollY } } // 記錄滑動距離 }], { useNativeDriver: true }) // 使用原生動畫驅動 } scrollEventThrottle={1} >
<View onLayout={(e) => {
let { height } = e.nativeEvent.layout;
this.setState({ headHeight: height }); // 給頭部高度賦值
}}>
// 裏面放入第一部分組件
</View>
<StickyHeader stickyHeaderY={this.state.headHeight} // 把頭部高度傳入 stickyScrollY={this.state.scrollY} // 把滑動距離傳入 >
// 裏面放入第二部分組件
</StickyHeader>
// 這是第三部分的列表組件
<FlatList data={this.state.dataSource} renderItem={({item}) => this._createListItem(item)}
/>
</Animated.ScrollView>
複製代碼
具體代碼就是這樣實現了,算是比較完美的方案,特別是照顧了性能,各位能夠基於這個封裝來實現更復雜的需求,原理大概就是這個原理了,在前端動畫領域,本身確實也就剛入門水平,若有問題,請直接指出。
另外,這是我找的那個 組件 github的代碼地址:https://github.com/jiasongs/react-native-stickyheader,原地址附上,建議若是項目用了給人家一個star