ExtJS 6.5+ Modern 应用避开全面屏刘海的统一处理

CSS 变量 safe-area-inset-*

iso 提供了一些 css 变量,可以用来避开刘海区域(notch 区域)。这些 css 变量已经成为了 w3c 标准。

env(safe-area-inset-top)
env(safe-area-inset-right)
env(safe-area-inset-bottom)
env(safe-area-inset-left)

最简单的,只要给 body(如果是extjs,则是 #ext-viewport元素) 加上以下样式,即可避开刘海。

padding-top: env(safe-area-inset-top)
padding-right: env(safe-area-inset-right)
padding-bottom: env(safe-area-inset-bottom)
padding-left: env(safe-area-inset-left)

但是这样应用的 沉浸式体验就很不好,如下图,整个应用四周会变成空白的:
在这里插入图片描述
ps: 为了演示,我"夸张"地模拟了刘海区域的高度。

理想的效果是下面这样,应用的内容 也能填充到这些区域。只是操作类型的组件(比如按钮、输入框) 需要避开刘海。
在这里插入图片描述

实现方法

1、先来写一些 css 样式类

// safe-area
.topinset {
    padding-top: env(safe-area-inset-top);
}
.bottominset {
    padding-bottom: env(safe-area-inset-bottom);
}
#ext-viewport {
    padding-left: env(safe-area-inset-left);
    padding-right: env(safe-area-inset-right);
}

左右的 notch 区域我就简单处理了。主要是上下的刘海(顶部摄像头传感器,底部横线)。

2、识别出那些 view 需要处理,这是难点之一

一般包括

1)全屏的view

比如 Ext.Viewport 内加入的 view,还有 全屏显示的弹出层 Dialog/Sheet

  • 对于 Ext.Viewport 内加入的 view,我们可以监听 Ext.Viewport 的 add 事件,在事件处理函数中,对加入的 view 进行处理。

  • 对于全屏的弹出层,先要判断出那些是全屏的弹出层:

    • view.getTop() == 0 且 view.getHeight() == 100vh 或 100%
    • view.getCentered() == 0 且 view.getHeight() == 100vh 或 100%
    • view 限制在可视区域里面(即不能拖拽出可视区域外) 且 view.getHeight() == 100vh 或 100%

    这样,我们可以在 updateTop、updateCentered、updateHeight 等方法里面处理。

2)靠底部显示的 Sheet

对于靠底部显示的 Sheet,即 view.getBottom() == 0 的情形,我们可以在 updateBottom 里面处理。

3、如何处理

如果 view 有停靠顶部或底部的标题栏、工具栏等,那么我们应该将 padding 加到这些标题栏、工具栏上,如下:
在这里插入图片描述
如果view 没有标题栏、工具栏,那么应该加到可滚动容器 div 内,如下图:
在这里插入图片描述
此处必须要考虑到 view 的嵌套,一定要找到最终可以滚动的那个容器 div。

也就是说,如果 container1 用 fit/card 布局加了一个子 container2,那么应该处理 container2,而不是 container1。

当然还有一些细节,比如,可以滚动的那个容器 div 原本 已经有了 padding 样式,我们当然不能去覆盖它,所以需要另辟蹊径,比如给 div 的 :after 伪元素设置个高度:
在这里插入图片描述

主要代码

1、写一些 scss 样式类

// safe-area
.topinset {
    &:not(.x-button), &.x-button .x-inner-el {
        padding-top: env(safe-area-inset-top);
    }
}
.bottominset {
    &:not(.x-button), &.x-button .x-inner-el {
        padding-bottom: env(safe-area-inset-bottom);
    }
}

.topinset-margin {
    margin-top: env(safe-area-inset-top);
}
.bottominset-margin {
    margin-bottom: env(safe-area-inset-bottom);
}

#ext-viewport {
    padding-left: env(safe-area-inset-left);
    padding-right: env(safe-area-inset-right);
}

