Home | History | Annotate | Download | only in template
      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          */
     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          */
     64         void startListening();
     65 
     66         /**
     67          * Scroll the page content down by one page.
     68          */
     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      */
     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      */
    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      */
    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      */
    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      */
    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