扩展原生View

大约 11 分钟

扩展原生View

Kuikly已经封装了常用的View组件,例如TextImageList等组件,如果Kuikly提供的组件不满足你的需求或者你想复用native已有的UI组件的话, Kuikly允许你将现有的UI组件通过实现一些接口来暴露给Kuikly侧使用。

下面我们以MyImageView作为例子,来看看将原生的ImageView暴露给Kuikly使用需要做哪些工作

  1. Kuikly侧: 新增ImageView组件,供业务方使用
  2. Android侧: 实现IKuiklyRenderViewExport接口,完成androidImageView暴露给Kuikly
  3. iOS侧: 实现KuiklyRenderViewExportProtocol协议,完成iOSUIImageView暴露给Kuikly
  4. 鸿蒙侧: 继承KuiklyRenderBaseView类,按需实现callcreateArkUIViewsetProp方法

Kuikly侧

Kuikly组件由这四个部分组成:

  1. viewName: 组件对应到原生组件的名字
  2. Attr: 组件的属性,用于指定该组件含有哪些属性
  3. Event: 用于接收来自原生组件发送的事件
  4. test: 组件本身支持的方法,最终实现是在原生侧

我们完成上述的四部分,即可实现一个Kuikly侧的组件。

  1. 我们首先新建MyImageView, 并继承DeclarativeBaseView类,继承DeclarativeBaseView的时候,需要指定MyImageView对应的AttrEvent类型
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")
        }
    }    
}
  1. 接着在viewName()方法中,我们返回了HRImageView,表示该KuiklyMyImageView对应到原生组件暴露给Kuikly侧的名字是HRImageView

  2. 最后我们在createAttrcreateEvent中,返回了我们在DeclarativeBaseView泛型中指定的类型对应的实例

  3. 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中方法中,我们将传递进来的属性透传给了原生的组件
  1. 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)
        }
    }
}
  1. 实现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),但不支持同步返回结果。

  1. 最后我们编写MyImageView组件声明式的api方法,供业务侧调用
fun ViewContainer<*, *>.MyImage(init: MyImageView.() -> Unit) {
    addChild(MyImageView(), init)
} 
  1. 业务侧使用方式
    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平台如何暴露ImageViewKuikly

android侧

android侧要完成原生ImageView暴露给Kuikly侧,需要完成以下步骤

  1. 新建HRImageView并继承原生的ImageView, 然后实现IKuiklyRenderViewExport接口
  2. 实现IKuiklyRenderViewExport中的setProp方法
  3. 实现IKuiklyRenderViewExport中的call方法
  4. 注册HRImageView,完成HRImageView暴露给Kuikly

我们完成上述4部分后,即可实现将HRImageView组件暴露给Kuikly

  1. 我们首先新建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中实现了setPropcall方法

  1. setProp方法: Kuikly侧组件支持的属性事件调用都会走到这个方法。例如上述的属性src事件loadSuccess
  2. call方法: Kuikly侧组件支持的方法调用都会走到这个方法。例如上述的方法test

实现src属性和loadSuccess事件

前面讲到KuiklyImageView组件,它支持src属性loadSuccess事件,在运行的时候会调用到我们新建的HRImageViewsetProp方法,我们来看下如何实现

实现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)
    }
}

最后重写HRImageViewsetImageDrawable,在此方法中触发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侧,需要完成以下步骤

  1. 新建HRImageView并继承原生的UIImageView, 然后实现KuiklyRenderViewExportProtocol协议
  2. 实现KuiklyRenderViewExportProtocol中的hrv_setPropWithKey方法
  3. 实现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侧的HRImageViewhrv_setPropWithKey方法。 然后KUIKLY_SET_CSS_COMMON_PROP会使用运行时,调用HRImageView中匹配setCss_propKey的方法。以src属性作为例子,会调用 到HRImageViewsetCss_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侧的HRImageViewhrv_callWithMethod方法。 然后KUIKLY_CALL_CSS_METHOD会使用运行时,调用HRImageView中匹配css_method的方法。以test方法作为例子,会调用 到HRImageViewcss_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侧,需要完成以下步骤

  1. 新建HRImageView并继承KuiklyRenderBaseView
  2. 重写KuiklyRenderBaseView中的setProp方法
  3. 实现KuiklyRenderBaseView中的call方法
  4. 实现KuiklyRenderBaseView中的createArkUIView方法
  5. 注册HRImageView,将其暴露给Kuikly

我们完成上述5部分后,即可实现将HRImageView组件暴露给Kuikly

我们首先新建HRImageView,继承KuiklyRenderBaseView

@Observed
export class HRImageView extends KuiklyRenderBaseView {
    static readonly VIEW_NAME = 'HRImageView';
    ...
}

重写setProp方法

前面讲到KuiklyMyImageView组件,它支持src属性loadSuccess事件,在运行的时候会调用到我们新建的HRImageViewsetProp方法,我们来看下如何实现

实现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属性时,对应到端的组件,会走到HRImageViewsetProp方法,propKey为属性的名字,即src,而propValue为属性对应的值。HRImageView识别到propKeysrc时,使用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字符串,这个调用会对应到HRImageViewcall方法, 即

@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}`);
    }
    ...
}

在上述的代码中,我们实现KuiklyRenderBaseViewcall方法,识别到方法名字为"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"