Replace horizontal ScrollViews with tables in the full widgets picker
Test: Run gnl test, AddWidgetTest Run Robolectric tests for widgets Manual test video: https://drive.google.com/file/d/1uwCtVNIlC9150kv5eEfILfP5r5M7ARYm/view?usp=sharing Bug: 179797520 Change-Id: I2f4cdf84338a91b63967879d0c9268c312ace19b
This commit is contained in:
parent
1408c6459c
commit
e8d92342fa
|
@ -45,5 +45,5 @@
|
|||
launcher:iconSizeOverride="@dimen/widget_section_icon_size"
|
||||
launcher:layoutHorizontal="true" />
|
||||
|
||||
<include layout="@layout/widgets_scroll_container" />
|
||||
<include layout="@layout/widgets_table_container" />
|
||||
</LinearLayout>
|
||||
|
|
|
@ -0,0 +1,21 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- 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.
|
||||
-->
|
||||
<TableLayout
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:id="@+id/widgets_table"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="?android:attr/colorPrimaryDark" />
|
|
@ -32,6 +32,7 @@ import android.view.View;
|
|||
import android.view.View.OnClickListener;
|
||||
import android.view.View.OnLongClickListener;
|
||||
import android.widget.FrameLayout;
|
||||
import android.widget.TableRow;
|
||||
import android.widget.TextView;
|
||||
|
||||
import com.android.launcher3.DeviceProfile;
|
||||
|
@ -65,12 +66,12 @@ import java.util.ArrayList;
|
|||
import java.util.List;
|
||||
|
||||
@RunWith(RobolectricTestRunner.class)
|
||||
public final class WidgetsListRowViewHolderBinderTest {
|
||||
public final class WidgetsListTableViewHolderBinderTest {
|
||||
private static final String TEST_PACKAGE = "com.google.test";
|
||||
private static final String APP_NAME = "Test app";
|
||||
|
||||
private Context mContext;
|
||||
private WidgetsListRowViewHolderBinder mViewHolderBinder;
|
||||
private WidgetsListTableViewHolderBinder mViewHolderBinder;
|
||||
private InvariantDeviceProfile mTestProfile;
|
||||
// Replace ActivityController with ActivityScenario, which is the recommended way for activity
|
||||
// testing.
|
||||
|
@ -105,7 +106,7 @@ public final class WidgetsListRowViewHolderBinderTest {
|
|||
return componentWithLabel.getComponent().getShortClassName();
|
||||
}).when(mIconCache).getTitleNoCache(any());
|
||||
|
||||
mViewHolderBinder = new WidgetsListRowViewHolderBinder(
|
||||
mViewHolderBinder = new WidgetsListTableViewHolderBinder(
|
||||
mContext,
|
||||
LayoutInflater.from(mTestActivity),
|
||||
mOnIconClickListener,
|
||||
|
@ -129,16 +130,17 @@ public final class WidgetsListRowViewHolderBinderTest {
|
|||
mViewHolderBinder.bindViewHolder(viewHolder, entry);
|
||||
shadowOf(getMainLooper()).idle();
|
||||
|
||||
// THEN the cell container has 5 children: 3 widgets + 2 separators
|
||||
// Index: 0 1 2 3 4
|
||||
// THEN the table container has one row, which contains 3 widgets.
|
||||
// View: .SampleWidget0 | .SampleWidget1 | .SampleWidget2
|
||||
assertThat(viewHolder.cellContainer.getChildCount()).isEqualTo(5);
|
||||
assertThat(viewHolder.mTableContainer.getChildCount()).isEqualTo(1);
|
||||
TableRow row = (TableRow) viewHolder.mTableContainer.getChildAt(0);
|
||||
assertThat(row.getChildCount()).isEqualTo(3);
|
||||
// Widget 0 label is .SampleWidget0.
|
||||
assertWidgetCellWithLabel(viewHolder.cellContainer.getChildAt(0), ".SampleWidget0");
|
||||
assertWidgetCellWithLabel(row.getChildAt(0), ".SampleWidget0");
|
||||
// Widget 1 label is .SampleWidget1.
|
||||
assertWidgetCellWithLabel(viewHolder.cellContainer.getChildAt(2), ".SampleWidget1");
|
||||
assertWidgetCellWithLabel(row.getChildAt(1), ".SampleWidget1");
|
||||
// Widget 2 label is .SampleWidget2.
|
||||
assertWidgetCellWithLabel(viewHolder.cellContainer.getChildAt(4), ".SampleWidget2");
|
||||
assertWidgetCellWithLabel(row.getChildAt(2), ".SampleWidget2");
|
||||
}
|
||||
|
||||
private WidgetsListContentEntry generateSampleAppWithWidgets(String appName, String packageName,
|
|
@ -0,0 +1,201 @@
|
|||
/*
|
||||
* 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.util;
|
||||
|
||||
import static com.google.common.truth.Truth.assertThat;
|
||||
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.Mockito.doAnswer;
|
||||
import static org.robolectric.Shadows.shadowOf;
|
||||
|
||||
import android.appwidget.AppWidgetProviderInfo;
|
||||
import android.content.ComponentName;
|
||||
import android.content.Context;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.graphics.Point;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.os.UserHandle;
|
||||
|
||||
import com.android.launcher3.InvariantDeviceProfile;
|
||||
import com.android.launcher3.LauncherAppWidgetProviderInfo;
|
||||
import com.android.launcher3.icons.ComponentWithLabel;
|
||||
import com.android.launcher3.icons.IconCache;
|
||||
import com.android.launcher3.model.WidgetItem;
|
||||
import com.android.launcher3.pm.ShortcutConfigActivityInfo;
|
||||
import com.android.launcher3.widget.util.WidgetsTableUtils;
|
||||
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.MockitoAnnotations;
|
||||
import org.robolectric.RobolectricTestRunner;
|
||||
import org.robolectric.RuntimeEnvironment;
|
||||
import org.robolectric.shadows.ShadowPackageManager;
|
||||
import org.robolectric.util.ReflectionHelpers;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
@RunWith(RobolectricTestRunner.class)
|
||||
public final class WidgetsTableUtilsTest {
|
||||
private static final String TEST_PACKAGE = "com.google.test";
|
||||
|
||||
@Mock
|
||||
private IconCache mIconCache;
|
||||
|
||||
private Context mContext;
|
||||
private InvariantDeviceProfile mTestProfile;
|
||||
private WidgetItem mWidget1x1;
|
||||
private WidgetItem mWidget2x2;
|
||||
private WidgetItem mWidget2x3;
|
||||
private WidgetItem mWidget2x4;
|
||||
private WidgetItem mWidget4x4;
|
||||
|
||||
private WidgetItem mShortcut1;
|
||||
private WidgetItem mShortcut2;
|
||||
private WidgetItem mShortcut3;
|
||||
|
||||
@Before
|
||||
public void setUp() {
|
||||
MockitoAnnotations.initMocks(this);
|
||||
|
||||
mContext = RuntimeEnvironment.application;
|
||||
|
||||
mTestProfile = new InvariantDeviceProfile();
|
||||
mTestProfile.numRows = 5;
|
||||
mTestProfile.numColumns = 5;
|
||||
|
||||
initTestWidgets();
|
||||
initTestShortcuts();
|
||||
|
||||
doAnswer(invocation -> ((ComponentWithLabel) invocation.getArgument(0))
|
||||
.getComponent().getPackageName())
|
||||
.when(mIconCache).getTitleNoCache(any());
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
public void groupWidgetItemsIntoTable_widgetsOnly_maxSpansPerRow5_shouldGroupWidgetsInTable() {
|
||||
List<WidgetItem> widgetItems = List.of(mWidget4x4, mWidget2x3, mWidget1x1, mWidget2x4,
|
||||
mWidget2x2);
|
||||
|
||||
List<ArrayList<WidgetItem>> widgetItemInTable = WidgetsTableUtils.groupWidgetItemsIntoTable(
|
||||
widgetItems, /* maxSpansPerRow= */ 5);
|
||||
|
||||
// Row 0: 1x1, 2x2, 2x3
|
||||
// Row 1: 2x4
|
||||
// Row 2: 4x4
|
||||
assertThat(widgetItemInTable).hasSize(3);
|
||||
assertThat(widgetItemInTable.get(0)).containsExactly(mWidget1x1, mWidget2x2, mWidget2x3);
|
||||
assertThat(widgetItemInTable.get(1)).containsExactly(mWidget2x4);
|
||||
assertThat(widgetItemInTable.get(2)).containsExactly(mWidget4x4);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void groupWidgetItemsIntoTable_widgetsOnly_maxSpansPerRow4_shouldGroupWidgetsInTable() {
|
||||
List<WidgetItem> widgetItems = List.of(mWidget4x4, mWidget2x3, mWidget1x1, mWidget2x4,
|
||||
mWidget2x2);
|
||||
|
||||
List<ArrayList<WidgetItem>> widgetItemInTable = WidgetsTableUtils.groupWidgetItemsIntoTable(
|
||||
widgetItems, /* maxSpansPerRow= */ 4);
|
||||
|
||||
// Row 0: 1x1, 2x2
|
||||
// Row 1: 2x3, 2x4
|
||||
// Row 2: 4x4
|
||||
assertThat(widgetItemInTable).hasSize(3);
|
||||
assertThat(widgetItemInTable.get(0)).containsExactly(mWidget1x1, mWidget2x2);
|
||||
assertThat(widgetItemInTable.get(1)).containsExactly(mWidget2x3, mWidget2x4);
|
||||
assertThat(widgetItemInTable.get(2)).containsExactly(mWidget4x4);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void groupWidgetItemsIntoTable_mixItems_maxSpansPerRow4_shouldGroupWidgetsInTable() {
|
||||
List<WidgetItem> widgetItems = List.of(mWidget4x4, mShortcut3, mWidget2x3, mShortcut1,
|
||||
mWidget1x1, mShortcut2, mWidget2x4, mWidget2x2);
|
||||
|
||||
List<ArrayList<WidgetItem>> widgetItemInTable = WidgetsTableUtils.groupWidgetItemsIntoTable(
|
||||
widgetItems, /* maxSpansPerRow= */ 4);
|
||||
|
||||
// Row 0: 1x1, 2x2
|
||||
// Row 1: 2x3, 2x4
|
||||
// Row 2: 4x4
|
||||
// Row 3: shortcut3, shortcut1, shortcut2
|
||||
assertThat(widgetItemInTable).hasSize(4);
|
||||
assertThat(widgetItemInTable.get(0)).containsExactly(mWidget1x1, mWidget2x2);
|
||||
assertThat(widgetItemInTable.get(1)).containsExactly(mWidget2x3, mWidget2x4);
|
||||
assertThat(widgetItemInTable.get(2)).containsExactly(mWidget4x4);
|
||||
assertThat(widgetItemInTable.get(3)).containsExactly(mShortcut3, mShortcut2, mShortcut1);
|
||||
}
|
||||
|
||||
private void initTestWidgets() {
|
||||
List<Point> widgetSizes = List.of(new Point(1, 1), new Point(2, 2), new Point(2, 3),
|
||||
new Point(2, 4), new Point(4, 4));
|
||||
|
||||
ArrayList<WidgetItem> widgetItems = new ArrayList<>();
|
||||
widgetSizes.stream().forEach(
|
||||
widgetSize -> {
|
||||
ShadowPackageManager packageManager = shadowOf(mContext.getPackageManager());
|
||||
AppWidgetProviderInfo info = new AppWidgetProviderInfo();
|
||||
info.provider = ComponentName.createRelative(TEST_PACKAGE,
|
||||
".WidgetProvider_" + widgetSize.x + "x" + widgetSize.y);
|
||||
LauncherAppWidgetProviderInfo widgetInfo =
|
||||
LauncherAppWidgetProviderInfo.fromProviderInfo(mContext, info);
|
||||
widgetInfo.spanX = widgetSize.x;
|
||||
widgetInfo.spanY = widgetSize.y;
|
||||
ReflectionHelpers.setField(widgetInfo, "providerInfo",
|
||||
packageManager.addReceiverIfNotPresent(widgetInfo.provider));
|
||||
widgetItems.add(new WidgetItem(widgetInfo, mTestProfile, mIconCache));
|
||||
}
|
||||
);
|
||||
mWidget1x1 = widgetItems.get(0);
|
||||
mWidget2x2 = widgetItems.get(1);
|
||||
mWidget2x3 = widgetItems.get(2);
|
||||
mWidget2x4 = widgetItems.get(3);
|
||||
mWidget4x4 = widgetItems.get(4);
|
||||
}
|
||||
|
||||
private void initTestShortcuts() {
|
||||
PackageManager packageManager = mContext.getPackageManager();
|
||||
mShortcut1 = new WidgetItem(new TestShortcutConfigActivityInfo(
|
||||
ComponentName.createRelative(TEST_PACKAGE, ".shortcut1"), UserHandle.CURRENT),
|
||||
mIconCache, packageManager);
|
||||
mShortcut2 = new WidgetItem(new TestShortcutConfigActivityInfo(
|
||||
ComponentName.createRelative(TEST_PACKAGE, ".shortcut2"), UserHandle.CURRENT),
|
||||
mIconCache, packageManager);
|
||||
mShortcut3 = new WidgetItem(new TestShortcutConfigActivityInfo(
|
||||
ComponentName.createRelative(TEST_PACKAGE, ".shortcut3"), UserHandle.CURRENT),
|
||||
mIconCache, packageManager);
|
||||
|
||||
}
|
||||
|
||||
private final class TestShortcutConfigActivityInfo extends ShortcutConfigActivityInfo {
|
||||
|
||||
TestShortcutConfigActivityInfo(ComponentName componentName, UserHandle user) {
|
||||
super(componentName, user);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Drawable getFullResIcon(IconCache cache) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public CharSequence getLabel(PackageManager pm) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -43,4 +43,20 @@ public class WidgetItem extends ComponentKey {
|
|||
activityInfo = info;
|
||||
spanX = spanY = 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns {@code true} if this {@link WidgetItem} has the same type as the given
|
||||
* {@code otherItem}.
|
||||
*
|
||||
* For example, both items are widgets or both items are shortcuts.
|
||||
*/
|
||||
public boolean hasSameType(WidgetItem otherItem) {
|
||||
if (widgetInfo != null && otherItem.widgetInfo != null) {
|
||||
return true;
|
||||
}
|
||||
if (activityInfo != null && otherItem.activityInfo != null) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -39,6 +39,7 @@ import androidx.annotation.Nullable;
|
|||
import androidx.annotation.VisibleForTesting;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
import com.android.launcher3.DeviceProfile;
|
||||
import com.android.launcher3.Insettable;
|
||||
import com.android.launcher3.Launcher;
|
||||
import com.android.launcher3.LauncherAppState;
|
||||
|
@ -181,20 +182,30 @@ public class WidgetsFullSheet extends BaseWidgetSheet
|
|||
|
||||
@Override
|
||||
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
|
||||
DeviceProfile deviceProfile = mLauncher.getDeviceProfile();
|
||||
int widthUsed;
|
||||
if (mInsets.bottom > 0) {
|
||||
widthUsed = mInsets.left + mInsets.right;
|
||||
} else {
|
||||
Rect padding = mLauncher.getDeviceProfile().workspacePadding;
|
||||
Rect padding = deviceProfile.workspacePadding;
|
||||
widthUsed = Math.max(padding.left + padding.right,
|
||||
2 * (mInsets.left + mInsets.right));
|
||||
}
|
||||
|
||||
int heightUsed = mInsets.top + mLauncher.getDeviceProfile().edgeMarginPx;
|
||||
int heightUsed = mInsets.top + deviceProfile.edgeMarginPx;
|
||||
measureChildWithMargins(mContent, widthMeasureSpec,
|
||||
widthUsed, heightMeasureSpec, heightUsed);
|
||||
setMeasuredDimension(MeasureSpec.getSize(widthMeasureSpec),
|
||||
MeasureSpec.getSize(heightMeasureSpec));
|
||||
|
||||
int maxSpansPerRow = getMeasuredWidth() / (deviceProfile.cellWidthPx
|
||||
+ deviceProfile.workspaceCellPaddingXPx);
|
||||
mAdapters.get(AdapterHolder.PRIMARY).mWidgetsListAdapter.setMaxHorizontalSpansPerRow(
|
||||
maxSpansPerRow);
|
||||
if (mHasWorkProfile) {
|
||||
mAdapters.get(AdapterHolder.WORK).mWidgetsListAdapter.setMaxHorizontalSpansPerRow(
|
||||
maxSpansPerRow);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
|
@ -19,10 +19,10 @@ import android.content.Context;
|
|||
import android.util.Log;
|
||||
import android.util.SparseArray;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.View.OnClickListener;
|
||||
import android.view.View.OnLongClickListener;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.TableRow;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
@ -67,7 +67,7 @@ public class WidgetsListAdapter extends Adapter<ViewHolder> implements OnHeaderC
|
|||
|
||||
private final WidgetsDiffReporter mDiffReporter;
|
||||
private final SparseArray<ViewHolderBinder> mViewHolderBinders = new SparseArray<>();
|
||||
private final WidgetsListRowViewHolderBinder mWidgetsListRowViewHolderBinder;
|
||||
private final WidgetsListTableViewHolderBinder mWidgetsListTableViewHolderBinder;
|
||||
private final WidgetListBaseRowEntryComparator mRowComparator =
|
||||
new WidgetListBaseRowEntryComparator();
|
||||
|
||||
|
@ -84,9 +84,9 @@ public class WidgetsListAdapter extends Adapter<ViewHolder> implements OnHeaderC
|
|||
WidgetPreviewLoader widgetPreviewLoader, IconCache iconCache,
|
||||
OnClickListener iconClickListener, OnLongClickListener iconLongClickListener) {
|
||||
mDiffReporter = new WidgetsDiffReporter(iconCache, this);
|
||||
mWidgetsListRowViewHolderBinder = new WidgetsListRowViewHolderBinder(context,
|
||||
mWidgetsListTableViewHolderBinder = new WidgetsListTableViewHolderBinder(context,
|
||||
layoutInflater, iconClickListener, iconLongClickListener, widgetPreviewLoader);
|
||||
mViewHolderBinders.put(VIEW_TYPE_WIDGETS_LIST, mWidgetsListRowViewHolderBinder);
|
||||
mViewHolderBinders.put(VIEW_TYPE_WIDGETS_LIST, mWidgetsListTableViewHolderBinder);
|
||||
mViewHolderBinders.put(VIEW_TYPE_WIDGETS_HEADER,
|
||||
new WidgetsListHeaderViewHolderBinder(layoutInflater, this::onHeaderClicked));
|
||||
}
|
||||
|
@ -101,16 +101,16 @@ public class WidgetsListAdapter extends Adapter<ViewHolder> implements OnHeaderC
|
|||
* @see WidgetCell#setApplyBitmapDeferred(boolean)
|
||||
*/
|
||||
public void setApplyBitmapDeferred(boolean isDeferred, RecyclerView rv) {
|
||||
mWidgetsListRowViewHolderBinder.setApplyBitmapDeferred(isDeferred);
|
||||
mWidgetsListTableViewHolderBinder.setApplyBitmapDeferred(isDeferred);
|
||||
|
||||
for (int i = rv.getChildCount() - 1; i >= 0; i--) {
|
||||
ViewHolder viewHolder = rv.getChildViewHolder(rv.getChildAt(i));
|
||||
if (viewHolder.getItemViewType() == VIEW_TYPE_WIDGETS_LIST) {
|
||||
WidgetsRowViewHolder holder = (WidgetsRowViewHolder) viewHolder;
|
||||
for (int j = holder.cellContainer.getChildCount() - 1; j >= 0; j--) {
|
||||
View v = holder.cellContainer.getChildAt(j);
|
||||
if (v instanceof WidgetCell) {
|
||||
((WidgetCell) v).setApplyBitmapDeferred(isDeferred);
|
||||
for (int j = holder.mTableContainer.getChildCount() - 1; j >= 0; j--) {
|
||||
TableRow row = (TableRow) holder.mTableContainer.getChildAt(j);
|
||||
for (int k = row.getChildCount() - 1; k >= 0; k--) {
|
||||
((WidgetCell) row.getChildAt(k)).setApplyBitmapDeferred(isDeferred);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -204,6 +204,23 @@ public class WidgetsListAdapter extends Adapter<ViewHolder> implements OnHeaderC
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the max horizontal spans that are allowed for grouping more than one widgets in a table
|
||||
* row.
|
||||
*
|
||||
* <p>If there is only one widget in a row, that widget horizontal span is allowed to exceed
|
||||
* {@code maxHorizontalSpans}.
|
||||
* <p>Let's say the max horizontal spans is set to 5. Widgets can be grouped in the same row if
|
||||
* their total horizontal spans added don't exceed 5.
|
||||
* Example 1: Row 1: 2x2, 2x3, 1x1. Total horizontal spans is 5. This is okay.
|
||||
* Example 2: Row 1: 2x2, 4x3, 1x1. the total horizontal spans is 7. This is wrong.
|
||||
* 4x3 and 1x1 should be moved to a new row.
|
||||
* Example 3: Row 1: 6x4. This is okay because this is the only item in the row.
|
||||
*/
|
||||
public void setMaxHorizontalSpansPerRow(int maxHorizontalSpans) {
|
||||
mWidgetsListTableViewHolderBinder.setMaxSpansPerRow(maxHorizontalSpans);
|
||||
}
|
||||
|
||||
/** Comparator for sorting WidgetListRowEntry based on package title. */
|
||||
public static class WidgetListBaseRowEntryComparator implements
|
||||
Comparator<WidgetsListBaseEntry> {
|
||||
|
|
|
@ -22,25 +22,29 @@ import android.view.View;
|
|||
import android.view.View.OnClickListener;
|
||||
import android.view.View.OnLongClickListener;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.TableLayout;
|
||||
import android.widget.TableRow;
|
||||
|
||||
import com.android.launcher3.R;
|
||||
import com.android.launcher3.WidgetPreviewLoader;
|
||||
import com.android.launcher3.dragndrop.LivePreviewWidgetCell;
|
||||
import com.android.launcher3.model.WidgetItem;
|
||||
import com.android.launcher3.recyclerview.ViewHolderBinder;
|
||||
import com.android.launcher3.widget.WidgetCell;
|
||||
import com.android.launcher3.widget.model.WidgetsListContentEntry;
|
||||
import com.android.launcher3.widget.util.WidgetsTableUtils;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Binds data from {@link WidgetsListContentEntry} to UI elements in {@link WidgetsRowViewHolder}.
|
||||
*/
|
||||
public class WidgetsListRowViewHolderBinder
|
||||
public final class WidgetsListTableViewHolderBinder
|
||||
implements ViewHolderBinder<WidgetsListContentEntry, WidgetsRowViewHolder> {
|
||||
private static final boolean DEBUG = false;
|
||||
private static final String TAG = "WidgetsListRowViewHolderBinder";
|
||||
|
||||
private int mMaxSpansPerRow = 4;
|
||||
private final LayoutInflater mLayoutInflater;
|
||||
private final int mIndent;
|
||||
private final OnClickListener mIconClickListener;
|
||||
|
@ -48,7 +52,7 @@ public class WidgetsListRowViewHolderBinder
|
|||
private final WidgetPreviewLoader mWidgetPreviewLoader;
|
||||
private boolean mApplyBitmapDeferred = false;
|
||||
|
||||
public WidgetsListRowViewHolderBinder(
|
||||
public WidgetsListTableViewHolderBinder(
|
||||
Context context,
|
||||
LayoutInflater layoutInflater,
|
||||
OnClickListener iconClickListener,
|
||||
|
@ -70,6 +74,10 @@ public class WidgetsListRowViewHolderBinder
|
|||
mApplyBitmapDeferred = applyBitmapDeferred;
|
||||
}
|
||||
|
||||
public void setMaxSpansPerRow(int maxSpansPerRow) {
|
||||
mMaxSpansPerRow = maxSpansPerRow;
|
||||
}
|
||||
|
||||
@Override
|
||||
public WidgetsRowViewHolder newViewHolder(ViewGroup parent) {
|
||||
if (DEBUG) {
|
||||
|
@ -77,73 +85,92 @@ public class WidgetsListRowViewHolderBinder
|
|||
}
|
||||
|
||||
ViewGroup container = (ViewGroup) mLayoutInflater.inflate(
|
||||
R.layout.widgets_scroll_container, parent, false);
|
||||
R.layout.widgets_table_container, parent, false);
|
||||
|
||||
// if the end padding is 0, then container view (horizontal scroll view) doesn't respect
|
||||
// the end of the linear layout width + the start padding and doesn't allow scrolling.
|
||||
container.findViewById(R.id.widgets_cell_list).setPaddingRelative(mIndent, 0, 1, 0);
|
||||
container.findViewById(R.id.widgets_table).setPaddingRelative(mIndent, 0, 1, 0);
|
||||
|
||||
return new WidgetsRowViewHolder(container);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void bindViewHolder(WidgetsRowViewHolder holder, WidgetsListContentEntry entry) {
|
||||
List<WidgetItem> infoList = entry.mWidgets;
|
||||
|
||||
ViewGroup row = holder.cellContainer;
|
||||
TableLayout table = holder.mTableContainer;
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, String.format("onBindViewHolder [widget#=%d, row.getChildCount=%d]",
|
||||
infoList.size(), row.getChildCount()));
|
||||
Log.d(TAG, String.format("onBindViewHolder [widget#=%d, table.getChildCount=%d]",
|
||||
entry.mWidgets.size(), table.getChildCount()));
|
||||
}
|
||||
|
||||
// Add more views.
|
||||
// if there are too many, hide them.
|
||||
int expectedChildCount = infoList.size() + Math.max(0, infoList.size() - 1);
|
||||
int childCount = row.getChildCount();
|
||||
List<ArrayList<WidgetItem>> widgetItemsTable =
|
||||
WidgetsTableUtils.groupWidgetItemsIntoTable(entry.mWidgets, mMaxSpansPerRow);
|
||||
recycleTableBeforeBinding(table, widgetItemsTable);
|
||||
// Bind the widget items.
|
||||
for (int i = 0; i < widgetItemsTable.size(); i++) {
|
||||
List<WidgetItem> widgetItemsPerRow = widgetItemsTable.get(i);
|
||||
for (int j = 0; j < widgetItemsPerRow.size(); j++) {
|
||||
TableRow row = (TableRow) table.getChildAt(i);
|
||||
row.setVisibility(View.VISIBLE);
|
||||
WidgetCell widget = (WidgetCell) row.getChildAt(j);
|
||||
WidgetItem widgetItem = widgetItemsPerRow.get(j);
|
||||
widget.applyFromCellItem(widgetItem, mWidgetPreviewLoader);
|
||||
widget.setApplyBitmapDeferred(mApplyBitmapDeferred);
|
||||
widget.ensurePreview();
|
||||
widget.setVisibility(View.VISIBLE);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (expectedChildCount > childCount) {
|
||||
for (int i = childCount; i < expectedChildCount; i++) {
|
||||
if ((i & 1) == 1) {
|
||||
// Add a divider for odd index
|
||||
mLayoutInflater.inflate(R.layout.widget_list_divider, row);
|
||||
} else {
|
||||
// Add cell for even index
|
||||
LivePreviewWidgetCell widget = (LivePreviewWidgetCell) mLayoutInflater.inflate(
|
||||
R.layout.live_preview_widget_cell, row, false);
|
||||
/**
|
||||
* Adds and hides table rows and columns from {@code table} to ensure there is sufficient room
|
||||
* to display {@code widgetItemsTable}.
|
||||
*
|
||||
* <p>Instead of recreating all UI elements in {@code table}, this function recycles all
|
||||
* existing UI elements. Instead of deleting excessive elements, it hides them.
|
||||
*/
|
||||
private void recycleTableBeforeBinding(TableLayout table,
|
||||
List<ArrayList<WidgetItem>> widgetItemsTable) {
|
||||
// Hide extra table rows.
|
||||
for (int i = widgetItemsTable.size(); i < table.getChildCount(); i++) {
|
||||
table.getChildAt(i).setVisibility(View.GONE);
|
||||
}
|
||||
|
||||
for (int i = 0; i < widgetItemsTable.size(); i++) {
|
||||
List<WidgetItem> widgetItems = widgetItemsTable.get(i);
|
||||
TableRow tableRow;
|
||||
if (i < table.getChildCount()) {
|
||||
tableRow = (TableRow) table.getChildAt(i);
|
||||
} else {
|
||||
tableRow = new TableRow(table.getContext());
|
||||
table.addView(tableRow);
|
||||
}
|
||||
if (tableRow.getChildCount() > widgetItems.size()) {
|
||||
for (int j = widgetItems.size(); j < tableRow.getChildCount(); j++) {
|
||||
tableRow.getChildAt(j).setVisibility(View.GONE);
|
||||
}
|
||||
} else {
|
||||
for (int j = tableRow.getChildCount(); j < widgetItems.size(); j++) {
|
||||
WidgetCell widget = (WidgetCell) mLayoutInflater.inflate(
|
||||
R.layout.widget_cell, tableRow, false);
|
||||
// set up touch.
|
||||
widget.setOnClickListener(mIconClickListener);
|
||||
widget.setOnLongClickListener(mIconLongClickListener);
|
||||
row.addView(widget);
|
||||
tableRow.addView(widget);
|
||||
}
|
||||
}
|
||||
} else if (expectedChildCount < childCount) {
|
||||
for (int i = expectedChildCount; i < childCount; i++) {
|
||||
row.getChildAt(i).setVisibility(View.GONE);
|
||||
}
|
||||
}
|
||||
|
||||
// Bind the view in the widget horizontal tray region.
|
||||
for (int i = 0; i < infoList.size(); i++) {
|
||||
LivePreviewWidgetCell widget = (LivePreviewWidgetCell) row.getChildAt(2 * i);
|
||||
widget.reset();
|
||||
widget.applyFromCellItem(infoList.get(i), mWidgetPreviewLoader);
|
||||
widget.setApplyBitmapDeferred(mApplyBitmapDeferred);
|
||||
widget.ensurePreview();
|
||||
widget.setVisibility(View.VISIBLE);
|
||||
|
||||
if (i > 0) {
|
||||
row.getChildAt(2 * i - 1).setVisibility(View.VISIBLE);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void unbindViewHolder(WidgetsRowViewHolder holder) {
|
||||
int total = holder.cellContainer.getChildCount();
|
||||
for (int i = 0; i < total; i += 2) {
|
||||
WidgetCell widget = (WidgetCell) holder.cellContainer.getChildAt(i);
|
||||
widget.clear();
|
||||
int numOfRows = holder.mTableContainer.getChildCount();
|
||||
for (int i = 0; i < numOfRows; i++) {
|
||||
TableRow tableRow = (TableRow) holder.mTableContainer.getChildAt(i);
|
||||
int numOfCols = tableRow.getChildCount();
|
||||
for (int j = 0; j < numOfCols; j++) {
|
||||
WidgetCell widget = (WidgetCell) tableRow.getChildAt(j);
|
||||
widget.clear();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -16,6 +16,7 @@
|
|||
package com.android.launcher3.widget.picker;
|
||||
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.TableLayout;
|
||||
|
||||
import androidx.recyclerview.widget.RecyclerView.ViewHolder;
|
||||
|
||||
|
@ -24,11 +25,11 @@ import com.android.launcher3.R;
|
|||
/** A {@link ViewHolder} for showing widgets of an app in the full widget picker. */
|
||||
public final class WidgetsRowViewHolder extends ViewHolder {
|
||||
|
||||
public final ViewGroup cellContainer;
|
||||
public final TableLayout mTableContainer;
|
||||
|
||||
public WidgetsRowViewHolder(ViewGroup v) {
|
||||
super(v);
|
||||
|
||||
cellContainer = v.findViewById(R.id.widgets_cell_list);
|
||||
mTableContainer = v.findViewById(R.id.widgets_table);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,92 @@
|
|||
/*
|
||||
* 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.util;
|
||||
|
||||
import com.android.launcher3.model.WidgetItem;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/** An utility class which groups {@link WidgetItem}s into a table. */
|
||||
public final class WidgetsTableUtils {
|
||||
|
||||
/**
|
||||
* Groups widgets in the following order:
|
||||
* 1. Widgets always go before shortcuts.
|
||||
* 2. Widgets with smaller horizontal spans will be shown first.
|
||||
* 3. If widgets have the same horizontal spans, then widgets with a smaller vertical spans will
|
||||
* go first.
|
||||
* 4. If both widgets have the same horizontal and vertical spans, they will use the same order
|
||||
* from the given {@code widgetItems}.
|
||||
*/
|
||||
private static final Comparator<WidgetItem> WIDGET_SHORTCUT_COMPARATOR = (item, otherItem) -> {
|
||||
if (item.widgetInfo != null && otherItem.widgetInfo == null) return -1;
|
||||
|
||||
if (item.widgetInfo == null && otherItem.widgetInfo != null) return 1;
|
||||
if (item.spanX == otherItem.spanX) {
|
||||
if (item.spanY == otherItem.spanY) return 0;
|
||||
return item.spanY > otherItem.spanY ? 1 : -1;
|
||||
}
|
||||
return item.spanX > otherItem.spanX ? 1 : -1;
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Groups widgets items into a 2D array which matches their appearance in a UI table.
|
||||
*
|
||||
* <p>Grouping:
|
||||
* 1. Widgets and shortcuts never group together in the same row.
|
||||
* 2. The ordered widgets are grouped together in the same row until their total horizontal
|
||||
* spans exceed the {@code maxSpansPerRow}.
|
||||
* 3. The order shortcuts are grouped together in the same row until their total horizontal
|
||||
* spans exceed the {@code maxSpansPerRow}.
|
||||
*/
|
||||
public static List<ArrayList<WidgetItem>> groupWidgetItemsIntoTable(
|
||||
List<WidgetItem> widgetItems, final int maxSpansPerRow) {
|
||||
List<WidgetItem> sortedWidgetItems = widgetItems.stream().sorted(WIDGET_SHORTCUT_COMPARATOR)
|
||||
.collect(Collectors.toList());
|
||||
List<ArrayList<WidgetItem>> widgetItemsTable = new ArrayList<>();
|
||||
ArrayList<WidgetItem> widgetItemsAtRow = null;
|
||||
for (WidgetItem widgetItem : sortedWidgetItems) {
|
||||
if (widgetItemsAtRow == null) {
|
||||
widgetItemsAtRow = new ArrayList<>();
|
||||
widgetItemsTable.add(widgetItemsAtRow);
|
||||
}
|
||||
int numOfWidgetItems = widgetItemsAtRow.size();
|
||||
int totalHorizontalSpan = widgetItemsAtRow.stream().map(item -> item.spanX)
|
||||
.reduce(/* default= */ 0, Integer::sum);
|
||||
if (numOfWidgetItems == 0) {
|
||||
widgetItemsAtRow.add(widgetItem);
|
||||
} else if (widgetItem.spanX + totalHorizontalSpan <= maxSpansPerRow
|
||||
&& widgetItem.hasSameType(widgetItemsAtRow.get(numOfWidgetItems - 1))) {
|
||||
// Group items in the same row if
|
||||
// 1. they are with the same type, i.e. a row can only have widgets or shortcuts but
|
||||
// never a mix of both.
|
||||
// 2. the total number of horizontal spans are smaller than or equal to
|
||||
// MAX_SPAN_PER_ROW. If an item has a horizontal span > MAX_SPAN_PER_ROW, we just
|
||||
// place it in its own row regardless of the horizontal span limit.
|
||||
widgetItemsAtRow.add(widgetItem);
|
||||
} else {
|
||||
widgetItemsAtRow = new ArrayList<>();
|
||||
widgetItemsTable.add(widgetItemsAtRow);
|
||||
widgetItemsAtRow.add(widgetItem);
|
||||
}
|
||||
}
|
||||
return widgetItemsTable;
|
||||
}
|
||||
}
|
|
@ -27,7 +27,6 @@ import androidx.test.uiautomator.Direction;
|
|||
import androidx.test.uiautomator.UiObject2;
|
||||
import androidx.test.uiautomator.Until;
|
||||
|
||||
import com.android.launcher3.tapl.LauncherInstrumentation.GestureScope;
|
||||
import com.android.launcher3.testing.TestProtocol;
|
||||
|
||||
import java.util.Collection;
|
||||
|
@ -107,43 +106,25 @@ public final class Widgets extends LauncherInstrumentation.VisibleContainer {
|
|||
fullWidgetsPicker.wait(Until.scrollable(true), WAIT_TIME_MS));
|
||||
final Point displaySize = mLauncher.getRealDisplaySize();
|
||||
|
||||
final UiObject2 widgetsContainer = findTestAppWidgetsScrollContainer();
|
||||
final UiObject2 widgetsContainer = findTestAppWidgetsTableContainer();
|
||||
mLauncher.assertTrue("Can't locate widgets list for the test app: "
|
||||
+ mLauncher.getLauncherPackageName(),
|
||||
+ mLauncher.getLauncherPackageName(),
|
||||
widgetsContainer != null);
|
||||
final BySelector labelSelector = By.clazz("android.widget.TextView").text(labelText);
|
||||
int i = 0;
|
||||
for (; ; ) {
|
||||
final Collection<UiObject2> cells = widgetsContainer.getChildren();
|
||||
mLauncher.assertTrue("Widgets doesn't have 2 rows: ", cells.size() >= 2);
|
||||
for (UiObject2 cell : cells) {
|
||||
final UiObject2 label = cell.findObject(labelSelector);
|
||||
// The logic below doesn't handle the case which a widget cell of the given
|
||||
// label is not yet visible on the horizontal scrolling container. This won't be
|
||||
// an issue once we get rid of the horizontal scrolling container.
|
||||
if (label == null) continue;
|
||||
|
||||
final UiObject2 widget = cell;
|
||||
mLauncher.assertEquals(
|
||||
"View is not WidgetCell",
|
||||
"com.android.launcher3.widget.WidgetCell",
|
||||
widget.getClassName());
|
||||
|
||||
int maxWidth = 0;
|
||||
for (UiObject2 sibling : widget.getParent().getChildren()) {
|
||||
maxWidth = Math.max(mLauncher.getVisibleBounds(sibling).width(), maxWidth);
|
||||
}
|
||||
|
||||
if (mLauncher.getVisibleBounds(widget).bottom
|
||||
<= displaySize.y - mLauncher.getBottomGestureSize()) {
|
||||
int visibleDelta = maxWidth - mLauncher.getVisibleBounds(widget).width();
|
||||
if (visibleDelta > 0) {
|
||||
Rect parentBounds = mLauncher.getVisibleBounds(cell.getParent());
|
||||
mLauncher.linearGesture(parentBounds.centerX() + visibleDelta
|
||||
+ mLauncher.getTouchSlop(),
|
||||
parentBounds.centerY(), parentBounds.centerX(),
|
||||
parentBounds.centerY(), 10, true, GestureScope.INSIDE);
|
||||
final Collection<UiObject2> tableRows = widgetsContainer.getChildren();
|
||||
for (UiObject2 row : tableRows) {
|
||||
final Collection<UiObject2> widgetCells = row.getChildren();
|
||||
for (UiObject2 widget : widgetCells) {
|
||||
final UiObject2 label = widget.findObject(labelSelector);
|
||||
if (label == null) {
|
||||
continue;
|
||||
}
|
||||
mLauncher.assertEquals(
|
||||
"View is not WidgetCell",
|
||||
"com.android.launcher3.widget.WidgetCell",
|
||||
widget.getClassName());
|
||||
|
||||
return new Widget(mLauncher, widget);
|
||||
}
|
||||
|
@ -151,7 +132,7 @@ public final class Widgets extends LauncherInstrumentation.VisibleContainer {
|
|||
|
||||
mLauncher.assertTrue("Too many attempts", ++i <= 40);
|
||||
final int scroll = getWidgetsScroll();
|
||||
mLauncher.scrollToLastVisibleRow(widgetsContainer, cells, 0);
|
||||
mLauncher.scrollToLastVisibleRow(fullWidgetsPicker, tableRows, 0);
|
||||
final int newScroll = getWidgetsScroll();
|
||||
mLauncher.assertTrue(
|
||||
"Scrolled in a wrong direction in Widgets: from " + scroll + " to "
|
||||
|
@ -162,13 +143,13 @@ public final class Widgets extends LauncherInstrumentation.VisibleContainer {
|
|||
}
|
||||
|
||||
/** Finds the widgets list of this test app from the collapsed full widgets picker. */
|
||||
private UiObject2 findTestAppWidgetsScrollContainer() {
|
||||
private UiObject2 findTestAppWidgetsTableContainer() {
|
||||
final BySelector headerSelector = By.res(mLauncher.getLauncherPackageName(),
|
||||
"widgets_list_header");
|
||||
final BySelector targetAppSelector = By.clazz("android.widget.TextView").text(
|
||||
mLauncher.getContext().getPackageName());
|
||||
final BySelector widgetsContainerSelector = By.res(mLauncher.getLauncherPackageName(),
|
||||
"widgets_cell_list");
|
||||
"widgets_table");
|
||||
|
||||
boolean hasHeaderExpanded = false;
|
||||
for (int i = 0; i < 40; i++) {
|
||||
|
@ -196,14 +177,12 @@ public final class Widgets extends LauncherInstrumentation.VisibleContainer {
|
|||
// Look for a widgets list.
|
||||
UiObject2 widgetsContainer = fullWidgetsPicker.findObject(widgetsContainerSelector);
|
||||
if (widgetsContainer != null) {
|
||||
// Make sure the widgets list is fully visible on the screen.
|
||||
mLauncher.scrollToLastVisibleRow(fullWidgetsPicker,
|
||||
widgetsContainer.getChildren(), 0);
|
||||
return widgetsContainer;
|
||||
}
|
||||
mLauncher.scrollToLastVisibleRow(fullWidgetsPicker, List.of(headerTitle), 0);
|
||||
} else {
|
||||
mLauncher.scrollToLastVisibleRow(fullWidgetsPicker, header.getChildren(), 0);
|
||||
mLauncher.scrollToLastVisibleRow(fullWidgetsPicker, fullWidgetsPicker.getChildren(),
|
||||
0);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in New Issue