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
17package com.android.deskclock;
18
19import android.animation.ValueAnimator;
20import android.support.v7.widget.RecyclerView;
21import android.view.View;
22import android.widget.AbsListView;
23import android.widget.ListView;
24
25import com.android.deskclock.data.DataModel;
26import com.android.deskclock.uidata.TabScrollListener;
27import com.android.deskclock.uidata.UiDataModel;
28import com.android.deskclock.uidata.UiDataModel.Tab;
29
30import 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 */
40public 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     */
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     */
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     */
88    public DropShadowController(View dropShadowView, RecyclerView recyclerView) {
89        this(dropShadowView);
90        mRecyclerView = recyclerView;
91        mRecyclerView.addOnScrollListener(mScrollChangeWatcher);
92        updateDropShadow(!Utils.isScrolledToTop(recyclerView));
93    }
94
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     */
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     */
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
151        public void onScrolled(RecyclerView view, int dx, int dy) {
152            updateDropShadow(!Utils.isScrolledToTop(view));
153        }
154
155        // ListView scrolled.
156        @Override
157        public void onScrollStateChanged(AbsListView view, int scrollState) {}
158
159        @Override
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.
166        public void selectedTabScrollToTopChanged(Tab selectedTab, boolean scrolledToTop) {
167            updateDropShadow(!scrolledToTop);
168        }
169    }
170}