From 120d6476f6fd3edc621a17da4954635e0e173d07 Mon Sep 17 00:00:00 2001 From: Alina Zaidi Date: Fri, 9 Apr 2021 09:58:53 +0100 Subject: [PATCH] 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 --- res/values/dimens.xml | 3 + .../android/launcher3/views/ArrowTipView.java | 61 +++++++++++++- .../widget/picker/WidgetsFullSheet.java | 81 +++++++++++++++++++ 3 files changed, 144 insertions(+), 1 deletion(-) diff --git a/res/values/dimens.xml b/res/values/dimens.xml index 51dddab6f7..638eec7426 100644 --- a/res/values/dimens.xml +++ b/res/values/dimens.xml @@ -140,6 +140,9 @@ 8dp 2dp + 120dp + 4dp + 0dp 0dp diff --git a/src/com/android/launcher3/views/ArrowTipView.java b/src/com/android/launcher3/views/ArrowTipView.java index 1f12a2f8c8..89ff8216c9 100644 --- a/src/com/android/launcher3/views/ArrowTipView.java +++ b/src/com/android/launcher3/views/ArrowTipView.java @@ -21,6 +21,7 @@ import android.graphics.CornerPathEffect; 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.view.ViewGroup; 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 @@ import com.android.launcher3.graphics.TriangleShape; */ 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 @@ public class ArrowTipView extends AbstractFloatingView { 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( @@ -164,6 +169,60 @@ public class ArrowTipView extends AbstractFloatingView { return this; } + /** + * 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 */ diff --git a/src/com/android/launcher3/widget/picker/WidgetsFullSheet.java b/src/com/android/launcher3/widget/picker/WidgetsFullSheet.java index a4257a208e..5d9a2e2f88 100644 --- a/src/com/android/launcher3/widget/picker/WidgetsFullSheet.java +++ b/src/com/android/launcher3/widget/picker/WidgetsFullSheet.java @@ -41,6 +41,7 @@ import android.widget.TextView; 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.R; 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 com.android.launcher3.workprofile.PersonalWorkSlidingTabStrip.OnActivePag 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 @@ public class WidgetsFullSheet extends BaseWidgetSheet 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 @@ public class WidgetsFullSheet extends BaseWidgetSheet mCurrentUser.equals(entry.mPkgItem.user); private final Predicate 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 @@ public class WidgetsFullSheet extends BaseWidgetSheet mSearchAndRecommendationViewHolder.mSearchBar.initialize( mLauncher.getPopupDataProvider(), /* searchModeListener= */ this); + + if (!hasSeenEducationTip()) { + addOnLayoutChangeListener(mLayoutChangeListenerToShowTips); + } } @Override @@ -563,6 +601,49 @@ public class WidgetsFullSheet extends BaseWidgetSheet 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;