diff options
author | Maurice Lam <yukl@google.com> | 2015-10-05 19:06:12 -0700 |
---|---|---|
committer | Maurice Lam <yukl@google.com> | 2015-10-06 20:35:32 -0700 |
commit | 5bf291fde3dfd64f264d525534730514a279c8fc (patch) | |
tree | 9dd79e2e05eb4820590d8fc5f481b5e94dc51f91 /library | |
parent | 48c0aa4ca75887986ba3139b7ef07ed5cc078610 (diff) | |
download | setupwizard-5bf291fde3dfd64f264d525534730514a279c8fc.tar.gz |
[SuwLib] Implement Items framework
Items framework is a framework modeled after preferences, which uses
XML to inflate a collection of items. Instead of using activity or
fragment directly like preferences, a ListAdapter is created and you
can use that with any existing ListViews.
SetupWizardItemsLayout is a convenient wrapper around
SetupWizardListLayout which will automatically inflates
android:entries in its attributes to populate the list.
Note: A recycler view adapter is under consideration
Change-Id: I5eb8853c160cf86fa5b6f21a01dfa4b0030643f6
Diffstat (limited to 'library')
16 files changed, 1243 insertions, 0 deletions
diff --git a/library/eclair-mr1/res/layout/suw_items_text.xml b/library/eclair-mr1/res/layout/suw_items_text.xml new file mode 100644 index 0000000..89a4894 --- /dev/null +++ b/library/eclair-mr1/res/layout/suw_items_text.xml @@ -0,0 +1,48 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + Copyright (c) 2015 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. +--> + +<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:tools="http://schemas.android.com/tools" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:gravity="center_vertical" + android:minHeight="?attr/listPreferredItemHeight" + android:orientation="vertical" + android:paddingEnd="?attr/listPreferredItemPaddingRight" + android:paddingLeft="?attr/listPreferredItemPaddingLeft" + android:paddingRight="?attr/listPreferredItemPaddingRight" + android:paddingStart="?attr/listPreferredItemPaddingLeft"> + + <TextView + android:id="@+id/suw_items_title" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:gravity="start" + android:textAlignment="viewStart" + android:textAppearance="?attr/textAppearanceListItem" + tools:ignore="UnusedAttribute" /> + + <TextView + android:id="@+id/suw_items_summary" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:gravity="start" + android:textAlignment="viewStart" + android:textAppearance="?attr/textAppearanceListItemSmall" + tools:ignore="UnusedAttribute" /> + +</LinearLayout> diff --git a/library/eclair-mr1/res/values/styles.xml b/library/eclair-mr1/res/values/styles.xml index 27116ea..2c89807 100644 --- a/library/eclair-mr1/res/values/styles.xml +++ b/library/eclair-mr1/res/values/styles.xml @@ -30,8 +30,11 @@ <item name="android:windowSoftInputMode">adjustResize</item> <item name="colorAccent">@color/suw_color_accent_dark</item> + <item name="listPreferredItemPaddingLeft">@dimen/suw_layout_margin_sides</item> + <item name="listPreferredItemPaddingRight">@dimen/suw_layout_margin_sides</item> <item name="suwCardBackground">@drawable/suw_card_bg_dark</item> <item name="suwNavBarTheme">@style/SuwNavBarThemeDark</item> + <item name="textAppearanceListItemSmall">@style/TextAppearance.AppCompat.Body1</item> </style> <style name="SuwThemeMaterial.Light" parent="Theme.AppCompat.Light.NoActionBar"> @@ -45,8 +48,11 @@ <item name="android:windowSoftInputMode">adjustResize</item> <item name="colorAccent">@color/suw_color_accent_light</item> + <item name="listPreferredItemPaddingLeft">@dimen/suw_layout_margin_sides</item> + <item name="listPreferredItemPaddingRight">@dimen/suw_layout_margin_sides</item> <item name="suwCardBackground">@drawable/suw_card_bg_light</item> <item name="suwNavBarTheme">@style/SuwNavBarThemeLight</item> + <item name="textAppearanceListItemSmall">@style/TextAppearance.AppCompat.Body1</item> </style> <!-- Content styles --> diff --git a/library/main/res/values/attrs.xml b/library/main/res/values/attrs.xml index 6d43280..62fa7e7 100644 --- a/library/main/res/values/attrs.xml +++ b/library/main/res/values/attrs.xml @@ -49,4 +49,17 @@ <attr name="suwContainer" format="reference" /> </declare-styleable> + <declare-styleable name="SuwSetupWizardItemsLayout"> + <attr name="android:entries" /> + </declare-styleable> + + <declare-styleable name="SuwItem"> + <attr name="android:enabled" /> + <attr name="android:icon" /> + <attr name="android:id" /> + <attr name="android:title" /> + <attr name="android:layout" /> + <attr name="android:summary" /> + </declare-styleable> + </resources> diff --git a/library/main/src/com/android/setupwizardlib/SetupWizardItemsLayout.java b/library/main/src/com/android/setupwizardlib/SetupWizardItemsLayout.java new file mode 100644 index 0000000..fbf6618 --- /dev/null +++ b/library/main/src/com/android/setupwizardlib/SetupWizardItemsLayout.java @@ -0,0 +1,49 @@ +/* + * Copyright (C) 2015 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.setupwizardlib; + +import android.content.Context; +import android.content.res.TypedArray; +import android.util.AttributeSet; + +import com.android.setupwizardlib.items.ItemAdapter; +import com.android.setupwizardlib.items.ItemGroup; +import com.android.setupwizardlib.items.ItemInflater; + +public class SetupWizardItemsLayout extends SetupWizardListLayout { + + public SetupWizardItemsLayout(Context context, AttributeSet attrs) { + super(context, attrs); + init(context, attrs, 0); + } + + public SetupWizardItemsLayout(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + init(context, attrs, defStyleAttr); + } + + private void init(Context context, AttributeSet attrs, int defStyleAttr) { + TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.SuwSetupWizardItemsLayout, + defStyleAttr, 0); + int xml = a.getResourceId(R.styleable.SuwSetupWizardItemsLayout_android_entries, 0); + if (xml != 0) { + ItemGroup inflated = (ItemGroup) new ItemInflater(context).inflate(xml); + setAdapter(ItemAdapter.create(inflated)); + } + a.recycle(); + } +} diff --git a/library/main/src/com/android/setupwizardlib/items/GenericInflater.java b/library/main/src/com/android/setupwizardlib/items/GenericInflater.java new file mode 100644 index 0000000..af4ffcc --- /dev/null +++ b/library/main/src/com/android/setupwizardlib/items/GenericInflater.java @@ -0,0 +1,497 @@ +/* + * Copyright (C) 2007 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.setupwizardlib.items; + +import java.io.IOException; +import java.lang.reflect.Constructor; +import java.util.HashMap; + +import org.xmlpull.v1.XmlPullParser; +import org.xmlpull.v1.XmlPullParserException; + +import android.content.Context; +import android.content.res.XmlResourceParser; +import android.util.AttributeSet; +import android.util.Log; +import android.util.Xml; +import android.view.ContextThemeWrapper; +import android.view.InflateException; + +/** + * Generic XML inflater. This class is modeled after {@code android.preference.GenericInflater}, + * which is in turn modeled after {@code LayoutInflater}. This can be used to recursively inflate a + * hierarchy of items. All items in the hierarchy must inherit the generic type {@code T}, and the + * specific implementation is expected to handle inserting child items into the parent, by + * implementing {@link #onAddChildItem(Object, Object)}. + * + * @param <T> Type of the items to inflate + * + * Modified from android.preference.GenericInflater + */ +public abstract class GenericInflater<T> { + + private static final String TAG = "GenericInflater"; + private static final boolean DEBUG = false; + + protected final Context mContext; + + // these are optional, set by the caller + private boolean mFactorySet; + private Factory<T> mFactory; + + private final Object[] mConstructorArgs = new Object[2]; + + private static final Class[] mConstructorSignature = new Class[] { + Context.class, AttributeSet.class}; + + private static final HashMap<String, Constructor<?>> sConstructorMap = new HashMap<>(); + + private String mDefaultPackage; + + public interface Factory<T> { + /** + * Hook you can supply that is called when inflating from a + * inflater. You can use this to customize the tag + * names available in your XML files. + * <p> + * Note that it is good practice to prefix these custom names with your + * package (i.e., com.coolcompany.apps) to avoid conflicts with system + * names. + * + * @param name Tag name to be inflated. + * @param context The context the item is being created in. + * @param attrs Inflation attributes as specified in XML file. + * @return Newly created item. Return null for the default behavior. + */ + T onCreateItem(String name, Context context, AttributeSet attrs); + } + + private static class FactoryMerger<T> implements Factory<T> { + private final Factory<T> mF1, mF2; + + FactoryMerger(Factory<T> f1, Factory<T> f2) { + mF1 = f1; + mF2 = f2; + } + + public T onCreateItem(String name, Context context, AttributeSet attrs) { + T v = mF1.onCreateItem(name, context, attrs); + if (v != null) return v; + return mF2.onCreateItem(name, context, attrs); + } + } + + /** + * Create a new inflater instance associated with a + * particular Context. + * + * @param context The Context in which this inflater will + * create its items; most importantly, this supplies the theme + * from which the default values for their attributes are + * retrieved. + */ + protected GenericInflater(Context context) { + mContext = context; + } + + /** + * Create a new inflater instance that is a copy of an + * existing inflater, optionally with its Context + * changed. For use in implementing {@link #cloneInContext}. + * + * @param original The original inflater to copy. + * @param newContext The new Context to use. + */ + protected GenericInflater(GenericInflater<T> original, Context newContext) { + mContext = newContext; + mFactory = original.mFactory; + } + + /** + * Create a copy of the existing inflater object, with the copy + * pointing to a different Context than the original. This is used by + * {@link ContextThemeWrapper} to create a new inflater to go along + * with the new Context theme. + * + * @param newContext The new Context to associate with the new inflater. + * May be the same as the original Context if desired. + * + * @return Returns a brand spanking new inflater object associated with + * the given Context. + */ + public abstract GenericInflater cloneInContext(Context newContext); + + /** + * Sets the default package that will be searched for classes to construct + * for tag names that have no explicit package. + * + * @param defaultPackage The default package. This will be prepended to the + * tag name, so it should end with a period. + */ + public void setDefaultPackage(String defaultPackage) { + mDefaultPackage = defaultPackage; + } + + /** + * Returns the default package, or null if it is not set. + * + * @see #setDefaultPackage(String) + * @return The default package. + */ + public String getDefaultPackage() { + return mDefaultPackage; + } + + /** + * Return the context we are running in, for access to resources, class + * loader, etc. + */ + public Context getContext() { + return mContext; + } + + /** + * Return the current factory (or null). This is called on each element + * name. If the factory returns an item, add that to the hierarchy. If it + * returns null, proceed to call onCreateItem(name). + */ + public final Factory<T> getFactory() { + return mFactory; + } + + /** + * Attach a custom Factory interface for creating items while using this + * inflater. This must not be null, and can only be set + * once; after setting, you can not change the factory. This is called on + * each element name as the XML is parsed. If the factory returns an item, + * that is added to the hierarchy. If it returns null, the next factory + * default {@link #onCreateItem} method is called. + * <p> + * If you have an existing inflater and want to add your + * own factory to it, use {@link #cloneInContext} to clone the existing + * instance and then you can use this function (once) on the returned new + * instance. This will merge your own factory with whatever factory the + * original instance is using. + */ + public void setFactory(Factory<T> factory) { + if (mFactorySet) { + throw new IllegalStateException("" + + "A factory has already been set on this inflater"); + } + if (factory == null) { + throw new NullPointerException("Given factory can not be null"); + } + mFactorySet = true; + if (mFactory == null) { + mFactory = factory; + } else { + mFactory = new FactoryMerger<>(factory, mFactory); + } + } + + public T inflate(int resource) { + return inflate(resource, null); + } + + + /** + * Inflate a new item hierarchy from the specified xml resource. Throws + * InflaterException if there is an error. + * + * @param resource ID for an XML resource to load (e.g., + * <code>R.layout.main_page</code>) + * @param root Optional parent of the generated hierarchy. + * @return The root of the inflated hierarchy. If root was supplied, + * this is the root item; otherwise it is the root of the inflated + * XML file. + */ + public T inflate(int resource, T root) { + return inflate(resource, root, root != null); + } + + /** + * Inflate a new hierarchy from the specified xml node. Throws + * InflaterException if there is an error. * + * <p> + * <em><strong>Important</strong></em> For performance + * reasons, inflation relies heavily on pre-processing of XML files + * that is done at build time. Therefore, it is not currently possible to + * use inflater with an XmlPullParser over a plain XML file at runtime. + * + * @param parser XML dom node containing the description of the + * hierarchy. + * @param root Optional parent of the generated hierarchy. + * @return The root of the inflated hierarchy. If root was supplied, + * this is the that; otherwise it is the root of the inflated + * XML file. + */ + public T inflate(XmlPullParser parser, T root) { + return inflate(parser, root, root != null); + } + + /** + * Inflate a new hierarchy from the specified xml resource. Throws + * InflaterException if there is an error. + * + * @param resource ID for an XML resource to load (e.g., + * <code>R.layout.main_page</code>) + * @param root Optional root to be the parent of the generated hierarchy (if + * <em>attachToRoot</em> is true), or else simply an object that + * provides a set of values for root of the returned + * hierarchy (if <em>attachToRoot</em> is false.) + * @param attachToRoot Whether the inflated hierarchy should be attached to + * the root parameter? + * @return The root of the inflated hierarchy. If root was supplied and + * attachToRoot is true, this is root; otherwise it is the root of + * the inflated XML file. + */ + public T inflate(int resource, T root, boolean attachToRoot) { + if (DEBUG) Log.v(TAG, "INFLATING from resource: " + resource); + XmlResourceParser parser = getContext().getResources().getXml(resource); + try { + return inflate(parser, root, attachToRoot); + } finally { + parser.close(); + } + } + + /** + * Inflate a new hierarchy from the specified XML node. Throws + * InflaterException if there is an error. + * <p> + * <em><strong>Important</strong></em> For performance + * reasons, inflation relies heavily on pre-processing of XML files + * that is done at build time. Therefore, it is not currently possible to + * use inflater with an XmlPullParser over a plain XML file at runtime. + * + * @param parser XML dom node containing the description of the + * hierarchy. + * @param root Optional to be the parent of the generated hierarchy (if + * <em>attachToRoot</em> is true), or else simply an object that + * provides a set of values for root of the returned + * hierarchy (if <em>attachToRoot</em> is false.) + * @param attachToRoot Whether the inflated hierarchy should be attached to + * the root parameter? + * @return The root of the inflated hierarchy. If root was supplied and + * attachToRoot is true, this is root; otherwise it is the root of + * the inflated XML file. + */ + public T inflate(XmlPullParser parser, T root, boolean attachToRoot) { + synchronized (mConstructorArgs) { + final AttributeSet attrs = Xml.asAttributeSet(parser); + mConstructorArgs[0] = mContext; + T result; + + try { + // Look for the root node. + int type; + while ((type = parser.next()) != XmlPullParser.START_TAG + && type != XmlPullParser.END_DOCUMENT) { + } + + if (type != XmlPullParser.START_TAG) { + throw new InflateException(parser.getPositionDescription() + + ": No start tag found!"); + } + + if (DEBUG) { + Log.v(TAG, "**************************"); + Log.v(TAG, "Creating root: " + + parser.getName()); + Log.v(TAG, "**************************"); + } + // Temp is the root that was found in the xml + T xmlRoot = createItemFromTag(parser, parser.getName(), attrs); + + result = onMergeRoots(root, attachToRoot, xmlRoot); + + if (DEBUG) Log.v(TAG, "-----> start inflating children"); + // Inflate all children under temp + rInflate(parser, result, attrs); + if (DEBUG) Log.v(TAG, "-----> done inflating children"); + } catch (XmlPullParserException e) { + InflateException ex = new InflateException(e.getMessage()); + ex.initCause(e); + throw ex; + } catch (IOException e) { + InflateException ex = new InflateException( + parser.getPositionDescription() + + ": " + e.getMessage()); + ex.initCause(e); + throw ex; + } + + return result; + } + } + + /** + * Low-level function for instantiating by name. This attempts to + * instantiate class of the given <var>name</var> found in this + * inflater's ClassLoader. + * + * <p> + * There are two things that can happen in an error case: either the + * exception describing the error will be thrown, or a null will be + * returned. You must deal with both possibilities -- the former will happen + * the first time createItem() is called for a class of a particular name, + * the latter every time there-after for that class name. + * + * @param name The full name of the class to be instantiated. + * @param attrs The XML attributes supplied for this instance. + * + * @return The newly instantiated item, or null. + */ + public final T createItem(String name, String prefix, AttributeSet attrs) + throws ClassNotFoundException, InflateException { + Constructor constructor = sConstructorMap.get(name); + + try { + if (constructor == null) { + // Class not found in the cache, see if it's real, + // and try to add it + Class<?> clazz = mContext.getClassLoader().loadClass( + prefix != null ? (prefix + name) : name); + constructor = clazz.getConstructor(mConstructorSignature); + constructor.setAccessible(true); + sConstructorMap.put(name, constructor); + } + + Object[] args = mConstructorArgs; + args[1] = attrs; + //noinspection unchecked + return (T) constructor.newInstance(args); + } catch (NoSuchMethodException e) { + InflateException ie = new InflateException(attrs.getPositionDescription() + + ": Error inflating class " + + (prefix != null ? (prefix + name) : name)); + ie.initCause(e); + throw ie; + + } catch (ClassNotFoundException e) { + // If loadClass fails, we should propagate the exception. + throw e; + } catch (Exception e) { + InflateException ie = new InflateException(attrs.getPositionDescription() + + ": Error inflating class " + + (prefix != null ? (prefix + name) : name)); + ie.initCause(e); + throw ie; + } + } + + /** + * This routine is responsible for creating the correct subclass of item + * given the xml element name. Override it to handle custom item objects. If + * you override this in your subclass be sure to call through to + * super.onCreateItem(name) for names you do not recognize. + * + * @param name The fully qualified class name of the item to be create. + * @param attrs An AttributeSet of attributes to apply to the item. + * @return The item created. + */ + protected T onCreateItem(String name, AttributeSet attrs) throws ClassNotFoundException { + return createItem(name, mDefaultPackage, attrs); + } + + private T createItemFromTag(XmlPullParser parser, String name, AttributeSet attrs) { + if (DEBUG) Log.v(TAG, "******** Creating item: " + name); + + try { + T item = (mFactory == null) ? null : mFactory.onCreateItem(name, mContext, attrs); + + if (item == null) { + if (-1 == name.indexOf('.')) { + item = onCreateItem(name, attrs); + } else { + item = createItem(name, null, attrs); + } + } + + if (DEBUG) Log.v(TAG, "Created item is: " + item); + return item; + + } catch (InflateException e) { + throw e; + + } catch (Exception e) { + InflateException ie = new InflateException(attrs + .getPositionDescription() + + ": Error inflating class " + name); + ie.initCause(e); + throw ie; + } + } + + /** + * Recursive method used to descend down the xml hierarchy and instantiate + * items, instantiate their children, and then call onFinishInflate(). + */ + private void rInflate(XmlPullParser parser, T node, final AttributeSet attrs) + throws XmlPullParserException, IOException { + final int depth = parser.getDepth(); + + int type; + while (((type = parser.next()) != XmlPullParser.END_TAG || + parser.getDepth() > depth) && type != XmlPullParser.END_DOCUMENT) { + + if (type != XmlPullParser.START_TAG) { + continue; + } + + if (onCreateCustomFromTag(parser, node, attrs)) { + continue; + } + + if (DEBUG) Log.v(TAG, "Now inflating tag: " + parser.getName()); + String name = parser.getName(); + + T item = createItemFromTag(parser, name, attrs); + + if (DEBUG) Log.v(TAG, "Creating params from parent: " + node); + + onAddChildItem(node, item); + + + if (DEBUG) Log.v(TAG, "-----> start inflating children"); + rInflate(parser, item, attrs); + if (DEBUG) Log.v(TAG, "-----> done inflating children"); + } + } + + /** + * Before this inflater tries to create an item from the tag, this method + * will be called. The parser will be pointing to the start of a tag, you + * must stop parsing and return when you reach the end of this element! + * + * @param parser XML dom node containing the description of the hierarchy. + * @param node The item that should be the parent of whatever you create. + * @param attrs An AttributeSet of attributes to apply to the item. + * @return Whether you created a custom object (true), or whether this + * inflater should proceed to create an item. + */ + protected boolean onCreateCustomFromTag(XmlPullParser parser, T node, + final AttributeSet attrs) throws XmlPullParserException { + return false; + } + + protected abstract void onAddChildItem(T parent, T child); + + protected T onMergeRoots(T givenRoot, boolean attachToGivenRoot, T xmlRoot) { + return xmlRoot; + } +} diff --git a/library/main/src/com/android/setupwizardlib/items/Item.java b/library/main/src/com/android/setupwizardlib/items/Item.java new file mode 100644 index 0000000..9c6832a --- /dev/null +++ b/library/main/src/com/android/setupwizardlib/items/Item.java @@ -0,0 +1,88 @@ +/* + * Copyright (C) 2015 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.setupwizardlib.items; + +import android.content.Context; +import android.content.res.TypedArray; +import android.graphics.drawable.Drawable; +import android.util.AttributeSet; +import android.view.View; +import android.widget.TextView; + +import com.android.setupwizardlib.R; + +/** + * Definition of an item in SetupWizardItemsLayout. An item is usually defined in XML and inflated + * using {@link ItemInflater}. + */ +public class Item { + + private boolean mEnabled; + private Drawable mIcon; + private int mId; + private int mLayoutRes; + private CharSequence mSummary; + private CharSequence mTitle; + + public Item(Context context, AttributeSet attrs) { + TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.SuwItem); + mEnabled = a.getBoolean(R.styleable.SuwItem_android_enabled, true); + mIcon = a.getDrawable(R.styleable.SuwItem_android_icon); + mId = a.getResourceId(R.styleable.SuwItem_android_id, 0); + mTitle = a.getText(R.styleable.SuwItem_android_title); + mSummary = a.getText(R.styleable.SuwItem_android_summary); + mLayoutRes = a.getResourceId(R.styleable.SuwItem_android_layout, + getDefaultLayoutResource()); + a.recycle(); + } + + protected int getDefaultLayoutResource() { + return R.layout.suw_items_text; + } + + public boolean isEnabled() { + return mEnabled; + } + + public Drawable getIcon() { + return mIcon; + } + + public int getId() { + return mId; + } + + public int getLayoutResource() { + return mLayoutRes; + } + + public CharSequence getSummary() { + return mSummary; + } + + public CharSequence getTitle() { + return mTitle; + } + + public void onBindView(View view) { + // TODO: Show icon if defined + TextView label = (TextView) view.findViewById(R.id.suw_items_title); + label.setText(getTitle()); + TextView summary = (TextView) view.findViewById(R.id.suw_items_summary); + summary.setText(getSummary()); + } +} diff --git a/library/main/src/com/android/setupwizardlib/items/ItemAdapter.java b/library/main/src/com/android/setupwizardlib/items/ItemAdapter.java new file mode 100644 index 0000000..3fad091 --- /dev/null +++ b/library/main/src/com/android/setupwizardlib/items/ItemAdapter.java @@ -0,0 +1,112 @@ +/* + * Copyright (C) 2015 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.setupwizardlib.items; + +import android.util.SparseIntArray; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.BaseAdapter; + +/** + * An adapter typically used with ListView to display a list of Items. The list of items used to + * create this adapter can be inflated by {@link ItemInflater} from XML. + */ +public class ItemAdapter extends BaseAdapter { + + private final Item[] mItems; + private ViewTypes mViewTypes = new ViewTypes(); + + public static ItemAdapter create(ItemGroup items) { + return new ItemAdapter(items.getChildren()); + } + + public ItemAdapter(Item[] items) { + mItems = items; + refreshViewTypes(); + } + + @Override + public int getCount() { + return mItems.length; + } + + @Override + public Item getItem(int position) { + return mItems[position]; + } + + @Override + public long getItemId(int position) { + return position; + } + + @Override + public int getItemViewType(int position) { + int layoutRes = getItem(position).getLayoutResource(); + return mViewTypes.get(layoutRes); + } + + @Override + public int getViewTypeCount() { + return mViewTypes.size(); + } + + public void refreshViewTypes() { + for (Item item : mItems) { + mViewTypes.add(item.getLayoutResource()); + } + } + + @Override + public View getView(int position, View convertView, ViewGroup parent) { + Item item = getItem(position); + if (convertView == null) { + LayoutInflater inflater = LayoutInflater.from(parent.getContext()); + convertView = inflater.inflate(item.getLayoutResource(), parent, false); + } + item.onBindView(convertView); + return convertView; + } + + /** + * A helper class to pack a sparse set of integers (e.g. resource IDs) to a contiguous list of + * integers (e.g. adapter positions), providing mapping to retrieve the original ID from a given + * position. This is used to pack the view types of the adapter into contiguous integers from + * a given layout resource. + */ + private static class ViewTypes { + private SparseIntArray mPositionMap = new SparseIntArray(); + private int nextPosition = 0; + + public int add(int id) { + if (mPositionMap.indexOfKey(id) < 0) { + mPositionMap.put(id, nextPosition); + nextPosition++; + } + return mPositionMap.get(id); + } + + public int size() { + return mPositionMap.size(); + } + + public int get(int id) { + return mPositionMap.get(id); + } + } +} diff --git a/library/main/src/com/android/setupwizardlib/items/ItemGroup.java b/library/main/src/com/android/setupwizardlib/items/ItemGroup.java new file mode 100644 index 0000000..3752e68 --- /dev/null +++ b/library/main/src/com/android/setupwizardlib/items/ItemGroup.java @@ -0,0 +1,40 @@ +/* + * Copyright (C) 2015 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.setupwizardlib.items; + +import android.content.Context; +import android.util.AttributeSet; + +import java.util.ArrayList; +import java.util.List; + +public class ItemGroup extends Item { + + private List<Item> mItems = new ArrayList<>(); + + public ItemGroup(Context context, AttributeSet attrs) { + super(context, attrs); + } + + public void addChild(Item child) { + mItems.add(child); + } + + public Item[] getChildren() { + return mItems.toArray(new Item[mItems.size()]); + } +} diff --git a/library/main/src/com/android/setupwizardlib/items/ItemInflater.java b/library/main/src/com/android/setupwizardlib/items/ItemInflater.java new file mode 100644 index 0000000..e25e2ad --- /dev/null +++ b/library/main/src/com/android/setupwizardlib/items/ItemInflater.java @@ -0,0 +1,59 @@ +/* + * Copyright (C) 2015 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.setupwizardlib.items; + +import android.content.Context; + +/** + * Inflate {@link Item} hierarchies from XML files. + * + * Modified from android.support.v7.preference.PreferenceInflater + */ +public class ItemInflater extends GenericInflater<Item> { + + private static final String TAG = "ItemInflater"; + + private final Context mContext; + + public ItemInflater(Context context) { + super(context); + mContext = context; + setDefaultPackage(Item.class.getPackage().getName() + "."); + } + + @Override + public ItemInflater cloneInContext(Context newContext) { + return new ItemInflater(newContext); + } + + /** + * Return the context we are running in, for access to resources, class + * loader, etc. + */ + public Context getContext() { + return mContext; + } + + @Override + protected void onAddChildItem(Item parent, Item child) { + if (parent instanceof ItemGroup) { + ((ItemGroup) parent).addChild(child); + } else { + throw new IllegalArgumentException("Cannot add child item to " + parent); + } + } +} diff --git a/library/platform/res/layout-v21/suw_items_text.xml b/library/platform/res/layout-v21/suw_items_text.xml new file mode 100644 index 0000000..cda5487 --- /dev/null +++ b/library/platform/res/layout-v21/suw_items_text.xml @@ -0,0 +1,41 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + Copyright (c) 2015 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. +--> + +<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:gravity="center_vertical" + android:minHeight="?android:attr/listPreferredItemHeight" + android:orientation="vertical" + android:paddingEnd="?android:attr/listPreferredItemPaddingEnd" + android:paddingStart="?android:attr/listPreferredItemPaddingStart"> + + <TextView + android:id="@+id/suw_items_title" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:textAlignment="viewStart" + android:textAppearance="?android:attr/textAppearanceListItem" /> + + <TextView + android:id="@+id/suw_items_summary" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:textAlignment="viewStart" + android:textAppearance="?android:attr/textAppearanceListItemSmall" /> + +</LinearLayout> diff --git a/library/platform/res/values-v21/styles.xml b/library/platform/res/values-v21/styles.xml index c6c5f12..1acc47a 100644 --- a/library/platform/res/values-v21/styles.xml +++ b/library/platform/res/values-v21/styles.xml @@ -29,8 +29,11 @@ <item name="android:indeterminateTint">@color/suw_progress_bar_color_dark</item> <!-- Specify the indeterminateTintMode to work around a bug in Lollipop --> <item name="android:indeterminateTintMode">src_in</item> + <item name="android:listPreferredItemPaddingEnd">@dimen/suw_layout_margin_sides</item> + <item name="android:listPreferredItemPaddingStart">@dimen/suw_layout_margin_sides</item> <item name="android:navigationBarColor">@android:color/black</item> <item name="android:statusBarColor">@android:color/black</item> + <item name="android:textAppearanceListItemSmall">@android:style/TextAppearance.Material.Body1</item> <item name="android:textColorLink">@color/suw_link_color_dark</item> <item name="android:windowAnimationStyle">@style/Animation.SuwWindowAnimation</item> <item name="android:windowSoftInputMode">adjustResize</item> @@ -44,8 +47,11 @@ <item name="android:indeterminateTint">@color/suw_progress_bar_color_light</item> <!-- Specify the indeterminateTintMode to work around a bug in Lollipop --> <item name="android:indeterminateTintMode">src_in</item> + <item name="android:listPreferredItemPaddingEnd">@dimen/suw_layout_margin_sides</item> + <item name="android:listPreferredItemPaddingStart">@dimen/suw_layout_margin_sides</item> <item name="android:navigationBarColor">@android:color/black</item> <item name="android:statusBarColor">@android:color/black</item> + <item name="android:textAppearanceListItemSmall">@android:style/TextAppearance.Material.Body1</item> <item name="android:textColorLink">@color/suw_link_color_light</item> <item name="android:windowAnimationStyle">@style/Animation.SuwWindowAnimation</item> <item name="android:windowSoftInputMode">adjustResize</item> diff --git a/library/test/res/xml/test_items.xml b/library/test/res/xml/test_items.xml new file mode 100644 index 0000000..3906222 --- /dev/null +++ b/library/test/res/xml/test_items.xml @@ -0,0 +1,29 @@ +<!-- + Copyright (C) 2015 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. +--> + +<ItemGroup xmlns:android="http://schemas.android.com/apk/res/android"> + + <Item + android:id="@+id/test_item_1" + android:title="Title1" + android:summary="Summary1" /> + + <Item + android:id="@+id/test_item_2" + android:title="Title2" + android:summary="Summary2" /> + +</ItemGroup> diff --git a/library/test/src/com/android/setupwizardlib/test/ItemAdapterTest.java b/library/test/src/com/android/setupwizardlib/test/ItemAdapterTest.java new file mode 100644 index 0000000..1ba677c --- /dev/null +++ b/library/test/src/com/android/setupwizardlib/test/ItemAdapterTest.java @@ -0,0 +1,91 @@ +/* + * Copyright (C) 2015 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.setupwizardlib.test; + +import android.content.Context; +import android.test.AndroidTestCase; +import android.test.suitebuilder.annotation.SmallTest; +import android.view.View; +import android.view.ViewGroup; +import android.widget.FrameLayout; +import android.widget.TextView; + +import com.android.setupwizardlib.R; +import com.android.setupwizardlib.items.Item; +import com.android.setupwizardlib.items.ItemAdapter; + +import java.util.Arrays; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; + +public class ItemAdapterTest extends AndroidTestCase { + + private Item[] mItems; + + @Override + protected void setUp() throws Exception { + super.setUp(); + mItems = new Item[]{ + new TestItem(mContext, 1), + new TestItem(mContext, 2), + new TestItem(mContext, 3) + }; + } + + @SmallTest + public void testAdapter() { + ItemAdapter adapter = new ItemAdapter(mItems); + assertEquals("Adapter should have 3 items", 3, adapter.getCount()); + assertEquals("Adapter should return the first item", mItems[0], adapter.getItem(0)); + assertEquals("ID should be same as position", 2, adapter.getItemId(2)); + + // Each test item has its own layout resource, and therefore its own view type + assertEquals("Should have 3 different view types", 3, adapter.getViewTypeCount()); + HashSet<Integer> viewTypes = new HashSet<>(3); + viewTypes.add(adapter.getItemViewType(0)); + viewTypes.add(adapter.getItemViewType(1)); + viewTypes.add(adapter.getItemViewType(2)); + + assertEquals("View types should be 0, 1, 2", new HashSet<>(Arrays.asList(0, 1, 2)), viewTypes); + } + + private static class TestItem extends Item { + + private int mNum; + + public TestItem(Context context, int num) { + super(context, null); + mNum = num; + } + + @Override + public int getLayoutResource() { + return mNum * 10; + } + + @Override + public CharSequence getTitle() { + return "TestTitle" + mNum; + } + + @Override + public CharSequence getSummary() { + return "TestSummary" + mNum; + } + } +} diff --git a/library/test/src/com/android/setupwizardlib/test/ItemGroupTest.java b/library/test/src/com/android/setupwizardlib/test/ItemGroupTest.java new file mode 100644 index 0000000..272b840 --- /dev/null +++ b/library/test/src/com/android/setupwizardlib/test/ItemGroupTest.java @@ -0,0 +1,40 @@ +/* + * Copyright (C) 2015 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.setupwizardlib.test; + +import android.test.AndroidTestCase; +import android.test.suitebuilder.annotation.SmallTest; + +import com.android.setupwizardlib.items.Item; +import com.android.setupwizardlib.items.ItemGroup; + +import java.util.Arrays; + +public class ItemGroupTest extends AndroidTestCase { + + @SmallTest + public void testOnBindView() { + ItemGroup itemGroup = new ItemGroup(mContext, null); + Item child1 = new Item(mContext, null); + Item child2 = new Item(mContext, null); + itemGroup.addChild(child1); + itemGroup.addChild(child2); + + assertEquals("Item group should contain children in the order they were added", + Arrays.asList(child1, child2), Arrays.asList(itemGroup.getChildren())); + } +} diff --git a/library/test/src/com/android/setupwizardlib/test/ItemInflaterTest.java b/library/test/src/com/android/setupwizardlib/test/ItemInflaterTest.java new file mode 100644 index 0000000..faf191e --- /dev/null +++ b/library/test/src/com/android/setupwizardlib/test/ItemInflaterTest.java @@ -0,0 +1,48 @@ +/* + * Copyright (C) 2015 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.setupwizardlib.test; + +import android.test.InstrumentationTestCase; +import android.test.suitebuilder.annotation.SmallTest; + +import com.android.setupwizardlib.items.Item; +import com.android.setupwizardlib.items.ItemGroup; +import com.android.setupwizardlib.items.ItemInflater; + +public class ItemInflaterTest extends InstrumentationTestCase { + + @SmallTest + public void testDefaultPackage() { + ItemInflater inflater = new ItemInflater(getInstrumentation().getContext()); + assertEquals("Default package should be the one containing Item class", + "com.android.setupwizardlib.items.", inflater.getDefaultPackage()); + } + + @SmallTest + public void testInflate() { + ItemInflater inflater = new ItemInflater(getInstrumentation().getContext()); + Item item = inflater.inflate(R.xml.test_items); + assertTrue("Inflated item should be ItemGroup", item instanceof ItemGroup); + ItemGroup itemGroup = (ItemGroup) item; + Item[] children = itemGroup.getChildren(); + assertEquals("Title of first child should be Title1", "Title1", children[0].getTitle()); + assertEquals("ID of second child should be test_item_2", R.id.test_item_2, + children[1].getId()); + assertEquals("Summary of second child should be Summary2", "Summary2", + children[1].getSummary()); + } +} diff --git a/library/test/src/com/android/setupwizardlib/test/ItemTest.java b/library/test/src/com/android/setupwizardlib/test/ItemTest.java new file mode 100644 index 0000000..e6bf94e --- /dev/null +++ b/library/test/src/com/android/setupwizardlib/test/ItemTest.java @@ -0,0 +1,76 @@ +/* + * Copyright (C) 2015 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.setupwizardlib.test; + +import android.content.Context; +import android.test.AndroidTestCase; +import android.test.suitebuilder.annotation.SmallTest; +import android.view.View; +import android.view.ViewGroup; +import android.widget.FrameLayout; +import android.widget.TextView; + +import com.android.setupwizardlib.R; +import com.android.setupwizardlib.items.Item; + +public class ItemTest extends AndroidTestCase { + + private TextView mTitleView; + private TextView mSummaryView; + + @SmallTest + public void testOnBindView() { + Item item = new TestItem(mContext); + View view = createLayout(); + + item.onBindView(view); + + assertEquals("Title should be \"TestTitle\"", "TestTitle", mTitleView.getText().toString()); + assertEquals("Summary should be \"TestSummary\"", "TestSummary", + mSummaryView.getText().toString()); + } + + private ViewGroup createLayout() { + ViewGroup root = new FrameLayout(mContext); + + mTitleView = new TextView(mContext); + mTitleView.setId(R.id.suw_items_title); + root.addView(mTitleView); + mSummaryView = new TextView(mContext); + mSummaryView.setId(R.id.suw_items_summary); + root.addView(mSummaryView); + + return root; + } + + private static class TestItem extends Item { + + public TestItem(Context context) { + super(context, null); + } + + @Override + public CharSequence getTitle() { + return "TestTitle"; + } + + @Override + public CharSequence getSummary() { + return "TestSummary"; + } + } +} |