├── .gitignore ├── .idea ├── .name ├── compiler.xml ├── copyright │ └── profiles_settings.xml ├── encodings.xml ├── gradle.xml ├── misc.xml ├── modules.xml └── runConfigurations.xml ├── DynamicLayoutInflator.iml ├── LICENSE ├── README.md ├── app ├── .gitignore ├── app.iml ├── build.gradle ├── proguard-rules.pro └── src │ ├── androidTest │ └── java │ │ └── com │ │ └── nickandjerry │ │ └── dynamiclayoutinflator │ │ └── ApplicationTest.java │ └── main │ ├── AndroidManifest.xml │ ├── assets │ └── testlayout.xml │ ├── java │ └── com │ │ └── nickandjerry │ │ └── dynamiclayoutinflator │ │ ├── DimensionConverter.java │ │ ├── DynamicLayoutInflator.java │ │ └── MainActivity.java │ └── res │ ├── layout │ └── activity_main.xml │ ├── menu │ └── menu_main.xml │ ├── mipmap-hdpi │ └── ic_launcher.png │ ├── mipmap-mdpi │ └── ic_launcher.png │ ├── mipmap-xhdpi │ └── ic_launcher.png │ ├── mipmap-xxhdpi │ └── ic_launcher.png │ ├── values-w820dp │ └── dimens.xml │ └── values │ ├── dimens.xml │ ├── strings.xml │ └── styles.xml ├── build.gradle ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat └── settings.gradle /.gitignore: -------------------------------------------------------------------------------- 1 | # Built application files 2 | *.apk 3 | *.ap_ 4 | 5 | # Files for the Dalvik VM 6 | *.dex 7 | 8 | # Java class files 9 | *.class 10 | 11 | # Generated files 12 | bin/ 13 | gen/ 14 | 15 | # Gradle files 16 | .gradle/ 17 | build/ 18 | /*/build/ 19 | 20 | # Local configuration file (sdk path, etc) 21 | local.properties 22 | 23 | # Proguard folder generated by Eclipse 24 | proguard/ 25 | 26 | # Log Files 27 | *.log 28 | 29 | /.idea/workspace.xml 30 | /.idea/libraries 31 | .DS_Store 32 | /captures 33 | 34 | *~ 35 | -------------------------------------------------------------------------------- /.idea/.name: -------------------------------------------------------------------------------- 1 | DynamicLayoutInflator -------------------------------------------------------------------------------- /.idea/compiler.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 22 | -------------------------------------------------------------------------------- /.idea/copyright/profiles_settings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /.idea/encodings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /.idea/gradle.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 22 | 23 | -------------------------------------------------------------------------------- /.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 16 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 43 | -------------------------------------------------------------------------------- /.idea/modules.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /.idea/runConfigurations.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 11 | 12 | -------------------------------------------------------------------------------- /DynamicLayoutInflator.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Nick White 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # DynamicLayoutInflator 2 | Inflate android XML layouts at runtime 3 | 4 | Example: (see MainActivity.java for details) 5 | 6 | ```java 7 | protected void onCreate(Bundle savedInstanceState) { 8 | super.onCreate(savedInstanceState); 9 | setContentView(R.layout.activity_main); 10 | RelativeLayout main = (RelativeLayout)findViewById(R.id.main_top); 11 | try { 12 | View view = DynamicLayoutInflator.inflateName(this, "testlayout"); 13 | main.addView(view); 14 | // The above two lines could also be written like this, if you know that testlayout is in your assets: 15 | // View view = DynamicLayoutInflator.inflate(this, getAssets().open("testlayout.xml"), main); 16 | DynamicLayoutInflator.setDelegate(view, this); 17 | // If we have , this is how to access it: 18 | TextView someTextView = (TextView)DynamicLayoutInflator.findViewByIdString("message"); 19 | } catch (IOException e) { 20 | // This happens if getAssets().open() fails to find your layout 21 | e.printStackTrace(); 22 | } 23 | 24 | // Alternatively, you can pass in a String of xml: 25 | RelativeLayout layout = (RelativeLayout)DynamicLayoutInflator.inflate(this, ""); 26 | } 27 | 28 | // Since we set this class as the delegate, anything in the layout that has onClick="ohHello(2)" will log "howdy number: 2" 29 | public void ohHello(int i) { 30 | Log.d("nick", "howdy number: " + i); 31 | } 32 | ``` 33 | 34 | ## See also: 35 | 36 | [iOS dynamic xml layouts](https://github.com/nickwah/NWLayoutInflator) 37 | 38 | ## License 39 | 40 | Open source under the MIT license. See LICENSE for details. 41 | -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /app/app.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 8 | 9 | 10 | 11 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | -------------------------------------------------------------------------------- /app/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.application' 2 | 3 | android { 4 | compileSdkVersion 22 5 | buildToolsVersion "22.0.1" 6 | 7 | defaultConfig { 8 | applicationId "com.nickandjerry.dynamiclayoutinflator" 9 | minSdkVersion 15 10 | targetSdkVersion 22 11 | versionCode 1 12 | versionName "1.0" 13 | } 14 | buildTypes { 15 | release { 16 | minifyEnabled false 17 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' 18 | } 19 | } 20 | } 21 | 22 | dependencies { 23 | compile fileTree(dir: 'libs', include: ['*.jar']) 24 | compile 'com.android.support:appcompat-v7:22.2.0' 25 | } 26 | -------------------------------------------------------------------------------- /app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # By default, the flags in this file are appended to flags specified 3 | # in /Users/nick/files/adt-bundle-mac-x86_64-20131030/sdk/tools/proguard/proguard-android.txt 4 | # You can edit the include path and order by changing the proguardFiles 5 | # directive in build.gradle. 6 | # 7 | # For more details, see 8 | # http://developer.android.com/guide/developing/tools/proguard.html 9 | 10 | # Add any project specific keep options here: 11 | 12 | # If your project uses WebView with JS, uncomment the following 13 | # and specify the fully qualified class name to the JavaScript interface 14 | # class: 15 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 16 | # public *; 17 | #} 18 | -------------------------------------------------------------------------------- /app/src/androidTest/java/com/nickandjerry/dynamiclayoutinflator/ApplicationTest.java: -------------------------------------------------------------------------------- 1 | package com.nickandjerry.dynamiclayoutinflator; 2 | 3 | import android.app.Application; 4 | import android.test.ApplicationTestCase; 5 | 6 | /** 7 | * Testing Fundamentals 8 | */ 9 | public class ApplicationTest extends ApplicationTestCase { 10 | public ApplicationTest() { 11 | super(Application.class); 12 | } 13 | } -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 10 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /app/src/main/assets/testlayout.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/java/com/nickandjerry/dynamiclayoutinflator/DimensionConverter.java: -------------------------------------------------------------------------------- 1 | package com.nickandjerry.dynamiclayoutinflator; 2 | 3 | import android.util.DisplayMetrics; 4 | import android.util.Log; 5 | import android.util.TypedValue; 6 | import android.view.ViewGroup; 7 | 8 | import java.util.Collections; 9 | import java.util.HashMap; 10 | import java.util.Map; 11 | import java.util.regex.Matcher; 12 | import java.util.regex.Pattern; 13 | 14 | /** 15 | * Created by nick on 8/10/15. 16 | * 17 | * Taken from http://stackoverflow.com/questions/8343971/how-to-parse-a-dimension-string-and-convert-it-to-a-dimension-value 18 | */ 19 | public class DimensionConverter { 20 | public static Mapcached = new HashMap<>(); 21 | 22 | // -- Initialize dimension string to constant lookup. 23 | public static final Map dimensionConstantLookup = initDimensionConstantLookup(); 24 | private static Map initDimensionConstantLookup() { 25 | Map m = new HashMap(); 26 | m.put("px", TypedValue.COMPLEX_UNIT_PX); 27 | m.put("dip", TypedValue.COMPLEX_UNIT_DIP); 28 | m.put("dp", TypedValue.COMPLEX_UNIT_DIP); 29 | m.put("sp", TypedValue.COMPLEX_UNIT_SP); 30 | m.put("pt", TypedValue.COMPLEX_UNIT_PT); 31 | m.put("in", TypedValue.COMPLEX_UNIT_IN); 32 | m.put("mm", TypedValue.COMPLEX_UNIT_MM); 33 | return Collections.unmodifiableMap(m); 34 | } 35 | // -- Initialize pattern for dimension string. 36 | private static final Pattern DIMENSION_PATTERN = Pattern.compile("^\\s*(\\d+(\\.\\d+)*)\\s*([a-zA-Z]+)\\s*$"); 37 | 38 | public static int stringToDimensionPixelSize(String dimension, DisplayMetrics metrics, ViewGroup parent, boolean horizontal) { 39 | if (dimension.endsWith("%")) { 40 | float pct = Float.parseFloat(dimension.substring(0, dimension.length() - 1)) / 100.0f; 41 | return (int)(pct * (horizontal ? parent.getMeasuredWidth() : parent.getMeasuredHeight())); 42 | } 43 | return stringToDimensionPixelSize(dimension, metrics); 44 | } 45 | public static int stringToDimensionPixelSize(String dimension, DisplayMetrics metrics) { 46 | // -- Mimics TypedValue.complexToDimensionPixelSize(int data, DisplayMetrics metrics). 47 | final float f; 48 | if (cached.containsKey(dimension)) { 49 | f = cached.get(dimension); 50 | } else { 51 | InternalDimension internalDimension = stringToInternalDimension(dimension); 52 | final float value = internalDimension.value; 53 | f = TypedValue.applyDimension(internalDimension.unit, value, metrics); 54 | cached.put(dimension, f); 55 | } 56 | final int res = (int)(f+0.5f); 57 | if (res != 0) return res; 58 | if (f == 0) return 0; 59 | if (f > 0) return 1; 60 | return -1; 61 | } 62 | 63 | public static float stringToDimension(String dimension, DisplayMetrics metrics) { 64 | if (cached.containsKey(dimension)) return cached.get(dimension); 65 | // -- Mimics TypedValue.complexToDimension(int data, DisplayMetrics metrics). 66 | InternalDimension internalDimension = stringToInternalDimension(dimension); 67 | float val = TypedValue.applyDimension(internalDimension.unit, internalDimension.value, metrics); 68 | cached.put(dimension, val); 69 | return val; 70 | } 71 | 72 | private static InternalDimension stringToInternalDimension(String dimension) { 73 | // -- Match target against pattern. 74 | Matcher matcher = DIMENSION_PATTERN.matcher(dimension); 75 | if (matcher.matches()) { 76 | // -- Match found. 77 | // -- Extract value. 78 | float value = Float.valueOf(matcher.group(1)).floatValue(); 79 | // -- Extract dimension units. 80 | String unit = matcher.group(3).toLowerCase(); 81 | // -- Get Android dimension constant. 82 | Integer dimensionUnit = dimensionConstantLookup.get(unit); 83 | if (dimensionUnit == null) { 84 | // -- Invalid format. 85 | throw new NumberFormatException(); 86 | } else { 87 | // -- Return valid dimension. 88 | return new InternalDimension(value, dimensionUnit); 89 | } 90 | } else { 91 | Log.e("DimensionConverter", "Invalid number format: " + dimension); 92 | // -- Invalid format. 93 | throw new NumberFormatException(); 94 | } 95 | } 96 | 97 | private static class InternalDimension { 98 | float value; 99 | int unit; 100 | 101 | public InternalDimension(float value, int unit) { 102 | this.value = value; 103 | this.unit = unit; 104 | } 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /app/src/main/java/com/nickandjerry/dynamiclayoutinflator/DynamicLayoutInflator.java: -------------------------------------------------------------------------------- 1 | package com.nickandjerry.dynamiclayoutinflator; 2 | 3 | import android.annotation.SuppressLint; 4 | import android.content.Context; 5 | import android.content.res.Resources; 6 | import android.graphics.Color; 7 | import android.graphics.Typeface; 8 | import android.graphics.drawable.Drawable; 9 | import android.graphics.drawable.GradientDrawable; 10 | import android.graphics.drawable.StateListDrawable; 11 | import android.os.Build; 12 | import android.support.annotation.Nullable; 13 | import android.text.InputType; 14 | import android.text.TextUtils; 15 | import android.util.Log; 16 | import android.util.TypedValue; 17 | import android.view.Gravity; 18 | import android.view.LayoutInflater; 19 | import android.view.View; 20 | import android.view.ViewGroup; 21 | import android.widget.Button; 22 | import android.widget.EditText; 23 | import android.widget.FrameLayout; 24 | import android.widget.ImageView; 25 | import android.widget.LinearLayout; 26 | import android.widget.RelativeLayout; 27 | import android.widget.TextView; 28 | 29 | import org.json.JSONArray; 30 | import org.json.JSONException; 31 | import org.w3c.dom.Document; 32 | import org.w3c.dom.NamedNodeMap; 33 | import org.w3c.dom.Node; 34 | import org.w3c.dom.NodeList; 35 | import org.xml.sax.SAXException; 36 | 37 | import java.io.ByteArrayInputStream; 38 | import java.io.File; 39 | import java.io.FileInputStream; 40 | import java.io.FileNotFoundException; 41 | import java.io.IOException; 42 | import java.io.InputStream; 43 | import java.lang.reflect.Constructor; 44 | import java.lang.reflect.InvocationTargetException; 45 | import java.lang.reflect.Method; 46 | import java.util.HashMap; 47 | import java.util.Map; 48 | 49 | import javax.xml.parsers.DocumentBuilder; 50 | import javax.xml.parsers.DocumentBuilderFactory; 51 | import javax.xml.parsers.ParserConfigurationException; 52 | 53 | /** 54 | * Copyright Nicholas White 2015. 55 | * Source: https://github.com/nickwah/DynamicLayoutInflator 56 | * 57 | * Licensed under the MIT License: 58 | * 59 | Permission is hereby granted, free of charge, to any person obtaining a copy 60 | of this software and associated documentation files (the "Software"), to deal 61 | in the Software without restriction, including without limitation the rights 62 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 63 | copies of the Software, and to permit persons to whom the Software is 64 | furnished to do so, subject to the following conditions: 65 | 66 | The above copyright notice and this permission notice shall be included in 67 | all copies or substantial portions of the Software. 68 | 69 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 70 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 71 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 72 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 73 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 74 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 75 | THE SOFTWARE. 76 | */ 77 | public class DynamicLayoutInflator { 78 | private static final String ns = null; 79 | public static final int NO_LAYOUT_RULE = -999; 80 | public static final String[] CORNERS = {"TopLeft", "TopRight", "BottomRight", "BottomLeft"}; 81 | public static int highestIdNumberUsed = 1234567; 82 | 83 | public interface ViewParamRunnable { 84 | void apply(View view, String value, ViewGroup parent, Map attrs); 85 | } 86 | public static Map viewRunnables; 87 | 88 | public interface ImageLoader { 89 | void loadImage(ImageView view, String url); 90 | void loadRoundedImage(ImageView view, String url, int radius); 91 | } 92 | 93 | private static ImageLoader imageLoader = null; 94 | public static void setImageLoader(ImageLoader il) { 95 | imageLoader = il; 96 | } 97 | 98 | public static class DynamicLayoutInfo { 99 | public DynamicLayoutInfo() { 100 | nameToIdNumber = new HashMap<>(); 101 | } 102 | public HashMap nameToIdNumber; 103 | public Object delegate; 104 | public GradientDrawable bgDrawable; 105 | } 106 | 107 | public static void setDelegate(View root, Object delegate) { 108 | DynamicLayoutInfo info; 109 | if (root.getTag() == null || !(root.getTag() instanceof DynamicLayoutInfo)) { 110 | info = new DynamicLayoutInfo(); 111 | root.setTag(info); 112 | } else { 113 | info = (DynamicLayoutInfo)root.getTag(); 114 | } 115 | info.delegate = delegate; 116 | } 117 | 118 | public static View inflateName(Context context, String name) { 119 | return inflateName(context, name, null); 120 | } 121 | public static View inflateName(Context context, String name, ViewGroup parent) { 122 | if (name.startsWith("<")) { 123 | // Assume it's XML 124 | return DynamicLayoutInflator.inflate(context, name, parent); 125 | } else { 126 | File savedFile = context.getFileStreamPath(name + ".xml"); 127 | try { 128 | InputStream fileStream = new FileInputStream(savedFile); 129 | return DynamicLayoutInflator.inflate(context, fileStream, parent); 130 | } catch (FileNotFoundException e) { 131 | } 132 | try { 133 | InputStream assetStream = context.getAssets().open(name + ".xml"); 134 | return DynamicLayoutInflator.inflate(context, assetStream, parent); 135 | } catch (IOException e) { 136 | } 137 | int id = context.getResources().getIdentifier(name, "layout", context.getPackageName()); 138 | if (id > 0) { 139 | LayoutInflater inflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE); 140 | return inflater.inflate(id, parent, false); 141 | } 142 | } 143 | return null; 144 | } 145 | 146 | public static View inflate(Context context, File xmlPath) { 147 | InputStream inputStream = null; 148 | try { 149 | inputStream = new FileInputStream(xmlPath); 150 | } catch (FileNotFoundException e) { 151 | e.printStackTrace(); 152 | return null; 153 | } 154 | return DynamicLayoutInflator.inflate(context, inputStream); 155 | } 156 | 157 | public static View inflate(Context context, String xml) { 158 | InputStream inputStream = new ByteArrayInputStream(xml.getBytes()); 159 | return DynamicLayoutInflator.inflate(context, inputStream); 160 | } 161 | 162 | public static View inflate(Context context, String xml, ViewGroup parent) { 163 | InputStream inputStream = new ByteArrayInputStream(xml.getBytes()); 164 | return DynamicLayoutInflator.inflate(context, inputStream, parent); 165 | } 166 | 167 | public static View inflate(Context context, InputStream inputStream) { 168 | return inflate(context, inputStream, null); 169 | } 170 | public static View inflate(Context context, InputStream inputStream, ViewGroup parent) { 171 | try { 172 | DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance(); 173 | dbf.setNamespaceAware(true); 174 | DocumentBuilder db = dbf.newDocumentBuilder(); 175 | Document document = db.parse(inputStream); 176 | try { 177 | return inflate(context, document.getDocumentElement(), parent); 178 | } finally { 179 | inputStream.close(); 180 | } 181 | } catch (IOException e) { 182 | e.printStackTrace(); 183 | } catch (ParserConfigurationException e) { 184 | e.printStackTrace(); 185 | } catch (SAXException e) { 186 | e.printStackTrace(); 187 | } 188 | return null; 189 | } 190 | 191 | public static View inflate(Context context, Node node) { 192 | return inflate(context, node, null); 193 | } 194 | public static View inflate(Context context, Node node, ViewGroup parent) { 195 | View mainView = getViewForName(context, node.getNodeName()); 196 | if (parent != null) parent.addView(mainView); // have to add to parent to enable certain layout attrs 197 | applyAttributes(mainView, getAttributesMap(node), parent); 198 | if (mainView instanceof ViewGroup && node.hasChildNodes()) { 199 | parseChildren(context, node, (ViewGroup) mainView); 200 | } 201 | return mainView; 202 | } 203 | 204 | private static void parseChildren(Context context, Node node, ViewGroup mainView) { 205 | NodeList nodeList = node.getChildNodes(); 206 | for (int i = 0; i < nodeList.getLength(); i++) { 207 | Node currentNode = nodeList.item(i); 208 | if (currentNode.getNodeType() != Node.ELEMENT_NODE) continue; 209 | inflate(context, currentNode, mainView); // this recursively can call parseChildren 210 | } 211 | } 212 | 213 | private static View getViewForName(Context context, String name) { 214 | try { 215 | if (!name.contains(".")) { 216 | name = "android.widget." + name; 217 | } 218 | Class clazz = Class.forName(name); 219 | Constructor constructor = clazz.getConstructor(Context.class); 220 | return (View)constructor.newInstance(context); 221 | } catch (ClassNotFoundException e) { 222 | e.printStackTrace(); 223 | } catch (NoSuchMethodException e) { 224 | e.printStackTrace(); 225 | } catch (InvocationTargetException e) { 226 | e.printStackTrace(); 227 | } catch (InstantiationException e) { 228 | e.printStackTrace(); 229 | } catch (IllegalAccessException e) { 230 | e.printStackTrace(); 231 | } 232 | return null; 233 | } 234 | 235 | private static HashMap getAttributesMap(Node currentNode) { 236 | NamedNodeMap attributeMap = currentNode.getAttributes(); 237 | int attributeCount = attributeMap.getLength(); 238 | HashMap attributes = new HashMap<>(attributeCount); 239 | for (int j = 0; j < attributeCount; j++) { 240 | Node attr = attributeMap.item(j); 241 | String nodeName = attr.getNodeName(); 242 | if (nodeName.startsWith("android:")) nodeName = nodeName.substring(8); 243 | attributes.put(nodeName, attr.getNodeValue()); 244 | } 245 | return attributes; 246 | } 247 | 248 | @SuppressLint("NewApi") 249 | private static void applyAttributes(View view, Map attrs, ViewGroup parent) { 250 | if (viewRunnables == null) createViewRunnables(); 251 | ViewGroup.LayoutParams layoutParams = view.getLayoutParams(); 252 | int layoutRule; 253 | int marginLeft = 0, marginRight = 0, marginTop = 0, marginBottom = 0, 254 | paddingLeft = 0, paddingRight = 0, paddingTop = 0, paddingBottom = 0; 255 | boolean hasCornerRadius = false, hasCornerRadii = false; 256 | for (Map.Entry entry : attrs.entrySet()) { 257 | String attr = entry.getKey(); 258 | if (viewRunnables.containsKey(attr)) { 259 | viewRunnables.get(attr).apply(view, entry.getValue(), parent, attrs); 260 | continue; 261 | } 262 | if (attr.startsWith("cornerRadius")) { 263 | hasCornerRadius = true; 264 | hasCornerRadii = !attr.equals("cornerRadius"); 265 | continue; 266 | } 267 | layoutRule = NO_LAYOUT_RULE; 268 | boolean layoutTarget = false; 269 | switch (attr) { 270 | case "id": 271 | String idValue = parseId(entry.getValue()); 272 | if (parent != null) { 273 | DynamicLayoutInfo info = getDynamicLayoutInfo(parent); 274 | int newId = highestIdNumberUsed++; 275 | view.setId(newId); 276 | info.nameToIdNumber.put(idValue, newId); 277 | } 278 | break; 279 | case "width": 280 | case "layout_width": 281 | switch (entry.getValue()) { 282 | case "wrap_content": 283 | layoutParams.width = ViewGroup.LayoutParams.WRAP_CONTENT; 284 | break; 285 | case "fill_parent": 286 | case "match_parent": 287 | layoutParams.width = ViewGroup.LayoutParams.MATCH_PARENT; 288 | break; 289 | default: 290 | layoutParams.width = DimensionConverter.stringToDimensionPixelSize(entry.getValue(), view.getResources().getDisplayMetrics(), parent, true); 291 | break; 292 | } 293 | break; 294 | case "height": 295 | case "layout_height": 296 | switch (entry.getValue()) { 297 | case "wrap_content": 298 | layoutParams.height = ViewGroup.LayoutParams.WRAP_CONTENT; 299 | break; 300 | case "fill_parent": 301 | case "match_parent": 302 | layoutParams.height = ViewGroup.LayoutParams.MATCH_PARENT; 303 | break; 304 | default: 305 | layoutParams.height = DimensionConverter.stringToDimensionPixelSize(entry.getValue(), view.getResources().getDisplayMetrics(), parent, false); 306 | break; 307 | } 308 | break; 309 | case "layout_gravity": 310 | if (parent != null && parent instanceof LinearLayout) { 311 | ((LinearLayout.LayoutParams)layoutParams).gravity = parseGravity(entry.getValue()); 312 | } else if (parent != null && parent instanceof FrameLayout) { 313 | ((FrameLayout.LayoutParams)layoutParams).gravity = parseGravity(entry.getValue()); 314 | } 315 | break; 316 | case "layout_weight": 317 | if (parent != null && parent instanceof LinearLayout) { 318 | ((LinearLayout.LayoutParams)layoutParams).weight = Float.parseFloat(entry.getValue()); 319 | } 320 | break; 321 | case "layout_below": 322 | layoutRule = RelativeLayout.BELOW; 323 | layoutTarget = true; 324 | break; 325 | case "layout_above": 326 | layoutRule = RelativeLayout.ABOVE; 327 | layoutTarget = true; 328 | break; 329 | case "layout_toLeftOf": 330 | layoutRule = RelativeLayout.LEFT_OF; 331 | layoutTarget = true; 332 | break; 333 | case "layout_toRightOf": 334 | layoutRule = RelativeLayout.RIGHT_OF; 335 | layoutTarget = true; 336 | break; 337 | case "layout_alignBottom": 338 | layoutRule = RelativeLayout.ALIGN_BOTTOM; 339 | layoutTarget = true; 340 | break; 341 | case "layout_alignTop": 342 | layoutRule = RelativeLayout.ALIGN_TOP; 343 | layoutTarget = true; 344 | break; 345 | case "layout_alignLeft": 346 | case "layout_alignStart": 347 | layoutRule = RelativeLayout.ALIGN_LEFT; 348 | layoutTarget = true; 349 | break; 350 | case "layout_alignRight": 351 | case "layout_alignEnd": 352 | layoutRule = RelativeLayout.ALIGN_RIGHT; 353 | layoutTarget = true; 354 | break; 355 | case "layout_alignParentBottom": 356 | layoutRule = RelativeLayout.ALIGN_PARENT_BOTTOM; 357 | break; 358 | case "layout_alignParentTop": 359 | layoutRule = RelativeLayout.ALIGN_PARENT_TOP; 360 | break; 361 | case "layout_alignParentLeft": 362 | case "layout_alignParentStart": 363 | layoutRule = RelativeLayout.ALIGN_PARENT_LEFT; 364 | break; 365 | case "layout_alignParentRight": 366 | case "layout_alignParentEnd": 367 | layoutRule = RelativeLayout.ALIGN_PARENT_RIGHT; 368 | break; 369 | case "layout_centerHorizontal": 370 | layoutRule = RelativeLayout.CENTER_HORIZONTAL; 371 | break; 372 | case "layout_centerVertical": 373 | layoutRule = RelativeLayout.CENTER_VERTICAL; 374 | break; 375 | case "layout_centerInParent": 376 | layoutRule = RelativeLayout.CENTER_IN_PARENT; 377 | break; 378 | case "layout_margin": 379 | marginLeft = marginRight = marginTop = marginBottom = DimensionConverter.stringToDimensionPixelSize(entry.getValue(), view.getResources().getDisplayMetrics()); 380 | break; 381 | case "layout_marginLeft": 382 | marginLeft = DimensionConverter.stringToDimensionPixelSize(entry.getValue(), view.getResources().getDisplayMetrics(), parent, true); 383 | break; 384 | case "layout_marginTop": 385 | marginTop = DimensionConverter.stringToDimensionPixelSize(entry.getValue(), view.getResources().getDisplayMetrics(), parent, false); 386 | break; 387 | case "layout_marginRight": 388 | marginRight = DimensionConverter.stringToDimensionPixelSize(entry.getValue(), view.getResources().getDisplayMetrics(), parent, true); 389 | break; 390 | case "layout_marginBottom": 391 | marginBottom = DimensionConverter.stringToDimensionPixelSize(entry.getValue(), view.getResources().getDisplayMetrics(), parent, false); 392 | break; 393 | case "padding": 394 | paddingBottom = paddingLeft = paddingRight = paddingTop = DimensionConverter.stringToDimensionPixelSize(entry.getValue(), view.getResources().getDisplayMetrics()); 395 | break; 396 | case "paddingLeft": 397 | paddingLeft = DimensionConverter.stringToDimensionPixelSize(entry.getValue(), view.getResources().getDisplayMetrics()); 398 | break; 399 | case "paddingTop": 400 | paddingTop = DimensionConverter.stringToDimensionPixelSize(entry.getValue(), view.getResources().getDisplayMetrics()); 401 | break; 402 | case "paddingRight": 403 | paddingRight = DimensionConverter.stringToDimensionPixelSize(entry.getValue(), view.getResources().getDisplayMetrics()); 404 | break; 405 | case "paddingBottom": 406 | paddingBottom = DimensionConverter.stringToDimensionPixelSize(entry.getValue(), view.getResources().getDisplayMetrics()); 407 | break; 408 | 409 | } 410 | if (layoutRule != NO_LAYOUT_RULE && parent instanceof RelativeLayout) { 411 | if (layoutTarget) { 412 | int anchor = idNumFromIdString(parent, parseId(entry.getValue())); 413 | ((RelativeLayout.LayoutParams) layoutParams).addRule(layoutRule, anchor); 414 | } else if (entry.getValue().equals("true")) { 415 | ((RelativeLayout.LayoutParams) layoutParams).addRule(layoutRule); 416 | } 417 | } 418 | } 419 | // TODO: this is a giant mess; come up with a simpler way of deciding what to draw for the background 420 | if (attrs.containsKey("background") || attrs.containsKey("borderColor")) { 421 | String bgValue = attrs.containsKey("background") ? attrs.get("background") : null; 422 | if (bgValue != null && bgValue.startsWith("@drawable/")) { 423 | view.setBackground(getDrawableByName(view, bgValue)); 424 | } else if (bgValue == null || bgValue.startsWith("#") || bgValue.startsWith("@color")) { 425 | if (view instanceof Button || attrs.containsKey("pressedColor")) { 426 | int bgColor = parseColor(view, bgValue == null ? "#00000000" : bgValue); 427 | int pressedColor; 428 | if (attrs.containsKey("pressedColor")) { 429 | pressedColor = parseColor(view, attrs.get("pressedColor")); 430 | } else { 431 | pressedColor = adjustBrightness(bgColor, 0.9f); 432 | } 433 | GradientDrawable gd = new GradientDrawable(); 434 | gd.setColor(bgColor); 435 | GradientDrawable pressedGd = new GradientDrawable(); 436 | pressedGd.setColor(pressedColor); 437 | if (hasCornerRadii) { 438 | float radii[] = new float[8]; 439 | for (int i = 0; i < CORNERS.length; i++) { 440 | String corner = CORNERS[i]; 441 | if (attrs.containsKey("cornerRadius" + corner)) { 442 | radii[i * 2] = radii[i * 2 + 1] = DimensionConverter.stringToDimension(attrs.get("cornerRadius" + corner), view.getResources().getDisplayMetrics()); 443 | } 444 | gd.setCornerRadii(radii); 445 | pressedGd.setCornerRadii(radii); 446 | } 447 | } else if (hasCornerRadius) { 448 | float cornerRadius = DimensionConverter.stringToDimension(attrs.get("cornerRadius"), view.getResources().getDisplayMetrics()); 449 | gd.setCornerRadius(cornerRadius); 450 | pressedGd.setCornerRadius(cornerRadius); 451 | } 452 | if (attrs.containsKey("borderColor")) { 453 | String borderWidth = "1dp"; 454 | if (attrs.containsKey("borderWidth")) { 455 | borderWidth = attrs.get("borderWidth"); 456 | } 457 | int borderWidthPx = DimensionConverter.stringToDimensionPixelSize(borderWidth, view.getResources().getDisplayMetrics()); 458 | gd.setStroke(borderWidthPx, parseColor(view, attrs.get("borderColor"))); 459 | pressedGd.setStroke(borderWidthPx, parseColor(view, attrs.get("borderColor"))); 460 | } 461 | StateListDrawable selector = new StateListDrawable(); 462 | selector.addState(new int[]{android.R.attr.state_pressed}, pressedGd); 463 | selector.addState(new int[]{}, gd); 464 | view.setBackground(selector); 465 | getDynamicLayoutInfo(view).bgDrawable = gd; 466 | } else if (hasCornerRadius || attrs.containsKey("borderColor")) { 467 | GradientDrawable gd = new GradientDrawable(); 468 | int bgColor = parseColor(view, bgValue == null ? "#00000000" : bgValue); 469 | gd.setColor(bgColor); 470 | if (hasCornerRadii) { 471 | float radii[] = new float[8]; 472 | for (int i = 0; i < CORNERS.length; i++) { 473 | String corner = CORNERS[i]; 474 | if (attrs.containsKey("cornerRadius" + corner)) { 475 | radii[i * 2] = radii[i * 2 + 1] = DimensionConverter.stringToDimension(attrs.get("cornerRadius" + corner), view.getResources().getDisplayMetrics()); 476 | } 477 | gd.setCornerRadii(radii); 478 | } 479 | } else if (hasCornerRadius) { 480 | float cornerRadius = DimensionConverter.stringToDimension(attrs.get("cornerRadius"), view.getResources().getDisplayMetrics()); 481 | gd.setCornerRadius(cornerRadius); 482 | } 483 | if (attrs.containsKey("borderColor")) { 484 | String borderWidth = "1dp"; 485 | if (attrs.containsKey("borderWidth")) { 486 | borderWidth = attrs.get("borderWidth"); 487 | } 488 | gd.setStroke(DimensionConverter.stringToDimensionPixelSize(borderWidth, view.getResources().getDisplayMetrics()), 489 | parseColor(view, attrs.get("borderColor"))); 490 | } 491 | view.setBackground(gd); 492 | getDynamicLayoutInfo(view).bgDrawable = gd; 493 | } else { 494 | view.setBackgroundColor(parseColor(view, bgValue)); 495 | } 496 | } 497 | } 498 | 499 | if (layoutParams instanceof ViewGroup.MarginLayoutParams) { 500 | ((ViewGroup.MarginLayoutParams) layoutParams).setMargins(marginLeft, marginTop, marginRight, marginBottom); 501 | } 502 | view.setPadding(paddingLeft, paddingTop, paddingRight, paddingBottom); 503 | view.setLayoutParams(layoutParams); 504 | } 505 | 506 | private static DynamicLayoutInfo getDynamicLayoutInfo(View parent) { 507 | DynamicLayoutInfo info; 508 | if (parent.getTag() != null && parent.getTag() instanceof DynamicLayoutInfo) { 509 | info = (DynamicLayoutInfo)parent.getTag(); 510 | } else { 511 | info = new DynamicLayoutInfo(); 512 | parent.setTag(info); 513 | } 514 | return info; 515 | } 516 | 517 | private static View.OnClickListener getClickListener(final ViewGroup myParent, final String methodName) { 518 | return new View.OnClickListener() { 519 | @Override 520 | public void onClick(View view) { 521 | ViewGroup root = myParent; 522 | DynamicLayoutInfo info = null; 523 | while (root != null && (root.getParent() instanceof ViewGroup)) { 524 | if (root.getTag() != null && root.getTag() instanceof DynamicLayoutInfo) { 525 | info = (DynamicLayoutInfo)root.getTag(); 526 | if (info.delegate != null) break; 527 | } 528 | root = (ViewGroup)root.getParent(); 529 | } 530 | if (info != null && info.delegate != null) { 531 | final Object delegate = info.delegate; 532 | invokeMethod(delegate, methodName, false, view); 533 | } else { 534 | Log.e("DynamicLayoutInflator", "Unable to find valid delegate for click named " + methodName); 535 | } 536 | } 537 | 538 | private void invokeMethod(Object delegate, final String methodName, boolean withView, View view) { 539 | Object[] args = null; 540 | String finalMethod = methodName; 541 | if (methodName.endsWith(")")) { 542 | String[] parts = methodName.split("[(]", 2); 543 | finalMethod = parts[0]; 544 | try { 545 | String argText = parts[1].replace(""", "\""); 546 | JSONArray arr = new JSONArray("[" + argText.substring(0, argText.length() - 1) + "]"); 547 | args = new Object[arr.length()]; 548 | for (int i = 0; i < arr.length(); i++) { 549 | args[i] = arr.get(i); 550 | } 551 | } catch (JSONException e) { 552 | e.printStackTrace(); 553 | } 554 | } else if (withView) { 555 | args = new Object[1]; 556 | args[0] = view; 557 | } 558 | Class klass = delegate.getClass(); 559 | try { 560 | 561 | Class[] argClasses = null; 562 | if (args != null && args.length > 0) { 563 | argClasses = new Class[args.length]; 564 | if (withView) { 565 | argClasses[0] = View.class; 566 | } else { 567 | for (int i = 0; i < args.length; i++) { 568 | Class argClass = args[i].getClass(); 569 | if (argClass == Integer.class) 570 | argClass = int.class; // Nobody uses Integer... 571 | argClasses[i] = argClass; 572 | } 573 | } 574 | } 575 | Method method = klass.getMethod(finalMethod, argClasses); 576 | method.invoke(delegate, args); 577 | } catch (IllegalAccessException e) { 578 | e.printStackTrace(); 579 | } catch (InvocationTargetException e) { 580 | e.printStackTrace(); 581 | } catch (NoSuchMethodException e) { 582 | e.printStackTrace(); 583 | if (!withView && !methodName.endsWith(")")) { 584 | invokeMethod(delegate, methodName, true, view); 585 | } 586 | } 587 | } 588 | }; 589 | } 590 | 591 | private static String parseId(String value) { 592 | if (value.startsWith("@+id/")) { 593 | return value.substring(5); 594 | } else if (value.startsWith("@id/")) { 595 | return value.substring(4); 596 | } 597 | return value; 598 | } 599 | 600 | private static int parseGravity(String value) { 601 | int gravity = Gravity.NO_GRAVITY; 602 | String[] parts = value.toLowerCase().split("[|]"); 603 | for (String part : parts) { 604 | switch (part) { 605 | case "center": 606 | gravity = gravity | Gravity.CENTER; 607 | break; 608 | case "left": 609 | case "textStart": 610 | gravity = gravity | Gravity.LEFT; 611 | break; 612 | case "right": 613 | case "textEnd": 614 | gravity = gravity | Gravity.RIGHT; 615 | break; 616 | case "top": 617 | gravity = gravity | Gravity.TOP; 618 | break; 619 | case "bottom": 620 | gravity = gravity | Gravity.BOTTOM; 621 | break; 622 | case "center_horizontal": 623 | gravity = gravity | Gravity.CENTER_HORIZONTAL; 624 | break; 625 | case "center_vertical": 626 | gravity = gravity | Gravity.CENTER_VERTICAL; 627 | break; 628 | } 629 | } 630 | return gravity; 631 | } 632 | 633 | public static int idNumFromIdString(View view, String id) { 634 | if (!(view instanceof ViewGroup)) return 0; 635 | Object tag = view.getTag(); 636 | if (!(tag instanceof DynamicLayoutInfo)) return 0; // not inflated by this class 637 | DynamicLayoutInfo info = (DynamicLayoutInfo)view.getTag(); 638 | if (!info.nameToIdNumber.containsKey(id)) { 639 | ViewGroup grp = (ViewGroup)view; 640 | for (int i = 0; i < grp.getChildCount(); i++) { 641 | int val = idNumFromIdString(grp.getChildAt(i), id); 642 | if (val != 0) return val; 643 | } 644 | return 0; 645 | } 646 | return info.nameToIdNumber.get(id); 647 | } 648 | 649 | @Nullable 650 | public static View findViewByIdString(View view, String id) { 651 | int idNum = idNumFromIdString(view, id); 652 | if (idNum == 0) return null; 653 | return view.findViewById(idNum); 654 | } 655 | 656 | public static int parseColor(View view, String text) { 657 | if (text.startsWith("@color/")) { 658 | Resources resources = view.getResources(); 659 | return resources.getColor(resources.getIdentifier(text.substring("@color/".length()), "color", view.getContext().getPackageName())); 660 | } 661 | if (text.length() == 4 && text.startsWith("#")) { 662 | text = "#" + text.charAt(1) + text.charAt(1) + text.charAt(2) + text.charAt(2) + text.charAt(3) + text.charAt(3); 663 | } 664 | return Color.parseColor(text); 665 | } 666 | 667 | public static int adjustBrightness(int color, float amount) { 668 | int red = color & 0xFF0000 >> 16; 669 | int green = color & 0x00FF00 >> 8; 670 | int blue = color & 0x0000FF; 671 | int result = (int)(blue * amount); 672 | result += (int)(green * amount) << 8; 673 | result += (int)(red * amount) << 16; 674 | return result; 675 | } 676 | 677 | public static Drawable getDrawableByName(View view, String name) { 678 | Resources resources = view.getResources(); 679 | return resources.getDrawable(resources.getIdentifier(name, "drawable", 680 | view.getContext().getPackageName())); 681 | } 682 | 683 | public static void createViewRunnables() { 684 | viewRunnables = new HashMap<>(30); 685 | viewRunnables.put("scaleType", new ViewParamRunnable() { 686 | @Override 687 | public void apply(View view, String value, ViewGroup parent, Map attrs) { 688 | if (view instanceof ImageView) { 689 | ImageView.ScaleType scaleType = ((ImageView)view).getScaleType(); 690 | switch (value.toLowerCase()) { 691 | case "center": 692 | scaleType = ImageView.ScaleType.CENTER; 693 | break; 694 | case "center_crop": 695 | scaleType = ImageView.ScaleType.CENTER_CROP; 696 | break; 697 | case "center_inside": 698 | scaleType = ImageView.ScaleType.CENTER_INSIDE; 699 | break; 700 | case "fit_center": 701 | scaleType = ImageView.ScaleType.FIT_CENTER; 702 | break; 703 | case "fit_end": 704 | scaleType = ImageView.ScaleType.FIT_END; 705 | break; 706 | case "fit_start": 707 | scaleType = ImageView.ScaleType.FIT_START; 708 | break; 709 | case "fit_xy": 710 | scaleType = ImageView.ScaleType.FIT_XY; 711 | break; 712 | case "matrix": 713 | scaleType = ImageView.ScaleType.MATRIX; 714 | break; 715 | } 716 | ((ImageView) view).setScaleType(scaleType); 717 | } 718 | } 719 | }); 720 | viewRunnables.put("orientation", new ViewParamRunnable() { 721 | @Override 722 | public void apply(View view, String value, ViewGroup parent, Map attrs) { 723 | if (view instanceof LinearLayout) { 724 | ((LinearLayout)view).setOrientation(value.equals("vertical") ? LinearLayout.VERTICAL : LinearLayout.HORIZONTAL); 725 | } 726 | } 727 | }); 728 | viewRunnables.put("text", new ViewParamRunnable() { 729 | @Override 730 | public void apply(View view, String value, ViewGroup parent, Map attrs) { 731 | if (view instanceof TextView) { 732 | ((TextView)view).setText(value); 733 | } 734 | } 735 | }); 736 | viewRunnables.put("textSize", new ViewParamRunnable() { 737 | @Override 738 | public void apply(View view, String value, ViewGroup parent, Map attrs) { 739 | if (view instanceof TextView) { 740 | ((TextView)view).setTextSize(TypedValue.COMPLEX_UNIT_PX, DimensionConverter.stringToDimension(value, view.getResources().getDisplayMetrics())); 741 | } 742 | } 743 | }); 744 | viewRunnables.put("textColor", new ViewParamRunnable() { 745 | @Override 746 | public void apply(View view, String value, ViewGroup parent, Map attrs) { 747 | if (view instanceof TextView) { 748 | ((TextView)view).setTextColor(parseColor(view, value)); 749 | } 750 | } 751 | }); 752 | viewRunnables.put("textStyle", new ViewParamRunnable() { 753 | @Override 754 | public void apply(View view, String value, ViewGroup parent, Map attrs) { 755 | if (view instanceof TextView) { 756 | int typeFace = Typeface.NORMAL; 757 | if (value.contains("bold")) typeFace |= Typeface.BOLD; 758 | else if (value.contains("italic")) typeFace |= Typeface.ITALIC; 759 | ((TextView) view).setTypeface(null, typeFace); 760 | } 761 | } 762 | }); 763 | viewRunnables.put("textAlignment", new ViewParamRunnable() { 764 | @Override 765 | public void apply(View view, String value, ViewGroup parent, Map attrs) { 766 | if (Build.VERSION.SDK_INT > Build.VERSION_CODES.KITKAT) { 767 | int alignment = View.TEXT_ALIGNMENT_TEXT_START; 768 | switch (value) { 769 | case "center": 770 | alignment = View.TEXT_ALIGNMENT_CENTER; 771 | break; 772 | case "left": 773 | case "textStart": 774 | break; 775 | case "right": 776 | case "textEnd": 777 | alignment = View.TEXT_ALIGNMENT_TEXT_END; 778 | break; 779 | } 780 | view.setTextAlignment(alignment); 781 | } else { 782 | int gravity = Gravity.LEFT; 783 | switch (value) { 784 | case "center": 785 | gravity = Gravity.CENTER; 786 | break; 787 | case "left": 788 | case "textStart": 789 | break; 790 | case "right": 791 | case "textEnd": 792 | gravity = Gravity.RIGHT; 793 | break; 794 | } 795 | ((TextView)view).setGravity(gravity); 796 | } 797 | } 798 | }); 799 | viewRunnables.put("ellipsize", new ViewParamRunnable() { 800 | @Override 801 | public void apply(View view, String value, ViewGroup parent, Map attrs) { 802 | if (view instanceof TextView) { 803 | TextUtils.TruncateAt where = TextUtils.TruncateAt.END; 804 | switch (value) { 805 | case "start": 806 | where = TextUtils.TruncateAt.START; 807 | break; 808 | case "middle": 809 | where = TextUtils.TruncateAt.MIDDLE; 810 | break; 811 | case "marquee": 812 | where = TextUtils.TruncateAt.MARQUEE; 813 | break; 814 | case "end": 815 | break; 816 | } 817 | ((TextView) view).setEllipsize(where); 818 | } 819 | } 820 | }); 821 | viewRunnables.put("singleLine", new ViewParamRunnable() { 822 | @Override 823 | public void apply(View view, String value, ViewGroup parent, Map attrs) { 824 | if (view instanceof TextView) { 825 | ((TextView)view).setSingleLine(); 826 | } 827 | } 828 | }); 829 | viewRunnables.put("hint", new ViewParamRunnable() { 830 | @Override 831 | public void apply(View view, String value, ViewGroup parent, Map attrs) { 832 | if (view instanceof EditText) { 833 | ((EditText)view).setHint(value); 834 | } 835 | } 836 | }); 837 | viewRunnables.put("inputType", new ViewParamRunnable() { 838 | @Override 839 | public void apply(View view, String value, ViewGroup parent, Map attrs) { 840 | if (view instanceof TextView) { 841 | int inputType = 0; 842 | switch (value) { 843 | case "textEmailAddress": 844 | inputType |= InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_EMAIL_ADDRESS; 845 | break; 846 | case "number": 847 | inputType |= InputType.TYPE_CLASS_NUMBER; 848 | break; 849 | case "phone": 850 | inputType |= InputType.TYPE_CLASS_PHONE; 851 | break; 852 | } 853 | if (inputType > 0) ((TextView)view).setInputType(inputType); 854 | } 855 | } 856 | }); 857 | viewRunnables.put("gravity", new ViewParamRunnable() { 858 | @Override 859 | public void apply(View view, String value, ViewGroup parent, Map attrs) { 860 | int gravity = parseGravity(value); 861 | if (view instanceof TextView) { 862 | ((TextView) view).setGravity(gravity); 863 | } else if (view instanceof LinearLayout) { 864 | ((LinearLayout)view).setGravity(gravity); 865 | } else if (view instanceof RelativeLayout) { 866 | ((RelativeLayout)view).setGravity(gravity); 867 | } 868 | } 869 | }); 870 | viewRunnables.put("src", new ViewParamRunnable() { 871 | @Override 872 | public void apply(View view, String value, ViewGroup parent, Map attrs) { 873 | if (view instanceof ImageView) { 874 | String imageName = value; 875 | if (imageName.startsWith("//")) imageName = "http:" + imageName; 876 | if (imageName.startsWith("http")) { 877 | if (imageLoader != null) { 878 | if (attrs.containsKey("cornerRadius")) { 879 | int radius = DimensionConverter.stringToDimensionPixelSize(attrs.get("cornerRadius"), view.getResources().getDisplayMetrics()); 880 | imageLoader.loadRoundedImage((ImageView)view, imageName, radius); 881 | } else { 882 | imageLoader.loadImage((ImageView) view, imageName); 883 | } 884 | } 885 | } else if (imageName.startsWith("@drawable/")) { 886 | imageName = imageName.substring("@drawable/".length()); 887 | ((ImageView)view).setImageDrawable(getDrawableByName(view, imageName)); 888 | } 889 | } 890 | } 891 | }); 892 | viewRunnables.put("visibility", new ViewParamRunnable() { 893 | @Override 894 | public void apply(View view, String value, ViewGroup parent, Map attrs) { 895 | int visibility = View.VISIBLE; 896 | String visValue = value.toLowerCase(); 897 | if (visValue.equals("gone")) visibility = View.GONE; 898 | else if (visValue.equals("invisible")) visibility = View.INVISIBLE; 899 | view.setVisibility(visibility); 900 | } 901 | }); 902 | viewRunnables.put("clickable", new ViewParamRunnable() { 903 | @Override 904 | public void apply(View view, String value, ViewGroup parent, Map attrs) { 905 | view.setClickable(value.equals("true")); 906 | } 907 | }); 908 | viewRunnables.put("tag", new ViewParamRunnable() { 909 | @Override 910 | public void apply(View view, String value, ViewGroup parent, Map attrs) { 911 | // Sigh, this is dangerous because we use tags for other purposes 912 | if (view.getTag() == null) view.setTag(value); 913 | } 914 | }); 915 | viewRunnables.put("onClick", new ViewParamRunnable() { 916 | @Override 917 | public void apply(View view, String value, ViewGroup parent, Map attrs) { 918 | view.setOnClickListener(getClickListener(parent, value)); 919 | } 920 | }); 921 | } 922 | 923 | } 924 | -------------------------------------------------------------------------------- /app/src/main/java/com/nickandjerry/dynamiclayoutinflator/MainActivity.java: -------------------------------------------------------------------------------- 1 | package com.nickandjerry.dynamiclayoutinflator; 2 | 3 | import android.support.v7.app.ActionBarActivity; 4 | import android.os.Bundle; 5 | import android.util.Log; 6 | import android.view.Menu; 7 | import android.view.MenuItem; 8 | import android.view.View; 9 | import android.widget.RelativeLayout; 10 | 11 | import java.io.IOException; 12 | import java.io.InputStreamReader; 13 | 14 | 15 | public class MainActivity extends ActionBarActivity { 16 | 17 | @Override 18 | protected void onCreate(Bundle savedInstanceState) { 19 | super.onCreate(savedInstanceState); 20 | setContentView(R.layout.activity_main); 21 | RelativeLayout main = (RelativeLayout)findViewById(R.id.main_top); 22 | try { 23 | View view = DynamicLayoutInflator.inflate(this, getAssets().open("testlayout.xml"), main); 24 | DynamicLayoutInflator.setDelegate(view, this); 25 | } catch (IOException e) { 26 | e.printStackTrace(); 27 | } 28 | } 29 | 30 | public void ohHello() { 31 | Log.d("nick", "howdy"); 32 | } 33 | 34 | public void logStringNum(String text, int num) { 35 | Log.d("nick", "logging: " + text + " - " + num); 36 | } 37 | 38 | @Override 39 | public boolean onCreateOptionsMenu(Menu menu) { 40 | // Inflate the menu; this adds items to the action bar if it is present. 41 | getMenuInflater().inflate(R.menu.menu_main, menu); 42 | return true; 43 | } 44 | 45 | @Override 46 | public boolean onOptionsItemSelected(MenuItem item) { 47 | // Handle action bar item clicks here. The action bar will 48 | // automatically handle clicks on the Home/Up button, so long 49 | // as you specify a parent activity in AndroidManifest.xml. 50 | int id = item.getItemId(); 51 | 52 | //noinspection SimplifiableIfStatement 53 | if (id == R.id.action_settings) { 54 | return true; 55 | } 56 | 57 | return super.onOptionsItemSelected(item); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_main.xml: -------------------------------------------------------------------------------- 1 | 7 | 8 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /app/src/main/res/menu/menu_main.xml: -------------------------------------------------------------------------------- 1 | 4 | 6 | 7 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nickwah/DynamicLayoutInflator/409dc6f56c941a74ad66afde8b8463af635bc930/app/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nickwah/DynamicLayoutInflator/409dc6f56c941a74ad66afde8b8463af635bc930/app/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nickwah/DynamicLayoutInflator/409dc6f56c941a74ad66afde8b8463af635bc930/app/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nickwah/DynamicLayoutInflator/409dc6f56c941a74ad66afde8b8463af635bc930/app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/values-w820dp/dimens.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 64dp 6 | 7 | -------------------------------------------------------------------------------- /app/src/main/res/values/dimens.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 16dp 4 | 16dp 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | DynamicLayoutInflator 3 | 4 | Hello world! 5 | Settings 6 | 7 | -------------------------------------------------------------------------------- /app/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | // Top-level build file where you can add configuration options common to all sub-projects/modules. 2 | 3 | buildscript { 4 | repositories { 5 | jcenter() 6 | } 7 | dependencies { 8 | classpath 'com.android.tools.build:gradle:2.1.0' 9 | 10 | // NOTE: Do not place your application dependencies here; they belong 11 | // in the individual module build.gradle files 12 | } 13 | } 14 | 15 | allprojects { 16 | repositories { 17 | jcenter() 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | # Project-wide Gradle settings. 2 | 3 | # IDE (e.g. Android Studio) users: 4 | # Gradle settings configured through the IDE *will override* 5 | # any settings specified in this file. 6 | 7 | # For more details on how to configure your build environment visit 8 | # http://www.gradle.org/docs/current/userguide/build_environment.html 9 | 10 | # Specifies the JVM arguments used for the daemon process. 11 | # The setting is particularly useful for tweaking memory settings. 12 | # Default value: -Xmx10248m -XX:MaxPermSize=256m 13 | # org.gradle.jvmargs=-Xmx2048m -XX:MaxPermSize=512m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8 14 | 15 | # When configured, Gradle will run in incubating parallel mode. 16 | # This option should only be used with decoupled projects. More details, visit 17 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects 18 | # org.gradle.parallel=true -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nickwah/DynamicLayoutInflator/409dc6f56c941a74ad66afde8b8463af635bc930/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Thu May 12 12:45:55 PDT 2016 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | distributionUrl=https\://services.gradle.org/distributions/gradle-2.10-all.zip 7 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | ############################################################################## 4 | ## 5 | ## Gradle start up script for UN*X 6 | ## 7 | ############################################################################## 8 | 9 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 10 | DEFAULT_JVM_OPTS="" 11 | 12 | APP_NAME="Gradle" 13 | APP_BASE_NAME=`basename "$0"` 14 | 15 | # Use the maximum available, or set MAX_FD != -1 to use that value. 16 | MAX_FD="maximum" 17 | 18 | warn ( ) { 19 | echo "$*" 20 | } 21 | 22 | die ( ) { 23 | echo 24 | echo "$*" 25 | echo 26 | exit 1 27 | } 28 | 29 | # OS specific support (must be 'true' or 'false'). 30 | cygwin=false 31 | msys=false 32 | darwin=false 33 | case "`uname`" in 34 | CYGWIN* ) 35 | cygwin=true 36 | ;; 37 | Darwin* ) 38 | darwin=true 39 | ;; 40 | MINGW* ) 41 | msys=true 42 | ;; 43 | esac 44 | 45 | # For Cygwin, ensure paths are in UNIX format before anything is touched. 46 | if $cygwin ; then 47 | [ -n "$JAVA_HOME" ] && JAVA_HOME=`cygpath --unix "$JAVA_HOME"` 48 | fi 49 | 50 | # Attempt to set APP_HOME 51 | # Resolve links: $0 may be a link 52 | PRG="$0" 53 | # Need this for relative symlinks. 54 | while [ -h "$PRG" ] ; do 55 | ls=`ls -ld "$PRG"` 56 | link=`expr "$ls" : '.*-> \(.*\)$'` 57 | if expr "$link" : '/.*' > /dev/null; then 58 | PRG="$link" 59 | else 60 | PRG=`dirname "$PRG"`"/$link" 61 | fi 62 | done 63 | SAVED="`pwd`" 64 | cd "`dirname \"$PRG\"`/" >&- 65 | APP_HOME="`pwd -P`" 66 | cd "$SAVED" >&- 67 | 68 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 69 | 70 | # Determine the Java command to use to start the JVM. 71 | if [ -n "$JAVA_HOME" ] ; then 72 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 73 | # IBM's JDK on AIX uses strange locations for the executables 74 | JAVACMD="$JAVA_HOME/jre/sh/java" 75 | else 76 | JAVACMD="$JAVA_HOME/bin/java" 77 | fi 78 | if [ ! -x "$JAVACMD" ] ; then 79 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 80 | 81 | Please set the JAVA_HOME variable in your environment to match the 82 | location of your Java installation." 83 | fi 84 | else 85 | JAVACMD="java" 86 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 87 | 88 | Please set the JAVA_HOME variable in your environment to match the 89 | location of your Java installation." 90 | fi 91 | 92 | # Increase the maximum file descriptors if we can. 93 | if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then 94 | MAX_FD_LIMIT=`ulimit -H -n` 95 | if [ $? -eq 0 ] ; then 96 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 97 | MAX_FD="$MAX_FD_LIMIT" 98 | fi 99 | ulimit -n $MAX_FD 100 | if [ $? -ne 0 ] ; then 101 | warn "Could not set maximum file descriptor limit: $MAX_FD" 102 | fi 103 | else 104 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 105 | fi 106 | fi 107 | 108 | # For Darwin, add options to specify how the application appears in the dock 109 | if $darwin; then 110 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 111 | fi 112 | 113 | # For Cygwin, switch paths to Windows format before running java 114 | if $cygwin ; then 115 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 116 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 117 | 118 | # We build the pattern for arguments to be converted via cygpath 119 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 120 | SEP="" 121 | for dir in $ROOTDIRSRAW ; do 122 | ROOTDIRS="$ROOTDIRS$SEP$dir" 123 | SEP="|" 124 | done 125 | OURCYGPATTERN="(^($ROOTDIRS))" 126 | # Add a user-defined pattern to the cygpath arguments 127 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 128 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 129 | fi 130 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 131 | i=0 132 | for arg in "$@" ; do 133 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 134 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 135 | 136 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 137 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 138 | else 139 | eval `echo args$i`="\"$arg\"" 140 | fi 141 | i=$((i+1)) 142 | done 143 | case $i in 144 | (0) set -- ;; 145 | (1) set -- "$args0" ;; 146 | (2) set -- "$args0" "$args1" ;; 147 | (3) set -- "$args0" "$args1" "$args2" ;; 148 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;; 149 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 150 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 151 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 152 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 153 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 154 | esac 155 | fi 156 | 157 | # Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules 158 | function splitJvmOpts() { 159 | JVM_OPTS=("$@") 160 | } 161 | eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS 162 | JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME" 163 | 164 | exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@" 165 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @if "%DEBUG%" == "" @echo off 2 | @rem ########################################################################## 3 | @rem 4 | @rem Gradle startup script for Windows 5 | @rem 6 | @rem ########################################################################## 7 | 8 | @rem Set local scope for the variables with windows NT shell 9 | if "%OS%"=="Windows_NT" setlocal 10 | 11 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 12 | set DEFAULT_JVM_OPTS= 13 | 14 | set DIRNAME=%~dp0 15 | if "%DIRNAME%" == "" set DIRNAME=. 16 | set APP_BASE_NAME=%~n0 17 | set APP_HOME=%DIRNAME% 18 | 19 | @rem Find java.exe 20 | if defined JAVA_HOME goto findJavaFromJavaHome 21 | 22 | set JAVA_EXE=java.exe 23 | %JAVA_EXE% -version >NUL 2>&1 24 | if "%ERRORLEVEL%" == "0" goto init 25 | 26 | echo. 27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 28 | echo. 29 | echo Please set the JAVA_HOME variable in your environment to match the 30 | echo location of your Java installation. 31 | 32 | goto fail 33 | 34 | :findJavaFromJavaHome 35 | set JAVA_HOME=%JAVA_HOME:"=% 36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 37 | 38 | if exist "%JAVA_EXE%" goto init 39 | 40 | echo. 41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 42 | echo. 43 | echo Please set the JAVA_HOME variable in your environment to match the 44 | echo location of your Java installation. 45 | 46 | goto fail 47 | 48 | :init 49 | @rem Get command-line arguments, handling Windowz variants 50 | 51 | if not "%OS%" == "Windows_NT" goto win9xME_args 52 | if "%@eval[2+2]" == "4" goto 4NT_args 53 | 54 | :win9xME_args 55 | @rem Slurp the command line arguments. 56 | set CMD_LINE_ARGS= 57 | set _SKIP=2 58 | 59 | :win9xME_args_slurp 60 | if "x%~1" == "x" goto execute 61 | 62 | set CMD_LINE_ARGS=%* 63 | goto execute 64 | 65 | :4NT_args 66 | @rem Get arguments from the 4NT Shell from JP Software 67 | set CMD_LINE_ARGS=%$ 68 | 69 | :execute 70 | @rem Setup the command line 71 | 72 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 73 | 74 | @rem Execute Gradle 75 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 76 | 77 | :end 78 | @rem End local scope for the variables with windows NT shell 79 | if "%ERRORLEVEL%"=="0" goto mainEnd 80 | 81 | :fail 82 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 83 | rem the _cmd.exe /c_ return code! 84 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 85 | exit /b 1 86 | 87 | :mainEnd 88 | if "%OS%"=="Windows_NT" endlocal 89 | 90 | :omega 91 | -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | include ':app' 2 | --------------------------------------------------------------------------------