1 /*
2  * Copyright (C) 2016 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.deskclock;
18 
19 import android.animation.ValueAnimator;
20 import android.support.v7.widget.RecyclerView;
21 import android.view.View;
22 import android.widget.AbsListView;
23 import android.widget.ListView;
24 
25 import com.android.deskclock.data.DataModel;
26 import com.android.deskclock.uidata.TabScrollListener;
27 import com.android.deskclock.uidata.UiDataModel;
28 import com.android.deskclock.uidata.UiDataModel.Tab;
29 
30 import static com.android.deskclock.AnimatorUtils.getAlphaAnimator;
31 
32 /**
33  * This controller encapsulates the logic that watches a model for changes to scroll state and
34  * updates the display state of an associated drop shadow. The observable model may take many forms
35  * including ListViews, RecyclerViews and this application's UiDataModel. Each of these models can
36  * indicate when content is scrolled to its top. When the content is scrolled to the top the drop
37  * shadow is hidden and the content appears flush with the app bar. When the content is scrolled
38  * up the drop shadow is displayed making the content appear to scroll below the app bar.
39  */
40 public final class DropShadowController {
41 
42     /** Updates {@link #mDropShadowView} in response to changes in the backing scroll model. */
43     private final ScrollChangeWatcher mScrollChangeWatcher = new ScrollChangeWatcher();
44 
45     /** Fades the {@link @mDropShadowView} in/out as scroll state changes. */
46     private final ValueAnimator mDropShadowAnimator;
47 
48     /** The component that displays a drop shadow. */
49     private final View mDropShadowView;
50 
51     /** Tab bar's hairline, which is hidden whenever the drop shadow is displayed. */
52     private View mHairlineView;
53 
54     // Supported sources of scroll position include: ListView, RecyclerView and UiDataModel.
55     private RecyclerView mRecyclerView;
56     private UiDataModel mUiDataModel;
57     private ListView mListView;
58 
59     /**
60      * @param dropShadowView to be hidden/shown as {@code uiDataModel} reports scrolling changes
61      * @param uiDataModel models the vertical scrolling state of the application's selected tab
62      * @param hairlineView at the bottom of the tab bar to be hidden or shown when the drop shadow
63      *                     is displayed or hidden, respectively.
64      */
DropShadowController(View dropShadowView, UiDataModel uiDataModel, View hairlineView)65     public DropShadowController(View dropShadowView, UiDataModel uiDataModel, View hairlineView) {
66         this(dropShadowView);
67         mUiDataModel = uiDataModel;
68         mUiDataModel.addTabScrollListener(mScrollChangeWatcher);
69         mHairlineView = hairlineView;
70         updateDropShadow(!uiDataModel.isSelectedTabScrolledToTop());
71     }
72 
73     /**
74      * @param dropShadowView to be hidden/shown as {@code listView} reports scrolling changes
75      * @param listView a scrollable view that dictates the visibility of {@code dropShadowView}
76      */
DropShadowController(View dropShadowView, ListView listView)77     public DropShadowController(View dropShadowView, ListView listView) {
78         this(dropShadowView);
79         mListView = listView;
80         mListView.setOnScrollListener(mScrollChangeWatcher);
81         updateDropShadow(!Utils.isScrolledToTop(listView));
82     }
83 
84     /**
85      * @param dropShadowView to be hidden/shown as {@code recyclerView} reports scrolling changes
86      * @param recyclerView a scrollable view that dictates the visibility of {@code dropShadowView}
87      */
DropShadowController(View dropShadowView, RecyclerView recyclerView)88     public DropShadowController(View dropShadowView, RecyclerView recyclerView) {
89         this(dropShadowView);
90         mRecyclerView = recyclerView;
91         mRecyclerView.addOnScrollListener(mScrollChangeWatcher);
92         updateDropShadow(!Utils.isScrolledToTop(recyclerView));
93     }
94 
DropShadowController(View dropShadowView)95     private DropShadowController(View dropShadowView) {
96         mDropShadowView = dropShadowView;
97         mDropShadowAnimator = getAlphaAnimator(mDropShadowView, 0f, 1f)
98                 .setDuration(UiDataModel.getUiDataModel().getShortAnimationDuration());
99     }
100 
101     /**
102      * Stop updating the drop shadow in response to scrolling changes. Stop listening to the backing
103      * scrollable entity for changes. This is important to avoid memory leaks.
104      */
stop()105     public void stop() {
106         if (mRecyclerView != null) {
107             mRecyclerView.removeOnScrollListener(mScrollChangeWatcher);
108         } else if (mListView != null) {
109             mListView.setOnScrollListener(null);
110         } else if (mUiDataModel != null) {
111             mUiDataModel.removeTabScrollListener(mScrollChangeWatcher);
112         }
113     }
114 
115     /**
116      * @param shouldShowDropShadow {@code true} indicates the drop shadow should be displayed;
117      *      {@code false} indicates the drop shadow should be hidden
118      */
updateDropShadow(boolean shouldShowDropShadow)119     private void updateDropShadow(boolean shouldShowDropShadow) {
120         if (!shouldShowDropShadow && mDropShadowView.getAlpha() != 0f) {
121             if (DataModel.getDataModel().isApplicationInForeground()) {
122                 mDropShadowAnimator.reverse();
123             } else {
124                 mDropShadowView.setAlpha(0f);
125             }
126             if (mHairlineView != null) {
127                 mHairlineView.setVisibility(View.VISIBLE);
128             }
129         }
130 
131         if (shouldShowDropShadow && mDropShadowView.getAlpha() != 1f) {
132             if (DataModel.getDataModel().isApplicationInForeground()) {
133                 mDropShadowAnimator.start();
134             } else {
135                 mDropShadowView.setAlpha(1f);
136             }
137             if (mHairlineView != null) {
138                 mHairlineView.setVisibility(View.INVISIBLE);
139             }
140         }
141     }
142 
143     /**
144      * Update the drop shadow as the scrollable entity is scrolled.
145      */
146     private final class ScrollChangeWatcher extends RecyclerView.OnScrollListener
147             implements TabScrollListener, AbsListView.OnScrollListener {
148 
149         // RecyclerView scrolled.
150         @Override
onScrolled(RecyclerView view, int dx, int dy)151         public void onScrolled(RecyclerView view, int dx, int dy) {
152             updateDropShadow(!Utils.isScrolledToTop(view));
153         }
154 
155         // ListView scrolled.
156         @Override
onScrollStateChanged(AbsListView view, int scrollState)157         public void onScrollStateChanged(AbsListView view, int scrollState) {}
158 
159         @Override
onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount)160         public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount,
161                 int totalItemCount) {
162             updateDropShadow(!Utils.isScrolledToTop(view));
163         }
164 
165         // UiDataModel reports scroll change.
selectedTabScrollToTopChanged(Tab selectedTab, boolean scrolledToTop)166         public void selectedTabScrollToTopChanged(Tab selectedTab, boolean scrolledToTop) {
167             updateDropShadow(!scrolledToTop);
168         }
169     }
170 }