1 /*
2  * Copyright (C) 2017 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.setupwizardlib.template;
18 
19 import android.os.Handler;
20 import android.os.Looper;
21 import android.support.annotation.NonNull;
22 import android.support.annotation.Nullable;
23 import android.support.annotation.StringRes;
24 import android.view.View;
25 import android.view.View.OnClickListener;
26 import android.widget.Button;
27 
28 import com.android.setupwizardlib.TemplateLayout;
29 import com.android.setupwizardlib.view.NavigationBar;
30 
31 /**
32  * A mixin to require the a scrollable container (BottomScrollView, RecyclerView or ListView) to
33  * be scrolled to bottom, making sure that the user sees all content above and below the fold.
34  */
35 public class RequireScrollMixin implements Mixin {
36 
37     /* static section */
38 
39     /**
40      * Listener for when the require-scroll state changes. Note that this only requires the user to
41      * scroll to the bottom once - if the user scrolled to the bottom and back-up, scrolling to
42      * bottom is not required again.
43      */
44     public interface OnRequireScrollStateChangedListener {
45 
46         /**
47          * Called when require-scroll state changed.
48          *
49          * @param scrollNeeded True if the user should be required to scroll to bottom.
50          */
onRequireScrollStateChanged(boolean scrollNeeded)51         void onRequireScrollStateChanged(boolean scrollNeeded);
52     }
53 
54     /**
55      * A delegate to detect scrollability changes and to scroll the page. This provides a layer
56      * of abstraction for BottomScrollView, RecyclerView and ListView. The delegate should call
57      * {@link #notifyScrollabilityChange(boolean)} when the view scrollability is changed.
58      */
59     interface ScrollHandlingDelegate {
60 
61         /**
62          * Starts listening to scrollability changes at the target scrollable container.
63          */
startListening()64         void startListening();
65 
66         /**
67          * Scroll the page content down by one page.
68          */
pageScrollDown()69         void pageScrollDown();
70     }
71 
72     /* non-static section */
73 
74     @NonNull
75     private final TemplateLayout mTemplateLayout;
76 
77     private final Handler mHandler = new Handler(Looper.getMainLooper());
78 
79     private boolean mRequiringScrollToBottom = false;
80 
81     // Whether the user have seen the more button yet.
82     private boolean mEverScrolledToBottom = false;
83 
84     private ScrollHandlingDelegate mDelegate;
85 
86     @Nullable
87     private OnRequireScrollStateChangedListener mListener;
88 
89     /**
90      * @param templateLayout The template containing this mixin
91      */
RequireScrollMixin(@onNull TemplateLayout templateLayout)92     public RequireScrollMixin(@NonNull TemplateLayout templateLayout) {
93         mTemplateLayout = templateLayout;
94     }
95 
96     /**
97      * Sets the delegate to handle scrolling. The type of delegate should depend on whether the
98      * scrolling view is a BottomScrollView, RecyclerView or ListView.
99      */
setScrollHandlingDelegate(@onNull ScrollHandlingDelegate delegate)100     public void setScrollHandlingDelegate(@NonNull ScrollHandlingDelegate delegate) {
101         mDelegate = delegate;
102     }
103 
104     /**
105      * Listen to require scroll state changes. When scroll is required,
106      * {@link OnRequireScrollStateChangedListener#onRequireScrollStateChanged(boolean)} is called
107      * with {@code true}, and vice versa.
108      */
setOnRequireScrollStateChangedListener( @ullable OnRequireScrollStateChangedListener listener)109     public void setOnRequireScrollStateChangedListener(
110             @Nullable OnRequireScrollStateChangedListener listener) {
111         mListener = listener;
112     }
113 
114     /**
115      * @return The scroll state listener previously set, or {@code null} if none is registered.
116      */
getOnRequireScrollStateChangedListener()117     public OnRequireScrollStateChangedListener getOnRequireScrollStateChangedListener() {
118         return mListener;
119     }
120 
121     /**
122      * Creates an {@link OnClickListener} which if scrolling is required, will scroll the page down,
123      * and if scrolling is not required, delegates to the wrapped {@code listener}. Note that you
124      * should call {@link #requireScroll()} as well in order to start requiring scrolling.
125      *
126      * @param listener The listener to be invoked when scrolling is not needed and the user taps on
127      *                 the button. If {@code null}, the click listener will be a no-op when scroll
128      *                 is not required.
129      * @return A new {@link OnClickListener} which will scroll the page down or delegate to the
130      *         given listener depending on the current require-scroll state.
131      */
createOnClickListener(@ullable final OnClickListener listener)132     public OnClickListener createOnClickListener(@Nullable final OnClickListener listener) {
133         return new OnClickListener() {
134             @Override
135             public void onClick(View view) {
136                 if (mRequiringScrollToBottom) {
137                     mDelegate.pageScrollDown();
138                 } else if (listener != null) {
139                     listener.onClick(view);
140                 }
141             }
142         };
143     }
144 
145     /**
146      * Coordinate with the given navigation bar to require scrolling on the page. The more button
147      * will be shown instead of the next button while scrolling is required.
148      */
149     public void requireScrollWithNavigationBar(@NonNull final NavigationBar navigationBar) {
150         setOnRequireScrollStateChangedListener(
151                 new OnRequireScrollStateChangedListener() {
152                     @Override
153                     public void onRequireScrollStateChanged(boolean scrollNeeded) {
154                         navigationBar.getMoreButton()
155                                 .setVisibility(scrollNeeded ? View.VISIBLE : View.GONE);
156                         navigationBar.getNextButton()
157                                 .setVisibility(scrollNeeded ? View.GONE : View.VISIBLE);
158                     }
159                 });
160         navigationBar.getMoreButton().setOnClickListener(createOnClickListener(null));
161         requireScroll();
162     }
163 
164     /**
165      * @see #requireScrollWithButton(Button, CharSequence, OnClickListener)
166      */
167     public void requireScrollWithButton(
168             @NonNull Button button,
169             @StringRes int moreText,
170             @Nullable OnClickListener onClickListener) {
171         requireScrollWithButton(button, button.getContext().getText(moreText), onClickListener);
172     }
173 
174     /**
175      * Use the given {@code button} to require scrolling. When scrolling is required, the button
176      * label will change to {@code moreText}, and tapping the button will cause the page to scroll
177      * down.
178      *
179      * <p>Note: Calling {@link View#setOnClickListener} on the button after this method will remove
180      * its link to the require-scroll mechanism. If you need to do that, obtain the click listener
181      * from {@link #createOnClickListener(OnClickListener)}.
182      *
183      * <p>Note: The normal button label is taken from the button's text at the time of calling this
184      * method. Calling {@link android.widget.TextView#setText} after calling this method causes
185      * undefined behavior.
186      *
187      * @param button The button to use for require scroll. The button's "normal" label is taken from
188      *               the text at the time of calling this method, and the click listener of it will
189      *               be replaced.
190      * @param moreText The button label when scroll is required.
191      * @param onClickListener The listener for clicks when scrolling is not required.
192      */
193     public void requireScrollWithButton(
194             @NonNull final Button button,
195             final CharSequence moreText,
196             @Nullable OnClickListener onClickListener) {
197         final CharSequence nextText = button.getText();
198         button.setOnClickListener(createOnClickListener(onClickListener));
199         setOnRequireScrollStateChangedListener(new OnRequireScrollStateChangedListener() {
200             @Override
201             public void onRequireScrollStateChanged(boolean scrollNeeded) {
202                 button.setText(scrollNeeded ? moreText : nextText);
203             }
204         });
205         requireScroll();
206     }
207 
208     /**
209      * @return True if scrolling is required. Note that this mixin only requires the user to
210      * scroll to the bottom once - if the user scrolled to the bottom and back-up, scrolling to
211      * bottom is not required again.
212      */
213     public boolean isScrollingRequired() {
214         return mRequiringScrollToBottom;
215     }
216 
217     /**
218      * Start requiring scrolling on the layout. After calling this method, this mixin will start
219      * listening to scroll events from the scrolling container, and call
220      * {@link OnRequireScrollStateChangedListener} when the scroll state changes.
221      */
222     public void requireScroll() {
223         mDelegate.startListening();
224     }
225 
226     /**
227      * {@link ScrollHandlingDelegate} should call this method when the scrollability of the
228      * scrolling container changed, so this mixin can recompute whether scrolling should be
229      * required.
230      *
231      * @param canScrollDown True if the view can scroll down further.
232      */
233     void notifyScrollabilityChange(boolean canScrollDown) {
234         if (canScrollDown == mRequiringScrollToBottom) {
235             // Already at the desired require-scroll state
236             return;
237         }
238         if (canScrollDown) {
239             if (!mEverScrolledToBottom) {
240                 postScrollStateChange(true);
241                 mRequiringScrollToBottom = true;
242             }
243         } else {
244             postScrollStateChange(false);
245             mRequiringScrollToBottom = false;
246             mEverScrolledToBottom = true;
247         }
248     }
249 
250     private void postScrollStateChange(final boolean scrollNeeded) {
251         mHandler.post(new Runnable() {
252             @Override
253             public void run() {
254                 if (mListener != null) {
255                     mListener.onRequireScrollStateChanged(scrollNeeded);
256                 }
257             }
258         });
259     }
260 }
261