React Native 中封装 Android 原生 UI 组件指南

Viewed 0

Android 原生UI组件

在现代App开发中,存在大量原生UI部件,包括平台内置组件、第三方库或自定义收藏。React Native 已经封装了常见组件如 ScrollViewTextInput,但无法覆盖所有情况。幸运的是,在React Native应用中集成现有原生组件非常简单。

本指南将介绍如何构建原生UI组件,以React Native核心库中的ImageView为例,演示完整流程。假设您已有Android编程经验。

ImageView 示例

为了让JavaScript端使用ImageView,需要完成以下准备工作。原生视图由ViewManager的派生类(通常使用SimpleViewManager)创建和管理。SimpleViewManager适用于此场景,因为它支持公共属性如背景颜色、透明度和Flexbox布局。

这些子类是单例,React Native为每个管理器创建一个实例。它们创建原生视图并交给NativeViewHierarchyManager处理属性更新,同时代理视图事件并发送回JavaScript。

提供原生视图的基本步骤包括:创建ViewManager子类、实现createViewInstance方法、使用@ReactProp@ReactPropGroup注解导出属性设置器、在应用程序包中注册视图管理类,以及实现JavaScript模块。

1. 创建 ViewManager 的子类

ReactImageManager为例,它继承自SimpleViewManager<ReactImageView>,其中ReactImageView是管理的视图类型。getName方法返回的名称用于JavaScript端引用。

public class ReactImageManager extends SimpleViewManager<ReactImageView> {
  public static final String REACT_CLASS = "RCTImageView";
  ReactApplicationContext mCallerContext;

  public ReactImageManager(ReactApplicationContext reactContext) {
    mCallerContext = reactContext;
  }

  @Override
  public String getName() {
    return REACT_CLASS;
  }
}

2. 实现方法 createViewInstance

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

@Override
public ReactImageView createViewInstance(ThemedReactContext context) {
  return new ReactImageView(context, Fresco.newDraweeControllerBuilder(), null, mCallerContext);
}

3. 通过 @ReactProp@ReactPropGroup 注解导出属性设置器

导出属性时,使用@ReactProp@ReactPropGroup注解的公共方法。第一个参数是视图实例,第二个是属性值,支持类型包括booleanintfloatdoubleStringBooleanIntegerReadableArrayReadableMap(Kotlin中对应类型)。

@ReactProp注解必须包含name参数指定JavaScript端属性名,可选参数如defaultBooleandefaultIntdefaultFloat提供默认值。

@ReactProp(name = "src")
public void setSrc(ReactImageView view, @Nullable ReadableArray sources) {
  view.setSource(sources);
}

@ReactProp(name = "borderRadius", defaultFloat = 0f)
public void setBorderRadius(ReactImageView view, float borderRadius) {
  view.setBorderRadius(borderRadius);
}

@ReactProp(name = ViewProps.RESIZE_MODE)
public void setResizeMode(ReactImageView view, @Nullable String resizeMode) {
  view.setScaleType(ImageResizeMode.toScaleType(resizeMode));
}

4. 注册 ViewManager

将视图管理器注册到应用的createViewManagers方法中。

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

完成代码后,重新编译应用(例如运行yarn android)。

5. 实现对应的 JavaScript 模块

创建JavaScript模块定义接口,建议使用TypeScript或注释说明结构。

import { requireNativeComponent } from 'react-native';

/**
 * Composes `View`.
 *
 * - src: Array<{url: string}>
 * - borderRadius: number
 * - resizeMode: 'cover' | 'contain' | 'stretch'
 */
export default requireNativeComponent('RCTImageView');

requireNativeComponent接受原生视图名。对于复杂逻辑如事件处理,可以用React组件封装。

事件处理

处理用户事件如缩放或拖动时,原生事件应触发JavaScript端视图事件,通过getId()关联视图。

在原生视图中发送事件:

