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.content.Context; 19 import android.content.Intent; 20 import android.content.res.Resources; 21 import android.support.animation.DynamicAnimation; 22 import android.support.animation.SpringAnimation; 23 import android.support.v4.view.accessibility.AccessibilityEventCompat; 24 import android.support.v4.view.accessibility.AccessibilityNodeInfoCompat; 25 import android.support.v4.view.accessibility.AccessibilityRecordCompat; 26 import android.support.v7.widget.GridLayoutManager; 27 import android.support.v7.widget.RecyclerView; 28 import android.view.Gravity; 29 import android.view.LayoutInflater; 30 import android.view.View; 31 import android.view.View.OnFocusChangeListener; 32 import android.view.ViewConfiguration; 33 import android.view.ViewGroup; 34 import android.view.accessibility.AccessibilityEvent; 35 import android.widget.TextView; 36 37 import com.android.launcher3.AppInfo; 38 import com.android.launcher3.BubbleTextView; 39 import com.android.launcher3.Launcher; 40 import com.android.launcher3.R; 41 import com.android.launcher3.Utilities; 42 import com.android.launcher3.allapps.AlphabeticalAppsList.AdapterItem; 43 import com.android.launcher3.anim.SpringAnimationHandler; 44 import com.android.launcher3.config.FeatureFlags; 45 import com.android.launcher3.discovery.AppDiscoveryAppInfo; 46 import com.android.launcher3.discovery.AppDiscoveryItemView; 47 import com.android.launcher3.util.PackageManagerHelper; 48 49 import java.util.List; 50 51 /** 52 * The grid view adapter of all the apps. 53 */ 54 public class AllAppsGridAdapter extends RecyclerView.Adapter<AllAppsGridAdapter.ViewHolder> { 55 56 public static final String TAG = "AppsGridAdapter"; 57 58 // A normal icon 59 public static final int VIEW_TYPE_ICON = 1 << 1; 60 // A prediction icon 61 public static final int VIEW_TYPE_PREDICTION_ICON = 1 << 2; 62 // The message shown when there are no filtered results 63 public static final int VIEW_TYPE_EMPTY_SEARCH = 1 << 3; 64 // The message to continue to a market search when there are no filtered results 65 public static final int VIEW_TYPE_SEARCH_MARKET = 1 << 4; 66 67 // We use various dividers for various purposes. They share enough attributes to reuse layouts, 68 // but differ in enough attributes to require different view types 69 70 // A divider that separates the apps list and the search market button 71 public static final int VIEW_TYPE_SEARCH_MARKET_DIVIDER = 1 << 5; 72 // The divider that separates prediction icons from the app list 73 public static final int VIEW_TYPE_PREDICTION_DIVIDER = 1 << 6; 74 public static final int VIEW_TYPE_APPS_LOADING_DIVIDER = 1 << 7; 75 public static final int VIEW_TYPE_DISCOVERY_ITEM = 1 << 8; 76 77 // Common view type masks 78 public static final int VIEW_TYPE_MASK_DIVIDER = VIEW_TYPE_SEARCH_MARKET_DIVIDER 79 | VIEW_TYPE_PREDICTION_DIVIDER; 80 public static final int VIEW_TYPE_MASK_ICON = VIEW_TYPE_ICON 81 | VIEW_TYPE_PREDICTION_ICON; 82 public static final int VIEW_TYPE_MASK_CONTENT = VIEW_TYPE_MASK_ICON 83 | VIEW_TYPE_DISCOVERY_ITEM; 84 public static final int VIEW_TYPE_MASK_HAS_SPRINGS = VIEW_TYPE_MASK_ICON 85 | VIEW_TYPE_PREDICTION_DIVIDER; 86 87 88 public interface BindViewCallback { onBindView(ViewHolder holder)89 void onBindView(ViewHolder holder); 90 } 91 92 /** 93 * ViewHolder for each icon. 94 */ 95 public static class ViewHolder extends RecyclerView.ViewHolder { 96 ViewHolder(View v)97 public ViewHolder(View v) { 98 super(v); 99 } 100 } 101 102 /** 103 * A subclass of GridLayoutManager that overrides accessibility values during app search. 104 */ 105 public class AppsGridLayoutManager extends GridLayoutManager { 106 AppsGridLayoutManager(Context context)107 public AppsGridLayoutManager(Context context) { 108 super(context, 1, GridLayoutManager.VERTICAL, false); 109 } 110 111 @Override onInitializeAccessibilityEvent(AccessibilityEvent event)112 public void onInitializeAccessibilityEvent(AccessibilityEvent event) { 113 super.onInitializeAccessibilityEvent(event); 114 115 // Ensure that we only report the number apps for accessibility not including other 116 // adapter views 117 final AccessibilityRecordCompat record = AccessibilityEventCompat 118 .asRecord(event); 119 record.setItemCount(mApps.getNumFilteredApps()); 120 record.setFromIndex(Math.max(0, 121 record.getFromIndex() - getRowsNotForAccessibility(record.getFromIndex()))); 122 record.setToIndex(Math.max(0, 123 record.getToIndex() - getRowsNotForAccessibility(record.getToIndex()))); 124 } 125 126 @Override getRowCountForAccessibility(RecyclerView.Recycler recycler, RecyclerView.State state)127 public int getRowCountForAccessibility(RecyclerView.Recycler recycler, 128 RecyclerView.State state) { 129 return super.getRowCountForAccessibility(recycler, state) - 130 getRowsNotForAccessibility(mApps.getAdapterItems().size() - 1); 131 } 132 133 @Override onInitializeAccessibilityNodeInfoForItem(RecyclerView.Recycler recycler, RecyclerView.State state, View host, AccessibilityNodeInfoCompat info)134 public void onInitializeAccessibilityNodeInfoForItem(RecyclerView.Recycler recycler, 135 RecyclerView.State state, View host, AccessibilityNodeInfoCompat info) { 136 super.onInitializeAccessibilityNodeInfoForItem(recycler, state, host, info); 137 138 ViewGroup.LayoutParams lp = host.getLayoutParams(); 139 AccessibilityNodeInfoCompat.CollectionItemInfoCompat cic = info.getCollectionItemInfo(); 140 if (!(lp instanceof LayoutParams) || (cic == null)) { 141 return; 142 } 143 LayoutParams glp = (LayoutParams) lp; 144 info.setCollectionItemInfo(AccessibilityNodeInfoCompat.CollectionItemInfoCompat.obtain( 145 cic.getRowIndex() - getRowsNotForAccessibility(glp.getViewAdapterPosition()), 146 cic.getRowSpan(), 147 cic.getColumnIndex(), 148 cic.getColumnSpan(), 149 cic.isHeading(), 150 cic.isSelected())); 151 } 152 153 /** 154 * Returns the number of rows before {@param adapterPosition}, including this position 155 * which should not be counted towards the collection info. 156 */ getRowsNotForAccessibility(int adapterPosition)157 private int getRowsNotForAccessibility(int adapterPosition) { 158 List<AdapterItem> items = mApps.getAdapterItems(); 159 adapterPosition = Math.max(adapterPosition, mApps.getAdapterItems().size() - 1); 160 int extraRows = 0; 161 for (int i = 0; i <= adapterPosition; i++) { 162 if (!isViewType(items.get(i).viewType, VIEW_TYPE_MASK_CONTENT)) { 163 extraRows++; 164 } 165 } 166 return extraRows; 167 } 168 } 169 170 /** 171 * Helper class to size the grid items. 172 */ 173 public class GridSpanSizer extends GridLayoutManager.SpanSizeLookup { 174 GridSpanSizer()175 public GridSpanSizer() { 176 super(); 177 setSpanIndexCacheEnabled(true); 178 } 179 180 @Override getSpanSize(int position)181 public int getSpanSize(int position) { 182 if (isIconViewType(mApps.getAdapterItems().get(position).viewType)) { 183 return 1; 184 } else { 185 // Section breaks span the full width 186 return mAppsPerRow; 187 } 188 } 189 } 190 191 private final Launcher mLauncher; 192 private final LayoutInflater mLayoutInflater; 193 private final AlphabeticalAppsList mApps; 194 private final GridLayoutManager mGridLayoutMgr; 195 private final GridSpanSizer mGridSizer; 196 private final View.OnClickListener mIconClickListener; 197 private final View.OnLongClickListener mIconLongClickListener; 198 199 private int mAppsPerRow; 200 201 private BindViewCallback mBindViewCallback; 202 private OnFocusChangeListener mIconFocusListener; 203 204 // The text to show when there are no search results and no market search handler. 205 private String mEmptySearchMessage; 206 // The intent to send off to the market app, updated each time the search query changes. 207 private Intent mMarketSearchIntent; 208 209 private SpringAnimationHandler<ViewHolder> mSpringAnimationHandler; 210 AllAppsGridAdapter(Launcher launcher, AlphabeticalAppsList apps, View.OnClickListener iconClickListener, View.OnLongClickListener iconLongClickListener)211 public AllAppsGridAdapter(Launcher launcher, AlphabeticalAppsList apps, View.OnClickListener 212 iconClickListener, View.OnLongClickListener iconLongClickListener) { 213 Resources res = launcher.getResources(); 214 mLauncher = launcher; 215 mApps = apps; 216 mEmptySearchMessage = res.getString(R.string.all_apps_loading_message); 217 mGridSizer = new GridSpanSizer(); 218 mGridLayoutMgr = new AppsGridLayoutManager(launcher); 219 mGridLayoutMgr.setSpanSizeLookup(mGridSizer); 220 mLayoutInflater = LayoutInflater.from(launcher); 221 mIconClickListener = iconClickListener; 222 mIconLongClickListener = iconLongClickListener; 223 if (FeatureFlags.LAUNCHER3_PHYSICS) { 224 mSpringAnimationHandler = new SpringAnimationHandler<>( 225 SpringAnimationHandler.Y_DIRECTION, new AllAppsSpringAnimationFactory()); 226 } 227 } 228 getSpringAnimationHandler()229 public SpringAnimationHandler getSpringAnimationHandler() { 230 return mSpringAnimationHandler; 231 } 232 isDividerViewType(int viewType)233 public static boolean isDividerViewType(int viewType) { 234 return isViewType(viewType, VIEW_TYPE_MASK_DIVIDER); 235 } 236 isIconViewType(int viewType)237 public static boolean isIconViewType(int viewType) { 238 return isViewType(viewType, VIEW_TYPE_MASK_ICON); 239 } 240 isViewType(int viewType, int viewTypeMask)241 public static boolean isViewType(int viewType, int viewTypeMask) { 242 return (viewType & viewTypeMask) != 0; 243 } 244 245 /** 246 * Sets the number of apps per row. 247 */ setNumAppsPerRow(int appsPerRow)248 public void setNumAppsPerRow(int appsPerRow) { 249 mAppsPerRow = appsPerRow; 250 mGridLayoutMgr.setSpanCount(appsPerRow); 251 } 252 getNumAppsPerRow()253 public int getNumAppsPerRow() { 254 return mAppsPerRow; 255 } 256 setIconFocusListener(OnFocusChangeListener focusListener)257 public void setIconFocusListener(OnFocusChangeListener focusListener) { 258 mIconFocusListener = focusListener; 259 } 260 261 /** 262 * Sets the last search query that was made, used to show when there are no results and to also 263 * seed the intent for searching the market. 264 */ setLastSearchQuery(String query)265 public void setLastSearchQuery(String query) { 266 Resources res = mLauncher.getResources(); 267 mEmptySearchMessage = res.getString(R.string.all_apps_no_search_results, query); 268 mMarketSearchIntent = PackageManagerHelper.getMarketSearchIntent(mLauncher, query); 269 } 270 271 /** 272 * Sets the callback for when views are bound. 273 */ setBindViewCallback(BindViewCallback cb)274 public void setBindViewCallback(BindViewCallback cb) { 275 mBindViewCallback = cb; 276 } 277 278 /** 279 * Returns the grid layout manager. 280 */ getLayoutManager()281 public GridLayoutManager getLayoutManager() { 282 return mGridLayoutMgr; 283 } 284 285 @Override onCreateViewHolder(ViewGroup parent, int viewType)286 public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { 287 switch (viewType) { 288 case VIEW_TYPE_ICON: 289 case VIEW_TYPE_PREDICTION_ICON: 290 BubbleTextView icon = (BubbleTextView) mLayoutInflater.inflate( 291 R.layout.all_apps_icon, parent, false); 292 icon.setOnClickListener(mIconClickListener); 293 icon.setOnLongClickListener(mIconLongClickListener); 294 icon.setLongPressTimeout(ViewConfiguration.getLongPressTimeout()); 295 icon.setOnFocusChangeListener(mIconFocusListener); 296 297 // Ensure the all apps icon height matches the workspace icons in portrait mode. 298 icon.getLayoutParams().height = mLauncher.getDeviceProfile().allAppsCellHeightPx; 299 return new ViewHolder(icon); 300 case VIEW_TYPE_DISCOVERY_ITEM: 301 AppDiscoveryItemView appDiscoveryItemView = (AppDiscoveryItemView) mLayoutInflater 302 .inflate(R.layout.all_apps_discovery_item, parent, false); 303 appDiscoveryItemView.init(mIconClickListener, mLauncher.getAccessibilityDelegate(), 304 mIconLongClickListener); 305 return new ViewHolder(appDiscoveryItemView); 306 case VIEW_TYPE_EMPTY_SEARCH: 307 return new ViewHolder(mLayoutInflater.inflate(R.layout.all_apps_empty_search, 308 parent, false)); 309 case VIEW_TYPE_SEARCH_MARKET: 310 View searchMarketView = mLayoutInflater.inflate(R.layout.all_apps_search_market, 311 parent, false); 312 searchMarketView.setOnClickListener(new View.OnClickListener() { 313 @Override 314 public void onClick(View v) { 315 mLauncher.startActivitySafely(v, mMarketSearchIntent, null); 316 } 317 }); 318 return new ViewHolder(searchMarketView); 319 case VIEW_TYPE_APPS_LOADING_DIVIDER: 320 View loadingDividerView = mLayoutInflater.inflate( 321 R.layout.all_apps_discovery_loading_divider, parent, false); 322 return new ViewHolder(loadingDividerView); 323 case VIEW_TYPE_PREDICTION_DIVIDER: 324 case VIEW_TYPE_SEARCH_MARKET_DIVIDER: 325 return new ViewHolder(mLayoutInflater.inflate( 326 R.layout.all_apps_divider, parent, false)); 327 default: 328 throw new RuntimeException("Unexpected view type"); 329 } 330 } 331 332 @Override onBindViewHolder(ViewHolder holder, int position)333 public void onBindViewHolder(ViewHolder holder, int position) { 334 switch (holder.getItemViewType()) { 335 case VIEW_TYPE_ICON: 336 case VIEW_TYPE_PREDICTION_ICON: 337 AppInfo info = mApps.getAdapterItems().get(position).appInfo; 338 BubbleTextView icon = (BubbleTextView) holder.itemView; 339 icon.applyFromApplicationInfo(info); 340 break; 341 case VIEW_TYPE_DISCOVERY_ITEM: 342 AppDiscoveryAppInfo appDiscoveryAppInfo = (AppDiscoveryAppInfo) 343 mApps.getAdapterItems().get(position).appInfo; 344 AppDiscoveryItemView view = (AppDiscoveryItemView) holder.itemView; 345 view.apply(appDiscoveryAppInfo); 346 break; 347 case VIEW_TYPE_EMPTY_SEARCH: 348 TextView emptyViewText = (TextView) holder.itemView; 349 emptyViewText.setText(mEmptySearchMessage); 350 emptyViewText.setGravity(mApps.hasNoFilteredResults() ? Gravity.CENTER : 351 Gravity.START | Gravity.CENTER_VERTICAL); 352 break; 353 case VIEW_TYPE_SEARCH_MARKET: 354 TextView searchView = (TextView) holder.itemView; 355 if (mMarketSearchIntent != null) { 356 searchView.setVisibility(View.VISIBLE); 357 } else { 358 searchView.setVisibility(View.GONE); 359 } 360 break; 361 case VIEW_TYPE_APPS_LOADING_DIVIDER: 362 int visLoading = mApps.isAppDiscoveryRunning() ? View.VISIBLE : View.GONE; 363 int visLoaded = !mApps.isAppDiscoveryRunning() ? View.VISIBLE : View.GONE; 364 holder.itemView.findViewById(R.id.loadingProgressBar).setVisibility(visLoading); 365 holder.itemView.findViewById(R.id.loadedDivider).setVisibility(visLoaded); 366 break; 367 case VIEW_TYPE_SEARCH_MARKET_DIVIDER: 368 // nothing to do 369 break; 370 } 371 if (mBindViewCallback != null) { 372 mBindViewCallback.onBindView(holder); 373 } 374 } 375 376 @Override onViewAttachedToWindow(ViewHolder holder)377 public void onViewAttachedToWindow(ViewHolder holder) { 378 int type = holder.getItemViewType(); 379 if (FeatureFlags.LAUNCHER3_PHYSICS && isViewType(type, VIEW_TYPE_MASK_HAS_SPRINGS)) { 380 mSpringAnimationHandler.add(holder.itemView, holder); 381 } 382 } 383 384 @Override onViewDetachedFromWindow(ViewHolder holder)385 public void onViewDetachedFromWindow(ViewHolder holder) { 386 int type = holder.getItemViewType(); 387 if (FeatureFlags.LAUNCHER3_PHYSICS && isViewType(type, VIEW_TYPE_MASK_HAS_SPRINGS)) { 388 mSpringAnimationHandler.remove(holder.itemView); 389 } 390 } 391 392 @Override onFailedToRecycleView(ViewHolder holder)393 public boolean onFailedToRecycleView(ViewHolder holder) { 394 // Always recycle and we will reset the view when it is bound 395 return true; 396 } 397 398 @Override getItemCount()399 public int getItemCount() { 400 return mApps.getAdapterItems().size(); 401 } 402 403 @Override getItemViewType(int position)404 public int getItemViewType(int position) { 405 AlphabeticalAppsList.AdapterItem item = mApps.getAdapterItems().get(position); 406 return item.viewType; 407 } 408 409 /** 410 * Helper class to set the SpringAnimation values for an item in the adapter. 411 */ 412 private class AllAppsSpringAnimationFactory 413 implements SpringAnimationHandler.AnimationFactory<ViewHolder> { 414 private static final float DEFAULT_MAX_VALUE_PX = 100; 415 private static final float DEFAULT_MIN_VALUE_PX = -DEFAULT_MAX_VALUE_PX; 416 417 // Damping ratio range is [0, 1] 418 private static final float SPRING_DAMPING_RATIO = 0.55f; 419 420 // Stiffness is a non-negative number. 421 private static final float MIN_SPRING_STIFFNESS = 580f; 422 private static final float MAX_SPRING_STIFFNESS = 900f; 423 424 // The amount by which each adjacent rows' stiffness will differ. 425 private static final float ROW_STIFFNESS_COEFFICIENT = 50f; 426 427 @Override initialize(ViewHolder vh)428 public SpringAnimation initialize(ViewHolder vh) { 429 return SpringAnimationHandler.forView(vh.itemView, DynamicAnimation.TRANSLATION_Y, 0); 430 } 431 432 /** 433 * @param spring A new or recycled SpringAnimation. 434 * @param vh The ViewHolder that {@param spring} is related to. 435 */ 436 @Override update(SpringAnimation spring, ViewHolder vh)437 public void update(SpringAnimation spring, ViewHolder vh) { 438 int numPredictedApps = Math.min(mAppsPerRow, mApps.getPredictedApps().size()); 439 int appPosition = getAppPosition(vh.getAdapterPosition(), numPredictedApps, 440 mAppsPerRow); 441 442 int col = appPosition % mAppsPerRow; 443 int row = appPosition / mAppsPerRow; 444 445 int numTotalRows = mApps.getNumAppRows() - 1; // zero-based count 446 if (row > (numTotalRows / 2)) { 447 // Mirror the rows so that the top row acts the same as the bottom row. 448 row = Math.abs(numTotalRows - row); 449 } 450 451 calculateSpringValues(spring, row, col); 452 } 453 454 @Override setDefaultValues(SpringAnimation spring)455 public void setDefaultValues(SpringAnimation spring) { 456 calculateSpringValues(spring, 0, mAppsPerRow / 2); 457 } 458 459 /** 460 * We manipulate the stiffness, min, and max values based on the items distance to the 461 * first row and the items distance to the center column to create the ^-shaped motion 462 * effect. 463 */ calculateSpringValues(SpringAnimation spring, int row, int col)464 private void calculateSpringValues(SpringAnimation spring, int row, int col) { 465 float rowFactor = (1 + row) * 0.5f; 466 float colFactor = getColumnFactor(col, mAppsPerRow); 467 468 float minValue = DEFAULT_MIN_VALUE_PX * (rowFactor + colFactor); 469 float maxValue = DEFAULT_MAX_VALUE_PX * (rowFactor + colFactor); 470 471 float stiffness = Utilities.boundToRange( 472 MAX_SPRING_STIFFNESS - (row * ROW_STIFFNESS_COEFFICIENT), 473 MIN_SPRING_STIFFNESS, 474 MAX_SPRING_STIFFNESS); 475 476 spring.setMinValue(minValue) 477 .setMaxValue(maxValue) 478 .getSpring() 479 .setStiffness(stiffness) 480 .setDampingRatio(SPRING_DAMPING_RATIO); 481 } 482 483 /** 484 * @return The app position is the position of the app in the Adapter if we ignored all 485 * other view types. 486 * 487 * The first app is at position 0, and the first app each following row is at a 488 * position that is a multiple of {@param appsPerRow}. 489 * 490 * ie. If there are 5 apps per row, and there are two rows of apps: 491 * 0 1 2 3 4 492 * 5 6 7 8 9 493 */ getAppPosition(int position, int numPredictedApps, int appsPerRow)494 private int getAppPosition(int position, int numPredictedApps, int appsPerRow) { 495 if (position < numPredictedApps) { 496 // Predicted apps are first in the adapter. 497 return position; 498 } 499 500 // There is at most 1 divider view between the predicted apps and the alphabetical apps. 501 int numDividerViews = numPredictedApps == 0 ? 0 : 1; 502 503 // This offset takes into consideration an incomplete row of predicted apps. 504 int numPredictedAppsOffset = appsPerRow - numPredictedApps; 505 return position + numPredictedAppsOffset - numDividerViews; 506 } 507 508 /** 509 * Increase the column factor as the distance increases between the column and the center 510 * column(s). 511 */ getColumnFactor(int col, int numCols)512 private float getColumnFactor(int col, int numCols) { 513 float centerColumn = numCols / 2; 514 int distanceToCenter = (int) Math.abs(col - centerColumn); 515 516 boolean evenNumberOfColumns = numCols % 2 == 0; 517 if (evenNumberOfColumns && col < centerColumn) { 518 distanceToCenter -= 1; 519 } 520 521 float factor = 0; 522 while (distanceToCenter > 0) { 523 if (distanceToCenter == 1) { 524 factor += 0.2f; 525 } else { 526 factor += 0.1f; 527 } 528 --distanceToCenter; 529 } 530 531 return factor; 532 } 533 } 534 } 535