1 /* 2 * Copyright (C) 2015 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.Paint; 22 import android.graphics.Point; 23 import android.graphics.Rect; 24 import android.os.Process; 25 import android.support.animation.DynamicAnimation; 26 import android.support.annotation.NonNull; 27 import android.support.annotation.Nullable; 28 import android.support.annotation.StringRes; 29 import android.support.v7.widget.LinearLayoutManager; 30 import android.support.v7.widget.RecyclerView; 31 import android.text.Selection; 32 import android.text.SpannableStringBuilder; 33 import android.util.AttributeSet; 34 import android.view.KeyEvent; 35 import android.view.LayoutInflater; 36 import android.view.MotionEvent; 37 import android.view.View; 38 import android.view.ViewGroup; 39 40 import com.android.launcher3.AppInfo; 41 import com.android.launcher3.DeviceProfile; 42 import com.android.launcher3.DeviceProfile.OnDeviceProfileChangeListener; 43 import com.android.launcher3.DragSource; 44 import com.android.launcher3.DropTarget.DragObject; 45 import com.android.launcher3.Insettable; 46 import com.android.launcher3.InsettableFrameLayout; 47 import com.android.launcher3.ItemInfo; 48 import com.android.launcher3.Launcher; 49 import com.android.launcher3.R; 50 import com.android.launcher3.Utilities; 51 import com.android.launcher3.config.FeatureFlags; 52 import com.android.launcher3.keyboard.FocusedItemDecorator; 53 import com.android.launcher3.userevent.nano.LauncherLogProto.Target; 54 import com.android.launcher3.util.ItemInfoMatcher; 55 import com.android.launcher3.util.Themes; 56 import com.android.launcher3.views.BottomUserEducationView; 57 import com.android.launcher3.views.RecyclerViewFastScroller; 58 import com.android.launcher3.views.SpringRelativeLayout; 59 60 /** 61 * The all apps view container. 62 */ 63 public class AllAppsContainerView extends SpringRelativeLayout implements DragSource, 64 Insettable, OnDeviceProfileChangeListener { 65 66 private static final float FLING_VELOCITY_MULTIPLIER = 135f; 67 // Starts the springs after at least 55% of the animation has passed. 68 private static final float FLING_ANIMATION_THRESHOLD = 0.55f; 69 70 private final Launcher mLauncher; 71 private final AdapterHolder[] mAH; 72 private final ItemInfoMatcher mPersonalMatcher = ItemInfoMatcher.ofUser(Process.myUserHandle()); 73 private final ItemInfoMatcher mWorkMatcher = ItemInfoMatcher.not(mPersonalMatcher); 74 private final AllAppsStore mAllAppsStore = new AllAppsStore(); 75 76 private final Paint mNavBarScrimPaint; 77 private int mNavBarScrimHeight = 0; 78 79 private SearchUiManager mSearchUiManager; 80 private View mSearchContainer; 81 private AllAppsPagedView mViewPager; 82 private FloatingHeaderView mHeader; 83 84 private SpannableStringBuilder mSearchQueryBuilder = null; 85 86 private boolean mUsingTabs; 87 private boolean mSearchModeWhileUsingTabs = false; 88 89 private RecyclerViewFastScroller mTouchHandler; 90 private final Point mFastScrollerOffset = new Point(); 91 92 public AllAppsContainerView(Context context) { 93 this(context, null); 94 } 95 96 public AllAppsContainerView(Context context, AttributeSet attrs) { 97 this(context, attrs, 0); 98 } 99 100 public AllAppsContainerView(Context context, AttributeSet attrs, int defStyleAttr) { 101 super(context, attrs, defStyleAttr); 102 103 mLauncher = Launcher.getLauncher(context); 104 mLauncher.addOnDeviceProfileChangeListener(this); 105 106 mSearchQueryBuilder = new SpannableStringBuilder(); 107 Selection.setSelection(mSearchQueryBuilder, 0); 108 109 mAH = new AdapterHolder[2]; 110 mAH[AdapterHolder.MAIN] = new AdapterHolder(false /* isWork */); 111 mAH[AdapterHolder.WORK] = new AdapterHolder(true /* isWork */); 112 113 mNavBarScrimPaint = new Paint(); 114 mNavBarScrimPaint.setColor(Themes.getAttrColor(context, R.attr.allAppsNavBarScrimColor)); 115 116 mAllAppsStore.addUpdateListener(this::onAppsUpdated); 117 118 addSpringView(R.id.all_apps_header); 119 addSpringView(R.id.apps_list_view); 120 addSpringView(R.id.all_apps_tabs_view_pager); 121 } 122 123 public AllAppsStore getAppsStore() { 124 return mAllAppsStore; 125 } 126 127 @Override 128 protected void setDampedScrollShift(float shift) { 129 // Bound the shift amount to avoid content from drawing on top (Y-val) of the QSB. 130 float maxShift = getSearchView().getHeight() / 2f; 131 super.setDampedScrollShift(Utilities.boundToRange(shift, -maxShift, maxShift)); 132 } 133 134 @Override 135 public void onDeviceProfileChanged(DeviceProfile dp) { 136 for (AdapterHolder holder : mAH) { 137 if (holder.recyclerView != null) { 138 // Remove all views and clear the pool, while keeping the data same. After this 139 // call, all the viewHolders will be recreated. 140 holder.recyclerView.swapAdapter(holder.recyclerView.getAdapter(), true); 141 holder.recyclerView.getRecycledViewPool().clear(); 142 } 143 } 144 } 145 146 private void onAppsUpdated() { 147 if (FeatureFlags.ALL_APPS_TABS_ENABLED) { 148 boolean hasWorkApps = false; 149 for (AppInfo app : mAllAppsStore.getApps()) { 150 if (mWorkMatcher.matches(app, null)) { 151 hasWorkApps = true; 152 break; 153 } 154 } 155 rebindAdapters(hasWorkApps); 156 } 157 } 158 159 /** 160 * Returns whether the view itself will handle the touch event or not. 161 */ 162 public boolean shouldContainerScroll(MotionEvent ev) { 163 // IF the MotionEvent is inside the search box, and the container keeps on receiving 164 // touch input, container should move down. 165 if (mLauncher.getDragLayer().isEventOverView(mSearchContainer, ev)) { 166 return true; 167 } 168 AllAppsRecyclerView rv = getActiveRecyclerView(); 169 if (rv == null) { 170 return true; 171 } 172 if (rv.getScrollbar().getThumbOffsetY() >= 0 && 173 mLauncher.getDragLayer().isEventOverView(rv.getScrollbar(), ev)) { 174 return false; 175 } 176 return rv.shouldContainerScroll(ev, mLauncher.getDragLayer()); 177 } 178 179 @Override 180 public boolean onInterceptTouchEvent(MotionEvent ev) { 181 if (ev.getAction() == MotionEvent.ACTION_DOWN) { 182 AllAppsRecyclerView rv = getActiveRecyclerView(); 183 if (rv != null && 184 rv.getScrollbar().isHitInParent(ev.getX(), ev.getY(), mFastScrollerOffset)) { 185 mTouchHandler = rv.getScrollbar(); 186 } 187 } 188 if (mTouchHandler != null) { 189 return mTouchHandler.handleTouchEvent(ev, mFastScrollerOffset); 190 } 191 return false; 192 } 193 194 @Override 195 public boolean onTouchEvent(MotionEvent ev) { 196 if (mTouchHandler != null) { 197 mTouchHandler.handleTouchEvent(ev, mFastScrollerOffset); 198 return true; 199 } 200 return false; 201 } 202 203 public String getDescription() { 204 @StringRes int descriptionRes; 205 if (mUsingTabs) { 206 descriptionRes = 207 mViewPager.getNextPage() == 0 208 ? R.string.all_apps_button_personal_label 209 : R.string.all_apps_button_work_label; 210 } else { 211 descriptionRes = R.string.all_apps_button_label; 212 } 213 return getContext().getString(descriptionRes); 214 } 215 216 public AllAppsRecyclerView getActiveRecyclerView() { 217 if (!mUsingTabs || mViewPager.getNextPage() == 0) { 218 return mAH[AdapterHolder.MAIN].recyclerView; 219 } else { 220 return mAH[AdapterHolder.WORK].recyclerView; 221 } 222 } 223 224 /** 225 * Resets the state of AllApps. 226 */ 227 public void reset(boolean animate) { 228 for (int i = 0; i < mAH.length; i++) { 229 if (mAH[i].recyclerView != null) { 230 mAH[i].recyclerView.scrollToTop(); 231 } 232 } 233 if (isHeaderVisible()) { 234 mHeader.reset(animate); 235 } 236 // Reset the search bar and base recycler view after transitioning home 237 mSearchUiManager.resetSearch(); 238 } 239 240 @Override 241 protected void onFinishInflate() { 242 super.onFinishInflate(); 243 244 // This is a focus listener that proxies focus from a view into the list view. This is to 245 // work around the search box from getting first focus and showing the cursor. 246 setOnFocusChangeListener((v, hasFocus) -> { 247 if (hasFocus && getActiveRecyclerView() != null) { 248 getActiveRecyclerView().requestFocus(); 249 } 250 }); 251 252 mHeader = findViewById(R.id.all_apps_header); 253 rebindAdapters(mUsingTabs, true /* force */); 254 255 mSearchContainer = findViewById(R.id.search_container_all_apps); 256 mSearchUiManager = (SearchUiManager) mSearchContainer; 257 mSearchUiManager.initialize(this); 258 } 259 260 public SearchUiManager getSearchUiManager() { 261 return mSearchUiManager; 262 } 263 264 @Override 265 public boolean dispatchKeyEvent(KeyEvent event) { 266 mSearchUiManager.preDispatchKeyEvent(event); 267 return super.dispatchKeyEvent(event); 268 } 269 270 @Override 271 public void onDropCompleted(View target, DragObject d, boolean success) { } 272 273 @Override 274 public void fillInLogContainerData(View v, ItemInfo info, Target target, Target targetParent) { 275 // This is filled in {@link AllAppsRecyclerView} 276 } 277 278 @Override 279 public void setInsets(Rect insets) { 280 DeviceProfile grid = mLauncher.getDeviceProfile(); 281 int leftRightPadding = grid.desiredWorkspaceLeftRightMarginPx 282 + grid.cellLayoutPaddingLeftRightPx; 283 284 for (int i = 0; i < mAH.length; i++) { 285 mAH[i].padding.bottom = insets.bottom; 286 mAH[i].padding.left = mAH[i].padding.right = leftRightPadding; 287 mAH[i].applyPadding(); 288 } 289 290 ViewGroup.MarginLayoutParams mlp = (MarginLayoutParams) getLayoutParams(); 291 if (grid.isVerticalBarLayout()) { 292 mlp.leftMargin = insets.left; 293 mlp.rightMargin = insets.right; 294 setPadding(grid.workspacePadding.left, 0, grid.workspacePadding.right, 0); 295 } else { 296 mlp.leftMargin = mlp.rightMargin = 0; 297 setPadding(0, 0, 0, 0); 298 } 299 setLayoutParams(mlp); 300 301 mNavBarScrimHeight = insets.bottom; 302 InsettableFrameLayout.dispatchInsets(this, insets); 303 } 304 305 @Override 306 protected void dispatchDraw(Canvas canvas) { 307 super.dispatchDraw(canvas); 308 309 if (mNavBarScrimHeight > 0) { 310 canvas.drawRect(0, getHeight() - mNavBarScrimHeight, getWidth(), getHeight(), 311 mNavBarScrimPaint); 312 } 313 } 314 315 private void rebindAdapters(boolean showTabs) { 316 rebindAdapters(showTabs, false /* force */); 317 } 318 319 private void rebindAdapters(boolean showTabs, boolean force) { 320 if (showTabs == mUsingTabs && !force) { 321 return; 322 } 323 replaceRVContainer(showTabs); 324 mUsingTabs = showTabs; 325 326 mAllAppsStore.unregisterIconContainer(mAH[AdapterHolder.MAIN].recyclerView); 327 mAllAppsStore.unregisterIconContainer(mAH[AdapterHolder.WORK].recyclerView); 328 329 if (mUsingTabs) { 330 mAH[AdapterHolder.MAIN].setup(mViewPager.getChildAt(0), mPersonalMatcher); 331 mAH[AdapterHolder.WORK].setup(mViewPager.getChildAt(1), mWorkMatcher); 332 onTabChanged(mViewPager.getNextPage()); 333 } else { 334 mAH[AdapterHolder.MAIN].setup(findViewById(R.id.apps_list_view), null); 335 mAH[AdapterHolder.WORK].recyclerView = null; 336 } 337 setupHeader(); 338 339 mAllAppsStore.registerIconContainer(mAH[AdapterHolder.MAIN].recyclerView); 340 mAllAppsStore.registerIconContainer(mAH[AdapterHolder.WORK].recyclerView); 341 } 342 343 private void replaceRVContainer(boolean showTabs) { 344 for (int i = 0; i < mAH.length; i++) { 345 if (mAH[i].recyclerView != null) { 346 mAH[i].recyclerView.setLayoutManager(null); 347 } 348 } 349 View oldView = getRecyclerViewContainer(); 350 int index = indexOfChild(oldView); 351 removeView(oldView); 352 int layout = showTabs ? R.layout.all_apps_tabs : R.layout.all_apps_rv_layout; 353 View newView = LayoutInflater.from(getContext()).inflate(layout, this, false); 354 addView(newView, index); 355 if (showTabs) { 356 mViewPager = (AllAppsPagedView) newView; 357 mViewPager.initParentViews(this); 358 mViewPager.getPageIndicator().setContainerView(this); 359 } else { 360 mViewPager = null; 361 } 362 } 363 364 public View getRecyclerViewContainer() { 365 return mViewPager != null ? mViewPager : findViewById(R.id.apps_list_view); 366 } 367 368 public void onTabChanged(int pos) { 369 mHeader.setMainActive(pos == 0); 370 reset(true /* animate */); 371 if (mAH[pos].recyclerView != null) { 372 mAH[pos].recyclerView.bindFastScrollbar(); 373 374 findViewById(R.id.tab_personal) 375 .setOnClickListener((View view) -> mViewPager.snapToPage(AdapterHolder.MAIN)); 376 findViewById(R.id.tab_work) 377 .setOnClickListener((View view) -> mViewPager.snapToPage(AdapterHolder.WORK)); 378 379 } 380 if (pos == AdapterHolder.WORK) { 381 BottomUserEducationView.showIfNeeded(mLauncher); 382 } 383 } 384 385 public AlphabeticalAppsList getApps() { 386 return mAH[AdapterHolder.MAIN].appsList; 387 } 388 389 public FloatingHeaderView getFloatingHeaderView() { 390 return mHeader; 391 } 392 393 public View getSearchView() { 394 return mSearchContainer; 395 } 396 397 public View getContentView() { 398 return mViewPager == null ? getActiveRecyclerView() : mViewPager; 399 } 400 401 public RecyclerViewFastScroller getScrollBar() { 402 AllAppsRecyclerView rv = getActiveRecyclerView(); 403 return rv == null ? null : rv.getScrollbar(); 404 } 405 406 public void setupHeader() { 407 mHeader.setVisibility(View.VISIBLE); 408 mHeader.setup(mAH, mAH[AllAppsContainerView.AdapterHolder.WORK].recyclerView == null); 409 410 int padding = mHeader.getMaxTranslation(); 411 for (int i = 0; i < mAH.length; i++) { 412 mAH[i].padding.top = padding; 413 mAH[i].applyPadding(); 414 } 415 } 416 417 public void setLastSearchQuery(String query) { 418 for (int i = 0; i < mAH.length; i++) { 419 mAH[i].adapter.setLastSearchQuery(query); 420 } 421 if (mUsingTabs) { 422 mSearchModeWhileUsingTabs = true; 423 rebindAdapters(false); // hide tabs 424 } 425 } 426 427 public void onClearSearchResult() { 428 if (mSearchModeWhileUsingTabs) { 429 rebindAdapters(true); // show tabs 430 mSearchModeWhileUsingTabs = false; 431 } 432 } 433 434 public void onSearchResultsChanged() { 435 for (int i = 0; i < mAH.length; i++) { 436 if (mAH[i].recyclerView != null) { 437 mAH[i].recyclerView.onSearchResultsChanged(); 438 } 439 } 440 } 441 442 public void setRecyclerViewVerticalFadingEdgeEnabled(boolean enabled) { 443 for (int i = 0; i < mAH.length; i++) { 444 mAH[i].applyVerticalFadingEdgeEnabled(enabled); 445 } 446 } 447 448 public void addElevationController(RecyclerView.OnScrollListener scrollListener) { 449 if (!mUsingTabs) { 450 mAH[AdapterHolder.MAIN].recyclerView.addOnScrollListener(scrollListener); 451 } 452 } 453 454 public boolean isHeaderVisible() { 455 return mHeader != null && mHeader.getVisibility() == View.VISIBLE; 456 } 457 458 public void onScrollUpEnd() { 459 if (mUsingTabs) { 460 ((PersonalWorkSlidingTabStrip) findViewById(R.id.tabs)).highlightWorkTabIfNecessary(); 461 } 462 } 463 464 /** 465 * Adds an update listener to {@param animator} that adds springs to the animation. 466 */ 467 public void addSpringFromFlingUpdateListener(ValueAnimator animator, float velocity) { 468 animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { 469 boolean shouldSpring = true; 470 471 @Override 472 public void onAnimationUpdate(ValueAnimator valueAnimator) { 473 if (shouldSpring 474 && valueAnimator.getAnimatedFraction() >= FLING_ANIMATION_THRESHOLD) { 475 int searchViewId = getSearchView().getId(); 476 addSpringView(searchViewId); 477 478 finishWithShiftAndVelocity(1, velocity * FLING_VELOCITY_MULTIPLIER, 479 new DynamicAnimation.OnAnimationEndListener() { 480 @Override 481 public void onAnimationEnd(DynamicAnimation animation, 482 boolean canceled, float value, float velocity) { 483 removeSpringView(searchViewId); 484 } 485 }); 486 487 shouldSpring = false; 488 } 489 } 490 }); 491 } 492 493 public class AdapterHolder { 494 public static final int MAIN = 0; 495 public static final int WORK = 1; 496 497 public final AllAppsGridAdapter adapter; 498 final LinearLayoutManager layoutManager; 499 final AlphabeticalAppsList appsList; 500 final Rect padding = new Rect(); 501 AllAppsRecyclerView recyclerView; 502 boolean verticalFadingEdge; 503 504 AdapterHolder(boolean isWork) { 505 appsList = new AlphabeticalAppsList(mLauncher, mAllAppsStore, isWork); 506 adapter = new AllAppsGridAdapter(mLauncher, appsList); 507 appsList.setAdapter(adapter); 508 layoutManager = adapter.getLayoutManager(); 509 } 510 511 void setup(@NonNull View rv, @Nullable ItemInfoMatcher matcher) { 512 appsList.updateItemFilter(matcher); 513 recyclerView = (AllAppsRecyclerView) rv; 514 recyclerView.setEdgeEffectFactory(createEdgeEffectFactory()); 515 recyclerView.setApps(appsList, mUsingTabs); 516 recyclerView.setLayoutManager(layoutManager); 517 recyclerView.setAdapter(adapter); 518 recyclerView.setHasFixedSize(true); 519 // No animations will occur when changes occur to the items in this RecyclerView. 520 recyclerView.setItemAnimator(null); 521 FocusedItemDecorator focusedItemDecorator = new FocusedItemDecorator(recyclerView); 522 recyclerView.addItemDecoration(focusedItemDecorator); 523 adapter.setIconFocusListener(focusedItemDecorator.getFocusListener()); 524 applyVerticalFadingEdgeEnabled(verticalFadingEdge); 525 applyPadding(); 526 } 527 528 void applyPadding() { 529 if (recyclerView != null) { 530 recyclerView.setPadding(padding.left, padding.top, padding.right, padding.bottom); 531 } 532 } 533 534 public void applyVerticalFadingEdgeEnabled(boolean enabled) { 535 verticalFadingEdge = enabled; 536 mAH[AdapterHolder.MAIN].recyclerView.setVerticalFadingEdgeEnabled(!mUsingTabs 537 && verticalFadingEdge); 538 } 539 } 540 } 541