/* * 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.support.annotation.NonNull; import android.support.annotation.Nullable; import android.support.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 */ @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. * *

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)}. * *

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