RN开发过程中,React Native是将原生控件封装桥接成JS组件来使用的,这保证了其性能的高效性。但是有时候官方封装的常用组件不能满足需求,就需要结合原生UI使用,例如:对原生实现的UI复用;复杂UI仍然需要原生自定义View实现。接下来就简单记录下RN开发过程中调用原生UI的流程。
例如下面这个UI效果,就需要用到Android原生自定义View实现:
以上图实现效果为例,分别从Android端和RN端说明。
Android端
1. 创建自定义控件CircleMenu;
public class CircleMenu extends View {
private Context mContext;
/** 点击外面 */
public static final int DL_TOUCH_OUTSIDE = -2;
/** 点击中间点 */
public static final int DL_TOUCH_CENTER = -1;
/** 中心点的坐标X */
private float mCoreX;
/** 中心点的坐标Y */
private float mCoreY;
/** 是否有中心按钮 */
private boolean mHasCoreMenu;
/** 中心按钮的默认背景 */
private int mCoreMenuNormalBackgroundColor;
/** 中间按钮的描边颜色 */
private int mCoreMenuStrokeColor;
/** 中间按钮的描边边框大小 */
private float mCoreMenuStrokeSize;
/** 中间按钮选中时的背景颜色 */
private int mCoreMenuSelectedBackgroundColor;
/** 中心按钮圆形半径 */
private float mCoreMenuRoundRadius;
/** 菜单数量 */
private int mRoundMenuNumber;
/** 菜单偏移角度 */
private float mRoundMenuDeviationDegree;
/** 菜单图片 */
private ArrayList mRoundMenuDrawableList = new ArrayList<>();
/** 是否画每个菜单扇形到中心点的直线 */
private boolean mIsDrawLineToCenter;
/** 菜单正常背景颜色 */
private int mRoundMenuNormalBackgroundColor;
/** 菜单点击背景颜色 */
private int mRoundMenuSelectedBackgroundColor;
/** 菜单描边颜色 */
private int mRoundMenuStrokeColor;
/** 菜单描边宽度 */
private float mRoundMenuStrokeSize;
/** 菜单图片与中心点的距离 百分数 */
private float mRoundMenuDistance;
/** 点击状态 -2是无点击,-1是点击中心圆,其他是点击菜单 */
private int onClickState = DL_TOUCH_OUTSIDE;
/** 记录按下时间,超过预设时间算长按按钮 */
private long mTouchTime;
public CircleMenu(Context context) {
super(context);
init(context, null);
}
public CircleMenu(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
init(context, attrs);
}
public CircleMenu(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init(context, attrs);
}
/**
* 初始化数据
* @param context
* @param attrs
*/
private void init(Context context, @Nullable AttributeSet attrs) {
mContext = context;
// 加载默认资源
final Resources res = getResources();
final boolean defaultHasCoreMenu = res.getBoolean(R.bool.default_has_core_menu);
final int defaultCoreMenuNormalBackgroundColor = res.getColor(R.color.default_core_menu_normal_background_color);
final int defaultCoreMenuStrokeColor = res.getColor(R.color.default_core_menu_stroke_color);
final float defaultCoreMenuStrokeSize = res.getDimension(R.dimen.default_core_menu_stroke_size);
final int defaultCoreMenuSelectedBackgroundColor = res.getColor(R.color.default_core_menu_selected_background_color);
final float defaultCoreMenuRoundRadius = res.getDimension(R.dimen.default_core_menu_round_radius);
final int defaultRoundMenuNumber = res.getInteger(R.integer.default_round_menu_number);
final int defaultRoundMenuDeviationDegree = res.getInteger(R.integer.default_round_menu_deviation_degree);
final boolean defaultIsDrawLineToCenter = res.getBoolean(R.bool.default_is_draw_line_to_center);
final int defaultRoundMenuNormalBackgroundColor = res.getColor(R.color.default_round_menu_normal_background_color);
final int defaultRoundMenuSelectedBackgroundColor = res.getColor(R.color.default_round_menu_selected_background_color);
final int defaultRoundMenuStrokeColor = res.getColor(R.color.default_round_menu_stroke_color);
final float defaultRoundMenuStrokeSize = res.getDimension(R.dimen.default_round_menu_stroke_size);
final float defaultRoundMenuDistance = res.getFraction(R.fraction.default_round_menu_distance, 1, 1);
// 读取配置信息
TypedArray a = mContext.obtainStyledAttributes(attrs, R.styleable.DLRoundMenuView);
mHasCoreMenu = a.getBoolean(R.styleable.DLRoundMenuView_RMHasCoreMenu, defaultHasCoreMenu);
mCoreMenuNormalBackgroundColor = a.getColor(R.styleable.DLRoundMenuView_RMCoreMenuNormalBackgroundColor, defaultCoreMenuNormalBackgroundColor);
mCoreMenuStrokeColor = a.getColor(R.styleable.DLRoundMenuView_RMCoreMenuStrokeColor, defaultCoreMenuStrokeColor);
mCoreMenuStrokeSize = a.getDimension(R.styleable.DLRoundMenuView_RMCoreMenuStrokeSize, defaultCoreMenuStrokeSize);
mCoreMenuSelectedBackgroundColor = a.getColor(R.styleable.DLRoundMenuView_RMCoreMenuSelectedBackgroundColor, defaultCoreMenuSelectedBackgroundColor);
mCoreMenuRoundRadius = a.getDimension(R.styleable.DLRoundMenuView_RMCoreMenuRoundRadius, defaultCoreMenuRoundRadius);
mRoundMenuNumber = a.getInteger(R.styleable.DLRoundMenuView_RMRoundMenuNumber, defaultRoundMenuNumber);
mRoundMenuDeviationDegree = a.getInteger(R.styleable.DLRoundMenuView_RMRoundMenuDeviationDegree, defaultRoundMenuDeviationDegree);
mIsDrawLineToCenter = a.getBoolean(R.styleable.DLRoundMenuView_RMIsDrawLineToCenter, defaultIsDrawLineToCenter);
mRoundMenuNormalBackgroundColor = a.getColor(R.styleable.DLRoundMenuView_RMRoundMenuNormalBackgroundColor, defaultRoundMenuNormalBackgroundColor);
mRoundMenuSelectedBackgroundColor = a.getColor(R.styleable.DLRoundMenuView_RMRoundMenuSelectedBackgroundColor, defaultRoundMenuSelectedBackgroundColor);
mRoundMenuStrokeColor = a.getColor(R.styleable.DLRoundMenuView_RMRoundMenuStrokeColor, defaultRoundMenuStrokeColor);
mRoundMenuStrokeSize = a.getDimension(R.styleable.DLRoundMenuView_RMRoundMenuStrokeSize, defaultRoundMenuStrokeSize);
mRoundMenuDistance = a.getFraction(R.styleable.DLRoundMenuView_RMRoundMenuDistance, 1, 1, defaultRoundMenuDistance);
// 释放内存,回收资源
a.recycle();
}
/**
* 测量
* @param widthMeasureSpec
* @param heightMeasureSpec
*/
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int widthSpecSize = MeasureSpec.getSize(widthMeasureSpec);
int heightSpecSize = MeasureSpec.getSize(heightMeasureSpec);
// 左右减去2,留足空间给算术裁切
setMeasuredDimension(widthSpecSize - 2, heightSpecSize - 2);
}
/**
* 绘制
* @param canvas
*/
@SuppressLint("DrawAllocation")
@Override
protected void onDraw(Canvas canvas) {
// 拿到中心位置
mCoreX = (float) getWidth() / 2;
mCoreY = (float) getHeight() / 2;
// 搞到一个正方形画板
RectF rect = new RectF(mRoundMenuStrokeSize, mRoundMenuStrokeSize,
getWidth() - mRoundMenuStrokeSize, getHeight() - mRoundMenuStrokeSize);
// 菜单数量要大于0
if (mRoundMenuNumber > 0) {
// 每个菜单弧形的角度
float sweepAngle = (float) 360 / mRoundMenuNumber;
// 一个重要的点 0度在正X轴上,所以需要让它回到正Y轴上
// 计算真正的偏移角度
// -90度回到正Y轴;-每个菜单占据角度的一半,使得菜单中央回到正Y轴;再加上用户自己想修改的角度偏移
/** 真实菜单偏移角度 */
float mRealRoundMenuDeviationDegree = mRoundMenuDeviationDegree - (sweepAngle / 2) - 90;
for (int i = 0; i < mRoundMenuNumber; i++) {
// 画扇形
Paint paint = new Paint();
paint.setAntiAlias(true);
paint.setColor(onClickState == i?mRoundMenuSelectedBackgroundColor:mRoundMenuNormalBackgroundColor);
canvas.drawArc(rect, mRealRoundMenuDeviationDegree + (i * sweepAngle), sweepAngle, true, paint);
// 画扇形描边
paint = new Paint();
paint.setAntiAlias(true);
paint.setStrokeWidth(mRoundMenuStrokeSize);
paint.setStyle(Paint.Style.STROKE);
paint.setColor(mRoundMenuStrokeColor);
canvas.drawArc(rect, mRealRoundMenuDeviationDegree + (i * sweepAngle), sweepAngle, mIsDrawLineToCenter, paint);
// 画图案
Bitmap roundMenuDrawable = mRoundMenuDrawableList.get(i);
if (null != roundMenuDrawable){
Matrix matrix = new Matrix();
matrix.postTranslate((float) ((mCoreX + getWidth() / 2 * mRoundMenuDistance) - (roundMenuDrawable.getWidth() / 2)),
mCoreY - ((float) roundMenuDrawable.getHeight() / 2));
matrix.postRotate(mRoundMenuDeviationDegree - 90 + (i * sweepAngle), mCoreX, mCoreY);
canvas.drawBitmap(roundMenuDrawable, matrix, null);
}
}
}
//画中心圆圈
if (mHasCoreMenu) {
// 画中心圆
RectF rect1 = new RectF(mCoreX - mCoreMenuRoundRadius, mCoreY - mCoreMenuRoundRadius,
mCoreX + mCoreMenuRoundRadius, mCoreY + mCoreMenuRoundRadius);
Paint paint = new Paint();
paint.setAntiAlias(true);
paint.setStrokeWidth(mCoreMenuStrokeSize);
paint.setColor(onClickState == -1?mCoreMenuSelectedBackgroundColor:mCoreMenuNormalBackgroundColor);
canvas.drawArc(rect1, 0, 360, true, paint);
//画描边
paint = new Paint();
paint.setAntiAlias(true);
paint.setStrokeWidth(mCoreMenuStrokeSize);
paint.setStyle(Paint.Style.STROKE);
paint.setColor(mCoreMenuStrokeColor);
canvas.drawArc(rect1, 0, 360, true, paint);
}
}
}
2. 创建ReactCircleMenuManager类继承自SimpleViewManager,供RN调用;
public class ReactCircleMenuManager extends SimpleViewManager {
@Override
public String getName() {
return "ReactCircleMenu";//
}
@Override
protected View createViewInstance(final ThemedReactContext reactContext) {
//final View view = LayoutInflater.from(reactContext).inflate(R.layout.shadow_layout, null);
final CircleMenuView circleMenuView = new CircleMenuView(reactContext);
// circleMenuView.setOnMenuClickListener(new OnMenuClickListener() {
// @Override
// public void OnMenuClick(int position) {
// Log.e("TAG", "点击了:"+position);
// WritableMap event = Arguments.createMap();
// event.putInt("position", position);
// reactContext.getJSModule(RCTEventEmitter.class).receiveEvent(circleMenuView.getId(), "onMenuClick", event);
// }
// });
return circleMenuView;
}
/**
* 接收传输的颜色参数
*/
@ReactProp(name = "color")
public void setColor(View view, String color) {
view.setBackgroundColor(Color.parseColor(color));
}
/**
* 回传点击事件到RN
* @return
*/
// @Nullable
// @Override
// public Map getExportedCustomDirectEventTypeConstants() {
// return MapBuilder.builder()
// .put("onMenuClick", MapBuilder.of("registrationName", "onMenuClick"))
// .build();
// }
}
3. 创建ReactCircleMenuPackage类并实现ReactPackage,在createViewManagers方法中返回CircleManager的实例。
public class ReactCircleMenuPackage implements ReactPackage {
/**
* 用户注册Native Modules
*/
@Override
public List createNativeModules(ReactApplicationContext reactContext) {
return Collections.emptyList();
}
/**
* 用于注册Native UI Components
* @param reactContext
* @return
*/
@Override
public List createViewManagers(ReactApplicationContext reactContext) {
return Collections.singletonList(
new ReactCircleMenuManager()
);
}
}
4. 在Application中注册ReactCircleMenuPackage
public class MainApplication extends MultiDexApplication implements ReactApplication {
private static MainApplication instance;
public static MainApplication getInstance() {
return instance;
}
private ReactNativeHost mReactNativeHost = new ReactNativeHost(this) {
@Override
public boolean getUseDeveloperSupport() {
return BuildConfig.DEBUG;
}
@Override
protected List getPackages() {
return Arrays.asList(
new MainReactPackage(),
new ReactCircleMenuPackage()//添加此处
);
}
@Override
protected String getJSMainModuleName() {
return "index";
}
};
public void setReactNativeHost(ReactNativeHost reactNativeHost) {
mReactNativeHost = reactNativeHost;
}
@Override
public ReactNativeHost getReactNativeHost() {
return mReactNativeHost;
}
@Override
public void onCreate() {
super.onCreate();
instance = this;
SoLoader.init(this, /* native exopackage */ false);
}
}
ReactNative端
- 创建创建circleMenu.js文件,并通过requireNativeComponent创建变量ReactCircleMenu;
import React, { Component } from "react";
import PropTypes from "prop-types";
import { View, requireNativeComponent } from "react-native";
//requireNativeComponent函数中的第一个参数就是ReactCircleMenuManager.getName返回的值。
const RCTCircleMenu = requireNativeComponent("ReactCircleMenu", {
propTypes: {
color: PropTypes.number,//定义传输属性类型
...View.propTypes // 包含默认的View的属性
}
});
export default class CircleMenu extends Component {
render() {
return (
);
}
/**
* 点击事件
*/
onMenuClick(e){
console.log(e.nativeEvent.position.toString());
}
}
- 调用CircleMenu.js文件
import React, {Component} from "react";
import {View, requireNativeComponent} from "react-native";
import CircleMenu from "../../widget/CircleMenu";
export default class DevPhoneIR extends Component {
render() {
return (
);
}
}
这样就实现了RN调用封装的原生组件,上边代码中可以看出,RN可以自定义属性给Android原生View,Android原生组件的点击事件也可以回传给RN。
当然也可以把原生xml布局封装为View形式使用,只需修改ReactCircleMenuManager类里createViewInstance方法的返回View即可。
@Override
protected View createViewInstance(final ThemedReactContext reactContext) {
final Vibrator vibrator = (Vibrator)reactContext.getSystemService(reactContext.VIBRATOR_SERVICE);
final View view = LayoutInflater.from(reactContext).inflate(R.layout.layout, null);
final TextView textView = view.findViewById(R.id.text);
final Button btn = view.findViewById(R.id.btn);
textView.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Log.e("TAG", "点击了:");
}
});
btn.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Log.e("TAG", "点击了1:");
ConsumerIrManagerApi.getIrManager(reactContext).transmit(Constants.FREQUENCY, NecPattern.buildPattern(
Constants.TV_USER_CODE_H,Constants.TV_USER_CODE_L, 0x14));
vibrator.vibrate(80);
WritableMap event = Arguments.createMap();
event.putInt("position", 0);
reactContext.getJSModule(RCTEventEmitter.class).receiveEvent(view.getId(), "onMenuClick", event);
}
});
return view;
}