Add tap-to-add button to widget picker

This change introduces an "Add" button that appears when a widget
preview is clicked in the widget picker. This button disappears when the
preview is clicked again, or another preview is clicked. When the button
is pressed, it adds that widget to the picker. The add button is
available in the app-specific widget sheet as well.

Bug: 323886237
Test: Manual
Flag: ACONFIG com.android.launcher3.enable_widget_tap_to_add DEVELOPMENT

Change-Id: I86a8a4c22119960c54a885fd2efeb91916b4f9a0
This commit is contained in:
Willie Koomson
2024-02-08 19:16:07 +00:00
parent a51a749bc9
commit cdc26951ff
16 changed files with 303 additions and 43 deletions

View File

@@ -215,3 +215,10 @@ flag {
description: "Enables the Home gesture animation"
bug: "308801666"
}
flag {
name: "enable_widget_tap_to_add"
namespace: "launcher"
description: "Enables an add button in the widget picker"
bug: "323886237"
}

24
res/drawable/ic_plus.xml Normal file
View File

@@ -0,0 +1,24 @@
<?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.
-->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="19dp"
android:height="18dp"
android:viewportWidth="19"
android:viewportHeight="18">
<path
android:pathData="M15.5,9.75H10.25V15H8.75V9.75H3.5V8.25H8.75V3H10.25V8.25H15.5V9.75Z"
android:fillColor="#ffffff"/>
</vector>

View File

@@ -0,0 +1,28 @@
<?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.
-->
<inset
xmlns:android="http://schemas.android.com/apk/res/android">
<ripple
android:color="?android:attr/colorControlHighlight">
<item>
<shape android:shape="rectangle">
<corners
android:radius="50dp"/>
<solid android:color="?attr/widgetPickerAddButtonBackgroundColor" />
</shape>
</item>
</ripple>
</inset>

View File

@@ -45,40 +45,70 @@
android:layout_margin="@dimen/profile_badge_margin"/>
</com.android.launcher3.widget.WidgetCellPreview>
<!-- The name of the widget. -->
<TextView
android:id="@+id/widget_name"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:ellipsize="end"
android:fadingEdge="horizontal"
android:gravity="center_horizontal|center_vertical"
android:singleLine="true"
android:maxLines="1"
android:textColor="?android:attr/textColorPrimary"
android:drawablePadding="@dimen/widget_cell_app_icon_padding"
android:textSize="@dimen/widget_cell_font_size" />
<!-- The original dimensions of the widget -->
<TextView
android:id="@+id/widget_dims"
<FrameLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center_horizontal"
android:textColor="?android:attr/textColorSecondary"
android:textSize="@dimen/widget_cell_font_size"
android:alpha="0.7" />
android:layout_height="wrap_content">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:id="@+id/widget_text_container"
android:orientation="vertical">
<!-- The name of the widget. -->
<TextView
android:id="@+id/widget_name"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:ellipsize="end"
android:fadingEdge="horizontal"
android:layout_gravity="center_horizontal"
android:gravity="center_horizontal|center_vertical"
android:singleLine="true"
android:maxLines="1"
android:textColor="?android:attr/textColorPrimary"
android:drawablePadding="@dimen/widget_cell_app_icon_padding"
android:textSize="@dimen/widget_cell_font_size" />
<TextView
android:id="@+id/widget_description"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center_horizontal"
android:textSize="@dimen/widget_cell_font_size"
android:textColor="?android:attr/textColorSecondary"
android:maxLines="2"
android:ellipsize="end"
android:fadingEdge="horizontal"
android:alpha="0.7" />
<!-- The original dimensions of the widget -->
<TextView
android:id="@+id/widget_dims"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center_horizontal"
android:textColor="?android:attr/textColorSecondary"
android:textSize="@dimen/widget_cell_font_size"
android:alpha="0.7" />
</merge>
<TextView
android:id="@+id/widget_description"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center_horizontal"
android:textSize="@dimen/widget_cell_font_size"
android:textColor="?android:attr/textColorSecondary"
android:maxLines="2"
android:ellipsize="end"
android:fadingEdge="horizontal"
android:alpha="0.7" />
</LinearLayout>
<Button
android:id="@+id/widget_add_button"
android:layout_width="wrap_content"
android:layout_height="@dimen/widget_cell_add_button_height"
android:layout_gravity="center"
android:minWidth="0dp"
android:paddingTop="@dimen/widget_cell_add_button_vertical_padding"
android:paddingBottom="@dimen/widget_cell_add_button_vertical_padding"
android:paddingStart="@dimen/widget_cell_add_button_start_padding"
android:paddingEnd="@dimen/widget_cell_add_button_end_padding"
android:text="@string/widget_add_button_label"
android:textColor="?attr/widgetPickerAddButtonTextColor"
android:textSize="@dimen/widget_cell_font_size"
android:gravity="center"
android:visibility="gone"
android:drawableLeft="@drawable/ic_plus"
android:drawablePadding="8dp"
android:drawableTint="?attr/widgetPickerAddButtonTextColor"
android:background="@drawable/widget_cell_add_button_background" />
</FrameLayout>
</merge>

