Merge "Implementation of PredictionRowView. > Enable to use floating for prediction row even without tabs. > Behind ALL_APPS_PREDICTION_ROW_VIEW feature flag. > Expand/Collapse personal/work tabs in stopped intermediate state." into ub-launcher3-master
diff --git a/res/layout/all_apps.xml b/res/layout/all_apps.xml
index 832aaef..c42c15c 100644
--- a/res/layout/all_apps.xml
+++ b/res/layout/all_apps.xml
@@ -35,21 +35,27 @@
android:id="@+id/all_apps_header"
android:layout_width="match_parent"
android:layout_height="wrap_content"
- android:clickable="true"
- android:paddingTop="30dp"
+ android:paddingTop="@dimen/all_apps_header_top_padding"
+ android:clipToPadding="false"
android:layout_below="@id/search_container_all_apps" >
<com.android.launcher3.allapps.PredictionRowView
android:id="@+id/header_content"
android:layout_width="match_parent"
- android:layout_height="wrap_content"/>
+ android:layout_height="wrap_content" />
+
+ <include layout="@layout/all_apps_divider"
+ android:id="@+id/divider"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_alignBottom="@+id/tabs" />
<com.android.launcher3.views.SlidingTabStrip
android:id="@+id/tabs"
android:layout_width="match_parent"
android:layout_height="@dimen/all_apps_header_tab_height"
android:layout_below="@id/header_content"
- android:orientation="horizontal">
+ android:orientation="horizontal" >
<Button
android:id="@+id/tab_personal"
android:layout_width="0dp"
@@ -67,7 +73,6 @@
android:textColor="@color/all_apps_tab_text"
android:background="?android:attr/selectableItemBackground"/>
</com.android.launcher3.views.SlidingTabStrip>
-
</RelativeLayout>
<!-- Note: we are reusing/repurposing a system attribute for search layout, because of a
diff --git a/res/layout/all_apps_tabs.xml b/res/layout/all_apps_tabs.xml
index fa1d591..54a9b88 100644
--- a/res/layout/all_apps_tabs.xml
+++ b/res/layout/all_apps_tabs.xml
@@ -25,7 +25,7 @@
android:clipChildren="false"
android:clipToPadding="false"
android:descendantFocusability="afterDescendants"
- android:paddingTop="30dp">
+ android:paddingTop="@dimen/all_apps_header_top_padding">
<include layout="@layout/all_apps_rv_layout" />
diff --git a/res/values/dimens.xml b/res/values/dimens.xml
index efe5043..266e0b0 100644
--- a/res/values/dimens.xml
+++ b/res/values/dimens.xml
@@ -92,6 +92,8 @@
<dimen name="all_apps_caret_workspace_offset">18dp</dimen>
<dimen name="all_apps_header_tab_height">50dp</dimen>
<dimen name="all_apps_tabs_indicator_height">2dp</dimen>
+ <dimen name="all_apps_header_top_padding">36dp</dimen>
+ <dimen name="all_apps_prediction_row_divider_height">17dp</dimen>
<!-- Search bar in All Apps -->
<dimen name="all_apps_header_max_elevation">3dp</dimen>
diff --git a/src/com/android/launcher3/allapps/AllAppsContainerView.java b/src/com/android/launcher3/allapps/AllAppsContainerView.java
index efd7b97..2bb95cb 100644
--- a/src/com/android/launcher3/allapps/AllAppsContainerView.java
+++ b/src/com/android/launcher3/allapps/AllAppsContainerView.java
@@ -281,6 +281,9 @@
mAH[i].recyclerView.scrollToTop();
}
}
+ if (mFloatingHeaderHandler != null) {
+ mFloatingHeaderHandler.reset();
+ }
// Reset the search bar and base recycler view after transitioning home
mSearchUiManager.reset();
}
@@ -301,6 +304,7 @@
});
mHeader = findViewById(R.id.all_apps_header);
+ mFloatingHeaderHandler = new FloatingHeaderHandler(mHeader);
rebindAdapters(mUsingTabs);
mSearchContainer = findViewById(R.id.search_container_all_apps);
@@ -431,10 +435,14 @@
mAH[AdapterHolder.WORK].setup(mViewPager.getChildAt(1), mWorkMatcher);
setupWorkProfileTabs();
setupHeader();
- mHeader.setVisibility(View.VISIBLE);
} else {
- mHeader.setVisibility(View.GONE);
mAH[AdapterHolder.MAIN].setup(findViewById(R.id.apps_list_view), null);
+ if (FeatureFlags.ALL_APPS_PREDICTION_ROW_VIEW) {
+ setupHeader();
+ } else {
+ mFloatingHeaderHandler = null;
+ mHeader.setVisibility(View.GONE);
+ }
}
applyTouchDelegate();
@@ -512,7 +520,7 @@
}
public void setPredictedApps(List<ComponentKeyMapper<AppInfo>> apps) {
- if (mUsingTabs) {
+ if (mFloatingHeaderHandler != null) {
mFloatingHeaderHandler.getContentView().setPredictedApps(apps);
}
mAH[AdapterHolder.MAIN].appsList.setPredictedApps(apps);
@@ -535,15 +543,24 @@
}
private void setupHeader() {
+ mHeader.setVisibility(View.VISIBLE);
int contentHeight = mLauncher.getDeviceProfile().allAppsCellHeightPx;
+ if (!mUsingTabs) {
+ contentHeight += getResources()
+ .getDimensionPixelSize(R.dimen.all_apps_prediction_row_divider_height);
+ }
RecyclerView mainRV = mAH[AdapterHolder.MAIN].recyclerView;
- RecyclerView workRV = mAH[AdapterHolder.WORK] != null
- ? mAH[AdapterHolder.WORK].recyclerView : null;
- mFloatingHeaderHandler = new FloatingHeaderHandler(mHeader, mainRV, workRV, contentHeight);
- mFloatingHeaderHandler.getContentView().setNumAppsPerRow(mNumPredictedAppsPerRow);
- mFloatingHeaderHandler.getContentView().setComponentToAppMap(mComponentToAppMap);
+ RecyclerView workRV = mAH[AdapterHolder.WORK].recyclerView;
+ mFloatingHeaderHandler.setup(mainRV, workRV, contentHeight);
+ mFloatingHeaderHandler.getContentView().setup(mAH[AdapterHolder.MAIN].adapter,
+ mComponentToAppMap, mNumPredictedAppsPerRow);
+
+ int padding = contentHeight;
+ if (!mUsingTabs) {
+ padding += mHeader.getPaddingTop() + mHeader.getPaddingBottom();
+ }
for (int i = 0; i < mAH.length; i++) {
- mAH[i].paddingTopForTabs = contentHeight;
+ mAH[i].paddingTopForTabs = padding;
mAH[i].applyPadding();
}
}
@@ -556,7 +573,9 @@
public void onSearchResultsChanged() {
for (int i = 0; i < mAH.length; i++) {
- mAH[i].recyclerView.onSearchResultsChanged();
+ if (mAH[i].recyclerView != null) {
+ mAH[i].recyclerView.onSearchResultsChanged();
+ }
}
}
@@ -640,9 +659,14 @@
void applyPadding() {
if (recyclerView != null) {
- int paddingTop = mUsingTabs ? paddingTopForTabs : padding.top;
+ int paddingTop = mUsingTabs || FeatureFlags.ALL_APPS_PREDICTION_ROW_VIEW
+ ? paddingTopForTabs : padding.top;
recyclerView.setPadding(padding.left, paddingTop, padding.right, padding.bottom);
}
+ if (mFloatingHeaderHandler != null) {
+ mFloatingHeaderHandler.getContentView()
+ .setPadding(padding.left, 0 , padding.right, 0);
+ }
}
void applyNumsPerRow() {
@@ -652,7 +676,7 @@
}
adapter.setNumAppsPerRow(mNumAppsPerRow);
appsList.setNumAppsPerRow(mNumAppsPerRow, mNumPredictedAppsPerRow);
- if (mUsingTabs && mFloatingHeaderHandler != null) {
+ if (mFloatingHeaderHandler != null) {
mFloatingHeaderHandler.getContentView()
.setNumAppsPerRow(mNumPredictedAppsPerRow);
}
diff --git a/src/com/android/launcher3/allapps/AlphabeticalAppsList.java b/src/com/android/launcher3/allapps/AlphabeticalAppsList.java
index f9dde2f..7cf2d95 100644
--- a/src/com/android/launcher3/allapps/AlphabeticalAppsList.java
+++ b/src/com/android/launcher3/allapps/AlphabeticalAppsList.java
@@ -460,7 +460,7 @@
mFastScrollerSections.clear();
mAdapterItems.clear();
- if (!FeatureFlags.ALL_APPS_TABS_ENABLED) {
+ if (!FeatureFlags.ALL_APPS_PREDICTION_ROW_VIEW) {
if (DEBUG_PREDICTIONS) {
if (mPredictedAppComponents.isEmpty() && !mApps.isEmpty()) {
mPredictedAppComponents.add(new ComponentKeyMapper<AppInfo>(new ComponentKey(mApps.get(0).componentName,
diff --git a/src/com/android/launcher3/allapps/FloatingHeaderHandler.java b/src/com/android/launcher3/allapps/FloatingHeaderHandler.java
index 984966b..0b39b7d 100644
--- a/src/com/android/launcher3/allapps/FloatingHeaderHandler.java
+++ b/src/com/android/launcher3/allapps/FloatingHeaderHandler.java
@@ -15,36 +15,52 @@
*/
package com.android.launcher3.allapps;
+import android.animation.ValueAnimator;
+import android.content.res.Resources;
import android.graphics.Rect;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.v7.widget.RecyclerView;
import android.view.View;
+import android.view.ViewGroup;
+import android.widget.RelativeLayout;
import com.android.launcher3.R;
-public class FloatingHeaderHandler extends RecyclerView.OnScrollListener {
+public class FloatingHeaderHandler extends RecyclerView.OnScrollListener
+ implements ValueAnimator.AnimatorUpdateListener {
- private final int mMaxTranslation;
private final View mHeaderView;
- private final PredictionRowView mContentView;
- private final RecyclerView mMainRV;
- private final RecyclerView mWorkRV;
+ private final PredictionRowView mPredictionRow;
+ private final ViewGroup mTabLayout;
+ private final View mDivider;
private final Rect mClip = new Rect(0, 0, Integer.MAX_VALUE, Integer.MAX_VALUE);
+ private final ValueAnimator mAnimator = ValueAnimator.ofInt(0, 0);
+ private RecyclerView mMainRV;
+ private RecyclerView mWorkRV;
+ private boolean mTopOnlyMode;
private boolean mHeaderHidden;
+ private int mMaxTranslation;
private int mSnappedScrolledY;
private int mTranslationY;
private int mMainScrolledY;
private int mWorkScrolledY;
private boolean mMainRVActive;
- public FloatingHeaderHandler(@NonNull View header, @NonNull RecyclerView personalRV,
- @Nullable RecyclerView workRV, int contentHeight) {
+ public FloatingHeaderHandler(@NonNull ViewGroup header) {
mHeaderView = header;
- mContentView = mHeaderView.findViewById(R.id.header_content);
- mContentView.getLayoutParams().height = contentHeight;
- mMaxTranslation = contentHeight;
+ mTabLayout = header.findViewById(R.id.tabs);
+ mDivider = header.findViewById(R.id.divider);
+ mPredictionRow = header.findViewById(R.id.header_content);
+ }
+
+ public void setup(@NonNull RecyclerView personalRV, @Nullable RecyclerView workRV,
+ int predictionRowHeight) {
+ mTopOnlyMode = workRV == null;
+ mTabLayout.setVisibility(mTopOnlyMode ? View.GONE : View.VISIBLE);
+ mPredictionRow.getLayoutParams().height = predictionRowHeight;
+ mMaxTranslation = predictionRowHeight;
mMainRV = personalRV;
mMainRV.addOnScrollListener(this);
mWorkRV = workRV;
@@ -52,6 +68,18 @@
workRV.addOnScrollListener(this);
}
setMainActive(true);
+ setupDivider();
+ }
+
+ private void setupDivider() {
+ Resources res = mHeaderView.getResources();
+ int verticalGap = res.getDimensionPixelSize(R.dimen.all_apps_divider_margin_vertical);
+ int sideGap = res.getDimensionPixelSize(R.dimen.dynamic_grid_edge_margin);
+ mDivider.setPadding(sideGap, verticalGap,sideGap, mTopOnlyMode ? verticalGap : 0);
+ RelativeLayout.LayoutParams lp = (RelativeLayout.LayoutParams) mDivider.getLayoutParams();
+ lp.removeRule(RelativeLayout.ALIGN_BOTTOM);
+ lp.addRule(RelativeLayout.ALIGN_BOTTOM, mTopOnlyMode ? R.id.header_content : R.id.tabs);
+ mDivider.setLayoutParams(lp);
}
public void setMainActive(boolean active) {
@@ -65,7 +93,15 @@
}
public PredictionRowView getContentView() {
- return mContentView;
+ return mPredictionRow;
+ }
+
+ public ViewGroup getTabLayout() {
+ return mTabLayout;
+ }
+
+ public View getDivider() {
+ return mDivider;
}
@Override
@@ -75,27 +111,39 @@
return;
}
+ if (mAnimator.isStarted()) {
+ mAnimator.cancel();
+ }
+
int current = isMainRV
? (mMainScrolledY -= dy)
: (mWorkScrolledY -= dy);
- if (dy == 0) {
- setExpanded(true);
- } else {
- moved(current);
- apply();
- }
+ moved(current);
+ apply();
+ }
+
+ public void reset() {
+ mMainScrolledY = 0;
+ mWorkScrolledY = 0;
+ setExpanded(true);
+ }
+
+ private boolean canSnapAt(int currentScrollY) {
+ return !mTopOnlyMode || Math.abs(currentScrollY) <= mPredictionRow.getHeight();
}
private void moved(final int currentScrollY) {
if (mHeaderHidden) {
if (currentScrollY <= mSnappedScrolledY) {
- mSnappedScrolledY = currentScrollY;
+ if (canSnapAt(currentScrollY)) {
+ mSnappedScrolledY = currentScrollY;
+ }
} else {
mHeaderHidden = false;
}
mTranslationY = currentScrollY;
- } else {
+ } else if (!mHeaderHidden) {
mTranslationY = currentScrollY - mSnappedScrolledY - mMaxTranslation;
// update state vars
@@ -110,20 +158,36 @@
}
private void apply() {
+ int uncappedTranslationY = mTranslationY;
mTranslationY = Math.max(mTranslationY, -mMaxTranslation);
- mHeaderView.setTranslationY(mTranslationY);
+ mPredictionRow.setTranslationY(uncappedTranslationY);
+ mTabLayout.setTranslationY(mTranslationY);
+ mDivider.setTranslationY(mTopOnlyMode ? uncappedTranslationY : mTranslationY);
mClip.top = mMaxTranslation + mTranslationY;
+ // clipping on a draw might cause additional redraw
mMainRV.setClipBounds(mClip);
if (mWorkRV != null) {
mWorkRV.setClipBounds(mClip);
}
}
+ @Override
+ public void onScrollStateChanged(RecyclerView recyclerView, int newState) {
+ if (!mTopOnlyMode && newState == RecyclerView.SCROLL_STATE_IDLE
+ && mTranslationY != -mMaxTranslation && mTranslationY != 0) {
+ float scroll = Math.abs(getCurrentScroll());
+ boolean expand = scroll > mMaxTranslation
+ ? Math.abs(mTranslationY) < mMaxTranslation / 2 : true;
+ setExpanded(expand);
+ }
+ }
+
private void setExpanded(boolean expand) {
int translateTo = expand ? 0 : -mMaxTranslation;
- mTranslationY = translateTo;
- apply();
-
+ mAnimator.setIntValues(mTranslationY, translateTo);
+ mAnimator.addUpdateListener(this);
+ mAnimator.setDuration(150);
+ mAnimator.start();
mHeaderHidden = !expand;
mSnappedScrolledY = expand ? getCurrentScroll() - mMaxTranslation : getCurrentScroll();
}
@@ -136,4 +200,10 @@
return mMainRVActive ? mMainScrolledY : mWorkScrolledY;
}
+ @Override
+ public void onAnimationUpdate(ValueAnimator animation) {
+ mTranslationY = (Integer) animation.getAnimatedValue();
+ apply();
+ }
+
}
diff --git a/src/com/android/launcher3/allapps/PredictionRowView.java b/src/com/android/launcher3/allapps/PredictionRowView.java
index 5551f07..45ef6c1 100644
--- a/src/com/android/launcher3/allapps/PredictionRowView.java
+++ b/src/com/android/launcher3/allapps/PredictionRowView.java
@@ -21,9 +21,11 @@
import android.support.annotation.Nullable;
import android.util.AttributeSet;
import android.util.Log;
+import android.view.View;
import android.widget.LinearLayout;
import com.android.launcher3.AppInfo;
+import com.android.launcher3.BubbleTextView;
import com.android.launcher3.config.FeatureFlags;
import com.android.launcher3.util.ComponentKey;
import com.android.launcher3.util.ComponentKeyMapper;
@@ -43,6 +45,8 @@
private final List<ComponentKeyMapper<AppInfo>> mPredictedAppComponents = new ArrayList<>();
// The set of predicted apps resolved from the component names and the current set of apps
private final List<AppInfo> mPredictedApps = new ArrayList<>();
+ // This adapter is only used to create an identical item w/ same behavior as in the all apps RV
+ private AllAppsGridAdapter mAdapter;
public PredictionRowView(@NonNull Context context) {
this(context, null);
@@ -53,8 +57,11 @@
setOrientation(LinearLayout.HORIZONTAL);
}
- public void setComponentToAppMap(HashMap<ComponentKey, AppInfo> componentToAppMap) {
- this.mComponentToAppMap = componentToAppMap;
+ public void setup(AllAppsGridAdapter adapter,
+ HashMap<ComponentKey, AppInfo> componentToAppMap, int numPredictedAppsPerRow) {
+ mAdapter = adapter;
+ mComponentToAppMap = componentToAppMap;
+ mNumPredictedAppsPerRow = numPredictedAppsPerRow;
}
/**
@@ -64,10 +71,6 @@
mNumPredictedAppsPerRow = numPredictedAppsPerRow;
}
- public void onAppsUpdated() {
- // TODO
- }
-
/**
* Returns the predicted apps.
*/
@@ -87,15 +90,35 @@
public void setPredictedApps(List<ComponentKeyMapper<AppInfo>> apps) {
mPredictedAppComponents.clear();
mPredictedAppComponents.addAll(apps);
+ mPredictedApps.addAll(processPredictedAppComponents(mPredictedAppComponents));
+ onAppsUpdated();
+ }
- List<AppInfo> newPredictedApps = processPredictedAppComponents(apps);
- // We only need to do work if any of the visible predicted apps have changed.
- if (!newPredictedApps.equals(mPredictedApps)) {
- if (newPredictedApps.size() == mPredictedApps.size()) {
- swapInNewPredictedApps(newPredictedApps);
+ private void onAppsUpdated() {
+ if (getChildCount() != mNumPredictedAppsPerRow) {
+ while (getChildCount() > mNumPredictedAppsPerRow) {
+ removeViewAt(0);
+ }
+ while (getChildCount() < mNumPredictedAppsPerRow) {
+ AllAppsGridAdapter.ViewHolder holder = mAdapter
+ .onCreateViewHolder(this, AllAppsGridAdapter.VIEW_TYPE_ICON);
+ BubbleTextView icon = (BubbleTextView) holder.itemView;
+ LinearLayout.LayoutParams params =
+ new LayoutParams(0, icon.getLayoutParams().height);
+ params.weight = 1;
+ icon.setLayoutParams(params);
+ addView(icon);
+ }
+ }
+
+ for (int i = 0; i < getChildCount(); i++) {
+ BubbleTextView icon = (BubbleTextView) getChildAt(i);
+ icon.reset();
+ if (mPredictedApps.size() > i) {
+ icon.setVisibility(View.VISIBLE);
+ icon.applyFromApplicationInfo(mPredictedApps.get(i));
} else {
- // We need to update the appIndex of all the items.
- onAppsUpdated();
+ icon.setVisibility(View.INVISIBLE);
}
}
}
@@ -124,16 +147,4 @@
}
return predictedApps;
}
-
- /**
- * Swaps out the old predicted apps with the new predicted apps, in place. This optimization
- * allows us to skip an entire relayout that would otherwise be called by notifyDataSetChanged.
- *
- * Note: This should only be called if the # of predicted apps is the same.
- * This method assumes that predicted apps are the first items in the adapter.
- */
- private void swapInNewPredictedApps(List<AppInfo> apps) {
- // TODO
- }
-
}
diff --git a/src/com/android/launcher3/config/BaseFlags.java b/src/com/android/launcher3/config/BaseFlags.java
index 1924710..7cf3da0 100644
--- a/src/com/android/launcher3/config/BaseFlags.java
+++ b/src/com/android/launcher3/config/BaseFlags.java
@@ -61,4 +61,6 @@
// When enabled shows a work profile tab in all apps
public static final boolean ALL_APPS_TABS_ENABLED = false;
+ // When enabled prediction row is rendered as it's own custom view
+ public static final boolean ALL_APPS_PREDICTION_ROW_VIEW = false;
}