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 android.animation.ValueAnimator;
19 import android.content.Context;
20 import android.graphics.Point;
21 import android.graphics.Rect;
22 import android.util.ArrayMap;
23 import android.util.AttributeSet;
24 import android.view.MotionEvent;
25 import android.view.View;
26 import android.view.ViewGroup;
27 import android.widget.LinearLayout;
28 
29 import androidx.annotation.NonNull;
30 import androidx.annotation.Nullable;
31 import androidx.recyclerview.widget.RecyclerView;
32 
33 import com.android.launcher3.Insettable;
34 import com.android.launcher3.R;
35 import com.android.launcher3.allapps.ActivityAllAppsContainerView.AdapterHolder;
36 import com.android.launcher3.config.FeatureFlags;
37 import com.android.launcher3.util.PluginManagerWrapper;
38 import com.android.launcher3.views.ActivityContext;
39 import com.android.systemui.plugins.AllAppsRow;
40 import com.android.systemui.plugins.AllAppsRow.OnHeightUpdatedListener;
41 import com.android.systemui.plugins.PluginListener;
42 
43 import java.util.ArrayList;
44 import java.util.Arrays;
45 import java.util.Map;
46 
47 public class FloatingHeaderView extends LinearLayout implements
48         ValueAnimator.AnimatorUpdateListener, PluginListener<AllAppsRow>, Insettable,
49         OnHeightUpdatedListener {
50 
51     private final Rect mRVClip = new Rect(0, 0, Integer.MAX_VALUE, Integer.MAX_VALUE);
52     private final Rect mHeaderClip = new Rect(0, 0, Integer.MAX_VALUE, Integer.MAX_VALUE);
53     private final ValueAnimator mAnimator = ValueAnimator.ofInt(0, 0);
54     private final Point mTempOffset = new Point();
55     private final RecyclerView.OnScrollListener mOnScrollListener =
56             new RecyclerView.OnScrollListener() {
57                 @Override
58                 public void onScrollStateChanged(@NonNull RecyclerView rv, int newState) {}
59 
60                 @Override
61                 public void onScrolled(@NonNull RecyclerView rv, int dx, int dy) {
62                     if (rv != mCurrentRV) {
63                         return;
64                     }
65 
66                     if (mAnimator.isStarted()) {
67                         mAnimator.cancel();
68                     }
69 
70                     int current = -mCurrentRV.computeVerticalScrollOffset();
71                     boolean headerCollapsed = mHeaderCollapsed;
72                     moved(current);
73                     applyVerticalMove();
74                     if (headerCollapsed != mHeaderCollapsed) {
75                         ActivityAllAppsContainerView<?> parent =
76                                 (ActivityAllAppsContainerView<?>) getParent();
77                         parent.invalidateHeader();
78                     }
79                 }
80             };
81 
82     protected final Map<AllAppsRow, PluginHeaderRow> mPluginRows = new ArrayMap<>();
83 
84     // These two values are necessary to ensure that the header protection is drawn correctly.
85     private final int mTabsAdditionalPaddingTop;
86     private final int mTabsAdditionalPaddingBottom;
87 
88     protected ViewGroup mTabLayout;
89     private AllAppsRecyclerView mMainRV;
90     private AllAppsRecyclerView mWorkRV;
91     private SearchRecyclerView mSearchRV;
92     private AllAppsRecyclerView mCurrentRV;
93     protected int mSnappedScrolledY;
94     private int mTranslationY;
95 
96     private boolean mForwardToRecyclerView;
97 
98     protected boolean mTabsHidden;
99     protected int mMaxTranslation;
100 
101     // Whether the header has been scrolled off-screen.
102     private boolean mHeaderCollapsed;
103     // Whether floating rows like predicted apps are hidden.
104     private boolean mFloatingRowsCollapsed;
105     // Total height of all current floating rows. Collapsed rows == 0 height.
106     private int mFloatingRowsHeight;
107 
108     // This is initialized once during inflation and stays constant after that. Fixed views
109     // cannot be added or removed dynamically.
110     private FloatingHeaderRow[] mFixedRows = FloatingHeaderRow.NO_ROWS;
111 
112     // Array of all fixed rows and plugin rows. This is initialized every time a plugin is
113     // enabled or disabled, and represent the current set of all rows.
114     private FloatingHeaderRow[] mAllRows = FloatingHeaderRow.NO_ROWS;
115 
FloatingHeaderView(@onNull Context context)116     public FloatingHeaderView(@NonNull Context context) {
117         this(context, null);
118     }
119 
FloatingHeaderView(@onNull Context context, @Nullable AttributeSet attrs)120     public FloatingHeaderView(@NonNull Context context, @Nullable AttributeSet attrs) {
121         super(context, attrs);
122         mTabsAdditionalPaddingTop = context.getResources()
123                 .getDimensionPixelSize(R.dimen.all_apps_header_top_adjustment);
124         mTabsAdditionalPaddingBottom = context.getResources()
125                 .getDimensionPixelSize(R.dimen.all_apps_header_bottom_adjustment);
126     }
127 
128     @Override
onFinishInflate()129     protected void onFinishInflate() {
130         super.onFinishInflate();
131         mTabLayout = findViewById(R.id.tabs);
132 
133         // Find all floating header rows.
134         ArrayList<FloatingHeaderRow> rows = new ArrayList<>();
135         int count = getChildCount();
136         for (int i = 0; i < count; i++) {
137             View child = getChildAt(i);
138             if (child instanceof FloatingHeaderRow) {
139                 rows.add((FloatingHeaderRow) child);
140             }
141         }
142         mFixedRows = rows.toArray(new FloatingHeaderRow[rows.size()]);
143         mAllRows = mFixedRows;
144         updateFloatingRowsHeight();
145     }
146 
147     @Override
onAttachedToWindow()148     protected void onAttachedToWindow() {
149         super.onAttachedToWindow();
150         PluginManagerWrapper.INSTANCE.get(getContext()).addPluginListener(this,
151                 AllAppsRow.class, true /* allowMultiple */);
152     }
153 
154     @Override
onDetachedFromWindow()155     protected void onDetachedFromWindow() {
156         super.onDetachedFromWindow();
157         PluginManagerWrapper.INSTANCE.get(getContext()).removePluginListener(this);
158     }
159 
recreateAllRowsArray()160     private void recreateAllRowsArray() {
161         int pluginCount = mPluginRows.size();
162         if (pluginCount == 0) {
163             mAllRows = mFixedRows;
164         } else {
165             int count = mFixedRows.length;
166             mAllRows = new FloatingHeaderRow[count + pluginCount];
167             for (int i = 0; i < count; i++) {
168                 mAllRows[i] = mFixedRows[i];
169             }
170 
171             for (PluginHeaderRow row : mPluginRows.values()) {
172                 mAllRows[count] = row;
173                 count++;
174             }
175         }
176         updateFloatingRowsHeight();
177     }
178 
179     @Override
onPluginConnected(AllAppsRow allAppsRowPlugin, Context context)180     public void onPluginConnected(AllAppsRow allAppsRowPlugin, Context context) {
181         PluginHeaderRow headerRow = new PluginHeaderRow(allAppsRowPlugin, this);
182         addView(headerRow.mView, indexOfChild(mTabLayout));
183         mPluginRows.put(allAppsRowPlugin, headerRow);
184         recreateAllRowsArray();
185         allAppsRowPlugin.setOnHeightUpdatedListener(this);
186     }
187 
188     @Override
onHeightUpdated()189     public void onHeightUpdated() {
190         int oldMaxHeight = mMaxTranslation;
191         updateExpectedHeight();
192 
193         if (mMaxTranslation != oldMaxHeight || mFloatingRowsCollapsed) {
194             ActivityAllAppsContainerView parent = (ActivityAllAppsContainerView) getParent();
195             if (parent != null) {
196                 parent.setupHeader();
197             }
198         }
199     }
200 
201     @Override
onPluginDisconnected(AllAppsRow plugin)202     public void onPluginDisconnected(AllAppsRow plugin) {
203         PluginHeaderRow row = mPluginRows.get(plugin);
204         removeView(row.mView);
205         mPluginRows.remove(plugin);
206         recreateAllRowsArray();
207         onHeightUpdated();
208     }
209 
210     @Override
getFocusedChild()211     public View getFocusedChild() {
212         if (FeatureFlags.ENABLE_DEVICE_SEARCH.get()) {
213             for (FloatingHeaderRow row : mAllRows) {
214                 if (row.hasVisibleContent() && row.isVisible()) {
215                     return row.getFocusedChild();
216                 }
217             }
218             return null;
219         }
220         return super.getFocusedChild();
221     }
222 
setup(AllAppsRecyclerView mainRV, AllAppsRecyclerView workRV, SearchRecyclerView searchRV, int activeRV, boolean tabsHidden)223     void setup(AllAppsRecyclerView mainRV, AllAppsRecyclerView workRV, SearchRecyclerView searchRV,
224             int activeRV, boolean tabsHidden) {
225         for (FloatingHeaderRow row : mAllRows) {
226             row.setup(this, mAllRows, tabsHidden);
227         }
228 
229         mTabsHidden = tabsHidden;
230         maybeSetTabVisibility(VISIBLE);
231         updateExpectedHeight();
232         mMainRV = mainRV;
233         mWorkRV = workRV;
234         mSearchRV = searchRV;
235         setActiveRV(activeRV);
236         reset(false);
237     }
238 
239     /** Whether this header has been set up previously. */
isSetUp()240     boolean isSetUp() {
241         return mMainRV != null;
242     }
243 
244     /** Set the active AllApps RV which will adjust the alpha of the header when scrolled. */
setActiveRV(int rvType)245     void setActiveRV(int rvType) {
246         if (mCurrentRV != null) {
247             mCurrentRV.removeOnScrollListener(mOnScrollListener);
248         }
249         mCurrentRV =
250                 rvType == AdapterHolder.MAIN ? mMainRV
251                 : rvType == AdapterHolder.WORK ? mWorkRV : mSearchRV;
252         mCurrentRV.addOnScrollListener(mOnScrollListener);
253         maybeSetTabVisibility(rvType == AdapterHolder.SEARCH ? GONE : VISIBLE);
254     }
255 
256     /** Update tab visibility to the given state, only if tabs are active (work profile exists). */
maybeSetTabVisibility(int visibility)257     void maybeSetTabVisibility(int visibility) {
258         mTabLayout.setVisibility(mTabsHidden ? GONE : visibility);
259     }
260 
updateExpectedHeight()261     private void updateExpectedHeight() {
262         updateFloatingRowsHeight();
263         mMaxTranslation = 0;
264         if (mFloatingRowsCollapsed) {
265             return;
266         }
267         mMaxTranslation += mFloatingRowsHeight;
268         if (!mTabsHidden) {
269             mMaxTranslation += mTabsAdditionalPaddingBottom
270                     + getResources().getDimensionPixelSize(R.dimen.all_apps_tabs_margin_top);
271         }
272     }
273 
getMaxTranslation()274     int getMaxTranslation() {
275         if (mMaxTranslation == 0 && (mTabsHidden || mFloatingRowsCollapsed)) {
276             return getResources().getDimensionPixelSize(R.dimen.all_apps_search_bar_bottom_padding);
277         } else if (mMaxTranslation > 0 && mTabsHidden) {
278             return mMaxTranslation + getPaddingTop();
279         } else {
280             return mMaxTranslation;
281         }
282     }
283 
canSnapAt(int currentScrollY)284     private boolean canSnapAt(int currentScrollY) {
285         return Math.abs(currentScrollY) <= mMaxTranslation;
286     }
287 
moved(final int currentScrollY)288     private void moved(final int currentScrollY) {
289         if (mHeaderCollapsed) {
290             if (currentScrollY <= mSnappedScrolledY) {
291                 if (canSnapAt(currentScrollY)) {
292                     mSnappedScrolledY = currentScrollY;
293                 }
294             } else {
295                 mHeaderCollapsed = false;
296             }
297             mTranslationY = currentScrollY;
298         } else {
299             mTranslationY = currentScrollY - mSnappedScrolledY - mMaxTranslation;
300 
301             // update state vars
302             if (mTranslationY >= 0) { // expanded: must not move down further
303                 mTranslationY = 0;
304                 mSnappedScrolledY = currentScrollY - mMaxTranslation;
305             } else if (mTranslationY <= -mMaxTranslation) { // hide or stay hidden
306                 mHeaderCollapsed = true;
307                 mSnappedScrolledY = -mMaxTranslation;
308             }
309         }
310     }
311 
applyVerticalMove()312     protected void applyVerticalMove() {
313         int uncappedTranslationY = mTranslationY;
314         mTranslationY = Math.max(mTranslationY, -mMaxTranslation);
315 
316         if (mFloatingRowsCollapsed || uncappedTranslationY < mTranslationY - getPaddingTop()) {
317             // we hide it completely if already capped (for opening search anim)
318             for (FloatingHeaderRow row : mAllRows) {
319                 row.setVerticalScroll(0, true /* isScrolledOut */);
320             }
321         } else {
322             for (FloatingHeaderRow row : mAllRows) {
323                 row.setVerticalScroll(uncappedTranslationY, false /* isScrolledOut */);
324             }
325         }
326 
327         mTabLayout.setTranslationY(mTranslationY);
328 
329         int clipTop = getPaddingTop() - mTabsAdditionalPaddingTop;
330         if (mTabsHidden) {
331             // Add back spacing that is otherwise covered by the tabs.
332             clipTop += mTabsAdditionalPaddingTop;
333         }
334         mRVClip.top = mTabsHidden || mFloatingRowsCollapsed ? clipTop : 0;
335         mHeaderClip.top = clipTop;
336         // clipping on a draw might cause additional redraw
337         setClipBounds(mHeaderClip);
338         if (mMainRV != null) {
339             mMainRV.setClipBounds(mRVClip);
340         }
341         if (mWorkRV != null) {
342             mWorkRV.setClipBounds(mRVClip);
343         }
344         if (mSearchRV != null) {
345             mSearchRV.setClipBounds(mRVClip);
346         }
347     }
348 
349     /**
350      * Hides all the floating rows
351      */
setFloatingRowsCollapsed(boolean collapsed)352     public void setFloatingRowsCollapsed(boolean collapsed) {
353         if (mFloatingRowsCollapsed == collapsed) {
354             return;
355         }
356 
357         mFloatingRowsCollapsed = collapsed;
358         onHeightUpdated();
359     }
360 
getClipTop()361     public int getClipTop() {
362         return mHeaderClip.top;
363     }
364 
reset(boolean animate)365     public void reset(boolean animate) {
366         if (mAnimator.isStarted()) {
367             mAnimator.cancel();
368         }
369         if (animate) {
370             mAnimator.setIntValues(mTranslationY, 0);
371             mAnimator.addUpdateListener(this);
372             mAnimator.setDuration(150);
373             mAnimator.start();
374         } else {
375             mTranslationY = 0;
376             applyVerticalMove();
377         }
378         mHeaderCollapsed = false;
379         mSnappedScrolledY = -mMaxTranslation;
380         mCurrentRV.scrollToTop();
381     }
382 
isExpanded()383     public boolean isExpanded() {
384         return !mHeaderCollapsed;
385     }
386 
387     /** Returns true if personal/work tabs are currently in use. */
usingTabs()388     public boolean usingTabs() {
389         return !mTabsHidden;
390     }
391 
getTabLayout()392     ViewGroup getTabLayout() {
393         return mTabLayout;
394     }
395 
396     /** Calculates the combined height of any floating rows (e.g. predicted apps, app divider). */
updateFloatingRowsHeight()397     private void updateFloatingRowsHeight() {
398         mFloatingRowsHeight =
399                 Arrays.stream(mAllRows).mapToInt(FloatingHeaderRow::getExpectedHeight).sum();
400     }
401 
402     /** Gets the combined height of any floating rows (e.g. predicted apps, app divider). */
getFloatingRowsHeight()403     int getFloatingRowsHeight() {
404         return mFloatingRowsHeight;
405     }
406 
getTabsAdditionalPaddingBottom()407     int getTabsAdditionalPaddingBottom() {
408         return mTabsAdditionalPaddingBottom;
409     }
410 
411     @Override
onAnimationUpdate(ValueAnimator animation)412     public void onAnimationUpdate(ValueAnimator animation) {
413         mTranslationY = (Integer) animation.getAnimatedValue();
414         applyVerticalMove();
415     }
416 
417     @Override
onInterceptTouchEvent(MotionEvent ev)418     public boolean onInterceptTouchEvent(MotionEvent ev) {
419         calcOffset(mTempOffset);
420         ev.offsetLocation(mTempOffset.x, mTempOffset.y);
421         mForwardToRecyclerView = mCurrentRV.onInterceptTouchEvent(ev);
422         ev.offsetLocation(-mTempOffset.x, -mTempOffset.y);
423         return mForwardToRecyclerView || super.onInterceptTouchEvent(ev);
424     }
425 
426     @Override
onTouchEvent(MotionEvent event)427     public boolean onTouchEvent(MotionEvent event) {
428         if (mForwardToRecyclerView) {
429             // take this view's and parent view's (view pager) location into account
430             calcOffset(mTempOffset);
431             event.offsetLocation(mTempOffset.x, mTempOffset.y);
432             try {
433                 return mCurrentRV.onTouchEvent(event);
434             } finally {
435                 event.offsetLocation(-mTempOffset.x, -mTempOffset.y);
436             }
437         } else {
438             return super.onTouchEvent(event);
439         }
440     }
441 
calcOffset(Point p)442     private void calcOffset(Point p) {
443         p.x = getLeft() - mCurrentRV.getLeft() - ((ViewGroup) mCurrentRV.getParent()).getLeft();
444         p.y = getTop() - mCurrentRV.getTop() - ((ViewGroup) mCurrentRV.getParent()).getTop();
445     }
446 
447     @Override
hasOverlappingRendering()448     public boolean hasOverlappingRendering() {
449         return false;
450     }
451 
452     @Override
setInsets(Rect insets)453     public void setInsets(Rect insets) {
454         Rect allAppsPadding = ActivityContext.lookupContext(getContext())
455                 .getDeviceProfile().allAppsPadding;
456         setPadding(allAppsPadding.left, getPaddingTop(), allAppsPadding.right, getPaddingBottom());
457     }
458 
findFixedRowByType(Class<T> type)459     public <T extends FloatingHeaderRow> T findFixedRowByType(Class<T> type) {
460         for (FloatingHeaderRow row : mAllRows) {
461             if (row.getTypeClass() == type) {
462                 return (T) row;
463             }
464         }
465         return null;
466     }
467 
468     /**
469      * Returns visible height of FloatingHeaderView contents requiring header protection or the
470      * expected header protection height.
471      */
getPeripheralProtectionHeight(boolean expected)472     int getPeripheralProtectionHeight(boolean expected) {
473         if (expected) {
474             return getTabLayout().getBottom() - getPaddingTop() + getPaddingBottom()
475                     - mMaxTranslation;
476         }
477         // we only want to show protection when work tab is available and header is either
478         // collapsed or animating to/from collapsed state
479         if (mTabsHidden || mFloatingRowsCollapsed || !mHeaderCollapsed) {
480             return 0;
481         }
482         return Math.max(0,
483                 getTabLayout().getBottom() - getPaddingTop() + getPaddingBottom() + mTranslationY);
484     }
485 }
486