/* * 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 com.android.launcher3.util.HorizontalInsettableView; import com.android.launcher3.util.MultiTranslateDelegate; import com.android.launcher3.views.ActivityContext; /** * View class that represents the bottom row of the home screen. */ public class Hotseat extends CellLayout implements Insettable { // 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 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); } /** * 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; } 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) { float adjustedBorderSpace = dp.getHotseatAdjustedBorderSpaceForBubbleBar(getContext()); if (hasBubbles && Float.compare(adjustedBorderSpace, 0f) != 0) { getShortcutsAndWidgets().setTranslationProvider(child -> { int index = getShortcutsAndWidgets().indexOfChild(child); float borderSpaceDelta = adjustedBorderSpace - dp.hotseatBorderSpace; return dp.iconSizePx + index * borderSpaceDelta; }); 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(); float adjustedBorderSpace = dp.getHotseatAdjustedBorderSpaceForBubbleBar(getContext()); if (Float.compare(adjustedBorderSpace, 0f) == 0) { return; } ShortcutAndWidgetContainer icons = getShortcutsAndWidgets(); AnimatorSet animatorSet = new AnimatorSet(); float borderSpaceDelta = adjustedBorderSpace - dp.hotseatBorderSpace; // update the translation provider for future layout passes of hotseat icons. if (isBubbleBarVisible) { icons.setTranslationProvider(child -> { int index = icons.indexOfChild(child); return dp.iconSizePx + index * borderSpaceDelta; }); } else { icons.setTranslationProvider(null); } for (int i = 0; i < icons.getChildCount(); i++) { View child = icons.getChildAt(i); float tx = isBubbleBarVisible ? dp.iconSizePx + i * borderSpaceDelta : 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)); } } if (mQsb instanceof HorizontalInsettableView) { HorizontalInsettableView horizontalInsettableQsb = (HorizontalInsettableView) mQsb; ValueAnimator qsbAnimator = ValueAnimator.ofFloat(0f, 1f); qsbAnimator.addUpdateListener(animation -> { float fraction = qsbAnimator.getAnimatedFraction(); float insetFraction = isBubbleBarVisible ? (float) dp.iconSizePx * fraction / dp.hotseatQsbWidth : (float) dp.iconSizePx * (1 - fraction) / dp.hotseatQsbWidth; 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; } @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 just our ShortcutAndWidgetContainer. */ public void setIconsAlpha(float alpha) { getShortcutsAndWidgets().setAlpha(alpha); } /** * Sets the alpha value of just our QSB. */ public void setQsbAlpha(float alpha) { mQsb.setAlpha(alpha); } public float getIconsAlpha() { return getShortcutsAndWidgets().getAlpha(); } /** * Returns the QSB inside hotseat */ public View getQsb() { return mQsb; } }