View File

@@ -46,6 +46,10 @@
@android:color/system_neutral2_200</color>
<color name="widget_picker_collapse_handle_color_dark">
@android:color/system_neutral2_700</color>
<color name="widget_picker_add_button_background_color_dark">
@android:color/system_accent1_200</color>
<color name="widget_picker_add_button_text_color_dark">
@android:color/system_accent1_800</color>
<color name="work_fab_bg_color">
@android:color/system_accent1_200</color>

View File

@@ -97,6 +97,10 @@
@android:color/system_neutral2_700</color>
<color name="widget_picker_collapse_handle_color_light">
@android:color/system_neutral2_200</color>
<color name="widget_picker_add_button_background_color_light">
@android:color/system_accent1_600</color>
<color name="widget_picker_add_button_text_color_light">
@android:color/system_accent1_0</color>
<color name="work_fab_bg_color">
@android:color/system_accent1_200</color>

View File

@@ -72,6 +72,8 @@
<attr name="widgetPickerSelectedTabTextColor" format="color"/>
<attr name="widgetPickerUnselectedTabTextColor" format="color"/>
<attr name="widgetPickerCollapseHandleColor" format="color"/>
<attr name="widgetPickerAddButtonBackgroundColor" format="color"/>
<attr name="widgetPickerAddButtonTextColor" format="color"/>
<!-- BubbleTextView specific attributes. -->
<declare-styleable name="BubbleTextView">

View File

@@ -113,6 +113,8 @@
<color name="widget_picker_selected_tab_text_color_light">#FFFFFF</color>
<color name="widget_picker_unselected_tab_text_color_light">#444746</color>
<color name="widget_picker_collapse_handle_color_light">#C4C7C5</color>
<color name="widget_picker_add_button_background_color_light">#0B57D0</color>
<color name="widget_picker_add_button_text_color_light">#0B57D0</color>
<color name="widget_picker_primary_surface_color_dark">#1F2020</color>
<color name="widget_picker_secondary_surface_color_dark">#393939</color>
@@ -128,6 +130,8 @@
<color name="widget_picker_selected_tab_text_color_dark">#2D312F</color>
<color name="widget_picker_unselected_tab_text_color_dark">#C4C7C5</color>
<color name="widget_picker_collapse_handle_color_dark">#444746</color>
<color name="widget_picker_add_button_background_color_dark">#062E6F</color>
<color name="widget_picker_add_button_text_color_dark">#FFFFFF</color>
<color name="material_color_on_secondary_fixed_variant">#3F4759</color>
<color name="material_color_on_tertiary_fixed_variant">#583E5B</color>

View File

@@ -180,6 +180,10 @@
<dimen name="widget_cell_font_size">14sp</dimen>
<dimen name="widget_cell_app_icon_size">24dp</dimen>
<dimen name="widget_cell_app_icon_padding">8dp</dimen>
<dimen name="widget_cell_add_button_height">48dp</dimen>
<dimen name="widget_cell_add_button_start_padding">8dp</dimen>
<dimen name="widget_cell_add_button_end_padding">16dp</dimen>
<dimen name="widget_cell_add_button_vertical_padding">10dp</dimen>
<dimen name="widget_tabs_button_horizontal_padding">4dp</dimen>
<dimen name="widget_tabs_horizontal_padding">16dp</dimen>

View File

