/* * Copyright 2016 Flipkart Internet Pvt. Ltd. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.flipkart.android.proteus.processor; import android.content.Context; import android.content.res.ColorStateList; import android.content.res.Resources; import android.content.res.TypedArray; import android.graphics.Color; import android.graphics.drawable.ColorDrawable; import android.graphics.drawable.Drawable; import android.graphics.drawable.GradientDrawable; import android.graphics.drawable.LayerDrawable; import android.graphics.drawable.LevelListDrawable; import android.graphics.drawable.RippleDrawable; import android.graphics.drawable.StateListDrawable; import android.os.Build; import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.text.TextUtils; import android.util.Log; import android.util.Pair; import android.view.View; import android.webkit.URLUtil; import android.webkit.ValueCallback; import com.flipkart.android.proteus.parser.ParseHelper; import com.flipkart.android.proteus.toolbox.ColorUtils; import com.flipkart.android.proteus.toolbox.NetworkDrawableHelper; import com.flipkart.android.proteus.toolbox.ProteusConstants; import com.flipkart.android.proteus.view.ProteusView; import com.flipkart.android.proteus.view.manager.ProteusViewManager; import com.google.gson.Gson; import com.google.gson.JsonArray; import com.google.gson.JsonElement; import com.google.gson.JsonObject; import com.google.gson.annotations.SerializedName; import java.util.ArrayList; import java.util.List; /** * Use this as the base processor for references like @drawable or remote resources with http:// urls. */ public abstract class DrawableResourceProcessor<V extends View> extends AttributeProcessor<V> { private static final String TAG = "DrawableResource"; private static final String DRAWABLE_RIPPLE = "ripple"; private static final String DRAWABLE_SELECTOR = "selector"; private static final String DRAWABLE_SHAPE = "shape"; private static final String DRAWABLE_LAYER_LIST = "layer-list"; private static final String DRAWABLE_LEVEL_LIST = "level-list"; private static final String TYPE = "type"; private static final String CHILDREN = "children"; private static final String TYPE_CORNERS = "corners"; private static final String TYPE_GRADIENT = "gradient"; private static final String TYPE_PADDING = "padding"; private static final String TYPE_SIZE = "size"; private static final String TYPE_SOLID = "solid"; private static final String TYPE_STROKE = "stroke"; private static final String SHAPE_RECTANGLE = "rectangle"; private static final String SHAPE_OVAL = "oval"; private static final String SHAPE_LINE = "line"; private static final String SHAPE_RING = "ring"; private static final String LINEAR_GRADIENT = "linear"; private static final String RADIAL_GRADIENT = "radial"; private static final String SWEEP_GRADIENT = "sweep"; private static Gson sGson = new Gson(); public static GradientDrawable loadGradientDrawable(Context context, JsonObject value) { ShapeDrawableJson shapeDrawable = sGson.fromJson(value, ShapeDrawableJson.class); return shapeDrawable.init(context); } private static Drawable loadRippleDrawable(@NonNull Context context, @NonNull JsonObject value, @NonNull String attributeKey, @NonNull View view) { RippleDrawableJson rippleDrawable = sGson.fromJson(value, RippleDrawableJson.class); return rippleDrawable.init(context, attributeKey, view); } @Override public void handle(String key, JsonElement value, V view) { if (value.isJsonPrimitive()) { handleString(key, value.getAsString(), view); } else if (value.isJsonObject()) { handleElement(key, value, view); } else { if (ProteusConstants.isLoggingEnabled()) { Log.e(TAG, "Resource for key: " + key + " must be a primitive or an object. value -> " + value.toString()); } } } /** * This block handles different drawables. * Selector and LayerListDrawable are handled here. * Override this to handle more types of drawables * * @param attributeKey * @param attributeValue * @param view */ protected void handleElement(String attributeKey, JsonElement attributeValue, V view) { JsonObject jsonObject = attributeValue.getAsJsonObject(); JsonElement type = jsonObject.get(TYPE); String drawableType = type.getAsString(); JsonElement childrenElement = null; switch (drawableType) { case DRAWABLE_SELECTOR: final StateListDrawable stateListDrawable = new StateListDrawable(); childrenElement = jsonObject.get(CHILDREN); if (childrenElement != null) { JsonArray children = childrenElement.getAsJsonArray(); for (JsonElement childElement : children) { JsonObject child = childElement.getAsJsonObject(); final Pair<int[], JsonElement> state = ParseHelper.parseState(child); if (state != null) { DrawableResourceProcessor<V> processor = new DrawableResourceProcessor<V>() { @Override public void setDrawable(V view, Drawable drawable) { stateListDrawable.addState(state.first, drawable); } }; processor.handle(attributeKey, state.second, view); } } } setDrawable(view, stateListDrawable); break; case DRAWABLE_SHAPE: GradientDrawable gradientDrawable = loadGradientDrawable(view.getContext(), jsonObject); if (null != gradientDrawable) { setDrawable(view, gradientDrawable); } break; case DRAWABLE_LAYER_LIST: final List<Pair<Integer, Drawable>> drawables = new ArrayList<>(); childrenElement = jsonObject.get(CHILDREN); if (childrenElement != null) { JsonArray children = childrenElement.getAsJsonArray(); for (JsonElement childElement : children) { JsonObject child = childElement.getAsJsonObject(); final Pair<Integer, JsonElement> layerPair = ParseHelper.parseLayer(child); if (null != layerPair) { DrawableResourceProcessor<V> processor = new DrawableResourceProcessor<V>() { @Override public void setDrawable(V view, Drawable drawable) { drawables.add(new Pair<>(layerPair.first, drawable)); onLayerDrawableFinish(view, drawables); } }; processor.handle(attributeKey, layerPair.second, view); } } } break; case DRAWABLE_LEVEL_LIST: final LevelListDrawable levelListDrawable = new LevelListDrawable(); childrenElement = jsonObject.get(CHILDREN); if (childrenElement != null) { JsonArray children = childrenElement.getAsJsonArray(); for (JsonElement childElement : children) { LayerListDrawableItem layerListDrawableItem = sGson.fromJson(childElement, LayerListDrawableItem.class); layerListDrawableItem.addItem(view.getContext(), levelListDrawable); } } break; case DRAWABLE_RIPPLE: Drawable rippleDrawable = loadRippleDrawable(view.getContext(), jsonObject, attributeKey, view); if (null != rippleDrawable) { setDrawable(view, rippleDrawable); } break; } } private void onLayerDrawableFinish(V view, List<Pair<Integer, Drawable>> drawables) { Drawable[] drawableContainer = new Drawable[drawables.size()]; // iterate and create an array of drawables to be used for the constructor for (int i = 0; i < drawables.size(); i++) { Pair<Integer, Drawable> drawable = drawables.get(i); drawableContainer[i] = drawable.second; } // put them in the constructor LayerDrawable layerDrawable = new LayerDrawable(drawableContainer); // we could have avoided the following loop if layer drawable has a method to add drawable and set id at same time for (int i = 0; i < drawables.size(); i++) { Pair<Integer, Drawable> drawable = drawables.get(i); layerDrawable.setId(i, drawable.first); drawableContainer[i] = drawable.second; } setDrawable(view, layerDrawable); } /** * Any string based drawables are handled here. Color, local resource and remote image urls. * * @param attributeKey * @param attributeValue * @param view */ protected void handleString(String attributeKey, final String attributeValue, final V view) { ProteusViewManager viewManager = ((ProteusView) view).getViewManager(); boolean synchronousRendering = viewManager.getLayoutBuilder().isSynchronousRendering(); if (ParseHelper.isLocalResourceAttribute(attributeValue)) { int attributeId = ParseHelper.getAttributeId(view.getContext(), attributeValue); if (0 != attributeId) { TypedArray ta = view.getContext().obtainStyledAttributes(new int[]{attributeId}); Drawable drawable = ta.getDrawable(0 /* index */); ta.recycle(); setDrawable(view, drawable); } } else if (ParseHelper.isColor(attributeValue)) { setDrawable(view, new ColorDrawable(ParseHelper.parseColor(attributeValue))); } else if (ParseHelper.isLocalDrawableResource(attributeValue)) { try { Resources r = view.getContext().getResources(); int drawableId = r.getIdentifier(attributeValue, "drawable", view.getContext().getPackageName()); Drawable drawable = r.getDrawable(drawableId); setDrawable(view, drawable); } catch (Exception ex) { System.out.println("Could not load local resource " + attributeValue); } } else if (URLUtil.isValidUrl(attributeValue)) { NetworkDrawableHelper.DrawableCallback callback = new NetworkDrawableHelper.DrawableCallback() { @Override public void onDrawableLoad(String url, final Drawable drawable) { setDrawable(view, drawable); } @Override public void onDrawableError(String url, String reason, Drawable errorDrawable) { System.out.println("Could not load " + url + " : " + reason); if (errorDrawable != null) { setDrawable(view, errorDrawable); } } }; new NetworkDrawableHelper(view, attributeValue, synchronousRendering, callback, viewManager.getLayoutBuilder().getNetworkDrawableHelper(), viewManager.getLayout()); } } public abstract void setDrawable(V view, Drawable drawable); private abstract static class GradientDrawableElement { private int mTempColor = 0; public abstract void apply(Context context, GradientDrawable gradientDrawable); protected int loadColor(Context context, JsonElement colorValue) { mTempColor = 0; if (null != colorValue && !colorValue.isJsonNull()) { ColorUtils.loadColor(context, colorValue, new ValueCallback<Integer>() { @Override public void onReceiveValue(Integer value) { mTempColor = value; } }, new ValueCallback<ColorStateList>() { @Override public void onReceiveValue(ColorStateList value) { } }); } return mTempColor; } } private static class Corners extends GradientDrawableElement { @SerializedName("radius") public String radius; @SerializedName("topLeftRadius") public String topLeftRadius; @SerializedName("topRightRadius") public String topRightRadius; @SerializedName("bottomLeftRadius") public String bottomLeftRadius; @SerializedName("bottomRightRadius") public String bottomRightRadius; @Override public void apply(Context context, GradientDrawable gradientDrawable) { if (!TextUtils.isEmpty(radius)) { gradientDrawable.setCornerRadius(ParseHelper.parseDimension(radius, context)); } float fTopLeftRadius = TextUtils.isEmpty(topLeftRadius) ? 0 : ParseHelper.parseDimension(topLeftRadius, context); float fTopRightRadius = TextUtils.isEmpty(topRightRadius) ? 0 : ParseHelper.parseDimension(topRightRadius, context); float fBottomRightRadius = TextUtils.isEmpty(bottomRightRadius) ? 0 : ParseHelper.parseDimension(bottomRightRadius, context); float fBottomLeftRadius = TextUtils.isEmpty(bottomLeftRadius) ? 0 : ParseHelper.parseDimension(bottomLeftRadius, context); if (fTopLeftRadius != 0 || fTopRightRadius != 0 || fBottomRightRadius != 0 || fBottomLeftRadius != 0) { // The corner radii are specified in clockwise order (see Path.addRoundRect()) gradientDrawable.setCornerRadii(new float[]{ fTopLeftRadius, fTopLeftRadius, fTopRightRadius, fTopRightRadius, fBottomRightRadius, fBottomRightRadius, fBottomLeftRadius, fBottomLeftRadius }); } } } private static class Solid extends GradientDrawableElement { @SerializedName("color") public JsonElement color; @Override public void apply(Context context, final GradientDrawable gradientDrawable) { ColorUtils.loadColor(context, color, new ValueCallback<Integer>() { /** * Invoked when the value is available. * * @param value The value. */ @Override public void onReceiveValue(Integer value) { gradientDrawable.setColor(value); } }, new ValueCallback<ColorStateList>() { /** * Invoked when the value is available. * * @param value The value. */ @Override public void onReceiveValue(ColorStateList value) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { gradientDrawable.setColor(value); } } }); } } private static class Gradient extends GradientDrawableElement { @SerializedName("angle") public Integer angle; @SerializedName("centerX") public Float centerX; @SerializedName("centerY") public Float centerY; @SerializedName("centerColor") public JsonElement centerColor; @SerializedName("endColor") public JsonElement endColor; @SerializedName("gradientRadius") public Float gradientRadius; @SerializedName("startColor") public JsonElement startColor; @SerializedName("gradientType") public String gradientType; @SerializedName("useLevel") public Boolean useLevel; public static GradientDrawable.Orientation getOrientation(Integer angle) { GradientDrawable.Orientation orientation = GradientDrawable.Orientation.LEFT_RIGHT; if (null != angle) { angle %= 360; if (angle % 45 == 0) { switch (angle) { case 0: orientation = GradientDrawable.Orientation.LEFT_RIGHT; break; case 45: orientation = GradientDrawable.Orientation.BL_TR; break; case 90: orientation = GradientDrawable.Orientation.BOTTOM_TOP; break; case 135: orientation = GradientDrawable.Orientation.BR_TL; break; case 180: orientation = GradientDrawable.Orientation.RIGHT_LEFT; break; case 225: orientation = GradientDrawable.Orientation.TR_BL; break; case 270: orientation = GradientDrawable.Orientation.TOP_BOTTOM; break; case 315: orientation = GradientDrawable.Orientation.TL_BR; break; } } } return orientation; } public static GradientDrawable init(@Nullable int[] colors, @Nullable Integer angle) { return colors != null ? new GradientDrawable(getOrientation(angle), colors) : new GradientDrawable(); } @Override public void apply(Context context, GradientDrawable gradientDrawable) { if (null != centerX && null != centerY) { gradientDrawable.setGradientCenter(centerX, centerY); } if (null != gradientRadius) { gradientDrawable.setGradientRadius(gradientRadius); } if (!TextUtils.isEmpty(gradientType)) { switch (gradientType) { case LINEAR_GRADIENT: gradientDrawable.setGradientType(GradientDrawable.LINEAR_GRADIENT); break; case RADIAL_GRADIENT: gradientDrawable.setGradientType(GradientDrawable.RADIAL_GRADIENT); break; case SWEEP_GRADIENT: gradientDrawable.setGradientType(GradientDrawable.SWEEP_GRADIENT); break; } } } public GradientDrawable init(Context context) { int[] colors = null; if (centerColor != null) { colors = new int[3]; colors[0] = loadColor(context, startColor); colors[1] = loadColor(context, centerColor); colors[2] = loadColor(context, endColor); } else { colors = new int[2]; colors[0] = loadColor(context, startColor); colors[1] = loadColor(context, endColor); } return init(colors, angle); } } private static class Size extends GradientDrawableElement { @SerializedName("width") public String width; @SerializedName("height") public String height; @Override public void apply(Context context, GradientDrawable gradientDrawable) { gradientDrawable.setSize((int) ParseHelper.parseDimension(width, context), (int) ParseHelper.parseDimension(height, context)); } } private static class Stroke extends GradientDrawableElement { @SerializedName("width") public String width; @SerializedName("color") public JsonElement color; @SerializedName("dashWidth") public String dashWidth; @SerializedName("dashGap") public String dashGap; @Override public void apply(Context context, GradientDrawable gradientDrawable) { if (null == dashWidth) { gradientDrawable.setStroke((int) ParseHelper.parseDimension(width, context), loadColor(context, color)); } else if (null != dashWidth) { gradientDrawable.setStroke((int) ParseHelper.parseDimension(width, context), loadColor(context, color), ParseHelper.parseDimension(dashWidth, context), ParseHelper.parseDimension(dashGap, context)); } } } private static class LayerListDrawableItem { @SerializedName("minLevel") public Integer minLevel; @SerializedName("maxLevel") public Integer maxLevel; @SerializedName("drawable") public JsonElement drawable; public void addItem(Context context, final LevelListDrawable levelListDrawable) { DrawableResourceProcessor<View> processor = new DrawableResourceProcessor<View>() { @Override public void setDrawable(View view, Drawable drawable) { levelListDrawable.addLevel(minLevel, maxLevel, drawable); } }; } } private static class ShapeDrawableJson { @SerializedName("shape") public String shape; @SerializedName("innerRadius") public String innerRadius; @SerializedName("innerRadiusRatio") public Float innerRadiusRatio; @SerializedName("thickness") public String thickness; @SerializedName("thicknessRatio") public Float thicknessRatio; @SerializedName("children") public JsonArray children; public GradientDrawable init(Context context) { ArrayList<GradientDrawableElement> elements = null; Gradient gradient = null; if (children != null && children.size() > 0) { elements = new ArrayList<>(children.size()); for (JsonElement jsonElement : children) { if (jsonElement.isJsonObject()) { String typeKey = jsonElement.getAsJsonObject().getAsJsonPrimitive(TYPE).getAsString(); GradientDrawableElement element = null; switch (typeKey) { case TYPE_CORNERS: element = sGson.fromJson(jsonElement, Corners.class); break; case TYPE_PADDING: break; case TYPE_SIZE: element = sGson.fromJson(jsonElement, Size.class); break; case TYPE_SOLID: element = sGson.fromJson(jsonElement, Solid.class); break; case TYPE_STROKE: element = sGson.fromJson(jsonElement, Stroke.class); break; case TYPE_GRADIENT: gradient = sGson.fromJson(jsonElement, Gradient.class); element = gradient; break; } if (null != element) { elements.add(element); } } } } GradientDrawable gradientDrawable = (null != gradient) ? gradient.init(context) : new GradientDrawable(); if (!TextUtils.isEmpty(shape)) { int shapeInt = -1; switch (shape) { case SHAPE_RECTANGLE: shapeInt = GradientDrawable.RECTANGLE; break; case SHAPE_OVAL: shapeInt = GradientDrawable.OVAL; break; case SHAPE_LINE: shapeInt = GradientDrawable.LINE; break; case SHAPE_RING: shapeInt = GradientDrawable.RING; break; } if (-1 != shapeInt) { gradientDrawable.setShape(shapeInt); } } if (null != elements) { for (GradientDrawableElement element : elements) { element.apply(context, gradientDrawable); } } return gradientDrawable; } } private static class RippleDrawableJson { @SerializedName("color") @NonNull public JsonElement color; @SerializedName("mask") @Nullable public JsonElement mask; @SerializedName("content") @Nullable public JsonElement content; @SerializedName("defaultBackground") @Nullable public JsonElement defaultBackground; private transient ColorStateList colorStateList = null; private transient Drawable contentDrawable = null; private transient Drawable maskDrawable = null; private transient Drawable defaultBackgroundDrawable = null; @Nullable Drawable init(@NonNull Context context, @NonNull String attributeKey, @NonNull View view) { Drawable resultDrawable = null; ColorUtils.loadColor(context, color, new ValueCallback<Integer>() { @Override public void onReceiveValue(Integer value) { int[][] states = new int[][] { new int[] {} }; int[] colors = new int[] { value }; colorStateList = new ColorStateList(states, colors); } }, new ValueCallback<ColorStateList>() { @Override public void onReceiveValue(ColorStateList value) { colorStateList = value; } }); if (null != content) { DrawableResourceProcessor contentDrawableProcessor = new DrawableResourceProcessor() { @Override public void setDrawable(View view, Drawable drawable) { contentDrawable = drawable; } }; contentDrawableProcessor.handle(attributeKey, content, view); } if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP && null != colorStateList) { if (null != mask) { DrawableResourceProcessor maskDrawableProcessor = new DrawableResourceProcessor() { @Override public void setDrawable(View view, Drawable drawable) { maskDrawable = drawable; } }; maskDrawableProcessor.handle(attributeKey, mask, view); } resultDrawable = new RippleDrawable(colorStateList, contentDrawable, maskDrawable); } else if (null != defaultBackground) { DrawableResourceProcessor defaultDrawableProcessor = new DrawableResourceProcessor() { @Override public void setDrawable(View view, Drawable drawable) { defaultBackgroundDrawable = drawable; } }; defaultDrawableProcessor.handle(attributeKey, defaultBackground, view); resultDrawable = defaultBackgroundDrawable; } else if (null != colorStateList && contentDrawable != null) { int pressedColor = colorStateList.getColorForState(new int[]{android.R.attr.state_pressed}, colorStateList.getDefaultColor()); int focussedColor = colorStateList.getColorForState(new int[]{android.R.attr.state_focused}, pressedColor); ColorDrawable pressedColorDrawable = new ColorDrawable(pressedColor); ColorDrawable focussedColorDrawable = new ColorDrawable(focussedColor); StateListDrawable stateListDrawable = new StateListDrawable(); stateListDrawable.addState(new int[]{android.R.attr.state_enabled, android.R.attr.state_pressed}, pressedColorDrawable); stateListDrawable.addState(new int[]{android.R.attr.state_enabled, android.R.attr.state_focused}, focussedColorDrawable); stateListDrawable.addState(new int[]{android.R.attr.state_enabled}, contentDrawable); resultDrawable = stateListDrawable; } return resultDrawable; } } }