Merge branch 'feature/listview' into 'master'

Feature/listview



See merge request !10
This commit is contained in:
pengfeizhou 2019-11-16 15:35:19 +08:00
commit 19c700705b
60 changed files with 2012 additions and 1060 deletions

View File

@ -22,15 +22,16 @@ import android.widget.FrameLayout;
import androidx.appcompat.app.AppCompatActivity;
import org.greenrobot.eventbus.EventBus;
import org.greenrobot.eventbus.Subscribe;
import org.greenrobot.eventbus.ThreadMode;
import java.io.IOException;
import pub.doric.dev.DevPanel;
import pub.doric.DoricContext;
import pub.doric.DoricDriver;
import pub.doric.dev.DevPanel;
import pub.doric.dev.LocalServer;
import pub.doric.dev.event.EnterDebugEvent;
import pub.doric.dev.event.QuitDebugEvent;
@ -49,9 +50,8 @@ public class MainActivity extends AppCompatActivity {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
doricContext = DoricContext.create(this, DoricUtils.readAssetFile("demo/Snake.js"), "test");
doricContext = DoricContext.create(this, DoricUtils.readAssetFile("demo/ListDemo.js"), "test");
doricContext.init(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT);
// doricContext.callEntity("log");
doricContext.getRootNode().setRootView((FrameLayout) findViewById(R.id.root));
LocalServer localServer = new LocalServer(getApplicationContext(), 8910);

View File

@ -20,6 +20,8 @@ import android.text.TextUtils;
import pub.doric.plugin.ShaderPlugin;
import pub.doric.shader.HLayoutNode;
import pub.doric.shader.ImageNode;
import pub.doric.shader.list.ListItemNode;
import pub.doric.shader.list.ListNode;
import pub.doric.shader.RootNode;
import pub.doric.shader.StackNode;
import pub.doric.shader.TextNode;
@ -66,6 +68,8 @@ public class DoricRegistry {
this.registerViewNode(StackNode.class);
this.registerViewNode(VLayoutNode.class);
this.registerViewNode(HLayoutNode.class);
this.registerViewNode(ListNode.class);
this.registerViewNode(ListItemNode.class);
initRegistry(this);
}

View File

@ -64,6 +64,15 @@ public class SettableFuture<T> {
return mResult;
}
public T get() {
try {
mReadyLatch.await();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
return mResult;
}
public static class TimeoutException extends RuntimeException {
public TimeoutException() {

View File

@ -197,7 +197,7 @@ public class DoricJSEngine implements Handler.Callback, DoricTimerExtension.Time
values.add(DoricUtils.toJavaValue(arg));
}
return mDoricJSE.invokeMethod(DoricConstant.GLOBAL_DORIC, method,
values.toArray(new JavaValue[values.size()]), true);
values.toArray(new JavaValue[values.size()]), false);
}
@Override

View File

@ -1,15 +1,14 @@
package pub.doric.engine.remote;
import com.github.pengfeizhou.jscore.JSDecoder;
import com.github.pengfeizhou.jscore.JSONBuilder;
import com.github.pengfeizhou.jscore.JavaFunction;
import com.github.pengfeizhou.jscore.JavaValue;
import com.google.gson.JsonArray;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.JsonPrimitive;
import org.greenrobot.eventbus.EventBus;
import org.jetbrains.annotations.NotNull;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import java.io.EOFException;
@ -25,14 +24,10 @@ import okhttp3.Response;
import okhttp3.WebSocket;
import okhttp3.WebSocketListener;
import pub.doric.dev.event.QuitDebugEvent;
import pub.doric.utils.DoricUtils;
public class RemoteJSExecutor {
private final WebSocket webSocket;
private final Map<String, JavaFunction> globalFunctions = new HashMap<>();
private JSDecoder temp;
public RemoteJSExecutor() {
@ -41,7 +36,7 @@ public class RemoteJSExecutor {
.readTimeout(10, TimeUnit.SECONDS)
.writeTimeout(10, TimeUnit.SECONDS)
.build();
final Request request = new Request.Builder().url("ws://192.168.25.175:2080").build();
final Request request = new Request.Builder().url("ws://192.168.24.79:2080").build();
final Thread current = Thread.currentThread();
webSocket = okHttpClient.newWebSocket(request, new WebSocketListener() {
@ -67,55 +62,28 @@ public class RemoteJSExecutor {
@Override
public void onMessage(@NotNull WebSocket webSocket, @NotNull String text) {
JsonElement je = DoricUtils.gson.fromJson(text, JsonElement.class);
if (je instanceof JsonObject) {
JsonObject jo = ((JsonObject) je);
String cmd = jo.get("cmd").getAsString();
try {
JSONObject jsonObject = new JSONObject(text);
String cmd = jsonObject.optString("cmd");
switch (cmd) {
case "injectGlobalJSFunction": {
String name = jo.get("name").getAsString();
JsonArray arguments = jo.getAsJsonArray("arguments");
JSDecoder[] decoders = new JSDecoder[arguments.size()];
for (int i = 0; i < arguments.size(); i++) {
if (arguments.get(i).isJsonPrimitive()) {
JsonPrimitive jp = ((JsonPrimitive) arguments.get(i));
if (jp.isNumber()) {
ValueBuilder vb = new ValueBuilder(jp.getAsNumber());
JSDecoder decoder = new JSDecoder(vb.build());
decoders[i] = decoder;
} else if (jp.isBoolean()) {
ValueBuilder vb = new ValueBuilder(jp.getAsBoolean());
JSDecoder decoder = new JSDecoder(vb.build());
decoders[i] = decoder;
} else if (jp.isString()) {
ValueBuilder vb = new ValueBuilder(jp.getAsString());
JSDecoder decoder = new JSDecoder(vb.build());
decoders[i] = decoder;
}
} else {
try {
ValueBuilder vb = new ValueBuilder(new JSONObject(DoricUtils.gson.toJson(arguments.get(i))));
JSDecoder decoder = new JSDecoder(vb.build());
decoders[i] = decoder;
} catch (Exception ex) {
ex.printStackTrace();
}
}
String name = jsonObject.optString("name");
JSONArray arguments = jsonObject.optJSONArray("arguments");
assert arguments != null;
JSDecoder[] decoders = new JSDecoder[arguments.length()];
for (int i = 0; i < arguments.length(); i++) {
Object o = arguments.get(i);
decoders[i] = new JSDecoder(new ValueBuilder(o).build());
}
globalFunctions.get(name).exec(decoders);
}
break;
case "invokeMethod": {
try {
Object result = jo.get("result");
Object result = jsonObject.opt("result");
ValueBuilder vb = new ValueBuilder(result);
JSDecoder decoder = new JSDecoder(vb.build());
temp = decoder;
temp = new JSDecoder(vb.build());
System.out.println(result);
} catch (Exception ex) {
ex.printStackTrace();
@ -125,6 +93,8 @@ public class RemoteJSExecutor {
}
break;
}
} catch (JSONException e) {
e.printStackTrace();
}
}
});
@ -141,33 +111,29 @@ public class RemoteJSExecutor {
public void injectGlobalJSFunction(String name, JavaFunction javaFunction) {
globalFunctions.put(name, javaFunction);
JsonObject jo = new JsonObject();
jo.addProperty("cmd", "injectGlobalJSFunction");
jo.addProperty("name", name);
webSocket.send(DoricUtils.gson.toJson(jo));
webSocket.send(new JSONBuilder().put("cmd", "injectGlobalJSFunction")
.put("name", name).toString()
);
}
public void injectGlobalJSObject(String name, JavaValue javaValue) {
}
public JSDecoder invokeMethod(String objectName, String functionName, JavaValue[] javaValues, boolean hashKey) {
JsonObject jo = new JsonObject();
jo.addProperty("cmd", "invokeMethod");
jo.addProperty("objectName", objectName);
jo.addProperty("functionName", functionName);
JsonArray ja = new JsonArray();
JSONArray jsonArray = new JSONArray();
for (JavaValue javaValue : javaValues) {
JsonObject argument = new JsonObject();
argument.addProperty("type", javaValue.getType());
argument.addProperty("value", javaValue.getValue());
ja.add(argument);
jsonArray.put(new JSONBuilder()
.put("type", javaValue.getType())
.put("value", javaValue.getValue())
.toJSONObject());
}
jo.add("javaValues", ja);
jo.addProperty("hashKey", hashKey);
webSocket.send(DoricUtils.gson.toJson(jo));
webSocket.send(new JSONBuilder()
.put("cmd", "invokeMethod")
.put("objectName", objectName)
.put("functionName", functionName)
.put("javaValues", jsonArray)
.put("hashKey", hashKey)
.toString());
LockSupport.park(Thread.currentThread());
return temp;

View File

@ -16,6 +16,7 @@
package pub.doric.plugin;
import pub.doric.DoricContext;
import pub.doric.async.AsyncResult;
import pub.doric.extension.bridge.DoricMethod;
import pub.doric.extension.bridge.DoricPlugin;
import pub.doric.utils.DoricLog;
@ -49,7 +50,23 @@ public class ShaderPlugin extends DoricJavaPlugin {
rootNode.render(jsObject.getProperty("props").asObject());
return null;
}
}, ThreadMode.UI);
}, ThreadMode.UI).setCallback(new AsyncResult.Callback<Object>() {
@Override
public void onResult(Object result) {
}
@Override
public void onError(Throwable t) {
t.printStackTrace();
DoricLog.e("Shader.render:error%s", t.getLocalizedMessage());
}
@Override
public void onFinish() {
}
});
} catch (Exception e) {
e.printStackTrace();
DoricLog.e("Shader.render:error%s", e.getLocalizedMessage());

View File

@ -15,159 +15,140 @@
*/
package pub.doric.shader;
import android.util.SparseArray;
import android.view.ViewGroup;
import pub.doric.DoricContext;
import pub.doric.utils.DoricUtils;
import com.github.pengfeizhou.jscore.JSArray;
import com.github.pengfeizhou.jscore.JSObject;
import com.github.pengfeizhou.jscore.JSValue;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* @Description: com.github.penfeizhou.doric.widget
* @Author: pengfei.zhou
* @CreateDate: 2019-07-20
*/
public abstract class GroupNode<F extends ViewGroup> extends ViewNode<F> {
private Map<String, ViewNode> mChildrenNode = new HashMap<>();
private SparseArray<ViewNode> mIndexInfo = new SparseArray<>();
public abstract class GroupNode<F extends ViewGroup> extends SuperNode<F> {
private ArrayList<ViewNode> mChildNodes = new ArrayList<>();
private ArrayList<String> mChildViewIds = new ArrayList<>();
protected boolean mReusable = false;
public GroupNode(DoricContext doricContext) {
super(doricContext);
}
@Override
protected void blend(F view, ViewGroup.LayoutParams layoutParams, String name, JSValue prop) {
super.blend(view, layoutParams, name, prop);
switch (name) {
case "children":
JSArray jsArray = prop.asArray();
int i;
List<ViewNode> tobeRemoved = new ArrayList<>();
for (i = 0; i < jsArray.size(); i++) {
JSValue jsValue = jsArray.get(i);
if (!jsValue.isObject()) {
continue;
}
JSObject childObj = jsValue.asObject();
String type = childObj.getProperty("type").asString().value();
String id = childObj.getProperty("id").asString().value();
ViewNode child = mChildrenNode.get(id);
if (child == null) {
child = ViewNode.create(getDoricContext(), type);
child.index = i;
child.mParent = this;
child.mId = id;
mChildrenNode.put(id, child);
} else {
if (i != child.index) {
mIndexInfo.remove(i);
child.index = i;
mView.removeView(child.getView());
protected void blend(F view, String name, JSValue prop) {
if ("children".equals(name)) {
JSArray ids = prop.asArray();
mChildViewIds.clear();
for (int i = 0; i < ids.size(); i++) {
mChildViewIds.add(ids.get(i).asString().value());
}
} else {
super.blend(view, name, prop);
}
}
@Override
public void blend(JSObject jsObject) {
super.blend(jsObject);
configChildNode();
}
private void configChildNode() {
for (int idx = 0; idx < mChildViewIds.size(); idx++) {
String id = mChildViewIds.get(idx);
JSObject model = getSubModel(id);
String type = model.getProperty("type").asString().value();
if (idx < mChildNodes.size()) {
ViewNode oldNode = mChildNodes.get(idx);
if (id.equals(oldNode.getId())) {
//The same,skip
} else {
if (mReusable) {
if (oldNode.getType().equals(type)) {
//Same type,can be reused
oldNode.setId(id);
oldNode.blend(model.getProperty("props").asObject());
} else {
//Replace this view
mChildNodes.remove(idx);
mView.removeView(oldNode.getDoricLayer());
ViewNode newNode = ViewNode.create(getDoricContext(), type);
newNode.setId(id);
if (newNode instanceof GroupNode) {
((GroupNode) newNode).mReusable = this.mReusable;
}
newNode.init(this);
newNode.blend(model.getProperty("props").asObject());
mChildNodes.add(idx, newNode);
mView.addView(newNode.getDoricLayer(), idx, newNode.getLayoutParams());
}
tobeRemoved.remove(child);
}
} else {
//Find in remain nodes
int position = -1;
for (int start = idx + 1; start < mChildNodes.size(); start++) {
ViewNode node = mChildNodes.get(start);
if (id.equals(node.getId())) {
//Found
position = start;
break;
}
}
if (position >= 0) {
//Found swap idx,position
ViewNode reused = mChildNodes.remove(position);
ViewNode abandoned = mChildNodes.remove(idx);
mChildNodes.set(idx, reused);
mChildNodes.set(position, abandoned);
//View swap index
mView.removeView(reused.getDoricLayer());
mView.addView(reused.getDoricLayer(), idx);
mView.removeView(abandoned.getDoricLayer());
mView.addView(abandoned.getDoricLayer(), position);
} else {
//Not found,insert
ViewNode newNode = ViewNode.create(getDoricContext(), type);
newNode.setId(id);
newNode.init(this);
newNode.blend(model.getProperty("props").asObject());
ViewNode node = mIndexInfo.get(i);
if (node != null && node != child) {
mView.removeViewAt(i);
mIndexInfo.remove(i);
tobeRemoved.add(node);
}
ViewGroup.LayoutParams params = child.getLayoutParams();
if (params == null) {
params = generateDefaultLayoutParams();
}
child.blend(childObj.getProperty("props").asObject(), params);
if (mIndexInfo.get(i) == null) {
mView.addView(child.getView(), i, child.getLayoutParams());
mIndexInfo.put(i, child);
mChildNodes.add(idx, newNode);
mView.addView(newNode.getDoricLayer(), idx, newNode.getLayoutParams());
}
}
}
int count = mView.getChildCount();
while (i < count) {
ViewNode node = mIndexInfo.get(i);
if (node != null) {
mChildrenNode.remove(node.getId());
mIndexInfo.remove(i);
tobeRemoved.remove(node);
mView.removeView(node.getView());
}
i++;
} else {
//Insert
ViewNode newNode = ViewNode.create(getDoricContext(), type);
newNode.setId(id);
if (newNode instanceof GroupNode) {
((GroupNode) newNode).mReusable = this.mReusable;
}
for (ViewNode node : tobeRemoved) {
mChildrenNode.remove(node.getId());
}
break;
default:
super.blend(view, layoutParams, name, prop);
break;
newNode.init(this);
newNode.blend(model.getProperty("props").asObject());
mChildNodes.add(newNode);
mView.addView(newNode.getDoricLayer(), idx, newNode.getLayoutParams());
}
}
int size = mChildNodes.size();
for (int idx = mChildViewIds.size(); idx < size; idx++) {
ViewNode viewNode = mChildNodes.remove(mChildViewIds.size());
mView.removeView(viewNode.getDoricLayer());
}
}
protected ViewGroup.LayoutParams generateDefaultLayoutParams() {
return new ViewGroup.LayoutParams(0, 0);
}
protected void blendChild(ViewNode viewNode, JSObject jsObject) {
JSValue jsValue = jsObject.getProperty("margin");
JSValue widthSpec = jsObject.getProperty("widthSpec");
JSValue heightSpec = jsObject.getProperty("widthSpec");
ViewGroup.LayoutParams layoutParams = viewNode.getLayoutParams();
if (widthSpec.isNumber()) {
switch (widthSpec.asNumber().toInt()) {
case 1:
layoutParams.width = ViewGroup.LayoutParams.WRAP_CONTENT;
break;
case 2:
layoutParams.width = ViewGroup.LayoutParams.MATCH_PARENT;
break;
default:
break;
}
}
if (heightSpec.isNumber()) {
switch (heightSpec.asNumber().toInt()) {
case 1:
layoutParams.height = ViewGroup.LayoutParams.WRAP_CONTENT;
break;
case 2:
layoutParams.height = ViewGroup.LayoutParams.MATCH_PARENT;
break;
default:
break;
}
}
if (jsValue.isObject() && layoutParams instanceof ViewGroup.MarginLayoutParams) {
JSValue topVal = jsValue.asObject().getProperty("top");
if (topVal.isNumber()) {
((ViewGroup.MarginLayoutParams) layoutParams).topMargin = DoricUtils.dp2px(topVal.asNumber().toFloat());
}
JSValue leftVal = jsValue.asObject().getProperty("left");
if (leftVal.isNumber()) {
((ViewGroup.MarginLayoutParams) layoutParams).leftMargin = DoricUtils.dp2px(leftVal.asNumber().toFloat());
}
JSValue rightVal = jsValue.asObject().getProperty("right");
if (rightVal.isNumber()) {
((ViewGroup.MarginLayoutParams) layoutParams).rightMargin = DoricUtils.dp2px(rightVal.asNumber().toFloat());
}
JSValue bottomVal = jsValue.asObject().getProperty("bottom");
if (bottomVal.isNumber()) {
((ViewGroup.MarginLayoutParams) layoutParams).bottomMargin = DoricUtils.dp2px(bottomVal.asNumber().toFloat());
@Override
protected void blendSubNode(JSObject subProp) {
String subNodeId = subProp.getProperty("id").asString().value();
for (ViewNode node : mChildNodes) {
if (subNodeId.equals(node.getId())) {
node.blend(subProp.getProperty("props").asObject());
break;
}
}
}

View File

@ -34,8 +34,8 @@ public class HLayoutNode extends LinearNode {
}
@Override
protected LinearLayout build(JSObject jsObject) {
LinearLayout linearLayout = super.build(jsObject);
protected LinearLayout build() {
LinearLayout linearLayout = super.build();
linearLayout.setOrientation(LinearLayout.HORIZONTAL);
return linearLayout;
}

View File

@ -46,12 +46,12 @@ public class ImageNode extends ViewNode<ImageView> {
}
@Override
protected ImageView build(JSObject jsObject) {
protected ImageView build() {
return new ImageView(getContext());
}
@Override
protected void blend(ImageView view, ViewGroup.LayoutParams layoutParams, String name, JSValue prop) {
protected void blend(ImageView view, String name, JSValue prop) {
if ("imageUrl".equals(name)) {
Glide.with(getContext()).load(prop.asString().value())
.listener(new RequestListener<Drawable>() {
@ -67,7 +67,7 @@ public class ImageNode extends ViewNode<ImageView> {
})
.into(view);
} else {
super.blend(view, layoutParams, name, prop);
super.blend(view, name, prop);
}
}
}

View File

@ -36,8 +36,8 @@ public class LinearNode extends GroupNode<LinearLayout> {
}
@Override
protected void blendChild(ViewNode viewNode, JSObject layoutConfig) {
super.blendChild(viewNode, layoutConfig);
protected void blendSubLayoutConfig(ViewNode viewNode, JSObject layoutConfig) {
super.blendSubLayoutConfig(viewNode, layoutConfig);
JSValue jsValue = layoutConfig.getProperty("alignment");
if (jsValue.isNumber()) {
((LinearLayout.LayoutParams) viewNode.getLayoutParams()).gravity = jsValue.asNumber().toInt();
@ -54,12 +54,12 @@ public class LinearNode extends GroupNode<LinearLayout> {
}
@Override
protected LinearLayout build(JSObject jsObject) {
protected LinearLayout build() {
return new LinearLayout(getContext());
}
@Override
protected void blend(LinearLayout view, ViewGroup.LayoutParams params, String name, JSValue prop) {
protected void blend(LinearLayout view, String name, JSValue prop) {
switch (name) {
case "space":
ShapeDrawable shapeDrawable;
@ -82,7 +82,7 @@ public class LinearNode extends GroupNode<LinearLayout> {
view.setGravity(prop.asNumber().toInt());
break;
default:
super.blend(view, params, name, prop);
super.blend(view, name, prop);
break;
}
}

View File

@ -36,12 +36,17 @@ public class RootNode extends StackNode {
}
@Override
public View getView() {
public View getDoricLayer() {
return mView;
}
public void setRootView(FrameLayout rootView) {
this.mView = rootView;
mLayoutParams = rootView.getLayoutParams();
if (mLayoutParams == null) {
mLayoutParams = new ViewGroup.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
rootView.setLayoutParams(mLayoutParams);
}
}
@Override
@ -50,6 +55,6 @@ public class RootNode extends StackNode {
}
public void render(JSObject props) {
blend(props, getLayoutParams());
blend(props);
}
}

View File

@ -36,8 +36,8 @@ public class StackNode extends GroupNode<FrameLayout> {
}
@Override
protected void blendChild(ViewNode viewNode, JSObject jsObject) {
super.blendChild(viewNode, jsObject);
protected void blendSubLayoutConfig(ViewNode viewNode, JSObject jsObject) {
super.blendSubLayoutConfig(viewNode, jsObject);
JSValue jsValue = jsObject.getProperty("alignment");
if (jsValue.isNumber()) {
((FrameLayout.LayoutParams) viewNode.getLayoutParams()).gravity = jsValue.asNumber().toInt();
@ -45,18 +45,18 @@ public class StackNode extends GroupNode<FrameLayout> {
}
@Override
protected FrameLayout build(JSObject jsObject) {
protected FrameLayout build() {
return new FrameLayout(getContext());
}
@Override
protected void blend(FrameLayout view, ViewGroup.LayoutParams layoutParams, String name, JSValue prop) {
protected void blend(FrameLayout view, String name, JSValue prop) {
switch (name) {
case "gravity":
view.setForegroundGravity(prop.asNumber().toInt());
break;
default:
super.blend(view, layoutParams, name, prop);
super.blend(view, name, prop);
}
}

View File

@ -0,0 +1,148 @@
/*
* 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;
import android.view.View;
import android.view.ViewGroup;
import com.github.pengfeizhou.jscore.JSArray;
import com.github.pengfeizhou.jscore.JSObject;
import com.github.pengfeizhou.jscore.JSValue;
import java.util.HashMap;
import java.util.Map;
import pub.doric.DoricContext;
import pub.doric.utils.DoricUtils;
/**
* @Description: pub.doric.shader
* @Author: pengfei.zhou
* @CreateDate: 2019-11-13
*/
public abstract class SuperNode<V extends View> extends ViewNode<V> {
private Map<String, JSObject> subNodes = new HashMap<>();
public SuperNode(DoricContext doricContext) {
super(doricContext);
}
protected ViewGroup.LayoutParams generateDefaultLayoutParams() {
return new ViewGroup.LayoutParams(0, 0);
}
@Override
protected void blend(V view, String name, JSValue prop) {
if (name.equals("subviews")) {
if (prop.isArray()) {
JSArray subviews = prop.asArray();
for (int i = 0; i < subviews.size(); i++) {
JSObject subNode = subviews.get(i).asObject();
mixinSubNode(subNode);
blendSubNode(subNode);
}
}
} else {
super.blend(view, name, prop);
}
}
private void mixinSubNode(JSObject subNode) {
String id = subNode.getProperty("id").asString().value();
JSObject targetNode = subNodes.get(id);
if (targetNode == null) {
subNodes.put(id, subNode);
} else {
mixin(subNode, targetNode);
}
}
public JSObject getSubModel(String id) {
return subNodes.get(id);
}
public void setSubModel(String id, JSObject model) {
subNodes.put(id, model);
}
public void clearSubModel() {
subNodes.clear();
}
protected abstract void blendSubNode(JSObject subProperties);
protected void blendSubLayoutConfig(ViewNode viewNode, JSObject jsObject) {
JSValue margin = jsObject.getProperty("margin");
JSValue widthSpec = jsObject.getProperty("widthSpec");
JSValue heightSpec = jsObject.getProperty("heightSpec");
ViewGroup.LayoutParams layoutParams = viewNode.getLayoutParams();
if (widthSpec.isNumber()) {
switch (widthSpec.asNumber().toInt()) {
case 1:
layoutParams.width = ViewGroup.LayoutParams.WRAP_CONTENT;
break;
case 2:
layoutParams.width = ViewGroup.LayoutParams.MATCH_PARENT;
break;
default:
break;
}
}
if (heightSpec.isNumber()) {
switch (heightSpec.asNumber().toInt()) {
case 1:
layoutParams.height = ViewGroup.LayoutParams.WRAP_CONTENT;
break;
case 2:
layoutParams.height = ViewGroup.LayoutParams.MATCH_PARENT;
break;
default:
break;
}
}
if (margin.isObject() && layoutParams instanceof ViewGroup.MarginLayoutParams) {
JSValue topVal = margin.asObject().getProperty("top");
if (topVal.isNumber()) {
((ViewGroup.MarginLayoutParams) layoutParams).topMargin = DoricUtils.dp2px(topVal.asNumber().toFloat());
}
JSValue leftVal = margin.asObject().getProperty("left");
if (leftVal.isNumber()) {
((ViewGroup.MarginLayoutParams) layoutParams).leftMargin = DoricUtils.dp2px(leftVal.asNumber().toFloat());
}
JSValue rightVal = margin.asObject().getProperty("right");
if (rightVal.isNumber()) {
((ViewGroup.MarginLayoutParams) layoutParams).rightMargin = DoricUtils.dp2px(rightVal.asNumber().toFloat());
}
JSValue bottomVal = margin.asObject().getProperty("bottom");
if (bottomVal.isNumber()) {
((ViewGroup.MarginLayoutParams) layoutParams).bottomMargin = DoricUtils.dp2px(bottomVal.asNumber().toFloat());
}
}
}
private void mixin(JSObject src, JSObject target) {
JSObject srcProps = src.getProperty("props").asObject();
JSObject targetProps = target.getProperty("props").asObject();
for (String key : srcProps.propertySet()) {
JSValue jsValue = srcProps.getProperty(key);
if ("subviews".equals(key) && jsValue.isArray()) {
continue;
}
targetProps.asObject().setProperty(key, jsValue);
}
}
}

View File

@ -16,13 +16,11 @@
package pub.doric.shader;
import android.util.TypedValue;
import android.view.ViewGroup;
import android.widget.TextView;
import pub.doric.DoricContext;
import pub.doric.extension.bridge.DoricPlugin;
import com.github.pengfeizhou.jscore.JSObject;
import com.github.pengfeizhou.jscore.JSValue;
/**
@ -37,12 +35,12 @@ public class TextNode extends ViewNode<TextView> {
}
@Override
protected TextView build(JSObject jsObject) {
protected TextView build() {
return new TextView(getContext());
}
@Override
protected void blend(TextView view, ViewGroup.LayoutParams params, String name, JSValue prop) {
protected void blend(TextView view, String name, JSValue prop) {
switch (name) {
case "text":
view.setText(prop.asString().toString());
@ -57,7 +55,7 @@ public class TextNode extends ViewNode<TextView> {
view.setGravity(prop.asNumber().toInt());
break;
default:
super.blend(view, params, name, prop);
super.blend(view, name, prop);
break;
}
}

View File

@ -20,8 +20,6 @@ import android.widget.LinearLayout;
import pub.doric.DoricContext;
import pub.doric.extension.bridge.DoricPlugin;
import com.github.pengfeizhou.jscore.JSObject;
/**
* @Description: com.github.penfeizhou.doric.shader
* @Author: pengfei.zhou
@ -34,8 +32,8 @@ public class VLayoutNode extends LinearNode {
}
@Override
protected LinearLayout build(JSObject jsObject) {
LinearLayout linearLayout = super.build(jsObject);
protected LinearLayout build() {
LinearLayout linearLayout = super.build();
linearLayout.setOrientation(LinearLayout.VERTICAL);
return linearLayout;
}

View File

@ -22,11 +22,13 @@ import android.widget.FrameLayout;
import pub.doric.DoricContext;
import pub.doric.DoricRegistry;
import pub.doric.async.AsyncResult;
import pub.doric.utils.DoricContextHolder;
import pub.doric.utils.DoricConstant;
import pub.doric.utils.DoricMetaInfo;
import pub.doric.utils.DoricUtils;
import com.github.pengfeizhou.jscore.JSDecoder;
import com.github.pengfeizhou.jscore.JSObject;
import com.github.pengfeizhou.jscore.JSValue;
@ -39,10 +41,10 @@ import java.util.LinkedList;
*/
public abstract class ViewNode<T extends View> extends DoricContextHolder {
protected T mView;
int index;
GroupNode mParent;
SuperNode mSuperNode;
String mId;
private ViewGroup.LayoutParams mLayoutParams;
protected ViewGroup.LayoutParams mLayoutParams;
private String mType;
public ViewNode(DoricContext doricContext) {
super(doricContext);
@ -50,7 +52,25 @@ public abstract class ViewNode<T extends View> extends DoricContextHolder {
private DoricLayer doricLayer;
public View getView() {
public void init(SuperNode superNode) {
this.mSuperNode = superNode;
this.mLayoutParams = superNode.generateDefaultLayoutParams();
this.doricLayer = new DoricLayer(getContext());
this.doricLayer.setLayoutParams(mLayoutParams);
this.mView = build();
FrameLayout.LayoutParams params = new FrameLayout.LayoutParams(mLayoutParams.width, mLayoutParams.height);
doricLayer.addView(mView, params);
}
public void setId(String id) {
this.mId = id;
}
public String getType(){
return mType;
}
public View getDoricLayer() {
return doricLayer;
}
@ -58,53 +78,46 @@ public abstract class ViewNode<T extends View> extends DoricContextHolder {
return getDoricContext().getContext();
}
protected abstract T build(JSObject jsObject);
protected abstract T build();
void blend(JSObject jsObject, ViewGroup.LayoutParams layoutParams) {
mLayoutParams = layoutParams;
if (mView == null) {
mView = build(jsObject);
}
if (getView() == null) {
doricLayer = new DoricLayer(getContext());
FrameLayout.LayoutParams params = new FrameLayout.LayoutParams(layoutParams.width, layoutParams.height);
doricLayer.addView(mView, params);
}
for (String prop : jsObject.propertySet()) {
blend(mView, layoutParams, prop, jsObject.getProperty(prop));
public void blend(JSObject jsObject) {
if (jsObject != null) {
for (String prop : jsObject.propertySet()) {
blend(mView, prop, jsObject.getProperty(prop));
}
}
ViewGroup.LayoutParams params = mView.getLayoutParams();
if (params != null) {
params.width = layoutParams.width;
params.height = layoutParams.height;
params.width = mLayoutParams.width;
params.height = mLayoutParams.height;
} else {
params = layoutParams;
params = mLayoutParams;
}
mView.setLayoutParams(params);
}
protected void blend(T view, ViewGroup.LayoutParams layoutParams, String name, JSValue prop) {
protected void blend(T view, String name, JSValue prop) {
switch (name) {
case "width":
if (layoutParams.width >= 0) {
layoutParams.width = DoricUtils.dp2px(prop.asNumber().toFloat());
if (mLayoutParams.width >= 0) {
mLayoutParams.width = DoricUtils.dp2px(prop.asNumber().toFloat());
}
break;
case "height":
if (layoutParams.height >= 0) {
layoutParams.height = DoricUtils.dp2px(prop.asNumber().toFloat());
if (mLayoutParams.height >= 0) {
mLayoutParams.height = DoricUtils.dp2px(prop.asNumber().toFloat());
}
break;
case "x":
if (layoutParams instanceof ViewGroup.MarginLayoutParams) {
if (mLayoutParams instanceof ViewGroup.MarginLayoutParams) {
float x = prop.asNumber().toFloat();
((ViewGroup.MarginLayoutParams) layoutParams).leftMargin = DoricUtils.dp2px(x);
((ViewGroup.MarginLayoutParams) mLayoutParams).leftMargin = DoricUtils.dp2px(x);
}
break;
case "y":
if (layoutParams instanceof ViewGroup.MarginLayoutParams) {
if (mLayoutParams instanceof ViewGroup.MarginLayoutParams) {
float y = prop.asNumber().toFloat();
((ViewGroup.MarginLayoutParams) layoutParams).topMargin = DoricUtils.dp2px(y);
((ViewGroup.MarginLayoutParams) mLayoutParams).topMargin = DoricUtils.dp2px(y);
}
break;
case "bgColor":
@ -120,8 +133,8 @@ public abstract class ViewNode<T extends View> extends DoricContextHolder {
});
break;
case "layoutConfig":
if (prop.isObject() && mParent != null) {
mParent.blendChild(this, prop.asObject());
if (prop.isObject() && mSuperNode != null) {
mSuperNode.blendSubLayoutConfig(this, prop.asObject());
}
break;
case "border":
@ -172,26 +185,28 @@ public abstract class ViewNode<T extends View> extends DoricContextHolder {
ViewNode viewNode = this;
do {
ids.push(viewNode.mId);
viewNode = viewNode.mParent;
viewNode = viewNode.mSuperNode;
} while (viewNode != null && !(viewNode instanceof RootNode));
return ids.toArray(new String[0]);
}
public void callJSResponse(String funcId, Object... args) {
public AsyncResult<JSDecoder> callJSResponse(String funcId, Object... args) {
final Object[] nArgs = new Object[args.length + 2];
nArgs[0] = getIdList();
nArgs[1] = funcId;
if (args.length > 0) {
System.arraycopy(args, 0, nArgs, 2, args.length);
}
getDoricContext().callEntity(DoricConstant.DORIC_ENTITY_RESPONSE, nArgs);
return getDoricContext().callEntity(DoricConstant.DORIC_ENTITY_RESPONSE, nArgs);
}
public static ViewNode create(DoricContext doricContext, String type) {
DoricRegistry registry = doricContext.getDriver().getRegistry();
DoricMetaInfo<ViewNode> clz = registry.acquireViewNodeInfo(type);
return clz.createInstance(doricContext);
ViewNode ret = clz.createInstance(doricContext);
ret.mType = type;
return ret;
}
public ViewGroup.LayoutParams getLayoutParams() {

View File

@ -0,0 +1,137 @@
/*
* 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.list;
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
*/
public class ListAdapter extends RecyclerView.Adapter<ListAdapter.DoricViewHolder> {
private final ListNode listNode;
String renderItemFuncId;
int itemCount = 0;
int batchCount = 15;
SparseArray<String> itemValues = new SparseArray<>();
ListAdapter(ListNode listNode) {
this.listNode = listNode;
}
@NonNull
@Override
public DoricViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
ListItemNode node = (ListItemNode) ViewNode.create(listNode.getDoricContext(), "ListItem");
node.init(listNode);
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.listItemNode.setId(jsObject.getProperty("id").asString().value());
holder.listItemNode.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<JSDecoder> asyncResult = listNode.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);
listNode.setSubModel(itemId, itemModel);
}
return listNode.getSubModel(itemValues.get(position));
}
} catch (Exception e) {
e.printStackTrace();
}
return new JSNull();
} else {
JSObject childModel = listNode.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 {
ListItemNode listItemNode;
DoricViewHolder(ListItemNode node, @NonNull View itemView) {
super(itemView);
listItemNode = node;
}
}
}

View File

@ -0,0 +1,48 @@
/*
* 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.list;
import android.widget.FrameLayout;
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 = "ListItem")
public class ListItemNode extends StackNode {
public String identifier = "";
public ListItemNode(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);
}
}
}

View File

@ -0,0 +1,94 @@
/*
* 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.list;
import androidx.recyclerview.widget.LinearLayoutManager;
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: com.github.penfeizhou.doric.widget
* @Author: pengfei.zhou
* @CreateDate: 2019-11-12
*/
@DoricPlugin(name = "List")
public class ListNode extends SuperNode<RecyclerView> {
private final ListAdapter listAdapter;
public ListNode(DoricContext doricContext) {
super(doricContext);
this.listAdapter = new ListAdapter(this);
}
@Override
protected void blendSubNode(JSObject subProperties) {
listAdapter.blendSubNode(subProperties);
}
@Override
protected RecyclerView build() {
RecyclerView recyclerView = new RecyclerView(getContext());
recyclerView.setLayoutManager(new LinearLayoutManager(getContext()));
recyclerView.setAdapter(this.listAdapter);
return recyclerView;
}
@Override
public void blend(JSObject jsObject) {
super.blend(jsObject);
if (mView != null) {
mView.post(new Runnable() {
@Override
public void run() {
listAdapter.notifyDataSetChanged();
}
});
}
}
@Override
protected void blend(RecyclerView view, String name, JSValue prop) {
switch (name) {
case "itemCount":
this.listAdapter.itemCount = prop.asNumber().toInt();
break;
case "renderItem":
this.listAdapter.renderItemFuncId = prop.asString().value();
// If reset renderItem,should reset native cache.
this.listAdapter.itemValues.clear();
clearSubModel();
break;
case "batchCount":
this.listAdapter.batchCount = 15;
break;
default:
super.blend(view, name, prop);
break;
}
}
@Override
protected void blendSubLayoutConfig(ViewNode viewNode, JSObject jsObject) {
super.blendSubLayoutConfig(viewNode, jsObject);
}
}

View File

@ -14,4 +14,9 @@ org.gradle.jvmargs=-Xmx1536m
# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
# org.gradle.parallel=true
# To re-enable the build cache, either delete the following
# line or set the property to 'true'.
android.enableBuildCache=false

View File

@ -1,4 +1,5 @@
export default [
'src/Counter',
'src/Snake',
'src/ListDemo',
]

View File

@ -13,7 +13,9 @@ class CounterView extends ViewHolder<CountModel> {
text({
textSize: 40,
layoutConfig: {
alignment: new Gravity().center()
alignment: new Gravity().center(),
widthSpec: LayoutSpec.WRAP_CONTENT,
heightSpec: LayoutSpec.WRAP_CONTENT,
},
}).also(it => { this.number = it }),
text({
@ -25,7 +27,9 @@ class CounterView extends ViewHolder<CountModel> {
},
corners: 5,
layoutConfig: {
alignment: new Gravity().center()
alignment: new Gravity().center(),
widthSpec: LayoutSpec.WRAP_CONTENT,
heightSpec: LayoutSpec.WRAP_CONTENT,
},
shadow: {
color: Color.parse("#00ff00"),

71
demo/src/ListDemo.ts Normal file
View File

@ -0,0 +1,71 @@
import { Group, Panel, List, text, gravity, Color, Stack, LayoutSpec, list, NativeCall, listItem, log, vlayout } from "doric";
const colors = [
"#f0932b",
"#eb4d4b",
"#6ab04c",
"#e056fd",
"#686de0",
"#30336b",
]
@Entry
class ListPanel extends Panel {
build(rootView: Group): void {
rootView.addChild(vlayout([
text({
text: "ListDemo",
layoutConfig: {
widthSpec: LayoutSpec.AT_MOST,
heightSpec: LayoutSpec.EXACTLY,
},
textSize: 30,
textColor: Color.parse("#535c68"),
bgColor: Color.parse("#dff9fb"),
textAlignment: gravity().center(),
height: 50,
}),
list({
itemCount: 1000,
renderItem: (idx: number) => {
return listItem(text({
layoutConfig: {
widthSpec: LayoutSpec.AT_MOST,
heightSpec: LayoutSpec.WRAP_CONTENT,
margin: {
left: 10,
right: 50,
top: 50,
bottom: 10,
},
},
text: `Cell At Line ${idx}`,
textAlignment: gravity().center(),
textColor: Color.parse("#ffffff"),
textSize: 20,
})).also(it => {
it.gravity = gravity().center()
it.bgColor = Color.parse(colors[idx % colors.length])
it.layoutConfig = {
widthSpec: LayoutSpec.AT_MOST,
heightSpec: LayoutSpec.EXACTLY,
}
it.height = 100
it.onClick = () => {
log(`Click item at ${idx}`)
it.bgColor = Color.parse('#000000')
log(`bgcolor is ${Color.parse('#000000').toModel()}`)
}
})
},
layoutConfig: {
widthSpec: LayoutSpec.AT_MOST,
heightSpec: LayoutSpec.AT_MOST,
},
}),
]).also(it => {
it.layoutConfig = {
widthSpec: LayoutSpec.AT_MOST,
heightSpec: LayoutSpec.AT_MOST,
}
}))
}
}

View File

@ -20,6 +20,7 @@
E2334AFE22E9D2070098A085 /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = E2334AFD22E9D2070098A085 /* main.m */; };
E2334B0822E9D2070098A085 /* ExampleTests.m in Sources */ = {isa = PBXBuildFile; fileRef = E2334B0722E9D2070098A085 /* ExampleTests.m */; };
E2334B1322E9D2070098A085 /* ExampleUITests.m in Sources */ = {isa = PBXBuildFile; fileRef = E2334B1222E9D2070098A085 /* ExampleUITests.m */; };
E2BF7BF7237E8E9F001B0EDC /* ListDemo.js in Resources */ = {isa = PBXBuildFile; fileRef = E2BF7BF6237E8E9F001B0EDC /* ListDemo.js */; };
/* End PBXBuildFile section */
/* Begin PBXContainerItemProxy section */
@ -67,6 +68,7 @@
E2334B0E22E9D2070098A085 /* ExampleUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = ExampleUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
E2334B1222E9D2070098A085 /* ExampleUITests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = ExampleUITests.m; sourceTree = "<group>"; };
E2334B1422E9D2070098A085 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
E2BF7BF6237E8E9F001B0EDC /* ListDemo.js */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.javascript; path = ListDemo.js; sourceTree = "<group>"; };
/* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */
@ -123,6 +125,7 @@
E21DC9D12302865E00660C5C /* src */ = {
isa = PBXGroup;
children = (
E2BF7BF6237E8E9F001B0EDC /* ListDemo.js */,
E21DC9D22302865E00660C5C /* Snake.js */,
E21DC9D32302865E00660C5C /* Counter.js */,
);
@ -295,6 +298,7 @@
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
E2BF7BF7237E8E9F001B0EDC /* ListDemo.js in Resources */,
E21DC9D42302870000660C5C /* Snake.js in Resources */,
E21DC9D52302870000660C5C /* Counter.js in Resources */,
E2334AFB22E9D2070098A085 /* LaunchScreen.storyboard in Resources */,

View File

@ -26,11 +26,11 @@ @implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
NSString *path = [[NSBundle bundleForClass:[self class]] pathForResource:@"Snake" ofType:@"js"];
NSString *path = [[NSBundle bundleForClass:[self class]] pathForResource:@"ListDemo" ofType:@"js"];
NSString *jsContent = [NSString stringWithContentsOfFile:path encoding:NSUTF8StringEncoding error:nil];
self.doricContext = [[DoricContext alloc] initWithScript:jsContent source:@"test.js"];
[self.doricContext.rootNode setupRootView:[[DoricStackView new] also:^(DoricStackView *it) {
it.layoutConfig = [[DoricStackConfig alloc] initWithWidth:DoricLayoutAtMost height:DoricLayoutAtMost];
it.layoutConfig = [[DoricLayoutConfig alloc] initWithWidth:DoricLayoutAtMost height:DoricLayoutAtMost];
[self.view addSubview:it];
}]];
[self.doricContext initContextWithWidth:self.view.width height:self.view.height];

View File

@ -25,7 +25,7 @@
@implementation DoricContextHolder
- (instancetype)initWithContext:(DoricContext *)doricContext {
if (self = [super init]) {
if (self = [self init]) {
_doricContext = doricContext;
}
return self;

View File

@ -28,6 +28,8 @@
#import "DoricHLayoutNode.h"
#import "DoricTextNode.h"
#import "DoricImageNode.h"
#import "DoricListNode.h"
#import "DoricListItemNode.h"
@interface DoricRegistry ()
@ -58,6 +60,8 @@ - (void)innerRegister {
[self registerViewNode:DoricHLayoutNode.class withName:@"HLayout"];
[self registerViewNode:DoricTextNode.class withName:@"Text"];
[self registerViewNode:DoricImageNode.class withName:@"Image"];
[self registerViewNode:DoricListNode.class withName:@"List"];
[self registerViewNode:DoricListItemNode.class withName:@"ListItem"];
}
- (void)registerJSBundle:(NSString *)bundle withName:(NSString *)name {

View File

@ -20,19 +20,12 @@
// Created by pengfei.zhou on 2019/7/30.
//
#import "DoricViewNode.h"
#import "DoricSuperNode.h"
NS_ASSUME_NONNULL_BEGIN
@interface DoricGroupNode <V:UIView *, P:DoricLayoutConfig *> : DoricViewNode<V>
@property(nonatomic, strong) NSMutableDictionary *children;
@property(nonatomic, strong) NSMutableArray *indexedChildren;
- (void)blendChild:(DoricViewNode *)child layoutConfig:(NSDictionary *)layoutConfig;
- (P)generateDefaultLayoutParams;
@interface DoricGroupNode <V:UIView *> : DoricSuperNode<V>
@property(nonatomic, assign) BOOL reusable;
@end
NS_ASSUME_NONNULL_END

View File

@ -23,124 +23,140 @@
#import <Doric/DoricExtensions.h>
#import "DoricGroupNode.h"
@interface DoricGroupNode ()
@property(nonatomic, copy) NSArray<DoricViewNode *> *childNodes;
@property(nonatomic, copy) NSArray <NSString *> *childViewIds;
@end
@implementation DoricGroupNode
- (instancetype)initWithContext:(DoricContext *)doricContext {
if (self = [super initWithContext:doricContext]) {
_children = [[NSMutableDictionary alloc] init];
_indexedChildren = [[NSMutableArray alloc] init];
_childNodes = @[];
_childViewIds = @[];
_reusable = NO;
}
return self;
}
- (UIView *)build:(NSDictionary *)props {
- (UIView *)build {
UIView *ret = [[UIView alloc] init];
ret.clipsToBounds = YES;
return ret;
}
- (void)blendView:(UIView *)view forPropName:(NSString *)name propValue:(id)prop {
if ([name isEqualToString:@"children"]) {
NSArray *array = prop;
NSInteger i;
NSMutableArray *tobeRemoved = [[NSMutableArray alloc] init];
for (i = 0; i < array.count; i++) {
NSDictionary *val = array[i];
if (!val || (NSNull *) val == [NSNull null]) {
continue;
}
NSString *type = val[@"type"];
NSString *viewId = val[@"id"];
DoricViewNode *node = self.children[viewId];
if (node == nil) {
node = [DoricViewNode create:self.doricContext withType:type];
node.index = i;
node.parent = self;
node.viewId = viewId;
self.children[viewId] = node;
} else {
if (i != node.index) {
[self.indexedChildren removeObjectAtIndex:i];
node.index = i;
[node.view removeFromSuperview];
}
[tobeRemoved removeObject:node];
}
DoricViewNode *old = i >= self.indexedChildren.count ? nil : self.indexedChildren[i];
if (old && old != node) {
[old.view removeFromSuperview];
self.indexedChildren[i] = [NSNull null];
[tobeRemoved addObject:old];
}
DoricLayoutConfig *params = node.layoutConfig;
if (params == nil) {
params = [self generateDefaultLayoutParams];
node.layoutConfig = params;
}
[node blend:val[@"props"]];
if (self.indexedChildren.count <= i) {
[self.view addSubview:node.view];
[self.indexedChildren addObject:node];
} else if (self.indexedChildren[i] == [NSNull null]) {
self.indexedChildren[i] = node;
[self.view insertSubview:node.view atIndex:i];
}
}
NSInteger start = i;
while (start < self.indexedChildren.count) {
DoricViewNode *node = self.indexedChildren[(NSUInteger) start];
if (node) {
[self.children removeObjectForKey:node.viewId];
[node.view removeFromSuperview];
[tobeRemoved removeObject:node];
}
start++;
}
if (i < self.indexedChildren.count) {
[self.indexedChildren removeObjectsInRange:NSMakeRange((NSUInteger) i, self.indexedChildren.count - i)];
}
for (DoricViewNode *node in tobeRemoved) {
[self.children removeObjectForKey:node.viewId];
}
if ([@"children" isEqualToString:name]) {
self.childViewIds = prop;
} else {
[super blendView:view forPropName:name propValue:prop];
}
}
- (void)blend:(NSDictionary *)props {
[super blend:props];
[self configChildNodes];
}
- (DoricLayoutConfig *)generateDefaultLayoutParams {
DoricLayoutConfig *params = [[DoricLayoutConfig alloc] init];
return params;
}
- (void)blendChild:(DoricViewNode *)child layoutConfig:(NSDictionary *)layoutConfig {
DoricLayoutConfig *params = child.layoutConfig;
- (void)configChildNodes {
NSMutableArray *childNodes = [self.childNodes mutableCopy];
for (NSUInteger idx = 0; idx < self.childViewIds.count; idx++) {
NSString *viewId = self.childViewIds[idx];
NSDictionary *model = [self subModelOf:viewId];
NSString *type = model[@"type"];
if (idx < self.childNodes.count) {
DoricViewNode *oldNode = childNodes[idx];
if ([viewId isEqualToString:oldNode.viewId]) {
///Same,skip
} else {
if (self.reusable) {
if ([oldNode.type isEqualToString:type]) {
///Same type,can be reused
oldNode.viewId = viewId;
[oldNode blend:model[@"props"]];
} else {
///Replace this view
[childNodes removeObjectAtIndex:idx];
[oldNode.view removeFromSuperview];
DoricViewNode *viewNode = [DoricViewNode create:self.doricContext withType:type];
if ([viewNode isKindOfClass:[DoricGroupNode class]]) {
((DoricGroupNode *) viewNode).reusable = self.reusable;
}
viewNode.viewId = viewId;
[viewNode initWithSuperNode:self];
[viewNode blend:model[@"props"]];
[childNodes insertObject:viewNode atIndex:idx];
[self.view insertSubview:viewNode.view atIndex:idx];
}
} else {
///Find in remain nodes
NSInteger position = -1;
for (NSUInteger start = idx + 1; start < childNodes.count; start++) {
DoricViewNode *node = childNodes[start];
if ([viewId isEqualToString:node.viewId]) {
position = start;
break;
}
}
if (position >= 0) {
///Found ,swap idx,position
DoricViewNode *reused = childNodes[(NSUInteger) position];
[childNodes removeObjectAtIndex:(NSUInteger) position];
[childNodes removeObjectAtIndex:idx];
[childNodes insertObject:reused atIndex:idx];
[childNodes insertObject:oldNode atIndex:(NSUInteger) position];
[layoutConfig[@"widthSpec"] also:^(NSNumber *it) {
if (it) {
params.widthSpec = (DoricLayoutSpec) [it integerValue];
}
}];
[layoutConfig[@"heightSpec"] also:^(NSNumber *it) {
if (it) {
params.heightSpec = (DoricLayoutSpec) [it integerValue];
}
}];
if ([params isKindOfClass:DoricMarginConfig.class]) {
DoricMarginConfig *marginParams = (DoricMarginConfig *) params;
NSDictionary *margin = layoutConfig[@"margin"];
if (margin) {
marginParams.margin = DoricMarginMake(
[(NSNumber *) margin[@"left"] floatValue],
[(NSNumber *) margin[@"top"] floatValue],
[(NSNumber *) margin[@"right"] floatValue],
[(NSNumber *) margin[@"bottom"] floatValue]);
///View swap index
[reused.view removeFromSuperview];
[oldNode.view removeFromSuperview];
[self.view insertSubview:reused.view atIndex:idx];
[self.view insertSubview:oldNode.view atIndex:position];
} else {
///Not found,insert
DoricViewNode *viewNode = [DoricViewNode create:self.doricContext withType:type];
viewNode.viewId = viewId;
[viewNode initWithSuperNode:self];
[viewNode blend:model[@"props"]];
[childNodes insertObject:viewNode atIndex:idx];
[self.view insertSubview:viewNode.view atIndex:idx];
}
}
}
} else {
/// Insert
DoricViewNode *viewNode = [DoricViewNode create:self.doricContext withType:type];
if ([viewNode isKindOfClass:[DoricGroupNode class]]) {
((DoricGroupNode *) viewNode).reusable = self.reusable;
}
viewNode.viewId = viewId;
[viewNode initWithSuperNode:self];
[viewNode blend:model[@"props"]];
[childNodes addObject:viewNode];
[self.view addSubview:viewNode.view];
}
}
NSUInteger count = childNodes.count;
for (NSUInteger idx = self.childViewIds.count; idx < count; idx++) {
DoricViewNode *viewNode = childNodes.lastObject;
[childNodes removeLastObject];
[viewNode.view removeFromSuperview];
}
self.childNodes = [childNodes copy];
}
- (void)blendSubNode:(NSDictionary *)subModel {
NSString *viewId = subModel[@"id"];
[self.childNodes enumerateObjectsUsingBlock:^(DoricViewNode *obj, NSUInteger idx, BOOL *stop) {
if ([viewId isEqualToString:obj.viewId]) {
[obj blend:subModel[@"props"]];
*stop = YES;
}
}];
}
@end

View File

@ -22,5 +22,5 @@
#import "DoricGroupNode.h"
@interface DoricHLayoutNode : DoricGroupNode<DoricHLayoutView *, DoricLinearConfig *>
@interface DoricHLayoutNode : DoricGroupNode<DoricHLayoutView *>
@end

View File

@ -24,7 +24,7 @@
#import "DoricUtil.h"
@implementation DoricHLayoutNode
- (DoricHLayoutView *)build:(NSDictionary *)props {
- (DoricHLayoutView *)build {
return [DoricHLayoutView new];
}
@ -37,29 +37,4 @@ - (void)blendView:(DoricHLayoutView *)view forPropName:(NSString *)name propValu
[super blendView:view forPropName:name propValue:prop];
}
}
- (void)blendChild:(DoricViewNode *)child layoutConfig:(NSDictionary *)layoutConfig {
[super blendChild:child layoutConfig:layoutConfig];
if (![child.layoutConfig isKindOfClass:DoricLinearConfig.class]) {
DoricLog(@"blend DoricHLayoutView child error,layout params not match");
return;
}
DoricLinearConfig *params = (DoricLinearConfig *) child.layoutConfig;
NSDictionary *margin = layoutConfig[@"margin"];
if (margin) {
params.margin = DoricMarginMake(
[(NSNumber *) margin[@"left"] floatValue],
[(NSNumber *) margin[@"top"] floatValue],
[(NSNumber *) margin[@"right"] floatValue],
[(NSNumber *) margin[@"bottom"] floatValue]);
}
NSNumber *alignment = layoutConfig[@"alignment"];
if (alignment) {
params.alignment = (DoricGravity) [alignment integerValue];
}
}
- (DoricLinearConfig *)generateDefaultLayoutParams {
return [[DoricLinearConfig alloc] init];
}
@end

View File

@ -25,7 +25,7 @@
@implementation DoricImageNode
- (UIImageView *)build:(NSDictionary *)props {
- (UIImageView *)build {
return [[UIImageView alloc] init];
}

View File

@ -31,9 +31,9 @@ typedef struct DoricMargin DoricMargin;
DoricMargin DoricMarginMake(CGFloat left, CGFloat top, CGFloat right, CGFloat bottom);
typedef NS_ENUM(NSInteger, DoricLayoutSpec) {
DoricLayoutExact,
DoricLayoutWrapContent,
DoricLayoutAtMost,
DoricLayoutExact = 0,
DoricLayoutWrapContent = 1,
DoricLayoutAtMost = 2,
};
typedef NS_ENUM(NSInteger, DoricGravity) {
@ -54,43 +54,31 @@ typedef NS_ENUM(NSInteger, DoricGravity) {
@interface DoricLayoutConfig : NSObject
@property(nonatomic, assign) DoricLayoutSpec widthSpec;
@property(nonatomic, assign) DoricLayoutSpec heightSpec;
@property(nonatomic) DoricMargin margin;
@property(nonatomic, assign) DoricGravity alignment;
@property(nonatomic, assign) NSUInteger weight;
- (instancetype)init;
- (instancetype)initWithWidth:(DoricLayoutSpec)width height:(DoricLayoutSpec)height;
@end
@interface DoricStackConfig : DoricLayoutConfig
@end
@interface DoricMarginConfig : DoricLayoutConfig
@property(nonatomic) DoricMargin margin;
- (instancetype)initWithWidth:(DoricLayoutSpec)width height:(DoricLayoutSpec)height margin:(DoricMargin)margin;
@end
@interface DoricLinearConfig : DoricMarginConfig
@property(nonatomic, assign) NSUInteger weight;
@end
@interface DoricLayoutContainer <T :DoricLayoutConfig *> : UIView
- (T)configForChild:(__kindof UIView *)child;
- (void)layout;
- (void)requestLayout;
@end
@interface DoricStackView : DoricLayoutContainer<DoricStackConfig *>
@interface DoricLayoutContainer : UIView
@property(nonatomic, assign) DoricGravity gravity;
- (void)layout:(CGSize)targetSize;
- (CGSize)sizeContent:(CGSize)size;
@end
@interface DoricLinearView : DoricLayoutContainer<DoricLinearConfig *>
@property(nonatomic, assign) DoricGravity gravity;
@interface DoricStackView : DoricLayoutContainer
@end
@interface DoricLinearView : DoricLayoutContainer
@property(nonatomic, assign) CGFloat space;
@end

View File

@ -46,107 +46,70 @@ - (instancetype)initWithWidth:(DoricLayoutSpec)width height:(DoricLayoutSpec)hei
}
return self;
}
@end
@implementation DoricMarginConfig
- (instancetype)init {
if (self = [super init]) {
_margin = DoricMarginMake(0, 0, 0, 0);
}
return self;
}
- (instancetype)initWithWidth:(DoricLayoutSpec)width height:(DoricLayoutSpec)height margin:(DoricMargin)margin {
if (self = [super initWithWidth:width height:height]) {
if (self = [super init]) {
_widthSpec = width;
_heightSpec = height;
_margin = margin;
}
return self;
}
@end
@implementation DoricStackConfig
@end
@implementation DoricLinearConfig
@end
@interface DoricLayoutContainer ()
@property(nonatomic, assign) BOOL waitingLayout;
@property(nonatomic, assign) CGFloat contentWidth;
@property(nonatomic, assign) CGFloat contentHeight;
@property(nonatomic, assign) NSUInteger contentWeight;
@end
@implementation DoricLayoutContainer
- (instancetype)init {
if (self = [super init]) {
_waitingLayout = NO;
}
return self;
}
- (instancetype)initWithFrame:(CGRect)frame {
if (self = [super initWithFrame:frame]) {
_waitingLayout = NO;
}
return self;
}
- (instancetype)initWithCoder:(NSCoder *)coder {
if (self = [super initWithCoder:coder]) {
_waitingLayout = NO;
}
return self;
}
- (DoricLayoutConfig *)configForChild:(UIView *)child {
DoricLayoutConfig *config = child.layoutConfig;
if (!config) {
config = [[DoricLayoutConfig alloc] init];
}
return config;
}
- (void)requestLayout {
if ([self.superview isKindOfClass:[DoricLinearView class]]) {
[(DoricLinearView *) self.superview requestLayout];
return;
}
if (self.waitingLayout) {
return;
}
self.waitingLayout = YES;
__weak typeof(self) _self = self;
dispatch_async(dispatch_get_main_queue(), ^{
__strong typeof(_self) self = _self;
[self sizeToFit];
[self layout];
self.waitingLayout = NO;
});
}
- (void)setNeedsLayout {
[super setNeedsLayout];
if (self.waitingLayout) {
return;
}
[self requestLayout];
}
- (BOOL)waitingLayout {
- (void)layoutSubviews {
if ([self.superview isKindOfClass:[DoricLayoutContainer class]]) {
return [(DoricLayoutContainer *) self.superview waitingLayout];
[self.superview layoutSubviews];
} else {
CGSize size = [self sizeThatFits:CGSizeMake(self.superview.width, self.superview.height)];
[self layout:size];
}
return _waitingLayout;
}
- (void)layout {
[self.subviews enumerateObjectsUsingBlock:^(__kindof UIView *child, NSUInteger idx, BOOL *stop) {
if ([child isKindOfClass:[DoricLayoutContainer class]]) {
[(DoricLayoutContainer *) child layout];
}
}];
- (CGSize)sizeThatFits:(CGSize)size {
CGFloat width = self.width;
CGFloat height = self.height;
DoricLayoutConfig *config = self.layoutConfig;
if (!config) {
config = [DoricLayoutConfig new];
}
if (config.widthSpec == DoricLayoutAtMost
|| config.widthSpec == DoricLayoutWrapContent) {
width = size.width - config.margin.left - config.margin.right;
}
if (config.heightSpec == DoricLayoutAtMost
|| config.heightSpec == DoricLayoutWrapContent) {
height = size.height - config.margin.top - config.margin.bottom;
}
CGSize contentSize = [self sizeContent:CGSizeMake(width, height)];
if (config.widthSpec == DoricLayoutWrapContent) {
width = contentSize.width;
}
if (config.heightSpec == DoricLayoutWrapContent) {
height = contentSize.height;
}
return CGSizeMake(width, height);
}
- (CGSize)sizeContent:(CGSize)size {
return size;
}
- (void)layout:(CGSize)targetSize {
self.width = targetSize.width;
self.height = targetSize.height;
}
@end
@ -157,175 +120,223 @@ @interface DoricStackView ()
@end
@implementation DoricStackView
- (DoricStackConfig *)configForChild:(UIView *)child {
DoricStackConfig *config = (DoricStackConfig *) child.layoutConfig;
if (!config) {
config = [[DoricStackConfig alloc] init];
}
return config;
}
- (void)sizeToFit {
DoricLayoutConfig *config = self.layoutConfig;
self.contentWidth = 0;
self.contentHeight = 0;
- (CGSize)sizeContent:(CGSize)size {
CGFloat contentWidth = 0;
CGFloat contentHeight = 0;
for (UIView *child in self.subviews) {
if (child.isHidden) {
continue;
}
DoricStackConfig *childConfig = [self configForChild:child];
DoricLayoutConfig *childConfig = child.layoutConfig;
if (!childConfig) {
childConfig = [DoricLayoutConfig new];
}
CGSize childSize = CGSizeMake(child.width, child.height);
if ([child isKindOfClass:[DoricLayoutContainer class]]
|| childConfig.widthSpec == DoricLayoutWrapContent
|| childConfig.heightSpec == DoricLayoutWrapContent) {
[child sizeToFit];
childSize = [child sizeThatFits:CGSizeMake(size.width, size.height - contentHeight)];
}
self.contentWidth = MAX(self.contentWidth, child.width);
self.contentHeight = MAX(self.contentHeight, child.height);
}
if (config.widthSpec == DoricLayoutWrapContent) {
self.width = self.contentWidth;
} else if (config.widthSpec == DoricLayoutAtMost) {
self.width = self.superview.width;
}
if (config.heightSpec == DoricLayoutWrapContent) {
self.height = self.contentHeight;
} else if (config.heightSpec == DoricLayoutAtMost) {
self.height = self.superview.height;
if (childConfig.widthSpec == DoricLayoutExact) {
childSize.width = child.width;
} else if (childConfig.widthSpec == DoricLayoutAtMost) {
childSize.width = size.width;
}
if (childConfig.heightSpec == DoricLayoutExact) {
childSize.height = child.height;
} else if (childConfig.heightSpec == DoricLayoutAtMost) {
childSize.height = size.height - contentHeight;
}
if (childConfig.weight) {
childSize.height = child.height;
}
contentWidth = MAX(contentWidth, childSize.width + childConfig.margin.left + childConfig.margin.right);
contentHeight = MAX(contentHeight, childSize.height + childConfig.margin.top + childConfig.margin.bottom);
}
self.contentWidth = contentWidth;
self.contentHeight = contentHeight;
return CGSizeMake(contentWidth, contentHeight);
}
- (void)layout {
- (void)layout:(CGSize)targetSize {
for (UIView *child in self.subviews) {
if (child.isHidden) {
continue;
}
DoricStackConfig *childConfig = [self configForChild:child];
DoricLayoutConfig *childConfig = child.layoutConfig;
if (!childConfig) {
childConfig = [DoricLayoutConfig new];
}
CGSize size = [child sizeThatFits:CGSizeMake(targetSize.width, targetSize.height)];
if (childConfig.widthSpec == DoricLayoutExact) {
size.width = child.width;
}
if (childConfig.heightSpec == DoricLayoutExact) {
size.height = child.height;
}
if (childConfig.widthSpec == DoricLayoutExact) {
size.width = child.width;
} else if (childConfig.widthSpec == DoricLayoutAtMost) {
size.width = targetSize.width;
}
if (childConfig.heightSpec == DoricLayoutExact) {
size.height = child.height;
} else if (childConfig.heightSpec == DoricLayoutAtMost) {
size.height = targetSize.height;
}
child.width = size.width;
child.height = size.height;
DoricGravity gravity = childConfig.alignment | self.gravity;
if ((gravity & LEFT) == LEFT) {
child.left = 0;
} else if ((gravity & RIGHT) == RIGHT) {
child.right = self.width;
child.right = targetSize.width;
} else if ((gravity & CENTER_X) == CENTER_X) {
child.centerX = self.width / 2;
}
if ((gravity & TOP) == TOP) {
child.top = 0;
} else if ((gravity & BOTTOM) == BOTTOM) {
child.bottom = self.height;
} else if ((gravity & CENTER_Y) == CENTER_Y) {
child.centerY = self.height / 2;
}
if (childConfig.widthSpec == DoricLayoutAtMost) {
child.width = self.width;
}
if (childConfig.heightSpec == DoricLayoutAtMost) {
child.height = self.height;
}
if ([child isKindOfClass:[DoricLayoutContainer class]]) {
[(DoricLayoutContainer *) child layout];
}
}
}
@end
@interface DoricLinearView ()
@property(nonatomic, assign) CGFloat contentWidth;
@property(nonatomic, assign) CGFloat contentHeight;
@property(nonatomic, assign) NSUInteger contentWeight;
@end
@implementation DoricLinearView
- (DoricLinearConfig *)configForChild:(UIView *)child {
DoricLinearConfig *config = (DoricLinearConfig *) child.layoutConfig;
if (!config) {
config = [[DoricLinearConfig alloc] init];
}
return config;
}
@end
@implementation DoricVLayoutView
- (void)sizeToFit {
DoricLayoutConfig *config = self.layoutConfig;
self.contentWidth = 0;
self.contentHeight = 0;
self.contentWeight = 0;
for (UIView *child in self.subviews) {
if (child.isHidden) {
continue;
}
DoricLinearConfig *childConfig = [self configForChild:child];
if ([child isKindOfClass:[DoricLayoutContainer class]]
|| childConfig.widthSpec == DoricLayoutWrapContent
|| childConfig.heightSpec == DoricLayoutWrapContent) {
[child sizeToFit];
}
self.contentWidth = MAX(self.contentWidth, child.width + childConfig.margin.left + childConfig.margin.right);
self.contentHeight += child.height + self.space + childConfig.margin.top + childConfig.margin.bottom;
self.contentWeight += childConfig.weight;
}
self.contentHeight -= self.space;
if (config.widthSpec == DoricLayoutWrapContent) {
self.width = self.contentWidth;
} else if (config.widthSpec == DoricLayoutAtMost) {
self.width = self.superview.width;
}
if (config.heightSpec == DoricLayoutWrapContent) {
self.height = self.contentHeight;
} else if (config.heightSpec == DoricLayoutAtMost) {
self.height = self.superview.height;
}
if (self.contentWeight) {
CGFloat remain = self.height - self.contentHeight;
for (UIView *child in self.subviews) {
if (child.isHidden) {
continue;
}
DoricLinearConfig *childConfig = [self configForChild:child];
if (childConfig.weight) {
child.height += remain / self.contentWeight * childConfig.weight;
}
}
self.contentHeight = self.height;
}
}
- (void)layout {
CGFloat yStart = 0;
if ((self.gravity & TOP) == TOP) {
yStart = 0;
} else if ((self.gravity & BOTTOM) == BOTTOM) {
yStart = self.height - self.contentHeight;
} else if ((self.gravity & CENTER_Y) == CENTER_Y) {
yStart = (self.height - self.contentHeight) / 2;
}
for (UIView *child in self.subviews) {
if (child.isHidden) {
continue;
}
DoricLinearConfig *childConfig = [self configForChild:child];
DoricGravity gravity = childConfig.alignment | self.gravity;
if ((gravity & LEFT) == LEFT) {
child.left = 0;
} else if ((gravity & RIGHT) == RIGHT) {
child.right = self.width;
} else if ((gravity & CENTER_X) == CENTER_X) {
child.centerX = self.width / 2;
child.centerX = targetSize.width / 2;
} else {
if (childConfig.margin.left) {
child.left = childConfig.margin.left;
} else if (childConfig.margin.right) {
child.right = self.width - childConfig.margin.right;
child.right = targetSize.width - childConfig.margin.right;
}
}
if (childConfig.widthSpec == DoricLayoutAtMost) {
child.width = self.width;
if ((gravity & TOP) == TOP) {
child.top = 0;
} else if ((gravity & BOTTOM) == BOTTOM) {
child.bottom = targetSize.height;
} else if ((gravity & CENTER_Y) == CENTER_Y) {
child.centerY = targetSize.height / 2;
} else {
if (childConfig.margin.top) {
child.top = childConfig.margin.top;
} else if (childConfig.margin.bottom) {
child.bottom = targetSize.height - childConfig.margin.bottom;
}
}
if (childConfig.heightSpec == DoricLayoutAtMost) {
child.height = self.height - yStart - childConfig.margin.top - childConfig.margin.bottom - self.space;
if ([child isKindOfClass:[DoricLayoutContainer class]]) {
[(DoricLayoutContainer *) child layout:size];
}
}
self.width = targetSize.width;
self.height = targetSize.height;
}
@end
@implementation DoricLinearView
@end
@implementation DoricVLayoutView
- (CGSize)sizeContent:(CGSize)size {
CGFloat contentWidth = 0;
CGFloat contentHeight = 0;
NSUInteger contentWeight = 0;
for (UIView *child in self.subviews) {
if (child.isHidden) {
continue;
}
DoricLayoutConfig *childConfig = child.layoutConfig;
if (!childConfig) {
childConfig = [DoricLayoutConfig new];
}
CGSize childSize = CGSizeMake(child.width, child.height);
if ([child isKindOfClass:[DoricLayoutContainer class]]
|| childConfig.widthSpec == DoricLayoutWrapContent
|| childConfig.heightSpec == DoricLayoutWrapContent) {
childSize = [child sizeThatFits:CGSizeMake(size.width, size.height - contentHeight)];
}
if (childConfig.widthSpec == DoricLayoutExact) {
childSize.width = child.width;
} else if (childConfig.widthSpec == DoricLayoutAtMost) {
childSize.width = size.width;
}
if (childConfig.heightSpec == DoricLayoutExact) {
childSize.height = child.height;
} else if (childConfig.heightSpec == DoricLayoutAtMost) {
childSize.height = size.height - contentHeight;
}
if (childConfig.weight) {
childSize.height = child.height;
}
contentWidth = MAX(contentWidth, childSize.width + childConfig.margin.left + childConfig.margin.right);
contentHeight += childSize.height + self.space + childConfig.margin.top + childConfig.margin.bottom;
contentWeight += childConfig.weight;
}
contentHeight -= self.space;
self.contentWidth = contentWidth;
self.contentHeight = contentHeight;
self.contentWeight = contentWeight;
if (contentWeight) {
contentHeight = size.height;
}
return CGSizeMake(contentWidth, contentHeight);
}
- (void)layout:(CGSize)targetSize {
CGFloat yStart = 0;
if ((self.gravity & TOP) == TOP) {
yStart = 0;
} else if ((self.gravity & BOTTOM) == BOTTOM) {
yStart = targetSize.height - self.contentHeight;
} else if ((self.gravity & CENTER_Y) == CENTER_Y) {
yStart = (targetSize.height - self.contentHeight) / 2;
}
CGFloat remain = targetSize.height - self.contentHeight;
for (UIView *child in self.subviews) {
if (child.isHidden) {
continue;
}
DoricLayoutConfig *childConfig = child.layoutConfig;
if (!childConfig) {
childConfig = [DoricLayoutConfig new];
}
CGSize size = [child sizeThatFits:CGSizeMake(targetSize.width, targetSize.height - yStart)];
if (childConfig.widthSpec == DoricLayoutExact) {
size.width = child.width;
}
if (childConfig.heightSpec == DoricLayoutExact) {
size.height = child.height;
}
if (childConfig.widthSpec == DoricLayoutExact) {
size.width = child.width;
} else if (childConfig.widthSpec == DoricLayoutAtMost) {
size.width = targetSize.width;
}
if (childConfig.heightSpec == DoricLayoutExact) {
size.height = child.height;
} else if (childConfig.heightSpec == DoricLayoutAtMost) {
size.height = targetSize.height - yStart;
}
if (childConfig.weight) {
size.height = child.height;
}
if (childConfig.weight) {
size.height += remain / self.contentWeight * childConfig.weight;
}
child.width = size.width;
child.height = size.height;
DoricGravity gravity = childConfig.alignment | self.gravity;
if ((gravity & LEFT) == LEFT) {
child.left = 0;
} else if ((gravity & RIGHT) == RIGHT) {
child.right = self.width;
} else if ((gravity & CENTER_X) == CENTER_X) {
child.centerX = targetSize.width / 2;
} else {
if (childConfig.margin.left) {
child.left = childConfig.margin.left;
} else if (childConfig.margin.right) {
child.right = targetSize.width - childConfig.margin.right;
}
}
if (childConfig.margin.top) {
yStart += childConfig.margin.top;
@ -336,102 +347,124 @@ - (void)layout {
yStart += childConfig.margin.bottom;
}
if ([child isKindOfClass:[DoricLayoutContainer class]]) {
[(DoricLayoutContainer *) child layout];
[(DoricLayoutContainer *) child layout:size];
}
}
self.width = targetSize.width;
self.height = targetSize.height;
}
@end
@implementation DoricHLayoutView
- (void)sizeToFit {
DoricLinearConfig *config;
if ([self.superview isKindOfClass:[DoricLinearView class]]) {
config = [(DoricLinearView *) self.superview configForChild:self];
} else {
config = (DoricLinearConfig *) self.layoutConfig;
if (!config) {
config = [[DoricLinearConfig alloc] init];
}
}
self.contentWidth = 0;
self.contentHeight = 0;
self.contentWeight = 0;
- (CGSize)sizeContent:(CGSize)size {
CGFloat contentWidth = 0;
CGFloat contentHeight = 0;
NSUInteger contentWeight = 0;
for (UIView *child in self.subviews) {
if (child.isHidden) {
continue;
}
DoricLinearConfig *childConfig = [self configForChild:child];
DoricLayoutConfig *childConfig = child.layoutConfig;
if (!childConfig) {
childConfig = [DoricLayoutConfig new];
}
CGSize childSize = CGSizeMake(child.width, child.height);
if ([child isKindOfClass:[DoricLayoutContainer class]]
|| childConfig.widthSpec == DoricLayoutWrapContent
|| childConfig.heightSpec == DoricLayoutWrapContent) {
[child sizeToFit];
childSize = [child sizeThatFits:CGSizeMake(size.width - contentWidth, size.height)];
}
self.contentHeight = MAX(self.contentHeight, child.height + childConfig.margin.top + childConfig.margin.bottom);
self.contentWidth += child.width + self.space + childConfig.margin.left + childConfig.margin.right;
self.contentWeight += childConfig.weight;
}
self.contentWidth -= self.space;
if (config.widthSpec == DoricLayoutWrapContent) {
self.width = self.contentWidth;
} else if (config.widthSpec == DoricLayoutAtMost) {
self.width = self.superview.width;
}
if (config.heightSpec == DoricLayoutWrapContent) {
self.height = self.contentHeight;
} else if (config.heightSpec == DoricLayoutAtMost) {
self.height = self.superview.height;
}
if (self.contentWeight) {
CGFloat remain = self.width - self.contentWidth;
for (UIView *child in self.subviews) {
if (child.isHidden) {
continue;
}
DoricLinearConfig *childConfig = [self configForChild:child];
if (childConfig.weight) {
child.width += remain / self.contentWeight * childConfig.weight;
}
if (childConfig.widthSpec == DoricLayoutExact) {
childSize.width = child.width;
} else if (childConfig.widthSpec == DoricLayoutAtMost) {
childSize.width = size.width - contentWidth;
}
self.contentWidth = self.width;
if (childConfig.heightSpec == DoricLayoutExact) {
childSize.height = child.height;
} else if (childConfig.heightSpec == DoricLayoutAtMost) {
childSize.height = size.height;
}
if (childConfig.weight) {
childSize.width = child.width;
}
contentWidth += childSize.width + self.space + childConfig.margin.left + childConfig.margin.right;
contentHeight = MAX(contentHeight, childSize.height + childConfig.margin.top + childConfig.margin.bottom);
contentWeight += childConfig.weight;
}
contentWidth -= self.space;
self.contentWidth = contentWidth;
self.contentHeight = contentHeight;
self.contentWeight = contentWeight;
if (contentWeight) {
contentWidth = size.width;
}
return CGSizeMake(contentWidth, contentHeight);
}
- (void)layout {
- (void)layout:(CGSize)targetSize {
CGFloat xStart = 0;
if ((self.gravity & LEFT) == LEFT) {
if (self.contentWeight) {
xStart = 0;
} else if ((self.gravity & LEFT) == LEFT) {
xStart = 0;
} else if ((self.gravity & RIGHT) == RIGHT) {
xStart = self.width - self.contentWidth;
xStart = targetSize.width - self.contentWidth;
} else if ((self.gravity & CENTER_X) == CENTER_X) {
xStart = (self.width - self.contentWidth) / 2;
xStart = (targetSize.width - self.contentWidth) / 2;
}
CGFloat remain = targetSize.width - self.contentWidth;
for (UIView *child in self.subviews) {
if (child.isHidden) {
continue;
}
DoricLinearConfig *childConfig = [self configForChild:child];
DoricLayoutConfig *childConfig = child.layoutConfig;
if (!childConfig) {
childConfig = [DoricLayoutConfig new];
}
CGSize size = [child sizeThatFits:CGSizeMake(targetSize.width - xStart, targetSize.height)];
if (childConfig.widthSpec == DoricLayoutExact) {
size.width = child.width;
}
if (childConfig.heightSpec == DoricLayoutExact) {
size.height = child.height;
}
if (childConfig.widthSpec == DoricLayoutExact) {
size.width = child.width;
} else if (childConfig.widthSpec == DoricLayoutAtMost) {
size.width = targetSize.width - xStart;
}
if (childConfig.heightSpec == DoricLayoutExact) {
size.height = child.height;
} else if (childConfig.heightSpec == DoricLayoutAtMost) {
size.height = targetSize.height;
}
if (childConfig.weight) {
size.width = child.width;
}
if (childConfig.weight) {
size.width += remain / self.contentWeight * childConfig.weight;
}
child.width = size.width;
child.height = size.height;
DoricGravity gravity = childConfig.alignment | self.gravity;
if ((gravity & TOP) == TOP) {
child.top = 0;
} else if ((gravity & BOTTOM) == BOTTOM) {
child.bottom = self.height;
child.bottom = targetSize.height;
} else if ((gravity & CENTER_Y) == CENTER_Y) {
child.centerY = self.height / 2;
child.centerY = targetSize.height / 2;
} else {
if (childConfig.margin.top) {
child.top = childConfig.margin.top;
} else if (childConfig.margin.bottom) {
child.bottom = self.height - childConfig.margin.bottom;
child.bottom = targetSize.height - childConfig.margin.bottom;
}
}
if (childConfig.heightSpec == DoricLayoutAtMost) {
child.height = self.height;
}
if (childConfig.widthSpec == DoricLayoutAtMost) {
child.width = self.width - xStart - childConfig.margin.right - childConfig.margin.left - self.space;
}
if (childConfig.margin.left) {
xStart += childConfig.margin.left;
}
@ -441,9 +474,11 @@ - (void)layout {
xStart += childConfig.margin.right;
}
if ([child isKindOfClass:[DoricLayoutContainer class]]) {
[(DoricLayoutContainer *) child layout];
[(DoricLayoutContainer *) child layout:size];
}
}
self.width = targetSize.width;
self.height = targetSize.height;
}
@end

View File

@ -0,0 +1,25 @@
/*
* 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/15.
//
#import <Foundation/Foundation.h>
#import "DoricStackNode.h"
@interface DoricListItemNode : DoricStackNode
@end

View File

@ -0,0 +1,44 @@
/*
* 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/15.
//
#import "DoricListItemNode.h"
#import "DoricExtensions.h"
@interface DoricListItemNode ()
@end
@interface DoricListItemView : DoricStackView
@end
@implementation DoricListItemView
@end
@implementation DoricListItemNode
- (instancetype)initWithContext:(DoricContext *)doricContext {
if (self = [super initWithContext:doricContext]) {
self.reusable = YES;
}
return self;
}
- (DoricStackView *)build {
return [DoricListItemView new];
}
@end

View File

@ -0,0 +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/15.
//
#import <Foundation/Foundation.h>
#import "DoricSuperNode.h"
@interface DoricListNode : DoricSuperNode<UITableView *>
@end

View File

@ -0,0 +1,153 @@
/*
* 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/15.
//
#import <JavaScriptCore/JavaScriptCore.h>
#import "DoricListNode.h"
#import "DoricExtensions.h"
#import "DoricListItemNode.h"
@interface DoricTableViewCell : UITableViewCell
@property(nonatomic, strong) DoricListItemNode *doricListItemNode;
@end
@implementation DoricTableViewCell
@end
@interface DoricListNode () <UITableViewDataSource, UITableViewDelegate>
@property(nonatomic, strong) NSMutableDictionary <NSNumber *, NSString *> *itemViewIds;
@property(nonatomic, strong) NSMutableDictionary <NSNumber *, NSNumber *> *itemHeights;
@property(nonatomic, assign) NSUInteger itemCount;
@property(nonatomic, assign) NSUInteger batchCount;
@end
@implementation DoricListNode
- (instancetype)initWithContext:(DoricContext *)doricContext {
if (self = [super initWithContext:doricContext]) {
_itemViewIds = [NSMutableDictionary new];
_itemHeights = [NSMutableDictionary new];
_batchCount = 15;
}
return self;
}
- (UITableView *)build {
return [[UITableView new] also:^(UITableView *it) {
it.dataSource = self;
it.delegate = self;
it.separatorStyle = UITableViewCellSeparatorStyleNone;
}];
}
- (void)blendView:(UITableView *)view forPropName:(NSString *)name propValue:(id)prop {
if ([@"itemCount" isEqualToString:name]) {
self.itemCount = [prop unsignedIntegerValue];
} else if ([@"renderItem" isEqualToString:name]) {
[self.itemViewIds removeAllObjects];
[self clearSubModel];
} else if ([@"batchCount" isEqualToString:name]) {
self.batchCount = [prop unsignedIntegerValue];
} else {
[super blendView:view forPropName:name propValue:prop];
}
}
- (void)blend:(NSDictionary *)props {
[super blend:props];
}
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
return self.itemCount;
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
NSUInteger position = (NSUInteger) indexPath.row;
NSDictionary *model = [self itemModelAt:position];
NSDictionary *props = model[@"props"];
NSString *reuseId = props[@"identifier"];
DoricTableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:reuseId ?: @"doriccell"];
if (!cell) {
cell = [[DoricTableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:reuseId ?: @"doriccell"];
DoricListItemNode *listItemNode = [[DoricListItemNode alloc] initWithContext:self.doricContext];
[listItemNode initWithSuperNode:self];
cell.doricListItemNode = listItemNode;
[cell.contentView addSubview:listItemNode.view];
}
DoricListItemNode *node = cell.doricListItemNode;
node.viewId = model[@"id"];
[node blend:props];
CGSize size = [node.view sizeThatFits:CGSizeMake(cell.width, cell.height)];
[self callItem:position height:size.height];
return cell;
}
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath {
NSUInteger position = (NSUInteger) indexPath.row;
NSNumber *heightNumber = self.itemHeights[@(position)];
if (heightNumber) {
return [heightNumber floatValue];
} else {
return 44.f;
}
}
- (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];
}
}
- (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];
}];
}
}];
}
- (void)callItem:(NSUInteger)position height:(CGFloat)height {
NSNumber *old = self.itemHeights[@(position)];
if (old && old.floatValue == height) {
return;
}
self.itemHeights[@(position)] = @(height);
NSIndexPath *indexPath = [NSIndexPath indexPathForRow:position inSection:0];
[UIView performWithoutAnimation:^{
[self.view reloadRowsAtIndexPaths:@[indexPath] withRowAnimation:UITableViewRowAnimationNone];
}];
}
@end

View File

@ -25,13 +25,12 @@
@implementation DoricRootNode
- (void)setupRootView:(DoricStackView *)view {
self.view = view;
self.layoutConfig = view.layoutConfig;
}
- (void)render:(NSDictionary *)props {
[self blend:props];
}
- (void)requestLayout {
[self.view requestLayout];
[self.view setNeedsLayout];
}
@end

View File

@ -22,5 +22,5 @@
#import "DoricGroupNode.h"
@interface DoricStackNode : DoricGroupNode<DoricStackView *, DoricStackConfig *>
@interface DoricStackNode : DoricGroupNode<DoricStackView *>
@end

View File

@ -21,11 +21,10 @@
//
#import "DoricStackNode.h"
#import "DoricUtil.h"
@implementation DoricStackNode
- (DoricStackView *)build:(NSDictionary *)props {
- (DoricStackView *)build {
return [DoricStackView new];
}
@ -36,22 +35,4 @@ - (void)blendView:(DoricStackView *)view forPropName:(NSString *)name propValue:
[super blendView:view forPropName:name propValue:prop];
}
}
- (DoricStackConfig *)generateDefaultLayoutParams {
return [[DoricStackConfig alloc] init];
}
- (void)blendChild:(DoricViewNode *)child layoutConfig:(NSDictionary *)layoutConfig {
[super blendChild:child layoutConfig:layoutConfig];
if (![child.layoutConfig isKindOfClass:DoricStackConfig.class]) {
DoricLog(@"blend DoricHLayoutView child error,layout params not match");
return;
}
DoricStackConfig *params = (DoricStackConfig *) child.layoutConfig;
NSNumber *alignment = layoutConfig[@"alignment"];
if (alignment) {
params.alignment = (DoricGravity) [alignment integerValue];
}
}
@end

View File

@ -0,0 +1,35 @@
/*
* 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/15.
//
#import <Foundation/Foundation.h>
#import "DoricViewNode.h"
@interface DoricSuperNode<V:UIView *> : DoricViewNode<V>
- (DoricLayoutConfig *)generateDefaultLayoutParams;
- (void)blendSubNode:(DoricViewNode *)subNode layoutConfig:(NSDictionary *)layoutConfig;
- (void)blendSubNode:(NSDictionary *)subModel;
- (NSDictionary *)subModelOf:(NSString *)viewId;
- (void)setSubModel:(NSDictionary *)model in:(NSString *)viewId;
- (void)clearSubModel;
@end

View File

@ -0,0 +1,122 @@
/*
* 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/15.
//
#import "DoricSuperNode.h"
#import "DoricExtensions.h"
@interface DoricSuperNode ()
@property(nonatomic, strong) NSMutableDictionary <NSString *, NSMutableDictionary *> *subNodes;
@end
@implementation DoricSuperNode
- (instancetype)initWithContext:(DoricContext *)doricContext {
if (self = [super initWithContext:doricContext]) {
_subNodes = [NSMutableDictionary new];
}
return self;
}
- (void)blendView:(UIView *)view forPropName:(NSString *)name propValue:(id)prop {
if ([@"subviews" isEqualToString:name]) {
NSArray *subviews = prop;
for (NSDictionary *subModel in subviews) {
[self mixinSubNode:subModel];
[self blendSubNode:subModel];
}
} else {
[super blendView:view forPropName:name propValue:prop];
}
}
- (void)mixinSubNode:(NSDictionary *)dictionary {
NSString *viewId = dictionary[@"id"];
NSMutableDictionary *oldModel = self.subNodes[viewId];
if (oldModel) {
[self mixin:dictionary to:oldModel];
} else {
self.subNodes[viewId] = [dictionary mutableCopy];
}
}
- (void)mixin:(NSDictionary *)srcModel to:(NSMutableDictionary *)targetModel {
NSDictionary *srcProp = srcModel[@"props"];
NSMutableDictionary *targetProp = [targetModel[@"props"] mutableCopy];
[srcProp enumerateKeysAndObjectsUsingBlock:^(NSString *key, id obj, BOOL *stop) {
if (![@"subviews" isEqualToString:key]) {
targetProp[key] = obj;
}
}];
targetModel[@"props"] = [targetProp copy];
}
- (void)blendSubNode:(DoricViewNode *)subNode layoutConfig:(NSDictionary *)layoutConfig {
DoricLayoutConfig *params = subNode.layoutConfig;
[layoutConfig[@"widthSpec"] also:^(NSNumber *it) {
if (it) {
params.widthSpec = (DoricLayoutSpec) [it integerValue];
}
}];
[layoutConfig[@"heightSpec"] also:^(NSNumber *it) {
if (it) {
params.heightSpec = (DoricLayoutSpec) [it integerValue];
}
}];
NSDictionary *margin = layoutConfig[@"margin"];
if (margin) {
params.margin = DoricMarginMake(
[(NSNumber *) margin[@"left"] floatValue],
[(NSNumber *) margin[@"top"] floatValue],
[(NSNumber *) margin[@"right"] floatValue],
[(NSNumber *) margin[@"bottom"] floatValue]);
}
NSNumber *alignment = layoutConfig[@"alignment"];
if (alignment) {
params.alignment = (DoricGravity) [alignment integerValue];
}
}
- (void)blendSubNode:(NSDictionary *)subModel {
NSAssert(NO, @"Should override class:%@ ,method:%@.", NSStringFromClass([self class]),
NSStringFromSelector(_cmd));
}
- (DoricLayoutConfig *)generateDefaultLayoutParams {
DoricLayoutConfig *params = [[DoricLayoutConfig alloc] init];
return params;
}
- (NSDictionary *)subModelOf:(NSString *)viewId {
return self.subNodes[viewId];
}
- (void)setSubModel:(NSDictionary *)model in:(NSString *)viewId {
self.subNodes[viewId] = [model mutableCopy];
}
- (void)clearSubModel {
[self.subNodes removeAllObjects];
}
@end

View File

@ -25,7 +25,7 @@
#import "DoricGroupNode.h"
@implementation DoricTextNode
- (id)build:(NSDictionary *)props {
- (UILabel *)build {
return [[UILabel alloc] init];
}

View File

@ -22,5 +22,5 @@
#import "DoricGroupNode.h"
@interface DoricVLayoutNode : DoricGroupNode<DoricVLayoutView *, DoricLinearConfig *>
@interface DoricVLayoutNode : DoricGroupNode<DoricVLayoutView *>
@end

View File

@ -21,11 +21,10 @@
//
#import "DoricVLayoutNode.h"
#import "DoricUtil.h"
@implementation DoricVLayoutNode
- (DoricVLayoutView *)build:(NSDictionary *)props {
- (DoricVLayoutView *)build {
return [DoricVLayoutView new];
}
@ -39,28 +38,4 @@ - (void)blendView:(DoricVLayoutView *)view forPropName:(NSString *)name propValu
}
}
- (void)blendChild:(DoricViewNode *)child layoutConfig:(NSDictionary *)layoutconfig {
[super blendChild:child layoutConfig:layoutconfig];
if (![child.layoutConfig isKindOfClass:DoricLinearConfig.class]) {
DoricLog(@"blend DoricVLayoutView child error,layout params not match");
return;
}
DoricLinearConfig *params = (DoricLinearConfig *) child.layoutConfig;
NSDictionary *margin = layoutconfig[@"margin"];
if (margin) {
params.margin = DoricMarginMake(
[(NSNumber *) margin[@"left"] floatValue],
[(NSNumber *) margin[@"top"] floatValue],
[(NSNumber *) margin[@"right"] floatValue],
[(NSNumber *) margin[@"bottom"] floatValue]);
}
NSNumber *alignment = layoutconfig[@"alignment"];
if (alignment) {
params.alignment = (DoricGravity) [alignment integerValue];
}
}
- (DoricLinearConfig *)generateDefaultLayoutParams {
return [[DoricLinearConfig alloc] init];
}
@end

View File

@ -25,30 +25,33 @@
#import "UIView+Doric.h"
NS_ASSUME_NONNULL_BEGIN
@class DoricGroupNode;
@class DoricSuperNode;
@interface DoricViewNode <V:UIView *> : DoricContextHolder
@property(nonatomic, strong) V view;
@property(nonatomic, weak) DoricGroupNode *parent;
@property(nonatomic, weak) DoricSuperNode *superNode;
@property(nonatomic) NSInteger index;
@property(nonatomic, strong) NSString *viewId;
@property(nonatomic, copy) NSString *viewId;
@property(nonatomic, strong) DoricLayoutConfig *layoutConfig;
@property(nonatomic, copy) NSString *type;
@property(nonatomic, strong, readonly) NSArray<NSString *> *idList;
@property(nonatomic, readonly) DoricLayoutConfig *layoutConfig;
- (V)build:(NSDictionary *)props;
@property(nonatomic, readonly) NSArray<NSString *> *idList;
- (void)initWithSuperNode:(DoricSuperNode *)superNode;
- (V)build;
- (void)blend:(NSDictionary *)props;
- (void)blendView:(V)view forPropName:(NSString *)name propValue:(id)prop;
- (void)callJSResponse:(NSString *)funcId, ...;
- (DoricAsyncResult *)callJSResponse:(NSString *)funcId, ...;
+ (DoricViewNode *)create:(DoricContext *)context withType:(NSString *)type;
+ (__kindof DoricViewNode *)create:(DoricContext *)context withType:(NSString *)type;
- (void)requestLayout;
@end

View File

@ -25,6 +25,8 @@
#import "DoricGroupNode.h"
#import "DoricRootNode.h"
#import "DoricConstant.h"
#import "DoricSuperNode.h"
#import "DoricExtensions.h"
void DoricAddEllipticArcPath(CGMutablePathRef path,
CGPoint origin,
@ -77,14 +79,23 @@ - (instancetype)initWithContext:(DoricContext *)doricContext {
return self;
}
- (UIView *)build:(NSDictionary *)props {
- (void)initWithSuperNode:(DoricSuperNode *)superNode {
self.superNode = superNode;
self.view = [[self build] also:^(UIView *it) {
it.layoutConfig = [superNode generateDefaultLayoutParams];
}];
}
- (DoricLayoutConfig *)layoutConfig {
return self.view.layoutConfig;
}
- (UIView *)build {
return [[UIView alloc] init];
}
- (void)blend:(NSDictionary *)props {
if (self.view == nil) {
self.view = [self build:props];
}
self.view.layoutConfig = self.layoutConfig;
for (NSString *key in props) {
id value = props[key];
@ -110,8 +121,8 @@ - (void)blendView:(UIView *)view forPropName:(NSString *)name propValue:(id)prop
} else if ([name isEqualToString:@"bgColor"]) {
view.backgroundColor = DoricColor(prop);
} else if ([name isEqualToString:@"layoutConfig"]) {
if (self.parent && [prop isKindOfClass:[NSDictionary class]]) {
[self.parent blendChild:self layoutConfig:prop];
if (self.superNode && [prop isKindOfClass:[NSDictionary class]]) {
[self.superNode blendSubNode:self layoutConfig:prop];
}
} else if ([name isEqualToString:@"onClick"]) {
self.callbackIds[@"onClick"] = prop;
@ -176,13 +187,13 @@ - (void)onClick:(UIView *)view {
DoricViewNode *node = self;
do {
[ret addObject:node.viewId];
node = node.parent;
node = node.superNode;
} while (node && ![node isKindOfClass:[DoricRootNode class]]);
return [[ret reverseObjectEnumerator] allObjects];
}
- (void)callJSResponse:(NSString *)funcId, ... {
- (DoricAsyncResult *)callJSResponse:(NSString *)funcId, ... {
NSMutableArray *array = [[NSMutableArray alloc] init];
[array addObject:self.idList];
[array addObject:funcId];
@ -192,18 +203,21 @@ - (void)callJSResponse:(NSString *)funcId, ... {
while ((arg = va_arg(args, id)) != nil) {
[array addObject:arg];
}
[self.doricContext callEntity:DORIC_ENTITY_RESPONSE withArgumentsArray:array];
DoricAsyncResult *ret = [self.doricContext callEntity:DORIC_ENTITY_RESPONSE withArgumentsArray:array];
va_end(args);
return ret;
}
+ (DoricViewNode *)create:(DoricContext *)context withType:(NSString *)type {
+ (__kindof DoricViewNode *)create:(DoricContext *)context withType:(NSString *)type {
DoricRegistry *registry = context.driver.registry;
Class clz = [registry acquireViewNode:type];
return [(DoricViewNode *) [clz alloc] initWithContext:context];
DoricViewNode *viewNode = [(DoricViewNode *) [clz alloc] initWithContext:context];
viewNode.type = type;
return viewNode;
}
- (void)requestLayout {
[self.parent requestLayout];
[self.superNode requestLayout];
}
@end

View File

@ -43,6 +43,8 @@ typedef void(^DoricFinishCallback)(void);
- (BOOL)hasResult;
- (R)getResult;
- (R)waitUntilResult;
@end
NS_ASSUME_NONNULL_END

View File

@ -22,31 +22,32 @@
#import "DoricAsyncResult.h"
@interface DoricAsyncResult()
@property(nonatomic,strong) id result;
@interface DoricAsyncResult ()
@property(nonatomic, strong) id result;
@end
@implementation DoricAsyncResult
- (void)setupResult:(id)result {
self.result = result;
if(self.resultCallback){
if (self.resultCallback) {
self.resultCallback(result);
}
if(self.finishCallback){
if (self.finishCallback) {
self.finishCallback();
}
}
- (void)setupError:(NSException *)exception {
self.result = exception;
if(self.exceptionCallback){
if (self.exceptionCallback) {
self.exceptionCallback(exception);
}
if(self.finishCallback){
if (self.finishCallback) {
self.finishCallback();
}
}
- (BOOL)hasResult {
return self.result;
}
@ -57,22 +58,35 @@ - (id)getResult {
- (void)setResultCallback:(DoricResultCallback)callback {
_resultCallback = callback;
if(self.result && ![self.result isKindOfClass: [NSException class]]){
if (self.result && ![self.result isKindOfClass:[NSException class]]) {
callback(self.result);
}
}
- (void)setExceptionCallback:(DoricExceptionCallback)exceptionCallback {
_exceptionCallback = exceptionCallback;
if([self.result isKindOfClass: [NSException class]]){
if ([self.result isKindOfClass:[NSException class]]) {
exceptionCallback(self.result);
}
}
- (void)setFinishCallback:(DoricFinishCallback)callback {
_finishCallback = callback;
if(self.result){
if (self.result) {
callback();
}
}
- (id)waitUntilResult {
if (self.result) {
return self.result;
}
dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);
self.resultCallback = ^(id r) {
dispatch_semaphore_signal(semaphore);
};
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
return self.result;
}
@end

View File

@ -26,7 +26,7 @@ void DoricLog(NSString *_Nonnull format, ...);
UIColor *_Nonnull DoricColor(NSNumber *_Nonnull number);
NSBundle *DoricBundle();
NSBundle *_Nonnull DoricBundle(void);
#ifndef DC_LOCK
#define DC_LOCK(lock) dispatch_semaphore_wait(lock, DISPATCH_TIME_FOREVER);

View File

@ -14,6 +14,9 @@
* limitations under the License.
*/
export * from "./src/ui/view"
export * from "./src/ui/layout"
export * from "./src/ui/listview"
export * from "./src/ui/widgets"
export * from "./src/ui/panel"
export * from "./src/ui/declarative"
export * from "./src/util/color"

View File

@ -1,51 +1,23 @@
import { Text, Image, HLayout, VLayout, Stack, LayoutConfig, View } from './view'
import { Color, GradientColor } from '../util/color'
import { Gravity } from '../util/gravity'
/*
* 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.
*/
import { View, LayoutSpec } from './view'
import { Stack, HLayout, VLayout } from './layout'
import { IText, IImage, Text, Image } from './widgets'
import { IList, List } from './listview'
export interface IView {
width?: number
height?: number
bgColor?: Color | GradientColor
corners?: number | { leftTop?: number; rightTop?: number; leftBottom?: number; rightBottom?: number }
border?: { width: number; color: Color; }
shadow?: { color: Color; opacity: number; radius: number; offsetX: number; offsetY: number }
alpha?: number
hidden?: boolean
padding?: {
left?: number,
right?: number,
top?: number,
bottom?: number,
}
layoutConfig?: LayoutConfig
onClick?: Function
identifier?: string
}
export interface IText extends IView {
text?: string
textColor?: Color
textSize?: number
maxLines?: number
textAlignment?: Gravity
}
export interface IImage extends IView {
imageUrl?: string
}
export interface IStack extends IView {
gravity?: Gravity
}
export interface IVLayout extends IView {
space?: number
gravity?: Gravity
}
export interface IHLayout extends IView {
space?: number
gravity?: Gravity
}
export function text(config: IText) {
const ret = new Text
for (let key in config) {
@ -64,6 +36,10 @@ export function image(config: IImage) {
export function stack(views: View[]) {
const ret = new Stack
ret.layoutConfig = {
widthSpec: LayoutSpec.WRAP_CONTENT,
heightSpec: LayoutSpec.WRAP_CONTENT,
}
for (let v of views) {
ret.addChild(v)
}
@ -72,6 +48,10 @@ export function stack(views: View[]) {
export function hlayout(views: View[]) {
const ret = new HLayout
ret.layoutConfig = {
widthSpec: LayoutSpec.WRAP_CONTENT,
heightSpec: LayoutSpec.WRAP_CONTENT,
}
for (let v of views) {
ret.addChild(v)
}
@ -80,8 +60,20 @@ export function hlayout(views: View[]) {
export function vlayout(views: View[]) {
const ret = new VLayout
ret.layoutConfig = {
widthSpec: LayoutSpec.WRAP_CONTENT,
heightSpec: LayoutSpec.WRAP_CONTENT,
}
for (let v of views) {
ret.addChild(v)
}
return ret
}
export function list(config: IList) {
const ret = new List
for (let key in config) {
Reflect.set(ret, key, Reflect.get(config, key, config), ret)
}
return ret
}

View File

@ -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.
*/
import { LayoutConfig, Group, Property, IView } from "./view";
import { Gravity } from "../util/gravity";
export interface IStack extends IView {
gravity?: Gravity
}
export class Stack extends Group implements IStack {
@Property
gravity?: Gravity
}
export class Root extends Stack {
}
class LinearLayout extends Group {
@Property
space?: number
@Property
gravity?: Gravity
}
export interface IVLayout extends IView {
space?: number
gravity?: Gravity
}
export class VLayout extends LinearLayout implements VLayout {
}
export interface IHLayout extends IView {
space?: number
gravity?: Gravity
}
export class HLayout extends LinearLayout implements IHLayout {
}

View File

@ -0,0 +1,86 @@
/*
* 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.
*/
import { View, Property, LayoutSpec, Superview, IView } from "./view";
import { Stack } from "./layout";
export function listItem(item: View) {
return (new ListItem).also((it) => {
it.layoutConfig = {
widthSpec: LayoutSpec.WRAP_CONTENT,
heightSpec: LayoutSpec.WRAP_CONTENT,
}
it.addChild(item)
})
}
export class ListItem extends Stack {
/**
* Set to reuse native view
*/
@Property
identifier?: string
}
export interface IList extends IView {
renderItem: (index: number) => ListItem
itemCount: number
batchCount?: number
}
export class List extends Superview implements IList {
private cachedViews: Map<string, ListItem> = new Map
private ignoreDirtyCallOnce = false
allSubviews() {
return this.cachedViews.values()
}
@Property
itemCount = 0
@Property
renderItem!: (index: number) => ListItem
@Property
batchCount = 15
private getItem(itemIdx: number) {
let view = this.cachedViews.get(`${itemIdx}`)
if (view === undefined) {
view = this.renderItem(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 listItem = this.getItem(start + idx)
return listItem.toModel()
})
}
}

View File

@ -14,9 +14,10 @@
* limitations under the License.
*/
import './../runtime/global'
import { View, Group, Root } from "./view";
import { loge, log } from '../util/log';
import { View, Group } from "./view";
import { loge } from '../util/log';
import { Model } from '../util/types';
import { Root } from './layout';
export function NativeCall(target: Panel, propertyKey: string, descriptor: PropertyDescriptor) {
@ -93,13 +94,13 @@ export abstract class Panel {
for (let i = 2; i < arguments.length; i++) {
argumentsList.push(arguments[i])
}
Reflect.apply(v.responseCallback, v, argumentsList)
return Reflect.apply(v.responseCallback, v, argumentsList)
}
private retrospectView(ids: string[]): View {
return ids.reduce((acc: View, cur) => {
if (Reflect.has(acc, "subViewById")) {
return Reflect.apply(Reflect.get(acc, "subViewById"), acc, [cur])
if (Reflect.has(acc, "subviewById")) {
return Reflect.apply(Reflect.get(acc, "subviewById"), acc, [cur])
}
return acc
}, this.__root__)
@ -112,13 +113,13 @@ export abstract class Panel {
}
private hookBeforeNativeCall() {
this.__root__.clean()
}
private hookAfterNativeCall() {
if (this.__root__.isDirty()) {
const model = this.__root__.toModel()
this.nativeRender(model)
this.__root__.clean()
}
}

View File

@ -35,13 +35,36 @@ export interface LayoutConfig {
bottom?: number,
}
alignment?: Gravity
//Only affective in VLayout or HLayout
weight?: number
}
export function Property(target: Object, propKey: string) {
Reflect.defineMetadata(propKey, true, target)
}
export abstract class View implements Modeling {
export interface IView {
width?: number
height?: number
bgColor?: Color | GradientColor
corners?: number | { leftTop?: number; rightTop?: number; leftBottom?: number; rightBottom?: number }
border?: { width: number; color: Color; }
shadow?: { color: Color; opacity: number; radius: number; offsetX: number; offsetY: number }
alpha?: number
hidden?: boolean
padding?: {
left?: number,
right?: number,
top?: number,
bottom?: number,
}
layoutConfig?: LayoutConfig
onClick?: Function
identifier?: string
}
export abstract class View implements Modeling, IView {
@Property
width: number = 0
@ -89,13 +112,7 @@ export abstract class View implements Modeling {
@Property
onClick?: Function
/**
* Set to reuse native view
*/
@Property
identifier?: string
parent?: Group
superview?: Superview
callbacks: Map<String, Function> = new Map
@ -106,7 +123,10 @@ export abstract class View implements Modeling {
}
private id2Callback(id: string) {
const f = this.callbacks.get(id)
let f = this.callbacks.get(id)
if (f === undefined) {
f = Reflect.get(this, id) as Function
}
return f
}
@ -173,7 +193,11 @@ export abstract class View implements Modeling {
}
/** Anchor end*/
__dirty_props__: { [index: string]: Model | undefined } = {}
private __dirty_props__: { [index: string]: Model | undefined } = {}
get dirtyProps() {
return this.__dirty_props__
}
nativeViewModel = {
id: this.viewId,
@ -188,9 +212,6 @@ export abstract class View implements Modeling {
newV = obj2Model(newV)
}
this.__dirty_props__[propKey] = newV
if (this.parent instanceof Group) {
this.parent.onChildPropertyChanged(this)
}
}
clean() {
@ -212,7 +233,7 @@ export abstract class View implements Modeling {
for (let i = 1; i < arguments.length; i++) {
argumentsList.push(arguments[i])
}
Reflect.apply(f, this, argumentsList)
return Reflect.apply(f, this, argumentsList)
} else {
loge(`Cannot find callback:${id} for ${JSON.stringify(this.toModel())}`)
}
@ -221,249 +242,80 @@ export abstract class View implements Modeling {
toModel() {
return this.nativeViewModel
}
let(block: (it: this) => void) {
block(this)
}
also(block: (it: this) => void) {
block(this)
return this
}
in(group: Group) {
group.addChild(this)
}
}
export interface StackConfig extends LayoutConfig {
}
export interface LinearConfig extends LayoutConfig {
weight?: number
}
export interface SuperView {
subViewById(id: string): View | undefined
}
export abstract class Group extends View implements SuperView {
@Property
readonly children: View[] = new Proxy([], {
set: (target, index, value) => {
if (index === 'length') {
this.getDirtyChildrenModel().length = value as number
} else if (typeof index === 'string'
&& parseInt(index) >= 0
&& value instanceof View) {
value.parent = this
const childrenModel = this.getDirtyChildrenModel()
childrenModel[parseInt(index)] = value.nativeViewModel
}
if (this.parent) {
this.parent.onChildPropertyChanged(this)
}
return Reflect.set(target, index, value)
}
})
subViewById(id: string): View | undefined {
for (let view of this.children) {
if (view.viewId === id) {
return view
export abstract class Superview extends View {
subviewById(id: string): View | undefined {
for (let v of this.allSubviews()) {
if (v.viewId === id) {
return v
}
}
return undefined
}
addChild(view: View) {
this.children.push(view)
abstract allSubviews(): Iterable<View>
isDirty() {
if (super.isDirty()) {
return true
} else {
for (const v of this.allSubviews()) {
if (v.isDirty()) {
return true
}
}
}
return false
}
clean() {
this.children.forEach(e => { e.clean() })
for (let v of this.allSubviews()) {
v.clean()
}
super.clean()
}
getDirtyChildrenModel(): Model[] {
if (this.__dirty_props__.children === undefined) {
this.__dirty_props__.children = []
}
return this.__dirty_props__.children as Model[]
}
toModel() {
if (this.__dirty_props__.children != undefined) {
(this.__dirty_props__.children as Model[]).length = this.children.length
const subviews = []
for (let v of this.allSubviews()) {
if (v.isDirty()) {
subviews.push(v.toModel())
}
}
this.dirtyProps.subviews = subviews
return super.toModel()
}
}
onChildPropertyChanged(child: View) {
this.getDirtyChildrenModel()[this.children.indexOf(child)] = child.nativeViewModel
this.getDirtyChildrenModel().length = this.children.length
if (this.parent) {
this.parent.onChildPropertyChanged(this)
export abstract class Group extends Superview {
readonly children: View[] = new Proxy([], {
set: (target, index, value) => {
const ret = Reflect.set(target, index, value)
// Let getDirty return true
this.dirtyProps.children = this.children.map(e => e.viewId)
return ret
}
})
allSubviews() {
return this.children
}
isDirty() {
return super.isDirty()
addChild(view: View) {
this.children.push(view)
}
}
export class Stack extends Group {
@Property
gravity?: Gravity
}
export class Scroller extends View implements SuperView {
@Property
contentView?: View
subViewById(id: string): View | undefined {
return this.contentView
}
}
export class Root extends Stack {
}
class LinearLayout extends Group {
@Property
space?: number
@Property
gravity?: Gravity
}
export class VLayout extends LinearLayout {
}
export class HLayout extends LinearLayout {
}
export class Text extends View {
@Property
text?: string
@Property
textColor?: Color
@Property
textSize?: number
@Property
maxLines?: number
@Property
textAlignment?: Gravity
}
export class Image extends View {
@Property
imageUrl?: string
}
export class List extends View implements SuperView {
private cachedViews: Map<string, View> = new Map
subViewById(id: string): View | undefined {
return this.cachedViews.get(id)
}
@Property
itemCount = 0
@Property
renderItem!: (index: number) => View
private getItem(itemIdx: number) {
let view = this.cachedViews.get(`${itemIdx}`)
if (view === undefined) {
view = this.renderItem(itemIdx)
this.cachedViews.set(`${itemIdx}`, view)
}
return view
}
@Property
private renderBunchedItems(items: number[]): View[] {
return items.map(e => this.getItem(e))
}
}
export class SectionList extends View implements SuperView {
private cachedViews: Map<string, View> = new Map
subViewById(id: string): View | undefined {
return this.cachedViews.get(id)
}
@Property
sectionRowsCount: number[] = []
@Property
renderSectionHeader!: (sectionIdx: number) => View
@Property
renderItem!: (sectionIdx: number, itemIdx: number) => View
@Property
sectionHeaderSticky = true
setupSectionRows(sectionCount: number, numberOfSection: (section: number) => number) {
this.sectionRowsCount = [...Array(sectionCount).keys()].map(e => numberOfSection(e))
}
private getItem(sectionIdx: number, itemIdx: number) {
let view = this.cachedViews.get(`${sectionIdx}:${itemIdx}`)
if (view === undefined) {
view = this.renderItem(sectionIdx, itemIdx)
this.cachedViews.set(`${sectionIdx}:${itemIdx}`, view)
}
return view
}
private getSectionHeader(sectionIdx: number) {
let view = this.cachedViews.get(`${sectionIdx}:`)
if (view === undefined) {
view = this.renderSectionHeader(sectionIdx)
this.cachedViews.set(`${sectionIdx}:`, view)
}
return view
}
@Property
private renderBunchedItems(items: Array<{ itemIdx: number, sectionIdx: number }>,
headers: number[]): { items: View[], headers: View[] } {
return {
items: items.map(e => this.getItem(e.sectionIdx, e.itemIdx)),
headers: headers.map(e => this.getSectionHeader(e))
}
}
}
export class Slide extends View implements SuperView {
@Property
pageCount = 0
@Property
renderPage!: (pageIdx: number) => View
private cachedViews: Map<string, View> = new Map
subViewById(id: string): View | undefined {
return this.cachedViews.get(id)
}
private getPage(pageIdx: number) {
let view = this.cachedViews.get(`${pageIdx}`)
if (view === undefined) {
view = this.renderPage(pageIdx)
this.cachedViews.set(`${pageIdx}`, view)
}
return view
}
@Property
private renderBunchedPages(pages: number[]): View[] {
return pages.map(e => this.getPage(e))
}
}

View File

@ -0,0 +1,52 @@
/*
* 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.
*/
import { IView, View, Property } from "./view"
import { Color } from "../util/color"
import { Gravity } from "../util/gravity"
export interface IText extends IView {
text?: string
textColor?: Color
textSize?: number
maxLines?: number
textAlignment?: Gravity
}
export class Text extends View implements IText {
@Property
text?: string
@Property
textColor?: Color
@Property
textSize?: number
@Property
maxLines?: number
@Property
textAlignment?: Gravity
}
export interface IImage extends IView {
imageUrl?: string
}
export class Image extends View implements IImage {
@Property
imageUrl?: string
}

View File

@ -77,3 +77,6 @@ export class Gravity implements Modeling {
}
}
export function gravity() {
return new Gravity
}

View File

@ -35,7 +35,7 @@ export function obj2Model(obj: Model): Model {
}
}
type _M = string | number | boolean | Modeling | { [index: string]: Model | undefined }
type _M = string | number | boolean | Modeling | { [index: string]: Model } | undefined
export type Model = _M | Array<_M>
export type Binder<T> = (v: T) => void