summaryrefslogtreecommitdiff
path: root/library/main/src/com/android/setupwizardlib/template/RequireScrollMixin.java
blob: fd3303b858f6f964fe51bc1d080f20693209583a (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
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
/*
 * 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.view.View;
import android.view.View.OnClickListener;
import android.widget.Button;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.StringRes;

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);
                }
            }
        });
    }
}