Add education tip to widget picker.
A tip is shown on the first widget/shortcut in the recommended table.
If there are no recommended widgets, a tip is shown on first widget
in an expanded header.
There is a delay of few milliseconds, to let the WidgetCells be
completely rendered on screen before getting their location.
Test: Manually tested
Bug: 184920163
Change-Id: I2637e84e7fc467b27888023434e3578a4b8ed4d6
diff --git a/res/values/dimens.xml b/res/values/dimens.xml
index 51dddab..638eec7 100644
--- a/res/values/dimens.xml
+++ b/res/values/dimens.xml
@@ -140,6 +140,9 @@
<dimen name="widget_row_padding">8dp</dimen>
<dimen name="widget_row_divider">2dp</dimen>
+ <dimen name="widget_picker_education_tip_width">120dp</dimen>
+ <dimen name="widget_picker_education_tip_min_margin">4dp</dimen>
+
<!-- Padding applied to shortcut previews -->
<dimen name="shortcut_preview_padding_left">0dp</dimen>
<dimen name="shortcut_preview_padding_right">0dp</dimen>
diff --git a/src/com/android/launcher3/views/ArrowTipView.java b/src/com/android/launcher3/views/ArrowTipView.java
index 1f12a2f..89ff821 100644
--- a/src/com/android/launcher3/views/ArrowTipView.java
+++ b/src/com/android/launcher3/views/ArrowTipView.java
@@ -21,6 +21,7 @@
import android.graphics.Paint;
import android.graphics.drawable.ShapeDrawable;
import android.os.Handler;
+import android.util.Log;
import android.util.TypedValue;
import android.view.Gravity;
import android.view.MotionEvent;
@@ -29,6 +30,8 @@
import android.widget.LinearLayout;
import android.widget.TextView;
+import androidx.annotation.Nullable;
+import androidx.annotation.Px;
import androidx.core.content.ContextCompat;
import com.android.launcher3.AbstractFloatingView;
@@ -43,6 +46,7 @@
*/
public class ArrowTipView extends AbstractFloatingView {
+ private static final String TAG = ArrowTipView.class.getSimpleName();
private static final long AUTO_CLOSE_TIMEOUT_MILLIS = 10 * 1000;
private static final long SHOW_DELAY_MS = 200;
private static final long SHOW_DURATION_MS = 300;
@@ -105,7 +109,8 @@
arrowLp.width, arrowLp.height, false));
Paint arrowPaint = arrowDrawable.getPaint();
TypedValue typedValue = new TypedValue();
- context.getTheme().resolveAttribute(android.R.attr.colorAccent, typedValue, true);
+ context.getTheme()
+ .resolveAttribute(android.R.attr.colorAccent, typedValue, true);
arrowPaint.setColor(ContextCompat.getColor(getContext(), typedValue.resourceId));
// The corner path effect won't be reflected in the shadow, but shouldn't be noticeable.
arrowPaint.setPathEffect(new CornerPathEffect(
@@ -165,6 +170,60 @@
}
/**
+ * Show the ArrowTipView (tooltip) custom aligned.
+ *
+ * @param text The text to be shown in the tooltip.
+ * @param arrowXCoord The X coordinate for the arrow on the tip. The arrow is usually in the
+ * center of ArrowTipView unless the ArrowTipView goes beyond screen margin.
+ * @param yCoord The Y coordinate of the bottom of the tooltip.
+ * @return The tool tip view.
+ */
+ @Nullable public ArrowTipView showAtLocation(String text, int arrowXCoord, int yCoord) {
+ ViewGroup parent = mActivity.getDragLayer();
+ @Px int parentViewWidth = parent.getWidth();
+ @Px int textViewWidth = getContext().getResources()
+ .getDimensionPixelSize(R.dimen.widget_picker_education_tip_width);
+ @Px int minViewMargin = getContext().getResources()
+ .getDimensionPixelSize(R.dimen.widget_picker_education_tip_min_margin);
+ if (parentViewWidth < textViewWidth + 2 * minViewMargin) {
+ Log.w(TAG, "Cannot display tip on a small screen of size: " + parentViewWidth);
+ return null;
+ }
+
+ TextView textView = findViewById(R.id.text);
+ textView.setText(text);
+ textView.setWidth(textViewWidth);
+ parent.addView(this);
+ requestLayout();
+
+ post(() -> setY(yCoord - getHeight()));
+ post(() -> {
+ float halfWidth = getWidth() / 2f;
+ float xCoord;
+ if (arrowXCoord - halfWidth < minViewMargin) {
+ xCoord = minViewMargin;
+ } else if (arrowXCoord + halfWidth > parentViewWidth - minViewMargin) {
+ xCoord = parentViewWidth - minViewMargin - getWidth();
+ } else {
+ xCoord = arrowXCoord - halfWidth;
+ }
+ setX(xCoord);
+ findViewById(R.id.arrow).setX(arrowXCoord - xCoord);
+ requestLayout();
+ });
+
+ setAlpha(0);
+ animate()
+ .alpha(1f)
+ .withLayer()
+ .setStartDelay(SHOW_DELAY_MS)
+ .setDuration(SHOW_DURATION_MS)
+ .setInterpolator(Interpolators.DEACCEL)
+ .start();
+ return this;
+ }
+
+ /**
* Register a callback fired when toast is hidden
*/
public ArrowTipView setOnClosedCallback(Runnable runnable) {
diff --git a/src/com/android/launcher3/widget/picker/WidgetsFullSheet.java b/src/com/android/launcher3/widget/picker/WidgetsFullSheet.java
index a4257a2..5d9a2e2 100644
--- a/src/com/android/launcher3/widget/picker/WidgetsFullSheet.java
+++ b/src/com/android/launcher3/widget/picker/WidgetsFullSheet.java
@@ -41,6 +41,7 @@
import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
+import androidx.core.view.ViewCompat;
import androidx.recyclerview.widget.RecyclerView;
import com.android.launcher3.DeviceProfile;
@@ -51,6 +52,7 @@
import com.android.launcher3.anim.PendingAnimation;
import com.android.launcher3.compat.AccessibilityManagerCompat;
import com.android.launcher3.model.WidgetItem;
+import com.android.launcher3.views.ArrowTipView;
import com.android.launcher3.views.RecyclerViewFastScroller;
import com.android.launcher3.views.TopRoundedCornerView;
import com.android.launcher3.widget.BaseWidgetSheet;
@@ -66,6 +68,7 @@
import java.util.ArrayList;
import java.util.List;
import java.util.function.Predicate;
+import java.util.stream.IntStream;
/**
* Popup for showing the full list of available widgets
@@ -78,11 +81,13 @@
private static final long DEFAULT_OPEN_DURATION = 267;
private static final long FADE_IN_DURATION = 150;
+ private static final long EDUCATION_TIP_DELAY_MS = 200;
private static final float VERTICAL_START_POSITION = 0.3f;
// The widget recommendation table can easily take over the entire screen on devices with small
// resolution or landscape on phone. This ratio defines the max percentage of content area that
// the table can display.
private static final float RECOMMENDATION_TABLE_HEIGHT_RATIO = 0.75f;
+ private static final String WIDGETS_EDUCATION_TIP_SEEN = "launcher.widgets_education_tip_seen";
private final Rect mInsets = new Rect();
private final boolean mHasWorkProfile;
@@ -92,6 +97,35 @@
mCurrentUser.equals(entry.mPkgItem.user);
private final Predicate<WidgetsListBaseEntry> mWorkWidgetsFilter =
mPrimaryWidgetsFilter.negate();
+ private final OnLayoutChangeListener mLayoutChangeListenerToShowTips =
+ new OnLayoutChangeListener() {
+ @Override
+ public void onLayoutChange(View v, int left, int top, int right, int bottom,
+ int oldLeft, int oldTop, int oldRight, int oldBottom) {
+ if (hasSeenEducationTip()) {
+ removeOnLayoutChangeListener(this);
+ return;
+ }
+
+ // Widgets are loaded asynchronously, We are adding a delay because we only want
+ // to show the tip when the widget preview has finished loading and rendering in
+ // this view.
+ removeCallbacks(mShowEducationTipTask);
+ postDelayed(mShowEducationTipTask, EDUCATION_TIP_DELAY_MS);
+ }
+ };
+
+ private final Runnable mShowEducationTipTask = () -> {
+ if (hasSeenEducationTip()) {
+ removeOnLayoutChangeListener(mLayoutChangeListenerToShowTips);
+ return;
+ }
+ View viewForTip = getViewToShowEducationTip();
+ if (viewForTip != null && ViewCompat.isLaidOut(viewForTip)) {
+ removeOnLayoutChangeListener(mLayoutChangeListenerToShowTips);
+ showEducationTipOnView(viewForTip);
+ }
+ };
private final int mTabsHeight;
private final int mWidgetCellHorizontalPadding;
@@ -170,6 +204,10 @@
mSearchAndRecommendationViewHolder.mSearchBar.initialize(
mLauncher.getPopupDataProvider(), /* searchModeListener= */ this);
+
+ if (!hasSeenEducationTip()) {
+ addOnLayoutChangeListener(mLayoutChangeListenerToShowTips);
+ }
}
@Override
@@ -563,6 +601,49 @@
mSearchAndRecommendationViewHolder.mSearchBar.clearSearchBarFocus();
}
+ private void showEducationTipOnView(View view) {
+ mLauncher.getSharedPrefs().edit().putBoolean(WIDGETS_EDUCATION_TIP_SEEN, true).apply();
+ int[] coords = new int[2];
+ view.getLocationOnScreen(coords);
+ ArrowTipView arrowTipView = new ArrowTipView(mLauncher);
+ arrowTipView.showAtLocation(
+ getContext().getString(R.string.long_press_widget_to_add),
+ /* arrowXCoord= */coords[0] + view.getWidth() / 2,
+ /* yCoord= */coords[1]);
+ }
+
+ @Nullable private View getViewToShowEducationTip() {
+ if (mSearchAndRecommendationViewHolder.mRecommendedWidgetsTable.getVisibility() == VISIBLE
+ && mSearchAndRecommendationViewHolder.mRecommendedWidgetsTable.getChildCount() > 0
+ ) {
+ return ((ViewGroup) mSearchAndRecommendationViewHolder.mRecommendedWidgetsTable
+ .getChildAt(0)).getChildAt(0);
+ }
+
+ AdapterHolder adapterHolder = mAdapters.get(mIsInSearchMode
+ ? AdapterHolder.SEARCH
+ : mViewPager == null
+ ? AdapterHolder.PRIMARY
+ : mViewPager.getCurrentPage());
+ WidgetsRowViewHolder viewHolderForTip =
+ (WidgetsRowViewHolder) IntStream.range(
+ 0, adapterHolder.mWidgetsListAdapter.getItemCount())
+ .mapToObj(adapterHolder.mWidgetsRecyclerView::
+ findViewHolderForAdapterPosition)
+ .filter(viewHolder -> viewHolder instanceof WidgetsRowViewHolder)
+ .findFirst()
+ .orElse(null);
+ if (viewHolderForTip != null) {
+ return ((ViewGroup) viewHolderForTip.mTableContainer.getChildAt(0)).getChildAt(0);
+ }
+
+ return null;
+ }
+
+ private boolean hasSeenEducationTip() {
+ return mLauncher.getSharedPrefs().getBoolean(WIDGETS_EDUCATION_TIP_SEEN, false);
+ }
+
/** A holder class for holding adapters & their corresponding recycler view. */
private final class AdapterHolder {
static final int PRIMARY = 0;