summaryrefslogtreecommitdiff
path: root/library/main/src/com/android/setupwizardlib/template/RequireScrollMixin.java
blob: 02bcc1c605fa36ead1f773be0bb2011894a3a6bd (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
/*
 * 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 androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.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 */

  private final Handler handler = new Handler(Looper.getMainLooper());

  private boolean requiringScrollToBottom = false;

  // Whether the user have seen the more button yet.
  private boolean everScrolledToBottom = false;

  private ScrollHandlingDelegate delegate;

  @Nullable private OnRequireScrollStateChangedListener listener;

  /** @param templateLayout The template containing this mixin */
  public RequireScrollMixin(@NonNull TemplateLayout 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) {
    this.delegate = 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) {
    this.listener = listener;
  }

  /** @return The scroll state listener previously set, or {@code null} if none is registered. */
  public OnRequireScrollStateChangedListener getOnRequireScrollStateChangedListener() {
    return listener;
  }

  /**
   * 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 (requiringScrollToBottom) {
          delegate.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 requiringScrollToBottom;
  }

  /**
   * 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() {
    delegate.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 == requiringScrollToBottom) {
      // Already at the desired require-scroll state
      return;
    }
    if (canScrollDown) {
      if (!everScrolledToBottom) {
        postScrollStateChange(true);
        requiringScrollToBottom = true;
      }
    } else {
      postScrollStateChange(false);
      requiringScrollToBottom = false;
      everScrolledToBottom = true;
    }
  }

  private void postScrollStateChange(final boolean scrollNeeded) {
    handler.post(
        new Runnable() {
          @Override
          public void run() {
            if (listener != null) {
              listener.onRequireScrollStateChanged(scrollNeeded);
            }
          }
        });
  }
}