Merge "Add support for AppCompat widgets"
diff --git a/bridge/src/android/content/res/BridgeTypedArray.java b/bridge/src/android/content/res/BridgeTypedArray.java
index d0e431a..1d5ac0c 100644
--- a/bridge/src/android/content/res/BridgeTypedArray.java
+++ b/bridge/src/android/content/res/BridgeTypedArray.java
@@ -584,6 +584,7 @@
if (value == null) {
return defValue;
}
+ value = value.trim();
// if the value is just an integer, return it.
try {
@@ -595,6 +596,11 @@
// pass
}
+ if (value.startsWith("#")) {
+ // this looks like a color, do not try to parse it
+ return defValue;
+ }
+
// Handle the @id/<name>, @+id/<name> and @android:id/<name>
// We need to return the exact value that was compiled (from the various R classes),
// as these values can be reused internally with calls to findViewById().
diff --git a/bridge/src/android/view/BridgeInflater.java b/bridge/src/android/view/BridgeInflater.java
index f38fb19..3667f58 100644
--- a/bridge/src/android/view/BridgeInflater.java
+++ b/bridge/src/android/view/BridgeInflater.java
@@ -42,9 +42,24 @@
import android.widget.NumberPicker;
import java.io.File;
+import java.util.Arrays;
+import java.util.Collections;
import java.util.HashMap;
+import java.util.HashSet;
import java.util.Map;
+import java.util.Set;
+import static com.android.SdkConstants.AUTO_COMPLETE_TEXT_VIEW;
+import static com.android.SdkConstants.BUTTON;
+import static com.android.SdkConstants.CHECKED_TEXT_VIEW;
+import static com.android.SdkConstants.CHECK_BOX;
+import static com.android.SdkConstants.EDIT_TEXT;
+import static com.android.SdkConstants.IMAGE_BUTTON;
+import static com.android.SdkConstants.MULTI_AUTO_COMPLETE_TEXT_VIEW;
+import static com.android.SdkConstants.RADIO_BUTTON;
+import static com.android.SdkConstants.SEEK_BAR;
+import static com.android.SdkConstants.SPINNER;
+import static com.android.SdkConstants.TEXT_VIEW;
import static com.android.layoutlib.bridge.android.BridgeContext.getBaseContext;
/**
@@ -53,6 +68,13 @@
public final class BridgeInflater extends LayoutInflater {
private final LayoutlibCallback mLayoutlibCallback;
+ /**
+ * If true, the inflater will try to replace the framework widgets with the AppCompat versions.
+ * Ideally, this should be based on the activity being an AppCompat activity but since that is
+ * not trivial to check from layoutlib, we currently base the decision on the current theme
+ * being an AppCompat theme.
+ */
+ private boolean mLoadAppCompatViews;
private boolean mIsInMerge = false;
private ResourceReference mResourceReference;
private Map<View, String> mOpenDrawerLayouts;
@@ -60,6 +82,15 @@
// Keep in sync with the same value in LayoutInflater.
private static final int[] ATTRS_THEME = new int[] {com.android.internal.R.attr.theme };
+ private static final String APPCOMPAT_WIDGET_PREFIX = "android.support.v7.widget.AppCompat";
+ /** List of platform widgets that have an AppCompat version */
+ private static final Set<String> APPCOMPAT_VIEWS = Collections.unmodifiableSet(
+ new HashSet<>(
+ Arrays.asList(TEXT_VIEW, "ImageSwitcher", BUTTON, EDIT_TEXT, SPINNER,
+ IMAGE_BUTTON, CHECK_BOX, RADIO_BUTTON, CHECKED_TEXT_VIEW,
+ AUTO_COMPLETE_TEXT_VIEW, MULTI_AUTO_COMPLETE_TEXT_VIEW, "RatingBar",
+ SEEK_BAR)));
+
/**
* List of class prefixes which are tried first by default.
* <p/>
@@ -75,13 +106,15 @@
return sClassPrefixList;
}
- protected BridgeInflater(LayoutInflater original, Context newContext) {
+ private BridgeInflater(LayoutInflater original, Context newContext) {
super(original, newContext);
newContext = getBaseContext(newContext);
if (newContext instanceof BridgeContext) {
mLayoutlibCallback = ((BridgeContext) newContext).getLayoutlibCallback();
+ mLoadAppCompatViews = ((BridgeContext) newContext).isAppCompatTheme();
} else {
mLayoutlibCallback = null;
+ mLoadAppCompatViews = false;
}
}
@@ -91,10 +124,11 @@
* @param context The Android application context.
* @param layoutlibCallback the {@link LayoutlibCallback} object.
*/
- public BridgeInflater(Context context, LayoutlibCallback layoutlibCallback) {
+ public BridgeInflater(BridgeContext context, LayoutlibCallback layoutlibCallback) {
super(context);
mLayoutlibCallback = layoutlibCallback;
mConstructorArgs[0] = context;
+ mLoadAppCompatViews = context.isAppCompatTheme();
}
@Override
@@ -102,28 +136,38 @@
View view = null;
try {
- // First try to find a class using the default Android prefixes
- for (String prefix : sClassPrefixList) {
+ if (mLoadAppCompatViews && APPCOMPAT_VIEWS.contains(name)) {
+ // We are using an AppCompat theme so try to load the appcompat views
+ view = loadCustomView(APPCOMPAT_WIDGET_PREFIX + name, attrs);
+
+ if (view == null) {
+ mLoadAppCompatViews = false; // Do not try anymore
+ }
+ } else {
+
+ // First try to find a class using the default Android prefixes
+ for (String prefix : sClassPrefixList) {
+ try {
+ view = createView(name, prefix, attrs);
+ if (view != null) {
+ break;
+ }
+ } catch (ClassNotFoundException e) {
+ // Ignore. We'll try again using the base class below.
+ }
+ }
+
+ // Next try using the parent loader. This will most likely only work for
+ // fully-qualified class names.
try {
- view = createView(name, prefix, attrs);
- if (view != null) {
- break;
+ if (view == null) {
+ view = super.onCreateView(name, attrs);
}
} catch (ClassNotFoundException e) {
- // Ignore. We'll try again using the base class below.
+ // Ignore. We'll try again using the custom view loader below.
}
}
- // Next try using the parent loader. This will most likely only work for
- // fully-qualified class names.
- try {
- if (view == null) {
- view = super.onCreateView(name, attrs);
- }
- } catch (ClassNotFoundException e) {
- // Ignore. We'll try again using the custom view loader below.
- }
-
// Finally try again using the custom view loader
if (view == null) {
view = loadCustomView(name, attrs);
diff --git a/bridge/src/com/android/layoutlib/bridge/android/BridgeContext.java b/bridge/src/com/android/layoutlib/bridge/android/BridgeContext.java
index 194eecc..72ac4c3 100644
--- a/bridge/src/com/android/layoutlib/bridge/android/BridgeContext.java
+++ b/bridge/src/com/android/layoutlib/bridge/android/BridgeContext.java
@@ -110,6 +110,7 @@
*/
@SuppressWarnings("deprecation") // For use of Pair.
public final class BridgeContext extends Context {
+ private static final String PREFIX_THEME_APPCOMPAT = "Theme.AppCompat";
/** The map adds cookies to each view so that IDE can link xml tags to views. */
private final HashMap<View, Object> mViewKeyMap = new HashMap<>();
@@ -153,6 +154,7 @@
private ClassLoader mClassLoader;
private IBinder mBinder;
private PackageManager mPackageManager;
+ private Boolean mIsThemeAppCompat;
/**
* Some applications that target both pre API 17 and post API 17, set the newer attrs to
@@ -479,6 +481,36 @@
return Pair.of(null, Boolean.FALSE);
}
+ /**
+ * Returns whether the current selected theme is based on AppCompat
+ */
+ public boolean isAppCompatTheme() {
+ // If a cached value exists, return it.
+ if (mIsThemeAppCompat != null) {
+ return mIsThemeAppCompat;
+ }
+ // Ideally, we should check if the corresponding activity extends
+ // android.support.v7.app.ActionBarActivity, and not care about the theme name at all.
+ StyleResourceValue defaultTheme = mRenderResources.getDefaultTheme();
+ // We can't simply check for parent using resources.themeIsParentOf() since the
+ // inheritance structure isn't really what one would expect. The first common parent
+ // between Theme.AppCompat.Light and Theme.AppCompat is Theme.Material (for v21).
+ boolean isThemeAppCompat = false;
+ for (int i = 0; i < 50; i++) {
+ if (defaultTheme == null) {
+ break;
+ }
+ // for loop ensures that we don't run into cyclic theme inheritance.
+ if (defaultTheme.getName().startsWith(PREFIX_THEME_APPCOMPAT)) {
+ isThemeAppCompat = true;
+ break;
+ }
+ defaultTheme = mRenderResources.getParent(defaultTheme);
+ }
+ mIsThemeAppCompat = isThemeAppCompat;
+ return isThemeAppCompat;
+ }
+
@SuppressWarnings("deprecation")
private ILayoutPullParser getParser(ResourceReference resource) {
ILayoutPullParser parser;
diff --git a/bridge/src/com/android/layoutlib/bridge/impl/Layout.java b/bridge/src/com/android/layoutlib/bridge/impl/Layout.java
index 1afd90d..537fa77 100644
--- a/bridge/src/com/android/layoutlib/bridge/impl/Layout.java
+++ b/bridge/src/com/android/layoutlib/bridge/impl/Layout.java
@@ -20,7 +20,6 @@
import com.android.ide.common.rendering.api.RenderResources;
import com.android.ide.common.rendering.api.ResourceValue;
import com.android.ide.common.rendering.api.SessionParams;
-import com.android.ide.common.rendering.api.StyleResourceValue;
import com.android.layoutlib.bridge.Bridge;
import com.android.layoutlib.bridge.android.BridgeContext;
import com.android.layoutlib.bridge.android.RenderParamsFlags;
@@ -94,7 +93,6 @@
private static final String ATTR_WINDOW_TITLE_SIZE = "windowTitleSize";
private static final String ATTR_WINDOW_TRANSLUCENT_STATUS = StatusBar.ATTR_TRANSLUCENT;
private static final String ATTR_WINDOW_TRANSLUCENT_NAV = NavigationBar.ATTR_TRANSLUCENT;
- private static final String PREFIX_THEME_APPCOMPAT = "Theme.AppCompat";
// Default sizes
private static final int DEFAULT_STATUS_BAR_HEIGHT = 25;
@@ -236,7 +234,7 @@
boolean isMenu = "menu".equals(params.getFlag(RenderParamsFlags.FLAG_KEY_ROOT_TAG));
BridgeActionBar actionBar;
- if (mBuilder.isThemeAppCompat() && !isMenu) {
+ if (context.isAppCompatTheme() && !isMenu) {
actionBar = new AppCompatActionBar(context, params);
} else {
actionBar = new FrameworkActionBar(context, params);
@@ -324,8 +322,6 @@
private boolean mTranslucentStatus;
private boolean mTranslucentNav;
- private Boolean mIsThemeAppCompat;
-
public Builder(@NonNull SessionParams params, @NonNull BridgeContext context) {
mParams = params;
mContext = context;
@@ -365,7 +361,7 @@
}
// Check if an actionbar is needed
boolean windowActionBar = getBooleanThemeValue(mResources, ATTR_WINDOW_ACTION_BAR,
- !isThemeAppCompat(), true);
+ !mContext.isAppCompatTheme(), true);
if (windowActionBar) {
mActionBarSize = getDimension(ATTR_ACTION_BAR_SIZE, true, DEFAULT_TITLE_BAR_HEIGHT);
} else {
@@ -420,33 +416,6 @@
return mParams.getHardwareConfig().hasSoftwareButtons();
}
- private boolean isThemeAppCompat() {
- // If a cached value exists, return it.
- if (mIsThemeAppCompat != null) {
- return mIsThemeAppCompat;
- }
- // Ideally, we should check if the corresponding activity extends
- // android.support.v7.app.ActionBarActivity, and not care about the theme name at all.
- StyleResourceValue defaultTheme = mResources.getDefaultTheme();
- // We can't simply check for parent using resources.themeIsParentOf() since the
- // inheritance structure isn't really what one would expect. The first common parent
- // between Theme.AppCompat.Light and Theme.AppCompat is Theme.Material (for v21).
- boolean isThemeAppCompat = false;
- for (int i = 0; i < 50; i++) {
- if (defaultTheme == null) {
- break;
- }
- // for loop ensures that we don't run into cyclic theme inheritance.
- if (defaultTheme.getName().startsWith(PREFIX_THEME_APPCOMPAT)) {
- isThemeAppCompat = true;
- break;
- }
- defaultTheme = mResources.getParent(defaultTheme);
- }
- mIsThemeAppCompat = isThemeAppCompat;
- return isThemeAppCompat;
- }
-
/**
* Return true if the status bar or nav bar are present, they are not translucent (i.e
* content doesn't overlap with them).
diff --git a/bridge/src/com/android/layoutlib/bridge/impl/ResourceHelper.java b/bridge/src/com/android/layoutlib/bridge/impl/ResourceHelper.java
index a21de56..c197e40 100644
--- a/bridge/src/com/android/layoutlib/bridge/impl/ResourceHelper.java
+++ b/bridge/src/com/android/layoutlib/bridge/impl/ResourceHelper.java
@@ -77,6 +77,7 @@
*/
public static int getColor(String value) {
if (value != null) {
+ value = value.trim();
if (!value.startsWith("#")) {
if (value.startsWith(SdkConstants.PREFIX_THEME_REF)) {
throw new NumberFormatException(String.format(