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