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