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