Merge changes from topic "categories" into main

* changes:
  Initial UI changes for displaying categorized suggestions.
  Use full-width two picker in both orientations in tablets
  Use derived padding instead of static 300dp for large portrait displays
  Align items in the center within each table row within suggestions.
This commit is contained in:
Shamali Patwa
2024-02-20 17:42:31 +00:00
committed by Android (Google) Code Review
20 changed files with 538 additions and 109 deletions
+58
View File
@@ -0,0 +1,58 @@
<?xml version="1.0" encoding="utf-8"?><!--
~ Copyright (C) 2024 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.
-->
<merge xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:launcher="http://schemas.android.com/apk/res-auto">
<!--
Shown when there are more than one pages
Note: on page change, using accessibility live region lets user know that the title has changed.
-->
<TextView
android:id="@+id/recommendations_page_title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="12dp"
android:layout_marginTop="16dp"
android:accessibilityLiveRegion="polite"
android:gravity="center_horizontal"
android:lineHeight="20sp"
android:textColor="?attr/widgetPickerTitleColor"
android:textFontWeight="500"
android:textSize="16sp"
android:visibility="gone" />
<!-- Shown when there are more than one pages -->
<com.android.launcher3.pageindicators.PageIndicatorDots
android:id="@+id/widget_recommendations_page_indicator"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:elevation="1dp"
android:visibility="gone" />
<!--
Note: importantForAccessibility = yes on this view ensures that with talkback, when user
swipes right on the last item in current page, they are taken to the next page. And, doing
the same on the last page, takes them to the next section e.g. apps list in single pane
picker.
-->
<com.android.launcher3.widget.picker.WidgetRecommendationsView
android:id="@+id/widget_recommendations_view"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:background="@drawable/widgets_surface_background"
android:importantForAccessibility="yes"
launcher:pageIndicator="@+id/widget_recommendations_page_indicator" />
</merge>
@@ -0,0 +1,21 @@
<?xml version="1.0" encoding="utf-8"?><!--
~ Copyright (C) 2024 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.
-->
<com.android.launcher3.widget.picker.WidgetsRecommendationTableLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingHorizontal="@dimen/widget_recommendations_table_horizontal_padding"
android:paddingVertical="@dimen/widget_recommendations_table_vertical_padding" />
+8 -5
View File
@@ -73,15 +73,18 @@
<include layout="@layout/widgets_search_bar" />
</FrameLayout>
<com.android.launcher3.widget.picker.WidgetsRecommendationTableLayout
android:id="@+id/recommended_widget_table"
<!-- Shown when there are recommendations to display -->
<LinearLayout
android:id="@+id/widget_recommendations_container"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:layout_marginHorizontal="@dimen/widget_list_horizontal_margin"
android:background="@drawable/widgets_surface_background"
android:paddingVertical="@dimen/recommended_widgets_table_vertical_padding"
android:visibility="gone" />
android:orientation="vertical"
android:layout_marginHorizontal="@dimen/widget_list_horizontal_margin"
android:visibility="gone">
<include layout="@layout/widget_recommendations" />
</LinearLayout>
<com.android.launcher3.workprofile.PersonalWorkSlidingTabStrip
android:id="@+id/tabs"
@@ -56,14 +56,17 @@
<include layout="@layout/widgets_search_bar" />
</FrameLayout>
<com.android.launcher3.widget.picker.WidgetsRecommendationTableLayout
android:id="@+id/recommended_widget_table"
<!-- Shown when there are recommendations to display -->
<LinearLayout
android:id="@+id/widget_recommendations_container"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:background="@drawable/widgets_surface_background"
android:paddingVertical="@dimen/recommended_widgets_table_vertical_padding"
android:visibility="gone" />
android:orientation="vertical"
android:visibility="gone">
<include layout="@layout/widget_recommendations" />
</LinearLayout>
</com.android.launcher3.views.StickyHeaderLayout>
</merge>
+8 -5
View File
@@ -118,13 +118,16 @@
android:background="@drawable/widgets_surface_background"
android:importantForAccessibility="yes"
android:id="@+id/right_pane">
<com.android.launcher3.widget.picker.WidgetsRecommendationTableLayout
android:id="@+id/recommended_widget_table"
<!-- Shown when there are recommendations to display -->
<LinearLayout
android:id="@+id/widget_recommendations_container"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingHorizontal=
"@dimen/widget_list_horizontal_margin_two_pane"
android:visibility="gone" />
android:background="@drawable/widgets_surface_background"
android:orientation="vertical"
android:visibility="gone">
<include layout="@layout/widget_recommendations" />
</LinearLayout>
</LinearLayout>
</ScrollView>
</FrameLayout>
-3
View File
@@ -32,7 +32,4 @@
<!-- Widget picker-->
<dimen name="widget_list_horizontal_margin">49dp</dimen>
<dimen name="widget_list_horizontal_margin_two_pane">24dp</dimen>
<!-- Bottom sheet-->
<dimen name="bottom_sheet_extra_top_padding">0dp</dimen>
</resources>
-3
View File
@@ -38,9 +38,6 @@
<!-- Widget picker-->
<dimen name="widget_list_horizontal_margin">30dp</dimen>
<!-- Bottom sheet-->
<dimen name="bottom_sheet_extra_top_padding">300dp</dimen>
<!-- Folder spaces -->
<dimen name="folder_footer_horiz_padding">24dp</dimen>
</resources>
+2 -4
View File
@@ -182,9 +182,8 @@
<dimen name="widget_apps_tabs_vertical_padding">6dp</dimen>
<dimen name="widget_picker_landscape_tablet_left_right_margin">117dp</dimen>
<dimen name="widget_picker_two_panels_left_right_margin">0dp</dimen>
<dimen name="recommended_widgets_table_vertical_padding">8dp</dimen>
<dimen name="widget_recommendations_table_vertical_padding">8dp</dimen>
<dimen name="widget_recommendations_table_horizontal_padding">16dp</dimen>
<!-- Bottom margin for the search and recommended widgets container without work profile -->
<dimen name="search_and_recommended_widgets_container_bottom_margin">16dp</dimen>
<!-- Bottom margin for the search and recommended widgets container with work profile -->
@@ -454,7 +453,6 @@
<dimen name="padded_rounded_button_padding">8dp</dimen>
<!-- Bottom sheet related parameters -->
<dimen name="bottom_sheet_extra_top_padding">0dp</dimen>
<dimen name="bottom_sheet_handle_area_height">36dp</dimen>
<dimen name="bottom_sheet_handle_width">32dp</dimen>
<dimen name="bottom_sheet_handle_height">4dp</dimen>
+1 -1
View File
@@ -71,7 +71,7 @@
<!-- Widget suggestions header title in the full widgets picker for large screen devices
in landscape mode. [CHAR_LIMIT=50] -->
<string name="suggested_widgets_header_title">Suggestions</string>
<string name="productivity_widget_recommendation_category_label">Boost your day</string>
<string name="productivity_widget_recommendation_category_label">Your Daily Essentials</string>
<string name="news_widget_recommendation_category_label">News For You</string>
<string name="social_and_entertainment_widget_recommendation_category_label">Your Chill Zone</string>
<string name="fitness_widget_recommendation_category_label">Reach Your Fitness Goals</string>
+12 -9
View File
@@ -416,15 +416,18 @@ public class DeviceProfile {
gridVisualizationPaddingY = res.getDimensionPixelSize(
R.dimen.grid_visualization_vertical_cell_spacing);
// Tablet portrait mode uses a single pane widget picker and extra padding may be applied on
// top to avoid making it look too elongated.
final boolean applyExtraTopPadding = isTablet
&& !isLandscape
&& (aspectRatio > MIN_ASPECT_RATIO_FOR_EXTRA_TOP_PADDING);
bottomSheetTopPadding = mInsets.top // statusbar height
+ (applyExtraTopPadding ? res.getDimensionPixelSize(
R.dimen.bottom_sheet_extra_top_padding) : 0)
+ (isTablet ? 0 : edgeMarginPx); // phones need edgeMarginPx additional padding
{
// In large screens, in portrait mode, a bottom sheet can appear too elongated, so, we
// apply additional padding.
final boolean applyExtraTopPadding = isTablet
&& !isLandscape
&& (aspectRatio > MIN_ASPECT_RATIO_FOR_EXTRA_TOP_PADDING);
final int derivedTopPadding = heightPx / 6;
bottomSheetTopPadding = mInsets.top // statusbar height
+ (applyExtraTopPadding ? derivedTopPadding : 0)
+ (isTablet ? 0 : edgeMarginPx); // phones need edgeMarginPx additional padding
}
bottomSheetOpenDuration = res.getInteger(R.integer.config_bottomSheetOpenDuration);
bottomSheetCloseDuration = res.getInteger(R.integer.config_bottomSheetCloseDuration);
if (isTablet) {
@@ -367,7 +367,7 @@ public class PageIndicatorDots extends View implements Insettable, PageIndicator
mNumPages = numMarkers;
// If the last page gets removed we want to go to the previous page.
if (mNumPages == mActivePage) {
if (mNumPages > 0 && mNumPages == mActivePage) {
mActivePage--;
CURRENT_POSITION.set(this, (float) mActivePage);
}
@@ -222,6 +222,7 @@ public class PopupDataProvider implements NotificationListener.NotificationsChan
}
/** Returns the recommended widgets mapped by their category. */
@NonNull
public Map<WidgetRecommendationCategory, List<WidgetItem>> getCategorizedRecommendedWidgets() {
Map<ComponentKey, WidgetItem> allWidgetItems = mAllWidgets.stream()
.filter(entry -> entry instanceof WidgetsListContentEntry)
@@ -232,7 +233,8 @@ public class PopupDataProvider implements NotificationListener.NotificationsChan
Function.identity()
));
return mRecommendedWidgets.stream()
.filter(itemInfo -> itemInfo instanceof PendingAddWidgetInfo)
.filter(itemInfo -> itemInfo instanceof PendingAddWidgetInfo
&& ((PendingAddWidgetInfo) itemInfo).recommendationCategory != null)
.collect(Collectors.groupingBy(
it -> ((PendingAddWidgetInfo) it).recommendationCategory,
Collectors.collectingAndThen(
@@ -16,6 +16,7 @@
package com.android.launcher3.widget;
import static com.android.app.animation.Interpolators.EMPHASIZED;
import static com.android.launcher3.Flags.enableCategorizedWidgetSuggestions;
import static com.android.launcher3.Flags.enableUnfoldedTwoPanePicker;
import static com.android.launcher3.LauncherPrefs.WIDGETS_EDUCATION_TIP_SEEN;
@@ -62,8 +63,10 @@ public abstract class BaseWidgetSheet extends AbstractSlideInView<BaseActivity>
protected final Rect mInsets = new Rect();
@Px protected int mContentHorizontalMargin;
@Px protected int mWidgetCellHorizontalPadding;
@Px
protected int mContentHorizontalMargin;
@Px
protected int mWidgetCellHorizontalPadding;
protected int mNavBarScrimHeight;
private final Paint mNavBarScrimPaint;
@@ -196,7 +199,7 @@ public abstract class BaseWidgetSheet extends AbstractSlideInView<BaseActivity>
DeviceProfile deviceProfile = mActivityContext.getDeviceProfile();
int widthUsed;
if (deviceProfile.isTablet) {
widthUsed = Math.max(2 * getTabletMargin(deviceProfile),
widthUsed = Math.max(2 * getTabletHorizontalMargin(deviceProfile),
2 * (mInsets.left + mInsets.right));
} else if (mInsets.bottom > 0) {
widthUsed = mInsets.left + mInsets.right;
@@ -212,7 +215,11 @@ public abstract class BaseWidgetSheet extends AbstractSlideInView<BaseActivity>
MeasureSpec.getSize(heightMeasureSpec));
}
private int getTabletMargin(DeviceProfile deviceProfile) {
private int getTabletHorizontalMargin(DeviceProfile deviceProfile) {
// All bottom-sheets showing widgets will be full-width across all devices.
if (enableCategorizedWidgetSuggestions()) {
return 0;
}
if (deviceProfile.isLandscape && !deviceProfile.isTwoPanels) {
return getResources().getDimensionPixelSize(
R.dimen.widget_picker_landscape_tablet_left_right_margin);
@@ -367,6 +367,16 @@ public class WidgetCell extends LinearLayout {
}
}
/**
* Shows or hides the long description displayed below each widget.
*
* @param show a flag that shows the long description of the widget if {@code true}, hides it if
* {@code false}.
*/
public void showDescription(boolean show) {
mWidgetDescription.setVisibility(show ? VISIBLE : GONE);
}
@Override
public boolean onTouchEvent(MotionEvent ev) {
super.onTouchEvent(ev);
@@ -16,6 +16,7 @@
package com.android.launcher3.widget;
import static com.android.launcher3.Flags.enableCategorizedWidgetSuggestions;
import static com.android.launcher3.LauncherSettings.Favorites.CONTAINER_BOTTOM_WIDGETS_TRAY;
import android.content.Context;
@@ -187,7 +188,13 @@ public class WidgetsBottomSheet extends BaseWidgetSheet {
mWidgetCellHorizontalPadding)
.forEach(row -> {
TableRow tableRow = new TableRow(getContext());
tableRow.setGravity(Gravity.TOP);
if (enableCategorizedWidgetSuggestions()) {
// Vertically center align items, so that even if they don't fill bounds,
// they can look organized when placed together in a row.
tableRow.setGravity(Gravity.CENTER_VERTICAL);
} else {
tableRow.setGravity(Gravity.TOP);
}
row.forEach(widgetItem -> {
WidgetCell widget = addItemCell(tableRow);
widget.applyFromCellItem(widgetItem);
@@ -0,0 +1,258 @@
/*
* Copyright (C) 2024 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 static com.android.launcher3.widget.util.WidgetsTableUtils.groupWidgetItemsUsingRowPxWithoutReordering;
import android.content.Context;
import android.util.AttributeSet;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;
import androidx.annotation.Nullable;
import androidx.annotation.Px;
import com.android.launcher3.DeviceProfile;
import com.android.launcher3.Launcher;
import com.android.launcher3.PagedView;
import com.android.launcher3.R;
import com.android.launcher3.model.WidgetItem;
import com.android.launcher3.pageindicators.PageIndicatorDots;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;
/**
* A {@link PagedView} that displays widget recommendations in categories with dots as paged
* indicators.
*/
public final class WidgetRecommendationsView extends PagedView<PageIndicatorDots> {
private @Px float mAvailableHeight = Float.MAX_VALUE;
private static final int MAX_CATEGORIES = 3;
private TextView mRecommendationPageTitle;
private final List<String> mCategoryTitles = new ArrayList<>();
@Nullable
private OnLongClickListener mWidgetCellOnLongClickListener;
@Nullable
private OnClickListener mWidgetCellOnClickListener;
public WidgetRecommendationsView(Context context) {
this(context, /* attrs= */ null);
}
public WidgetRecommendationsView(Context context, AttributeSet attrs) {
this(context, attrs, /* defStyleAttr= */ 0);
}
public WidgetRecommendationsView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
}
@Override
public void initParentViews(View parent) {
super.initParentViews(parent);
mRecommendationPageTitle = parent.findViewById(R.id.recommendations_page_title);
}
/** 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;
}
/**
* Displays all the provided recommendations in a single table if they fit.
*
* @param recommendedWidgets list of widgets to be displayed in recommendation section.
* @param availableHeight height in px that can be used to display the recommendations;
* recommendations that don't fit in this height won't be shown
* @param availableWidth width in px that the recommendations should display in
* @param cellPadding padding in px that should be applied to each widget in the
* recommendations
* @return {@code false} if no recommendations could fit in the available space.
*/
public boolean setRecommendations(
List<WidgetItem> recommendedWidgets, final @Px float availableHeight,
final @Px int availableWidth, final @Px int cellPadding) {
this.mAvailableHeight = availableHeight;
removeAllViews();
maybeDisplayInTable(recommendedWidgets, availableWidth, cellPadding);
updateTitleAndIndicator();
return getChildCount() > 0;
}
/**
* Displays the recommendations grouped by categories as pages.
* <p>In case of a single category, no title is displayed for it.</p>
*
* @param recommendations a map of widget items per recommendation category
* @param availableHeight height in px that can be used to display the recommendations;
* recommendations that don't fit in this height won't be shown
* @param availableWidth width in px that the recommendations should display in
* @param cellPadding padding in px that should be applied to each widget in the
* recommendations
* @return {@code false} if no recommendations could fit in the available space.
*/
public boolean setRecommendations(
Map<WidgetRecommendationCategory, List<WidgetItem>> recommendations,
final @Px float availableHeight, final @Px int availableWidth,
final @Px int cellPadding) {
this.mAvailableHeight = availableHeight;
Context context = getContext();
removeAllViews();
int displayedCategories = 0;
// Render top MAX_CATEGORIES in separate tables. Each table becomes a page.
for (Map.Entry<WidgetRecommendationCategory, List<WidgetItem>> entry :
new TreeMap<>(recommendations).entrySet()) {
// If none of the recommendations for the category could fit in the mAvailableHeight, we
// don't want to add that category; and we look for the next one.
if (maybeDisplayInTable(entry.getValue(), availableWidth, cellPadding)) {
mCategoryTitles.add(
context.getResources().getString(entry.getKey().categoryTitleRes));
displayedCategories++;
}
if (displayedCategories == MAX_CATEGORIES) {
break;
}
}
updateTitleAndIndicator();
return getChildCount() > 0;
}
/** Displays the page title and paging indicator if there are multiple pages. */
private void updateTitleAndIndicator() {
int titleAndIndicatorVisibility = getPageCount() > 1 ? View.VISIBLE : View.GONE;
mRecommendationPageTitle.setVisibility(titleAndIndicatorVisibility);
mPageIndicator.setVisibility(titleAndIndicatorVisibility);
}
@Override
protected void notifyPageSwitchListener(int prevPage) {
if (getPageCount() > 1) {
// Since the title is outside the paging scroll, we update the title on page switch.
mRecommendationPageTitle.setText(mCategoryTitles.get(getNextPage()));
super.notifyPageSwitchListener(prevPage);
requestLayout();
}
}
@Override
protected boolean canScroll(float absVScroll, float absHScroll) {
// Allow only horizontal scroll.
return (absHScroll > absVScroll) && super.canScroll(absVScroll, absHScroll);
}
@Override
protected void onScrollChanged(int l, int t, int oldl, int oldt) {
super.onScrollChanged(l, t, oldl, oldt);
mPageIndicator.setScroll(l, mMaxScroll);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
boolean hasMultiplePages = getChildCount() > 0;
if (hasMultiplePages) {
int finalWidth = MeasureSpec.getSize(widthMeasureSpec);
int desiredHeight = 0;
for (int i = 0; i < getChildCount(); i++) {
View child = getChildAt(i);
measureChild(child, widthMeasureSpec, heightMeasureSpec);
if (mAvailableHeight == Float.MAX_VALUE) {
// When we are not limited by height, use currentPage's height. This is the case
// when the paged layout is placed in a scrollable container. We cannot use
// height
// of tallest child in such case, as it will display a scrollbar even for
// smaller
// pages that don't have more content.
if (i == mCurrentPage) {
int parentHeight = MeasureSpec.getSize(heightMeasureSpec);
desiredHeight = Math.max(parentHeight, child.getMeasuredHeight());
}
} else {
// Use height of tallest child when we are limited to a certain height.
desiredHeight = Math.max(desiredHeight, child.getMeasuredHeight());
}
}
int finalHeight = resolveSizeAndState(desiredHeight, heightMeasureSpec, 0);
setMeasuredDimension(finalWidth, finalHeight);
} else {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
}
}
/**
* Groups the provided recommendations into rows and displays them in a table if at least one
* fits.
* <p>Returns false if none of the recommendations could fit.</p>
*/
private boolean maybeDisplayInTable(List<WidgetItem> recommendedWidgets,
final @Px int availableWidth, final @Px int cellPadding) {
Context context = getContext();
DeviceProfile deviceProfile = Launcher.getLauncher(context).getDeviceProfile();
LayoutInflater inflater = LayoutInflater.from(context);
List<ArrayList<WidgetItem>> rows = groupWidgetItemsUsingRowPxWithoutReordering(
recommendedWidgets,
context,
deviceProfile,
availableWidth,
cellPadding);
WidgetsRecommendationTableLayout recommendationsTable =
(WidgetsRecommendationTableLayout) inflater.inflate(
R.layout.widget_recommendations_table,
/* root=*/ this,
/* attachToRoot=*/ false);
recommendationsTable.setWidgetCellOnClickListener(mWidgetCellOnClickListener);
recommendationsTable.setWidgetCellLongClickListener(mWidgetCellOnLongClickListener);
boolean displayedAtLeastOne = recommendationsTable.setRecommendedWidgets(rows,
mAvailableHeight);
if (displayedAtLeastOne) {
addView(recommendationsTable);
}
return displayedAtLeastOne;
}
/** Returns location of a widget cell for displaying the "touch and hold" education tip. */
public View getViewForEducationTip() {
if (getChildCount() > 0) {
// first page (a table layout) -> first item (a widget cell).
return ((ViewGroup) getChildAt(0)).getChildAt(0);
}
return null;
}
}
@@ -17,6 +17,7 @@ package com.android.launcher3.widget.picker;
import static android.view.View.MeasureSpec.makeMeasureSpec;
import static com.android.launcher3.Flags.enableCategorizedWidgetSuggestions;
import static com.android.launcher3.Flags.enableUnfoldedTwoPanePicker;
import static com.android.launcher3.LauncherAnimUtils.VIEW_TRANSLATE_Y;
import static com.android.launcher3.LauncherPrefs.WIDGETS_EDUCATION_DIALOG_SEEN;
@@ -45,6 +46,7 @@ import android.view.WindowInsetsController;
import android.view.animation.AnimationUtils;
import android.view.animation.Interpolator;
import android.widget.Button;
import android.widget.LinearLayout;
import android.widget.TextView;
import android.window.BackEvent;
@@ -65,7 +67,6 @@ import com.android.launcher3.Utilities;
import com.android.launcher3.anim.PendingAnimation;
import com.android.launcher3.compat.AccessibilityManagerCompat;
import com.android.launcher3.model.UserManagerState;
import com.android.launcher3.model.WidgetItem;
import com.android.launcher3.pm.UserCache;
import com.android.launcher3.views.ArrowTipView;
import com.android.launcher3.views.RecyclerViewFastScroller;
@@ -76,7 +77,6 @@ import com.android.launcher3.widget.BaseWidgetSheet;
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;
@@ -162,12 +162,13 @@ public class WidgetsFullSheet extends BaseWidgetSheet
@Nullable PersonalWorkPagedView mViewPager;
private boolean mIsInSearchMode;
private boolean mIsNoWidgetsViewNeeded;
@Px private int mMaxSpanPerRow;
@Px protected int mMaxSpanPerRow;
protected DeviceProfile mDeviceProfile;
protected TextView mNoWidgetsView;
protected StickyHeaderLayout mSearchScrollView;
protected WidgetsRecommendationTableLayout mRecommendedWidgetsTable;
protected WidgetRecommendationsView mWidgetRecommendationsView;
protected LinearLayout mWidgetRecommendationsContainer;
protected View mTabBar;
protected View mSearchBarContainer;
protected WidgetsSearchBar mSearchBar;
@@ -221,9 +222,14 @@ public class WidgetsFullSheet extends BaseWidgetSheet
setupViews();
mRecommendedWidgetsTable = mSearchScrollView.findViewById(R.id.recommended_widget_table);
mRecommendedWidgetsTable.setWidgetCellLongClickListener(this);
mRecommendedWidgetsTable.setWidgetCellOnClickListener(this);
mWidgetRecommendationsContainer = mSearchScrollView.findViewById(
R.id.widget_recommendations_container);
mWidgetRecommendationsView = mSearchScrollView.findViewById(
R.id.widget_recommendations_view);
mWidgetRecommendationsView.initParentViews(mWidgetRecommendationsContainer);
mWidgetRecommendationsView.setWidgetCellLongClickListener(this);
mWidgetRecommendationsView.setWidgetCellOnClickListener(this);
mHeaderTitle = mSearchScrollView.findViewById(R.id.title);
onRecommendedWidgetsBound();
@@ -551,7 +557,7 @@ public class WidgetsFullSheet extends BaseWidgetSheet
protected void setViewVisibilityBasedOnSearch(boolean isInSearchMode) {
mIsInSearchMode = isInSearchMode;
if (isInSearchMode) {
mRecommendedWidgetsTable.setVisibility(GONE);
mWidgetRecommendationsContainer.setVisibility(GONE);
if (mHasWorkProfile) {
mViewPager.setVisibility(GONE);
mTabBar.setVisibility(GONE);
@@ -580,40 +586,44 @@ public class WidgetsFullSheet extends BaseWidgetSheet
if (mIsInSearchMode) {
return;
}
List<WidgetItem> recommendedWidgets =
mActivityContext.getPopupDataProvider().getRecommendedWidgets();
mHasRecommendedWidgets = recommendedWidgets.size() > 0;
if (mHasRecommendedWidgets) {
float noWidgetsViewHeight = 0;
if (mIsNoWidgetsViewNeeded) {
// Make sure recommended section leaves enough space for noWidgetsView.
Rect noWidgetsViewTextBounds = new Rect();
mNoWidgetsView.getPaint()
.getTextBounds(mNoWidgetsView.getText().toString(), /* start= */ 0,
mNoWidgetsView.getText().length(), noWidgetsViewTextBounds);
noWidgetsViewHeight = noWidgetsViewTextBounds.height();
}
if (!isTwoPane()) {
doMeasure(
makeMeasureSpec(mActivityContext.getDeviceProfile().availableWidthPx,
MeasureSpec.EXACTLY),
makeMeasureSpec(mActivityContext.getDeviceProfile().availableHeightPx,
MeasureSpec.EXACTLY));
}
float maxTableHeight = getMaxTableHeight(noWidgetsViewHeight);
List<ArrayList<WidgetItem>> recommendedWidgetsInTable =
WidgetsTableUtils.groupWidgetItemsUsingRowPxWithoutReordering(
recommendedWidgets,
mActivityContext,
mActivityContext.getDeviceProfile(),
mMaxSpanPerRow,
mWidgetCellHorizontalPadding);
mRecommendedWidgetsTable.setRecommendedWidgets(
recommendedWidgetsInTable, maxTableHeight);
if (enableCategorizedWidgetSuggestions()) {
mHasRecommendedWidgets = mWidgetRecommendationsView.setRecommendations(
mActivityContext.getPopupDataProvider().getCategorizedRecommendedWidgets(),
/* availableHeight= */ getMaxAvailableHeightForRecommendations(),
/* availableWidth= */ mMaxSpanPerRow,
/* cellPadding= */ mWidgetCellHorizontalPadding
);
} else {
mRecommendedWidgetsTable.setVisibility(GONE);
mHasRecommendedWidgets = mWidgetRecommendationsView.setRecommendations(
mActivityContext.getPopupDataProvider().getRecommendedWidgets(),
/* availableHeight= */ getMaxAvailableHeightForRecommendations(),
/* availableWidth= */ mMaxSpanPerRow,
/* cellPadding= */ mWidgetCellHorizontalPadding
);
}
mWidgetRecommendationsContainer.setVisibility(mHasRecommendedWidgets ? VISIBLE : GONE);
}
@Px
private float getMaxAvailableHeightForRecommendations() {
float noWidgetsViewHeight = 0;
if (mIsNoWidgetsViewNeeded) {
// Make sure recommended section leaves enough space for noWidgetsView.
Rect noWidgetsViewTextBounds = new Rect();
mNoWidgetsView.getPaint()
.getTextBounds(mNoWidgetsView.getText().toString(), /* start= */ 0,
mNoWidgetsView.getText().length(), noWidgetsViewTextBounds);
noWidgetsViewHeight = noWidgetsViewTextBounds.height();
}
if (!isTwoPane()) {
doMeasure(
makeMeasureSpec(mActivityContext.getDeviceProfile().availableWidthPx,
MeasureSpec.EXACTLY),
makeMeasureSpec(mActivityContext.getDeviceProfile().availableHeightPx,
MeasureSpec.EXACTLY));
}
return getMaxTableHeight(noWidgetsViewHeight);
}
/** b/209579563: "Widgets" header should be focused first. */
@@ -622,7 +632,8 @@ public class WidgetsFullSheet extends BaseWidgetSheet
return mHeaderTitle;
}
protected float getMaxTableHeight(float noWidgetsViewHeight) {
@Px
protected float getMaxTableHeight(@Px float noWidgetsViewHeight) {
return (mContent.getMeasuredHeight()
- mTabsHeight - getHeaderViewHeight()
- noWidgetsViewHeight)
@@ -699,7 +710,9 @@ public class WidgetsFullSheet extends BaseWidgetSheet
private static int getWidgetSheetId(BaseActivity activity) {
boolean isTwoPane = (activity.getDeviceProfile().isTablet
&& activity.getDeviceProfile().isLandscape
// Enables two pane picker for tablets in all orientations when the
// enableCategorizedWidgetSuggestions flag is on.
&& (activity.getDeviceProfile().isLandscape || enableCategorizedWidgetSuggestions())
&& !activity.getDeviceProfile().isTwoPanels)
// Enables two pane picker for unfolded foldables if the flag is on.
|| (activity.getDeviceProfile().isTwoPanels && enableUnfoldedTwoPanePicker());
@@ -813,28 +826,40 @@ public class WidgetsFullSheet extends BaseWidgetSheet
public void onDeviceProfileChanged(DeviceProfile dp) {
super.onDeviceProfileChanged(dp);
if (mDeviceProfile.isLandscape != dp.isLandscape && dp.isTablet && !dp.isTwoPanels) {
handleClose(false);
show(BaseActivity.fromContext(getContext()), false);
} else if (!isTwoPane()) {
reset();
resetExpandedHeaders();
}
// When folding/unfolding the foldables, we need to switch between the regular widget picker
// and the two pane picker, so we rebuild the picker with the correct layout.
if (mDeviceProfile.isTwoPanels != dp.isTwoPanels && enableUnfoldedTwoPanePicker()) {
if (shouldRecreateLayout(/*oldDp=*/ mDeviceProfile, /*newDp=*/ dp)) {
SparseArray<Parcelable> widgetsState = new SparseArray<>();
saveHierarchyState(widgetsState);
handleClose(false);
WidgetsFullSheet sheet = show(BaseActivity.fromContext(getContext()), false);
sheet.restoreHierarchyState(widgetsState);
sheet.restorePreviousAdapterHolderType(getCurrentAdapterHolderType());
} else if (!isTwoPane()) {
reset();
resetExpandedHeaders();
}
mDeviceProfile = dp;
}
/**
* Indicates if layout should be re-created on device profile change - so that a different
* layout can be displayed.
*/
private static boolean shouldRecreateLayout(DeviceProfile oldDp, DeviceProfile newDp) {
// When folding/unfolding the foldables, we need to switch between the regular widget picker
// and the two pane picker, so we rebuild the picker with the correct layout.
boolean isFoldUnFold =
oldDp.isTwoPanels != newDp.isTwoPanels && enableUnfoldedTwoPanePicker();
// In tablets, on orientation change we switch between single and two pane picker unless the
// categorized suggestions flag was on. With the categorized suggestions feature, we use a
// two pane picker across all orientations.
boolean useDifferentLayoutOnOrientationChange =
(!enableCategorizedWidgetSuggestions() && (newDp.isTablet && !newDp.isTwoPanels
&& oldDp.isLandscape != newDp.isLandscape));
return isFoldUnFold || useDifferentLayoutOnOrientationChange;
}
@Override
public void onBackInvoked() {
if (mIsInSearchMode) {
@@ -855,9 +880,8 @@ public class WidgetsFullSheet extends BaseWidgetSheet
}
@Nullable private View getViewToShowEducationTip() {
if (mRecommendedWidgetsTable.getVisibility() == VISIBLE
&& mRecommendedWidgetsTable.getChildCount() > 0) {
return ((ViewGroup) mRecommendedWidgetsTable.getChildAt(0)).getChildAt(0);
if (mWidgetRecommendationsContainer.getVisibility() == VISIBLE) {
return mWidgetRecommendationsView.getViewForEducationTip();
}
AdapterHolder adapterHolder = mAdapters.get(mIsInSearchMode
@@ -15,6 +15,8 @@
*/
package com.android.launcher3.widget.picker;
import static com.android.launcher3.Flags.enableCategorizedWidgetSuggestions;
import android.content.Context;
import android.graphics.Bitmap;
import android.util.Log;
@@ -147,7 +149,13 @@ public final class WidgetsListTableViewHolderBinder
tableRow = (TableRow) table.getChildAt(i);
} else {
tableRow = new TableRow(table.getContext());
tableRow.setGravity(Gravity.TOP);
if (enableCategorizedWidgetSuggestions()) {
// Vertically center align items, so that even if they don't fill bounds, they
// can look organized when placed together in a row.
tableRow.setGravity(Gravity.CENTER_VERTICAL);
} else {
tableRow.setGravity(Gravity.TOP);
}
table.addView(tableRow);
}
if (tableRow.getChildCount() > widgetItems.size()) {
@@ -15,6 +15,7 @@
*/
package com.android.launcher3.widget.picker;
import static com.android.launcher3.Flags.enableCategorizedWidgetSuggestions;
import static com.android.launcher3.LauncherSettings.Favorites.CONTAINER_WIDGETS_PREDICTION;
import android.content.Context;
@@ -61,7 +62,7 @@ public final class WidgetsRecommendationTableLayout extends TableLayout {
super(context, attrs);
// There are 1 row for title, 1 row for dimension and 2 rows for description.
mWidgetsRecommendationTableVerticalPadding = 2 * getResources()
.getDimensionPixelSize(R.dimen.recommended_widgets_table_vertical_padding);
.getDimensionPixelSize(R.dimen.widget_recommendations_table_vertical_padding);
mWidgetCellVerticalPadding = 2 * getResources()
.getDimensionPixelSize(R.dimen.widget_cell_vertical_padding);
mWidgetCellTextViewsHeight = 4 * getResources().getDimension(R.dimen.widget_cell_font_size);
@@ -84,17 +85,20 @@ public final class WidgetsRecommendationTableLayout extends TableLayout {
* <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.
*
* <p>Returns {@code false} if none of the widgets could fit</p>
*/
public void setRecommendedWidgets(List<ArrayList<WidgetItem>> recommendedWidgets,
public boolean setRecommendedWidgets(List<ArrayList<WidgetItem>> recommendedWidgets,
float recommendationTableMaxHeight) {
mRecommendationTableMaxHeight = recommendationTableMaxHeight;
RecommendationTableData data = fitRecommendedWidgetsToTableSpace(/* previewScale= */ 1f,
recommendedWidgets);
bindData(data);
return !data.mRecommendationTable.isEmpty();
}
private void bindData(RecommendationTableData data) {
if (data.mRecommendationTable.size() == 0) {
if (data.mRecommendationTable.isEmpty()) {
setVisibility(GONE);
return;
}
@@ -104,12 +108,20 @@ public final class WidgetsRecommendationTableLayout extends TableLayout {
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);
if (enableCategorizedWidgetSuggestions()) {
// Vertically center align items, so that even if they don't fill bounds, they can
// look organized when placed together in a row.
tableRow.setGravity(Gravity.CENTER_VERTICAL);
} else {
tableRow.setGravity(Gravity.TOP);
}
for (WidgetItem widgetItem : widgetItems) {
WidgetCell widgetCell = addItemCell(tableRow);
widgetCell.applyFromCellItem(widgetItem, data.mPreviewScale);
widgetCell.showBadge();
if (enableCategorizedWidgetSuggestions()) {
widgetCell.showDescription(false);
}
}
addView(tableRow);
}
@@ -15,6 +15,7 @@
*/
package com.android.launcher3.widget.picker;
import static com.android.launcher3.Flags.enableCategorizedWidgetSuggestions;
import static com.android.launcher3.Flags.enableUnfoldedTwoPanePicker;
import android.content.Context;
@@ -109,9 +110,15 @@ public class WidgetsTwoPaneSheet extends WidgetsFullSheet {
mWidgetsListTableViewHolderBinder =
new WidgetsListTableViewHolderBinder(mActivityContext, layoutInflater, this, this);
mRecommendedWidgetsTable = mContent.findViewById(R.id.recommended_widget_table);
mRecommendedWidgetsTable.setWidgetCellLongClickListener(this);
mRecommendedWidgetsTable.setWidgetCellOnClickListener(this);
mWidgetRecommendationsContainer = mContent.findViewById(
R.id.widget_recommendations_container);
mWidgetRecommendationsView = mContent.findViewById(
R.id.widget_recommendations_view);
mWidgetRecommendationsView.initParentViews(mWidgetRecommendationsContainer);
mWidgetRecommendationsView.setWidgetCellLongClickListener(this);
mWidgetRecommendationsView.setWidgetCellOnClickListener(this);
mHeaderTitle = mContent.findViewById(R.id.title);
mRightPane = mContent.findViewById(R.id.right_pane);
mRightPane.setOutlineProvider(mViewOutlineProviderRightPane);
@@ -213,7 +220,7 @@ public class WidgetsTwoPaneSheet extends WidgetsFullSheet {
mSuggestedWidgetsHeader.setExpanded(true);
resetExpandedHeaders();
mRightPane.removeAllViews();
mRightPane.addView(mRecommendedWidgetsTable);
mRightPane.addView(mWidgetRecommendationsContainer);
mRightPaneScrollView.setScrollY(0);
mRightPane.setAccessibilityPaneTitle(suggestionsRightPaneTitle);
mSuggestedWidgetsPackageUserKey = PackageUserKey.fromPackageItemInfo(packageItemInfo);
@@ -224,7 +231,8 @@ public class WidgetsTwoPaneSheet extends WidgetsFullSheet {
}
@Override
protected float getMaxTableHeight(float noWidgetsViewHeight) {
@Px
protected float getMaxTableHeight(@Px float noWidgetsViewHeight) {
return Float.MAX_VALUE;
}
@@ -308,15 +316,25 @@ public class WidgetsTwoPaneSheet extends WidgetsFullSheet {
if (mSuggestedWidgetsHeader != null) {
mSuggestedWidgetsHeader.setExpanded(false);
}
WidgetsListContentEntry contentEntryToBind;
if (enableCategorizedWidgetSuggestions()) {
// Setting max span size enables row to understand how to fit more than one item
// in a row.
contentEntryToBind = contentEntry.withMaxSpanSize(mMaxSpanPerRow);
} else {
contentEntryToBind = contentEntry;
}
WidgetsRowViewHolder widgetsRowViewHolder =
mWidgetsListTableViewHolderBinder.newViewHolder(mRightPane);
mWidgetsListTableViewHolderBinder.bindViewHolder(widgetsRowViewHolder,
contentEntry,
contentEntryToBind,
ViewHolderBinder.POSITION_FIRST | ViewHolderBinder.POSITION_LAST,
Collections.EMPTY_LIST);
widgetsRowViewHolder.mDataCallback = data -> {
mWidgetsListTableViewHolderBinder.bindViewHolder(widgetsRowViewHolder,
contentEntry,
contentEntryToBind,
ViewHolderBinder.POSITION_FIRST | ViewHolderBinder.POSITION_LAST,
Collections.singletonList(data));
};