Loading res/values/dimens.xml +3 −0 Original line number Diff line number Diff line Loading @@ -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> Loading src/com/android/launcher3/views/ArrowTipView.java +60 −1 Original line number Diff line number Diff line Loading @@ -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; Loading @@ -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; Loading @@ -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; Loading Loading @@ -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( Loading Loading @@ -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 */ Loading src/com/android/launcher3/widget/picker/WidgetsFullSheet.java +81 −0 Original line number Diff line number Diff line Loading @@ -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; Loading @@ -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; Loading @@ -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 Loading @@ -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; Loading @@ -92,6 +97,35 @@ public class WidgetsFullSheet extends BaseWidgetSheet 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; Loading Loading @@ -170,6 +204,10 @@ public class WidgetsFullSheet extends BaseWidgetSheet mSearchAndRecommendationViewHolder.mSearchBar.initialize( mLauncher.getPopupDataProvider(), /* searchModeListener= */ this); if (!hasSeenEducationTip()) { addOnLayoutChangeListener(mLayoutChangeListenerToShowTips); } } @Override Loading Loading @@ -564,6 +602,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; Loading Loading
res/values/dimens.xml +3 −0 Original line number Diff line number Diff line Loading @@ -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> Loading
src/com/android/launcher3/views/ArrowTipView.java +60 −1 Original line number Diff line number Diff line Loading @@ -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; Loading @@ -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; Loading @@ -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; Loading Loading @@ -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( Loading Loading @@ -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 */ Loading
src/com/android/launcher3/widget/picker/WidgetsFullSheet.java +81 −0 Original line number Diff line number Diff line Loading @@ -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; Loading @@ -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; Loading @@ -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 Loading @@ -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; Loading @@ -92,6 +97,35 @@ public class WidgetsFullSheet extends BaseWidgetSheet 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; Loading Loading @@ -170,6 +204,10 @@ public class WidgetsFullSheet extends BaseWidgetSheet mSearchAndRecommendationViewHolder.mSearchBar.initialize( mLauncher.getPopupDataProvider(), /* searchModeListener= */ this); if (!hasSeenEducationTip()) { addOnLayoutChangeListener(mLayoutChangeListenerToShowTips); } } @Override Loading Loading @@ -564,6 +602,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; Loading