diff --git a/NeoModule/src/main/java/io/neomodule/NeoModule.java b/NeoModule/src/main/java/io/neomodule/NeoModule.java new file mode 100644 index 0000000..c4b2992 --- /dev/null +++ b/NeoModule/src/main/java/io/neomodule/NeoModule.java @@ -0,0 +1,9 @@ +package io.neomodule; + +/** + * @author kiva + */ + +public abstract class NeoModule { + public abstract void launch(); +} diff --git a/NeoModule/src/main/java/io/neomodule/layout/Configuration.java b/NeoModule/src/main/java/io/neomodule/layout/Configuration.java new file mode 100644 index 0000000..37f6aeb --- /dev/null +++ b/NeoModule/src/main/java/io/neomodule/layout/Configuration.java @@ -0,0 +1,280 @@ +package io.neomodule.layout; + +import android.graphics.Typeface; +import android.os.Build; +import android.text.InputType; +import android.text.TextUtils; +import android.util.TypedValue; +import android.view.Gravity; +import android.view.View; +import android.view.ViewGroup; +import android.widget.EditText; +import android.widget.ImageView; +import android.widget.LinearLayout; +import android.widget.RelativeLayout; +import android.widget.TextView; + +import java.util.HashMap; +import java.util.Map; + +import io.neomodule.layout.abs.ImageLoader; +import io.neomodule.layout.abs.ViewAttributeRunnable; +import io.neomodule.layout.utils.AttributeParser; +import io.neomodule.layout.utils.DimensionConverter; + +/** + * @author kiva + */ + +public class Configuration { + public final int noLayoutRule = -999; + public final String[] viewCorners = {"TopLeft", "TopRight", "BottomRight", "BottomLeft"}; + + public Map viewRunnables; + public ImageLoader imageLoader = null; + + Configuration() { + } + + void createViewRunnablesIfNeeded() { + if (viewRunnables != null) { + return; + } + viewRunnables = new HashMap<>(30); + viewRunnables.put("scaleType", new ViewAttributeRunnable() { + @Override + public void apply(View view, String value, ViewGroup parent, Map attrs) { + if (view instanceof ImageView) { + ImageView.ScaleType scaleType = ((ImageView) view).getScaleType(); + switch (value.toLowerCase()) { + case "center": + scaleType = ImageView.ScaleType.CENTER; + break; + case "center_crop": + scaleType = ImageView.ScaleType.CENTER_CROP; + break; + case "center_inside": + scaleType = ImageView.ScaleType.CENTER_INSIDE; + break; + case "fit_center": + scaleType = ImageView.ScaleType.FIT_CENTER; + break; + case "fit_end": + scaleType = ImageView.ScaleType.FIT_END; + break; + case "fit_start": + scaleType = ImageView.ScaleType.FIT_START; + break; + case "fit_xy": + scaleType = ImageView.ScaleType.FIT_XY; + break; + case "matrix": + scaleType = ImageView.ScaleType.MATRIX; + break; + } + ((ImageView) view).setScaleType(scaleType); + } + } + }); + viewRunnables.put("orientation", new ViewAttributeRunnable() { + @Override + public void apply(View view, String value, ViewGroup parent, Map attrs) { + if (view instanceof LinearLayout) { + ((LinearLayout) view).setOrientation(value.equals("vertical") ? LinearLayout.VERTICAL : LinearLayout.HORIZONTAL); + } + } + }); + viewRunnables.put("text", new ViewAttributeRunnable() { + @Override + public void apply(View view, String value, ViewGroup parent, Map attrs) { + if (view instanceof TextView) { + ((TextView) view).setText(value); + } + } + }); + viewRunnables.put("textSize", new ViewAttributeRunnable() { + @Override + public void apply(View view, String value, ViewGroup parent, Map attrs) { + if (view instanceof TextView) { + ((TextView) view).setTextSize(TypedValue.COMPLEX_UNIT_PX, DimensionConverter.toDimension(value, view.getResources().getDisplayMetrics())); + } + } + }); + viewRunnables.put("textColor", new ViewAttributeRunnable() { + @Override + public void apply(View view, String value, ViewGroup parent, Map attrs) { + if (view instanceof TextView) { + ((TextView) view).setTextColor(AttributeParser.parseColor(view, value)); + } + } + }); + viewRunnables.put("textStyle", new ViewAttributeRunnable() { + @Override + public void apply(View view, String value, ViewGroup parent, Map attrs) { + if (view instanceof TextView) { + int typeFace = Typeface.NORMAL; + if (value.contains("bold")) typeFace |= Typeface.BOLD; + else if (value.contains("italic")) typeFace |= Typeface.ITALIC; + ((TextView) view).setTypeface(null, typeFace); + } + } + }); + viewRunnables.put("textAlignment", new ViewAttributeRunnable() { + @Override + public void apply(View view, String value, ViewGroup parent, Map attrs) { + if (Build.VERSION.SDK_INT > Build.VERSION_CODES.KITKAT) { + int alignment = View.TEXT_ALIGNMENT_TEXT_START; + switch (value) { + case "center": + alignment = View.TEXT_ALIGNMENT_CENTER; + break; + case "left": + case "textStart": + break; + case "right": + case "textEnd": + alignment = View.TEXT_ALIGNMENT_TEXT_END; + break; + } + view.setTextAlignment(alignment); + } else { + int gravity = Gravity.LEFT; + switch (value) { + case "center": + gravity = Gravity.CENTER; + break; + case "left": + case "textStart": + break; + case "right": + case "textEnd": + gravity = Gravity.RIGHT; + break; + } + ((TextView) view).setGravity(gravity); + } + } + }); + viewRunnables.put("ellipsize", new ViewAttributeRunnable() { + @Override + public void apply(View view, String value, ViewGroup parent, Map attrs) { + if (view instanceof TextView) { + TextUtils.TruncateAt where = TextUtils.TruncateAt.END; + switch (value) { + case "start": + where = TextUtils.TruncateAt.START; + break; + case "middle": + where = TextUtils.TruncateAt.MIDDLE; + break; + case "marquee": + where = TextUtils.TruncateAt.MARQUEE; + break; + case "end": + break; + } + ((TextView) view).setEllipsize(where); + } + } + }); + viewRunnables.put("singleLine", new ViewAttributeRunnable() { + @Override + public void apply(View view, String value, ViewGroup parent, Map attrs) { + if (view instanceof TextView) { + ((TextView) view).setSingleLine(); + } + } + }); + viewRunnables.put("hint", new ViewAttributeRunnable() { + @Override + public void apply(View view, String value, ViewGroup parent, Map attrs) { + if (view instanceof EditText) { + ((EditText) view).setHint(value); + } + } + }); + viewRunnables.put("inputType", new ViewAttributeRunnable() { + @Override + public void apply(View view, String value, ViewGroup parent, Map attrs) { + if (view instanceof TextView) { + int inputType = 0; + switch (value) { + case "textEmailAddress": + inputType |= InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_EMAIL_ADDRESS; + break; + case "number": + inputType |= InputType.TYPE_CLASS_NUMBER; + break; + case "phone": + inputType |= InputType.TYPE_CLASS_PHONE; + break; + } + if (inputType > 0) ((TextView) view).setInputType(inputType); + } + } + }); + viewRunnables.put("gravity", new ViewAttributeRunnable() { + @Override + public void apply(View view, String value, ViewGroup parent, Map attrs) { + int gravity = AttributeParser.parseGravity(value); + if (view instanceof TextView) { + ((TextView) view).setGravity(gravity); + } else if (view instanceof LinearLayout) { + ((LinearLayout) view).setGravity(gravity); + } else if (view instanceof RelativeLayout) { + ((RelativeLayout) view).setGravity(gravity); + } + } + }); + viewRunnables.put("src", new ViewAttributeRunnable() { + @Override + public void apply(View view, String value, ViewGroup parent, Map attrs) { + if (view instanceof ImageView) { + String imageName = value; + if (imageName.startsWith("//")) imageName = "http:" + imageName; + if (imageName.startsWith("http")) { + if (imageLoader != null) { + if (attrs.containsKey("cornerRadius")) { + int radius = DimensionConverter.toDimensionPixelSize(attrs.get("cornerRadius"), view.getResources().getDisplayMetrics()); + imageLoader.loadRoundedImage((ImageView) view, imageName, radius); + } else { + imageLoader.loadImage((ImageView) view, imageName); + } + } + } else if (imageName.startsWith("@drawable/")) { + imageName = imageName.substring("@drawable/".length()); + ((ImageView) view).setImageDrawable(AttributeParser.getDrawableByName(view, imageName)); + } + } + } + }); + viewRunnables.put("visibility", new ViewAttributeRunnable() { + @Override + public void apply(View view, String value, ViewGroup parent, Map attrs) { + int visibility = View.VISIBLE; + String visValue = value.toLowerCase(); + if (visValue.equals("gone")) visibility = View.GONE; + else if (visValue.equals("invisible")) visibility = View.INVISIBLE; + view.setVisibility(visibility); + } + }); + viewRunnables.put("clickable", new ViewAttributeRunnable() { + @Override + public void apply(View view, String value, ViewGroup parent, Map attrs) { + view.setClickable(value.equals("true")); + } + }); + viewRunnables.put("tag", new ViewAttributeRunnable() { + @Override + public void apply(View view, String value, ViewGroup parent, Map attrs) { + throw new IllegalStateException("You cannot set tag in this situation, because we have other purpose."); + } + }); + viewRunnables.put("onClick", new ViewAttributeRunnable() { + @Override + public void apply(View view, String value, ViewGroup parent, Map attrs) { + view.setOnClickListener(AttributeParser.parseOnClick(parent, value)); + } + }); + } +} diff --git a/NeoModule/src/main/java/io/neomodule/layout/LayoutInfo.java b/NeoModule/src/main/java/io/neomodule/layout/LayoutInfo.java new file mode 100644 index 0000000..d28c0e5 --- /dev/null +++ b/NeoModule/src/main/java/io/neomodule/layout/LayoutInfo.java @@ -0,0 +1,15 @@ +package io.neomodule.layout; + +import android.graphics.drawable.GradientDrawable; + +import java.util.HashMap; + +public class LayoutInfo { + public HashMap nameToIdNumber; + public Object delegate; + public GradientDrawable backgroundDrawable; + + public LayoutInfo() { + nameToIdNumber = new HashMap<>(); + } +} \ No newline at end of file diff --git a/NeoModule/src/main/java/io/neomodule/layout/NeoLayoutInflater.java b/NeoModule/src/main/java/io/neomodule/layout/NeoLayoutInflater.java new file mode 100644 index 0000000..0ad9408 --- /dev/null +++ b/NeoModule/src/main/java/io/neomodule/layout/NeoLayoutInflater.java @@ -0,0 +1,235 @@ +package io.neomodule.layout; + +import android.content.Context; +import android.support.annotation.Nullable; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; + +import org.w3c.dom.Document; +import org.w3c.dom.NamedNodeMap; +import org.w3c.dom.Node; +import org.w3c.dom.NodeList; +import org.xml.sax.SAXException; + +import java.io.ByteArrayInputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.InputStream; +import java.lang.reflect.Constructor; +import java.lang.reflect.InvocationTargetException; +import java.util.HashMap; +import java.util.Map; + +import javax.xml.parsers.DocumentBuilder; +import javax.xml.parsers.DocumentBuilderFactory; +import javax.xml.parsers.ParserConfigurationException; + +import io.neomodule.layout.abs.ImageLoader; +import io.neomodule.layout.utils.AttributeApply; +import io.neomodule.layout.utils.UniqueId; + +public class NeoLayoutInflater { + private static Configuration CONFIG = new Configuration(); + + public static void setImageLoader(ImageLoader il) { + CONFIG.imageLoader = il; + } + + public static void setDelegate(View root, Object delegate) { + LayoutInfo info; + if (root.getTag() == null || !(root.getTag() instanceof LayoutInfo)) { + info = new LayoutInfo(); + root.setTag(info); + } else { + info = (LayoutInfo) root.getTag(); + } + info.delegate = delegate; + } + + @Nullable + public static View inflateName(Context context, String name) { + return inflateName(context, name, null); + } + + @Nullable + public static View inflateName(Context context, String name, ViewGroup parent) { + if (name.startsWith("<")) { + // Assume it's XML + return NeoLayoutInflater.inflate(context, name, parent); + } else { + File savedFile = context.getFileStreamPath(name + ".xml"); + try { + InputStream fileStream = new FileInputStream(savedFile); + return NeoLayoutInflater.inflate(context, fileStream, parent); + } catch (FileNotFoundException e) { + } + + try { + InputStream assetStream = context.getAssets().open(name + ".xml"); + return NeoLayoutInflater.inflate(context, assetStream, parent); + } catch (IOException e) { + } + int id = context.getResources().getIdentifier(name, "layout", context.getPackageName()); + if (id > 0) { + LayoutInflater inflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE); + return inflater.inflate(id, parent, false); + } + } + return null; + } + + @Nullable + public static View inflate(Context context, File xmlPath) { + InputStream inputStream = null; + try { + inputStream = new FileInputStream(xmlPath); + } catch (FileNotFoundException e) { + e.printStackTrace(); + return null; + } + return NeoLayoutInflater.inflate(context, inputStream); + } + + @Nullable + public static View inflate(Context context, String xml) { + InputStream inputStream = new ByteArrayInputStream(xml.getBytes()); + return NeoLayoutInflater.inflate(context, inputStream); + } + + @Nullable + public static View inflate(Context context, String xml, ViewGroup parent) { + InputStream inputStream = new ByteArrayInputStream(xml.getBytes()); + return NeoLayoutInflater.inflate(context, inputStream, parent); + } + + @Nullable + public static View inflate(Context context, InputStream inputStream) { + return inflate(context, inputStream, null); + } + + @Nullable + public static View inflate(Context context, InputStream inputStream, ViewGroup parent) { + try { + DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance(); + dbf.setNamespaceAware(true); + DocumentBuilder db = dbf.newDocumentBuilder(); + Document document = db.parse(inputStream); + try { + return inflate(context, document.getDocumentElement(), parent); + } finally { + inputStream.close(); + } + } catch (IOException | ParserConfigurationException | SAXException e) { + e.printStackTrace(); + } + return null; + } + + @SuppressWarnings("unchecked") + @Nullable + public static T findViewById(View view, String id) { + int idNum = UniqueId.idFromString(view, id); + if (idNum == 0) return null; + return (T) view.findViewById(idNum); + } + + @Nullable + private static View inflate(Context context, Node node) { + return inflate(context, node, null); + } + + @Nullable + private static View inflate(Context context, Node node, ViewGroup parent) { + View mainView = constructView(context, node.getNodeName()); + if (parent != null) + parent.addView(mainView); // have to add to parent to enable certain layout attrs + applyAttributes(mainView, getAttributesMap(node), parent); + if (mainView instanceof ViewGroup && node.hasChildNodes()) { + parseChildren(context, node, (ViewGroup) mainView); + } + return mainView; + } + + /** + * 遍历节点里的每一个字节点,并解析成 View + * + * @param context Context + * @param node 节点 + * @param mainView 父视图 + */ + private static void parseChildren(Context context, Node node, ViewGroup mainView) { + NodeList nodeList = node.getChildNodes(); + for (int i = 0; i < nodeList.getLength(); i++) { + Node currentNode = nodeList.item(i); + if (currentNode.getNodeType() != Node.ELEMENT_NODE) continue; + inflate(context, currentNode, mainView); // this recursively can call parseChildren + } + } + + /** + * 创建 xml 里定义的节点名的 View + * 如果节点名里没有带包名,就默认创建 android.widget 包下的实例,如 EditText, TextView + * 如果节点名里带了包名,就创建对应的实例,如 com.xxx.view.SomeView + * + * @param context Context + * @param name xml里的节点名 + * @return 对应的 View + */ + private static View constructView(Context context, String name) { + try { + if (!name.contains(".")) { + name = "android.widget." + name; + } + Class clazz = Class.forName(name); + Constructor constructor = clazz.getConstructor(Context.class); + + return (View) constructor.newInstance(context); + } catch (ClassNotFoundException + | NoSuchMethodException + | InstantiationException + | InvocationTargetException + | IllegalAccessException e) { + e.printStackTrace(); + } + return null; + } + + /** + * 得到一个节点下所有的 xml 属性 + * + * @param currentNode 节点 + * @return 属性的名字和值 + */ + private static HashMap getAttributesMap(Node currentNode) { + NamedNodeMap attributeMap = currentNode.getAttributes(); + int attributeCount = attributeMap.getLength(); + HashMap attributes = new HashMap<>(attributeCount); + + for (int i = 0; i < attributeCount; i++) { + Node attr = attributeMap.item(i); + String nodeName = attr.getNodeName(); + + // 跳过头部的 namespace + if (nodeName.startsWith("android:")) { + nodeName = nodeName.substring(8); + } + attributes.put(nodeName, attr.getNodeValue()); + } + return attributes; + } + + /** + * 对一个 View 设置属性 + * + * @param view 需要设置属性的view + * @param attrs 所有属性 + * @param parent view的父视图 + */ + private static void applyAttributes(View view, Map attrs, ViewGroup parent) { + CONFIG.createViewRunnablesIfNeeded(); + new AttributeApply(view, attrs, parent).apply(CONFIG); + } +} diff --git a/NeoModule/src/main/java/io/neomodule/layout/abs/ImageLoader.java b/NeoModule/src/main/java/io/neomodule/layout/abs/ImageLoader.java new file mode 100644 index 0000000..5f42144 --- /dev/null +++ b/NeoModule/src/main/java/io/neomodule/layout/abs/ImageLoader.java @@ -0,0 +1,9 @@ +package io.neomodule.layout.abs; + +import android.widget.ImageView; + +public interface ImageLoader { + void loadImage(ImageView view, String url); + + void loadRoundedImage(ImageView view, String url, int radius); +} \ No newline at end of file diff --git a/NeoModule/src/main/java/io/neomodule/layout/abs/ViewAttributeRunnable.java b/NeoModule/src/main/java/io/neomodule/layout/abs/ViewAttributeRunnable.java new file mode 100644 index 0000000..01d9d2b --- /dev/null +++ b/NeoModule/src/main/java/io/neomodule/layout/abs/ViewAttributeRunnable.java @@ -0,0 +1,10 @@ +package io.neomodule.layout.abs; + +import android.view.View; +import android.view.ViewGroup; + +import java.util.Map; + +public interface ViewAttributeRunnable { + void apply(View view, String value, ViewGroup parent, Map attrs); +} diff --git a/NeoModule/src/main/java/io/neomodule/layout/listener/OnClickForwarder.java b/NeoModule/src/main/java/io/neomodule/layout/listener/OnClickForwarder.java new file mode 100644 index 0000000..c3e83ff --- /dev/null +++ b/NeoModule/src/main/java/io/neomodule/layout/listener/OnClickForwarder.java @@ -0,0 +1,97 @@ +package io.neomodule.layout.listener; + +import android.util.Log; +import android.view.View; +import android.view.ViewGroup; + +import org.json.JSONArray; +import org.json.JSONException; + +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; + +import io.neomodule.layout.LayoutInfo; + +/** + * @author kiva + */ + +public class OnClickForwarder implements View.OnClickListener { + private ViewGroup parent; + private String methodName; + + public OnClickForwarder(ViewGroup parent, String methodName) { + this.parent = parent; + this.methodName = methodName; + } + + @Override + public void onClick(View view) { + ViewGroup root = parent; + LayoutInfo info = null; + while (root != null && (root.getParent() instanceof ViewGroup)) { + if (root.getTag() != null && root.getTag() instanceof LayoutInfo) { + info = (LayoutInfo) root.getTag(); + if (info.delegate != null) break; + } + root = (ViewGroup) root.getParent(); + } + if (info != null && info.delegate != null) { + final Object delegate = info.delegate; + invokeMethod(delegate, methodName, false, view); + } else { + Log.e("DynamicLayoutInflater", "Unable to find valid delegate for click named " + methodName); + } + } + + private void invokeMethod(Object delegate, final String methodName, boolean withView, View view) { + Object[] args = null; + String finalMethod = methodName; + if (methodName.endsWith(")")) { + String[] parts = methodName.split("[(]", 2); + finalMethod = parts[0]; + try { + String argText = parts[1].replace(""", "\""); + JSONArray arr = new JSONArray("[" + argText.substring(0, argText.length() - 1) + "]"); + args = new Object[arr.length()]; + for (int i = 0; i < arr.length(); i++) { + args[i] = arr.get(i); + } + } catch (JSONException e) { + e.printStackTrace(); + } + } else if (withView) { + args = new Object[1]; + args[0] = view; + } + Class klass = delegate.getClass(); + try { + + Class[] argClasses = null; + if (args != null && args.length > 0) { + argClasses = new Class[args.length]; + if (withView) { + argClasses[0] = View.class; + } else { + for (int i = 0; i < args.length; i++) { + Class argClass = args[i].getClass(); + if (argClass == Integer.class) + argClass = int.class; // Nobody uses Integer... + argClasses[i] = argClass; + } + } + } + Method method = klass.getMethod(finalMethod, argClasses); + method.invoke(delegate, args); + } catch (IllegalAccessException e) { + e.printStackTrace(); + } catch (InvocationTargetException e) { + e.printStackTrace(); + } catch (NoSuchMethodException e) { + e.printStackTrace(); + if (!withView && !methodName.endsWith(")")) { + invokeMethod(delegate, methodName, true, view); + } + } + } +} diff --git a/NeoModule/src/main/java/io/neomodule/layout/utils/AttributeApply.java b/NeoModule/src/main/java/io/neomodule/layout/utils/AttributeApply.java new file mode 100644 index 0000000..1cf8fd1 --- /dev/null +++ b/NeoModule/src/main/java/io/neomodule/layout/utils/AttributeApply.java @@ -0,0 +1,504 @@ +package io.neomodule.layout.utils; + +import android.graphics.drawable.GradientDrawable; +import android.graphics.drawable.StateListDrawable; +import android.view.View; +import android.view.ViewGroup; +import android.widget.Button; +import android.widget.FrameLayout; +import android.widget.LinearLayout; +import android.widget.RelativeLayout; + +import java.util.Map; + +import io.neomodule.layout.Configuration; +import io.neomodule.layout.LayoutInfo; + +/** + * @author kiva + */ + +public class AttributeApply { + /** + * 存储处理中的状态 + */ + private static class Status { + int layoutRule; + int marginLeft = 0; + int marginRight = 0; + int marginTop = 0; + int marginBottom = 0; + int paddingLeft = 0; + int paddingRight = 0; + int paddingTop = 0; + int paddingBottom = 0; + boolean hasCornerRadius = false; + boolean hasCornerRadii = false; + boolean layoutTarget = false; + + Status(Configuration config) { + this.layoutRule = config.noLayoutRule; + } + } + + private View view; + private Map attrs; + private ViewGroup parent; + + public AttributeApply(View view, Map attrs, ViewGroup parent) { + this.view = view; + this.attrs = attrs; + this.parent = parent; + } + + /** + * 把xml里定义的属性设置到 View 中 + * + * @param config LayoutInflater的配置 + */ + public void apply(Configuration config) { + Status status = new Status(config); + ViewGroup.LayoutParams layoutParams = view.getLayoutParams(); + + for (Map.Entry entry : attrs.entrySet()) { + String attr = entry.getKey(); + // 如果存在预设的处理方式,我们就不管了 + if (config.viewRunnables.containsKey(attr)) { + config.viewRunnables.get(attr).apply(view, entry.getValue(), parent, attrs); + continue; + } + + if (attr.startsWith("cornerRadius")) { + status.hasCornerRadius = true; + status.hasCornerRadii = !attr.equals("cornerRadius"); + continue; + } + + applyAttribute(attr, entry, layoutParams, status); + + if (status.layoutRule != config.noLayoutRule && parent instanceof RelativeLayout) { + if (status.layoutTarget) { + int anchor = UniqueId.idFromString(parent, AttributeParser.parseId(entry.getValue())); + ((RelativeLayout.LayoutParams) layoutParams).addRule(status.layoutRule, anchor); + } else if (entry.getValue().equals("true")) { + ((RelativeLayout.LayoutParams) layoutParams).addRule(status.layoutRule); + } + } + } + + // 处理 View 的背景 + if (attrs.containsKey("background") || attrs.containsKey("borderColor")) { + String backgroundValue = attrs.containsKey("background") ? attrs.get("background") : null; + + // 如果直接从 drawable 中拿那就简单多了 + if (backgroundValue != null && backgroundValue.startsWith("@drawable/")) { + applyBackgroundDrawable(backgroundValue); + + } else if (backgroundValue == null + || backgroundValue.startsWith("#") + || backgroundValue.startsWith("@color")) { + applyBackgroundColor(config, status, backgroundValue); + } + } + + applyParsedMargin(status, layoutParams); + applyParsedPadding(status); + view.setLayoutParams(layoutParams); + } + + /** + * 解析每一个属性 + * + * @param attr 属性名 + * @param entry 属性的名和值 + * @param layoutParams 父视图的 LayoutParams + * @param status 处理状态 + */ + private void applyAttribute(String attr, Map.Entry entry, + ViewGroup.LayoutParams layoutParams, Status status) { + if (attr.startsWith("layout_margin")) { + applyLayoutMargin(attr, entry, status); + return; + } + + if (attr.startsWith("padding")) { + applyPadding(attr, entry, status); + return; + } + + switch (attr) { + case "id": + applyId(entry); + break; + case "width": + case "layout_width": + applyLayoutWidth(layoutParams, entry); + break; + case "height": + case "layout_height": + applyLayoutHeight(layoutParams, entry); + break; + case "layout_gravity": + applyGravity(layoutParams, entry); + break; + case "layout_weight": + applyLayoutWeight(layoutParams, entry); + break; + case "layout_below": + status.layoutRule = RelativeLayout.BELOW; + status.layoutTarget = true; + break; + case "layout_above": + status.layoutRule = RelativeLayout.ABOVE; + status.layoutTarget = true; + break; + case "layout_toLeftOf": + status.layoutRule = RelativeLayout.LEFT_OF; + status.layoutTarget = true; + break; + case "layout_toRightOf": + status.layoutRule = RelativeLayout.RIGHT_OF; + status.layoutTarget = true; + break; + case "layout_alignBottom": + status.layoutRule = RelativeLayout.ALIGN_BOTTOM; + status.layoutTarget = true; + break; + case "layout_alignTop": + status.layoutRule = RelativeLayout.ALIGN_TOP; + status.layoutTarget = true; + break; + case "layout_alignLeft": + case "layout_alignStart": + status.layoutRule = RelativeLayout.ALIGN_LEFT; + status.layoutTarget = true; + break; + case "layout_alignRight": + case "layout_alignEnd": + status.layoutRule = RelativeLayout.ALIGN_RIGHT; + status.layoutTarget = true; + break; + case "layout_alignParentBottom": + status.layoutRule = RelativeLayout.ALIGN_PARENT_BOTTOM; + break; + case "layout_alignParentTop": + status.layoutRule = RelativeLayout.ALIGN_PARENT_TOP; + break; + case "layout_alignParentLeft": + case "layout_alignParentStart": + status.layoutRule = RelativeLayout.ALIGN_PARENT_LEFT; + break; + case "layout_alignParentRight": + case "layout_alignParentEnd": + status.layoutRule = RelativeLayout.ALIGN_PARENT_RIGHT; + break; + case "layout_centerHorizontal": + status.layoutRule = RelativeLayout.CENTER_HORIZONTAL; + break; + case "layout_centerVertical": + status.layoutRule = RelativeLayout.CENTER_VERTICAL; + break; + case "layout_centerInParent": + status.layoutRule = RelativeLayout.CENTER_IN_PARENT; + break; + } + } + + /** + * 从 status 中读取处理完毕的 padding 系列值,并且应用到 View 上 + * + * @param status 处理状态 + */ + private void applyParsedPadding(Status status) { + view.setPadding(status.paddingLeft, status.paddingTop, status.paddingRight, status.paddingBottom); + } + + /** + * 从 status 中读取处理完毕的 layout_margin 系列值,并且应用到 View 上 + * + * @param status 处理状态 + * @param layoutParams 父视图的 LayoutParams + */ + private void applyParsedMargin(Status status, ViewGroup.LayoutParams layoutParams) { + if (layoutParams instanceof ViewGroup.MarginLayoutParams) { + ((ViewGroup.MarginLayoutParams) layoutParams).setMargins(status.marginLeft, + status.marginTop, + status.marginRight, + status.marginBottom); + } + } + + /** + * 从当前 app 的 drawable 中得到背景并设置到 View 上 + * + * @param drawableResourceName 资源在 drawable 下的名字 + */ + private void applyBackgroundDrawable(String drawableResourceName) { + view.setBackground(AttributeParser.getDrawableByName(view, drawableResourceName)); + } + + /** + * 解析一个 # 开头的颜色值或从当前app 的 R.color 下取得颜色应用于 View 的背景颜色 + * + * @param config LayoutInflater 配置 + * @param status 处理状态 + * @param colorValue # 或者 @color/ 开头的颜色值 + */ + private void applyBackgroundColor(Configuration config, Status status, String colorValue) { + int validatedColor = AttributeParser.parseColor(view, colorValue == null ? "#00000000" : colorValue); + + if (view instanceof Button || attrs.containsKey("pressedColor")) { + // 按钮有按下效果,所以我们视为同一种情况 + applyPressedColor(config, status, validatedColor); + + } else if (status.hasCornerRadius || attrs.containsKey("borderColor")) { + GradientDrawable gd = new GradientDrawable(); + gd.setColor(validatedColor); + + // 把 pressed 和正常视为同一种情况 + applyCorners(config, status, gd, gd); + applyBorderColor(gd, gd); + + view.setBackground(gd); + getLayoutInfo().backgroundDrawable = gd; + + } else { + view.setBackgroundColor(validatedColor); + } + } + + /** + * 处理 pressedColor 或者 Button 的背景色 + * @param config LayoutInflater 配置 + * @param status 出炉状态 + * @param colorValue 颜色值 + */ + private void applyPressedColor(Configuration config, Status status, int colorValue) { + int pressedColor; + + if (attrs.containsKey("pressedColor")) { + pressedColor = AttributeParser.parseColor(view, attrs.get("pressedColor")); + } else { + pressedColor = AttributeParser.adjustBrightness(colorValue, 0.9f); + } + + GradientDrawable gd = new GradientDrawable(); + gd.setColor(colorValue); + GradientDrawable pressedGd = new GradientDrawable(); + pressedGd.setColor(pressedColor); + + applyCorners(config, status, gd, pressedGd); + applyBorderColor(gd, pressedGd); + + StateListDrawable selector = new StateListDrawable(); + selector.addState(new int[]{android.R.attr.state_pressed}, pressedGd); + selector.addState(new int[]{}, gd); + view.setBackground(selector); + + getLayoutInfo().backgroundDrawable = gd; + } + + /** + * 处理 borderColor + * @param gd 背景 + * @param pressedGd 按下时的背景,如果没有,设置为 gd 即可 + */ + private void applyBorderColor(GradientDrawable gd, GradientDrawable pressedGd) { + if (attrs.containsKey("borderColor")) { + String borderWidth = "1dp"; + if (attrs.containsKey("borderWidth")) { + borderWidth = attrs.get("borderWidth"); + } + int borderWidthPx = DimensionConverter.toDimensionPixelSize(borderWidth, view.getResources().getDisplayMetrics()); + gd.setStroke(borderWidthPx, AttributeParser.parseColor(view, attrs.get("borderColor"))); + pressedGd.setStroke(borderWidthPx, AttributeParser.parseColor(view, attrs.get("borderColor"))); + } + } + + /** + * 处理 cornerRadius 或者 cornerRadiusXXX + * @param config LayoutInflater 配置 + * @param status 出炉状态 + * @param gd 背景 + * @param pressedGd 按下背景,如果没有,设置为 gd 即可 + */ + private void applyCorners(Configuration config, Status status, GradientDrawable gd, GradientDrawable pressedGd) { + if (status.hasCornerRadii) { + float radii[] = new float[8]; + for (int i = 0; i < config.viewCorners.length; i++) { + String corner = config.viewCorners[i]; + if (attrs.containsKey("cornerRadius" + corner)) { + radii[i * 2] = radii[i * 2 + 1] = DimensionConverter.toDimension(attrs.get("cornerRadius" + corner), view.getResources().getDisplayMetrics()); + } + gd.setCornerRadii(radii); + pressedGd.setCornerRadii(radii); + } + + } else if (status.hasCornerRadius) { + float cornerRadius = DimensionConverter.toDimension(attrs.get("cornerRadius"), view.getResources().getDisplayMetrics()); + gd.setCornerRadius(cornerRadius); + pressedGd.setCornerRadius(cornerRadius); + } + } + + /** + * 设置 xml 属性中的 padding 系列属性,如 padding="10dp", paddingLeft="16dp" + * + * @param attr 属性名,需要判断是 padding 还是 paddingXXX + * @param entry 属性值 + * @param status 处理状态 + */ + private void applyPadding(String attr, Map.Entry entry, Status status) { + switch (attr) { + case "padding": + status.paddingBottom = status.paddingLeft = status.paddingRight = status.paddingTop + = DimensionConverter.toDimensionPixelSize(entry.getValue(), + view.getResources().getDisplayMetrics()); + break; + case "paddingLeft": + status.paddingLeft = DimensionConverter.toDimensionPixelSize(entry.getValue(), + view.getResources().getDisplayMetrics()); + break; + case "paddingTop": + status.paddingTop = DimensionConverter.toDimensionPixelSize(entry.getValue(), + view.getResources().getDisplayMetrics()); + break; + case "paddingRight": + status.paddingRight = DimensionConverter.toDimensionPixelSize(entry.getValue(), + view.getResources().getDisplayMetrics()); + break; + case "paddingBottom": + status.paddingBottom = DimensionConverter.toDimensionPixelSize(entry.getValue(), + view.getResources().getDisplayMetrics()); + break; + + } + } + + /** + * 设置 xml 属性中的 layout_margin 系列属性,如 layout_margin="10dp", layout_marginRight="16dp" + * + * @param attr 属性名,需要判断是 layout_margin 还是 layout_marginXXX + * @param entry 属性值 + * @param status 处理状态 + */ + private void applyLayoutMargin(String attr, Map.Entry entry, Status status) { + switch (attr) { + case "layout_margin": + status.marginLeft = status.marginRight = status.marginTop = status.marginBottom + = DimensionConverter.toDimensionPixelSize(entry.getValue(), + view.getResources().getDisplayMetrics()); + break; + case "layout_marginLeft": + status.marginLeft = DimensionConverter.toDimensionPixelSize(entry.getValue(), + view.getResources().getDisplayMetrics(), parent, true); + break; + case "layout_marginTop": + status.marginTop = DimensionConverter.toDimensionPixelSize(entry.getValue(), + view.getResources().getDisplayMetrics(), parent, false); + break; + case "layout_marginRight": + status.marginRight = DimensionConverter.toDimensionPixelSize(entry.getValue(), + view.getResources().getDisplayMetrics(), parent, true); + break; + case "layout_marginBottom": + status.marginBottom = DimensionConverter.toDimensionPixelSize(entry.getValue(), + view.getResources().getDisplayMetrics(), parent, false); + break; + } + } + + /** + * 设置 layout_weight + * + * @param layoutParams 父视图的 LayoutParams + * @param entry 属性值 + */ + private void applyLayoutWeight(ViewGroup.LayoutParams layoutParams, Map.Entry entry) { + if (parent != null && parent instanceof LinearLayout) { + ((LinearLayout.LayoutParams) layoutParams).weight = Float.parseFloat(entry.getValue()); + } + } + + /** + * 设置 gravity + * + * @param layoutParams 父视图的 LayoutParams + * @param entry 属性值 + */ + private void applyGravity(ViewGroup.LayoutParams layoutParams, Map.Entry entry) { + if (parent != null && parent instanceof LinearLayout) { + ((LinearLayout.LayoutParams) layoutParams).gravity = AttributeParser.parseGravity(entry.getValue()); + } else if (parent != null && parent instanceof FrameLayout) { + ((FrameLayout.LayoutParams) layoutParams).gravity = AttributeParser.parseGravity(entry.getValue()); + } + } + + /** + * 设置 layout_height + * + * @param layoutParams 父视图的 LayoutParams + * @param entry 属性值 + */ + private void applyLayoutHeight(ViewGroup.LayoutParams layoutParams, Map.Entry entry) { + switch (entry.getValue()) { + case "wrap_content": + layoutParams.height = ViewGroup.LayoutParams.WRAP_CONTENT; + break; + case "fill_parent": + case "match_parent": + layoutParams.height = ViewGroup.LayoutParams.MATCH_PARENT; + break; + default: + layoutParams.height = DimensionConverter.toDimensionPixelSize(entry.getValue(), view.getResources().getDisplayMetrics(), parent, false); + break; + } + } + + /** + * 设置 layout_width + * + * @param layoutParams 父视图的 LayoutParams + * @param entry 属性值 + */ + private void applyLayoutWidth(ViewGroup.LayoutParams layoutParams, Map.Entry entry) { + switch (entry.getValue()) { + case "wrap_content": + layoutParams.width = ViewGroup.LayoutParams.WRAP_CONTENT; + break; + case "fill_parent": + case "match_parent": + layoutParams.width = ViewGroup.LayoutParams.MATCH_PARENT; + break; + default: + layoutParams.width = DimensionConverter.toDimensionPixelSize(entry.getValue(), view.getResources().getDisplayMetrics(), parent, true); + break; + } + } + + /** + * 设置 id + * + * @param entry 属性值 + */ + private void applyId(Map.Entry entry) { + String idString = AttributeParser.parseId(entry.getValue()); + if (parent != null) { + LayoutInfo info = getLayoutInfo(); + int newId = UniqueId.newId(); + view.setId(newId); + info.nameToIdNumber.put(idString, newId); + } + } + + private LayoutInfo getLayoutInfo() { + LayoutInfo info; + if (parent.getTag() != null && parent.getTag() instanceof LayoutInfo) { + info = (LayoutInfo) parent.getTag(); + } else { + info = new LayoutInfo(); + parent.setTag(info); + } + return info; + } +} diff --git a/NeoModule/src/main/java/io/neomodule/layout/utils/AttributeParser.java b/NeoModule/src/main/java/io/neomodule/layout/utils/AttributeParser.java new file mode 100644 index 0000000..2392ce3 --- /dev/null +++ b/NeoModule/src/main/java/io/neomodule/layout/utils/AttributeParser.java @@ -0,0 +1,89 @@ +package io.neomodule.layout.utils; + +import android.content.res.Resources; +import android.graphics.Color; +import android.graphics.drawable.Drawable; +import android.view.Gravity; +import android.view.View; +import android.view.ViewGroup; + +import io.neomodule.layout.listener.OnClickForwarder; + +/** + * @author kiva + */ + +public class AttributeParser { + public static int adjustBrightness(int color, float amount) { + int red = color & 0xFF0000 >> 16; + int green = color & 0x00FF00 >> 8; + int blue = color & 0x0000FF; + int result = (int) (blue * amount); + result += (int) (green * amount) << 8; + result += (int) (red * amount) << 16; + return result; + } + + public static String parseId(String value) { + if (value.startsWith("@+id/")) { + return value.substring(5); + } else if (value.startsWith("@id/")) { + return value.substring(4); + } + return value; + } + + public static int parseGravity(String value) { + int gravity = Gravity.NO_GRAVITY; + String[] parts = value.toLowerCase().split("[|]"); + for (String part : parts) { + switch (part) { + case "center": + gravity = gravity | Gravity.CENTER; + break; + case "left": + case "textStart": + gravity = gravity | Gravity.LEFT; + break; + case "right": + case "textEnd": + gravity = gravity | Gravity.RIGHT; + break; + case "top": + gravity = gravity | Gravity.TOP; + break; + case "bottom": + gravity = gravity | Gravity.BOTTOM; + break; + case "center_horizontal": + gravity = gravity | Gravity.CENTER_HORIZONTAL; + break; + case "center_vertical": + gravity = gravity | Gravity.CENTER_VERTICAL; + break; + } + } + return gravity; + } + + public static Drawable getDrawableByName(View view, String name) { + Resources resources = view.getResources(); + return resources.getDrawable(resources.getIdentifier(name, "drawable", + view.getContext().getPackageName())); + } + + public static int parseColor(View view, String text) { + if (text.startsWith("@color/")) { + Resources resources = view.getResources(); + return resources.getColor(resources.getIdentifier(text.substring("@color/".length()), "color", view.getContext().getPackageName())); + } + if (text.length() == 4 && text.startsWith("#")) { + text = "#" + text.charAt(1) + text.charAt(1) + text.charAt(2) + text.charAt(2) + text.charAt(3) + text.charAt(3); + } + return Color.parseColor(text); + } + + public static View.OnClickListener parseOnClick(ViewGroup parent, String methodName) { + return new OnClickForwarder(parent, methodName); + } +} diff --git a/NeoModule/src/main/java/io/neomodule/layout/utils/DimensionConverter.java b/NeoModule/src/main/java/io/neomodule/layout/utils/DimensionConverter.java new file mode 100644 index 0000000..b1eaa9b --- /dev/null +++ b/NeoModule/src/main/java/io/neomodule/layout/utils/DimensionConverter.java @@ -0,0 +1,104 @@ +package io.neomodule.layout.utils; + +import android.util.DisplayMetrics; +import android.util.Log; +import android.util.TypedValue; +import android.view.ViewGroup; + +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +public class DimensionConverter { + private static Map cached = new HashMap<>(); + + // -- Initialize dimension string to constant lookup. + private static final Map dimensionConstantLookup = initDimensionConstantLookup(); + + private static Map initDimensionConstantLookup() { + Map m = new HashMap<>(); + m.put("px", TypedValue.COMPLEX_UNIT_PX); + m.put("dip", TypedValue.COMPLEX_UNIT_DIP); + m.put("dp", TypedValue.COMPLEX_UNIT_DIP); + m.put("sp", TypedValue.COMPLEX_UNIT_SP); + m.put("pt", TypedValue.COMPLEX_UNIT_PT); + m.put("in", TypedValue.COMPLEX_UNIT_IN); + m.put("mm", TypedValue.COMPLEX_UNIT_MM); + return Collections.unmodifiableMap(m); + } + + // -- Initialize pattern for dimension string. + private static final Pattern DIMENSION_PATTERN = Pattern.compile("^\\s*(\\d+(\\.\\d+)*)\\s*([a-zA-Z]+)\\s*$"); + + public static int toDimensionPixelSize(String dimension, DisplayMetrics metrics, ViewGroup parent, boolean horizontal) { + if (dimension.endsWith("%")) { + float pct = Float.parseFloat(dimension.substring(0, dimension.length() - 1)) / 100.0f; + return (int) (pct * (horizontal ? parent.getMeasuredWidth() : parent.getMeasuredHeight())); + } + return toDimensionPixelSize(dimension, metrics); + } + + public static int toDimensionPixelSize(String dimension, DisplayMetrics metrics) { + // -- Mimics TypedValue.complexToDimensionPixelSize(int data, DisplayMetrics metrics). + final float f; + if (cached.containsKey(dimension)) { + f = cached.get(dimension); + } else { + InternalDimension internalDimension = toInternalDimension(dimension); + final float value = internalDimension.value; + f = TypedValue.applyDimension(internalDimension.unit, value, metrics); + cached.put(dimension, f); + } + final int res = (int) (f + 0.5f); + if (res != 0) return res; + if (f == 0) return 0; + if (f > 0) return 1; + return -1; + } + + public static float toDimension(String dimension, DisplayMetrics metrics) { + if (cached.containsKey(dimension)) return cached.get(dimension); + // -- Mimics TypedValue.complexToDimension(int data, DisplayMetrics metrics). + InternalDimension internalDimension = toInternalDimension(dimension); + float val = TypedValue.applyDimension(internalDimension.unit, internalDimension.value, metrics); + cached.put(dimension, val); + return val; + } + + private static InternalDimension toInternalDimension(String dimension) { + // -- Match target against pattern. + Matcher matcher = DIMENSION_PATTERN.matcher(dimension); + if (matcher.matches()) { + // -- Match found. + // -- Extract value. + float value = Float.valueOf(matcher.group(1)); + // -- Extract dimension units. + String unit = matcher.group(3).toLowerCase(); + // -- Get Android dimension constant. + Integer dimensionUnit = dimensionConstantLookup.get(unit); + if (dimensionUnit == null) { + // -- Invalid format. + throw new NumberFormatException(); + } else { + // -- Return valid dimension. + return new InternalDimension(value, dimensionUnit); + } + } else { + Log.e("DimensionConverter", "Invalid number format: " + dimension); + // -- Invalid format. + throw new NumberFormatException(); + } + } + + private static class InternalDimension { + float value; + int unit; + + InternalDimension(float value, int unit) { + this.value = value; + this.unit = unit; + } + } +} diff --git a/NeoModule/src/main/java/io/neomodule/layout/utils/UniqueId.java b/NeoModule/src/main/java/io/neomodule/layout/utils/UniqueId.java new file mode 100644 index 0000000..1a3935f --- /dev/null +++ b/NeoModule/src/main/java/io/neomodule/layout/utils/UniqueId.java @@ -0,0 +1,34 @@ +package io.neomodule.layout.utils; + +import android.view.View; +import android.view.ViewGroup; + +import io.neomodule.layout.LayoutInfo; + +/** + * @author kiva + */ + +public class UniqueId { + private static int ID = 52019; + + public static int newId() { + return ID++; + } + + public static int idFromString(View view, String id) { + if (!(view instanceof ViewGroup)) return 0; + Object tag = view.getTag(); + if (!(tag instanceof LayoutInfo)) return 0; // not inflated by this class + LayoutInfo info = (LayoutInfo) view.getTag(); + if (!info.nameToIdNumber.containsKey(id)) { + ViewGroup grp = (ViewGroup) view; + for (int i = 0; i < grp.getChildCount(); i++) { + int val = idFromString(grp.getChildAt(i), id); + if (val != 0) return val; + } + return 0; + } + return info.nameToIdNumber.get(id); + } +} diff --git a/app/build.gradle b/app/build.gradle index 87c436f..39522c4 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -8,8 +8,8 @@ android { applicationId "io.neoterm" minSdkVersion 21 targetSdkVersion 26 - versionCode 24 - versionName "1.2.2" + versionCode 25 + versionName "1.2.3" testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" resConfigs "zh-rCN", "zh-rTW" externalNativeBuild { diff --git a/build.gradle b/build.gradle index 387d1f9..fc948b2 100644 --- a/build.gradle +++ b/build.gradle @@ -1,7 +1,7 @@ // Top-level build file where you can add configuration options common to all sub-projects/modules. buildscript { - ext.kotlin_version = '1.1.3-2' + ext.kotlin_version = '1.1.4' repositories { maven { url 'https://dl.google.com/dl/android/maven2/' } jcenter() diff --git a/neomodule/.gitignore b/neomodule/.gitignore new file mode 100644 index 0000000..796b96d --- /dev/null +++ b/neomodule/.gitignore @@ -0,0 +1 @@ +/build diff --git a/neomodule/build.gradle b/neomodule/build.gradle new file mode 100644 index 0000000..e85a433 --- /dev/null +++ b/neomodule/build.gradle @@ -0,0 +1,32 @@ +apply plugin: 'com.android.library' + +android { + compileSdkVersion 26 + buildToolsVersion "26.0.1" + + + defaultConfig { + minSdkVersion 21 + targetSdkVersion 26 + versionCode 1 + versionName "1.0" + + testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" + + } + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' + } + } +} + +dependencies { + implementation fileTree(dir: 'libs', include: ['*.jar']) + + implementation 'com.android.support:appcompat-v7:26.0.1' + testImplementation 'junit:junit:4.12' + androidTestImplementation 'com.android.support.test:runner:1.0.0' + androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.0' +} diff --git a/neomodule/proguard-rules.pro b/neomodule/proguard-rules.pro new file mode 100644 index 0000000..0a0a5bc --- /dev/null +++ b/neomodule/proguard-rules.pro @@ -0,0 +1,25 @@ +# Add project specific ProGuard rules here. +# By default, the flags in this file are appended to flags specified +# in /Users/kiva/devel/android-sdk/tools/proguard/proguard-android.txt +# You can edit the include path and order by changing the proguardFiles +# directive in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# Add any project specific keep options here: + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile diff --git a/neomodule/src/androidTest/java/io/neomodule/ExampleInstrumentedTest.java b/neomodule/src/androidTest/java/io/neomodule/ExampleInstrumentedTest.java new file mode 100644 index 0000000..bed5013 --- /dev/null +++ b/neomodule/src/androidTest/java/io/neomodule/ExampleInstrumentedTest.java @@ -0,0 +1,26 @@ +package io.neomodule; + +import android.content.Context; +import android.support.test.InstrumentationRegistry; +import android.support.test.runner.AndroidJUnit4; + +import org.junit.Test; +import org.junit.runner.RunWith; + +import static org.junit.Assert.*; + +/** + * Instrumented test, which will execute on an Android device. + * + * @see Testing documentation + */ +@RunWith(AndroidJUnit4.class) +public class ExampleInstrumentedTest { + @Test + public void useAppContext() throws Exception { + // Context of the app under test. + Context appContext = InstrumentationRegistry.getTargetContext(); + + assertEquals("io.neomodule.test", appContext.getPackageName()); + } +} diff --git a/neomodule/src/main/AndroidManifest.xml b/neomodule/src/main/AndroidManifest.xml new file mode 100644 index 0000000..1729736 --- /dev/null +++ b/neomodule/src/main/AndroidManifest.xml @@ -0,0 +1,2 @@ + diff --git a/neomodule/src/main/res/values/strings.xml b/neomodule/src/main/res/values/strings.xml new file mode 100644 index 0000000..bcd4233 --- /dev/null +++ b/neomodule/src/main/res/values/strings.xml @@ -0,0 +1,3 @@ + + NeoModule + diff --git a/neomodule/src/test/java/io/neomodule/ExampleUnitTest.java b/neomodule/src/test/java/io/neomodule/ExampleUnitTest.java new file mode 100644 index 0000000..c0105bb --- /dev/null +++ b/neomodule/src/test/java/io/neomodule/ExampleUnitTest.java @@ -0,0 +1,17 @@ +package io.neomodule; + +import org.junit.Test; + +import static org.junit.Assert.*; + +/** + * Example local unit test, which will execute on the development machine (host). + * + * @see Testing documentation + */ +public class ExampleUnitTest { + @Test + public void addition_isCorrect() throws Exception { + assertEquals(4, 2 + 2); + } +} \ No newline at end of file diff --git a/settings.gradle b/settings.gradle index 8f3cf9f..c961af1 100644 --- a/settings.gradle +++ b/settings.gradle @@ -1 +1 @@ -include ':app', ':chrome-tabs', ':NeoLang' +include ':app', ':chrome-tabs', ':NeoLang', ':NeoModule'