App内嵌H5全屏页面容器组件如何解决弹性滚动的背景问题

1,666 阅读4分钟

一、背景

    在app内部,很多页面需要利用H5开发,这些H5渲染在app提供的webview容器里,app会提供两种形式的webview:

  1. 带导航的webview:
    该类webview会自带顶部导航,H5开发人员只需利用jsBridge动态配置导航(如标题、右侧action等),H5页面相关的内容会渲染在导航之下的主体区域。
  2. 沉浸式webview:
    该类webview不带导航,整个页面连同手机设备顶部的状态栏都是H5内,通常该类webview是为满足某些定制化的导航场景(小程序也有类似的场景),此时,H5不仅需要开发页面主体部分,同时也要开发一个导航栏。

针对第二种webview,H5开发需要一个沉浸式的页面容器组件,该组件包含导航和页面主体,主体区域支持超出部分滚动,本文主要提供一种页面容器的组件以及相关问题的解决方案。

二、技术栈

本文示例采用react框架开发,使用hook开发组件

三、页面导航

在开发容器组件前,我们首先要开发一个通用的导航组件。

import React, { memo, useState, useCallback, useLayoutEffect } from 'react';import PropTypes from 'prop-types';import classnames from 'classnames';

import { useHistory } from 'react-router-dom';import { bridge } from 'jsbridge';

import WhiteLeftArrow from './icons/icon_arrow_left_white.png';

import BlackLeftArrow from './icons/icon_arrow_left_black.png';
import './style.less';

/** 
 * 沉浸式页面标题栏
 * @param {string} title 页面标题; 
 * @param {string} theme 主题颜色; 
 * @param {object} style 标题的主题配置; 
 * @param {object} action 标题右侧的action按钮名称; 
 * @param {boolean} bordered 是否带底部灰色边框; 
 * @param {function} goBack 返回按钮的点击事件(不传默认返回上一层页面); 
*/
function Navigator(props) {  
  const history = useHistory();  
  const barHeight = Number(
    window.localStorage.getItem('STATUS_BAR_HEIGHT')
  ); 
  const { title, theme, style, action, bordered, goBack } = props;  
  const initHeight = barHeight && barHeight > 0 ? barHeight : 20;  
  const [systemStatusBarHeight, setSystemStatusBarHeight] = useState(    initHeight  ); 
  const { name, onClick } = action; 
  
  useLayoutEffect(() => {    
    if (!barHeight) {      
      bridge.nativeGetDeviceInfo().then((res) => {        
        const { pixelDensity, statusBarHeight } = res;        
        const height = statusBarHeight / pixelDensity;        
        setSystemStatusBarHeight(height);        
        window.localStorage.setItem('STATUS_BAR_HEIGHT', height);      
      });    
     }  
  }, [barHeight]);  
  
  const goToPrevPage = useCallback(() => {    
    if (!goBack) {      
      history.go(-1);      
      return;    
    }    
    
    goBack();  
  }, [goBack, history]);  
  
  return (    
    <div 
      className={classnames('navigator--global-component', { bordered })}
      style={{ paddingTop: systemStatusBarHeight, ...style }}    
    >      
       <div className="navigator__inner-wrapper">        
       <div className="left-icon" onClick={goToPrevPage}>          
         <img            
           alt="left arrow"            
           src={theme === 'white' ? BlackLeftArrow : WhiteLeftArrow}          
         />        
       </div>        
       <div className="page-title">{title}</div>        
       {action && (          
         <div className="right-action" onClick={onClick}>            
           {name}          
         </div>        
        )}      
       </div>    
    </div>  
  );
}

Navigator.propTypes = {  
  title: PropTypes.string,  
  theme: PropTypes.string,  
  style: PropTypes.object,  
  bordered: PropTypes.bool,  
  action: PropTypes.object,  
  goBack: PropTypes.func,
};

Navigator.defaultProps = {  
  title: '',  
  theme: 'white',  
  style: {},  
  bordered: false,  
  action: {    
    name: '',    
    onClick: () => {},  
  },  
  goBack: null,
};

export default memo(Navigator);

此处需要通过jsbridge与原生交互,来获取系统状态栏的高度做H5导航的自适应。至此,我们就开发好了我们的导航栏,具体样式根据公司UI设计来,此处不贴样式了,eg

四、页面容器组件

基于开发好的导航(若公司组件库已有导航组件也可直接使用),我们之后可以开发我们的页面容器:

