React Native 封装原生 Android WebView 组件完整教程

Viewed 0

React Native 已经封装了大部分常见组件,如 ScrollView 和 TextInput,但不可能涵盖所有组件。有时开发者可能需要封装自己的原生组件,幸运的是在 React Native 中封装和植入已有组件非常简单。

原生视图需要通过 ViewManager 的派生类(通常继承 SimpleViewManager)来创建和管理。SimpleViewManager 适用于常见场景,能处理背景颜色、透明度、Flexbox 布局等公共属性。这些管理器本质上是单例,React Native 只会为每个管理器创建一个实例。它们创建原生视图并交给 NativeViewHierarchyManager,后者在需要时委托它们设置和更新视图属性。ViewManager 还代理视图的所有委托,并向 JavaScript 发送对应事件。

封装原生视图的基本步骤如下:

  1. 创建 ViewManager 的子类。
  2. 实现 createViewInstance 方法。
  3. 使用 @ReactProp 或 @ReactPropGroup 注解导出属性设置器。
  4. 将视图管理类注册到应用程序包的 createViewManagers 中。
  5. 实现对应的 JavaScript 模块。

本文以 WebView 为例,演示如何封装原生 Android UI 组件。

1. 创建 ViewManager 的子类

创建 ReactWebViewManager 类,继承自 SimpleViewManager。getName 方法返回的名称用于在 JavaScript 端引用该原生视图类型。

public class ReactWebViewManager extends SimpleViewManager<WebView> {
    @Override
    public String getName() {
        return "AndroidRCTWebView";
    }
}

2. 实现 createViewInstance 方法

视图在 createViewInstance 中创建,并初始化为默认状态。所有属性设置通过后续的 updateView 进行。

@Override
protected WebView createViewInstance(ThemedReactContext reactContext) {
    WebView webView = new WebView(reactContext);
    webView.setWebViewClient(new WebViewClient() {
        @Override
        public boolean shouldOverrideUrlLoading(WebView view, String url) {
            view.loadUrl(url);
            return true;
        }
    });
    return webView;
}

3. 通过 @ReactProp 注解导出属性设置方法

使用 @ReactProp 注解导出属性设置方法,供 JavaScript 使用。方法第一个参数是视图实例,第二个参数是属性值,返回类型为 void。支持的类型包括 boolean、int、float、double、String、Boolean、Integer、ReadableArray、ReadableMap。

@ReactProp(name = "url")
public void setUrl(WebView view, @Nullable String url) {
    view.loadUrl(url);
}

@ReactProp(name = "html")
public void setHtml(WebView view, @Nullable String html) {
    view.loadData(html, "text/html; charset=utf-8", "UTF-8");
}

完整的 ReactWebViewManager 类源码如下:

import android.support.annotation.Nullable;
import android.webkit.WebView;
import android.webkit.WebViewClient;
import com.facebook.react.uimanager.SimpleViewManager;
import com.facebook.react.uimanager.ThemedReactContext;
import com.facebook.react.uimanager.annotations.ReactProp;

public class ReactWebViewManager extends SimpleViewManager<WebView> {
    @Override
    public String getName() {
        return "AndroidRCTWebView";
    }

    @Override
    protected WebView createViewInstance(ThemedReactContext reactContext) {
        WebView webView = new WebView(reactContext);
        webView.setWebViewClient(new WebViewClient() {
            @Override
            public boolean shouldOverrideUrlLoading(WebView view, String url) {
                view.loadUrl(url);
                return true;
            }
        });
        return webView;
    }

    @ReactProp(name = "url")
    public void setUrl(WebView view, @Nullable String url) {
        view.loadUrl(url);
    }

    @ReactProp(name = "html")
    public void setHtml(WebView view, @Nullable String html) {
        view.loadData(html, "text/html; charset=utf-8", "UTF-8");
    }
}

4. 注册 ViewManager

在 Java 中将视图控制器注册到应用中,通过实现 ReactPackage 接口,并在 createViewManagers 方法中返回视图管理类列表。

import com.facebook.react.ReactPackage;
import com.facebook.react.bridge.NativeModule;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.uimanager.ViewManager;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;

public class AppReactPackage implements ReactPackage {
    @Override
    public List<NativeModule> createNativeModules(ReactApplicationContext reactContext) {
        return Collections.emptyList();
    }

    @Override
    public List<ViewManager> createViewManagers(ReactApplicationContext reactContext) {
        return Arrays.<ViewManager>asList(
                new ReactWebViewManager()
        );
    }
}

然后在 MainApplication.java 的 getPackages 方法中添加该包:

public class MainApplication extends Application implements ReactApplication {
    private final ReactNativeHost mReactNativeHost = new ReactNativeHost(this) {
        @Override
        public boolean getUseDeveloperSupport() {
            return BuildConfig.DEBUG;
        }

        @Override
        protected List<ReactPackage> getPackages() {
            return Arrays.<ReactPackage>asList(
                    new MainReactPackage(),
                    new AppReactPackage() // 添加自定义包
            );
        }

        @Override
        protected String getJSMainModuleName() {
            return "index";
        }
    };