.topinset-container {
    &>.x-body-el:not(.x-layout-fit):not(.x-layout-card),
    &>.x-dock > .x-body-el:not(.x-layout-fit):not(.x-layout-card),
    &>.x-body-wrap-el > .x-body-el:not(.x-layout-fit):not(.x-layout-card),
    &>.x-body-wrap-el > .x-dock > .x-body-el:not(.x-layout-fit):not(.x-layout-card)
    {
        &:before {
            content: '';
            display: block;
            clear: both;
            padding-top: env(safe-area-inset-top);
        }
    }
}
.bottominset-container {
    &:not(.x-list) {
        &>.x-body-el:not(.x-layout-fit):not(.x-layout-card),
        &>.x-dock > .x-body-el:not(.x-layout-fit):not(.x-layout-card),
        &>.x-body-wrap-el > .x-body-el:not(.x-layout-fit):not(.x-layout-card),
        &>.x-body-wrap-el > .x-dock > .x-body-el:not(.x-layout-fit):not(.x-layout-card)
        {
            &:after {
                content: '';
                display: block;
                clear: both;
                padding-bottom: env(safe-area-inset-bottom);
            }
        }
    }
    &.x-list {
        &:not(.x-infinite) > .x-body-el > .x-list-outer-ct > .x-list-inner-ct,
        &.x-infinite > .x-body-el > .x-list-outer-ct > .x-scroller-spacer
        {
            padding-bottom: env(safe-area-inset-bottom);
        }
    }
}

2、写一个帮助类方法

功能是遍历 某个 view 的 布局结构,找出 需要 inset 的停靠工具栏 或 容器

Ext.define('MyApp.util.Utils', {
    alternateClassName: 'Utils',
    singleton: true,
 
    /**
     * 遍历 view 的层次结构(fit/card layout),给 view 设置 safe-area-inset-*
     * @param {Ext.Container} view
     * @param {Boolean} needBottomInset 是否需要底部的 inset
     * @param {Boolean} needTopInset 是否需要顶部的 inset
     */
    iterateFullViewToInset(view, needBottomInset, needTopInset) {
        if (!Ext.os.is.iOS) return; // 目前只需要处理 ios
        if (!view || view.destroyed || view.destroying || !view.getDockedItems) return;

        const need = view.needInset || {};

        if (needBottomInset === null) needBottomInset = need.bottom;
        if (needTopInset === null) needTopInset = need.top;

        view.needInset = {
            bottom: needBottomInset,
            top: needTopInset
        };

        if (!view.initialized) { // 等到 view 初始化完毕时 才去处理 padding,避免无谓的消耗
            view.un({
                initialize: Utils.iterateFullViewToInset
            });
            view.on({
                initialize: Utils.iterateFullViewToInset,
                args: [view, null, null]
            });

            return;
        }

        let bottomSetted = false;
        let topSetted = false;

		// 先处理 停靠在顶部或底部的 工具栏、Header 等
        const dockedItems = view.getDockedItems();
        if (view.getHeader && view.getHeader()) {
            dockedItems.unshift(view.getHeader());
        }
        if (dockedItems.length) {
            for (let i = 0; i < dockedItems.length; i++) {
                const item = dockedItems[i],
                    docked = item.getDocked();
                if (['bottom', 'top'].includes(docked)) {
                    if (!item.isHiddenchangeSyncInset) {
                        item.on({
                            hiddenchange: Utils.iterateFullViewToInset,
                            args: [view, null, null]
                        });
                        item.isHiddenchangeSyncInset = true;
                    }

                    if (docked == 'bottom') {
                        if (needBottomInset && !item.getHidden() && !bottomSetted) {
                            item.element.addCls('bottominset');
                            bottomSetted = true;
                        } else {
                            item.element.removeCls('bottominset');
                        }
                    }

                    if (docked == 'top') {
                        if (needTopInset && !item.getHidden() && !topSetted) {
                            item.element.addCls('topinset');
                            topSetted = true;
                        } else {
                            item.element.removeCls('topinset');
                        }
                    }
                }
            }
        }

        const clsb = 'bottominset-container',
            clst = 'topinset-container';

		// 如果没有停靠顶部底部的工具栏,则给容器内可以滚动的 view 加 inset
        const layout = view.getLayout();
        let done = false;
        if (needBottomInset && !bottomSetted || needTopInset && !topSetted) {
            if (layout) {
                let items;
                if (['fit', 'card', 'hbox'].includes(layout.type)) {
                    items = view.getInnerItems();
                } else if (layout.type == 'vbox') {
                    const innerItems = view.getInnerItems();
                    if (innerItems.length == 1 && innerItems[0].getFlex() == 1) {
                        items = [innerItems[0]];
                    }
                }
                if (items) {
                    items.forEach(x => {
                        Utils.iterateFullViewToInset(x, needBottomInset && !bottomSetted, needTopInset && !topSetted);
                    });
                    done = true;
                }
            }
            if (!done) {
                if (needBottomInset && !bottomSetted) {
                    view.element.addCls(clsb);
                } else {
                    view.element.removeCls(clsb);
                }
                if (needTopInset && !topSetted) {
                    view.element.addCls(clst);
                } else {
                    view.element.removeCls(clst);
                }
            } else {
                view.element.removeCls(clsb);
                view.element.removeCls(clst);
            }
        } else {
            if (layout) {
                let items;
                if (['fit', 'card', 'hbox'].includes(layout.type)) {
                    items = view.getInnerItems();
                } else if (layout.type == 'vbox') {
                    const innerItems = view.getInnerItems();
                    if (innerItems.length == 1 && innerItems[0].getFlex() == 1) {
                        items = [innerItems[0]];
                    }
                }
                if (items) {
                    view.getInnerItems().forEach(x => {
                        Utils.iterateFullViewToInset(x, false, false);
                    });
                    done = true;
                }
            }

            if (!done) {
                view.element.removeCls(clsb);
                view.element.removeCls(clst);
            }
        }

        if (!view.isAddRemoveSyncInset) {
            if (view.getHeader) {
                const originalUpdateHeader = view.updateHeader;
                view.updateHeader = function (header) {
                    originalUpdateHeader.apply(view, arguments);
                    Utils.iterateFullViewToInset(view, null, null);
                };
            }
            view.on({
                add() {
                    Utils.iterateFullViewToInset(view, null, null);
                },
                remove() {
                    Utils.iterateFullViewToInset(view, null, null);
                }
            });
            view.isAddRemoveSyncInset = true;
        }
    }
});

