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