扩展原生View
扩展原生View
Kuikly
已经封装了常用的View组件,例如Text
、Image
和List
等组件,如果Kuikly
提供的组件不满足你的需求或者你想复用native已有的UI组件的话, Kuikly
允许你将现有的UI组件通过实现一些接口来暴露给Kuikly
侧使用。
下面我们以MyImageView
作为例子,来看看将原生的ImageView
暴露给Kuikly
使用需要做哪些工作
Kuikly
侧: 新增ImageView组件,供业务方使用Android
侧: 实现IKuiklyRenderViewExport
接口,完成android
的ImageView
暴露给Kuikly
侧iOS
侧: 实现KuiklyRenderViewExportProtocol
协议,完成iOS
的UIImageView
暴露给Kuikly
侧- 鸿蒙侧: 继承
KuiklyRenderBaseView
类,按需实现call
、createArkUIView
和setProp
方法
Kuikly侧
Kuikly
组件由这四个部分组成:
viewName
: 组件对应到原生组件的名字Attr
: 组件的属性,用于指定该组件含有哪些属性Event
: 用于接收来自原生组件发送的事件test
: 组件本身支持的方法,最终实现是在原生侧
我们完成上述的四部分,即可实现一个Kuikly
侧的组件。
- 我们首先新建
MyImageView
, 并继承DeclarativeBaseView
类,继承DeclarativeBaseView
的时候,需要指定MyImageView
对应的Attr
和Event
类型
class MyImageView : DeclarativeBaseView<MyImageAttr, MyImageEvent>() {
override fun createAttr(): MyImageAttr {
return MyImageAttr()
}
override fun createEvent(): MyImageEvent {
return MyImageEvent()
}
override fun viewName(): String {
return "HRImageView"
}
fun test() {
performTaskWhenRenderViewDidLoad {
renderView?.callMethod("test", "params")
}
}
}
接着在
viewName()
方法中,我们返回了HRImageView
,表示该Kuikly
的MyImageView
对应到原生组件暴露给Kuikly
侧的名字是HRImageView
最后我们在
createAttr
和createEvent
中,返回了我们在DeclarativeBaseView
泛型中指定的类型对应的实例Attr
表示Kuikly
侧组件支持的属性集。在这个例子中,我们返回了MyImageAttr
来表示支持的属性
class MyImageAttr : Attr() {
/**
* 设置src
* @param src 数据源
* @return this
*/
fun src(src: String): MyImageAttr {
"src" with src
return this
}
}
// MyImageAttr中,我们编写了src方法,表示MyImageView组件支持src属性,用于设置MyImageView的数据源。
// 在src中方法中,我们将传递进来的属性透传给了原生的组件
Event
表示Kuikly
侧组件支持的事件。在这个例子中,我们返回了MyImageEvent
来表示支持的事件
/**
* MyImageEvent支持loadSuccess事件,表示Image的图片加载上屏时
* 会触发loadSuccess中的handler闭包调用
*/
class MyImageEvent : Event() {
/*
* 图片成功加载时回调
* 由原生侧的组件触发
*/
fun loadSuccess(handler: (LoadSuccessParams) -> Unit) {
register(LOAD_SUCCESS) {
handler(LoadSuccessParams.decode(it))
}
}
companion object {
const val LOAD_SUCCESS = "loadSuccess"
}
}
data class LoadSuccessParams(
val src: String
) {
companion object {
fun decode(params: Any?): LoadSuccessParams {
val tempParams = params as? JSONObject ?: JSONObject()
val src = tempParams.optString("src", "")
return LoadSuccessParams(src)
}
}
}
- 实现
test
方法
class MyImageView : DeclarativeBaseView<MyImageAttr, MyImageEvent>() {
...
fun test() {
performTaskWhenRenderViewDidLoad {
renderView?.callMethod("test", "params")
}
}
...
}
test为组件自身暴露出去的方法,这种方法真正的实现在原生侧实现的,在这个例子中,如果MyImageView对应到原生的组件为HRImageView,那这个test方法会调用到HRImageView的call("test", "params")方法中
注意
View的方法支持异步回调结果,即调用方法时传入回调函数renderView?.callMethod("test", "params", callback)
,但不支持同步返回结果。
- 最后我们编写
MyImageView
组件声明式的api方法,供业务侧调用
fun ViewContainer<*, *>.MyImage(init: MyImageView.() -> Unit) {
addChild(MyImageView(), init)
}
- 业务侧使用方式
override fun body(): ViewBuilder {
val ctx = this
return {
MyImage {
attr {
size(176f, 132f)
src("https://vfiles.gtimg.cn/wuji_dashboard/xy/starter/844aa82b.png")
}
event {
loadSuccess {
}
}
}
}
}
完成上述步骤后, 业务方使用可以使用这个MyImageView
组件了,但是具体的渲染还需要对应的平台实现,下面我们来看android
平台和iOS
平台如何暴露ImageView
给Kuikly
侧
android侧
android
侧要完成原生ImageView
暴露给Kuikly
侧,需要完成以下步骤
- 新建
HRImageView
并继承原生的ImageView
, 然后实现IKuiklyRenderViewExport
接口 - 实现
IKuiklyRenderViewExport
中的setProp
方法 - 实现
IKuiklyRenderViewExport
中的call
方法 - 注册
HRImageView
,完成HRImageView
暴露给Kuikly
侧
我们完成上述4部分后,即可实现将HRImageView
组件暴露给Kuikly
侧
- 我们首先新建HRImageView,并继承原生的
ImageView
,然后实现IKuiklyRenderViewExport
接口
open class HRImageView(context: Context) : ImageView(context), IKuiklyRenderViewExport {
override fun setProp(propKey: String, propValue: Any): Boolean {
return super.setProp(propKey, propValue)
}
override fun call(method: String, params: String?, callback: KuiklyRenderCallback?): Any? {
return super.call(method, params, callback)
}
}
HRImageView
中实现了setProp
和call
方法
setProp
方法:Kuikly
侧组件支持的属性和事件调用都会走到这个方法。例如上述的属性src
和事件loadSuccess
call
方法:Kuikly
侧组件支持的方法调用都会走到这个方法。例如上述的方法test
实现src属性和loadSuccess事件
前面讲到Kuikly
的ImageView
组件,它支持src属性和loadSuccess事件,在运行的时候会调用到我们新建的HRImageView
的setProp
方法,我们来看下如何实现
实现src属性
open class HRImageView(context: Context) : ImageView(context), IKuiklyRenderViewExport {
private var src = ""
override fun setProp(propKey: String, propValue: Any): Boolean {
return when (propKey) {
"src" -> {
setSrc(propValue as String)
true
}
else -> super.setProp(propKey, propValue)
}
}
private fun setSrc(url: String) {
if (src == url) {
return
}
src = url
setImageDrawable(null)
// 加载并设置图片
val creator = Picasso.get().load(src)
val target = object : Target {
override fun onBitmapLoaded(bitmap: Bitmap, from: Picasso.LoadedFrom) {
setImageDrawable(BitmapDrawable(bitmap))
}
override fun onBitmapFailed(e: Exception?, errorDrawable: Drawable?) {
setImageDrawable(null)
}
override fun onPrepareLoad(placeHolderDrawable: Drawable?) {
}
}
creator.into(target)
}
}
当Kuikly侧的MyImageView运行到设置src属性时,对应到端的组件,会走到HRImageView的setProp方法,propKey为属性的名字,即src,而propValue为 属性对应的值。HRImageView识别到propKey为src时,使用Kuikly侧组件传递过来的src来加载Drawable,然后设置给HRImageView。
实现loadSuccess事件
open class HRImageView(context: Context) : ImageView(context), IKuiklyRenderViewExport {
private var loadSuccessCallback: KuiklyRenderCallback? = null
override fun setProp(propKey: String, propValue: Any): Boolean {
return when (propKey) {
...
"loadSuccess" -> {
loadSuccessCallback = propValue as KuiklyRenderCallback
true
}
else -> super.setProp(propKey, propValue)
}
}
private fun setSrc(url: String) {
if (src == url) {
return
}
src = url
setImageDrawable(null)
// 加载并设置图片
val creator = Picasso.get().load(src)
val target = object : Target {
override fun onBitmapLoaded(bitmap: Bitmap, from: Picasso.LoadedFrom) {
setImageDrawable(BitmapDrawable(bitmap))
}
override fun onBitmapFailed(e: Exception?, errorDrawable: Drawable?) {
setImageDrawable(null)
}
override fun onPrepareLoad(placeHolderDrawable: Drawable?) {
}
}
creator.into(target)
}
override fun setImageDrawable(drawable: Drawable?) {
super.setImageDrawable(drawable)
loadSuccessCallback?.invoke(mapOf(
PROP_SRC to src
))
}
}
loadSuccess表示,原生的ImageView的图片加载成功后,回调该事件给Kuikly侧的组件,即MyImageView。 所以我们在HRImageView中定义了private var loadSuccessCallback: KuiklyRenderCallback? = null
变量,并在setProp方法中赋值
override fun setProp(propKey: String, propValue: Any): Boolean {
return when (propKey) {
...
"loadSuccess" -> {
loadSuccessCallback = propValue as KuiklyRenderCallback
true
}
else -> super.setProp(propKey, propValue)
}
}
最后重写HRImageView
的setImageDrawable,在此方法中触发loadSuccessCallback回调
实现call方法
在Kuikly侧的ImageView含有一个test方法,该方法实现为:
fun test() {
performTaskWhenRenderViewDidLoad {
renderView?.callMethod("test", "params")
}
}
我们看到renderView?.callMethod
方法传递了test方法名字和一个params字符串,这个调用会对应到HRImageView的call方法, 即
open class HRImageView(context: Context) : ImageView(context), IKuiklyRenderViewExport {
override fun call(method: String, params: String?, callback: KuiklyRenderCallback?): Any? {
return when(method) {
"test" -> callTestMethod(params)
else -> super.call(method, params, callback)
}
}
private fun callTestMethod(params: String?) {
Log.d("HRImageView", "callTestMethod: $params")
}
}
在上述的代码中,我们重写HRImageView的侧call
方法,识别到方法名字为"test"时, 调用callTestMethod
方法,以此来响应Kuikly侧的ImageView.test方法的调用
注册HRImageView到Kuikly中
原生侧完成HRImageView的编写后,还需要注册暴露给Kuikly侧,指定这个UI组件对应Kuikly侧组件的名字。我们在实现了KuiklyRenderViewDelegatorDelegate
接口的类中重写registerExternalRenderView方法, 然后调用renderViewExport完成HRImageView的注册暴露
override fun registerExternalRenderView(kuiklyRenderExport: IKuiklyRenderExport) {
super.registerExternalRenderView(kuiklyRenderExport)
with(kuiklyRenderExport) {
renderViewExport("HRImageView", { context ->
HRImageView(context)
})
}
}
注意
"HRImageView"为Kuikly中ImageView.getName返回的字符串
iOS侧
iOS
侧要完成原生UIImageView
暴露给Kuikly
侧,需要完成以下步骤
- 新建
HRImageView
并继承原生的UIImageView
, 然后实现KuiklyRenderViewExportProtocol
协议 - 实现
KuiklyRenderViewExportProtocol
中的hrv_setPropWithKey
方法 - 实现
KuiklyRenderViewExportProtocol
中的hrv_callWithMethod
方法
我们完成上述3部分后,即可实现将HRImageView
组件暴露给Kuikly
侧
我们首先新建HRImageView
并继承原生的UIImageView
, 然后实现KuiklyRenderViewExportProtocol
协议
注意
类名必须与Kuikly侧的ImageView.viewName()返回的值一样
#import <UIKIt/UIKit.h>
#import "KuiklyRenderViewExportProtocol.h"
NS_ASSUME_NONNULL_BEGIN
/*
* @brief 暴露给Kotlin侧调用的Image组件
*/
@interface HRImageView : UIImageView<KuiklyRenderViewExportProtocol>
@end
NS_ASSUME_NONNULL_END
实现hrv_setPropWithKey方法
接下来,我们实现hrv_setPropWithKey,并且调用KUIKLY_SET_CSS_COMMON_PROP宏。
#import "HRImageView.h"
#import "KRComponentDefine.h"
/*
* @brief 暴露给Kotlin侧调用的Image组件
*/
@interface HRImageView()
@end
@implementation HRImageView
@synthesize hr_rootView;
#pragma mark - KuiklyRenderViewExportProtocol
...
- (void)hrv_setPropWithKey:(NSString *)propKey propValue:(id)propValue {
KUIKLY_SET_CSS_COMMON_PROP;
}
...
@end
实现src属性
当Kuikly
侧的ImageView
调用到ImageAttr
的src方法时,会调用到iOS侧的HRImageView
的hrv_setPropWithKey
方法。 然后KUIKLY_SET_CSS_COMMON_PROP会使用运行时,调用HRImageView
中匹配setCss_propKey
的方法。以src
属性作为例子,会调用 到HRImageView
的setCss_src方法。因此,我们在HRImageView
中新增setCss_src方法,以响应Kuikly
侧的调用
#import "HRImageView.h"
#import "HRComponentDefine.h"
/*
* @brief 暴露给Kotlin侧调用的Image组件
*/
@interface HRImageView()
@end
@implementation HRImageView
@synthesize hr_rootView;
#pragma mark - KuiklyRenderViewExportProtocol
...
- (void)hrv_setPropWithKey:(NSString *)propKey propValue:(id)propValue {
KUIKLY_SET_CSS_COMMON_PROP;
}
- (void)setCss_src:(NSString *)css_src {
1. css_src为kuikly传递过来的参数
2. 使用css_src去下载图片得到一个UIImage
3. 最后将UIImage设置给UIImageVIew
}
...
@end
实现loadSuccess事件
loadSuccess回调的实现步骤与src相似。
#import "HRImageView.h"
#import "HRComponentDefine.h"
/*
* @brief 暴露给Kotlin侧调用的Image组件
*/
@interface HRImageView()
/** 图片加载成功回调事件 */
@property (nonatomic, strong, nullable) KuiklyRenderCallback css_loadSuccess;
@end
@implementation HRImageView
@synthesize hr_rootView;
#pragma mark - KuiklyRenderViewExportProtocol
...
- (void)hrv_setPropWithKey:(NSString *)propKey propValue:(id)propValue {
KUIKLY_SET_CSS_COMMON_PROP;
}
- (void)setCss_loadSuccess:(KuiklyRenderCallback)css_loadSuccess {
if (_css_loadSuccess != css_loadSuccess) {
_css_loadSuccess = css_loadSuccess;
if (css_loadSuccess && self.image) {
[self p_fireLoadSuccessEvent];
}
}
}
- (void)setImage:(UIImage *)image {
if (image && self.image != image) {
[self p_fireLoadSuccessEvent];
}
[super setImage:image];
}
-(void)p_fireLoadSuccessEvent {
if (_css_loadSuccess) {
_css_loadSuccess( @{@"src" : self.css_src ?: @"" } );
}
}
...
@end
实现hrv_callWithMethod方法
在实现完属性和回调后,我们来看看如何实现Kuikly
侧的test
方法调用。
#import "HRImageView.h"
#import "HRComponentDefine.h"
/*
* @brief 暴露给Kotlin侧调用的Image组件
*/
@interface HRImageView()
@end
@implementation HRImageView
@synthesize hr_rootView;
#pragma mark - KuiklyRenderViewExportProtocol
...
- (NSString * _Nullable)hrv_callWithMethod:(NSString *)method params:(NSString *)params callback:(KuiklyRenderCallback)callback {
KUIKLY_CALL_CSS_METHOD;
return nil;
}
...
@end
当Kuikly
侧的ImageView
调用test方法时,会调用到iOS侧的HRImageView
的hrv_callWithMethod
方法。 然后KUIKLY_CALL_CSS_METHOD会使用运行时,调用HRImageView
中匹配css_method
的方法。以test
方法作为例子,会调用 到HRImageView
的css_test方法。因此,我们在HRImageView
中新增css_test方法,以响应Kuikly
侧的调用
#import "HRImageView.h"
#import "HRComponentDefine.h"
/*
* @brief 暴露给Kotlin侧调用的Image组件
*/
@interface HRImageView()
@end
@implementation HRImageView
@synthesize hr_rootView;
#pragma mark - KuiklyRenderViewExportProtocol
...
- (NSString * _Nullable)hrv_callWithMethod:(NSString *)method params:(NSString *)params callback:(KuiklyRenderCallback)callback {
KUIKLY_CALL_CSS_METHOD;
return nil;
}
- (void)css_test {
// 实现test方法
}
...
@end
完成以上步骤后,即可在iOS侧扩展Kuikly
组件
提示
iOS侧的kuikly组件是通过运行时暴露给Kuikly侧,因此无需手动注册
鸿蒙侧
鸿蒙侧要完成原生Image
暴露给Kuikly
侧,需要完成以下步骤
- 新建
HRImageView
并继承KuiklyRenderBaseView
类 - 重写
KuiklyRenderBaseView
中的setProp
方法 - 实现
KuiklyRenderBaseView
中的call
方法 - 实现
KuiklyRenderBaseView
中的createArkUIView
方法 - 注册
HRImageView
,将其暴露给Kuikly
侧
我们完成上述5部分后,即可实现将HRImageView
组件暴露给Kuikly
侧
我们首先新建HRImageView
,继承KuiklyRenderBaseView
类
@Observed
export class HRImageView extends KuiklyRenderBaseView {
static readonly VIEW_NAME = 'HRImageView';
...
}
重写setProp方法
前面讲到Kuikly
的MyImageView
组件,它支持src属性和loadSuccess事件,在运行的时候会调用到我们新建的HRImageView
的setProp
方法,我们来看下如何实现
实现src属性
@Observed
export class HRImageView extends KuiklyRenderBaseView {
static readonly VIEW_NAME = 'HRImageView';
cssSrc: string | null = null;
image: PixelMap | undefined = undefined;
setProp(propKey: string, propValue: KRAny | KuiklyRenderCallback): boolean {
switch (propKey) {
case 'src':
this.setSrc(propValue as string);
return true;
default:
return super.setProp(propKey, propValue);
}
}
setSrc(url: string): void {
1. 将this.cssSrc设为kuikly传递过来的参数
2. 使用this.cssSrc去下载图片得到一个PixelMap
3. 最后将PixelMap设置给this.image
}
}
当Kuikly侧的MyImageView
运行到设置src
属性时,对应到端的组件,会走到HRImageView
的setProp
方法,propKey
为属性的名字,即src
,而propValue
为属性对应的值。HRImageView
识别到propKey
为src
时,使用kuikly侧组件传递过来的src
来加载PixelMap
。
实现loadSuccess事件
@Observed
export class HRImageView extends KuiklyRenderBaseView {
static readonly VIEW_NAME = 'HRImageView';
src: string | null = null;
image: PixelMap | undefined = undefined;
loadSuccessCallback: KuiklyRenderCallback | null = null;
setProp(propKey: string, propValue: KRAny | KuiklyRenderCallback): boolean {
switch (propKey) {
case 'src':
this.setSrc(propValue as string);
return true;
case 'loadSuccess':
this.setLoadSuccessCallback(propValue as KuiklyRenderCallback);
return true;
default:
return super.setProp(propKey, propValue);
}
}
setSrc(url: string): void {
1. 将this.src设为kuikly传递过来的参数
2. 使用this.src去下载图片得到一个PixelMap
3. 最后将PixelMap设置给this.image
}
setLoadSuccessCallback(callback: KuiklyRenderCallback): void {
if (this.loadSuccessCallback != callback) {
this.loadSuccessCallback = callback;
if (this.loadSuccessCallback != null && this.image) {
this.fileLoadSuccess();
}
}
}
setImage(image: PixelMap): void {
if (image && this.image != image) {
this.image = image;
this.fileLoadSuccess();
}
}
fileLoadSuccess(): void {
if (this.loadSuccessCallback) {
this.loadSuccessCallback({
"src": this.src
});
}
}
}
loadSuccess
表示,原生的PixelMap
图片加载成功后,回调该事件给Kuikly侧的组件,即MyImageView
。所以我们在HRImageView
中定义了loadSuccessCallback: KuiklyRenderCallback | null = null;
变量,并在setProp
方法中赋值,并在PixelMap
成功加载时调用loadSuccessCallback
实现call方法
在Kuikly侧的ImageView
含有一个test
方法,该方法实现为:
fun test() {
performTaskWhenRenderViewDidLoad {
renderView?.callMethod("test", "params")
}
}
我们看到renderView?.callMethod
方法传递了test
方法名字和一个params
字符串,这个调用会对应到HRImageView
的call
方法, 即
@Observed
export class HRImageView extends KuiklyRenderBaseView {
...
call(method: string, params: KTAny, callback: KuiklyRenderCallback | null): void {
switch (method) {
case 'test':
this.callTestMethod(params as string);
return;
}
}
callTestMethod(params: string) {
console.log(`HRImageView callTestMethod: ${params}`);
}
...
}
在上述的代码中,我们实现KuiklyRenderBaseView
的call
方法,识别到方法名字为"test"时, 调用callTestMethod
方法,以此来响应Kuikly侧的ImageView.test
方法的调用。
实现createArkUIView方法
createArkUIView
方法用于创建组件实际的ArkUI视图,返回ComponentContent
实例。
@Observed
export class HRImageView extends KuiklyRenderBaseView {
...
createArkUIView(): ComponentContent<KuiklyRenderBaseView> {
const uiContext = this.getUIContext() as UIContext
return new ComponentContent<KuiklyRenderBaseView>(uiContext, wrapBuilder<[KuiklyRenderBaseView]>(createMyImageView), this)
}
...
}
@Builder
function createMyImageView(view: KuiklyRenderBaseView) {
// 构造你的UI,如Column
Column(){
Image((view as HRImageView).image)
.width('100%')
.height('100%')
}.width('100%').height('100%')
}
注册HRImageView到Kuikly中
原生侧完成HRImageView的编写后,还需要注册暴露给Kuikly侧,指定这个UI组件对应Kuikly侧组件的名字。我们在实现了IKuiklyViewDelegate
接口的类中重写getCustomRenderViewCreatorRegisterMap方法:
export class KuiklyViewDelegate extends IKuiklyViewDelegate {
...
getCustomRenderViewCreatorRegisterMap(): Map<string, KRRenderViewExportCreator> {
const map: Map<string, KRRenderViewExportCreator> = new Map();
map.set(HRImageView.VIEW_NAME, () => new HRImageView());
return map;
}
}
注意
HRImageView.VIEW_NAME需要与Kuikly侧的组件名相同,为"HRImageView"