3、增加一个 override

放在项目 overrides 目录下
主要功能是统一处理 全屏和靠底部显示的悬浮弹出层,比如 Dialog 和 Sheet

/**
 * 底部工具栏添加 bottominset
 */
Ext.define(null, {
    override: 'Ext.Panel',

    updateTop(top) {
        const me = this;
        me.callParent(arguments);

        me.syncNtochInset();
    },

    updateCentered(centered) {
        const me = this;
        me.callParent(arguments);

        me.syncNtochInset();
    },

    updateBottom(bottom) {
        const me = this;
        me.callParent(arguments);

        me.syncNtochInset();
    },

    updateFloated(floated) {
        const me = this;
        me.callParent(arguments);

        me.syncNtochInset();
    },

    updateHeight(height) {
        const me = this;
        me.callParent(arguments);

        me.syncNtochInset();
    },

    updateHeader(header, oldHeader) {
        const me = this;
        me.callParent(arguments);

        if (header && ['bottom', 'top'].includes(header.getDocked())) {
            me.syncNtochInset();
        }
    },

    initialize() {
        const me = this;

        me.on({
            initialize: 'syncNtochInset',
            scope: me
        });

        me.callParent(arguments);
    },

    doAdd(item) {
        const me = this;
        me.callParent(arguments);

        if (item && ['bottom', 'top'].includes(item.getDocked())) {
            me.syncNtochInset();
        }
    },

    doRemove(item, index, destroy) {
        const me = this;

        if (item && ['bottom', 'top'].includes(item.getDocked())) {
            me.syncNtochInset();
        }

        me.callParent(arguments);
    },

    syncNtochInset() {
        const me = this;
        if (!Ext.os.is.iOS || !me.initialized || me.destroyed || me.destroying) return;

        const floated = me.getFloated();
        if (!floated) return;

        const centered = me.getCentered(),
            top = me.getTop(),
            bottom = me.getBottom(),
            height = me.getHeight(),
            draggable = me.getDraggable();

        // 底部是否需要 padding-bottom
        const b = floated && ( // 悬浮层
            bottom == 0 || // 停靠底部
            (top == 0 || centered ||
                (me.getConstrainDrag && me.getConstrainDrag()) ||
                (draggable && draggable.getConstrain() && draggable.getConstrain().getElement().dom === document.body)) &&
            (height == '100vh' || height == '100%') // 或者全屏
        );

        // 顶部是否需要 padding-top
        const t = floated && ( // 悬浮层
            top == 0 || // 停靠顶部
            (bottom == 0 || centered ||
                (me.getConstrainDrag && me.getConstrainDrag()) ||
                (draggable && draggable.getConstrain() && draggable.getConstrain().getElement().dom === document.body)) &&
            (height == '100vh' || height == '100%') // 或者全屏
        );

        Utils.iterateFullViewToInset(me, b, t);
    }
});

4、统一处理 Ext.Viewport 内加入的 view

Application.jslaunch 方法中监听 addremove 事件,以便处理 view

launch: function () {
	this.callParent(arguments);
	Ext.Viewport.on({
	    add(ct, item) {
	        if(!item.getFloated()) Utils.iterateFullViewToInset(item, true, true);
	    },
	    remove(ct, item) {
	        if(!item.getFloated()) Utils.iterateFullViewToInset(item, false, false);
	    }
	});
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

神秘_博士

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值