diff options
author | Maurice Lam <yukl@google.com> | 2017-03-28 12:48:40 -0700 |
---|---|---|
committer | Maurice Lam <yukl@google.com> | 2017-03-28 14:23:21 -0700 |
commit | 83862bb59558fc044de9aa0d6e9407be53af8b81 (patch) | |
tree | 032855cc188420699c048478a6a24c44731a3151 /library/recyclerview | |
parent | 9955331ed7bda114488b1a4701456ec478ff63bf (diff) | |
download | setupwizard-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')
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 + * <style android:name="MyActivityTheme"> + * <item android:name="preferenceTheme">@style/MyPreferenceTheme</item> + * </style> + * + * <style android:name="MyPreferenceTheme"> + * <item android:name="preferenceFragmentStyle">@style/MyPreferenceFragmentStyle</item> + * </style> + * + * <style android:name="MyPreferenceFragmentStyle"> + * <item android:name="android:layout">@layout/my_preference_layout</item> + * </style> + * }</pre> + * + * where {@code my_preference_layout} is a layout that contains + * {@link com.android.setupwizardlib.GlifPreferenceLayout}. + * + * <p />Example: + * <pre>{@code + * <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 + * <style android:name="MyActivityTheme"> + * <item android:name="preferenceTheme">@style/MyPreferenceTheme</item> + * </style> + * + * <style android:name="MyPreferenceTheme"> + * <item android:name="preferenceFragmentStyle">@style/MyPreferenceFragmentStyle</item> + * </style> + * + * <style android:name="MyPreferenceFragmentStyle"> + * <item android:name="android:layout">@layout/my_preference_layout</item> + * </style> + * }</pre> + * + * where {@code my_preference_layout} is a layout that contains + * {@link com.android.setupwizardlib.SetupWizardPreferenceLayout}. + * + * <p />Example: + * <pre>{@code + * <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)); + } +} |