Custom native tags

Updated at:

You can define custom native tags to extend the built-in tag set of dynamic cards with platform-specific UI components on Android, iOS, and HarmonyOS.

Implement custom tags on the client

Android

Inherit CCardWidget and implement its abstract methods. The primary method is onCreateView, which returns the custom View.

public class CustomCubeWidget extends CCardWidget {
    private static final String TAG = "CustomCubeWidget";
    private Map<String, Object> createMap;
    private TextView root;

    public CustomCubeWidget(Context context) {
        super(context);
    }

    @Override
    public View onCreateView(Map<String, Object> params, int width, int height) {
        MPLogger.debug(TAG, "onCreateView:" + width + "," + height);
        createMap = params;
        for (String key : params.keySet()) {
            MPLogger.debug(TAG, key + ":" + params.get(key));
        }
        if (root == null) {
            root = new TextView(getContext());
            setText(root, params.get("value") + "");
            root.setOnClickListener(new View.OnClickListener() {
                @Override
                public void onClick(View view) {
                    Map<String, Object> eventParams = new HashMap<>();
                    eventParams.put("p1", "widget:" + System.currentTimeMillis());
                    CubeService.instance().getEngine().sendEvent(createMap, "on-WidgetToCube", eventParams);
                }
            });
        }
        return root;
    }

    @Override
    public void onReuse(Map<String, Object> params, int width, int height) {
        MPLogger.debug(TAG, "onReuse");
    }

    @Override
    public void onUpdateData(Map<String, Object> params) {
        MPLogger.debug(TAG, "onUpdateData");
    }

    @Override
    public void onRecycleAndCached() {
        MPLogger.debug(TAG, "onRecycleAndCached");
    }

    @Override
    public boolean canReuse() {
        MPLogger.debug(TAG, "canReuse");
        return false;
    }

    @Override
    public void onDestroy() {
        MPLogger.debug(TAG, "onDestroy");
    }

    @JsMethod
    public void cubeToWidget(JSONObject param, CubeJSCallback callback) {
        if (root != null) {
            setText(root, "Received a custom method call from the card. Parameter: " + param.toJSONString());
        }
        callback.invoke("cubeToWidget callback data: " + System.currentTimeMillis());
    }

    private void setText(TextView tv, String msg) {
        tv.setText("I am a native component. Click me to notify the card of the event callback, " + msg);
    }
}

iOS

Inherit CCardWidget and implement its protocol methods. The onCreateView and updateData methods are required. Other methods are optional.

#import "CustomCardView.h"

@interface CustomCardView ()

@property (nonatomic, strong) UILabel *titleLabel;

@end

@implementation CustomCardView

// Required
/**
 * Creates the UI. This represents the view UI of the component to be displayed.
 *
 * @param data  A map containing data declared by the component on the card. This includes styles, events, and properties. The corresponding keys are DATA_KEY_STYLES, DATA_KEY_EVENTS, and DATA_KEY_ATTRS.
 * @param size  The size (width and height) of the view.
 * @return An active UIView.
 */
- (UIView *)onCreateView:(NSDictionary *)data size:(CGSize)size {
    NSLog(@"Print data:%@", data);
    
    UIView *customView = [[UIView alloc] initWithFrame:CGRectMake(0, 0, size.width, size.height)];
    customView.backgroundColor = [UIColor redColor];
    
    self.titleLabel = [[UILabel alloc] initWithFrame:CGRectMake(0, 0, size.width, 30)];
    // The data source is the provided data.
    self.titleLabel.text = @"A";
    [customView addSubview:self.titleLabel];
    
    return customView;
}

// Required
/**
 * Updates the component data.
 * @param data
 */
- (void)onUpdateData:(NSDictionary *)data {
    NSLog(@"Print data:%@", data);
}

/**
 *  Prepares data for an existing control retrieved from the reuse pool before it is used.
 *  @param data Initialization data.
 *  @param size The size of the component when it is reused.
 */
