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