Skip to content

KeyboardInsetsView 支持webview富文本场景吗 #64

@dotaeye

Description

@dotaeye

尝试在富文本中使用,键盘出来的位置不对,且整个导航都不见了

import React, {
  useState,
  useEffect,
  useCallback,
  useRef,
  useContext,
} from 'react';
import {
  View,
  Text,
  TextInput,
  TouchableOpacity,
  StyleSheet,
  Alert,
  BackHandler,
  Platform,
  Image,
  findNodeHandle,
  Animated,
} from 'react-native';
import {useNavigation} from '@react-navigation/native';
import {Navbar} from '../../components';
import {KeyboardInsetsView, getEdgeInsetsForView} from '@sdcx/keyboard-insets';
import {WebView} from 'react-native-webview';
import MaterialDesignIcons from '@react-native-vector-icons/material-design-icons';
import {useSetState} from 'ahooks';
import FontContext from '@/context/FontContext';
import {ViewDriver} from './driver/ViewDriver';
import {Driver} from './driver/Driver';
import {KeyboardDriver} from './driver/KeyboardDriver';
import {SafeAreaView} from 'react-native-safe-area-context';

const commands = {
  BOLD: 'BOLD',
  ITALIC: 'ITALIC',
  UL: 'unordered-list-item',
  OL: 'ordered-list-item',
  BLOCKQUOTE: 'blockquote',
  IMAGE: 'insert-image',
  CAMERA: 'open-camera',
  FONT: 'change-font',
};

const defaultActions = [
  {
    label: 'Bold',
    command: commands.BOLD,
    textIcon: 'format-bold',
    inline: true,
  },
  {
    label: 'Italic',
    command: commands.ITALIC,
    textIcon: 'format-italic',
    inline: true,
  },
  {
    label: 'UL',
    command: commands.UL,
    textIcon: 'format-list-bulleted',
    block: true,
  },
  {
    label: 'OL',
    command: commands.OL,
    textIcon: 'format-list-numbered',
    block: true,
  },
  {
    label: 'BLOCKQUOTE',
    command: commands.BLOCKQUOTE,
    textIcon: 'format-quote-open',
    block: true,
  },
  {
    label: 'IMAGE',
    command: commands.IMAGE,
    textIcon: 'image',
    block: true,
  },
  {
    label: 'CAMERA',
    command: commands.CAMERA,
    textIcon: 'camera',
    block: true,
  },
  {
    label: 'FONT',
    command: commands.FONT,
    textIcon: 'format-font',
    block: true,
    navigation: true,
  },
];

