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