@@ -64,6 +64,12 @@
<!-- Spoken text for a screen reader. The placeholder text is the widget name.
[CHAR_LIMIT=none]-->
<string name="widget_preview_context_description"><xliff:g id="widget_name" example="Calendar month view">%1$s</xliff:g> widget</string>
<!-- Spoken text for a screen reader. The first placeholder text is the widget name, the
remaining placeholders are for the widget dimensions.
[CHAR_LIMIT=none]-->
<string name="widget_preview_name_and_dims_content_description">
<xliff:g id="widget_name" example="Calendar month view">%1$s</xliff:g> widget, %2$d wide by %3$d high
</string>
<!-- Message to tell the user to press and hold a widget/icon to add it to the home screen.
[CHAR LIMIT=NONE] -->
<string name="add_item_request_drag_hint">Touch &amp; hold the widget to move it around the home screen</string>
@@ -125,6 +131,12 @@
<!-- A widget category label for grouping widgets related to note taking. [CHAR_LIMIT=30] -->
<string name="widget_category_note_taking">Note-taking</string>
<!-- Text on the button that adds a widget to the home screen. [CHAR_LIMIT=15] -->
<string name="widget_add_button_label">Add</string>
<!-- Accessibility content description for the button that adds a widget to the home screen. The
placeholder text is the widget name. [CHAR_LIMIT=none] -->
<string name="widget_add_button_content_description">Add <xliff:g id="widget_name" example="Calendar month view">%1$s</xliff:g> widget</string>
<!-- Title of a dialog. This dialog lets a user know how they can use widgets on their phone.
[CHAR_LIMIT=NONE] -->
<string name="widget_education_header">Useful info at your fingertips</string>

View File

@@ -261,6 +261,10 @@
@color/widget_picker_unselected_tab_text_color_light</item>
<item name="widgetPickerCollapseHandleColor">
@color/widget_picker_collapse_handle_color_light</item>
<item name="widgetPickerAddButtonBackgroundColor">
@color/widget_picker_add_button_background_color_light</item>
<item name="widgetPickerAddButtonTextColor">
@color/widget_picker_add_button_text_color_light</item>
</style>
<style name="WidgetContainerTheme.Dark" parent="AppTheme.Dark">
<item name="android:colorEdgeEffect">?android:attr/textColorSecondary</item>
@@ -292,6 +296,10 @@
@color/widget_picker_unselected_tab_text_color_dark</item>
<item name="widgetPickerCollapseHandleColor">
@color/widget_picker_collapse_handle_color_dark</item>
<item name="widgetPickerAddButtonBackgroundColor">
@color/widget_picker_add_button_background_color_dark</item>
<item name="widgetPickerAddButtonTextColor">
@color/widget_picker_add_button_text_color_dark</item>
</style>
<style name="FastScrollerPopup" parent="@android:style/TextAppearance.DeviceDefault.DialogWindowTitle">

View File

@@ -751,6 +751,9 @@ public class StatsLogManager implements ResourceBasedOverride {
+ " metric.")
LAUNCHER_SPLIT_SELECTION_EXIT_INTERRUPTED(1612),
@UiEvent(doc = "User tapped add widget button in widget sheet.")
LAUNCHER_WIDGET_ADD_BUTTON_TAP(1622),
// ADD MORE
;

View File