const WriteLetter: React.FC = () => {
  const navigation = useNavigation();
  const {globalFontName, fontData} = useContext(FontContext);

  const injectedScript = `
      window.ENV_RN = true;
      window.__Editor_Fonts = ${JSON.stringify(fontData)};
    `;

  const webviewRef = useRef<WebView | null>(null);
  const [blockState, setBlockState] = useSetState({
    block: '',
    style: [],
  });
  const [showTabbar, setShowTabbar] = useState(false);

  const inputRef = useRef<TextInput>(null); // 输入框
  const senderRef = useRef<View>(null); // 发送框
  const [bottom, setBottom] = useState(0); // 底部距离

  const onLayout = useCallback(() => {
    const viewTag = findNodeHandle(senderRef.current);
    if (viewTag === null) {
      return;
    }

    getEdgeInsetsForView(viewTag, insets => {
      console.log('onLayout', insets);
      setBottom(insets.bottom!);
    });
  }, []);

  const emoji = useRef(new ViewDriver('emoji')).current;
  const toolbox = useRef(new ViewDriver('toolbox')).current;
  const keyboard = useRef(
    new KeyboardDriver(inputRef, () => {
      webviewRef.current?.postMessage(
        JSON.stringify({
          messageType: 'richEditor',
          type: 'SHOW_TOOLBAR',
        }),
      );
    }),
  ).current;

  const [driver, setDriver] = useState<Driver>();
  const [translateY, setTranslateY] = useState(new Animated.Value(0));
  const driverState = {bottom, driver, setDriver, setTranslateY};

  const mainStyle = {
    transform: [
      {
        translateY: translateY,
      },
    ],
  };

  const onMessage = useCallback(
    (event: {nativeEvent: {data?: string}}) => {
      console.log('receive message', event.nativeEvent.data);
      const action = JSON.parse(event.nativeEvent.data || '{}');

      if (action.messageType !== 'richEditor') return;

      if (action.type === 'SHOW_TOOLBAR') {
        if (showTabbar) return;
        setShowTabbar(true);
        console.log('SHOW_TOOLBAR', action);
      } else if (action.type === 'HIDE_TOOLBAR') {
        if (!showTabbar) return;
        setShowTabbar(false);
      } else if (action.type === 'GET_CONTENT') {
        console.log('GET_CONTENT', action);
      } else {
        setBlockState(action);
      }
    },
    [showTabbar],
  );

  const onSendCommand = useCallback((action: any) => {
    if (action.navigation) {
      navigation.navigate('FontList');
      return;
    }
    if (webviewRef.current) {
      console.log('onSendCommand', action);
    }
    webviewRef.current?.postMessage(
      JSON.stringify({
        messageType: 'richEditor',
        type: 'DOCUMENT_COMMAND',
        ...action,
      }),
    );
  }, []);

  const enableFont = useCallback(
    (font: any) => {
      console.log('enableFont-send to webview', font);
      webviewRef.current?.postMessage(
        JSON.stringify({
          messageType: 'richEditor',
          type: 'ENABLE_FONT',
          data: font,
        }),
      );
    },
    [globalFontName],
  );

  useEffect(() => {
    const currentFont = fontData.find(
      item => item.scriptName === globalFontName,
    );
    enableFont(currentFont);
    if (webviewRef.current) {
      webviewRef.current.postMessage(
        JSON.stringify({
          messageType: 'richEditor',
          type: 'SET_CONTENT',
          content: '',
        }),
      );
    }
  }, [globalFontName]);

  const selectItems = [blockState.block, ...blockState.style];

  return (
    <View style={[styles.container]}>
      <Navbar
        title="富文本测试"
        left={{
          goBack: true,
        }}
        navigation={navigation}
      />
      <KeyboardInsetsView
        onKeyboard={keyboard.createCallback(driverState)}
        style={[
          {
            flex: 1,
          },
          mainStyle,
        ]}>
        <WebView
          ref={webviewRef}
          source={{
            uri: `${
              Platform.OS === 'android' ? 'file:///android_asset/' : ''
            }Web.bundle/index.html`,
          }}
          originWhitelist={['*']}
          webviewDebuggingEnabled
          injectedJavaScriptBeforeContentLoaded={injectedScript}
          injectedJavaScriptObject={fontData}
          hideKeyboardAccessoryView
          onMessage={onMessage}
          style={{
            flex: 1,
          }}
        />
        <View
          style={{
            height: showTabbar ? 48 : 0,
            width: '100%',
            flexDirection: 'row',
            justifyContent: 'flex-start',
            alignItems: 'center',
            backgroundColor: 'rgb(199,202,209)',
          }}
          ref={senderRef}
          onLayout={onLayout}>
          {defaultActions.map((item, index) => {
            return (
              <TouchableOpacity
                key={index}
                onPress={() => {
                  if (item.command === commands.FONT) {
                    emoji.shown ? keyboard.show() : emoji.show(driverState);
                  } else if (item.command === commands.IMAGE) {
                    toolbox.toggle(driverState);
                  } else {
                    onSendCommand(item);
                  }
                }}
                style={{
                  width: 48,
                  height: 48,
                  justifyContent: 'center',
                  alignItems: 'center',
                  backgroundColor: selectItems.includes(item.command)
                    ? '#eee'
                    : 'rgb(199,202,209)',
                }}>
                <MaterialDesignIcons
                  name={item.textIcon}
                  size={24}
                  color={'#000'}
                />
              </TouchableOpacity>
            );
          })}
        </View>
      </KeyboardInsetsView>
      <SafeAreaView edges={['bottom']} />

      <Animated.View
        style={[styles.absolute, styles.red, emoji.style]}
        onLayout={emoji.onLayout}>
        <View style={styles.emoji}>
          <Text style={styles.text}>表情包</Text>
        </View>
        <SafeAreaView edges={['bottom']} />
      </Animated.View>
      <Animated.View
        style={[styles.absolute, styles.blue, toolbox.style]}
        onLayout={toolbox.onLayout}>
        <View style={styles.toolbox}>
          <Text style={styles.text}>工具箱</Text>
        </View>
        <SafeAreaView edges={['bottom']} />
      </Animated.View>
    </View>
  );
};

const styles = StyleSheet.create({
  container: {
    flex: 1,
  },
  absolute: {
    position: 'absolute',
    bottom: 0,
    left: 0,
    right: 0,
  },
  emoji: {
    height: 300,
    justifyContent: 'center',
    alignItems: 'center',
  },
  blue: {
    backgroundColor: 'darkkhaki',
  },
  red: {
    backgroundColor: 'cadetblue',
  },
  toolbox: {
    height: 200,
    justifyContent: 'center',
    alignItems: 'center',
  },
  text: {
    fontSize: 48,
    color: 'darkgray',
  },

  navBarRight: {
    flexDirection: 'row',
    alignItems: 'center',
  },
  navBarButton: {
    padding: 8,
    marginLeft: 8,
  },

  sendTime: {
    fontSize: 12,
    opacity: 0.7,
  },
  scrollView: {
    flex: 1,
  },
  scrollContent: {
    flexGrow: 1,
  },
  editorContainer: {
    flex: 1,
    margin: 16,
    borderRadius: 8,
    padding: 16,
  },
  titleInput: {
    fontSize: 18,
    fontWeight: 'bold',
    paddingVertical: 12,
    borderBottomWidth: 1,
    marginBottom: 16,
  },
  contentInput: {
    fontSize: 16,
    lineHeight: 24,
    minHeight: 200,
    textAlignVertical: 'top',
  },
  wordCount: {
    alignItems: 'flex-end',
    marginTop: 8,
  },
  wordCountText: {
    fontSize: 12,
    opacity: 0.7,
  },
  toolbar: {
    flexDirection: 'row',
    alignItems: 'center',
    padding: 16,
    borderTopWidth: 1,
  },
  toolbarButton: {
    padding: 8,
    marginRight: 16,
  },
  toolbarSpacer: {
    flex: 1,
  },
  draftButton: {
    minWidth: 80,
  },
});

export default WriteLetter;

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions