Feature: Dynamic layout inflater
This commit is contained in:
parent
437879248c
commit
c92754b73a
9
NeoModule/src/main/java/io/neomodule/NeoModule.java
Normal file
9
NeoModule/src/main/java/io/neomodule/NeoModule.java
Normal file
@ -0,0 +1,9 @@
|
||||
package io.neomodule;
|
||||
|
||||
/**
|
||||
* @author kiva
|
||||
*/
|
||||
|
||||
public abstract class NeoModule {
|
||||
public abstract void launch();
|
||||
}
|
280
NeoModule/src/main/java/io/neomodule/layout/Configuration.java
Normal file
280
NeoModule/src/main/java/io/neomodule/layout/Configuration.java
Normal 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));
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
15
NeoModule/src/main/java/io/neomodule/layout/LayoutInfo.java
Normal file
15
NeoModule/src/main/java/io/neomodule/layout/LayoutInfo.java
Normal 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<>();
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
@ -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);
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -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 {
|
||||
|
@ -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
1
neomodule/.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
||||
/build
|
32
neomodule/build.gradle
Normal file
32
neomodule/build.gradle
Normal 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
25
neomodule/proguard-rules.pro
vendored
Normal 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
|
@ -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());
|
||||
}
|
||||
}
|
2
neomodule/src/main/AndroidManifest.xml
Normal file
2
neomodule/src/main/AndroidManifest.xml
Normal file
@ -0,0 +1,2 @@
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
package="io.neomodule" />
|
3
neomodule/src/main/res/values/strings.xml
Normal file
3
neomodule/src/main/res/values/strings.xml
Normal file
@ -0,0 +1,3 @@
|
||||
<resources>
|
||||
<string name="app_name">NeoModule</string>
|
||||
</resources>
|
17
neomodule/src/test/java/io/neomodule/ExampleUnitTest.java
Normal file
17
neomodule/src/test/java/io/neomodule/ExampleUnitTest.java
Normal 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);
|
||||
}
|
||||
}
|
@ -1 +1 @@
|
||||
include ':app', ':chrome-tabs', ':NeoLang'
|
||||
include ':app', ':chrome-tabs', ':NeoLang', ':NeoModule'
|
||||
|
Loading…
Reference in New Issue
Block a user