在RN中已经提供了好多UI组件供开发者使用,社区里也有很多第三方的开源的UI组件可供开发者使用。 但是在应用开发的过程中,我们难免需要自定义UI组件来满足需求。
本文主要通过实现一个滑动(双击)放大、缩小图片得到组件来分享下如何创建一个UI Component。
正文之前
刚开始我只是看了官网的这篇文章然而并没有去看系统的组件(Image,ScrollView 等) 的实现。想到要做一个能够监测手势去放大缩小图片的时候我的第一个想法就是“如何在RN中监控、获取用户手势”。看完这些我已经方了,赶紧看下源码中的ScrollView是如何处理手势的。而实际情况是 RN 中只是通过 Native UI Component 的形式调用的Native 中 的ScrollView 。至于手势动效都是 Native ScrollView自己的处理。
后来想想才发现自己走了些许弯路,RN 最终就是渲染的 Native 组件。 Native组件本身的功能都是可以用的,我们只是通过RN Native UI Component的形式给 Native 组件设置一些初始的属性。
实现思路
现在再想“如何去做一个滑动(双击)放大\缩小图片得到组件”,我们只要在Native中写一个有此功能的组件,通过RN使用一下这个组件就行了。想到 RN 中图片加载已经使用了Fresco框架,而且已经有一个基于Fresco的图片放大缩小控件PhotoDraweeView。所以,我们可以直接用PhotoDraweeView。思路明确后,开始干。
Native中定义控件
在 Native 中,就是一个 PhotoDraweeView ,暴露两个方法,一个用来在 JS 中通过Props设置控件的数据源(图片地址),另一个用来真正的加载图片。
public class ZoomableImageView extends PhotoDraweeView {
private Uri mUri = null;
public ZoomableImageView(Context context) {
super(context);
}
public void setSource(@Nullable String source, @NonNull ResourceDrawableIdHelper resourceDrawableIdHelper) {
if (source != null) {
try {
mUri = Uri.parse(source);
if (mUri.getScheme() == null) {
mUri = null;
}
} catch (Exception ignore) {
}
}
}
//from PhotoDraweeView demo
public void updateIfNeeded() {
if (mUri == null) {
return; //不关心占位图
}
PipelineDraweeControllerBuilder controller = Fresco.newDraweeControllerBuilder();
controller.setUri(mUri);
controller.setOldController(getController());
// You need setControllerListener
controller.setControllerListener(new BaseControllerListener<ImageInfo>() {
@Override
public void onFinalImageSet(String id, ImageInfo imageInfo, Animatable animatable) {
super.onFinalImageSet(id, imageInfo, animatable);
if (imageInfo == null) {
return;
}
update(imageInfo.getWidth(), imageInfo.getHeight());
}
});
setController(controller.build());
setOnPhotoTapListener(new OnPhotoTapListener() {
@Override
public void onPhotoTap(View view, float x, float y) {
Toast.makeText(view.getContext(), "onPhotoTap : x = " + x + ";" + " y = " + y,
Toast.LENGTH_SHORT).show();
}
});
setOnViewTapListener(new OnViewTapListener() {
@Override
public void onViewTap(View view, float x, float y) {
Toast.makeText(view.getContext(), "onViewTap", Toast.LENGTH_SHORT).show();
}
});
setOnLongClickListener(new View.OnLongClickListener() {
@Override
public boolean onLongClick(View v) {
Toast.makeText(v.getContext(), "onLongClick", Toast.LENGTH_SHORT).show();
return true;
}
});
}
}
View Manager
如果你对View Manager不了解,可以参考官网的介绍。
View Manager主要决定组件的Name和它会暴露出去哪些属性。
- createViewInstance方法创建具体的组件
- getName 方法决定了Component 的 Name ,通过JS调用的时候是通过Name来调用的
- @ReactProp 决定了Native暴露出的属性与JS中控件props的对应关系
- 值得一提的是onAfterUpdateTransaction 方法,这个回调会在所有有@ReactProp注解的方法更新完属性后被调用。我们通过它来加载具体的图片。
public class ZoomableImageViewManager extends SimpleViewManager<ZoomableImageView> {
private static final String REACT_CLASS = "ZoomableImageViewAndroid";
private ResourceDrawableIdHelper mResourceDrawableIdHelper;
ZoomableImageViewManager(ReactApplicationContext context) {
mResourceDrawableIdHelper = new ResourceDrawableIdHelper();
}
@Override
public String getName() {
return REACT_CLASS;
}
@Override
protected ZoomableImageView createViewInstance(ThemedReactContext reactContext) {
return new ZoomableImageView(reactContext);
}
@ReactProp(name = "src")
public void setSource(ZoomableImageView view, @Nullable String source) {
view.setSource(source, mResourceDrawableIdHelper);
}
@Override
protected void onAfterUpdateTransaction(ZoomableImageView view) {
super.onAfterUpdateTransaction(view);
view.updateIfNeeded();
}
}
Register the ViewManager
将组件注册到一个React Package中。
public class ZoomableImageViewPackage implements ReactPackage {
@Override
public List<Class<? extends JavaScriptModule>> createJSModules() {
return Collections.emptyList();
}
@Override
public List<ViewManager> createViewManagers(ReactApplicationContext reactContext) {
List<ViewManager> viewManagers = new ArrayList<>();
viewManagers.add(new ZoomableImageViewManager(reactContext));
return viewManagers;
}
@Override
public List<NativeModule> createNativeModules(ReactApplicationContext reactContext) {
return Collections.emptyList();
}
}
Native初始化时加载对应的React Package
初始化时需要初始化React Package,之后才能在 RN JS 中调用到此Native组件。
@Override
protected List<ReactPackage> getPackages() {
return Arrays.<ReactPackage>asList(
new MainReactPackage(),
new ZoomableImageViewPackage(),
);
}
RN JS 中使用Native组件
export default class ZoomableImageView extends Component {
static propTypes = {
src: PropTypes.string,
...View.propTypes
};
render() {
if (this.props.src) {
console.warn('src is null');
}
return <ZoomableImageViewAndroid src={this.props.src} />
}
return null
}
}
const ZoomableImageViewAndroid = requireNativeComponent('ZoomableImageViewAndroid', ZoomableImageView);
最后,一个支持双击,双指滑动实现放大缩小的图片浏览控件就实现了。 它使用PhotoDraweeView来实现具体手势监听和图片缩放功能。PhotoDraweeView还有一些有用配置,如果要暴露给JS来配置,则要通过@ReactProp注解来暴露出来。
功能实现的同时,实际上是对Custom UI Component的加深理解。 Android本身控件库已经是非常丰富,通过Custom UI Component的形式,可以快速实现在JS中使用Native组件。
那么问题来了, RN中的ListView为何不是通过Native来实现的呢?