Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 120d6476 authored by Alina Zaidi's avatar Alina Zaidi
Browse files

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
parent 5427fa7b
Loading
Loading
Loading
Loading
+3 −0
Original line number Diff line number Diff line
@@ -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>
+60 −1
Original line number Diff line number Diff line
@@ -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
     */
+81 −0
Original line number Diff line number Diff line
@@ -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<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 @@ 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;