    @Override
    public ReactNativeHost getReactNativeHost() {
        return mReactNativeHost;
    }

    @Override
    public void onCreate() {
        super.onCreate();
        SoLoader.init(this, false);
    }
}

注意导入包时避免与 Android 内部类名冲突。

5. 实现对应的 JavaScript 模块

创建 JavaScript 模块,通过 propTypes 描述属性类型,并使用 requireNativeComponent 引用原生组件。

import { requireNativeComponent, View } from 'react-native';
import PropTypes from 'prop-types';

let iface = {
    name: 'WebView',
    propTypes: {
        url: PropTypes.string,
        html: PropTypes.string,
        ...View.propTypes // 包含默认的 View 属性
    }
};

module.exports = requireNativeComponent('AndroidRCTWebView', iface);

requireNativeComponent 的第一个参数是原生视图名称,第二个参数是描述组件接口的对象。propTypes 用于检查属性使用是否正确。如果需要在 JavaScript 端处理事件,可以将原生组件封装在普通 React 组件中。

使用封装的原生组件

在 React Native 组件中导入并使用 WebView:

import React from 'react';
import { View } from 'react-native';
import WebView from '../common/WebView';

export default class FavoritePage extends React.Component {
    render() {
        return (
            <View>
                <WebView
                    url="https://www.baidu.com"
                    style={{width: 200, height: 400}}/>
            </View>
        );
    }
}

现在,封装的原生组件可以在 React Native 中使用。但为了处理事件(如滚动),需要进一步操作。

事件处理

为了在 JavaScript 层处理原生事件(如 WebView 的滚动),需要继承 WebView 并重写事件方法,将事件传递给 JavaScript 层。

创建 RCTWebView 类,重写 onScrollChanged 方法:

import android.content.Context;
import android.util.AttributeSet;
import android.webkit.WebView;
import com.facebook.react.bridge.Arguments;
import com.facebook.react.bridge.ReactContext;
import com.facebook.react.bridge.WritableMap;
import com.facebook.react.uimanager.events.RCTEventEmitter;

public class RCTWebView extends WebView {
    public RCTWebView(Context context) {
        super(context);
    }

    public RCTWebView(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    public RCTWebView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }

    @Override
    protected void onScrollChanged(int l, int t, int oldl, int oldt) {
        super.onScrollChanged(l, t, oldl, oldt);
        WritableMap event = Arguments.createMap();
        event.putInt("ScrollX", l);
        event.putInt("ScrollY", t);
        ReactContext reactContext = (ReactContext) getContext();
        reactContext.getJSModule(RCTEventEmitter.class).receiveEvent(
                getId(), "topChange", event);
    }
}

然后修改 ReactWebViewManager 的 createViewInstance 方法,返回 RCTWebView 实例:

@Override
protected WebView createViewInstance(ThemedReactContext reactContext) {
    WebView webView = new RCTWebView(reactContext); // 使用自定义 WebView
    webView.setWebViewClient(new WebViewClient() {
        @Override
        public boolean shouldOverrideUrlLoading(WebView view, String url) {
            view.loadUrl(url);
            return true;
        }
    });
    return webView;
}

在 JavaScript 层,更新 WebView 组件以处理事件:

import React from 'react';
import { requireNativeComponent, View } from 'react-native';
import PropTypes from 'prop-types';

var RCTWebView = requireNativeComponent('AndroidRCTWebView', WebView, {
    nativeOnly: {onChange: true}
});

export default class WebView extends React.Component {
    static propTypes = {
        url: PropTypes.string,
        html: PropTypes.string,
        onScrollChange: PropTypes.func,
        ...View.propTypes
    };

    _onChange(event) {
        if (!this.props.onScrollChange) {
            return;
        }
        this.props.onScrollChange({ScrollX: event.nativeEvent.ScrollX, ScrollY: event.nativeEvent.ScrollY});
    }

    render() {
        return (
            <RCTWebView {...this.props} onChange={(event) => this._onChange(event)}/>
        );
    }
}

onChange 函数映射到 Java 层的 topChange 事件,并通过 onScrollChange 属性传递滚动数据。

最后,在组件中使用并处理滚动事件:

export default class FavoritePage extends React.Component {
    constructor(props) {
        super(props);
        this.state = { a: 22 };
    }

    render() {
        return (
            <View>
                <WebView
                    onScrollChange={this.onWebViewScroll}
                    url="https://www.baidu.com"
                    style={{width: 200, height: 200}}/>
            </View>
        );
    }

    onWebViewScroll(event) {
        console.log(event);
    }
}

现在,当在 WebView 上滑动时,滚动事件数据会在控制台打印出来。通过这种方式,可以扩展封装的原生组件以支持复杂交互。

0 Answers