Added support for layer list drawables

This commit is contained in:
Akash Yadav
2022-02-11 13:11:39 +05:30
parent bb6dbedf41
commit 240ce59cf8
8 changed files with 318 additions and 57 deletions
+1
View File
@@ -57,6 +57,7 @@
<entry key="..\:/Projects/AndroidStudioProjects/AndroidIDE/app/src/main/res/menu/menu_editor.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/empy_drawable.xml" value="0.147" />
<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" />
Binary file not shown.
@@ -55,13 +55,13 @@ public abstract class DrawableParserFactory {
* 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 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.
* issues creating the parser.
* @throws XmlPullParserException Thrown by the {@link XmlPullParser#setInput(Reader)}.
* @throws IOException Thrown by {@link XmlPullParser#next()}
* @throws IOException Thrown by {@link XmlPullParser#next()}
*/
@Nullable
public static IDrawableParser newParser (@NonNull final Context context, @NonNull final File file, final IResourceTable resourceFinder) throws XmlPullParserException, IOException {
@@ -86,13 +86,13 @@ public abstract class DrawableParserFactory {
/**
* Create a new drawable parser for the given xml code.
*
* @param xmlDrawable The XML code. This must be valid.
* @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.
* issues creating the parser.
* @throws XmlPullParserException Thrown by the {@link XmlPullParser#setInput(Reader)}.
* @throws IOException Thrown by {@link XmlPullParser#next()}
* @throws IOException Thrown by {@link XmlPullParser#next()}
*/
@Nullable
public static IDrawableParser newXmlDrawableParser (String xmlDrawable, IResourceTable resourceFinder, DisplayMetrics displayMetrics) throws XmlPullParserException, IOException {
@@ -106,33 +106,45 @@ public abstract class DrawableParserFactory {
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;
}
return parserForTag (xmlDrawable, resourceFinder, displayMetrics, parser, name);
}
event = parser.next ();
}
// TODO Implement parsers for these root tags if possible
// 1. <bitmap>
// 2. <nine-patch>
// 2. <nine-patch>------ DONE
// 3. <layer-list>
// 4. <selector>
// 5. <level-list>
// 6. <transition>
// 7. <inset>
// 7. <inset>----------- DONE
// 8. <clip>
// 9. <scale>
// We don't support parsing of this type of drawable
// Cannot parse this type of drawable
return null;
}
@Nullable
public static IDrawableParser parserForTag (String xmlDrawable, IResourceTable resourceFinder, DisplayMetrics displayMetrics, XmlPullParser parser, @NonNull String name) throws XmlPullParserException {
switch (name) {
case "shape":
return new ShapeDrawableParser (parser, resourceFinder, displayMetrics, IDrawableParser.ANY_DEPTH);
case "inset":
return new InsetDrawableParser (parser, resourceFinder, displayMetrics, IDrawableParser.ANY_DEPTH);
case "layer-list":
return new LayerListParser (parser, resourceFinder, displayMetrics, IDrawableParser.ANY_DEPTH);
case "vector":
if (xmlDrawable != null) {
return new NoParser (VectorMasterDrawable.fromXML (xmlDrawable));
}
return null;
default:
return null;
}
}
/**
* Instead of parsing anything, this returns the provided drawable.
*/
@@ -141,10 +153,10 @@ public abstract class DrawableParserFactory {
private final Drawable parsed;
protected NoParser (final Drawable parsed) {
super (null, null, null);
super (null, null, null, ANY_DEPTH);
this.parsed = parsed;
}
@Override
public Drawable parse () {
return parsed;
@@ -33,15 +33,44 @@ import org.xmlpull.v1.XmlPullParser;
*/
public abstract class IDrawableParser extends CommonParseUtils {
protected final XmlPullParser parser;
public static final int ANY_DEPTH = -1;
protected IDrawableParser (XmlPullParser parser, IResourceTable resourceFinder, DisplayMetrics displayMetrics) {
super(resourceFinder, displayMetrics);
protected final XmlPullParser parser;
protected int minDepth;
protected IDrawableParser (XmlPullParser parser, IResourceTable resourceFinder, DisplayMetrics displayMetrics, int minDepth) {
super (resourceFinder, displayMetrics);
this.parser = parser;
this.minDepth = minDepth;
}
/**
* Parse the drawable using the already provided parser and data.
*
* @return The parsed {@link Drawable} or <code>null</code> if the parse was unsuccessful.
* @throws Exception If any fatal error occurred while parsing the drawable.
*/
public abstract Drawable parse () throws Exception;
/**
* Set the minimum depth that this parser should keep parsing.
* Subclasses are expected to parse child tags with depths greater
* than this specified value.
*
* @param minDepth The minimum depth of this parser.
* @return <code>this</code> instance for chained method calls.
*/
public IDrawableParser setMinDepth (int minDepth) {
this.minDepth = minDepth;
return this;
}
/**
* Find the index of the attribute with the given name.
*
* @param name The name of the attribute to look for.
* @return The index of the attribute or <code>-1</code>.
*/
protected int attrIndex (final String name) {
for (int i = 0; i < this.parser.getAttributeCount (); i++) {
if (this.parser.getAttributeName (i).equals (name)) {
@@ -52,23 +81,45 @@ public abstract class IDrawableParser extends CommonParseUtils {
return -1;
}
/**
* Get the value of the attribute at the given index.
*
* @param index The index of the attribute.
* @return The value of the attribute.
*/
protected String value (int index) {
return parser.getAttributeValue (index);
}
protected boolean isApi26() {
/**
* Checks if the drawable parser is allowed to parse at the current depth.
* This must be checked if the drawable parser keeps looking for {@link XmlPullParser#START_TAG}
* or {@link XmlPullParser#END_TAG}.
* <p>
* The {@link LayerListParser} can contain tags which are expected to be parsed by other parsers. So,
* if the nesting parser does not check if this method if <code>true</code>, it might consume all
* the events and the {@link LayerListParser} will have invalid data to parse. In some cases, the nested
* parser might consume all the events until {@link XmlPullParser#END_DOCUMENT}.
*
* @return <code>true</code> if the parser can keep parsing, <code>false</code> otherwise.
*/
protected boolean canParse () {
return this.parser.getDepth () >= minDepth;
}
protected boolean isApi26 () {
return Build.VERSION.SDK_INT >= 26;
}
protected boolean isApi28() {
protected boolean isApi28 () {
return Build.VERSION.SDK_INT >= 28;
}
protected boolean isApi29() {
protected boolean isApi29 () {
return Build.VERSION.SDK_INT >= 29;
}
protected boolean isApi30() {
protected boolean isApi30 () {
return Build.VERSION.SDK_INT >= 30;
}
}
@@ -35,8 +35,8 @@ import org.xmlpull.v1.XmlPullParser;
*/
public class InsetDrawableParser extends IDrawableParser {
protected InsetDrawableParser (XmlPullParser parser, IResourceTable resourceFinder, DisplayMetrics displayMetrics) {
super (parser, resourceFinder, displayMetrics);
protected InsetDrawableParser (XmlPullParser parser, IResourceTable resourceFinder, DisplayMetrics displayMetrics, int minDepth) {
super (parser, resourceFinder, displayMetrics, minDepth);
}
@Override
@@ -0,0 +1,131 @@
/*
* 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 static com.itsaky.inflater.util.Preconditions.assertNotBlank;
import android.graphics.drawable.Drawable;
import android.graphics.drawable.LayerDrawable;
import android.util.DisplayMetrics;
import androidx.annotation.NonNull;
import com.itsaky.androidide.app.BaseApplication;
import com.itsaky.inflater.IResourceTable;
import com.itsaky.inflater.util.TextDrawable;
import org.jetbrains.annotations.Contract;
import org.xmlpull.v1.XmlPullParser;
/**
* Parser for parsing &lt;layer-list&gt; drawables.
*
* @author Akash Yadav
*/
public class LayerListParser extends IDrawableParser {
protected LayerListParser (XmlPullParser parser, IResourceTable resourceFinder, DisplayMetrics displayMetrics, int minDepth) {
super (parser, resourceFinder, displayMetrics, minDepth);
}
@Override
public Drawable parse () throws Exception {
final var layer = new LayerDrawable (new Drawable[0]);
var index = -1;
var value = "";
var event = parser.getEventType ();
int left = 0, top = 0, right = 0, bottom = 0;
var parseInner = true;
while (event != XmlPullParser.END_DOCUMENT && canParse ()) {
if (event == XmlPullParser.START_TAG) {
final var name = parser.getName ();
if ("item".equals (name)) {
index = attrIndex ("drawable");
Drawable drawable = null;
if (index != -1) {
value = value (index);
assertNotBlank (value, "Invalid value for android:drawable attribute");
drawable = parseDrawable (value, BaseApplication.getBaseInstance ());
// if the android:drawable is specified, we do not parse the inner elements of <item>
parseInner = false;
}
index = attrIndex ("left");
left = index == -1 ? 0 : parseDimension (value (index), 0);
index = attrIndex ("top");
top = index == -1 ? 0 : parseDimension (value (index), 0);
index = attrIndex ("right");
right = index == -1 ? 0 : parseDimension (value (index), 0);
index = attrIndex ("bottom");
bottom = index == -1 ? 0 : parseDimension (value (index), 0);
if (drawable == null) {
parseInner = true;
} else {
addLayer (layer, drawable, left, top, right, bottom);
}
if (!parseInner) {
skipToEndTag (this.parser.getDepth ());
}
} else if (parser.getDepth () > 2 && parseInner) {
// depth 1 = <layer-list>
// depth 2 = <item>
// depth > 2 = child drawables inside <item> tag
final var parser = DrawableParserFactory.parserForTag (null, resourceFinder, displayMetrics, this.parser, name);
if (parser != null) {
parser.setMinDepth (this.parser.getDepth ());
var d = parser.parse ();
if (d == null) {
d = unsupported ();
}
addLayer (layer, d, left, top, right, bottom);
} else {
addLayer (layer, unsupported (), left, top, right, bottom);
}
}
}
event = parser.next ();
}
return layer;
}
private void addLayer (LayerDrawable layer, Drawable drawable, int left, int top, int right, int bottom) {
final int index = layer.addLayer (drawable);
layer.setLayerInset (index, left, top, right, bottom);
}
private void skipToEndTag (int depth) throws Exception {
for (var event = parser.getEventType (); event != XmlPullParser.END_DOCUMENT; event = parser.next ()) {
if (event == XmlPullParser.END_TAG && depth == parser.getDepth ()) {
break;
}
}
}
@NonNull
@Contract(" -> new")
private Drawable unsupported () {
return new TextDrawable ("Unsupported drawable", displayMetrics);
}
}
@@ -44,14 +44,14 @@ import java.util.Arrays;
*/
public class ShapeDrawableParser extends IDrawableParser {
protected ShapeDrawableParser (XmlPullParser parser, IResourceTable resourceFinder, DisplayMetrics displayMetrics) {
super (parser, resourceFinder, displayMetrics);
protected ShapeDrawableParser (XmlPullParser parser, IResourceTable resourceFinder, DisplayMetrics displayMetrics, int minDepth) {
super (parser, resourceFinder, displayMetrics, minDepth);
}
@Nullable
@Override
public Drawable parse () throws Exception {
final var drawable = new GradientDrawable ();
var index = attrIndex ("shape");
@@ -66,7 +66,7 @@ public class ShapeDrawableParser extends IDrawableParser {
index = attrIndex ("thickness");
drawable.setThickness (index == -1 ? 0 : parseDimension (value (index), 0));
index = attrIndex ("thicknessRatio");
drawable.setThicknessRatio (index == -1 ? 1f : Float.parseFloat (value (index)));
}
@@ -75,7 +75,7 @@ public class ShapeDrawableParser extends IDrawableParser {
drawable.setUseLevel (index != -1 && parseBoolean (value (index)));
var event = parser.getEventType ();
while (event != XmlPullParser.END_DOCUMENT) {
while (event != XmlPullParser.END_DOCUMENT && canParse ()) {
final var name = parser.getName ();
if (event == XmlPullParser.START_TAG) {
switch (name) {
@@ -85,16 +85,16 @@ public class ShapeDrawableParser extends IDrawableParser {
case "gradient":
parseGradient (drawable);
break;
case "padding" :
case "padding":
parsePadding (drawable);
break;
case "size" :
case "size":
parseSize (drawable);
break;
case "solid" :
case "solid":
parseSolid (drawable);
break;
case "stroke" :
case "stroke":
parseStroke (drawable);
break;
}
@@ -108,7 +108,7 @@ public class ShapeDrawableParser extends IDrawableParser {
private void parseStroke (@NonNull final GradientDrawable drawable) {
var index = attrIndex ("width");
var strokeWidth = 0;
if (index != - 1) {
if (index != -1) {
drawable.setStroke (strokeWidth = parseDimension (value (index), 0), Color.TRANSPARENT);
}
@@ -158,7 +158,7 @@ public class ShapeDrawableParser extends IDrawableParser {
final var rect = new Rect ();
if (!drawable.getPadding (rect)) {
rect.set (0,0,0,0);
rect.set (0, 0, 0, 0);
}
var changed = false;
@@ -167,19 +167,19 @@ public class ShapeDrawableParser extends IDrawableParser {
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);
@@ -196,26 +196,26 @@ public class ShapeDrawableParser extends IDrawableParser {
drawable.setOrientation (index == -1 ? GradientDrawable.Orientation.LEFT_RIGHT : parseGradientOrientation (value (index)));
index = attrIndex ("centerX");
var center = index == - 1 ? drawable.getGradientCenterX () : Float.parseFloat (value (index));
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));
center = index == -1 ? drawable.getGradientCenterX () : Float.parseFloat (value (index));
drawable.setGradientCenter (drawable.getGradientCenterX (), center);
final var colors = new int [3];
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 ());
colors[1] = parseColor (value (index), appContext ());
changed = true;
}
index = attrIndex ("endColor");
if (index != -1) {
colors [2] = parseColor (value (index), appContext ());
colors[2] = parseColor (value (index), appContext ());
changed = true;
}
@@ -241,11 +241,11 @@ public class ShapeDrawableParser extends IDrawableParser {
private int parseGradientType (String value) {
switch (value) {
case "radial" :
case "radial":
return GradientDrawable.RADIAL_GRADIENT;
case "sweep" :
case "sweep":
return GradientDrawable.SWEEP_GRADIENT;
case "linear" :
case "linear":
default:
return GradientDrawable.LINEAR_GRADIENT;
}
@@ -307,14 +307,14 @@ public class ShapeDrawableParser extends IDrawableParser {
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");
@@ -331,13 +331,13 @@ public class ShapeDrawableParser extends IDrawableParser {
@Contract(pure = true)
private int parseShape (@NonNull String value) {
switch (value) {
case "oval" :
case "oval":
return GradientDrawable.OVAL;
case "line":
return GradientDrawable.LINE;
case "ring" :
case "ring":
return GradientDrawable.RING;
case "rectangle" :
case "rectangle":
default:
return GradientDrawable.RECTANGLE;
}
@@ -0,0 +1,66 @@
package com.itsaky.inflater.util;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.ColorFilter;
import android.graphics.Paint;
import android.graphics.Paint.Align;
import android.graphics.Rect;
import android.graphics.drawable.Drawable;
import android.util.DisplayMetrics;
import android.util.TypedValue;
import androidx.annotation.NonNull;
public class TextDrawable extends Drawable {
private static final int DEFAULT_COLOR = Color.WHITE;
private static final int DEFAULT_TEXT_SIZE = 15;
private final Paint mPaint;
private final CharSequence mText;
private final int mIntrinsicWidth;
private final int mIntrinsicHeight;
public TextDrawable (CharSequence text, @NonNull DisplayMetrics displayMetrics) {
mText = text;
mPaint = new Paint (Paint.ANTI_ALIAS_FLAG);
mPaint.setColor (DEFAULT_COLOR);
mPaint.setTextAlign (Align.CENTER);
float textSize = TypedValue.applyDimension (TypedValue.COMPLEX_UNIT_SP,
DEFAULT_TEXT_SIZE, displayMetrics);
mPaint.setTextSize (textSize);
mIntrinsicWidth = (int) (mPaint.measureText (mText, 0, mText.length ()) + .5);
mIntrinsicHeight = mPaint.getFontMetricsInt (null);
}
@Override
public void draw (Canvas canvas) {
Rect bounds = getBounds ();
canvas.drawText (mText, 0, mText.length (),
bounds.centerX (), bounds.centerY (), mPaint);
}
@Override
public int getOpacity () {
return mPaint.getAlpha ();
}
@Override
public int getIntrinsicWidth () {
return mIntrinsicWidth;
}
@Override
public int getIntrinsicHeight () {
return mIntrinsicHeight;
}
@Override
public void setAlpha (int alpha) {
mPaint.setAlpha (alpha);
}
@Override
public void setColorFilter (ColorFilter filter) {
mPaint.setColorFilter (filter);
}
}