/* * Copyright (C) 2017 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; import static com.android.app.animation.Interpolators.EMPHASIZED; import static com.android.launcher3.Flags.enableWidgetTapToAdd; import static com.android.launcher3.LauncherState.NORMAL; import static com.android.launcher3.anim.AnimatorListeners.forSuccessCallback; import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_WIDGET_ADD_BUTTON_TAP; import android.content.Context; import android.graphics.Canvas; import android.graphics.Paint; import android.graphics.Rect; import android.util.AttributeSet; import android.util.Log; import android.view.View; import android.view.View.OnClickListener; import android.view.View.OnLongClickListener; import android.view.WindowInsets; import android.view.animation.Interpolator; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.Px; import com.android.launcher3.BaseActivity; import com.android.launcher3.DeviceProfile; import com.android.launcher3.DeviceProfile.OnDeviceProfileChangeListener; import com.android.launcher3.Insettable; import com.android.launcher3.Launcher; import com.android.launcher3.PendingAddItemInfo; import com.android.launcher3.R; import com.android.launcher3.model.WidgetItem; import com.android.launcher3.popup.PopupDataProvider; import com.android.launcher3.testing.TestLogging; import com.android.launcher3.testing.shared.TestProtocol; import com.android.launcher3.util.SystemUiController; import com.android.launcher3.util.Themes; import com.android.launcher3.util.window.WindowManagerProxy; import com.android.launcher3.views.AbstractSlideInView; import java.util.concurrent.atomic.AtomicBoolean; /** * Base class for various widgets popup */ public abstract class BaseWidgetSheet extends AbstractSlideInView implements OnClickListener, OnLongClickListener, PopupDataProvider.PopupDataChangeListener, Insettable, OnDeviceProfileChangeListener { /** The default number of cells that can fit horizontally in a widget sheet. */ public static final int DEFAULT_MAX_HORIZONTAL_SPANS = 4; protected final Rect mInsets = new Rect(); @Px protected int mContentHorizontalMargin; @Px protected int mWidgetCellHorizontalPadding; protected int mNavBarScrimHeight; private final Paint mNavBarScrimPaint; private boolean mDisableNavBarScrim = false; @Nullable private WidgetCell mWidgetCellWithAddButton = null; @Nullable private WidgetItem mLastSelectedWidgetItem = null; public BaseWidgetSheet(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); mContentHorizontalMargin = getWidgetListHorizontalMargin(); mWidgetCellHorizontalPadding = getResources().getDimensionPixelSize( R.dimen.widget_cell_horizontal_padding); mNavBarScrimPaint = new Paint(); mNavBarScrimPaint.setColor(Themes.getNavBarScrimColor(mActivityContext)); } /** * Returns the margins to be applied to the left and right of the widget apps list. */ protected int getWidgetListHorizontalMargin() { return getResources().getDimensionPixelSize( R.dimen.widget_list_horizontal_margin); } protected int getScrimColor(Context context) { return context.getResources().getColor(R.color.widgets_picker_scrim); } @Override protected void onAttachedToWindow() { super.onAttachedToWindow(); WindowInsets windowInsets = WindowManagerProxy.INSTANCE.get(getContext()) .normalizeWindowInsets(getContext(), getRootWindowInsets(), new Rect()); mNavBarScrimHeight = getNavBarScrimHeight(windowInsets); mActivityContext.getPopupDataProvider().setChangeListener(this); mActivityContext.addOnDeviceProfileChangeListener(this); } @Override protected void onDetachedFromWindow() { super.onDetachedFromWindow(); mActivityContext.getPopupDataProvider().setChangeListener(null); mActivityContext.removeOnDeviceProfileChangeListener(this); } @Override public void onDeviceProfileChanged(DeviceProfile dp) { int navBarScrimColor = Themes.getNavBarScrimColor(mActivityContext); if (mNavBarScrimPaint.getColor() != navBarScrimColor) { mNavBarScrimPaint.setColor(navBarScrimColor); invalidate(); } setupNavBarColor(); } @Override public final void onClick(View v) { WidgetCell wc; if (v instanceof WidgetCell view) { wc = view; } else if (v.getParent() instanceof WidgetCell parent) { wc = parent; } else { return; } if (enableWidgetTapToAdd()) { scrollToWidgetCell(wc); if (mWidgetCellWithAddButton != null) { if (mWidgetCellWithAddButton.isShowingAddButton()) { // If there is a add button currently showing, hide it. mWidgetCellWithAddButton.hideAddButton(/* animate= */ true); } else { // The last recorded widget cell to show an add button is no longer showing it, // likely because the widget cell has been recycled or lost focus. If this is // the cell that has been clicked, we will show it below. mWidgetCellWithAddButton = null; } } if (mWidgetCellWithAddButton != wc) { // If click is on a cell not showing an add button, show it now. final PendingAddItemInfo info = (PendingAddItemInfo) wc.getTag(); if (mActivityContext instanceof Launcher) { wc.showAddButton((view) -> addWidget(info)); } else { wc.showAddButton((view) -> mActivityContext.getItemOnClickListener() .onClick(wc)); } } mWidgetCellWithAddButton = mWidgetCellWithAddButton != wc ? wc : null; if (mWidgetCellWithAddButton != null) { mLastSelectedWidgetItem = mWidgetCellWithAddButton.getWidgetItem(); } else { mLastSelectedWidgetItem = null; } } else { mActivityContext.getItemOnClickListener().onClick(wc); } } @Override protected float getShiftRange() { // We add the extra height added during predictive back / swipe up to the shift range, so // that the idle interpolator knows to animate the view off fully. return mContent.getHeight() + getBottomOffsetPx(); } /** * Click handler for tap to add button. This handler assumes we are in the Launcher activity and * should not be used when the widget sheet is displayed elsewhere. */ private void addWidget(@NonNull PendingAddItemInfo info) { // Using a boolean flag here to make sure the callback is only run once. This should never // happen because we close the sheet and it will be reconstructed the next time it is // needed. final AtomicBoolean hasRun = new AtomicBoolean(false); addOnCloseListener(() -> { if (hasRun.get()) return; hasRun.set(true); // Going to NORMAL state will also dismiss the All Apps view if it is showing. Launcher launcher = Launcher.getLauncher(mActivityContext); launcher.getStateManager().goToState(NORMAL, forSuccessCallback(() -> { launcher.getAccessibilityDelegate().addToWorkspace(info, /*accessibility=*/ false, /*finishCallback=*/ (success) -> { mActivityContext.getStatsLogManager() .logger() .withItemInfo(info) .log(LAUNCHER_WIDGET_ADD_BUTTON_TAP); }); })); }); close(/* animate= */ true); } /** * Scroll to show the widget cell. If both the bottom and top of the cell are clipped, this will * prioritize showing the bottom of the cell (where the add button is). */ private void scrollToWidgetCell(@NonNull WidgetCell wc) { final int headerTopClip = getHeaderTopClip(wc); final Rect visibleRect = new Rect(); final boolean isPartiallyVisible = wc.getLocalVisibleRect(visibleRect); int scrollByY = 0; if (isPartiallyVisible) { final int scrollPadding = getResources() .getDimensionPixelSize(R.dimen.widget_cell_add_button_scroll_padding); final int topClip = visibleRect.top + headerTopClip; final int bottomClip = wc.getHeight() - visibleRect.bottom; if (bottomClip != 0) { scrollByY = bottomClip + scrollPadding; } else if (topClip != 0) { scrollByY = -topClip - scrollPadding; } } if (isPartiallyVisible && scrollByY == 0) { // Widget is fully visible. return; } else if (!isPartiallyVisible) { Log.e("BaseWidgetSheet", "click on invisible WidgetCell should not be possible"); return; } scrollCellContainerByY(wc, scrollByY); } /** * Find the nearest scrollable container of the given WidgetCell, and scroll by the given * amount. */ protected abstract void scrollCellContainerByY(WidgetCell wc, int scrollByY); /** * Return the top clip of any sticky headers over the given cell. */ protected int getHeaderTopClip(@NonNull WidgetCell cell) { return 0; } /** * Returns the component of the widget that is currently showing an add button, if any. */ @Nullable protected WidgetItem getLastSelectedWidgetItem() { return mLastSelectedWidgetItem; } @Override public boolean onLongClick(View v) { TestLogging.recordEvent(TestProtocol.SEQUENCE_MAIN, "Widgets.onLongClick"); v.cancelLongPress(); boolean result; if (v instanceof WidgetCell) { result = mActivityContext.getAllAppsItemLongClickListener().onLongClick(v); } else if (v.getParent() instanceof WidgetCell wc) { result = mActivityContext.getAllAppsItemLongClickListener().onLongClick(wc); } else { return true; } if (result) { close(true); } return result; } @Override public void setInsets(Rect insets) { mInsets.set(insets); @Px int contentHorizontalMargin = getWidgetListHorizontalMargin(); if (contentHorizontalMargin != mContentHorizontalMargin) { onContentHorizontalMarginChanged(contentHorizontalMargin); mContentHorizontalMargin = contentHorizontalMargin; } } /** Enables or disables the sheet's nav bar scrim. */ public void disableNavBarScrim(boolean disable) { mDisableNavBarScrim = disable; } private int getNavBarScrimHeight(WindowInsets insets) { if (mDisableNavBarScrim) { return 0; } else { return insets.getTappableElementInsets().bottom; } } @Override public WindowInsets onApplyWindowInsets(WindowInsets insets) { mNavBarScrimHeight = getNavBarScrimHeight(insets); return super.onApplyWindowInsets(insets); } @Override protected void dispatchDraw(Canvas canvas) { super.dispatchDraw(canvas); if (mNavBarScrimHeight > 0) { canvas.drawRect(0, getHeight() - mNavBarScrimHeight, getWidth(), getHeight(), mNavBarScrimPaint); } } /** Called when the horizontal margin of the content view has changed. */ protected abstract void onContentHorizontalMarginChanged(int contentHorizontalMarginInPx); /** * Measures the dimension of this view and its children by taking system insets, navigation bar, * status bar, into account. */ protected void doMeasure(int widthMeasureSpec, int heightMeasureSpec) { DeviceProfile deviceProfile = mActivityContext.getDeviceProfile(); int widthUsed; if (deviceProfile.isTablet) { widthUsed = Math.max(2 * getTabletHorizontalMargin(deviceProfile), 2 * (mInsets.left + mInsets.right)); } else if (mInsets.bottom > 0) { widthUsed = mInsets.left + mInsets.right; } else { Rect padding = deviceProfile.workspacePadding; widthUsed = Math.max(padding.left + padding.right, 2 * (mInsets.left + mInsets.right)); } measureChildWithMargins(mContent, widthMeasureSpec, widthUsed, heightMeasureSpec, deviceProfile.bottomSheetTopPadding); setMeasuredDimension(MeasureSpec.getSize(widthMeasureSpec), MeasureSpec.getSize(heightMeasureSpec)); } /** Returns the horizontal margins to be applied to the widget sheet. **/ protected int getTabletHorizontalMargin(DeviceProfile deviceProfile) { return deviceProfile.allAppsLeftRightMargin; } @Override protected Interpolator getIdleInterpolator() { return mActivityContext.getDeviceProfile().isTablet ? EMPHASIZED : super.getIdleInterpolator(); } protected void onCloseComplete() { super.onCloseComplete(); clearNavBarColor(); } protected void clearNavBarColor() { getSystemUiController().updateUiState( SystemUiController.UI_STATE_WIDGET_BOTTOM_SHEET, 0); } protected void setupNavBarColor() { boolean isNavBarDark = Themes.getAttrBoolean(getContext(), R.attr.isMainColorDark); // In light mode, landscape reverses navbar background color. boolean isPhoneLandscape = !mActivityContext.getDeviceProfile().isTablet && mInsets.bottom == 0; if (!isNavBarDark && isPhoneLandscape) { isNavBarDark = true; } getSystemUiController().updateUiState(SystemUiController.UI_STATE_WIDGET_BOTTOM_SHEET, isNavBarDark ? SystemUiController.FLAG_DARK_NAV : SystemUiController.FLAG_LIGHT_NAV); } protected SystemUiController getSystemUiController() { return mActivityContext.getSystemUiController(); } @Override protected void setTranslationShift(float translationShift) { super.setTranslationShift(translationShift); if (mActivityContext instanceof Launcher ls) { ls.onWidgetsTransition(1 - translationShift); } } }