- (void)onReuse:(NSDictionary *)data size:(CGSize)size {
    NSLog(@"Print data:%@", data);
    NSLog(@"Print size:%@", size);
    
    // The data source is the provided data.
    self.titleLabel.text = @"B";

}

/**
 *  Specifies whether the component can be reused.
 *  To improve efficiency, extension components can support reuse. For example, if a custom tag component is removed from the current view due to a data update, the component is placed in a reuse pool if it supports reuse. The next time the component is displayed, it is retrieved directly from the reuse pool.
 * @return YES: Reuse. NO: Do not reuse.
 */
- (BOOL)canReuse {
    return YES;
}

/**
 *  Cleans up a reusable component. If the component supports reuse (canReuse returns true), the onRecycleAndCached method is called after the component enters the reuse pool. This indicates that the component is off-screen and in the cache, and its resources need to be cleared.
 */
- (void)onRecycleAndCache {
    // Clear the SubView content on the custom view.
    self.titleLabel.text = @"";
}

HarmonyOS

Create a custom component by calling Component

@Component
export struct MyCustomComponent {
  @State
  node: CKNode = new CKNode();
  @State
  cardInstance: CubeCard | null = null;
  @State
  text: string = ""

  aboutToAppear(): void {
    if (this.node) {
      this.node.component = this;
      this.updateWidgetData()
      this.node.onChange = (type: NodeChangeType, changeNode: CKNode, childNode: CKNode | null) => {
        this.updateWidgetData()
      };
    }
  }

  build() {
    if (this.node) {
      Column() {
        Text("I am a native component. Click me to notify the card of the event callback, "+this.text).onClick((event) => {
          let args: Record<string, object> = {}
          args["p1"] = "widget:" + systemDateTime.getTime() as ESObject
          CWidgetApi.getInstance().sendEventToJS(this.node, 'on-WidgetToCube', args)
        })
      }
      .width(this.node.size.width)
      .height(this.node.size.height)
      .position(this.node.position)
    }
  }

  updateWidgetData(): void {
    this.text = this.node.attributes.get("value") as string
  }

  public cubeToWidget(instance: FalconInstance, args: object, func: IFalconCallback): void {
    this.text = "Received a custom method call from the card. Parameter: "+JSON.stringify(args)
    func.invoke("cubeToWidget callback data: " + systemDateTime.getTime())
  }
}

Create a ComponentBuilder

@Builder
export function buildMyCustomComponent(builder: ComponentBuilder) {
  MyCustomComponent({
    node: builder.node,
    cardInstance: builder.cardInstance
  })
}

Register custom tags on the client

Android

The first parameter is the custom tag name. On the card, this tag is enclosed in angle brackets (<>). Add a prefix to prevent conflicts with built-in dynamic card tags. The second parameter is the fully qualified class name of the custom tag implementation. Make sure this class is not obfuscated by ProGuard.

Collection<CubeWidgetInfo> widgetInfos = new LinkedList<>();
widgetInfos.add(new CubeWidgetInfo("custom-widget", CustomCubeWidget.class.getName()));
CubeService.instance().getEngine().registerWidgets(widgetInfos);

iOS

The first parameter is the custom tag name, which must match the tag used in the card. On the card, this tag is enclosed in angle brackets (<>). Add a prefix to prevent conflicts with built-in dynamic card tags. The second parameter is the class name of the implementation class.

CubeWidgetInfo *widgetInfo = [CubeWidgetInfo new];
widgetInfo.tag = @"custom-widget";
widgetInfo.className = [CustomCardView class];
    
NSMutableArray *widgetArray = [NSMutableArray array];
[widgetArray addObject:widgetInfo];
[[[CubeService sharedInstance] getEngine] registerWidgets:[NSArray arrayWithArray:widgetArray]];

HarmonyOS

