• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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.google.android.setupdesign.template;
18 
19 import android.content.Context;
20 import android.os.Handler;
21 import android.os.Looper;
22 import androidx.annotation.NonNull;
23 import androidx.annotation.Nullable;
24 import androidx.annotation.StringRes;
25 import android.view.View;
26 import android.view.View.OnClickListener;
27 import android.widget.Button;
28 import com.google.android.setupcompat.internal.TemplateLayout;
29 import com.google.android.setupcompat.template.FooterButton;
30 import com.google.android.setupcompat.template.Mixin;
31 import com.google.android.setupdesign.view.NavigationBar;
32 
33 /**
34  * A mixin to require the a scrollable container (BottomScrollView, RecyclerView or ListView) to be
35  * scrolled to bottom, making sure that the user sees all content above and below the fold.
36  */
37 public class RequireScrollMixin implements Mixin {
38 
39   /* static section */
40 
41   /**
42    * Listener for when the require-scroll state changes. Note that this only requires the user to
43    * scroll to the bottom once - if the user scrolled to the bottom and back-up, scrolling to bottom
44    * is not required again.
45    */
46   public interface OnRequireScrollStateChangedListener {
47 
48     /**
49      * Called when require-scroll state changed.
50      *
51      * @param scrollNeeded True if the user should be required to scroll to bottom.
52      */
onRequireScrollStateChanged(boolean scrollNeeded)53     void onRequireScrollStateChanged(boolean scrollNeeded);
54   }
55 
56   /**
57    * A delegate to detect scrollability changes and to scroll the page. This provides a layer of
58    * abstraction for BottomScrollView, RecyclerView and ListView. The delegate should call {@link
59    * #notifyScrollabilityChange(boolean)} when the view scrollability is changed.
60    */
61   interface ScrollHandlingDelegate {
62 
63     /** Starts listening to scrollability changes at the target scrollable container. */
startListening()64     void startListening();
65 
66     /** Scroll the page content down by one page. */
pageScrollDown()67     void pageScrollDown();
68   }
69 
70   /* non-static section */
71 
72   private final Handler handler = new Handler(Looper.getMainLooper());
73 
74   private boolean requiringScrollToBottom = false;
75 
76   // Whether the user have seen the more button yet.
77   private boolean everScrolledToBottom = false;
78 
79   private ScrollHandlingDelegate delegate;
80 
81   @Nullable private OnRequireScrollStateChangedListener listener;
82 
83   /** @param templateLayout The template containing this mixin */
RequireScrollMixin(@onNull TemplateLayout templateLayout)84   public RequireScrollMixin(@NonNull TemplateLayout templateLayout) {
85   }
86 
87   /**
88    * Sets the delegate to handle scrolling. The type of delegate should depend on whether the
89    * scrolling view is a BottomScrollView, RecyclerView or ListView.
90    */
setScrollHandlingDelegate(@onNull ScrollHandlingDelegate delegate)91   public void setScrollHandlingDelegate(@NonNull ScrollHandlingDelegate delegate) {
92     this.delegate = delegate;
93   }
94 
95   /**
96    * Listen to require scroll state changes. When scroll is required, {@link
97    * OnRequireScrollStateChangedListener#onRequireScrollStateChanged(boolean)} is called with {@code
98    * true}, and vice versa.
99    */
setOnRequireScrollStateChangedListener( @ullable OnRequireScrollStateChangedListener listener)100   public void setOnRequireScrollStateChangedListener(
101       @Nullable OnRequireScrollStateChangedListener listener) {
102     this.listener = listener;
103   }
104 
105   /** @return The scroll state listener previously set, or {@code null} if none is registered. */
getOnRequireScrollStateChangedListener()106   public OnRequireScrollStateChangedListener getOnRequireScrollStateChangedListener() {
107     return listener;
108   }
109 
110   /**
111    * Creates an {@link OnClickListener} which if scrolling is required, will scroll the page down,
112    * and if scrolling is not required, delegates to the wrapped {@code listener}. Note that you
113    * should call {@link #requireScroll()} as well in order to start requiring scrolling.
114    *
115    * @param listener The listener to be invoked when scrolling is not needed and the user taps on
116    *     the button. If {@code null}, the click listener will be a no-op when scroll is not
117    *     required.
118    * @return A new {@link OnClickListener} which will scroll the page down or delegate to the given
119    *     listener depending on the current require-scroll state.
120    */
createOnClickListener(@ullable final OnClickListener listener)121   public OnClickListener createOnClickListener(@Nullable final OnClickListener listener) {
122     return new OnClickListener() {
123       @Override
124       public void onClick(View view) {
125         if (requiringScrollToBottom) {
126           delegate.pageScrollDown();
127         } else if (listener != null) {
128           listener.onClick(view);
129         }
130       }
131     };
132   }
133 
134   /**
135    * Coordinate with the given navigation bar to require scrolling on the page. The more button will
136    * be shown instead of the next button while scrolling is required.
137    */
138   public void requireScrollWithNavigationBar(@NonNull final NavigationBar navigationBar) {
139     setOnRequireScrollStateChangedListener(
140         new OnRequireScrollStateChangedListener() {
141           @Override
142           public void onRequireScrollStateChanged(boolean scrollNeeded) {
143             navigationBar.getMoreButton().setVisibility(scrollNeeded ? View.VISIBLE : View.GONE);
144             navigationBar.getNextButton().setVisibility(scrollNeeded ? View.GONE : View.VISIBLE);
145           }
146         });
147     navigationBar.getMoreButton().setOnClickListener(createOnClickListener(null));
148     requireScroll();
149   }
150 
151   /** @see #requireScrollWithButton(Button, CharSequence, OnClickListener) */
152   public void requireScrollWithButton(
153       @NonNull Button button, @StringRes int moreText, @Nullable OnClickListener onClickListener) {
154     requireScrollWithButton(button, button.getContext().getText(moreText), onClickListener);
155   }
156 
157   /**
158    * Use the given {@code button} to require scrolling. When scrolling is required, the button label
159    * will change to {@code moreText}, and tapping the button will cause the page to scroll down.
160    *
161    * <p>Note: Calling {@link View#setOnClickListener} on the button after this method will remove
162    * its link to the require-scroll mechanism. If you need to do that, obtain the click listener
163    * from {@link #createOnClickListener(OnClickListener)}.
164    *
165    * <p>Note: The normal button label is taken from the button's text at the time of calling this
166    * method. Calling {@link android.widget.TextView#setText} after calling this method causes
167    * undefined behavior.
168    *
169    * @param button The button to use for require scroll. The button's "normal" label is taken from
170    *     the text at the time of calling this method, and the click listener of it will be replaced.
171    * @param moreText The button label when scroll is required.
172    * @param onClickListener The listener for clicks when scrolling is not required.
173    */
174   public void requireScrollWithButton(
175       @NonNull final Button button,
176       final CharSequence moreText,
177       @Nullable OnClickListener onClickListener) {
178     final CharSequence nextText = button.getText();
179     button.setOnClickListener(createOnClickListener(onClickListener));
180     setOnRequireScrollStateChangedListener(
181         new OnRequireScrollStateChangedListener() {
182           @Override
183           public void onRequireScrollStateChanged(boolean scrollNeeded) {
184             button.setText(scrollNeeded ? moreText : nextText);
185           }
186         });
187     requireScroll();
188   }
189 
190   /**
191    * Use the given {@code button} to require scrolling. When scrolling is required, the button label
192    * will change to {@code moreText}, and tapping the button will cause the page to scroll down.
193    *
194    * <p>Note: Calling {@link View#setOnClickListener} on the button after this method will remove
195    * its link to the require-scroll mechanism. If you need to do that, obtain the click listener
196    * from {@link #createOnClickListener(OnClickListener)}.
197    *
198    * <p>Note: The normal button label is taken from the button's text at the time of calling this
199    * method. Calling {@link android.widget.TextView#setText} after calling this method causes
200    * undefined behavior.
201    *
202    * @param button The button to use for require scroll. The button's "normal" label is taken from
203    *     the text at the time of calling this method, and the click listener of it will be replaced.
204    * @param moreText The button label when scroll is required.
205    * @param onClickListener The listener for clicks when scrolling is not required.
206    */
207   public void requireScrollWithButton(
208       @NonNull Context context,
209       @NonNull FooterButton button,
210       @StringRes int moreText,
211       @Nullable OnClickListener onClickListener) {
212     requireScrollWithButton(button, context.getText(moreText), onClickListener);
213   }
214 
215   /**
216    * Use the given {@code button} to require scrolling. When scrolling is required, the button label
217    * will change to {@code moreText}, and tapping the button will cause the page to scroll down.
218    *
219    * <p>Note: Calling {@link View#setOnClickListener} on the button after this method will remove
220    * its link to the require-scroll mechanism. If you need to do that, obtain the click listener
221    * from {@link #createOnClickListener(OnClickListener)}.
222    *
223    * <p>Note: The normal button label is taken from the button's text at the time of calling this
224    * method. Calling {@link android.widget.TextView#setText} after calling this method causes
225    * undefined behavior.
226    *
227    * @param button The button to use for require scroll. The button's "normal" label is taken from
228    *     the text at the time of calling this method, and the click listener of it will be replaced.
229    * @param moreText The button label when scroll is required.
230    * @param onClickListener The listener for clicks when scrolling is not required.
231    */
232   public void requireScrollWithButton(
233       @NonNull final FooterButton button,
234       final CharSequence moreText,
235       @Nullable OnClickListener onClickListener) {
236     final CharSequence nextText = button.getText();
237     button.setOnClickListener(createOnClickListener(onClickListener));
238     setOnRequireScrollStateChangedListener(
239         new OnRequireScrollStateChangedListener() {
240           @Override
241           public void onRequireScrollStateChanged(boolean scrollNeeded) {
242             button.setText(scrollNeeded ? moreText : nextText);
243           }
244         });
245     requireScroll();
246   }
247 
248   /**
249    * @return True if scrolling is required. Note that this mixin only requires the user to scroll to
250    *     the bottom once - if the user scrolled to the bottom and back-up, scrolling to bottom is
251    *     not required again.
252    */
253   public boolean isScrollingRequired() {
254     return requiringScrollToBottom;
255   }
256 
257   /**
258    * Start requiring scrolling on the layout. After calling this method, this mixin will start
259    * listening to scroll events from the scrolling container, and call {@link
260    * OnRequireScrollStateChangedListener} when the scroll state changes.
261    */
262   public void requireScroll() {
263     delegate.startListening();
264   }
265 
266   /**
267    * {@link ScrollHandlingDelegate} should call this method when the scrollability of the scrolling
268    * container changed, so this mixin can recompute whether scrolling should be required.
269    *
270    * @param canScrollDown True if the view can scroll down further.
271    */
272   void notifyScrollabilityChange(boolean canScrollDown) {
273     if (canScrollDown == requiringScrollToBottom) {
274       // Already at the desired require-scroll state
275       return;
276     }
277     if (canScrollDown) {
278       if (!everScrolledToBottom) {
279         postScrollStateChange(true);
280         requiringScrollToBottom = true;
281       }
282     } else {
283       postScrollStateChange(false);
284       requiringScrollToBottom = false;
285       everScrolledToBottom = true;
286     }
287   }
288 
289   private void postScrollStateChange(final boolean scrollNeeded) {
290     handler.post(
291         new Runnable() {
292           @Override
293           public void run() {
294             if (listener != null) {
295               listener.onRequireScrollStateChanged(scrollNeeded);
296             }
297           }
298         });
299   }
300 }
301