Implement ShapeDrawableParser

This commit is contained in:
Akash Yadav
2022-01-01 17:44:40 +05:30
parent 5cf99ed2e0
commit c8fd786cfd
25 changed files with 1110 additions and 314 deletions
+1
View File
@@ -14,6 +14,7 @@
<entry key="..\:/Projects/AndroidStudioProjects/AndroidIDE/app/src/main/res/layout/layout_about_items.xml" value="0.20512820512820512" />
<entry key="..\:/Projects/AndroidStudioProjects/AndroidIDE/app/src/main/res/menu/menu_editor_nav.xml" value="0.20512820512820512" />
<entry key="..\:/Projects/AndroidStudioProjects/AndroidIDE/common/src/main/res/layout/layout_color_picker.xml" value="0.264" />
<entry key="..\:/Projects/AndroidStudioProjects/AndroidIDE/xml-inflater/src/main/res/drawable/test.xml" value="0.1675925925925926" />
<entry key="..\:/Projects/AndroidStudioProjects/android-ide/app/src/main/res/drawable-mdpi/bg_widgets_list.xml" value="0.3105485232067511" />
<entry key="..\:/Projects/AndroidStudioProjects/android-ide/app/src/main/res/drawable-mdpi/ic_widget_progress.xml" value="0.3071729957805907" />
<entry key="..\:/Projects/AndroidStudioProjects/android-ide/app/src/main/res/drawable/bg_design_container_background.xml" value="0.3105485232067511" />
@@ -1,29 +1,32 @@
/************************************************************************************
* This file is part of AndroidIDE.
*
*
* AndroidIDE is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
*
* AndroidIDE is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
*
* You should have received a copy of the GNU General Public License
* along with AndroidIDE. If not, see <https://www.gnu.org/licenses/>.
*
**************************************************************************************/
**************************************************************************************/
package com.itsaky.inflater;
import android.content.Context;
import android.util.DisplayMetrics;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.annotation.StringRes;
import androidx.core.content.ContextCompat;
import com.itsaky.androidide.app.BaseApplication;
import com.itsaky.androidide.ui.util.Preconditions;
import com.itsaky.androidide.utils.Logger;
@@ -36,11 +39,13 @@ import com.itsaky.inflater.impl.UiView;
import com.itsaky.inflater.impl.UiViewGroup;
import com.itsaky.widgets.WidgetInfo;
import com.itsaky.widgets.models.Widget;
import java.io.File;
import java.lang.reflect.Constructor;
import java.lang.reflect.Method;
import java.util.Set;
import java.util.regex.Pattern;
import org.jsoup.Jsoup;
import org.jsoup.nodes.Attribute;
import org.jsoup.nodes.Attributes;
@@ -59,60 +64,60 @@ class XMLLayoutInflater extends BaseLayoutInflater {
private ContextProvider contextProvider;
private static final Logger LOG = Logger.instance("XMLLayoutInflater");
private static final Logger LOG = Logger.instance ("XMLLayoutInflater");
public static final String ATTR_ADAPTER_SUFFIX = "AttrAdapter";
private XMLLayoutInflater () {
throw new UnsupportedOperationException();
throw new UnsupportedOperationException ();
}
XMLLayoutInflater(@NonNull LayoutInflaterConfiguration config) {
XMLLayoutInflater (@NonNull LayoutInflaterConfiguration config) {
this.resDirs = config.resDirs;
this.attrInfo = config.attrInfo;
this.widgetInfo = config.widgetInfo;
this.resFinder = config.resFinder;
this.contextProvider = config.contextProvider;
Preconditions.assertAllNotNull("LayoutInflater parameters cannot be null", resDirs, attrInfo, widgetInfo, resFinder);
Preconditions.assertAllNotNull ("LayoutInflater parameters cannot be null", resDirs, attrInfo, widgetInfo, resFinder);
}
@Override
@NonNull
protected IView doInflate(String layout, ViewGroup parent) throws InflateException {
protected IView doInflate (String layout, ViewGroup parent) throws InflateException {
try {
IDTable.newRound();
IDTable.newRound ();
// Notify
preInflate();
preInflate ();
final Document doc = Jsoup.parse(layout, "", Parser.xmlParser());
final Document doc = Jsoup.parse (layout, "", Parser.xmlParser ());
if (doc.childrenSize() <= 0) {
if (doc.childrenSize () <= 0) {
throw new InflateException ("No views added");
}
if (doc.childrenSize() > 1) {
if (doc.childrenSize () > 1) {
throw new InflateException ("More than one root element was found. An XML layout can have only one root element.");
}
IView root = onCreateView(doc.child(0), parent);
IView root = onCreateView (doc.child (0), parent);
if (root == null) {
root = onCreateErrorView(TextView.class.getName(), "No views added");
root = onCreateErrorView (TextView.class.getName (), "No views added");
}
// Notify
postInflate(root);
postInflate (root);
return root;
} catch (Throwable th) {
throw new InflateException("Unable to inflate layout", th);
throw new InflateException ("Unable to inflate layout", th);
}
}
@Override
public void resetContextProvider(ILayoutInflater.ContextProvider provider) {
public void resetContextProvider (ILayoutInflater.ContextProvider provider) {
this.contextProvider = provider;
}
@@ -127,203 +132,205 @@ class XMLLayoutInflater extends BaseLayoutInflater {
return null;
}
final String name = tag.tagName().trim();
if (name.equals("include")) {
return createFromInclude (tag.attributes(), parent);
final String name = tag.tagName ().trim ();
if (name.equals ("include")) {
return createFromInclude (tag.attributes (), parent);
}
IView root = null;
int style = tag.hasAttr("style") ? parseFrameworkStyle (tag.attr("style")) : 0;
if (!name.contains(".")) {
int style = tag.hasAttr ("style") ? parseFrameworkStyle (tag.attr ("style")) : 0;
if (!name.contains (".")) {
root = createFromSimpleName (name, parent, style);
} else {
root = createFromQualifiedName (name, parent, style);
}
if (root == null) {
return null;
}
registerAttributeAdaptersTo (root);
if (!tag.attributes().isEmpty()) {
addAttributesTo(root, tag.attributes());
if (!tag.attributes ().isEmpty ()) {
addAttributesTo (root, tag.attributes ());
}
if (root instanceof IViewGroup) {
addChildren (tag, (IViewGroup) root, parent);
}
postCreateView(root);
postCreateView (root);
return root;
}
protected void addChildren(@NonNull Element tag, IViewGroup root, ViewGroup parent) {
if (tag.childrenSize() > 0 && !root.isPlaceholder()) {
for (Element child : tag.children()) {
final BaseView created = (BaseView) onCreateView(child, (ViewGroup) root.asView());
if(created != null) {
root.addView(created);
created.setParent(root);
protected void addChildren (@NonNull Element tag, IViewGroup root, ViewGroup parent) {
if (tag.childrenSize () > 0 && !root.isPlaceholder ()) {
for (Element child : tag.children ()) {
final BaseView created = (BaseView) onCreateView (child, (ViewGroup) root.asView ());
if (created != null) {
root.addView (created);
created.setParent (root);
}
}
}
}
protected int parseFrameworkStyle (String value) throws InflateException{
protected int parseFrameworkStyle (String value) throws InflateException {
try {
if (value.startsWith("?android:attr/")) {
final String name = underscorize (value.substring("?android:attr/".length()));
return (int) android.R.attr.class.getField(name).get(null);
} else if (value.startsWith("@android:style/")) {
final String name = underscorize (value.substring("@android:style/".length()));
return (int) android.R.style.class.getField(name).get(null);
if (value.startsWith ("?android:attr/")) {
final String name = underscorize (value.substring ("?android:attr/".length ()));
return (int) android.R.attr.class.getField (name).get (null);
} else if (value.startsWith ("@android:style/")) {
final String name = underscorize (value.substring ("@android:style/".length ()));
return (int) android.R.style.class.getField (name).get (null);
}
} catch (Throwable th) {
throw new InflateException (th);
}
return 0;
}
@NonNull
private String underscorize(@NonNull String substring) {
return substring.replace(".", "_");
}
protected void registerAttributeAdaptersTo(@NonNull IView root) {
root.registerAttributeAdapter(onCreateAttributeAdapter (root.asView()));
private String underscorize (@NonNull String substring) {
return substring.replace (".", "_");
}
protected IAttributeAdapter onCreateAttributeAdapter(View view) {
protected void registerAttributeAdaptersTo (@NonNull IView root) {
root.registerAttributeAdapter (onCreateAttributeAdapter (root.asView ()));
}
protected IAttributeAdapter onCreateAttributeAdapter (View view) {
final var displayMetrics = contextProvider.getContext ().getResources ().getDisplayMetrics ();
try {
final String adapterName = view.getClass().getName().concat(ATTR_ADAPTER_SUFFIX);
final String name = "com.itsaky.inflater.adapters.".concat(adapterName);
final Class <? extends IAttributeAdapter> adapterClass = getClass()
.getClassLoader()
.loadClass(name)
.asSubclass(IAttributeAdapter.class);
final var adapter = adapterClass.newInstance();
adapter.setResourceFinder (resFinder);
return adapter;
final String adapterName = view.getClass ().getName ().concat (ATTR_ADAPTER_SUFFIX);
final String name = "com.itsaky.inflater.adapters.".concat (adapterName);
final Class<? extends IAttributeAdapter> adapterClass = getClass ()
.getClassLoader ()
.loadClass (name)
.asSubclass (IAttributeAdapter.class);
final var constructor = adapterClass.getConstructor (IResourceFinder.class, DisplayMetrics.class);
final var adapter = constructor.newInstance (resFinder, displayMetrics);
adapter.setResourceFinder (resFinder);
return adapter;
} catch (Throwable th) {
LOG.error (BaseApplication.getBaseInstance().getString(com.itsaky.inflater.R.string.err_no_attr_adapter, view.getClass().getName()), th);
LOG.error (BaseApplication.getBaseInstance ().getString (com.itsaky.inflater.R.string.err_no_attr_adapter, view.getClass ().getName ()), th);
}
// If we cannot find a suitable adapter, fall back to using common adapters
IAttributeAdapter adapter = null;
if (view instanceof ViewGroup) {
adapter = new ViewGroupAttrAdapter ();
adapter = new ViewGroupAttrAdapter (resFinder, displayMetrics);
} else {
adapter = new ViewAttrAdapter ();
adapter = new ViewAttrAdapter (resFinder, displayMetrics);
}
return adapter;
}
protected void addAttributesTo(IView view, Attributes attributes) {
assertNotnull(attributes, "Cannot apply null attributes!");
protected void addAttributesTo (IView view, Attributes attributes) {
assertNotnull (attributes, "Cannot apply null attributes!");
for (Attribute attr : attributes) {
final String key = attr.getKey();
final String[] split = !key.contains(":") ? new String[] {"", key} : key.split(Pattern.quote(":"), 2);
final String key = attr.getKey ();
final String[] split = !key.contains (":") ? new String[]{"", key} : key.split (Pattern.quote (":"), 2);
final String namespace = split[0];
final String name = split[1];
final String value = attr.getValue();
final String value = attr.getValue ();
final IAttribute iAttr = asAttribute (namespace, name, value);
view.addAttribute(iAttr);
postApplyAttribute(iAttr, view);
view.addAttribute (iAttr);
postApplyAttribute (iAttr, view);
}
}
protected IAttribute asAttribute(String namespace, String name, String value) {
protected IAttribute asAttribute (String namespace, String name, String value) {
return new UiAttribute (namespace, name, value);
}
protected IView create (@NonNull String name, ViewGroup parent, int style) throws InflateException {
if (!name.contains(".")) {
if (!name.contains (".")) {
return createFromSimpleName (name, parent, style);
} else {
return createFromQualifiedName (name, parent, style);
}
}
protected IView createFromInclude (Attributes attrs, ViewGroup parent) {
throw new UnsupportedOperationException ("Inflating from <include> is not supported yet!");
}
protected IView createFromSimpleName(String name, ViewGroup parent, int style) throws InflateException {
final Widget widget = widgetInfo.getWidgetBySimpleName(name);
protected IView createFromSimpleName (String name, ViewGroup parent, int style) throws InflateException {
final Widget widget = widgetInfo.getWidgetBySimpleName (name);
if (widget == null) {
LOG.error("Unable to inflate view. widget == null");
return onCreateErrorView(name, getString(com.itsaky.inflater.R.string.msg_cannot_create_view, name));
LOG.error ("Unable to inflate view. widget == null");
return onCreateErrorView (name, getString (com.itsaky.inflater.R.string.msg_cannot_create_view, name));
}
return createFromQualifiedName(widget.name, parent, style);
return createFromQualifiedName (widget.name, parent, style);
}
protected IView createFromQualifiedName(String name, ViewGroup parent, int style) throws InflateException {
assertNotBlank(name, "Invalid tag name: " + name);
protected IView createFromQualifiedName (String name, ViewGroup parent, int style) throws InflateException {
assertNotBlank (name, "Invalid tag name: " + name);
// TODO Try to load classes directly from .class files if possible
try {
final View created = createAndroidViewForName(name);
final View created = createAndroidViewForName (name);
final BaseView view =
created instanceof ViewGroup
? new UiViewGroup (name, (ViewGroup) created)
: new UiView (name, created);
return applyLayoutParams(view, parent);
created instanceof ViewGroup
? new UiViewGroup (name, (ViewGroup) created)
: new UiView (name, created);
return applyLayoutParams (view, parent);
} catch (Throwable th) {
final var msg = getString(R.string.msg_cannot_create_view, name);
LOG.error(msg, th);
return onCreateErrorView(name, msg);
final var msg = getString (R.string.msg_cannot_create_view, name);
LOG.error (msg, th);
return onCreateErrorView (name, msg);
}
}
@NonNull
protected View createAndroidViewForName(String name) throws ClassNotFoundException, NoSuchMethodException, IllegalAccessException, InstantiationException, java.lang.reflect.InvocationTargetException {
final Class<? extends View> loaded = Class.forName(name).asSubclass(View.class);
final Constructor<? extends View> constructor = loaded.getConstructor(Context.class /*, AttributeSet.class, int.class*/);
final View created = constructor.newInstance(contextProvider.getContext()/*, null, style*/);
protected View createAndroidViewForName (String name) throws ClassNotFoundException, NoSuchMethodException, IllegalAccessException, InstantiationException, java.lang.reflect.InvocationTargetException {
final Class<? extends View> loaded = Class.forName (name).asSubclass (View.class);
final Constructor<? extends View> constructor = loaded.getConstructor (Context.class /*, AttributeSet.class, int.class*/);
final View created = constructor.newInstance (contextProvider.getContext ()/*, null, style*/);
return created;
}
protected IView applyLayoutParams(@NonNull IView view, ViewGroup parent) {
final ViewGroup.LayoutParams params = generateLayoutParams(parent);
view.asView().setLayoutParams(params);
protected IView applyLayoutParams (@NonNull IView view, ViewGroup parent) {
final ViewGroup.LayoutParams params = generateLayoutParams (parent);
view.asView ().setLayoutParams (params);
return view;
}
protected IView onCreateErrorView (String name, String msg) throws InflateException {
final Context ctx = contextProvider.getContext();
assertNotnull(ctx, "Context is null!");
final Context ctx = contextProvider.getContext ();
assertNotnull (ctx, "Context is null!");
final TextView error = new TextView (ctx);
error.setText(msg);
error.setLayoutParams(new ViewGroup.LayoutParams (-2, -2));
error.setTextColor(ContextCompat.getColor(ctx, android.R.color.black));
error.setBackgroundColor(ContextCompat.getColor(ctx, android.R.color.white));
error.setText (msg);
error.setLayoutParams (new ViewGroup.LayoutParams (-2, -2));
error.setTextColor (ContextCompat.getColor (ctx, android.R.color.black));
error.setBackgroundColor (ContextCompat.getColor (ctx, android.R.color.white));
return new UiView (name, error, true);
}
private String getString (@StringRes int id, Object... format) throws InflateException {
assertNotnull(contextProvider.getContext(), "Context is null!");
return contextProvider.getContext().getString(id, format);
assertNotnull (contextProvider.getContext (), "Context is null!");
return contextProvider.getContext ().getString (id, format);
}
@NonNull
protected ViewGroup.LayoutParams generateLayoutParams (ViewGroup parent) {
try {
final Class<?> clazz = parent.getClass();
final Class<?> clazz = parent.getClass ();
Method method = null;
try {
method = clazz.getDeclaredMethod("generateDefaultLayoutParams");
method = clazz.getDeclaredMethod ("generateDefaultLayoutParams");
} catch (NoSuchMethodException | SecurityException e) {
// If the method is not implicitly declared in this ViewGroup,
// Then try to get the method from its superclass
@@ -333,8 +340,8 @@ class XMLLayoutInflater extends BaseLayoutInflater {
// No null check here
// If we reach here, then method is definitely not null
method.setAccessible(true);
return (ViewGroup.LayoutParams) method.invoke(parent);
method.setAccessible (true);
return (ViewGroup.LayoutParams) method.invoke (parent);
} catch (Throwable th) {
throw new InflateException ("Unable to create layout params for parent: " + parent, th);
}
@@ -19,13 +19,7 @@ package com.itsaky.inflater.adapters.android.view;
import android.content.Context;
import android.content.res.Resources;
import android.graphics.Color;
import android.graphics.NinePatch;
import android.graphics.PorterDuff;
import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.ColorDrawable;
import android.graphics.drawable.Drawable;
import android.graphics.drawable.NinePatchDrawable;
import android.os.Build;
import android.util.DisplayMetrics;
import android.util.TypedValue;
@@ -36,22 +30,15 @@ import android.widget.LinearLayout;
import android.widget.RelativeLayout;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.core.content.ContextCompat;
import androidx.core.view.GravityCompat;
import com.blankj.utilcode.util.ImageUtils;
import com.itsaky.androidide.utils.Logger;
import com.itsaky.inflater.IAttribute;
import com.itsaky.inflater.IAttributeAdapter;
import com.itsaky.inflater.IDTable;
import com.itsaky.inflater.IResourceFinder;
import com.sdsmdg.harjot.vectormaster.VectorMasterDrawable;
import com.itsaky.inflater.util.CommonParseUtils;
import org.xmlpull.v1.XmlPullParserException;
import java.io.File;
import java.lang.reflect.Field;
import java.util.regex.Pattern;
/**
@@ -61,18 +48,17 @@ import java.util.regex.Pattern;
*
* @author Akash Yadav
*/
public class ViewAttrAdapter implements IAttributeAdapter {
private static final Pattern HEX_COLOR = Pattern.compile("#[a-fA-F0-9]{6,8}");
public class ViewAttrAdapter extends CommonParseUtils implements IAttributeAdapter {
protected static final Logger LOG = Logger.instance ("BaseViewAttrAdapter");
public static final int ENUM_NOT_FOUND = -4116;
protected IResourceFinder resFinder;
public ViewAttrAdapter (@NonNull IResourceFinder resourceFinder, DisplayMetrics displayMetrics) {
super (resourceFinder, displayMetrics);
}
@Override
public void setResourceFinder (IResourceFinder resourceFinder) {
this.resFinder = resourceFinder;
this.resourceFinder = resourceFinder;
}
@Override
@@ -87,7 +73,6 @@ public class ViewAttrAdapter implements IAttributeAdapter {
final String value = attribute.getValue();
final ViewGroup.LayoutParams params = view.getLayoutParams();
final Resources res = view.getResources();
final DisplayMetrics dm = res.getDisplayMetrics();
final Context ctx = view.getContext();
if (!canHandleNamespace(namespace)) {
@@ -97,10 +82,10 @@ public class ViewAttrAdapter implements IAttributeAdapter {
boolean handled = true;
switch (name) {
case "layout_height" :
params.height = parseDimension (value, -2, dm);
params.height = parseDimension (value, -2, displayMetrics);
break;
case "layout_width" :
params.width = parseDimension (value, -2, dm);
params.width = parseDimension (value, -2, displayMetrics);
break;
case "alpha" :
view.setAlpha(parseFloat(value));
@@ -135,13 +120,13 @@ public class ViewAttrAdapter implements IAttributeAdapter {
view.setDuplicateParentStateEnabled(parseBoolean(value));
break;
case "elevation" :
view.setElevation(parseDimension(value, 0, dm));
view.setElevation(parseDimension(value, 0, displayMetrics));
break;
case "fadeScrollbars" :
view.setScrollbarFadingEnabled(parseBoolean(value));
break;
case "fadingEdgeLength" :
view.setFadingEdgeLength(parseDimension(value, 0, dm));
view.setFadingEdgeLength(parseDimension(value, 0, displayMetrics));
break;
case "filterTouchesWhenObscured" :
view.setFilterTouchesWhenObscured(parseBoolean(value));
@@ -162,37 +147,37 @@ public class ViewAttrAdapter implements IAttributeAdapter {
view.setId(parseId (value));
break;
case "minHeight" :
view.setMinimumHeight(parseDimension(value, 0, dm));
view.setMinimumHeight(parseDimension(value, 0, displayMetrics));
break;
case "minWidth" :
view.setMinimumWidth(parseDimension(value, 0, dm));
view.setMinimumWidth(parseDimension(value, 0, displayMetrics));
break;
case "padding" :
final int padding = parseDimension(value, 0, dm);
final int padding = parseDimension(value, 0, displayMetrics);
view.setPaddingRelative(padding, padding, padding, padding);
break;
case "paddingLeft" :
final int paddingLeft = parseDimension(value, 0, dm);
final int paddingLeft = parseDimension(value, 0, displayMetrics);
view.setPadding(paddingLeft, view.getPaddingTop(), view.getPaddingRight(), view.getPaddingBottom());
break;
case "paddingTop" :
final int paddingTop = parseDimension(value, 0, dm);
final int paddingTop = parseDimension(value, 0, displayMetrics);
view.setPadding(view.getPaddingLeft(), paddingTop, view.getPaddingRight(), view.getPaddingBottom());
break;
case "paddingRight" :
final int paddingRight = parseDimension(value, 0, dm);
final int paddingRight = parseDimension(value, 0, displayMetrics);
view.setPadding(view.getPaddingLeft(), view.getPaddingTop(), paddingRight, view.getPaddingBottom());
break;
case "paddingBottom" :
final int paddingBottom = parseDimension(value, 0, dm);
final int paddingBottom = parseDimension(value, 0, displayMetrics);
view.setPadding(view.getPaddingLeft(), view.getPaddingTop(), view.getPaddingRight(), paddingBottom);
break;
case "paddingStart" :
final int paddingStart = parseDimension(value, 0, dm);
final int paddingStart = parseDimension(value, 0, displayMetrics);
view.setPaddingRelative(paddingStart, view.getPaddingTop(), view.getPaddingEnd(), view.getPaddingBottom());
break;
case "paddingEnd" :
final int paddingEnd = parseDimension(value, 0, dm);
final int paddingEnd = parseDimension(value, 0, displayMetrics);
view.setPaddingRelative(view.getPaddingStart(), view.getPaddingTop(), paddingEnd, view.getPaddingBottom());
break;
case "rotation" :
@@ -253,7 +238,7 @@ public class ViewAttrAdapter implements IAttributeAdapter {
// ----- Handle attributes related to parent ------
if (!handled && params instanceof LinearLayout.LayoutParams) {
handled = handleLinearLayoutParams ((LinearLayout.LayoutParams) params, name, value, dm);
handled = handleLinearLayoutParams ((LinearLayout.LayoutParams) params, name, value, displayMetrics);
}
if (!handled && params instanceof RelativeLayout.LayoutParams) {
@@ -261,7 +246,7 @@ public class ViewAttrAdapter implements IAttributeAdapter {
}
if (!handled && params instanceof ViewGroup.MarginLayoutParams) {
handled = handleMarginParams((ViewGroup.MarginLayoutParams) params, name, value, dm);
handled = handleMarginParams((ViewGroup.MarginLayoutParams) params, name, value, displayMetrics);
}
view.setLayoutParams(params);
@@ -535,24 +520,11 @@ public class ViewAttrAdapter implements IAttributeAdapter {
protected String parseString(@NonNull String value) {
if (value.startsWith("@")) {
return parseString(resFinder.findString(value.substring("@string/".length())));
return parseString(resourceFinder.findString(value.substring("@string/".length())));
}
return value;
}
protected boolean parseBoolean(@NonNull String value) {
if (!value.startsWith("@")) {
if ("true".equals(value)) {
return true;
} else if ("false".equals(value)) {
return false;
}
} else {
// TODO Find resource value in booleans.xml
}
return false;
}
protected PorterDuff.Mode parsePorterDuffMode(@NonNull String mode) {
switch (mode) {
case "add" :
@@ -572,121 +544,6 @@ public class ViewAttrAdapter implements IAttributeAdapter {
}
}
protected Drawable parseDrawable (String value, final Context ctx) {
if (HEX_COLOR.matcher(value).matches()) {
return drawableForColor(value);
} else if (value.startsWith("@")) {
// First check if this is a reference to an android resource
if (value.startsWith("@android:")) {
final String typeAndValue = value.substring("@android:".length());
final String[] split = typeAndValue.split(Pattern.quote("/")); // For @android:color/white, it will be ["color", "white"]
final String type = split[0];
final String typeVal = split[1];
final int id = findAndroidResId (type, typeVal);
if (id != -1) {
switch (type) {
case "color" :
return drawableForColor(ContextCompat.getColor(ctx, id));
case "drawable" :
return ContextCompat.getDrawable(ctx, id);
default :
return newTransparentDrawable();
}
}
} else {
// We found a reference to another resource
if (value.startsWith("@drawable/")) {
final File drawable = resFinder.findDrawable (value.substring("@drawable/".length()));
if (drawable == null) {
return null;
}
if (drawable.getName ().endsWith (".xml")) {
// TODO This might not always be a vector drawable
// Implement a drawable parser that can parse everything listed here: https://developer.android.com/guide/topics/resources/drawable-resource
try {
return VectorMasterDrawable.fromXMLFile (drawable);
} catch (XmlPullParserException e) {
LOG.error ("Failed to parse XML Drawable", e);
return null;
}
} else {
return parseImageDrawable (ctx, drawable);
}
} else if (value.startsWith("@color/")) {
final String color = resFinder.findColor(value.substring("@color/".length()));
// TODO Check if this color resource is a selector
return parseDrawable(color, ctx);
}
}
}
return newTransparentDrawable();
}
// NinePatch has not been tested
@Nullable
private Drawable parseImageDrawable (final Context context, final File file) {
final var bitmap = ImageUtils.getBitmap (file);
if (bitmap == null) {
return null;
}
final var chunk = bitmap.getNinePatchChunk ();
if (NinePatch.isNinePatchChunk (chunk)) {
return new NinePatchDrawable (context.getResources (), new NinePatch (bitmap, chunk));
}
return new BitmapDrawable (context.getResources (), bitmap);
}
private int findAndroidResId(String type, String name) {
try {
final Class<?> typeClass = android.R.class.getClassLoader().loadClass("android.R$" + type);
final Field typeField = typeClass.getDeclaredField(name);
typeField.setAccessible(true);
return (int) typeField.get(null);
} catch (Throwable th) {
return -1;
}
}
protected int parseColor (String color, final Context ctx) {
if (HEX_COLOR.matcher(color).matches()) {
try {
return Color.parseColor(color);
} catch (Throwable th) {
// Ignored
}
} else if (color.startsWith("@color/")) {
return parseColor(resFinder.findColor(color.substring("@color/".length())), ctx);
} else if (color.startsWith("@android:color/")) {
final int id = findAndroidResId("color", color.substring("@android:color/".length()));
return ContextCompat.getColor(ctx, id);
}
return Color.parseColor("#00ffffff");
}
protected Drawable drawableForColor (String color) {
try {
return drawableForColor(Color.parseColor(color));
} catch (Throwable th) {
return newTransparentDrawable();
}
}
protected Drawable drawableForColor (int color) {
return new ColorDrawable (color);
}
protected Drawable newTransparentDrawable () {
return new ColorDrawable (Color.TRANSPARENT);
}
protected float parseFloat (String value) {
try {
return Float.parseFloat(value);
@@ -716,7 +573,7 @@ public class ViewAttrAdapter implements IAttributeAdapter {
return (int) TypedValue.applyDimension(getUnitForDimensionType(dimensionType), dimen, dm);
} else if (c == '@') {
String name = value.substring("@dimen/".length());
return parseDimension(resFinder.findDimension(name), defaultValue, dm);
return parseDimension(resourceFinder.findDimension(name), defaultValue, dm);
} else if (Character.isLetter(c)) {
// This could be one of the following :
// 1. match_parent
@@ -18,12 +18,14 @@
package com.itsaky.inflater.adapters.android.view;
import android.animation.LayoutTransition;
import android.util.DisplayMetrics;
import android.view.View;
import android.view.ViewGroup;
import androidx.annotation.NonNull;
import com.itsaky.inflater.IAttribute;
import com.itsaky.inflater.IResourceFinder;
import org.jetbrains.annotations.Contract;
@@ -34,6 +36,10 @@ import org.jetbrains.annotations.Contract;
*/
public class ViewGroupAttrAdapter extends ViewAttrAdapter {
public ViewGroupAttrAdapter (@NonNull IResourceFinder resourceFinder, DisplayMetrics displayMetrics) {
super (resourceFinder, displayMetrics);
}
@Override
public boolean isApplicableTo(View view) {
return view instanceof ViewGroup;
@@ -17,10 +17,12 @@
package com.itsaky.inflater.adapters.android.widget;
import android.util.DisplayMetrics;
import android.view.View;
import android.widget.AbsSeekBar;
import androidx.annotation.NonNull;
import com.itsaky.inflater.IAttribute;
import com.itsaky.inflater.IResourceFinder;
/**
* Attribute adapter for handling attributes related to
@@ -29,7 +31,11 @@ import com.itsaky.inflater.IAttribute;
* @author Akash Yadav
*/
public abstract class AbsSeekBarAttrAdapter extends ProgressBarAttrAdapter {
public AbsSeekBarAttrAdapter (@NonNull IResourceFinder resourceFinder, DisplayMetrics displayMetrics) {
super (resourceFinder, displayMetrics);
}
@Override
public boolean isApplicableTo(View view) {
return view instanceof AbsSeekBar;
@@ -17,13 +17,21 @@
package com.itsaky.inflater.adapters.android.widget;
import android.util.DisplayMetrics;
import android.view.View;
import android.widget.AbsSpinner;
import androidx.annotation.NonNull;
import com.itsaky.inflater.IAttribute;
import com.itsaky.inflater.IResourceFinder;
public class AbsSpinnerAttrAdapter extends AdapterViewAttrAdapter {
public AbsSpinnerAttrAdapter (@NonNull IResourceFinder resourceFinder, DisplayMetrics displayMetrics) {
super (resourceFinder, displayMetrics);
}
@Override
public boolean isApplicableTo(View view) {
return view instanceof AbsSpinner;
@@ -45,7 +53,7 @@ public class AbsSpinnerAttrAdapter extends AdapterViewAttrAdapter {
switch (name) {
case "entries" :
final var adapter = newSimpleAdapter(context, resFinder.findArray(value));
final var adapter = newSimpleAdapter(context, resourceFinder.findArray(value));
adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
spinner.setAdapter(adapter);
break;
@@ -18,11 +18,15 @@
package com.itsaky.inflater.adapters.android.widget;
import android.content.Context;
import android.util.DisplayMetrics;
import android.view.View;
import android.widget.AdapterView;
import android.widget.ArrayAdapter;
import androidx.annotation.NonNull;
import com.itsaky.inflater.IAttribute;
import com.itsaky.inflater.IResourceFinder;
import com.itsaky.inflater.adapters.android.view.ViewGroupAttrAdapter;
/**
@@ -32,7 +36,11 @@ import com.itsaky.inflater.adapters.android.view.ViewGroupAttrAdapter;
* @author Akash Yadav
*/
public abstract class AdapterViewAttrAdapter extends ViewGroupAttrAdapter {
public AdapterViewAttrAdapter (@NonNull IResourceFinder resourceFinder, DisplayMetrics displayMetrics) {
super (resourceFinder, displayMetrics);
}
@Override
public boolean isApplicableTo(View view) {
return view instanceof AdapterView;
@@ -17,10 +17,14 @@
package com.itsaky.inflater.adapters.android.widget;
import android.util.DisplayMetrics;
import android.view.View;
import android.widget.Button;
import androidx.annotation.NonNull;
import com.itsaky.inflater.IAttribute;
import com.itsaky.inflater.IResourceFinder;
/**
* Adapter for handling attributes related to Button.
@@ -31,7 +35,11 @@ import com.itsaky.inflater.IAttribute;
* @author Akash Yadav
*/
public class ButtonAttrAdapter extends TextViewAttrAdapter {
public ButtonAttrAdapter (@NonNull IResourceFinder resourceFinder, DisplayMetrics displayMetrics) {
super (resourceFinder, displayMetrics);
}
@Override
public boolean isApplicableTo(View view) {
return view instanceof Button;
@@ -16,16 +16,24 @@
*/
package com.itsaky.inflater.adapters.android.widget;
import android.util.DisplayMetrics;
import android.view.View;
import android.widget.CheckBox;
import androidx.annotation.NonNull;
import com.itsaky.inflater.IAttribute;
import com.itsaky.inflater.IResourceFinder;
/**
* Attribute handler for handling attributes related to CheckBox
*/
public class CheckBoxAttrAdapter extends CompondButtonAttrAdapter {
public CheckBoxAttrAdapter (@NonNull IResourceFinder resourceFinder, DisplayMetrics displayMetrics) {
super (resourceFinder, displayMetrics);
}
@Override
public boolean isApplicableTo(View view) {
return view instanceof CheckBox;
@@ -16,10 +16,14 @@
*/
package com.itsaky.inflater.adapters.android.widget;
import android.util.DisplayMetrics;
import android.view.View;
import android.widget.CheckedTextView;
import androidx.annotation.NonNull;
import com.itsaky.inflater.IAttribute;
import com.itsaky.inflater.IResourceFinder;
/**
* Attribute handler for handling attributes related to
@@ -28,7 +32,11 @@ import com.itsaky.inflater.IAttribute;
* @author Akash Yadav
*/
public class CheckedTextViewAttrAdapter extends TextViewAttrAdapter {
public CheckedTextViewAttrAdapter (@NonNull IResourceFinder resourceFinder, DisplayMetrics displayMetrics) {
super (resourceFinder, displayMetrics);
}
@Override
public boolean isApplicableTo(View view) {
return view instanceof CheckedTextView;
@@ -1,10 +1,14 @@
package com.itsaky.inflater.adapters.android.widget;
import android.content.Context;
import android.util.DisplayMetrics;
import android.view.View;
import android.widget.CompoundButton;
import androidx.annotation.NonNull;
import com.itsaky.inflater.IAttribute;
import com.itsaky.inflater.IResourceFinder;
/**
* Attribute handler for handling attibutes to CompoundButton
@@ -12,7 +16,11 @@ import com.itsaky.inflater.IAttribute;
* @author Akash Yadav
*/
public class CompondButtonAttrAdapter extends ButtonAttrAdapter {
public CompondButtonAttrAdapter (@NonNull IResourceFinder resourceFinder, DisplayMetrics displayMetrics) {
super (resourceFinder, displayMetrics);
}
@Override
public boolean isApplicableTo(View view) {
return view instanceof CompoundButton;
@@ -17,13 +17,21 @@
package com.itsaky.inflater.adapters.android.widget;
import android.util.DisplayMetrics;
import android.view.View;
import android.widget.EditText;
import androidx.annotation.NonNull;
import com.itsaky.inflater.IAttribute;
import com.itsaky.inflater.IResourceFinder;
public class EditTextAttrAdapter extends TextViewAttrAdapter {
public EditTextAttrAdapter (@NonNull IResourceFinder resourceFinder, DisplayMetrics displayMetrics) {
super (resourceFinder, displayMetrics);
}
@Override
public boolean isApplicableTo(View view) {
return view instanceof EditText;
@@ -16,10 +16,14 @@
*/
package com.itsaky.inflater.adapters.android.widget;
import android.util.DisplayMetrics;
import android.view.View;
import android.widget.ImageButton;
import androidx.annotation.NonNull;
import com.itsaky.inflater.IAttribute;
import com.itsaky.inflater.IResourceFinder;
/**
* Attribute handler for handling attributes related to
@@ -29,6 +33,10 @@ import com.itsaky.inflater.IAttribute;
*/
public class ImageButtonAttrAdapter extends ImageViewAttrAdapter {
public ImageButtonAttrAdapter (@NonNull IResourceFinder resourceFinder, DisplayMetrics displayMetrics) {
super (resourceFinder, displayMetrics);
}
@Override
public boolean isApplicableTo(View view) {
return view instanceof ImageButton;
@@ -22,8 +22,11 @@ import android.graphics.drawable.Drawable;
import android.util.DisplayMetrics;
import android.view.View;
import android.widget.ImageView;
import androidx.annotation.NonNull;
import androidx.core.content.ContextCompat;
import com.itsaky.inflater.IAttribute;
import com.itsaky.inflater.IResourceFinder;
import com.itsaky.inflater.adapters.android.view.ViewAttrAdapter;
/**
@@ -33,7 +36,11 @@ import com.itsaky.inflater.adapters.android.view.ViewAttrAdapter;
* @author Akash Yadav
*/
public class ImageViewAttrAdapter extends ViewAttrAdapter {
public ImageViewAttrAdapter (@NonNull IResourceFinder resourceFinder, DisplayMetrics displayMetrics) {
super (resourceFinder, displayMetrics);
}
@Override
public boolean isApplicableTo(View view) {
return view instanceof ImageView;
@@ -16,12 +16,14 @@
*/
package com.itsaky.inflater.adapters.android.widget;
import android.util.DisplayMetrics;
import android.view.View;
import android.widget.LinearLayout;
import androidx.annotation.NonNull;
import com.itsaky.inflater.IAttribute;
import com.itsaky.inflater.IResourceFinder;
import com.itsaky.inflater.adapters.android.view.ViewGroupAttrAdapter;
/**
@@ -30,7 +32,11 @@ import com.itsaky.inflater.adapters.android.view.ViewGroupAttrAdapter;
* @author Akash Yadav
*/
public class LinearLayoutAttrAdapter extends ViewGroupAttrAdapter {
public LinearLayoutAttrAdapter (@NonNull IResourceFinder resourceFinder, DisplayMetrics displayMetrics) {
super (resourceFinder, displayMetrics);
}
@Override
public boolean isApplicableTo(View view) {
return view instanceof LinearLayout;
@@ -21,7 +21,11 @@ import android.content.Context;
import android.util.DisplayMetrics;
import android.view.View;
import android.widget.ProgressBar;
import androidx.annotation.NonNull;
import com.itsaky.inflater.IAttribute;
import com.itsaky.inflater.IResourceFinder;
import com.itsaky.inflater.adapters.android.view.ViewAttrAdapter;
/**
@@ -31,7 +35,11 @@ import com.itsaky.inflater.adapters.android.view.ViewAttrAdapter;
* @author Akash Yadav
*/
public class ProgressBarAttrAdapter extends ViewAttrAdapter {
public ProgressBarAttrAdapter (@NonNull IResourceFinder resourceFinder, DisplayMetrics displayMetrics) {
super (resourceFinder, displayMetrics);
}
@Override
public boolean isApplicableTo(View view) {
return view instanceof ProgressBar;
@@ -17,10 +17,14 @@
package com.itsaky.inflater.adapters.android.widget;
import android.util.DisplayMetrics;
import android.view.View;
import android.widget.RadioButton;
import androidx.annotation.NonNull;
import com.itsaky.inflater.IAttribute;
import com.itsaky.inflater.IResourceFinder;
/**
* Attribute adapter for handling attributes related to
@@ -29,7 +33,11 @@ import com.itsaky.inflater.IAttribute;
* @author Akash Yadav
*/
public class RadioButtonAttrAdapter extends CompondButtonAttrAdapter {
public RadioButtonAttrAdapter (@NonNull IResourceFinder resourceFinder, DisplayMetrics displayMetrics) {
super (resourceFinder, displayMetrics);
}
@Override
public boolean isApplicableTo(View view) {
return view instanceof RadioButton;
@@ -16,9 +16,14 @@
*/
package com.itsaky.inflater.adapters.android.widget;
import android.util.DisplayMetrics;
import android.view.View;
import android.widget.RelativeLayout;
import androidx.annotation.NonNull;
import com.itsaky.inflater.IAttribute;
import com.itsaky.inflater.IResourceFinder;
import com.itsaky.inflater.adapters.android.view.ViewGroupAttrAdapter;
/**
@@ -28,7 +33,11 @@ import com.itsaky.inflater.adapters.android.view.ViewGroupAttrAdapter;
* @author Akash Yadav
*/
public class RelativeLayoutAttrAdapter extends ViewGroupAttrAdapter {
public RelativeLayoutAttrAdapter (@NonNull IResourceFinder resourceFinder, DisplayMetrics displayMetrics) {
super (resourceFinder, displayMetrics);
}
@Override
public boolean isApplicableTo(View view) {
return view instanceof RelativeLayout;
@@ -17,12 +17,14 @@
package com.itsaky.inflater.adapters.android.widget;
import android.util.DisplayMetrics;
import android.view.View;
import android.widget.SeekBar;
import androidx.annotation.NonNull;
import com.itsaky.inflater.IAttribute;
import com.itsaky.inflater.IResourceFinder;
/**
* Attribute adapter for handling attributes related to
@@ -31,7 +33,11 @@ import com.itsaky.inflater.IAttribute;
* @author Akash Yadav
*/
public class SeekBarAttrAdapter extends AbsSeekBarAttrAdapter {
public SeekBarAttrAdapter (@NonNull IResourceFinder resourceFinder, DisplayMetrics displayMetrics) {
super (resourceFinder, displayMetrics);
}
@Override
public boolean isApplicableTo(View view) {
return view instanceof SeekBar;
@@ -17,12 +17,14 @@
package com.itsaky.inflater.adapters.android.widget;
import android.util.DisplayMetrics;
import android.view.View;
import android.widget.Spinner;
import androidx.annotation.NonNull;
import com.itsaky.inflater.IAttribute;
import com.itsaky.inflater.IResourceFinder;
/**
* Attribute adapter for handling attributes related to
@@ -31,7 +33,11 @@ import com.itsaky.inflater.IAttribute;
* @author Akash Yadav
*/
public class SpinnerAttrAdapter extends AbsSpinnerAttrAdapter {
public SpinnerAttrAdapter (@NonNull IResourceFinder resourceFinder, DisplayMetrics displayMetrics) {
super (resourceFinder, displayMetrics);
}
@Override
public boolean isApplicableTo(View view) {
return view instanceof Spinner;
@@ -33,6 +33,7 @@ import androidx.annotation.NonNull;
import com.blankj.utilcode.util.SizeUtils;
import com.itsaky.inflater.IAttribute;
import com.itsaky.inflater.IResourceFinder;
import com.itsaky.inflater.adapters.android.view.ViewAttrAdapter;
import java.util.regex.Pattern;
@@ -42,7 +43,11 @@ import java.util.regex.Pattern;
* @author Akash Yadav
*/
public class TextViewAttrAdapter extends ViewAttrAdapter {
public TextViewAttrAdapter (@NonNull IResourceFinder resourceFinder, DisplayMetrics displayMetrics) {
super (resourceFinder, displayMetrics);
}
@Override
public boolean isApplicableTo(View view) {
return view instanceof TextView;
@@ -0,0 +1,142 @@
/*
* This file is part of AndroidIDE.
*
* AndroidIDE is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* AndroidIDE is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with AndroidIDE. If not, see <https://www.gnu.org/licenses/>.
*/
package com.itsaky.inflater.drawable;
import android.content.Context;
import android.graphics.NinePatch;
import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.Drawable;
import android.graphics.drawable.NinePatchDrawable;
import android.util.DisplayMetrics;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.blankj.utilcode.util.FileIOUtils;
import com.blankj.utilcode.util.ImageUtils;
import com.itsaky.inflater.IResourceFinder;
import com.sdsmdg.harjot.vectormaster.VectorMasterDrawable;
import org.xmlpull.v1.XmlPullParser;
import org.xmlpull.v1.XmlPullParserException;
import org.xmlpull.v1.XmlPullParserFactory;
import java.io.File;
import java.io.IOException;
import java.io.Reader;
import java.io.StringReader;
/**
* Utility methods for creating instances of suitable drawable parsers for a file
* or an XML code.
*
* @author Akash Yadav
*/
public abstract class DrawableParserFactory {
/**
* Create a new drawable parser for the given file. If the given file is not an XML Document,
* then a no-op parser is returned which simply returns the drawable for that file. If the file
* is an XML document, then a suitable parser is determined by looking at the root tag of the
* drawable.
*
* @param context The context. This will be used for obtaining app resources.
* @param file The file to parse.
* @param resourceFinder The resource finder. Used for finding references to another resources.
* @return A new {@link IDrawableParser} instance if the inputs are valid, or {@code null} if there were any
* issues creating the parser.
* @throws XmlPullParserException Thrown by the {@link XmlPullParser#setInput(Reader)}.
* @throws IOException Thrown by {@link XmlPullParser#next()}
*/
@Nullable
public static IDrawableParser newParser (@NonNull final Context context, @NonNull final File file, final IResourceFinder resourceFinder) throws XmlPullParserException, IOException {
if (file.getName ().endsWith (".xml")) {
final var code = FileIOUtils.readFile2String (file);
return DrawableParserFactory.newXmlDrawableParser (code, resourceFinder, context.getResources ().getDisplayMetrics ());
} else {
final var bitmap = ImageUtils.getBitmap (file);
if (bitmap == null) {
return null;
}
final var chunk = bitmap.getNinePatchChunk ();
if (NinePatch.isNinePatchChunk (chunk)) {
return new NoParser (new NinePatchDrawable (context.getResources (), new NinePatch (bitmap, chunk)));
}
return new NoParser (new BitmapDrawable (context.getResources (), bitmap));
}
}
/**
* Create a new drawable parser for the given xml code.
*
* @param xmlDrawable The XML code. This must be valid.
* @param resourceFinder The resource finder. Used for finding references to another resources.
* @param displayMetrics This will be used by generated parsers to parse dimension vlaues.
* @return A new {@link IDrawableParser} instance if the inputs are valid, or {@code null} if there were any
* issues creating the parser.
* @throws XmlPullParserException Thrown by the {@link XmlPullParser#setInput(Reader)}.
* @throws IOException Thrown by {@link XmlPullParser#next()}
*/
@Nullable
public static IDrawableParser newXmlDrawableParser (String xmlDrawable, IResourceFinder resourceFinder, DisplayMetrics displayMetrics) throws XmlPullParserException, IOException {
final var factory = XmlPullParserFactory.newInstance ();
factory.setNamespaceAware (true);
final var parser = factory.newPullParser ();
parser.setInput (new StringReader (xmlDrawable));
var event = parser.getEventType ();
while (event != XmlPullParser.END_DOCUMENT) {
if (event == XmlPullParser.START_TAG) {
final var name = parser.getName ();
switch (name) {
case "shape" :
return new ShapeDrawableParser (parser, resourceFinder, displayMetrics);
case "vector" :
return new NoParser (VectorMasterDrawable.fromXML (xmlDrawable));
default:
return null;
}
}
event = parser.next ();
}
// We don't support parsing of this type of drawable
return null;
}
/**
* Instead of parsing anything, this returns the provided drawable.
*/
private static class NoParser extends IDrawableParser {
private final Drawable parsed;
protected NoParser (final Drawable parsed) {
super (null, null, null);
this.parsed = parsed;
}
@Override
public Drawable parse () {
return parsed;
}
}
}
@@ -0,0 +1,74 @@
/*
* This file is part of AndroidIDE.
*
* AndroidIDE is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* AndroidIDE is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with AndroidIDE. If not, see <https://www.gnu.org/licenses/>.
*/
package com.itsaky.inflater.drawable;
import android.graphics.drawable.Drawable;
import android.os.Build;
import android.util.DisplayMetrics;
import com.itsaky.inflater.IResourceFinder;
import com.itsaky.inflater.util.CommonParseUtils;
import org.xmlpull.v1.XmlPullParser;
/**
* Base class for drawable parsers.
*
* @author Akash Yadav
*/
public abstract class IDrawableParser extends CommonParseUtils {
protected final XmlPullParser parser;
protected IDrawableParser (XmlPullParser parser, IResourceFinder resourceFinder, DisplayMetrics displayMetrics) {
super(resourceFinder, displayMetrics);
this.parser = parser;
}
public abstract Drawable parse () throws Exception;
protected int attrIndex (final String name) {
for (int i = 0; i < this.parser.getAttributeCount (); i++) {
if (this.parser.getAttributeName (i).equals (name)) {
return i;
}
}
return -1;
}
protected String value (int index) {
return parser.getAttributeValue (index);
}
protected boolean isApi26() {
return Build.VERSION.SDK_INT >= 26;
}
protected boolean isApi28() {
return Build.VERSION.SDK_INT >= 28;
}
protected boolean isApi29() {
return Build.VERSION.SDK_INT >= 29;
}
protected boolean isApi30() {
return Build.VERSION.SDK_INT >= 30;
}
}
@@ -0,0 +1,349 @@
/*
* This file is part of AndroidIDE.
*
* AndroidIDE is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* AndroidIDE is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with AndroidIDE. If not, see <https://www.gnu.org/licenses/>.
*/
package com.itsaky.inflater.drawable;
import android.content.Context;
import android.graphics.Color;
import android.graphics.Rect;
import android.graphics.drawable.Drawable;
import android.graphics.drawable.GradientDrawable;
import android.util.DisplayMetrics;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.itsaky.androidide.app.BaseApplication;
import com.itsaky.inflater.IResourceFinder;
import org.jetbrains.annotations.Contract;
import org.xmlpull.v1.XmlPullParser;
import java.util.Arrays;
/**
* Parses a drawable whose root element is
* {@code <shape>}. If the parse was successful, returns a {@link GradientDrawable},
* otherwise {@code null}.
*
* @author Akash Yadav
*/
public class ShapeDrawableParser extends IDrawableParser {
protected ShapeDrawableParser (XmlPullParser parser, IResourceFinder resourceFinder, DisplayMetrics displayMetrics) {
super (parser, resourceFinder, displayMetrics);
}
@Nullable
@Override
public Drawable parse () throws Exception {
final var drawable = new GradientDrawable ();
var index = attrIndex ("shape");
drawable.setShape (index == -1 ? GradientDrawable.RECTANGLE : parseShape (this.parser.getAttributeValue (index)));
if (isApi29 ()) {
index = attrIndex ("innerRadius");
drawable.setInnerRadius (index == -1 ? 0 : parseDimension (value (index), 0));
index = attrIndex ("innerRadiusRatio");
drawable.setInnerRadiusRatio (index == -1 ? 1f : Float.parseFloat (value (index)));
index = attrIndex ("thickness");
drawable.setThickness (index == -1 ? 0 : parseDimension (value (index), 0));
index = attrIndex ("thicknessRatio");
drawable.setThicknessRatio (index == -1 ? 1f : Float.parseFloat (value (index)));
}
index = attrIndex ("useLevel");
drawable.setUseLevel (index != -1 && parseBoolean (value (index)));
var event = parser.getEventType ();
while (event != XmlPullParser.END_DOCUMENT) {
final var name = parser.getName ();
if (event == XmlPullParser.START_TAG) {
switch (name) {
case "corners":
parseCorners (drawable);
break;
case "gradient":
parseGradient (drawable);
break;
case "padding" :
parsePadding (drawable);
break;
case "size" :
parseSize (drawable);
break;
case "solid" :
parseSolid (drawable);
break;
case "stroke" :
parseStroke (drawable);
break;
}
}
event = parser.next ();
}
return drawable;
}
private void parseStroke (@NonNull final GradientDrawable drawable) {
var index = attrIndex ("width");
var strokeWidth = 0;
if (index != - 1) {
drawable.setStroke (strokeWidth = parseDimension (value (index), 0), Color.TRANSPARENT);
}
index = attrIndex ("color");
var strokeColor = Color.TRANSPARENT;
if (index != -1) {
drawable.setStroke (strokeWidth, strokeColor = parseColor (value (index), appContext ()));
}
index = attrIndex ("dashWidth");
var dashWidth = 0;
if (index != -1) {
drawable.setStroke (strokeWidth, strokeColor, dashWidth = parseDimension (value (index), 0), 0);
}
index = attrIndex ("dashGap");
if (index != -1) {
drawable.setStroke (strokeWidth, strokeColor, dashWidth, parseDimension (value (index), 0));
}
}
private void parseSolid (@NonNull final GradientDrawable drawable) {
var index = attrIndex ("color");
if (index != -1) {
drawable.setColor (parseColor (value (index), appContext ()));
}
}
private void parseSize (@NonNull final GradientDrawable drawable) {
var index = attrIndex ("width");
if (index != -1) {
drawable.setSize (parseDimension (value (index), 0), drawable.getIntrinsicHeight ());
}
index = attrIndex ("height");
if (index != -1) {
drawable.setSize (drawable.getIntrinsicWidth (), parseDimension (value (index), 0));
}
}
private void parsePadding (@NonNull final GradientDrawable drawable) {
// Padding is available from API 29 only
if (!isApi29 ()) {
return;
}
final var rect = new Rect ();
if (!drawable.getPadding (rect)) {
rect.set (0,0,0,0);
}
var changed = false;
var index = attrIndex ("left");
if (index != -1) {
rect.left = parseDimension (value (index), 0);
changed = true;
}
index = attrIndex ("top");
if (index != -1) {
rect.top = parseDimension (value (index), 0);
changed = true;
}
index = attrIndex ("right");
if (index != -1) {
rect.right = parseDimension (value (index), 0);
changed = true;
}
index = attrIndex ("bottom");
if (index != -1) {
rect.bottom = parseDimension (value (index), 0);
changed = true;
}
if (changed) {
drawable.setPadding (rect.left, rect.top, rect.right, rect.bottom);
}
}
private void parseGradient (@NonNull final GradientDrawable drawable) {
var index = attrIndex ("angle");
drawable.setOrientation (index == -1 ? GradientDrawable.Orientation.LEFT_RIGHT : parseGradientOrientation (value (index)));
index = attrIndex ("centerX");
var center = index == - 1 ? drawable.getGradientCenterX () : Float.parseFloat (value (index));
drawable.setGradientCenter (center, drawable.getGradientCenterY ());
index = attrIndex ("centerY");
center = index == - 1 ? drawable.getGradientCenterX () : Float.parseFloat (value (index));
drawable.setGradientCenter (drawable.getGradientCenterX (), center);
final var colors = new int [3];
var changed = false;
Arrays.fill (colors, -1);
index = attrIndex ("centerColor");
if (index != -1) {
colors [1] = parseColor (value (index), appContext ());
changed = true;
}
index = attrIndex ("endColor");
if (index != -1) {
colors [2] = parseColor (value (index), appContext ());
changed = true;
}
index = attrIndex ("gradientRadius");
drawable.setGradientRadius (index == -1 ? drawable.getGradientRadius () : Float.parseFloat (value (index)));
index = attrIndex ("startColor");
if (index != -1) {
colors[0] = parseColor (value (index), appContext ());
changed = true;
}
if (changed) {
drawable.setColors (colors);
}
index = attrIndex ("type");
drawable.setGradientType (index == -1 ? GradientDrawable.LINEAR_GRADIENT : parseGradientType (value (index)));
index = attrIndex ("useLevel");
drawable.setUseLevel (index != -1 && parseBoolean (value (index)));
}
private int parseGradientType (String value) {
switch (value) {
case "radial" :
return GradientDrawable.RADIAL_GRADIENT;
case "sweep" :
return GradientDrawable.SWEEP_GRADIENT;
case "linear" :
default:
return GradientDrawable.LINEAR_GRADIENT;
}
}
private GradientDrawable.Orientation parseGradientOrientation (String value) {
final var angle = Integer.parseInt (value);
// Angle must be between 0-315 and a multiple of 45
// Angle moves in anti-clockwise direction
if (angle == 45) {
return GradientDrawable.Orientation.BL_TR;
}
if (angle == 90) {
return GradientDrawable.Orientation.BOTTOM_TOP;
}
if (angle == 135) {
return GradientDrawable.Orientation.BR_TL;
}
if (angle == 180) {
return GradientDrawable.Orientation.RIGHT_LEFT;
}
if (angle == 225) {
return GradientDrawable.Orientation.TR_BL;
}
if (angle == 270) {
return GradientDrawable.Orientation.TOP_BOTTOM;
}
if (angle == 315) {
return GradientDrawable.Orientation.TL_BR;
}
// Angle 0 or any invalid valid should make the orientation left to right
return GradientDrawable.Orientation.LEFT_RIGHT;
}
private void parseCorners (@NonNull GradientDrawable drawable) {
int index;
index = attrIndex ("radius");
drawable.setCornerRadius (index == -1 ? 0f : parseDimension (value (index), 0));
var changed = false;
final var radii = new float[8];
Arrays.fill (radii, 0f);
index = attrIndex ("topLeftRadius");
if (index != -1) {
radii[0] = radii[1] = parseDimension (value (index), 0);
changed = true;
}
index = attrIndex ("topRightRadius");
if (index != -1) {
radii[2] = radii[3] = parseDimension (value (index), 0);
changed = true;
}
index = attrIndex ("bottomLeftRadius");
if (index != -1) {
radii[4] = radii[5] = parseDimension (value (index), 0);
changed = true;
}
index = attrIndex ("bottomRightRadius");
if (index != -1) {
radii[6] = radii[7] = parseDimension (value (index), 0);
changed = true;
}
if (changed) {
drawable.setCornerRadii (radii);
}
}
@Contract(pure = true)
private int parseShape (@NonNull String value) {
switch (value) {
case "oval" :
return GradientDrawable.OVAL;
case "line":
return GradientDrawable.LINE;
case "ring" :
return GradientDrawable.RING;
case "rectangle" :
default:
return GradientDrawable.RECTANGLE;
}
}
protected Context appContext () {
return BaseApplication.getBaseInstance ();
}
}
@@ -0,0 +1,235 @@
/*
* This file is part of AndroidIDE.
*
* AndroidIDE is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* AndroidIDE is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with AndroidIDE. If not, see <https://www.gnu.org/licenses/>.
*/
package com.itsaky.inflater.util;
import android.content.Context;
import android.graphics.Color;
import android.graphics.NinePatch;
import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.ColorDrawable;
import android.graphics.drawable.Drawable;
import android.graphics.drawable.NinePatchDrawable;
import android.util.DisplayMetrics;
import android.util.TypedValue;
import android.view.ViewGroup;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.core.content.ContextCompat;
import com.blankj.utilcode.util.ImageUtils;
import com.itsaky.androidide.utils.Logger;
import com.itsaky.inflater.IResourceFinder;
import com.itsaky.inflater.drawable.DrawableParserFactory;
import com.sdsmdg.harjot.vectormaster.VectorMasterDrawable;
import org.xmlpull.v1.XmlPullParserException;
import java.io.File;
import java.io.IOException;
import java.lang.reflect.Field;
import java.util.regex.Pattern;
/**
* Contains methods which are commonly used by layout inflater,
* attribute adapters and drawable parsers.
*
* @author Akash Yadav
*/
public class CommonParseUtils {
private static final Pattern HEX_COLOR = Pattern.compile("#[a-fA-F0-9]{6,8}");
protected IResourceFinder resourceFinder;
protected final DisplayMetrics displayMetrics;
private static final Logger LOG = Logger.instance ("CommonParseUtils");
public CommonParseUtils (@NonNull IResourceFinder resourceFinder, DisplayMetrics displayMetrics) {
this.resourceFinder = resourceFinder;
this.displayMetrics = displayMetrics;
}
protected boolean parseBoolean(@NonNull String value) {
if (!value.startsWith("@")) {
if ("true".equals(value)) {
return true;
} else if ("false".equals(value)) {
return false;
}
} else {
// TODO Find resource value in booleans.xml
}
return false;
}
protected int parseDimension(final String value, int defaultValue) {
if (value == null) {
return defaultValue;
}
char c = value.charAt(0);
if (Character.isDigit(c)) {
// A dimension value which starts with a digit. E.g.: 1dp, 12sp, 123px, etc.
String dimensionVal = "";
int index = 0;
while (Character.isDigit(c = value.charAt(index))) {
dimensionVal += c;
index ++;
}
final int dimen = Integer.parseInt(dimensionVal);
final String dimensionType = value.substring(index);
return (int) TypedValue.applyDimension(getUnitForDimensionType(dimensionType), dimen, displayMetrics);
} else if (c == '@') {
String name = value.substring("@dimen/".length());
return parseDimension(resourceFinder.findDimension(name), defaultValue);
} else if (Character.isLetter(c)) {
// This could be one of the following :
// 1. match_parent
// 2. wrap_content
// 3. fill_parent
switch (value) {
case "match_parent" :
case "fill_parent" :
return ViewGroup.LayoutParams.MATCH_PARENT;
case "wrap_content" :
default :
return ViewGroup.LayoutParams.WRAP_CONTENT;
}
}
return defaultValue;
}
protected int getUnitForDimensionType(@NonNull String dimensionType) {
switch (dimensionType) {
case "dp" :
return TypedValue.COMPLEX_UNIT_DIP;
case "sp" :
return TypedValue.COMPLEX_UNIT_SP;
case "px" :
return TypedValue.COMPLEX_UNIT_PX;
case "pt" :
return TypedValue.COMPLEX_UNIT_PT;
case "in" :
return TypedValue.COMPLEX_UNIT_IN;
case "mm" :
return TypedValue.COMPLEX_UNIT_MM;
}
return TypedValue.COMPLEX_UNIT_DIP;
}
protected int parseColor (String color, final Context ctx) {
if (HEX_COLOR.matcher(color).matches()) {
try {
return Color.parseColor(color);
} catch (Throwable th) {
// Ignored
}
} else if (color.startsWith("@color/")) {
return parseColor(resourceFinder.findColor(color.substring("@color/".length())), ctx);
} else if (color.startsWith("@android:color/")) {
final int id = findAndroidResId("color", color.substring("@android:color/".length()));
return ContextCompat.getColor(ctx, id);
}
return Color.parseColor("#00ffffff");
}
protected Drawable parseDrawable (String value, final Context ctx) {
if (HEX_COLOR.matcher(value).matches()) {
return drawableForColor(value);
} else if (value.startsWith("@")) {
// First check if this is a reference to an android resource
if (value.startsWith("@android:")) {
final String typeAndValue = value.substring("@android:".length());
final String[] split = typeAndValue.split(Pattern.quote("/")); // For @android:color/white, it will be ["color", "white"]
final String type = split[0];
final String typeVal = split[1];
final int id = findAndroidResId (type, typeVal);
if (id != -1) {
switch (type) {
case "color" :
return drawableForColor(ContextCompat.getColor(ctx, id));
case "drawable" :
return ContextCompat.getDrawable(ctx, id);
default :
return newTransparentDrawable();
}
}
} else {
// We found a reference to another resource
if (value.startsWith("@drawable/")) {
final File drawable = resourceFinder.findDrawable (value.substring("@drawable/".length()));
if (drawable == null) {
return null;
}
try {
final var parser = DrawableParserFactory.newParser (ctx, drawable, resourceFinder);
if (parser == null) {
return null;
}
return parser.parse();
} catch (Exception e) {
LOG.error ("Error parsing drawable", e);
return null;
}
} else if (value.startsWith("@color/")) {
final String color = resourceFinder.findColor(value.substring("@color/".length()));
// TODO Check if this color resource is a selector
return parseDrawable(color, ctx);
}
}
}
return newTransparentDrawable();
}
private int findAndroidResId(String type, String name) {
try {
final Class<?> typeClass = android.R.class.getClassLoader().loadClass("android.R$" + type);
final Field typeField = typeClass.getDeclaredField(name);
typeField.setAccessible(true);
return (int) typeField.get(null);
} catch (Throwable th) {
return -1;
}
}
protected Drawable drawableForColor (String color) {
try {
return drawableForColor(Color.parseColor(color));
} catch (Throwable th) {
return newTransparentDrawable();
}
}
protected Drawable drawableForColor (int color) {
return new ColorDrawable (color);
}
protected Drawable newTransparentDrawable () {
return new ColorDrawable (Color.TRANSPARENT);
}
}