summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--library/full-support/src/com/android/setupwizardlib/GlifRecyclerLayout.java130
-rw-r--r--library/full-support/src/com/android/setupwizardlib/items/RecyclerItemAdapter.java4
-rw-r--r--library/full-support/src/com/android/setupwizardlib/view/HeaderRecyclerView.java187
-rw-r--r--library/full-support/test/res/layout/test_glif_recycler_layout.xml20
-rw-r--r--library/full-support/test/src/com/android/setupwizardlib/test/GlifRecyclerLayoutTest.java126
-rw-r--r--library/main/res/layout/suw_glif_recycler_template.xml24
-rw-r--r--library/main/res/values/attrs.xml11
-rw-r--r--library/main/src/com/android/setupwizardlib/GlifLayout.java19
-rw-r--r--library/rules.gradle1
9 files changed, 510 insertions, 12 deletions
diff --git a/library/full-support/src/com/android/setupwizardlib/GlifRecyclerLayout.java b/library/full-support/src/com/android/setupwizardlib/GlifRecyclerLayout.java
new file mode 100644
index 0000000..b7e2171
--- /dev/null
+++ b/library/full-support/src/com/android/setupwizardlib/GlifRecyclerLayout.java
@@ -0,0 +1,130 @@
+/*
+ * 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.annotation.TargetApi;
+import android.content.Context;
+import android.content.res.TypedArray;
+import android.os.Build.VERSION_CODES;
+import android.support.v7.widget.LinearLayoutManager;
+import android.support.v7.widget.RecyclerView;
+import android.util.AttributeSet;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.ImageView;
+import android.widget.TextView;
+
+import com.android.setupwizardlib.items.ItemGroup;
+import com.android.setupwizardlib.items.ItemInflater;
+import com.android.setupwizardlib.items.RecyclerItemAdapter;
+import com.android.setupwizardlib.view.HeaderRecyclerView;
+
+/**
+ * A GLIF themed layout with a RecyclerView. {@code android:entries} can also be used to specify an
+ * {@link com.android.setupwizardlib.items.ItemHierarchy} to be used with this layout in XML.
+ */
+public class GlifRecyclerLayout extends GlifLayout {
+
+ private RecyclerView mRecyclerView;
+ private TextView mHeaderTextView;
+ private ImageView mIconView;
+
+ public GlifRecyclerLayout(Context context) {
+ this(context, 0, 0);
+ }
+
+ public GlifRecyclerLayout(Context context, int template) {
+ this(context, template, 0);
+ }
+
+ public GlifRecyclerLayout(Context context, int template, int containerId) {
+ super(context, template, containerId);
+ init(context, null, 0);
+ }
+
+ public GlifRecyclerLayout(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ init(context, attrs, 0);
+ }
+
+ @TargetApi(VERSION_CODES.HONEYCOMB)
+ public GlifRecyclerLayout(Context context, AttributeSet attrs, int defStyleAttr) {
+ super(context, attrs, defStyleAttr);
+ init(context, attrs, defStyleAttr);
+ }
+
+ private void init(Context context, AttributeSet attrs, int defStyleAttr) {
+ final TypedArray a = context.obtainStyledAttributes(attrs,
+ R.styleable.SuwGlifRecyclerLayout, defStyleAttr, 0);
+ final int xml = a.getResourceId(R.styleable.SuwGlifRecyclerLayout_android_entries, 0);
+ if (xml != 0) {
+ final ItemGroup inflated = (ItemGroup) new ItemInflater(context).inflate(xml);
+ setAdapter(new RecyclerItemAdapter(inflated));
+ }
+ a.recycle();
+ }
+
+ @Override
+ protected View onInflateTemplate(LayoutInflater inflater, int template) {
+ if (template == 0) {
+ template = R.layout.suw_glif_recycler_template;
+ }
+ return super.onInflateTemplate(inflater, template);
+ }
+
+ @Override
+ protected ViewGroup findContainer(int containerId) {
+ if (containerId == 0) {
+ containerId = R.id.suw_recycler_view;
+ }
+ return super.findContainer(containerId);
+ }
+
+ @Override
+ protected void onTemplateInflated() {
+ mRecyclerView = (RecyclerView) findViewById(R.id.suw_recycler_view);
+ mRecyclerView.setLayoutManager(new LinearLayoutManager(getContext()));
+ if (mRecyclerView instanceof HeaderRecyclerView) {
+ final View header = ((HeaderRecyclerView) mRecyclerView).getHeader();
+ mHeaderTextView = (TextView) header.findViewById(R.id.suw_layout_title);
+ mIconView = (ImageView) header.findViewById(R.id.suw_layout_icon);
+ }
+ }
+
+ @Override
+ protected TextView getHeaderTextView() {
+ return mHeaderTextView;
+ }
+
+ @Override
+ protected ImageView getIconView() {
+ return mIconView;
+ }
+
+ public RecyclerView getRecyclerView() {
+ return mRecyclerView;
+ }
+
+ public void setAdapter(RecyclerView.Adapter adapter) {
+ getRecyclerView().setAdapter(adapter);
+ }
+
+ public RecyclerView.Adapter getAdapter() {
+ return getRecyclerView().getAdapter();
+ }
+}
diff --git a/library/full-support/src/com/android/setupwizardlib/items/RecyclerItemAdapter.java b/library/full-support/src/com/android/setupwizardlib/items/RecyclerItemAdapter.java
index d3da5a1..2624d98 100644
--- a/library/full-support/src/com/android/setupwizardlib/items/RecyclerItemAdapter.java
+++ b/library/full-support/src/com/android/setupwizardlib/items/RecyclerItemAdapter.java
@@ -53,7 +53,6 @@ public class RecyclerItemAdapter extends RecyclerView.Adapter<RecyclerView.ViewH
public RecyclerItemAdapter(ItemHierarchy hierarchy) {
mItemHierarchy = hierarchy;
mItemHierarchy.registerObserver(this);
- setHasStableIds(true);
}
public IItem getItem(int position) {
@@ -64,7 +63,8 @@ public class RecyclerItemAdapter extends RecyclerView.Adapter<RecyclerView.ViewH
public long getItemId(int position) {
IItem mItem = getItem(position);
if (mItem instanceof AbstractItem) {
- return ((AbstractItem) mItem).getId();
+ final int id = ((AbstractItem) mItem).getId();
+ return id > 0 ? id : RecyclerView.NO_ID;
} else {
return RecyclerView.NO_ID;
}
diff --git a/library/full-support/src/com/android/setupwizardlib/view/HeaderRecyclerView.java b/library/full-support/src/com/android/setupwizardlib/view/HeaderRecyclerView.java
new file mode 100644
index 0000000..d96b05f
--- /dev/null
+++ b/library/full-support/src/com/android/setupwizardlib/view/HeaderRecyclerView.java
@@ -0,0 +1,187 @@
+/*
+ * 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.view;
+
+import android.content.Context;
+import android.content.res.TypedArray;
+import android.os.Build;
+import android.support.v7.widget.RecyclerView;
+import android.util.AttributeSet;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.accessibility.AccessibilityEvent;
+
+import com.android.setupwizardlib.R;
+import com.android.setupwizardlib.annotations.VisibleForTesting;
+
+/**
+ * A RecyclerView that can display a header item at the start of the list. The header can be set by
+ * {@code app:suwHeader} in XML. Note that the header will not be inflated until a layout manager
+ * is set.
+ */
+public class HeaderRecyclerView extends RecyclerView {
+
+ private static class HeaderViewHolder extends ViewHolder {
+
+ public HeaderViewHolder(View itemView) {
+ super(itemView);
+ }
+ }
+
+ /**
+ * An adapter that can optionally add one header item to the RecyclerView.
+ */
+ public static class HeaderAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> {
+
+ private RecyclerView.Adapter mAdapter;
+ private View mHeader;
+ private static final int HEADER_VIEW_TYPE = Integer.MAX_VALUE;
+
+ public HeaderAdapter(RecyclerView.Adapter adapter) {
+ mAdapter = adapter;
+ setHasStableIds(mAdapter.hasStableIds());
+ }
+
+ @Override
+ public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
+ if (viewType == HEADER_VIEW_TYPE) {
+ return new HeaderViewHolder(mHeader);
+ } else {
+ return mAdapter.onCreateViewHolder(parent, viewType);
+ }
+ }
+
+ @Override
+ @SuppressWarnings("unchecked")
+ public void onBindViewHolder(ViewHolder holder, int position) {
+ if (mHeader != null) {
+ position--;
+ }
+ if (position >= 0) {
+ mAdapter.onBindViewHolder(holder, position);
+ }
+ }
+
+ @Override
+ public int getItemViewType(int position) {
+ if (mHeader != null) {
+ position--;
+ }
+ if (position < 0) {
+ return HEADER_VIEW_TYPE;
+ }
+ return mAdapter.getItemViewType(position);
+ }
+
+ @Override
+ public int getItemCount() {
+ int count = mAdapter.getItemCount();
+ if (mHeader != null) {
+ count++;
+ }
+ return count;
+ }
+
+ @Override
+ public long getItemId(int position) {
+ if (mHeader != null) {
+ position--;
+ }
+ if (position < 0) {
+ return Long.MAX_VALUE;
+ }
+ return mAdapter.getItemId(position);
+ }
+
+ public void setHeader(View header) {
+ mHeader = header;
+ }
+
+ @VisibleForTesting
+ public RecyclerView.Adapter getWrappedAdapter() {
+ return mAdapter;
+ }
+ }
+
+ private View mHeader;
+ private int mHeaderRes;
+
+ public HeaderRecyclerView(Context context) {
+ super(context);
+ init(null, 0);
+ }
+
+ public HeaderRecyclerView(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ init(attrs, 0);
+ }
+
+ public HeaderRecyclerView(Context context, AttributeSet attrs, int defStyleAttr) {
+ super(context, attrs, defStyleAttr);
+ init(attrs, defStyleAttr);
+ }
+
+ private void init(AttributeSet attrs, int defStyleAttr) {
+ final TypedArray a = getContext().obtainStyledAttributes(attrs,
+ R.styleable.SuwHeaderRecyclerView, defStyleAttr, 0);
+ mHeaderRes = a.getResourceId(R.styleable.SuwHeaderRecyclerView_suwHeader, 0);
+ a.recycle();
+ }
+
+ @Override
+ public void onInitializeAccessibilityEvent(AccessibilityEvent event) {
+ super.onInitializeAccessibilityEvent(event);
+
+ // Decoration-only headers should not count as an item for accessibility, adjust the
+ // accessibility event to account for that.
+ final int numberOfHeaders = mHeader != null ? 1 : 0;
+ event.setItemCount(event.getItemCount() - numberOfHeaders);
+ event.setFromIndex(Math.max(event.getFromIndex() - numberOfHeaders, 0));
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH) {
+ event.setToIndex(Math.max(event.getToIndex() - numberOfHeaders, 0));
+ }
+ }
+
+ /**
+ * Gets the header view of this RecyclerView, or {@code null} if there are no headers.
+ */
+ public View getHeader() {
+ return mHeader;
+ }
+
+ @Override
+ public void setLayoutManager(LayoutManager layout) {
+ super.setLayoutManager(layout);
+ if (layout != null && mHeader == null && mHeaderRes != 0) {
+ // Inflating a child view requires the layout manager to be set. Check here to see if
+ // any header item is specified in XML and inflate them.
+ final LayoutInflater inflater = LayoutInflater.from(getContext());
+ mHeader = inflater.inflate(mHeaderRes, this, false);
+ }
+ }
+
+ @Override
+ public void setAdapter(Adapter adapter) {
+ if (mHeader != null && adapter != null) {
+ final HeaderAdapter headerAdapter = new HeaderAdapter(adapter);
+ headerAdapter.setHeader(mHeader);
+ adapter = headerAdapter;
+ }
+ super.setAdapter(adapter);
+ }
+}
diff --git a/library/full-support/test/res/layout/test_glif_recycler_layout.xml b/library/full-support/test/res/layout/test_glif_recycler_layout.xml
new file mode 100644
index 0000000..45a3928
--- /dev/null
+++ b/library/full-support/test/res/layout/test_glif_recycler_layout.xml
@@ -0,0 +1,20 @@
+<!--
+ 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.
+-->
+
+<com.android.setupwizardlib.GlifRecyclerLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent" />
diff --git a/library/full-support/test/src/com/android/setupwizardlib/test/GlifRecyclerLayoutTest.java b/library/full-support/test/src/com/android/setupwizardlib/test/GlifRecyclerLayoutTest.java
new file mode 100644
index 0000000..74f5e73
--- /dev/null
+++ b/library/full-support/test/src/com/android/setupwizardlib/test/GlifRecyclerLayoutTest.java
@@ -0,0 +1,126 @@
+/*
+ * 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.support.v7.widget.RecyclerView;
+import android.test.InstrumentationTestCase;
+import android.test.suitebuilder.annotation.SmallTest;
+import android.view.ContextThemeWrapper;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.ImageView;
+import android.widget.TextView;
+
+import com.android.setupwizardlib.GlifRecyclerLayout;
+import com.android.setupwizardlib.view.HeaderRecyclerView;
+
+public class GlifRecyclerLayoutTest extends InstrumentationTestCase {
+
+ private Context mContext;
+
+ @Override
+ protected void setUp() throws Exception {
+ super.setUp();
+ mContext = new ContextThemeWrapper(getInstrumentation().getContext(),
+ R.style.SuwThemeGlif_Light);
+ }
+
+ @SmallTest
+ public void testDefaultTemplate() {
+ GlifRecyclerLayout layout = new TestLayout(mContext);
+ assertRecyclerTemplateInflated(layout);
+ }
+
+ @SmallTest
+ public void testInflateFromXml() {
+ LayoutInflater inflater = LayoutInflater.from(mContext);
+ GlifRecyclerLayout layout = (GlifRecyclerLayout)
+ inflater.inflate(R.layout.test_glif_recycler_layout, null);
+ assertRecyclerTemplateInflated(layout);
+ }
+
+ @SmallTest
+ public void testGetRecyclerView() {
+ GlifRecyclerLayout layout = new TestLayout(mContext);
+ assertRecyclerTemplateInflated(layout);
+ assertNotNull("getRecyclerView should not be null", layout.getRecyclerView());
+ }
+
+ @SmallTest
+ public void testAdapter() {
+ GlifRecyclerLayout layout = new TestLayout(mContext);
+ assertRecyclerTemplateInflated(layout);
+
+ final RecyclerView.Adapter adapter = new RecyclerView.Adapter() {
+ @Override
+ public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int position) {
+ return new RecyclerView.ViewHolder(new View(parent.getContext())) {};
+ }
+
+ @Override
+ public void onBindViewHolder(RecyclerView.ViewHolder viewHolder, int position) {
+ }
+
+ @Override
+ public int getItemCount() {
+ return 0;
+ }
+ };
+ layout.setAdapter(adapter);
+
+ final RecyclerView.Adapter gotAdapter = layout.getAdapter();
+ if (gotAdapter instanceof HeaderRecyclerView.HeaderAdapter) {
+ assertSame("Adapter got from GlifRecyclerLayout should be same as set",
+ adapter, ((HeaderRecyclerView.HeaderAdapter) gotAdapter).getWrappedAdapter());
+ } else {
+ assertSame("Adapter got from GlifRecyclerLayout should be same as set",
+ adapter, gotAdapter);
+ }
+ }
+
+ private void assertRecyclerTemplateInflated(GlifRecyclerLayout layout) {
+ View recyclerView = layout.findViewById(R.id.suw_recycler_view);
+ assertTrue("@id/suw_recycler_view should be a RecyclerView",
+ recyclerView instanceof RecyclerView);
+
+ if (layout instanceof TestLayout) {
+ assertNotNull("Header text view should not be null",
+ ((TestLayout) layout).getHeaderTextView());
+ assertNotNull("Icon view should not be null", ((TestLayout) layout).getIconView());
+ }
+ }
+
+ // Make some methods public for testing
+ public static class TestLayout extends GlifRecyclerLayout {
+
+ public TestLayout(Context context) {
+ super(context);
+ }
+
+ @Override
+ public TextView getHeaderTextView() {
+ return super.getHeaderTextView();
+ }
+
+ @Override
+ public ImageView getIconView() {
+ return super.getIconView();
+ }
+ }
+}
diff --git a/library/main/res/layout/suw_glif_recycler_template.xml b/library/main/res/layout/suw_glif_recycler_template.xml
new file mode 100644
index 0000000..d8ae7b5
--- /dev/null
+++ b/library/main/res/layout/suw_glif_recycler_template.xml
@@ -0,0 +1,24 @@
+<?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.
+-->
+
+<com.android.setupwizardlib.view.HeaderRecyclerView
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ android:id="@+id/suw_recycler_view"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ app:suwHeader="@layout/suw_glif_header" />
diff --git a/library/main/res/values/attrs.xml b/library/main/res/values/attrs.xml
index 3ce3218..1ca1a17 100644
--- a/library/main/res/values/attrs.xml
+++ b/library/main/res/values/attrs.xml
@@ -28,6 +28,7 @@
<attr name="suwNavBarTheme" format="reference" />
<!-- Custom view attributes -->
+ <attr name="suwHeader" format="reference" />
<attr name="suwHeaderText" format="string" localization="suggested" />
<declare-styleable name="SuwIllustration">
@@ -35,7 +36,11 @@
</declare-styleable>
<declare-styleable name="SuwStickyHeaderListView">
- <attr name="suwHeader" format="reference" />
+ <attr name="suwHeader" />
+ </declare-styleable>
+
+ <declare-styleable name="SuwHeaderRecyclerView">
+ <attr name="suwHeader" />
</declare-styleable>
<declare-styleable name="SuwGlifLayout">
@@ -48,6 +53,10 @@
<attr name="android:entries" />
</declare-styleable>
+ <declare-styleable name="SuwGlifRecyclerLayout">
+ <attr name="android:entries" />
+ </declare-styleable>
+
<declare-styleable name="SuwSetupWizardLayout">
<attr name="suwBackground" format="color|reference" />
<attr name="suwBackgroundTile" format="color|reference" />
diff --git a/library/main/src/com/android/setupwizardlib/GlifLayout.java b/library/main/src/com/android/setupwizardlib/GlifLayout.java
index 09fafec..294cc43 100644
--- a/library/main/src/com/android/setupwizardlib/GlifLayout.java
+++ b/library/main/src/com/android/setupwizardlib/GlifLayout.java
@@ -126,34 +126,35 @@ public class GlifLayout extends TemplateLayout {
return view instanceof ScrollView ? (ScrollView) view : null;
}
+ protected TextView getHeaderTextView() {
+ return (TextView) findViewById(R.id.suw_layout_title);
+ }
+
public void setHeaderText(int title) {
- final TextView titleView = (TextView) findViewById(R.id.suw_layout_title);
- if (titleView != null) {
- titleView.setText(title);
- }
+ setHeaderText(getContext().getResources().getText(title));
}
public void setHeaderText(CharSequence title) {
- final TextView titleView = (TextView) findViewById(R.id.suw_layout_title);
+ final TextView titleView = getHeaderTextView();
if (titleView != null) {
titleView.setText(title);
}
}
public CharSequence getHeaderText() {
- final TextView titleView = (TextView) findViewById(R.id.suw_layout_title);
+ final TextView titleView = getHeaderTextView();
return titleView != null ? titleView.getText() : null;
}
public void setHeaderColor(ColorStateList color) {
- final TextView titleView = (TextView) findViewById(R.id.suw_layout_title);
+ final TextView titleView = getHeaderTextView();
if (titleView != null) {
titleView.setTextColor(color);
}
}
public ColorStateList getHeaderColor() {
- final TextView titleView = (TextView) findViewById(R.id.suw_layout_title);
+ final TextView titleView = getHeaderTextView();
return titleView != null ? titleView.getTextColors() : null;
}
@@ -169,7 +170,7 @@ public class GlifLayout extends TemplateLayout {
return iconView != null ? iconView.getDrawable() : null;
}
- private ImageView getIconView() {
+ protected ImageView getIconView() {
return (ImageView) findViewById(R.id.suw_layout_icon);
}
}
diff --git a/library/rules.gradle b/library/rules.gradle
index 7660853..e99c994 100644
--- a/library/rules.gradle
+++ b/library/rules.gradle
@@ -98,6 +98,7 @@ android {
androidTestFullSupport {
java.srcDirs = ['full-support/test/src']
+ res.srcDirs = ['full-support/test/res']
}
}
}