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