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 package com.android.launcher3.allapps;
17 
18 import static com.android.launcher3.LauncherAnimUtils.VIEW_ALPHA;
19 
20 import android.animation.ValueAnimator;
21 import android.content.Context;
22 import android.graphics.Point;
23 import android.graphics.Rect;
24 import android.util.ArrayMap;
25 import android.util.AttributeSet;
26 import android.view.MotionEvent;
27 import android.view.View;
28 import android.view.ViewGroup;
29 import android.view.animation.Interpolator;
30 import android.widget.LinearLayout;
31 
32 import androidx.annotation.NonNull;
33 import androidx.annotation.Nullable;
34 import androidx.recyclerview.widget.RecyclerView;
35 
36 import com.android.launcher3.BaseDraggingActivity;
37 import com.android.launcher3.DeviceProfile;
38 import com.android.launcher3.Insettable;
39 import com.android.launcher3.R;
40 import com.android.launcher3.anim.PropertySetter;
41 import com.android.launcher3.uioverrides.plugins.PluginManagerWrapper;
42 import com.android.systemui.plugins.AllAppsRow;
43 import com.android.systemui.plugins.AllAppsRow.OnHeightUpdatedListener;
44 import com.android.systemui.plugins.PluginListener;
45 
46 import java.util.ArrayList;
47 import java.util.Map;
48 
49 public class FloatingHeaderView extends LinearLayout implements
50         ValueAnimator.AnimatorUpdateListener, PluginListener<AllAppsRow>, Insettable,
51         OnHeightUpdatedListener {
52 
53     private final Rect mClip = new Rect(0, 0, Integer.MAX_VALUE, Integer.MAX_VALUE);
54     private final ValueAnimator mAnimator = ValueAnimator.ofInt(0, 0);
55     private final Point mTempOffset = new Point();
56     private final RecyclerView.OnScrollListener mOnScrollListener = new RecyclerView.OnScrollListener() {
57         @Override
58         public void onScrollStateChanged(RecyclerView recyclerView, int newState) {
59         }
60 
61         @Override
62         public void onScrolled(RecyclerView rv, int dx, int dy) {
63             if (rv != mCurrentRV) {
64                 return;
65             }
66 
67             if (mAnimator.isStarted()) {
68                 mAnimator.cancel();
69             }
70 
71             int current = -mCurrentRV.getCurrentScrollY();
72             moved(current);
73             applyVerticalMove();
74         }
75     };
76 
77     private final int mHeaderTopPadding;
78 
79     protected final Map<AllAppsRow, PluginHeaderRow> mPluginRows = new ArrayMap<>();
80 
81     protected ViewGroup mTabLayout;
82     private AllAppsRecyclerView mMainRV;
83     private AllAppsRecyclerView mWorkRV;
84     private AllAppsRecyclerView mCurrentRV;
85     private ViewGroup mParent;
86     private boolean mHeaderCollapsed;
87     private int mSnappedScrolledY;
88     private int mTranslationY;
89 
90     private boolean mAllowTouchForwarding;
91     private boolean mForwardToRecyclerView;
92 
93     protected boolean mTabsHidden;
94     protected int mMaxTranslation;
95     private boolean mMainRVActive = true;
96 
97     private boolean mCollapsed = false;
98 
99     // This is initialized once during inflation and stays constant after that. Fixed views
100     // cannot be added or removed dynamically.
101     private FloatingHeaderRow[] mFixedRows = FloatingHeaderRow.NO_ROWS;
102 
103     // Array of all fixed rows and plugin rows. This is initialized every time a plugin is
104     // enabled or disabled, and represent the current set of all rows.
105     private FloatingHeaderRow[] mAllRows = FloatingHeaderRow.NO_ROWS;
106 
FloatingHeaderView(@onNull Context context)107     public FloatingHeaderView(@NonNull Context context) {
108         this(context, null);
109     }
110 
FloatingHeaderView(@onNull Context context, @Nullable AttributeSet attrs)111     public FloatingHeaderView(@NonNull Context context, @Nullable AttributeSet attrs) {
112         super(context, attrs);
113         mHeaderTopPadding = context.getResources()
114                 .getDimensionPixelSize(R.dimen.all_apps_header_top_padding);
115     }
116 
117     @Override
onFinishInflate()118     protected void onFinishInflate() {
119         super.onFinishInflate();
120         mTabLayout = findViewById(R.id.tabs);
121 
122         // Find all floating header rows.
123         ArrayList<FloatingHeaderRow> rows = new ArrayList<>();
124         int count = getChildCount();
125         for (int i = 0; i < count; i++) {
126             View child = getChildAt(i);
127             if (child instanceof FloatingHeaderRow) {
128                 rows.add((FloatingHeaderRow) child);
129             }
130         }
131         mFixedRows = rows.toArray(new FloatingHeaderRow[rows.size()]);
132         mAllRows = mFixedRows;
133     }
134 
135     @Override
onAttachedToWindow()136     protected void onAttachedToWindow() {
137         super.onAttachedToWindow();
138         PluginManagerWrapper.INSTANCE.get(getContext()).addPluginListener(this,
139                 AllAppsRow.class, true /* allowMultiple */);
140     }
141 
142     @Override
onDetachedFromWindow()143     protected void onDetachedFromWindow() {
144         super.onDetachedFromWindow();
145         PluginManagerWrapper.INSTANCE.get(getContext()).removePluginListener(this);
146     }
147 
recreateAllRowsArray()148     private void recreateAllRowsArray() {
149         int pluginCount = mPluginRows.size();
150         if (pluginCount == 0) {
151             mAllRows = mFixedRows;
152         } else {
153             int count = mFixedRows.length;
154             mAllRows = new FloatingHeaderRow[count + pluginCount];
155             for (int i = 0; i < count; i++) {
156                 mAllRows[i] = mFixedRows[i];
157             }
158 
159             for (PluginHeaderRow row : mPluginRows.values()) {
160                 mAllRows[count] = row;
161                 count++;
162             }
163         }
164     }
165 
166     @Override
onPluginConnected(AllAppsRow allAppsRowPlugin, Context context)167     public void onPluginConnected(AllAppsRow allAppsRowPlugin, Context context) {
168         PluginHeaderRow headerRow = new PluginHeaderRow(allAppsRowPlugin, this);
169         addView(headerRow.mView, indexOfChild(mTabLayout));
170         mPluginRows.put(allAppsRowPlugin, headerRow);
171         recreateAllRowsArray();
172         allAppsRowPlugin.setOnHeightUpdatedListener(this);
173     }
174 
175     @Override
onHeightUpdated()176     public void onHeightUpdated() {
177         int oldMaxHeight = mMaxTranslation;
178         updateExpectedHeight();
179 
180         if (mMaxTranslation != oldMaxHeight) {
181             AllAppsContainerView parent = (AllAppsContainerView) getParent();
182             if (parent != null) {
183                 parent.setupHeader();
184             }
185         }
186     }
187 
188     @Override
onPluginDisconnected(AllAppsRow plugin)189     public void onPluginDisconnected(AllAppsRow plugin) {
190         PluginHeaderRow row = mPluginRows.get(plugin);
191         removeView(row.mView);
192         mPluginRows.remove(plugin);
193         recreateAllRowsArray();
194         onHeightUpdated();
195     }
196 
setup(AllAppsContainerView.AdapterHolder[] mAH, boolean tabsHidden)197     public void setup(AllAppsContainerView.AdapterHolder[] mAH, boolean tabsHidden) {
198         for (FloatingHeaderRow row : mAllRows) {
199             row.setup(this, mAllRows, tabsHidden);
200         }
201         updateExpectedHeight();
202 
203         mTabsHidden = tabsHidden;
204         mTabLayout.setVisibility(tabsHidden ? View.GONE : View.VISIBLE);
205         mMainRV = setupRV(mMainRV, mAH[AllAppsContainerView.AdapterHolder.MAIN].recyclerView);
206         mWorkRV = setupRV(mWorkRV, mAH[AllAppsContainerView.AdapterHolder.WORK].recyclerView);
207         mParent = (ViewGroup) mMainRV.getParent();
208         setMainActive(mMainRVActive || mWorkRV == null);
209         reset(false);
210     }
211 
setupRV(AllAppsRecyclerView old, AllAppsRecyclerView updated)212     private AllAppsRecyclerView setupRV(AllAppsRecyclerView old, AllAppsRecyclerView updated) {
213         if (old != updated && updated != null ) {
214             updated.addOnScrollListener(mOnScrollListener);
215         }
216         return updated;
217     }
218 
updateExpectedHeight()219     private void updateExpectedHeight() {
220         mMaxTranslation = 0;
221         if (mCollapsed) {
222             return;
223         }
224         for (FloatingHeaderRow row : mAllRows) {
225             mMaxTranslation += row.getExpectedHeight();
226         }
227     }
228 
setMainActive(boolean active)229     public void setMainActive(boolean active) {
230         mCurrentRV = active ? mMainRV : mWorkRV;
231         mMainRVActive = active;
232     }
233 
getMaxTranslation()234     public int getMaxTranslation() {
235         if (mMaxTranslation == 0 && mTabsHidden) {
236             return getResources().getDimensionPixelSize(R.dimen.all_apps_search_bar_bottom_padding);
237         } else if (mMaxTranslation > 0 && mTabsHidden) {
238             return mMaxTranslation + getPaddingTop();
239         } else {
240             return mMaxTranslation;
241         }
242     }
243 
canSnapAt(int currentScrollY)244     private boolean canSnapAt(int currentScrollY) {
245         return Math.abs(currentScrollY) <= mMaxTranslation;
246     }
247 
moved(final int currentScrollY)248     private void moved(final int currentScrollY) {
249         if (mHeaderCollapsed) {
250             if (currentScrollY <= mSnappedScrolledY) {
251                 if (canSnapAt(currentScrollY)) {
252                     mSnappedScrolledY = currentScrollY;
253                 }
254             } else {
255                 mHeaderCollapsed = false;
256             }
257             mTranslationY = currentScrollY;
258         } else if (!mHeaderCollapsed) {
259             mTranslationY = currentScrollY - mSnappedScrolledY - mMaxTranslation;
260 
261             // update state vars
262             if (mTranslationY >= 0) { // expanded: must not move down further
263                 mTranslationY = 0;
264                 mSnappedScrolledY = currentScrollY - mMaxTranslation;
265             } else if (mTranslationY <= -mMaxTranslation) { // hide or stay hidden
266                 mHeaderCollapsed = true;
267                 mSnappedScrolledY = -mMaxTranslation;
268             }
269         }
270     }
271 
applyVerticalMove()272     protected void applyVerticalMove() {
273         int uncappedTranslationY = mTranslationY;
274         mTranslationY = Math.max(mTranslationY, -mMaxTranslation);
275 
276         if (mCollapsed || uncappedTranslationY < mTranslationY - mHeaderTopPadding) {
277             // we hide it completely if already capped (for opening search anim)
278             for (FloatingHeaderRow row : mAllRows) {
279                 row.setVerticalScroll(0, true /* isScrolledOut */);
280             }
281         } else {
282             for (FloatingHeaderRow row : mAllRows) {
283                 row.setVerticalScroll(uncappedTranslationY, false /* isScrolledOut */);
284             }
285         }
286 
287         mTabLayout.setTranslationY(mTranslationY);
288         mClip.top = mMaxTranslation + mTranslationY;
289         // clipping on a draw might cause additional redraw
290         mMainRV.setClipBounds(mClip);
291         if (mWorkRV != null) {
292             mWorkRV.setClipBounds(mClip);
293         }
294     }
295 
296     /**
297      * Hides all the floating rows
298      */
setCollapsed(boolean collapse)299     public void setCollapsed(boolean collapse) {
300         if (mCollapsed == collapse) return;
301 
302         mCollapsed = collapse;
303         onHeightUpdated();
304     }
305 
reset(boolean animate)306     public void reset(boolean animate) {
307         if (mAnimator.isStarted()) {
308             mAnimator.cancel();
309         }
310         if (animate) {
311             mAnimator.setIntValues(mTranslationY, 0);
312             mAnimator.addUpdateListener(this);
313             mAnimator.setDuration(150);
314             mAnimator.start();
315         } else {
316             mTranslationY = 0;
317             applyVerticalMove();
318         }
319         mHeaderCollapsed = false;
320         mSnappedScrolledY = -mMaxTranslation;
321         mCurrentRV.scrollToTop();
322     }
323 
isExpanded()324     public boolean isExpanded() {
325         return !mHeaderCollapsed;
326     }
327 
328     @Override
onAnimationUpdate(ValueAnimator animation)329     public void onAnimationUpdate(ValueAnimator animation) {
330         mTranslationY = (Integer) animation.getAnimatedValue();
331         applyVerticalMove();
332     }
333 
334     @Override
onInterceptTouchEvent(MotionEvent ev)335     public boolean onInterceptTouchEvent(MotionEvent ev) {
336         if (!mAllowTouchForwarding) {
337             mForwardToRecyclerView = false;
338             return super.onInterceptTouchEvent(ev);
339         }
340         calcOffset(mTempOffset);
341         ev.offsetLocation(mTempOffset.x, mTempOffset.y);
342         mForwardToRecyclerView = mCurrentRV.onInterceptTouchEvent(ev);
343         ev.offsetLocation(-mTempOffset.x, -mTempOffset.y);
344         return mForwardToRecyclerView || super.onInterceptTouchEvent(ev);
345     }
346 
347     @Override
onTouchEvent(MotionEvent event)348     public boolean onTouchEvent(MotionEvent event) {
349         if (mForwardToRecyclerView) {
350             // take this view's and parent view's (view pager) location into account
351             calcOffset(mTempOffset);
352             event.offsetLocation(mTempOffset.x, mTempOffset.y);
353             try {
354                 return mCurrentRV.onTouchEvent(event);
355             } finally {
356                 event.offsetLocation(-mTempOffset.x, -mTempOffset.y);
357             }
358         } else {
359             return super.onTouchEvent(event);
360         }
361     }
362 
calcOffset(Point p)363     private void calcOffset(Point p) {
364         p.x = getLeft() - mCurrentRV.getLeft() - mParent.getLeft();
365         p.y = getTop() - mCurrentRV.getTop() - mParent.getTop();
366     }
367 
setContentVisibility(boolean hasHeader, boolean hasAllAppsContent, PropertySetter setter, Interpolator headerFade, Interpolator allAppsFade)368     public void setContentVisibility(boolean hasHeader, boolean hasAllAppsContent,
369             PropertySetter setter, Interpolator headerFade, Interpolator allAppsFade) {
370         for (FloatingHeaderRow row : mAllRows) {
371             row.setContentVisibility(hasHeader, hasAllAppsContent, setter, headerFade, allAppsFade);
372         }
373 
374         allowTouchForwarding(hasAllAppsContent);
375         setter.setFloat(mTabLayout, VIEW_ALPHA, hasAllAppsContent ? 1 : 0, headerFade);
376     }
377 
allowTouchForwarding(boolean allow)378     protected void allowTouchForwarding(boolean allow) {
379         mAllowTouchForwarding = allow;
380     }
381 
hasVisibleContent()382     public boolean hasVisibleContent() {
383         for (FloatingHeaderRow row : mAllRows) {
384             if (row.hasVisibleContent()) {
385                 return true;
386             }
387         }
388         return false;
389     }
390 
391     @Override
hasOverlappingRendering()392     public boolean hasOverlappingRendering() {
393         return false;
394     }
395 
396     @Override
setInsets(Rect insets)397     public void setInsets(Rect insets) {
398         DeviceProfile grid = BaseDraggingActivity.fromContext(getContext()).getDeviceProfile();
399         for (FloatingHeaderRow row : mAllRows) {
400             row.setInsets(insets, grid);
401         }
402     }
403 
findFixedRowByType(Class<T> type)404     public <T extends FloatingHeaderRow> T findFixedRowByType(Class<T> type) {
405         for (FloatingHeaderRow row : mAllRows) {
406             if (row.getTypeClass() == type) {
407                 return (T) row;
408             }
409         }
410         return null;
411     }
412 }
413 
414 
415