diff --git a/res/layout/widget_cell_content.xml b/res/layout/widget_cell_content.xml index 7b85d9bcd5..a3d0070697 100644 --- a/res/layout/widget_cell_content.xml +++ b/res/layout/widget_cell_content.xml @@ -18,10 +18,11 @@ android:layout_height="wrap_content"> + should be done using margins. + width & height are set at runtime after scaling the preview image. --> + android:textSize="@dimen/widget_cell_font_size" /> @@ -48,7 +49,7 @@ android:layout_height="wrap_content" android:gravity="center_horizontal" android:textColor="?android:attr/textColorTertiary" - android:textSize="14sp" + android:textSize="@dimen/widget_cell_font_size" android:alpha="0.8" /> + + diff --git a/res/layout/widgets_search_bar.xml b/res/layout/widgets_search_bar.xml index 021058b65d..1db7462a0f 100644 --- a/res/layout/widgets_search_bar.xml +++ b/res/layout/widgets_search_bar.xml @@ -6,7 +6,8 @@ android:layout_height="wrap_content" android:orientation="horizontal" android:layout_marginTop="16dp" - android:background="@drawable/bg_widgets_searchbox"> + android:background="@drawable/bg_widgets_searchbox" + android:elevation="2dp"> 8dp 16dp + 14sp 28dp diff --git a/src/com/android/launcher3/widget/WidgetCell.java b/src/com/android/launcher3/widget/WidgetCell.java index 5328041d16..1b0e1ce0b2 100644 --- a/src/com/android/launcher3/widget/WidgetCell.java +++ b/src/com/android/launcher3/widget/WidgetCell.java @@ -74,6 +74,7 @@ public class WidgetCell extends LinearLayout implements OnLayoutChangeListener { protected int mPreviewHeight; protected int mPresetPreviewSize; private int mCellSize; + private float mPreviewScale = 1f; private WidgetImageView mWidgetImage; private TextView mWidgetName; @@ -254,8 +255,8 @@ public class WidgetCell extends LinearLayout implements OnLayoutChangeListener { } if (drawable != null) { LayoutParams layoutParams = (LayoutParams) mWidgetImage.getLayoutParams(); - layoutParams.width = drawable.getIntrinsicWidth(); - layoutParams.height = drawable.getIntrinsicHeight(); + layoutParams.width = (int) (drawable.getIntrinsicWidth() * mPreviewScale); + layoutParams.height = (int) (drawable.getIntrinsicHeight() * mPreviewScale); mWidgetImage.setLayoutParams(layoutParams); mWidgetImage.setDrawable(drawable, mWidgetPreviewLoader.getBadgeForUser(mItem.user, @@ -305,10 +306,16 @@ public class WidgetCell extends LinearLayout implements OnLayoutChangeListener { /** Sets the widget preview image size in number of cells. */ public void setPreviewSize(int spanX, int spanY) { + setPreviewSize(spanX, spanY, 1f); + } + + /** Sets the widget preview image size, in number of cells, and preview scale. */ + public void setPreviewSize(int spanX, int spanY, float previewScale) { int padding = 2 * getResources() .getDimensionPixelSize(R.dimen.widget_preview_shortcut_padding); mPreviewWidth = mDeviceProfile.cellWidthPx * spanX + padding; mPreviewHeight = mDeviceProfile.cellHeightPx * spanY + padding; + mPreviewScale = previewScale; } @Override diff --git a/src/com/android/launcher3/widget/picker/SearchAndRecommendationsScrollController.java b/src/com/android/launcher3/widget/picker/SearchAndRecommendationsScrollController.java index 7eb5b834fd..7f840776b2 100644 --- a/src/com/android/launcher3/widget/picker/SearchAndRecommendationsScrollController.java +++ b/src/com/android/launcher3/widget/picker/SearchAndRecommendationsScrollController.java @@ -35,6 +35,7 @@ final class SearchAndRecommendationsScrollController implements private final SearchAndRecommendationViewHolder mViewHolder; private final WidgetsRecyclerView mPrimaryRecyclerView; private final WidgetsRecyclerView mSearchRecyclerView; + private final int mTabsHeight; // The following are only non null if mHasWorkProfile is true. @Nullable private final WidgetsRecyclerView mWorkRecyclerView; @@ -42,10 +43,28 @@ final class SearchAndRecommendationsScrollController implements @Nullable private final PersonalWorkPagedView mPrimaryWorkViewPager; private WidgetsRecyclerView mCurrentRecyclerView; - private int mMaxCollapsibleHeight = 0; + + /** + * The vertical distance, in pixels, until the search is pinned at the top of the screen when + * the user scrolls down the recycler view. + */ + private int mCollapsibleHeightForSearch = 0; + /** + * The vertical distance, in pixels, until the recommendation table disappears from the top of + * the screen when the user scrolls down the recycler view. + */ + private int mCollapsibleHeightForRecommendation = 0; + /** + * The vertical distance, in pixels, until the tabs is pinned at the top of the screen when the + * user scrolls down the recycler view. + * + *

Always 0 if there is no work profile. + */ + private int mCollapsibleHeightForTabs = 0; SearchAndRecommendationsScrollController( boolean hasWorkProfile, + int tabsHeight, SearchAndRecommendationViewHolder viewHolder, WidgetsRecyclerView primaryRecyclerView, @Nullable WidgetsRecyclerView workRecyclerView, @@ -55,46 +74,63 @@ final class SearchAndRecommendationsScrollController implements mHasWorkProfile = hasWorkProfile; mViewHolder = viewHolder; mPrimaryRecyclerView = primaryRecyclerView; + mCurrentRecyclerView = mPrimaryRecyclerView; mWorkRecyclerView = workRecyclerView; mSearchRecyclerView = searchRecyclerView; mPrimaryWorkTabsView = personalWorkTabsView; mPrimaryWorkViewPager = primaryWorkViewPager; mCurrentRecyclerView = mPrimaryRecyclerView; + mTabsHeight = tabsHeight; } /** Sets the current active {@link WidgetsRecyclerView}. */ public void setCurrentRecyclerView(WidgetsRecyclerView currentRecyclerView) { mCurrentRecyclerView = currentRecyclerView; + mCurrentRecyclerView = currentRecyclerView; + mViewHolder.mHeaderTitle.setTranslationY(0); + mViewHolder.mRecommendedWidgetsTable.setTranslationY(0); + mViewHolder.mSearchBar.setTranslationY(0); + + if (mHasWorkProfile) { + mPrimaryWorkTabsView.setTranslationY(0); + } } /** * Updates the margin and padding of {@link WidgetsFullSheet} to accumulate collapsible views. + * + * @return {@code true} if margins or/and padding of views in the search and recommendations + * container have been updated. */ - public void updateMarginAndPadding() { - // The maximum vertical distance, in pixels, until the last collapsible element is not - // visible from the screen when the user scrolls down the recycler view. - mMaxCollapsibleHeight = mViewHolder.mContainer.getPaddingTop() - + measureHeightWithVerticalMargins(mViewHolder.mCollapseHandle) - + measureHeightWithVerticalMargins(mViewHolder.mHeaderTitle); + public boolean updateMarginAndPadding() { + boolean hasMarginOrPaddingUpdated = false; + mCollapsibleHeightForSearch = measureHeightWithVerticalMargins(mViewHolder.mHeaderTitle); + mCollapsibleHeightForRecommendation = + measureHeightWithVerticalMargins(mViewHolder.mHeaderTitle) + + measureHeightWithVerticalMargins(mViewHolder.mCollapseHandle) + + measureHeightWithVerticalMargins((View) mViewHolder.mSearchBar) + + measureHeightWithVerticalMargins(mViewHolder.mRecommendedWidgetsTable); int topContainerHeight = measureHeightWithVerticalMargins(mViewHolder.mContainer); if (mHasWorkProfile) { + mCollapsibleHeightForTabs = measureHeightWithVerticalMargins(mViewHolder.mHeaderTitle) + + measureHeightWithVerticalMargins(mViewHolder.mRecommendedWidgetsTable); // In a work profile setup, the full widget sheet contains the following views: - // ------- -| - // Widgets -|---> LinearLayout for search & recommendations - // Search bar -| - // Personal | Work + // ------- (pinned) -| + // Widgets (collapsible) -|---> LinearLayout for search & recommendations + // Search bar (pinned) -| + // Widgets recommendation (collapsible)-| + // Personal | Work (pinned) // View Pager // // Views after the search & recommendations are not bound by RelativelyLayout param. // To position them on the expected location, padding & margin are added to these views // Tabs should have a padding of the height of the search & recommendations container. - mPrimaryWorkTabsView.setPadding( - mPrimaryWorkTabsView.getPaddingLeft(), - topContainerHeight, - mPrimaryWorkTabsView.getPaddingRight(), - mPrimaryWorkTabsView.getPaddingBottom()); + RelativeLayout.LayoutParams tabsLayoutParams = + (RelativeLayout.LayoutParams) mPrimaryWorkTabsView.getLayoutParams(); + tabsLayoutParams.topMargin = topContainerHeight; + mPrimaryWorkTabsView.setLayoutParams(tabsLayoutParams); // Instead of setting the top offset directly, we split the top offset into two values: // 1. topOffsetAfterAllViewsCollapsed: this is the top offset after all collapsible @@ -124,39 +160,52 @@ final class SearchAndRecommendationsScrollController implements // // When the views are first inflated, the sum of topOffsetAfterAllViewsCollapsed and // mMaxCollapsibleDistance should equal to the top container height. - int tabsViewActualHeight = measureHeightWithVerticalMargins(mPrimaryWorkTabsView) - - mPrimaryWorkTabsView.getPaddingTop(); int topOffsetAfterAllViewsCollapsed = - topContainerHeight + tabsViewActualHeight - mMaxCollapsibleHeight; + topContainerHeight + mTabsHeight - mCollapsibleHeightForTabs; - RelativeLayout.LayoutParams layoutParams = + RelativeLayout.LayoutParams viewPagerLayoutParams = (RelativeLayout.LayoutParams) mPrimaryWorkViewPager.getLayoutParams(); - layoutParams.setMargins(0, topOffsetAfterAllViewsCollapsed, 0, 0); - mPrimaryWorkViewPager.setLayoutParams(layoutParams); - mPrimaryWorkViewPager.requestLayout(); + if (viewPagerLayoutParams.topMargin != topOffsetAfterAllViewsCollapsed) { + viewPagerLayoutParams.topMargin = topOffsetAfterAllViewsCollapsed; + mPrimaryWorkViewPager.setLayoutParams(viewPagerLayoutParams); + hasMarginOrPaddingUpdated = true; + } - mPrimaryRecyclerView.setPadding( - mPrimaryRecyclerView.getPaddingLeft(), - mMaxCollapsibleHeight, - mPrimaryRecyclerView.getPaddingRight(), - mPrimaryRecyclerView.getPaddingBottom()); - mWorkRecyclerView.setPadding( - mWorkRecyclerView.getPaddingLeft(), - mMaxCollapsibleHeight, - mWorkRecyclerView.getPaddingRight(), - mWorkRecyclerView.getPaddingBottom()); + if (mPrimaryRecyclerView.getPaddingTop() != mCollapsibleHeightForTabs) { + mPrimaryRecyclerView.setPadding( + mPrimaryRecyclerView.getPaddingLeft(), + mCollapsibleHeightForTabs, + mPrimaryRecyclerView.getPaddingRight(), + mPrimaryRecyclerView.getPaddingBottom()); + hasMarginOrPaddingUpdated = true; + } + if (mWorkRecyclerView.getPaddingTop() != mCollapsibleHeightForTabs) { + mWorkRecyclerView.setPadding( + mWorkRecyclerView.getPaddingLeft(), + mCollapsibleHeightForTabs, + mWorkRecyclerView.getPaddingRight(), + mWorkRecyclerView.getPaddingBottom()); + hasMarginOrPaddingUpdated = true; + } } else { - mPrimaryRecyclerView.setPadding( - mPrimaryRecyclerView.getPaddingLeft(), - topContainerHeight, - mPrimaryRecyclerView.getPaddingRight(), - mPrimaryRecyclerView.getPaddingBottom()); + if (mPrimaryRecyclerView.getPaddingTop() != topContainerHeight) { + mPrimaryRecyclerView.setPadding( + mPrimaryRecyclerView.getPaddingLeft(), + topContainerHeight, + mPrimaryRecyclerView.getPaddingRight(), + mPrimaryRecyclerView.getPaddingBottom()); + hasMarginOrPaddingUpdated = true; + } } - mSearchRecyclerView.setPadding( - mSearchRecyclerView.getPaddingLeft(), - topContainerHeight, - mSearchRecyclerView.getPaddingRight(), - mSearchRecyclerView.getPaddingBottom()); + if (mSearchRecyclerView.getPaddingTop() != topContainerHeight) { + mSearchRecyclerView.setPadding( + mSearchRecyclerView.getPaddingLeft(), + topContainerHeight, + mSearchRecyclerView.getPaddingRight(), + mSearchRecyclerView.getPaddingBottom()); + hasMarginOrPaddingUpdated = true; + } + return hasMarginOrPaddingUpdated; } /** @@ -168,13 +217,22 @@ final class SearchAndRecommendationsScrollController implements // Always use the recycler view offset because fast scroller offset has a different scale. int recyclerViewYOffset = mCurrentRecyclerView.getCurrentScrollY(); if (recyclerViewYOffset < 0) return; - if (mMaxCollapsibleHeight > 0) { - int yDisplacement = Math.max(-recyclerViewYOffset, -mMaxCollapsibleHeight); + + if (mCollapsibleHeightForRecommendation > 0) { + int yDisplacement = Math.max(-recyclerViewYOffset, + -mCollapsibleHeightForRecommendation); mViewHolder.mHeaderTitle.setTranslationY(yDisplacement); - mViewHolder.mSearchBar.setTranslationY(yDisplacement); - if (mHasWorkProfile) { - mPrimaryWorkTabsView.setTranslationY(yDisplacement); - } + mViewHolder.mRecommendedWidgetsTable.setTranslationY(yDisplacement); + } + + if (mCollapsibleHeightForSearch > 0) { + int searchYDisplacement = Math.max(-recyclerViewYOffset, -mCollapsibleHeightForSearch); + mViewHolder.mSearchBar.setTranslationY(searchYDisplacement); + } + + if (mHasWorkProfile && mCollapsibleHeightForTabs > 0) { + int yDisplacementForTabs = Math.max(-recyclerViewYOffset, -mCollapsibleHeightForTabs); + mPrimaryWorkTabsView.setTranslationY(yDisplacementForTabs); } } @@ -189,6 +247,9 @@ final class SearchAndRecommendationsScrollController implements /** private the height, in pixel, + the vertical margins of a given view. */ private static int measureHeightWithVerticalMargins(View view) { + if (view.getVisibility() != View.VISIBLE) { + return 0; + } MarginLayoutParams marginLayoutParams = (MarginLayoutParams) view.getLayoutParams(); return view.getMeasuredHeight() + marginLayoutParams.bottomMargin + marginLayoutParams.topMargin; diff --git a/src/com/android/launcher3/widget/picker/WidgetsFullSheet.java b/src/com/android/launcher3/widget/picker/WidgetsFullSheet.java index dc375ca414..1c34ece115 100644 --- a/src/com/android/launcher3/widget/picker/WidgetsFullSheet.java +++ b/src/com/android/launcher3/widget/picker/WidgetsFullSheet.java @@ -33,6 +33,7 @@ import android.util.SparseArray; import android.view.LayoutInflater; import android.view.MotionEvent; import android.view.View; +import android.view.ViewGroup; import android.view.animation.AnimationUtils; import android.view.animation.Interpolator; import android.widget.TextView; @@ -48,6 +49,7 @@ import com.android.launcher3.LauncherAppState; import com.android.launcher3.R; import com.android.launcher3.anim.PendingAnimation; import com.android.launcher3.compat.AccessibilityManagerCompat; +import com.android.launcher3.model.WidgetItem; import com.android.launcher3.views.RecyclerViewFastScroller; import com.android.launcher3.views.TopRoundedCornerView; import com.android.launcher3.widget.BaseWidgetSheet; @@ -55,6 +57,7 @@ import com.android.launcher3.widget.LauncherAppWidgetHost.ProviderChangedListene import com.android.launcher3.widget.model.WidgetsListBaseEntry; import com.android.launcher3.widget.picker.search.SearchModeListener; import com.android.launcher3.widget.picker.search.WidgetsSearchBar; +import com.android.launcher3.widget.util.WidgetsTableUtils; import com.android.launcher3.workprofile.PersonalWorkPagedView; import com.android.launcher3.workprofile.PersonalWorkSlidingTabStrip.OnActivePageChangedListener; @@ -68,10 +71,15 @@ import java.util.function.Predicate; public class WidgetsFullSheet extends BaseWidgetSheet implements Insettable, ProviderChangedListener, OnActivePageChangedListener, WidgetsRecyclerView.HeaderViewDimensionsProvider, SearchModeListener { + private static final String TAG = WidgetsFullSheet.class.getSimpleName(); private static final long DEFAULT_OPEN_DURATION = 267; private static final long FADE_IN_DURATION = 150; private static final float VERTICAL_START_POSITION = 0.3f; + // The widget recommendation table can easily take over the entire screen on devices with small + // resolution or landscape on phone. This ratio defines the max percentage of content area that + // the table can display. + private static final float RECOMMENDATION_TABLE_HEIGHT_RATIO = 0.75f; private final Rect mInsets = new Rect(); private final boolean mHasWorkProfile; @@ -81,10 +89,12 @@ public class WidgetsFullSheet extends BaseWidgetSheet mCurrentUser.equals(entry.mPkgItem.user); private final Predicate mWorkWidgetsFilter = mPrimaryWidgetsFilter.negate(); + private final int mTabsHeight; + private final int mWidgetCellHorizontalPadding; @Nullable private PersonalWorkPagedView mViewPager; - private int mInitialTabsHeight = 0; private boolean mIsInSearchMode; + private int mMaxSpansPerRow = 4; private View mTabsView; private TextView mNoWidgetsView; private SearchAndRecommendationViewHolder mSearchAndRecommendationViewHolder; @@ -96,6 +106,12 @@ public class WidgetsFullSheet extends BaseWidgetSheet mAdapters.put(AdapterHolder.PRIMARY, new AdapterHolder(AdapterHolder.PRIMARY)); mAdapters.put(AdapterHolder.WORK, new AdapterHolder(AdapterHolder.WORK)); mAdapters.put(AdapterHolder.SEARCH, new AdapterHolder(AdapterHolder.SEARCH)); + mTabsHeight = mHasWorkProfile + ? getContext().getResources() + .getDimensionPixelSize(R.dimen.all_apps_header_tab_height) + : 0; + mWidgetCellHorizontalPadding = 2 * getResources().getDimensionPixelOffset( + R.dimen.widget_cell_horizontal_padding); } public WidgetsFullSheet(Context context, AttributeSet attrs) { @@ -140,6 +156,7 @@ public class WidgetsFullSheet extends BaseWidgetSheet findViewById(R.id.search_and_recommendations_container)); mSearchAndRecommendationsScrollController = new SearchAndRecommendationsScrollController( mHasWorkProfile, + mTabsHeight, mSearchAndRecommendationViewHolder, findViewById(R.id.primary_widgets_list_view), mHasWorkProfile ? findViewById(R.id.work_widgets_list_view) : null, @@ -150,6 +167,7 @@ public class WidgetsFullSheet extends BaseWidgetSheet mNoWidgetsView = findViewById(R.id.no_widgets_text); + onRecommendedWidgetsBound(); onWidgetsBound(); mSearchAndRecommendationViewHolder.mSearchBar.initialize( @@ -257,6 +275,22 @@ public class WidgetsFullSheet extends BaseWidgetSheet @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + doMeasure(widthMeasureSpec, heightMeasureSpec); + + if (mSearchAndRecommendationsScrollController.updateMarginAndPadding()) { + doMeasure(widthMeasureSpec, heightMeasureSpec); + } + + if (updateMaxSpansPerRow()) { + doMeasure(widthMeasureSpec, heightMeasureSpec); + + if (mSearchAndRecommendationsScrollController.updateMarginAndPadding()) { + doMeasure(widthMeasureSpec, heightMeasureSpec); + } + } + } + + private void doMeasure(int widthMeasureSpec, int heightMeasureSpec) { DeviceProfile deviceProfile = mLauncher.getDeviceProfile(); int widthUsed; if (mInsets.bottom > 0) { @@ -272,24 +306,29 @@ public class WidgetsFullSheet extends BaseWidgetSheet widthUsed, heightMeasureSpec, heightUsed); setMeasuredDimension(MeasureSpec.getSize(widthMeasureSpec), MeasureSpec.getSize(heightMeasureSpec)); + } - int paddingPx = 2 * getResources().getDimensionPixelOffset( - R.dimen.widget_cell_horizontal_padding); - int maxSpansPerRow = getMeasuredWidth() / (deviceProfile.cellWidthPx + paddingPx); - mAdapters.get(AdapterHolder.PRIMARY).mWidgetsListAdapter.setMaxHorizontalSpansPerRow( - maxSpansPerRow); - mAdapters.get(AdapterHolder.SEARCH).mWidgetsListAdapter.setMaxHorizontalSpansPerRow( - maxSpansPerRow); - if (mHasWorkProfile) { - mAdapters.get(AdapterHolder.WORK).mWidgetsListAdapter.setMaxHorizontalSpansPerRow( - maxSpansPerRow); + /** Returns {@code true} if the max spans have been updated. */ + private boolean updateMaxSpansPerRow() { + if (getMeasuredWidth() == 0) return false; + + int previousMaxSpansPerRow = mMaxSpansPerRow; + mMaxSpansPerRow = getMeasuredWidth() + / (mLauncher.getDeviceProfile().cellWidthPx + mWidgetCellHorizontalPadding); + + if (previousMaxSpansPerRow != mMaxSpansPerRow) { + mAdapters.get(AdapterHolder.PRIMARY).mWidgetsListAdapter.setMaxHorizontalSpansPerRow( + mMaxSpansPerRow); + mAdapters.get(AdapterHolder.SEARCH).mWidgetsListAdapter.setMaxHorizontalSpansPerRow( + mMaxSpansPerRow); + if (mHasWorkProfile) { + mAdapters.get(AdapterHolder.WORK).mWidgetsListAdapter.setMaxHorizontalSpansPerRow( + mMaxSpansPerRow); + } + onRecommendedWidgetsBound(); + return true; } - - if (mInitialTabsHeight == 0 && mTabsView != null) { - mInitialTabsHeight = measureHeightWithVerticalMargins(mTabsView); - } - - mSearchAndRecommendationsScrollController.updateMarginAndPadding(); + return false; } @Override @@ -346,6 +385,8 @@ public class WidgetsFullSheet extends BaseWidgetSheet mViewPager.snapToPage(AdapterHolder.PRIMARY); } attachScrollbarToRecyclerView(mAdapters.get(AdapterHolder.PRIMARY).mWidgetsRecyclerView); + + mSearchAndRecommendationsScrollController.updateMarginAndPadding(); } @Override @@ -357,6 +398,8 @@ public class WidgetsFullSheet extends BaseWidgetSheet private void setViewVisibilityBasedOnSearch(boolean isInSearchMode) { mIsInSearchMode = isInSearchMode; + mSearchAndRecommendationViewHolder.mRecommendedWidgetsTable + .setVisibility(isInSearchMode ? GONE : VISIBLE); if (mHasWorkProfile) { mViewPager.setVisibility(isInSearchMode ? GONE : VISIBLE); mTabsView.setVisibility(isInSearchMode ? GONE : VISIBLE); @@ -374,6 +417,25 @@ public class WidgetsFullSheet extends BaseWidgetSheet mAdapters.get(AdapterHolder.WORK).mWidgetsListAdapter.resetExpandedHeader(); } + @Override + public void onRecommendedWidgetsBound() { + List recommendedWidgets = + mLauncher.getPopupDataProvider().getRecommendedWidgets(); + WidgetsRecommendationTableLayout table = + mSearchAndRecommendationViewHolder.mRecommendedWidgetsTable; + if (recommendedWidgets.size() > 0) { + float maxTableHeight = + (mLauncher.getDeviceProfile().heightPx - mTabsHeight - getHeaderViewHeight()) + * RECOMMENDATION_TABLE_HEIGHT_RATIO; + List> recommendedWidgetsInTable = + WidgetsTableUtils.groupWidgetItemsIntoTable(recommendedWidgets, + mMaxSpansPerRow); + table.setRecommendedWidgets(recommendedWidgetsInTable, maxTableHeight); + } else { + table.setVisibility(GONE); + } + } + private void open(boolean animate) { if (animate) { if (getPopupContainer().getInsets().bottom > 0) { @@ -534,20 +596,29 @@ public class WidgetsFullSheet extends BaseWidgetSheet mWidgetsRecyclerView.setEdgeEffectFactory( ((TopRoundedCornerView) mContent).createEdgeEffectFactory()); mWidgetsListAdapter.setApplyBitmapDeferred(false, mWidgetsRecyclerView); + mWidgetsListAdapter.setMaxHorizontalSpansPerRow(mMaxSpansPerRow); } } final class SearchAndRecommendationViewHolder { - final View mContainer; + final ViewGroup mContainer; final View mCollapseHandle; final WidgetsSearchBar mSearchBar; final TextView mHeaderTitle; + final WidgetsRecommendationTableLayout mRecommendedWidgetsTable; - SearchAndRecommendationViewHolder(View searchAndRecommendationContainer) { + SearchAndRecommendationViewHolder(ViewGroup searchAndRecommendationContainer) { mContainer = searchAndRecommendationContainer; mCollapseHandle = mContainer.findViewById(R.id.collapse_handle); mSearchBar = mContainer.findViewById(R.id.widgets_search_bar); mHeaderTitle = mContainer.findViewById(R.id.title); + mRecommendedWidgetsTable = mContainer.findViewById(R.id.recommended_widget_table); + mRecommendedWidgetsTable.setWidgetCellOnTouchListener((view, event) -> { + getRecyclerView().onTouchEvent(event); + return false; + }); + mRecommendedWidgetsTable.setWidgetCellLongClickListener(WidgetsFullSheet.this); + mRecommendedWidgetsTable.setWidgetCellOnClickListener(WidgetsFullSheet.this); } } } diff --git a/src/com/android/launcher3/widget/picker/WidgetsRecommendationTableLayout.java b/src/com/android/launcher3/widget/picker/WidgetsRecommendationTableLayout.java new file mode 100644 index 0000000000..6569fb0d48 --- /dev/null +++ b/src/com/android/launcher3/widget/picker/WidgetsRecommendationTableLayout.java @@ -0,0 +1,175 @@ +/* + * Copyright (C) 2021 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.widget.picker; + +import android.content.Context; +import android.util.AttributeSet; +import android.view.Gravity; +import android.view.LayoutInflater; +import android.view.ViewGroup; +import android.widget.TableLayout; +import android.widget.TableRow; + +import androidx.annotation.Nullable; + +import com.android.launcher3.DeviceProfile; +import com.android.launcher3.Launcher; +import com.android.launcher3.LauncherAppState; +import com.android.launcher3.R; +import com.android.launcher3.model.WidgetItem; +import com.android.launcher3.widget.WidgetCell; +import com.android.launcher3.widget.WidgetImageView; + +import java.util.ArrayList; +import java.util.List; + +/** A {@link TableLayout} for showing recommended widgets. */ +public final class WidgetsRecommendationTableLayout extends TableLayout { + private static final float SCALE_DOWN_RATIO = 0.9f; + private final DeviceProfile mDeviceProfile; + private final float mWidgetCellTextViewsHeight; + + private float mRecommendationTableMaxHeight = Float.MAX_VALUE; + @Nullable private OnLongClickListener mWidgetCellOnLongClickListener; + @Nullable private OnClickListener mWidgetCellOnClickListener; + @Nullable private OnTouchListener mWidgetCellOnTouchListener; + + public WidgetsRecommendationTableLayout(Context context) { + this(context, /* attrs= */ null); + } + + public WidgetsRecommendationTableLayout(Context context, AttributeSet attrs) { + super(context, attrs); + mDeviceProfile = Launcher.getLauncher(context).getDeviceProfile(); + // There are 1 row for title, 1 row for dimension and 2 rows for description. + mWidgetCellTextViewsHeight = 4 * getResources().getDimension(R.dimen.widget_cell_font_size); + } + + /** Sets a {@link android.view.View.OnLongClickListener} for all widget cells in this table. */ + public void setWidgetCellLongClickListener(OnLongClickListener onLongClickListener) { + mWidgetCellOnLongClickListener = onLongClickListener; + } + + /** Sets a {@link android.view.View.OnClickListener} for all widget cells in this table. */ + public void setWidgetCellOnClickListener(OnClickListener widgetCellOnClickListener) { + mWidgetCellOnClickListener = widgetCellOnClickListener; + } + + /** Sets a {@link android.view.View.OnTouchListener} for all widget cells in this table. */ + public void setWidgetCellOnTouchListener(OnTouchListener widgetCellOnTouchListener) { + mWidgetCellOnTouchListener = widgetCellOnTouchListener; + } + + /** + * Sets a list of recommended widgets that would like to be displayed in this table within the + * desired {@code recommendationTableMaxHeight}. + * + *

If the content can't fit {@code recommendationTableMaxHeight}, this view will remove a + * last row from the {@code recommendedWidgets} until it fits or only one row left. If the only + * row still doesn't fit, we scale down the preview image. + */ + public void setRecommendedWidgets(List> recommendedWidgets, + float recommendationTableMaxHeight) { + mRecommendationTableMaxHeight = recommendationTableMaxHeight; + RecommendationTableData data = fitRecommendedWidgetsToTableSpace(/* previewScale= */ 1f, + recommendedWidgets); + bindData(data); + } + + private void bindData(RecommendationTableData data) { + if (data.mRecommendationTable.size() == 0) { + setVisibility(GONE); + return; + } + + removeAllViews(); + + for (int i = 0; i < data.mRecommendationTable.size(); i++) { + List widgetItems = data.mRecommendationTable.get(i); + TableRow tableRow = new TableRow(getContext()); + tableRow.setGravity(Gravity.TOP); + + for (WidgetItem widgetItem : widgetItems) { + WidgetCell widgetCell = addItemCell(tableRow); + widgetCell.setPreviewSize(widgetItem.spanX, widgetItem.spanY, data.mPreviewScale); + widgetCell.applyFromCellItem(widgetItem, + LauncherAppState.getInstance(getContext()).getWidgetCache()); + widgetCell.ensurePreview(); + } + addView(tableRow); + } + setVisibility(VISIBLE); + } + + private WidgetCell addItemCell(ViewGroup parent) { + WidgetCell widget = (WidgetCell) LayoutInflater.from( + getContext()).inflate(R.layout.widget_cell, parent, false); + + widget.setOnTouchListener(mWidgetCellOnTouchListener); + WidgetImageView preview = widget.findViewById(R.id.widget_preview); + preview.setOnClickListener(mWidgetCellOnClickListener); + preview.setOnLongClickListener(mWidgetCellOnLongClickListener); + widget.setAnimatePreview(false); + + parent.addView(widget); + return widget; + } + + private RecommendationTableData fitRecommendedWidgetsToTableSpace( + float previewScale, + List> recommendedWidgetsInTable) { + // A naive estimation of the widgets recommendation table height without inflation. + float totalHeight = 0; + for (int i = 0; i < recommendedWidgetsInTable.size(); i++) { + List widgetItems = recommendedWidgetsInTable.get(i); + float rowHeight = 0; + for (int j = 0; j < widgetItems.size(); j++) { + float previewHeight = widgetItems.get(j).spanY * mDeviceProfile.allAppsCellHeightPx + * previewScale; + rowHeight = Math.max(rowHeight, previewHeight + mWidgetCellTextViewsHeight); + } + totalHeight += rowHeight; + } + + if (totalHeight < mRecommendationTableMaxHeight) { + return new RecommendationTableData(recommendedWidgetsInTable, previewScale); + } + + if (recommendedWidgetsInTable.size() > 1) { + // We don't want to scale down widgets preview unless we really need to. Reduce the + // num of row by 1 to see if it fits. + return fitRecommendedWidgetsToTableSpace( + previewScale, + recommendedWidgetsInTable.subList(/* fromIndex= */0, + /* toIndex= */recommendedWidgetsInTable.size() - 1)); + } + + float nextPreviewScale = previewScale * SCALE_DOWN_RATIO; + return fitRecommendedWidgetsToTableSpace(nextPreviewScale, recommendedWidgetsInTable); + } + + /** Data class for the widgets recommendation table and widgets preview scaling. */ + private class RecommendationTableData { + private final List> mRecommendationTable; + private final float mPreviewScale; + + RecommendationTableData(List> recommendationTable, + float previewScale) { + mRecommendationTable = recommendationTable; + mPreviewScale = previewScale; + } + } +} diff --git a/tests/tapl/com/android/launcher3/tapl/Widgets.java b/tests/tapl/com/android/launcher3/tapl/Widgets.java index 22f4d31bd5..fe4c71282f 100644 --- a/tests/tapl/com/android/launcher3/tapl/Widgets.java +++ b/tests/tapl/com/android/launcher3/tapl/Widgets.java @@ -30,7 +30,6 @@ import androidx.test.uiautomator.Until; import com.android.launcher3.testing.TestProtocol; import java.util.Collection; -import java.util.List; /** * All widgets container. @@ -106,7 +105,8 @@ public final class Widgets extends LauncherInstrumentation.VisibleContainer { fullWidgetsPicker.wait(Until.scrollable(true), WAIT_TIME_MS)); final Point displaySize = mLauncher.getRealDisplaySize(); - final UiObject2 widgetsContainer = findTestAppWidgetsTableContainer(); + Rect headerRect = new Rect(); + final UiObject2 widgetsContainer = findTestAppWidgetsTableContainer(headerRect); mLauncher.assertTrue("Can't locate widgets list for the test app: " + mLauncher.getLauncherPackageName(), widgetsContainer != null); @@ -132,7 +132,12 @@ public final class Widgets extends LauncherInstrumentation.VisibleContainer { mLauncher.assertTrue("Too many attempts", ++i <= 40); final int scroll = getWidgetsScroll(); - mLauncher.scrollToLastVisibleRow(fullWidgetsPicker, tableRows, 0); + mLauncher.scroll( + fullWidgetsPicker, + Direction.DOWN, + headerRect, + 10, + true); final int newScroll = getWidgetsScroll(); mLauncher.assertTrue( "Scrolled in a wrong direction in Widgets: from " + scroll + " to " @@ -143,7 +148,7 @@ public final class Widgets extends LauncherInstrumentation.VisibleContainer { } /** Finds the widgets list of this test app from the collapsed full widgets picker. */ - private UiObject2 findTestAppWidgetsTableContainer() { + private UiObject2 findTestAppWidgetsTableContainer(Rect outHeaderRect) { final BySelector headerSelector = By.res(mLauncher.getLauncherPackageName(), "widgets_list_header"); final BySelector targetAppSelector = By.clazz("android.widget.TextView").text( @@ -156,6 +161,7 @@ public final class Widgets extends LauncherInstrumentation.VisibleContainer { UiObject2 fullWidgetsPicker = verifyActiveContainer(); UiObject2 header = fullWidgetsPicker.findObject(headerSelector); + outHeaderRect.set(0, 0, 0, header.getVisibleBounds().height()); mLauncher.assertTrue("Can't find a widget header", header != null); // Look for a header that has the test app name. @@ -179,11 +185,14 @@ public final class Widgets extends LauncherInstrumentation.VisibleContainer { if (widgetsContainer != null) { return widgetsContainer; } - mLauncher.scrollToLastVisibleRow(fullWidgetsPicker, List.of(headerTitle), 0); - } else { - mLauncher.scrollToLastVisibleRow(fullWidgetsPicker, fullWidgetsPicker.getChildren(), - 0); + } + mLauncher.scroll( + fullWidgetsPicker, + Direction.DOWN, + outHeaderRect, + /* steps= */ 10, + /* slowDown= */ true); } return null;