/******************************************************************************* * 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.styling.adapters; import java.util.ArrayList; import java.util.List; import java.util.Map; import android.graphics.drawable.Drawable; import android.graphics.drawable.LayerDrawable; import android.graphics.drawable.StateListDrawable; import android.view.View; import android.widget.ToggleButton; import com.pixate.freestyle.annotations.PXDocElement; import com.pixate.freestyle.styling.PXRuleSet; import com.pixate.freestyle.styling.stylers.PXStylerContext; import com.pixate.freestyle.styling.virtualStyleables.PXVirtualToggle; import com.pixate.freestyle.util.PXDrawableUtil; /** * A {@link ToggleButton} style adapter. A toggle button adapter can style the * button's background, as we as the button's internal toggle image. Usually, a * user will pick one or the other, however it's possible to layer a * semi-transparent 'toggle' icon. For example: * * <pre> * Style the toggle's background to reflect the selection state. * In this case, the button will still render the second drawable layer * that mapped to android.R.id.toggle (for example, a green bar indicator) * * .toggle { * background-image: url(off.svg); * } * * .toggle:checked { * background-image: url(on.svg); * } * * Style the android.R.id.toggle layer using a virtual 'toggle' child: * * .toggle toggle:off { * background-image: url(off.svg); * background-size: 200px; * } * * .toggle toggle:on { * background-image: url(on.svg); * background-size: 200px; * } * * </pre> * * @author Shalom Gibly */ @PXDocElement public class PXToggleButtonStyleAdapter extends PXCompoundButtonStyleAdapter { private static final String ELEMENT_NAME = "toggle-button"; private static PXToggleButtonStyleAdapter instance; protected PXToggleButtonStyleAdapter() { } /** * Returns an instance of this {@link PXToggleButtonStyleAdapter} */ public static PXToggleButtonStyleAdapter getInstance() { synchronized (PXToggleButtonStyleAdapter.class) { if (instance == null) { instance = new PXToggleButtonStyleAdapter(); } } return instance; } @Override public boolean updateStyle(List<PXRuleSet> ruleSets, List<PXStylerContext> contexts) { if (!super.updateStyle(ruleSets, contexts)) { return false; } PXStylerContext context = contexts.get(0); // Grab the existing states from the ToggleButton. // The ToggleButton returns a LayerDrawable that holds the background // drawable and the toggle drawable. Here, we are only interested in the // button's background, while the toggle will be handled by the virtual // toggle child. View view = (View) context.getStyleable(); Drawable background = view.getBackground(); LayerDrawable layerDrawable = null; if (background instanceof LayerDrawable) { layerDrawable = (LayerDrawable) background; background = layerDrawable.findDrawableByLayerId(android.R.id.background); } Map<int[], Drawable> existingStates = PXDrawableUtil.getExistingStates(background); if (existingStates == null || existingStates.isEmpty()) { int rulesetSize = ruleSets.size(); // No state-lists here, so simply loop and set the backgrounds for (int i = 0; i < rulesetSize; i++) { context = contexts.get(i); PXDrawableUtil.setBackgroundDrawable((View) context.getStyleable(), context.getBackgroundImage()); } return true; } // Update the layer Drawable drawable = PXDrawableUtil.createDrawable(this, existingStates, ruleSets, contexts); if (layerDrawable != null) { layerDrawable.setDrawableByLayerId(android.R.id.background, drawable); } else { PXDrawableUtil.setBackgroundDrawable(view, drawable); } return true; } /** * This method is called when the view's background was null, or did not * have any states assigned. The call will directly update the views in the * context list with the background image generated by each of the contexts. * A new StateListDrawable that will be applied as the View's background. * Subclasses may overwrite. * * @param ruleSets * @param contexts */ protected void updateWithNewStates(List<PXRuleSet> ruleSets, List<PXStylerContext> contexts) { // We should have the same view in all contexts, so grab the first // and set it with the new background StateListDrawable drawable = PXDrawableUtil.createNewStateListDrawable(this, ruleSets, contexts); if (drawable != null) { // Set the background on the right level of the toggle button ToggleButton toggleButton = (ToggleButton) contexts.get(0).getStyleable(); Drawable background = toggleButton.getBackground(); if (background instanceof LayerDrawable) { LayerDrawable layerDrawable = (LayerDrawable) background; layerDrawable.setDrawableByLayerId(android.R.id.background, drawable); } else { // we don't deal with any other background type (non-default) // (TODO: We may need to hack our way to create a new // LayerDrawable with the right layers id's) PXDrawableUtil.setBackgroundDrawable((View) toggleButton, drawable); } } } @Override protected List<Object> getVirtualChildren(Object styleable) { List<Object> superVirtuals = super.getVirtualChildren(styleable); List<Object> result = new ArrayList<Object>(superVirtuals.size() + 1); result.addAll(superVirtuals); result.add(new PXVirtualToggle(styleable)); return result; } /* * (non-Javadoc) * @see * com.pixate.freestyle.styling.adapters.PXTextViewStyleAdapter#getElementName * (java.lang.Object) */ public String getElementName(Object object) { return ELEMENT_NAME; } /* * (non-Javadoc) * @see com.pixate.freestyle.styling.adapters.PXCompoundButtonStyleAdapter# * createAdditionalDrawableStates(int) */ @Override public int[][] createAdditionalDrawableStates(int initialValue) { // A default ToggleButton instance states contain the following // values. Here, we try to generate what's missing when we only get a // single 'state' value from our pseudo class. Note that a state will // still be applied when the other values in the array are negative // (which implied a 'not'). // Note that these are the states for the backround of the button, not // for the toggle icon inside it (a second layer). That icon is handled // as a virtual child named 'toggle'. // @formatter:off // { -android.R.attr.state_window_focused, android.R.attr.state_enabled } // { -android.R.attr.state_window_focused, -android.R.attr.state_enabled } // { android.R.attr.state_pressed } // { android.R.attr.state_focused, android.R.attr.state_enabled } // { android.R.attr.state_enabled } // { android.R.attr.state_focused } // { } (default android.r.attr.drawable) // @formatter:on List<int[]> states = new ArrayList<int[]>(4); // check for some special cases. // @formatter:off switch (initialValue) { case android.R.attr.state_enabled: states.add(new int[] { -android.R.attr.state_window_focused, android.R.attr.state_enabled }); states.add(new int[] { android.R.attr.state_focused, android.R.attr.state_enabled }); break; case android.R.attr.state_focused: states.add(new int[] { android.R.attr.state_focused, android.R.attr.state_enabled }); break; case android.R.attr.drawable: states.add(new int[] { -android.R.attr.state_window_focused, -android.R.attr.state_enabled }); states.add(new int[] {}); break; default: break; } // @formatter:on states.add(new int[] { initialValue }); return states.toArray(new int[states.size()][]); } }