diff options
| author | 2016-07-19 15:40:52 +0000 | |
|---|---|---|
| committer | 2016-07-19 15:40:54 +0000 | |
| commit | ae65e9fb7ffbf5f769d4bdc1bf224c18d23beb2f (patch) | |
| tree | 81e33b4f0757a022d4048dd07917231e12f17c02 | |
| parent | e775cd261e7b5785c2193d2d347d3da4edaad4d7 (diff) | |
| parent | 8e8071bac1992e20df7e0cefcc9a45d0e1a5218f (diff) | |
Merge "Add support for appcompat preferences rendering"
5 files changed, 328 insertions, 21 deletions
diff --git a/tools/layoutlib/bridge/src/android/preference/BridgePreferenceInflater.java b/tools/layoutlib/bridge/src/android/preference/BridgePreferenceInflater.java index 4f00b5da08a8..aa393a976d12 100644 --- a/tools/layoutlib/bridge/src/android/preference/BridgePreferenceInflater.java +++ b/tools/layoutlib/bridge/src/android/preference/BridgePreferenceInflater.java @@ -21,6 +21,7 @@ import com.android.layoutlib.bridge.android.BridgeXmlBlockParser; import android.content.Context; import android.util.AttributeSet; +import android.view.InflateException; public class BridgePreferenceInflater extends PreferenceInflater { @@ -42,7 +43,15 @@ public class BridgePreferenceInflater extends PreferenceInflater { viewKey = ((BridgeXmlBlockParser) attrs).getViewCookie(); } - Preference preference = super.onCreateItem(name, attrs); + Preference preference = null; + try { + preference = super.onCreateItem(name, attrs); + } catch (ClassNotFoundException | InflateException exception) { + // name is probably not a valid preference type + if ("SwitchPreferenceCompat".equals(name)) { + preference = super.onCreateItem("SwitchPreference", attrs); + } + } if (viewKey != null && bc != null) { bc.addCookie(preference, viewKey); diff --git a/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/android/support/RecyclerViewUtil.java b/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/android/support/RecyclerViewUtil.java index d432120ccb6f..ab278195f328 100644 --- a/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/android/support/RecyclerViewUtil.java +++ b/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/android/support/RecyclerViewUtil.java @@ -21,6 +21,7 @@ import com.android.ide.common.rendering.api.LayoutlibCallback; import com.android.layoutlib.bridge.Bridge; import com.android.layoutlib.bridge.android.BridgeContext; import com.android.layoutlib.bridge.android.RenderParamsFlags; +import com.android.layoutlib.bridge.util.ReflectionUtils; import com.android.layoutlib.bridge.util.ReflectionUtils.ReflectionException; import android.annotation.NonNull; @@ -116,7 +117,7 @@ public class RecyclerViewUtil { private static void setProperty(@NonNull Object object, @NonNull String propertyClassName, @NonNull Object propertyValue, @NonNull String propertySetter) throws ReflectionException { - Class<?> propertyClass = getClassInstance(propertyValue, propertyClassName); + Class<?> propertyClass = ReflectionUtils.getClassInstance(propertyValue, propertyClassName); setProperty(object, propertyClass, propertyValue, propertySetter); } @@ -126,22 +127,4 @@ public class RecyclerViewUtil { invoke(getMethod(object.getClass(), propertySetter, propertyClass), object, propertyValue); } - /** - * Looks through the class hierarchy of {@code object} at runtime and returns the class matching - * the name {@code className}. - * <p/> - * This is used when we cannot use Class.forName() since the class we want was loaded from a - * different ClassLoader. - */ - @NonNull - private static Class<?> getClassInstance(@NonNull Object object, @NonNull String className) { - Class<?> superClass = object.getClass(); - while (superClass != null) { - if (className.equals(superClass.getName())) { - return superClass; - } - superClass = superClass.getSuperclass(); - } - throw new RuntimeException("invalid object/classname combination."); - } } diff --git a/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/android/support/SupportPreferencesUtil.java b/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/android/support/SupportPreferencesUtil.java new file mode 100644 index 000000000000..0124e83d79e8 --- /dev/null +++ b/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/android/support/SupportPreferencesUtil.java @@ -0,0 +1,280 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * 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.android.layoutlib.bridge.android.support; + +import com.android.ide.common.rendering.api.LayoutlibCallback; +import com.android.ide.common.rendering.api.RenderResources; +import com.android.ide.common.rendering.api.ResourceValue; +import com.android.ide.common.rendering.api.StyleResourceValue; +import com.android.layoutlib.bridge.android.BridgeContext; +import com.android.layoutlib.bridge.android.BridgeXmlBlockParser; +import com.android.layoutlib.bridge.util.ReflectionUtils.ReflectionException; + +import org.xmlpull.v1.XmlPullParser; +import org.xmlpull.v1.XmlPullParserException; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.content.Context; +import android.view.ContextThemeWrapper; +import android.view.View; +import android.view.ViewGroup; +import android.widget.LinearLayout; +import android.widget.LinearLayout.LayoutParams; +import android.widget.ScrollView; + +import java.io.IOException; +import java.lang.reflect.Method; +import java.util.ArrayList; + +import static com.android.layoutlib.bridge.util.ReflectionUtils.getClassInstance; +import static com.android.layoutlib.bridge.util.ReflectionUtils.getMethod; +import static com.android.layoutlib.bridge.util.ReflectionUtils.invoke; + +/** + * Class with utility methods to instantiate Preferences provided by the support library. + * This class uses reflection to access the support preference objects so it heavily depends on + * the API being stable. + */ +public class SupportPreferencesUtil { + private static final String PREFERENCE_PKG = "android.support.v7.preference"; + private static final String PREFERENCE_MANAGER = PREFERENCE_PKG + ".PreferenceManager"; + private static final String PREFERENCE_GROUP = PREFERENCE_PKG + ".PreferenceGroup"; + private static final String PREFERENCE_GROUP_ADAPTER = + PREFERENCE_PKG + ".PreferenceGroupAdapter"; + private static final String PREFERENCE_INFLATER = PREFERENCE_PKG + ".PreferenceInflater"; + + private SupportPreferencesUtil() { + } + + @NonNull + private static Object instantiateClass(@NonNull LayoutlibCallback callback, + @NonNull String className, @Nullable Class[] constructorSignature, + @Nullable Object[] constructorArgs) throws ReflectionException { + try { + Object instance = callback.loadClass(className, constructorSignature, constructorArgs); + if (instance == null) { + throw new ClassNotFoundException(className + " class not found"); + } + return instance; + } catch (ClassNotFoundException e) { + throw new ReflectionException(e); + } + } + + @NonNull + private static Object createPreferenceGroupAdapter(@NonNull LayoutlibCallback callback, + @NonNull Object preferenceScreen) throws ReflectionException { + Class<?> preferenceGroupClass = getClassInstance(preferenceScreen, PREFERENCE_GROUP); + + return instantiateClass(callback, PREFERENCE_GROUP_ADAPTER, + new Class[]{preferenceGroupClass}, new Object[]{preferenceScreen}); + } + + @NonNull + private static Object createInflatedPreference(@NonNull LayoutlibCallback callback, + @NonNull Context context, @NonNull XmlPullParser parser, @NonNull Object preferenceScreen, + @NonNull Object preferenceManager) throws ReflectionException { + Class<?> preferenceGroupClass = getClassInstance(preferenceScreen, PREFERENCE_GROUP); + Object preferenceInflater = instantiateClass(callback, PREFERENCE_INFLATER, + new Class[]{Context.class, preferenceManager.getClass()}, + new Object[]{context, preferenceManager}); + Object inflatedPreference = invoke( + getMethod(preferenceInflater.getClass(), "inflate", XmlPullParser.class, + preferenceGroupClass), preferenceInflater, parser, null); + + if (inflatedPreference == null) { + throw new ReflectionException("inflate method returned null"); + } + + return inflatedPreference; + } + + /** + * Returns a themed wrapper context of {@link BridgeContext} with the theme specified in + * ?attr/preferenceTheme applied to it. + */ + @Nullable + private static Context getThemedContext(@NonNull BridgeContext bridgeContext) { + RenderResources resources = bridgeContext.getRenderResources(); + ResourceValue preferenceTheme = resources.findItemInTheme("preferenceTheme", false); + + if (preferenceTheme != null) { + // resolve it, if needed. + preferenceTheme = resources.resolveResValue(preferenceTheme); + } + if (preferenceTheme instanceof StyleResourceValue) { + int styleId = bridgeContext.getDynamicIdByStyle(((StyleResourceValue) preferenceTheme)); + if (styleId != 0) { + return new ContextThemeWrapper(bridgeContext, styleId); + } + } + + return null; + } + + /** + * Returns a {@link LinearLayout} containing all the UI widgets representing the preferences + * passed in the group adapter. + */ + @Nullable + private static LinearLayout setUpPreferencesListView(@NonNull BridgeContext bridgeContext, + @NonNull Context themedContext, @NonNull ArrayList<Object> viewCookie, + @NonNull Object preferenceGroupAdapter) throws ReflectionException { + // Setup the LinearLayout that will contain the preferences + LinearLayout listView = new LinearLayout(themedContext); + listView.setOrientation(LinearLayout.VERTICAL); + listView.setLayoutParams( + new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT)); + + if (!viewCookie.isEmpty()) { + bridgeContext.addViewKey(listView, viewCookie.get(0)); + } + + // Get all the preferences and add them to the LinearLayout + Integer preferencesCount = + (Integer) invoke(getMethod(preferenceGroupAdapter.getClass(), "getItemCount"), + preferenceGroupAdapter); + if (preferencesCount == null) { + return listView; + } + + Method getItemId = getMethod(preferenceGroupAdapter.getClass(), "getItemId", int.class); + Method getItemViewType = + getMethod(preferenceGroupAdapter.getClass(), "getItemViewType", int.class); + Method onCreateViewHolder = + getMethod(preferenceGroupAdapter.getClass(), "onCreateViewHolder", ViewGroup.class, + int.class); + for (int i = 0; i < preferencesCount; i++) { + Long id = (Long) invoke(getItemId, preferenceGroupAdapter, i); + if (id == null) { + continue; + } + + // Get the type of the preference layout and bind it to a newly created view holder + Integer type = (Integer) invoke(getItemViewType, preferenceGroupAdapter, i); + Object viewHolder = + invoke(onCreateViewHolder, preferenceGroupAdapter, listView, type); + if (viewHolder == null) { + continue; + } + invoke(getMethod(preferenceGroupAdapter.getClass(), "onBindViewHolder", + viewHolder.getClass(), int.class), preferenceGroupAdapter, viewHolder, i); + + try { + // Get the view from the view holder and add it to our layout + View itemView = + (View) viewHolder.getClass().getField("itemView").get(viewHolder); + + int arrayPosition = id.intValue() - 1; // IDs are 1 based + if (arrayPosition >= 0 && arrayPosition < viewCookie.size()) { + bridgeContext.addViewKey(itemView, viewCookie.get(arrayPosition)); + } + listView.addView(itemView); + } catch (IllegalAccessException | NoSuchFieldException ignored) { + } + } + + return listView; + } + + /** + * Inflates a preferences layout using the support library. If the support library is not + * available, this method will return null without advancing the parsers. + */ + @Nullable + public static View inflatePreference(@NonNull BridgeContext bridgeContext, + @NonNull XmlPullParser parser, @Nullable ViewGroup root) { + try { + LayoutlibCallback callback = bridgeContext.getLayoutlibCallback(); + + Context context = getThemedContext(bridgeContext); + if (context == null) { + // Probably we couldn't find the "preferenceTheme" in the theme + return null; + } + + // Create PreferenceManager + Object preferenceManager = + instantiateClass(callback, PREFERENCE_MANAGER, new Class[]{Context.class}, + new Object[]{context}); + + // From this moment on, we can assume that we found the support library and that + // nothing should fail + + // Create PreferenceScreen + Object preferenceScreen = + invoke(getMethod(preferenceManager.getClass(), "createPreferenceScreen", + Context.class), preferenceManager, context); + if (preferenceScreen == null) { + return null; + } + + // Setup a parser that stores the list of cookies in the same order as the preferences + // are inflated. That way we can later reconstruct the list using the preference id + // since they are sequential and start in 1. + ArrayList<Object> viewCookie = new ArrayList<>(); + if (parser instanceof BridgeXmlBlockParser) { + // Setup a parser that stores the XmlTag + parser = new BridgeXmlBlockParser(parser, null, false) { + @Override + public Object getViewCookie() { + return ((BridgeXmlBlockParser) getParser()).getViewCookie(); + } + + @Override + public int next() throws XmlPullParserException, IOException { + int ev = super.next(); + if (ev == XmlPullParser.START_TAG) { + viewCookie.add(this.getViewCookie()); + } + + return ev; + } + }; + } + + // Create the PreferenceInflater + Object inflatedPreference = + createInflatedPreference(callback, context, parser, preferenceScreen, + preferenceManager); + + // Setup the RecyclerView (set adapter and layout manager) + Object preferenceGroupAdapter = + createPreferenceGroupAdapter(callback, inflatedPreference); + + // Instead of just setting the group adapter as adapter for a RecyclerView, we manually + // get all the items and add them to a LinearLayout. This allows us to set the view + // cookies so the preferences are correctly linked to their XML. + LinearLayout listView = setUpPreferencesListView(bridgeContext, context, viewCookie, + preferenceGroupAdapter); + + ScrollView scrollView = new ScrollView(context); + scrollView.setLayoutParams( + new ViewGroup.LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT)); + scrollView.addView(listView); + + if (root != null) { + root.addView(scrollView); + } + + return scrollView; + } catch (ReflectionException e) { + return null; + } + } +} diff --git a/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/impl/RenderSessionImpl.java b/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/impl/RenderSessionImpl.java index c890793e290f..feed04509075 100644 --- a/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/impl/RenderSessionImpl.java +++ b/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/impl/RenderSessionImpl.java @@ -45,6 +45,7 @@ import com.android.layoutlib.bridge.android.BridgeXmlBlockParser; import com.android.layoutlib.bridge.android.RenderParamsFlags; import com.android.layoutlib.bridge.android.graphics.NopCanvas; import com.android.layoutlib.bridge.android.support.DesignLibUtil; +import com.android.layoutlib.bridge.android.support.SupportPreferencesUtil; import com.android.layoutlib.bridge.impl.binding.FakeAdapter; import com.android.layoutlib.bridge.impl.binding.FakeExpandableAdapter; import com.android.resources.ResourceType; @@ -326,8 +327,14 @@ public class RenderSessionImpl extends RenderAction<SessionParams> { boolean isPreference = "PreferenceScreen".equals(rootTag); View view; if (isPreference) { - view = Preference_Delegate.inflatePreference(getContext(), mBlockParser, + // First try to use the support library inflater. If something fails, fallback + // to the system preference inflater. + view = SupportPreferencesUtil.inflatePreference(getContext(), mBlockParser, mContentRoot); + if (view == null) { + view = Preference_Delegate.inflatePreference(getContext(), mBlockParser, + mContentRoot); + } } else { view = mInflater.inflate(mBlockParser, mContentRoot); } diff --git a/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/util/ReflectionUtils.java b/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/util/ReflectionUtils.java index 7ce27b6a55fa..040191e859ba 100644 --- a/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/util/ReflectionUtils.java +++ b/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/util/ReflectionUtils.java @@ -37,6 +37,15 @@ public class ReflectionUtils { } } + @NonNull + public static Method getAccessibleMethod(@NonNull Class<?> clazz, @NonNull String name, + @Nullable Class<?>... params) throws ReflectionException { + Method method = getMethod(clazz, name, params); + method.setAccessible(true); + + return method; + } + @Nullable public static Object invoke(@NonNull Method method, @Nullable Object object, @Nullable Object... args) throws ReflectionException { @@ -74,6 +83,25 @@ public class ReflectionUtils { } /** + * Looks through the class hierarchy of {@code object} at runtime and returns the class matching + * the name {@code className}. + * <p> + * This is used when we cannot use Class.forName() since the class we want was loaded from a + * different ClassLoader. + */ + @NonNull + public static Class<?> getClassInstance(@NonNull Object object, @NonNull String className) { + Class<?> superClass = object.getClass(); + while (superClass != null) { + if (className.equals(superClass.getName())) { + return superClass; + } + superClass = superClass.getSuperclass(); + } + throw new RuntimeException("invalid object/classname combination."); + } + + /** * Wraps all reflection related exceptions. Created since ReflectiveOperationException was * introduced in 1.7 and we are still on 1.6 */ |