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