public void onReceiveNativeEvent() {
  WritableMap event = Arguments.createMap();
  event.putString("message", "MyMessage");
  ReactContext reactContext = (ReactContext)getContext();
  reactContext.getJSModule(RCTEventEmitter.class)
    .receiveEvent(getId(), "topChange", event);
}

ViewManager中注册事件映射到JavaScript回调:

public Map getExportedCustomBubblingEventTypeConstants() {
  return MapBuilder.builder().put(
    "topChange",
    MapBuilder.of(
      "phasedRegistrationNames",
      MapBuilder.of("bubbled", "onChange")
    )
  ).build();
}

JavaScript端封装组件处理事件:

import React, { useCallback } from 'react';

const MyCustomView = ({ onChangeMessage, ...props }) => {
  const onChange = useCallback((event) => {
    if (!onChangeMessage) {
      return;
    }
    onChangeMessage(event.nativeEvent.message);
  }, [onChangeMessage]);

  return <RCTMyCustomView {...props} onChange={onChange} />;
};

const RCTMyCustomView = requireNativeComponent('RCTMyCustomView');

与 Android Fragment 的整合实例

对于更精细的控制,可以使用Android Fragments替代直接返回View,以便利用生命周期方法如onViewCreatedonPauseonResume

1. 创建一个自定义视图

继承FrameLayout创建CustomView

package com.mypackage;

import android.content.Context;
import android.graphics.Color;
import android.widget.FrameLayout;
import android.widget.TextView;
import androidx.annotation.NonNull;

public class CustomView extends FrameLayout {
  public CustomView(@NonNull Context context) {
    super(context);
    this.setPadding(16,16,16,16);
    this.setBackgroundColor(Color.parseColor("#5FD3F3"));
    TextView text = new TextView(context);
    text.setText("Welcome to Android Fragments with React Native.");
    this.addView(text);
  }
}

2. 创建一个 Fragment

定义MyFragment管理视图生命周期。

package com.mypackage;

import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import androidx.fragment.app.Fragment;
import com.mypackage.CustomView;

public class MyFragment extends Fragment {
  CustomView customView;

  @Override
  public View onCreateView(LayoutInflater inflater, ViewGroup parent, Bundle savedInstanceState) {
    super.onCreateView(inflater, parent, savedInstanceState);
    customView = new CustomView(this.getContext());
    return customView;
  }

  @Override
  public void onViewCreated(View view, Bundle savedInstanceState) {
    super.onViewCreated(view, savedInstanceState);
    // 添加自定义逻辑
  }

  @Override
  public void onPause() {
    super.onPause();
    // 添加自定义逻辑
  }

  @Override
  public void onResume() {
    super.onResume();
    // 添加自定义逻辑
  }

  @Override
  public void onDestroy() {
    super.onDestroy();
    // 添加自定义逻辑
  }
}

3. 创建 ViewManager 子类

MyViewManager继承ViewGroupManager<FrameLayout>,处理Fragment创建和布局。

package com.mypackage;

import android.view.Choreographer;
import android.view.View;
import android.view.ViewGroup;
import android.widget.FrameLayout;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.fragment.app.FragmentActivity;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.bridge.ReadableArray;
import com.facebook.react.common.MapBuilder;
import com.facebook.react.uimanager.annotations.ReactPropGroup;
import com.facebook.react.uimanager.ViewGroupManager;
import com.facebook.react.uimanager.ThemedReactContext;
import java.util.Map;

public class MyViewManager extends ViewGroupManager<FrameLayout> {
  public static final String REACT_CLASS = "MyViewManager";
  public final int COMMAND_CREATE = 1;
  private int propWidth;
  private int propHeight;
  ReactApplicationContext reactContext;

  public MyViewManager(ReactApplicationContext reactContext) {
    this.reactContext = reactContext;
  }

  @Override
  public String getName() {
    return REACT_CLASS;
  }

