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