• Home
  • History
  • Annotate
  • 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