diff options
24 files changed, 1030 insertions, 778 deletions
diff --git a/library/full-support/src/com/android/setupwizardlib/GlifRecyclerLayout.java b/library/full-support/src/com/android/setupwizardlib/GlifRecyclerLayout.java index 755baec..d1a7947 100644 --- a/library/full-support/src/com/android/setupwizardlib/GlifRecyclerLayout.java +++ b/library/full-support/src/com/android/setupwizardlib/GlifRecyclerLayout.java @@ -29,6 +29,8 @@ 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 @@ -65,6 +67,10 @@ public class GlifRecyclerLayout extends GlifLayout { 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 diff --git a/library/full-support/src/com/android/setupwizardlib/SetupWizardRecyclerLayout.java b/library/full-support/src/com/android/setupwizardlib/SetupWizardRecyclerLayout.java index 228bfeb..870a805 100644 --- a/library/full-support/src/com/android/setupwizardlib/SetupWizardRecyclerLayout.java +++ b/library/full-support/src/com/android/setupwizardlib/SetupWizardRecyclerLayout.java @@ -22,14 +22,13 @@ 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.util.Log; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import com.android.setupwizardlib.template.RecyclerMixin; -import com.android.setupwizardlib.util.RecyclerViewRequireScrollHelper; -import com.android.setupwizardlib.view.NavigationBar; +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}. @@ -66,6 +65,11 @@ public class SetupWizardRecyclerLayout extends SetupWizardLayout { 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 @@ -134,18 +138,6 @@ public class SetupWizardRecyclerLayout extends SetupWizardLayout { return super.findViewById(id); } - @Override - public void requireScrollToBottom() { - final NavigationBar navigationBar = getNavigationBar(); - final RecyclerView recyclerView = getRecyclerView(); - if (navigationBar != null && recyclerView != null) { - RecyclerViewRequireScrollHelper.requireScroll(navigationBar, recyclerView); - } else { - Log.e(TAG, "Both suw_layout_navigation_bar and suw_recycler_view must exist in" - + " the template to require scrolling."); - } - } - /** * 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). diff --git a/library/full-support/src/com/android/setupwizardlib/template/RecyclerViewScrollHandlingDelegate.java b/library/full-support/src/com/android/setupwizardlib/template/RecyclerViewScrollHandlingDelegate.java new file mode 100644 index 0000000..41fb03e --- /dev/null +++ b/library/full-support/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/full-support/src/com/android/setupwizardlib/util/RecyclerViewRequireScrollHelper.java b/library/full-support/src/com/android/setupwizardlib/util/RecyclerViewRequireScrollHelper.java deleted file mode 100644 index ad9354a..0000000 --- a/library/full-support/src/com/android/setupwizardlib/util/RecyclerViewRequireScrollHelper.java +++ /dev/null @@ -1,75 +0,0 @@ -/* - * 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.util; - -import android.support.v7.widget.RecyclerView; - -import com.android.setupwizardlib.view.NavigationBar; - -/** - * Add this helper to require the recycler view to be scrolled to the bottom, making sure that the - * user sees all content on the screen. This will change the navigation bar to show the more button - * instead of the next button when there is more content to be seen. When the more button is - * clicked, the scroll view will be scrolled one page down. - */ -public class RecyclerViewRequireScrollHelper extends AbstractRequireScrollHelper { - - public static void requireScroll(NavigationBar navigationBar, RecyclerView recyclerView) { - new RecyclerViewRequireScrollHelper(navigationBar, recyclerView).requireScroll(); - } - - private final RecyclerView mRecyclerView; - - private RecyclerViewRequireScrollHelper(NavigationBar navigationBar, - RecyclerView recyclerView) { - super(navigationBar); - mRecyclerView = recyclerView; - } - - @Override - protected void requireScroll() { - super.requireScroll(); - mRecyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() { - @Override - public void onScrolled(RecyclerView recyclerView, int dx, int dy) { - if (!canScrollDown()) { - notifyScrolledToBottom(); - } else { - notifyRequiresScroll(); - } - } - }); - - if (canScrollDown()) { - notifyRequiresScroll(); - } - } - - private boolean canScrollDown() { - // Compatibility implementation of View#canScrollVertically - final int offset = mRecyclerView.computeVerticalScrollOffset(); - final int range = mRecyclerView.computeVerticalScrollRange() - - mRecyclerView.computeVerticalScrollExtent(); - return range != 0 && offset < range - 1; - } - - @Override - protected void pageScrollDown() { - final int height = mRecyclerView.getHeight(); - mRecyclerView.smoothScrollBy(0, height); - } -} diff --git a/library/full-support/test/instrumentation/src/com/android/setupwizardlib/test/RecyclerViewRequireScrollHelperTest.java b/library/full-support/test/instrumentation/src/com/android/setupwizardlib/test/RecyclerViewRequireScrollHelperTest.java deleted file mode 100644 index fae1f1a..0000000 --- a/library/full-support/test/instrumentation/src/com/android/setupwizardlib/test/RecyclerViewRequireScrollHelperTest.java +++ /dev/null @@ -1,146 +0,0 @@ -/* - * 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 android.content.Context; -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.View; - -import com.android.setupwizardlib.util.RecyclerViewRequireScrollHelper; -import com.android.setupwizardlib.view.NavigationBar; - -import org.junit.Before; -import org.junit.Test; -import org.junit.runner.RunWith; - -@RunWith(AndroidJUnit4.class) -@SmallTest -public class RecyclerViewRequireScrollHelperTest { - - private TestRecyclerView mRecyclerView; - private NavigationBar mNavigationBar; - - @Before - public void setUp() throws Exception { - final Context context = InstrumentationRegistry.getContext(); - mRecyclerView = new TestRecyclerView(context); - mNavigationBar = new TestNavigationBar(context); - - mRecyclerView.layout(0, 0, 50, 50); - } - - @Test - public void testRequireScroll() { - RecyclerViewRequireScrollHelper.requireScroll(mNavigationBar, mRecyclerView); - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.FROYO) { - assertEquals("More button should be shown initially", View.VISIBLE, - mNavigationBar.getMoreButton().getVisibility()); - assertEquals("Next button should be gone initially", View.GONE, - mNavigationBar.getNextButton().getVisibility()); - } - } - - @Test - public void testScrolledToBottom() { - RecyclerViewRequireScrollHelper.requireScroll(mNavigationBar, mRecyclerView); - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.FROYO) { - assertEquals("More button should be shown when scroll is required", View.VISIBLE, - mNavigationBar.getMoreButton().getVisibility()); - assertEquals("Next button should not be shown when scroll is required", View.GONE, - mNavigationBar.getNextButton().getVisibility()); - - mRecyclerView.scrollOffset = 20; - mRecyclerView.listener.onScrolled(mRecyclerView, 0, 20); - assertEquals("More button should be hidden when scrolled to bottom", View.GONE, - mNavigationBar.getMoreButton().getVisibility()); - assertEquals("Next button should be shown when scrolled to bottom", View.VISIBLE, - mNavigationBar.getNextButton().getVisibility()); - } - } - - @Test - public void testClickScrollButton() { - RecyclerViewRequireScrollHelper.requireScroll(mNavigationBar, mRecyclerView); - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.FROYO) { - assertEquals("ScrollView page should be initially 0", 0, mRecyclerView.scrollDistance); - mNavigationBar.getMoreButton().performClick(); - assertEquals("ScrollView page should be scrolled by 50px", - 50, mRecyclerView.scrollDistance); - } - } - - private static class TestRecyclerView extends RecyclerView { - - public int scrollOffset = 0; - public int scrollRange = 20; - public int scrollExtent = 0; - - public int scrollDistance = 0; - - public OnScrollListener listener; - - TestRecyclerView(Context context) { - super(context); - } - - @Override - public void addOnScrollListener(OnScrollListener listener) { - super.addOnScrollListener(listener); - this.listener = listener; - } - - @Override - public int computeVerticalScrollOffset() { - return scrollOffset; - } - - @Override - public int computeVerticalScrollRange() { - return scrollRange; - } - - @Override - public int computeVerticalScrollExtent() { - return scrollExtent; - } - - @Override - public void smoothScrollBy(int dx, int dy) { - super.smoothScrollBy(dx, dy); - scrollDistance += dy; - } - } - - private static class TestNavigationBar extends NavigationBar { - - TestNavigationBar(Context context) { - super(context); - } - - @Override - public boolean post(Runnable action) { - action.run(); - return true; - } - } -} diff --git a/library/full-support/test/robotest/src/com/android/setupwizardlib/template/RecyclerViewScrollHandlingDelegateTest.java b/library/full-support/test/robotest/src/com/android/setupwizardlib/template/RecyclerViewScrollHandlingDelegateTest.java new file mode 100644 index 0000000..b509389 --- /dev/null +++ b/library/full-support/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)); + } +} diff --git a/library/main/res/layout/suw_glif_template_content.xml b/library/main/res/layout/suw_glif_template_content.xml index f5a32a6..0fe35a0 100644 --- a/library/main/res/layout/suw_glif_template_content.xml +++ b/library/main/res/layout/suw_glif_template_content.xml @@ -24,7 +24,7 @@ <!-- Ignore UnusedAttribute: scrollIndicators is new in M. Default to no indicators in older versions. --> - <ScrollView + <com.android.setupwizardlib.view.BottomScrollView android:id="@+id/suw_scroll_view" android:layout_width="match_parent" android:layout_height="0dp" @@ -48,7 +48,7 @@ </LinearLayout> - </ScrollView> + </com.android.setupwizardlib.view.BottomScrollView> <ViewStub android:id="@+id/suw_layout_footer" diff --git a/library/main/src/com/android/setupwizardlib/GlifLayout.java b/library/main/src/com/android/setupwizardlib/GlifLayout.java index 667d699..f4d52a5 100644 --- a/library/main/src/com/android/setupwizardlib/GlifLayout.java +++ b/library/main/src/com/android/setupwizardlib/GlifLayout.java @@ -41,6 +41,8 @@ import com.android.setupwizardlib.template.ColoredHeaderMixin; import com.android.setupwizardlib.template.HeaderMixin; import com.android.setupwizardlib.template.IconMixin; import com.android.setupwizardlib.template.ProgressBarMixin; +import com.android.setupwizardlib.template.RequireScrollMixin; +import com.android.setupwizardlib.template.ScrollViewScrollHandlingDelegate; import com.android.setupwizardlib.view.StatusBarBackgroundLayout; /** @@ -106,6 +108,14 @@ public class GlifLayout extends TemplateLayout { registerMixin(IconMixin.class, new IconMixin(this, attrs, defStyleAttr)); registerMixin(ProgressBarMixin.class, new ProgressBarMixin(this)); registerMixin(ButtonFooterMixin.class, new ButtonFooterMixin(this)); + final RequireScrollMixin requireScrollMixin = new RequireScrollMixin(this); + registerMixin(RequireScrollMixin.class, requireScrollMixin); + + final ScrollView scrollView = getScrollView(); + if (scrollView != null) { + requireScrollMixin.setScrollHandlingDelegate( + new ScrollViewScrollHandlingDelegate(requireScrollMixin, scrollView)); + } TypedArray a = getContext().obtainStyledAttributes(attrs, R.styleable.SuwGlifLayout, defStyleAttr, 0); diff --git a/library/main/src/com/android/setupwizardlib/GlifListLayout.java b/library/main/src/com/android/setupwizardlib/GlifListLayout.java index 14c7bd7..c6443f9 100644 --- a/library/main/src/com/android/setupwizardlib/GlifListLayout.java +++ b/library/main/src/com/android/setupwizardlib/GlifListLayout.java @@ -28,6 +28,8 @@ import android.widget.ListAdapter; import android.widget.ListView; import com.android.setupwizardlib.template.ListMixin; +import com.android.setupwizardlib.template.ListViewScrollHandlingDelegate; +import com.android.setupwizardlib.template.RequireScrollMixin; /** * A GLIF themed layout with a ListView. {@code android:entries} can also be used to specify an @@ -70,6 +72,10 @@ public class GlifListLayout extends GlifLayout { private void init(Context context, AttributeSet attrs, int defStyleAttr) { mListMixin = new ListMixin(this, attrs, defStyleAttr); registerMixin(ListMixin.class, mListMixin); + + final RequireScrollMixin requireScrollMixin = getMixin(RequireScrollMixin.class); + requireScrollMixin.setScrollHandlingDelegate( + new ListViewScrollHandlingDelegate(requireScrollMixin, getListView())); } @Override diff --git a/library/main/src/com/android/setupwizardlib/SetupWizardLayout.java b/library/main/src/com/android/setupwizardlib/SetupWizardLayout.java index 2364e9b..065d2ef 100644 --- a/library/main/src/com/android/setupwizardlib/SetupWizardLayout.java +++ b/library/main/src/com/android/setupwizardlib/SetupWizardLayout.java @@ -42,8 +42,8 @@ import android.widget.TextView; import com.android.setupwizardlib.template.HeaderMixin; import com.android.setupwizardlib.template.NavigationBarMixin; import com.android.setupwizardlib.template.ProgressBarMixin; -import com.android.setupwizardlib.util.RequireScrollHelper; -import com.android.setupwizardlib.view.BottomScrollView; +import com.android.setupwizardlib.template.RequireScrollMixin; +import com.android.setupwizardlib.template.ScrollViewScrollHandlingDelegate; import com.android.setupwizardlib.view.Illustration; import com.android.setupwizardlib.view.NavigationBar; @@ -82,6 +82,14 @@ public class SetupWizardLayout extends TemplateLayout { registerMixin(HeaderMixin.class, new HeaderMixin(this, attrs, defStyleAttr)); registerMixin(ProgressBarMixin.class, new ProgressBarMixin(this)); registerMixin(NavigationBarMixin.class, new NavigationBarMixin(this)); + final RequireScrollMixin requireScrollMixin = new RequireScrollMixin(this); + registerMixin(RequireScrollMixin.class, requireScrollMixin); + + final ScrollView scrollView = getScrollView(); + if (scrollView != null) { + requireScrollMixin.setScrollHandlingDelegate( + new ScrollViewScrollHandlingDelegate(requireScrollMixin, scrollView)); + } final TypedArray a = getContext().obtainStyledAttributes(attrs, R.styleable.SuwSetupWizardLayout, defStyleAttr, 0); @@ -156,11 +164,7 @@ public class SetupWizardLayout extends TemplateLayout { final SavedState ss = (SavedState) state; super.onRestoreInstanceState(ss.getSuperState()); final boolean isProgressBarShown = ss.mIsProgressBarShown; - if (isProgressBarShown) { - showProgressBar(); - } else { - hideProgressBar(); - } + setProgressBarShown(isProgressBarShown); } @Override @@ -189,13 +193,12 @@ public class SetupWizardLayout extends TemplateLayout { } public void requireScrollToBottom() { + final RequireScrollMixin requireScrollMixin = getMixin(RequireScrollMixin.class); final NavigationBar navigationBar = getNavigationBar(); - final ScrollView scrollView = getScrollView(); - if (navigationBar != null && (scrollView instanceof BottomScrollView)) { - RequireScrollHelper.requireScroll(navigationBar, (BottomScrollView) scrollView); + if (navigationBar != null) { + requireScrollMixin.requireScrollWithNavigationBar(navigationBar); } else { - Log.e(TAG, "Both suw_layout_navigation_bar and suw_bottom_scroll_view must exist in" - + " the template to require scrolling."); + Log.e(TAG, "Cannot require scroll. Navigation bar is null."); } } diff --git a/library/main/src/com/android/setupwizardlib/SetupWizardListLayout.java b/library/main/src/com/android/setupwizardlib/SetupWizardListLayout.java index bb96d58..0457451 100644 --- a/library/main/src/com/android/setupwizardlib/SetupWizardListLayout.java +++ b/library/main/src/com/android/setupwizardlib/SetupWizardListLayout.java @@ -21,7 +21,6 @@ import android.content.Context; import android.graphics.drawable.Drawable; import android.os.Build.VERSION_CODES; import android.util.AttributeSet; -import android.util.Log; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; @@ -29,8 +28,8 @@ import android.widget.ListAdapter; import android.widget.ListView; import com.android.setupwizardlib.template.ListMixin; -import com.android.setupwizardlib.util.ListViewRequireScrollHelper; -import com.android.setupwizardlib.view.NavigationBar; +import com.android.setupwizardlib.template.ListViewScrollHandlingDelegate; +import com.android.setupwizardlib.template.RequireScrollMixin; public class SetupWizardListLayout extends SetupWizardLayout { @@ -65,6 +64,10 @@ public class SetupWizardListLayout extends SetupWizardLayout { private void init(Context context, AttributeSet attrs, int defStyleAttr) { mListMixin = new ListMixin(this, attrs, defStyleAttr); registerMixin(ListMixin.class, mListMixin); + + final RequireScrollMixin requireScrollMixin = getMixin(RequireScrollMixin.class); + requireScrollMixin.setScrollHandlingDelegate( + new ListViewScrollHandlingDelegate(requireScrollMixin, getListView())); } @Override @@ -101,18 +104,6 @@ public class SetupWizardListLayout extends SetupWizardLayout { return mListMixin.getAdapter(); } - @Override - public void requireScrollToBottom() { - final NavigationBar navigationBar = getNavigationBar(); - final ListView listView = getListView(); - if (navigationBar != null && listView != null) { - ListViewRequireScrollHelper.requireScroll(navigationBar, listView); - } else { - Log.e(TAG, "Both suw_layout_navigation_bar and list must exist in" - + " the template to require scrolling."); - } - } - /** * 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). diff --git a/library/main/src/com/android/setupwizardlib/template/ListViewScrollHandlingDelegate.java b/library/main/src/com/android/setupwizardlib/template/ListViewScrollHandlingDelegate.java new file mode 100644 index 0000000..f55d06d --- /dev/null +++ b/library/main/src/com/android/setupwizardlib/template/ListViewScrollHandlingDelegate.java @@ -0,0 +1,87 @@ +/* + * 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.util.Log; +import android.widget.AbsListView; +import android.widget.ListAdapter; +import android.widget.ListView; + +import com.android.setupwizardlib.template.RequireScrollMixin.ScrollHandlingDelegate; + +/** + * {@link ScrollHandlingDelegate} which analyzes scroll events from {@link ListView} and + * notifies {@link RequireScrollMixin} about scrollability changes. + */ +public class ListViewScrollHandlingDelegate implements ScrollHandlingDelegate, + AbsListView.OnScrollListener { + + private static final String TAG = "ListViewDelegate"; + + private static final int SCROLL_DURATION = 500; + + @NonNull + private final RequireScrollMixin mRequireScrollMixin; + + @Nullable + private final ListView mListView; + + public ListViewScrollHandlingDelegate( + @NonNull RequireScrollMixin requireScrollMixin, + @Nullable ListView listView) { + mRequireScrollMixin = requireScrollMixin; + mListView = listView; + } + + @Override + public void startListening() { + if (mListView != null) { + mListView.setOnScrollListener(this); + + final ListAdapter adapter = mListView.getAdapter(); + if (mListView.getLastVisiblePosition() < adapter.getCount()) { + mRequireScrollMixin.notifyScrollabilityChange(true); + } + } else { + Log.w(TAG, "Cannot require scroll. List view is null"); + } + } + + @Override + public void pageScrollDown() { + if (mListView != null) { + final int height = mListView.getHeight(); + mListView.smoothScrollBy(height, SCROLL_DURATION); + } + } + + @Override + public void onScrollStateChanged(AbsListView view, int scrollState) { + } + + @Override + public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, + int totalItemCount) { + if (firstVisibleItem + visibleItemCount >= totalItemCount) { + mRequireScrollMixin.notifyScrollabilityChange(false); + } else { + mRequireScrollMixin.notifyScrollabilityChange(true); + } + } +} diff --git a/library/main/src/com/android/setupwizardlib/template/RequireScrollMixin.java b/library/main/src/com/android/setupwizardlib/template/RequireScrollMixin.java new file mode 100644 index 0000000..231c064 --- /dev/null +++ b/library/main/src/com/android/setupwizardlib/template/RequireScrollMixin.java @@ -0,0 +1,260 @@ +/* + * 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.os.Handler; +import android.os.Looper; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import android.support.annotation.StringRes; +import android.view.View; +import android.view.View.OnClickListener; +import android.widget.Button; + +import com.android.setupwizardlib.TemplateLayout; +import com.android.setupwizardlib.view.NavigationBar; + +/** + * A mixin to require the a scrollable container (BottomScrollView, RecyclerView or ListView) to + * be scrolled to bottom, making sure that the user sees all content above and below the fold. + */ +public class RequireScrollMixin implements Mixin { + + /* static section */ + + /** + * Listener for when the require-scroll state changes. Note that this only requires the user to + * scroll to the bottom once - if the user scrolled to the bottom and back-up, scrolling to + * bottom is not required again. + */ + public interface OnRequireScrollStateChangedListener { + + /** + * Called when require-scroll state changed. + * + * @param scrollNeeded True if the user should be required to scroll to bottom. + */ + void onRequireScrollStateChanged(boolean scrollNeeded); + } + + /** + * A delegate to detect scrollability changes and to scroll the page. This provides a layer + * of abstraction for BottomScrollView, RecyclerView and ListView. The delegate should call + * {@link #notifyScrollabilityChange(boolean)} when the view scrollability is changed. + */ + interface ScrollHandlingDelegate { + + /** + * Starts listening to scrollability changes at the target scrollable container. + */ + void startListening(); + + /** + * Scroll the page content down by one page. + */ + void pageScrollDown(); + } + + /* non-static section */ + + @NonNull + private final TemplateLayout mTemplateLayout; + + private final Handler mHandler = new Handler(Looper.getMainLooper()); + + private boolean mRequiringScrollToBottom = false; + + // Whether the user have seen the more button yet. + private boolean mEverScrolledToBottom = false; + + private ScrollHandlingDelegate mDelegate; + + @Nullable + private OnRequireScrollStateChangedListener mListener; + + /** + * @param templateLayout The template containing this mixin + */ + public RequireScrollMixin(@NonNull TemplateLayout templateLayout) { + mTemplateLayout = templateLayout; + } + + /** + * Sets the delegate to handle scrolling. The type of delegate should depend on whether the + * scrolling view is a BottomScrollView, RecyclerView or ListView. + */ + public void setScrollHandlingDelegate(@NonNull ScrollHandlingDelegate delegate) { + mDelegate = delegate; + } + + /** + * Listen to require scroll state changes. When scroll is required, + * {@link OnRequireScrollStateChangedListener#onRequireScrollStateChanged(boolean)} is called + * with {@code true}, and vice versa. + */ + public void setOnRequireScrollStateChangedListener( + @Nullable OnRequireScrollStateChangedListener listener) { + mListener = listener; + } + + /** + * @return The scroll state listener previously set, or {@code null} if none is registered. + */ + public OnRequireScrollStateChangedListener getOnRequireScrollStateChangedListener() { + return mListener; + } + + /** + * Creates an {@link OnClickListener} which if scrolling is required, will scroll the page down, + * and if scrolling is not required, delegates to the wrapped {@code listener}. Note that you + * should call {@link #requireScroll()} as well in order to start requiring scrolling. + * + * @param listener The listener to be invoked when scrolling is not needed and the user taps on + * the button. If {@code null}, the click listener will be a no-op when scroll + * is not required. + * @return A new {@link OnClickListener} which will scroll the page down or delegate to the + * given listener depending on the current require-scroll state. + */ + public OnClickListener createOnClickListener(@Nullable final OnClickListener listener) { + return new OnClickListener() { + @Override + public void onClick(View view) { + if (mRequiringScrollToBottom) { + mDelegate.pageScrollDown(); + } else if (listener != null) { + listener.onClick(view); + } + } + }; + } + + /** + * Coordinate with the given navigation bar to require scrolling on the page. The more button + * will be shown instead of the next button while scrolling is required. + */ + public void requireScrollWithNavigationBar(@NonNull final NavigationBar navigationBar) { + setOnRequireScrollStateChangedListener( + new OnRequireScrollStateChangedListener() { + @Override + public void onRequireScrollStateChanged(boolean scrollNeeded) { + navigationBar.getMoreButton() + .setVisibility(scrollNeeded ? View.VISIBLE : View.GONE); + navigationBar.getNextButton() + .setVisibility(scrollNeeded ? View.GONE : View.VISIBLE); + } + }); + navigationBar.getMoreButton().setOnClickListener(createOnClickListener(null)); + requireScroll(); + } + + /** + * @see #requireScrollWithButton(Button, CharSequence, OnClickListener) + */ + public void requireScrollWithButton( + @NonNull Button button, + @StringRes int moreText, + @Nullable OnClickListener onClickListener) { + requireScrollWithButton(button, button.getContext().getText(moreText), onClickListener); + } + + /** + * Use the given {@code button} to require scrolling. When scrolling is required, the button + * label will change to {@code moreText}, and tapping the button will cause the page to scroll + * down. + * + * <p>Note: Calling {@link View#setOnClickListener} on the button after this method will remove + * its link to the require-scroll mechanism. If you need to do that, obtain the click listener + * from {@link #createOnClickListener(OnClickListener)}. + * + * <p>Note: The normal button label is taken from the button's text at the time of calling this + * method. Calling {@link android.widget.TextView#setText} after calling this method causes + * undefined behavior. + * + * @param button The button to use for require scroll. The button's "normal" label is taken from + * the text at the time of calling this method, and the click listener of it will + * be replaced. + * @param moreText The button label when scroll is required. + * @param onClickListener The listener for clicks when scrolling is not required. + */ + public void requireScrollWithButton( + @NonNull final Button button, + final CharSequence moreText, + @Nullable OnClickListener onClickListener) { + final CharSequence nextText = button.getText(); + button.setOnClickListener(createOnClickListener(onClickListener)); + setOnRequireScrollStateChangedListener(new OnRequireScrollStateChangedListener() { + @Override + public void onRequireScrollStateChanged(boolean scrollNeeded) { + button.setText(scrollNeeded ? moreText : nextText); + } + }); + requireScroll(); + } + + /** + * @return True if scrolling is required. Note that this mixin only requires the user to + * scroll to the bottom once - if the user scrolled to the bottom and back-up, scrolling to + * bottom is not required again. + */ + public boolean isScrollingRequired() { + return mRequiringScrollToBottom; + } + + /** + * Start requiring scrolling on the layout. After calling this method, this mixin will start + * listening to scroll events from the scrolling container, and call + * {@link OnRequireScrollStateChangedListener} when the scroll state changes. + */ + public void requireScroll() { + mDelegate.startListening(); + } + + /** + * {@link ScrollHandlingDelegate} should call this method when the scrollability of the + * scrolling container changed, so this mixin can recompute whether scrolling should be + * required. + * + * @param canScrollDown True if the view can scroll down further. + */ + void notifyScrollabilityChange(boolean canScrollDown) { + if (canScrollDown == mRequiringScrollToBottom) { + // Already at the desired require-scroll state + return; + } + if (canScrollDown) { + if (!mEverScrolledToBottom) { + postScrollStateChange(true); + mRequiringScrollToBottom = true; + } + } else { + postScrollStateChange(false); + mRequiringScrollToBottom = false; + mEverScrolledToBottom = true; + } + } + + private void postScrollStateChange(final boolean scrollNeeded) { + mHandler.post(new Runnable() { + @Override + public void run() { + if (mListener != null) { + mListener.onRequireScrollStateChanged(scrollNeeded); + } + } + }); + } +} diff --git a/library/main/src/com/android/setupwizardlib/template/ScrollViewScrollHandlingDelegate.java b/library/main/src/com/android/setupwizardlib/template/ScrollViewScrollHandlingDelegate.java new file mode 100644 index 0000000..d159465 --- /dev/null +++ b/library/main/src/com/android/setupwizardlib/template/ScrollViewScrollHandlingDelegate.java @@ -0,0 +1,80 @@ +/* + * 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.util.Log; +import android.widget.ScrollView; + +import com.android.setupwizardlib.template.RequireScrollMixin.ScrollHandlingDelegate; +import com.android.setupwizardlib.view.BottomScrollView; +import com.android.setupwizardlib.view.BottomScrollView.BottomScrollListener; + +/** + * {@link ScrollHandlingDelegate} which analyzes scroll events from {@link BottomScrollView} and + * notifies {@link RequireScrollMixin} about scrollability changes. + */ +public class ScrollViewScrollHandlingDelegate + implements ScrollHandlingDelegate, BottomScrollListener { + + private static final String TAG = "ScrollViewDelegate"; + + @NonNull + private final RequireScrollMixin mRequireScrollMixin; + + @Nullable + private final BottomScrollView mScrollView; + + public ScrollViewScrollHandlingDelegate( + @NonNull RequireScrollMixin requireScrollMixin, + @Nullable ScrollView scrollView) { + mRequireScrollMixin = requireScrollMixin; + if (scrollView instanceof BottomScrollView) { + mScrollView = (BottomScrollView) scrollView; + } else { + Log.w(TAG, "Cannot set non-BottomScrollView. Found=" + scrollView); + mScrollView = null; + } + } + + @Override + public void onScrolledToBottom() { + mRequireScrollMixin.notifyScrollabilityChange(false); + } + + @Override + public void onRequiresScroll() { + mRequireScrollMixin.notifyScrollabilityChange(true); + } + + @Override + public void startListening() { + if (mScrollView != null) { + mScrollView.setBottomScrollListener(this); + } else { + Log.w(TAG, "Cannot require scroll. Scroll view is null."); + } + } + + @Override + public void pageScrollDown() { + if (mScrollView != null) { + mScrollView.pageScroll(ScrollView.FOCUS_DOWN); + } + } +} diff --git a/library/main/src/com/android/setupwizardlib/util/AbstractRequireScrollHelper.java b/library/main/src/com/android/setupwizardlib/util/AbstractRequireScrollHelper.java deleted file mode 100644 index 2697371..0000000 --- a/library/main/src/com/android/setupwizardlib/util/AbstractRequireScrollHelper.java +++ /dev/null @@ -1,78 +0,0 @@ -/* - * 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.util; - -import android.view.View; - -import com.android.setupwizardlib.view.NavigationBar; - -/** - * Add this helper to require the scroll view to be scrolled to the bottom, making sure that the - * user sees all content on the screen. This will change the navigation bar to show the more button - * instead of the next button when there is more content to be seen. When the more button is - * clicked, the scroll view will be scrolled one page down. - */ -public abstract class AbstractRequireScrollHelper implements View.OnClickListener { - - private final NavigationBar mNavigationBar; - - private boolean mScrollNeeded; - // Whether the user have seen the more button yet. - private boolean mScrollNotified = false; - - protected AbstractRequireScrollHelper(NavigationBar navigationBar) { - mNavigationBar = navigationBar; - } - - protected void requireScroll() { - mNavigationBar.getMoreButton().setOnClickListener(this); - } - - protected void notifyScrolledToBottom() { - if (mScrollNeeded) { - mNavigationBar.post(new Runnable() { - @Override - public void run() { - mNavigationBar.getNextButton().setVisibility(View.VISIBLE); - mNavigationBar.getMoreButton().setVisibility(View.GONE); - } - }); - mScrollNeeded = false; - mScrollNotified = true; - } - } - - protected void notifyRequiresScroll() { - if (!mScrollNeeded && !mScrollNotified) { - mNavigationBar.post(new Runnable() { - @Override - public void run() { - mNavigationBar.getNextButton().setVisibility(View.GONE); - mNavigationBar.getMoreButton().setVisibility(View.VISIBLE); - } - }); - mScrollNeeded = true; - } - } - - @Override - public void onClick(View view) { - pageScrollDown(); - } - - protected abstract void pageScrollDown(); -} diff --git a/library/main/src/com/android/setupwizardlib/util/ListViewRequireScrollHelper.java b/library/main/src/com/android/setupwizardlib/util/ListViewRequireScrollHelper.java deleted file mode 100644 index 7877569..0000000 --- a/library/main/src/com/android/setupwizardlib/util/ListViewRequireScrollHelper.java +++ /dev/null @@ -1,81 +0,0 @@ -/* - * 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.util; - -import android.os.Build; -import android.widget.AbsListView; -import android.widget.ListAdapter; -import android.widget.ListView; - -import com.android.setupwizardlib.view.NavigationBar; - -/** - * Add this helper to require the list view to be scrolled to the bottom, making sure that the - * user sees all content on the screen. This will change the navigation bar to show the more button - * instead of the next button when there is more content to be seen. When the more button is - * clicked, the list view will be scrolled one page down. - */ -public class ListViewRequireScrollHelper extends AbstractRequireScrollHelper - implements AbsListView.OnScrollListener { - - public static void requireScroll(NavigationBar navigationBar, ListView listView) { - new ListViewRequireScrollHelper(navigationBar, listView).requireScroll(); - } - - private final ListView mListView; - - private ListViewRequireScrollHelper(NavigationBar navigationBar, ListView listView) { - super(navigationBar); - mListView = listView; - } - - @Override - protected void requireScroll() { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.FROYO) { - // APIs to scroll a list only exists on Froyo or above. - super.requireScroll(); - mListView.setOnScrollListener(this); - - final ListAdapter adapter = mListView.getAdapter(); - if (mListView.getLastVisiblePosition() < adapter.getCount()) { - notifyRequiresScroll(); - } - } - } - - @Override - protected void pageScrollDown() { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.FROYO) { - final int height = mListView.getHeight(); - mListView.smoothScrollBy(height, 500); - } - } - - @Override - public void onScrollStateChanged(AbsListView view, int scrollState) { - } - - @Override - public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, - int totalItemCount) { - if (firstVisibleItem + visibleItemCount >= totalItemCount) { - notifyScrolledToBottom(); - } else { - notifyRequiresScroll(); - } - } -} diff --git a/library/main/src/com/android/setupwizardlib/util/RequireScrollHelper.java b/library/main/src/com/android/setupwizardlib/util/RequireScrollHelper.java deleted file mode 100644 index cce336f..0000000 --- a/library/main/src/com/android/setupwizardlib/util/RequireScrollHelper.java +++ /dev/null @@ -1,64 +0,0 @@ -/* - * 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.util; - -import android.widget.ScrollView; - -import com.android.setupwizardlib.view.BottomScrollView; -import com.android.setupwizardlib.view.NavigationBar; - -/** - * Add this helper to require the scroll view to be scrolled to the bottom, making sure that the - * user sees all content on the screen. This will change the navigation bar to show the more button - * instead of the next button when there is more content to be seen. When the more button is - * clicked, the scroll view will be scrolled one page down. - */ -public class RequireScrollHelper extends AbstractRequireScrollHelper - implements BottomScrollView.BottomScrollListener { - - public static void requireScroll(NavigationBar navigationBar, BottomScrollView scrollView) { - new RequireScrollHelper(navigationBar, scrollView).requireScroll(); - } - - private final BottomScrollView mScrollView; - - private RequireScrollHelper(NavigationBar navigationBar, BottomScrollView scrollView) { - super(navigationBar); - mScrollView = scrollView; - } - - @Override - protected void requireScroll() { - super.requireScroll(); - mScrollView.setBottomScrollListener(this); - } - - @Override - protected void pageScrollDown() { - mScrollView.pageScroll(ScrollView.FOCUS_DOWN); - } - - @Override - public void onScrolledToBottom() { - notifyScrolledToBottom(); - } - - @Override - public void onRequiresScroll() { - notifyRequiresScroll(); - } -} diff --git a/library/main/src/com/android/setupwizardlib/view/NavigationBar.java b/library/main/src/com/android/setupwizardlib/view/NavigationBar.java index 9bb123f..2a1dd28 100644 --- a/library/main/src/com/android/setupwizardlib/view/NavigationBar.java +++ b/library/main/src/com/android/setupwizardlib/view/NavigationBar.java @@ -35,7 +35,7 @@ import com.android.setupwizardlib.R; * next button. By default, the more button is hidden, and typically the next button will be hidden * if the more button is shown. * - * @see com.android.setupwizardlib.util.RequireScrollHelper + * @see com.android.setupwizardlib.template.RequireScrollMixin */ public class NavigationBar extends LinearLayout implements View.OnClickListener { diff --git a/library/self.gradle b/library/self.gradle index d5ce0d3..f6d14af 100644 --- a/library/self.gradle +++ b/library/self.gradle @@ -43,7 +43,7 @@ android.sourceSets { } testGingerbreadCompat { - java.srcDirs = ['eclair-mr1/test/robotest/src'] + java.srcDirs = ['eclair-mr1/test/robotest/src', 'full-support/test/robotest/src'] } } android.defaultConfig.testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" diff --git a/library/test/instrumentation/src/com/android/setupwizardlib/test/ListViewRequireScrollHelperTest.java b/library/test/instrumentation/src/com/android/setupwizardlib/test/ListViewRequireScrollHelperTest.java deleted file mode 100644 index 58ceb6b..0000000 --- a/library/test/instrumentation/src/com/android/setupwizardlib/test/ListViewRequireScrollHelperTest.java +++ /dev/null @@ -1,163 +0,0 @@ -/* - * 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 android.content.Context; -import android.os.Build; -import android.os.SystemClock; -import android.support.test.InstrumentationRegistry; -import android.support.test.filters.SmallTest; -import android.support.test.runner.AndroidJUnit4; -import android.view.View; -import android.view.ViewGroup; -import android.widget.BaseAdapter; -import android.widget.ListView; - -import com.android.setupwizardlib.util.ListViewRequireScrollHelper; -import com.android.setupwizardlib.view.NavigationBar; - -import org.junit.Before; -import org.junit.Test; -import org.junit.runner.RunWith; - -@RunWith(AndroidJUnit4.class) -@SmallTest -public class ListViewRequireScrollHelperTest { - - private TestListView mListView; - private NavigationBar mNavigationBar; - - @Before - public void setUp() throws Exception { - mListView = new TestListView(InstrumentationRegistry.getTargetContext()); - mNavigationBar = new TestNavigationBar(InstrumentationRegistry.getTargetContext()); - - mListView.layout(0, 0, 50, 50); - } - - @Test - public void testRequireScroll() throws Throwable { - ListViewRequireScrollHelper.requireScroll(mNavigationBar, mListView); - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.FROYO) { - assertEquals("More button should be shown initially", View.VISIBLE, - mNavigationBar.getMoreButton().getVisibility()); - assertEquals("Next button should be gone initially", View.GONE, - mNavigationBar.getNextButton().getVisibility()); - } - } - - @Test - public void testScrolledToBottom() throws Throwable { - ListViewRequireScrollHelper.requireScroll(mNavigationBar, mListView); - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.FROYO) { - SystemClock.sleep(500); - assertEquals("More button should be shown when scroll is required", View.VISIBLE, - mNavigationBar.getMoreButton().getVisibility()); - assertEquals("Next button should not be shown when scroll is required", View.GONE, - mNavigationBar.getNextButton().getVisibility()); - - InstrumentationRegistry.getInstrumentation().runOnMainSync(new Runnable() { - @Override - public void run() { - mListView.lastVisiblePosition = 20; - mListView.listener.onScroll(mListView, 2, 20, 20); - } - }); - SystemClock.sleep(500); - assertEquals("More button should be hidden when scrolled to bottom", View.GONE, - mNavigationBar.getMoreButton().getVisibility()); - assertEquals("Next button should be shown when scrolled to bottom", View.VISIBLE, - mNavigationBar.getNextButton().getVisibility()); - } - } - - @Test - public void testClickScrollButton() throws Throwable { - ListViewRequireScrollHelper.requireScroll(mNavigationBar, mListView); - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.FROYO) { - assertEquals("ScrollView page should be initially 0", 0, mListView.scrollDistance); - mNavigationBar.getMoreButton().performClick(); - assertEquals("ScrollView page should be scrolled by 50px", - 50, mListView.scrollDistance); - } - } - - private static class TestListView extends ListView { - - public int lastVisiblePosition = 0; - public int scrollDistance = 0; - public OnScrollListener listener; - - TestListView(Context context) { - super(context); - setAdapter(new BaseAdapter() { - @Override - public int getCount() { - return 20; - } - - @Override - public Object getItem(int position) { - return null; - } - - @Override - public long getItemId(int position) { - return position; - } - - @Override - public View getView(int position, View convertView, ViewGroup parent) { - return new View(parent.getContext()); - } - }); - } - - @Override - public int getLastVisiblePosition() { - return lastVisiblePosition; - } - - @Override - public void smoothScrollBy(int distance, int duration) { - super.smoothScrollBy(distance, duration); - scrollDistance += distance; - } - - @Override - public void setOnScrollListener(OnScrollListener l) { - super.setOnScrollListener(l); - listener = l; - } - } - - private static class TestNavigationBar extends NavigationBar { - - TestNavigationBar(Context context) { - super(context); - } - - @Override - public boolean post(Runnable action) { - // Make the post action synchronous - action.run(); - return true; - } - } -} diff --git a/library/test/instrumentation/src/com/android/setupwizardlib/test/RequireScrollHelperTest.java b/library/test/instrumentation/src/com/android/setupwizardlib/test/RequireScrollHelperTest.java deleted file mode 100644 index f5e4bbd..0000000 --- a/library/test/instrumentation/src/com/android/setupwizardlib/test/RequireScrollHelperTest.java +++ /dev/null @@ -1,125 +0,0 @@ -/* - * 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 android.content.Context; -import android.support.test.InstrumentationRegistry; -import android.support.test.filters.SmallTest; -import android.support.test.runner.AndroidJUnit4; -import android.view.View; - -import com.android.setupwizardlib.util.RequireScrollHelper; -import com.android.setupwizardlib.view.BottomScrollView; -import com.android.setupwizardlib.view.NavigationBar; - -import org.junit.Before; -import org.junit.Test; -import org.junit.runner.RunWith; - -@RunWith(AndroidJUnit4.class) -@SmallTest -public class RequireScrollHelperTest { - - private TestBottomScrollView mScrollView; - private NavigationBar mNavigationBar; - - @Before - public void setUp() throws Exception { - mScrollView = new TestBottomScrollView(InstrumentationRegistry.getContext()); - mNavigationBar = new TestNavigationBar(InstrumentationRegistry.getContext()); - } - - @Test - public void testRequireScroll() { - RequireScrollHelper.requireScroll(mNavigationBar, mScrollView); - assertEquals("More button should be gone initially", View.GONE, - mNavigationBar.getMoreButton().getVisibility()); - assertEquals("Next button should be shown", View.VISIBLE, - mNavigationBar.getNextButton().getVisibility()); - - mScrollView.listener.onRequiresScroll(); - assertEquals("More button should be shown when scroll is required", View.VISIBLE, - mNavigationBar.getMoreButton().getVisibility()); - assertEquals("Next button should not be shown when scroll is required", View.GONE, - mNavigationBar.getNextButton().getVisibility()); - } - - @Test - public void testScrolledToBottom() { - RequireScrollHelper.requireScroll(mNavigationBar, mScrollView); - mScrollView.listener.onRequiresScroll(); - assertEquals("More button should be shown when scroll is required", View.VISIBLE, - mNavigationBar.getMoreButton().getVisibility()); - assertEquals("Next button should not be shown when scroll is required", View.GONE, - mNavigationBar.getNextButton().getVisibility()); - - mScrollView.listener.onScrolledToBottom(); - assertEquals("More button should be hidden when scrolled to bottom", View.GONE, - mNavigationBar.getMoreButton().getVisibility()); - assertEquals("Next button should be shown when scrolled to bottom", View.VISIBLE, - mNavigationBar.getNextButton().getVisibility()); - } - - @Test - public void testClickScrollButton() { - RequireScrollHelper.requireScroll(mNavigationBar, mScrollView); - assertEquals("ScrollView page should be initially 0", 0, mScrollView.page); - mScrollView.listener.onRequiresScroll(); - mNavigationBar.getMoreButton().performClick(); - assertEquals("ScrollView page should be scrolled by 1", 1, mScrollView.page); - } - - private static class TestBottomScrollView extends BottomScrollView { - - public BottomScrollListener listener; - public int page = 0; - - TestBottomScrollView(Context context) { - super(context); - } - - @Override - public void setBottomScrollListener(BottomScrollListener listener) { - this.listener = listener; - } - - @Override - public boolean pageScroll(int direction) { - if (direction == FOCUS_DOWN) { - page++; - } else if (direction == FOCUS_UP) { - page--; - } - return super.pageScroll(direction); - } - } - - private static class TestNavigationBar extends NavigationBar { - - TestNavigationBar(Context context) { - super(context); - } - - @Override - public boolean post(Runnable action) { - action.run(); - return true; - } - } -} diff --git a/library/test/robotest/src/com/android/setupwizardlib/template/ListViewScrollHandlingDelegateTest.java b/library/test/robotest/src/com/android/setupwizardlib/template/ListViewScrollHandlingDelegateTest.java new file mode 100644 index 0000000..fa81dc0 --- /dev/null +++ b/library/test/robotest/src/com/android/setupwizardlib/template/ListViewScrollHandlingDelegateTest.java @@ -0,0 +1,121 @@ +/* + * 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.content.Context; +import android.view.View; +import android.view.ViewGroup; +import android.widget.AbsListView.OnScrollListener; +import android.widget.BaseAdapter; +import android.widget.ListView; + +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 ListViewScrollHandlingDelegateTest { + + @Mock + private RequireScrollMixin mRequireScrollMixin; + + private ListView mListView; + private ListViewScrollHandlingDelegate mDelegate; + private ArgumentCaptor<OnScrollListener> mListenerCaptor; + + @Before + public void setUp() throws Exception { + MockitoAnnotations.initMocks(this); + + mListView = spy(new TestListView(application)); + mDelegate = new ListViewScrollHandlingDelegate(mRequireScrollMixin, mListView); + + mListenerCaptor = ArgumentCaptor.forClass(OnScrollListener.class); + doNothing().when(mListView).setOnScrollListener(mListenerCaptor.capture()); + + mListView.layout(0, 0, 50, 50); + } + + @Test + public void testRequireScroll() throws Throwable { + mDelegate.startListening(); + + verify(mRequireScrollMixin).notifyScrollabilityChange(true); + } + + @Test + public void testScrolledToBottom() throws Throwable { + mDelegate.startListening(); + + verify(mRequireScrollMixin).notifyScrollabilityChange(true); + + doReturn(20).when(mListView).getLastVisiblePosition(); + mListenerCaptor.getValue().onScroll(mListView, 2, 20, 20); + + verify(mRequireScrollMixin).notifyScrollabilityChange(false); + } + + @Test + public void testPageScrollDown() throws Throwable { + mDelegate.pageScrollDown(); + verify(mListView).smoothScrollBy(eq(50), anyInt()); + } + + private static class TestListView extends ListView { + + TestListView(Context context) { + super(context); + setAdapter(new BaseAdapter() { + @Override + public int getCount() { + return 20; + } + + @Override + public Object getItem(int position) { + return null; + } + + @Override + public long getItemId(int position) { + return position; + } + + @Override + public View getView(int position, View convertView, ViewGroup parent) { + return new View(parent.getContext()); + } + }); + } + } +} diff --git a/library/test/robotest/src/com/android/setupwizardlib/template/RequireScrollMixinTest.java b/library/test/robotest/src/com/android/setupwizardlib/template/RequireScrollMixinTest.java new file mode 100644 index 0000000..8e39c43 --- /dev/null +++ b/library/test/robotest/src/com/android/setupwizardlib/template/RequireScrollMixinTest.java @@ -0,0 +1,168 @@ +/* + * 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.assertFalse; +import static org.junit.Assert.assertSame; +import static org.junit.Assert.assertTrue; +import static org.mockito.Matchers.any; +import static org.mockito.Matchers.eq; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoMoreInteractions; +import static org.robolectric.RuntimeEnvironment.application; + +import android.annotation.SuppressLint; +import android.view.View; +import android.view.View.OnClickListener; +import android.widget.Button; + +import com.android.setupwizardlib.BuildConfig; +import com.android.setupwizardlib.TemplateLayout; +import com.android.setupwizardlib.robolectric.SuwLibRobolectricTestRunner; +import com.android.setupwizardlib.template.RequireScrollMixin.OnRequireScrollStateChangedListener; +import com.android.setupwizardlib.template.RequireScrollMixin.ScrollHandlingDelegate; +import com.android.setupwizardlib.view.NavigationBar; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +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 RequireScrollMixinTest { + + @Mock + private TemplateLayout mTemplateLayout; + + @Mock + private ScrollHandlingDelegate mDelegate; + + private RequireScrollMixin mRequireScrollMixin; + + @Before + public void setUp() { + MockitoAnnotations.initMocks(this); + + doReturn(application).when(mTemplateLayout).getContext(); + mRequireScrollMixin = new RequireScrollMixin(mTemplateLayout); + mRequireScrollMixin.setScrollHandlingDelegate(mDelegate); + } + + @Test + public void testRequireScroll() { + mRequireScrollMixin.requireScroll(); + + verify(mDelegate).startListening(); + } + + @Test + public void testScrollStateChangedListener() { + OnRequireScrollStateChangedListener listener = + mock(OnRequireScrollStateChangedListener.class); + mRequireScrollMixin.setOnRequireScrollStateChangedListener(listener); + assertFalse("Scrolling should not be required initially", + mRequireScrollMixin.isScrollingRequired()); + + mRequireScrollMixin.notifyScrollabilityChange(true); + verify(listener).onRequireScrollStateChanged(true); + assertTrue("Scrolling should be required when there is more content below the fold", + mRequireScrollMixin.isScrollingRequired()); + + mRequireScrollMixin.notifyScrollabilityChange(false); + verify(listener).onRequireScrollStateChanged(false); + assertFalse("Scrolling should not be required after scrolling to bottom", + mRequireScrollMixin.isScrollingRequired()); + + // Once the user has scrolled to the bottom, they should not be forced to scroll down again + mRequireScrollMixin.notifyScrollabilityChange(true); + verifyNoMoreInteractions(listener); + + assertFalse("Scrolling should not be required after scrolling to bottom once", + mRequireScrollMixin.isScrollingRequired()); + + assertSame(listener, mRequireScrollMixin.getOnRequireScrollStateChangedListener()); + } + + @Test + public void testCreateOnClickListener() { + OnClickListener wrappedListener = mock(OnClickListener.class); + final OnClickListener onClickListener = + mRequireScrollMixin.createOnClickListener(wrappedListener); + + mRequireScrollMixin.notifyScrollabilityChange(true); + onClickListener.onClick(null); + + verify(wrappedListener, never()).onClick(any(View.class)); + verify(mDelegate).pageScrollDown(); + + mRequireScrollMixin.notifyScrollabilityChange(false); + onClickListener.onClick(null); + + verify(wrappedListener).onClick(any(View.class)); + } + + @Test + public void testRequireScrollWithNavigationBar() { + final NavigationBar navigationBar = new NavigationBar(application); + mRequireScrollMixin.requireScrollWithNavigationBar(navigationBar); + + mRequireScrollMixin.notifyScrollabilityChange(true); + assertEquals("More button should be visible", + View.VISIBLE, navigationBar.getMoreButton().getVisibility()); + assertEquals("Next button should be hidden", + View.GONE, navigationBar.getNextButton().getVisibility()); + + navigationBar.getMoreButton().performClick(); + verify(mDelegate).pageScrollDown(); + + mRequireScrollMixin.notifyScrollabilityChange(false); + assertEquals("More button should be hidden", + View.GONE, navigationBar.getMoreButton().getVisibility()); + assertEquals("Next button should be visible", + View.VISIBLE, navigationBar.getNextButton().getVisibility()); + } + + @SuppressLint("SetTextI18n") // It's OK for testing + @Test + public void testRequireScrollWithButton() { + final Button button = new Button(application); + button.setText("OriginalLabel"); + OnClickListener wrappedListener = mock(OnClickListener.class); + mRequireScrollMixin.requireScrollWithButton( + button, "TestMoreLabel", wrappedListener); + + assertEquals("Button label should be kept initially", "OriginalLabel", button.getText()); + + mRequireScrollMixin.notifyScrollabilityChange(true); + assertEquals("TestMoreLabel", button.getText()); + button.performClick(); + verify(wrappedListener, never()).onClick(eq(button)); + verify(mDelegate).pageScrollDown(); + + mRequireScrollMixin.notifyScrollabilityChange(false); + assertEquals("OriginalLabel", button.getText()); + button.performClick(); + verify(wrappedListener).onClick(eq(button)); + } +} diff --git a/library/test/robotest/src/com/android/setupwizardlib/template/ScrollViewScrollHandlingDelegateTest.java b/library/test/robotest/src/com/android/setupwizardlib/template/ScrollViewScrollHandlingDelegateTest.java new file mode 100644 index 0000000..f77e256 --- /dev/null +++ b/library/test/robotest/src/com/android/setupwizardlib/template/ScrollViewScrollHandlingDelegateTest.java @@ -0,0 +1,88 @@ +/* + * 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.spy; +import static org.mockito.Mockito.verify; +import static org.robolectric.RuntimeEnvironment.application; + +import com.android.setupwizardlib.BuildConfig; +import com.android.setupwizardlib.robolectric.SuwLibRobolectricTestRunner; +import com.android.setupwizardlib.view.BottomScrollView; +import com.android.setupwizardlib.view.BottomScrollView.BottomScrollListener; + +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 ScrollViewScrollHandlingDelegateTest { + + @Mock + private RequireScrollMixin mRequireScrollMixin; + + private BottomScrollView mScrollView; + private ScrollViewScrollHandlingDelegate mDelegate; + private ArgumentCaptor<BottomScrollListener> mListenerCaptor; + + @Before + public void setUp() throws Exception { + MockitoAnnotations.initMocks(this); + + mScrollView = spy(new BottomScrollView(application)); + mDelegate = new ScrollViewScrollHandlingDelegate(mRequireScrollMixin, mScrollView); + + mListenerCaptor = ArgumentCaptor.forClass(BottomScrollListener.class); + doNothing().when(mScrollView).setBottomScrollListener(mListenerCaptor.capture()); + + mScrollView.layout(0, 0, 50, 50); + } + + @Test + public void testRequireScroll() throws Throwable { + mDelegate.startListening(); + + mListenerCaptor.getValue().onRequiresScroll(); + verify(mRequireScrollMixin).notifyScrollabilityChange(true); + } + + @Test + public void testScrolledToBottom() throws Throwable { + mDelegate.startListening(); + + mListenerCaptor.getValue().onRequiresScroll(); + verify(mRequireScrollMixin).notifyScrollabilityChange(true); + + mListenerCaptor.getValue().onScrolledToBottom(); + + verify(mRequireScrollMixin).notifyScrollabilityChange(false); + } + + @Test + public void testPageScrollDown() throws Throwable { + mDelegate.pageScrollDown(); + verify(mScrollView).smoothScrollBy(anyInt(), eq(50)); + } +} |