From 80b318091ca4d98a75e59570a3fc744cfb27873d Mon Sep 17 00:00:00 2001 From: tianjiaxing Date: Wed, 2 Sep 2020 20:52:44 +0800 Subject: [PATCH] add: uni.createMediaQueryObserver Api and mediaQueryObserver Object --- docs/api/ui/mediaQuery-observer.md | 134 ++++++++++++++++++ lib/apis.js | 1 + lib/modules.json | 11 +- .../api/ui/create-media-query-observer.js | 47 ++++++ src/core/service/bridge/subscribe.js | 18 +++ src/core/service/plugins/index.js | 8 +- src/core/service/plugins/polyfill.js | 6 +- src/core/view/bridge/subscribe/api/index.js | 11 +- .../api/request-media-query-observer.js | 76 ++++++++++ 9 files changed, 302 insertions(+), 10 deletions(-) create mode 100644 docs/api/ui/mediaQuery-observer.md create mode 100644 src/core/service/api/ui/create-media-query-observer.js create mode 100644 src/core/view/bridge/subscribe/api/request-media-query-observer.js diff --git a/docs/api/ui/mediaQuery-observer.md b/docs/api/ui/mediaQuery-observer.md new file mode 100644 index 0000000000..eb23a6eb68 --- /dev/null +++ b/docs/api/ui/mediaQuery-observer.md @@ -0,0 +1,134 @@ +节点布局交叉状态 API 可用于监听两个或多个组件节点在布局位置上的相交状态。这一组API常常可以用于推断某些节点是否可以被用户看见、有多大比例可以被用户看见。 + +### uni.createIntersectionObserver([this], [options]) +创建并返回一个 ``IntersectionObserver`` 对象实例。 + +**this说明:** + +自定义组件实例。**支付宝小程序不支持此参数,传入仅为抹平写法差异** + +**options 的可选参数为:** + +|字段名|类型|说明| +|:-|:-|:-| +|thresholds|Array|一个数值数组,包含所有阈值。默认为 ``[0]``。| +|initialRatio|Number|初始的相交比例,如果调用时检测到的相交比例与这个值不相等且达到阈值,则会触发一次监听器的回调函数。默认为 ``0``。| +|observeAll|Boolean|是否同时观测多个参照节点(而非一个),如果设为 ``true``,``observe`` 的 ``targetSelector`` 将选中多个节点(注意:同时选中过多节点将影响渲染性能)| + +### IntersectionObserver 对象的方法列表 + +|方法|说明| +|:-|:-| +|IntersectionObserver.relativeTo(selector,[margins])|使用选择器指定一个节点,作为参照区域之一。| +|IntersectionObserver.relativeToViewport([margins])|指定页面显示区域作为参照区域之一| +|IntersectionObserver.observe(selector,[callback])|指定目标节点并开始监听相交状态变化情况。回调函数 ``callback`` 包含一个参数 ``result``| +|IntersectionObserver.disconnect()|停止监听。回调函数将不再触发。| + +**margins 参数:** 用来扩展(或收缩)参照节点布局区域的边界。 + +|属性|类型|默认值|是否必填|说明| +|:-|:-|:-|:-|:-| +|left|number||否|节点布局区域的左边界| +|right|number||否|节点布局区域的右边界| +|top|number||否|节点布局区域的上边界| +|bottom|number||否|节点布局区域的下边界| + +下面的示例代码中,如果目标节点 ``".test"`` 进入 ``".scroll"`` 区域以下 100px 时,就会触发回调函数。 +``` +uni.createIntersectionObserver(this).relativeTo('.scroll',{bottom: 100}).observe('.test', (res) => { + console.log(res); +}) +``` + +**observe 回调函数 result 包含的字段** + +|字段名|类型|说明| +|:-|:-|:-| +|intersectionRatio|Number|相交比例| +|intersectionRect|Object|相交区域的边界,包含 ``left``、``right``、``top``、``bottom`` 四项| +|boundingClientRect|Object|目标节点布局区域的边界,包含 ``left``、``right``、``top``、``bottom`` 四项| +|relativeRect|Object|参照区域的边界,包含 ``left``、``right``、``top``、``bottom`` 四项| +|time|Number|相交检测时的时间戳| + + +**Tips** + +- 与页面显示区域的相交区域并不准确代表用户可见的区域,因为参与计算的区域是“布局区域”,布局区域可能会在绘制时被其他节点裁剪隐藏(如祖先节点中 overflow 样式为 hidden 的节点)或遮盖(如 fixed 定位的节点)。 +- 节点交互状态 ``API`` 建议在 ``onReady`` 生命周期里监听,因为此 ``API`` 需要查找页面元素,``onReady`` 时页面已经完成初次渲染,已经能查找到对应的元素。 + +### 代码示例 + +``` + + + + +``` diff --git a/lib/apis.js b/lib/apis.js index 2c22441e47..853905a2fc 100644 --- a/lib/apis.js +++ b/lib/apis.js @@ -161,6 +161,7 @@ const ui = [ 'stopPullDownRefresh', 'createSelectorQuery', 'createIntersectionObserver', + 'createMediaQueryObserver', 'getMenuButtonBoundingClientRect' ] diff --git a/lib/modules.json b/lib/modules.json index b9457b674d..ce69a94a74 100644 --- a/lib/modules.json +++ b/lib/modules.json @@ -113,15 +113,15 @@ "uni.getBLEDeviceServices": true, "uni.getBLEDeviceCharacteristics": true, "uni.createBLEConnection": true, - "uni.closeBLEConnection": true, - "uni.setBLEMTU": true, + "uni.closeBLEConnection": true, + "uni.setBLEMTU": true, "uni.getBLEDeviceRSSI": true, "uni.onBeaconServiceChange": true, "uni.onBeaconUpdate": true, "uni.getBeacons": true, "uni.startBeaconDiscovery": true, - "uni.stopBeaconDiscovery": true, - "uni.onThemeChange": true, + "uni.stopBeaconDiscovery": true, + "uni.onThemeChange": true, "uni.onUIStyleChange": true } }, { @@ -157,7 +157,8 @@ "uni.stopPullDownRefresh": true, "uni.createSelectorQuery": true, "uni.createIntersectionObserver": true, - "uni.hideKeyboard": true, + "uni.createMediaQueryObserver": true, + "uni.hideKeyboard": true, "uni.onKeyboardHeightChange": true } }, { diff --git a/src/core/service/api/ui/create-media-query-observer.js b/src/core/service/api/ui/create-media-query-observer.js new file mode 100644 index 0000000000..0139d84dee --- /dev/null +++ b/src/core/service/api/ui/create-media-query-observer.js @@ -0,0 +1,47 @@ +import createCallbacks from 'uni-helpers/callbacks' + +import { + getCurrentPageVm +} from '../../platform' + +const createMediaQueryObserverCallbacks = createCallbacks('requestMediaQueryObserver') + +class ServiceMediaQueryObserver { + constructor (component, options) { + this.pageId = component.$page.id + this.component = component._$id || component // app-plus 平台传输_$id + this.options = options + } + + observe (options, callback) { + if (typeof callback !== 'function') { + return + } + this.options = options + + this.reqId = createMediaQueryObserverCallbacks.push(callback) + + UniServiceJSBridge.publishHandler('requestMediaQueryObserver', { + reqId: this.reqId, + component: this.component, + options: this.options + }, this.pageId) + } + + disconnect () { + UniServiceJSBridge.publishHandler('destroyMediaQueryObserver', { + reqId: this.reqId + }, this.pageId) + } +} + +export function createMediaQueryObserver (context, options) { + if (!context._isVue) { + options = context + context = null + } + if (context) { + return new ServiceMediaQueryObserver(context, options) + } + return new ServiceMediaQueryObserver(getCurrentPageVm('createMediaQueryObserver'), options) +} diff --git a/src/core/service/bridge/subscribe.js b/src/core/service/bridge/subscribe.js index c5e6dee2dd..27f11c9f1f 100644 --- a/src/core/service/bridge/subscribe.js +++ b/src/core/service/bridge/subscribe.js @@ -50,6 +50,23 @@ export default function initSubscribe (subscribe, { } } + const requestMediaQueryObserverCallbacks = createCallbacks('requestMediaQueryObserver') + + function onRequestMediaQueryObserver ({ + reqId, + reqEnd, + res + }) { + const callback = requestMediaQueryObserverCallbacks.get(reqId) + if (callback) { + if (reqEnd) { + requestMediaQueryObserverCallbacks.pop(reqId) + return + } + callback(res) + } + } + if (__PLATFORM__ === 'h5') { subscribe('onPageReady', createPageEvent('onReady')) } @@ -59,4 +76,5 @@ export default function initSubscribe (subscribe, { subscribe('onRequestComponentInfo', onRequestComponentInfo) subscribe('onRequestComponentObserver', onRequestComponentObserver) + subscribe('onRequestMediaQueryObserver', onRequestMediaQueryObserver) } diff --git a/src/core/service/plugins/index.js b/src/core/service/plugins/index.js index 34617aee54..01aa6e1d88 100644 --- a/src/core/service/plugins/index.js +++ b/src/core/service/plugins/index.js @@ -73,7 +73,7 @@ export default { initPolyfill(Vue) lifecycleMixin(Vue) - + /* eslint-disable no-undef */ if (typeof __UNI_ROUTER_BASE__ !== 'undefined') { __uniConfig.router.base = __UNI_ROUTER_BASE__ @@ -194,7 +194,11 @@ export default { return uni.createIntersectionObserver(this, args) } + Vue.prototype.createMediaQueryObserver = function createMediaQueryObserver (args) { + return uni.createMediaQueryObserver(this, args) + } + Vue.use(VueRouter) } -} +} diff --git a/src/core/service/plugins/polyfill.js b/src/core/service/plugins/polyfill.js index c5ebb7461a..7e9c53cb56 100644 --- a/src/core/service/plugins/polyfill.js +++ b/src/core/service/plugins/polyfill.js @@ -70,6 +70,10 @@ export function initPolyfill (Vue) { return uni.createIntersectionObserver(this, options) } + Vue.prototype.createMediaQueryObserver = function createMediaQueryObserver (options) { + return uni.createMediaQueryObserver(this, options) + } + Vue.prototype.selectComponent = function selectComponent (selector) { return querySelector(this, parseSelector(selector)) } @@ -77,4 +81,4 @@ export function initPolyfill (Vue) { Vue.prototype.selectAllComponents = function selectAllComponents (selector) { return querySelectorAll(this, parseSelector(selector), []) } -} +} diff --git a/src/core/view/bridge/subscribe/api/index.js b/src/core/view/bridge/subscribe/api/index.js index 24a0ca32cc..615e752f59 100644 --- a/src/core/view/bridge/subscribe/api/index.js +++ b/src/core/view/bridge/subscribe/api/index.js @@ -11,9 +11,16 @@ import { destroyComponentObserver } from './request-component-observer' +import { + requestMediaQueryObserver, + destroyMediaQueryObserver +} from './request-media-query-observer' + export default { setPageMeta, requestComponentInfo, requestComponentObserver, - destroyComponentObserver -} + destroyComponentObserver, + requestMediaQueryObserver, + destroyMediaQueryObserver +} diff --git a/src/core/view/bridge/subscribe/api/request-media-query-observer.js b/src/core/view/bridge/subscribe/api/request-media-query-observer.js new file mode 100644 index 0000000000..1b0b553179 --- /dev/null +++ b/src/core/view/bridge/subscribe/api/request-media-query-observer.js @@ -0,0 +1,76 @@ +const mediaQueryObservers = {} +const listeners = {} // 用公用对象存储监听器 + +// 拼接媒体查询条件 +function handleMediaQueryStr ($props) { + let mediaQueryStr = [] + const propsMenu = [ + 'width', + 'minWidth', + 'maxWidth', + 'height', + 'minHeight', + 'maxHeight', + 'orientation' + ] + for (const item of propsMenu) { + if (item !== 'orientation' && $props[item] !== '' && Number($props[item]) >= 0) { + mediaQueryStr.push(`(${humpToLine(item)}: ${Number($props[item])}px)`) + } + if (item === 'orientation' && $props[item]) { + mediaQueryStr.push(`(${humpToLine(item)}: ${$props[item]})`) + } + } + mediaQueryStr = mediaQueryStr.join(' and ') + return mediaQueryStr +} + +function humpToLine (name) { + return name.replace(/([A-Z])/g, '-$1').toLowerCase() +} + +// 请求媒体查询对象 +export function requestMediaQueryObserver ({ + reqId, + options +}, pageId) { + const pages = getCurrentPages() + const page = pages.find(page => page.$page.id === pageId) + + if (!page) { + throw new Error(`Not Found:Page[${pageId}]`) + } + + const pageVm = page.$vm + + // 创建一个媒体查询对象 + const mediaQueryObserver = mediaQueryObservers[reqId] = window.matchMedia(handleMediaQueryStr(options)) + + // 创建一个监听器 + const listener = listeners[reqId] = e => { + UniViewJSBridge.publishHandler('onRequestMediaQueryObserver', { + reqId, + res: e.matches + }, pageVm.$page.id) + } + + listener(mediaQueryObserver) // 监听前执行一次媒体查询 + mediaQueryObserver.addListener(listener) +} + +// 销毁媒体查询对象 +export function destroyMediaQueryObserver ({ + reqId +}) { + const listener = listeners[reqId] // 需要移除的某个监听 + const mediaQueryObserver = mediaQueryObservers[reqId] + + if (mediaQueryObserver) { + mediaQueryObserver.removeListener(listener) // 移除监听 + delete mediaQueryObservers[reqId] + UniViewJSBridge.publishHandler('onRequestMediaQueryObserver', { + reqId, + reqEnd: true + }) + } +} -- GitLab