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 */
16package com.android.launcher3.allapps;
17
18import static com.android.launcher3.anim.Interpolators.LINEAR;
19
20import android.animation.ValueAnimator;
21import android.content.Context;
22import android.graphics.Point;
23import android.graphics.Rect;
24import android.support.annotation.NonNull;
25import android.support.annotation.Nullable;
26import android.support.v7.widget.RecyclerView;
27import android.util.AttributeSet;
28import android.view.MotionEvent;
29import android.view.View;
30import android.view.ViewGroup;
31import android.widget.LinearLayout;
32
33import com.android.launcher3.R;
34import com.android.launcher3.anim.PropertySetter;
35
36public class FloatingHeaderView extends LinearLayout implements
37        ValueAnimator.AnimatorUpdateListener {
38
39    private final Rect mClip = new Rect(0, 0, Integer.MAX_VALUE, Integer.MAX_VALUE);
40    private final ValueAnimator mAnimator = ValueAnimator.ofInt(0, 0);
41    private final Point mTempOffset = new Point();
42    private final RecyclerView.OnScrollListener mOnScrollListener = new RecyclerView.OnScrollListener() {
43        @Override
44        public void onScrollStateChanged(RecyclerView recyclerView, int newState) {
45        }
46
47        @Override
48        public void onScrolled(RecyclerView rv, int dx, int dy) {
49            if (rv != mCurrentRV) {
50                return;
51            }
52
53            if (mAnimator.isStarted()) {
54                mAnimator.cancel();
55            }
56
57            int current = -mCurrentRV.getCurrentScrollY();
58            moved(current);
59            apply();
60        }
61    };
62
63    protected ViewGroup mTabLayout;
64    private AllAppsRecyclerView mMainRV;
65    private AllAppsRecyclerView mWorkRV;
66    private AllAppsRecyclerView mCurrentRV;
67    private ViewGroup mParent;
68    private boolean mHeaderCollapsed;
69    private int mSnappedScrolledY;
70    private int mTranslationY;
71
72    private boolean mAllowTouchForwarding;
73    private boolean mForwardToRecyclerView;
74
75    protected boolean mTabsHidden;
76    protected int mMaxTranslation;
77    private boolean mMainRVActive = true;
78
79    public FloatingHeaderView(@NonNull Context context) {
80        this(context, null);
81    }
82
83    public FloatingHeaderView(@NonNull Context context, @Nullable AttributeSet attrs) {
84        super(context, attrs);
85    }
86
87    @Override
88    protected void onFinishInflate() {
89        super.onFinishInflate();
90        mTabLayout = findViewById(R.id.tabs);
91    }
92
93    public void setup(AllAppsContainerView.AdapterHolder[] mAH, boolean tabsHidden) {
94        mTabsHidden = tabsHidden;
95        mTabLayout.setVisibility(tabsHidden ? View.GONE : View.VISIBLE);
96        mMainRV = setupRV(mMainRV, mAH[AllAppsContainerView.AdapterHolder.MAIN].recyclerView);
97        mWorkRV = setupRV(mWorkRV, mAH[AllAppsContainerView.AdapterHolder.WORK].recyclerView);
98        mParent = (ViewGroup) mMainRV.getParent();
99        setMainActive(mMainRVActive || mWorkRV == null);
100        reset(false);
101    }
102
103    private AllAppsRecyclerView setupRV(AllAppsRecyclerView old, AllAppsRecyclerView updated) {
104        if (old != updated && updated != null ) {
105            updated.addOnScrollListener(mOnScrollListener);
106        }
107        return updated;
108    }
109
110    public void setMainActive(boolean active) {
111        mCurrentRV = active ? mMainRV : mWorkRV;
112        mMainRVActive = active;
113    }
114
115    public int getMaxTranslation() {
116        if (mMaxTranslation == 0 && mTabsHidden) {
117            return getResources().getDimensionPixelSize(R.dimen.all_apps_search_bar_bottom_padding);
118        } else if (mMaxTranslation > 0 && mTabsHidden) {
119            return mMaxTranslation + getPaddingTop();
120        } else {
121            return mMaxTranslation;
122        }
123    }
124
125    private boolean canSnapAt(int currentScrollY) {
126        return Math.abs(currentScrollY) <= mMaxTranslation;
127    }
128
129    private void moved(final int currentScrollY) {
130        if (mHeaderCollapsed) {
131            if (currentScrollY <= mSnappedScrolledY) {
132                if (canSnapAt(currentScrollY)) {
133                    mSnappedScrolledY = currentScrollY;
134                }
135            } else {
136                mHeaderCollapsed = false;
137            }
138            mTranslationY = currentScrollY;
139        } else if (!mHeaderCollapsed) {
140            mTranslationY = currentScrollY - mSnappedScrolledY - mMaxTranslation;
141
142            // update state vars
143            if (mTranslationY >= 0) { // expanded: must not move down further
144                mTranslationY = 0;
145                mSnappedScrolledY = currentScrollY - mMaxTranslation;
146            } else if (mTranslationY <= -mMaxTranslation) { // hide or stay hidden
147                mHeaderCollapsed = true;
148                mSnappedScrolledY = -mMaxTranslation;
149            }
150        }
151    }
152
153    protected void applyScroll(int uncappedY, int currentY) { }
154
155    protected void apply() {
156        int uncappedTranslationY = mTranslationY;
157        mTranslationY = Math.max(mTranslationY, -mMaxTranslation);
158        applyScroll(uncappedTranslationY, mTranslationY);
159        mTabLayout.setTranslationY(mTranslationY);
160        mClip.top = mMaxTranslation + mTranslationY;
161        // clipping on a draw might cause additional redraw
162        mMainRV.setClipBounds(mClip);
163        if (mWorkRV != null) {
164            mWorkRV.setClipBounds(mClip);
165        }
166    }
167
168    public void reset(boolean animate) {
169        if (mAnimator.isStarted()) {
170            mAnimator.cancel();
171        }
172        if (animate) {
173            mAnimator.setIntValues(mTranslationY, 0);
174            mAnimator.addUpdateListener(this);
175            mAnimator.setDuration(150);
176            mAnimator.start();
177        } else {
178            mTranslationY = 0;
179            apply();
180        }
181        mHeaderCollapsed = false;
182        mSnappedScrolledY = -mMaxTranslation;
183        mCurrentRV.scrollToTop();
184    }
185
186    public boolean isExpanded() {
187        return !mHeaderCollapsed;
188    }
189
190    @Override
191    public void onAnimationUpdate(ValueAnimator animation) {
192        mTranslationY = (Integer) animation.getAnimatedValue();
193        apply();
194    }
195
196    @Override
197    public boolean onInterceptTouchEvent(MotionEvent ev) {
198        if (!mAllowTouchForwarding) {
199            mForwardToRecyclerView = false;
200            return super.onInterceptTouchEvent(ev);
201        }
202        calcOffset(mTempOffset);
203        ev.offsetLocation(mTempOffset.x, mTempOffset.y);
204        mForwardToRecyclerView = mCurrentRV.onInterceptTouchEvent(ev);
205        ev.offsetLocation(-mTempOffset.x, -mTempOffset.y);
206        return mForwardToRecyclerView || super.onInterceptTouchEvent(ev);
207    }
208
209    @Override
210    public boolean onTouchEvent(MotionEvent event) {
211        if (mForwardToRecyclerView) {
212            // take this view's and parent view's (view pager) location into account
213            calcOffset(mTempOffset);
214            event.offsetLocation(mTempOffset.x, mTempOffset.y);
215            try {
216                return mCurrentRV.onTouchEvent(event);
217            } finally {
218                event.offsetLocation(-mTempOffset.x, -mTempOffset.y);
219            }
220        } else {
221            return super.onTouchEvent(event);
222        }
223    }
224
225    private void calcOffset(Point p) {
226        p.x = getLeft() - mCurrentRV.getLeft() - mParent.getLeft();
227        p.y = getTop() - mCurrentRV.getTop() - mParent.getTop();
228    }
229
230    public void setContentVisibility(boolean hasHeader, boolean hasContent, PropertySetter setter) {
231        setter.setViewAlpha(this, hasContent ? 1 : 0, LINEAR);
232        allowTouchForwarding(hasContent);
233    }
234
235    protected void allowTouchForwarding(boolean allow) {
236        mAllowTouchForwarding = allow;
237    }
238
239    public boolean hasVisibleContent() {
240        return false;
241    }
242
243    @Override
244    public boolean hasOverlappingRendering() {
245        return false;
246    }
247}
248
249
250