@@ -18,6 +18,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.Flags.enableWidgetTapToAdd;
import static com.android.launcher3.LauncherPrefs.WIDGETS_EDUCATION_TIP_SEEN;
import android.content.Context;
@@ -41,8 +42,10 @@ import com.android.launcher3.DeviceProfile.OnDeviceProfileChangeListener;
import com.android.launcher3.Insettable;
import com.android.launcher3.Launcher;
import com.android.launcher3.LauncherPrefs;
import com.android.launcher3.PendingAddItemInfo;
import com.android.launcher3.R;
import com.android.launcher3.Utilities;
import com.android.launcher3.logging.StatsLogManager;
import com.android.launcher3.popup.PopupDataProvider;
import com.android.launcher3.testing.TestLogging;
import com.android.launcher3.testing.shared.TestProtocol;
@@ -73,6 +76,8 @@ public abstract class BaseWidgetSheet extends AbstractSlideInView<BaseActivity>
private boolean mDisableNavBarScrim = false;
@Nullable private WidgetCell mWidgetCellWithAddButton = null;
public BaseWidgetSheet(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
mContentHorizontalMargin = getWidgetListHorizontalMargin();
@@ -123,13 +128,49 @@ public abstract class BaseWidgetSheet extends AbstractSlideInView<BaseActivity>
@Override
public final void onClick(View v) {
if (v instanceof WidgetCell) {
mActivityContext.getItemOnClickListener().onClick(v);
} else if (v.getParent() instanceof WidgetCell wc) {
WidgetCell wc;
if (v instanceof WidgetCell view) {
wc = view;
} else if (v.getParent() instanceof WidgetCell parent) {
wc = parent;
} else {
return;
}
if (enableWidgetTapToAdd()) {
if (mWidgetCellWithAddButton != null) {
// If there is a add button currently showing, hide it.
mWidgetCellWithAddButton.hideAddButton(/* animate= */ true);
}
if (mWidgetCellWithAddButton != wc) {
// If click is on a cell not showing an add button, show it now.
final PendingAddItemInfo info = (PendingAddItemInfo) wc.getTag();
if (mActivityContext instanceof Launcher) {
wc.showAddButton((view) -> addWidget(info));
} else {
wc.showAddButton((view) -> mActivityContext.getItemOnClickListener()
.onClick(wc));
}
}
mWidgetCellWithAddButton = mWidgetCellWithAddButton != wc ? wc : null;
} else {
mActivityContext.getItemOnClickListener().onClick(wc);
}
}
/**
* Click handler for tap to add button.
*/
public void addWidget(PendingAddItemInfo info) {
mActivityContext.getStatsLogManager().logger().withItemInfo(info).log(
StatsLogManager.LauncherEvent.LAUNCHER_WIDGET_ADD_BUTTON_TAP);
handleClose(true);
Launcher.getLauncher(mActivityContext).getAccessibilityDelegate()
.addToWorkspace(info, /*accessibility=*/ false);
}
@Override
public boolean onLongClick(View v) {
TestLogging.recordEvent(TestProtocol.SEQUENCE_MAIN, "Widgets.onLongClick");

View File

@@ -18,6 +18,7 @@ package com.android.launcher3.widget;
import static android.appwidget.AppWidgetProviderInfo.WIDGET_CATEGORY_HOME_SCREEN;
import static com.android.launcher3.Flags.enableWidgetTapToAdd;
import static com.android.launcher3.LauncherSettings.Favorites.CONTAINER_WIDGETS_TRAY;
import static com.android.launcher3.widget.LauncherAppWidgetProviderInfo.fromProviderInfo;
import static com.android.launcher3.widget.util.WidgetSizes.getWidgetItemSizePx;
@@ -36,6 +37,7 @@ import android.view.View;
import android.view.ViewGroup;
import android.view.ViewPropertyAnimator;
import android.view.accessibility.AccessibilityNodeInfo;
import android.widget.Button;
import android.widget.FrameLayout;
import android.widget.ImageView;
import android.widget.LinearLayout;
@@ -77,6 +79,7 @@ public class WidgetCell extends LinearLayout {
private static final boolean DEBUG = false;
private static final int FADE_IN_DURATION_MS = 90;
private static final int ADD_BUTTON_FADE_DURATION_MS = 300;
/**
* The requested scale of the preview container. It can be lower than this as well.
@@ -89,6 +92,8 @@ public class WidgetCell extends LinearLayout {
private TextView mWidgetName;
private TextView mWidgetDims;
private TextView mWidgetDescription;
private Button mWidgetAddButton;
private LinearLayout mWidgetTextContainer;
private WidgetItem mItem;
private Size mWidgetSize;
@@ -141,6 +146,11 @@ public class WidgetCell extends LinearLayout {
mWidgetName = findViewById(R.id.widget_name);
mWidgetDims = findViewById(R.id.widget_dims);
mWidgetDescription = findViewById(R.id.widget_description);
mWidgetTextContainer = findViewById(R.id.widget_text_container);
mWidgetAddButton = findViewById(R.id.widget_add_button);
if (enableWidgetTapToAdd()) {
mWidgetAddButton.setVisibility(INVISIBLE);
}
}
public void setRemoteViewsPreview(RemoteViews view) {
@@ -181,6 +191,10 @@ public class WidgetCell extends LinearLayout {
showDescription(true);
showDimensions(true);
if (enableWidgetTapToAdd()) {
hideAddButton(/* animate= */ false);
}
if (mActiveRequest != null) {
mActiveRequest.cancel();
mActiveRequest = null;
@@ -223,12 +237,8 @@ public class WidgetCell extends LinearLayout {
initPreviewContainerSizeAndScale();
mWidgetName.setText(mItem.label);
mWidgetName.setContentDescription(
context.getString(R.string.widget_preview_context_description, mItem.label));
mWidgetDims.setText(context.getString(R.string.widget_dims_format,
mItem.spanX, mItem.spanY));
mWidgetDims.setContentDescription(context.getString(
R.string.widget_accessible_dims_format, mItem.spanX, mItem.spanY));
if (!TextUtils.isEmpty(mItem.description)) {
mWidgetDescription.setText(mItem.description);
mWidgetDescription.setVisibility(VISIBLE);
@@ -236,6 +246,14 @@ public class WidgetCell extends LinearLayout {
mWidgetDescription.setVisibility(GONE);
}
// Setting the content description on the WidgetCell itself ensures that it remains
// screen reader focusable when the add button is showing and the text is hidden.
setContentDescription(createContentDescription(context));
if (mWidgetAddButton != null) {
mWidgetAddButton.setContentDescription(context.getString(
R.string.widget_add_button_content_description, mItem.label));
}
if (item.activityInfo != null) {
setTag(new PendingAddShortcutInfo(item.activityInfo));
} else {
@@ -285,6 +303,16 @@ public class WidgetCell extends LinearLayout {
mPreviewContainerScale = Math.min(scaleX, scaleY);
}
private String createContentDescription(Context context) {
String contentDescription =
context.getString(R.string.widget_preview_name_and_dims_content_description,
mItem.label, mItem.spanX, mItem.spanY);
if (!TextUtils.isEmpty(mItem.description)) {
contentDescription += " " + mItem.description;
}
return contentDescription;
}
private void setAppWidgetHostViewPreview(
NavigableAppWidgetHostView appWidgetHostViewPreview,
LauncherAppWidgetProviderInfo providerInfo,
@@ -517,4 +545,55 @@ public class WidgetCell extends LinearLayout {
mIconLoadRequest = null;
}
}
/**
* Show tap to add button.
* @param callback Callback to be set on the button.
*/
public void showAddButton(View.OnClickListener callback) {
mWidgetAddButton.setAlpha(0F);
mWidgetAddButton.setVisibility(VISIBLE);
mWidgetAddButton.setOnClickListener(callback);
mWidgetAddButton.animate().cancel();
mWidgetAddButton.animate()
.alpha(1F)
.setDuration(ADD_BUTTON_FADE_DURATION_MS);
mWidgetTextContainer.animate().cancel();
mWidgetTextContainer.animate()
.alpha(0F)
.setDuration(ADD_BUTTON_FADE_DURATION_MS)
.withEndAction(() -> {
mWidgetTextContainer.setVisibility(INVISIBLE);
});
}
/**
* Hide tap to add button.
*/
public void hideAddButton(boolean animate) {
mWidgetAddButton.setOnClickListener(null);
mWidgetAddButton.animate().cancel();
mWidgetTextContainer.animate().cancel();
if (!animate) {
mWidgetAddButton.setVisibility(INVISIBLE);
mWidgetTextContainer.setVisibility(VISIBLE);
mWidgetTextContainer.setAlpha(1F);
return;
}
mWidgetAddButton.animate()
.alpha(0F)
.setDuration(ADD_BUTTON_FADE_DURATION_MS)
.withEndAction(() -> {
mWidgetAddButton.setVisibility(INVISIBLE);
});
mWidgetTextContainer.setAlpha(0F);
mWidgetTextContainer.setVisibility(VISIBLE);
mWidgetTextContainer.animate()
.alpha(1F)
.setDuration(ADD_BUTTON_FADE_DURATION_MS);
}
}

View File

@@ -12,6 +12,7 @@ import android.platform.test.annotations.RequiresFlagsDisabled
import android.platform.test.annotations.RequiresFlagsEnabled
import android.platform.test.flag.junit.CheckFlagsRule
import android.platform.test.flag.junit.DeviceFlagsValueProvider
import android.view.ContextThemeWrapper
import android.view.LayoutInflater
import android.widget.RemoteViews
import androidx.test.core.app.ApplicationProvider.getApplicationContext
@@ -53,7 +54,14 @@ class GeneratedPreviewTest {
context = getApplicationContext()
generatedPreview = RemoteViews(context.packageName, generatedPreviewLayout)
widgetCell =
LayoutInflater.from(ActivityContextWrapper(context))
LayoutInflater.from(
ActivityContextWrapper(
ContextThemeWrapper(
context,
com.android.launcher3.R.style.WidgetContainerTheme
)
)
)
.inflate(com.android.launcher3.R.layout.widget_cell, null) as WidgetCell
appWidgetProviderInfo =
AppWidgetProviderInfo()

View File

@@ -29,6 +29,7 @@ import android.content.ComponentName;
import android.content.Context;
import android.graphics.Bitmap;
import android.os.UserHandle;
import android.view.ContextThemeWrapper;
import android.view.LayoutInflater;
import android.view.View;
import android.view.View.OnClickListener;
@@ -96,7 +97,8 @@ public final class WidgetsListTableViewHolderBinderTest {
mViewHolderBinder = new WidgetsListTableViewHolderBinder(
mContext,
LayoutInflater.from(mContext),
LayoutInflater.from(new ContextThemeWrapper(mContext,
com.android.launcher3.R.style.WidgetContainerTheme)),
mOnIconClickListener,
mOnLongClickListener);
}