/*******************************************************************************
* 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.lang.reflect.Field;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import android.content.res.ColorStateList;
import android.graphics.Typeface;
import android.text.TextUtils.TruncateAt;
import android.widget.TextView;
import com.pixate.freestyle.annotations.PXDocElement;
import com.pixate.freestyle.annotations.PXDocProperty;
import com.pixate.freestyle.cg.shadow.PXShadow;
import com.pixate.freestyle.cg.shadow.PXShadowPaint;
import com.pixate.freestyle.styling.PXDeclaration;
import com.pixate.freestyle.styling.PXRuleSet;
import com.pixate.freestyle.styling.cache.PXStyleInfo;
import com.pixate.freestyle.styling.infos.PXLineBreakInfo.PXLineBreakMode;
import com.pixate.freestyle.styling.stylers.PXColorStyler;
import com.pixate.freestyle.styling.stylers.PXFontStyler;
import com.pixate.freestyle.styling.stylers.PXGenericStyler;
import com.pixate.freestyle.styling.stylers.PXStyler;
import com.pixate.freestyle.styling.stylers.PXStylerBase;
import com.pixate.freestyle.styling.stylers.PXStylerBase.PXDeclarationHandler;
import com.pixate.freestyle.styling.stylers.PXStylerBase.PXStylerInvocation;
import com.pixate.freestyle.styling.stylers.PXStylerContext;
import com.pixate.freestyle.styling.stylers.PXTextContentStyler;
import com.pixate.freestyle.styling.stylers.PXTextShadowStyler;
import com.pixate.freestyle.styling.virtualStyleables.PXVirtualBottomIcon;
import com.pixate.freestyle.styling.virtualStyleables.PXVirtualLeftIcon;
import com.pixate.freestyle.styling.virtualStyleables.PXVirtualRightIcon;
import com.pixate.freestyle.styling.virtualStyleables.PXVirtualTopIcon;
import com.pixate.freestyle.util.PXColorUtil;
import com.pixate.freestyle.util.PXLog;
@PXDocElement(properties = {
@PXDocProperty(name = "text-transform", syntax = "uppercase | lowercase"),
@PXDocProperty(name = "text-overflow", syntax = "word-wrap | character-wrap | ellipsis-head | ellipsis-tail | ellipsis-middle"),
@PXDocProperty(name = "compound-padding", syntax = "<length>") })
public class PXTextViewStyleAdapter extends PXViewStyleAdapter {
private static String TAG = PXTextViewStyleAdapter.class.getSimpleName();
private static String COLOR_PROPERTY = "color";
private static String ELEMENT_NAME = "text-view";
private static PXTextViewStyleAdapter sInstance;
protected PXTextViewStyleAdapter() {
}
/*
* (non-Javadoc)
* @see
* com.pixate.freestyle.styling.adapters.PXViewStyleAdapter#getElementName
* (java.lang.Object)
*/
@Override
public String getElementName(Object object) {
return ELEMENT_NAME;
}
@Override
protected List<Object> getVirtualChildren(Object styleable) {
List<Object> superVirtuals = super.getVirtualChildren(styleable);
List<Object> result = new ArrayList<Object>(superVirtuals.size() + 4);
result.addAll(superVirtuals);
result.add(new PXVirtualTopIcon(styleable));
result.add(new PXVirtualRightIcon(styleable));
result.add(new PXVirtualBottomIcon(styleable));
result.add(new PXVirtualLeftIcon(styleable));
return result;
}
/*
* (non-Javadoc)
* @see
* com.pixate.freestyle.styling.adapters.PXViewStyleAdapter#createStylers()
*/
@Override
protected List<PXStyler> createStylers() {
List<PXStyler> stylers = super.createStylers();
stylers.add(new PXTextShadowStyler(new PXStylerInvocation() {
public void invoke(Object view, PXStyler styler, PXStylerContext context) {
TextView textView = (TextView) view;
PXShadowPaint shadowPaint = context.getTextShadow();
// TODO Shadow group?
if (shadowPaint instanceof PXShadow) {
PXShadow shadow = (PXShadow) shadowPaint;
textView.setShadowLayer(0.5f, shadow.getHorizontalOffset(),
shadow.getVerticalOffset(), shadow.getColor());
}
}
}));
stylers.add(new PXFontStyler(new PXStylerInvocation() {
public void invoke(Object view, PXStyler styler, PXStylerContext context) {
TextView textView = (TextView) view;
Typeface typeface = context.getFont();
if (typeface != null) {
textView.setTypeface(typeface);
}
textView.setTextSize(context.getFontSize());
}
}));
stylers.add(PXColorStyler.getInstance());
// TODO Insets? Check relevance to Android.
stylers.add(new PXTextContentStyler(new PXStylerInvocation() {
public void invoke(Object view, PXStyler styler, PXStylerContext context) {
TextView textView = (TextView) view;
textView.setText(context.getText());
}
}));
Map<String, PXDeclarationHandler> handlers = new HashMap<String, PXStylerBase.PXDeclarationHandler>();
handlers.put("text-transform", new PXDeclarationHandler() {
public void process(PXDeclaration declaration, PXStylerContext stylerContext) {
TextView textView = (TextView) stylerContext.getStyleable();
String newTitle = declaration.transformString((String) textView.getText()
.toString());
textView.setText(newTitle);
}
});
handlers.put("text-overflow", new PXDeclarationHandler() {
public void process(PXDeclaration declaration, PXStylerContext stylerContext) {
TextView textView = (TextView) stylerContext.getStyleable();
PXLineBreakMode lineBreakMode = declaration.getLineBreakModeValue();
TruncateAt androidValue = lineBreakMode.getAndroidValue();
boolean changed = false;
if (androidValue != null) {
textView.setEllipsize(androidValue);
changed = true;
} else {
switch (lineBreakMode) {
case CLIP:
textView.setSingleLine();
changed = true;
break;
default:
// TODO can other values be set to
// transformation methods?
break;
}
}
if (!changed) {
// Set to defaults
textView.setSingleLine(false);
}
}
});
handlers.put("compound-padding", new PXDeclarationHandler() {
public void process(PXDeclaration declaration, PXStylerContext stylerContext) {
TextView styleable = (TextView) stylerContext.getStyleable();
int padding = (int) declaration.getFloatValue(stylerContext.getDisplayMetrics());
if (styleable.getCompoundDrawablePadding() != padding) {
styleable.setCompoundDrawablePadding(padding);
}
}
});
stylers.add(new PXGenericStyler(handlers));
return stylers;
}
@Override
public boolean updateStyle(List<PXRuleSet> ruleSets, List<PXStylerContext> contexts) {
if (!super.updateStyle(ruleSets, contexts)) {
return false;
}
PXStylerContext context = null;
// It needs to be the same View instance for all, so just grab the
// first.
TextView textView = (TextView) contexts.get(0).getStyleable();
// We will maintain any existing text states, while injecting the CSS
// defined states into the right places.
Map<int[], Integer> existingColorStates = getExistingColorStates(textView);
Map<int[], Integer> newStates = new LinkedHashMap<int[], Integer>();
Integer defaultColor = null;
int[] savedActivatedState = null;
for (int i = 0; i < ruleSets.size(); i++) {
context = contexts.get(i);
Object colorPropertyValue = context.getPropertyValue(COLOR_PROPERTY);
if (colorPropertyValue instanceof Number) {
String activeStateName = context.getActiveStateName();
if (activeStateName == null) {
activeStateName = PXStyleInfo.DEFAULT_STYLE;
}
int activeState = PXColorUtil.getStateValue(activeStateName);
int color = ((Number) colorPropertyValue).intValue();
if (activeState == android.R.attr.color) {
// remember the default color (will be inserted at the end)
defaultColor = color;
if (savedActivatedState == null) {
// set the same default color as 'activated' as well. We
// hold the activated state array in case we stumble
// into an explicit 'activated' color. In that case, we
// will call to replace it.
savedActivatedState = new int[] { android.R.attr.state_activated };
newStates.put(savedActivatedState, color);
}
} else {
if (activeState == android.R.attr.state_activated
&& savedActivatedState != null) {
// remove the existing state
newStates.remove(savedActivatedState);
savedActivatedState = new int[] { android.R.attr.state_activated };
newStates.put(savedActivatedState, color);
} else {
// just set the state
newStates.put(new int[] { activeState }, color);
}
}
}
}
int[][] states = new int[newStates.size() + existingColorStates.size()][];
int[] colors = new int[states.length];
int index = 0;
for (int[] state : newStates.keySet()) {
states[index] = state;
colors[index] = newStates.get(state);
index++;
}
for (int[] state : existingColorStates.keySet()) {
states[index] = state;
colors[index] = existingColorStates.get(state);
index++;
}
// check the last value in the array of states.
if (defaultColor != null) {
int[] lastState = states[states.length - 1];
if (lastState.length == 0 || lastState.length == 1
&& lastState[0] == android.R.attr.color) {
colors[colors.length - 1] = defaultColor;
}
}
textView.setTextColor(new ColorStateList(states, colors));
return true;
}
// Statics
public static PXTextViewStyleAdapter getInstance() {
synchronized (PXTextViewStyleAdapter.class) {
if (sInstance == null) {
sInstance = new PXTextViewStyleAdapter();
}
}
return sInstance;
}
@Override
public int[][] createAdditionalDrawableStates(int initialValue) {
// Looks like that by default there is no background set on the
// TextView, so we assume here that the acceptable states would be
// similar to a Button.
// @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:
// add anything that will be treated as the default. Note that
// in case an additional pseudo ruleset appears to deal with
// specific cases, it will take over.
states.add(new int[] { -android.R.attr.state_focused, android.R.attr.state_enabled });
states.add(new int[] { android.R.attr.state_focused, android.R.attr.state_enabled });
states.add(new int[] { android.R.attr.state_enabled });
states.add(new int[] { android.R.attr.state_focused });
states.add(new int[] {});
default:
break;
}
// @formatter:on
states.add(new int[] { initialValue });
return states.toArray(new int[states.size()][]);
}
// privates
private Map<int[], Integer> getExistingColorStates(TextView view) {
Map<int[], Integer> map = new LinkedHashMap<int[], Integer>();
ColorStateList textColors = view.getTextColors();
if (textColors != null) {
try {
Field specsField = textColors.getClass().getDeclaredField("mStateSpecs");
specsField.setAccessible(true);
int[][] stateSpecs = (int[][]) specsField.get(textColors);
Field colorsField = textColors.getClass().getDeclaredField("mColors");
colorsField.setAccessible(true);
int[] colors = (int[]) colorsField.get(textColors);
// These all should match
if (stateSpecs != null && colors != null && stateSpecs.length == colors.length) {
// load the map with the existing states
for (int i = 0; i < stateSpecs.length; i++) {
map.put(stateSpecs[i], colors[i]);
}
}
} catch (Exception e) {
PXLog.e(TAG, e, "Error getting the state set");
} finally {
}
}
return map;
}
}