The first parameter of CWidgetInfo is the custom tag name. On the card, this tag is enclosed in angle brackets (<>). Add a prefix to prevent conflicts with built-in dynamic card tags. The second parameter is the WrappedBuilder instance.

let customBuilder: WrappedBuilder<[ComponentBuilder]> = wrapBuilder(buildMyCustomComponent)
let customWidgetInfo: CWidgetInfo = new CWidgetInfo("custom-widget", customBuilder)
CubeEngine.getInstance().registerWidgets([customWidgetInfo])

Call custom tags from the card

Use the tag name you defined to reference the custom component. In this example, the tag is custom-widget.

<template>
    <div class="root">
        <text class="message" :value="message" @click="onClick()"></text>
        <custom-widget ref="widget" class="custom" :value="message2" @on-WidgetToCube="clientToCube(param)">
        </custom-widget>
    </div>
</template>

<script>

    export default {
        data: {
            message : 'This is the text of the card. Click me to call the custom component.',
            message2 : 'This is the value passed to the custom component.'
        },
        beforeCreate() {
            this.message = 'This is the text of the card. Click me to call the custom component.'
            this.message2 = 'This is the value passed to the custom component.'
        },
        didAppear() {
            
        },        
        methods: {
            onClick() {
                console.info('invoke on-click event');
                this.$refs.widget.cubeToWidget({
                    'auto':"This is the parameter sent to the custom component's method."
                },ret=>{
                    this.message = "Received the callback from the custom component's method. Parameters: "+JSON.stringify(ret);
                });
            },
            // Handles the event callback from the custom component.
            clientToCube(param){
                console.info(param.p1)
                this.message2 = "Received the event callback from the custom component. Parameters: "+JSON.stringify(param)
            }
        }
    }
</script>

<style>
    .root {
        display: flex;
        flex-direction: column;
        justify-content: center;
        align-items: center;
        background-color: white;
        width: 100%;
        height: 800rpx;
    }
    .custom {
        color: black;
        font-size: 20rpx;
        width: 100%;
        height: 600rpx;
    }
    .message {
        color: black;
        font-size: 20rpx;
        width: 100%;
        height: 200rpx;
    }
</style>

Methods for calling custom tags from the card

Card call

this.$refs.widget.cubeToWidget({
  'auto':"I am the parameter sent to the custom component method"
},ret=>{
  this.message = "Received the callback from the custom component method. Parameter: "+JSON.stringify(ret);
});

Client reception

Android

@JsMethod
public void cubeToWidget(JSONObject param, CubeJSCallback callback) {}

iOS

// Configure the method based on convention.
CK_EXPORT_METHOD(@selector(cubeToWidget:callBack:))
    
// Custom tag calls a native method.
- (void)cubeToWidget:(NSDictionary *)params callBack:(CubeModuleMethodCallback)callback {
    
}

HarmonyOS

public cubeToWidget(instance: FalconInstance, args: object, func: IFalconCallback): void {}

Client-side callbacks for notification cards

Client notification

Android

CubeService.instance().getEngine().sendEvent(Map<String, Object> componentData, String eventName, @Nullable Map<String, Object> eventParams)

iOS

/**
 * self.data: The data from the onCreateView method.
 * eventName: The method name on the card, for example, @"on-WidgetToCube".
 * eventParams: The parameters passed to the card, for example, @{@"p1":@"Custom tag calls a cube method"}.
 */
[[[CubeService sharedInstance] getEngine] 
     sendEvent:self.data 
     eventName:eventName
     eventParams:eventParams];

HarmonyOS

CWidgetApi.getInstance().sendEventToJS(node: CKNode, methodName: string, args: Record<string, Object> | null)

Card receives notification

The card listens for client notifications by binding @eventName to a handler, as shown in the following example.

<custom-widget ref="widget" class="custom" :value="message2" @on-WidgetToCube="clientToCube(param)">