summaryrefslogtreecommitdiff
path: root/library/recyclerview
diff options
context:
space:
mode:
authorMaurice Lam <yukl@google.com>2017-03-28 12:48:40 -0700
committerMaurice Lam <yukl@google.com>2017-03-28 14:23:21 -0700
commit83862bb59558fc044de9aa0d6e9407be53af8b81 (patch)
tree032855cc188420699c048478a6a24c44731a3151 /library/recyclerview
parent9955331ed7bda114488b1a4701456ec478ff63bf (diff)
downloadsetupwizard-83862bb59558fc044de9aa0d6e9407be53af8b81.tar.gz
Rename SuwLib directories
Rename eclair-mr1 to gingerbread to reflect the min SDK version change, and full-support to recyclerview to better reflect what's inside the directory Also added comments and applied style fixes to keep checkstyle happy. Test: Existing tests pass Change-Id: I20332f718f2aae04092d5e45de944b1efce1a596
Diffstat (limited to 'library/recyclerview')
-rw-r--r--library/recyclerview/res/layout/suw_glif_preference_recycler_view.xml26
-rw-r--r--library/recyclerview/res/layout/suw_glif_preference_template_header.xml38
-rw-r--r--library/recyclerview/res/layout/suw_glif_recycler_template_card.xml49
-rw-r--r--library/recyclerview/res/layout/suw_glif_recycler_template_compact.xml26
-rw-r--r--library/recyclerview/res/layout/suw_glif_recycler_template_content.xml43
-rw-r--r--library/recyclerview/res/layout/suw_preference_recycler_view_header.xml26
-rw-r--r--library/recyclerview/res/layout/suw_preference_recycler_view_normal.xml23
-rw-r--r--library/recyclerview/res/layout/suw_preference_template_header.xml35
-rw-r--r--library/recyclerview/res/layout/suw_recycler_template_card.xml77
-rw-r--r--library/recyclerview/res/layout/suw_recycler_template_card_wide.xml78
-rw-r--r--library/recyclerview/res/layout/suw_recycler_template_header.xml39
-rw-r--r--library/recyclerview/res/layout/suw_recycler_template_header_collapsed.xml65
-rw-r--r--library/recyclerview/res/values-land/layouts.xml26
-rw-r--r--library/recyclerview/res/values-sw600dp-land/layouts.xml26
-rw-r--r--library/recyclerview/res/values-sw600dp/layouts.xml29
-rw-r--r--library/recyclerview/res/values/attrs.xml34
-rw-r--r--library/recyclerview/res/values/layouts.xml34
-rw-r--r--library/recyclerview/src/com/android/setupwizardlib/DividerItemDecoration.java242
-rw-r--r--library/recyclerview/src/com/android/setupwizardlib/GlifPreferenceLayout.java117
-rw-r--r--library/recyclerview/src/com/android/setupwizardlib/GlifRecyclerLayout.java169
-rw-r--r--library/recyclerview/src/com/android/setupwizardlib/SetupWizardPreferenceLayout.java115
-rw-r--r--library/recyclerview/src/com/android/setupwizardlib/SetupWizardRecyclerLayout.java168
-rw-r--r--library/recyclerview/src/com/android/setupwizardlib/items/ItemViewHolder.java58
-rw-r--r--library/recyclerview/src/com/android/setupwizardlib/items/RecyclerItemAdapter.java248
-rw-r--r--library/recyclerview/src/com/android/setupwizardlib/template/RecyclerMixin.java235
-rw-r--r--library/recyclerview/src/com/android/setupwizardlib/template/RecyclerViewScrollHandlingDelegate.java83
-rw-r--r--library/recyclerview/src/com/android/setupwizardlib/view/HeaderRecyclerView.java276
-rw-r--r--library/recyclerview/src/com/android/setupwizardlib/view/StickyHeaderRecyclerView.java143
-rw-r--r--library/recyclerview/test/instrumentation/res/layout/test_glif_recycler_layout.xml20
-rw-r--r--library/recyclerview/test/instrumentation/res/layout/test_list_item.xml20
-rw-r--r--library/recyclerview/test/instrumentation/res/layout/test_list_item_no_background.xml20
-rw-r--r--library/recyclerview/test/instrumentation/res/layout/test_recycler_layout.xml20
-rw-r--r--library/recyclerview/test/instrumentation/src/com/android/setupwizardlib/items/RecyclerItemAdapterTest.java144
-rw-r--r--library/recyclerview/test/instrumentation/src/com/android/setupwizardlib/template/RecyclerMixinTest.java132
-rw-r--r--library/recyclerview/test/instrumentation/src/com/android/setupwizardlib/test/DividerItemDecorationTest.java220
-rw-r--r--library/recyclerview/test/instrumentation/src/com/android/setupwizardlib/test/GlifPreferenceLayoutTest.java104
-rw-r--r--library/recyclerview/test/instrumentation/src/com/android/setupwizardlib/test/GlifRecyclerLayoutTest.java160
-rw-r--r--library/recyclerview/test/instrumentation/src/com/android/setupwizardlib/test/HeaderRecyclerViewTest.java177
-rw-r--r--library/recyclerview/test/instrumentation/src/com/android/setupwizardlib/test/SetupWizardPreferenceLayoutTest.java104
-rw-r--r--library/recyclerview/test/instrumentation/src/com/android/setupwizardlib/test/SetupWizardRecyclerLayoutTest.java164
-rw-r--r--library/recyclerview/test/robotest/src/com/android/setupwizardlib/template/RecyclerViewScrollHandlingDelegateTest.java89
41 files changed, 3902 insertions, 0 deletions
diff --git a/library/recyclerview/res/layout/suw_glif_preference_recycler_view.xml b/library/recyclerview/res/layout/suw_glif_preference_recycler_view.xml
new file mode 100644
index 0000000..af00160
--- /dev/null
+++ b/library/recyclerview/res/layout/suw_glif_preference_recycler_view.xml
@@ -0,0 +1,26 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ 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.
+-->
+
+<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"
+ android:clipChildren="false"
+ android:scrollbars="vertical"
+ app:suwHeader="@layout/suw_glif_header" />
diff --git a/library/recyclerview/res/layout/suw_glif_preference_template_header.xml b/library/recyclerview/res/layout/suw_glif_preference_template_header.xml
new file mode 100644
index 0000000..b870251
--- /dev/null
+++ b/library/recyclerview/res/layout/suw_glif_preference_template_header.xml
@@ -0,0 +1,38 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ 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.
+-->
+
+<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="match_parent"
+ android:orientation="vertical"
+ tools:ignore="UnusedResources">
+ <!-- Ignore UnusedResources: can be used by clients -->
+
+ <FrameLayout
+ android:id="@+id/suw_layout_content"
+ android:layout_width="match_parent"
+ android:layout_height="0dp"
+ android:layout_weight="1" />
+
+ <com.android.setupwizardlib.view.NavigationBar
+ android:id="@+id/suw_layout_navigation_bar"
+ style="@style/SuwNavBarTheme"
+ android:layout_width="match_parent"
+ android:layout_height="@dimen/suw_navbar_height" />
+
+</LinearLayout>
diff --git a/library/recyclerview/res/layout/suw_glif_recycler_template_card.xml b/library/recyclerview/res/layout/suw_glif_recycler_template_card.xml
new file mode 100644
index 0000000..7b5c6b0
--- /dev/null
+++ b/library/recyclerview/res/layout/suw_glif_recycler_template_card.xml
@@ -0,0 +1,49 @@
+<?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:id="@+id/suw_pattern_bg"
+ style="@style/SuwGlifCardBackground"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:fitsSystemWindows="true"
+ android:gravity="center_horizontal"
+ android:orientation="vertical">
+
+ <View
+ android:layout_width="0dp"
+ android:layout_height="0dp"
+ android:layout_weight="1"
+ android:visibility="invisible" />
+
+ <com.android.setupwizardlib.view.IntrinsicSizeFrameLayout
+ style="@style/SuwGlifCardContainer"
+ android:layout_width="@dimen/suw_glif_card_width"
+ android:layout_height="wrap_content"
+ android:height="@dimen/suw_glif_card_height">
+
+ <include layout="@layout/suw_glif_recycler_template_content" />
+
+ </com.android.setupwizardlib.view.IntrinsicSizeFrameLayout>
+
+ <View
+ android:layout_width="0dp"
+ android:layout_height="0dp"
+ android:layout_weight="1"
+ android:visibility="invisible" />
+
+</LinearLayout>
diff --git a/library/recyclerview/res/layout/suw_glif_recycler_template_compact.xml b/library/recyclerview/res/layout/suw_glif_recycler_template_compact.xml
new file mode 100644
index 0000000..9081efb
--- /dev/null
+++ b/library/recyclerview/res/layout/suw_glif_recycler_template_compact.xml
@@ -0,0 +1,26 @@
+<?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.StatusBarBackgroundLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/suw_pattern_bg"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent">
+
+ <include layout="@layout/suw_glif_recycler_template_content" />
+
+</com.android.setupwizardlib.view.StatusBarBackgroundLayout>
diff --git a/library/recyclerview/res/layout/suw_glif_recycler_template_content.xml b/library/recyclerview/res/layout/suw_glif_recycler_template_content.xml
new file mode 100644
index 0000000..e8d209b
--- /dev/null
+++ b/library/recyclerview/res/layout/suw_glif_recycler_template_content.xml
@@ -0,0 +1,43 @@
+<?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:app="http://schemas.android.com/apk/res-auto"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:orientation="vertical">
+
+ <!-- Ignore UnusedAttribute: scrollIndicators is new in M. Default to no indicators in older
+ versions. -->
+ <com.android.setupwizardlib.view.HeaderRecyclerView
+ android:id="@+id/suw_recycler_view"
+ android:layout_width="match_parent"
+ android:layout_height="0dp"
+ android:layout_weight="1"
+ android:scrollbars="vertical"
+ android:scrollIndicators="?attr/suwScrollIndicators"
+ app:suwHeader="@layout/suw_glif_header"
+ tools:ignore="UnusedAttribute" />
+
+ <ViewStub
+ android:id="@+id/suw_layout_footer"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content" />
+
+</LinearLayout>
diff --git a/library/recyclerview/res/layout/suw_preference_recycler_view_header.xml b/library/recyclerview/res/layout/suw_preference_recycler_view_header.xml
new file mode 100644
index 0000000..20e1d19
--- /dev/null
+++ b/library/recyclerview/res/layout/suw_preference_recycler_view_header.xml
@@ -0,0 +1,26 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ 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.
+-->
+
+<com.android.setupwizardlib.view.StickyHeaderRecyclerView
+ 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"
+ android:clipChildren="false"
+ android:scrollbars="vertical"
+ app:suwHeader="@layout/suw_list_header" />
diff --git a/library/recyclerview/res/layout/suw_preference_recycler_view_normal.xml b/library/recyclerview/res/layout/suw_preference_recycler_view_normal.xml
new file mode 100644
index 0000000..0979d91
--- /dev/null
+++ b/library/recyclerview/res/layout/suw_preference_recycler_view_normal.xml
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ 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.
+-->
+
+<android.support.v7.widget.RecyclerView
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/suw_recycler_view"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:scrollbars="vertical" />
diff --git a/library/recyclerview/res/layout/suw_preference_template_header.xml b/library/recyclerview/res/layout/suw_preference_template_header.xml
new file mode 100644
index 0000000..6377616
--- /dev/null
+++ b/library/recyclerview/res/layout/suw_preference_template_header.xml
@@ -0,0 +1,35 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ 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.
+-->
+
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:orientation="vertical">
+
+ <FrameLayout
+ android:id="@+id/suw_layout_content"
+ android:layout_width="match_parent"
+ android:layout_height="0dp"
+ android:layout_weight="1" />
+
+ <com.android.setupwizardlib.view.NavigationBar
+ android:id="@+id/suw_layout_navigation_bar"
+ style="@style/SuwNavBarTheme"
+ android:layout_width="match_parent"
+ android:layout_height="@dimen/suw_navbar_height" />
+
+</LinearLayout>
diff --git a/library/recyclerview/res/layout/suw_recycler_template_card.xml b/library/recyclerview/res/layout/suw_recycler_template_card.xml
new file mode 100644
index 0000000..1d7b143
--- /dev/null
+++ b/library/recyclerview/res/layout/suw_recycler_template_card.xml
@@ -0,0 +1,77 @@
+<?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="match_parent"
+ android:orientation="vertical">
+
+ <com.android.setupwizardlib.view.Illustration
+ android:id="@+id/suw_layout_decor"
+ android:layout_width="match_parent"
+ android:layout_height="0dp"
+ android:layout_weight="1"
+ android:background="@drawable/suw_layout_background">
+
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:orientation="vertical"
+ android:paddingLeft="@dimen/suw_card_port_margin_sides"
+ android:paddingRight="@dimen/suw_card_port_margin_sides">
+
+ <TextView
+ android:id="@+id/suw_layout_title"
+ style="@style/SuwCardTitle"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content" />
+
+ <FrameLayout
+ android:layout_width="match_parent"
+ android:layout_height="0dp"
+ android:layout_weight="1"
+ android:background="?attr/suwCardBackground"
+ android:elevation="@dimen/suw_card_elevation"
+ tools:ignore="UnusedAttribute">
+
+ <android.support.v7.widget.RecyclerView
+ android:id="@+id/suw_recycler_view"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:scrollbars="vertical" />
+
+ <FrameLayout
+ android:id="@+id/suw_layout_content"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent" />
+
+ <include layout="@layout/suw_progress_bar_stub" />
+
+ </FrameLayout>
+
+ </LinearLayout>
+
+ </com.android.setupwizardlib.view.Illustration>
+
+ <com.android.setupwizardlib.view.NavigationBar
+ android:id="@+id/suw_layout_navigation_bar"
+ style="@style/SuwNavBarTheme"
+ android:layout_width="match_parent"
+ android:layout_height="@dimen/suw_navbar_height" />
+
+</LinearLayout>
diff --git a/library/recyclerview/res/layout/suw_recycler_template_card_wide.xml b/library/recyclerview/res/layout/suw_recycler_template_card_wide.xml
new file mode 100644
index 0000000..e5e876f
--- /dev/null
+++ b/library/recyclerview/res/layout/suw_recycler_template_card_wide.xml
@@ -0,0 +1,78 @@
+<?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="match_parent"
+ android:orientation="vertical">
+
+ <com.android.setupwizardlib.view.Illustration
+ android:id="@+id/suw_layout_decor"
+ android:layout_width="match_parent"
+ android:layout_height="0dp"
+ android:layout_weight="1"
+ android:background="@drawable/suw_layout_background">
+
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:gravity="start|top"
+ android:weightSum="16">
+
+ <TextView
+ android:id="@+id/suw_layout_title"
+ style="@style/SuwCardTitle"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="@dimen/suw_card_land_header_text_margin_top"
+ android:layout_weight="6" />
+
+ <FrameLayout
+ android:layout_width="0dp"
+ android:layout_height="match_parent"
+ android:layout_weight="8"
+ android:background="?attr/suwCardBackground"
+ android:elevation="@dimen/suw_card_elevation"
+ tools:ignore="UnusedAttribute">
+
+ <android.support.v7.widget.RecyclerView
+ android:id="@+id/suw_recycler_view"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:scrollbars="vertical" />
+
+ <FrameLayout
+ android:id="@+id/suw_layout_content"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent" />
+
+ <include layout="@layout/suw_progress_bar_stub" />
+
+ </FrameLayout>
+
+ </LinearLayout>
+
+ </com.android.setupwizardlib.view.Illustration>
+
+ <com.android.setupwizardlib.view.NavigationBar
+ android:id="@+id/suw_layout_navigation_bar"
+ style="@style/SuwNavBarTheme"
+ android:layout_width="match_parent"
+ android:layout_height="@dimen/suw_navbar_height" />
+
+</LinearLayout>
diff --git a/library/recyclerview/res/layout/suw_recycler_template_header.xml b/library/recyclerview/res/layout/suw_recycler_template_header.xml
new file mode 100644
index 0000000..d2c9622
--- /dev/null
+++ b/library/recyclerview/res/layout/suw_recycler_template_header.xml
@@ -0,0 +1,39 @@
+<?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:app="http://schemas.android.com/apk/res-auto"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:orientation="vertical">
+
+ <com.android.setupwizardlib.view.StickyHeaderRecyclerView
+ android:id="@+id/suw_recycler_view"
+ android:layout_width="match_parent"
+ android:layout_height="0dp"
+ android:layout_weight="1"
+ android:clipChildren="false"
+ android:scrollbars="vertical"
+ app:suwHeader="@layout/suw_list_header" />
+
+ <com.android.setupwizardlib.view.NavigationBar
+ android:id="@+id/suw_layout_navigation_bar"
+ style="@style/SuwNavBarTheme"
+ android:layout_width="match_parent"
+ android:layout_height="@dimen/suw_navbar_height" />
+
+</LinearLayout>
diff --git a/library/recyclerview/res/layout/suw_recycler_template_header_collapsed.xml b/library/recyclerview/res/layout/suw_recycler_template_header_collapsed.xml
new file mode 100644
index 0000000..1960f0d
--- /dev/null
+++ b/library/recyclerview/res/layout/suw_recycler_template_header_collapsed.xml
@@ -0,0 +1,65 @@
+<?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="match_parent"
+ android:orientation="vertical">
+
+ <FrameLayout
+ android:id="@+id/suw_layout_decor"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:background="@drawable/suw_layout_background"
+ android:elevation="@dimen/suw_title_area_elevation"
+ tools:ignore="UnusedAttribute">
+
+ <TextView
+ android:id="@+id/suw_layout_title"
+ style="@style/SuwHeaderTitle"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content" />
+
+ </FrameLayout>
+
+ <include layout="@layout/suw_progress_bar_stub" />
+
+ <FrameLayout
+ android:layout_width="match_parent"
+ android:layout_height="0dp"
+ android:layout_weight="1">
+
+ <android.support.v7.widget.RecyclerView
+ android:id="@+id/suw_recycler_view"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:scrollbars="vertical" />
+
+ <FrameLayout android:id="@+id/suw_layout_content"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent" />
+
+ </FrameLayout>
+
+ <com.android.setupwizardlib.view.NavigationBar
+ android:id="@+id/suw_layout_navigation_bar"
+ style="@style/SuwNavBarTheme"
+ android:layout_width="match_parent"
+ android:layout_height="@dimen/suw_navbar_height" />
+
+</LinearLayout>
diff --git a/library/recyclerview/res/values-land/layouts.xml b/library/recyclerview/res/values-land/layouts.xml
new file mode 100644
index 0000000..3aacec9
--- /dev/null
+++ b/library/recyclerview/res/values-land/layouts.xml
@@ -0,0 +1,26 @@
+<?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.
+-->
+
+<resources>
+
+ <item name="suw_preference_recycler_view" type="layout">@layout/suw_preference_recycler_view_normal</item>
+ <item name="suw_preference_template" type="layout">@layout/suw_no_scroll_template_header_collapsed</item>
+ <item name="suw_recycler_template" type="layout">@layout/suw_recycler_template_header_collapsed</item>
+ <item name="suw_recycler_template_short" type="layout">@layout/suw_recycler_template_header_collapsed</item>
+
+</resources>
+
diff --git a/library/recyclerview/res/values-sw600dp-land/layouts.xml b/library/recyclerview/res/values-sw600dp-land/layouts.xml
new file mode 100644
index 0000000..0feed90
--- /dev/null
+++ b/library/recyclerview/res/values-sw600dp-land/layouts.xml
@@ -0,0 +1,26 @@
+<?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.
+-->
+
+<resources>
+
+ <item name="suw_preference_recycler_view" type="layout">@layout/suw_preference_recycler_view_normal</item>
+ <item name="suw_preference_template" type="layout">@layout/suw_no_scroll_template_card_wide</item>
+ <item name="suw_recycler_template" type="layout">@layout/suw_recycler_template_card_wide</item>
+ <item name="suw_recycler_template_short" type="layout">@layout/suw_recycler_template_card_wide</item>
+
+</resources>
+
diff --git a/library/recyclerview/res/values-sw600dp/layouts.xml b/library/recyclerview/res/values-sw600dp/layouts.xml
new file mode 100644
index 0000000..bfd4863
--- /dev/null
+++ b/library/recyclerview/res/values-sw600dp/layouts.xml
@@ -0,0 +1,29 @@
+<?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.
+-->
+
+<resources>
+
+ <item name="suw_preference_recycler_view" type="layout">@layout/suw_preference_recycler_view_normal</item>
+ <item name="suw_preference_template" type="layout">@layout/suw_no_scroll_template_card</item>
+ <item name="suw_recycler_template" type="layout">@layout/suw_recycler_template_card</item>
+ <item name="suw_recycler_template_short" type="layout">@layout/suw_recycler_template_card</item>
+
+ <item name="suw_glif_preference_template" type="layout">@layout/suw_glif_blank_template_card</item>
+ <item name="suw_glif_recycler_template" type="layout">@layout/suw_glif_recycler_template_card</item>
+
+</resources>
+
diff --git a/library/recyclerview/res/values/attrs.xml b/library/recyclerview/res/values/attrs.xml
new file mode 100644
index 0000000..e4fb41f
--- /dev/null
+++ b/library/recyclerview/res/values/attrs.xml
@@ -0,0 +1,34 @@
+<?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.
+-->
+
+<resources>
+
+ <attr name="suwHasStableIds" format="boolean|reference" />
+
+ <declare-styleable name="SuwRecyclerItemAdapter">
+ <attr name="android:colorBackground" />
+ <attr name="android:selectableItemBackground" />
+ <attr name="selectableItemBackground" />
+ </declare-styleable>
+
+ <declare-styleable name="SuwRecyclerMixin">
+ <attr name="android:entries" />
+ <attr name="suwDividerInset" />
+ <attr name="suwHasStableIds" />
+ </declare-styleable>
+
+</resources>
diff --git a/library/recyclerview/res/values/layouts.xml b/library/recyclerview/res/values/layouts.xml
new file mode 100644
index 0000000..f0b1e9d
--- /dev/null
+++ b/library/recyclerview/res/values/layouts.xml
@@ -0,0 +1,34 @@
+<?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.
+-->
+
+<resources xmlns:tools="http://schemas.android.com/tools">
+
+ <item name="suw_preference_recycler_view" type="layout">@layout/suw_preference_recycler_view_header</item>
+ <item name="suw_preference_template" type="layout">@layout/suw_preference_template_header</item>
+ <item name="suw_recycler_template" type="layout">@layout/suw_recycler_template_header</item>
+
+ <item
+ name="suw_recycler_template_short"
+ type="layout"
+ tools:ignore="UnusedResources">@layout/suw_recycler_template_header_collapsed</item>
+ <!-- Ignore UnusedResources: can be used by clients -->
+
+ <item name="suw_glif_preference_template" type="layout">@layout/suw_glif_blank_template_compact</item>
+ <item name="suw_glif_recycler_template" type="layout">@layout/suw_glif_recycler_template_compact</item>
+
+</resources>
+
diff --git a/library/recyclerview/src/com/android/setupwizardlib/DividerItemDecoration.java b/library/recyclerview/src/com/android/setupwizardlib/DividerItemDecoration.java
new file mode 100644
index 0000000..13010ba
--- /dev/null
+++ b/library/recyclerview/src/com/android/setupwizardlib/DividerItemDecoration.java
@@ -0,0 +1,242 @@
+/*
+ * 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.graphics.Canvas;
+import android.graphics.Rect;
+import android.graphics.drawable.Drawable;
+import android.support.annotation.IntDef;
+import android.support.v4.view.ViewCompat;
+import android.support.v7.widget.RecyclerView;
+import android.view.View;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+/**
+ * An {@link android.support.v7.widget.RecyclerView.ItemDecoration} for RecyclerView to draw
+ * dividers between items. This ItemDecoration will draw the drawable specified by
+ * {@link #setDivider(android.graphics.drawable.Drawable)} as the divider in between each item by
+ * default, and the behavior of whether the divider is shown can be customized by subclassing
+ * {@link com.android.setupwizardlib.DividerItemDecoration.DividedViewHolder}.
+ *
+ * <p>Modified from v14 PreferenceFragment.DividerDecoration, added with inset capabilities.
+ */
+public class DividerItemDecoration extends RecyclerView.ItemDecoration {
+
+ /* static section */
+
+ /**
+ * An interface to be implemented by a {@link RecyclerView.ViewHolder} which controls whether
+ * dividers should be shown above and below that item.
+ */
+ public interface DividedViewHolder {
+
+ /**
+ * Returns whether divider is allowed above this item. A divider will be shown only if both
+ * items immediately above and below it allows this divider.
+ */
+ boolean isDividerAllowedAbove();
+
+ /**
+ * Returns whether divider is allowed below this item. A divider will be shown only if both
+ * items immediately above and below it allows this divider.
+ */
+ boolean isDividerAllowedBelow();
+ }
+
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef({
+ DIVIDER_CONDITION_EITHER,
+ DIVIDER_CONDITION_BOTH})
+ public @interface DividerCondition {}
+
+ public static final int DIVIDER_CONDITION_EITHER = 0;
+ public static final int DIVIDER_CONDITION_BOTH = 1;
+
+ /**
+ * @deprecated Use {@link #DividerItemDecoration(android.content.Context)}
+ */
+ @Deprecated
+ public static DividerItemDecoration getDefault(Context context) {
+ return new DividerItemDecoration(context);
+ }
+
+ /* non-static section */
+
+ private Drawable mDivider;
+ private int mDividerHeight;
+ private int mDividerIntrinsicHeight;
+ @DividerCondition
+ private int mDividerCondition;
+
+ public DividerItemDecoration() {
+ }
+
+ public DividerItemDecoration(Context context) {
+ final TypedArray a = context.obtainStyledAttributes(R.styleable.SuwDividerItemDecoration);
+ final Drawable divider = a.getDrawable(
+ R.styleable.SuwDividerItemDecoration_android_listDivider);
+ final int dividerHeight = a.getDimensionPixelSize(
+ R.styleable.SuwDividerItemDecoration_android_dividerHeight, 0);
+ @DividerCondition final int dividerCondition = a.getInt(
+ R.styleable.SuwDividerItemDecoration_suwDividerCondition,
+ DIVIDER_CONDITION_EITHER);
+ a.recycle();
+
+ setDivider(divider);
+ setDividerHeight(dividerHeight);
+ setDividerCondition(dividerCondition);
+ }
+
+ @Override
+ public void onDraw(Canvas c, RecyclerView parent, RecyclerView.State state) {
+ if (mDivider == null) {
+ return;
+ }
+ final int childCount = parent.getChildCount();
+ final int width = parent.getWidth();
+ final int dividerHeight = mDividerHeight != 0 ? mDividerHeight : mDividerIntrinsicHeight;
+ for (int childViewIndex = 0; childViewIndex < childCount; childViewIndex++) {
+ final View view = parent.getChildAt(childViewIndex);
+ if (shouldDrawDividerBelow(view, parent)) {
+ final int top = (int) ViewCompat.getY(view) + view.getHeight();
+ mDivider.setBounds(0, top, width, top + dividerHeight);
+ mDivider.draw(c);
+ }
+ }
+ }
+
+ @Override
+ public void getItemOffsets(Rect outRect, View view, RecyclerView parent,
+ RecyclerView.State state) {
+ if (shouldDrawDividerBelow(view, parent)) {
+ outRect.bottom = mDividerHeight != 0 ? mDividerHeight : mDividerIntrinsicHeight;
+ }
+ }
+
+ private boolean shouldDrawDividerBelow(View view, RecyclerView parent) {
+ final RecyclerView.ViewHolder holder = parent.getChildViewHolder(view);
+ final int index = holder.getLayoutPosition();
+ final int lastItemIndex = parent.getAdapter().getItemCount() - 1;
+ if (isDividerAllowedBelow(holder)) {
+ if (mDividerCondition == DIVIDER_CONDITION_EITHER) {
+ // Draw the divider without consulting the next item if we only
+ // need permission for either above or below.
+ return true;
+ }
+ } else if (mDividerCondition == DIVIDER_CONDITION_BOTH || index == lastItemIndex) {
+ // Don't draw if the current view holder doesn't allow drawing below
+ // and the current theme requires permission for both the item below and above.
+ // Also, if this is the last item, there is no item below to ask permission
+ // for whether to draw a divider above, so don't draw it.
+ return false;
+ }
+ // Require permission from index below to draw the divider.
+ if (index < lastItemIndex) {
+ final RecyclerView.ViewHolder nextHolder =
+ parent.findViewHolderForLayoutPosition(index + 1);
+ if (!isDividerAllowedAbove(nextHolder)) {
+ // Don't draw if the next view holder doesn't allow drawing above
+ return false;
+ }
+ }
+ return true;
+ }
+
+ /**
+ * Whether a divider is allowed above the view holder. The allowed values will be combined
+ * according to {@link #getDividerCondition()}. The default implementation delegates to
+ * {@link com.android.setupwizardlib.DividerItemDecoration.DividedViewHolder}, or simply allows
+ * the divider if the view holder doesn't implement {@code DividedViewHolder}. Subclasses can
+ * override this to give more information to decide whether a divider should be drawn.
+ *
+ * @return True if divider is allowed above this view holder.
+ */
+ protected boolean isDividerAllowedAbove(RecyclerView.ViewHolder viewHolder) {
+ return !(viewHolder instanceof DividedViewHolder)
+ || ((DividedViewHolder) viewHolder).isDividerAllowedAbove();
+ }
+
+ /**
+ * Whether a divider is allowed below the view holder. The allowed values will be combined
+ * according to {@link #getDividerCondition()}. The default implementation delegates to
+ * {@link com.android.setupwizardlib.DividerItemDecoration.DividedViewHolder}, or simply allows
+ * the divider if the view holder doesn't implement {@code DividedViewHolder}. Subclasses can
+ * override this to give more information to decide whether a divider should be drawn.
+ *
+ * @return True if divider is allowed below this view holder.
+ */
+ protected boolean isDividerAllowedBelow(RecyclerView.ViewHolder viewHolder) {
+ return !(viewHolder instanceof DividedViewHolder)
+ || ((DividedViewHolder) viewHolder).isDividerAllowedBelow();
+ }
+
+ /**
+ * Sets the drawable to be used as the divider.
+ */
+ public void setDivider(Drawable divider) {
+ if (divider != null) {
+ mDividerIntrinsicHeight = divider.getIntrinsicHeight();
+ } else {
+ mDividerIntrinsicHeight = 0;
+ }
+ mDivider = divider;
+ }
+
+ /**
+ * Gets the drawable currently used as the divider.
+ */
+ public Drawable getDivider() {
+ return mDivider;
+ }
+
+ /**
+ * Sets the divider height, in pixels.
+ */
+ public void setDividerHeight(int dividerHeight) {
+ mDividerHeight = dividerHeight;
+ }
+
+ /**
+ * Gets the divider height, in pixels.
+ */
+ public int getDividerHeight() {
+ return mDividerHeight;
+ }
+
+ /**
+ * Sets whether the divider needs permission from both the item view holder below
+ * and above from where the divider would draw itself or just needs permission from
+ * one or the other before drawing itself.
+ */
+ public void setDividerCondition(@DividerCondition int dividerCondition) {
+ mDividerCondition = dividerCondition;
+ }
+
+ /**
+ * Gets whether the divider needs permission from both the item view holder below
+ * and above from where the divider would draw itself or just needs permission from
+ * one or the other before drawing itself.
+ */
+ @DividerCondition
+ public int getDividerCondition() {
+ return mDividerCondition;
+ }
+}
diff --git a/library/recyclerview/src/com/android/setupwizardlib/GlifPreferenceLayout.java b/library/recyclerview/src/com/android/setupwizardlib/GlifPreferenceLayout.java
new file mode 100644
index 0000000..d337e84
--- /dev/null
+++ b/library/recyclerview/src/com/android/setupwizardlib/GlifPreferenceLayout.java
@@ -0,0 +1,117 @@
+/*
+ * 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.setupwizardlib;
+
+import android.content.Context;
+import android.os.Bundle;
+import android.support.v7.widget.RecyclerView;
+import android.util.AttributeSet;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+
+import com.android.setupwizardlib.template.RecyclerMixin;
+
+/**
+ * A layout to be used with {@code PreferenceFragment} in v14 support library. This can be specified
+ * as the {@code android:layout} in the {@code app:preferenceFragmentStyle} in
+ * {@code app:preferenceTheme}.
+ *
+ * <p />Example:
+ * <pre>{@code
+ * &lt;style android:name="MyActivityTheme">
+ * &lt;item android:name="preferenceTheme">@style/MyPreferenceTheme&lt;/item>
+ * &lt;/style>
+ *
+ * &lt;style android:name="MyPreferenceTheme">
+ * &lt;item android:name="preferenceFragmentStyle">@style/MyPreferenceFragmentStyle&lt;/item>
+ * &lt;/style>
+ *
+ * &lt;style android:name="MyPreferenceFragmentStyle">
+ * &lt;item android:name="android:layout">@layout/my_preference_layout&lt;/item>
+ * &lt;/style>
+ * }</pre>
+ *
+ * where {@code my_preference_layout} is a layout that contains
+ * {@link com.android.setupwizardlib.GlifPreferenceLayout}.
+ *
+ * <p />Example:
+ * <pre>{@code
+ * &lt;com.android.setupwizardlib.GlifPreferenceLayout
+ * xmlns:android="http://schemas.android.com/apk/res/android"
+ * android:id="@id/list_container"
+ * android:layout_width="match_parent"
+ * android:layout_height="match_parent" />
+ * }</pre>
+ *
+ * <p />Fragments using this layout <em>must</em> delegate {@code onCreateRecyclerView} to the
+ * implementation in this class:
+ * {@link #onCreateRecyclerView(android.view.LayoutInflater, android.view.ViewGroup,
+ * android.os.Bundle)}
+ */
+public class GlifPreferenceLayout extends GlifRecyclerLayout {
+
+ public GlifPreferenceLayout(Context context) {
+ super(context);
+ }
+
+ public GlifPreferenceLayout(Context context, int template, int containerId) {
+ super(context, template, containerId);
+ }
+
+ public GlifPreferenceLayout(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ }
+
+ public GlifPreferenceLayout(Context context, AttributeSet attrs, int defStyleAttr) {
+ super(context, attrs, defStyleAttr);
+ }
+
+ @Override
+ protected ViewGroup findContainer(int containerId) {
+ if (containerId == 0) {
+ containerId = R.id.suw_layout_content;
+ }
+ return super.findContainer(containerId);
+ }
+
+ /**
+ * This method must be called in {@code PreferenceFragment#onCreateRecyclerView}.
+ */
+ public RecyclerView onCreateRecyclerView(LayoutInflater inflater, ViewGroup parent,
+ Bundle savedInstanceState) {
+ return mRecyclerMixin.getRecyclerView();
+ }
+
+ @Override
+ protected View onInflateTemplate(LayoutInflater inflater, int template) {
+ if (template == 0) {
+ template = R.layout.suw_glif_preference_template;
+ }
+ return super.onInflateTemplate(inflater, template);
+ }
+
+ @Override
+ protected void onTemplateInflated() {
+ // Inflate the recycler view here, so attributes on the decoration views can be applied
+ // immediately.
+ final LayoutInflater inflater = LayoutInflater.from(getContext());
+ RecyclerView recyclerView = (RecyclerView) inflater.inflate(
+ R.layout.suw_glif_preference_recycler_view, this, false);
+ mRecyclerMixin = new RecyclerMixin(this, recyclerView);
+ }
+}
diff --git a/library/recyclerview/src/com/android/setupwizardlib/GlifRecyclerLayout.java b/library/recyclerview/src/com/android/setupwizardlib/GlifRecyclerLayout.java
new file mode 100644
index 0000000..d1a7947
--- /dev/null
+++ b/library/recyclerview/src/com/android/setupwizardlib/GlifRecyclerLayout.java
@@ -0,0 +1,169 @@
+/*
+ * 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.graphics.drawable.Drawable;
+import android.os.Build.VERSION_CODES;
+import android.support.v7.widget.RecyclerView;
+import android.support.v7.widget.RecyclerView.Adapter;
+import android.support.v7.widget.RecyclerView.ViewHolder;
+import android.util.AttributeSet;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+
+import com.android.setupwizardlib.template.RecyclerMixin;
+import com.android.setupwizardlib.template.RecyclerViewScrollHandlingDelegate;
+import com.android.setupwizardlib.template.RequireScrollMixin;
+
+/**
+ * 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 {
+
+ protected RecyclerMixin mRecyclerMixin;
+
+ 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) {
+ mRecyclerMixin.parseAttributes(attrs, defStyleAttr);
+ registerMixin(RecyclerMixin.class, mRecyclerMixin);
+
+ final RequireScrollMixin requireScrollMixin = getMixin(RequireScrollMixin.class);
+ requireScrollMixin.setScrollHandlingDelegate(
+ new RecyclerViewScrollHandlingDelegate(requireScrollMixin, getRecyclerView()));
+ }
+
+ @Override
+ protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
+ super.onLayout(changed, left, top, right, bottom);
+ mRecyclerMixin.onLayout();
+ }
+
+ @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 void onTemplateInflated() {
+ final View recyclerView = findViewById(R.id.suw_recycler_view);
+ if (recyclerView instanceof RecyclerView) {
+ mRecyclerMixin = new RecyclerMixin(this, (RecyclerView) recyclerView);
+ } else {
+ throw new IllegalStateException(
+ "GlifRecyclerLayout should use a template with recycler view");
+ }
+ }
+
+ @Override
+ protected ViewGroup findContainer(int containerId) {
+ if (containerId == 0) {
+ containerId = R.id.suw_recycler_view;
+ }
+ return super.findContainer(containerId);
+ }
+
+ @Override
+ public View findManagedViewById(int id) {
+ final View header = mRecyclerMixin.getHeader();
+ if (header != null) {
+ final View view = header.findViewById(id);
+ if (view != null) {
+ return view;
+ }
+ }
+ return super.findViewById(id);
+ }
+
+ /**
+ * @see RecyclerMixin#setDividerItemDecoration(DividerItemDecoration)
+ */
+ public void setDividerItemDecoration(DividerItemDecoration decoration) {
+ mRecyclerMixin.setDividerItemDecoration(decoration);
+ }
+
+ /**
+ * @see RecyclerMixin#getRecyclerView()
+ */
+ public RecyclerView getRecyclerView() {
+ return mRecyclerMixin.getRecyclerView();
+ }
+
+ /**
+ * @see RecyclerMixin#setAdapter(Adapter)
+ */
+ public void setAdapter(Adapter<? extends ViewHolder> adapter) {
+ mRecyclerMixin.setAdapter(adapter);
+ }
+
+ /**
+ * @see RecyclerMixin#getAdapter()
+ */
+ public Adapter<? extends ViewHolder> getAdapter() {
+ return mRecyclerMixin.getAdapter();
+ }
+
+ /**
+ * @see RecyclerMixin#setDividerInset(int)
+ */
+ public void setDividerInset(int inset) {
+ mRecyclerMixin.setDividerInset(inset);
+ }
+
+ /**
+ * @see RecyclerMixin#getDividerInset()
+ */
+ public int getDividerInset() {
+ return mRecyclerMixin.getDividerInset();
+ }
+
+ /**
+ * @see RecyclerMixin#getDivider()
+ */
+ public Drawable getDivider() {
+ return mRecyclerMixin.getDivider();
+ }
+}
diff --git a/library/recyclerview/src/com/android/setupwizardlib/SetupWizardPreferenceLayout.java b/library/recyclerview/src/com/android/setupwizardlib/SetupWizardPreferenceLayout.java
new file mode 100644
index 0000000..6570694
--- /dev/null
+++ b/library/recyclerview/src/com/android/setupwizardlib/SetupWizardPreferenceLayout.java
@@ -0,0 +1,115 @@
+/*
+ * 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.setupwizardlib;
+
+import android.content.Context;
+import android.os.Bundle;
+import android.support.v7.widget.RecyclerView;
+import android.util.AttributeSet;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+
+import com.android.setupwizardlib.template.RecyclerMixin;
+
+/**
+ * A layout to be used with {@code PreferenceFragment} in v14 support library. This can be specified
+ * as the {@code android:layout} in the {@code app:preferenceFragmentStyle} in
+ * {@code app:preferenceTheme}.
+ *
+ * <p />Example:
+ * <pre>{@code
+ * &lt;style android:name="MyActivityTheme">
+ * &lt;item android:name="preferenceTheme">@style/MyPreferenceTheme&lt;/item>
+ * &lt;/style>
+ *
+ * &lt;style android:name="MyPreferenceTheme">
+ * &lt;item android:name="preferenceFragmentStyle">@style/MyPreferenceFragmentStyle&lt;/item>
+ * &lt;/style>
+ *
+ * &lt;style android:name="MyPreferenceFragmentStyle">
+ * &lt;item android:name="android:layout">@layout/my_preference_layout&lt;/item>
+ * &lt;/style>
+ * }</pre>
+ *
+ * where {@code my_preference_layout} is a layout that contains
+ * {@link com.android.setupwizardlib.SetupWizardPreferenceLayout}.
+ *
+ * <p />Example:
+ * <pre>{@code
+ * &lt;com.android.setupwizardlib.SetupWizardPreferenceLayout
+ * xmlns:android="http://schemas.android.com/apk/res/android"
+ * android:id="@id/list_container"
+ * android:layout_width="match_parent"
+ * android:layout_height="match_parent" />
+ * }</pre>
+ *
+ * <p />Fragments using this layout <em>must</em> delegate {@code onCreateRecyclerView} to the
+ * implementation in this class: {@link #onCreateRecyclerView}
+ */
+public class SetupWizardPreferenceLayout extends SetupWizardRecyclerLayout {
+
+ public SetupWizardPreferenceLayout(Context context) {
+ super(context);
+ }
+
+ public SetupWizardPreferenceLayout(Context context, int template, int containerId) {
+ super(context, template, containerId);
+ }
+
+ public SetupWizardPreferenceLayout(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ }
+
+ public SetupWizardPreferenceLayout(Context context, AttributeSet attrs, int defStyleAttr) {
+ super(context, attrs, defStyleAttr);
+ }
+
+ @Override
+ protected ViewGroup findContainer(int containerId) {
+ if (containerId == 0) {
+ containerId = R.id.suw_layout_content;
+ }
+ return super.findContainer(containerId);
+ }
+
+ /**
+ * This method must be called in {@code PreferenceFragment#onCreateRecyclerView}.
+ */
+ public RecyclerView onCreateRecyclerView(LayoutInflater inflater, ViewGroup parent,
+ Bundle savedInstanceState) {
+ return mRecyclerMixin.getRecyclerView();
+ }
+
+ @Override
+ protected View onInflateTemplate(LayoutInflater inflater, int template) {
+ if (template == 0) {
+ template = R.layout.suw_preference_template;
+ }
+ return super.onInflateTemplate(inflater, template);
+ }
+
+ @Override
+ protected void onTemplateInflated() {
+ // Inflate the recycler view here, so attributes on the decoration views can be applied
+ // immediately.
+ final LayoutInflater inflater = LayoutInflater.from(getContext());
+ RecyclerView recyclerView = (RecyclerView) inflater.inflate(
+ R.layout.suw_preference_recycler_view, this, false);
+ mRecyclerMixin = new RecyclerMixin(this, recyclerView);
+ }
+}
diff --git a/library/recyclerview/src/com/android/setupwizardlib/SetupWizardRecyclerLayout.java b/library/recyclerview/src/com/android/setupwizardlib/SetupWizardRecyclerLayout.java
new file mode 100644
index 0000000..870a805
--- /dev/null
+++ b/library/recyclerview/src/com/android/setupwizardlib/SetupWizardRecyclerLayout.java
@@ -0,0 +1,168 @@
+/*
+ * 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.graphics.drawable.Drawable;
+import android.support.v7.widget.RecyclerView;
+import android.support.v7.widget.RecyclerView.Adapter;
+import android.support.v7.widget.RecyclerView.ViewHolder;
+import android.util.AttributeSet;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+
+import com.android.setupwizardlib.template.RecyclerMixin;
+import com.android.setupwizardlib.template.RecyclerViewScrollHandlingDelegate;
+import com.android.setupwizardlib.template.RequireScrollMixin;
+
+/**
+ * A setup wizard layout for use with {@link android.support.v7.widget.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.
+ *
+ * @see SetupWizardListLayout
+ */
+public class SetupWizardRecyclerLayout extends SetupWizardLayout {
+
+ private static final String TAG = "RecyclerLayout";
+
+ protected RecyclerMixin mRecyclerMixin;
+
+ public SetupWizardRecyclerLayout(Context context) {
+ this(context, 0, 0);
+ }
+
+ public SetupWizardRecyclerLayout(Context context, int template, int containerId) {
+ super(context, template, containerId);
+ init(context, null, 0);
+ }
+
+ public SetupWizardRecyclerLayout(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ init(context, attrs, 0);
+ }
+
+ public SetupWizardRecyclerLayout(Context context, AttributeSet attrs, int defStyleAttr) {
+ super(context, attrs, defStyleAttr);
+ init(context, attrs, defStyleAttr);
+ }
+
+ private void init(Context context, AttributeSet attrs, int defStyleAttr) {
+ mRecyclerMixin.parseAttributes(attrs, defStyleAttr);
+ registerMixin(RecyclerMixin.class, mRecyclerMixin);
+
+
+ final RequireScrollMixin requireScrollMixin = getMixin(RequireScrollMixin.class);
+ requireScrollMixin.setScrollHandlingDelegate(
+ new RecyclerViewScrollHandlingDelegate(requireScrollMixin, getRecyclerView()));
+ }
+
+ @Override
+ protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
+ super.onLayout(changed, left, top, right, bottom);
+ mRecyclerMixin.onLayout();
+ }
+
+ /**
+ * @see RecyclerMixin#getAdapter()
+ */
+ public Adapter<? extends ViewHolder> getAdapter() {
+ return mRecyclerMixin.getAdapter();
+ }
+
+ /**
+ * @see RecyclerMixin#setAdapter(Adapter)
+ */
+ public void setAdapter(Adapter<? extends ViewHolder> adapter) {
+ mRecyclerMixin.setAdapter(adapter);
+ }
+
+ /**
+ * @see RecyclerMixin#getRecyclerView()
+ */
+ public RecyclerView getRecyclerView() {
+ return mRecyclerMixin.getRecyclerView();
+ }
+
+ @Override
+ protected ViewGroup findContainer(int containerId) {
+ if (containerId == 0) {
+ containerId = R.id.suw_recycler_view;
+ }
+ return super.findContainer(containerId);
+ }
+
+ @Override
+ protected View onInflateTemplate(LayoutInflater inflater, int template) {
+ if (template == 0) {
+ template = R.layout.suw_recycler_template;
+ }
+ return super.onInflateTemplate(inflater, template);
+ }
+
+ @Override
+ protected void onTemplateInflated() {
+ final View recyclerView = findViewById(R.id.suw_recycler_view);
+ if (recyclerView instanceof RecyclerView) {
+ mRecyclerMixin = new RecyclerMixin(this, (RecyclerView) recyclerView);
+ } else {
+ throw new IllegalStateException(
+ "SetupWizardRecyclerLayout should use a template with recycler view");
+ }
+ }
+
+ @Override
+ public View findManagedViewById(int id) {
+ final View header = mRecyclerMixin.getHeader();
+ if (header != null) {
+ final View view = header.findViewById(id);
+ if (view != null) {
+ return view;
+ }
+ }
+ return super.findViewById(id);
+ }
+
+ /**
+ * Sets the start inset of the divider. This will use the default divider drawable set in the
+ * theme and inset it {@code inset} pixels to the right (or left in RTL layouts).
+ *
+ * @param inset The number of pixels to inset on the "start" side of the list divider. Typically
+ * this will be either {@code @dimen/suw_items_icon_divider_inset} or
+ * {@code @dimen/suw_items_text_divider_inset}.
+ *
+ * @see RecyclerMixin#setDividerInset(int)
+ */
+ public void setDividerInset(int inset) {
+ mRecyclerMixin.setDividerInset(inset);
+ }
+
+ /**
+ * @see RecyclerMixin#getDividerInset()
+ */
+ public int getDividerInset() {
+ return mRecyclerMixin.getDividerInset();
+ }
+
+ /**
+ * @see RecyclerMixin#getDivider()
+ */
+ public Drawable getDivider() {
+ return mRecyclerMixin.getDivider();
+ }
+}
diff --git a/library/recyclerview/src/com/android/setupwizardlib/items/ItemViewHolder.java b/library/recyclerview/src/com/android/setupwizardlib/items/ItemViewHolder.java
new file mode 100644
index 0000000..231f81d
--- /dev/null
+++ b/library/recyclerview/src/com/android/setupwizardlib/items/ItemViewHolder.java
@@ -0,0 +1,58 @@
+/*
+ * 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.support.v7.widget.RecyclerView;
+import android.view.View;
+
+import com.android.setupwizardlib.DividerItemDecoration;
+
+class ItemViewHolder extends RecyclerView.ViewHolder
+ implements DividerItemDecoration.DividedViewHolder {
+
+ private boolean mIsEnabled;
+ private IItem mItem;
+
+ ItemViewHolder(View itemView) {
+ super(itemView);
+ }
+
+ @Override
+ public boolean isDividerAllowedAbove() {
+ return mIsEnabled;
+ }
+
+ @Override
+ public boolean isDividerAllowedBelow() {
+ return mIsEnabled;
+ }
+
+ public void setEnabled(boolean isEnabled) {
+ mIsEnabled = isEnabled;
+ itemView.setClickable(isEnabled);
+ itemView.setEnabled(isEnabled);
+ itemView.setFocusable(isEnabled);
+ }
+
+ public void setItem(IItem item) {
+ mItem = item;
+ }
+
+ public IItem getItem() {
+ return mItem;
+ }
+}
diff --git a/library/recyclerview/src/com/android/setupwizardlib/items/RecyclerItemAdapter.java b/library/recyclerview/src/com/android/setupwizardlib/items/RecyclerItemAdapter.java
new file mode 100644
index 0000000..a676c60
--- /dev/null
+++ b/library/recyclerview/src/com/android/setupwizardlib/items/RecyclerItemAdapter.java
@@ -0,0 +1,248 @@
+/*
+ * 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.res.TypedArray;
+import android.graphics.Rect;
+import android.graphics.drawable.Drawable;
+import android.graphics.drawable.LayerDrawable;
+import android.support.annotation.VisibleForTesting;
+import android.support.v7.widget.RecyclerView;
+import android.util.Log;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+
+import com.android.setupwizardlib.R;
+
+/**
+ * An adapter used with RecyclerView to display an {@link ItemHierarchy}. The item hierarchy used to
+ * create this adapter can be inflated by {@link com.android.setupwizardlib.items.ItemInflater} from
+ * XML.
+ */
+public class RecyclerItemAdapter extends RecyclerView.Adapter<ItemViewHolder>
+ implements ItemHierarchy.Observer {
+
+ private static final String TAG = "RecyclerItemAdapter";
+
+ /**
+ * A view tag set by {@link View#setTag(Object)}. If set on the root view of a layout, it will
+ * not create the default background for the list item. This means the item will not have ripple
+ * touch feedback by default.
+ */
+ public static final String TAG_NO_BACKGROUND = "noBackground";
+
+ /**
+ * Listener for item selection in this adapter.
+ */
+ public interface OnItemSelectedListener {
+
+ /**
+ * Called when an item in this adapter is clicked.
+ *
+ * @param item The Item corresponding to the position being clicked.
+ */
+ void onItemSelected(IItem item);
+ }
+
+ private final ItemHierarchy mItemHierarchy;
+ private OnItemSelectedListener mListener;
+
+ public RecyclerItemAdapter(ItemHierarchy hierarchy) {
+ mItemHierarchy = hierarchy;
+ mItemHierarchy.registerObserver(this);
+ }
+
+ /**
+ * Gets the item at the given position.
+ *
+ * @see ItemHierarchy#getItemAt(int)
+ */
+ public IItem getItem(int position) {
+ return mItemHierarchy.getItemAt(position);
+ }
+
+ @Override
+ public long getItemId(int position) {
+ IItem mItem = getItem(position);
+ if (mItem instanceof AbstractItem) {
+ final int id = ((AbstractItem) mItem).getId();
+ return id > 0 ? id : RecyclerView.NO_ID;
+ } else {
+ return RecyclerView.NO_ID;
+ }
+ }
+
+ @Override
+ public int getItemCount() {
+ return mItemHierarchy.getCount();
+ }
+
+ @Override
+ public ItemViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
+ final LayoutInflater inflater = LayoutInflater.from(parent.getContext());
+ final View view = inflater.inflate(viewType, parent, false);
+ final ItemViewHolder viewHolder = new ItemViewHolder(view);
+
+ final Object viewTag = view.getTag();
+ if (!TAG_NO_BACKGROUND.equals(viewTag)) {
+ final TypedArray typedArray = parent.getContext()
+ .obtainStyledAttributes(R.styleable.SuwRecyclerItemAdapter);
+ Drawable selectableItemBackground = typedArray.getDrawable(
+ R.styleable.SuwRecyclerItemAdapter_android_selectableItemBackground);
+ if (selectableItemBackground == null) {
+ selectableItemBackground = typedArray.getDrawable(
+ R.styleable.SuwRecyclerItemAdapter_selectableItemBackground);
+ }
+
+ final Drawable background = typedArray.getDrawable(
+ R.styleable.SuwRecyclerItemAdapter_android_colorBackground);
+
+ if (selectableItemBackground == null || background == null) {
+ Log.e(TAG, "Cannot resolve required attributes."
+ + " selectableItemBackground=" + selectableItemBackground
+ + " background=" + background);
+ } else {
+ final Drawable[] layers = {background, selectableItemBackground};
+ view.setBackgroundDrawable(new PatchedLayerDrawable(layers));
+ }
+
+ typedArray.recycle();
+ }
+
+ view.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View view) {
+ final IItem item = viewHolder.getItem();
+ if (mListener != null && item != null && item.isEnabled()) {
+ mListener.onItemSelected(item);
+ }
+ }
+ });
+
+ return viewHolder;
+ }
+
+ @Override
+ public void onBindViewHolder(ItemViewHolder holder, int position) {
+ final IItem item = getItem(position);
+ item.onBindView(holder.itemView);
+ holder.setEnabled(item.isEnabled());
+ holder.setItem(item);
+ }
+
+ @Override
+ public int getItemViewType(int position) {
+ // Use layout resource as item view type. RecyclerView item type does not have to be
+ // contiguous.
+ IItem item = getItem(position);
+ return item.getLayoutResource();
+ }
+
+ @Override
+ public void onChanged(ItemHierarchy hierarchy) {
+ notifyDataSetChanged();
+ }
+
+ @Override
+ public void onItemRangeChanged(ItemHierarchy itemHierarchy, int positionStart, int itemCount) {
+ notifyItemRangeChanged(positionStart, itemCount);
+ }
+
+ @Override
+ public void onItemRangeInserted(ItemHierarchy itemHierarchy, int positionStart, int itemCount) {
+ notifyItemRangeInserted(positionStart, itemCount);
+ }
+
+ @Override
+ public void onItemRangeMoved(ItemHierarchy itemHierarchy, int fromPosition, int toPosition,
+ int itemCount) {
+ // There is no notifyItemRangeMoved
+ // https://code.google.com/p/android/issues/detail?id=125984
+ if (itemCount == 1) {
+ notifyItemMoved(fromPosition, toPosition);
+ } else {
+ // If more than one, degenerate into the catch-all data set changed callback, since I'm
+ // not sure how recycler view handles multiple calls to notifyItemMoved (if the result
+ // is committed after every notification then naively calling
+ // notifyItemMoved(from + i, to + i) is wrong).
+ // Logging this in case this is a more common occurrence than expected.
+ Log.i(TAG, "onItemRangeMoved with more than one item");
+ notifyDataSetChanged();
+ }
+ }
+
+ @Override
+ public void onItemRangeRemoved(ItemHierarchy itemHierarchy, int positionStart, int itemCount) {
+ notifyItemRangeRemoved(positionStart, itemCount);
+ }
+
+ /**
+ * Find an item hierarchy within the root hierarchy.
+ *
+ * @see ItemHierarchy#findItemById(int)
+ */
+ public ItemHierarchy findItemById(int id) {
+ return mItemHierarchy.findItemById(id);
+ }
+
+ /**
+ * Gets the root item hierarchy in this adapter.
+ */
+ public ItemHierarchy getRootItemHierarchy() {
+ return mItemHierarchy;
+ }
+
+ /**
+ * Sets the listener to listen for when user clicks on a item.
+ *
+ * @see OnItemSelectedListener
+ */
+ public void setOnItemSelectedListener(OnItemSelectedListener listener) {
+ mListener = listener;
+ }
+
+ /**
+ * Before Lollipop, LayerDrawable always return true in getPadding, even if the children layers
+ * do not have any padding. Patch the implementation so that getPadding returns false if the
+ * padding is empty.
+ *
+ * When getPadding is true, the padding of the view will be replaced by the padding of the
+ * drawable when {@link View#setBackgroundDrawable(Drawable)} is called. This patched class
+ * makes sure layer drawables without padding does not clear out original padding on the view.
+ */
+ @VisibleForTesting
+ static class PatchedLayerDrawable extends LayerDrawable {
+
+ /**
+ * {@inheritDoc}
+ */
+ PatchedLayerDrawable(Drawable[] layers) {
+ super(layers);
+ }
+
+ @Override
+ public boolean getPadding(Rect padding) {
+ final boolean superHasPadding = super.getPadding(padding);
+ return superHasPadding
+ && !(padding.left == 0
+ && padding.top == 0
+ && padding.right == 0
+ && padding.bottom == 0);
+ }
+ }
+}
diff --git a/library/recyclerview/src/com/android/setupwizardlib/template/RecyclerMixin.java b/library/recyclerview/src/com/android/setupwizardlib/template/RecyclerMixin.java
new file mode 100644
index 0000000..56751d4
--- /dev/null
+++ b/library/recyclerview/src/com/android/setupwizardlib/template/RecyclerMixin.java
@@ -0,0 +1,235 @@
+/*
+ * Copyright (C) 2017 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.template;
+
+import android.content.Context;
+import android.content.res.TypedArray;
+import android.graphics.drawable.Drawable;
+import android.os.Build;
+import android.os.Build.VERSION_CODES;
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
+import android.support.v7.widget.LinearLayoutManager;
+import android.support.v7.widget.RecyclerView;
+import android.support.v7.widget.RecyclerView.Adapter;
+import android.support.v7.widget.RecyclerView.ViewHolder;
+import android.util.AttributeSet;
+import android.view.View;
+
+import com.android.setupwizardlib.DividerItemDecoration;
+import com.android.setupwizardlib.R;
+import com.android.setupwizardlib.TemplateLayout;
+import com.android.setupwizardlib.items.ItemHierarchy;
+import com.android.setupwizardlib.items.ItemInflater;
+import com.android.setupwizardlib.items.RecyclerItemAdapter;
+import com.android.setupwizardlib.util.DrawableLayoutDirectionHelper;
+import com.android.setupwizardlib.view.HeaderRecyclerView;
+import com.android.setupwizardlib.view.HeaderRecyclerView.HeaderAdapter;
+
+/**
+ * A {@link Mixin} for interacting with templates with recycler views. This mixin constructor takes
+ * the instance of the recycler view to allow it to be instantiated dynamically, as in the case for
+ * preference fragments.
+ *
+ * <p>Unlike typical mixins, this mixin is designed to be created in onTemplateInflated, which is
+ * called by the super constructor, and then parse the XML attributes later in the constructor.
+ */
+public class RecyclerMixin implements Mixin {
+
+ private TemplateLayout mTemplateLayout;
+
+ @NonNull
+ private final RecyclerView mRecyclerView;
+
+ @Nullable
+ private View mHeader;
+
+ @NonNull
+ private DividerItemDecoration mDividerDecoration;
+
+ private Drawable mDefaultDivider;
+ private Drawable mDivider;
+ private int mDividerInset;
+
+ /**
+ * Creates the RecyclerMixin. Unlike typical mixins which are created in the constructor, this
+ * mixin should be called in {@link TemplateLayout#onTemplateInflated()}, which is called by
+ * the super constructor, because the recycler view and the header needs to be made available
+ * before other mixins from the super class.
+ *
+ * @param layout The layout this mixin belongs to.
+ */
+ public RecyclerMixin(@NonNull TemplateLayout layout, @NonNull RecyclerView recyclerView) {
+ mTemplateLayout = layout;
+
+ mDividerDecoration = new DividerItemDecoration(mTemplateLayout.getContext());
+
+ // The recycler view needs to be available
+ mRecyclerView = recyclerView;
+ mRecyclerView.setLayoutManager(new LinearLayoutManager(mTemplateLayout.getContext()));
+
+ if (recyclerView instanceof HeaderRecyclerView) {
+ mHeader = ((HeaderRecyclerView) recyclerView).getHeader();
+ }
+
+ mRecyclerView.addItemDecoration(mDividerDecoration);
+ }
+
+ /**
+ * Parse XML attributes and configures this mixin and the recycler view accordingly. This should
+ * be called from the constructor of the layout.
+ *
+ * @param attrs The {@link AttributeSet} as passed into the constructor. Can be null if the
+ * layout was not created from XML.
+ * @param defStyleAttr The default style attribute as passed into the layout constructor. Can be
+ * 0 if it is not needed.
+ */
+ public void parseAttributes(@Nullable AttributeSet attrs, int defStyleAttr) {
+ final Context context = mTemplateLayout.getContext();
+ final TypedArray a = context.obtainStyledAttributes(
+ attrs, R.styleable.SuwRecyclerMixin, defStyleAttr, 0);
+
+ final int entries = a.getResourceId(R.styleable.SuwRecyclerMixin_android_entries, 0);
+ if (entries != 0) {
+ final ItemHierarchy inflated = new ItemInflater(context).inflate(entries);
+ final RecyclerItemAdapter adapter = new RecyclerItemAdapter(inflated);
+ adapter.setHasStableIds(a.getBoolean(
+ R.styleable.SuwRecyclerMixin_suwHasStableIds, false));
+ setAdapter(adapter);
+ }
+ int dividerInset =
+ a.getDimensionPixelSize(R.styleable.SuwRecyclerMixin_suwDividerInset, 0);
+ setDividerInset(dividerInset);
+ a.recycle();
+ }
+
+ /**
+ * @return The recycler view contained in the layout, as marked by
+ * {@code @id/suw_recycler_view}. This will return {@code null} if the recycler view
+ * doesn't exist in the layout.
+ */
+ @SuppressWarnings("NullableProblems") // If clients guarantee that the template has a recycler
+ // view, and call this after the template is inflated,
+ // this will not return null.
+ public RecyclerView getRecyclerView() {
+ return mRecyclerView;
+ }
+
+ /**
+ * Gets the header view of the recycler layout. This is useful for other mixins if they need to
+ * access views within the header, usually via {@link TemplateLayout#findManagedViewById(int)}.
+ */
+ @SuppressWarnings("NullableProblems") // If clients guarantee that the template has a header,
+ // this call will not return null.
+ public View getHeader() {
+ return mHeader;
+ }
+
+ /**
+ * Recycler mixin needs to update the dividers if the layout direction has changed. This method
+ * should be called when {@link View#onLayout(boolean, int, int, int, int)} of the template
+ * is called.
+ */
+ public void onLayout() {
+ if (mDivider == null) {
+ // Update divider in case layout direction has just been resolved
+ updateDivider();
+ }
+ }
+
+ /**
+ * Gets the adapter of the recycler view in this layout. If the adapter includes a header,
+ * this method will unwrap it and return the underlying adapter.
+ *
+ * @return The adapter, or {@code null} if the recycler view has no adapter.
+ */
+ public Adapter<? extends ViewHolder> getAdapter() {
+ @SuppressWarnings("unchecked") // RecyclerView.getAdapter returns raw type :(
+ final RecyclerView.Adapter<? extends ViewHolder> adapter = mRecyclerView.getAdapter();
+ if (adapter instanceof HeaderAdapter) {
+ return ((HeaderAdapter<? extends ViewHolder>) adapter).getWrappedAdapter();
+ }
+ return adapter;
+ }
+
+ /**
+ * Sets the adapter on the recycler view in this layout.
+ */
+ public void setAdapter(Adapter<? extends ViewHolder> adapter) {
+ mRecyclerView.setAdapter(adapter);
+ }
+
+ /**
+ * Sets the start inset of the divider. This will use the default divider drawable set in the
+ * theme and inset it {@code inset} pixels to the right (or left in RTL layouts).
+ *
+ * @param inset The number of pixels to inset on the "start" side of the list divider. Typically
+ * this will be either {@code @dimen/suw_items_glif_icon_divider_inset} or
+ * {@code @dimen/suw_items_glif_text_divider_inset}.
+ */
+ public void setDividerInset(int inset) {
+ mDividerInset = inset;
+ updateDivider();
+ }
+
+ /**
+ * @return The number of pixels inset on the start side of the divider.
+ */
+ public int getDividerInset() {
+ return mDividerInset;
+ }
+
+ private void updateDivider() {
+ boolean shouldUpdate = true;
+ if (Build.VERSION.SDK_INT >= VERSION_CODES.KITKAT) {
+ shouldUpdate = mTemplateLayout.isLayoutDirectionResolved();
+ }
+ if (shouldUpdate) {
+ if (mDefaultDivider == null) {
+ mDefaultDivider = mDividerDecoration.getDivider();
+ }
+ mDivider = DrawableLayoutDirectionHelper.createRelativeInsetDrawable(
+ mDefaultDivider,
+ mDividerInset /* start */,
+ 0 /* top */,
+ 0 /* end */,
+ 0 /* bottom */,
+ mTemplateLayout);
+ mDividerDecoration.setDivider(mDivider);
+ }
+ }
+
+ /**
+ * @return The drawable used as the divider.
+ */
+ public Drawable getDivider() {
+ return mDivider;
+ }
+
+ /**
+ * Sets the divider item decoration directly. This is a low level method which should be used
+ * only if custom divider behavior is needed, for example if the divider should be shown /
+ * hidden in some specific cases for view holders that cannot implement
+ * {@link com.android.setupwizardlib.DividerItemDecoration.DividedViewHolder}.
+ */
+ public void setDividerItemDecoration(@NonNull DividerItemDecoration decoration) {
+ mRecyclerView.removeItemDecoration(mDividerDecoration);
+ mDividerDecoration = decoration;
+ mRecyclerView.addItemDecoration(mDividerDecoration);
+ updateDivider();
+ }
+}
diff --git a/library/recyclerview/src/com/android/setupwizardlib/template/RecyclerViewScrollHandlingDelegate.java b/library/recyclerview/src/com/android/setupwizardlib/template/RecyclerViewScrollHandlingDelegate.java
new file mode 100644
index 0000000..41fb03e
--- /dev/null
+++ b/library/recyclerview/src/com/android/setupwizardlib/template/RecyclerViewScrollHandlingDelegate.java
@@ -0,0 +1,83 @@
+/*
+ * Copyright (C) 2017 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.template;
+
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
+import android.support.v7.widget.RecyclerView;
+import android.util.Log;
+
+import com.android.setupwizardlib.template.RequireScrollMixin.ScrollHandlingDelegate;
+
+/**
+ * {@link ScrollHandlingDelegate} which analyzes scroll events from {@link RecyclerView} and
+ * notifies {@link RequireScrollMixin} about scrollability changes.
+ */
+public class RecyclerViewScrollHandlingDelegate implements ScrollHandlingDelegate {
+
+ private static final String TAG = "RVRequireScrollMixin";
+
+ @Nullable
+ private final RecyclerView mRecyclerView;
+
+ @NonNull
+ private final RequireScrollMixin mRequireScrollMixin;
+
+ public RecyclerViewScrollHandlingDelegate(
+ @NonNull RequireScrollMixin requireScrollMixin,
+ @Nullable RecyclerView recyclerView) {
+ mRequireScrollMixin = requireScrollMixin;
+ mRecyclerView = recyclerView;
+ }
+
+ private boolean canScrollDown() {
+ if (mRecyclerView != null) {
+ // Compatibility implementation of View#canScrollVertically
+ final int offset = mRecyclerView.computeVerticalScrollOffset();
+ final int range = mRecyclerView.computeVerticalScrollRange()
+ - mRecyclerView.computeVerticalScrollExtent();
+ return range != 0 && offset < range - 1;
+ }
+ return false;
+ }
+
+ @Override
+ public void startListening() {
+ if (mRecyclerView != null) {
+ mRecyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() {
+ @Override
+ public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
+ mRequireScrollMixin.notifyScrollabilityChange(canScrollDown());
+ }
+ });
+
+ if (canScrollDown()) {
+ mRequireScrollMixin.notifyScrollabilityChange(true);
+ }
+ } else {
+ Log.w(TAG, "Cannot require scroll. Recycler view is null.");
+ }
+ }
+
+ @Override
+ public void pageScrollDown() {
+ if (mRecyclerView != null) {
+ final int height = mRecyclerView.getHeight();
+ mRecyclerView.smoothScrollBy(0, height);
+ }
+ }
+}
diff --git a/library/recyclerview/src/com/android/setupwizardlib/view/HeaderRecyclerView.java b/library/recyclerview/src/com/android/setupwizardlib/view/HeaderRecyclerView.java
new file mode 100644
index 0000000..cf13d01
--- /dev/null
+++ b/library/recyclerview/src/com/android/setupwizardlib/view/HeaderRecyclerView.java
@@ -0,0 +1,276 @@
+/*
+ * 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 android.widget.FrameLayout;
+
+import com.android.setupwizardlib.DividerItemDecoration;
+import com.android.setupwizardlib.R;
+
+/**
+ * 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
+ implements DividerItemDecoration.DividedViewHolder {
+
+ HeaderViewHolder(View itemView) {
+ super(itemView);
+ }
+
+ @Override
+ public boolean isDividerAllowedAbove() {
+ return false;
+ }
+
+ @Override
+ public boolean isDividerAllowedBelow() {
+ return false;
+ }
+ }
+
+ /**
+ * An adapter that can optionally add one header item to the RecyclerView.
+ *
+ * @param <CVH> Type of the content view holder. i.e. view holder type of the wrapped adapter.
+ */
+ public static class HeaderAdapter<CVH extends ViewHolder>
+ extends RecyclerView.Adapter<ViewHolder> {
+
+ private static final int HEADER_VIEW_TYPE = Integer.MAX_VALUE;
+
+ private RecyclerView.Adapter<CVH> mAdapter;
+ private View mHeader;
+
+ private final AdapterDataObserver mObserver = new AdapterDataObserver() {
+
+ @Override
+ public void onChanged() {
+ notifyDataSetChanged();
+ }
+
+ @Override
+ public void onItemRangeChanged(int positionStart, int itemCount) {
+ if (mHeader != null) {
+ positionStart++;
+ }
+ notifyItemRangeChanged(positionStart, itemCount);
+ }
+
+ @Override
+ public void onItemRangeInserted(int positionStart, int itemCount) {
+ if (mHeader != null) {
+ positionStart++;
+ }
+ notifyItemRangeInserted(positionStart, itemCount);
+ }
+
+ @Override
+ public void onItemRangeMoved(int fromPosition, int toPosition, int itemCount) {
+ if (mHeader != null) {
+ fromPosition++;
+ toPosition++;
+ }
+ // Why is there no notifyItemRangeMoved?
+ for (int i = 0; i < itemCount; i++) {
+ notifyItemMoved(fromPosition + i, toPosition + i);
+ }
+ }
+
+ @Override
+ public void onItemRangeRemoved(int positionStart, int itemCount) {
+ if (mHeader != null) {
+ positionStart++;
+ }
+ notifyItemRangeRemoved(positionStart, itemCount);
+ }
+ };
+
+ public HeaderAdapter(RecyclerView.Adapter<CVH> adapter) {
+ mAdapter = adapter;
+ mAdapter.registerAdapterDataObserver(mObserver);
+ setHasStableIds(mAdapter.hasStableIds());
+ }
+
+ @Override
+ public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
+ // Returning the same view (mHeader) results in crash ".. but view is not a real child."
+ // The framework creates more than one instance of header because of "disappear"
+ // animations applied on the header and this necessitates creation of another header
+ // view to use after the animation. We work around this restriction by returning an
+ // empty FrameLayout to which the header is attached using #onBindViewHolder method.
+ if (viewType == HEADER_VIEW_TYPE) {
+ FrameLayout frameLayout = new FrameLayout(parent.getContext());
+ FrameLayout.LayoutParams params = new FrameLayout.LayoutParams(
+ FrameLayout.LayoutParams.MATCH_PARENT,
+ FrameLayout.LayoutParams.WRAP_CONTENT);
+ frameLayout.setLayoutParams(params);
+ return new HeaderViewHolder(frameLayout);
+ } else {
+ return mAdapter.onCreateViewHolder(parent, viewType);
+ }
+ }
+
+ @Override
+ @SuppressWarnings("unchecked") // Non-header position always return type CVH
+ public void onBindViewHolder(ViewHolder holder, int position) {
+ if (mHeader != null) {
+ position--;
+ }
+
+ if (holder instanceof HeaderViewHolder) {
+ if (mHeader == null) {
+ throw new IllegalStateException("HeaderViewHolder cannot find mHeader");
+ }
+ if (mHeader.getParent() != null) {
+ ((ViewGroup) mHeader.getParent()).removeView(mHeader);
+ }
+ FrameLayout mHeaderParent = (FrameLayout) holder.itemView;
+ mHeaderParent.addView(mHeader);
+ } else {
+ mAdapter.onBindViewHolder((CVH) 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;
+ }
+
+ public RecyclerView.Adapter<CVH> 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;
+ }
+
+ /**
+ * Set the view to use as the header of this recycler view.
+ * Note: This must be called before setAdapter.
+ */
+ public void setHeader(View header) {
+ mHeader = header;
+ }
+
+ @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
+ @SuppressWarnings("rawtypes,unchecked") // RecyclerView.setAdapter uses raw type :(
+ 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/recyclerview/src/com/android/setupwizardlib/view/StickyHeaderRecyclerView.java b/library/recyclerview/src/com/android/setupwizardlib/view/StickyHeaderRecyclerView.java
new file mode 100644
index 0000000..d51ea56
--- /dev/null
+++ b/library/recyclerview/src/com/android/setupwizardlib/view/StickyHeaderRecyclerView.java
@@ -0,0 +1,143 @@
+/*
+ * 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.setupwizardlib.view;
+
+import android.annotation.TargetApi;
+import android.content.Context;
+import android.graphics.Canvas;
+import android.graphics.RectF;
+import android.os.Build;
+import android.util.AttributeSet;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.WindowInsets;
+
+/**
+ * This class provides sticky header functionality in a recycler view, to use with
+ * SetupWizardIllustration. To use this, add a header tagged with "sticky". The header will continue
+ * to be drawn when the sticky element hits the top of the view.
+ *
+ * <p>There are a few things to note:
+ * <ol>
+ * <li>The view does not work well with padding. b/16190933
+ * <li>If fitsSystemWindows is true, then this will offset the sticking position by the height of
+ * the system decorations at the top of the screen.
+ * </ol>
+ */
+public class StickyHeaderRecyclerView extends HeaderRecyclerView {
+
+ private View mSticky;
+ private int mStatusBarInset = 0;
+ private RectF mStickyRect = new RectF();
+
+ public StickyHeaderRecyclerView(Context context) {
+ super(context);
+ }
+
+ public StickyHeaderRecyclerView(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ }
+
+ public StickyHeaderRecyclerView(Context context, AttributeSet attrs, int defStyleAttr) {
+ super(context, attrs, defStyleAttr);
+ }
+
+ @Override
+ protected void onLayout(boolean changed, int l, int t, int r, int b) {
+ super.onLayout(changed, l, t, r, b);
+ if (mSticky == null) {
+ updateStickyView();
+ }
+ if (mSticky != null) {
+ final View headerView = getHeader();
+ if (headerView != null && headerView.getHeight() == 0) {
+ headerView.layout(0, -headerView.getMeasuredHeight(),
+ headerView.getMeasuredWidth(), 0);
+ }
+ }
+ }
+
+ @Override
+ protected void onMeasure(int widthSpec, int heightSpec) {
+ super.onMeasure(widthSpec, heightSpec);
+ if (mSticky != null) {
+ measureChild(getHeader(), widthSpec, heightSpec);
+ }
+ }
+
+ /**
+ * Call this method when the "sticky" view has changed, so this view can update its internal
+ * states as well.
+ */
+ public void updateStickyView() {
+ final View header = getHeader();
+ if (header != null) {
+ mSticky = header.findViewWithTag("sticky");
+ }
+ }
+
+ @Override
+ public void draw(Canvas canvas) {
+ super.draw(canvas);
+ if (mSticky != null) {
+ final View headerView = getHeader();
+ final int saveCount = canvas.save();
+ // The view to draw when sticking to the top
+ final View drawTarget = headerView != null ? headerView : mSticky;
+ // The offset to draw the view at when sticky
+ final int drawOffset = headerView != null ? mSticky.getTop() : 0;
+ // Position of the draw target, relative to the outside of the scrollView
+ final int drawTop = drawTarget.getTop();
+ if (drawTop + drawOffset < mStatusBarInset || !drawTarget.isShown()) {
+ // RecyclerView does not translate the canvas, so we can simply draw at the top
+ mStickyRect.set(0, -drawOffset + mStatusBarInset, drawTarget.getWidth(),
+ drawTarget.getHeight() - drawOffset + mStatusBarInset);
+ canvas.translate(0, mStickyRect.top);
+ canvas.clipRect(0, 0, drawTarget.getWidth(), drawTarget.getHeight());
+ drawTarget.draw(canvas);
+ } else {
+ mStickyRect.setEmpty();
+ }
+ canvas.restoreToCount(saveCount);
+ }
+ }
+
+ @Override
+ @TargetApi(Build.VERSION_CODES.LOLLIPOP)
+ public WindowInsets onApplyWindowInsets(WindowInsets insets) {
+ if (getFitsSystemWindows()) {
+ mStatusBarInset = insets.getSystemWindowInsetTop();
+ insets.replaceSystemWindowInsets(
+ insets.getSystemWindowInsetLeft(),
+ 0, /* top */
+ insets.getSystemWindowInsetRight(),
+ insets.getSystemWindowInsetBottom()
+ );
+ }
+ return insets;
+ }
+
+ @Override
+ public boolean dispatchTouchEvent(MotionEvent ev) {
+ if (mStickyRect.contains(ev.getX(), ev.getY())) {
+ ev.offsetLocation(-mStickyRect.left, -mStickyRect.top);
+ return getHeader().dispatchTouchEvent(ev);
+ } else {
+ return super.dispatchTouchEvent(ev);
+ }
+ }
+}
diff --git a/library/recyclerview/test/instrumentation/res/layout/test_glif_recycler_layout.xml b/library/recyclerview/test/instrumentation/res/layout/test_glif_recycler_layout.xml
new file mode 100644
index 0000000..45a3928
--- /dev/null
+++ b/library/recyclerview/test/instrumentation/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/recyclerview/test/instrumentation/res/layout/test_list_item.xml b/library/recyclerview/test/instrumentation/res/layout/test_list_item.xml
new file mode 100644
index 0000000..220067d
--- /dev/null
+++ b/library/recyclerview/test/instrumentation/res/layout/test_list_item.xml
@@ -0,0 +1,20 @@
+<!--
+ Copyright (C) 2017 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.
+-->
+
+<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:tag="foobar" />
diff --git a/library/recyclerview/test/instrumentation/res/layout/test_list_item_no_background.xml b/library/recyclerview/test/instrumentation/res/layout/test_list_item_no_background.xml
new file mode 100644
index 0000000..0968e92
--- /dev/null
+++ b/library/recyclerview/test/instrumentation/res/layout/test_list_item_no_background.xml
@@ -0,0 +1,20 @@
+<!--
+ Copyright (C) 2017 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.
+-->
+
+<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:tag="noBackground" />
diff --git a/library/recyclerview/test/instrumentation/res/layout/test_recycler_layout.xml b/library/recyclerview/test/instrumentation/res/layout/test_recycler_layout.xml
new file mode 100644
index 0000000..8b7602e
--- /dev/null
+++ b/library/recyclerview/test/instrumentation/res/layout/test_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.SetupWizardRecyclerLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent" />
diff --git a/library/recyclerview/test/instrumentation/src/com/android/setupwizardlib/items/RecyclerItemAdapterTest.java b/library/recyclerview/test/instrumentation/src/com/android/setupwizardlib/items/RecyclerItemAdapterTest.java
new file mode 100644
index 0000000..3867bfe
--- /dev/null
+++ b/library/recyclerview/test/instrumentation/src/com/android/setupwizardlib/items/RecyclerItemAdapterTest.java
@@ -0,0 +1,144 @@
+/*
+ * 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 static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertSame;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.Matchers.anyObject;
+import static org.mockito.Matchers.eq;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+
+import android.graphics.Rect;
+import android.graphics.drawable.Drawable;
+import android.graphics.drawable.ShapeDrawable;
+import android.graphics.drawable.shapes.RectShape;
+import android.support.test.InstrumentationRegistry;
+import android.support.test.filters.SmallTest;
+import android.support.test.runner.AndroidJUnit4;
+import android.support.v7.widget.RecyclerView.AdapterDataObserver;
+import android.widget.FrameLayout;
+
+import com.android.setupwizardlib.items.RecyclerItemAdapter.PatchedLayerDrawable;
+import com.android.setupwizardlib.test.R;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class RecyclerItemAdapterTest {
+
+ private Item[] mItems = new Item[5];
+ private ItemGroup mItemGroup = new ItemGroup();
+
+ @Before
+ public void setUp() throws Exception {
+ for (int i = 0; i < 5; i++) {
+ Item item = new Item();
+ item.setTitle("TestTitle" + i);
+ item.setId(i);
+ // Layout resource: 0 -> 1, 1 -> 11, 2 -> 21, 3 -> 1, 4 -> 11.
+ // (Resource IDs cannot be 0)
+ item.setLayoutResource((i % 3) * 10 + 1);
+ mItems[i] = item;
+ mItemGroup.addChild(item);
+ }
+ }
+
+ @Test
+ public void testAdapter() {
+ RecyclerItemAdapter adapter = new RecyclerItemAdapter(mItemGroup);
+ assertEquals("Adapter should have 5 items", 5, adapter.getItemCount());
+ assertEquals("Adapter should return the first item", mItems[0], adapter.getItem(0));
+ assertEquals("ID should be same as position", 2, adapter.getItemId(2));
+
+ // ViewType is same as layout resource for RecyclerItemAdapter
+ assertEquals("Second item should have view type 21", 21, adapter.getItemViewType(2));
+ }
+
+ @Test
+ public void testGetRootItemHierarchy() {
+ RecyclerItemAdapter adapter = new RecyclerItemAdapter(mItemGroup);
+ ItemHierarchy root = adapter.getRootItemHierarchy();
+ assertSame("Root item hierarchy should be mItemGroup", mItemGroup, root);
+ }
+
+ @Test
+ public void testPatchedLayerDrawableNoPadding() {
+ ShapeDrawable child = new ShapeDrawable(new RectShape());
+ child.setPadding(0, 0, 0, 0);
+ PatchedLayerDrawable drawable = new PatchedLayerDrawable(new Drawable[] { child });
+
+ Rect padding = new Rect();
+ assertFalse("Patched layer drawable should not have padding", drawable.getPadding(padding));
+ assertEquals(new Rect(0, 0, 0, 0), padding);
+ }
+
+ @Test
+ public void testPatchedLayerDrawableWithPadding() {
+ ShapeDrawable child = new ShapeDrawable(new RectShape());
+ child.setPadding(10, 10, 10, 10);
+ PatchedLayerDrawable drawable = new PatchedLayerDrawable(new Drawable[] { child });
+
+ Rect padding = new Rect();
+ assertTrue("Patched layer drawable should have padding", drawable.getPadding(padding));
+ assertEquals(new Rect(10, 10, 10, 10), padding);
+ }
+
+ @Test
+ public void testAdapterNotifications() {
+ RecyclerItemAdapter adapter = new RecyclerItemAdapter(mItemGroup);
+ final AdapterDataObserver observer = mock(AdapterDataObserver.class);
+ adapter.registerAdapterDataObserver(observer);
+
+ mItems[0].setTitle("Child 1");
+ verify(observer).onItemRangeChanged(eq(0), eq(1), anyObject());
+
+ mItemGroup.removeChild(mItems[1]);
+ verify(observer).onItemRangeRemoved(eq(1), eq(1));
+
+ mItemGroup.addChild(mItems[1]);
+ verify(observer).onItemRangeInserted(eq(4), eq(1));
+ }
+
+ @Test
+ public void testCreateViewHolder() {
+ RecyclerItemAdapter adapter = new RecyclerItemAdapter(mItemGroup);
+ FrameLayout parent = new FrameLayout(InstrumentationRegistry.getContext());
+
+ final ItemViewHolder viewHolder =
+ adapter.onCreateViewHolder(parent, R.layout.test_list_item);
+ assertNotNull("Background should be set", viewHolder.itemView.getBackground());
+ assertEquals("foobar", viewHolder.itemView.getTag());
+ }
+
+ @Test
+ public void testCreateViewHolderNoBcakground() {
+ RecyclerItemAdapter adapter = new RecyclerItemAdapter(mItemGroup);
+ FrameLayout parent = new FrameLayout(InstrumentationRegistry.getContext());
+
+ final ItemViewHolder viewHolder =
+ adapter.onCreateViewHolder(parent, R.layout.test_list_item_no_background);
+ assertNull("Background should be null", viewHolder.itemView.getBackground());
+ }
+}
diff --git a/library/recyclerview/test/instrumentation/src/com/android/setupwizardlib/template/RecyclerMixinTest.java b/library/recyclerview/test/instrumentation/src/com/android/setupwizardlib/template/RecyclerMixinTest.java
new file mode 100644
index 0000000..79105d6
--- /dev/null
+++ b/library/recyclerview/test/instrumentation/src/com/android/setupwizardlib/template/RecyclerMixinTest.java
@@ -0,0 +1,132 @@
+/*
+ * Copyright (C) 2017 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.template;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertSame;
+import static org.mockito.AdditionalAnswers.delegatesTo;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.spy;
+
+import android.content.Context;
+import android.graphics.Rect;
+import android.graphics.drawable.Drawable;
+import android.graphics.drawable.InsetDrawable;
+import android.os.Build.VERSION;
+import android.os.Build.VERSION_CODES;
+import android.support.test.InstrumentationRegistry;
+import android.support.test.filters.SmallTest;
+import android.support.test.runner.AndroidJUnit4;
+import android.support.v7.widget.RecyclerView;
+import android.support.v7.widget.RecyclerView.Adapter;
+import android.view.View;
+
+import com.android.setupwizardlib.TemplateLayout;
+import com.android.setupwizardlib.test.R;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class RecyclerMixinTest {
+
+ private Context mContext;
+ private TemplateLayout mTemplateLayout;
+
+ private RecyclerView mRecyclerView;
+
+ @Mock
+ private Adapter mAdapter;
+
+ @Before
+ public void setUp() {
+ MockitoAnnotations.initMocks(this);
+
+ mContext = InstrumentationRegistry.getTargetContext();
+ mTemplateLayout = spy(new TemplateLayout(mContext, R.layout.test_template,
+ R.id.suw_layout_content));
+
+ mRecyclerView = mock(RecyclerView.class, delegatesTo(new RecyclerView(mContext)));
+
+ doReturn(true).when(mTemplateLayout).isLayoutDirectionResolved();
+ }
+
+ @Test
+ public void testGetRecyclerView() {
+ RecyclerMixin mixin = new RecyclerMixin(mTemplateLayout, mRecyclerView);
+ assertSame(mRecyclerView, mixin.getRecyclerView());
+ }
+
+ @Test
+ public void testGetAdapter() {
+ mRecyclerView.setAdapter(mAdapter);
+
+ RecyclerMixin mixin = new RecyclerMixin(mTemplateLayout, mRecyclerView);
+ assertSame(mAdapter, mixin.getAdapter());
+ }
+
+ @Test
+ public void testSetAdapter() {
+ assertNull(mRecyclerView.getAdapter());
+
+ RecyclerMixin mixin = new RecyclerMixin(mTemplateLayout, mRecyclerView);
+ mixin.setAdapter(mAdapter);
+
+ assertSame(mAdapter, mRecyclerView.getAdapter());
+ }
+
+ @Test
+ public void testDividerInset() {
+ RecyclerMixin mixin = new RecyclerMixin(mTemplateLayout, mRecyclerView);
+ mixin.setDividerInset(123);
+
+ assertEquals(123, mixin.getDividerInset());
+
+ final Drawable divider = mixin.getDivider();
+ InsetDrawable insetDrawable = (InsetDrawable) divider;
+ Rect rect = new Rect();
+ insetDrawable.getPadding(rect);
+
+ assertEquals(new Rect(123, 0, 0, 0), rect);
+ }
+
+ @Test
+ public void testDividerInsetRtl() {
+ if (VERSION.SDK_INT >= VERSION_CODES.JELLY_BEAN_MR1) {
+ doReturn(View.LAYOUT_DIRECTION_RTL).when(mTemplateLayout).getLayoutDirection();
+
+ RecyclerMixin mixin = new RecyclerMixin(mTemplateLayout, mRecyclerView);
+ mixin.setDividerInset(123);
+
+ assertEquals(123, mixin.getDividerInset());
+
+ final Drawable divider = mixin.getDivider();
+ InsetDrawable insetDrawable = (InsetDrawable) divider;
+ Rect rect = new Rect();
+ insetDrawable.getPadding(rect);
+
+ assertEquals(new Rect(0, 0, 123, 0), rect);
+ }
+ // else the test passes
+ }
+}
diff --git a/library/recyclerview/test/instrumentation/src/com/android/setupwizardlib/test/DividerItemDecorationTest.java b/library/recyclerview/test/instrumentation/src/com/android/setupwizardlib/test/DividerItemDecorationTest.java
new file mode 100644
index 0000000..747d1ba
--- /dev/null
+++ b/library/recyclerview/test/instrumentation/src/com/android/setupwizardlib/test/DividerItemDecorationTest.java
@@ -0,0 +1,220 @@
+/*
+ * 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 static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertSame;
+
+import android.content.Context;
+import android.graphics.Bitmap;
+import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.Paint;
+import android.graphics.PorterDuff;
+import android.graphics.PorterDuffXfermode;
+import android.graphics.drawable.ColorDrawable;
+import android.graphics.drawable.Drawable;
+import android.support.test.InstrumentationRegistry;
+import android.support.test.filters.SmallTest;
+import android.support.test.runner.AndroidJUnit4;
+import android.support.v7.widget.LinearLayoutManager;
+import android.support.v7.widget.RecyclerView;
+import android.view.View;
+import android.view.ViewGroup;
+
+import com.android.setupwizardlib.DividerItemDecoration;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class DividerItemDecorationTest {
+
+ @Test
+ public void testDivider() {
+ final DividerItemDecoration decoration = new DividerItemDecoration();
+ Drawable divider = new ColorDrawable();
+ decoration.setDivider(divider);
+ assertSame("Divider should be same as set", divider, decoration.getDivider());
+ }
+
+ @Test
+ public void testDividerHeight() {
+ final DividerItemDecoration decoration = new DividerItemDecoration();
+ decoration.setDividerHeight(123);
+ assertEquals("Divider height should be 123", 123, decoration.getDividerHeight());
+ }
+
+ @Test
+ public void testShouldDrawDividerBelowWithEitherCondition() {
+ // Set up the item decoration, with 1px red divider line
+ final DividerItemDecoration decoration = new DividerItemDecoration();
+ Drawable divider = new ColorDrawable(Color.RED);
+ decoration.setDivider(divider);
+ decoration.setDividerHeight(1);
+
+ Bitmap bitmap = drawDecoration(decoration, true, true);
+
+ // Draw the expected result on a bitmap
+ Bitmap expectedBitmap = Bitmap.createBitmap(20, 20, Bitmap.Config.ARGB_4444);
+ Canvas expectedCanvas = new Canvas(expectedBitmap);
+ Paint paint = new Paint();
+ paint.setColor(Color.RED);
+ expectedCanvas.drawRect(0, 5, 20, 6, paint);
+ expectedCanvas.drawRect(0, 10, 20, 11, paint);
+ expectedCanvas.drawRect(0, 15, 20, 16, paint);
+ // Compare the two bitmaps
+ assertBitmapEquals(expectedBitmap, bitmap);
+
+ bitmap.recycle();
+ bitmap = drawDecoration(decoration, false, true);
+ // should still be the same.
+ assertBitmapEquals(expectedBitmap, bitmap);
+
+ bitmap.recycle();
+ bitmap = drawDecoration(decoration, true, false);
+ // last item should not have a divider below it now
+ paint.setColor(Color.TRANSPARENT);
+ paint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.CLEAR));
+ expectedCanvas.drawRect(0, 15, 20, 16, paint);
+ assertBitmapEquals(expectedBitmap, bitmap);
+
+ bitmap.recycle();
+ bitmap = drawDecoration(decoration, false, false);
+ // everything should be transparent now
+ expectedCanvas.drawRect(0, 5, 20, 6, paint);
+ expectedCanvas.drawRect(0, 10, 20, 11, paint);
+ assertBitmapEquals(expectedBitmap, bitmap);
+
+ }
+
+ @Test
+ public void testShouldDrawDividerBelowWithBothCondition() {
+ // Set up the item decoration, with 1px green divider line
+ final DividerItemDecoration decoration = new DividerItemDecoration();
+ Drawable divider = new ColorDrawable(Color.GREEN);
+ decoration.setDivider(divider);
+ decoration.setDividerHeight(1);
+ decoration.setDividerCondition(DividerItemDecoration.DIVIDER_CONDITION_BOTH);
+
+ Bitmap bitmap = drawDecoration(decoration, true, true);
+ Paint paint = new Paint();
+ paint.setColor(Color.GREEN);
+ paint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.ADD));
+ Bitmap expectedBitmap = Bitmap.createBitmap(20, 20, Bitmap.Config.ARGB_4444);
+ Canvas expectedCanvas = new Canvas(expectedBitmap);
+ expectedCanvas.drawRect(0, 5, 20, 6, paint);
+ expectedCanvas.drawRect(0, 10, 20, 11, paint);
+ expectedCanvas.drawRect(0, 15, 20, 16, paint);
+ // Should have all the dividers
+ assertBitmapEquals(expectedBitmap, bitmap);
+
+ bitmap.recycle();
+ bitmap = drawDecoration(decoration, false, true);
+ paint.setColor(Color.TRANSPARENT);
+ paint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.CLEAR));
+ expectedCanvas.drawRect(0, 5, 20, 6, paint);
+ expectedCanvas.drawRect(0, 10, 20, 11, paint);
+ assertBitmapEquals(expectedBitmap, bitmap);
+
+ bitmap.recycle();
+ bitmap = drawDecoration(decoration, true, false);
+ // nothing should be drawn now.
+ expectedCanvas.drawRect(0, 15, 20, 16, paint);
+ assertBitmapEquals(expectedBitmap, bitmap);
+
+ bitmap.recycle();
+ bitmap = drawDecoration(decoration, false, false);
+ assertBitmapEquals(expectedBitmap, bitmap);
+ }
+
+ private Bitmap drawDecoration(DividerItemDecoration decoration, final boolean allowDividerAbove,
+ final boolean allowDividerBelow) {
+ // Set up the canvas to be drawn
+ Bitmap bitmap = Bitmap.createBitmap(20, 20, Bitmap.Config.ARGB_4444);
+ Canvas canvas = new Canvas(bitmap);
+
+ final Context context = InstrumentationRegistry.getContext();
+ // Set up recycler view with vertical linear layout manager
+ RecyclerView testRecyclerView = new RecyclerView(context);
+ testRecyclerView.setLayoutManager(new LinearLayoutManager(context));
+
+ // Set up adapter with 3 items, each 5px tall
+ testRecyclerView.setAdapter(new RecyclerView.Adapter() {
+ @Override
+ public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup viewGroup, int i) {
+ final View itemView = new View(context);
+ itemView.setMinimumWidth(20);
+ itemView.setMinimumHeight(5);
+ return ViewHolder.createInstance(itemView, allowDividerAbove, allowDividerBelow);
+ }
+
+ @Override
+ public void onBindViewHolder(RecyclerView.ViewHolder viewHolder, int i) {
+ }
+
+ @Override
+ public int getItemCount() {
+ return 3;
+ }
+ });
+
+ testRecyclerView.layout(0, 0, 20, 20);
+ decoration.onDraw(canvas, testRecyclerView, null);
+ return bitmap;
+ }
+
+ private void assertBitmapEquals(Bitmap expected, Bitmap actual) {
+ assertEquals("Width should be the same", expected.getWidth(), actual.getWidth());
+ assertEquals("Height should be the same", expected.getHeight(), actual.getHeight());
+ for (int x = 0; x < expected.getWidth(); x++) {
+ for (int y = 0; y < expected.getHeight(); y++) {
+ assertEquals("Pixel at (" + x + ", " + y + ") should be the same",
+ expected.getPixel(x, y), actual.getPixel(x, y));
+ }
+ }
+ }
+
+ private static class ViewHolder extends RecyclerView.ViewHolder
+ implements DividerItemDecoration.DividedViewHolder {
+
+ private boolean mAllowDividerAbove;
+ private boolean mAllowDividerBelow;
+
+ public static ViewHolder createInstance(View itemView, boolean allowDividerAbove,
+ boolean allowDividerBelow) {
+ return new ViewHolder(itemView, allowDividerAbove, allowDividerBelow);
+ }
+
+ private ViewHolder(View itemView, boolean allowDividerAbove, boolean allowDividerBelow) {
+ super(itemView);
+ mAllowDividerAbove = allowDividerAbove;
+ mAllowDividerBelow = allowDividerBelow;
+ }
+
+ @Override
+ public boolean isDividerAllowedAbove() {
+ return mAllowDividerAbove;
+ }
+
+ @Override
+ public boolean isDividerAllowedBelow() {
+ return mAllowDividerBelow;
+ }
+ }
+}
diff --git a/library/recyclerview/test/instrumentation/src/com/android/setupwizardlib/test/GlifPreferenceLayoutTest.java b/library/recyclerview/test/instrumentation/src/com/android/setupwizardlib/test/GlifPreferenceLayoutTest.java
new file mode 100644
index 0000000..791e11f
--- /dev/null
+++ b/library/recyclerview/test/instrumentation/src/com/android/setupwizardlib/test/GlifPreferenceLayoutTest.java
@@ -0,0 +1,104 @@
+/*
+ * 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.setupwizardlib.test;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+
+import android.content.Context;
+import android.graphics.drawable.Drawable;
+import android.graphics.drawable.InsetDrawable;
+import android.os.Build;
+import android.support.test.InstrumentationRegistry;
+import android.support.test.filters.SmallTest;
+import android.support.test.runner.AndroidJUnit4;
+import android.support.v7.widget.RecyclerView;
+import android.view.ContextThemeWrapper;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+
+import com.android.setupwizardlib.GlifPreferenceLayout;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class GlifPreferenceLayoutTest {
+
+ private Context mContext;
+
+ @Before
+ public void setUp() throws Exception {
+ mContext = new ContextThemeWrapper(InstrumentationRegistry.getContext(),
+ R.style.SuwThemeGlif_Light);
+ }
+
+ @Test
+ public void testDefaultTemplate() {
+ GlifPreferenceLayout layout = new GlifPreferenceLayout(mContext);
+ assertPreferenceTemplateInflated(layout);
+ }
+
+ @Test
+ public void testGetRecyclerView() {
+ GlifPreferenceLayout layout = new GlifPreferenceLayout(mContext);
+ assertPreferenceTemplateInflated(layout);
+ assertNotNull("getRecyclerView should not be null", layout.getRecyclerView());
+ }
+
+ @Test
+ public void testOnCreateRecyclerView() {
+ GlifPreferenceLayout layout = new GlifPreferenceLayout(mContext);
+ assertPreferenceTemplateInflated(layout);
+ final RecyclerView recyclerView = layout.onCreateRecyclerView(LayoutInflater.from(mContext),
+ layout, null /* savedInstanceState */);
+ assertNotNull("RecyclerView created should not be null", recyclerView);
+ }
+
+ @Test
+ public void testDividerInset() {
+ GlifPreferenceLayout layout = new GlifPreferenceLayout(mContext);
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
+ layout.setLayoutDirection(View.LAYOUT_DIRECTION_LTR);
+ }
+ assertPreferenceTemplateInflated(layout);
+
+ layout.addView(layout.onCreateRecyclerView(LayoutInflater.from(mContext), layout,
+ null /* savedInstanceState */));
+
+ layout.setDividerInset(10);
+ assertEquals("Divider inset should be 10", 10, layout.getDividerInset());
+
+ final Drawable divider = layout.getDivider();
+ assertTrue("Divider should be instance of InsetDrawable", divider instanceof InsetDrawable);
+ }
+
+ private void assertPreferenceTemplateInflated(GlifPreferenceLayout layout) {
+ View contentContainer = layout.findViewById(R.id.suw_layout_content);
+ assertTrue("@id/suw_layout_content should be a ViewGroup",
+ contentContainer instanceof ViewGroup);
+
+ assertNotNull("Header text view should not be null",
+ layout.findManagedViewById(R.id.suw_layout_title));
+ assertNotNull("Icon view should not be null",
+ layout.findManagedViewById(R.id.suw_layout_icon));
+ }
+}
diff --git a/library/recyclerview/test/instrumentation/src/com/android/setupwizardlib/test/GlifRecyclerLayoutTest.java b/library/recyclerview/test/instrumentation/src/com/android/setupwizardlib/test/GlifRecyclerLayoutTest.java
new file mode 100644
index 0000000..b27564d
--- /dev/null
+++ b/library/recyclerview/test/instrumentation/src/com/android/setupwizardlib/test/GlifRecyclerLayoutTest.java
@@ -0,0 +1,160 @@
+/*
+ * 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 static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertSame;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+import android.content.Context;
+import android.graphics.drawable.Drawable;
+import android.graphics.drawable.InsetDrawable;
+import android.os.Build;
+import android.support.test.InstrumentationRegistry;
+import android.support.test.filters.SmallTest;
+import android.support.test.runner.AndroidJUnit4;
+import android.support.v7.widget.RecyclerView;
+import android.support.v7.widget.RecyclerView.Adapter;
+import android.view.ContextThemeWrapper;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.View.MeasureSpec;
+import android.view.ViewGroup;
+
+import com.android.setupwizardlib.GlifRecyclerLayout;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class GlifRecyclerLayoutTest {
+
+ private Context mContext;
+
+ @Before
+ public void setUp() throws Exception {
+ mContext = new ContextThemeWrapper(InstrumentationRegistry.getContext(),
+ R.style.SuwThemeGlif_Light);
+ }
+
+ @Test
+ public void testDefaultTemplate() {
+ GlifRecyclerLayout layout = new GlifRecyclerLayout(mContext);
+ assertRecyclerTemplateInflated(layout);
+ }
+
+ @Test
+ public void testInflateFromXml() {
+ LayoutInflater inflater = LayoutInflater.from(mContext);
+ GlifRecyclerLayout layout = (GlifRecyclerLayout)
+ inflater.inflate(R.layout.test_glif_recycler_layout, null);
+ assertRecyclerTemplateInflated(layout);
+ }
+
+ @Test
+ public void testGetRecyclerView() {
+ GlifRecyclerLayout layout = new GlifRecyclerLayout(mContext);
+ assertRecyclerTemplateInflated(layout);
+ assertNotNull("getRecyclerView should not be null", layout.getRecyclerView());
+ }
+
+ @Test
+ public void testAdapter() {
+ GlifRecyclerLayout layout = new GlifRecyclerLayout(mContext);
+ assertRecyclerTemplateInflated(layout);
+
+ final RecyclerView.Adapter adapter = createTestAdapter(1);
+ layout.setAdapter(adapter);
+
+ final RecyclerView.Adapter gotAdapter = layout.getAdapter();
+ // Note: The wrapped adapter should be returned, not the HeaderAdapter.
+ assertSame("Adapter got from GlifRecyclerLayout should be same as set",
+ adapter, gotAdapter);
+ }
+
+ @Test
+ public void testLayout() {
+ GlifRecyclerLayout layout = new GlifRecyclerLayout(mContext);
+ assertRecyclerTemplateInflated(layout);
+
+ layout.setAdapter(createTestAdapter(3));
+
+ layout.measure(
+ MeasureSpec.makeMeasureSpec(500, MeasureSpec.EXACTLY),
+ MeasureSpec.makeMeasureSpec(500, MeasureSpec.EXACTLY));
+ layout.layout(0, 0, 500, 500);
+ // Test that the layout code doesn't crash.
+ }
+
+ @Test
+ public void testDividerInset() {
+ GlifRecyclerLayout layout = new GlifRecyclerLayout(mContext);
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
+ layout.setLayoutDirection(View.LAYOUT_DIRECTION_LTR);
+ }
+ assertRecyclerTemplateInflated(layout);
+
+ layout.setDividerInset(10);
+ assertEquals("Divider inset should be 10", 10, layout.getDividerInset());
+
+ final Drawable divider = layout.getDivider();
+ assertTrue("Divider should be instance of InsetDrawable", divider instanceof InsetDrawable);
+ }
+
+ @Test
+ public void testTemplateWithNoRecyclerView() {
+ try {
+ new GlifRecyclerLayout(mContext, R.layout.suw_glif_template);
+ fail("Creating GlifRecyclerLayout with no recycler view should throw exception");
+ } catch (Exception e) {
+ // pass
+ }
+ }
+
+ 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);
+
+ assertNotNull("Header text view should not be null",
+ layout.findManagedViewById(R.id.suw_layout_title));
+ assertNotNull("Icon view should not be null",
+ layout.findManagedViewById(R.id.suw_layout_icon));
+ }
+
+ private Adapter createTestAdapter(final int itemCount) {
+ return 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 itemCount;
+ }
+ };
+ }
+}
diff --git a/library/recyclerview/test/instrumentation/src/com/android/setupwizardlib/test/HeaderRecyclerViewTest.java b/library/recyclerview/test/instrumentation/src/com/android/setupwizardlib/test/HeaderRecyclerViewTest.java
new file mode 100644
index 0000000..d9f52cd
--- /dev/null
+++ b/library/recyclerview/test/instrumentation/src/com/android/setupwizardlib/test/HeaderRecyclerViewTest.java
@@ -0,0 +1,177 @@
+/*
+ * 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 static org.mockito.Matchers.eq;
+import static org.mockito.Mockito.verify;
+
+import android.support.test.InstrumentationRegistry;
+import android.support.test.filters.SmallTest;
+import android.support.test.runner.AndroidJUnit4;
+import android.support.v7.widget.RecyclerView;
+import android.view.View;
+import android.view.ViewGroup;
+
+import com.android.setupwizardlib.view.HeaderRecyclerView.HeaderAdapter;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+/**
+ * Test for {@link com.android.setupwizardlib.view.HeaderRecyclerView}
+ */
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class HeaderRecyclerViewTest {
+
+ private TestAdapter mWrappedAdapter;
+ private HeaderAdapter mHeaderAdapter;
+
+ @Mock
+ private RecyclerView.AdapterDataObserver mObserver;
+
+ @Before
+ public void setUp() {
+ MockitoAnnotations.initMocks(this);
+
+ mWrappedAdapter = new TestAdapter();
+
+ mHeaderAdapter = new HeaderAdapter(mWrappedAdapter);
+ mHeaderAdapter.registerAdapterDataObserver(mObserver);
+ }
+
+ /**
+ * Test that notifyDataSetChanged gets propagated by HeaderRecyclerView's adapter.
+ */
+ @Test
+ public void testNotifyChanged() {
+ mWrappedAdapter.notifyDataSetChanged();
+
+ verify(mObserver).onChanged();
+ }
+
+ /**
+ * Test that notifyItemChanged gets propagated by HeaderRecyclerView's adapter.
+ */
+ @Test
+ public void testNotifyItemChangedNoHeader() {
+ mWrappedAdapter.notifyItemChanged(12);
+
+ verify(mObserver).onItemRangeChanged(eq(12), eq(1), eq(null));
+ }
+
+ /**
+ * Test that notifyItemChanged gets propagated by HeaderRecyclerView's adapter and adds 1 to the
+ * position for the extra header items.
+ */
+ @Test
+ public void testNotifyItemChangedWithHeader() {
+ mHeaderAdapter.setHeader(new View(InstrumentationRegistry.getTargetContext()));
+ mWrappedAdapter.notifyItemChanged(12);
+
+ verify(mObserver).onItemRangeChanged(eq(13), eq(1), eq(null));
+ }
+
+ /**
+ * Test that notifyItemInserted gets propagated by HeaderRecyclerView's adapter.
+ */
+ @Test
+ public void testNotifyItemInsertedNoHeader() {
+ mWrappedAdapter.notifyItemInserted(12);
+
+ verify(mObserver).onItemRangeInserted(eq(12), eq(1));
+ }
+
+ /**
+ * Test that notifyItemInserted gets propagated by HeaderRecyclerView's adapter and adds 1 to
+ * the position for the extra header item.
+ */
+ @Test
+ public void testNotifyItemInsertedWithHeader() {
+ mHeaderAdapter.setHeader(new View(InstrumentationRegistry.getTargetContext()));
+ mWrappedAdapter.notifyItemInserted(12);
+
+ verify(mObserver).onItemRangeInserted(eq(13), eq(1));
+ }
+
+ /**
+ * Test that notifyItemRemoved gets propagated by HeaderRecyclerView's adapter.
+ */
+ @Test
+ public void testNotifyItemRemovedNoHeader() {
+ mWrappedAdapter.notifyItemRemoved(12);
+
+ verify(mObserver).onItemRangeRemoved(eq(12), eq(1));
+ }
+
+ /**
+ * Test that notifyItemRemoved gets propagated by HeaderRecyclerView's adapter and adds 1 to
+ * the position for the extra header item.
+ */
+ @Test
+ public void testNotifyItemRemovedWithHeader() {
+ mHeaderAdapter.setHeader(new View(InstrumentationRegistry.getTargetContext()));
+ mWrappedAdapter.notifyItemRemoved(12);
+
+ verify(mObserver).onItemRangeRemoved(eq(13), eq(1));
+ }
+
+ /**
+ * Test that notifyItemMoved gets propagated by HeaderRecyclerView's adapter.
+ */
+ @Test
+ public void testNotifyItemMovedNoHeader() {
+ mWrappedAdapter.notifyItemMoved(12, 18);
+
+ verify(mObserver).onItemRangeMoved(eq(12), eq(18), eq(1));
+ }
+
+ /**
+ * Test that notifyItemMoved gets propagated by HeaderRecyclerView's adapter and adds 1 to
+ * the position for the extra header item.
+ */
+ @Test
+ public void testNotifyItemMovedWithHeader() {
+ mHeaderAdapter.setHeader(new View(InstrumentationRegistry.getTargetContext()));
+ mWrappedAdapter.notifyItemMoved(12, 18);
+
+ verify(mObserver).onItemRangeMoved(eq(13), eq(19), eq(1));
+ }
+
+ /**
+ * Test adapter to be wrapped inside {@link HeaderAdapter} to to send item change notifications.
+ */
+ public static class TestAdapter extends RecyclerView.Adapter {
+
+ @Override
+ public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup viewGroup, int i) {
+ return null;
+ }
+
+ @Override
+ public void onBindViewHolder(RecyclerView.ViewHolder viewHolder, int i) {
+ }
+
+ @Override
+ public int getItemCount() {
+ return 0;
+ }
+ }
+}
diff --git a/library/recyclerview/test/instrumentation/src/com/android/setupwizardlib/test/SetupWizardPreferenceLayoutTest.java b/library/recyclerview/test/instrumentation/src/com/android/setupwizardlib/test/SetupWizardPreferenceLayoutTest.java
new file mode 100644
index 0000000..486d2cf
--- /dev/null
+++ b/library/recyclerview/test/instrumentation/src/com/android/setupwizardlib/test/SetupWizardPreferenceLayoutTest.java
@@ -0,0 +1,104 @@
+/*
+ * 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 static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+
+import android.content.Context;
+import android.graphics.drawable.Drawable;
+import android.graphics.drawable.InsetDrawable;
+import android.os.Build;
+import android.support.test.InstrumentationRegistry;
+import android.support.test.filters.SmallTest;
+import android.support.test.runner.AndroidJUnit4;
+import android.support.v7.widget.RecyclerView;
+import android.view.ContextThemeWrapper;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+
+import com.android.setupwizardlib.SetupWizardPreferenceLayout;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class SetupWizardPreferenceLayoutTest {
+
+ private Context mContext;
+
+ @Before
+ public void setUp() throws Exception {
+ mContext = new ContextThemeWrapper(InstrumentationRegistry.getContext(),
+ R.style.SuwThemeMaterial_Light);
+ }
+
+ @Test
+ public void testDefaultTemplate() {
+ SetupWizardPreferenceLayout layout = new SetupWizardPreferenceLayout(mContext);
+ assertPreferenceTemplateInflated(layout);
+ }
+
+ @Test
+ public void testGetRecyclerView() {
+ SetupWizardPreferenceLayout layout = new SetupWizardPreferenceLayout(mContext);
+ assertPreferenceTemplateInflated(layout);
+ assertNotNull("getRecyclerView should not be null", layout.getRecyclerView());
+ }
+
+ @Test
+ public void testOnCreateRecyclerView() {
+ SetupWizardPreferenceLayout layout = new SetupWizardPreferenceLayout(mContext);
+ assertPreferenceTemplateInflated(layout);
+ final RecyclerView recyclerView = layout.onCreateRecyclerView(LayoutInflater.from(mContext),
+ layout, null /* savedInstanceState */);
+ assertNotNull("RecyclerView created should not be null", recyclerView);
+ }
+
+ @Test
+ public void testDividerInset() {
+ SetupWizardPreferenceLayout layout = new SetupWizardPreferenceLayout(mContext);
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
+ layout.setLayoutDirection(View.LAYOUT_DIRECTION_LTR);
+ }
+ assertPreferenceTemplateInflated(layout);
+
+ layout.addView(layout.onCreateRecyclerView(LayoutInflater.from(mContext), layout,
+ null /* savedInstanceState */));
+
+ layout.setDividerInset(10);
+ assertEquals("Divider inset should be 10", 10, layout.getDividerInset());
+
+ final Drawable divider = layout.getDivider();
+ assertTrue("Divider should be instance of InsetDrawable", divider instanceof InsetDrawable);
+ }
+
+ private void assertPreferenceTemplateInflated(SetupWizardPreferenceLayout layout) {
+ View contentContainer = layout.findViewById(R.id.suw_layout_content);
+ assertTrue("@id/suw_layout_content should be a ViewGroup",
+ contentContainer instanceof ViewGroup);
+
+ assertNotNull("Header text view should not be null",
+ layout.findManagedViewById(R.id.suw_layout_title));
+ assertNotNull("Decoration view should not be null",
+ layout.findManagedViewById(R.id.suw_layout_decor));
+ }
+}
diff --git a/library/recyclerview/test/instrumentation/src/com/android/setupwizardlib/test/SetupWizardRecyclerLayoutTest.java b/library/recyclerview/test/instrumentation/src/com/android/setupwizardlib/test/SetupWizardRecyclerLayoutTest.java
new file mode 100644
index 0000000..4a72992
--- /dev/null
+++ b/library/recyclerview/test/instrumentation/src/com/android/setupwizardlib/test/SetupWizardRecyclerLayoutTest.java
@@ -0,0 +1,164 @@
+/*
+ * 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 static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertSame;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+import android.content.Context;
+import android.graphics.drawable.Drawable;
+import android.graphics.drawable.InsetDrawable;
+import android.os.Build;
+import android.support.test.InstrumentationRegistry;
+import android.support.test.filters.SmallTest;
+import android.support.test.runner.AndroidJUnit4;
+import android.support.v7.widget.RecyclerView;
+import android.support.v7.widget.RecyclerView.Adapter;
+import android.support.v7.widget.RecyclerView.ViewHolder;
+import android.view.ContextThemeWrapper;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.View.MeasureSpec;
+import android.view.ViewGroup;
+
+import com.android.setupwizardlib.SetupWizardRecyclerLayout;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class SetupWizardRecyclerLayoutTest {
+
+ private Context mContext;
+
+ @Before
+ public void setUp() throws Exception {
+ mContext = new ContextThemeWrapper(InstrumentationRegistry.getContext(),
+ R.style.SuwThemeMaterial_Light);
+ }
+
+ @Test
+ public void testDefaultTemplate() {
+ SetupWizardRecyclerLayout layout = new SetupWizardRecyclerLayout(mContext);
+ assertRecyclerTemplateInflated(layout);
+ }
+
+ @Test
+ public void testInflateFromXml() {
+ LayoutInflater inflater = LayoutInflater.from(mContext);
+ SetupWizardRecyclerLayout layout = (SetupWizardRecyclerLayout)
+ inflater.inflate(R.layout.test_recycler_layout, null);
+ assertRecyclerTemplateInflated(layout);
+ }
+
+ @Test
+ public void testGetRecyclerView() {
+ SetupWizardRecyclerLayout layout = new SetupWizardRecyclerLayout(mContext);
+ assertRecyclerTemplateInflated(layout);
+ assertNotNull("getRecyclerView should not be null", layout.getRecyclerView());
+ }
+
+ @Test
+ public void testAdapter() {
+ SetupWizardRecyclerLayout layout = new SetupWizardRecyclerLayout(mContext);
+ assertRecyclerTemplateInflated(layout);
+
+ final Adapter adapter = createTestAdapter(1);
+ layout.setAdapter(adapter);
+
+ final Adapter gotAdapter = layout.getAdapter();
+ // Note: The wrapped adapter should be returned, not the HeaderAdapter.
+ assertSame("Adapter got from SetupWizardLayout should be same as set",
+ adapter, gotAdapter);
+ }
+
+ @Test
+ public void testLayout() {
+ SetupWizardRecyclerLayout layout = new SetupWizardRecyclerLayout(mContext);
+ assertRecyclerTemplateInflated(layout);
+
+ layout.setAdapter(createTestAdapter(3));
+
+ layout.measure(
+ MeasureSpec.makeMeasureSpec(500, MeasureSpec.EXACTLY),
+ MeasureSpec.makeMeasureSpec(500, MeasureSpec.EXACTLY));
+ layout.layout(0, 0, 500, 500);
+ // Test that the layout code doesn't crash.
+ }
+
+ @Test
+ public void testDividerInset() {
+ SetupWizardRecyclerLayout layout = new SetupWizardRecyclerLayout(mContext);
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
+ layout.setLayoutDirection(View.LAYOUT_DIRECTION_LTR);
+ }
+ assertRecyclerTemplateInflated(layout);
+
+ layout.setDividerInset(10);
+ assertEquals("Divider inset should be 10", 10, layout.getDividerInset());
+
+ final Drawable divider = layout.getDivider();
+ assertTrue("Divider should be instance of InsetDrawable", divider instanceof InsetDrawable);
+ }
+
+ @Test
+ public void testTemplateWithNoRecyclerView() {
+ try {
+ new SetupWizardRecyclerLayout(
+ mContext,
+ R.layout.suw_glif_template,
+ R.id.suw_recycler_view);
+ fail("Creating SetupWizardRecyclerLayout with no recycler view should throw exception");
+ } catch (Exception e) {
+ // pass
+ }
+ }
+
+ private void assertRecyclerTemplateInflated(SetupWizardRecyclerLayout layout) {
+ View recyclerView = layout.findViewById(R.id.suw_recycler_view);
+ assertTrue("@id/suw_recycler_view should be a RecyclerView",
+ recyclerView instanceof RecyclerView);
+
+ assertNotNull("Header text view should not be null",
+ layout.findManagedViewById(R.id.suw_layout_title));
+ assertNotNull("Decoration view should not be null",
+ layout.findManagedViewById(R.id.suw_layout_decor));
+ }
+
+ private Adapter createTestAdapter(final int itemCount) {
+ return new Adapter() {
+ @Override
+ public ViewHolder onCreateViewHolder(ViewGroup parent, int position) {
+ return new ViewHolder(new View(parent.getContext())) {};
+ }
+
+ @Override
+ public void onBindViewHolder(ViewHolder viewHolder, int position) {
+ }
+
+ @Override
+ public int getItemCount() {
+ return itemCount;
+ }
+ };
+ }
+}
diff --git a/library/recyclerview/test/robotest/src/com/android/setupwizardlib/template/RecyclerViewScrollHandlingDelegateTest.java b/library/recyclerview/test/robotest/src/com/android/setupwizardlib/template/RecyclerViewScrollHandlingDelegateTest.java
new file mode 100644
index 0000000..b509389
--- /dev/null
+++ b/library/recyclerview/test/robotest/src/com/android/setupwizardlib/template/RecyclerViewScrollHandlingDelegateTest.java
@@ -0,0 +1,89 @@
+/*
+ * Copyright (C) 2017 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.template;
+
+import static org.mockito.Matchers.anyInt;
+import static org.mockito.Matchers.eq;
+import static org.mockito.Mockito.doNothing;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.verify;
+import static org.robolectric.RuntimeEnvironment.application;
+
+import android.support.v7.widget.RecyclerView;
+import android.support.v7.widget.RecyclerView.OnScrollListener;
+
+import com.android.setupwizardlib.BuildConfig;
+import com.android.setupwizardlib.robolectric.SuwLibRobolectricTestRunner;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+import org.robolectric.annotation.Config;
+
+@Config(constants = BuildConfig.class, sdk = { Config.OLDEST_SDK, Config.NEWEST_SDK })
+@RunWith(SuwLibRobolectricTestRunner.class)
+public class RecyclerViewScrollHandlingDelegateTest {
+
+ @Mock
+ private RequireScrollMixin mRequireScrollMixin;
+
+ private RecyclerView mRecyclerView;
+ private RecyclerViewScrollHandlingDelegate mDelegate;
+ private ArgumentCaptor<OnScrollListener> mListenerCaptor;
+
+ @Before
+ public void setUp() throws Exception {
+ MockitoAnnotations.initMocks(this);
+
+ mRecyclerView = spy(new RecyclerView(application));
+ doReturn(20).when(mRecyclerView).computeVerticalScrollRange();
+ doReturn(0).when(mRecyclerView).computeVerticalScrollExtent();
+ doReturn(0).when(mRecyclerView).computeVerticalScrollOffset();
+ mListenerCaptor = ArgumentCaptor.forClass(OnScrollListener.class);
+ doNothing().when(mRecyclerView).addOnScrollListener(mListenerCaptor.capture());
+
+ mDelegate = new RecyclerViewScrollHandlingDelegate(mRequireScrollMixin, mRecyclerView);
+ mRecyclerView.layout(0, 0, 50, 50);
+ }
+
+ @Test
+ public void testRequireScroll() {
+ mDelegate.startListening();
+ verify(mRequireScrollMixin).notifyScrollabilityChange(true);
+ }
+
+ @Test
+ public void testScrolledToBottom() {
+ mDelegate.startListening();
+ verify(mRequireScrollMixin).notifyScrollabilityChange(true);
+
+ doReturn(20).when(mRecyclerView).computeVerticalScrollOffset();
+ mListenerCaptor.getValue().onScrolled(mRecyclerView, 0, 20);
+
+ verify(mRequireScrollMixin).notifyScrollabilityChange(false);
+ }
+
+ @Test
+ public void testClickScrollButton() {
+ mDelegate.pageScrollDown();
+ verify(mRecyclerView).smoothScrollBy(anyInt(), eq(50));
+ }
+}