/******************************************************************************* * Copyright 2012-present Pixate, Inc. * * 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.pixate.freestyle.util; import java.lang.reflect.Method; import java.util.Arrays; import java.util.Collections; import java.util.HashMap; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Set; import android.annotation.TargetApi; import android.graphics.Bitmap; import android.graphics.RectF; import android.graphics.drawable.BitmapDrawable; import android.graphics.drawable.ColorDrawable; import android.graphics.drawable.Drawable; import android.graphics.drawable.DrawableContainer; import android.graphics.drawable.DrawableContainer.DrawableContainerState; import android.graphics.drawable.LayerDrawable; import android.graphics.drawable.StateListDrawable; import android.os.Build; import android.view.View; import android.widget.ImageView; import com.pixate.freestyle.PixateFreestyle; import com.pixate.freestyle.cg.paints.PXPaint; import com.pixate.freestyle.cg.shapes.PXRectangle; import com.pixate.freestyle.styling.PXRuleSet; import com.pixate.freestyle.styling.adapters.PXStyleAdapter; import com.pixate.freestyle.styling.cache.PXStyleInfo; import com.pixate.freestyle.styling.stylers.PXStylerContext; /** * A utility class for {@link Drawable} related functionalities. * * @author Shalom Gibly */ public class PXDrawableUtil { private static String TAG = PXDrawableUtil.class.getSimpleName(); // Holds all the possible Android Drawable state names and values. Note that // the keys that are used in this map will omit the "state_" prefix that // android defines in the name of the attribute. private static final Map<String, Integer> STATES; static { STATES = new HashMap<String, Integer>(); STATES.put("focused", android.R.attr.state_focused); STATES.put("window_focused", android.R.attr.state_window_focused); STATES.put("enabled", android.R.attr.state_enabled); STATES.put("checked", android.R.attr.state_checked); STATES.put("checkable", android.R.attr.state_checkable); STATES.put("selected", android.R.attr.state_selected); STATES.put("active", android.R.attr.state_active); STATES.put("single", android.R.attr.state_single); STATES.put("first", android.R.attr.state_first); STATES.put("middle", android.R.attr.state_middle); STATES.put("last", android.R.attr.state_last); STATES.put("pressed", android.R.attr.state_pressed); STATES.put("activated", android.R.attr.state_activated); STATES.put("above-anchor", android.R.attr.state_above_anchor); STATES.put("multiline", android.R.attr.state_multiline); // note that default is "drawable" STATES.put(PXStyleInfo.DEFAULT_STYLE, android.R.attr.drawable); if (PixateFreestyle.ICS_OR_BETTER) { addICSStates(STATES); } } @TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH) private static void addICSStates(Map<String, Integer> states) { states.put("hovered", android.R.attr.state_hovered); states.put("drag_can_accept", android.R.attr.state_drag_can_accept); states.put("drag_hovered", android.R.attr.state_drag_hovered); } /** * Returns a map of the {@link Drawable} supported states. * <ul> * <li>"state_focused" * <li>"state_window_focused" * <li>"state_enabled" * <li>"state_checked" * <li>"state_checkable" * <li>"state_selected" * <li>"state_active" * <li>"state_single" * <li>"state_first" * <li>"state_mid" * <li>"state_last" * <li>"state_pressed" * <li>"state_activated" * <li>"state_multiline" * <li>"state_above_anchor" * <li>"state_hovered" (ICE_CREAM_SANDWICH+) * <li>"state_drag_can_accept" (ICE_CREAM_SANDWICH+) * <li>"state_drag_hovered" (ICE_CREAM_SANDWICH+) * <li>"drawable" (default) * </ul> * * @return A supported states map that */ public static Map<String, Integer> getSupportedStates() { return new HashMap<String, Integer>(STATES); } /** * Returns the integer state value that is mapped to the given state name. * In case none can be mapped, the method returns {@link Integer#MIN_VALUE}. * * @param stateName * @return The drawable state integer value; {@link Integer#MIN_VALUE} in * case the given state name cannot be matched. */ public static int getStateValue(String stateName) { if (stateName != null && STATES.containsKey(stateName)) { return STATES.get(stateName); } return Integer.MIN_VALUE; } /** * Creates a {@link Drawable} by rendering the {@link PXPaint} into an a * drawable image. * * @param width * @param height * @param paint * @return A new {@link Drawable} for the {@link PXPaint}. */ public static Drawable createDrawable(float width, float height, PXPaint paint) { return createDrawable(new RectF(0, 0, width, height), paint); } /** * Creates a {@link Drawable} by rendering the {@link PXPaint} into an a * drawable image. * * @param bounds * @param paint * @return A new {@link Drawable} for the {@link PXPaint}. */ public static Drawable createDrawable(RectF bounds, PXPaint paint) { PXRectangle rectangle = new PXRectangle(bounds); rectangle.setFillColor(paint); return rectangle.renderToImage(bounds, paint.isOpaque()); } /** * Sets a background {@link Drawable} on a view. In case the call is set to * check for a layer-drawable and there is an existing {@link LayerDrawable} * on the given View, set/replace the layer with the * {@code android.R.id.background} id. * * @param view * @param drawable * @param checkForLayer Indicate if this method should check for a * {@link LayerDrawable} when applying a background. */ public static void setBackgroundDrawable(View view, Drawable drawable, boolean checkForLayer) { Drawable background = view.getBackground(); if (checkForLayer && background instanceof LayerDrawable) { LayerDrawable layeredBG = (LayerDrawable) background; layeredBG.setDrawableByLayerId(android.R.id.background, drawable); layeredBG.invalidateSelf(); } else { setBackgroundDrawable(view, drawable); } } /** * Sets a background {@link Drawable} on a view. * * @param view * @param drawable */ @SuppressWarnings("deprecation") public static void setBackgroundDrawable(View view, Drawable drawable) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) { setBackgroundJB(view, drawable); } else { view.setBackgroundDrawable(drawable); } } @TargetApi(Build.VERSION_CODES.JELLY_BEAN) private static void setBackgroundJB(View view, Drawable drawable) { view.setBackground(drawable); } /** * Sets a {@link Bitmap} background. In case the call is set to check for a * layer-drawable and there is an existing {@link LayerDrawable} on the * given View, set/replace the layer with the * {@code android.R.id.background} id. * * @param view * @param bitmap * @param checkForLayer Indicate if this method should check for a * {@link LayerDrawable} when applying a background. */ public static void setBackground(View view, Bitmap bitmap, boolean checkForLayer) { if (view instanceof ImageView) { ((ImageView) view).setImageBitmap(bitmap); return; } BitmapDrawable newDrawable = new BitmapDrawable(PixateFreestyle.getAppContext() .getResources(), bitmap); Drawable background = view.getBackground(); if (background instanceof ColorDrawable) { // keep the background color so it would show when the bitmap is // transparent LayerDrawable layerDrawable = new LayerDrawable(new Drawable[] { background, newDrawable }); layerDrawable.setId(1, android.R.id.background); setBackgroundDrawable(view, layerDrawable); } else { setBackgroundDrawable(view, newDrawable, checkForLayer); } } /** * Returns a {@link Map} that holds a mapping from the existing state-lists * in the background {@link Drawable}. * * @param background A {@link Drawable} background ( * {@link StateListDrawable}). * @return A {@link Map}. An empty map in case the background * <code>null</code>, or is not a {@link StateListDrawable}. */ public static Map<int[], Drawable> getExistingStates(Drawable background) { LinkedHashMap<int[], Drawable> map = new LinkedHashMap<int[], Drawable>(); if (background instanceof StateListDrawable) { // Grab the existing states. Note that the API hides some of the // public functionality with the @hide tag, so we have to access // those through reflection... StateListDrawable stateList = (StateListDrawable) background; DrawableContainerState containerState = (DrawableContainerState) stateList .getConstantState(); Drawable[] children = containerState.getChildren(); try { // This method is public but hidden ("pending API council") Method method = stateList.getClass().getMethod("getStateSet", int.class); for (int i = 0; i < containerState.getChildCount(); i++) { Object state = method.invoke(stateList, i); if (state instanceof int[]) { map.put((int[]) state, children[i]); } } } catch (Exception e) { PXLog.e(TAG, e, "Error getting the state set"); } } return map; } /** * Check if two Drawables are equal. A regular check for a Drawable equals * just checks for the instance reference, while this check is doing a * deeper equals when dealing with {@link DrawableContainer} instances. In * these cases, the method will run equals on each of the child drawables in * the container (order is importance as well). * * @param d1 * @param d2 * @return <code>true</code> if the drawables are equal, <code>false</code> * otherwise. */ public static boolean isEquals(Drawable d1, Drawable d2) { if (d1 == d2) { return true; } if (d1 == null || d2 == null) { return false; } if (d1 instanceof DrawableContainer && d2 instanceof DrawableContainer) { // Try to match the content of those containers DrawableContainerState containerState1 = (DrawableContainerState) ((DrawableContainer) d1) .getConstantState(); DrawableContainerState containerState2 = (DrawableContainerState) ((DrawableContainer) d2) .getConstantState(); return Arrays.equals(containerState1.getChildren(), containerState2.getChildren()); } return d1.equals(d2); } /** * A utility method that will create a new {@link StateListDrawable} from * the given contexts and the existing {@link View}'s background that is in * that context. * * @param adapter A {@link PXStyleAdapter}. This will be used to create * additional states (see * {@link PXStyleAdapter#createAdditionalDrawableStates(int)} * @param existingStates The states that exist in the {@link View} that is * being styled. Note that in case that the existing states are * empty (or <code>null</code>), there may be a need to call the * {@link #createNewStateListDrawable(PXStyleAdapter, List, List)} * method instead. * @param ruleSets * @param contexts * @return A new {@link StateListDrawable} * @see #createNewStateListDrawable(PXStyleAdapter, List, List) */ public static Drawable createDrawable(PXStyleAdapter adapter, Map<int[], Drawable> existingStates, List<PXRuleSet> ruleSets, List<PXStylerContext> contexts) { PXStylerContext context = contexts.get(0); Set<int[]> statesKeys; if (existingStates != null) { statesKeys = existingStates.keySet(); } else { statesKeys = Collections.emptySet(); } // Will hold the new background. StateListDrawable stateListDrawable = adapter.shouldAdjustDrawableBounds() ? (new StateListDrawableWithBoundsChange()) : (new StateListDrawable()); // For every image we have in the contexts, create a drawable and insert // it into the StateListDrawable. The assumption is that every context // will provide a drawable for a different state. Otherwise, we may end // up with multiple drawables for the same state and Android will pick // the first one it hits. We also want to keep the order of the original // state list, so we do that insertion in two parts. First, we collect // all the states indexes that will be 'replaced', and then we do the // actual construction of the new state-list while weaving in drawables // from the original list and our contexts. Map<Integer, Drawable> newStatesPositions = new LinkedHashMap<Integer, Drawable>(); int rulesetSize = ruleSets.size(); for (int i = 0; i < rulesetSize; i++) { context = contexts.get(i); Drawable drawable = (context.usesImage() || context.usesColorOnly()) ? context .getBackgroundImage() : null; if (drawable != null && existingStates != null && !existingStates.isEmpty()) { String activeStateName = context.getActiveStateName(); if (activeStateName == null) { activeStateName = PXStyleInfo.DEFAULT_STYLE; } // Artificially add states to the one we got. For example, add a // 'pressed' state to a 'checked' state. int[][] activeStates = adapter.createAdditionalDrawableStates(PXDrawableUtil .getStateValue(activeStateName)); // Find the index we would like to insert this state. Remember // it and its newly assigned drawable. int index = 0; for (int[] state : statesKeys) { for (int[] activeState : activeStates) { if (Arrays.equals(state, activeState)) { // we set the new position, intentionally not // breaking out of the loop (last occurrence wins) newStatesPositions.put(index, drawable); } } index++; } } } // At this point we have the indexes in the original state list that we // would like to replace with our drawables, so we loop and create a the // new state list. int index = 0; for (int[] state : statesKeys) { Drawable drawable = newStatesPositions.get(index); if (drawable != null) { // NOTE: This is super important for Spinners and // CheckedTextViews! Update the state list drawable bounds size. if (adapter.shouldAdjustDrawableBounds()) { stateListDrawable.getBounds().union(drawable.getBounds()); } stateListDrawable.addState(state, drawable); } else { // add the existing one. stateListDrawable.addState(state, existingStates.get(state)); } index++; } return stateListDrawable.mutate(); } /** * Creates a new {@link Drawable}. The returned {@link Drawable} can be a * {@link StateListDrawable}, or in case we only have a single context, it * will be a {@link BitmapDrawable}. * * @param adapter * @param ruleSets * @param contexts * @return A new {@link Drawable} * @see #createNewStateListDrawable(PXStyleAdapter, List, List) */ public static Drawable createNewDrawable(PXStyleAdapter adapter, List<PXRuleSet> ruleSets, List<PXStylerContext> contexts) { Drawable drawable; if (contexts.size() == 1 && PXStyleInfo.DEFAULT_STYLE.equals(contexts.get(0).getActiveStateName())) { drawable = contexts.get(0).getBackgroundImage(); } else { drawable = createNewStateListDrawable(adapter, ruleSets, contexts); } return drawable; } /** * Creates a new {@link StateListDrawable} by looking into the contexts and * generating one according to their states. * * @param adapter * @param ruleSets * @param contexts * @return A new {@link StateListDrawable}. <code>null</code> in case the * contexts is <code>null</code> or empty. */ public static StateListDrawable createNewStateListDrawable(PXStyleAdapter adapter, List<PXRuleSet> ruleSets, List<PXStylerContext> contexts) { if (contexts != null && !contexts.isEmpty()) { // No state-lists here, so simply loop and set the backgrounds StateListDrawable stateListDrawable = adapter.shouldAdjustDrawableBounds() ? (new StateListDrawableWithBoundsChange()) : (new StateListDrawable()); int[][] deferredDefaultStates = null; Drawable defaultDrawable = null; int rulesetSize = ruleSets.size(); for (int i = 0; i < rulesetSize; i++) { PXStylerContext context = contexts.get(i); String activeStateName = context.getActiveStateName(); if (activeStateName == null) { activeStateName = PXStyleInfo.DEFAULT_STYLE; } Drawable drawable = (context.usesImage() || context.usesColorOnly()) ? context .getBackgroundImage() : null; // Artificially add states to the one we got. For example, add a // 'pressed' state to a 'checked' state. int stateValue = PXDrawableUtil.getStateValue(activeStateName); if (stateValue == android.R.attr.drawable) { deferredDefaultStates = adapter.createAdditionalDrawableStates(stateValue); defaultDrawable = drawable; } else { int[][] activeStates = adapter.createAdditionalDrawableStates(stateValue); for (int[] activeState : activeStates) { stateListDrawable.addState(activeState, drawable); } } } if (deferredDefaultStates != null && defaultDrawable != null) { // add the defaults at the end of the state list. for (int[] activeState : deferredDefaultStates) { stateListDrawable.addState(activeState, defaultDrawable); } } return stateListDrawable; } return null; } private static class StateListDrawableWithBoundsChange extends StateListDrawable { @Override public void setBounds(int left, int top, int right, int bottom) { super.setBounds(left, top, right, bottom); onBoundsChange(getBounds()); } } }