diff options
Diffstat (limited to 'src')
17 files changed, 442 insertions, 112 deletions
diff --git a/src/com/android/launcher3/DeviceProfile.java b/src/com/android/launcher3/DeviceProfile.java index c85ca49c43..090208a33c 100644 --- a/src/com/android/launcher3/DeviceProfile.java +++ b/src/com/android/launcher3/DeviceProfile.java @@ -30,8 +30,8 @@ import static com.android.launcher3.testing.shared.ResourceUtils.INVALID_RESOURC import static com.android.launcher3.testing.shared.ResourceUtils.pxFromDp; import static com.android.launcher3.testing.shared.ResourceUtils.roundPxValueFromFloat; import static com.android.wm.shell.Flags.enableBubbleBar; -import static com.android.wm.shell.Flags.enableTinyTaskbar; import static com.android.wm.shell.Flags.enableBubbleBarOnPhones; +import static com.android.wm.shell.Flags.enableTinyTaskbar; import android.annotation.SuppressLint; import android.content.Context; @@ -508,9 +508,11 @@ public class DeviceProfile { bottomSheetOpenDuration = res.getInteger(R.integer.config_bottomSheetOpenDuration); bottomSheetCloseDuration = res.getInteger(R.integer.config_bottomSheetCloseDuration); - if (isTablet) { + if (shouldShowAllAppsOnSheet()) { bottomSheetWorkspaceScale = workspaceContentScale; - if (isMultiDisplay) { + if (Flags.allAppsBlur()) { + bottomSheetDepth = 2f; + } else if (isMultiDisplay) { // TODO(b/259893832): Revert to use maxWallpaperScale to calculate bottomSheetDepth // when screen recorder bug is fixed. if (enableScalingRevealHomeAnimation()) { @@ -1830,10 +1832,17 @@ public class DeviceProfile { workspacePageIndicatorHeight - mWorkspacePageIndicatorOverlapWorkspace; } int paddingTop = workspaceTopPadding + (mIsScalableGrid ? 0 : edgeMarginPx); - // On isFixedLandscapeMode on phones we already have padding because of the camera hole - int paddingSide = inv.isFixedLandscape ? 0 : desiredWorkspaceHorizontalMarginPx; + int paddingLeft = desiredWorkspaceHorizontalMarginPx; + int paddingRight = desiredWorkspaceHorizontalMarginPx; - padding.set(paddingSide, paddingTop, paddingSide, paddingBottom); + // In fixed Landscape we don't need padding on the side next to the cutout because + // the cutout is already adding padding to all of Launcher, we only need on the other + // side + if (inv.isFixedLandscape) { + paddingLeft = isSeascape() ? desiredWorkspaceHorizontalMarginPx : 0; + paddingRight = isSeascape() ? 0 : desiredWorkspaceHorizontalMarginPx; + } + padding.set(paddingLeft, paddingTop, paddingRight, paddingBottom); } insetPadding(workspacePadding, cellLayoutPaddingPx); } @@ -1931,7 +1940,24 @@ public class DeviceProfile { hotseatBarPadding.set(mHotseatBarWorkspaceSpacePx, paddingTop, mInsets.right + mHotseatBarEdgePaddingPx, paddingBottom); } - } else if (isTaskbarPresent || inv.isFixedLandscape) { + } else if (inv.isFixedLandscape) { + // Center the QSB vertically with hotseat + int hotseatBarBottomPadding = getHotseatBarBottomPadding(); + int hotseatPlusQSBWidth = getHotseatRequiredWidth(); + int qsbWidth = getAdditionalQsbSpace(); + int availableWidthPxForHotseat = availableWidthPx - Math.abs(workspacePadding.width()) + - Math.abs(cellLayoutPaddingPx.width()); + int remainingSpaceOnSide = (availableWidthPxForHotseat - hotseatPlusQSBWidth) / 2; + + hotseatBarPadding.set( + (remainingSpaceOnSide + qsbWidth) + mInsets.left + workspacePadding.left + + cellLayoutPaddingPx.left, + hotseatBarSizePx - hotseatBarBottomPadding - hotseatCellHeightPx, + remainingSpaceOnSide + mInsets.right + workspacePadding.right + + cellLayoutPaddingPx.right, + hotseatBarBottomPadding + ); + } else if (isTaskbarPresent) { // Center the QSB vertically with hotseat int hotseatBarBottomPadding = getHotseatBarBottomPadding(); int hotseatBarTopPadding = @@ -1950,11 +1976,6 @@ public class DeviceProfile { } startSpacing += getAdditionalQsbSpace(); - if (inv.isFixedLandscape) { - endSpacing += mInsets.right; - startSpacing += mInsets.left; - } - hotseatBarPadding.top = hotseatBarTopPadding; hotseatBarPadding.bottom = hotseatBarBottomPadding; boolean isRtl = Utilities.isRtl(context.getResources()); @@ -2164,7 +2185,8 @@ public class DeviceProfile { } public boolean isSeascape() { - return rotationHint == Surface.ROTATION_270 && isVerticalBarLayout(); + return rotationHint == Surface.ROTATION_270 + && (isVerticalBarLayout() || inv.isFixedLandscape); } public boolean shouldFadeAdjacentWorkspaceScreens() { diff --git a/src/com/android/launcher3/allapps/ActivityAllAppsContainerView.java b/src/com/android/launcher3/allapps/ActivityAllAppsContainerView.java index fafa60bf6d..f60896eb6d 100644 --- a/src/com/android/launcher3/allapps/ActivityAllAppsContainerView.java +++ b/src/com/android/launcher3/allapps/ActivityAllAppsContainerView.java @@ -181,6 +181,7 @@ public class ActivityAllAppsContainerView<T extends Context & ActivityContext> private ScrimView mScrimView; private int mHeaderColor; private int mBottomSheetBackgroundColor; + private float mBottomSheetBackgroundAlpha = 1f; private int mTabsProtectionAlpha; @Nullable private AllAppsTransitionController mAllAppsTransitionController; @@ -311,7 +312,17 @@ public class ActivityAllAppsContainerView<T extends Context & ActivityContext> 0, 0 // Bottom left }; - mBottomSheetBackgroundColor = getContext().getColor(R.color.materialColorSurfaceDim); + if (Flags.allAppsBlur()) { + int resId = Utilities.isDarkTheme(getContext()) + ? android.R.color.system_accent1_800 : android.R.color.system_accent1_100; + int layerAbove = ColorUtils.setAlphaComponent(getResources().getColor(resId, null), + (int) (0.4f * 255)); + int layerBelow = ColorUtils.setAlphaComponent(Color.WHITE, (int) (0.1f * 255)); + mBottomSheetBackgroundColor = ColorUtils.compositeColors(layerAbove, layerBelow); + } else { + mBottomSheetBackgroundColor = getContext().getColor(R.color.materialColorSurfaceDim); + } + mBottomSheetBackgroundAlpha = Color.alpha(mBottomSheetBackgroundColor) / 255.0f; updateBackgroundVisibility(mActivityContext.getDeviceProfile()); mSearchUiManager.initializeSearch(this); } @@ -1152,7 +1163,7 @@ public class ActivityAllAppsContainerView<T extends Context & ActivityContext> if (!grid.isVerticalBarLayout() || FeatureFlags.enableResponsiveWorkspace()) { int topPadding = grid.allAppsPadding.top; - if (isSearchBarFloating() && !grid.isTablet) { + if (isSearchBarFloating() && !grid.shouldShowAllAppsOnSheet()) { topPadding += getResources().getDimensionPixelSize( R.dimen.all_apps_additional_top_padding_floating_search); } @@ -1401,7 +1412,7 @@ public class ActivityAllAppsContainerView<T extends Context & ActivityContext> // Draw full background panel for tablets. if (hasBottomSheet) { mHeaderPaint.setColor(mBottomSheetBackgroundColor); - mHeaderPaint.setAlpha(255); + mHeaderPaint.setAlpha((int) (mBottomSheetBackgroundAlpha * 255)); mTmpRectF.set( leftWithScale, @@ -1424,6 +1435,10 @@ public class ActivityAllAppsContainerView<T extends Context & ActivityContext> return; } + if (hasBottomSheet) { + mHeaderPaint.setAlpha((int) (mHeaderPaint.getAlpha() * mBottomSheetBackgroundAlpha)); + } + // Draw header on background panel final float headerBottomNoScale = getHeaderBottom() + getVisibleContainerView().getPaddingTop(); @@ -1455,7 +1470,11 @@ public class ActivityAllAppsContainerView<T extends Context & ActivityContext> mHeaderPaint.setColor(Color.BLUE); mHeaderPaint.setAlpha(255); } else { - mHeaderPaint.setAlpha((int) (getAlpha() * mTabsProtectionAlpha)); + float tabAlpha = getAlpha() * mTabsProtectionAlpha; + if (hasBottomSheet) { + tabAlpha *= mBottomSheetBackgroundAlpha; + } + mHeaderPaint.setAlpha((int) tabAlpha); } float left = 0f; float right = canvas.getWidth(); @@ -1507,7 +1526,7 @@ public class ActivityAllAppsContainerView<T extends Context & ActivityContext> public int getHeaderBottom() { int bottom = (int) getTranslationY() + mHeader.getClipTop(); if (isSearchBarFloating()) { - if (mActivityContext.getDeviceProfile().isTablet) { + if (mActivityContext.getDeviceProfile().shouldShowAllAppsOnSheet()) { return bottom + mBottomSheetBackground.getTop(); } return bottom; diff --git a/src/com/android/launcher3/allapps/AllAppsTransitionController.java b/src/com/android/launcher3/allapps/AllAppsTransitionController.java index 4cc31d296e..350f763939 100644 --- a/src/com/android/launcher3/allapps/AllAppsTransitionController.java +++ b/src/com/android/launcher3/allapps/AllAppsTransitionController.java @@ -290,7 +290,8 @@ public class AllAppsTransitionController private void onScaleProgressChanged() { final float scaleProgress = mAllAppScale.value; SCALE_PROPERTY.set(mLauncher.getAppsView(), scaleProgress); - if (!mLauncher.getAppsView().isSearching() || !mLauncher.getDeviceProfile().isTablet) { + if (!mLauncher.getAppsView().isSearching() + || !mLauncher.getDeviceProfile().shouldShowAllAppsOnSheet()) { mLauncher.getScrimView().setScrimHeaderScale(scaleProgress); } diff --git a/src/com/android/launcher3/dragndrop/DragController.java b/src/com/android/launcher3/dragndrop/DragController.java index 4127dd15ad..284faba9da 100644 --- a/src/com/android/launcher3/dragndrop/DragController.java +++ b/src/com/android/launcher3/dragndrop/DragController.java @@ -16,6 +16,7 @@ package com.android.launcher3.dragndrop; +import static com.android.launcher3.Flags.removeAppsRefreshOnRightClick; import static com.android.launcher3.model.data.ItemInfoWithIcon.FLAG_NOT_PINNABLE; import android.graphics.Point; @@ -521,17 +522,21 @@ public abstract class DragController<T extends ActivityContext> mDragObject.dragComplete = true; if (mIsInPreDrag) { - if (dropTarget != null) { - dropTarget.onDragExit(mDragObject); + if (removeAppsRefreshOnRightClick()) { + mDragObject.cancelled = true; + } else { + if (dropTarget != null) { + dropTarget.onDragExit(mDragObject); + } + return; } - return; } // Drop onto the target. boolean accepted = false; if (dropTarget != null) { dropTarget.onDragExit(mDragObject); - if (dropTarget.acceptDrop(mDragObject)) { + if (!mIsInPreDrag && dropTarget.acceptDrop(mDragObject)) { if (flingAnimation != null) { flingAnimation.run(); } else { diff --git a/src/com/android/launcher3/dragndrop/LauncherDragController.java b/src/com/android/launcher3/dragndrop/LauncherDragController.java index 4aa3673586..dd433c02d3 100644 --- a/src/com/android/launcher3/dragndrop/LauncherDragController.java +++ b/src/com/android/launcher3/dragndrop/LauncherDragController.java @@ -15,7 +15,10 @@ */ package com.android.launcher3.dragndrop; +import static android.view.View.VISIBLE; + import static com.android.launcher3.AbstractFloatingView.TYPE_DISCOVERY_BOUNCE; +import static com.android.launcher3.Flags.removeAppsRefreshOnRightClick; import static com.android.launcher3.LauncherAnimUtils.SPRING_LOADED_EXIT_DELAY; import static com.android.launcher3.LauncherState.EDIT_MODE; import static com.android.launcher3.LauncherState.NORMAL; @@ -25,6 +28,7 @@ import android.content.res.Resources; import android.graphics.Rect; import android.graphics.drawable.Drawable; import android.view.HapticFeedbackConstants; +import android.view.MotionEvent; import android.view.View; import androidx.annotation.Nullable; @@ -33,10 +37,13 @@ import androidx.annotation.VisibleForTesting; import com.android.launcher3.AbstractFloatingView; import com.android.launcher3.DragSource; import com.android.launcher3.DropTarget; +import com.android.launcher3.DropTarget.DragObject; import com.android.launcher3.Launcher; import com.android.launcher3.R; import com.android.launcher3.accessibility.DragViewStateAnnouncer; +import com.android.launcher3.dragndrop.DragOptions.PreDragCondition; import com.android.launcher3.model.data.ItemInfo; +import com.android.launcher3.util.TouchUtil; import com.android.launcher3.widget.util.WidgetDragScaleUtils; /** @@ -47,6 +54,9 @@ public class LauncherDragController extends DragController<Launcher> { private static final boolean PROFILE_DRAWING_DURING_DRAG = false; private final FlingToDeleteHelper mFlingToDeleteHelper; + /** Whether or not the drag operation is triggered by mouse right click. */ + private boolean mIsInMouseRightClick = false; + public LauncherDragController(Launcher launcher) { super(launcher); mFlingToDeleteHelper = new FlingToDeleteHelper(launcher); @@ -69,6 +79,28 @@ public class LauncherDragController extends DragController<Launcher> { android.os.Debug.startMethodTracing("Launcher"); } + if (removeAppsRefreshOnRightClick() && mIsInMouseRightClick + && options.preDragCondition == null + && originalView instanceof View v) { + options.preDragCondition = new PreDragCondition() { + + @Override + public boolean shouldStartDrag(double distanceDragged) { + return false; + } + + @Override + public void onPreDragStart(DragObject dragObject) { + // Set it to visible so the text of FolderIcon would not flash (avoid it from + // being invisible and then visible) + v.setVisibility(VISIBLE); + } + + @Override + public void onPreDragEnd(DragObject dragObject, boolean dragStarted) { } + }; + } + mActivity.hideKeyboard(); AbstractFloatingView.closeOpenViews(mActivity, false, TYPE_DISCOVERY_BOUNCE); @@ -191,7 +223,7 @@ public class LauncherDragController extends DragController<Launcher> { @Override protected void exitDrag() { - if (!mActivity.isInState(EDIT_MODE)) { + if (!mIsInPreDrag && !mActivity.isInState(EDIT_MODE)) { mActivity.getStateManager().goToState(NORMAL, SPRING_LOADED_EXIT_DELAY); } } @@ -218,4 +250,13 @@ public class LauncherDragController extends DragController<Launcher> { dropCoordinates); return mActivity.getWorkspace(); } + + /** + * Intercepts touch events from a drag source view. + */ + @Override + public boolean onControllerInterceptTouchEvent(MotionEvent ev) { + mIsInMouseRightClick = TouchUtil.isMouseRightClickDownOrMove(ev); + return super.onControllerInterceptTouchEvent(ev); + } } diff --git a/src/com/android/launcher3/folder/Folder.java b/src/com/android/launcher3/folder/Folder.java index 28032569b2..0ae95196fd 100644 --- a/src/com/android/launcher3/folder/Folder.java +++ b/src/com/android/launcher3/folder/Folder.java @@ -102,7 +102,6 @@ import com.android.launcher3.model.data.WorkspaceItemInfo; import com.android.launcher3.pageindicators.PageIndicatorDots; import com.android.launcher3.util.Executors; import com.android.launcher3.util.LauncherBindableItemsContainer; -import com.android.launcher3.util.LauncherBindableItemsContainer.ItemOperator; import com.android.launcher3.util.Thunk; import com.android.launcher3.views.ActivityContext; import com.android.launcher3.views.BaseDragLayer; @@ -1116,13 +1115,15 @@ public class Folder extends AbstractFloatingView implements ClipPathView, DragSo View icon = (mCurrentDragView != null && mCurrentDragView.getTag() == info) ? mCurrentDragView : mContent.createNewView(info); ArrayList<View> views = getIconsInReadingOrder(); - info.rank = Utilities.boundToRange(info.rank, 0, views.size()); - views.add(info.rank, icon); - mContent.arrangeChildren(views); - mItemsInvalidated = true; - - try (SuppressInfoChanges s = new SuppressInfoChanges()) { - mFolderIcon.onDrop(d, true /* itemReturnedOnFailedDrop */); + if (!views.contains(icon)) { + info.rank = Utilities.boundToRange(info.rank, 0, views.size()); + views.add(info.rank, icon); + mContent.arrangeChildren(views); + mItemsInvalidated = true; + + try (SuppressInfoChanges s = new SuppressInfoChanges()) { + mFolderIcon.onDrop(d, true /* itemReturnedOnFailedDrop */); + } } } diff --git a/src/com/android/launcher3/folder/FolderPagedView.java b/src/com/android/launcher3/folder/FolderPagedView.java index bebe1a49ef..0963421f8a 100644 --- a/src/com/android/launcher3/folder/FolderPagedView.java +++ b/src/com/android/launcher3/folder/FolderPagedView.java @@ -47,6 +47,7 @@ import com.android.launcher3.keyboard.ViewGroupFocusHelper; import com.android.launcher3.model.data.AppPairInfo; import com.android.launcher3.model.data.ItemInfo; import com.android.launcher3.model.data.WorkspaceItemInfo; +import com.android.launcher3.pageindicators.Direction; import com.android.launcher3.pageindicators.PageIndicatorDots; import com.android.launcher3.util.LauncherBindableItemsContainer.ItemOperator; import com.android.launcher3.util.Thunk; @@ -129,6 +130,8 @@ public class FolderPagedView extends PagedView<PageIndicatorDots> implements Cli public void setFolder(Folder folder) { mFolder = folder; mPageIndicator = folder.findViewById(R.id.folder_page_indicator); + mPageIndicator.setArrowClickListener(direction -> snapToPageImmediately( + (Direction.END == direction) ? mCurrentPage + 1 : mCurrentPage - 1)); initParentViews(folder); } diff --git a/src/com/android/launcher3/graphics/GridCustomizationsProxy.java b/src/com/android/launcher3/graphics/GridCustomizationsProxy.java index 70b9f46c1f..48519ce8bc 100644 --- a/src/com/android/launcher3/graphics/GridCustomizationsProxy.java +++ b/src/com/android/launcher3/graphics/GridCustomizationsProxy.java @@ -113,6 +113,7 @@ public class GridCustomizationsProxy implements ProxyProvider { private static final String KEY_SHAPE_OPTIONS = "/shape_options"; // default_grid is for setting grid and shape to system settings private static final String KEY_DEFAULT_GRID = "/default_grid"; + private static final String SET_SHAPE = "/shape"; private static final String METHOD_GET_PREVIEW = "get_preview"; @@ -130,6 +131,7 @@ public class GridCustomizationsProxy implements ProxyProvider { private static final int MESSAGE_ID_UPDATE_SHAPE = 2586; private static final int MESSAGE_ID_UPDATE_GRID = 7414; private static final int MESSAGE_ID_UPDATE_COLOR = 856; + private static final int MESSAGE_ID_UPDATE_ICON_THEMED = 311; // Set of all active previews used to track duplicate memory allocations private final Set<PreviewLifecycleObserver> mActivePreviews = @@ -264,6 +266,12 @@ public class GridCustomizationsProxy implements ProxyProvider { mContext.getContentResolver().notifyChange(uri, null); return 1; } + case SET_SHAPE: + if (Flags.newCustomizationPickerUi()) { + mPrefs.put(PREF_ICON_SHAPE, + requireNonNullElse(values.getAsString(KEY_SHAPE_KEY), "")); + } + return 1; case ICON_THEMED: case SET_ICON_THEMED: { mThemeManager.setMonoThemeEnabled(values.getAsBoolean(BOOLEAN_VALUE)); @@ -384,6 +392,12 @@ public class GridCustomizationsProxy implements ProxyProvider { renderer.previewColor(message.getData()); } break; + case MESSAGE_ID_UPDATE_ICON_THEMED: + if (Flags.newCustomizationPickerUi()) { + Boolean iconThemed = message.getData().getBoolean(BOOLEAN_VALUE); + // TODO Update icon themed in the preview + } + break; default: // Unknown command, destroy lifecycle Log.d(TAG, "Unknown preview command: " + message.what + ", destroying preview"); diff --git a/src/com/android/launcher3/graphics/ShapeDelegate.kt b/src/com/android/launcher3/graphics/ShapeDelegate.kt index 7c042929e2..01bfe3018c 100644 --- a/src/com/android/launcher3/graphics/ShapeDelegate.kt +++ b/src/com/android/launcher3/graphics/ShapeDelegate.kt @@ -18,6 +18,7 @@ package com.android.launcher3.graphics import android.animation.Animator import android.animation.AnimatorListenerAdapter import android.animation.ValueAnimator +import android.animation.ValueAnimator.AnimatorUpdateListener import android.graphics.Canvas import android.graphics.Color import android.graphics.Matrix @@ -41,7 +42,6 @@ import androidx.graphics.shapes.SvgPathParser import androidx.graphics.shapes.rectangle import androidx.graphics.shapes.toPath import androidx.graphics.shapes.transformed -import com.android.launcher3.anim.RoundedRectRevealOutlineProvider import com.android.launcher3.icons.GraphicsUtils import com.android.launcher3.views.ClipPathView @@ -127,16 +127,20 @@ interface ShapeDelegate { endRadius: Float, isReversed: Boolean, ): ValueAnimator where T : View, T : ClipPathView { - return object : - RoundedRectRevealOutlineProvider( - (startRect.width() / 2f) * radiusRatio, - endRadius, - startRect, - endRect, - ) { - override fun shouldRemoveElevationDuringAnimation() = true - } - .createRevealAnimator(target, isReversed) + val startRadius = (startRect.width() / 2f) * radiusRatio + return ClipAnimBuilder(target) { progress, path -> + val radius = (1 - progress) * startRadius + progress * endRadius + path.addRoundRect( + (1 - progress) * startRect.left + progress * endRect.left, + (1 - progress) * startRect.top + progress * endRect.top, + (1 - progress) * startRect.right + progress * endRect.right, + (1 - progress) * startRect.bottom + progress * endRect.bottom, + radius, + radius, + Path.Direction.CW, + ) + } + .toAnim(isReversed) } override fun equals(other: Any?) = @@ -220,38 +224,44 @@ interface ShapeDelegate { ), ) - val va = - if (isReversed) ValueAnimator.ofFloat(1f, 0f) else ValueAnimator.ofFloat(0f, 1f) - va.addListener( - object : AnimatorListenerAdapter() { - private var oldOutlineProvider: ViewOutlineProvider? = null - - override fun onAnimationStart(animation: Animator) { - target.apply { - oldOutlineProvider = outlineProvider - outlineProvider = null - translationZ = -target.elevation - } - } + return ClipAnimBuilder(target, morph::toPath).toAnim(isReversed) + } + } - override fun onAnimationEnd(animation: Animator) { - target.apply { - translationZ = 0f - setClipPath(null) - outlineProvider = oldOutlineProvider - } - } - } - ) + private class ClipAnimBuilder<T>(val target: T, val pathProvider: (Float, Path) -> Unit) : + AnimatorListenerAdapter(), AnimatorUpdateListener where T : View, T : ClipPathView { - val path = Path() - va.addUpdateListener { anim: ValueAnimator -> - path.reset() - morph.toPath(anim.animatedValue as Float, path) - target.setClipPath(path) + private var oldOutlineProvider: ViewOutlineProvider? = null + val path = Path() + + override fun onAnimationStart(animation: Animator) { + target.apply { + oldOutlineProvider = outlineProvider + outlineProvider = null + translationZ = -target.elevation + } + } + + override fun onAnimationEnd(animation: Animator) { + target.apply { + translationZ = 0f + setClipPath(null) + outlineProvider = oldOutlineProvider } - return va } + + override fun onAnimationUpdate(anim: ValueAnimator) { + path.reset() + pathProvider.invoke(anim.animatedValue as Float, path) + target.setClipPath(path) + } + + fun toAnim(isReversed: Boolean) = + (if (isReversed) ValueAnimator.ofFloat(1f, 0f) else ValueAnimator.ofFloat(0f, 1f)) + .also { + it.addListener(this) + it.addUpdateListener(this) + } } companion object { diff --git a/src/com/android/launcher3/pageindicators/PageIndicator.java b/src/com/android/launcher3/pageindicators/PageIndicator.java index 0640bf3672..a6f76c4d44 100644 --- a/src/com/android/launcher3/pageindicators/PageIndicator.java +++ b/src/com/android/launcher3/pageindicators/PageIndicator.java @@ -15,6 +15,8 @@ */ package com.android.launcher3.pageindicators; +import java.util.function.Consumer; + /** * Base class for a page indicator. */ @@ -27,6 +29,14 @@ public interface PageIndicator { void setMarkersCount(int numMarkers); /** + * This is only going to be used by the FolderPagedView's PageIndicator. A refactor is planned + * to separate the two purposes of this class, but in the meantime, this indicator will serve to + * let the folder snap to the page of its click, and also tell the PageIndicator not to draw + * arrows if the click listener is null (at least until after this is refactored). + */ + void setArrowClickListener(Consumer<Direction> listener); + + /** * Sets a flag indicating whether to pause scroll. * <p>Should be set to {@code true} while the screen is binding or new data is being applied, * and to {@code false} once done. This prevents animation conflicts due to scrolling during diff --git a/src/com/android/launcher3/pageindicators/PageIndicatorArrowClickListener.kt b/src/com/android/launcher3/pageindicators/PageIndicatorArrowClickListener.kt new file mode 100644 index 0000000000..970d210ad9 --- /dev/null +++ b/src/com/android/launcher3/pageindicators/PageIndicatorArrowClickListener.kt @@ -0,0 +1,26 @@ +/* + * Copyright (C) 2025 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.pageindicators + +interface PageIndicatorArrowClickListener { + fun onArrowClick(direction: Direction) +} + +enum class Direction { + END, + START, +} diff --git a/src/com/android/launcher3/pageindicators/PageIndicatorDots.java b/src/com/android/launcher3/pageindicators/PageIndicatorDots.java index a9a26a1631..384f87623a 100644 --- a/src/com/android/launcher3/pageindicators/PageIndicatorDots.java +++ b/src/com/android/launcher3/pageindicators/PageIndicatorDots.java @@ -32,11 +32,13 @@ import android.graphics.Paint; import android.graphics.Paint.Style; import android.graphics.Rect; import android.graphics.RectF; +import android.graphics.drawable.VectorDrawable; import android.os.Handler; import android.os.Looper; import android.util.AttributeSet; import android.util.FloatProperty; import android.util.IntProperty; +import android.view.MotionEvent; import android.view.View; import android.view.ViewConfiguration; import android.view.ViewOutlineProvider; @@ -51,6 +53,8 @@ import com.android.launcher3.R; import com.android.launcher3.Utilities; import com.android.launcher3.util.Themes; +import java.util.function.Consumer; + /** * {@link PageIndicator} which shows dots per page. The active page is shown with the current * accent color. @@ -71,6 +75,12 @@ public class PageIndicatorDots extends View implements Insettable, PageIndicator private static final int ENTER_ANIMATION_STAGGERED_DELAY = 150; private static final int ENTER_ANIMATION_DURATION = 400; + private static final int LARGE_HEIGHT_MULTIPLIER = 12; + private static final int SMALL_HEIGHT_MULTIPLIER = 4; + private static final int LARGE_WIDTH_MULTIPLIER = 5; + private static final int SMALL_WIDTH_MULTIPLIER = 3; + private static final float ARROW_TOUCH_BOX_FACTOR = 5f; + private static final int PAGE_INDICATOR_ALPHA = 255; private static final int DOT_ALPHA = 128; private static final float DOT_ALPHA_FRACTION = 0.5f; @@ -78,12 +88,14 @@ public class PageIndicatorDots extends View implements Insettable, PageIndicator private static final int VISIBLE_ALPHA = 255; private static final int INVISIBLE_ALPHA = 0; private Paint mPaginationPaint; + private Consumer<Direction> mOnArrowClickListener; // This value approximately overshoots to 1.5 times the original size. private static final float ENTER_ANIMATION_OVERSHOOT_TENSION = 4.9f; // This is used to optimize the onDraw method by not constructing a new RectF each draw. private static final RectF sTempRect = new RectF(); + private static final RectF sLastActiveRect = new RectF(); private static final FloatProperty<PageIndicatorDots> CURRENT_POSITION = new FloatProperty<PageIndicatorDots>("current_position") { @@ -102,23 +114,27 @@ public class PageIndicatorDots extends View implements Insettable, PageIndicator private static final IntProperty<PageIndicatorDots> PAGINATION_ALPHA = new IntProperty<PageIndicatorDots>("pagination_alpha") { - @Override - public Integer get(PageIndicatorDots obj) { - return obj.mPaginationPaint.getAlpha(); - } + @Override + public Integer get(PageIndicatorDots obj) { + return obj.mPaginationPaint.getAlpha(); + } - @Override - public void setValue(PageIndicatorDots obj, int alpha) { - obj.mPaginationPaint.setAlpha(alpha); - obj.invalidate(); - } - }; + @Override + public void setValue(PageIndicatorDots obj, int alpha) { + obj.mPaginationPaint.setAlpha(alpha); + obj.invalidate(); + } + }; private final Handler mDelayedPaginationFadeHandler = new Handler(Looper.getMainLooper()); private final float mDotRadius; private final float mGapWidth; private final float mCircleGap; private final boolean mIsRtl; + private final VectorDrawable mArrowRight; + private final VectorDrawable mArrowLeft; + private final Rect mArrowRightBounds = new Rect(); + private final Rect mArrowLeftBounds = new Rect(); private int mNumPages; private int mActivePage; @@ -170,6 +186,8 @@ public class PageIndicatorDots extends View implements Insettable, PageIndicator : DOT_GAP_FACTOR * mDotRadius; setOutlineProvider(new MyOutlineProver()); mIsRtl = Utilities.isRtl(getResources()); + mArrowRight = (VectorDrawable) getResources().getDrawable(R.drawable.ic_chevron_end); + mArrowLeft = (VectorDrawable) getResources().getDrawable(R.drawable.ic_chevron_start); } @Override @@ -408,6 +426,11 @@ public class PageIndicatorDots extends View implements Insettable, PageIndicator } @Override + public void setArrowClickListener(Consumer<Direction> listener) { + mOnArrowClickListener = listener; + } + + @Override public void setPauseScroll(boolean pause, boolean isTwoPanels) { mIsTwoPanels = isTwoPanels; @@ -422,11 +445,16 @@ public class PageIndicatorDots extends View implements Insettable, PageIndicator @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { // TODO(b/394355070): Verify Folder Entry Animation works correctly with visual updates - // Add extra spacing of mDotRadius on all sides so than entry animation could be run. + // Add extra spacing of mDotRadius on all sides so than entry animation could be run + // and so the hitboxes of arrows can be clicked easier. int width = MeasureSpec.getMode(widthMeasureSpec) == MeasureSpec.EXACTLY ? - MeasureSpec.getSize(widthMeasureSpec) : (int) ((mNumPages * 3 + 2) * mDotRadius); + MeasureSpec.getSize(widthMeasureSpec) + : (int) ((mNumPages * ((enableLauncherVisualRefresh()) + ? LARGE_WIDTH_MULTIPLIER : SMALL_WIDTH_MULTIPLIER) + 2) * mDotRadius); int height = MeasureSpec.getMode(heightMeasureSpec) == MeasureSpec.EXACTLY - ? MeasureSpec.getSize(heightMeasureSpec) : (int) (4 * mDotRadius); + ? MeasureSpec.getSize(heightMeasureSpec) + : (int) (((enableLauncherVisualRefresh()) + ? LARGE_HEIGHT_MULTIPLIER : SMALL_HEIGHT_MULTIPLIER) * mDotRadius); setMeasuredDimension(width, height); } @@ -446,18 +474,51 @@ public class PageIndicatorDots extends View implements Insettable, PageIndicator float y = getHeight() / 2; if (mEntryAnimationRadiusFactors != null) { - // During entry animation, only draw the circles - // TODO(b/394355070): Verify Folder Entry Animation works correctly - visual updates + if (enableLauncherVisualRefresh()) { + x -= mDotRadius; + if (mIsRtl) { + x = getWidth() - x; + circleGap = -circleGap; + } + sTempRect.top = y - mDotRadius; + sTempRect.bottom = y + mDotRadius; - if (mIsRtl) { - x = getWidth() - x; - circleGap = -circleGap; - } - for (int i = 0; i < mEntryAnimationRadiusFactors.length; i++) { - mPaginationPaint.setAlpha(i == mActivePage ? PAGE_INDICATOR_ALPHA : DOT_ALPHA); - canvas.drawCircle(x, y, mDotRadius * mEntryAnimationRadiusFactors[i], - mPaginationPaint); - x += circleGap; + for (int i = 0; i < mEntryAnimationRadiusFactors.length; i++) { + if (i == mActivePage) { + if (mIsRtl) { + sTempRect.left = x - (mDotRadius * 3); + sTempRect.right = x + mDotRadius; + x += circleGap - (mDotRadius * 2); + } else { + sTempRect.left = x - mDotRadius; + sTempRect.right = x + (mDotRadius * 3); + x += circleGap + (mDotRadius * 2); + } + scale(sTempRect, mEntryAnimationRadiusFactors[i]); + float scaledRadius = mDotRadius * mEntryAnimationRadiusFactors[i]; + mPaginationPaint.setAlpha(PAGE_INDICATOR_ALPHA); + canvas.drawRoundRect(sTempRect, scaledRadius, scaledRadius, + mPaginationPaint); + } else { + mPaginationPaint.setAlpha(DOT_ALPHA); + canvas.drawCircle(x, y, mDotRadius * mEntryAnimationRadiusFactors[i], + mPaginationPaint); + x += circleGap; + } + } + } else { + // During entry animation, only draw the circles + + if (mIsRtl) { + x = getWidth() - x; + circleGap = -circleGap; + } + for (int i = 0; i < mEntryAnimationRadiusFactors.length; i++) { + mPaginationPaint.setAlpha(i == mActivePage ? PAGE_INDICATOR_ALPHA : DOT_ALPHA); + canvas.drawCircle(x, y, mDotRadius * mEntryAnimationRadiusFactors[i], + mPaginationPaint); + x += circleGap; + } } } else { // Save the current alpha value, so we can reset to it again after drawing the dots @@ -471,12 +532,31 @@ public class PageIndicatorDots extends View implements Insettable, PageIndicator sTempRect.bottom = y + mDotRadius; sTempRect.left = x - diameter; - float posDif = Math.abs(mLastPosition - mCurrentPosition); + float currentPosition = mCurrentPosition; + float lastPosition = mLastPosition; + + if (mIsRtl) { + currentPosition = mNumPages - currentPosition - 1; + lastPosition = mNumPages - lastPosition - 1; + } + float posDif = Math.abs(lastPosition - currentPosition); float boundedPosition = (posDif > 1) - ? Math.round(mCurrentPosition) - : mCurrentPosition; + ? Math.round(currentPosition) + : currentPosition; float bounceProgress = (posDif > 1) ? posDif - 1 : 0; - float bounceAdjustment = Math.abs(mCurrentPosition - boundedPosition) * diameter; + float bounceAdjustment = Math.abs(currentPosition - boundedPosition) * diameter; + + if (mOnArrowClickListener != null && boundedPosition >= 1) { + // Here we draw the Left Arrow + mArrowLeft.setAlpha(alpha); + int size = (int) (mGapWidth * 4); + mArrowLeftBounds.left = (int) (sTempRect.left - mGapWidth - size); + mArrowLeftBounds.top = (int) (y - size / 2); + mArrowLeftBounds.right = (int) (sTempRect.left - mGapWidth); + mArrowLeftBounds.bottom = (int) (y + size / 2); + mArrowLeft.setBounds(mArrowLeftBounds); + mArrowLeft.draw(canvas); + } // Here we draw the dots, one at a time from the left-most dot to the right-most dot // 1.0 => 000000 000000111111 000000 @@ -498,10 +578,10 @@ public class PageIndicatorDots extends View implements Insettable, PageIndicator // While the animation is shifting the active pagination dots size from // the previously active one, to the newly active dot, there is no bounce // adjustment. The bounce happens in the "Overshoot" phase of the animation. - // mLastPosition is used to determine when the currentPosition is just + // lastPosition is used to determine when the currentPosition is just // leaving the page, or if it is in the overshoot phase. if (boundedPosition == i && bounceProgress != 0) { - if (mLastPosition < mCurrentPosition) { + if (lastPosition < currentPosition) { sTempRect.left -= bounceAdjustment; } else { sTempRect.right += bounceAdjustment; @@ -510,19 +590,34 @@ public class PageIndicatorDots extends View implements Insettable, PageIndicator } else { sTempRect.right = sTempRect.left + diameter; - if (mLastPosition == i && bounceProgress != 0) { - if (mLastPosition > mCurrentPosition) { + if (lastPosition == i && bounceProgress != 0) { + if (lastPosition > currentPosition) { sTempRect.left += bounceAdjustment; } else { sTempRect.right -= bounceAdjustment; } } } + if (Math.round(mCurrentPosition) == i) { + sLastActiveRect.set(sTempRect); + } canvas.drawRoundRect(sTempRect, mDotRadius, mDotRadius, mPaginationPaint); // TODO(b/394355070) Verify RTL experience works correctly with visual updates sTempRect.left = sTempRect.right + mGapWidth; } + + if (mOnArrowClickListener != null && boundedPosition <= mNumPages - 2) { + // Here we draw the Right Arrow + mArrowRight.setAlpha(alpha); + int size = (int) (mGapWidth * 4); + mArrowRightBounds.left = (int) sTempRect.left; + mArrowRightBounds.top = (int) (y - size / 2); + mArrowRightBounds.right = (int) (int) (sTempRect.left + size); + mArrowRightBounds.bottom = (int) (y + size / 2); + mArrowRight.setBounds(mArrowRightBounds); + mArrowRight.draw(canvas); + } } else { // Here we draw the dots mPaginationPaint.setAlpha((int) (alpha * DOT_ALPHA_FRACTION)); @@ -541,6 +636,38 @@ public class PageIndicatorDots extends View implements Insettable, PageIndicator } } + @Override + public boolean onTouchEvent(MotionEvent ev) { + if (mOnArrowClickListener == null) { + // No - Op. Don't care about touch events + } else if ((mIsRtl && withinExpandedBounds(mArrowRightBounds, ev)) + || (!mIsRtl && withinExpandedBounds(mArrowLeftBounds, ev))) { + mOnArrowClickListener.accept(Direction.START); + } else if ((mIsRtl && withinExpandedBounds(mArrowLeftBounds, ev)) + || (!mIsRtl && withinExpandedBounds(mArrowRightBounds, ev))) { + mOnArrowClickListener.accept(Direction.END); + } + return super.onTouchEvent(ev); + } + + // For larger Touch box + private boolean withinExpandedBounds(Rect rect, MotionEvent ev) { + RectF scaledRect = new RectF(rect); + scale(scaledRect, ARROW_TOUCH_BOX_FACTOR); + return scaledRect.contains(ev.getX(), ev.getY()); + } + + private static void scale(RectF rect, float factor) { + float horizontalAdjustment = rect.width() * (factor - 1) / 2; + float verticalAdjustment = rect.height() * (factor - 1) / 2; + + rect.top -= verticalAdjustment; + rect.bottom += verticalAdjustment; + + rect.left -= horizontalAdjustment; + rect.right += horizontalAdjustment; + } + private RectF getActiveRect() { float startCircle = (int) mCurrentPosition; float delta = mCurrentPosition - startCircle; @@ -593,8 +720,8 @@ public class PageIndicatorDots extends View implements Insettable, PageIndicator @Override public void getOutline(View view, Outline outline) { if (mEntryAnimationRadiusFactors == null) { - // TODO(b/394355070): Verify Outline works correctly with visual updates - RectF activeRect = getActiveRect(); + RectF activeRect = enableLauncherVisualRefresh() + ? sLastActiveRect : getActiveRect(); outline.setRoundRect( (int) activeRect.left, (int) activeRect.top, diff --git a/src/com/android/launcher3/touch/AllAppsSwipeController.java b/src/com/android/launcher3/touch/AllAppsSwipeController.java index 2cc4909712..f86fd6ac33 100644 --- a/src/com/android/launcher3/touch/AllAppsSwipeController.java +++ b/src/com/android/launcher3/touch/AllAppsSwipeController.java @@ -22,6 +22,7 @@ import static com.android.app.animation.Interpolators.EMPHASIZED_DECELERATE; import static com.android.app.animation.Interpolators.FINAL_FRAME; import static com.android.app.animation.Interpolators.INSTANT; import static com.android.app.animation.Interpolators.LINEAR; +import static com.android.app.animation.Interpolators.clampToProgress; import static com.android.launcher3.LauncherState.ALL_APPS; import static com.android.launcher3.LauncherState.NORMAL; import static com.android.launcher3.states.StateAnimationConfig.ANIM_ALL_APPS_FADE; @@ -39,6 +40,7 @@ import android.view.animation.Interpolator; import com.android.app.animation.Interpolators; import com.android.launcher3.AbstractFloatingView; +import com.android.launcher3.Flags; import com.android.launcher3.Launcher; import com.android.launcher3.LauncherState; import com.android.launcher3.states.StateAnimationConfig; @@ -53,10 +55,10 @@ public class AllAppsSwipeController extends AbstractStateChangeTouchController { private static final float ALL_APPS_SCRIM_VISIBLE_THRESHOLD = 0.1f; private static final float ALL_APPS_STAGGERED_FADE_THRESHOLD = 0.5f; - public static final Interpolator ALL_APPS_SCRIM_RESPONDER = + private static final Interpolator ALL_APPS_SCRIM_RESPONDER = Interpolators.clampToProgress( LINEAR, ALL_APPS_SCRIM_VISIBLE_THRESHOLD, ALL_APPS_STAGGERED_FADE_THRESHOLD); - public static final Interpolator ALL_APPS_CLAMPING_RESPONDER = + private static final Interpolator ALL_APPS_CLAMPING_RESPONDER = Interpolators.clampToProgress( LINEAR, 1 - ALL_APPS_CONTENT_FADE_MAX_CLAMPING_THRESHOLD, @@ -207,7 +209,16 @@ public class AllAppsSwipeController extends AbstractStateChangeTouchController { } config.setInterpolator(ANIM_WORKSPACE_SCALE, DECELERATED_EASE); config.setInterpolator(ANIM_DEPTH, DECELERATED_EASE); - if (launcher.getDeviceProfile().isPhone) { + if (Flags.allAppsBlur()) { + if (!config.isUserControlled()) { + config.setInterpolator(ANIM_DEPTH, EMPHASIZED_DECELERATE); + } + config.setInterpolator(ANIM_WORKSPACE_FADE, + clampToProgress(LINEAR, 1 - ALL_APPS_SCRIM_VISIBLE_THRESHOLD, 1)); + config.setInterpolator(ANIM_HOTSEAT_FADE, + clampToProgress(LINEAR, 1 - ALL_APPS_SCRIM_VISIBLE_THRESHOLD, 1)); + } else if (launcher.getDeviceProfile().isPhone) { + // On phones without blur, reveal the workspace and hotseat when leaving All Apps. config.setInterpolator(ANIM_WORKSPACE_FADE, INSTANT); config.setInterpolator(ANIM_HOTSEAT_FADE, INSTANT); config.animFlags |= StateAnimationConfig.SKIP_DEPTH_CONTROLLER; @@ -253,7 +264,14 @@ public class AllAppsSwipeController extends AbstractStateChangeTouchController { } config.setInterpolator(ANIM_WORKSPACE_SCALE, DECELERATED_EASE); config.setInterpolator(ANIM_DEPTH, DECELERATED_EASE); - if (launcher.getDeviceProfile().isPhone) { + if (Flags.allAppsBlur()) { + config.setInterpolator(ANIM_DEPTH, LINEAR); + config.setInterpolator(ANIM_WORKSPACE_FADE, + clampToProgress(LINEAR, 0, ALL_APPS_SCRIM_VISIBLE_THRESHOLD)); + config.setInterpolator(ANIM_HOTSEAT_FADE, + clampToProgress(LINEAR, 0, ALL_APPS_SCRIM_VISIBLE_THRESHOLD)); + } else if (launcher.getDeviceProfile().isPhone) { + // On phones without blur, hide the workspace and hotseat when entering All Apps. config.setInterpolator(ANIM_WORKSPACE_FADE, FINAL_FRAME); config.setInterpolator(ANIM_HOTSEAT_FADE, FINAL_FRAME); config.animFlags |= StateAnimationConfig.SKIP_DEPTH_CONTROLLER; diff --git a/src/com/android/launcher3/touch/WorkspaceTouchListener.java b/src/com/android/launcher3/touch/WorkspaceTouchListener.java index d72e6f9e39..576f17616b 100644 --- a/src/com/android/launcher3/touch/WorkspaceTouchListener.java +++ b/src/com/android/launcher3/touch/WorkspaceTouchListener.java @@ -131,7 +131,7 @@ public class WorkspaceTouchListener extends GestureDetector.SimpleOnGestureListe } boolean isInAllAppsBottomSheet = mLauncher.isInState(ALL_APPS) - && mLauncher.getDeviceProfile().isTablet; + && mLauncher.getDeviceProfile().shouldShowAllAppsOnSheet(); final boolean result; if (mLongPressState == STATE_COMPLETED) { diff --git a/src/com/android/launcher3/util/SimpleBroadcastReceiver.java b/src/com/android/launcher3/util/SimpleBroadcastReceiver.java index 7a40abe62b..53e0bce182 100644 --- a/src/com/android/launcher3/util/SimpleBroadcastReceiver.java +++ b/src/com/android/launcher3/util/SimpleBroadcastReceiver.java @@ -123,6 +123,32 @@ public class SimpleBroadcastReceiver extends BroadcastReceiver { } } + /** + * Same as {@link #register(Runnable, int, String...)} above but with additional permission + * params utilizine the original {@link Context}. + */ + @AnyThread + public void register(@Nullable Runnable completionCallback, + String broadcastPermission, int flags, String... actions) { + if (Looper.myLooper() == mHandler.getLooper()) { + registerInternal(mContext, completionCallback, broadcastPermission, flags, actions); + } else { + mHandler.post(() -> registerInternal(mContext, completionCallback, broadcastPermission, + flags, actions)); + } + } + + /** Register broadcast receiver with permission and run completion callback if passed. */ + @AnyThread + private void registerInternal( + @NonNull Context context, @Nullable Runnable completionCallback, + String broadcastPermission, int flags, String... actions) { + context.registerReceiver(this, getFilter(actions), broadcastPermission, null, flags); + if (completionCallback != null) { + completionCallback.run(); + } + } + /** Same as {@link #register(Runnable, String...)} above but with pkg name. */ @AnyThread public void registerPkgActions(@Nullable String pkg, String... actions) { diff --git a/src/com/android/launcher3/views/ScrimView.java b/src/com/android/launcher3/views/ScrimView.java index ce58de1ee5..ec71f3118a 100644 --- a/src/com/android/launcher3/views/ScrimView.java +++ b/src/com/android/launcher3/views/ScrimView.java @@ -54,8 +54,7 @@ public class ScrimView extends View implements Insettable { } @Override - public void setInsets(Rect insets) { - } + public void setInsets(Rect insets) {} @Override public boolean hasOverlappingRendering() { diff --git a/src/com/android/launcher3/workprofile/PersonalWorkSlidingTabStrip.java b/src/com/android/launcher3/workprofile/PersonalWorkSlidingTabStrip.java index e94f3a065d..185c3ee9ed 100644 --- a/src/com/android/launcher3/workprofile/PersonalWorkSlidingTabStrip.java +++ b/src/com/android/launcher3/workprofile/PersonalWorkSlidingTabStrip.java @@ -26,9 +26,12 @@ import androidx.annotation.Nullable; import com.android.launcher3.DeviceProfile; import com.android.launcher3.R; +import com.android.launcher3.pageindicators.Direction; import com.android.launcher3.pageindicators.PageIndicator; import com.android.launcher3.views.ActivityContext; +import java.util.function.Consumer; + /** * Supports two indicator colors, dedicated for personal and work tabs. */ @@ -78,6 +81,11 @@ public class PersonalWorkSlidingTabStrip extends LinearLayout implements PageInd } @Override + public void setArrowClickListener(Consumer<Direction> listener) { + // No-Op. All Apps doesn't need accessibility arrows for single click navigation. + } + + @Override public boolean hasOverlappingRendering() { return false; } |