import React, { useRef, useEffect, useState } from 'react';
import PropTypes from 'prop-types';
import Navigator from '@/components/navigator';
import EmptyData from '@/components/empty-data';

import './style.less';

/** 
 * 页面容器,包含头部和页面主体部分; 
 * @param {object} navigatorConf 导航配置信息; 
 * @param {any} children 页面内容; 
 * @param {boolean} isEmpty 是否空页面; 
 * @param {boolean} autoBgSize 页面背景大小是否自适应; 
 * @param {any} emptyTips 空页面时的提示文案; 
 * @param {object} pageStyle 页面主体的样式; 
 * @param {object} bodyStyle 页面主体的样式; 
 * 
 * 备注: 
 * 此处可实现H5弹性滚动下拉时背景色与头部保持一直的功能,具体实现如下: 
 * 
 * eg: 
 * pageStyle={{ 
 *  backgroundColor: '#eceded', 
 *  backgroundImage: 'linear-gradient(68deg, #ff3c31 0%, #ff9a46 100%)', 
 *  backgroundRepeat: 'no-repeat',
 * }} 
 * bodyStyle={{ background: '#eceded' }} 
 * 设置pageStyle的背景色与页面主体的背景色一致,设置pageStyle的背景与导航一致,开启autoBgSize, 
 * 开启后,组件会根据页面滚动方向,动态的设置背景的大小,实现: 
 * 手势往下拉时,顶部弹性滚动出现与导航背景相同的背景色; 
 * 手势往上拉时,底部弹性滚动出现与页面主体相同的背景色; 
 */
function PageContainer(props) {
  const pageRef = useRef();  
  const {    
    navigatorConf,    
    children,
    isEmpty,
    emptyTips,
    pageStyle,
    autoBgSize,
    bodyStyle,
  } = props;
  const [style, setStyle] = useState(pageStyle);
  const [scrollDirection, setScrollDirection] = useState('down');

  useEffect(() => {
    const handlePageScroll = (e) => {
      const { scrollTop } = e.target;

      if (scrollTop > 0) {
        setScrollDirection('up');
      } else {
        setScrollDirection('down');
      }
    };
    const pageBody = document.querySelector('.page-container__body');

    if (autoBgSize) {
      pageBody.addEventListener('scroll', handlePageScroll, false);
    }

    return () => {
      autoBgSize &&
        pageBody.removeEventListener('scroll', handlePageScroll, false);
    };
  }, [autoBgSize]);

  useEffect(() => {
    if (autoBgSize) {
      if (scrollDirection === 'down') {
        setStyle({
          ...pageStyle,
          backgroundSize: '100%',
        });
      } else {
        setStyle({
          ...pageStyle,
          backgroundSize: '0',
        });
      }
    }
  }, [autoBgSize, scrollDirection, pageStyle]);

  return (
    <div className="page-container--layout">
      <Navigator {...navigatorConf} />
      <div className="page-container__body" style={style} ref={pageRef}>
        <div className="page-container__main" style={bodyStyle}>
          {isEmpty ? <EmptyData tips={emptyTips} /> : children}
        </div>
      </div>
    </div>
  );
}

PageContainer.propTypes = {
  navigatorConf: PropTypes.objectOf(PropTypes.any),
  children: PropTypes.any,
  isEmpty: PropTypes.bool,
  autoBgSize: PropTypes.bool,
  emptyTips: PropTypes.string,
  pageStyle: PropTypes.objectOf(PropTypes.any),
  bodyStyle: PropTypes.objectOf(PropTypes.any),
};

PageContainer.defaultProps = {
  navigatorConf: {},
  children: null,
  isEmpty: false,
  autoBgSize: false,
  emptyTips: '',
  pageStyle: {},
  bodyStyle: {},
};

export default PageContainer;

该容器高度可配置化,同时也解决了移动端弹性滚动时,主体区域的弹性滚动显示的区域颜色与导航栏颜色不同导致的视觉脱节问题,之前开发时,就有UI提出不能弹性下拉时候中间出现与头部导航栏背景色不同的颜色,要求往下拉的时候,弹性区域颜色保持和导航背景一致,往上拉的时候,弹性区域的颜色保持与页面主体颜色一致,通过autoBgSize的配置,可以开启该项弹性滚动的颜色适配,当然如果主体区域颜色本身和导航颜色一致或者不明显脱节,可不用做适配。具体效果示例:

好啦,至此,所有的功能都以完成,主要是根据滚动方向动态设置backgroundSize,如果对你有帮助,请点个赞。