Feature: Dynamic layout inflater

This commit is contained in:
zt515 2017-08-18 01:14:56 +08:00
parent 437879248c
commit c92754b73a
21 changed files with 1496 additions and 4 deletions

View File

@ -0,0 +1,9 @@
package io.neomodule;
/**
* @author kiva
*/
public abstract class NeoModule {
public abstract void launch();
}

View File

@ -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<String, ViewAttributeRunnable> 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<String, String> 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<String, String> 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<String, String> 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<String, String> 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<String, String> 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<String, String> 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<String, String> 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<String, String> 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<String, String> attrs) {
if (view instanceof TextView) {
((TextView) view).setSingleLine();
}
}
});
viewRunnables.put("hint", new ViewAttributeRunnable() {
@Override
public void apply(View view, String value, ViewGroup parent, Map<String, String> 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<String, String> 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<String, String> 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<String, String> 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<String, String> 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<String, String> attrs) {
view.setClickable(value.equals("true"));
}
});
viewRunnables.put("tag", new ViewAttributeRunnable() {
@Override
public void apply(View view, String value, ViewGroup parent, Map<String, String> 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<String, String> attrs) {
view.setOnClickListener(AttributeParser.parseOnClick(parent, value));
}
});
}
}

View File

@ -0,0 +1,15 @@
package io.neomodule.layout;
import android.graphics.drawable.GradientDrawable;
import java.util.HashMap;
public class LayoutInfo {
public HashMap<String, Integer> nameToIdNumber;
public Object delegate;
public GradientDrawable backgroundDrawable;
public LayoutInfo() {
nameToIdNumber = new HashMap<>();
}
}

View File

@ -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 extends View> 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<String, String> getAttributesMap(Node currentNode) {
NamedNodeMap attributeMap = currentNode.getAttributes();
int attributeCount = attributeMap.getLength();
HashMap<String, String> 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<String, String> attrs, ViewGroup parent) {
CONFIG.createViewRunnablesIfNeeded();
new AttributeApply(view, attrs, parent).apply(CONFIG);
}
}

View File

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

View File

@ -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<String, String> attrs);
}

View File

@ -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("&quot;", "\"");
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);
}
}
}
}

View File

@ -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<String, String> attrs;
private ViewGroup parent;
public AttributeApply(View view, Map<String, String> 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<String, String> 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<String, String> 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<String, String> 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<String, String> 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<String, String> 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<String, String> 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<String, String> 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<String, String> 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<String, String> 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;
}
}

View File

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

View File

@ -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<String, Float> cached = new HashMap<>();
// -- Initialize dimension string to constant lookup.
private static final Map<String, Integer> dimensionConstantLookup = initDimensionConstantLookup();
private static Map<String, Integer> initDimensionConstantLookup() {
Map<String, Integer> 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;
}
}
}

View File

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

View File

@ -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 {

View File

@ -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()

1
neomodule/.gitignore vendored Normal file
View File

@ -0,0 +1 @@
/build

32
neomodule/build.gradle Normal file
View File

@ -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'
}

25
neomodule/proguard-rules.pro vendored Normal file
View File

@ -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

View File

@ -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 <a href="http://d.android.com/tools/testing">Testing documentation</a>
*/
@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());
}
}

View File

@ -0,0 +1,2 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="io.neomodule" />

View File

@ -0,0 +1,3 @@
<resources>
<string name="app_name">NeoModule</string>
</resources>

View File

@ -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 <a href="http://d.android.com/tools/testing">Testing documentation</a>
*/
public class ExampleUnitTest {
@Test
public void addition_isCorrect() throws Exception {
assertEquals(4, 2 + 2);
}
}

View File

@ -1 +1 @@
include ':app', ':chrome-tabs', ':NeoLang'
include ':app', ':chrome-tabs', ':NeoLang', ':NeoModule'