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