/* * Copyright (C) 2011 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; import static com.android.launcher3.LauncherAnimUtils.VIEW_TRANSLATE_X; import static com.android.launcher3.util.MultiTranslateDelegate.INDEX_BUBBLE_ADJUSTMENT_ANIM; import android.animation.AnimatorSet; import android.animation.ObjectAnimator; import android.animation.ValueAnimator; import android.content.Context; import android.graphics.Rect; import android.util.AttributeSet; import android.view.Gravity; import android.view.LayoutInflater; import android.view.MotionEvent; import android.view.View; import android.view.ViewDebug; import android.view.ViewGroup; import android.widget.FrameLayout; import androidx.annotation.IntDef; import androidx.annotation.Nullable; import com.android.launcher3.util.HorizontalInsettableView; import com.android.launcher3.util.MultiPropertyFactory; import com.android.launcher3.util.MultiPropertyFactory.MultiProperty; import com.android.launcher3.util.MultiTranslateDelegate; import com.android.launcher3.util.MultiValueAlpha; import com.android.launcher3.views.ActivityContext; import java.io.PrintWriter; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; /** * View class that represents the bottom row of the home screen. */ public class Hotseat extends CellLayout implements Insettable { public static final int ALPHA_CHANNEL_TASKBAR_ALIGNMENT = 0; public static final int ALPHA_CHANNEL_PREVIEW_RENDERER = 1; public static final int ALPHA_CHANNEL_TASKBAR_STASH = 2; public static final int ALPHA_CHANNEL_CHANNELS_COUNT = 3; @Retention(RetentionPolicy.RUNTIME) @IntDef({ALPHA_CHANNEL_TASKBAR_ALIGNMENT, ALPHA_CHANNEL_PREVIEW_RENDERER, ALPHA_CHANNEL_TASKBAR_STASH}) public @interface HotseatQsbAlphaId { } public static final int ICONS_TRANSLATION_X_NAV_BAR_ALIGNMENT = 0; public static final int ICONS_TRANSLATION_X_CHANNELS_COUNT = 1; @Retention(RetentionPolicy.RUNTIME) @IntDef({ICONS_TRANSLATION_X_NAV_BAR_ALIGNMENT}) public @interface IconsTranslationX { } // Ratio of empty space, qsb should take up to appear visually centered. public static final float QSB_CENTER_FACTOR = .325f; private static final int BUBBLE_BAR_ADJUSTMENT_ANIMATION_DURATION_MS = 250; @ViewDebug.ExportedProperty(category = "launcher") private boolean mHasVerticalHotseat; private Workspace mWorkspace; private boolean mSendTouchToWorkspace; private final MultiValueAlpha mIconsAlphaChannels; private final MultiValueAlpha mQsbAlphaChannels; private @Nullable MultiProperty mQsbTranslationX; private final MultiPropertyFactory mIconsTranslationXFactory; private final View mQsb; public Hotseat(Context context) { this(context, null); } public Hotseat(Context context, AttributeSet attrs) { this(context, attrs, 0); } public Hotseat(Context context, AttributeSet attrs, int defStyle) { super(context, attrs, defStyle); mQsb = LayoutInflater.from(context).inflate(R.layout.search_container_hotseat, this, false); addView(mQsb); mIconsAlphaChannels = new MultiValueAlpha(getShortcutsAndWidgets(), ALPHA_CHANNEL_CHANNELS_COUNT); if (mQsb instanceof Reorderable qsbReorderable) { mQsbTranslationX = qsbReorderable.getTranslateDelegate() .getTranslationX(MultiTranslateDelegate.INDEX_NAV_BAR_ANIM); } mIconsTranslationXFactory = new MultiPropertyFactory<>(getShortcutsAndWidgets(), VIEW_TRANSLATE_X, ICONS_TRANSLATION_X_CHANNELS_COUNT, Float::sum); mQsbAlphaChannels = new MultiValueAlpha(mQsb, ALPHA_CHANNEL_CHANNELS_COUNT); } /** Provides translation X for hotseat icons for the channel. */ public MultiProperty getIconsTranslationX(@IconsTranslationX int channelId) { return mIconsTranslationXFactory.get(channelId); } /** Provides translation X for hotseat Qsb. */ @Nullable public MultiProperty getQsbTranslationX() { return mQsbTranslationX; } /** * Returns orientation specific cell X given invariant order in the hotseat */ public int getCellXFromOrder(int rank) { return mHasVerticalHotseat ? 0 : rank; } /** * Returns orientation specific cell Y given invariant order in the hotseat */ public int getCellYFromOrder(int rank) { return mHasVerticalHotseat ? (getCountY() - (rank + 1)) : 0; } boolean isHasVerticalHotseat() { return mHasVerticalHotseat; } public void resetLayout(boolean hasVerticalHotseat) { ActivityContext activityContext = ActivityContext.lookupContext(getContext()); boolean bubbleBarEnabled = activityContext.isBubbleBarEnabled(); boolean hasBubbles = activityContext.hasBubbles(); removeAllViewsInLayout(); mHasVerticalHotseat = hasVerticalHotseat; DeviceProfile dp = mActivity.getDeviceProfile(); if (bubbleBarEnabled) { if (dp.shouldAdjustHotseatForBubbleBar(getContext(), hasBubbles)) { getShortcutsAndWidgets().setTranslationProvider( cellX -> dp.getHotseatAdjustedTranslation(getContext(), cellX)); if (mQsb instanceof HorizontalInsettableView) { HorizontalInsettableView insettableQsb = (HorizontalInsettableView) mQsb; final float insetFraction = (float) dp.iconSizePx / dp.hotseatQsbWidth; // post this to the looper so that QSB has a chance to redraw itself, e.g. // after device rotation mQsb.post(() -> insettableQsb.setHorizontalInsets(insetFraction)); } } else { getShortcutsAndWidgets().setTranslationProvider(null); if (mQsb instanceof HorizontalInsettableView) { ((HorizontalInsettableView) mQsb).setHorizontalInsets(0); } } } resetCellSize(dp); if (hasVerticalHotseat) { setGridSize(1, dp.numShownHotseatIcons); } else { setGridSize(dp.numShownHotseatIcons, 1); } } /** * Adjust the hotseat icons for the bubble bar. * *

When the bubble bar becomes visible, if needed, this method animates the hotseat icons * to reduce the spacing between them and make room for the bubble bar. The QSB width is * animated as well to align with the hotseat icons. * *

When the bubble bar goes away, any adjustments that were previously made are reversed. */ public void adjustForBubbleBar(boolean isBubbleBarVisible) { DeviceProfile dp = mActivity.getDeviceProfile(); boolean shouldAdjust = isBubbleBarVisible && dp.shouldAdjustHotseatOrQsbForBubbleBar(getContext()); boolean shouldAdjustHotseat = shouldAdjust && dp.shouldAlignBubbleBarWithHotseat(); ShortcutAndWidgetContainer icons = getShortcutsAndWidgets(); // update the translation provider for future layout passes of hotseat icons. if (shouldAdjustHotseat) { icons.setTranslationProvider( cellX -> dp.getHotseatAdjustedTranslation(getContext(), cellX)); } else { icons.setTranslationProvider(null); } AnimatorSet animatorSet = new AnimatorSet(); for (int i = 0; i < icons.getChildCount(); i++) { View child = icons.getChildAt(i); float tx = shouldAdjustHotseat ? dp.getHotseatAdjustedTranslation(getContext(), i) : 0; if (child instanceof Reorderable) { MultiTranslateDelegate mtd = ((Reorderable) child).getTranslateDelegate(); animatorSet.play( mtd.getTranslationX(INDEX_BUBBLE_ADJUSTMENT_ANIM).animateToValue(tx)); } else { animatorSet.play(ObjectAnimator.ofFloat(child, VIEW_TRANSLATE_X, tx)); } } //TODO(b/381109832) refactor & simplify adjustment logic boolean shouldAdjustQsb = shouldAdjustHotseat || (shouldAdjust && dp.shouldAlignBubbleBarWithQSB()); if (mQsb instanceof HorizontalInsettableView horizontalInsettableQsb) { final float currentInsetFraction = horizontalInsettableQsb.getHorizontalInsets(); final float targetInsetFraction = shouldAdjustQsb ? (float) dp.iconSizePx / dp.hotseatQsbWidth : 0; ValueAnimator qsbAnimator = ValueAnimator.ofFloat(currentInsetFraction, targetInsetFraction); qsbAnimator.addUpdateListener(animation -> { float insetFraction = (float) animation.getAnimatedValue(); horizontalInsettableQsb.setHorizontalInsets(insetFraction); }); animatorSet.play(qsbAnimator); } animatorSet.setDuration(BUBBLE_BAR_ADJUSTMENT_ANIMATION_DURATION_MS).start(); } @Override public void setInsets(Rect insets) { FrameLayout.LayoutParams lp = (FrameLayout.LayoutParams) getLayoutParams(); DeviceProfile grid = mActivity.getDeviceProfile(); if (grid.isVerticalBarLayout()) { mQsb.setVisibility(View.GONE); lp.height = ViewGroup.LayoutParams.MATCH_PARENT; if (grid.isSeascape()) { lp.gravity = Gravity.LEFT; lp.width = grid.hotseatBarSizePx + insets.left; } else { lp.gravity = Gravity.RIGHT; lp.width = grid.hotseatBarSizePx + insets.right; } } else { mQsb.setVisibility(View.VISIBLE); lp.gravity = Gravity.BOTTOM; lp.width = ViewGroup.LayoutParams.MATCH_PARENT; lp.height = grid.hotseatBarSizePx; } Rect padding = grid.getHotseatLayoutPadding(getContext()); setPadding(padding.left, padding.top, padding.right, padding.bottom); setLayoutParams(lp); InsettableFrameLayout.dispatchInsets(this, insets); } public void setWorkspace(Workspace w) { mWorkspace = w; setCellLayoutContainer(w); } @Override public boolean onInterceptTouchEvent(MotionEvent ev) { // We allow horizontal workspace scrolling from within the Hotseat. We do this by delegating // touch intercept the Workspace, and if it intercepts, delegating touch to the Workspace // for the remainder of the this input stream. int yThreshold = getMeasuredHeight() - getPaddingBottom(); if (mWorkspace != null && ev.getY() <= yThreshold) { mSendTouchToWorkspace = mWorkspace.onInterceptTouchEvent(ev); return mSendTouchToWorkspace; } return false; } @Override public boolean onTouchEvent(MotionEvent event) { // See comment in #onInterceptTouchEvent if (mSendTouchToWorkspace) { final int action = event.getAction(); switch (action & MotionEvent.ACTION_MASK) { case MotionEvent.ACTION_UP: case MotionEvent.ACTION_CANCEL: mSendTouchToWorkspace = false; } return mWorkspace.onTouchEvent(event); } // Always let touch follow through to Workspace. return false; } @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { super.onMeasure(widthMeasureSpec, heightMeasureSpec); DeviceProfile dp = mActivity.getDeviceProfile(); mQsb.measure(MeasureSpec.makeMeasureSpec(dp.hotseatQsbWidth, MeasureSpec.EXACTLY), MeasureSpec.makeMeasureSpec(dp.hotseatQsbHeight, MeasureSpec.EXACTLY)); } @Override protected void onLayout(boolean changed, int l, int t, int r, int b) { super.onLayout(changed, l, t, r, b); int qsbMeasuredWidth = mQsb.getMeasuredWidth(); int left; DeviceProfile dp = mActivity.getDeviceProfile(); if (dp.isQsbInline) { int qsbSpace = dp.hotseatBorderSpace; left = Utilities.isRtl(getResources()) ? r - getPaddingRight() + qsbSpace : l + getPaddingLeft() - qsbMeasuredWidth - qsbSpace; } else { left = (r - l - qsbMeasuredWidth) / 2; } int right = left + qsbMeasuredWidth; int bottom = b - t - dp.getQsbOffsetY(); int top = bottom - dp.hotseatQsbHeight; mQsb.layout(left, top, right, bottom); } /** * Sets the alpha value of the specified alpha channel of just our ShortcutAndWidgetContainer. */ public void setIconsAlpha(float alpha, @HotseatQsbAlphaId int channelId) { getIconsAlpha(channelId).setValue(alpha); } /** * Sets the alpha value of just our QSB. */ public void setQsbAlpha(float alpha, @HotseatQsbAlphaId int channelId) { getQsbAlpha(channelId).setValue(alpha); } /** Returns the alpha channel for ShortcutAndWidgetContainer */ public MultiProperty getIconsAlpha(@HotseatQsbAlphaId int channelId) { return mIconsAlphaChannels.get(channelId); } /** Returns the alpha channel for Qsb */ public MultiProperty getQsbAlpha(@HotseatQsbAlphaId int channelId) { return mQsbAlphaChannels.get(channelId); } /** * Returns the QSB inside hotseat */ public View getQsb() { return mQsb; } /** Dumps the Hotseat internal state */ public void dump(String prefix, PrintWriter writer) { writer.println(prefix + "Hotseat:"); mIconsAlphaChannels.dump( prefix + "\t", writer, "mIconsAlphaChannels", "ALPHA_CHANNEL_TASKBAR_ALIGNMENT", "ALPHA_CHANNEL_PREVIEW_RENDERER", "ALPHA_CHANNEL_TASKBAR_STASH"); mQsbAlphaChannels.dump( prefix + "\t", writer, "mQsbAlphaChannels", "ALPHA_CHANNEL_TASKBAR_ALIGNMENT", "ALPHA_CHANNEL_PREVIEW_RENDERER", "ALPHA_CHANNEL_TASKBAR_STASH" ); } }