Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feat intersection observer #1649

Merged
merged 16 commits into from
Nov 1, 2024
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
import { ENV_OBJ } from '../../../common/js'

function createIntersectionObserver (component, options = {}) {
if (options.observeAll) {
options.selectAll = options.observeAll
}
return ENV_OBJ.createIntersectionObserver(options)
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import IntersectionObserver from './rnIntersectionObserver'

function createIntersectionObserver (comp, opt, config) {
return new IntersectionObserver(comp, opt, config)
}

export {
createIntersectionObserver
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,202 @@
import { isArray, noop } from "@mpxjs/utils"
import throttle from 'lodash/throttle'
import { Dimensions } from 'react-native'
import { getFocusedNavigation } from '../../../common/js'


class RNIntersectionObserver {
constructor (component, options, intersectionCtx) {
this.component = component
this.options = options
this.thresholds = options.thresholds.sort((a,b ) => a-b) || [0]
this.initialRatio = options.initialRatio || 0
this.observeAll = options.observeAll || false

// 组件上挂载对应的observers,用于在组件销毁的时候进行批量disconnect
this.component._intersectionObservers = this.component.__intersectionObservers || []
this.component._intersectionObservers.push(this)

this.observerRef = null
this.relativeRef = null
this.margins = {top: 0, bottom: 0, left: 0, right: 0}
this.callback = noop

this.throttleMeasure = this.getThrottleMeasure(options.throttleTime || 100)

// 记录上一次相交的比例
this.previousIntersectionRatio = []

// 添加实例添加到上下文中,滚动组件可以获取到上下文内的实例从而触发滚动
if (intersectionCtx && Array.isArray(intersectionCtx) && !intersectionCtx.includes(this)) {
thuman marked this conversation as resolved.
Show resolved Hide resolved
intersectionCtx.push(this)
this.intersectionCtx = intersectionCtx
}

return this
}
relativeTo (selector, margins) {
thuman marked this conversation as resolved.
Show resolved Hide resolved
const relativeRef = this.component.__selectRef(selector, 'node')
if (isArray(relativeRef)) {
this.relativeRef = relativeRef[0]
} else {
this.relativeRef = relativeRef
}
if (relativeRef) {
this.relativeRef = relativeRef
this.margins = margins || this.margins
}
return this
}
relativeToViewport(margins) {
thuman marked this conversation as resolved.
Show resolved Hide resolved
this.relativeRef = null
thuman marked this conversation as resolved.
Show resolved Hide resolved
this.margins = margins || this.margins
return this
}
observe (selector, callback) {
if (this.observerRef) {
console.error('"observe" call can be only called once in IntersectionObserver')
return
}
let targetRef = null
if (this.observeAll){
targetRef = this.component.__selectRef(selector, 'node', true)
} else {
targetRef = this.component.__selectRef(selector, 'node')
}
if (!targetRef || targetRef.length === 0) {
console.error('intersection observer target not found')
return
}
this.observerRef = isArray(targetRef) ? targetRef : [targetRef]
this.callback = callback
this._measureTarget(true)
}
_getWindowRect() {
if (this.windowRect) return this.windowRect
const navigation = getFocusedNavigation()
const screen = Dimensions.get('screen')
const windowRect = {
top: navigation.isCustomHeader ? this.margins.top : navigation.headerHeight,
left: this.margins.left,
right: screen.width - this.margins.right,
bottom: navigation.layout.height + navigation.headerHeight - this.margins.bottom
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

测试一下vh的计算逻辑

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

vh计算逻辑不对,需要改一下

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

vh 使用navigation.layout 因为是异步在初始计算的时候还是会不符合预期,后续才是对的

}
this.windowRect = windowRect
return this.windowRect
}
_getReferenceRect(targetRef) {
if (!targetRef) {
thuman marked this conversation as resolved.
Show resolved Hide resolved
return Promise.resolve([this._getWindowRect()])
} else {
if (!isArray(targetRef)) targetRef = [targetRef]
const targetPromiseQueue = []
targetRef.forEach((targetRefItem) => {
const target = targetRefItem.getNodeInstance().nodeRef.current
if (!target) console.error('intersection observer target ref not found')
thuman marked this conversation as resolved.
Show resolved Hide resolved
if (target) targetPromiseQueue.push(new Promise((resolve) => {
target.measureInWindow(
(x, y, width, height) => {
const boundingClientRect = {
left: x,
top: y,
right: x + width,
bottom: y + height,
width: width,
height: height
};
resolve(boundingClientRect)
},
);
}))
})
return Promise.all(targetPromiseQueue)
}
}
// 计算相交区域
_measureIntersection(targetRect, relativeRect) {
function restrictValueInRange(start = 0, end = 0, value = 0) {
return Math.min(Math.max(start, value), end);
}

const visibleRect = {
left: restrictValueInRange(relativeRect.left, relativeRect.right, targetRect.left),
top: restrictValueInRange(relativeRect.top, relativeRect.bottom, targetRect.top),
right: restrictValueInRange(relativeRect.left, relativeRect.right, targetRect.right),
bottom: restrictValueInRange(relativeRect.top, relativeRect.bottom, targetRect.bottom),
}

const targetArea =(targetRect.bottom - targetRect.top) * (targetRect.right - targetRect.left);
const visibleArea = (visibleRect.bottom - visibleRect.top) * (visibleRect.right - visibleRect.left);

return {
intersectionRatio: targetArea ? visibleArea / targetArea : 0,
intersectionRect: {
top: visibleRect.top,
bottom: relativeRect.bottom,
left: visibleRect.left,
right: relativeRect.right,
}
}
}
getThrottleMeasure(throttleTime) {
return throttle(() => {
this._measureTarget()
}, throttleTime)
}
// 计算节点的rect信息
_measureTarget(isInit = false) {
Promise.all([
this._getReferenceRect(this.observerRef),
this._getReferenceRect(this.relativeRef)
]).then(([observeRects, relativeRects]) => {
observeRects.forEach((observeRect, index) => {
const { intersectionRatio, intersectionRect } = this._measureIntersection(observeRect, relativeRects[0])
thuman marked this conversation as resolved.
Show resolved Hide resolved
const isCallback = isInit ? intersectionRatio >= this.initialRatio : this._isInsected(intersectionRatio, this.previousIntersectionRatio[index])
// 初次调用的
if (isCallback) {
this.callback({
index: index,
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

rm

id: this.observerRef[index].getNodeInstance().props?.current?.id,
dataset: this.observerRef[index].getNodeInstance().props?.current?.dataset || {},
intersectionRatio: Math.round(intersectionRatio * 100) / 100,
intersectionRect,
boundingClientRect: observeRect,
relativeRect: relativeRects[0],
time: Date.now()
})
}
this.previousIntersectionRatio[index] = intersectionRatio
})
}).catch((e) => {
console.log('_measureTarget fail', e)
})
}

// 如果上一个与当前这个处于同一个thresholds区间的话,则不用触发
_isInsected = (intersectionRatio, previousIntersectionRatio) => {
thuman marked this conversation as resolved.
Show resolved Hide resolved
// console.log('nowintersectionRatio, previousIntersectionRatio', [intersectionRatio, previousIntersectionRatio])
let nowIndex = -1
let previousIndex = -1
this.thresholds.forEach((item, index) => {
if (intersectionRatio >= item) {
nowIndex = index
}
if (previousIntersectionRatio >= item) {
previousIndex = index
}
})
return !(nowIndex === previousIndex)
};

relativeToViewport (margins) {
thuman marked this conversation as resolved.
Show resolved Hide resolved
this._relativeInfo = { relativeRef: null, margins }
return this
}
disconnect () {
if (this.intersectionCtx && this.intersectionCtx.includes(this)) {
thuman marked this conversation as resolved.
Show resolved Hide resolved
this.intersectionCtx.splice(this.intersectionCtx.indexOf(this, 1))
}
}
}

export default RNIntersectionObserver
1 change: 1 addition & 0 deletions packages/core/src/core/proxy.js
Original file line number Diff line number Diff line change
Expand Up @@ -208,6 +208,7 @@ export default class MpxProxy {
if (this.update) this.update.active = false
this.callHook(UNMOUNTED)
this.state = UNMOUNTED
this._intersectionObservers?.forEach(observer => observer.disconnect())
}

isUnmounted () {
Expand Down
3 changes: 1 addition & 2 deletions packages/core/src/platform/export/index.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@

export {
watchEffect,
watchSyncEffect,
Expand Down Expand Up @@ -42,4 +41,4 @@ export {

export {
useI18n
} from '../../platform/builtInMixins/i18nMixin'
} from '../../platform/builtInMixins/i18nMixin'
33 changes: 21 additions & 12 deletions packages/core/src/platform/patch/react/getDefaultOptions.ios.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@ import MpxProxy from '../../../core/proxy'
import { BEFOREUPDATE, ONLOAD, UPDATED, ONSHOW, ONHIDE, ONRESIZE, REACTHOOKSEXEC } from '../../../core/innerLifecycle'
import mergeOptions from '../../../core/mergeOptions'
import { queueJob } from '../../../observer/scheduler'
import { createSelectorQuery } from '@mpxjs/api-proxy'
import { createSelectorQuery, createIntersectionObserver } from '@mpxjs/api-proxy'
import { IntersectionObserverContext } from '@mpxjs/webpack-plugin/lib/runtime/components/react/dist/context'

function getSystemInfo () {
const window = ReactNative.Dimensions.get('window')
Expand Down Expand Up @@ -63,7 +64,7 @@ function getRootProps (props) {
return rootProps
}

function createInstance ({ propsRef, type, rawOptions, currentInject, validProps, components, pageId }) {
function createInstance ({ propsRef, type, rawOptions, currentInject, validProps, components, pageId, intersectionCtx }) {
const instance = Object.create({
setData (data, callback) {
return this.__mpxProxy.forceUpdate(data, { sync: true }, callback)
Expand Down Expand Up @@ -178,8 +179,8 @@ function createInstance ({ propsRef, type, rawOptions, currentInject, validProps
createSelectorQuery () {
return createSelectorQuery().in(this)
},
createIntersectionObserver () {
error('createIntersectionObserver is not supported in react native, please use ref instead')
createIntersectionObserver (opt) {
return createIntersectionObserver(this, opt, intersectionCtx)
},
...rawOptions.methods
}, {
Expand Down Expand Up @@ -344,12 +345,13 @@ export function getDefaultOptions ({ type, rawOptions = {}, currentInject }) {
const defaultOptions = memo(forwardRef((props, ref) => {
const instanceRef = useRef(null)
const propsRef = useRef(null)
const intersectionCtx = useContext(IntersectionObserverContext)
const pageId = useContext(RouteContext)
propsRef.current = props
let isFirst = false
if (!instanceRef.current) {
isFirst = true
instanceRef.current = createInstance({ propsRef, type, rawOptions, currentInject, validProps, components, pageId })
instanceRef.current = createInstance({ propsRef, type, rawOptions, currentInject, validProps, components, pageId, intersectionCtx })
}
const instance = instanceRef.current
useImperativeHandle(ref, () => {
Expand Down Expand Up @@ -409,10 +411,11 @@ export function getDefaultOptions ({ type, rawOptions = {}, currentInject }) {
}))

if (type === 'page') {
const { Provider, useSafeAreaInsets, GestureHandlerRootView } = global.__navigationHelper
const { Provider, useSafeAreaInsets, GestureHandlerRootView, useHeaderHeight } = global.__navigationHelper
const pageConfig = Object.assign({}, global.__mpxPageConfig, currentInject.pageConfig)
const Page = ({ navigation, route }) => {
const currentPageId = useMemo(() => ++pageId, [])
const intersectionObservers = useRef([])
thuman marked this conversation as resolved.
Show resolved Hide resolved
usePageStatus(navigation, currentPageId)

useLayoutEffect(() => {
Expand All @@ -427,6 +430,8 @@ export function getDefaultOptions ({ type, rawOptions = {}, currentInject }) {
}, [])

navigation.insets = useSafeAreaInsets()
navigation.headerHeight = useHeaderHeight()
navigation.isCustomHeader = pageConfig.navigationStyle === 'custom'

return createElement(GestureHandlerRootView,
{
Expand All @@ -445,19 +450,23 @@ export function getDefaultOptions ({ type, rawOptions = {}, currentInject }) {
{
value: currentPageId
},
createElement(defaultOptions,
createElement(IntersectionObserverContext.Provider,
{
navigation,
route,
id: currentPageId
}
value: intersectionObservers.current
},
createElement(defaultOptions,
{
navigation,
route,
id: currentPageId
}
)
)
)
)
)
}
return Page
}

return defaultOptions
}
4 changes: 3 additions & 1 deletion packages/webpack-plugin/lib/react/processScript.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ module.exports = function (script, {
import { getComponent } from ${stringifyRequest(loaderContext, optionProcessorPath)}
import { NavigationContainer, createNavigationContainerRef, StackActions } from '@react-navigation/native'
import { createNativeStackNavigator } from '@react-navigation/native-stack'
import { useHeaderHeight } from '@react-navigation/elements';
import { Provider } from '@ant-design/react-native'
import { SafeAreaProvider, useSafeAreaInsets } from 'react-native-safe-area-context'
import { GestureHandlerRootView } from 'react-native-gesture-handler'
Expand All @@ -38,7 +39,8 @@ global.__navigationHelper = {
GestureHandlerRootView: GestureHandlerRootView,
Provider: Provider,
SafeAreaProvider: SafeAreaProvider,
useSafeAreaInsets: useSafeAreaInsets
useSafeAreaInsets: useSafeAreaInsets,
useHeaderHeight: useHeaderHeight
}\n`
const { pagesMap, firstPage } = buildPagesMap({
localPagesMap,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,10 @@ export interface FormContextValue {
reset: () => void
}

export interface IntersectionObserver {
throttleMeasure: () => void
}

export const MovableAreaContext = createContext({ width: 0, height: 0 })

export const FormContext = createContext<FormContextValue | null>(null)
Expand All @@ -38,3 +42,5 @@ export const LabelContext = createContext<LabelContextValue | null>(null)
export const PickerContext = createContext(null)

export const VarContext = createContext({})

export const IntersectionObserverContext = createContext<IntersectionObserver[] | null>(null)
Loading
Loading