  @Override
  public FrameLayout createViewInstance(ThemedReactContext reactContext) {
    return new FrameLayout(reactContext);
  }

  @Nullable
  @Override
  public Map<String, Integer> getCommandsMap() {
    return MapBuilder.of("create", COMMAND_CREATE);
  }

  @Override
  public void receiveCommand(@NonNull FrameLayout root, String commandId, @Nullable ReadableArray args) {
    super.receiveCommand(root, commandId, args);
    int reactNativeViewId = args.getInt(0);
    int commandIdInt = Integer.parseInt(commandId);
    switch (commandIdInt) {
      case COMMAND_CREATE:
        createFragment(root, reactNativeViewId);
        break;
      default: {}
    }
  }

  @ReactPropGroup(names = {"width", "height"}, customType = "Style")
  public void setStyle(FrameLayout view, int index, Integer value) {
    if (index == 0) {
      propWidth = value;
    }
    if (index == 1) {
      propHeight = value;
    }
  }

  public void createFragment(FrameLayout root, int reactNativeViewId) {
    ViewGroup parentView = (ViewGroup) root.findViewById(reactNativeViewId);
    setupLayout(parentView);
    final MyFragment myFragment = new MyFragment();
    FragmentActivity activity = (FragmentActivity) reactContext.getCurrentActivity();
    activity.getSupportFragmentManager()
      .beginTransaction()
      .replace(reactNativeViewId, myFragment, String.valueOf(reactNativeViewId))
      .commit();
  }

  public void setupLayout(View view) {
    Choreographer.getInstance().postFrameCallback(new Choreographer.FrameCallback() {
      @Override
      public void doFrame(long frameTimeNanos) {
        manuallyLayoutChildren(view);
        view.getViewTreeObserver().dispatchOnGlobalLayout();
        Choreographer.getInstance().postFrameCallback(this);
      }
    });
  }

  public void manuallyLayoutChildren(View view) {
    int width = propWidth;
    int height = propHeight;
    view.measure(
      View.MeasureSpec.makeMeasureSpec(width, View.MeasureSpec.EXACTLY),
      View.MeasureSpec.makeMeasureSpec(height, View.MeasureSpec.EXACTLY));
    view.layout(0, 0, width, height);
  }
}

4. 注册 ViewManager

在自定义包中注册视图管理器。

package com.mypackage;

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

public class MyPackage implements ReactPackage {
  @Override
  public List<ViewManager> createViewManagers(ReactApplicationContext reactContext) {
    return Arrays.<ViewManager>asList(new MyViewManager(reactContext));
  }
}

5. 注册 Package

在应用主类中添加自定义包。

@Override
protected List<ReactPackage> getPackages() {
  List<ReactPackage> packages = new PackageList(this).getPackages();
  packages.add(new MyPackage()); // 添加自定义包
  return packages;
}

6. 执行 JavaScript 模块

首先,导出原生组件。

import { requireNativeComponent } from 'react-native';
export const MyViewManager = requireNativeComponent('MyViewManager');

然后,在React组件中调用create命令初始化Fragment。

import React, { useEffect, useRef } from 'react';
import { UIManager, findNodeHandle } from 'react-native';
import { MyViewManager } from './my-view-manager';

const createFragment = (viewId: number) =>
  UIManager.dispatchViewManagerCommand(
    viewId,
    UIManager.MyViewManager.Commands.create.toString(),
    [viewId],
  );

export const MyView = ({ style }) => {
  const ref = useRef(null);
  useEffect(() => {
    const viewId = findNodeHandle(ref.current);
    createFragment(viewId!);
  }, []);

  return (
    <MyViewManager
      style={{
        ...(style || {}),
        height: style?.height !== undefined ? style.height : '100%',
        width: style?.width !== undefined ? style.width : '100%',
      }}
      ref={ref}
    />
  );
};

属性设置器可使用@ReactProp注解,参考前面ImageView示例。

0 Answers