Merge "Renders widget recommendations" into sc-dev am: 682af23651

Original change: https://googleplex-android-review.googlesource.com/c/platform/packages/apps/Launcher3/+/13790505

Change-Id: I6ce9dc364276fd2672f53300d8cb46f83361cc13
This commit is contained in:
Steven Ng
2021-03-19 23:26:05 +00:00
committed by Automerger Merge Worker
9 changed files with 421 additions and 85 deletions
+6 -5
View File
@@ -18,10 +18,11 @@
android:layout_height="wrap_content">
<!-- The image of the widget. This view does not support padding. Any placement adjustment
should be done using margins. -->
should be done using margins.
width & height are set at runtime after scaling the preview image. -->
<com.android.launcher3.widget.WidgetImageView
android:id="@+id/widget_preview"
android:layout_width="wrap_content"
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_weight="1"
android:importantForAccessibility="no"
@@ -38,7 +39,7 @@
android:singleLine="true"
android:maxLines="1"
android:textColor="?android:attr/textColorPrimary"
android:textSize="14sp" />
android:textSize="@dimen/widget_cell_font_size" />
<!-- The original dimensions of the widget (can't be the same text as above due to different
style. -->
@@ -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" />
<TextView
@@ -56,7 +57,7 @@
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center_horizontal"
android:textSize="14sp"
android:textSize="@dimen/widget_cell_font_size"
android:textColor="?android:attr/textColorTertiary"
android:maxLines="2"
android:ellipsize="end"
@@ -18,12 +18,15 @@
android:id="@+id/search_and_recommendations_container"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="16dp"
android:paddingHorizontal="16dp"
android:layout_marginBottom="16dp"
android:orientation="vertical">
<View
android:id="@+id/collapse_handle"
android:layout_width="48dp"
android:layout_height="2dp"
android:layout_marginTop="16dp"
android:elevation="2dp"
android:layout_gravity="center_horizontal"
android:background="?android:attr/textColorSecondary"/>
<TextView
@@ -36,4 +39,11 @@
android:textColor="?android:attr/textColorSecondary"
android:text="@string/widget_button_text"/>
<include layout="@layout/widgets_search_bar"/>
<com.android.launcher3.widget.picker.WidgetsRecommendationTableLayout
android:id="@+id/recommended_widget_table"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:visibility="gone" />
</LinearLayout>
+2 -1
View File
@@ -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">
<com.android.launcher3.ExtendedEditText
android:id="@+id/widgets_search_bar_edit_text"
+1
View File
@@ -109,6 +109,7 @@
<!-- Widget tray -->
<dimen name="widget_cell_vertical_padding">8dp</dimen>
<dimen name="widget_cell_horizontal_padding">16dp</dimen>
<dimen name="widget_cell_font_size">14sp</dimen>
<dimen name="widget_list_top_bottom_corner_radius">28dp</dimen>
@@ -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
@@ -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.
*
* <p>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;
@@ -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<WidgetsListBaseEntry> 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<WidgetItem> recommendedWidgets =
mLauncher.getPopupDataProvider().getRecommendedWidgets();
WidgetsRecommendationTableLayout table =
mSearchAndRecommendationViewHolder.mRecommendedWidgetsTable;
if (recommendedWidgets.size() > 0) {
float maxTableHeight =
(mLauncher.getDeviceProfile().heightPx - mTabsHeight - getHeaderViewHeight())
* RECOMMENDATION_TABLE_HEIGHT_RATIO;
List<ArrayList<WidgetItem>> 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);
}
}
}
@@ -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}.
*
* <p>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<ArrayList<WidgetItem>> 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<WidgetItem> 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<ArrayList<WidgetItem>> recommendedWidgetsInTable) {
// A naive estimation of the widgets recommendation table height without inflation.
float totalHeight = 0;
for (int i = 0; i < recommendedWidgetsInTable.size(); i++) {
List<WidgetItem> 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<ArrayList<WidgetItem>> mRecommendationTable;
private final float mPreviewScale;
RecommendationTableData(List<ArrayList<WidgetItem>> recommendationTable,
float previewScale) {
mRecommendationTable = recommendationTable;
mPreviewScale = previewScale;
}
}
}
@@ -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;