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