diff --git a/Android/doric/src/main/java/pub/doric/DoricRegistry.java b/Android/doric/src/main/java/pub/doric/DoricRegistry.java index 3747cdce..afc03619 100644 --- a/Android/doric/src/main/java/pub/doric/DoricRegistry.java +++ b/Android/doric/src/main/java/pub/doric/DoricRegistry.java @@ -28,6 +28,8 @@ import pub.doric.shader.StackNode; import pub.doric.shader.TextNode; import pub.doric.shader.VLayoutNode; import pub.doric.shader.ViewNode; +import pub.doric.shader.slider.SlideItemNode; +import pub.doric.shader.slider.SliderNode; import pub.doric.utils.DoricMetaInfo; import pub.doric.plugin.DoricJavaPlugin; import pub.doric.plugin.ModalPlugin; @@ -72,6 +74,8 @@ public class DoricRegistry { this.registerViewNode(ListNode.class); this.registerViewNode(ListItemNode.class); this.registerViewNode(ScrollerNode.class); + this.registerViewNode(SliderNode.class); + this.registerViewNode(SlideItemNode.class); initRegistry(this); } diff --git a/Android/doric/src/main/java/pub/doric/shader/SuperNode.java b/Android/doric/src/main/java/pub/doric/shader/SuperNode.java index 6cdcadae..bf8ed08c 100644 --- a/Android/doric/src/main/java/pub/doric/shader/SuperNode.java +++ b/Android/doric/src/main/java/pub/doric/shader/SuperNode.java @@ -148,4 +148,33 @@ public abstract class SuperNode extends ViewNode { } } + private boolean viewIdIsEqual(JSObject src, JSObject target) { + String srcId = src.asObject().getProperty("id").asString().value(); + String targetId = target.asObject().getProperty("id").asString().value(); + return srcId.equals(targetId); + } + + protected void recursiveMixin(JSObject src, JSObject target) { + JSObject srcProps = src.getProperty("props").asObject(); + JSObject targetProps = target.getProperty("props").asObject(); + JSValue oriSubviews = targetProps.getProperty("subviews"); + for (String key : srcProps.propertySet()) { + JSValue jsValue = srcProps.getProperty(key); + if ("subviews".equals(key) && jsValue.isArray()) { + JSValue[] subviews = jsValue.asArray().toArray(); + for (JSValue subview : subviews) { + if (oriSubviews.isArray()) { + for (JSValue targetSubview : oriSubviews.asArray().toArray()) { + if (viewIdIsEqual(subview.asObject(), targetSubview.asObject())) { + recursiveMixin(subview.asObject(), targetSubview.asObject()); + break; + } + } + } + } + continue; + } + targetProps.asObject().setProperty(key, jsValue); + } + } } diff --git a/Android/doric/src/main/java/pub/doric/shader/list/ListAdapter.java b/Android/doric/src/main/java/pub/doric/shader/list/ListAdapter.java index ca821c54..37123db3 100644 --- a/Android/doric/src/main/java/pub/doric/shader/list/ListAdapter.java +++ b/Android/doric/src/main/java/pub/doric/shader/list/ListAdapter.java @@ -37,7 +37,7 @@ import pub.doric.shader.ViewNode; * @Author: pengfei.zhou * @CreateDate: 2019-11-12 */ -public class ListAdapter extends RecyclerView.Adapter { +class ListAdapter extends RecyclerView.Adapter { private final ListNode listNode; String renderItemFuncId; diff --git a/Android/doric/src/main/java/pub/doric/shader/list/ListNode.java b/Android/doric/src/main/java/pub/doric/shader/list/ListNode.java index 4a8feb43..3349c844 100644 --- a/Android/doric/src/main/java/pub/doric/shader/list/ListNode.java +++ b/Android/doric/src/main/java/pub/doric/shader/list/ListNode.java @@ -44,7 +44,17 @@ public class ListNode extends SuperNode { @Override protected void blendSubNode(JSObject subProperties) { - listAdapter.blendSubNode(subProperties); + String viewId = subProperties.getProperty("id").asString().value(); + ViewNode node = getSubNodeById(viewId); + if (node != null) { + node.blend(subProperties.getProperty("props").asObject()); + } else { + JSObject oldModel = getSubModel(viewId); + if (oldModel != null) { + recursiveMixin(subProperties, oldModel); + } + listAdapter.blendSubNode(subProperties); + } } @Override @@ -81,7 +91,7 @@ public class ListNode extends SuperNode { clearSubModel(); break; case "batchCount": - this.listAdapter.batchCount = 15; + this.listAdapter.batchCount = prop.asNumber().toInt(); break; default: super.blend(view, name, prop); diff --git a/Android/doric/src/main/java/pub/doric/shader/slider/SlideAdapter.java b/Android/doric/src/main/java/pub/doric/shader/slider/SlideAdapter.java new file mode 100644 index 00000000..d67c7169 --- /dev/null +++ b/Android/doric/src/main/java/pub/doric/shader/slider/SlideAdapter.java @@ -0,0 +1,136 @@ +/* + * Copyright [2019] [Doric.Pub] + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package pub.doric.shader.slider; + +import android.text.TextUtils; +import android.util.SparseArray; +import android.view.View; +import android.view.ViewGroup; + +import androidx.annotation.NonNull; +import androidx.recyclerview.widget.RecyclerView; + +import com.github.pengfeizhou.jscore.JSArray; +import com.github.pengfeizhou.jscore.JSDecoder; +import com.github.pengfeizhou.jscore.JSNull; +import com.github.pengfeizhou.jscore.JSObject; +import com.github.pengfeizhou.jscore.JSValue; + +import pub.doric.async.AsyncResult; +import pub.doric.shader.ViewNode; + +/** + * @Description: com.github.penfeizhou.doric.widget + * @Author: pengfei.zhou + * @CreateDate: 2019-11-12 + */ +class SlideAdapter extends RecyclerView.Adapter { + + private final SliderNode sliderNode; + int itemCount = 0; + int batchCount = 3; + SparseArray itemValues = new SparseArray<>(); + + SlideAdapter(SliderNode sliderNode) { + this.sliderNode = sliderNode; + } + + @NonNull + @Override + public DoricViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { + SlideItemNode node = (SlideItemNode) ViewNode.create(sliderNode.getDoricContext(), "SlideItem"); + node.init(sliderNode); + return new DoricViewHolder(node, node.getDoricLayer()); + } + + @Override + public void onBindViewHolder(@NonNull DoricViewHolder holder, int position) { + JSValue jsValue = getItemModel(position); + if (jsValue.isObject()) { + JSObject jsObject = jsValue.asObject(); + holder.slideItemNode.setId(jsObject.getProperty("id").asString().value()); + holder.slideItemNode.blend(jsObject.getProperty("props").asObject()); + } + } + + @Override + public int getItemCount() { + return itemCount; + } + + @Override + public int getItemViewType(int position) { + JSValue value = getItemModel(position); + if (value.isObject()) { + if (value.asObject().getProperty("identifier").isString()) { + return value.asObject().getProperty("identifier").asString().value().hashCode(); + } + } + return super.getItemViewType(position); + } + + private JSValue getItemModel(final int position) { + String id = itemValues.get(position); + if (TextUtils.isEmpty(id)) { + AsyncResult asyncResult = sliderNode.callJSResponse( + "renderBunchedItems", + position, + batchCount); + try { + JSDecoder jsDecoder = asyncResult.synchronous().get(); + JSValue result = jsDecoder.decode(); + if (result.isArray()) { + JSArray jsArray = result.asArray(); + for (int i = 0; i < jsArray.size(); i++) { + JSObject itemModel = jsArray.get(i).asObject(); + String itemId = itemModel.getProperty("id").asString().value(); + itemValues.put(i + position, itemId); + sliderNode.setSubModel(itemId, itemModel); + } + return sliderNode.getSubModel(itemValues.get(position)); + } + } catch (Exception e) { + e.printStackTrace(); + } + return new JSNull(); + } else { + JSObject childModel = sliderNode.getSubModel(id); + if (childModel == null) { + return new JSNull(); + } else { + return childModel; + } + } + } + + + void blendSubNode(JSObject subProperties) { + for (int i = 0; i < itemValues.size(); i++) { + if (subProperties.getProperty("id").asString().value().equals(itemValues.valueAt(i))) { + notifyItemChanged(i); + } + } + } + + static class DoricViewHolder extends RecyclerView.ViewHolder { + SlideItemNode slideItemNode; + + DoricViewHolder(SlideItemNode node, @NonNull View itemView) { + super(itemView); + slideItemNode = node; + } + } +} diff --git a/Android/doric/src/main/java/pub/doric/shader/slider/SlideItemNode.java b/Android/doric/src/main/java/pub/doric/shader/slider/SlideItemNode.java new file mode 100644 index 00000000..88c257a0 --- /dev/null +++ b/Android/doric/src/main/java/pub/doric/shader/slider/SlideItemNode.java @@ -0,0 +1,56 @@ +/* + * Copyright [2019] [Doric.Pub] + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package pub.doric.shader.slider; + +import android.widget.FrameLayout; + +import com.github.pengfeizhou.jscore.JSObject; +import com.github.pengfeizhou.jscore.JSValue; + +import pub.doric.DoricContext; +import pub.doric.extension.bridge.DoricPlugin; +import pub.doric.shader.StackNode; + +/** + * @Description: com.github.penfeizhou.doric.widget + * @Author: pengfei.zhou + * @CreateDate: 2019-11-12 + */ +@DoricPlugin(name = "SlideItem") +public class SlideItemNode extends StackNode { + public String identifier = ""; + + public SlideItemNode(DoricContext doricContext) { + super(doricContext); + this.mReusable = true; + } + + @Override + protected void blend(FrameLayout view, String name, JSValue prop) { + if ("identifier".equals(name)) { + this.identifier = prop.asString().value(); + } else { + super.blend(view, name, prop); + } + } + + @Override + public void blend(JSObject jsObject) { + super.blend(jsObject); + getDoricLayer().getLayoutParams().width = getLayoutParams().width; + getDoricLayer().getLayoutParams().height = getLayoutParams().height; + } +} diff --git a/Android/doric/src/main/java/pub/doric/shader/slider/SliderNode.java b/Android/doric/src/main/java/pub/doric/shader/slider/SliderNode.java new file mode 100644 index 00000000..8cfceaa5 --- /dev/null +++ b/Android/doric/src/main/java/pub/doric/shader/slider/SliderNode.java @@ -0,0 +1,125 @@ +/* + * Copyright [2019] [Doric.Pub] + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package pub.doric.shader.slider; + +import android.view.View; + +import androidx.recyclerview.widget.LinearLayoutManager; +import androidx.recyclerview.widget.PagerSnapHelper; +import androidx.recyclerview.widget.RecyclerView; + +import com.github.pengfeizhou.jscore.JSObject; +import com.github.pengfeizhou.jscore.JSValue; + +import pub.doric.DoricContext; +import pub.doric.extension.bridge.DoricPlugin; +import pub.doric.shader.SuperNode; +import pub.doric.shader.ViewNode; + +/** + * @Description: pub.doric.shader + * @Author: pengfei.zhou + * @CreateDate: 2019-11-19 + */ +@DoricPlugin(name = "Slider") +public class SliderNode extends SuperNode { + private final SlideAdapter slideAdapter; + + public SliderNode(DoricContext doricContext) { + super(doricContext); + this.slideAdapter = new SlideAdapter(this); + } + + @Override + protected RecyclerView build() { + RecyclerView recyclerView = new RecyclerView(getContext()); + + LinearLayoutManager layoutManager = new LinearLayoutManager(getContext()); + layoutManager.setOrientation(LinearLayoutManager.HORIZONTAL); + recyclerView.setLayoutManager(layoutManager); + PagerSnapHelper mPagerSnapHelper = new PagerSnapHelper(); + mPagerSnapHelper.attachToRecyclerView(recyclerView); + recyclerView.setAdapter(this.slideAdapter); + return recyclerView; + } + + @Override + public ViewNode getSubNodeById(String id) { + RecyclerView.LayoutManager manager = mView.getLayoutManager(); + if (manager == null) { + return null; + } + for (int i = 0; i < manager.getChildCount(); i++) { + View view = manager.getChildAt(i); + if (view == null) { + continue; + } + SlideAdapter.DoricViewHolder viewHolder = (SlideAdapter.DoricViewHolder) mView.getChildViewHolder(view); + if (id.equals(viewHolder.slideItemNode.getId())) { + return viewHolder.slideItemNode; + } + } + return null; + } + + @Override + protected void blendSubNode(JSObject subProperties) { + String viewId = subProperties.getProperty("id").asString().value(); + ViewNode node = getSubNodeById(viewId); + if (node != null) { + node.blend(subProperties.getProperty("props").asObject()); + } else { + JSObject oldModel = getSubModel(viewId); + if (oldModel != null) { + recursiveMixin(subProperties, oldModel); + } + slideAdapter.blendSubNode(subProperties); + } + } + + @Override + public void blend(JSObject jsObject) { + super.blend(jsObject); + if (mView != null) { + mView.post(new Runnable() { + @Override + public void run() { + slideAdapter.notifyDataSetChanged(); + } + }); + } + } + + @Override + protected void blend(RecyclerView view, String name, JSValue prop) { + switch (name) { + case "itemCount": + this.slideAdapter.itemCount = prop.asNumber().toInt(); + break; + case "renderItem": + // If reset renderItem,should reset native cache. + this.slideAdapter.itemValues.clear(); + clearSubModel(); + break; + case "batchCount": + this.slideAdapter.batchCount = prop.asNumber().toInt(); + break; + default: + super.blend(view, name, prop); + break; + } + } +} diff --git a/demo/index.ts b/demo/index.ts index 21a61315..158cd4e3 100644 --- a/demo/index.ts +++ b/demo/index.ts @@ -3,4 +3,5 @@ export default [ 'src/Snake', 'src/ListDemo', 'src/ScrollerDemo', + 'src/SliderDemo', ] \ No newline at end of file diff --git a/demo/src/ListDemo.ts b/demo/src/ListDemo.ts index c8451f99..49d692a5 100644 --- a/demo/src/ListDemo.ts +++ b/demo/src/ListDemo.ts @@ -54,6 +54,12 @@ class ListPanel extends Panel { textSize: 20, height: 50, bgColor: Color.parse('#00ffff'), + }).also(it => { + let start = 0 + it.onClick = () => { + log(`clicked text:${start}`) + it.text = `${start++}` + } }), ]).also(it => { it.layoutConfig = { diff --git a/demo/src/SliderDemo.ts b/demo/src/SliderDemo.ts new file mode 100644 index 00000000..49ca4b8a --- /dev/null +++ b/demo/src/SliderDemo.ts @@ -0,0 +1,60 @@ +import { Group, Panel, List, text, gravity, Color, Stack, LayoutSpec, list, NativeCall, listItem, log, vlayout, Gravity, hlayout, slider, slideItem } from "doric"; +const colors = [ + "#f0932b", + "#eb4d4b", + "#6ab04c", + "#e056fd", + "#686de0", + "#30336b", +] +@Entry +class SliderPanel extends Panel { + build(rootView: Group): void { + rootView.addChild(vlayout([ + text({ + text: "SliderDemo", + layoutConfig: { + widthSpec: LayoutSpec.AT_MOST, + heightSpec: LayoutSpec.EXACTLY, + }, + textSize: 30, + textColor: Color.parse("#535c68"), + bgColor: Color.parse("#dff9fb"), + textAlignment: gravity().center(), + height: 50, + }), + slider({ + itemCount: 100, + renderPage: (idx) => { + return slideItem(text({ + layoutConfig: { + widthSpec: LayoutSpec.AT_MOST, + heightSpec: LayoutSpec.EXACTLY, + alignment: gravity().center(), + }, + text: `Page At Line ${idx}`, + textAlignment: gravity().center(), + textColor: Color.parse("#ffffff"), + textSize: 20, + height: 300, + bgColor: Color.parse(colors[idx % colors.length]), + }).also(it => { + let start = idx + it.onClick = () => { + it.bgColor = Color.parse(colors[++start % colors.length]) + } + })) + }, + layoutConfig: { + widthSpec: LayoutSpec.AT_MOST, + heightSpec: LayoutSpec.WRAP_CONTENT, + }, + }), + ]).also(it => { + it.layoutConfig = { + widthSpec: LayoutSpec.AT_MOST, + heightSpec: LayoutSpec.AT_MOST, + } + })) + } +} \ No newline at end of file diff --git a/iOS/Pod/Classes/DoricDriver.h b/iOS/Pod/Classes/DoricDriver.h index 1a321205..4cebc069 100644 --- a/iOS/Pod/Classes/DoricDriver.h +++ b/iOS/Pod/Classes/DoricDriver.h @@ -52,6 +52,8 @@ NS_ASSUME_NONNULL_BEGIN - (void)connectDevKit:(NSString *)url; - (void)disconnectDevKit; + +- (void)ensureSyncInMainQueue:(dispatch_block_t)block; @end NS_ASSUME_NONNULL_END diff --git a/iOS/Pod/Classes/DoricDriver.m b/iOS/Pod/Classes/DoricDriver.m index 6c535266..adc9f0aa 100644 --- a/iOS/Pod/Classes/DoricDriver.m +++ b/iOS/Pod/Classes/DoricDriver.m @@ -183,4 +183,11 @@ - (void)disconnectDevKit { } } +- (void)ensureSyncInMainQueue:(dispatch_block_t)block { + if (strcmp(dispatch_queue_get_label(DISPATCH_CURRENT_QUEUE_LABEL), dispatch_queue_get_label(dispatch_get_main_queue())) == 0) { + block(); + } else { + dispatch_async(dispatch_get_main_queue(), block); + } +} @end diff --git a/iOS/Pod/Classes/DoricRegistry.m b/iOS/Pod/Classes/DoricRegistry.m index aad88e40..44334ace 100644 --- a/iOS/Pod/Classes/DoricRegistry.m +++ b/iOS/Pod/Classes/DoricRegistry.m @@ -31,6 +31,8 @@ #import "DoricListNode.h" #import "DoricListItemNode.h" #import "DoricScrollerNode.h" +#import "DoricSliderNode.h" +#import "DoricSlideItemNode.h" @interface DoricRegistry () @@ -64,6 +66,8 @@ - (void)innerRegister { [self registerViewNode:DoricListNode.class withName:@"List"]; [self registerViewNode:DoricListItemNode.class withName:@"ListItem"]; [self registerViewNode:DoricScrollerNode.class withName:@"Scroller"]; + [self registerViewNode:DoricSliderNode.class withName:@"Slider"]; + [self registerViewNode:DoricSlideItemNode.class withName:@"SlideItem"]; } - (void)registerJSBundle:(NSString *)bundle withName:(NSString *)name { diff --git a/iOS/Pod/Classes/Shader/DoricListNode.m b/iOS/Pod/Classes/Shader/DoricListNode.m index 0986968a..10792b9b 100644 --- a/iOS/Pod/Classes/Shader/DoricListNode.m +++ b/iOS/Pod/Classes/Shader/DoricListNode.m @@ -29,6 +29,23 @@ @interface DoricTableViewCell : UITableViewCell @implementation DoricTableViewCell @end +@interface DoricTableView : UITableView +@end + +@implementation DoricTableView +- (CGSize)sizeThatFits:(CGSize)size { + if (self.subviews.count > 0) { + CGFloat width = size.width; + for (UIView *child in self.subviews) { + width = MAX(child.width, width); + } + return CGSizeMake(width, size.width); + } + return size; +} +@end + + @interface DoricListNode () @property(nonatomic, strong) NSMutableDictionary *itemViewIds; @property(nonatomic, strong) NSMutableDictionary *itemHeights; @@ -47,7 +64,7 @@ - (instancetype)initWithContext:(DoricContext *)doricContext { } - (UITableView *)build { - return [[UITableView new] also:^(UITableView *it) { + return [[DoricTableView new] also:^(UITableView *it) { it.dataSource = self; it.delegate = self; it.separatorStyle = UITableViewCellSeparatorStyleNone; @@ -129,15 +146,23 @@ - (NSDictionary *)itemModelAt:(NSUInteger)position { - (void)blendSubNode:(NSDictionary *)subModel { NSString *viewId = subModel[@"id"]; - [self.itemViewIds enumerateKeysAndObjectsUsingBlock:^(NSNumber *_Nonnull key, NSString *_Nonnull obj, BOOL *_Nonnull stop) { - if ([viewId isEqualToString:obj]) { - *stop = YES; - NSIndexPath *indexPath = [NSIndexPath indexPathForRow:[key integerValue] inSection:0]; - [UIView performWithoutAnimation:^{ - [self.view reloadRowsAtIndexPaths:@[indexPath] withRowAnimation:UITableViewRowAnimationNone]; - }]; - } - }]; + DoricViewNode *viewNode = [self subNodeWithViewId:viewId]; + if (viewNode) { + [viewNode blend:subModel[@"props"]]; + } else { + NSMutableDictionary *model = [[self subModelOf:viewId] mutableCopy]; + [self recursiveMixin:subModel to:model]; + [self setSubModel:model in:viewId]; + [self.itemViewIds enumerateKeysAndObjectsUsingBlock:^(NSNumber *_Nonnull key, NSString *_Nonnull obj, BOOL *_Nonnull stop) { + if ([viewId isEqualToString:obj]) { + *stop = YES; + NSIndexPath *indexPath = [NSIndexPath indexPathForRow:[key integerValue] inSection:0]; + [UIView performWithoutAnimation:^{ + [self.view reloadRowsAtIndexPaths:@[indexPath] withRowAnimation:UITableViewRowAnimationNone]; + }]; + } + }]; + } } - (void)callItem:(NSUInteger)position height:(CGFloat)height { @@ -154,7 +179,7 @@ - (void)callItem:(NSUInteger)position height:(CGFloat)height { - (DoricViewNode *)subNodeWithViewId:(NSString *)viewId { __block DoricViewNode *ret = nil; - dispatch_sync(dispatch_get_main_queue(), ^{ + [self.doricContext.driver ensureSyncInMainQueue:^{ for (UITableViewCell *tableViewCell in self.view.visibleCells) { if ([tableViewCell isKindOfClass:[DoricTableViewCell class]]) { DoricListItemNode *node = ((DoricTableViewCell *) tableViewCell).doricListItemNode; @@ -164,7 +189,7 @@ - (DoricViewNode *)subNodeWithViewId:(NSString *)viewId { } } } - }); + }]; return ret; } diff --git a/iOS/Pod/Classes/Shader/DoricScrollerNode.h b/iOS/Pod/Classes/Shader/DoricScrollerNode.h index a61a48cb..c1184e41 100644 --- a/iOS/Pod/Classes/Shader/DoricScrollerNode.h +++ b/iOS/Pod/Classes/Shader/DoricScrollerNode.h @@ -1,7 +1,24 @@ +/* + * Copyright [2019] [Doric.Pub] + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ // -// Created by pengfei.zhou on 2019/11/19. +// DoricScrollerNode.h +// Doric +// +// Created by pengfei.zhou on 2019/11/19. // - #import #import "DoricSuperNode.h" diff --git a/iOS/Pod/Classes/Shader/DoricScrollerNode.m b/iOS/Pod/Classes/Shader/DoricScrollerNode.m index 0c48225d..2f0900c0 100644 --- a/iOS/Pod/Classes/Shader/DoricScrollerNode.m +++ b/iOS/Pod/Classes/Shader/DoricScrollerNode.m @@ -1,7 +1,24 @@ +/* + * Copyright [2019] [Doric.Pub] + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ // -// Created by pengfei.zhou on 2019/11/19. +// DoricScrollerNode.m +// Doric +// +// Created by pengfei.zhou on 2019/11/19. // - #import "DoricScrollerNode.h" #import "DoricExtensions.h" diff --git a/iOS/Pod/Classes/Shader/DoricSlideItemNode.h b/iOS/Pod/Classes/Shader/DoricSlideItemNode.h new file mode 100644 index 00000000..195c173c --- /dev/null +++ b/iOS/Pod/Classes/Shader/DoricSlideItemNode.h @@ -0,0 +1,26 @@ +/* + * Copyright [2019] [Doric.Pub] + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +// +// DoricSlideItemNode.h +// Doric +// +// Created by pengfei.zhou on 2019/11/19. +// +#import +#import "DoricStackNode.h" + +@interface DoricSlideItemNode : DoricStackNode +@end diff --git a/iOS/Pod/Classes/Shader/DoricSlideItemNode.m b/iOS/Pod/Classes/Shader/DoricSlideItemNode.m new file mode 100644 index 00000000..ff840a5c --- /dev/null +++ b/iOS/Pod/Classes/Shader/DoricSlideItemNode.m @@ -0,0 +1,47 @@ +/* + * Copyright [2019] [Doric.Pub] + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +// +// DoricSlideItemNode.m +// Doric +// +// Created by pengfei.zhou on 2019/11/19. +// +#import "DoricSlideItemNode.h" + +@interface DoricSlideItemView : DoricStackView +@end + +@implementation DoricSlideItemView +@end + +@implementation DoricSlideItemNode + +- (instancetype)initWithContext:(DoricContext *)doricContext { + if (self = [super initWithContext:doricContext]) { + self.reusable = YES; + } + return self; +} + +- (void)initWithSuperNode:(DoricSuperNode *)superNode { + [super initWithSuperNode:superNode]; + self.reusable = YES; +} + +- (DoricStackView *)build { + return [DoricSlideItemView new]; +} +@end \ No newline at end of file diff --git a/iOS/Pod/Classes/Shader/DoricSliderNode.h b/iOS/Pod/Classes/Shader/DoricSliderNode.h new file mode 100644 index 00000000..61da6935 --- /dev/null +++ b/iOS/Pod/Classes/Shader/DoricSliderNode.h @@ -0,0 +1,27 @@ +/* + * Copyright [2019] [Doric.Pub] + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +// +// DoricSliderNode.h +// Doric +// +// Created by pengfei.zhou on 2019/11/19. +// +#import + +#import "DoricSuperNode.h" + +@interface DoricSliderNode : DoricSuperNode +@end \ No newline at end of file diff --git a/iOS/Pod/Classes/Shader/DoricSliderNode.m b/iOS/Pod/Classes/Shader/DoricSliderNode.m new file mode 100644 index 00000000..540d457c --- /dev/null +++ b/iOS/Pod/Classes/Shader/DoricSliderNode.m @@ -0,0 +1,190 @@ +/* + * Copyright [2019] [Doric.Pub] + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +// +// DoricSliderNode.m +// Doric +// +// Created by pengfei.zhou on 2019/11/19. +// +#import +#import "DoricSliderNode.h" +#import "Doric.h" +#import "DoricSlideItemNode.h" + +@interface DoricCollectionViewCell : UICollectionViewCell +@property(nonatomic, strong) DoricSlideItemNode *doricSlideItemNode; +@end + +@implementation DoricCollectionViewCell +@end + +@interface DoricSliderNode () +@property(nonatomic, strong) NSMutableDictionary *itemViewIds; +@property(nonatomic, assign) NSUInteger itemCount; +@property(nonatomic, assign) NSUInteger batchCount; +@end + +@interface DoricCollectionView : UICollectionView +@end + +@implementation DoricCollectionView +- (CGSize)sizeThatFits:(CGSize)size { + if (self.subviews.count > 0) { + CGFloat height = size.height; + for (UIView *child in self.subviews) { + height = MAX(child.height, height); + } + return CGSizeMake(height, size.height); + } + return size; +} +@end + +@implementation DoricSliderNode +- (instancetype)initWithContext:(DoricContext *)doricContext { + if (self = [super initWithContext:doricContext]) { + _itemViewIds = [NSMutableDictionary new]; + _batchCount = 15; + } + return self; +} + +- (UICollectionView *)build { + UICollectionViewFlowLayout *flowLayout = [[UICollectionViewFlowLayout alloc] init]; + [flowLayout setScrollDirection:UICollectionViewScrollDirectionHorizontal]; + + return [[[DoricCollectionView alloc] initWithFrame:CGRectZero + collectionViewLayout:flowLayout] + also:^(UICollectionView *it) { + it.backgroundColor = [UIColor whiteColor]; + it.pagingEnabled = YES; + it.delegate = self; + it.dataSource = self; + [it registerClass:[DoricCollectionViewCell class] forCellWithReuseIdentifier:@"doricCell"]; + }]; +} + +- (void)blendView:(UICollectionView *)view forPropName:(NSString *)name propValue:(id)prop { + if ([@"itemCount" isEqualToString:name]) { + self.itemCount = [prop unsignedIntegerValue]; + } else if ([@"renderPage" isEqualToString:name]) { + [self.itemViewIds removeAllObjects]; + [self clearSubModel]; + } else if ([@"batchCount" isEqualToString:name]) { + self.batchCount = [prop unsignedIntegerValue]; + } else { + [super blendView:view forPropName:name propValue:prop]; + } +} + +- (NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section { + return self.itemCount; +} + +- (CGSize)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout *)collectionViewLayout sizeForItemAtIndexPath:(NSIndexPath *)indexPath { + return CGSizeMake(self.view.width, self.view.height); +} + +- (CGFloat)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout *)collectionViewLayout minimumInteritemSpacingForSectionAtIndex:(NSInteger)section { + return 0; +} + +- (CGFloat)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout *)collectionViewLayout minimumLineSpacingForSectionAtIndex:(NSInteger)section { + return 0; +} + +- (__kindof UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath { + NSUInteger position = (NSUInteger) indexPath.row; + NSDictionary *model = [self itemModelAt:position]; + NSDictionary *props = model[@"props"]; + DoricCollectionViewCell *cell = [collectionView dequeueReusableCellWithReuseIdentifier:@"doricCell" forIndexPath:indexPath]; + if (!cell.doricSlideItemNode) { + DoricSlideItemNode *slideItemNode = [[DoricSlideItemNode alloc] initWithContext:self.doricContext]; + [slideItemNode initWithSuperNode:self]; + cell.doricSlideItemNode = slideItemNode; + [cell.contentView addSubview:slideItemNode.view]; + } + DoricSlideItemNode *node = cell.doricSlideItemNode; + node.viewId = model[@"id"]; + [node blend:props]; + [node.view setNeedsLayout]; + return cell; +} + +- (BOOL)collectionView:(UICollectionView *)collectionView shouldSelectItemAtIndexPath:(NSIndexPath *)indexPath { + return NO; +} + +- (NSDictionary *)itemModelAt:(NSUInteger)position { + NSString *viewId = self.itemViewIds[@(position)]; + if (viewId && viewId.length > 0) { + return [self subModelOf:viewId]; + } else { + DoricAsyncResult *result = [self callJSResponse:@"renderBunchedItems", @(position), @(self.batchCount), nil]; + JSValue *models = [result waitUntilResult]; + NSArray *array = [models toArray]; + [array enumerateObjectsUsingBlock:^(NSDictionary *obj, NSUInteger idx, BOOL *stop) { + NSString *thisViewId = obj[@"id"]; + [self setSubModel:obj in:thisViewId]; + NSUInteger pos = position + idx; + self.itemViewIds[@(pos)] = thisViewId; + }]; + return array[0]; + } +} + +- (DoricViewNode *)subNodeWithViewId:(NSString *)viewId { + __block DoricViewNode *ret = nil; + [self.doricContext.driver ensureSyncInMainQueue:^{ + for (UICollectionViewCell *collectionViewCell in self.view.visibleCells) { + if ([collectionViewCell isKindOfClass:[DoricCollectionViewCell class]]) { + DoricSlideItemNode *node = ((DoricCollectionViewCell *) collectionViewCell).doricSlideItemNode; + if ([viewId isEqualToString:node.viewId]) { + ret = node; + break; + } + } + } + }]; + return ret; +} + +- (void)blendSubNode:(NSDictionary *)subModel { + NSString *viewId = subModel[@"id"]; + DoricViewNode *viewNode = [self subNodeWithViewId:viewId]; + if (viewNode) { + [viewNode blend:subModel[@"props"]]; + } else { + NSMutableDictionary *model = [[self subModelOf:viewId] mutableCopy]; + [self recursiveMixin:subModel to:model]; + [self setSubModel:model in:viewId]; + [self.itemViewIds enumerateKeysAndObjectsUsingBlock:^(NSNumber *_Nonnull key, NSString *_Nonnull obj, BOOL *_Nonnull stop) { + if ([viewId isEqualToString:obj]) { + *stop = YES; + NSIndexPath *indexPath = [NSIndexPath indexPathForRow:[key integerValue] inSection:0]; + [UIView performWithoutAnimation:^{ + [self.view reloadItemsAtIndexPaths:@[indexPath]]; + }]; + } + }]; + } +} + +- (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView { + NSUInteger pageIndex = (NSUInteger) (scrollView.contentOffset.x / scrollView.width); + scrollView.contentOffset = CGPointMake(pageIndex * scrollView.width, scrollView.contentOffset.y); +} +@end diff --git a/iOS/Pod/Classes/Shader/DoricSuperNode.h b/iOS/Pod/Classes/Shader/DoricSuperNode.h index 42c58b5e..ec456c99 100644 --- a/iOS/Pod/Classes/Shader/DoricSuperNode.h +++ b/iOS/Pod/Classes/Shader/DoricSuperNode.h @@ -36,4 +36,6 @@ - (void)clearSubModel; - (DoricViewNode *)subNodeWithViewId:(NSString *)viewId; + +- (void)recursiveMixin:(NSDictionary *)srcModel to:(NSMutableDictionary *)targetModel; @end \ No newline at end of file diff --git a/iOS/Pod/Classes/Shader/DoricSuperNode.m b/iOS/Pod/Classes/Shader/DoricSuperNode.m index f3120924..029d38c3 100644 --- a/iOS/Pod/Classes/Shader/DoricSuperNode.m +++ b/iOS/Pod/Classes/Shader/DoricSuperNode.m @@ -65,6 +65,36 @@ - (void)mixin:(NSDictionary *)srcModel to:(NSMutableDictionary *)targetModel { targetModel[@"props"] = [targetProp copy]; } +- (void)recursiveMixin:(NSDictionary *)srcModel to:(NSMutableDictionary *)targetModel { + NSDictionary *srcProp = srcModel[@"props"]; + NSMutableDictionary *targetProp = [targetModel[@"props"] mutableCopy]; + NSArray *targetOri = targetProp[@"subviews"]; + + [srcProp enumerateKeysAndObjectsUsingBlock:^(NSString *key, id obj, BOOL *stop) { + if ([@"subviews" isEqualToString:key]) { + NSArray *subviews = obj; + NSMutableArray *targetSubviews = [targetOri mutableCopy]; + if (subviews) { + for (NSDictionary *subview in subviews) { + NSString *viewId = subview[@"id"]; + [targetSubviews enumerateObjectsUsingBlock:^(NSDictionary *obj, NSUInteger idx, BOOL *stop) { + if ([viewId isEqualToString:obj[@"id"]]) { + NSMutableDictionary *mutableDictionary = [obj mutableCopy]; + [self recursiveMixin:subview to:mutableDictionary]; + targetSubviews[idx] = [mutableDictionary copy]; + *stop = YES; + } + }]; + } + targetProp[@"subviews"] = [targetSubviews copy]; + } + } else { + targetProp[key] = obj; + } + }]; + targetModel[@"props"] = [targetProp copy]; +} + - (void)blendSubNode:(DoricViewNode *)subNode layoutConfig:(NSDictionary *)layoutConfig { DoricLayoutConfig *params = subNode.layoutConfig; diff --git a/js-framework/index.ts b/js-framework/index.ts index 8ad97435..02e3c137 100644 --- a/js-framework/index.ts +++ b/js-framework/index.ts @@ -15,7 +15,8 @@ */ export * from "./src/ui/view" export * from "./src/ui/layout" -export * from "./src/ui/listview" +export * from "./src/ui/list" +export * from "./src/ui/slider" export * from "./src/ui/scroller" export * from "./src/ui/widgets" export * from "./src/ui/panel" diff --git a/js-framework/src/ui/declarative.ts b/js-framework/src/ui/declarative.ts index 13ce2903..9a609cca 100644 --- a/js-framework/src/ui/declarative.ts +++ b/js-framework/src/ui/declarative.ts @@ -16,7 +16,8 @@ import { View, LayoutSpec } from './view' import { Stack, HLayout, VLayout } from './layout' import { IText, IImage, Text, Image } from './widgets' -import { IList, List } from './listview' +import { IList, List } from './list' +import { ISlider, Slider } from './slider' export function text(config: IText) { const ret = new Text @@ -84,4 +85,12 @@ export function list(config: IList) { Reflect.set(ret, key, Reflect.get(config, key, config), ret) } return ret +} + +export function slider(config: ISlider) { + const ret = new Slider + for (let key in config) { + Reflect.set(ret, key, Reflect.get(config, key, config), ret) + } + return ret } \ No newline at end of file diff --git a/js-framework/src/ui/listview.ts b/js-framework/src/ui/list.ts similarity index 100% rename from js-framework/src/ui/listview.ts rename to js-framework/src/ui/list.ts diff --git a/js-framework/src/ui/slider.ts b/js-framework/src/ui/slider.ts new file mode 100644 index 00000000..5584445a --- /dev/null +++ b/js-framework/src/ui/slider.ts @@ -0,0 +1,72 @@ +import { Superview, View, LayoutSpec, Property, IView } from "./view"; +import { Stack } from "./layout"; + +export function slideItem(item: View) { + return (new SlideItem).also((it) => { + it.layoutConfig = { + widthSpec: LayoutSpec.AT_MOST, + heightSpec: LayoutSpec.WRAP_CONTENT, + } + it.addChild(item) + }) +} + +export class SlideItem extends Stack { + /** + * Set to reuse native view + */ + @Property + identifier?: string +} + +export interface ISlider extends IView { + renderPage: (index: number) => SlideItem + itemCount: number + batchCount?: number +} + +export class Slider extends Superview implements ISlider { + private cachedViews: Map = new Map + + private ignoreDirtyCallOnce = false + + allSubviews() { + return this.cachedViews.values() + } + @Property + itemCount = 0 + + @Property + renderPage!: (index: number) => SlideItem + + @Property + batchCount = 3 + + + private getItem(itemIdx: number) { + let view = this.cachedViews.get(`${itemIdx}`) + if (view === undefined) { + view = this.renderPage(itemIdx) + view.superview = this + this.cachedViews.set(`${itemIdx}`, view) + } + return view + } + + isDirty() { + if (this.ignoreDirtyCallOnce) { + this.ignoreDirtyCallOnce = false + //Ignore the dirty call once. + return false + } + return super.isDirty() + } + + private renderBunchedItems(start: number, length: number) { + this.ignoreDirtyCallOnce = true; + return new Array(Math.min(length, this.itemCount - start)).fill(0).map((_, idx) => { + const slideItem = this.getItem(start + idx) + return slideItem.toModel() + }) + } +} \ No newline at end of file diff --git a/js-framework/src/util/color.ts b/js-framework/src/util/color.ts index 2bc8098a..c16e4456 100644 --- a/js-framework/src/util/color.ts +++ b/js-framework/src/util/color.ts @@ -19,8 +19,19 @@ import { Modeling } from "./types"; * Store color as format AARRGGBB or RRGGBB */ export class Color implements Modeling { - + static BLACK = new Color(0xFF000000) + static DKGRAY = new Color(0xFF444444) + static GRAY = new Color(0xFF888888) + static LTGRAY = new Color(0xFFCCCCCC) + static WHITE = new Color(0xFFFFFFFF) + static RED = new Color(0xFFFF0000) + static GREEN = new Color(0xFF00FF00) + static BLUE = new Color(0xFF0000FF) + static YELLOW = new Color(0xFFFFFF00) + static CYAN = new Color(0xFF00FFFF) + static MAGENTA = new Color(0xFFFF00FF) static TRANSPARENT = new Color(0) + _value: number = 0 constructor(v: number) {