diff options
Diffstat (limited to 'src/com/android')
39 files changed, 1531 insertions, 664 deletions
diff --git a/src/com/android/launcher3/AlphabeticalAppsList.java b/src/com/android/launcher3/AlphabeticalAppsList.java index 477c00fe8..dc75637e5 100644 --- a/src/com/android/launcher3/AlphabeticalAppsList.java +++ b/src/com/android/launcher3/AlphabeticalAppsList.java @@ -3,6 +3,7 @@ package com.android.launcher3; import android.content.ComponentName; import android.content.Context; import android.support.v7.widget.RecyclerView; +import android.util.Log; import com.android.launcher3.compat.AlphabeticIndexCompat; import com.android.launcher3.compat.UserHandleCompat; import com.android.launcher3.compat.UserManagerCompat; @@ -13,24 +14,28 @@ import java.util.Collections; import java.util.Comparator; import java.util.HashMap; import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.TreeMap; /** * A private class to manage access to an app name comparator. */ class AppNameComparator { - private UserManagerCompat mUserManager; - private Comparator<AppInfo> mAppNameComparator; + private final UserManagerCompat mUserManager; + private final Collator mCollator; + private final Comparator<AppInfo> mAppInfoComparator; + private final Comparator<String> mSectionNameComparator; private HashMap<UserHandleCompat, Long> mUserSerialCache = new HashMap<>(); public AppNameComparator(Context context) { - final Collator collator = Collator.getInstance(); + mCollator = Collator.getInstance(); mUserManager = UserManagerCompat.getInstance(context); - mAppNameComparator = new Comparator<AppInfo>() { + mAppInfoComparator = new Comparator<AppInfo>() { public final int compare(AppInfo a, AppInfo b) { - // Order by the title - int result = collator.compare(a.title.toString().trim(), - b.title.toString().trim()); + // Order by the title in the current locale + int result = compareTitles(a.title.toString(), b.title.toString()); if (result == 0) { // If two apps have the same title, then order by the component name result = a.componentName.compareTo(b.componentName); @@ -49,15 +54,45 @@ class AppNameComparator { return result; } }; + mSectionNameComparator = new Comparator<String>() { + @Override + public int compare(String o1, String o2) { + return compareTitles(o1, o2); + } + }; } /** * Returns a locale-aware comparator that will alphabetically order a list of applications. */ - public Comparator<AppInfo> getComparator() { + public Comparator<AppInfo> getAppInfoComparator() { // Clear the user serial cache so that we get serials as needed in the comparator mUserSerialCache.clear(); - return mAppNameComparator; + return mAppInfoComparator; + } + + /** + * Returns a locale-aware comparator that will alphabetically order a list of section names. + */ + public Comparator<String> getSectionNameComparator() { + return mSectionNameComparator; + } + + /** + * Compares two titles with the same return value semantics as Comparator. + */ + private int compareTitles(String titleA, String titleB) { + // Ensure that we de-prioritize any titles that don't start with a linguistic letter or digit + boolean aStartsWithLetter = Character.isLetterOrDigit(titleA.codePointAt(0)); + boolean bStartsWithLetter = Character.isLetterOrDigit(titleB.codePointAt(0)); + if (aStartsWithLetter && !bStartsWithLetter) { + return -1; + } else if (!aStartsWithLetter && bStartsWithLetter) { + return 1; + } + + // Order by the title in the current locale + return mCollator.compare(titleA, titleB); } /** @@ -78,21 +113,37 @@ class AppNameComparator { */ public class AlphabeticalAppsList { + public static final String TAG = "AlphabeticalAppsList"; + private static final boolean DEBUG = false; + /** * Info about a section in the alphabetic list */ public static class SectionInfo { - // The name of this section - public String sectionName; // The number of applications in this section - public int numAppsInSection; - // The section AdapterItem for this section - public AdapterItem sectionItem; + public int numApps; + // The section break AdapterItem for this section + public AdapterItem sectionBreakItem; // The first app AdapterItem for this section public AdapterItem firstAppItem; + } - public SectionInfo(String name) { - sectionName = name; + /** + * Info about a fast scroller section, depending if sections are merged, the fast scroller + * sections will not be the same set as the section headers. + */ + public static class FastScrollSectionInfo { + // The section name + public String sectionName; + // To map the touch (from 0..1) to the index in the app list to jump to in the fast + // scroller, we use the fraction in range (0..1) of the app index / total app count. + public float appRangeFraction; + // The AdapterItem to scroll to for this section + public AdapterItem appItem; + + public FastScrollSectionInfo(String sectionName, float appRangeFraction) { + this.sectionName = sectionName; + this.appRangeFraction = appRangeFraction; } } @@ -100,34 +151,48 @@ public class AlphabeticalAppsList { * Info about a particular adapter item (can be either section or app) */ public static class AdapterItem { + /** Section & App properties */ // The index of this adapter item in the list public int position; // Whether or not the item at this adapter position is a section or not public boolean isSectionHeader; - // The name of this section, or the section that this app is contained in - public String sectionName; - // The associated AppInfo, or null if this adapter item is a section - public AppInfo appInfo; - // The index of this app (not including sections), or -1 if this adapter item is a section - public int appIndex; - - public static AdapterItem asSection(int pos, String name) { + // The section for this item + public SectionInfo sectionInfo; + + /** App-only properties */ + // The section name of this app. Note that there can be multiple items with different + // sectionNames in the same section + public String sectionName = null; + // The index of this app in the section + public int sectionAppIndex = -1; + // The associated AppInfo for the app + public AppInfo appInfo = null; + // The index of this app not including sections + public int appIndex = -1; + // Whether or not this is a predicted app + public boolean isPredictedApp; + + public static AdapterItem asSectionBreak(int pos, SectionInfo section) { AdapterItem item = new AdapterItem(); item.position = pos; item.isSectionHeader = true; - item.sectionName = name; - item.appInfo = null; - item.appIndex = -1; + item.sectionInfo = section; + section.sectionBreakItem = item; return item; } - public static AdapterItem asApp(int pos, String sectionName, AppInfo appInfo, int appIndex) { + public static AdapterItem asApp(int pos, SectionInfo section, String sectionName, + int sectionAppIndex, AppInfo appInfo, int appIndex, + boolean isPredictedApp) { AdapterItem item = new AdapterItem(); item.position = pos; item.isSectionHeader = false; + item.sectionInfo = section; item.sectionName = sectionName; + item.sectionAppIndex = sectionAppIndex; item.appInfo = appInfo; item.appIndex = appIndex; + item.isPredictedApp = isPredictedApp; return item; } } @@ -136,25 +201,76 @@ public class AlphabeticalAppsList { * A filter interface to limit the set of applications in the apps list. */ public interface Filter { - public boolean retainApp(AppInfo info, String sectionName); + boolean retainApp(AppInfo info, String sectionName); + } + + /** + * Common interface for different merging strategies. + */ + private interface MergeAlgorithm { + boolean continueMerging(int sectionAppCount, int numAppsPerRow, int mergeCount); + } + + /** + * The logic we use to merge sections on tablets. + */ + private static class TabletMergeAlgorithm implements MergeAlgorithm { + + @Override + public boolean continueMerging(int sectionAppCount, int numAppsPerRow, int mergeCount) { + // Merge EVERYTHING + return true; + } + } + + /** + * The logic we use to merge sections on phones. + */ + private static class PhoneMergeAlgorithm implements MergeAlgorithm { + + private int mMinAppsPerRow; + private int mMinRowsInMergedSection; + private int mMaxAllowableMerges; + + public PhoneMergeAlgorithm(int minAppsPerRow, int minRowsInMergedSection, int maxNumMerges) { + mMinAppsPerRow = minAppsPerRow; + mMinRowsInMergedSection = minRowsInMergedSection; + mMaxAllowableMerges = maxNumMerges; + } + + @Override + public boolean continueMerging(int sectionAppCount, int numAppsPerRow, int mergeCount) { + // Continue merging if the number of hanging apps on the final row is less than some + // fixed number (ragged), the merged rows has yet to exceed some minimum row count, + // and while the number of merged sections is less than some fixed number of merges + int rows = sectionAppCount / numAppsPerRow; + int cols = sectionAppCount % numAppsPerRow; + return (0 < cols && cols < mMinAppsPerRow) && + rows < mMinRowsInMergedSection && + mergeCount < mMaxAllowableMerges; + } } - // The maximum number of rows allowed in a merged section before we stop merging - private static final int MAX_ROWS_IN_MERGED_SECTION = Integer.MAX_VALUE; + private static final int MIN_ROWS_IN_MERGED_SECTION_PHONE = 3; + private static final int MAX_NUM_MERGES_PHONE = 2; + private Context mContext; private List<AppInfo> mApps = new ArrayList<>(); private List<AppInfo> mFilteredApps = new ArrayList<>(); private List<AdapterItem> mSectionedFilteredApps = new ArrayList<>(); private List<SectionInfo> mSections = new ArrayList<>(); + private List<FastScrollSectionInfo> mFastScrollerSections = new ArrayList<>(); + private List<ComponentName> mPredictedApps = new ArrayList<>(); + private HashMap<CharSequence, String> mCachedSectionNames = new HashMap<>(); private RecyclerView.Adapter mAdapter; private Filter mFilter; private AlphabeticIndexCompat mIndexer; private AppNameComparator mAppNameComparator; + private MergeAlgorithm mMergeAlgorithm; private int mNumAppsPerRow; - // The maximum number of section merges we allow at a given time before we stop merging - private int mMaxAllowableMerges = Integer.MAX_VALUE; public AlphabeticalAppsList(Context context, int numAppsPerRow) { + mContext = context; mIndexer = new AlphabeticIndexCompat(context); mAppNameComparator = new AppNameComparator(context); setNumAppsPerRow(numAppsPerRow); @@ -165,7 +281,16 @@ public class AlphabeticalAppsList { */ public void setNumAppsPerRow(int numAppsPerRow) { mNumAppsPerRow = numAppsPerRow; - mMaxAllowableMerges = (int) Math.ceil(numAppsPerRow / 2f); + + // Update the merge algorithm + DeviceProfile grid = LauncherAppState.getInstance().getDynamicGrid().getDeviceProfile(); + if (grid.isPhone()) { + mMergeAlgorithm = new PhoneMergeAlgorithm((int) Math.ceil(numAppsPerRow / 2f), + MIN_ROWS_IN_MERGED_SECTION_PHONE, MAX_NUM_MERGES_PHONE); + } else { + mMergeAlgorithm = new TabletMergeAlgorithm(); + } + onAppsUpdated(); } @@ -184,6 +309,13 @@ public class AlphabeticalAppsList { } /** + * Returns fast scroller sections of all the current filtered applications. + */ + public List<FastScrollSectionInfo> getFastScrollerSections() { + return mFastScrollerSections; + } + + /** * Returns the current filtered list of applications broken down into their sections. */ public List<AdapterItem> getAdapterItems() { @@ -223,10 +355,20 @@ public class AlphabeticalAppsList { } /** + * Sets the current set of predicted apps. Since this can be called before we get the full set + * of applications, we should merge the results only in onAppsUpdated() which is idempotent. + */ + public void setPredictedApps(List<ComponentName> apps) { + mPredictedApps.clear(); + mPredictedApps.addAll(apps); + onAppsUpdated(); + mAdapter.notifyDataSetChanged(); + } + + /** * Sets the current set of apps. */ public void setApps(List<AppInfo> apps) { - Collections.sort(apps, mAppNameComparator.getComparator()); mApps.clear(); mApps.addAll(apps); onAppsUpdated(); @@ -241,6 +383,8 @@ public class AlphabeticalAppsList { for (AppInfo info : apps) { addApp(info); } + onAppsUpdated(); + mAdapter.notifyDataSetChanged(); } /** @@ -251,12 +395,12 @@ public class AlphabeticalAppsList { int index = mApps.indexOf(info); if (index != -1) { mApps.set(index, info); - onAppsUpdated(); - mAdapter.notifyItemChanged(index); } else { addApp(info); } } + onAppsUpdated(); + mAdapter.notifyDataSetChanged(); } /** @@ -267,10 +411,10 @@ public class AlphabeticalAppsList { int removeIndex = findAppByComponent(mApps, info); if (removeIndex != -1) { mApps.remove(removeIndex); - onAppsUpdated(); - mAdapter.notifyDataSetChanged(); } } + onAppsUpdated(); + mAdapter.notifyDataSetChanged(); } /** @@ -290,14 +434,12 @@ public class AlphabeticalAppsList { } /** - * Implementation to actually add an app to the alphabetic list + * Implementation to actually add an app to the alphabetic list, but does not notify. */ private void addApp(AppInfo info) { - int index = Collections.binarySearch(mApps, info, mAppNameComparator.getComparator()); + int index = Collections.binarySearch(mApps, info, mAppNameComparator.getAppInfoComparator()); if (index < 0) { mApps.add(-(index + 1), info); - onAppsUpdated(); - mAdapter.notifyDataSetChanged(); } } @@ -305,89 +447,166 @@ public class AlphabeticalAppsList { * Updates internals when the set of apps are updated. */ private void onAppsUpdated() { - // Recreate the filtered and sectioned apps (for convenience for the grid layout) + // Sort the list of apps + Collections.sort(mApps, mAppNameComparator.getAppInfoComparator()); + + // As a special case for some languages (currently only Simplified Chinese), we may need to + // coalesce sections + Locale curLocale = mContext.getResources().getConfiguration().locale; + TreeMap<String, ArrayList<AppInfo>> sectionMap = null; + boolean localeRequiresSectionSorting = curLocale.equals(Locale.SIMPLIFIED_CHINESE); + if (localeRequiresSectionSorting) { + // Compute the section headers. We use a TreeMap with the section name comparator to + // ensure that the sections are ordered when we iterate over it later + sectionMap = new TreeMap<>(mAppNameComparator.getSectionNameComparator()); + for (AppInfo info : mApps) { + // Add the section to the cache + String sectionName = mCachedSectionNames.get(info.title); + if (sectionName == null) { + sectionName = mIndexer.computeSectionName(info.title); + mCachedSectionNames.put(info.title, sectionName); + } + + // Add it to the mapping + ArrayList<AppInfo> sectionApps = sectionMap.get(sectionName); + if (sectionApps == null) { + sectionApps = new ArrayList<>(); + sectionMap.put(sectionName, sectionApps); + } + sectionApps.add(info); + } + } else { + // Just compute the section headers for use below + for (AppInfo info : mApps) { + // Add the section to the cache + String sectionName = mCachedSectionNames.get(info.title); + if (sectionName == null) { + sectionName = mIndexer.computeSectionName(info.title); + mCachedSectionNames.put(info.title, sectionName); + } + } + } + + // Prepare to update the list of sections, filtered apps, etc. mFilteredApps.clear(); mSections.clear(); mSectionedFilteredApps.clear(); + mFastScrollerSections.clear(); SectionInfo lastSectionInfo = null; + String lastSectionName = null; + FastScrollSectionInfo lastFastScrollerSectionInfo = null; int position = 0; int appIndex = 0; - for (AppInfo info : mApps) { - String sectionName = mIndexer.computeSectionName(info.title.toString().trim()); + List<AppInfo> allApps = new ArrayList<>(); + + // Add the predicted apps to the combined list + int numPredictedApps = 0; + if (mPredictedApps != null && !mPredictedApps.isEmpty() && !hasFilter()) { + for (ComponentName cn : mPredictedApps) { + for (AppInfo info : mApps) { + if (cn.equals(info.componentName)) { + allApps.add(info); + numPredictedApps++; + break; + } + } + // Stop at the number of predicted apps + if (numPredictedApps == mNumAppsPerRow) { + break; + } + } + } + + // Add all the other apps to the combined list + if (localeRequiresSectionSorting) { + for (Map.Entry<String, ArrayList<AppInfo>> entry : sectionMap.entrySet()) { + allApps.addAll(entry.getValue()); + } + } else { + allApps.addAll(mApps); + } + + // Recreate the filtered and sectioned apps (for convenience for the grid layout) from the + // ordered set of sections + int numApps = allApps.size(); + for (int i = 0; i < numApps; i++) { + boolean isPredictedApp = i < numPredictedApps; + AppInfo info = allApps.get(i); + // The section name was computed above so this should be find + String sectionName = isPredictedApp ? "" : mCachedSectionNames.get(info.title); // Check if we want to retain this app if (mFilter != null && !mFilter.retainApp(info, sectionName)) { continue; } - // Create a new section if necessary - if (lastSectionInfo == null || !lastSectionInfo.sectionName.equals(sectionName)) { - lastSectionInfo = new SectionInfo(sectionName); + // Create a new section if the section names do not match + if (lastSectionInfo == null || + (!isPredictedApp && !sectionName.equals(lastSectionName))) { + lastSectionName = sectionName; + lastSectionInfo = new SectionInfo(); + lastFastScrollerSectionInfo = new FastScrollSectionInfo(sectionName, + (float) appIndex / numApps); mSections.add(lastSectionInfo); + mFastScrollerSections.add(lastFastScrollerSectionInfo); - // Create a new section item, this item is used to break the flow of items in the - // list - AdapterItem sectionItem = AdapterItem.asSection(position++, sectionName); - if (!AppsContainerView.GRID_HIDE_SECTION_HEADERS && !hasFilter()) { - lastSectionInfo.sectionItem = sectionItem; + // Create a new section item to break the flow of items in the list + if (!hasFilter()) { + AdapterItem sectionItem = AdapterItem.asSectionBreak(position++, lastSectionInfo); mSectionedFilteredApps.add(sectionItem); } } // Create an app item - AdapterItem appItem = AdapterItem.asApp(position++, sectionName, info, appIndex++); - lastSectionInfo.numAppsInSection++; + AdapterItem appItem = AdapterItem.asApp(position++, lastSectionInfo, sectionName, + lastSectionInfo.numApps++, info, appIndex++, isPredictedApp); if (lastSectionInfo.firstAppItem == null) { lastSectionInfo.firstAppItem = appItem; + lastFastScrollerSectionInfo.appItem = appItem; } mSectionedFilteredApps.add(appItem); mFilteredApps.add(info); } + // Go through each section and try and merge some of the sections if (AppsContainerView.GRID_MERGE_SECTIONS && !hasFilter()) { - // Go through each section and try and merge some of the sections - int minNumAppsPerRow = (int) Math.ceil(mNumAppsPerRow / 2f); int sectionAppCount = 0; for (int i = 0; i < mSections.size(); i++) { SectionInfo section = mSections.get(i); - String mergedSectionName = section.sectionName; - sectionAppCount = section.numAppsInSection; + sectionAppCount = section.numApps; int mergeCount = 1; - // Merge rows if the last app in this section is in a column that is greater than - // 0, but less than the min number of apps per row. In addition, apply the - // constraint to stop merging if the number of rows in the section is greater than - // some limit, and also if there are no lessons to merge. - while (0 < (sectionAppCount % mNumAppsPerRow) && - (sectionAppCount % mNumAppsPerRow) < minNumAppsPerRow && - (int) Math.ceil(sectionAppCount / mNumAppsPerRow) < MAX_ROWS_IN_MERGED_SECTION && + + // Merge rows based on the current strategy + while (mMergeAlgorithm.continueMerging(sectionAppCount, mNumAppsPerRow, mergeCount) && (i + 1) < mSections.size()) { SectionInfo nextSection = mSections.remove(i + 1); - // Merge the section names - if (AppsContainerView.GRID_MERGE_SECTION_HEADERS) { - mergedSectionName += nextSection.sectionName; - } + // Remove the next section break - mSectionedFilteredApps.remove(nextSection.sectionItem); - if (AppsContainerView.GRID_MERGE_SECTION_HEADERS) { - // Update the section names for the two sections - int pos = mSectionedFilteredApps.indexOf(section.firstAppItem); - for (int j = pos; j < (pos + section.numAppsInSection + nextSection.numAppsInSection); j++) { - AdapterItem item = mSectionedFilteredApps.get(j); - item.sectionName = mergedSectionName; - } + mSectionedFilteredApps.remove(nextSection.sectionBreakItem); + int pos = mSectionedFilteredApps.indexOf(section.firstAppItem); + // Point the section for these new apps to the merged section + int nextPos = pos + section.numApps; + for (int j = nextPos; j < (nextPos + nextSection.numApps); j++) { + AdapterItem item = mSectionedFilteredApps.get(j); + item.sectionInfo = section; + item.sectionAppIndex += section.numApps; } - // Update the following adapter items of the removed section - int pos = mSectionedFilteredApps.indexOf(nextSection.firstAppItem); + + // Update the following adapter items of the removed section item + pos = mSectionedFilteredApps.indexOf(nextSection.firstAppItem); for (int j = pos; j < mSectionedFilteredApps.size(); j++) { AdapterItem item = mSectionedFilteredApps.get(j); item.position--; } - section.numAppsInSection += nextSection.numAppsInSection; - sectionAppCount += nextSection.numAppsInSection; - mergeCount++; - if (mergeCount >= mMaxAllowableMerges) { - break; + section.numApps += nextSection.numApps; + sectionAppCount += nextSection.numApps; + + if (DEBUG) { + Log.d(TAG, "Merging: " + nextSection.firstAppItem.sectionName + + " to " + section.firstAppItem.sectionName + + " mergedNumRows: " + (sectionAppCount / mNumAppsPerRow)); } + mergeCount++; } } } diff --git a/src/com/android/launcher3/AppInfo.java b/src/com/android/launcher3/AppInfo.java index 7c6b0664c..58a57a1fe 100644 --- a/src/com/android/launcher3/AppInfo.java +++ b/src/com/android/launcher3/AppInfo.java @@ -105,7 +105,7 @@ public class AppInfo extends ItemInfo { public AppInfo(AppInfo info) { super(info); componentName = info.componentName; - title = info.title.toString(); + title = Utilities.trim(info.title); intent = new Intent(info.intent); flags = info.flags; firstInstallTime = info.firstInstallTime; @@ -114,7 +114,7 @@ public class AppInfo extends ItemInfo { @Override public String toString() { - return "ApplicationInfo(title=" + title.toString() + " id=" + this.id + return "ApplicationInfo(title=" + title + " id=" + this.id + " type=" + this.itemType + " container=" + this.container + " screen=" + screenId + " cellX=" + cellX + " cellY=" + cellY + " spanX=" + spanX + " spanY=" + spanY + " dropPos=" + Arrays.toString(dropPos) diff --git a/src/com/android/launcher3/AppsContainerRecyclerView.java b/src/com/android/launcher3/AppsContainerRecyclerView.java index 7f64be2f5..e918bc2ee 100644 --- a/src/com/android/launcher3/AppsContainerRecyclerView.java +++ b/src/com/android/launcher3/AppsContainerRecyclerView.java @@ -30,23 +30,32 @@ import android.view.MotionEvent; import android.view.View; import android.view.ViewConfiguration; -import com.android.launcher3.util.Thunk; - import java.util.List; /** * A RecyclerView with custom fastscroll support. This is the main container for the all apps * icons. */ -public class AppsContainerRecyclerView extends RecyclerView - implements RecyclerView.OnItemTouchListener { +public class AppsContainerRecyclerView extends BaseContainerRecyclerView { - private static final float FAST_SCROLL_OVERLAY_Y_OFFSET_FACTOR = 1.5f; - private static final int SCROLL_DELTA_THRESHOLD = 4; + /** + * The current scroll state of the recycler view. We use this in updateVerticalScrollbarBounds() + * and scrollToPositionAtProgress() to determine the scroll position of the recycler view so + * that we can calculate what the scroll bar looks like, and where to jump to from the fast + * scroller. + */ + private static class ScrollPositionState { + // The index of the first app in the row (Note that is this not the position) + int rowFirstAppIndex; + // The index of the first visible row + int rowIndex; + // The offset of the first visible row + int rowTopOffset; + // The height of a given row (they are currently all the same height) + int rowHeight; + } - /** Keeps the last known scrolling delta/velocity along y-axis. */ - @Thunk int mDy = 0; - private float mDeltaThreshold; + private static final float FAST_SCROLL_OVERLAY_Y_OFFSET_FACTOR = 1.5f; private AlphabeticalAppsList mApps; private int mNumAppsPerRow; @@ -66,7 +75,8 @@ public class AppsContainerRecyclerView extends RecyclerView private int mScrollbarWidth; private int mScrollbarMinHeight; private int mScrollbarInset; - private RecyclerView.OnScrollListener mScrollListenerProxy; + private Rect mBackgroundPadding = new Rect(); + private ScrollPositionState mScrollPosState = new ScrollPositionState(); public AppsContainerRecyclerView(Context context) { this(context, null); @@ -100,21 +110,7 @@ public class AppsContainerRecyclerView extends RecyclerView mScrollbarInset = res.getDimensionPixelSize(R.dimen.apps_view_fast_scroll_scrubber_touch_inset); setFastScrollerAlpha(getFastScrollerAlpha()); - mDeltaThreshold = getResources().getDisplayMetrics().density * SCROLL_DELTA_THRESHOLD; - - ScrollListener listener = new ScrollListener(); - setOnScrollListener(listener); - } - - private class ScrollListener extends RecyclerView.OnScrollListener { - public ScrollListener() { - } - - @Override - public void onScrolled(RecyclerView recyclerView, int dx, int dy) { - mDy = dy; - mScrollListenerProxy.onScrolled(recyclerView, dx, dy); - } + setOverScrollMode(View.OVER_SCROLL_NEVER); } /** @@ -131,11 +127,10 @@ public class AppsContainerRecyclerView extends RecyclerView mNumAppsPerRow = rowSize; } - /** - * Sets an additional scroll listener, not necessary in master support lib. - */ - public void setOnScrollListenerProxy(RecyclerView.OnScrollListener listener) { - mScrollListenerProxy = listener; + @Override + public void setBackground(Drawable background) { + super.setBackground(background); + background.getPadding(mBackgroundPadding); } /** @@ -160,6 +155,14 @@ public class AppsContainerRecyclerView extends RecyclerView return mScrollbarWidth; } + /** + * Scrolls this recycler view to the top. + */ + public void scrollToTop() { + scrollToPosition(0); + updateScrollY(0); + } + @Override protected void onFinishInflate() { super.onFinishInflate(); @@ -187,10 +190,6 @@ public class AppsContainerRecyclerView extends RecyclerView handleTouchEvent(ev); } - public void onRequestDisallowInterceptTouchEvent(boolean disallowIntercept) { - // DO NOT REMOVE, NEEDED IMPLEMENTATION FOR M BUILDS - } - /** * Handles the touch event and determines whether to show the fast scroller (or updates it if * it is already showing). @@ -206,8 +205,7 @@ public class AppsContainerRecyclerView extends RecyclerView // Keep track of the down positions mDownX = mLastX = x; mDownY = mLastY = y; - if ((Math.abs(mDy) < mDeltaThreshold && - getScrollState() != RecyclerView.SCROLL_STATE_IDLE)) { + if (shouldStopScroll(ev)) { stopScroll(); } break; @@ -265,7 +263,7 @@ public class AppsContainerRecyclerView extends RecyclerView * Draws the fast scroller popup. */ private void drawFastScrollerPopup(Canvas canvas) { - if (mFastScrollAlpha > 0f) { + if (mFastScrollAlpha > 0f && !mFastScrollSectionName.isEmpty()) { int x; int y; boolean isRtl = (getResources().getConfiguration().getLayoutDirection() == @@ -274,7 +272,7 @@ public class AppsContainerRecyclerView extends RecyclerView // Calculate the position for the fast scroller popup Rect bgBounds = mFastScrollerBg.getBounds(); if (isRtl) { - x = getPaddingLeft() + getScrollBarSize(); + x = mBackgroundPadding.left + getScrollBarSize(); } else { x = getWidth() - getPaddingRight() - getScrollBarSize() - bgBounds.width(); } @@ -290,8 +288,9 @@ public class AppsContainerRecyclerView extends RecyclerView mFastScrollTextPaint.setAlpha((int) (mFastScrollAlpha * 255)); mFastScrollTextPaint.getTextBounds(mFastScrollSectionName, 0, mFastScrollSectionName.length(), mFastScrollTextBounds); + float textWidth = mFastScrollTextPaint.measureText(mFastScrollSectionName); canvas.drawText(mFastScrollSectionName, - (bgBounds.width() - mFastScrollTextBounds.width()) / 2, + (bgBounds.width() - textWidth) / 2, bgBounds.height() - (bgBounds.height() - mFastScrollTextBounds.height()) / 2, mFastScrollTextPaint); canvas.restoreToCount(restoreCount); @@ -316,43 +315,47 @@ public class AppsContainerRecyclerView extends RecyclerView * Invalidates the fast scroller popup. */ private void invalidateFastScroller() { - invalidate(getWidth() - getPaddingRight() - getScrollBarSize() - + invalidate(getWidth() - mBackgroundPadding.right - getScrollBarSize() - mFastScrollerBg.getIntrinsicWidth(), 0, getWidth(), getHeight()); } /** - * Maps the progress (from 0..1) to the position that should be visible + * Maps the touch (from 0..1) to the adapter position that should be visible. */ - private String scrollToPositionAtProgress(float progress) { - List<AlphabeticalAppsList.SectionInfo> sections = mApps.getSections(); - if (sections.isEmpty()) { + private String scrollToPositionAtProgress(float touchFraction) { + // Ensure that we have any sections + List<AlphabeticalAppsList.FastScrollSectionInfo> fastScrollSections = + mApps.getFastScrollerSections(); + if (fastScrollSections.isEmpty()) { return ""; } - // Find the position of the first application in the section that contains the row at the - // current progress - List<AlphabeticalAppsList.AdapterItem> items = mApps.getAdapterItems(); - int rowAtProgress = (int) (progress * getNumRows()); - int rowCount = 0; - AlphabeticalAppsList.SectionInfo lastSectionInfo = null; - for (AlphabeticalAppsList.SectionInfo section : sections) { - int numRowsInSection = (int) Math.ceil((float) section.numAppsInSection / mNumAppsPerRow); - if (rowCount + numRowsInSection >= rowAtProgress) { - lastSectionInfo = section; + AlphabeticalAppsList.FastScrollSectionInfo lastScrollSection = fastScrollSections.get(0); + for (int i = 1; i < fastScrollSections.size(); i++) { + AlphabeticalAppsList.FastScrollSectionInfo scrollSection = fastScrollSections.get(i); + if (lastScrollSection.appRangeFraction <= touchFraction && + touchFraction < scrollSection.appRangeFraction) { break; } - rowCount += numRowsInSection; + lastScrollSection = scrollSection; } - int position = items.indexOf(lastSectionInfo.firstAppItem); // Scroll the position into view, anchored at the top of the screen if possible. We call the // scroll method on the LayoutManager directly since it is not exposed by RecyclerView. LinearLayoutManager layoutManager = (LinearLayoutManager) getLayoutManager(); stopScroll(); - layoutManager.scrollToPositionWithOffset(position, 0); + layoutManager.scrollToPositionWithOffset(lastScrollSection.appItem.position, 0); + + // We need to workaround the RecyclerView to get the right scroll position after scrolling + List<AlphabeticalAppsList.AdapterItem> items = mApps.getAdapterItems(); + getCurScrollState(mScrollPosState, items); + if (mScrollPosState.rowIndex != -1) { + int rowIndex = findRowForAppIndex(mScrollPosState.rowFirstAppIndex); + int y = (rowIndex * mScrollPosState.rowHeight) - mScrollPosState.rowTopOffset; + updateScrollY(y); + } - // Return the section name of the row - return lastSectionInfo.sectionName; + return lastScrollSection.sectionName; } /** @@ -372,44 +375,29 @@ public class AppsContainerRecyclerView extends RecyclerView int y; boolean isRtl = (getResources().getConfiguration().getLayoutDirection() == LAYOUT_DIRECTION_RTL); - int rowIndex = -1; - int rowTopOffset = -1; - int rowHeight = -1; int rowCount = getNumRows(); - int childCount = getChildCount(); - for (int i = 0; i < childCount; i++) { - View child = getChildAt(i); - int position = getChildPosition(child); - if (position != NO_POSITION) { - AlphabeticalAppsList.AdapterItem item = items.get(position); - if (!item.isSectionHeader) { - rowIndex = findRowForAppIndex(item.appIndex); - rowTopOffset = getLayoutManager().getDecoratedTop(child); - rowHeight = child.getHeight(); - break; - } - } - } + getCurScrollState(mScrollPosState, items); - if (rowIndex != -1) { + if (mScrollPosState.rowIndex != -1) { int height = getHeight() - getPaddingTop() - getPaddingBottom(); - int totalScrollHeight = rowCount * rowHeight; + int totalScrollHeight = rowCount * mScrollPosState.rowHeight; if (totalScrollHeight > height) { int scrollbarHeight = Math.max(mScrollbarMinHeight, (int) (height / ((float) totalScrollHeight / height))); // Calculate the position and size of the scroll bar if (isRtl) { - x = getPaddingLeft(); + x = mBackgroundPadding.left; } else { - x = getWidth() - getPaddingRight() - mScrollbarWidth; + x = getWidth() - mBackgroundPadding.right - mScrollbarWidth; } // To calculate the offset, we compute the percentage of the total scrollable height // that the user has already scrolled and then map that to the scroll bar bounds int availableY = totalScrollHeight - height; int availableScrollY = height - scrollbarHeight; - y = (rowIndex * rowHeight) - rowTopOffset; + y = (mScrollPosState.rowIndex * mScrollPosState.rowHeight) - + mScrollPosState.rowTopOffset; y = getPaddingTop() + (int) (((float) (getPaddingTop() + y) / availableY) * availableScrollY); @@ -421,18 +409,18 @@ public class AppsContainerRecyclerView extends RecyclerView } /** - * Returns the row index for a given position in the list. + * Returns the row index for a app index in the list. */ - private int findRowForAppIndex(int position) { + private int findRowForAppIndex(int index) { List<AlphabeticalAppsList.SectionInfo> sections = mApps.getSections(); int appIndex = 0; int rowCount = 0; for (AlphabeticalAppsList.SectionInfo info : sections) { - int numRowsInSection = (int) Math.ceil((float) info.numAppsInSection / mNumAppsPerRow); - if (appIndex + info.numAppsInSection > position) { - return rowCount + ((position - appIndex) / mNumAppsPerRow); + int numRowsInSection = (int) Math.ceil((float) info.numApps / mNumAppsPerRow); + if (appIndex + info.numApps > index) { + return rowCount + ((index - appIndex) / mNumAppsPerRow); } - appIndex += info.numAppsInSection; + appIndex += info.numApps; rowCount += numRowsInSection; } return appIndex; @@ -445,9 +433,35 @@ public class AppsContainerRecyclerView extends RecyclerView List<AlphabeticalAppsList.SectionInfo> sections = mApps.getSections(); int rowCount = 0; for (AlphabeticalAppsList.SectionInfo info : sections) { - int numRowsInSection = (int) Math.ceil((float) info.numAppsInSection / mNumAppsPerRow); + int numRowsInSection = (int) Math.ceil((float) info.numApps / mNumAppsPerRow); rowCount += numRowsInSection; } return rowCount; } + + /** + * Returns the current scroll state. + */ + private void getCurScrollState(ScrollPositionState stateOut, + List<AlphabeticalAppsList.AdapterItem> items) { + stateOut.rowFirstAppIndex = -1; + stateOut.rowIndex = -1; + stateOut.rowTopOffset = -1; + stateOut.rowHeight = -1; + int childCount = getChildCount(); + for (int i = 0; i < childCount; i++) { + View child = getChildAt(i); + int position = getChildPosition(child); + if (position != NO_POSITION) { + AlphabeticalAppsList.AdapterItem item = items.get(position); + if (!item.isSectionHeader) { + stateOut.rowFirstAppIndex = item.appIndex; + stateOut.rowIndex = findRowForAppIndex(item.appIndex); + stateOut.rowTopOffset = getLayoutManager().getDecoratedTop(child); + stateOut.rowHeight = child.getHeight(); + break; + } + } + } + } } diff --git a/src/com/android/launcher3/AppsContainerSearchEditTextView.java b/src/com/android/launcher3/AppsContainerSearchEditTextView.java new file mode 100644 index 000000000..c688237b2 --- /dev/null +++ b/src/com/android/launcher3/AppsContainerSearchEditTextView.java @@ -0,0 +1,65 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.launcher3; + +import android.content.Context; +import android.util.AttributeSet; +import android.view.KeyEvent; +import android.widget.EditText; + + +/** + * The edit text for the search container + */ +public class AppsContainerSearchEditTextView extends EditText { + + /** + * Implemented by listeners of the back key. + */ + public interface OnBackKeyListener { + public void onBackKey(); + } + + private OnBackKeyListener mBackKeyListener; + + public AppsContainerSearchEditTextView(Context context) { + this(context, null); + } + + public AppsContainerSearchEditTextView(Context context, AttributeSet attrs) { + this(context, attrs, 0); + } + + public AppsContainerSearchEditTextView(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + } + + public void setOnBackKeyListener(OnBackKeyListener listener) { + mBackKeyListener = listener; + } + + @Override + public boolean onKeyPreIme(int keyCode, KeyEvent event) { + // If this is a back key, propagate the key back to the listener + if (keyCode == KeyEvent.KEYCODE_BACK && event.getAction() == KeyEvent.ACTION_UP) { + if (mBackKeyListener != null) { + mBackKeyListener.onBackKey(); + } + return false; + } + return super.onKeyPreIme(keyCode, event); + } +} diff --git a/src/com/android/launcher3/AppsContainerView.java b/src/com/android/launcher3/AppsContainerView.java index 9122427fd..5dac9f1e8 100644 --- a/src/com/android/launcher3/AppsContainerView.java +++ b/src/com/android/launcher3/AppsContainerView.java @@ -15,6 +15,7 @@ */ package com.android.launcher3; +import android.content.ComponentName; import android.content.Context; import android.content.res.Resources; import android.graphics.Point; @@ -31,30 +32,35 @@ import android.view.ViewConfiguration; import android.view.inputmethod.EditorInfo; import android.view.inputmethod.InputMethodManager; import android.widget.EditText; -import android.widget.FrameLayout; import android.widget.LinearLayout; import android.widget.TextView; import com.android.launcher3.util.Thunk; import java.util.List; +import java.util.regex.Pattern; /** - * The all apps list view container. + * The all apps view container. */ -public class AppsContainerView extends FrameLayout implements DragSource, Insettable, TextWatcher, - TextView.OnEditorActionListener, LauncherTransitionable, View.OnTouchListener, +public class AppsContainerView extends BaseContainerView implements DragSource, Insettable, + TextWatcher, TextView.OnEditorActionListener, LauncherTransitionable, View.OnTouchListener, View.OnClickListener, View.OnLongClickListener { public static final boolean GRID_MERGE_SECTIONS = true; - public static final boolean GRID_MERGE_SECTION_HEADERS = false; public static final boolean GRID_HIDE_SECTION_HEADERS = false; private static final boolean ALLOW_SINGLE_APP_LAUNCH = true; - private static final boolean DYNAMIC_HEADER_ELEVATION = false; + private static final boolean DYNAMIC_HEADER_ELEVATION = true; + private static final boolean DISMISS_SEARCH_ON_BACK = true; private static final float HEADER_ELEVATION_DP = 4; + // How far the user has to scroll in order to reach the full elevation + private static final float HEADER_SCROLL_TO_ELEVATION_DP = 16; private static final int FADE_IN_DURATION = 175; - private static final int FADE_OUT_DURATION = 125; + private static final int FADE_OUT_DURATION = 100; + private static final int SEARCH_TRANSLATION_X_DP = 18; + + private static final Pattern SPLIT_PATTERN = Pattern.compile("[\\s|\\p{javaSpaceChar}]+"); @Thunk Launcher mLauncher; @Thunk AlphabeticalAppsList mApps; @@ -68,18 +74,14 @@ public class AppsContainerView extends FrameLayout implements DragSource, Insett private View mSearchBarContainerView; private View mSearchButtonView; private View mDismissSearchButtonView; - private EditText mSearchBarEditView; + private AppsContainerSearchEditTextView mSearchBarEditView; private int mNumAppsPerRow; private Point mLastTouchDownPos = new Point(-1, -1); private Point mLastTouchPos = new Point(); - private Rect mInsets = new Rect(); - private Rect mFixedBounds = new Rect(); private int mContentMarginStart; // Normal container insets private int mContainerInset; - // Fixed bounds container insets - private int mFixedBoundsContainerInset; // RecyclerView scroll position @Thunk int mRecyclerViewScrollY; @@ -99,8 +101,6 @@ public class AppsContainerView extends FrameLayout implements DragSource, Insett mContainerInset = context.getResources().getDimensionPixelSize( R.dimen.apps_container_inset); - mFixedBoundsContainerInset = context.getResources().getDimensionPixelSize( - R.dimen.apps_container_fixed_bounds_inset); mLauncher = (Launcher) context; mNumAppsPerRow = grid.appsViewNumCols; mApps = new AlphabeticalAppsList(context, mNumAppsPerRow); @@ -114,6 +114,13 @@ public class AppsContainerView extends FrameLayout implements DragSource, Insett } /** + * Sets the current set of predicted apps. + */ + public void setPredictedApps(List<ComponentName> apps) { + mApps.setPredictedApps(apps); + } + + /** * Sets the current set of apps. */ public void setApps(List<AppInfo> apps) { @@ -146,16 +153,15 @@ public class AppsContainerView extends FrameLayout implements DragSource, Insett */ public void hideHeaderBar() { mHeaderView.setVisibility(View.GONE); - updateBackgrounds(); - updatePaddings(); + onUpdateBackgrounds(); + onUpdatePaddings(); } /** * Scrolls this list view to the top. */ public void scrollToTop() { - mAppsRecyclerView.scrollToPosition(0); - mRecyclerViewScrollY = 0; + mAppsRecyclerView.scrollToTop(); } /** @@ -199,10 +205,25 @@ public class AppsContainerView extends FrameLayout implements DragSource, Insett mSearchBarContainerView = findViewById(R.id.app_search_container); mDismissSearchButtonView = mSearchBarContainerView.findViewById(R.id.dismiss_search_button); mDismissSearchButtonView.setOnClickListener(this); - mSearchBarEditView = (EditText) findViewById(R.id.app_search_box); + mSearchBarEditView = (AppsContainerSearchEditTextView) findViewById(R.id.app_search_box); if (mSearchBarEditView != null) { mSearchBarEditView.addTextChangedListener(this); mSearchBarEditView.setOnEditorActionListener(this); + if (DISMISS_SEARCH_ON_BACK) { + mSearchBarEditView.setOnBackKeyListener( + new AppsContainerSearchEditTextView.OnBackKeyListener() { + @Override + public void onBackKey() { + // Only hide the search field if there is no query, or if there + // are no filtered results + String query = Utilities.trim( + mSearchBarEditView.getEditableText().toString()); + if (query.isEmpty() || mApps.hasNoFilteredResults()) { + hideSearchField(true, true); + } + } + }); + } } mAppsRecyclerView = (AppsContainerRecyclerView) findViewById(R.id.apps_list_view); mAppsRecyclerView.setApps(mApps); @@ -210,61 +231,96 @@ public class AppsContainerView extends FrameLayout implements DragSource, Insett mAppsRecyclerView.setLayoutManager(mLayoutManager); mAppsRecyclerView.setAdapter(mAdapter); mAppsRecyclerView.setHasFixedSize(true); - mAppsRecyclerView.setOnScrollListenerProxy(new RecyclerView.OnScrollListener() { - @Override - public void onScrollStateChanged(RecyclerView recyclerView, int newState) { - // Do nothing - } - - @Override - public void onScrolled(RecyclerView recyclerView, int dx, int dy) { - mRecyclerViewScrollY += dy; - onRecyclerViewScrolled(); - } - }); + mAppsRecyclerView.setOnScrollListenerProxy( + new BaseContainerRecyclerView.OnScrollToListener() { + @Override + public void onScrolledTo(int x, int y) { + mRecyclerViewScrollY = y; + onRecyclerViewScrolled(); + } + }); if (mItemDecoration != null) { mAppsRecyclerView.addItemDecoration(mItemDecoration); } - updateBackgrounds(); - updatePaddings(); + onUpdateBackgrounds(); + onUpdatePaddings(); } @Override - public void setInsets(Rect insets) { - mInsets.set(insets); - updatePaddings(); + protected void onFixedBoundsUpdated() { + // Update the number of items in the grid + LauncherAppState app = LauncherAppState.getInstance(); + DeviceProfile grid = app.getDynamicGrid().getDeviceProfile(); + if (grid.updateAppsViewNumCols(getContext().getResources(), mFixedBounds.width())) { + mNumAppsPerRow = grid.appsViewNumCols; + mAppsRecyclerView.setNumAppsPerRow(mNumAppsPerRow); + mAdapter.setNumAppsPerRow(mNumAppsPerRow); + mApps.setNumAppsPerRow(mNumAppsPerRow); + } } /** - * Sets the fixed bounds for this Apps view. + * Update the padding of the Apps view and children. To ensure that the RecyclerView has the + * full width to handle touches right to the edge of the screen, we only apply the top and + * bottom padding to the AppsContainerView and then the left/right padding on the RecyclerView + * itself. In particular, the left/right padding is applied to the background of the view, + * and then additionally inset by the start margin. */ - public void setFixedBounds(Context context, Rect fixedBounds) { - if (!fixedBounds.isEmpty() && !fixedBounds.equals(mFixedBounds)) { - // Update the number of items in the grid - LauncherAppState app = LauncherAppState.getInstance(); - DeviceProfile grid = app.getDynamicGrid().getDeviceProfile(); - if (grid.updateAppsViewNumCols(context.getResources(), fixedBounds.width())) { - mNumAppsPerRow = grid.appsViewNumCols; - mAppsRecyclerView.setNumAppsPerRow(mNumAppsPerRow); - mAdapter.setNumAppsPerRow(mNumAppsPerRow); - mApps.setNumAppsPerRow(mNumAppsPerRow); - } + @Override + protected void onUpdatePaddings() { + boolean isRtl = (getResources().getConfiguration().getLayoutDirection() == + LAYOUT_DIRECTION_RTL); + boolean hasSearchBar = (mSearchBarEditView != null) && + (mSearchBarEditView.getVisibility() == View.VISIBLE); - mFixedBounds.set(fixedBounds); - if (Launcher.DISABLE_ALL_APPS_SEARCH_INTEGRATION) { - mFixedBounds.top = mInsets.top; - mFixedBounds.bottom = getMeasuredHeight(); - } + if (mFixedBounds.isEmpty()) { + // If there are no fixed bounds, then use the default padding and insets + setPadding(mInsets.left, mContainerInset + mInsets.top, mInsets.right, + mContainerInset + mInsets.bottom); + } else { + // If there are fixed bounds, then we update the padding to reflect the fixed bounds. + setPadding(mFixedBounds.left, mFixedBounds.top, getMeasuredWidth() - mFixedBounds.right, + mFixedBounds.bottom); + } + + // Update the apps recycler view, inset it by the container inset as well + DeviceProfile grid = LauncherAppState.getInstance().getDynamicGrid().getDeviceProfile(); + int startMargin = grid.isPhone() ? mContentMarginStart : 0; + int inset = mFixedBounds.isEmpty() ? mContainerInset : mFixedBoundsContainerInset; + if (isRtl) { + mAppsRecyclerView.setPadding(inset + mAppsRecyclerView.getScrollbarWidth(), inset, + inset + startMargin, inset); + } else { + mAppsRecyclerView.setPadding(inset + startMargin, inset, + inset + mAppsRecyclerView.getScrollbarWidth(), inset); + } + + // Update the header bar + if (hasSearchBar) { + LinearLayout.LayoutParams lp = + (LinearLayout.LayoutParams) mHeaderView.getLayoutParams(); + lp.leftMargin = lp.rightMargin = inset; } - // Post the updates since they can trigger a relayout, and this call can be triggered from - // a layout pass itself. - post(new Runnable() { - @Override - public void run() { - updateBackgrounds(); - updatePaddings(); - } - }); + } + + /** + * Update the background of the Apps view and children. + */ + @Override + protected void onUpdateBackgrounds() { + int inset = mFixedBounds.isEmpty() ? mContainerInset : mFixedBoundsContainerInset; + boolean hasSearchBar = (mSearchBarEditView != null) && + (mSearchBarEditView.getVisibility() == View.VISIBLE); + + // Update the background of the reveal view and list to be inset with the fixed bound + // insets instead of the default insets + mAppsRecyclerView.setBackground(new InsetDrawable( + getContext().getResources().getDrawable( + hasSearchBar ? R.drawable.apps_list_search_bg : R.drawable.apps_list_bg), + inset, 0, inset, 0)); + getRevealView().setBackground(new InsetDrawable( + getContext().getResources().getDrawable(R.drawable.apps_reveal_bg), + inset, 0, inset, 0)); } @Override @@ -392,27 +448,40 @@ public class AppsContainerView extends FrameLayout implements DragSource, Insett @Override public void afterTextChanged(final Editable s) { - if (s.toString().isEmpty()) { + String queryText = s.toString(); + if (queryText.isEmpty()) { mApps.setFilter(null); } else { String formatStr = getResources().getString(R.string.apps_view_no_search_results); - mAdapter.setEmptySearchText(String.format(formatStr, s.toString())); + mAdapter.setEmptySearchText(String.format(formatStr, queryText)); - final String filterText = s.toString().toLowerCase().replaceAll("\\s+", ""); + // Do an intersection of the words in the query and each title, and filter out all the + // apps that don't match all of the words in the query. + final String queryTextLower = queryText.toLowerCase(); + final String[] queryWords = SPLIT_PATTERN.split(queryTextLower); mApps.setFilter(new AlphabeticalAppsList.Filter() { @Override public boolean retainApp(AppInfo info, String sectionName) { - String title = info.title.toString(); - if (sectionName.toLowerCase().contains(filterText)) { + if (sectionName.toLowerCase().contains(queryTextLower)) { return true; } - String[] words = title.toLowerCase().split("\\s+"); - for (int i = 0; i < words.length; i++) { - if (words[i].startsWith(filterText)) { - return true; + String title = info.title.toString(); + String[] words = SPLIT_PATTERN.split(title.toLowerCase()); + for (int qi = 0; qi < queryWords.length; qi++) { + boolean foundMatch = false; + for (int i = 0; i < words.length; i++) { + if (words[i].startsWith(queryWords[qi])) { + foundMatch = true; + break; + } + } + if (!foundMatch) { + // If there is a word in the query that does not match any words in this + // title, so skip it. + return false; } } - return false; + return true; } }); } @@ -473,11 +542,16 @@ public class AppsContainerView extends FrameLayout implements DragSource, Insett * Updates the container when the recycler view is scrolled. */ private void onRecyclerViewScrolled() { - if (DYNAMIC_HEADER_ELEVATION) { - int elevation = Math.min(mRecyclerViewScrollY, DynamicGrid.pxFromDp(HEADER_ELEVATION_DP, - getContext().getResources().getDisplayMetrics())); - if (Float.compare(mHeaderView.getElevation(), elevation) != 0) { - mHeaderView.setElevation(elevation); + if (DYNAMIC_HEADER_ELEVATION && Utilities.isLmpOrAbove()) { + int elevation = DynamicGrid.pxFromDp(HEADER_ELEVATION_DP, + getContext().getResources().getDisplayMetrics()); + int scrollToElevation = DynamicGrid.pxFromDp(HEADER_SCROLL_TO_ELEVATION_DP, + getContext().getResources().getDisplayMetrics()); + float elevationPct = (float) Math.min(mRecyclerViewScrollY, scrollToElevation) / + scrollToElevation; + float newElevation = elevation * elevationPct; + if (Float.compare(mHeaderView.getElevation(), newElevation) != 0) { + mHeaderView.setElevation(newElevation); } } } @@ -531,72 +605,20 @@ public class AppsContainerView extends FrameLayout implements DragSource, Insett } /** - * Update the padding of the Apps view and children. To ensure that the RecyclerView has the - * full width to handle touches right to the edge of the screen, we only apply the top and - * bottom padding to the AppsContainerView and then the left/right padding on the RecyclerView - * itself. In particular, the left/right padding is applied to the background of the view, - * and then additionally inset by the start margin. - */ - private void updatePaddings() { - boolean isRtl = (getResources().getConfiguration().getLayoutDirection() == - LAYOUT_DIRECTION_RTL); - boolean hasSearchBar = (mSearchBarEditView != null) && - (mSearchBarEditView.getVisibility() == View.VISIBLE); - - if (mFixedBounds.isEmpty()) { - // If there are no fixed bounds, then use the default padding and insets - setPadding(mInsets.left, mContainerInset + mInsets.top, mInsets.right, - mContainerInset + mInsets.bottom); - } else { - // If there are fixed bounds, then we update the padding to reflect the fixed bounds. - setPadding(mFixedBounds.left, mFixedBounds.top + mFixedBoundsContainerInset, - getMeasuredWidth() - mFixedBounds.right, - mInsets.bottom + mFixedBoundsContainerInset); - } - - // Update the apps recycler view - int inset = mFixedBounds.isEmpty() ? mContainerInset : mFixedBoundsContainerInset; - if (isRtl) { - mAppsRecyclerView.setPadding(inset, inset, inset + mContentMarginStart, inset); - } else { - mAppsRecyclerView.setPadding(inset + mContentMarginStart, inset, inset, inset); - } - - // Update the header - if (hasSearchBar) { - LinearLayout.LayoutParams lp = - (LinearLayout.LayoutParams) mHeaderView.getLayoutParams(); - lp.leftMargin = lp.rightMargin = inset; - } - } - - /** - * Update the background of the Apps view and children. - */ - private void updateBackgrounds() { - int inset = mFixedBounds.isEmpty() ? mContainerInset : mFixedBoundsContainerInset; - boolean hasSearchBar = (mSearchBarEditView != null) && - (mSearchBarEditView.getVisibility() == View.VISIBLE); - - // Update the background of the reveal view and list to be inset with the fixed bound - // insets instead of the default insets - mAppsRecyclerView.setBackground(new InsetDrawable( - getContext().getResources().getDrawable( - hasSearchBar ? R.drawable.apps_list_search_bg : R.drawable.apps_list_bg), - inset, 0, inset, 0)); - getRevealView().setBackground(new InsetDrawable( - getContext().getResources().getDrawable(R.drawable.apps_reveal_bg), - inset, 0, inset, 0)); - } - - /** * Shows the search field. */ private void showSearchField() { // Show the search bar and focus the search + final int translationX = DynamicGrid.pxFromDp(SEARCH_TRANSLATION_X_DP, + getContext().getResources().getDisplayMetrics()); mSearchBarContainerView.setVisibility(View.VISIBLE); mSearchBarContainerView.setAlpha(0f); - mSearchBarContainerView.animate().alpha(1f).setDuration(FADE_IN_DURATION).withLayer() + mSearchBarContainerView.setTranslationX(translationX); + mSearchBarContainerView.animate() + .alpha(1f) + .translationX(0) + .setDuration(FADE_IN_DURATION) + .withLayer() .withEndAction(new Runnable() { @Override public void run() { @@ -605,38 +627,57 @@ public class AppsContainerView extends FrameLayout implements DragSource, Insett InputMethodManager.SHOW_IMPLICIT); } }); - mSearchButtonView.animate().alpha(0f).setDuration(FADE_OUT_DURATION).withLayer(); + mSearchButtonView.animate() + .alpha(0f) + .translationX(-translationX) + .setDuration(FADE_OUT_DURATION) + .withLayer(); } /** * Hides the search field. */ private void hideSearchField(boolean animated, final boolean returnFocusToRecyclerView) { + final boolean resetTextField = mSearchBarEditView.getText().toString().length() > 0; + final int translationX = DynamicGrid.pxFromDp(SEARCH_TRANSLATION_X_DP, + getContext().getResources().getDisplayMetrics()); if (animated) { // Hide the search bar and focus the recycler view - mSearchBarContainerView.animate().alpha(0f).setDuration(FADE_IN_DURATION).withLayer() + mSearchBarContainerView.animate() + .alpha(0f) + .translationX(0) + .setDuration(FADE_IN_DURATION) + .withLayer() .withEndAction(new Runnable() { @Override public void run() { mSearchBarContainerView.setVisibility(View.INVISIBLE); - mSearchBarEditView.setText(""); + if (resetTextField) { + mSearchBarEditView.setText(""); + } mApps.setFilter(null); if (returnFocusToRecyclerView) { mAppsRecyclerView.requestFocus(); } - scrollToTop(); } }); - mSearchButtonView.animate().alpha(1f).setDuration(FADE_OUT_DURATION).withLayer(); + mSearchButtonView.setTranslationX(-translationX); + mSearchButtonView.animate() + .alpha(1f) + .translationX(0) + .setDuration(FADE_OUT_DURATION) + .withLayer(); } else { mSearchBarContainerView.setVisibility(View.INVISIBLE); - mSearchBarEditView.setText(""); + if (resetTextField) { + mSearchBarEditView.setText(""); + } mApps.setFilter(null); mSearchButtonView.setAlpha(1f); + mSearchButtonView.setTranslationX(0f); if (returnFocusToRecyclerView) { mAppsRecyclerView.requestFocus(); } - scrollToTop(); } getInputMethodManager().hideSoftInputFromWindow(getWindowToken(), 0); } diff --git a/src/com/android/launcher3/AppsGridAdapter.java b/src/com/android/launcher3/AppsGridAdapter.java index 62d9129c9..4014e3804 100644 --- a/src/com/android/launcher3/AppsGridAdapter.java +++ b/src/com/android/launcher3/AppsGridAdapter.java @@ -4,11 +4,10 @@ import android.content.Context; import android.content.res.Resources; import android.graphics.Canvas; import android.graphics.Paint; -import android.graphics.Point; +import android.graphics.PointF; import android.graphics.Rect; import android.support.v7.widget.GridLayoutManager; import android.support.v7.widget.RecyclerView; -import android.util.Log; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; @@ -36,13 +35,11 @@ class AppsGridAdapter extends RecyclerView.Adapter<AppsGridAdapter.ViewHolder> { */ public static class ViewHolder extends RecyclerView.ViewHolder { public View mContent; - public boolean mIsSectionHeader; public boolean mIsEmptyRow; - public ViewHolder(View v, boolean isSectionHeader, boolean isEmptyRow) { + public ViewHolder(View v, boolean isEmptyRow) { super(v); mContent = v; - mIsSectionHeader = isSectionHeader; mIsEmptyRow = isEmptyRow; } } @@ -66,11 +63,7 @@ class AppsGridAdapter extends RecyclerView.Adapter<AppsGridAdapter.ViewHolder> { if (mApps.getAdapterItems().get(position).isSectionHeader) { // Section break spans full width - if (AppsContainerView.GRID_HIDE_SECTION_HEADERS) { - return 0; - } else { - return mAppsPerRow; - } + return mAppsPerRow; } else { return 1; } @@ -84,7 +77,7 @@ class AppsGridAdapter extends RecyclerView.Adapter<AppsGridAdapter.ViewHolder> { private static final boolean FADE_OUT_SECTIONS = false; - private HashMap<String, Point> mCachedSectionBounds = new HashMap<>(); + private HashMap<String, PointF> mCachedSectionBounds = new HashMap<>(); private Rect mTmpBounds = new Rect(); @Override @@ -93,76 +86,91 @@ class AppsGridAdapter extends RecyclerView.Adapter<AppsGridAdapter.ViewHolder> { return; } + DeviceProfile grid = LauncherAppState.getInstance().getDynamicGrid().getDeviceProfile(); List<AlphabeticalAppsList.AdapterItem> items = mApps.getAdapterItems(); - String lastSectionName = null; - int appIndexInSection = 0; + boolean hasDrawnPredictedAppDivider = false; + int childCount = parent.getChildCount(); int lastSectionTop = 0; int lastSectionHeight = 0; - for (int i = 0; i < parent.getChildCount(); i++) { + for (int i = 0; i < childCount; i++) { View child = parent.getChildAt(i); ViewHolder holder = (ViewHolder) parent.getChildViewHolder(child); - if (shouldDrawItemSection(holder, child, i, items)) { - int cellTopOffset = (2 * child.getPaddingTop()); + if (!isValidHolderAndChild(holder, child, items)) { + continue; + } + + if (shouldDrawItemDivider(holder, items) && !hasDrawnPredictedAppDivider) { + // Draw the divider under the predicted app + parent.getBackground().getPadding(mTmpBounds); + int top = child.getTop() + child.getHeight(); + c.drawLine(mTmpBounds.left, top, parent.getWidth() - mTmpBounds.right, top, + mPredictedAppsDividerPaint); + hasDrawnPredictedAppDivider = true; + + } else if (grid.isPhone() && shouldDrawItemSection(holder, i, items)) { + // At this point, we only draw sections for each section break; + int viewTopOffset = (2 * child.getPaddingTop()); int pos = holder.getPosition(); AlphabeticalAppsList.AdapterItem item = items.get(pos); - if (!item.sectionName.equals(lastSectionName)) { - lastSectionName = item.sectionName; - - // Find the section code points - String sectionBegin = null; - String sectionEnd = null; - int charOffset = 0; - while (charOffset < item.sectionName.length()) { - int codePoint = item.sectionName.codePointAt(charOffset); - int codePointSize = Character.charCount(codePoint); - if (charOffset == 0) { - // The first code point - sectionBegin = item.sectionName.substring(charOffset, charOffset + codePointSize); - } else if ((charOffset + codePointSize) >= item.sectionName.length()) { - // The last code point - sectionEnd = item.sectionName.substring(charOffset, charOffset + codePointSize); - } - charOffset += codePointSize; + AlphabeticalAppsList.SectionInfo sectionInfo = item.sectionInfo; + + // Draw all the sections for this index + String lastSectionName = item.sectionName; + for (int j = item.sectionAppIndex; j < sectionInfo.numApps; j++, pos++) { + AlphabeticalAppsList.AdapterItem nextItem = items.get(pos); + String sectionName = nextItem.sectionName; + if (nextItem.sectionInfo != sectionInfo) { + break; } + if (j > item.sectionAppIndex && sectionName.equals(lastSectionName)) { + continue; + } + - Point sectionBeginBounds = getAndCacheSectionBounds(sectionBegin); - int minTop = cellTopOffset + sectionBeginBounds.y; - int top = child.getTop() + cellTopOffset + sectionBeginBounds.y; - int left = mIsRtl ? parent.getWidth() - mPaddingStart - mStartMargin : + // Find the section name bounds + PointF sectionBounds = getAndCacheSectionBounds(sectionName); + + // Calculate where to draw the section + int sectionBaseline = (int) (viewTopOffset + sectionBounds.y); + int x = mIsRtl ? parent.getWidth() - mPaddingStart - mStartMargin : mPaddingStart; - int col = appIndexInSection % mAppsPerRow; - int nextRowPos = Math.min(pos - col + mAppsPerRow, items.size() - 1); - int alpha = 255; - boolean fixedToRow = !items.get(nextRowPos).sectionName.equals(item.sectionName); - if (fixedToRow) { - alpha = Math.min(255, (int) (255 * (Math.max(0, top) / (float) minTop))); - } else { - // If we aren't fixed to the current row, then bound into the viewport - top = Math.max(minTop, top); + x += (int) ((mStartMargin - sectionBounds.x) / 2f); + int y = child.getTop() + sectionBaseline; + + // Determine whether this is the last row with apps in that section, if + // so, then fix the section to the row allowing it to scroll past the + // baseline, otherwise, bound it to the baseline so it's in the viewport + int appIndexInSection = items.get(pos).sectionAppIndex; + int nextRowPos = Math.min(items.size() - 1, + pos + mAppsPerRow - (appIndexInSection % mAppsPerRow)); + AlphabeticalAppsList.AdapterItem nextRowItem = items.get(nextRowPos); + boolean fixedToRow = !sectionName.equals(nextRowItem.sectionName); + if (!fixedToRow) { + y = Math.max(sectionBaseline, y); } - if (lastSectionHeight > 0 && top <= (lastSectionTop + lastSectionHeight)) { - top += lastSectionTop - top + lastSectionHeight; + + // In addition, if it overlaps with the last section that was drawn, then + // offset it so that it does not overlap + if (lastSectionHeight > 0 && y <= (lastSectionTop + lastSectionHeight)) { + y += lastSectionTop - y + lastSectionHeight; } + + // Draw the section header if (FADE_OUT_SECTIONS) { + int alpha = 255; + if (fixedToRow) { + alpha = Math.min(255, + (int) (255 * (Math.max(0, y) / (float) sectionBaseline))); + } mSectionTextPaint.setAlpha(alpha); } - if (sectionEnd != null) { - Point sectionEndBounds = getAndCacheSectionBounds(sectionEnd); - c.drawText(sectionBegin + "/" + sectionEnd, - left + (mStartMargin - sectionBeginBounds.x - sectionEndBounds.x) / 2, top, - mSectionTextPaint); - } else { - c.drawText(sectionBegin, left + (mStartMargin - sectionBeginBounds.x) / 2, top, - mSectionTextPaint); - } - lastSectionTop = top; - lastSectionHeight = sectionBeginBounds.y + mSectionHeaderOffset; + c.drawText(sectionName, x, y, mSectionTextPaint); + + lastSectionTop = y; + lastSectionHeight = (int) (sectionBounds.y + mSectionHeaderOffset); + lastSectionName = sectionName; } - } - if (holder.mIsSectionHeader) { - appIndexInSection = 0; - } else { - appIndexInSection++; + i += (sectionInfo.numApps - item.sectionAppIndex); } } } @@ -173,17 +181,23 @@ class AppsGridAdapter extends RecyclerView.Adapter<AppsGridAdapter.ViewHolder> { // Do nothing } - private Point getAndCacheSectionBounds(String sectionName) { - Point bounds = mCachedSectionBounds.get(sectionName); + /** + * Given a section name, return the bounds of the given section name. + */ + private PointF getAndCacheSectionBounds(String sectionName) { + PointF bounds = mCachedSectionBounds.get(sectionName); if (bounds == null) { mSectionTextPaint.getTextBounds(sectionName, 0, sectionName.length(), mTmpBounds); - bounds = new Point(mTmpBounds.width(), mTmpBounds.height()); + bounds = new PointF(mSectionTextPaint.measureText(sectionName), mTmpBounds.height()); mCachedSectionBounds.put(sectionName, bounds); } return bounds; } - private boolean shouldDrawItemSection(ViewHolder holder, View child, int childIndex, + /** + * Returns whether we consider this a valid view holder for us to draw a divider or section for. + */ + private boolean isValidHolderAndChild(ViewHolder holder, View child, List<AlphabeticalAppsList.AdapterItem> items) { // Ensure item is not already removed GridLayoutManager.LayoutParams lp = (GridLayoutManager.LayoutParams) @@ -195,25 +209,44 @@ class AppsGridAdapter extends RecyclerView.Adapter<AppsGridAdapter.ViewHolder> { if (holder == null) { return false; } + // Ensure we have a holder position + int pos = holder.getPosition(); + if (pos < 0 || pos >= items.size()) { + return false; + } + return true; + } + + /** + * Returns whether to draw the divider for a given child. + */ + private boolean shouldDrawItemDivider(ViewHolder holder, List<AlphabeticalAppsList.AdapterItem> items) { + int pos = holder.getPosition(); + return items.get(pos).isPredictedApp; + } + + /** + * Returns whether to draw the section for the given child. + */ + private boolean shouldDrawItemSection(ViewHolder holder, int childIndex, + List<AlphabeticalAppsList.AdapterItem> items) { + int pos = holder.getPosition(); + AlphabeticalAppsList.AdapterItem item = items.get(pos); + // Ensure it's not an empty row if (holder.mIsEmptyRow) { return false; } - // Ensure we have a holder position - int pos = holder.getPosition(); - if (pos < 0 || pos >= items.size()) { + // Ensure this is not a section break + if (item.isSectionHeader) { return false; } - // Ensure this is not a section header - if (items.get(pos).isSectionHeader) { + // Ensure this is not a predicted app + if (item.isPredictedApp) { return false; } - // Only draw the header for the first item in a section, or whenever the sub-sections - // changes (if AppsContainerView.GRID_MERGE_SECTIONS is true, but - // AppsContainerView.GRID_MERGE_SECTION_HEADERS is false) - return (childIndex == 0) || - items.get(pos - 1).isSectionHeader && !items.get(pos).isSectionHeader || - (!items.get(pos - 1).sectionName.equals(items.get(pos).sectionName)); + // Draw the section header for the first item in each section + return (childIndex == 0) || (items.get(pos - 1).isSectionHeader && !item.isSectionHeader); } } @@ -234,6 +267,7 @@ class AppsGridAdapter extends RecyclerView.Adapter<AppsGridAdapter.ViewHolder> { @Thunk int mStartMargin; @Thunk int mSectionHeaderOffset; @Thunk Paint mSectionTextPaint; + @Thunk Paint mPredictedAppsDividerPaint; public AppsGridAdapter(Context context, AlphabeticalAppsList apps, int appsPerRow, @@ -251,16 +285,20 @@ class AppsGridAdapter extends RecyclerView.Adapter<AppsGridAdapter.ViewHolder> { mTouchListener = touchListener; mIconClickListener = iconClickListener; mIconLongClickListener = iconLongClickListener; - if (!AppsContainerView.GRID_HIDE_SECTION_HEADERS) { - mStartMargin = res.getDimensionPixelSize(R.dimen.apps_grid_view_start_margin); - mSectionHeaderOffset = res.getDimensionPixelSize(R.dimen.apps_grid_section_y_offset); - } + mStartMargin = res.getDimensionPixelSize(R.dimen.apps_grid_view_start_margin); + mSectionHeaderOffset = res.getDimensionPixelSize(R.dimen.apps_grid_section_y_offset); mPaddingStart = res.getDimensionPixelSize(R.dimen.apps_container_inset); + mSectionTextPaint = new Paint(); mSectionTextPaint.setTextSize(res.getDimensionPixelSize( R.dimen.apps_view_section_text_size)); mSectionTextPaint.setColor(res.getColor(R.color.apps_view_section_text_color)); mSectionTextPaint.setAntiAlias(true); + + mPredictedAppsDividerPaint = new Paint(); + mPredictedAppsDividerPaint.setStrokeWidth(DynamicGrid.pxFromDp(1f, res.getDisplayMetrics())); + mPredictedAppsDividerPaint.setColor(0x1E000000); + mPredictedAppsDividerPaint.setAntiAlias(true); } /** @@ -297,10 +335,7 @@ class AppsGridAdapter extends RecyclerView.Adapter<AppsGridAdapter.ViewHolder> { */ public RecyclerView.ItemDecoration getItemDecoration() { // We don't draw any headers when we are uncomfortably dense - if (!AppsContainerView.GRID_HIDE_SECTION_HEADERS) { - return mItemDecoration; - } - return null; + return mItemDecoration; } /** @@ -315,10 +350,9 @@ class AppsGridAdapter extends RecyclerView.Adapter<AppsGridAdapter.ViewHolder> { switch (viewType) { case EMPTY_VIEW_TYPE: return new ViewHolder(mLayoutInflater.inflate(R.layout.apps_empty_view, parent, - false), false /* isSectionRow */, true /* isEmptyRow */); + false), true /* isEmptyRow */); case SECTION_BREAK_VIEW_TYPE: - return new ViewHolder(new View(parent.getContext()), true /* isSectionRow */, - false /* isEmptyRow */); + return new ViewHolder(new View(parent.getContext()), false /* isEmptyRow */); case ICON_VIEW_TYPE: BubbleTextView icon = (BubbleTextView) mLayoutInflater.inflate( R.layout.apps_grid_row_icon_view, parent, false); @@ -326,7 +360,7 @@ class AppsGridAdapter extends RecyclerView.Adapter<AppsGridAdapter.ViewHolder> { icon.setOnClickListener(mIconClickListener); icon.setOnLongClickListener(mIconLongClickListener); icon.setFocusable(true); - return new ViewHolder(icon, false /* isSectionRow */, false /* isEmptyRow */); + return new ViewHolder(icon, false /* isEmptyRow */); default: throw new RuntimeException("Unexpected view type"); } diff --git a/src/com/android/launcher3/BaseContainerRecyclerView.java b/src/com/android/launcher3/BaseContainerRecyclerView.java new file mode 100644 index 000000000..59e20ca2f --- /dev/null +++ b/src/com/android/launcher3/BaseContainerRecyclerView.java @@ -0,0 +1,133 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.launcher3; + +import android.content.Context; +import android.support.v7.widget.RecyclerView; +import android.util.AttributeSet; +import android.view.MotionEvent; +import com.android.launcher3.util.Thunk; + +/** + * A base {@link RecyclerView}, which will NOT intercept a touch sequence unless the scrolling + * velocity is below a predefined threshold. + */ +public class BaseContainerRecyclerView extends RecyclerView + implements RecyclerView.OnItemTouchListener { + + /** + * Listener to get notified when the absolute scroll changes. + */ + public interface OnScrollToListener { + void onScrolledTo(int x, int y); + } + + private static final int SCROLL_DELTA_THRESHOLD_DP = 4; + + /** Keeps the last known scrolling delta/velocity along y-axis. */ + @Thunk int mDy = 0; + @Thunk int mScrollY; + private float mDeltaThreshold; + private OnScrollToListener mScrollToListener; + + public BaseContainerRecyclerView(Context context) { + this(context, null); + } + + public BaseContainerRecyclerView(Context context, AttributeSet attrs) { + this(context, attrs, 0); + } + + public BaseContainerRecyclerView(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + mDeltaThreshold = getResources().getDisplayMetrics().density * SCROLL_DELTA_THRESHOLD_DP; + + ScrollListener listener = new ScrollListener(); + setOnScrollListener(listener); + } + + private class ScrollListener extends OnScrollListener { + public ScrollListener() { + // Do nothing + } + + @Override + public void onScrolled(RecyclerView recyclerView, int dx, int dy) { + mDy = dy; + mScrollY += dy; + if (mScrollToListener != null) { + mScrollToListener.onScrolledTo(0, mScrollY); + } + } + } + + /** + * Sets an additional scroll listener, only needed for LMR1 version of the support lib. + */ + public void setOnScrollListenerProxy(OnScrollToListener listener) { + mScrollToListener = listener; + } + + @Override + protected void onFinishInflate() { + super.onFinishInflate(); + addOnItemTouchListener(this); + } + + @Override + public boolean onInterceptTouchEvent(RecyclerView rv, MotionEvent ev) { + if (shouldStopScroll(ev)) { + stopScroll(); + } + return false; + } + + @Override + public void onTouchEvent(RecyclerView rv, MotionEvent ev) { + // Do nothing. + } + + public void onRequestDisallowInterceptTouchEvent(boolean disallowIntercept) { + // DO NOT REMOVE, NEEDED IMPLEMENTATION FOR M BUILDS + } + + /** + * Updates the scroll position, used to workaround a RecyclerView issue with scrolling to + * position. + */ + protected void updateScrollY(int scrollY) { + mScrollY = scrollY; + if (mScrollToListener != null) { + mScrollToListener.onScrolledTo(0, mScrollY); + } + } + + /** + * Returns whether this {@link MotionEvent} should trigger the scroll to be stopped. + */ + protected boolean shouldStopScroll(MotionEvent ev) { + if (ev.getAction() == MotionEvent.ACTION_DOWN) { + if ((Math.abs(mDy) < mDeltaThreshold && + getScrollState() != RecyclerView.SCROLL_STATE_IDLE)) { + // now the touch events are being passed to the {@link WidgetCell} until the + // touch sequence goes over the touch slop. + return true; + } + } + return false; + } +}
\ No newline at end of file diff --git a/src/com/android/launcher3/BaseContainerView.java b/src/com/android/launcher3/BaseContainerView.java new file mode 100644 index 000000000..bd1c625e3 --- /dev/null +++ b/src/com/android/launcher3/BaseContainerView.java @@ -0,0 +1,101 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.launcher3; + +import android.content.Context; +import android.graphics.Rect; +import android.util.AttributeSet; +import android.widget.FrameLayout; + +/** + * A base container view, which supports resizing. + */ +public class BaseContainerView extends FrameLayout implements Insettable { + + protected Rect mInsets = new Rect(); + protected Rect mFixedBounds = new Rect(); + protected int mFixedBoundsContainerInset; + + public BaseContainerView(Context context) { + this(context, null); + } + + public BaseContainerView(Context context, AttributeSet attrs) { + this(context, attrs, 0); + } + + public BaseContainerView(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + mFixedBoundsContainerInset = context.getResources().getDimensionPixelSize( + R.dimen.container_fixed_bounds_inset); + } + + @Override + final public void setInsets(Rect insets) { + mInsets.set(insets); + onUpdateBackgrounds(); + onUpdatePaddings(); + } + + /** + * Sets the fixed bounds for this container view. + */ + final public void setFixedBounds(Rect fixedBounds) { + if (!fixedBounds.isEmpty() && !fixedBounds.equals(mFixedBounds)) { + mFixedBounds.set(fixedBounds); + if (Launcher.DISABLE_ALL_APPS_SEARCH_INTEGRATION) { + mFixedBounds.top = mInsets.top; + mFixedBounds.bottom = mInsets.bottom; + } + // To ensure that the child RecyclerView has the full width to handle touches right to + // the edge of the screen, we only apply the top and bottom padding to the bounds + mFixedBounds.top += mFixedBoundsContainerInset; + mFixedBounds.bottom += mFixedBoundsContainerInset; + onFixedBoundsUpdated(); + } + // Post the updates since they can trigger a relayout, and this call can be triggered from + // a layout pass itself. + post(new Runnable() { + @Override + public void run() { + onUpdateBackgrounds(); + onUpdatePaddings(); + } + }); + } + + /** + * Update the UI in response to a change in the fixed bounds. + */ + protected void onFixedBoundsUpdated() { + // Do nothing + } + + /** + * Update the paddings in response to a change in the bounds or insets. + */ + protected void onUpdatePaddings() { + // Do nothing + } + + /** + * Update the backgrounds in response to a change in the bounds or insets. + */ + protected void onUpdateBackgrounds() { + // Do nothing + } +}
\ No newline at end of file diff --git a/src/com/android/launcher3/BubbleTextView.java b/src/com/android/launcher3/BubbleTextView.java index ae6ebba34..d32c91919 100644 --- a/src/com/android/launcher3/BubbleTextView.java +++ b/src/com/android/launcher3/BubbleTextView.java @@ -28,13 +28,13 @@ import android.graphics.drawable.Drawable; import android.util.AttributeSet; import android.util.SparseArray; import android.util.TypedValue; -import android.view.Gravity; import android.view.KeyEvent; import android.view.MotionEvent; import android.view.ViewConfiguration; import android.widget.TextView; import com.android.launcher3.IconCache.IconLoadRequest; +import com.android.launcher3.widget.PackageItemInfo; /** * TextView that draws a bubble behind the text. We cannot use a LineBackgroundSpan @@ -52,13 +52,14 @@ public class BubbleTextView extends TextView { private static final int SHADOW_SMALL_COLOUR = 0xCC000000; static final float PADDING_V = 3.0f; + private static final int DISPLAY_WORKSPACE = 0; + private static final int DISPLAY_ALL_APPS = 1; + private Drawable mIcon; private final Drawable mBackground; private final CheckLongPressHelper mLongPressHelper; private final HolographicOutlineHelper mOutlineHelper; - // TODO: Remove custom background handling code, as no instance of BubbleTextView use any - // background. private boolean mBackgroundSizeChanged; private Bitmap mPressedBackground; @@ -69,8 +70,6 @@ public class BubbleTextView extends TextView { private final boolean mCustomShadowsEnabled; private final boolean mLayoutHorizontal; private final int mIconSize; - private final int mIconPaddingSize; - private final int mTextSize; private int mTextColor; private boolean mStayPressed; @@ -95,14 +94,21 @@ public class BubbleTextView extends TextView { R.styleable.BubbleTextView, defStyle, 0); mCustomShadowsEnabled = a.getBoolean(R.styleable.BubbleTextView_customShadows, true); mLayoutHorizontal = a.getBoolean(R.styleable.BubbleTextView_layoutHorizontal, false); - mIconSize = a.getDimensionPixelSize(R.styleable.BubbleTextView_iconSizeOverride, - grid.allAppsIconSizePx); - mIconPaddingSize = a.getDimensionPixelSize(R.styleable.BubbleTextView_iconPaddingOverride, - grid.iconDrawablePaddingPx); - mTextSize = a.getDimensionPixelSize(R.styleable.BubbleTextView_textSizeOverride, - grid.allAppsIconTextSizePx); mDeferShadowGenerationOnTouch = a.getBoolean(R.styleable.BubbleTextView_deferShadowGeneration, false); + + int display = a.getInteger(R.styleable.BubbleTextView_iconDisplay, DISPLAY_WORKSPACE); + int defaultIconSize = grid.iconSizePx; + if (display == DISPLAY_WORKSPACE) { + setTextSize(TypedValue.COMPLEX_UNIT_PX, grid.iconTextSizePx); + } else if (display == DISPLAY_ALL_APPS) { + setTextSize(TypedValue.COMPLEX_UNIT_PX, grid.allAppsIconTextSizePx); + defaultIconSize = grid.allAppsIconSizePx; + } + + mIconSize = a.getDimensionPixelSize(R.styleable.BubbleTextView_iconSizeOverride, + defaultIconSize); + a.recycle(); if (mCustomShadowsEnabled) { @@ -113,11 +119,6 @@ public class BubbleTextView extends TextView { mBackground = null; } - // If we are laying out horizontal, then center the text vertically - if (mLayoutHorizontal) { - setGravity(Gravity.CENTER_VERTICAL); - } - mLongPressHelper = new CheckLongPressHelper(this); mOutlineHelper = HolographicOutlineHelper.obtain(getContext()); @@ -128,26 +129,18 @@ public class BubbleTextView extends TextView { setAccessibilityDelegate(LauncherAppState.getInstance().getAccessibilityDelegate()); } - public void onFinishInflate() { - super.onFinishInflate(); - - // Ensure we are using the right text size - setTextSize(TypedValue.COMPLEX_UNIT_PX, mTextSize); - } - - public void applyFromShortcutInfo(ShortcutInfo info, IconCache iconCache, - boolean setDefaultPadding) { - applyFromShortcutInfo(info, iconCache, setDefaultPadding, false); + public void applyFromShortcutInfo(ShortcutInfo info, IconCache iconCache) { + applyFromShortcutInfo(info, iconCache, false); } public void applyFromShortcutInfo(ShortcutInfo info, IconCache iconCache, - boolean setDefaultPadding, boolean promiseStateChanged) { + boolean promiseStateChanged) { Bitmap b = info.getIcon(iconCache); FastBitmapDrawable iconDrawable = Utilities.createIconDrawable(b); iconDrawable.setGhostModeEnabled(info.isDisabled != 0); - setIcon(iconDrawable, mIconSize, setDefaultPadding ? mIconPaddingSize : -1); + setIcon(iconDrawable, mIconSize); if (info.contentDescription != null) { setContentDescription(info.contentDescription); } @@ -160,7 +153,7 @@ public class BubbleTextView extends TextView { } public void applyFromApplicationInfo(AppInfo info) { - setIcon(Utilities.createIconDrawable(info.iconBitmap), mIconSize, mIconPaddingSize); + setIcon(Utilities.createIconDrawable(info.iconBitmap), mIconSize); setText(info.title); if (info.contentDescription != null) { setContentDescription(info.contentDescription); @@ -172,6 +165,20 @@ public class BubbleTextView extends TextView { verifyHighRes(); } + public void applyFromPackageItemInfo(PackageItemInfo info) { + setIcon(Utilities.createIconDrawable(info.iconBitmap), mIconSize); + setText(info.title); + if (info.contentDescription != null) { + setContentDescription(info.contentDescription); + } + // We don't need to check the info since it's not a ShortcutInfo + super.setTag(info); + + // Verify high res immediately + verifyHighRes(); + } + + @Override protected boolean setFrame(int left, int top, int right, int bottom) { if (getLeft() != left || getRight() != right || getTop() != top || getBottom() != bottom) { @@ -409,7 +416,7 @@ public class BubbleTextView extends TextView { preloadDrawable = (PreloadIconDrawable) mIcon; } else { preloadDrawable = new PreloadIconDrawable(mIcon, getPreloaderTheme()); - setIcon(preloadDrawable, mIconSize, -1); + setIcon(preloadDrawable, mIconSize); } preloadDrawable.setLevel(progressLevel); @@ -437,7 +444,7 @@ public class BubbleTextView extends TextView { /** * Sets the icon for this view based on the layout direction. */ - private Drawable setIcon(Drawable icon, int iconSize, int drawablePadding) { + private Drawable setIcon(Drawable icon, int iconSize) { mIcon = icon; if (iconSize != -1) { mIcon.setBounds(0, 0, iconSize, iconSize); @@ -447,9 +454,6 @@ public class BubbleTextView extends TextView { } else { setCompoundDrawablesRelative(null, mIcon, null, null); } - if (drawablePadding != -1) { - setCompoundDrawablePadding(drawablePadding); - } return icon; } @@ -463,7 +467,9 @@ public class BubbleTextView extends TextView { applyFromApplicationInfo((AppInfo) info); } else if (info instanceof ShortcutInfo) { applyFromShortcutInfo((ShortcutInfo) info, - LauncherAppState.getInstance().getIconCache(), false); + LauncherAppState.getInstance().getIconCache()); + } else if (info instanceof PackageItemInfo) { + applyFromPackageItemInfo((PackageItemInfo) info); } } } @@ -488,6 +494,12 @@ public class BubbleTextView extends TextView { mIconLoadRequest = LauncherAppState.getInstance().getIconCache() .updateIconInBackground(BubbleTextView.this, info); } + } else if (getTag() instanceof PackageItemInfo) { + PackageItemInfo info = (PackageItemInfo) getTag(); + if (info.usingLowResIcon) { + mIconLoadRequest = LauncherAppState.getInstance().getIconCache() + .updateIconInBackground(BubbleTextView.this, info); + } } } } diff --git a/src/com/android/launcher3/CellLayout.java b/src/com/android/launcher3/CellLayout.java index 65c67025f..72eabf177 100644 --- a/src/com/android/launcher3/CellLayout.java +++ b/src/com/android/launcher3/CellLayout.java @@ -3051,4 +3051,21 @@ public class CellLayout extends ViewGroup { public boolean findVacantCell(int spanX, int spanY, int[] outXY) { return Utilities.findVacantCell(outXY, spanX, spanY, mCountX, mCountY, mOccupied); } + + public boolean isRegionVacant(int x, int y, int spanX, int spanY) { + int x2 = x + spanX - 1; + int y2 = y + spanY - 1; + if (x < 0 || y < 0 || x2 >= mCountX || y2 >= mCountY) { + return false; + } + for (int i = x; i <= x2; i++) { + for (int j = y; j <= y2; j++) { + if (mOccupied[i][j]) { + return false; + } + } + } + + return true; + } } diff --git a/src/com/android/launcher3/DeviceProfile.java b/src/com/android/launcher3/DeviceProfile.java index 918517ebd..3bbf0e7d8 100644 --- a/src/com/android/launcher3/DeviceProfile.java +++ b/src/com/android/launcher3/DeviceProfile.java @@ -69,8 +69,10 @@ public class DeviceProfile { String name; float minWidthDps; float minHeightDps; - public float numRows; - public float numColumns; + public int numRows; + public int numColumns; + public int numFolderRows; + public int numFolderColumns; float numHotseatIcons; float iconSize; private float iconTextSize; @@ -80,12 +82,11 @@ public class DeviceProfile { int defaultLayoutId; boolean isLandscape; - boolean isTablet; - boolean isLargeTablet; + public boolean isTablet; + public boolean isLargeTablet; public boolean isLayoutRtl; boolean transposeLayoutWithOrientation; - int desiredWorkspaceLeftRightMarginPx; public int edgeMarginPx; Rect defaultWidgetPadding; @@ -138,8 +139,9 @@ public class DeviceProfile { private ArrayList<DeviceProfileCallbacks> mCallbacks = new ArrayList<DeviceProfileCallbacks>(); - DeviceProfile(String n, float w, float h, float r, float c, - float is, float its, float hs, float his, int dlId) { + DeviceProfile(String n, float w, float h, + int r, int c, int fr, int fc, + float is, float its, float hs, float his, int dlId) { // Ensure that we have an odd number of hotseat items (since we need to place all apps) if (hs % 2 == 0) { throw new RuntimeException("All Device Profiles must have an odd number of hotseat spaces"); @@ -148,8 +150,12 @@ public class DeviceProfile { name = n; minWidthDps = w; minHeightDps = h; + numRows = r; numColumns = c; + numFolderRows = fr; + numFolderColumns = fc; + iconSize = is; iconTextSize = its; numHotseatIcons = hs; @@ -210,6 +216,9 @@ public class DeviceProfile { // Snap to the closest column count numColumns = closestProfile.numColumns; + numFolderRows = closestProfile.numFolderRows; + numFolderColumns = closestProfile.numFolderColumns; + // Snap to the closest hotseat size numHotseatIcons = closestProfile.numHotseatIcons; hotseatAllAppsRank = (int) (numHotseatIcons / 2); @@ -266,8 +275,8 @@ public class DeviceProfile { DeviceProfile partnerDp = p.getDeviceProfileOverride(dm); if (partnerDp != null) { if (partnerDp.numRows > 0 && partnerDp.numColumns > 0) { - numRows = partnerDp.numRows; - numColumns = partnerDp.numColumns; + numRows = numFolderRows = partnerDp.numRows; + numColumns = numFolderColumns = partnerDp.numColumns; } if (partnerDp.allAppsShortEdgeCount > 0 && partnerDp.allAppsLongEdgeCount > 0) { allAppsShortEdgeCount = partnerDp.allAppsShortEdgeCount; @@ -428,13 +437,6 @@ public class DeviceProfile { } public boolean updateAppsViewNumCols(Resources res, int containerWidth) { - if (AppsContainerView.GRID_HIDE_SECTION_HEADERS) { - if (appsViewNumCols != allAppsNumCols) { - appsViewNumCols = allAppsNumCols; - return true; - } - return false; - } int appsViewLeftMarginPx = res.getDimensionPixelSize(R.dimen.apps_grid_view_start_margin); int availableAppsWidthPx = (containerWidth > 0) ? containerWidth : availableWidthPx; diff --git a/src/com/android/launcher3/DragLayer.java b/src/com/android/launcher3/DragLayer.java index 91f97fa4a..2efdb06b9 100644 --- a/src/com/android/launcher3/DragLayer.java +++ b/src/com/android/launcher3/DragLayer.java @@ -38,7 +38,6 @@ import android.view.animation.Interpolator; import android.widget.FrameLayout; import android.widget.TextView; -import com.android.launcher3.InsettableFrameLayout.LayoutParams; import com.android.launcher3.util.Thunk; import java.util.ArrayList; @@ -153,6 +152,14 @@ public class DragLayer extends InsettableFrameLayout { return false; } + private boolean isEventOverDropTargetBar(MotionEvent ev) { + getDescendantRectRelativeToSelf(mLauncher.getSearchBar(), mHitRect); + if (mHitRect.contains((int) ev.getX(), (int) ev.getY())) { + return true; + } + return false; + } + public void setBlockTouch(boolean block) { mBlockTouches = block; } @@ -188,10 +195,16 @@ public class DragLayer extends InsettableFrameLayout { } } - getDescendantRectRelativeToSelf(currentFolder, hitRect); if (!isEventOverFolder(currentFolder, ev)) { - mLauncher.closeFolder(); - return true; + if (isInAccessibleDrag()) { + // Do not close the folder if in drag and drop. + if (!isEventOverDropTargetBar(ev)) { + return true; + } + } else { + mLauncher.closeFolder(); + return true; + } } } return false; @@ -228,11 +241,12 @@ public class DragLayer extends InsettableFrameLayout { getContext().getSystemService(Context.ACCESSIBILITY_SERVICE); if (accessibilityManager.isTouchExplorationEnabled()) { final int action = ev.getAction(); - boolean isOverFolder; + boolean isOverFolderOrSearchBar; switch (action) { case MotionEvent.ACTION_HOVER_ENTER: - isOverFolder = isEventOverFolder(currentFolder, ev); - if (!isOverFolder) { + isOverFolderOrSearchBar = isEventOverFolder(currentFolder, ev) || + (isInAccessibleDrag() && isEventOverDropTargetBar(ev)); + if (!isOverFolderOrSearchBar) { sendTapOutsideFolderAccessibilityEvent(currentFolder.isEditingName()); mHoverPointClosesFolder = true; return true; @@ -240,12 +254,13 @@ public class DragLayer extends InsettableFrameLayout { mHoverPointClosesFolder = false; break; case MotionEvent.ACTION_HOVER_MOVE: - isOverFolder = isEventOverFolder(currentFolder, ev); - if (!isOverFolder && !mHoverPointClosesFolder) { + isOverFolderOrSearchBar = isEventOverFolder(currentFolder, ev) || + (isInAccessibleDrag() && isEventOverDropTargetBar(ev)); + if (!isOverFolderOrSearchBar && !mHoverPointClosesFolder) { sendTapOutsideFolderAccessibilityEvent(currentFolder.isEditingName()); mHoverPointClosesFolder = true; return true; - } else if (!isOverFolder) { + } else if (!isOverFolderOrSearchBar) { return true; } mHoverPointClosesFolder = false; @@ -268,6 +283,12 @@ public class DragLayer extends InsettableFrameLayout { } } + private boolean isInAccessibleDrag() { + LauncherAccessibilityDelegate delegate = LauncherAppState + .getInstance().getAccessibilityDelegate(); + return delegate != null && delegate.isInAccessibleDrag(); + } + @Override public boolean onRequestSendAccessibilityEvent(View child, AccessibilityEvent event) { Folder currentFolder = mLauncher.getWorkspace().getOpenFolder(); @@ -275,6 +296,10 @@ public class DragLayer extends InsettableFrameLayout { if (child == currentFolder) { return super.onRequestSendAccessibilityEvent(child, event); } + + if (isInAccessibleDrag() && child instanceof SearchDropTargetBar) { + return super.onRequestSendAccessibilityEvent(child, event); + } // Skip propagating onRequestSendAccessibilityEvent all for other children // when a folder is open return false; @@ -288,6 +313,10 @@ public class DragLayer extends InsettableFrameLayout { if (currentFolder != null) { // Only add the folder as a child for accessibility when it is open childrenForAccessibility.add(currentFolder); + + if (isInAccessibleDrag()) { + childrenForAccessibility.add(mLauncher.getSearchBar()); + } } else { super.addChildrenForAccessibility(childrenForAccessibility); } diff --git a/src/com/android/launcher3/DragView.java b/src/com/android/launcher3/DragView.java index a4b6704ac..3eec3d9ee 100644 --- a/src/com/android/launcher3/DragView.java +++ b/src/com/android/launcher3/DragView.java @@ -129,6 +129,10 @@ public class DragView extends View { int ms = View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED); measure(ms, ms); mPaint = new Paint(Paint.FILTER_BITMAP_FLAG); + + if (Utilities.isLmpOrAbove()) { + setElevation(getResources().getDimension(R.dimen.drag_elevation)); + } } /** Sets the scale of the view over the normal workspace icon size. */ diff --git a/src/com/android/launcher3/DynamicGrid.java b/src/com/android/launcher3/DynamicGrid.java index 24da97fc6..d22427f44 100644 --- a/src/com/android/launcher3/DynamicGrid.java +++ b/src/com/android/launcher3/DynamicGrid.java @@ -59,30 +59,30 @@ public class DynamicGrid { DEFAULT_ICON_SIZE_PX = pxFromDp(DEFAULT_ICON_SIZE_DP, dm); // Our phone profiles include the bar sizes in each orientation deviceProfiles.add(new DeviceProfile("Super Short Stubby", - 255, 300, 2, 3, 48, 13, 3, 48, R.xml.default_workspace_4x4)); + 255, 300, 2, 3, 2, 3, 48, 13, 3, 48, R.xml.default_workspace_4x4)); deviceProfiles.add(new DeviceProfile("Shorter Stubby", - 255, 400, 3, 3, 48, 13, 3, 48, R.xml.default_workspace_4x4)); + 255, 400, 3, 3, 3, 3, 48, 13, 3, 48, R.xml.default_workspace_4x4)); deviceProfiles.add(new DeviceProfile("Short Stubby", - 275, 420, 3, 4, 48, 13, 5, 48, R.xml.default_workspace_4x4)); + 275, 420, 3, 4, 3, 4, 48, 13, 5, 48, R.xml.default_workspace_4x4)); deviceProfiles.add(new DeviceProfile("Stubby", - 255, 450, 3, 4, 48, 13, 5, 48, R.xml.default_workspace_4x4)); + 255, 450, 3, 4, 3, 4, 48, 13, 5, 48, R.xml.default_workspace_4x4)); deviceProfiles.add(new DeviceProfile("Nexus S", - 296, 491.33f, 4, 4, 48, 13, 5, 48, R.xml.default_workspace_4x4)); + 296, 491.33f, 4, 4, 4, 4, 48, 13, 5, 48, R.xml.default_workspace_4x4)); deviceProfiles.add(new DeviceProfile("Nexus 4", - 335, 567, 4, 4, DEFAULT_ICON_SIZE_DP, 13, 5, 56, R.xml.default_workspace_4x4)); + 335, 567, 4, 4, 4, 4, DEFAULT_ICON_SIZE_DP, 13, 5, 56, R.xml.default_workspace_4x4)); deviceProfiles.add(new DeviceProfile("Nexus 5", - 359, 567, 4, 4, DEFAULT_ICON_SIZE_DP, 13, 5, 56, R.xml.default_workspace_4x4)); + 359, 567, 4, 4, 4, 4, DEFAULT_ICON_SIZE_DP, 13, 5, 56, R.xml.default_workspace_4x4)); deviceProfiles.add(new DeviceProfile("Large Phone", - 406, 694, 5, 5, 64, 14.4f, 5, 56, R.xml.default_workspace_5x5)); + 406, 694, 5, 5, 4, 4, 64, 14.4f, 5, 56, R.xml.default_workspace_5x5)); // The tablet profile is odd in that the landscape orientation // also includes the nav bar on the side deviceProfiles.add(new DeviceProfile("Nexus 7", - 575, 904, 5, 6, 72, 14.4f, 7, 60, R.xml.default_workspace_5x6)); + 575, 904, 5, 6, 4, 5, 72, 14.4f, 7, 60, R.xml.default_workspace_5x6)); // Larger tablet profiles always have system bars on the top & bottom deviceProfiles.add(new DeviceProfile("Nexus 10", - 727, 1207, 5, 6, 76, 14.4f, 7, 64, R.xml.default_workspace_5x6)); + 727, 1207, 5, 6, 4, 5, 76, 14.4f, 7, 64, R.xml.default_workspace_5x6)); deviceProfiles.add(new DeviceProfile("20-inch Tablet", - 1527, 2527, 7, 7, 100, 20, 7, 72, R.xml.default_workspace_4x4)); + 1527, 2527, 7, 7, 6, 6, 100, 20, 7, 72, R.xml.default_workspace_4x4)); mMinWidth = dpiFromPx(minWidthPx, dm); mMinHeight = dpiFromPx(minHeightPx, dm); mProfile = new DeviceProfile(context, deviceProfiles, diff --git a/src/com/android/launcher3/Folder.java b/src/com/android/launcher3/Folder.java index f2d7a6991..377e8eeff 100644 --- a/src/com/android/launcher3/Folder.java +++ b/src/com/android/launcher3/Folder.java @@ -27,6 +27,7 @@ import android.content.res.Resources; import android.graphics.Point; import android.graphics.PointF; import android.graphics.Rect; +import android.graphics.drawable.InsetDrawable; import android.os.Build; import android.text.InputType; import android.text.Selection; @@ -182,6 +183,15 @@ public class Folder extends LinearLayout implements DragSource, View.OnClickList // name is complete, we have something to focus on, thus hiding the cursor and giving // reliable behavior when clicking the text field (since it will always gain focus on click). setFocusableInTouchMode(true); + + if (Utilities.isLmpOrAbove()) { + int padding = getResources().getDimensionPixelSize(R.dimen.folder_shadow_padding); + setBackground(new InsetDrawable( + getResources().getDrawable(R.drawable.apps_list_bg), + padding, padding, padding, padding)); + } else { + setBackgroundResource(R.drawable.quantum_panel); + } } @Override @@ -274,6 +284,9 @@ public class Folder extends LinearLayout implements DragSource, View.OnClickList for (int i = 0; i < mContent.getChildCount(); i++) { mContent.getPageAt(i).enableAccessibleDrag(enable, CellLayout.FOLDER_ACCESSIBILITY_DRAG); } + + mFooter.setImportantForAccessibility(enable ? IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS : + IMPORTANT_FOR_ACCESSIBILITY_AUTO); mLauncher.getWorkspace().setAddNewPageOnDrag(!enable); } @@ -295,13 +308,14 @@ public class Folder extends LinearLayout implements DragSource, View.OnClickList mFolderName.setHint(sHintText); // Convert to a string here to ensure that no other state associated with the text field // gets saved. - String newTitle = mFolderName.getText().toString(); + CharSequence newTitle = mFolderName.getText(); mInfo.setTitle(newTitle); LauncherModel.updateItemInDatabase(mLauncher, mInfo); if (commit) { sendCustomAccessibilityEvent(AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED, - String.format(getContext().getString(R.string.folder_renamed), newTitle)); + String.format(getContext().getString(R.string.folder_renamed), + newTitle.toString())); } // In order to clear the focus from the text field, we set the focus on ourself. This // ensures that every time the field is clicked, focus is gained, giving reliable behavior. @@ -469,9 +483,15 @@ public class Folder extends LinearLayout implements DragSource, View.OnClickList PropertyValuesHolder tx = PropertyValuesHolder.ofFloat("translationX", transX, 0); PropertyValuesHolder ty = PropertyValuesHolder.ofFloat("translationY", transY, 0); + Animator drift = LauncherAnimUtils.ofPropertyValuesHolder(this, tx, ty); + drift.setDuration(mMaterialExpandDuration); + drift.setStartDelay(mMaterialExpandStagger); + drift.setInterpolator(new LogDecelerateInterpolator(100, 0)); + int rx = (int) Math.max(Math.max(width - getPivotX(), 0), getPivotX()); int ry = (int) Math.max(Math.max(height - getPivotY(), 0), getPivotY()); float radius = (float) Math.hypot(rx, ry); + AnimatorSet anim = LauncherAnimUtils.createAnimatorSet(); Animator reveal = LauncherAnimUtils.createCircularReveal(this, (int) getPivotX(), (int) getPivotY(), 0, radius); @@ -490,10 +510,6 @@ public class Folder extends LinearLayout implements DragSource, View.OnClickList textAlpha.setStartDelay(mMaterialExpandStagger); textAlpha.setInterpolator(new AccelerateInterpolator(1.5f)); - Animator drift = LauncherAnimUtils.ofPropertyValuesHolder(this, tx, ty); - drift.setDuration(mMaterialExpandDuration); - drift.setStartDelay(mMaterialExpandStagger); - drift.setInterpolator(new LogDecelerateInterpolator(60, 0)); anim.play(drift); anim.play(iconsAlpha); @@ -503,10 +519,12 @@ public class Folder extends LinearLayout implements DragSource, View.OnClickList openFolderAnim = anim; mContentWrapper.setLayerType(LAYER_TYPE_HARDWARE, null); + mFooter.setLayerType(LAYER_TYPE_HARDWARE, null); onCompleteRunnable = new Runnable() { @Override public void run() { mContentWrapper.setLayerType(LAYER_TYPE_NONE, null); + mContentWrapper.setLayerType(LAYER_TYPE_NONE, null); } }; } @@ -1095,8 +1113,7 @@ public class Folder extends LinearLayout implements DragSource, View.OnClickList // Move the item from the folder to the workspace, in the position of the folder if (getItemCount() == 1) { ShortcutInfo finalItem = mInfo.contents.get(0); - child = mLauncher.createShortcut(R.layout.application, cellLayout, - finalItem); + child = mLauncher.createShortcut(cellLayout, finalItem); LauncherModel.addOrMoveItemInDatabase(mLauncher, finalItem, mInfo.container, mInfo.screenId, mInfo.cellX, mInfo.cellY); } diff --git a/src/com/android/launcher3/FolderIcon.java b/src/com/android/launcher3/FolderIcon.java index f5836c295..b161b1cd3 100644 --- a/src/com/android/launcher3/FolderIcon.java +++ b/src/com/android/launcher3/FolderIcon.java @@ -710,7 +710,7 @@ public class FolderIcon extends FrameLayout implements FolderListener { } public void onTitleChanged(CharSequence title) { - mFolderName.setText(title.toString()); + mFolderName.setText(title); setContentDescription(String.format(getContext().getString(R.string.folder_name_format), title)); } diff --git a/src/com/android/launcher3/FolderInfo.java b/src/com/android/launcher3/FolderInfo.java index aea21c95b..930f91103 100644 --- a/src/com/android/launcher3/FolderInfo.java +++ b/src/com/android/launcher3/FolderInfo.java @@ -92,7 +92,7 @@ public class FolderInfo extends ItemInfo { } public void setTitle(CharSequence title) { - this.title = title; + this.title = Utilities.trim(title); for (int i = 0; i < listeners.size(); i++) { listeners.get(i).onTitleChanged(title); } diff --git a/src/com/android/launcher3/FolderPagedView.java b/src/com/android/launcher3/FolderPagedView.java index f070a6bba..a6494d274 100644 --- a/src/com/android/launcher3/FolderPagedView.java +++ b/src/com/android/launcher3/FolderPagedView.java @@ -55,9 +55,6 @@ public class FolderPagedView extends PagedView { private static final int[] sTempPosArray = new int[2]; - // TODO: Remove this restriction - private static final int MAX_ITEMS_PER_PAGE = 4; - public final boolean rtlLayout; private final LayoutInflater mInflater; @@ -84,13 +81,8 @@ public class FolderPagedView extends PagedView { LauncherAppState app = LauncherAppState.getInstance(); DeviceProfile grid = app.getDynamicGrid().getDeviceProfile(); - if (ALLOW_FOLDER_SCROLL) { - mMaxCountX = Math.min((int) grid.numColumns, MAX_ITEMS_PER_PAGE); - mMaxCountY = Math.min((int) grid.numRows, MAX_ITEMS_PER_PAGE); - } else { - mMaxCountX = (int) grid.numColumns; - mMaxCountY = (int) grid.numRows; - } + mMaxCountX = (int) grid.numFolderColumns; + mMaxCountY = (int) grid.numFolderRows; mMaxItemsPerPage = mMaxCountX * mMaxCountY; @@ -210,7 +202,7 @@ public class FolderPagedView extends PagedView { public View createNewView(ShortcutInfo item) { final BubbleTextView textView = (BubbleTextView) mInflater.inflate( R.layout.folder_application, null, false); - textView.applyFromShortcutInfo(item, mIconCache, false); + textView.applyFromShortcutInfo(item, mIconCache); textView.setOnClickListener(mFolder); textView.setOnLongClickListener(mFolder); textView.setOnFocusChangeListener(mFocusIndicatorView); @@ -505,6 +497,10 @@ public class FolderPagedView extends PagedView { } } + public int getAllocatedContentSize() { + return mAllocatedContentSize; + } + /** * Reorders the items such that the {@param empty} spot moves to {@param target} */ diff --git a/src/com/android/launcher3/IconCache.java b/src/com/android/launcher3/IconCache.java index 6c2aa397d..fff07c6ed 100644 --- a/src/com/android/launcher3/IconCache.java +++ b/src/com/android/launcher3/IconCache.java @@ -374,6 +374,9 @@ public class IconCache { getTitleAndIcon(st, st.promisedIntent != null ? st.promisedIntent : st.intent, st.user, false); + } else if (info instanceof PackageItemInfo) { + PackageItemInfo pti = (PackageItemInfo) info; + getTitleAndIconForApp(pti.packageName, pti.user, false, pti); } mMainThreadExecutor.execute(new Runnable() { @@ -400,7 +403,7 @@ public class IconCache { UserHandleCompat user = info == null ? application.user : info.getUser(); CacheEntry entry = cacheLocked(application.componentName, info, user, false, useLowResIcon); - application.title = entry.title; + application.title = Utilities.trim(entry.title); application.iconBitmap = getNonNullIcon(entry, user); application.contentDescription = entry.contentDescription; application.usingLowResIcon = entry.isLowResIcon; @@ -413,7 +416,7 @@ public class IconCache { CacheEntry entry = cacheLocked(application.componentName, null, application.user, false, application.usingLowResIcon); if (entry.icon != null && !isDefaultIcon(entry.icon, application.user)) { - application.title = entry.title; + application.title = Utilities.trim(entry.title); application.iconBitmap = entry.icon; application.contentDescription = entry.contentDescription; application.usingLowResIcon = entry.isLowResIcon; @@ -464,7 +467,7 @@ public class IconCache { UserHandleCompat user, boolean usePkgIcon, boolean useLowResIcon) { CacheEntry entry = cacheLocked(component, info, user, usePkgIcon, useLowResIcon); shortcutInfo.setIcon(getNonNullIcon(entry, user)); - shortcutInfo.title = entry.title; + shortcutInfo.title = Utilities.trim(entry.title); shortcutInfo.usingFallbackIcon = isDefaultIcon(entry.icon, user); shortcutInfo.usingLowResIcon = entry.isLowResIcon; } @@ -477,7 +480,7 @@ public class IconCache { PackageItemInfo infoOut) { CacheEntry entry = getEntryForPackageLocked(packageName, user, useLowResIcon); infoOut.iconBitmap = getNonNullIcon(entry, user); - infoOut.title = entry.title; + infoOut.title = Utilities.trim(entry.title); infoOut.usingLowResIcon = entry.isLowResIcon; infoOut.contentDescription = entry.contentDescription; } @@ -530,7 +533,7 @@ public class IconCache { } if (TextUtils.isEmpty(entry.title) && info != null) { - entry.title = info.getLabel().toString(); + entry.title = info.getLabel(); entry.contentDescription = mUserManager.getBadgedLabelForUser(entry.title, user); } } diff --git a/src/com/android/launcher3/InstallShortcutReceiver.java b/src/com/android/launcher3/InstallShortcutReceiver.java index 23bcc8577..115598f7b 100644 --- a/src/com/android/launcher3/InstallShortcutReceiver.java +++ b/src/com/android/launcher3/InstallShortcutReceiver.java @@ -247,7 +247,7 @@ public class InstallShortcutReceiver extends BroadcastReceiver { try { PackageManager pm = context.getPackageManager(); ActivityInfo info = pm.getActivityInfo(intent.getComponent(), 0); - name = info.loadLabel(pm).toString(); + name = info.loadLabel(pm); } catch (PackageManager.NameNotFoundException nnfe) { return ""; } diff --git a/src/com/android/launcher3/Launcher.java b/src/com/android/launcher3/Launcher.java index 1de383c65..8603a35df 100644 --- a/src/com/android/launcher3/Launcher.java +++ b/src/com/android/launcher3/Launcher.java @@ -64,6 +64,7 @@ import android.os.Handler; import android.os.Message; import android.os.StrictMode; import android.os.SystemClock; +import android.preference.PreferenceManager; import android.text.Selection; import android.text.SpannableStringBuilder; import android.text.TextUtils; @@ -347,6 +348,11 @@ public class Launcher extends Activity private Canvas mFolderIconCanvas; private Rect mRectForFolderAnimation = new Rect(); + private DeviceProfile mDeviceProfile; + + // This is set to the view that launched the activity that navigated the user away from + // launcher. Since there is no callback for when the activity has finished launching, enable + // the press state and keep this reference to reset the press state when we return to launcher. private BubbleTextView mWaitingForResume; protected static HashMap<String, CustomAppWidget> sCustomAppWidgets = @@ -423,7 +429,7 @@ public class Launcher extends Activity LauncherAppState.getLauncherProvider().setLauncherProviderChangeListener(this); // Lazy-initialize the dynamic grid - DeviceProfile grid = app.initDynamicGrid(this); + mDeviceProfile = app.initDynamicGrid(this); // the LauncherApplication should call this, but in case of Instrumentation it might not be present yet mSharedPrefs = getSharedPreferences(LauncherAppState.getSharedPreferencesKey(), @@ -431,7 +437,7 @@ public class Launcher extends Activity mIsSafeModeEnabled = getPackageManager().isSafeMode(); mModel = app.setLauncher(this); mIconCache = app.getIconCache(); - mIconCache.flushInvalidIcons(grid); + mIconCache.flushInvalidIcons(mDeviceProfile); mDragController = new DragController(this); mInflater = getLayoutInflater(); mStateTransitionAnimation = new LauncherStateTransitionAnimation(this, this); @@ -457,7 +463,7 @@ public class Launcher extends Activity setContentView(R.layout.launcher); setupViews(); - grid.layout(this); + mDeviceProfile.layout(this); registerContentObservers(); @@ -528,7 +534,8 @@ public class Launcher extends Activity if (LOGD) { Log.d(TAG, "onAllAppsBoundsChanged(Rect): " + bounds); } - mAppsView.setFixedBounds(Launcher.this, bounds); + mAppsView.setFixedBounds(bounds); + mWidgetsView.setFixedBounds(bounds); } @Override @@ -1017,16 +1024,24 @@ public class Launcher extends Activity if (mOnResumeState == State.WORKSPACE) { showWorkspace(false); } else if (mOnResumeState == State.APPS) { - showAppsView(false /* animated */, false /* resetListToTop */); + boolean launchedFromApp = (mWaitingForResume != null); + // Don't update the predicted apps if the user is returning to launcher in the apps + // view after launching an app, as they may be depending on the UI to be static to + // switch to another app, otherwise, if it was + showAppsView(false /* animated */, false /* resetListToTop */, + !launchedFromApp /* updatePredictedApps */); } else if (mOnResumeState == State.WIDGETS) { showWidgetsView(false, false); } mOnResumeState = State.NONE; // Restore the apps state if we are in all apps - if (!Launcher.DISABLE_ALL_APPS_SEARCH_INTEGRATION && mState == State.APPS) { - if (mLauncherCallbacks != null) { - mLauncherCallbacks.onAllAppsShown(); + if (!Launcher.DISABLE_ALL_APPS_SEARCH_INTEGRATION) { + // Otherwise, notify the callbacks if we are in all apps mode + if (mState == State.APPS) { + if (mLauncherCallbacks != null) { + mLauncherCallbacks.onAllAppsShown(); + } } } @@ -1362,7 +1377,9 @@ public class Launcher extends Activity mPendingAddInfo.spanY = savedState.getInt(RUNTIME_STATE_PENDING_ADD_SPAN_Y); AppWidgetProviderInfo info = savedState.getParcelable( RUNTIME_STATE_PENDING_ADD_WIDGET_INFO); - mPendingAddWidgetInfo = LauncherAppWidgetProviderInfo.fromProviderInfo(this, info); + mPendingAddWidgetInfo = info == null ? + null : LauncherAppWidgetProviderInfo.fromProviderInfo(this, info); + mPendingAddWidgetId = savedState.getInt(RUNTIME_STATE_PENDING_ADD_WIDGET_ID); setWaitingForResult(true); mRestoring = true; @@ -1510,22 +1527,22 @@ public class Launcher extends Activity * @return A View inflated from R.layout.application. */ View createShortcut(ShortcutInfo info) { - return createShortcut(R.layout.application, - (ViewGroup) mWorkspace.getChildAt(mWorkspace.getCurrentPage()), info); + return createShortcut((ViewGroup) mWorkspace.getChildAt(mWorkspace.getCurrentPage()), info); } /** * Creates a view representing a shortcut inflated from the specified resource. * - * @param layoutResId The id of the XML layout used to create the shortcut. * @param parent The group the shortcut belongs to. * @param info The data structure describing the shortcut. * * @return A View inflated from layoutResId. */ - public View createShortcut(int layoutResId, ViewGroup parent, ShortcutInfo info) { - BubbleTextView favorite = (BubbleTextView) mInflater.inflate(layoutResId, parent, false); - favorite.applyFromShortcutInfo(info, mIconCache, true); + public View createShortcut(ViewGroup parent, ShortcutInfo info) { + BubbleTextView favorite = (BubbleTextView) mInflater.inflate(R.layout.application, + parent, false); + favorite.applyFromShortcutInfo(info, mIconCache); + favorite.setCompoundDrawablePadding(mDeviceProfile.iconDrawablePaddingPx); favorite.setOnClickListener(this); favorite.setOnFocusChangeListener(mFocusHandler); return favorite; @@ -2618,7 +2635,9 @@ public class Launcher extends Activity if (isAppsViewVisible()) { showWorkspace(true); } else { - showAppsView(true /* animated */, false /* resetListToTop */); + // Try and refresh the set of predicted apps before we enter launcher + showAppsView(true /* animated */, false /* resetListToTop */, + true /* updatePredictedApps */); } } @@ -3396,10 +3415,13 @@ public class Launcher extends Activity /** * Shows the apps view. */ - void showAppsView(boolean animated, boolean resetListToTop) { + void showAppsView(boolean animated, boolean resetListToTop, boolean updatePredictedApps) { if (resetListToTop) { mAppsView.scrollToTop(); } + if (updatePredictedApps) { + tryAndUpdatePredictedApps(); + } showAppsOrWidgets(animated, State.APPS); } @@ -3508,12 +3530,26 @@ public class Launcher extends Activity void exitSpringLoadedDragMode() { if (mState == State.APPS_SPRING_LOADED) { - showAppsView(true, false); + showAppsView(true /* animated */, false /* resetListToTop */, + false /* updatePredictedApps */); } else if (mState == State.WIDGETS_SPRING_LOADED) { showWidgetsView(true, false); } } + /** + * Updates the set of predicted apps if it hasn't been updated since the last time Launcher was + * resumed. + */ + private void tryAndUpdatePredictedApps() { + if (mLauncherCallbacks != null) { + List<ComponentName> apps = mLauncherCallbacks.getPredictedApps(); + if (!apps.isEmpty()) { + mAppsView.setPredictedApps(apps); + } + } + } + void lockAllApps() { // TODO } @@ -4156,13 +4192,11 @@ public class Launcher extends Activity } public boolean useVerticalBarLayout() { - return LauncherAppState.getInstance().getDynamicGrid(). - getDeviceProfile().isVerticalBarLayout(); + return mDeviceProfile.isVerticalBarLayout(); } protected Rect getSearchBarBounds() { - return LauncherAppState.getInstance().getDynamicGrid(). - getDeviceProfile().getSearchBarBounds(); + return mDeviceProfile.getSearchBarBounds(); } public void bindSearchablesChanged() { diff --git a/src/com/android/launcher3/LauncherAccessibilityDelegate.java b/src/com/android/launcher3/LauncherAccessibilityDelegate.java index 8a9a0508c..3992e6390 100644 --- a/src/com/android/launcher3/LauncherAccessibilityDelegate.java +++ b/src/com/android/launcher3/LauncherAccessibilityDelegate.java @@ -1,6 +1,9 @@ package com.android.launcher3; import android.annotation.TargetApi; +import android.app.AlertDialog; +import android.appwidget.AppWidgetProviderInfo; +import android.content.DialogInterface; import android.graphics.Rect; import android.os.Build; import android.os.Bundle; @@ -28,6 +31,7 @@ public class LauncherAccessibilityDelegate extends AccessibilityDelegate { private static final int ADD_TO_WORKSPACE = R.id.action_add_to_workspace; private static final int MOVE = R.id.action_move; private static final int MOVE_TO_WORKSPACE = R.id.action_move_to_workspace; + private static final int RESIZE = R.id.action_resize; public enum DragType { ICON, @@ -62,6 +66,8 @@ public class LauncherAccessibilityDelegate extends AccessibilityDelegate { launcher.getText(R.string.action_move))); mActions.put(MOVE_TO_WORKSPACE, new AccessibilityAction(MOVE_TO_WORKSPACE, launcher.getText(R.string.action_move_to_workspace))); + mActions.put(RESIZE, new AccessibilityAction(RESIZE, + launcher.getText(R.string.action_resize))); } @Override @@ -87,6 +93,10 @@ public class LauncherAccessibilityDelegate extends AccessibilityDelegate { if (item.container >= 0) { info.addAction(mActions.get(MOVE_TO_WORKSPACE)); + } else if (item instanceof LauncherAppWidgetInfo) { + if (!getSupportedResizeActions(host, (LauncherAppWidgetInfo) item).isEmpty()) { + info.addAction(mActions.get(RESIZE)); + } } } if ((item instanceof AppInfo) || (item instanceof PendingAddItemInfo)) { info.addAction(mActions.get(ADD_TO_WORKSPACE)); @@ -102,7 +112,7 @@ public class LauncherAccessibilityDelegate extends AccessibilityDelegate { return super.performAccessibilityAction(host, action, args); } - public boolean performAction(View host, final ItemInfo item, int action) { + public boolean performAction(final View host, final ItemInfo item, int action) { if (action == REMOVE) { if (DeleteDropTarget.removeWorkspaceOrFolderItem(mLauncher, item, host)) { announceConfirmation(R.string.item_removed); @@ -167,10 +177,97 @@ public class LauncherAccessibilityDelegate extends AccessibilityDelegate { announceConfirmation(R.string.item_moved); } }); + } else if (action == RESIZE) { + final LauncherAppWidgetInfo info = (LauncherAppWidgetInfo) item; + final ArrayList<Integer> actions = getSupportedResizeActions(host, info); + CharSequence[] labels = new CharSequence[actions.size()]; + for (int i = 0; i < actions.size(); i++) { + labels[i] = mLauncher.getText(actions.get(i)); + } + + new AlertDialog.Builder(mLauncher) + .setTitle(R.string.action_resize) + .setItems(labels, new DialogInterface.OnClickListener() { + + @Override + public void onClick(DialogInterface dialog, int which) { + performResizeAction(actions.get(which), host, info); + dialog.dismiss(); + } + }) + .show(); } return false; } + private ArrayList<Integer> getSupportedResizeActions(View host, LauncherAppWidgetInfo info) { + AppWidgetProviderInfo providerInfo = ((LauncherAppWidgetHostView) host).getAppWidgetInfo(); + ArrayList<Integer> actions = new ArrayList<>(); + + CellLayout layout = (CellLayout) host.getParent().getParent(); + if ((providerInfo.resizeMode & AppWidgetProviderInfo.RESIZE_HORIZONTAL) != 0) { + if (layout.isRegionVacant(info.cellX + info.spanX, info.cellY, 1, info.spanY) || + layout.isRegionVacant(info.cellX - 1, info.cellY, 1, info.spanY)) { + actions.add(R.string.action_increase_width); + } + + if (info.spanX > info.minSpanX && info.spanX > 1) { + actions.add(R.string.action_decrease_width); + } + } + + if ((providerInfo.resizeMode & AppWidgetProviderInfo.RESIZE_VERTICAL) != 0) { + if (layout.isRegionVacant(info.cellX, info.cellY + info.spanY, info.spanX, 1) || + layout.isRegionVacant(info.cellX, info.cellY - 1, info.spanX, 1)) { + actions.add(R.string.action_increase_height); + } + + if (info.spanY > info.minSpanY && info.spanY > 1) { + actions.add(R.string.action_decrease_height); + } + } + return actions; + } + + private void performResizeAction(int action, View host, LauncherAppWidgetInfo info) { + CellLayout.LayoutParams lp = (CellLayout.LayoutParams) host.getLayoutParams(); + CellLayout layout = (CellLayout) host.getParent().getParent(); + layout.markCellsAsUnoccupiedForView(host); + + if (action == R.string.action_increase_width) { + if (((host.getLayoutDirection() == View.LAYOUT_DIRECTION_RTL) + && layout.isRegionVacant(info.cellX - 1, info.cellY, 1, info.spanY)) + || !layout.isRegionVacant(info.cellX + info.spanX, info.cellY, 1, info.spanY)) { + lp.cellX --; + info.cellX --; + } + lp.cellHSpan ++; + info.spanX ++; + } else if (action == R.string.action_decrease_width) { + lp.cellHSpan --; + info.spanX --; + } else if (action == R.string.action_increase_height) { + if (!layout.isRegionVacant(info.cellX, info.cellY + info.spanY, info.spanX, 1)) { + lp.cellY --; + info.cellY --; + } + lp.cellVSpan ++; + info.spanY ++; + } else if (action == R.string.action_decrease_height) { + lp.cellVSpan --; + info.spanY --; + } + + layout.markCellsAsOccupiedForView(host); + Rect sizeRange = new Rect(); + AppWidgetResizeFrame.getWidgetSizeRanges(mLauncher, info.spanX, info.spanY, sizeRange); + ((LauncherAppWidgetHostView) host).updateAppWidgetSize(null, + sizeRange.left, sizeRange.top, sizeRange.right, sizeRange.bottom); + host.requestLayout(); + LauncherModel.updateItemInDatabase(mLauncher, info); + announceConfirmation(mLauncher.getString(R.string.widget_resized, info.spanX, info.spanY)); + } + @Thunk void announceConfirmation(int resId) { announceConfirmation(mLauncher.getResources().getString(resId)); } diff --git a/src/com/android/launcher3/LauncherAppWidgetProviderInfo.java b/src/com/android/launcher3/LauncherAppWidgetProviderInfo.java index bb4580ce7..af680f247 100644 --- a/src/com/android/launcher3/LauncherAppWidgetProviderInfo.java +++ b/src/com/android/launcher3/LauncherAppWidgetProviderInfo.java @@ -66,7 +66,7 @@ public class LauncherAppWidgetProviderInfo extends AppWidgetProviderInfo { public String getLabel(PackageManager packageManager) { if (isCustomWidget) { - return label.toString().trim(); + return Utilities.trim(label); } return super.loadLabel(packageManager); } diff --git a/src/com/android/launcher3/LauncherCallbacks.java b/src/com/android/launcher3/LauncherCallbacks.java index 25c86c977..0124d1f28 100644 --- a/src/com/android/launcher3/LauncherCallbacks.java +++ b/src/com/android/launcher3/LauncherCallbacks.java @@ -11,6 +11,7 @@ import android.view.ViewGroup; import java.io.FileDescriptor; import java.io.PrintWriter; import java.util.ArrayList; +import java.util.List; /** * LauncherCallbacks is an interface used to extend the Launcher activity. It includes many hooks @@ -90,6 +91,7 @@ public interface LauncherCallbacks { public boolean overrideWallpaperDimensions(); public boolean isLauncherPreinstalled(); public boolean overrideAllAppsSearch(); + public List<ComponentName> getPredictedApps(); /** * Returning true will immediately result in a call to {@link #setLauncherOverlayView(ViewGroup, diff --git a/src/com/android/launcher3/LauncherExtension.java b/src/com/android/launcher3/LauncherExtension.java index 8174af045..14ad6016c 100644 --- a/src/com/android/launcher3/LauncherExtension.java +++ b/src/com/android/launcher3/LauncherExtension.java @@ -15,6 +15,7 @@ import android.view.ViewGroup; import java.io.FileDescriptor; import java.io.PrintWriter; import java.util.ArrayList; +import java.util.List; /** * This class represents a very trivial LauncherExtension. It primarily serves as a simple @@ -259,6 +260,11 @@ public class LauncherExtension extends Launcher { } @Override + public List<ComponentName> getPredictedApps() { + return new ArrayList<>(); + } + + @Override public boolean isLauncherPreinstalled() { return false; } diff --git a/src/com/android/launcher3/LauncherModel.java b/src/com/android/launcher3/LauncherModel.java index 7efdf3284..658a3e287 100644 --- a/src/com/android/launcher3/LauncherModel.java +++ b/src/com/android/launcher3/LauncherModel.java @@ -969,7 +969,7 @@ public class LauncherModel extends BroadcastReceiver break; } - folderInfo.title = c.getString(titleIndex); + folderInfo.title = Utilities.trim(c.getString(titleIndex)); folderInfo.id = id; folderInfo.container = c.getInt(containerIndex); folderInfo.screenId = c.getInt(screenIndex); @@ -2144,7 +2144,7 @@ public class LauncherModel extends BroadcastReceiver id = c.getLong(idIndex); FolderInfo folderInfo = findOrMakeFolder(sBgFolders, id); - folderInfo.title = c.getString(titleIndex); + folderInfo.title = Utilities.trim(c.getString(titleIndex)); folderInfo.id = id; container = c.getInt(containerIndex); folderInfo.container = container; @@ -2331,6 +2331,22 @@ public class LauncherModel extends BroadcastReceiver return; } + // Remove any empty folder + LongArrayMap<FolderInfo> emptyFolders = sBgFolders.clone(); + for (ItemInfo item: sBgItemsIdMap) { + long container = item.container; + if (emptyFolders.containsKey(container)) { + emptyFolders.remove(container); + } + } + for (FolderInfo folder : emptyFolders) { + long folderId = folder.id; + sBgFolders.remove(folderId); + sBgItemsIdMap.remove(folderId); + sBgWorkspaceItems.remove(folder); + itemsToRemove.add(folderId); + } + if (itemsToRemove.size() > 0) { ContentProviderClient client = contentResolver.acquireContentProviderClient( contentUri); @@ -2810,6 +2826,8 @@ public class LauncherModel extends BroadcastReceiver } else { mHandler.post(r); } + loadAndBindWidgetsAndShortcuts(mApp.getContext(), tryGetCallbacks(oldCallbacks), + false /* refresh */); } private void loadAllApps() { @@ -2871,8 +2889,6 @@ public class LauncherModel extends BroadcastReceiver final Callbacks callbacks = tryGetCallbacks(oldCallbacks); if (callbacks != null) { callbacks.bindAllApplications(added); - loadAndBindWidgetsAndShortcuts(mApp.getContext(), callbacks, - true /* refresh */); if (DEBUG_LOADERS) { Log.d(TAG, "bound " + added.size() + " apps in " + (SystemClock.uptimeMillis() - bindTime) + "ms"); @@ -2885,6 +2901,8 @@ public class LauncherModel extends BroadcastReceiver // Cleanup any data stored for a deleted user. ManagedProfileHeuristic.processAllUsers(profiles, mContext); + loadAndBindWidgetsAndShortcuts(mApp.getContext(), tryGetCallbacks(oldCallbacks), + true /* refresh */); if (DEBUG_LOADERS) { Log.d(TAG, "Icons processed in " + (SystemClock.uptimeMillis() - loadTime) + "ms"); @@ -3197,7 +3215,7 @@ public class LauncherModel extends BroadcastReceiver if (appInfo != null && Intent.ACTION_MAIN.equals(si.intent.getAction()) && si.itemType == LauncherSettings.Favorites.ITEM_TYPE_APPLICATION) { si.updateIcon(mIconCache); - si.title = appInfo.title.toString(); + si.title = Utilities.trim(appInfo.title); si.contentDescription = appInfo.contentDescription; infoUpdated = true; } @@ -3426,18 +3444,17 @@ public class LauncherModel extends BroadcastReceiver if ((promiseType & ShortcutInfo.FLAG_RESTORED_ICON) != 0) { String title = (cursor != null) ? cursor.getString(titleIndex) : null; if (!TextUtils.isEmpty(title)) { - info.title = title; + info.title = Utilities.trim(title); } } else if ((promiseType & ShortcutInfo.FLAG_AUTOINTALL_ICON) != 0) { if (TextUtils.isEmpty(info.title)) { - info.title = (cursor != null) ? cursor.getString(titleIndex) : ""; + info.title = (cursor != null) ? Utilities.trim(cursor.getString(titleIndex)) : ""; } } else { throw new InvalidParameterException("Invalid restoreType " + promiseType); } - info.contentDescription = mUserManager.getBadgedLabelForUser( - info.title.toString(), info.user); + info.contentDescription = mUserManager.getBadgedLabelForUser(info.title, info.user); info.itemType = LauncherSettings.Favorites.ITEM_TYPE_SHORTCUT; info.promisedIntent = intent; info.status = promiseType; @@ -3499,7 +3516,7 @@ public class LauncherModel extends BroadcastReceiver // from the db if (TextUtils.isEmpty(info.title) && c != null) { - info.title = c.getString(titleIndex); + info.title = Utilities.trim(c.getString(titleIndex)); } // fall back to the class name of the activity @@ -3509,8 +3526,7 @@ public class LauncherModel extends BroadcastReceiver info.itemType = LauncherSettings.Favorites.ITEM_TYPE_APPLICATION; info.user = user; - info.contentDescription = mUserManager.getBadgedLabelForUser( - info.title.toString(), info.user); + info.contentDescription = mUserManager.getBadgedLabelForUser(info.title, info.user); if (lai != null) { info.flags = AppInfo.initFlags(lai); } @@ -3576,7 +3592,7 @@ public class LauncherModel extends BroadcastReceiver // TODO: If there's an explicit component and we can't install that, delete it. - info.title = c.getString(titleIndex); + info.title = Utilities.trim(c.getString(titleIndex)); int iconType = c.getInt(iconTypeIndex); switch (iconType) { @@ -3654,9 +3670,8 @@ public class LauncherModel extends BroadcastReceiver } info.setIcon(icon); - info.title = name; - info.contentDescription = mUserManager.getBadgedLabelForUser( - info.title.toString(), info.user); + info.title = Utilities.trim(name); + info.contentDescription = mUserManager.getBadgedLabelForUser(info.title, info.user); info.intent = intent; info.customIcon = customIcon; info.iconResource = iconResource; @@ -3697,16 +3712,16 @@ public class LauncherModel extends BroadcastReceiver labelA = mLabelCache.get(a); } else { labelA = (a instanceof LauncherAppWidgetProviderInfo) - ? mManager.loadLabel((LauncherAppWidgetProviderInfo) a) - : ((ResolveInfo) a).loadLabel(mPackageManager).toString().trim(); + ? Utilities.trim(mManager.loadLabel((LauncherAppWidgetProviderInfo) a)) + : Utilities.trim(((ResolveInfo) a).loadLabel(mPackageManager)); mLabelCache.put(a, labelA); } if (mLabelCache.containsKey(b)) { labelB = mLabelCache.get(b); } else { labelB = (b instanceof LauncherAppWidgetProviderInfo) - ? mManager.loadLabel((LauncherAppWidgetProviderInfo) b) - : ((ResolveInfo) b).loadLabel(mPackageManager).toString().trim(); + ? Utilities.trim(mManager.loadLabel((LauncherAppWidgetProviderInfo) b)) + : Utilities.trim(((ResolveInfo) b).loadLabel(mPackageManager)); mLabelCache.put(b, labelB); } return mCollator.compare(labelA, labelB); diff --git a/src/com/android/launcher3/LauncherStateTransitionAnimation.java b/src/com/android/launcher3/LauncherStateTransitionAnimation.java index 51f84bfcd..73ae51c3e 100644 --- a/src/com/android/launcher3/LauncherStateTransitionAnimation.java +++ b/src/com/android/launcher3/LauncherStateTransitionAnimation.java @@ -378,11 +378,12 @@ public class LauncherStateTransitionAnimation { dispatchOnLauncherTransitionStart(toView, animated, false); // Enable all necessary layers + boolean isLmpOrAbove = Utilities.isLmpOrAbove(); for (View v : layerViews.keySet()) { if (layerViews.get(v) == BUILD_AND_SET_LAYER) { v.setLayerType(View.LAYER_TYPE_HARDWARE, null); } - if (Utilities.isViewAttachedToWindow(v)) { + if (isLmpOrAbove && Utilities.isViewAttachedToWindow(v)) { v.buildLayer(); } } @@ -697,11 +698,12 @@ public class LauncherStateTransitionAnimation { dispatchOnLauncherTransitionStart(toView, animated, false); // Enable all necessary layers + boolean isLmpOrAbove = Utilities.isLmpOrAbove(); for (View v : layerViews.keySet()) { if (layerViews.get(v) == BUILD_AND_SET_LAYER) { v.setLayerType(View.LAYER_TYPE_HARDWARE, null); } - if (Utilities.isLmpOrAbove()) { + if (isLmpOrAbove && Utilities.isViewAttachedToWindow(v)) { v.buildLayer(); } } diff --git a/src/com/android/launcher3/ShortcutInfo.java b/src/com/android/launcher3/ShortcutInfo.java index 6354fcd28..8be48721c 100644 --- a/src/com/android/launcher3/ShortcutInfo.java +++ b/src/com/android/launcher3/ShortcutInfo.java @@ -153,7 +153,7 @@ public class ShortcutInfo extends ItemInfo { Bitmap icon, UserHandleCompat user) { this(); this.intent = intent; - this.title = title; + this.title = Utilities.trim(title); this.contentDescription = contentDescription; mIcon = icon; this.user = user; @@ -161,7 +161,7 @@ public class ShortcutInfo extends ItemInfo { public ShortcutInfo(Context context, ShortcutInfo info) { super(info); - title = info.title.toString(); + title = Utilities.trim(info.title); intent = new Intent(info.intent); if (info.iconResource != null) { iconResource = new Intent.ShortcutIconResource(); @@ -179,7 +179,7 @@ public class ShortcutInfo extends ItemInfo { /** TODO: Remove this. It's only called by ApplicationInfo.makeShortcut. */ public ShortcutInfo(AppInfo info) { super(info); - title = info.title.toString(); + title = Utilities.trim(info.title); intent = new Intent(info.intent); customIcon = false; flags = info.flags; @@ -281,7 +281,7 @@ public class ShortcutInfo extends ItemInfo { public static ShortcutInfo fromActivityInfo(LauncherActivityInfoCompat info, Context context) { final ShortcutInfo shortcut = new ShortcutInfo(); shortcut.user = info.getUser(); - shortcut.title = info.getLabel().toString(); + shortcut.title = Utilities.trim(info.getLabel()); shortcut.contentDescription = UserManagerCompat.getInstance(context) .getBadgedLabelForUser(info.getLabel(), info.getUser()); shortcut.customIcon = false; diff --git a/src/com/android/launcher3/Utilities.java b/src/com/android/launcher3/Utilities.java index 2dbf078a4..298174768 100644 --- a/src/com/android/launcher3/Utilities.java +++ b/src/com/android/launcher3/Utilities.java @@ -54,6 +54,8 @@ import java.io.ByteArrayOutputStream; import java.io.IOException; import java.util.ArrayList; import java.util.Comparator; +import java.util.regex.Matcher; +import java.util.regex.Pattern; /** * Various utilities shared amongst the Launcher's classes. @@ -67,6 +69,9 @@ public final class Utilities { private static final Rect sOldBounds = new Rect(); private static final Canvas sCanvas = new Canvas(); + private static final Pattern sTrimPattern = + Pattern.compile("^[\\s|\\p{javaSpaceChar}]*(.*)[\\s|\\p{javaSpaceChar}]*$"); + static { sCanvas.setDrawFilter(new PaintFlagsDrawFilter(Paint.DITHER_FLAG, Paint.FILTER_BITMAP_FLAG)); @@ -616,4 +621,14 @@ public final class Utilities { return false; } + + /** + * Trims the string, removing all whitespace at the beginning and end of the string. + * Non-breaking whitespaces are also removed. + */ + public static String trim(CharSequence s) { + // Just strip any sequence of whitespace or java space characters from the beginning and end + Matcher m = sTrimPattern.matcher(s); + return m.replaceAll("$1"); + } } diff --git a/src/com/android/launcher3/Workspace.java b/src/com/android/launcher3/Workspace.java index 5c2121ab8..4004b1d90 100644 --- a/src/com/android/launcher3/Workspace.java +++ b/src/com/android/launcher3/Workspace.java @@ -3558,8 +3558,7 @@ public class Workspace extends SmoothPagedView // Came from all apps -- make a copy info = ((AppInfo) info).makeShortcut(); } - view = mLauncher.createShortcut(R.layout.application, cellLayout, - (ShortcutInfo) info); + view = mLauncher.createShortcut(cellLayout, (ShortcutInfo) info); break; case LauncherSettings.Favorites.ITEM_TYPE_FOLDER: view = FolderIcon.fromXml(R.layout.folder_icon, mLauncher, cellLayout, @@ -4185,7 +4184,7 @@ public class Workspace extends SmoothPagedView && packageNames.contains(cn.getPackageName())) { shortcutInfo.isDisabled |= reason; BubbleTextView shortcut = (BubbleTextView) v; - shortcut.applyFromShortcutInfo(shortcutInfo, mIconCache, true, false); + shortcut.applyFromShortcutInfo(shortcutInfo, mIconCache); if (parent != null) { parent.invalidate(); @@ -4371,7 +4370,7 @@ public class Workspace extends SmoothPagedView BubbleTextView shortcut = (BubbleTextView) v; boolean oldPromiseState = getTextViewIcon(shortcut) instanceof PreloadIconDrawable; - shortcut.applyFromShortcutInfo(si, mIconCache, true, + shortcut.applyFromShortcutInfo(si, mIconCache, si.isPromise() != oldPromiseState); if (parent != null) { diff --git a/src/com/android/launcher3/accessibility/FolderAccessibilityHelper.java b/src/com/android/launcher3/accessibility/FolderAccessibilityHelper.java index fc105b4a4..ff9989036 100644 --- a/src/com/android/launcher3/accessibility/FolderAccessibilityHelper.java +++ b/src/com/android/launcher3/accessibility/FolderAccessibilityHelper.java @@ -24,23 +24,29 @@ import com.android.launcher3.R; * Implementation of {@link DragAndDropAccessibilityDelegate} to support DnD in a folder. */ public class FolderAccessibilityHelper extends DragAndDropAccessibilityDelegate { + + /** + * 0-index position for the first cell in {@link #mView} in {@link #mParent}. + */ private final int mStartPosition; + private final FolderPagedView mParent; + public FolderAccessibilityHelper(CellLayout layout) { super(layout); - FolderPagedView parent = (FolderPagedView) layout.getParent(); + mParent = (FolderPagedView) layout.getParent(); - int index = parent.indexOfChild(layout); - mStartPosition = 1 + index * layout.getCountX() * layout.getCountY(); + int index = mParent.indexOfChild(layout); + mStartPosition = index * layout.getCountX() * layout.getCountY(); } @Override protected int intersectsValidDropTarget(int id) { - return id; + return Math.min(id, mParent.getAllocatedContentSize() - mStartPosition - 1); } @Override protected String getLocationDescriptionForIconDrop(int id) { - return mContext.getString(R.string.move_to_position, id + mStartPosition); + return mContext.getString(R.string.move_to_position, id + mStartPosition + 1); } @Override diff --git a/src/com/android/launcher3/accessibility/WorkspaceAccessibilityHelper.java b/src/com/android/launcher3/accessibility/WorkspaceAccessibilityHelper.java index 42e9e3c58..6f89d0eb0 100644 --- a/src/com/android/launcher3/accessibility/WorkspaceAccessibilityHelper.java +++ b/src/com/android/launcher3/accessibility/WorkspaceAccessibilityHelper.java @@ -145,7 +145,7 @@ public class WorkspaceAccessibilityHelper extends DragAndDropAccessibilityDelega if (info instanceof ShortcutInfo) { return mContext.getString(R.string.create_folder_with, info.title); } else if (info instanceof FolderInfo) { - if (TextUtils.isEmpty(info.title.toString().trim())) { + if (TextUtils.isEmpty(info.title)) { // Find the first item in the folder. FolderInfo folder = (FolderInfo) info; ShortcutInfo firstItem = null; diff --git a/src/com/android/launcher3/compat/AlphabeticIndexCompat.java b/src/com/android/launcher3/compat/AlphabeticIndexCompat.java index f890706ff..ec1fb669f 100644 --- a/src/com/android/launcher3/compat/AlphabeticIndexCompat.java +++ b/src/com/android/launcher3/compat/AlphabeticIndexCompat.java @@ -1,6 +1,7 @@ package com.android.launcher3.compat; import android.content.Context; +import com.android.launcher3.Utilities; import java.lang.reflect.Constructor; import java.lang.reflect.Method; @@ -51,12 +52,15 @@ class BaseAlphabeticIndex { */ public class AlphabeticIndexCompat extends BaseAlphabeticIndex { + private static final String MID_DOT = "\u2219"; + private Object mAlphabeticIndex; private Method mAddLabelsMethod; private Method mSetMaxLabelCountMethod; private Method mGetBucketIndexMethod; private Method mGetBucketLabelMethod; private boolean mHasValidAlphabeticIndex; + private String mDefaultMiscLabel; public AlphabeticIndexCompat(Context context) { super(); @@ -71,12 +75,20 @@ public class AlphabeticIndexCompat extends BaseAlphabeticIndex { mAlphabeticIndex = ctor.newInstance(curLocale); try { // Ensure we always have some base English locale buckets - if (!curLocale.getLanguage().equals(new Locale("en").getLanguage())) { + if (!curLocale.getLanguage().equals(Locale.ENGLISH.getLanguage())) { mAddLabelsMethod.invoke(mAlphabeticIndex, Locale.ENGLISH); } } catch (Exception e) { e.printStackTrace(); } + if (curLocale.getLanguage().equals(Locale.JAPANESE.getLanguage())) { + // Japanese character 他 ("misc") + mDefaultMiscLabel = "\u4ed6"; + // TODO(winsonc, omakoto): We need to handle Japanese sections better, especially the kanji + } else { + // Dot + mDefaultMiscLabel = MID_DOT; + } mHasValidAlphabeticIndex = true; } catch (Exception e) { mHasValidAlphabeticIndex = false; @@ -102,16 +114,25 @@ public class AlphabeticIndexCompat extends BaseAlphabeticIndex { /** * Computes the section name for an given string {@param s}. */ - public String computeSectionName(String s) { + public String computeSectionName(CharSequence cs) { + String s = Utilities.trim(cs); String sectionName = getBucketLabel(getBucketIndex(s)); - if (sectionName.trim().isEmpty() && s.length() > 0) { - boolean startsWithDigit = Character.isDigit(Character.codePointAt(s.trim(), 0)); + if (Utilities.trim(sectionName).isEmpty() && s.length() > 0) { + int c = s.codePointAt(0); + boolean startsWithDigit = Character.isDigit(c); if (startsWithDigit) { // Digit section return "#"; } else { - // Unknown section - return "\u2022"; + boolean startsWithLetter = Character.isLetter(c); + if (startsWithLetter) { + return mDefaultMiscLabel; + } else { + // In languages where these differ, this ensures that we differentiate + // between the misc section in the native language and a misc section + // for everything else. + return MID_DOT; + } } } return sectionName; diff --git a/src/com/android/launcher3/compat/AppWidgetManagerCompatV16.java b/src/com/android/launcher3/compat/AppWidgetManagerCompatV16.java index 767f16f62..967b53b0b 100644 --- a/src/com/android/launcher3/compat/AppWidgetManagerCompatV16.java +++ b/src/com/android/launcher3/compat/AppWidgetManagerCompatV16.java @@ -46,7 +46,7 @@ class AppWidgetManagerCompatV16 extends AppWidgetManagerCompat { @Override public String loadLabel(LauncherAppWidgetProviderInfo info) { - return info.label.trim(); + return Utilities.trim(info.label); } @Override diff --git a/src/com/android/launcher3/widget/PackageItemInfo.java b/src/com/android/launcher3/widget/PackageItemInfo.java index 1a1de55c2..8f45a7754 100644 --- a/src/com/android/launcher3/widget/PackageItemInfo.java +++ b/src/com/android/launcher3/widget/PackageItemInfo.java @@ -49,7 +49,7 @@ public class PackageItemInfo extends ItemInfo { @Override public String toString() { - return "PackageItemInfo(title=" + title.toString() + " id=" + this.id + return "PackageItemInfo(title=" + title + " id=" + this.id + " type=" + this.itemType + " container=" + this.container + " screen=" + screenId + " cellX=" + cellX + " cellY=" + cellY + " spanX=" + spanX + " spanY=" + spanY + " dropPos=" + Arrays.toString(dropPos) diff --git a/src/com/android/launcher3/widget/WidgetCell.java b/src/com/android/launcher3/widget/WidgetCell.java index 2df170eff..7ca4df979 100644 --- a/src/com/android/launcher3/widget/WidgetCell.java +++ b/src/com/android/launcher3/widget/WidgetCell.java @@ -49,7 +49,15 @@ public class WidgetCell extends LinearLayout implements OnLayoutChangeListener { private static final boolean DEBUG = false; private static final int FADE_IN_DURATION_MS = 90; + + /** Widget cell width is calculated by multiplying this factor to grid cell width. */ + private static final float WIDTH_SCALE = 2.6f; + + /** Widget preview width is calculated by multiplying this factor to the widget cell width. */ + private static final float PREVIEW_SCALE = 0.8f; + private int mPresetPreviewSize; + int cellSize; private ImageView mWidgetImage; private TextView mWidgetName; @@ -76,12 +84,17 @@ public class WidgetCell extends LinearLayout implements OnLayoutChangeListener { final Resources r = context.getResources(); mDimensionsFormatString = r.getString(R.string.widget_dims_format); - mPresetPreviewSize = r.getDimensionPixelSize(R.dimen.widget_preview_size); + setContainerWidth(); setWillNotDraw(false); setClipToPadding(false); setAccessibilityDelegate(LauncherAppState.getInstance().getAccessibilityDelegate()); + } + private void setContainerWidth() { + DeviceProfile profile = LauncherAppState.getInstance().getDynamicGrid().getDeviceProfile(); + cellSize = (int) (profile.cellWidthPx * WIDTH_SCALE); + mPresetPreviewSize = (int) (profile.cellWidthPx * WIDTH_SCALE * PREVIEW_SCALE); } @Override @@ -222,10 +235,9 @@ public class WidgetCell extends LinearLayout implements OnLayoutChangeListener { * Helper method to get the string info of the tag. */ private String getTagToString() { - if (getTag() instanceof PendingAddWidgetInfo) { - return ((PendingAddWidgetInfo)getTag()).toString(); - } else if (getTag() instanceof PendingAddShortcutInfo) { - return ((PendingAddShortcutInfo)getTag()).toString(); + if (getTag() instanceof PendingAddWidgetInfo || + getTag() instanceof PendingAddShortcutInfo) { + return getTag().toString(); } return ""; } diff --git a/src/com/android/launcher3/widget/WidgetsContainerRecyclerView.java b/src/com/android/launcher3/widget/WidgetsContainerRecyclerView.java index 65694bfaa..6f15324c1 100644 --- a/src/com/android/launcher3/widget/WidgetsContainerRecyclerView.java +++ b/src/com/android/launcher3/widget/WidgetsContainerRecyclerView.java @@ -17,26 +17,13 @@ package com.android.launcher3.widget; import android.content.Context; -import android.support.v7.widget.RecyclerView; import android.util.AttributeSet; -import android.view.MotionEvent; - -import com.android.launcher3.util.Thunk; +import com.android.launcher3.BaseContainerRecyclerView; /** * The widgets recycler view container. - * <p> - * Overwritten to NOT intercept a touch sequence that started when the {@link RecycleView} - * scrolling slowing down below the internally defined threshold. */ -public class WidgetsContainerRecyclerView extends RecyclerView - implements RecyclerView.OnItemTouchListener { - - private static final int SCROLL_DELTA_THRESHOLD = 4; - - /** Keeps the last known scrolling delta/velocity along y-axis. */ - @Thunk int mDy = 0; - private float mDeltaThreshold; +public class WidgetsContainerRecyclerView extends BaseContainerRecyclerView { public WidgetsContainerRecyclerView(Context context) { this(context, null); @@ -48,47 +35,6 @@ public class WidgetsContainerRecyclerView extends RecyclerView public WidgetsContainerRecyclerView(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); - mDeltaThreshold = getResources().getDisplayMetrics().density * SCROLL_DELTA_THRESHOLD; - - ScrollListener listener = new ScrollListener(); - setOnScrollListener(listener); - } - - private class ScrollListener extends RecyclerView.OnScrollListener { - public ScrollListener() { - } - - @Override - public void onScrolled(RecyclerView recyclerView, int dx, int dy) { - mDy = dy; - } } - @Override - protected void onFinishInflate() { - super.onFinishInflate(); - addOnItemTouchListener(this); - } - - @Override - public boolean onInterceptTouchEvent(RecyclerView rv, MotionEvent ev) { - if (ev.getAction() == MotionEvent.ACTION_DOWN) { - if ((Math.abs(mDy) < mDeltaThreshold && - getScrollState() != RecyclerView.SCROLL_STATE_IDLE)) { - // now the touch events are being passed to the {@link WidgetCell} until the - // touch sequence goes over the touch slop. - stopScroll(); - } - } - return false; - } - - @Override - public void onTouchEvent(RecyclerView rv, MotionEvent ev) { - // Do nothing. - } - - public void onRequestDisallowInterceptTouchEvent(boolean disallowIntercept) { - // DO NOT REMOVE, NEEDED IMPLEMENTATION FOR M BUILDS - } }
\ No newline at end of file diff --git a/src/com/android/launcher3/widget/WidgetsContainerView.java b/src/com/android/launcher3/widget/WidgetsContainerView.java index 22e29f304..f8d7d9256 100644 --- a/src/com/android/launcher3/widget/WidgetsContainerView.java +++ b/src/com/android/launcher3/widget/WidgetsContainerView.java @@ -28,10 +28,9 @@ import android.support.v7.widget.RecyclerView.State; import android.util.AttributeSet; import android.util.Log; import android.view.View; -import android.widget.FrameLayout; import android.widget.ImageView; import android.widget.Toast; - +import com.android.launcher3.BaseContainerView; import com.android.launcher3.CellLayout; import com.android.launcher3.DeleteDropTarget; import com.android.launcher3.DragController; @@ -55,8 +54,8 @@ import java.util.ArrayList; /** * The widgets list view container. */ -public class WidgetsContainerView extends FrameLayout implements Insettable, - View.OnLongClickListener, View.OnClickListener, DragSource{ +public class WidgetsContainerView extends BaseContainerView + implements View.OnLongClickListener, View.OnClickListener, DragSource{ private static final String TAG = "WidgetsContainerView"; private static final boolean DEBUG = false; @@ -129,6 +128,7 @@ public class WidgetsContainerView extends FrameLayout implements Insettable, }); mPadding.set(getPaddingLeft(), getPaddingTop(), getPaddingRight(), getPaddingBottom()); + onUpdatePaddings(); } // @@ -364,13 +364,17 @@ public class WidgetsContainerView extends FrameLayout implements Insettable, // Container rendering related. // - /* - * @see Insettable#setInsets(Rect) - */ @Override - public void setInsets(Rect insets) { - setPadding(mPadding.left + insets.left, mPadding.top + insets.top, - mPadding.right + insets.right, mPadding.bottom + insets.bottom); + protected void onUpdatePaddings() { + if (mFixedBounds.isEmpty()) { + // If there are no fixed bounds, then use the default padding and insets + setPadding(mPadding.left + mInsets.left, mPadding.top + mInsets.top, + mPadding.right + mInsets.right, mPadding.bottom + mInsets.bottom); + } else { + // If there are fixed bounds, then we update the padding to reflect the fixed bounds. + setPadding(mFixedBounds.left, mFixedBounds.top, getMeasuredWidth() - mFixedBounds.right, + mFixedBounds.bottom); + } } /** diff --git a/src/com/android/launcher3/widget/WidgetsListAdapter.java b/src/com/android/launcher3/widget/WidgetsListAdapter.java index a7728a11b..d6e062874 100644 --- a/src/com/android/launcher3/widget/WidgetsListAdapter.java +++ b/src/com/android/launcher3/widget/WidgetsListAdapter.java @@ -17,21 +17,27 @@ package com.android.launcher3.widget; import android.content.Context; import android.content.pm.ResolveInfo; +import android.support.v7.widget.RecyclerView; +import android.content.res.Resources; import android.support.v7.widget.RecyclerView.Adapter; import android.util.Log; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; -import android.widget.ImageView; -import android.widget.TextView; +import android.view.ViewGroup.LayoutParams; +import android.view.ViewGroup.MarginLayoutParams; +import android.widget.LinearLayout; +import com.android.launcher3.BubbleTextView; + +import com.android.launcher3.DeviceProfile; +import com.android.launcher3.DynamicGrid; import com.android.launcher3.IconCache; import com.android.launcher3.Launcher; import com.android.launcher3.LauncherAppState; import com.android.launcher3.LauncherAppWidgetProviderInfo; import com.android.launcher3.R; import com.android.launcher3.WidgetPreviewLoader; -import com.android.launcher3.compat.UserHandleCompat; import java.util.List; @@ -46,7 +52,7 @@ import java.util.List; public class WidgetsListAdapter extends Adapter<WidgetsRowViewHolder> { private static final String TAG = "WidgetsListAdapter"; - private static final boolean DEBUG = false; + private static final boolean DEBUG = true; private Context mContext; private Launcher mLauncher; @@ -59,6 +65,9 @@ public class WidgetsListAdapter extends Adapter<WidgetsRowViewHolder> { private View.OnClickListener mIconClickListener; private View.OnLongClickListener mIconLongClickListener; + private static final int PRESET_INDENT_SIZE_TABLET = 56; + private int mIndent = 0; + public WidgetsListAdapter(Context context, View.OnClickListener iconClickListener, View.OnLongClickListener iconLongClickListener, @@ -71,6 +80,8 @@ public class WidgetsListAdapter extends Adapter<WidgetsRowViewHolder> { mLauncher = launcher; mIconCache = LauncherAppState.getInstance().getIconCache(); + + setContainerHeight(); } public void setWidgetsModel(WidgetsModel w) { @@ -96,6 +107,7 @@ public class WidgetsListAdapter extends Adapter<WidgetsRowViewHolder> { // Add more views. // if there are too many, hide them. int diff = infoList.size() - row.getChildCount(); + if (diff > 0) { for (int i = 0; i < diff; i++) { WidgetCell widget = new WidgetCell(mContext); @@ -105,6 +117,11 @@ public class WidgetsListAdapter extends Adapter<WidgetsRowViewHolder> { // set up touch. widget.setOnClickListener(mIconClickListener); widget.setOnLongClickListener(mIconLongClickListener); + LayoutParams lp = widget.getLayoutParams(); + lp.height = widget.cellSize; + lp.width = widget.cellSize; + widget.setLayoutParams(lp); + row.addView(widget); } } else if (diff < 0) { @@ -115,16 +132,8 @@ public class WidgetsListAdapter extends Adapter<WidgetsRowViewHolder> { // Bind the views in the application info section. PackageItemInfo infoOut = mWidgetsModel.getPackageItemInfo(pos); - if (infoOut.usingLowResIcon) { - // TODO(hyunyoungs): call this in none UI thread in the same way as BubbleTextView. - mIconCache.getTitleAndIconForApp(infoOut.packageName, - UserHandleCompat.myUserHandle(), false /* useLowResIcon */, infoOut); - } - - TextView tv = ((TextView) holder.getContent().findViewById(R.id.section)); - tv.setText(infoOut.title); - ImageView iv = (ImageView) holder.getContent().findViewById(R.id.section_image); - iv.setImageBitmap(infoOut.iconBitmap); + BubbleTextView tv = ((BubbleTextView) holder.getContent().findViewById(R.id.section)); + tv.applyFromPackageItemInfo(infoOut); // Bind the view in the widget horizontal tray region. for (int i=0; i < infoList.size(); i++) { @@ -156,6 +165,10 @@ public class WidgetsListAdapter extends Adapter<WidgetsRowViewHolder> { ViewGroup container = (ViewGroup) mLayoutInflater.inflate( R.layout.widgets_list_row_view, parent, false); + LinearLayout cellList = (LinearLayout) container.findViewById(R.id.widgets_cell_list); + MarginLayoutParams lp = (MarginLayoutParams) cellList.getLayoutParams(); + lp.setMarginStart(mIndent); + cellList.setLayoutParams(lp); return new WidgetsRowViewHolder(container); } @@ -180,4 +193,12 @@ public class WidgetsListAdapter extends Adapter<WidgetsRowViewHolder> { } return mWidgetPreviewLoader; } + + private void setContainerHeight() { + Resources r = mContext.getResources(); + DeviceProfile profile = LauncherAppState.getInstance().getDynamicGrid().getDeviceProfile(); + if (profile.isLargeTablet || profile.isTablet) { + mIndent = DynamicGrid.pxFromDp(PRESET_INDENT_SIZE_TABLET, r.getDisplayMetrics()); + } + } } |