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.uioverrides.plugins.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 updateExpectedHeight(); 229 230 mTabsHidden = tabsHidden; 231 mTabLayout.setVisibility(tabsHidden ? View.GONE : View.VISIBLE); 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 } 254 updateExpectedHeight()255 private void updateExpectedHeight() { 256 updateFloatingRowsHeight(); 257 mMaxTranslation = 0; 258 if (mFloatingRowsCollapsed) { 259 return; 260 } 261 mMaxTranslation += mFloatingRowsHeight; 262 if (!mTabsHidden) { 263 mMaxTranslation += mTabsAdditionalPaddingBottom 264 + getResources().getDimensionPixelSize(R.dimen.all_apps_tabs_margin_top); 265 } 266 } 267 getMaxTranslation()268 int getMaxTranslation() { 269 if (mMaxTranslation == 0 && (mTabsHidden || mFloatingRowsCollapsed)) { 270 return getResources().getDimensionPixelSize(R.dimen.all_apps_search_bar_bottom_padding); 271 } else if (mMaxTranslation > 0 && mTabsHidden) { 272 return mMaxTranslation + getPaddingTop(); 273 } else { 274 return mMaxTranslation; 275 } 276 } 277 canSnapAt(int currentScrollY)278 private boolean canSnapAt(int currentScrollY) { 279 return Math.abs(currentScrollY) <= mMaxTranslation; 280 } 281 moved(final int currentScrollY)282 private void moved(final int currentScrollY) { 283 if (mHeaderCollapsed) { 284 if (currentScrollY <= mSnappedScrolledY) { 285 if (canSnapAt(currentScrollY)) { 286 mSnappedScrolledY = currentScrollY; 287 } 288 } else { 289 mHeaderCollapsed = false; 290 } 291 mTranslationY = currentScrollY; 292 } else { 293 mTranslationY = currentScrollY - mSnappedScrolledY - mMaxTranslation; 294 295 // update state vars 296 if (mTranslationY >= 0) { // expanded: must not move down further 297 mTranslationY = 0; 298 mSnappedScrolledY = currentScrollY - mMaxTranslation; 299 } else if (mTranslationY <= -mMaxTranslation) { // hide or stay hidden 300 mHeaderCollapsed = true; 301 mSnappedScrolledY = -mMaxTranslation; 302 } 303 } 304 } 305 applyVerticalMove()306 protected void applyVerticalMove() { 307 int uncappedTranslationY = mTranslationY; 308 mTranslationY = Math.max(mTranslationY, -mMaxTranslation); 309 310 if (mFloatingRowsCollapsed || uncappedTranslationY < mTranslationY - getPaddingTop()) { 311 // we hide it completely if already capped (for opening search anim) 312 for (FloatingHeaderRow row : mAllRows) { 313 row.setVerticalScroll(0, true /* isScrolledOut */); 314 } 315 } else { 316 for (FloatingHeaderRow row : mAllRows) { 317 row.setVerticalScroll(uncappedTranslationY, false /* isScrolledOut */); 318 } 319 } 320 321 mTabLayout.setTranslationY(mTranslationY); 322 323 int clipTop = getPaddingTop() - mTabsAdditionalPaddingTop; 324 if (mTabsHidden) { 325 // Add back spacing that is otherwise covered by the tabs. 326 clipTop += mTabsAdditionalPaddingTop; 327 } 328 mRVClip.top = mTabsHidden || mFloatingRowsCollapsed ? clipTop : 0; 329 mHeaderClip.top = clipTop; 330 // clipping on a draw might cause additional redraw 331 setClipBounds(mHeaderClip); 332 if (mMainRV != null) { 333 mMainRV.setClipBounds(mRVClip); 334 } 335 if (mWorkRV != null) { 336 mWorkRV.setClipBounds(mRVClip); 337 } 338 if (mSearchRV != null) { 339 mSearchRV.setClipBounds(mRVClip); 340 } 341 } 342 343 /** 344 * Hides all the floating rows 345 */ setFloatingRowsCollapsed(boolean collapsed)346 public void setFloatingRowsCollapsed(boolean collapsed) { 347 if (mFloatingRowsCollapsed == collapsed) { 348 return; 349 } 350 351 mFloatingRowsCollapsed = collapsed; 352 onHeightUpdated(); 353 } 354 getClipTop()355 public int getClipTop() { 356 return mHeaderClip.top; 357 } 358 reset(boolean animate)359 public void reset(boolean animate) { 360 if (mAnimator.isStarted()) { 361 mAnimator.cancel(); 362 } 363 if (animate) { 364 mAnimator.setIntValues(mTranslationY, 0); 365 mAnimator.addUpdateListener(this); 366 mAnimator.setDuration(150); 367 mAnimator.start(); 368 } else { 369 mTranslationY = 0; 370 applyVerticalMove(); 371 } 372 mHeaderCollapsed = false; 373 mSnappedScrolledY = -mMaxTranslation; 374 mCurrentRV.scrollToTop(); 375 } 376 isExpanded()377 public boolean isExpanded() { 378 return !mHeaderCollapsed; 379 } 380 381 /** Returns true if personal/work tabs are currently in use. */ usingTabs()382 public boolean usingTabs() { 383 return !mTabsHidden; 384 } 385 getTabLayout()386 ViewGroup getTabLayout() { 387 return mTabLayout; 388 } 389 390 /** Calculates the combined height of any floating rows (e.g. predicted apps, app divider). */ updateFloatingRowsHeight()391 private void updateFloatingRowsHeight() { 392 mFloatingRowsHeight = 393 Arrays.stream(mAllRows).mapToInt(FloatingHeaderRow::getExpectedHeight).sum(); 394 } 395 396 /** Gets the combined height of any floating rows (e.g. predicted apps, app divider). */ getFloatingRowsHeight()397 int getFloatingRowsHeight() { 398 return mFloatingRowsHeight; 399 } 400 getTabsAdditionalPaddingBottom()401 int getTabsAdditionalPaddingBottom() { 402 return mTabsAdditionalPaddingBottom; 403 } 404 405 @Override onAnimationUpdate(ValueAnimator animation)406 public void onAnimationUpdate(ValueAnimator animation) { 407 mTranslationY = (Integer) animation.getAnimatedValue(); 408 applyVerticalMove(); 409 } 410 411 @Override onInterceptTouchEvent(MotionEvent ev)412 public boolean onInterceptTouchEvent(MotionEvent ev) { 413 calcOffset(mTempOffset); 414 ev.offsetLocation(mTempOffset.x, mTempOffset.y); 415 mForwardToRecyclerView = mCurrentRV.onInterceptTouchEvent(ev); 416 ev.offsetLocation(-mTempOffset.x, -mTempOffset.y); 417 return mForwardToRecyclerView || super.onInterceptTouchEvent(ev); 418 } 419 420 @Override onTouchEvent(MotionEvent event)421 public boolean onTouchEvent(MotionEvent event) { 422 if (mForwardToRecyclerView) { 423 // take this view's and parent view's (view pager) location into account 424 calcOffset(mTempOffset); 425 event.offsetLocation(mTempOffset.x, mTempOffset.y); 426 try { 427 return mCurrentRV.onTouchEvent(event); 428 } finally { 429 event.offsetLocation(-mTempOffset.x, -mTempOffset.y); 430 } 431 } else { 432 return super.onTouchEvent(event); 433 } 434 } 435 calcOffset(Point p)436 private void calcOffset(Point p) { 437 p.x = getLeft() - mCurrentRV.getLeft() - ((ViewGroup) mCurrentRV.getParent()).getLeft(); 438 p.y = getTop() - mCurrentRV.getTop() - ((ViewGroup) mCurrentRV.getParent()).getTop(); 439 } 440 441 @Override hasOverlappingRendering()442 public boolean hasOverlappingRendering() { 443 return false; 444 } 445 446 @Override setInsets(Rect insets)447 public void setInsets(Rect insets) { 448 int leftRightPadding = ActivityContext.lookupContext(getContext()) 449 .getDeviceProfile().allAppsLeftRightPadding; 450 setPadding(leftRightPadding, getPaddingTop(), leftRightPadding, getPaddingBottom()); 451 } 452 findFixedRowByType(Class<T> type)453 public <T extends FloatingHeaderRow> T findFixedRowByType(Class<T> type) { 454 for (FloatingHeaderRow row : mAllRows) { 455 if (row.getTypeClass() == type) { 456 return (T) row; 457 } 458 } 459 return null; 460 } 461 462 /** 463 * Returns visible height of FloatingHeaderView contents requiring header protection 464 */ getPeripheralProtectionHeight()465 int getPeripheralProtectionHeight() { 466 // we only want to show protection when work tab is available and header is either 467 // collapsed or animating to/from collapsed state 468 if (mTabsHidden || mFloatingRowsCollapsed || !mHeaderCollapsed) { 469 return 0; 470 } 471 return Math.max(0, 472 getTabLayout().getBottom() - getPaddingTop() + getPaddingBottom() + mTranslationY); 473 } 474 } 475